diff --git a/.gitmodules b/.gitmodules index b8f4bdc3..cbdbc997 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "hev-socks5-tunnel"] path = hev-socks5-tunnel url = https://github.com/heiher/hev-socks5-tunnel +[submodule "material-design-icons"] + path = material-design-icons + url = https://github.com/google/material-design-icons.git diff --git a/AndroidLibXrayLite/libv2ray_main.go b/AndroidLibXrayLite/libv2ray_main.go index 77b4fecc..9b21396b 100644 --- a/AndroidLibXrayLite/libv2ray_main.go +++ b/AndroidLibXrayLite/libv2ray_main.go @@ -50,6 +50,7 @@ type PingCallback interface { OnResult(guid string, delay int64) } + // CoreCallbackHandler defines interface for receiving callbacks and notifications from the core service type CoreCallbackHandler interface { Startup() int diff --git a/V2rayNG/app/src/main/java/xyz/zarazaex/olc/handler/AngConfigManager.kt b/V2rayNG/app/src/main/java/xyz/zarazaex/olc/handler/AngConfigManager.kt index 890ba7b4..ac1f8076 100644 --- a/V2rayNG/app/src/main/java/xyz/zarazaex/olc/handler/AngConfigManager.kt +++ b/V2rayNG/app/src/main/java/xyz/zarazaex/olc/handler/AngConfigManager.kt @@ -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() diff --git a/V2rayNG/app/src/main/java/xyz/zarazaex/olc/ui/CheckUpdateActivity.kt b/V2rayNG/app/src/main/java/xyz/zarazaex/olc/ui/CheckUpdateActivity.kt index 0c2016a5..ebf2883d 100644 --- a/V2rayNG/app/src/main/java/xyz/zarazaex/olc/ui/CheckUpdateActivity.kt +++ b/V2rayNG/app/src/main/java/xyz/zarazaex/olc/ui/CheckUpdateActivity.kt @@ -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() } -} \ No newline at end of file +} diff --git a/V2rayNG/app/src/main/java/xyz/zarazaex/olc/ui/GroupServerFragment.kt b/V2rayNG/app/src/main/java/xyz/zarazaex/olc/ui/GroupServerFragment.kt index edbfb0b6..8300d1fa 100644 --- a/V2rayNG/app/src/main/java/xyz/zarazaex/olc/ui/GroupServerFragment.kt +++ b/V2rayNG/app/src/main/java/xyz/zarazaex/olc/ui/GroupServerFragment.kt @@ -287,6 +287,13 @@ class GroupServerFragment : BaseFragment(), //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 */ diff --git a/V2rayNG/app/src/main/java/xyz/zarazaex/olc/ui/MainActivity.kt b/V2rayNG/app/src/main/java/xyz/zarazaex/olc/ui/MainActivity.kt index 21589ed8..3f003910 100644 --- a/V2rayNG/app/src/main/java/xyz/zarazaex/olc/ui/MainActivity.kt +++ b/V2rayNG/app/src/main/java/xyz/zarazaex/olc/ui/MainActivity.kt @@ -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() } diff --git a/V2rayNG/app/src/main/java/xyz/zarazaex/olc/ui/MainRecyclerAdapter.kt b/V2rayNG/app/src/main/java/xyz/zarazaex/olc/ui/MainRecyclerAdapter.kt index 03915023..c6806b58 100644 --- a/V2rayNG/app/src/main/java/xyz/zarazaex/olc/ui/MainRecyclerAdapter.kt +++ b/V2rayNG/app/src/main/java/xyz/zarazaex/olc/ui/MainRecyclerAdapter.kt @@ -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 { diff --git a/V2rayNG/app/src/main/java/xyz/zarazaex/olc/ui/PerAppProxyActivity.kt b/V2rayNG/app/src/main/java/xyz/zarazaex/olc/ui/PerAppProxyActivity.kt index c8e01d45..96b1cd20 100644 --- a/V2rayNG/app/src/main/java/xyz/zarazaex/olc/ui/PerAppProxyActivity.kt +++ b/V2rayNG/app/src/main/java/xyz/zarazaex/olc/ui/PerAppProxyActivity.kt @@ -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) { diff --git a/V2rayNG/app/src/main/java/xyz/zarazaex/olc/ui/PerAppProxyAdapter.kt b/V2rayNG/app/src/main/java/xyz/zarazaex/olc/ui/PerAppProxyAdapter.kt index 2a454710..62e97d13 100644 --- a/V2rayNG/app/src/main/java/xyz/zarazaex/olc/ui/PerAppProxyAdapter.kt +++ b/V2rayNG/app/src/main/java/xyz/zarazaex/olc/ui/PerAppProxyAdapter.kt @@ -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, + val apps: MutableList, val viewModel: PerAppProxyViewModel -) :RecyclerView.Adapter() { +) : RecyclerView.Adapter() { 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) + } + } } } } diff --git a/V2rayNG/app/src/main/java/xyz/zarazaex/olc/util/MarkdownUtil.kt b/V2rayNG/app/src/main/java/xyz/zarazaex/olc/util/MarkdownUtil.kt new file mode 100644 index 00000000..f288bbd6 --- /dev/null +++ b/V2rayNG/app/src/main/java/xyz/zarazaex/olc/util/MarkdownUtil.kt @@ -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 + } +} diff --git a/V2rayNG/app/src/main/java/xyz/zarazaex/olc/viewmodel/MainViewModel.kt b/V2rayNG/app/src/main/java/xyz/zarazaex/olc/viewmodel/MainViewModel.kt index edd7b451..29a44606 100644 --- a/V2rayNG/app/src/main/java/xyz/zarazaex/olc/viewmodel/MainViewModel.kt +++ b/V2rayNG/app/src/main/java/xyz/zarazaex/olc/viewmodel/MainViewModel.kt @@ -207,13 +207,19 @@ class MainViewModel(application: Application) : AndroidViewModel(application) { */ fun collectAllCountries(): Map { val result = mutableMapOf() + 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() } diff --git a/V2rayNG/app/src/main/res/drawable/bolt_24.xml b/V2rayNG/app/src/main/res/drawable/bolt_24.xml new file mode 100644 index 00000000..ace6f736 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/bolt_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable/kid_star_24.xml b/V2rayNG/app/src/main/res/drawable/kid_star_24.xml new file mode 100644 index 00000000..027b2cb4 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/kid_star_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable/public_24.xml b/V2rayNG/app/src/main/res/drawable/public_24.xml new file mode 100644 index 00000000..017be84b --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/public_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/drawable/rocket_launch_24.xml b/V2rayNG/app/src/main/res/drawable/rocket_launch_24.xml new file mode 100644 index 00000000..f1ccd7d5 --- /dev/null +++ b/V2rayNG/app/src/main/res/drawable/rocket_launch_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/V2rayNG/app/src/main/res/layout/activity_bypass_list.xml b/V2rayNG/app/src/main/res/layout/activity_bypass_list.xml index af14aafc..19c9a543 100644 --- a/V2rayNG/app/src/main/res/layout/activity_bypass_list.xml +++ b/V2rayNG/app/src/main/res/layout/activity_bypass_list.xml @@ -15,58 +15,58 @@ android:orientation="vertical" android:padding="@dimen/padding_spacing_dp16"> + + + android:orientation="horizontal" + android:gravity="center_horizontal"> - + - - - - - - - - - - + + + + @@ -77,4 +77,4 @@ android:scrollbars="vertical" app:layoutManager="androidx.recyclerview.widget.GridLayoutManager" /> - \ No newline at end of file + diff --git a/V2rayNG/app/src/main/res/layout/activity_check_update.xml b/V2rayNG/app/src/main/res/layout/activity_check_update.xml index 35142720..e629a80c 100644 --- a/V2rayNG/app/src/main/res/layout/activity_check_update.xml +++ b/V2rayNG/app/src/main/res/layout/activity_check_update.xml @@ -1,83 +1,58 @@ - + android:fitsSystemWindows="true" + 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"> - + + - - - - - - - - - - - - - - - - - - + android:paddingStart="@dimen/padding_spacing_dp16" + android:text="@string/update_check_for_update" + android:textAppearance="@style/TextAppearance.AppCompat.Subhead" /> - + + + + + + + diff --git a/V2rayNG/app/src/main/res/layout/activity_main.xml b/V2rayNG/app/src/main/res/layout/activity_main.xml index ccfbf0f3..0d91c898 100644 --- a/V2rayNG/app/src/main/res/layout/activity_main.xml +++ b/V2rayNG/app/src/main/res/layout/activity_main.xml @@ -16,10 +16,11 @@ android:layout_width="match_parent" android:layout_height="wrap_content"> - + android:layout_height="?attr/actionBarSize" + app:titleCentered="true" /> @@ -53,6 +54,7 @@ app:tabMode="fixed" app:tabGravity="fill" app:tabMaxWidth="0dp" + app:tabRippleColor="@android:color/transparent" app:tabTextAppearance="@style/TabLayoutTextStyle" /> - + android:maxLines="1" + android:paddingStart="@dimen/padding_spacing_dp8" + android:textAppearance="@style/TextAppearance.AppCompat.Subhead" /> - + + - - - - - \ No newline at end of file + diff --git a/V2rayNG/app/src/main/res/layout/item_recycler_main.xml b/V2rayNG/app/src/main/res/layout/item_recycler_main.xml index d56d20d7..c2b17cd4 100644 --- a/V2rayNG/app/src/main/res/layout/item_recycler_main.xml +++ b/V2rayNG/app/src/main/res/layout/item_recycler_main.xml @@ -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" /> - - + app:showAsAction="always" /> - #FFFFFF - \ No newline at end of file + #1C1C1E + diff --git a/V2rayNG/app/src/main/res/values/strings.xml b/V2rayNG/app/src/main/res/values/strings.xml index 52de2e01..76f1552d 100644 --- a/V2rayNG/app/src/main/res/values/strings.xml +++ b/V2rayNG/app/src/main/res/values/strings.xml @@ -1,6 +1,6 @@ - olcNG + olcng Switch Switch First use of this feature, please use the app to add server @@ -144,8 +144,9 @@ Downloading content Export to Clipboard Import from Clipboard - Per-app settings - Enable per-app + Раздельное туннелирование + Включить + Выберите приложения, которые будут использовать VPN. В режиме обхода — выбранные приложения НЕ используют VPN. Settings @@ -346,7 +347,7 @@ Internet Unavailable Error code: #%d Connected, tap to check connection - Not connected + Готово к подключению Number of running test tasks: %s Subscription imported Successfully diff --git a/V2rayNG/gradle/libs.versions.toml b/V2rayNG/gradle/libs.versions.toml index 95d57e16..47e0b465 100644 --- a/V2rayNG/gradle/libs.versions.toml +++ b/V2rayNG/gradle/libs.versions.toml @@ -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" } diff --git a/material-design-icons b/material-design-icons new file mode 160000 index 00000000..48150758 --- /dev/null +++ b/material-design-icons @@ -0,0 +1 @@ +Subproject commit 481507587f1bdfe712939398c4dc0ecc2079ea7c