Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
87ff9c5164 | ||
|
|
c61937a4b0 | ||
|
|
edf54851ae | ||
|
|
b766deafa5 | ||
|
|
7974066105 | ||
|
|
dc096ae3ed | ||
|
|
23172b1cd8 | ||
|
|
a9ed6a5e67 | ||
|
|
00b5334273 | ||
|
|
f11071fd4d | ||
|
|
82e28c1620 | ||
|
|
9ea170a2b2 | ||
|
|
2d82aec1eb | ||
|
|
4ee72db907 | ||
|
|
96a1455ebc | ||
|
|
e5ed91e90d |
10
README.md
@@ -2,12 +2,14 @@
|
|||||||
## О приложении
|
## О приложении
|
||||||
Приложение разработано для работы с модулем [zaprett](https://github.com/egor-white/zaprett)
|
Приложение разработано для работы с модулем [zaprett](https://github.com/egor-white/zaprett)
|
||||||
### [Официальный Telegram-канал приложения](https://t.me/zaprett_module)
|
### [Официальный Telegram-канал приложения](https://t.me/zaprett_module)
|
||||||
|
## ВНИМАНИЕ приложение работает только с root правами
|
||||||
### Данное приложение является ремейком [приложения](https://github.com/egor-white/zaprett-app) от [egor-white](https://github.com/egor-white)
|
### Данное приложение является ремейком [приложения](https://github.com/egor-white/zaprett-app) от [egor-white](https://github.com/egor-white)
|
||||||
|
|
||||||
На данный момент приложение умеет:
|
На данный момент приложение умеет:
|
||||||
* Включать, выключать и перезапускать модуль
|
* Запускать, останавливать и перезапускать модуль
|
||||||
* Работа с листами (добавление, включение и выключение)
|
* Работа с листами (добавление, включение и выключение, загрузка из репозитория)
|
||||||
* Авто обновление приложения
|
* Авто обновление приложения
|
||||||
|
|
||||||
|
## [Репозиторий с хостами](https://github.com/CherretGit/zaprett-hosts-repo)
|
||||||
|
|
||||||
## Скриншоты:
|
## Скриншоты:
|
||||||
<img src="images/1.png" width="300"><img src="images/2.png" width="300"><img src="images/3.png" width="300">
|
<img src="images/1.png" width="300"><img src="images/2.png" width="300"><img src="images/3.png" width="300"><img src="images/4.png" width="300"><img src="images/5.png" width="300">
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ plugins {
|
|||||||
alias(libs.plugins.kotlin.compose)
|
alias(libs.plugins.kotlin.compose)
|
||||||
id("com.google.gms.google-services")
|
id("com.google.gms.google-services")
|
||||||
id("com.google.firebase.crashlytics")
|
id("com.google.firebase.crashlytics")
|
||||||
|
kotlin("plugin.serialization") version "2.1.20"
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
@@ -14,8 +15,8 @@ android {
|
|||||||
applicationId = "com.cherret.zaprett"
|
applicationId = "com.cherret.zaprett"
|
||||||
minSdk = 30
|
minSdk = 30
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 9
|
versionCode = 11
|
||||||
versionName = "1.8"
|
versionName = "1.10"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
@@ -51,7 +52,7 @@ dependencies {
|
|||||||
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.okhttp3:okhttp:5.0.0-alpha.14")
|
||||||
implementation("com.squareup.moshi:moshi-kotlin:1.15.2")
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0")
|
||||||
implementation(platform("com.google.firebase:firebase-bom:33.12.0"))
|
implementation(platform("com.google.firebase:firebase-bom:33.12.0"))
|
||||||
implementation("com.google.firebase:firebase-analytics")
|
implementation("com.google.firebase:firebase-analytics")
|
||||||
implementation("com.google.firebase:firebase-crashlytics")
|
implementation("com.google.firebase:firebase-crashlytics")
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import androidx.annotation.StringRes
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Dashboard
|
import androidx.compose.material.icons.filled.Dashboard
|
||||||
|
import androidx.compose.material.icons.filled.Dns
|
||||||
import androidx.compose.material.icons.filled.Home
|
import androidx.compose.material.icons.filled.Home
|
||||||
import androidx.compose.material.icons.filled.Settings
|
import androidx.compose.material.icons.filled.Settings
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
@@ -28,6 +29,7 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
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.core.content.edit
|
||||||
@@ -36,22 +38,25 @@ 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 androidx.navigation.navArgument
|
||||||
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.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.screens.StrategyScreen
|
||||||
import com.cherret.zaprett.ui.theme.ZaprettTheme
|
import com.cherret.zaprett.ui.theme.ZaprettTheme
|
||||||
import com.google.firebase.Firebase
|
import com.google.firebase.Firebase
|
||||||
import com.google.firebase.analytics.FirebaseAnalytics
|
import com.google.firebase.analytics.FirebaseAnalytics
|
||||||
import com.google.firebase.analytics.analytics
|
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: ImageVector) {
|
||||||
object home : Screen("home", R.string.title_home, Icons.Default.Home)
|
object home : Screen("home", R.string.title_home, Icons.Default.Home)
|
||||||
object hosts : Screen("hosts", R.string.title_hosts, Icons.Default.Dashboard)
|
object hosts : Screen("hosts", R.string.title_hosts, Icons.Default.Dashboard)
|
||||||
|
object strategies : Screen("strategies", R.string.title_strategies, Icons.Default.Dns)
|
||||||
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.strategies, Screen.settings)
|
||||||
val hideNavBar = listOf("hosts_repo")
|
val hideNavBar = listOf("repo?source={source}")
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
private lateinit var firebaseAnalytics: FirebaseAnalytics
|
private lateinit var firebaseAnalytics: FirebaseAnalytics
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@@ -119,8 +124,19 @@ class MainActivity : ComponentActivity() {
|
|||||||
) {
|
) {
|
||||||
composable(Screen.home.route) { HomeScreen() }
|
composable(Screen.home.route) { HomeScreen() }
|
||||||
composable(Screen.hosts.route) { HostsScreen(navController) }
|
composable(Screen.hosts.route) { HostsScreen(navController) }
|
||||||
|
composable(Screen.strategies.route) { StrategyScreen(navController) }
|
||||||
composable(Screen.settings.route) { SettingsScreen() }
|
composable(Screen.settings.route) { SettingsScreen() }
|
||||||
composable("hosts_repo") { HostsRepoScreen(navController) }
|
composable(route = "repo?source={source}",arguments = listOf(navArgument("source") {})) { backStackEntry ->
|
||||||
|
val source = backStackEntry.arguments?.getString("source")
|
||||||
|
when (source) {
|
||||||
|
"hosts" -> {
|
||||||
|
HostsRepoScreen(navController, ::getAllLists, ::getHostList, "/lists")
|
||||||
|
}
|
||||||
|
"strategies" -> {
|
||||||
|
HostsRepoScreen(navController, ::getAllStrategies, ::getStrategiesList, "/strategies")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,6 +109,15 @@ fun getAllLists(): Array<String> {
|
|||||||
return emptyArray()
|
return emptyArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getAllStrategies(): Array<String> {
|
||||||
|
val listsDir = File("${getZaprettPath()}/strategies/")
|
||||||
|
if (listsDir.exists() && listsDir.isDirectory) {
|
||||||
|
val onlyNames = listsDir.list() ?: return emptyArray()
|
||||||
|
return onlyNames.map { "$listsDir/$it" }.toTypedArray()
|
||||||
|
}
|
||||||
|
return emptyArray()
|
||||||
|
}
|
||||||
|
|
||||||
fun getActiveLists(): Array<String> {
|
fun getActiveLists(): Array<String> {
|
||||||
val configFile = File("${getZaprettPath()}/config")
|
val configFile = File("${getZaprettPath()}/config")
|
||||||
if (configFile.exists()) {
|
if (configFile.exists()) {
|
||||||
@@ -127,6 +136,24 @@ fun getActiveLists(): Array<String> {
|
|||||||
return emptyArray()
|
return emptyArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getActiveStrategies(): Array<String> {
|
||||||
|
val configFile = File("${getZaprettPath()}/config")
|
||||||
|
if (configFile.exists()) {
|
||||||
|
val props = Properties()
|
||||||
|
return try {
|
||||||
|
FileInputStream(configFile).use { input ->
|
||||||
|
props.load(input)
|
||||||
|
}
|
||||||
|
val activeStrategies = props.getProperty("strategy", "")
|
||||||
|
Log.d("Active strategies", activeStrategies)
|
||||||
|
if (activeStrategies.isNotEmpty()) activeStrategies.split(",").toTypedArray() else emptyArray()
|
||||||
|
} catch (e: IOException) {
|
||||||
|
throw RuntimeException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return emptyArray()
|
||||||
|
}
|
||||||
|
|
||||||
fun enableList(path: String) {
|
fun enableList(path: String) {
|
||||||
val props = Properties()
|
val props = Properties()
|
||||||
val configFile = getConfigFile()
|
val configFile = getConfigFile()
|
||||||
@@ -136,7 +163,10 @@ fun enableList(path: String) {
|
|||||||
props.load(input)
|
props.load(input)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val activeLists = props.getProperty("activelists", "").split(",").toMutableList()
|
val activeLists = props.getProperty("activelists", "")
|
||||||
|
.split(",")
|
||||||
|
.filter { it.isNotBlank() }
|
||||||
|
.toMutableList()
|
||||||
if (path !in activeLists) {
|
if (path !in activeLists) {
|
||||||
activeLists.add(path)
|
activeLists.add(path)
|
||||||
}
|
}
|
||||||
@@ -149,6 +179,31 @@ fun enableList(path: String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun enableStrategy(path: String) {
|
||||||
|
val props = Properties()
|
||||||
|
val configFile = getConfigFile()
|
||||||
|
try {
|
||||||
|
if (configFile.exists()) {
|
||||||
|
FileInputStream(configFile).use { input ->
|
||||||
|
props.load(input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val activeStrategies = props.getProperty("strategy", "")
|
||||||
|
.split(",")
|
||||||
|
.filter { it.isNotBlank() }
|
||||||
|
.toMutableList()
|
||||||
|
if (path !in activeStrategies) {
|
||||||
|
activeStrategies.add(path)
|
||||||
|
}
|
||||||
|
props.setProperty("strategy", activeStrategies.joinToString(","))
|
||||||
|
FileOutputStream(configFile).use { output ->
|
||||||
|
props.store(output, "Don't place '/' in end of directory! Example: /sdcard")
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
throw RuntimeException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun disableList(path: String) {
|
fun disableList(path: String) {
|
||||||
val props = Properties()
|
val props = Properties()
|
||||||
val configFile = getConfigFile()
|
val configFile = getConfigFile()
|
||||||
@@ -158,7 +213,10 @@ fun disableList(path: String) {
|
|||||||
props.load(input)
|
props.load(input)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val activeLists = props.getProperty("activelists", "").split(",").toMutableList()
|
val activeLists = props.getProperty("activelists", "")
|
||||||
|
.split(",")
|
||||||
|
.filter { it.isNotBlank() }
|
||||||
|
.toMutableList()
|
||||||
if (path in activeLists) {
|
if (path in activeLists) {
|
||||||
activeLists.remove(path)
|
activeLists.remove(path)
|
||||||
}
|
}
|
||||||
@@ -169,4 +227,29 @@ fun disableList(path: String) {
|
|||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
throw RuntimeException(e)
|
throw RuntimeException(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun disableStrategy(path: String) {
|
||||||
|
val props = Properties()
|
||||||
|
val configFile = getConfigFile()
|
||||||
|
try {
|
||||||
|
if (configFile.exists()) {
|
||||||
|
FileInputStream(configFile).use { input ->
|
||||||
|
props.load(input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val activeStrategies = props.getProperty("strategy", "")
|
||||||
|
.split(",")
|
||||||
|
.filter { it.isNotBlank() }
|
||||||
|
.toMutableList()
|
||||||
|
if (path in activeStrategies) {
|
||||||
|
activeStrategies.remove(path)
|
||||||
|
}
|
||||||
|
props.setProperty("strategy", activeStrategies.joinToString(","))
|
||||||
|
FileOutputStream(configFile).use { output ->
|
||||||
|
props.store(output, "Don't place '/' in end of directory! Example: /sdcard")
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
throw RuntimeException(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -10,9 +10,8 @@ import android.net.Uri
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.squareup.moshi.Moshi
|
import kotlinx.serialization.Serializable
|
||||||
import com.squareup.moshi.Types
|
import kotlinx.serialization.json.Json
|
||||||
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
|
||||||
import okhttp3.Call
|
import okhttp3.Call
|
||||||
import okhttp3.Callback
|
import okhttp3.Callback
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
@@ -23,12 +22,10 @@ import java.io.File
|
|||||||
import java.security.MessageDigest
|
import java.security.MessageDigest
|
||||||
|
|
||||||
private val client = OkHttpClient()
|
private val client = OkHttpClient()
|
||||||
|
private val json = Json { ignoreUnknownKeys = true }
|
||||||
|
|
||||||
fun getHostList(callback: (List<HostsInfo>?) -> Unit) {
|
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 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 {
|
client.newCall(request).enqueue(object : Callback {
|
||||||
override fun onFailure(call: Call, e: IOException) {
|
override fun onFailure(call: Call, e: IOException) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
@@ -41,10 +38,29 @@ fun getHostList(callback: (List<HostsInfo>?) -> Unit) {
|
|||||||
callback(null)
|
callback(null)
|
||||||
}
|
}
|
||||||
val jsonString = response.body.string()
|
val jsonString = response.body.string()
|
||||||
val updateInfo = jsonAdapter.fromJson(jsonString)
|
val hostsInfo = json.decodeFromString<List<HostsInfo>>(jsonString)
|
||||||
if (updateInfo != null) {
|
callback(hostsInfo)
|
||||||
callback(updateInfo)
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getStrategiesList(callback: (List<HostsInfo>?) -> Unit) {
|
||||||
|
val request = Request.Builder().url("https://raw.githubusercontent.com/CherretGit/zaprett-hosts-repo/refs/heads/main/strategies.json").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) {
|
||||||
|
throw IOException()
|
||||||
|
callback(null)
|
||||||
}
|
}
|
||||||
|
val jsonString = response.body.string()
|
||||||
|
val hostsInfo = json.decodeFromString<List<HostsInfo>>(jsonString)
|
||||||
|
callback(hostsInfo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -93,9 +109,11 @@ fun getFileSha256(file: File): String {
|
|||||||
return digest.digest().joinToString("") { "%02x".format(it) }
|
return digest.digest().joinToString("") { "%02x".format(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
data class HostsInfo(
|
data class HostsInfo(
|
||||||
val name: String,
|
val name: String,
|
||||||
val description: String,
|
val description: String,
|
||||||
|
val author: String,
|
||||||
val hash: String,
|
val hash: String,
|
||||||
val url: String
|
val url: String
|
||||||
)
|
)
|
||||||
@@ -13,8 +13,8 @@ import android.provider.Settings
|
|||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import com.squareup.moshi.Moshi
|
import kotlinx.serialization.Serializable
|
||||||
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
import kotlinx.serialization.json.Json
|
||||||
import okhttp3.Call
|
import okhttp3.Call
|
||||||
import okhttp3.Callback
|
import okhttp3.Callback
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
@@ -24,11 +24,10 @@ import okio.IOException
|
|||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
private val client = OkHttpClient()
|
private val client = OkHttpClient()
|
||||||
|
private val json = Json { ignoreUnknownKeys = true }
|
||||||
|
|
||||||
fun getUpdate(callback: (UpdateInfo?) -> Unit) {
|
fun getUpdate(callback: (UpdateInfo?) -> Unit) {
|
||||||
val request = Request.Builder().url("https://raw.githubusercontent.com/CherretGit/zaprett-app/refs/heads/main/update.json").build()
|
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 {
|
client.newCall(request).enqueue(object : Callback {
|
||||||
override fun onFailure(call: Call, e: IOException) {
|
override fun onFailure(call: Call, e: IOException) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
@@ -41,7 +40,7 @@ fun getUpdate(callback: (UpdateInfo?) -> Unit) {
|
|||||||
callback(null)
|
callback(null)
|
||||||
}
|
}
|
||||||
val jsonString = response.body.string()
|
val jsonString = response.body.string()
|
||||||
val updateInfo = jsonAdapter.fromJson(jsonString)
|
val updateInfo = json.decodeFromString<UpdateInfo>(jsonString)
|
||||||
updateInfo?.versionCode?.let { versionCode ->
|
updateInfo?.versionCode?.let { versionCode ->
|
||||||
if (versionCode > BuildConfig.VERSION_CODE)
|
if (versionCode > BuildConfig.VERSION_CODE)
|
||||||
callback(updateInfo)
|
callback(updateInfo)
|
||||||
@@ -74,12 +73,6 @@ fun getChangelog(changelogUrl: String, callback: (String?) -> Unit) {
|
|||||||
fun download(context: Context, url: String): Long {
|
fun download(context: Context, url: String): Long {
|
||||||
val fileName = url.substringAfterLast("/")
|
val fileName = url.substringAfterLast("/")
|
||||||
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
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 {
|
val request = DownloadManager.Request(url.toUri()).apply {
|
||||||
setTitle(fileName)
|
setTitle(fileName)
|
||||||
setDescription("Загрузка $fileName")
|
setDescription("Загрузка $fileName")
|
||||||
@@ -90,7 +83,6 @@ fun download(context: Context, url: String): Long {
|
|||||||
return downloadManager.enqueue(request)
|
return downloadManager.enqueue(request)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun installApk(context: Context, uri: Uri) {
|
fun installApk(context: Context, uri: Uri) {
|
||||||
val file = File(uri.path!!)
|
val file = File(uri.path!!)
|
||||||
val apkUri = FileProvider.getUriForFile(context, "${context.packageName}.provider", file)
|
val apkUri = FileProvider.getUriForFile(context, "${context.packageName}.provider", file)
|
||||||
@@ -131,6 +123,7 @@ fun registerDownloadListener(context: Context, downloadId: Long, onDownloaded: (
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
data class UpdateInfo(
|
data class UpdateInfo(
|
||||||
val version: String?,
|
val version: String?,
|
||||||
val versionCode: Int?,
|
val versionCode: Int?,
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ fun HostsScreen(navController: NavController) {
|
|||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.btn_no_hosts),
|
stringResource(R.string.empty_list),
|
||||||
textAlign = TextAlign.Center
|
textAlign = TextAlign.Center
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -106,16 +106,21 @@ fun HostsScreen(navController: NavController) {
|
|||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
items(allLists) { item ->
|
items(allLists) { item ->
|
||||||
HostItem(
|
StrategyItem(
|
||||||
item = item,
|
item = item,
|
||||||
isChecked = checked[item] == true,
|
isChecked = checked[item] == true,
|
||||||
onCheckedChange = { isChecked ->
|
onCheckedChange = { isChecked ->
|
||||||
checked[item] = isChecked
|
checked[item] = isChecked
|
||||||
if (isChecked) enableList(item) else disableList(item)
|
if (isChecked) enableList(item) else disableList(item)
|
||||||
showRestartSnackbar(context, snackbarHostState, scope)
|
getStatus { isEnabled ->
|
||||||
|
if (isEnabled) {
|
||||||
|
showRestartSnackbar(context, snackbarHostState, scope)
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onDeleteClick = {
|
onDeleteClick = {
|
||||||
if (deleteHost(item)) {
|
val wasChecked = checked[item] == true
|
||||||
|
if (deleteStrategy(item)) {
|
||||||
allLists = getAllLists()
|
allLists = getAllLists()
|
||||||
activeLists = getActiveLists()
|
activeLists = getActiveLists()
|
||||||
checked.clear()
|
checked.clear()
|
||||||
@@ -123,7 +128,11 @@ fun HostsScreen(navController: NavController) {
|
|||||||
checked[list] = activeLists.contains(list)
|
checked[list] = activeLists.contains(list)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
showRestartSnackbar(context, snackbarHostState, scope)
|
getStatus { isEnabled ->
|
||||||
|
if (isEnabled && wasChecked) {
|
||||||
|
showRestartSnackbar(context, snackbarHostState, scope)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -140,7 +149,7 @@ fun HostsScreen(navController: NavController) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun HostItem(item: String, isChecked: Boolean, onCheckedChange: (Boolean) -> Unit, onDeleteClick: () -> Unit) {
|
private fun StrategyItem(item: String, isChecked: Boolean, onCheckedChange: (Boolean) -> Unit, onDeleteClick: () -> Unit) {
|
||||||
ElevatedCard(
|
ElevatedCard(
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 6.dp),
|
elevation = CardDefaults.cardElevation(defaultElevation = 6.dp),
|
||||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainer),
|
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainer),
|
||||||
@@ -194,7 +203,7 @@ private fun FloatingMenu(navController: NavController, launcher: ActivityResultL
|
|||||||
text = { Text(stringResource(R.string.btn_download_host)) },
|
text = { Text(stringResource(R.string.btn_download_host)) },
|
||||||
onClick = {
|
onClick = {
|
||||||
expanded = false
|
expanded = false
|
||||||
navController.navigate("hosts_repo") { launchSingleTop = true }
|
navController.navigate("repo?source=hosts") { launchSingleTop = true }
|
||||||
},
|
},
|
||||||
leadingIcon = {
|
leadingIcon = {
|
||||||
Icon(Icons.Default.Download, contentDescription = stringResource(R.string.btn_download_host))
|
Icon(Icons.Default.Download, contentDescription = stringResource(R.string.btn_download_host))
|
||||||
@@ -204,7 +213,7 @@ private fun FloatingMenu(navController: NavController, launcher: ActivityResultL
|
|||||||
text = { Text(stringResource(R.string.btn_add_host)) },
|
text = { Text(stringResource(R.string.btn_add_host)) },
|
||||||
onClick = {
|
onClick = {
|
||||||
expanded = false
|
expanded = false
|
||||||
addHost(launcher)
|
addStrategy(launcher)
|
||||||
},
|
},
|
||||||
leadingIcon = {
|
leadingIcon = {
|
||||||
Icon(Icons.Default.UploadFile, contentDescription = stringResource(R.string.btn_add_host))
|
Icon(Icons.Default.UploadFile, contentDescription = stringResource(R.string.btn_add_host))
|
||||||
@@ -213,7 +222,7 @@ private fun FloatingMenu(navController: NavController, launcher: ActivityResultL
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun addHost(launcher: ActivityResultLauncher<Array<String>>) {
|
private fun addStrategy(launcher: ActivityResultLauncher<Array<String>>) {
|
||||||
launcher.launch(arrayOf("text/plain"))
|
launcher.launch(arrayOf("text/plain"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,7 +249,7 @@ private fun copySelectedFile(context: Context, uri: Uri, snackbarHostState: Snac
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun deleteHost(item: String): Boolean {
|
private fun deleteStrategy(item: String): Boolean {
|
||||||
val hostFile = File(item)
|
val hostFile = File(item)
|
||||||
return if (hostFile.exists()) {
|
return if (hostFile.exists()) {
|
||||||
hostFile.delete()
|
hostFile.delete()
|
||||||
|
|||||||
@@ -50,9 +50,7 @@ import androidx.navigation.NavController
|
|||||||
import com.cherret.zaprett.HostsInfo
|
import com.cherret.zaprett.HostsInfo
|
||||||
import com.cherret.zaprett.R
|
import com.cherret.zaprett.R
|
||||||
import com.cherret.zaprett.download
|
import com.cherret.zaprett.download
|
||||||
import com.cherret.zaprett.getAllLists
|
|
||||||
import com.cherret.zaprett.getFileSha256
|
import com.cherret.zaprett.getFileSha256
|
||||||
import com.cherret.zaprett.getHostList
|
|
||||||
import com.cherret.zaprett.getZaprettPath
|
import com.cherret.zaprett.getZaprettPath
|
||||||
import com.cherret.zaprett.registerDownloadListenerHost
|
import com.cherret.zaprett.registerDownloadListenerHost
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -61,7 +59,7 @@ import java.io.File
|
|||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun HostsRepoScreen(navController: NavController) {
|
fun HostsRepoScreen(navController: NavController, getAllLists: () -> Array<String>, getHostList: ((List<HostsInfo>?) -> Unit) -> Unit, targetPath: String) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
var allLists by remember { mutableStateOf(getAllLists()) }
|
var allLists by remember { mutableStateOf(getAllLists()) }
|
||||||
var hostLists by remember { mutableStateOf<List<HostsInfo>?>(null) }
|
var hostLists by remember { mutableStateOf<List<HostsInfo>?>(null) }
|
||||||
@@ -87,8 +85,8 @@ fun HostsRepoScreen(navController: NavController) {
|
|||||||
TopAppBar(
|
TopAppBar(
|
||||||
title = {
|
title = {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.title_hosts_repo),
|
text = stringResource(R.string.title_repo),
|
||||||
fontSize = 40.sp,
|
fontSize = 30.sp,
|
||||||
fontFamily = FontFamily(Font(R.font.unbounded, FontWeight.Normal))
|
fontFamily = FontFamily(Font(R.font.unbounded, FontWeight.Normal))
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -127,7 +125,7 @@ fun HostsRepoScreen(navController: NavController) {
|
|||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.btn_no_hosts),
|
stringResource(R.string.empty_list),
|
||||||
textAlign = TextAlign.Center
|
textAlign = TextAlign.Center
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -161,6 +159,17 @@ fun HostsRepoScreen(navController: NavController) {
|
|||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(start = 16.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.title_author, item.author),
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
}
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -190,7 +199,7 @@ fun HostsRepoScreen(navController: NavController) {
|
|||||||
) { uri ->
|
) { uri ->
|
||||||
val sourceFile = File(uri.path!!)
|
val sourceFile = File(uri.path!!)
|
||||||
val targetFile = File(
|
val targetFile = File(
|
||||||
getZaprettPath() + "/lists",
|
getZaprettPath() + targetPath,
|
||||||
uri.lastPathSegment!!
|
uri.lastPathSegment!!
|
||||||
)
|
)
|
||||||
sourceFile.copyTo(targetFile, overwrite = true)
|
sourceFile.copyTo(targetFile, overwrite = true)
|
||||||
@@ -229,7 +238,7 @@ fun HostsRepoScreen(navController: NavController) {
|
|||||||
) { uri ->
|
) { uri ->
|
||||||
val sourceFile = File(uri.path!!)
|
val sourceFile = File(uri.path!!)
|
||||||
val targetFile = File(
|
val targetFile = File(
|
||||||
getZaprettPath() + "/lists",
|
getZaprettPath() + targetPath,
|
||||||
uri.lastPathSegment!!
|
uri.lastPathSegment!!
|
||||||
)
|
)
|
||||||
sourceFile.copyTo(targetFile, overwrite = true)
|
sourceFile.copyTo(targetFile, overwrite = true)
|
||||||
@@ -0,0 +1,284 @@
|
|||||||
|
package com.cherret.zaprett.ui.screens
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Environment
|
||||||
|
import android.provider.OpenableColumns
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
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.Download
|
||||||
|
import androidx.compose.material.icons.filled.UploadFile
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.Font
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import com.cherret.zaprett.R
|
||||||
|
import com.cherret.zaprett.*
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun StrategyScreen(navController: NavController) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
var allStrategies by remember { mutableStateOf(getAllStrategies()) }
|
||||||
|
var activeStrategies by remember { mutableStateOf(getActiveStrategies()) }
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
var isRefreshing by remember { mutableStateOf(false) }
|
||||||
|
val checked = remember {
|
||||||
|
mutableStateMapOf<String, Boolean>().apply {
|
||||||
|
allStrategies.forEach { list -> this[list] = activeStrategies.contains(list) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val filePickerLauncher = rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.OpenDocument()
|
||||||
|
) { uri ->
|
||||||
|
uri?.let { copySelectedFile(context, it, snackbarHostState, scope) }
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.title_strategies),
|
||||||
|
fontSize = 40.sp,
|
||||||
|
fontFamily = FontFamily(Font(R.font.unbounded, FontWeight.Normal))
|
||||||
|
)
|
||||||
|
},
|
||||||
|
windowInsets = WindowInsets(0)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
content = { paddingValues ->
|
||||||
|
PullToRefreshBox(
|
||||||
|
isRefreshing = isRefreshing,
|
||||||
|
onRefresh = {
|
||||||
|
isRefreshing = true
|
||||||
|
allStrategies = getAllStrategies()
|
||||||
|
activeStrategies = getActiveStrategies()
|
||||||
|
checked.clear()
|
||||||
|
allStrategies.forEach { list ->
|
||||||
|
checked[list] = activeStrategies.contains(list)
|
||||||
|
}
|
||||||
|
isRefreshing = false
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
LazyColumn(
|
||||||
|
contentPadding = paddingValues,
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
when {
|
||||||
|
allStrategies.isEmpty() != false -> {
|
||||||
|
item {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillParentMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.empty_list),
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
items(allStrategies) { item ->
|
||||||
|
StrategyItem(
|
||||||
|
item = item,
|
||||||
|
isChecked = checked[item] == true,
|
||||||
|
onCheckedChange = { isChecked ->
|
||||||
|
checked[item] = isChecked
|
||||||
|
if (isChecked) {
|
||||||
|
checked.keys.forEach { key ->
|
||||||
|
checked[key] = false
|
||||||
|
disableStrategy(key)
|
||||||
|
}
|
||||||
|
checked[item] = true
|
||||||
|
enableStrategy(item)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
checked[item] = false
|
||||||
|
disableStrategy(item)
|
||||||
|
}
|
||||||
|
getStatus { isEnabled ->
|
||||||
|
if (isEnabled) {
|
||||||
|
showRestartSnackbar(context, snackbarHostState, scope)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDeleteClick = {
|
||||||
|
val wasChecked = checked[item] == true
|
||||||
|
if (deleteStrategy(item)) {
|
||||||
|
allStrategies = getAllStrategies()
|
||||||
|
activeStrategies = getActiveStrategies()
|
||||||
|
checked.clear()
|
||||||
|
allStrategies.forEach { list ->
|
||||||
|
checked[list] = activeStrategies.contains(list)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
getStatus { isEnabled ->
|
||||||
|
if (isEnabled && wasChecked) {
|
||||||
|
showRestartSnackbar(context, snackbarHostState, scope)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
floatingActionButton = {
|
||||||
|
FloatingMenu(navController, filePickerLauncher)
|
||||||
|
},
|
||||||
|
snackbarHost = { SnackbarHost(hostState = snackbarHostState) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun StrategyItem(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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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("repo?source=strategies") { 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
|
||||||
|
addStrategy(launcher)
|
||||||
|
},
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(Icons.Default.UploadFile, contentDescription = stringResource(R.string.btn_add_host))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addStrategy(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() + "/strategies", 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 deleteStrategy(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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,8 @@
|
|||||||
<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_strategies">Стратегии</string>
|
||||||
|
<string name="title_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_update">Обновить</string>
|
||||||
@@ -29,7 +30,8 @@
|
|||||||
<string name="btn_download_host">Скачать</string>
|
<string name="btn_download_host">Скачать</string>
|
||||||
<string name="btn_add_host">Добавить</string>
|
<string name="btn_add_host">Добавить</string>
|
||||||
<string name="btn_back">Назад</string>
|
<string name="btn_back">Назад</string>
|
||||||
<string name="btn_no_hosts">Нет хостов</string>
|
<string name="empty_list">Пусто :(</string>
|
||||||
|
<string name="title_author">Автор: %1$s</string>
|
||||||
<string name="btn_install_host">Установить</string>
|
<string name="btn_install_host">Установить</string>
|
||||||
<string name="btn_update_host">Обновить</string>
|
<string name="btn_update_host">Обновить</string>
|
||||||
<string name="btn_installing_host">Установка</string>
|
<string name="btn_installing_host">Установка</string>
|
||||||
@@ -38,7 +40,7 @@
|
|||||||
<string name="btn_autorestart">Переодически перезапускать сервис</string>
|
<string name="btn_autorestart">Переодически перезапускать сервис</string>
|
||||||
<string name="btn_autoupdate">Авто обновление</string>
|
<string name="btn_autoupdate">Авто обновление</string>
|
||||||
<string name="btn_send_firebase_analytics">Отправлять аналитику Firebase</string>
|
<string name="btn_send_firebase_analytics">Отправлять аналитику Firebase</string>
|
||||||
<string name="alert_version">Version: %1$s → %2$s\nСписок изменений:\n%3$s</string>
|
<string name="alert_version">Версия: %1$s → %2$s\nСписок изменений:\n%3$s</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>
|
||||||
|
|||||||
@@ -2,7 +2,8 @@
|
|||||||
<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">Repository</string>
|
<string name="title_strategies">Strategies</string>
|
||||||
|
<string name="title_repo">Repository</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_update">Update</string>
|
||||||
@@ -29,7 +30,8 @@
|
|||||||
<string name="btn_download_host">Download</string>
|
<string name="btn_download_host">Download</string>
|
||||||
<string name="btn_add_host">Add</string>
|
<string name="btn_add_host">Add</string>
|
||||||
<string name="btn_back">Back</string>
|
<string name="btn_back">Back</string>
|
||||||
<string name="btn_no_hosts">No Hosts</string>
|
<string name="empty_list">Empty :(</string>
|
||||||
|
<string name="title_author">Author: %1$s</string>
|
||||||
<string name="btn_install_host">Install</string>
|
<string name="btn_install_host">Install</string>
|
||||||
<string name="btn_update_host">Update</string>
|
<string name="btn_update_host">Update</string>
|
||||||
<string name="btn_installing_host">Installing</string>
|
<string name="btn_installing_host">Installing</string>
|
||||||
@@ -44,7 +46,7 @@
|
|||||||
<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>
|
||||||
<string name="error_no_storage_message">The application requires permission to acess the storage to work properly</string>
|
<string name="error_no_storage_message">The application requires permission to access the storage to work properly</string>
|
||||||
<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>
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
Добавлена возможность загружать хосты из репозитория
|
Размер apk файла уменьшен до 4МБ
|
||||||
Рефакторинг кода
|
|
||||||
|
|||||||
BIN
images/1.png
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 76 KiB |
BIN
images/2.png
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 110 KiB |
BIN
images/3.png
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 84 KiB |
BIN
images/4.png
Normal file
|
After Width: | Height: | Size: 100 KiB |
BIN
images/5.png
Normal file
|
After Width: | Height: | Size: 100 KiB |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"version": "1.7",
|
"version": "1.9",
|
||||||
"versionCode": 8,
|
"versionCode": 10,
|
||||||
"downloadUrl": "https://github.com/CherretGit/zaprett-app/releases/download/1_7_0/app-release.apk",
|
"downloadUrl": "https://github.com/CherretGit/zaprett-app/releases/download/1_9_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"
|
||||||
}
|
}
|
||||||
|
|||||||