This commit is contained in:
zarazaex69
2026-05-04 14:03:08 +03:00
parent 3ccd7493a2
commit b87a0e8a6e
25 changed files with 364 additions and 229 deletions
@@ -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"
+2 -8
View File
@@ -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>
+5 -4
View File
@@ -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>
+1
View File
@@ -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" }