mirror of
https://github.com/openlibrecommunity/olcng.git
synced 2026-07-03 14:05:17 +02:00
REMOVEE
This commit is contained in:
@@ -612,7 +612,7 @@ object AngConfigManager {
|
||||
Log.i(AppConfig.TAG, url)
|
||||
val userAgent = it.subscription.userAgent
|
||||
|
||||
val timeout = if (url.startsWith("https://key.zarazaex.xyz/sub")) 3000 else 15000
|
||||
val timeout = if (url.startsWith("https://key.zarazaex.xyz/sub")) 3000 else 6000
|
||||
|
||||
var configText = try {
|
||||
val httpPort = SettingsManager.getHttpPort()
|
||||
|
||||
@@ -12,9 +12,9 @@ import xyz.zarazaex.olc.dto.CheckUpdateResult
|
||||
import xyz.zarazaex.olc.extension.toast
|
||||
import xyz.zarazaex.olc.extension.toastError
|
||||
import xyz.zarazaex.olc.extension.toastSuccess
|
||||
import xyz.zarazaex.olc.handler.MmkvManager
|
||||
import xyz.zarazaex.olc.handler.UpdateCheckerManager
|
||||
import xyz.zarazaex.olc.handler.V2RayNativeManager
|
||||
import xyz.zarazaex.olc.util.MarkdownUtil
|
||||
import xyz.zarazaex.olc.util.Utils
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -24,33 +24,29 @@ class CheckUpdateActivity : BaseActivity() {
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
//setContentView(binding.root)
|
||||
setContentViewWithToolbar(binding.root, showHomeAsUp = true, title = getString(R.string.update_check_for_update))
|
||||
|
||||
binding.layoutCheckUpdate.setOnClickListener {
|
||||
checkForUpdates(binding.checkPreRelease.isChecked)
|
||||
checkForUpdates()
|
||||
}
|
||||
|
||||
binding.checkPreRelease.setOnCheckedChangeListener { _, isChecked ->
|
||||
MmkvManager.encodeSettings(AppConfig.PREF_CHECK_UPDATE_PRE_RELEASE, isChecked)
|
||||
}
|
||||
binding.checkPreRelease.isChecked = true
|
||||
MmkvManager.encodeSettings(AppConfig.PREF_CHECK_UPDATE_PRE_RELEASE, true)
|
||||
// Hide the pre-release toggle - we always check releases
|
||||
binding.checkPreRelease.visibility = android.view.View.GONE
|
||||
|
||||
"v${BuildConfig.VERSION_NAME} (${V2RayNativeManager.getLibVersion()})".also {
|
||||
binding.tvVersion.text = it
|
||||
}
|
||||
|
||||
checkForUpdates(binding.checkPreRelease.isChecked)
|
||||
checkForUpdates()
|
||||
}
|
||||
|
||||
private fun checkForUpdates(includePreRelease: Boolean) {
|
||||
private fun checkForUpdates() {
|
||||
toast(R.string.update_checking_for_update)
|
||||
showLoading()
|
||||
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val result = UpdateCheckerManager.checkForUpdate(includePreRelease)
|
||||
val result = UpdateCheckerManager.checkForUpdate(false)
|
||||
if (result.hasUpdate) {
|
||||
showUpdateDialog(result)
|
||||
} else {
|
||||
@@ -67,9 +63,10 @@ class CheckUpdateActivity : BaseActivity() {
|
||||
}
|
||||
|
||||
private fun showUpdateDialog(result: CheckUpdateResult) {
|
||||
val message = result.releaseNotes?.let { MarkdownUtil.parseBasic(it) } ?: ""
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(getString(R.string.update_new_version_found, result.latestVersion))
|
||||
.setMessage(result.releaseNotes)
|
||||
.setMessage(message)
|
||||
.setPositiveButton(R.string.update_now) { _, _ ->
|
||||
result.downloadUrl?.let {
|
||||
Utils.openUri(this, it)
|
||||
@@ -78,4 +75,4 @@ class CheckUpdateActivity : BaseActivity() {
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,6 +287,13 @@ class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>(),
|
||||
//binding.refreshLayout.isRefreshing = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrolls to the top of the list (called on double-tap on tab)
|
||||
*/
|
||||
fun scrollToTop() {
|
||||
binding.recyclerView.smoothScrollToPosition(0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Scrolls to the currently selected server in the RecyclerView
|
||||
*/
|
||||
|
||||
@@ -74,7 +74,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(binding.root)
|
||||
setupToolbar(binding.toolbar, false, getString(R.string.title_server))
|
||||
setupToolbar(binding.toolbar, false, getString(R.string.app_name))
|
||||
|
||||
groupPagerAdapter = GroupPagerAdapter(this, emptyList())
|
||||
binding.viewPager.adapter = groupPagerAdapter
|
||||
@@ -172,8 +172,15 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
||||
setButtonsEnabled(!testing)
|
||||
if (testing) {
|
||||
binding.btnSummaryLite.setImageResource(R.drawable.ic_stop_24dp)
|
||||
// Allow stopping the test
|
||||
binding.btnSummaryLite.isEnabled = true
|
||||
binding.btnSummaryLite.alpha = 1.0f
|
||||
} else {
|
||||
binding.btnSummaryLite.setImageResource(R.drawable.ic_lite_bolt)
|
||||
binding.btnSummaryLite.setImageResource(R.drawable.bolt_24)
|
||||
// Re-apply running state to set correct enabled state
|
||||
val isRunning = mainViewModel.isRunning.value == true
|
||||
binding.btnSummaryLite.isEnabled = !isRunning
|
||||
binding.btnSummaryLite.alpha = if (isRunning) 0.5f else 1.0f
|
||||
}
|
||||
}
|
||||
|
||||
@@ -219,6 +226,18 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
||||
binding.viewPager.setCurrentItem(targetIndex, false)
|
||||
|
||||
binding.tabGroup.isVisible = groups.size > 1
|
||||
|
||||
// Double-tap on a tab scrolls to top of that group
|
||||
binding.tabGroup.addOnTabSelectedListener(object : com.google.android.material.tabs.TabLayout.OnTabSelectedListener {
|
||||
override fun onTabSelected(tab: com.google.android.material.tabs.TabLayout.Tab?) {}
|
||||
override fun onTabUnselected(tab: com.google.android.material.tabs.TabLayout.Tab?) {}
|
||||
override fun onTabReselected(tab: com.google.android.material.tabs.TabLayout.Tab?) {
|
||||
val currentItem = binding.viewPager.currentItem
|
||||
val itemId = groupPagerAdapter.getItemId(currentItem)
|
||||
val fragment = supportFragmentManager.findFragmentByTag("f$itemId") as? GroupServerFragment
|
||||
fragment?.scrollToTop()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun setButtonsEnabled(enabled: Boolean) {
|
||||
@@ -230,7 +249,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
||||
it.isEnabled = enabled
|
||||
it.icon?.alpha = if (enabled) 255 else 128
|
||||
}
|
||||
menu.findItem(R.id.dedupe_by_ip)?.let {
|
||||
menu.findItem(R.id.filter_by_country)?.let {
|
||||
it.isEnabled = enabled
|
||||
it.icon?.alpha = if (enabled) 255 else 128
|
||||
}
|
||||
@@ -409,13 +428,21 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
||||
binding.fab.contentDescription = getString(R.string.action_stop_service)
|
||||
setTestState(getString(R.string.connection_connected))
|
||||
binding.layoutTest.isFocusable = true
|
||||
// Block lightning while VPN is connected (unless test is running)
|
||||
if (mainViewModel.isTesting.value != true) {
|
||||
binding.btnSummaryLite.isEnabled = false
|
||||
binding.btnSummaryLite.alpha = 0.5f
|
||||
}
|
||||
} else {
|
||||
binding.fab.setImageResource(R.drawable.ic_play_24dp)
|
||||
binding.fab.setImageResource(R.drawable.rocket_launch_24)
|
||||
binding.fab.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(this, R.color.color_fab_inactive))
|
||||
binding.btnSummaryLite.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(this, R.color.color_fab_inactive))
|
||||
binding.fab.contentDescription = getString(R.string.tasker_start_service)
|
||||
setTestState(getString(R.string.connection_not_connected))
|
||||
binding.layoutTest.isFocusable = false
|
||||
// Enable lightning when VPN is disconnected
|
||||
binding.btnSummaryLite.isEnabled = true
|
||||
binding.btnSummaryLite.alpha = 1.0f
|
||||
}
|
||||
}
|
||||
|
||||
@@ -468,26 +495,6 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
||||
true
|
||||
}
|
||||
|
||||
R.id.dedupe_by_ip -> {
|
||||
androidx.appcompat.app.AlertDialog.Builder(this)
|
||||
.setTitle("Удалить дубликаты по IP")
|
||||
.setMessage("Будут удалены серверы с одинаковым IP-адресом. Оставим лучший по пингу (или первый попавшийся, если тест не запускался). Продолжить?")
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
showLoading()
|
||||
lifecycleScope.launch(kotlinx.coroutines.Dispatchers.IO) {
|
||||
val removed = mainViewModel.removeDuplicateByIp()
|
||||
kotlinx.coroutines.withContext(kotlinx.coroutines.Dispatchers.Main) {
|
||||
mainViewModel.reloadServerList()
|
||||
showStatus("Удалено дубликатов по IP: $removed")
|
||||
hideLoading()
|
||||
}
|
||||
}
|
||||
}
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
true
|
||||
}
|
||||
|
||||
R.id.sub_update -> {
|
||||
importConfigViaSub()
|
||||
true
|
||||
@@ -798,37 +805,46 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
||||
private fun showCountryFilterDialog() {
|
||||
showLoading()
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
// Collect countries from full server list (not only visible)
|
||||
mainViewModel.refreshCountryCache() // fire bg lookup for unknowns
|
||||
val allCountries = mainViewModel.collectAllCountries() // code → "🇷🇺 Россия"
|
||||
val currentFilter = mainViewModel.countryFilter
|
||||
mainViewModel.refreshCountryCache()
|
||||
// Collect all countries including UNKNOWN
|
||||
val allCountriesMap = mainViewModel.collectAllCountries().toMutableMap()
|
||||
// Add Unknown entry
|
||||
allCountriesMap[CountryDetector.UNKNOWN] = "🌐 Неизвестно"
|
||||
|
||||
val currentFilter = mainViewModel.countryFilter // empty = show all
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
hideLoading()
|
||||
if (allCountries.isEmpty()) {
|
||||
if (allCountriesMap.size <= 1) {
|
||||
showStatus("Нет серверов с известной страной")
|
||||
return@withContext
|
||||
}
|
||||
|
||||
val codes = allCountries.keys.toTypedArray()
|
||||
val labels = allCountries.values.toTypedArray()
|
||||
// empty filter = show all → treat as all checked
|
||||
val checked = BooleanArray(codes.size) {
|
||||
currentFilter.isEmpty() || codes[it] in currentFilter
|
||||
}
|
||||
val codes = allCountriesMap.keys.toTypedArray()
|
||||
val labels = allCountriesMap.values.toTypedArray()
|
||||
|
||||
// In exclude mode: checked = should be EXCLUDED
|
||||
// currentFilter stores included set (empty = show all)
|
||||
// Convert to excluded set for UI
|
||||
val allCodes = codes.toSet()
|
||||
val excludedByFilter = if (currentFilter.isEmpty()) emptySet()
|
||||
else allCodes - currentFilter
|
||||
|
||||
val checked = BooleanArray(codes.size) { codes[it] in excludedByFilter }
|
||||
|
||||
androidx.appcompat.app.AlertDialog.Builder(this@MainActivity)
|
||||
.setTitle("Фильтр по странам")
|
||||
.setTitle("Исключить страны")
|
||||
.setMultiChoiceItems(labels, checked) { _, which, isChecked ->
|
||||
checked[which] = isChecked
|
||||
}
|
||||
.setPositiveButton("Применить") { _, _ ->
|
||||
val selected = codes.filterIndexed { i, _ -> checked[i] }.toSet()
|
||||
// if all selected or none selected → no filter (show all)
|
||||
val filter = if (selected.size == codes.size || selected.isEmpty()) emptySet() else selected
|
||||
mainViewModel.applyCountryFilter(filter)
|
||||
val msg = if (filter.isEmpty()) "Показаны все страны"
|
||||
else "Фильтр: ${filter.joinToString { CountryDetector.codeToFlag(it) }}"
|
||||
val excluded = codes.filterIndexed { i, _ -> checked[i] }.toSet()
|
||||
// Convert excluded set to include filter (empty = show all)
|
||||
val included = if (excluded.isEmpty()) emptySet()
|
||||
else allCodes - excluded
|
||||
mainViewModel.applyCountryFilter(included)
|
||||
val msg = if (excluded.isEmpty()) "Показаны все страны"
|
||||
else "Скрыто: ${excluded.joinToString { CountryDetector.codeToFlag(it) }}"
|
||||
showStatus(msg)
|
||||
}
|
||||
.setNeutralButton("Сбросить") { _, _ ->
|
||||
@@ -859,15 +875,16 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
||||
}
|
||||
|
||||
private fun showUpdateAvailableDialog(result: xyz.zarazaex.olc.dto.CheckUpdateResult) {
|
||||
val message = result.releaseNotes?.let { xyz.zarazaex.olc.util.MarkdownUtil.parseBasic(it) } ?: ""
|
||||
AlertDialog.Builder(this)
|
||||
.setTitle(getString(R.string.update_new_version_found, result.latestVersion))
|
||||
.setMessage(result.releaseNotes)
|
||||
.setMessage(message)
|
||||
.setPositiveButton(R.string.update_now) { _, _ ->
|
||||
result.downloadUrl?.let {
|
||||
Utils.openUri(this, it)
|
||||
}
|
||||
}
|
||||
.setNegativeButton(android.R.string.ok, null)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
|
||||
@@ -126,7 +126,7 @@ class MainRecyclerAdapter(
|
||||
val isFav = item.profile.isFavorite
|
||||
// Set correct icon immediately, then animate scale bounce
|
||||
holder.itemMainBinding.ivFavorite.setImageResource(
|
||||
if (isFav) R.drawable.ic_star_filled else R.drawable.ic_star_empty
|
||||
if (isFav) R.drawable.ic_star_filled else R.drawable.kid_star_24
|
||||
)
|
||||
animateFavorite(holder.itemMainBinding.ivFavorite)
|
||||
}
|
||||
@@ -192,7 +192,7 @@ class MainRecyclerAdapter(
|
||||
|
||||
val isFav = profile.isFavorite
|
||||
holder.itemMainBinding.ivFavorite.setImageResource(
|
||||
if (isFav) R.drawable.ic_star_filled else R.drawable.ic_star_empty
|
||||
if (isFav) R.drawable.ic_star_filled else R.drawable.kid_star_24
|
||||
)
|
||||
|
||||
holder.itemMainBinding.ivFavorite.setOnClickListener {
|
||||
|
||||
@@ -90,7 +90,7 @@ class PerAppProxyActivity : BaseActivity() {
|
||||
}
|
||||
|
||||
appsAll = apps
|
||||
adapter = PerAppProxyAdapter(apps, viewModel)
|
||||
adapter = PerAppProxyAdapter(apps.toMutableList(), viewModel)
|
||||
binding.recyclerView.adapter = adapter
|
||||
|
||||
} catch (e: Exception) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package xyz.zarazaex.olc.ui
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@@ -9,9 +10,9 @@ import xyz.zarazaex.olc.dto.AppInfo
|
||||
import xyz.zarazaex.olc.viewmodel.PerAppProxyViewModel
|
||||
|
||||
class PerAppProxyAdapter(
|
||||
val apps: List<AppInfo>,
|
||||
val apps: MutableList<AppInfo>,
|
||||
val viewModel: PerAppProxyViewModel
|
||||
) :RecyclerView.Adapter<PerAppProxyAdapter.BaseViewHolder>() {
|
||||
) : RecyclerView.Adapter<PerAppProxyAdapter.BaseViewHolder>() {
|
||||
|
||||
companion object {
|
||||
private const val VIEW_TYPE_HEADER = 0
|
||||
@@ -29,17 +30,12 @@ class PerAppProxyAdapter(
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder {
|
||||
val ctx = parent.context
|
||||
|
||||
return when (viewType) {
|
||||
VIEW_TYPE_HEADER -> {
|
||||
val view = View(ctx)
|
||||
view.layoutParams = ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
0
|
||||
)
|
||||
view.layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0)
|
||||
BaseViewHolder(view)
|
||||
}
|
||||
|
||||
else -> AppViewHolder(ItemRecyclerBypassListBinding.inflate(LayoutInflater.from(ctx), parent, false))
|
||||
}
|
||||
}
|
||||
@@ -48,30 +44,53 @@ class PerAppProxyAdapter(
|
||||
|
||||
open class BaseViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView)
|
||||
|
||||
inner class AppViewHolder(private val itemBypassBinding: ItemRecyclerBypassListBinding) : BaseViewHolder(itemBypassBinding.root),
|
||||
View.OnClickListener {
|
||||
inner class AppViewHolder(private val itemBypassBinding: ItemRecyclerBypassListBinding) :
|
||||
BaseViewHolder(itemBypassBinding.root), View.OnClickListener {
|
||||
private lateinit var appInfo: AppInfo
|
||||
|
||||
fun bind(appInfo: AppInfo) {
|
||||
this.appInfo = appInfo
|
||||
|
||||
itemBypassBinding.icon.setImageDrawable(appInfo.appIcon)
|
||||
itemBypassBinding.name.text = if (appInfo.isSystemApp) {
|
||||
String.format("** %s", appInfo.appName)
|
||||
"** ${appInfo.appName}"
|
||||
} else {
|
||||
appInfo.appName
|
||||
}
|
||||
|
||||
itemBypassBinding.packageName.text = appInfo.packageName
|
||||
itemBypassBinding.checkBox.isChecked = viewModel.contains(appInfo.packageName)
|
||||
|
||||
itemView.setOnClickListener(this)
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
override fun onClick(v: View?) {
|
||||
val packageName = appInfo.packageName
|
||||
viewModel.toggle(packageName)
|
||||
itemBypassBinding.checkBox.isChecked = viewModel.contains(packageName)
|
||||
val isNowSelected = viewModel.contains(packageName)
|
||||
itemBypassBinding.checkBox.isChecked = isNowSelected
|
||||
|
||||
// Move selected items to top, unselected back to their position
|
||||
val currentPos = apps.indexOf(appInfo)
|
||||
if (currentPos < 0) return
|
||||
|
||||
if (isNowSelected) {
|
||||
// Find first non-selected item position (insert before it)
|
||||
val insertAt = apps.indexOfFirst { !viewModel.contains(it.packageName) }
|
||||
.takeIf { it >= 0 } ?: 0
|
||||
if (currentPos != insertAt) {
|
||||
apps.removeAt(currentPos)
|
||||
apps.add(insertAt, appInfo)
|
||||
notifyItemMoved(currentPos + 1, insertAt + 1) // +1 for header
|
||||
}
|
||||
} else {
|
||||
// Move to end of selected group
|
||||
val lastSelected = apps.indexOfLast { viewModel.contains(it.packageName) }
|
||||
val insertAt = if (lastSelected < 0) 0 else lastSelected + 1
|
||||
if (currentPos != insertAt) {
|
||||
apps.removeAt(currentPos)
|
||||
apps.add(insertAt, appInfo)
|
||||
notifyItemMoved(currentPos + 1, insertAt + 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
package xyz.zarazaex.olc.util
|
||||
|
||||
import android.content.Context
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.text.Spanned
|
||||
import android.text.style.StyleSpan
|
||||
import android.graphics.Typeface
|
||||
|
||||
/**
|
||||
* Simple markdown to Spanned converter for basic ** bold ** syntax without external libraries.
|
||||
*/
|
||||
object MarkdownUtil {
|
||||
/**
|
||||
* Convert simple markdown to Spanned. Supports:
|
||||
* - **bold**
|
||||
* - # headers (rendered as bold)
|
||||
* - - list items (bullet)
|
||||
*/
|
||||
fun parseBasic(text: String): CharSequence {
|
||||
val lines = text.split("\n")
|
||||
val builder = SpannableStringBuilder()
|
||||
|
||||
for (line in lines) {
|
||||
val processedLine = processLine(line.trimEnd())
|
||||
builder.append(processedLine)
|
||||
builder.append("\n")
|
||||
}
|
||||
|
||||
// Remove trailing newlines
|
||||
while (builder.isNotEmpty() && builder.last() == '\n') {
|
||||
builder.delete(builder.length - 1, builder.length)
|
||||
}
|
||||
|
||||
return builder
|
||||
}
|
||||
|
||||
private fun processLine(line: String): CharSequence {
|
||||
// Handle headers
|
||||
val headerLine = when {
|
||||
line.startsWith("### ") -> line.removePrefix("### ")
|
||||
line.startsWith("## ") -> line.removePrefix("## ")
|
||||
line.startsWith("# ") -> line.removePrefix("# ")
|
||||
else -> null
|
||||
}
|
||||
if (headerLine != null) {
|
||||
val sb = SpannableStringBuilder(processBold(headerLine))
|
||||
sb.setSpan(StyleSpan(Typeface.BOLD), 0, sb.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
return sb
|
||||
}
|
||||
|
||||
// Handle list items
|
||||
if (line.startsWith("- ") || line.startsWith("* ")) {
|
||||
return SpannableStringBuilder("• " + processBold(line.substring(2)))
|
||||
}
|
||||
|
||||
return processBold(line)
|
||||
}
|
||||
|
||||
private fun processBold(text: String): SpannableStringBuilder {
|
||||
val sb = SpannableStringBuilder()
|
||||
var i = 0
|
||||
while (i < text.length) {
|
||||
if (i + 1 < text.length && text[i] == '*' && text[i + 1] == '*') {
|
||||
val end = text.indexOf("**", i + 2)
|
||||
if (end > 0) {
|
||||
val boldText = text.substring(i + 2, end)
|
||||
val start = sb.length
|
||||
sb.append(boldText)
|
||||
sb.setSpan(StyleSpan(Typeface.BOLD), start, sb.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
i = end + 2
|
||||
} else {
|
||||
sb.append(text[i])
|
||||
i++
|
||||
}
|
||||
} else {
|
||||
sb.append(text[i])
|
||||
i++
|
||||
}
|
||||
}
|
||||
return sb
|
||||
}
|
||||
}
|
||||
@@ -207,13 +207,19 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
*/
|
||||
fun collectAllCountries(): Map<String, String> {
|
||||
val result = mutableMapOf<String, String>()
|
||||
var hasUnknown = false
|
||||
for (guid in serverList) {
|
||||
val profile = MmkvManager.decodeServerConfig(guid) ?: continue
|
||||
val code = CountryDetector.getCountryCode(profile.remarks, profile.server)
|
||||
if (code != CountryDetector.UNKNOWN) {
|
||||
if (code == CountryDetector.UNKNOWN) {
|
||||
hasUnknown = true
|
||||
} else {
|
||||
result[code] = "${CountryDetector.codeToFlag(code)} ${CountryDetector.codeToName(code)}"
|
||||
}
|
||||
}
|
||||
if (hasUnknown) {
|
||||
result[CountryDetector.UNKNOWN] = "🌐 Неизвестно"
|
||||
}
|
||||
return result.toSortedMap()
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M11 21h-1l1-7H7.5c-.58 0-.57-.32-.38-.66.19-.34.05-.08.07-.12C8.48 10.94 10.42 7.54 13 3h1l-1 7h3.5c.49 0 .56.33.47.51l-.07.15C12.96 17.55 11 21 11 21z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?attr/colorAccent"
|
||||
android:pathData="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm3.5-9l-1.41-4.5L12 9.91 9.91 6.5 8.5 11H5l2.62 2.47-1 3.53L9.5 15l2.5 1.88L14.5 15l2.88 2 -1-3.53L19 11h-3.5z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?attr/colorAccent"
|
||||
android:pathData="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M13.13 22.19L11.5 18.36C13.07 17.78 14.54 17 15.9 16.09L13.13 22.19zM5.64 12.5L1.81 10.87L7.91 8.1C7 9.46 6.22 10.93 5.64 12.5zM21.61 2.39C21.61 2.39 16.66 0.269 11 5.93C8.81 8.12 7.5 10.53 6.65 12.64C6.37 13.39 6.56 14.21 7.11 14.77L9.24 16.89C9.79 17.45 10.61 17.63 11.36 17.35C13.5 16.53 15.88 15.19 18.07 13C23.73 7.34 21.61 2.39 21.61 2.39zM14.54 9.46C13.76 8.68 13.76 7.41 14.54 6.63C15.32 5.85 16.59 5.85 17.37 6.63C18.14 7.41 18.15 8.68 17.37 9.46C16.59 10.24 15.32 10.24 14.54 9.46zM8.88 16.53L7.47 15.12L8.88 16.53zM6.24 22L9.88 18.36C9.54 18.27 9.21 18.12 8.91 17.91L5.27 21.55L6.24 22zM2 19.76L5.63 16.13C5.42 15.83 5.27 15.5 5.18 15.15L1.55 18.79L2 19.76zM3.45 22.45L7.09 18.82C6.74 18.73 6.41 18.58 6.11 18.37L2.47 22L3.45 22.45z"/>
|
||||
</vector>
|
||||
@@ -15,58 +15,58 @@
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/padding_spacing_dp16">
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:text="@string/split_tunneling_description"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
||||
android:alpha="0.7" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_horizontal">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/container_per_app_proxy"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/container_per_app_proxy"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal">
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/switch_per_app_proxy"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:maxLines="2"
|
||||
android:text="@string/per_app_proxy_settings_enable"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
||||
app:theme="@style/BrandedSwitch" />
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/switch_per_app_proxy"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:maxLines="2"
|
||||
android:text="@string/per_app_proxy_settings_enable"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
||||
app:theme="@style/BrandedSwitch" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/container_bypass_apps"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/switch_bypass_apps"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/switch_bypass_apps_mode"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
||||
app:theme="@style/BrandedSwitch" />
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/container_bypass_apps"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/switch_bypass_apps"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/switch_bypass_apps_mode"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
||||
app:theme="@style/BrandedSwitch" />
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
@@ -77,4 +77,4 @@
|
||||
android:scrollbars="vertical"
|
||||
app:layoutManager="androidx.recyclerview.widget.GridLayoutManager" />
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
@@ -1,83 +1,58 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true">
|
||||
android:fitsSystemWindows="true"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_check_update"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:gravity="center|start"
|
||||
android:orientation="horizontal"
|
||||
android:padding="@dimen/padding_spacing_dp16">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
<ImageView
|
||||
android:layout_width="@dimen/image_size_dp24"
|
||||
android:layout_height="@dimen/image_size_dp24"
|
||||
android:importantForAccessibility="no"
|
||||
app:srcCompat="@drawable/ic_check_update_24dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:gravity="center|start"
|
||||
android:orientation="horizontal"
|
||||
android:padding="@dimen/padding_spacing_dp16">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="@dimen/image_size_dp24"
|
||||
android:layout_height="@dimen/image_size_dp24"
|
||||
android:importantForAccessibility="no"
|
||||
app:srcCompat="@drawable/ic_source_code_24dp" />
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/check_pre_release"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:maxLines="1"
|
||||
android:paddingStart="@dimen/padding_spacing_dp16"
|
||||
android:text="@string/update_check_pre_release"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
||||
app:theme="@style/BrandedSwitch" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_check_update"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:gravity="center|start"
|
||||
android:orientation="horizontal"
|
||||
android:padding="@dimen/padding_spacing_dp16">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="@dimen/image_size_dp24"
|
||||
android:layout_height="@dimen/image_size_dp24"
|
||||
android:importantForAccessibility="no"
|
||||
app:srcCompat="@drawable/ic_check_update_24dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="@dimen/padding_spacing_dp16"
|
||||
android:text="@string/update_check_for_update"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal"
|
||||
android:padding="@dimen/padding_spacing_dp16">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_version"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/title_about"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small" />
|
||||
</LinearLayout>
|
||||
android:paddingStart="@dimen/padding_spacing_dp16"
|
||||
android:text="@string/update_check_for_update"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
<!-- Hidden pre-release toggle (kept in layout for binding compatibility) -->
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/check_pre_release"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_version"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:padding="@dimen/padding_spacing_dp16"
|
||||
android:gravity="center"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:alpha="0.5"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -16,10 +16,11 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize" />
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
app:titleCentered="true" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
@@ -53,6 +54,7 @@
|
||||
app:tabMode="fixed"
|
||||
app:tabGravity="fill"
|
||||
app:tabMaxWidth="0dp"
|
||||
app:tabRippleColor="@android:color/transparent"
|
||||
app:tabTextAppearance="@style/TabLayoutTextStyle" />
|
||||
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
@@ -88,7 +90,7 @@
|
||||
android:layout_marginEnd="16dp"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:src="@drawable/ic_lite_bolt"
|
||||
android:src="@drawable/bolt_24"
|
||||
app:tint="@color/colorWhite"
|
||||
app:backgroundTint="@color/color_fab_inactive"
|
||||
app:fabSize="normal"
|
||||
@@ -105,7 +107,7 @@
|
||||
android:contentDescription="@string/tasker_start_service"
|
||||
android:focusable="true"
|
||||
android:nextFocusLeft="@+id/layout_test"
|
||||
android:src="@drawable/ic_play_24dp"
|
||||
android:src="@drawable/rocket_launch_24"
|
||||
app:tint="@color/colorWhite"
|
||||
app:fabSize="normal"
|
||||
app:maxImageSize="28dp"
|
||||
|
||||
@@ -14,30 +14,23 @@
|
||||
android:layout_height="@dimen/view_height_dp48"
|
||||
android:padding="@dimen/padding_spacing_dp8" />
|
||||
|
||||
<LinearLayout
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/name"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1.0"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical">
|
||||
android:maxLines="1"
|
||||
android:paddingStart="@dimen/padding_spacing_dp8"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:maxLines="1"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
|
||||
<!-- package_name hidden but kept for adapter compatibility -->
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/package_name"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:visibility="gone" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/package_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:maxLines="3"
|
||||
android:paddingTop="@dimen/padding_spacing_dp8"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small" />
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.appcompat.widget.AppCompatCheckBox
|
||||
<com.google.android.material.checkbox.MaterialCheckBox
|
||||
android:id="@+id/check_box"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
@@ -45,4 +38,4 @@
|
||||
android:focusable="false"
|
||||
android:padding="@dimen/padding_spacing_dp8" />
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
@@ -113,7 +113,7 @@
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:padding="@dimen/padding_spacing_dp8"
|
||||
android:src="@drawable/ic_star_empty" />
|
||||
android:src="@drawable/kid_star_24" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_copy"
|
||||
|
||||
@@ -13,17 +13,11 @@
|
||||
android:title="Проверить задержку профилей"
|
||||
app:showAsAction="always" />
|
||||
|
||||
<item
|
||||
android:id="@+id/dedupe_by_ip"
|
||||
android:icon="@drawable/ic_delete_24dp"
|
||||
android:title="Удалить дубликаты по IP"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:id="@+id/filter_by_country"
|
||||
android:icon="@drawable/ic_outline_filter_alt_24"
|
||||
android:icon="@drawable/public_24"
|
||||
android:title="Фильтр по странам"
|
||||
app:showAsAction="never" />
|
||||
app:showAsAction="always" />
|
||||
|
||||
<item
|
||||
android:id="@+id/sub_update"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#FFFFFF</color>
|
||||
</resources>
|
||||
<color name="ic_launcher_background">#1C1C1E</color>
|
||||
</resources>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name" translatable="false">olcNG</string>
|
||||
<string name="app_name" translatable="false">olcng</string>
|
||||
<string name="app_widget_name">Switch</string>
|
||||
<string name="app_tile_name">Switch</string>
|
||||
<string name="app_tile_first_use">First use of this feature, please use the app to add server</string>
|
||||
@@ -144,8 +144,9 @@
|
||||
<string name="msg_downloading_content">Downloading content</string>
|
||||
<string name="menu_item_export_proxy_app">Export to Clipboard</string>
|
||||
<string name="menu_item_import_proxy_app">Import from Clipboard</string>
|
||||
<string name="per_app_proxy_settings">Per-app settings</string>
|
||||
<string name="per_app_proxy_settings_enable">Enable per-app</string>
|
||||
<string name="per_app_proxy_settings">Раздельное туннелирование</string>
|
||||
<string name="per_app_proxy_settings_enable">Включить</string>
|
||||
<string name="split_tunneling_description">Выберите приложения, которые будут использовать VPN. В режиме обхода — выбранные приложения НЕ используют VPN.</string>
|
||||
|
||||
<!-- Preferences -->
|
||||
<string name="title_settings">Settings</string>
|
||||
@@ -346,7 +347,7 @@
|
||||
<string name="connection_test_fail">Internet Unavailable</string>
|
||||
<string name="connection_test_error_status_code">Error code: #%d</string>
|
||||
<string name="connection_connected">Connected, tap to check connection</string>
|
||||
<string name="connection_not_connected">Not connected</string>
|
||||
<string name="connection_not_connected">Готово к подключению</string>
|
||||
<string name="connection_runing_task_left">Number of running test tasks: %s</string>
|
||||
|
||||
<string name="import_subscription_success">Subscription imported Successfully</string>
|
||||
|
||||
@@ -66,6 +66,7 @@ recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "r
|
||||
preference-ktx = { module = "androidx.preference:preference-ktx", version.ref = "preferenceKtx" }
|
||||
androidx-viewpager2 = { module = "androidx.viewpager2:viewpager2", version.ref = "viewpager2" }
|
||||
androidx-fragment = { module = "androidx.fragment:fragment-ktx", version.ref = "fragment" }
|
||||
|
||||
[plugins]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||
|
||||
Reference in New Issue
Block a user