30 Commits

Author SHA1 Message Date
zarazaex69 abeb58191e refactor(ui): improve connection testing, dialog styling, and status indicators
- major release, update app
2026-05-27 16:55:09 +03:00
zarazaex69 f55ac36b5a feat(ui): change dialog divider color from colorOutlineVariant to colorOutline 2026-05-27 14:03:55 +03:00
zarazaex69 63aec656d8 feat(ui): center dialog title, add divider, and flatten dialog corners 2026-05-27 13:55:45 +03:00
zarazaex69 2062d56fdb feat(country-filter): add flag emoji, country code, and circular background to dialog items 2026-05-27 13:43:40 +03:00
zarazaex69 0b02534724 upd git ignore 2026-05-24 17:08:30 +03:00
zarazaex69 a75deb1374 feat(ui): replace built-in multi-choice dialog with custom adapter 2026-05-24 16:31:09 +03:00
zarazaex69 fbb6da0c42 fix: prevent status reset during active connection test 2026-05-24 16:24:48 +03:00
zarazaex69 fc9f18858a refactor(ping): remove chunked batching in favor of single batch call 2026-05-24 16:22:31 +03:00
zarazaex69 9df74d3ad2 perf: increase ping chunk size from 5 to 20 2026-05-24 16:18:34 +03:00
zarazaex69 cbefd19abd refactor: process ping items in chunks of 5 2026-05-24 16:16:11 +03:00
zarazaex69 5dcba978a7 fix: prevent status reset during active operations and update RU string 2026-05-24 16:01:00 +03:00
zarazaex69 b26d1bde77 feat(country-filter): support multiple flag emojis per server 2026-05-24 15:51:44 +03:00
zarazaex69 2878456437 fix(ui): use secondary container color for FAB when disconnected 2026-05-24 15:42:10 +03:00
zarazaex69 d99677ca23 feat: replace MD3 round checkbox style with custom drawable 2026-05-24 15:25:25 +03:00
zarazaex69 4dff052c35 style(strings): improve split tunneling description wording 2026-05-24 15:22:53 +03:00
zarazaex69 4f1aca4928 style: apply round checkbox style to bypass list items 2026-05-24 15:22:26 +03:00
zarazaex69 35f2a1079b feat: add donate dialog shown on app startup 2026-05-24 15:19:43 +03:00
zarazaex69 0adcbfcaf5 chore: update dependencies and build tooling 2026-05-24 13:37:56 +03:00
zarazaex69 b81708ff5c fix(build): fallback to default paths when SDK/NDK dirs don't exist 2026-05-24 12:55:05 +03:00
zarazaex69 4ad24ec2dc build: update default ANDROID_HOME path to $HOME/android-sdk 2026-05-24 12:54:12 +03:00
zarazaex69 4fa5dd4bca fix: copy buton not display 2026-05-10 13:52:03 +03:00
zarazaex69 b2f3415421 feat: fix 2026-05-09 02:07:43 +03:00
zarazaex69 6e1c774d86 fix: ui lazyyyyy 2026-05-08 12:30:27 +03:00
zarazaex69 8e9e709d12 fix: server unselect bug 2026-05-08 12:04:49 +03:00
zarazaex69 649f305a82 fix: server select bug 2026-05-08 12:04:22 +03:00
zarazaex69 44005dffd3 fix: button lock 2026-05-08 11:58:42 +03:00
zarazaex69 bee7002f54 fix: big bugfix 2026-05-08 11:56:45 +03:00
zarazaex69 0363ebaabd fix: olcNG -> olcng 2026-05-08 11:22:08 +03:00
zarazaex69 88627bbf4f fix: titile remarke search 2026-05-08 11:19:53 +03:00
zarazaex69 ceec94e5db fix: unselect 2026-05-08 11:17:07 +03:00
39 changed files with 2001 additions and 801 deletions
+9
View File
@@ -66,3 +66,12 @@ Thumbs.db
add_subscription_mmkv.py add_subscription_mmkv.py
.gitignore .gitignore
material-design-icons material-design-icons
.kiro/agents/kirograph.json
.kiro/hooks/kirograph-compress-hint.kiro.hook
.kiro/settings/mcp.json
.kiro/hooks/kirograph-sync-if-dirty.kiro.hook
.kiro/steering/kirograph.md
.kirograph/config.json
.kirograph/kirograph.db
.kirograph/.session-id
.kirograph/token-savings.jsonl
+14 -12
View File
@@ -3,13 +3,13 @@ module github.com/2dust/AndroidLibXrayLite
go 1.26 go 1.26
require ( require (
github.com/xtls/xray-core v1.260327.0 github.com/xtls/xray-core v1.260327.1-0.20260509173629-1bdb488c9ec0
golang.org/x/mobile v0.0.0-20260312152759-81488f6aeb60 golang.org/x/mobile v0.0.0-20260520154334-0e4426e1883d
) )
require ( require (
github.com/andybalholm/brotli v1.2.0 // indirect github.com/andybalholm/brotli v1.2.0 // indirect
github.com/apernet/quic-go v0.59.1-0.20260217092621-db4786c77a22 // indirect github.com/apernet/quic-go v0.59.1-0.20260425001925-6c6cc9bcb716 // indirect
github.com/cloudflare/circl v1.6.3 // indirect github.com/cloudflare/circl v1.6.3 // indirect
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 // indirect github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 // indirect
github.com/google/btree v1.1.3 // indirect github.com/google/btree v1.1.3 // indirect
@@ -19,9 +19,10 @@ require (
github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/miekg/dns v1.1.72 // indirect github.com/miekg/dns v1.1.72 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pires/go-proxyproto v0.11.0 // indirect github.com/pires/go-proxyproto v0.12.0 // indirect
github.com/quic-go/qpack v0.6.0 // indirect github.com/quic-go/qpack v0.6.0 // indirect
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af // indirect github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af // indirect
github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/sagernet/sing v0.7.13 // indirect github.com/sagernet/sing v0.7.13 // indirect
github.com/sagernet/sing-shadowsocks v0.2.9 // indirect github.com/sagernet/sing-shadowsocks v0.2.9 // indirect
@@ -30,19 +31,20 @@ require (
github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f // indirect github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f // indirect
go.uber.org/mock v0.6.0 // indirect go.uber.org/mock v0.6.0 // indirect
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/crypto v0.49.0 // indirect golang.org/x/crypto v0.51.0 // indirect
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 // indirect
golang.org/x/mod v0.34.0 // indirect golang.org/x/mod v0.36.0 // indirect
golang.org/x/net v0.52.0 // indirect golang.org/x/net v0.54.0 // indirect
golang.org/x/sync v0.20.0 // indirect golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect golang.org/x/sys v0.44.0 // indirect
golang.org/x/text v0.35.0 // indirect golang.org/x/text v0.37.0 // indirect
golang.org/x/time v0.14.0 // indirect golang.org/x/time v0.14.0 // indirect
golang.org/x/tools v0.43.0 // indirect golang.org/x/tools v0.45.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect golang.zx2c4.com/wireguard/windows v1.0.1 // indirect
google.golang.org/grpc v1.79.3 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
google.golang.org/grpc v1.81.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 // indirect gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 // indirect
+40 -36
View File
@@ -1,7 +1,7 @@
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/apernet/quic-go v0.59.1-0.20260217092621-db4786c77a22 h1:00ziBGnLWQEcR9LThDwvxOznJJquJ9bYUdmBFnawLMU= github.com/apernet/quic-go v0.59.1-0.20260425001925-6c6cc9bcb716 h1:J1O+xpLuJWkdYbw5JPGwBqIHs2J8tiEP7Py9lPqkN2I=
github.com/apernet/quic-go v0.59.1-0.20260217092621-db4786c77a22/go.mod h1:Npbg8qBtAZlsAB3FWmqwlVh5jtVG6a4DlYsOylUpvzA= github.com/apernet/quic-go v0.59.1-0.20260425001925-6c6cc9bcb716/go.mod h1:Npbg8qBtAZlsAB3FWmqwlVh5jtVG6a4DlYsOylUpvzA=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8= github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
@@ -40,14 +40,16 @@ github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs= github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4= github.com/pires/go-proxyproto v0.12.0 h1:TTCxD66dU898tahivkqc3hoceZp7P44FnorWyo9d5vM=
github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU= github.com/pires/go-proxyproto v0.12.0/go.mod h1:qUvfqUMEoX7T8g0q7TQLDnhMjdTrxnG0hvpMn+7ePNI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af h1:er2acxbi3N1nvEq6HXHUAR1nTWEJmQfqiGR8EVT9rfs= github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af h1:er2acxbi3N1nvEq6HXHUAR1nTWEJmQfqiGR8EVT9rfs=
github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM= github.com/refraction-networking/utls v1.8.3-0.20260301010127-aa6edf4b11af/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/sagernet/sing v0.7.13 h1:XNYgd8e3cxMULs/LLJspdn/deHrnPWyrrglNHeCUAYM= github.com/sagernet/sing v0.7.13 h1:XNYgd8e3cxMULs/LLJspdn/deHrnPWyrrglNHeCUAYM=
@@ -62,58 +64,60 @@ github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zd
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f h1:iy2JRioxmUpoJ3SzbFPyTxHZMbR/rSHP7dOOgYaq1O8= github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f h1:iy2JRioxmUpoJ3SzbFPyTxHZMbR/rSHP7dOOgYaq1O8=
github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f/go.mod h1:DsJblcWDGt76+FVqBVwbwRhxyyNJsGV48gJLch0OOWI= github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f/go.mod h1:DsJblcWDGt76+FVqBVwbwRhxyyNJsGV48gJLch0OOWI=
github.com/xtls/xray-core v1.260327.0 h1:g4TzxMwyPrxslZh6uD+FiG3lXKTrnNO+b4ky2OhogHE= github.com/xtls/xray-core v1.260327.1-0.20260509173629-1bdb488c9ec0 h1:ft6HiTHelF1z9i3zZVPG9Q1LcLcvEB5jzBxky/+wljk=
github.com/xtls/xray-core v1.260327.0/go.mod h1:OXMlhBloFry8mw0KwWLWLd3RQyXJzEYsCGlgsX36h60= github.com/xtls/xray-core v1.260327.1-0.20260509173629-1bdb488c9ec0/go.mod h1:LWadz6mFBKGPHAe0KscKjugHjmQeyDt5Na6J86ol/hY=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM= golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc= golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
golang.org/x/mobile v0.0.0-20260312152759-81488f6aeb60 h1:MOzyaj0wu2xneBkzkg9LHNYjDBB4W5vP043A2SYQRPA= golang.org/x/mobile v0.0.0-20260520154334-0e4426e1883d h1:pWrEKZvKeqE2xPrylgBjgCyJSpPPt3L2WG2DmA+Xccg=
golang.org/x/mobile v0.0.0-20260312152759-81488f6aeb60/go.mod h1:th6VJvzjMbrYF8SduQY5rpD0HG0GleGxjadkqSxFs3k= golang.org/x/mobile v0.0.0-20260520154334-0e4426e1883d/go.mod h1:ltIbhcRzKgwHa4ZxKJeiv0nyzcXUUYCqMyO0Y+vPmXw=
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A= golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A=
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw= golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= golang.zx2c4.com/wireguard/windows v1.0.1 h1:eOxiDVbywPC+ZQqvdCK7x+ZwWXKbYv50TtH8ysFIbw8=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= golang.zx2c4.com/wireguard/windows v1.0.1/go.mod h1:+fbT3FFdX4zzYDLwJh5+HPEcNN/3HyNdzhNSVsQM+zs=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw=
google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+1 -1
View File
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<item name="app_name" type="string">olcNG</item> <item name="app_name" type="string">olcng</item>
</resources> </resources>
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name" translatable="false">olcNG</string> <string name="app_name" translatable="false">olcng</string>
</resources> </resources>
@@ -78,6 +78,10 @@ object AppConfig {
const val PREF_DYNAMIC_COLORS = "pref_dynamic_colors" const val PREF_DYNAMIC_COLORS = "pref_dynamic_colors"
const val PREF_SUBSCRIPTIONS_BOTTOM = "pref_subscriptions_bottom" const val PREF_SUBSCRIPTIONS_BOTTOM = "pref_subscriptions_bottom"
/** Donate dialog flags. */
const val PREF_DONATE_DIALOG_DISMISSED = "pref_donate_dialog_dismissed"
const val PREF_DONATE_DIALOG_POSTPONE_UNTIL = "pref_donate_dialog_postpone_until"
/** Cache keys. */ /** Cache keys. */
const val CACHE_SUBSCRIPTION_ID = "cache_subscription_id" const val CACHE_SUBSCRIPTION_ID = "cache_subscription_id"
@@ -3,8 +3,8 @@ package xyz.zarazaex.olc.dto
data class ServerAffiliationInfo(var testDelayMillis: Long = 0L) { data class ServerAffiliationInfo(var testDelayMillis: Long = 0L) {
fun getTestDelayString(): String { fun getTestDelayString(): String {
return when { return when {
testDelayMillis == 0L -> "" testDelayMillis == 0L -> "—ms"
testDelayMillis < 0L -> "Error" testDelayMillis < 0L -> "-ms"
else -> "${testDelayMillis}ms" else -> "${testDelayMillis}ms"
} }
} }
@@ -2,5 +2,7 @@ package xyz.zarazaex.olc.dto
data class ServersCache( data class ServersCache(
val guid: String, val guid: String,
val profile: ProfileItem val profile: ProfileItem,
val testDelayMillis: Long = 0L,
val isSelected: Boolean = false
) )
@@ -16,7 +16,11 @@ object CountryDetector {
// ── Emoji flag → ISO 2-letter country code ──────────────────────────────── // ── Emoji flag → ISO 2-letter country code ────────────────────────────────
/** Extract first flag emoji found in [text] and return its ISO country code (e.g. "RU"). */ /** Extract first flag emoji found in [text] and return its ISO country code (e.g. "RU"). */
fun extractFlagCode(text: String): String? { fun extractFlagCode(text: String): String? = extractAllFlagCodes(text).firstOrNull()
/** Extract all flag emojis found in [text] and return their ISO country codes. */
fun extractAllFlagCodes(text: String): List<String> {
val result = mutableListOf<String>()
val codePoints = text.codePoints().toArray() val codePoints = text.codePoints().toArray()
var i = 0 var i = 0
while (i < codePoints.size - 1) { while (i < codePoints.size - 1) {
@@ -25,11 +29,13 @@ object CountryDetector {
if (cp1 in 0x1F1E6..0x1F1FF && cp2 in 0x1F1E6..0x1F1FF) { if (cp1 in 0x1F1E6..0x1F1FF && cp2 in 0x1F1E6..0x1F1FF) {
val c1 = ('A'.code + (cp1 - 0x1F1E6)).toChar() val c1 = ('A'.code + (cp1 - 0x1F1E6)).toChar()
val c2 = ('A'.code + (cp2 - 0x1F1E6)).toChar() val c2 = ('A'.code + (cp2 - 0x1F1E6)).toChar()
return "$c1$c2" result.add("$c1$c2")
i += 2
continue
} }
i++ i++
} }
return null return result
} }
/** Get best country code for a server (emoji first, then cache). */ /** Get best country code for a server (emoji first, then cache). */
@@ -41,6 +47,16 @@ object CountryDetector {
return UNKNOWN return UNKNOWN
} }
/** Get all country codes for a server (all emojis, then cache fallback). */
fun getCountryCodes(remarks: String, serverIp: String?): List<String> {
val flags = extractAllFlagCodes(remarks)
if (flags.isNotEmpty()) return flags
if (!serverIp.isNullOrBlank() && !isPrivateIp(serverIp)) {
MmkvManager.getCountryCache(serverIp)?.let { return listOf(it) }
}
return listOf(UNKNOWN)
}
// ── Flag emoji rendering ────────────────────────────────────────────────── // ── Flag emoji rendering ──────────────────────────────────────────────────
/** ISO code → flag emoji string */ /** ISO code → flag emoji string */
@@ -700,9 +700,15 @@ object MmkvManager {
/** Persists ISO country code for [ip]. */ /** Persists ISO country code for [ip]. */
fun setCountryCache(ip: String, code: String) { countryCacheStorage.encode(ip, code) } fun setCountryCache(ip: String, code: String) { countryCacheStorage.encode(ip, code) }
/** Loads the user's country filter preference (set of ISO codes to SHOW, empty = show all). */ /** Loads the user's country filter preference (set of ISO codes to EXCLUDE, empty = show all). */
fun getCountryFilter(): Set<String> = fun getCountryFilter(): Set<String> {
settingsStorage.decodeStringSet("pref_country_filter") ?: emptySet() // v2: semantics changed from "included" to "excluded" — reset old data on first read
if (!settingsStorage.decodeBool("pref_country_filter_v2_migrated", false)) {
settingsStorage.removeValueForKey("pref_country_filter")
settingsStorage.encode("pref_country_filter_v2_migrated", true)
}
return settingsStorage.decodeStringSet("pref_country_filter") ?: emptySet()
}
/** Saves the user's country filter preference. */ /** Saves the user's country filter preference. */
fun setCountryFilter(codes: Set<String>) { fun setCountryFilter(codes: Set<String>) {
@@ -305,17 +305,25 @@ object V2RayServiceManager {
var time = -1L var time = -1L
var errorStr = "" var errorStr = ""
val urls = listOf(
SettingsManager.getDelayTestUrl(),
SettingsManager.getDelayTestUrl(true)
)
for (url in urls) {
if (time >= 0) break
try { try {
time = coreController.measureDelay(SettingsManager.getDelayTestUrl()) time = coreController.measureDelay(url)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "StartCore-Manager: Failed to measure delay", e) Log.e(AppConfig.TAG, "StartCore-Manager: Failed to measure delay", e)
errorStr = e.message?.substringAfter("\":") ?: "empty message" errorStr = e.message?.substringAfter("\":") ?: "empty message"
} }
}
// One more retry after a brief pause to reduce false negatives
if (time == -1L) { if (time == -1L) {
kotlinx.coroutines.delay(500)
try { try {
time = coreController.measureDelay(SettingsManager.getDelayTestUrl(true)) time = coreController.measureDelay(urls[0])
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "StartCore-Manager: Failed to measure delay", e)
errorStr = e.message?.substringAfter("\":") ?: "empty message" errorStr = e.message?.substringAfter("\":") ?: "empty message"
} }
} }
@@ -330,7 +338,7 @@ object V2RayServiceManager {
// Only fetch IP info if the delay test was successful // Only fetch IP info if the delay test was successful
if (time >= 0) { if (time >= 0) {
SpeedtestManager.getRemoteIPInfo()?.let { ip -> SpeedtestManager.getRemoteIPInfo()?.let { ip ->
MessageUtil.sendMsg2UI(service, AppConfig.MSG_MEASURE_DELAY_SUCCESS, "$result\n$ip") MessageUtil.sendMsg2UI(service, AppConfig.MSG_MEASURE_DELAY_SUCCESS, "$result $ip")
} }
} }
} }
@@ -14,6 +14,7 @@ import kotlinx.coroutines.launch
import xyz.zarazaex.olc.AppConfig import xyz.zarazaex.olc.AppConfig
import xyz.zarazaex.olc.dto.PingProgressUpdate import xyz.zarazaex.olc.dto.PingProgressUpdate
import xyz.zarazaex.olc.dto.PingResultItem import xyz.zarazaex.olc.dto.PingResultItem
import xyz.zarazaex.olc.handler.MmkvManager
import xyz.zarazaex.olc.handler.SettingsManager import xyz.zarazaex.olc.handler.SettingsManager
import xyz.zarazaex.olc.handler.V2RayNativeManager import xyz.zarazaex.olc.handler.V2RayNativeManager
import xyz.zarazaex.olc.handler.V2rayConfigManager import xyz.zarazaex.olc.handler.V2rayConfigManager
@@ -57,8 +58,15 @@ class RealPingWorkerService(
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
// Prepare configurations in parallel for faster startup // Prepare configurations in parallel for faster startup.
val shuffledGuids = guids.shuffled() // Keep the currently selected server at the front so it gets a result first.
val selectedGuid = MmkvManager.getSelectServer()
val shuffledGuids = if (selectedGuid != null && guids.contains(selectedGuid)) {
val rest = guids.filter { it != selectedGuid }.shuffled()
listOf(selectedGuid) + rest
} else {
guids.shuffled()
}
val deferredItems = shuffledGuids.map { guid -> val deferredItems = shuffledGuids.map { guid ->
async(Dispatchers.IO) { async(Dispatchers.IO) {
val configResult = V2rayConfigManager.getV2rayConfig4Speedtest(context, guid) val configResult = V2rayConfigManager.getV2rayConfig4Speedtest(context, guid)
@@ -12,7 +12,6 @@ import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.ItemTouchHelper
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.google.android.material.snackbar.Snackbar
import xyz.zarazaex.olc.AppConfig import xyz.zarazaex.olc.AppConfig
import xyz.zarazaex.olc.R import xyz.zarazaex.olc.R
import xyz.zarazaex.olc.contracts.MainAdapterListener import xyz.zarazaex.olc.contracts.MainAdapterListener
@@ -76,19 +75,21 @@ class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>(),
// // Set the distance to trigger sync to 160dp // // Set the distance to trigger sync to 160dp
// binding.refreshLayout.setDistanceToTriggerSync((160 * resources.displayMetrics.density).toInt()) // binding.refreshLayout.setDistanceToTriggerSync((160 * resources.displayMetrics.density).toInt())
mainViewModel.updateListAction.observe(viewLifecycleOwner) { index -> // Each fragment subscribes independently to the shared flow and filters its own subId.
if (mainViewModel.subscriptionId != subId) { // No onResume subscription switch needed — the active fragment's subId is always correct.
return@observe lifecycleScope.launch {
mainViewModel.serverListFlow.collect { list ->
if (mainViewModel.subscriptionId == subId) {
adapter.setData(list)
}
} }
// Log.d(TAG, "GroupServerFragment updateListAction subId=$subId")
adapter.setData(mainViewModel.serversCache, index)
} }
// Log.d(TAG, "GroupServerFragment onViewCreated: subId=$subId")
} }
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
// Tell ViewModel which tab is active so it can rebuild the correct list.
// This is the only place subscriptionId changes — no more races.
mainViewModel.subscriptionIdChanged(subId) mainViewModel.subscriptionIdChanged(subId)
} }
@@ -219,7 +220,7 @@ class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>(),
*/ */
private fun removeServerSub(guid: String, position: Int) { private fun removeServerSub(guid: String, position: Int) {
mainViewModel.removeServer(guid) mainViewModel.removeServer(guid)
adapter.removeServerSub(guid, position) // adapter updates automatically via serverListFlow
} }
/** /**
@@ -231,21 +232,15 @@ class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>(),
val selected = MmkvManager.getSelectServer() val selected = MmkvManager.getSelectServer()
if (guid == selected) { if (guid == selected) {
MmkvManager.setSelectServer("") MmkvManager.setSelectServer("")
val position = mainViewModel.getPosition(guid)
adapter.setSelectServer(position, position)
if (mainViewModel.isRunning.value == true) {
ownerActivity.restartV2Ray()
}
} else { } else {
MmkvManager.setSelectServer(guid) MmkvManager.setSelectServer(guid)
val fromPosition = mainViewModel.getPosition(selected.orEmpty()) }
val toPosition = mainViewModel.getPosition(guid) // Republish snapshot so DiffUtil picks up the selection change in card background
adapter.setSelectServer(fromPosition, toPosition) mainViewModel.reloadServerList()
if (mainViewModel.isRunning.value == true) { if (mainViewModel.isRunning.value == true) {
ownerActivity.restartV2Ray() ownerActivity.restartV2Ray()
} }
} }
}
private inner class ActivityAdapterListener : MainAdapterListener { private inner class ActivityAdapterListener : MainAdapterListener {
override fun onEdit(guid: String, position: Int) { override fun onEdit(guid: String, position: Int) {
@@ -310,9 +305,7 @@ class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>(),
return return
} }
// Find the position of the selected server val position = mainViewModel.serverListFlow.value.indexOfFirst { it.guid == selectedGuid }
val serversCache = mainViewModel.serversCache
val position = serversCache.indexOfFirst { it.guid == selectedGuid }
val recyclerView = binding.recyclerView val recyclerView = binding.recyclerView
if (position >= 0) { if (position >= 0) {
@@ -1,5 +1,7 @@
package xyz.zarazaex.olc.ui package xyz.zarazaex.olc.ui
import android.animation.ArgbEvaluator
import android.animation.ValueAnimator
import android.content.Intent import android.content.Intent
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.graphics.Color import android.graphics.Color
@@ -199,10 +201,17 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
} }
checkForUpdatesOnStartup() checkForUpdatesOnStartup()
showDonateDialogIfNeeded()
} }
private fun setupViewModel() { private fun setupViewModel() {
mainViewModel.updateTestResultAction.observe(this) { setTestState(it) } mainViewModel.updateTestResultAction.observe(this) { result ->
setTestState(result)
if (result != null && mainViewModel.isRunning.value == true) {
val isSuccess = result.contains(Regex("\\d+\\s*(ms|мс|毫秒)"))
setStatusDot(if (isSuccess) DotState.CONNECTED else DotState.FAILURE)
}
}
mainViewModel.isTesting.observe(this) { testing -> mainViewModel.isTesting.observe(this) { testing ->
if (testing) { if (testing) {
@@ -217,43 +226,52 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
binding.btnSummaryLite.isEnabled = true binding.btnSummaryLite.isEnabled = true
binding.btnSummaryLite.alpha = 1.0f binding.btnSummaryLite.alpha = 1.0f
binding.btnSummaryLite.setIconResource(R.drawable.ic_stop_24dp) binding.btnSummaryLite.setIconResource(R.drawable.ic_stop_24dp)
binding.btnSummaryLite.backgroundTintList = ColorStateList.valueOf( animateButtonTint(binding.btnSummaryLite,
com.google.android.material.color.MaterialColors.getColor(this, com.google.android.material.R.attr.colorPrimaryContainer, 0) com.google.android.material.color.MaterialColors.getColor(this, com.google.android.material.R.attr.colorSecondaryContainer, 0)
) )
} else { } else {
setButtonsEnabled(true) setButtonsEnabled(true)
binding.btnSummaryLite.setIconResource(R.drawable.bolt_24) binding.btnSummaryLite.setIconResource(R.drawable.bolt_24)
binding.btnSummaryLite.backgroundTintList = ColorStateList.valueOf( animateButtonTint(binding.btnSummaryLite,
com.google.android.material.color.MaterialColors.getColor(this, com.google.android.material.R.attr.colorSecondaryContainer, 0) com.google.android.material.color.MaterialColors.getColor(this, com.google.android.material.R.attr.colorSecondaryContainer, 0)
) )
if (!isLiteTesting) {
showStatus("Проверка завершена")
}
} }
} }
mainViewModel.liteTestFinished.observe(this) { finished -> mainViewModel.liteTestFinished.observe(this) { finished ->
if (finished && isLiteTesting) { if (finished && isLiteTesting) {
isLiteTesting = false isLiteTesting = false
mainViewModel.sortByTestResults()
val firstReachable = mainViewModel.serversCache.firstOrNull { cache -> // Ищем лучший сервер ДО сортировки, прямо из текущего cache
(MmkvManager.decodeServerAffiliationInfo(cache.guid)?.testDelayMillis ?: 0L) > 0L val firstReachable = mainViewModel.serversCache
} .filter { (MmkvManager.decodeServerAffiliationInfo(it.guid)?.testDelayMillis ?: 0L) > 0L }
.minByOrNull { MmkvManager.decodeServerAffiliationInfo(it.guid)?.testDelayMillis ?: Long.MAX_VALUE }
if (firstReachable != null) { if (firstReachable != null) {
MmkvManager.setSelectServer(firstReachable.guid) MmkvManager.setSelectServer(firstReachable.guid)
mainViewModel.reloadServerList() // reload AFTER selection so indicator renders correctly }
mainViewModel.suppressPinSelected = false
mainViewModel.sortByTestResults()
mainViewModel.reloadServerList()
if (firstReachable != null) {
showStatus("Подключаемся к быстрейшему серверу") showStatus("Подключаемся к быстрейшему серверу")
// Блокируем кнопки на время подключения
setButtonsEnabled(false)
applyRunningState(isLoading = true, isRunning = false) applyRunningState(isLoading = true, isRunning = false)
startV2RayWithPermission() startV2RayWithPermission()
} else { } else {
mainViewModel.reloadServerList()
showStatus("Нет доступных серверов!") showStatus("Нет доступных серверов!")
setButtonsEnabled(true) setButtonsEnabled(true)
} }
} }
} }
mainViewModel.isRunning.observe(this) { isRunning -> mainViewModel.isRunning.observe(this) { isRunning ->
if (!isFabOperationInProgress) {
applyRunningState(false, isRunning) applyRunningState(false, isRunning)
}
// Как только VPN только что подключился — обновляем подписки через него // Как только VPN только что подключился — обновляем подписки через него
if (isRunning && !wasRunning) { if (isRunning && !wasRunning) {
updateSubsViaVpn() updateSubsViaVpn()
@@ -367,12 +385,13 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
liteActionJob?.cancel() liteActionJob?.cancel()
liteActionJob = null liteActionJob = null
mainViewModel.cancelAllTests() mainViewModel.cancelAllTests()
mainViewModel.suppressPinSelected = false
isLiteTesting = false isLiteTesting = false
isFabOperationInProgress = false isFabOperationInProgress = false
showStatus("Остановлено") showStatus("Остановлено")
setButtonsEnabled(true) setButtonsEnabled(true)
binding.btnSummaryLite.setIconResource(R.drawable.bolt_24) binding.btnSummaryLite.setIconResource(R.drawable.bolt_24)
binding.btnSummaryLite.backgroundTintList = ColorStateList.valueOf( animateButtonTint(binding.btnSummaryLite,
com.google.android.material.color.MaterialColors.getColor(this, com.google.android.material.R.attr.colorSecondaryContainer, 0) com.google.android.material.color.MaterialColors.getColor(this, com.google.android.material.R.attr.colorSecondaryContainer, 0)
) )
hideLoading() hideLoading()
@@ -393,9 +412,18 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
showStatus("Обновление профилей...") showStatus("Обновление профилей...")
showLoading() showLoading()
// Иконка молнии → стоп пока идёт обновление // Иконка молнии → стоп пока идёт обновление; FAB и меню блокируем
binding.btnSummaryLite.setIconResource(R.drawable.ic_stop_24dp) binding.btnSummaryLite.setIconResource(R.drawable.ic_stop_24dp)
binding.btnSummaryLite.isEnabled = true
binding.btnSummaryLite.alpha = 1.0f
binding.fab.isEnabled = false
binding.fab.alpha = 0.5f
val menu = binding.toolbar.menu
menu.findItem(R.id.real_ping_all)?.let { it.isEnabled = false; it.icon?.alpha = 128 }
menu.findItem(R.id.filter_by_country)?.let { it.isEnabled = false; it.icon?.alpha = 128 }
menu.findItem(R.id.sub_update)?.let { it.isEnabled = false; it.icon?.alpha = 128 }
isLiteTesting = true isLiteTesting = true
mainViewModel.suppressPinSelected = true
val result = withContext(Dispatchers.IO) { mainViewModel.updateConfigViaSubAll() } val result = withContext(Dispatchers.IO) { mainViewModel.updateConfigViaSubAll() }
val removed = withContext(Dispatchers.IO) { mainViewModel.removeDuplicateByIpAll() } val removed = withContext(Dispatchers.IO) { mainViewModel.removeDuplicateByIpAll() }
@@ -453,6 +481,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
return return
} }
isFabOperationInProgress = true isFabOperationInProgress = true
applyRunningState(isLoading = true, isRunning = false)
lifecycleScope.launch { lifecycleScope.launch {
try { try {
@@ -460,12 +489,18 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
V2RayServiceManager.stopVService(this@MainActivity) V2RayServiceManager.stopVService(this@MainActivity)
delay(1000) delay(1000)
} }
if (MmkvManager.getSelectServer().isNullOrEmpty()) {
// Сервер был снят с выбора — просто остановились, разблокируем UI
applyRunningState(isLoading = false, isRunning = false)
return@launch
}
startV2Ray() startV2Ray()
delay(1000) delay(1000)
} catch (e: Exception) { } catch (e: Exception) {
Log.e(AppConfig.TAG, "Error in restartV2Ray", e) Log.e(AppConfig.TAG, "Error in restartV2Ray", e)
} finally { } finally {
isFabOperationInProgress = false isFabOperationInProgress = false
applyRunningState(isLoading = false, isRunning = mainViewModel.isRunning.value == true)
} }
} }
} }
@@ -480,6 +515,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
private fun showStatus(message: String) { private fun showStatus(message: String) {
statusResetJob?.cancel() statusResetJob?.cancel()
binding.tvTestState.text = message binding.tvTestState.text = message
if (isFabOperationInProgress || mainViewModel.isTesting.value == true) return
statusResetJob = lifecycleScope.launch { statusResetJob = lifecycleScope.launch {
delay(3000) delay(3000)
val isRunning = mainViewModel.isRunning.value == true val isRunning = mainViewModel.isRunning.value == true
@@ -502,47 +538,68 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
return ColorStateList.valueOf(color) return ColorStateList.valueOf(color)
} }
private fun animateButtonTint(view: com.google.android.material.button.MaterialButton, toColor: Int, duration: Long = 300L) {
val from = view.backgroundTintList?.defaultColor ?: toColor
if (from == toColor) { view.backgroundTintList = ColorStateList.valueOf(toColor); return }
ValueAnimator.ofArgb(from, toColor).apply {
this.duration = duration
addUpdateListener { view.backgroundTintList = ColorStateList.valueOf(it.animatedValue as Int) }
start()
}
}
private fun applyRunningState(isLoading: Boolean, isRunning: Boolean) { private fun applyRunningState(isLoading: Boolean, isRunning: Boolean) {
val secContainer = ColorStateList.valueOf( val secContainer = ColorStateList.valueOf(
com.google.android.material.color.MaterialColors.getColor(this, com.google.android.material.R.attr.colorSecondaryContainer, 0) com.google.android.material.color.MaterialColors.getColor(this, com.google.android.material.R.attr.colorSecondaryContainer, 0)
) )
if (isLoading) { if (isLoading) {
// Во время загрузки FAB и молния остаются доступны для отмены // Во время подключения: только FAB доступен для отмены, всё остальное заблокировано
binding.fab.isEnabled = true binding.fab.isEnabled = true
binding.fab.alpha = 1.0f binding.fab.alpha = 1.0f
binding.fab.backgroundTintList = secContainer
binding.btnSummaryLite.isEnabled = false
binding.btnSummaryLite.alpha = 0.5f
val menu = binding.toolbar.menu val menu = binding.toolbar.menu
menu.findItem(R.id.real_ping_all)?.let { it.isEnabled = false; it.icon?.alpha = 128 } menu.findItem(R.id.real_ping_all)?.let { it.isEnabled = false; it.icon?.alpha = 128 }
menu.findItem(R.id.filter_by_country)?.let { it.isEnabled = false; it.icon?.alpha = 128 } menu.findItem(R.id.filter_by_country)?.let { it.isEnabled = false; it.icon?.alpha = 128 }
menu.findItem(R.id.sub_update)?.let { it.isEnabled = false; it.icon?.alpha = 128 } menu.findItem(R.id.sub_update)?.let { it.isEnabled = false; it.icon?.alpha = 128 }
binding.btnSummaryLite.isEnabled = true
binding.btnSummaryLite.alpha = 1.0f
binding.fab.backgroundTintList = secContainer
setStatusDot(DotState.LOADING) setStatusDot(DotState.LOADING)
return return
} }
val onPrimary = ColorStateList.valueOf(
com.google.android.material.color.MaterialColors.getColor(this, com.google.android.material.R.attr.colorOnPrimary, 0)
)
val onSecContainer = ColorStateList.valueOf(
com.google.android.material.color.MaterialColors.getColor(this, com.google.android.material.R.attr.colorOnSecondaryContainer, 0)
)
if (isRunning) { if (isRunning) {
setSecondaryButtonsEnabled(false) setSecondaryButtonsEnabled(false)
binding.fab.isEnabled = true binding.fab.isEnabled = true
binding.fab.alpha = 1.0f binding.fab.alpha = 1.0f
binding.fab.backgroundTintList = accentColor() binding.fab.backgroundTintList = accentColor()
binding.btnSummaryLite.backgroundTintList = secContainer binding.fab.iconTint = onPrimary
animateButtonTint(binding.btnSummaryLite, secContainer.defaultColor)
binding.fab.contentDescription = getString(R.string.action_stop_service) binding.fab.contentDescription = getString(R.string.action_stop_service)
setTestState(getString(R.string.connection_connected)) setTestState(getString(R.string.connection_connected))
binding.layoutTest.isFocusable = true binding.layoutTest.isFocusable = true
setStatusDot(DotState.CONNECTED) setStatusDot(DotState.CONNECTED)
} else { } else {
setButtonsEnabled(true) setButtonsEnabled(true)
binding.fab.backgroundTintList = accentColor() binding.fab.backgroundTintList = secContainer
binding.btnSummaryLite.backgroundTintList = secContainer binding.fab.iconTint = onSecContainer
animateButtonTint(binding.btnSummaryLite, secContainer.defaultColor)
binding.fab.contentDescription = getString(R.string.tasker_start_service) binding.fab.contentDescription = getString(R.string.tasker_start_service)
if (mainViewModel.isTesting.value != true && statusResetJob?.isActive != true) {
setTestState(getString(R.string.connection_not_connected)) setTestState(getString(R.string.connection_not_connected))
}
binding.layoutTest.isFocusable = false binding.layoutTest.isFocusable = false
setStatusDot(DotState.IDLE) setStatusDot(DotState.IDLE)
} }
} }
private enum class DotState { IDLE, CONNECTED, LOADING } private enum class DotState { IDLE, CONNECTED, LOADING, FAILURE }
private fun setStatusDot(state: DotState) { private fun setStatusDot(state: DotState) {
val dot = binding.statusDot val dot = binding.statusDot
@@ -550,9 +607,31 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
dot.alpha = 1f; dot.scaleX = 1f; dot.scaleY = 1f dot.alpha = 1f; dot.scaleX = 1f; dot.scaleY = 1f
dot.backgroundTintList = ColorStateList.valueOf(when (state) { dot.backgroundTintList = ColorStateList.valueOf(when (state) {
DotState.CONNECTED -> ContextCompat.getColor(this, R.color.status_connected) DotState.CONNECTED -> ContextCompat.getColor(this, R.color.status_connected)
DotState.LOADING -> com.google.android.material.color.MaterialColors.getColor(this, com.google.android.material.R.attr.colorPrimaryContainer, 0) DotState.FAILURE -> ContextCompat.getColor(this, R.color.status_failure)
DotState.LOADING -> com.google.android.material.color.MaterialColors.getColor(this, androidx.appcompat.R.attr.colorPrimary, 0)
DotState.IDLE -> com.google.android.material.color.MaterialColors.getColor(this, com.google.android.material.R.attr.colorOutline, 0) DotState.IDLE -> com.google.android.material.color.MaterialColors.getColor(this, com.google.android.material.R.attr.colorOutline, 0)
}) })
if (state == DotState.LOADING) {
pulseDot(dot)
}
}
private fun pulseDot(dot: android.view.View) {
dot.animate()
.alpha(0.25f)
.setDuration(600)
.withEndAction {
if (dot.isAttachedToWindow) {
dot.animate()
.alpha(1f)
.setDuration(600)
.withEndAction {
if (dot.isAttachedToWindow && mainViewModel.isTesting.value == true) {
pulseDot(dot)
}
}.start()
}
}.start()
} }
override fun onResume() { override fun onResume() {
@@ -590,6 +669,8 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
searchItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener { searchItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(item: MenuItem): Boolean { override fun onMenuItemActionExpand(item: MenuItem): Boolean {
searchView.alpha = 0f
searchView.animate().alpha(1f).setDuration(220).start()
return true return true
} }
@@ -612,7 +693,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
} }
R.id.sub_update -> { R.id.sub_update -> {
setButtonsEnabled(false) setSecondaryButtonsEnabled(false)
importConfigViaSub() importConfigViaSub()
true true
} }
@@ -802,6 +883,74 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
} }
} }
private fun showDonateDialogIfNeeded() {
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_DONATE_DIALOG_DISMISSED)) return
val postponeUntil = MmkvManager.decodeSettingsLong(AppConfig.PREF_DONATE_DIALOG_POSTPONE_UNTIL, 0L)
if (System.currentTimeMillis() < postponeUntil) return
// Откладываем на следующий тик, чтобы Activity полностью отрисовался
binding.root.post { showDonateDialog() }
}
private fun showDonateDialog() {
val view = layoutInflater.inflate(R.layout.dialog_donate, null)
val tonValue = view.findViewById<TextView>(R.id.donate_ton_value)
val trcValue = view.findViewById<TextView>(R.id.donate_trc_value)
val btcValue = view.findViewById<TextView>(R.id.donate_btc_value)
val openCard = View.OnClickListener { openUrl(getString(R.string.donate_card_link_url)) }
view.findViewById<View>(R.id.donate_card_row).setOnClickListener(openCard)
view.findViewById<View>(R.id.donate_card_open).setOnClickListener(openCard)
view.findViewById<View>(R.id.donate_ton_copy).setOnClickListener {
copyToClipboard(tonValue.text.toString())
}
view.findViewById<View>(R.id.donate_trc_copy).setOnClickListener {
copyToClipboard(trcValue.text.toString())
}
view.findViewById<View>(R.id.donate_btc_copy).setOnClickListener {
copyToClipboard(btcValue.text.toString())
}
val titleStr = getString(R.string.donate_dialog_title)
val dialog = MaterialAlertDialogBuilder(this)
.setView(view)
.setNegativeButton(R.string.donate_btn_dont_show) { d, _ ->
MmkvManager.encodeSettings(AppConfig.PREF_DONATE_DIALOG_DISMISSED, true)
d.dismiss()
}
.setCancelable(true)
.create()
// Closing (X / outside / back) postpones for 24h
val postpone = {
val postponeUntil = System.currentTimeMillis() + 24L * 60 * 60 * 1000
MmkvManager.encodeSettings(AppConfig.PREF_DONATE_DIALOG_POSTPONE_UNTIL, postponeUntil)
}
dialog.setOnCancelListener { postpone() }
dialog.setCustomTitle(buildDialogTitleWithClose(titleStr) {
postpone()
dialog.dismiss()
})
dialog.show()
}
private fun copyToClipboard(text: String) {
val clipboard = getSystemService(android.content.Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager
clipboard.setPrimaryClip(android.content.ClipData.newPlainText("addr", text))
toast(R.string.donate_toast_copied)
}
private fun openUrl(url: String) {
try {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(intent)
} catch (e: Exception) {
Log.w("MainActivity", "Failed to open url: $url", e)
}
}
private fun delAllConfig() { private fun delAllConfig() {
MaterialAlertDialogBuilder(this).setMessage(R.string.del_config_comfirm) MaterialAlertDialogBuilder(this).setMessage(R.string.del_config_comfirm)
.setPositiveButton(android.R.string.ok) { _, _ -> .setPositiveButton(android.R.string.ok) { _, _ ->
@@ -964,27 +1113,39 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
} }
val codes = allCountriesMap.keys.toTypedArray() val codes = allCountriesMap.keys.toTypedArray()
val labels = allCountriesMap.values.toTypedArray()
// In exclude mode: checked = should be EXCLUDED // currentFilter stores excluded set (empty = show all)
// currentFilter stores included set (empty = show all) val checked = BooleanArray(codes.size) { codes[it] in currentFilter }
// 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 } val adapter = object : android.widget.BaseAdapter() {
override fun getCount() = codes.size
override fun getItem(pos: Int) = codes[pos]
override fun getItemId(pos: Int) = pos.toLong()
override fun getView(pos: Int, convertView: android.view.View?, parent: android.view.ViewGroup): android.view.View {
val view = convertView ?: layoutInflater.inflate(R.layout.item_dialog_country, parent, false)
val code = codes[pos]
val isUnknown = code == CountryDetector.UNKNOWN
view.findViewById<android.widget.TextView>(R.id.flag).text =
if (isUnknown) "🌐" else CountryDetector.codeToFlag(code)
view.findViewById<android.widget.TextView>(R.id.text).text =
if (isUnknown) "Неизвестно" else CountryDetector.codeToName(code)
view.findViewById<android.widget.TextView>(R.id.code).text =
if (isUnknown) "" else code
val cb = view.findViewById<com.google.android.material.checkbox.MaterialCheckBox>(R.id.check_box)
cb.isChecked = checked[pos]
view.setOnClickListener {
checked[pos] = !checked[pos]
cb.isChecked = checked[pos]
}
return view
}
}
val dialog = MaterialAlertDialogBuilder(this@MainActivity) val dialog = MaterialAlertDialogBuilder(this@MainActivity)
.setTitle("Исключить страны") .setAdapter(adapter, null)
.setMultiChoiceItems(labels, checked) { _, which, isChecked ->
checked[which] = isChecked
}
.setPositiveButton("Применить") { _, _ -> .setPositiveButton("Применить") { _, _ ->
val excluded = codes.filterIndexed { i, _ -> checked[i] }.toSet() val excluded = codes.filterIndexed { i, _ -> checked[i] }.toSet()
val included = if (excluded.isEmpty()) emptySet() mainViewModel.applyCountryFilter(excluded)
else allCodes - excluded
mainViewModel.applyCountryFilter(included)
val msg = if (excluded.isEmpty()) "Показаны все страны" val msg = if (excluded.isEmpty()) "Показаны все страны"
else "Скрыто: ${excluded.joinToString { CountryDetector.codeToFlag(it) }}" else "Скрыто: ${excluded.joinToString { CountryDetector.codeToFlag(it) }}"
showStatus(msg) showStatus(msg)
@@ -994,8 +1155,8 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
showStatus("Показаны все страны") showStatus("Показаны все страны")
} }
.create() .create()
dialog.show()
dialog.setCustomTitle(buildDialogTitleWithClose("Исключить страны") { dialog.dismiss() }) dialog.setCustomTitle(buildDialogTitleWithClose("Исключить страны") { dialog.dismiss() })
dialog.show()
} }
} }
} }
@@ -1021,7 +1182,6 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
val message = result.releaseNotes?.let { xyz.zarazaex.olc.util.MarkdownUtil.parseBasic(it) } ?: "" val message = result.releaseNotes?.let { xyz.zarazaex.olc.util.MarkdownUtil.parseBasic(it) } ?: ""
val titleStr = getString(R.string.update_new_version_found, result.latestVersion) val titleStr = getString(R.string.update_new_version_found, result.latestVersion)
val dialog = MaterialAlertDialogBuilder(this) val dialog = MaterialAlertDialogBuilder(this)
.setTitle(titleStr)
.setMessage(message) .setMessage(message)
.setPositiveButton(R.string.update_now) { _, _ -> .setPositiveButton(R.string.update_now) { _, _ ->
result.downloadUrl?.let { result.downloadUrl?.let {
@@ -1029,8 +1189,8 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
} }
} }
.create() .create()
dialog.show()
dialog.setCustomTitle(buildDialogTitleWithClose(titleStr) { dialog.dismiss() }) dialog.setCustomTitle(buildDialogTitleWithClose(titleStr) { dialog.dismiss() })
dialog.show()
} }
private fun buildDialogTitleWithClose(title: String, onClose: () -> Unit): View { private fun buildDialogTitleWithClose(title: String, onClose: () -> Unit): View {
@@ -59,46 +59,37 @@ class MainRecyclerAdapter(
} }
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
fun setData(newData: MutableList<ServersCache>?, position: Int = -1) { fun setData(newData: List<ServersCache>) {
val parsedNewData = newData?.toList() ?: emptyList() val oldData = data
val parsedNewData = newData
if (data.isEmpty() || parsedNewData.isEmpty() || position >= 0) { if (oldData.isEmpty() || parsedNewData.isEmpty()) {
data = parsedNewData.toMutableList() data = parsedNewData.toMutableList()
recomputePingRange() recomputePingRange()
if (position >= 0 && position in data.indices) {
notifyItemChanged(position)
} else {
notifyDataSetChanged() notifyDataSetChanged()
}
return return
} }
val oldData = data
val lm = recyclerView?.layoutManager as? androidx.recyclerview.widget.LinearLayoutManager val lm = recyclerView?.layoutManager as? androidx.recyclerview.widget.LinearLayoutManager
val firstVisible = lm?.findFirstVisibleItemPosition()?.coerceAtLeast(0) ?: 0 val firstVisible = lm?.findFirstVisibleItemPosition()?.coerceAtLeast(0) ?: 0
val isAtTop = firstVisible == 0 && (lm?.findViewByPosition(0)?.top ?: 0) >= 0 val isAtTop = firstVisible == 0 && (lm?.findViewByPosition(0)?.top ?: 0) >= 0
val firstVisibleGuid = if (!isAtTop) oldData.getOrNull(firstVisible)?.guid else null val firstVisibleGuid = if (!isAtTop) oldData.getOrNull(firstVisible)?.guid else null
val diffResult = val diffResult = androidx.recyclerview.widget.DiffUtil.calculateDiff(
androidx.recyclerview.widget.DiffUtil.calculateDiff(
object : androidx.recyclerview.widget.DiffUtil.Callback() { object : androidx.recyclerview.widget.DiffUtil.Callback() {
override fun getOldListSize() = oldData.size override fun getOldListSize() = oldData.size
override fun getNewListSize() = parsedNewData.size override fun getNewListSize() = parsedNewData.size
override fun areItemsTheSame(oldPos: Int, newPos: Int): Boolean { override fun areItemsTheSame(oldPos: Int, newPos: Int) =
return oldData[oldPos].guid == parsedNewData[newPos].guid oldData[oldPos].guid == parsedNewData[newPos].guid
}
override fun areContentsTheSame(oldPos: Int, newPos: Int): Boolean { override fun areContentsTheSame(oldPos: Int, newPos: Int): Boolean {
val oldProfile = oldData[oldPos].profile val old = oldData[oldPos]
val newProfile = parsedNewData[newPos].profile val new = parsedNewData[newPos]
val oldGuid = oldData[oldPos].guid return old.profile == new.profile &&
val newGuid = parsedNewData[newPos].guid old.profile.isFavorite == new.profile.isFavorite &&
return oldProfile == newProfile && old.isSelected == new.isSelected &&
oldProfile.isFavorite == newProfile.isFavorite && old.testDelayMillis == new.testDelayMillis
(oldGuid == MmkvManager.getSelectServer()) == (newGuid == MmkvManager.getSelectServer()) &&
MmkvManager.decodeServerAffiliationInfo(oldGuid)?.testDelayMillis ==
MmkvManager.decodeServerAffiliationInfo(newGuid)?.testDelayMillis
} }
override fun getChangePayload(oldPos: Int, newPos: Int): Any? { override fun getChangePayload(oldPos: Int, newPos: Int): Any? {
@@ -175,16 +166,19 @@ class MainRecyclerAdapter(
if (addressText.isEmpty()) View.GONE else View.VISIBLE if (addressText.isEmpty()) View.GONE else View.VISIBLE
// TestResult // TestResult
val aff = MmkvManager.decodeServerAffiliationInfo(guid) val delayMillis = data[position].testDelayMillis
holder.itemMainBinding.tvTestResult.text = aff?.getTestDelayString().orEmpty() holder.itemMainBinding.tvTestResult.text = when {
delayMillis == 0L -> "—ms"
delayMillis < 0L -> "-ms"
else -> "${delayMillis}ms"
}
holder.itemMainBinding.tvTestResult.setTextColor( holder.itemMainBinding.tvTestResult.setTextColor(
getPingColor(context, aff?.testDelayMillis) getPingColor(context, delayMillis)
) )
(holder.itemMainBinding.tvTestResult.layoutParams as? ViewGroup.MarginLayoutParams)?.marginStart = (holder.itemMainBinding.tvTestResult.layoutParams as? ViewGroup.MarginLayoutParams)?.marginStart =
if (addressText.isEmpty()) 0 else 6.dpToPx(context) if (addressText.isEmpty()) 0 else 6.dpToPx(context)
// Keep regular list items on the page surface; selected state is a quiet surface pill. val isSelected = data[position].isSelected
val isSelected = guid == MmkvManager.getSelectServer()
holder.itemMainBinding.cardContainer.apply { holder.itemMainBinding.cardContainer.apply {
val selectedColor = MaterialColors.getColor( val selectedColor = MaterialColors.getColor(
context, context,
@@ -274,9 +268,7 @@ class MainRecyclerAdapter(
private fun recomputePingRange() { private fun recomputePingRange() {
val delays = data.mapNotNull { item -> val delays = data.mapNotNull { item ->
MmkvManager.decodeServerAffiliationInfo(item.guid) item.testDelayMillis.takeIf { it > 0L }
?.testDelayMillis
?.takeIf { it > 0L }
} }
minReachablePing = delays.minOrNull() minReachablePing = delays.minOrNull()
maxReachablePing = delays.maxOrNull() maxReachablePing = delays.maxOrNull()
@@ -297,21 +289,6 @@ class MainRecyclerAdapter(
return subRemarks?.toString() ?: "" return subRemarks?.toString() ?: ""
} }
fun removeServerSub(guid: String, position: Int) {
val idx = data.indexOfFirst { it.guid == guid }
if (idx >= 0) {
data.removeAt(idx)
recomputePingRange()
notifyItemRemoved(idx)
notifyItemRangeChanged(idx, data.size - idx)
}
}
fun setSelectServer(fromPosition: Int, toPosition: Int) {
notifyItemChanged(fromPosition)
notifyItemChanged(toPosition)
}
private fun Int.dpToPx(context: android.content.Context): Int { private fun Int.dpToPx(context: android.content.Context): Int {
return (this * context.resources.displayMetrics.density).toInt() return (this * context.resources.displayMetrics.density).toInt()
} }
@@ -362,6 +339,8 @@ class MainRecyclerAdapter(
BaseViewHolder(itemFooterBinding.root) BaseViewHolder(itemFooterBinding.root)
override fun onItemMove(fromPosition: Int, toPosition: Int): Boolean { override fun onItemMove(fromPosition: Int, toPosition: Int): Boolean {
// ViewModel swaps both serverList and _serversCache, then publishSnapshot triggers setData.
// We optimistically swap local data + animate immediately for smooth drag UX.
mainViewModel.swapServer(fromPosition, toPosition) mainViewModel.swapServer(fromPosition, toPosition)
if (fromPosition < data.size && toPosition < data.size) { if (fromPosition < data.size && toPosition < data.size) {
Collections.swap(data, fromPosition, toPosition) Collections.swap(data, fromPosition, toPosition)
@@ -33,6 +33,9 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelChildren import kotlinx.coroutines.cancelChildren
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.util.Collections import java.util.Collections
@@ -42,15 +45,24 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
private var serverList = mutableListOf<String>() private var serverList = mutableListOf<String>()
var subscriptionId: String = MmkvManager.decodeSettingsString(AppConfig.CACHE_SUBSCRIPTION_ID, "").orEmpty() var subscriptionId: String = MmkvManager.decodeSettingsString(AppConfig.CACHE_SUBSCRIPTION_ID, "").orEmpty()
var keywordFilter = "" var keywordFilter = ""
/** ISO codes to show (empty = show all) */ /** ISO codes to EXCLUDE (empty = show all) */
var countryFilter: Set<String> = MmkvManager.getCountryFilter() var countryFilter: Set<String> = MmkvManager.getCountryFilter()
private set private set
val serversCache = mutableListOf<ServersCache>()
// Internal mutable cache — never exposed directly
private val _serversCache = mutableListOf<ServersCache>()
// Read-only snapshot for external consumers that need direct access (e.g. export, ping)
val serversCache: List<ServersCache> get() = _serversCache.toList()
// Single source of truth for the list UI — emits a new immutable snapshot on every change
private val _serverListFlow = MutableStateFlow<List<ServersCache>>(emptyList())
val serverListFlow: StateFlow<List<ServersCache>> = _serverListFlow.asStateFlow()
val isRunning by lazy { MutableLiveData<Boolean>() } val isRunning by lazy { MutableLiveData<Boolean>() }
val updateListAction by lazy { MutableLiveData<Int>() }
val updateTestResultAction by lazy { MutableLiveData<String>() } val updateTestResultAction by lazy { MutableLiveData<String>() }
val liteTestFinished = MutableLiveData<Boolean>() val liteTestFinished = MutableLiveData<Boolean>()
val isTesting by lazy { MutableLiveData<Boolean>().also { it.value = false } } val isTesting by lazy { MutableLiveData<Boolean>().also { it.value = false } }
var suppressPinSelected = false
private val tcpingTestScope by lazy { CoroutineScope(Dispatchers.IO) } private val tcpingTestScope by lazy { CoroutineScope(Dispatchers.IO) }
/** /**
@@ -120,9 +132,8 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
list list
} }
pinSelectedGuidToTop(serverList) if (!suppressPinSelected) pinSelectedGuidToTop(serverList)
updateCache() updateCache()
updateListAction.value = -1
} }
/** /**
@@ -132,10 +143,11 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
fun removeServer(guid: String) { fun removeServer(guid: String) {
serverList.remove(guid) serverList.remove(guid)
MmkvManager.removeServer(guid) MmkvManager.removeServer(guid)
val index = getPosition(guid) val index = _serversCache.indexOfFirst { it.guid == guid }
if (index >= 0) { if (index >= 0) {
serversCache.removeAt(index) _serversCache.removeAt(index)
} }
publishSnapshot()
} }
/** /**
@@ -149,17 +161,18 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
} }
Collections.swap(serverList, fromPosition, toPosition) Collections.swap(serverList, fromPosition, toPosition)
Collections.swap(serversCache, fromPosition, toPosition) Collections.swap(_serversCache, fromPosition, toPosition)
publishSnapshot()
MmkvManager.encodeServerList(serverList, subscriptionId) MmkvManager.encodeServerList(serverList, subscriptionId)
} }
/** /**
* Updates the cache of servers. * Rebuilds _serversCache from serverList and publishes a new snapshot to serverListFlow.
*/ */
@Synchronized @Synchronized
fun updateCache() { fun updateCache() {
serversCache.clear() _serversCache.clear()
val kw = keywordFilter.trim() val kw = keywordFilter.trim()
val searchRegex = try { val searchRegex = try {
if (kw.isNotEmpty()) Regex(kw, setOf(RegexOption.IGNORE_CASE)) else null if (kw.isNotEmpty()) Regex(kw, setOf(RegexOption.IGNORE_CASE)) else null
@@ -167,17 +180,19 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
null null
} }
val activeCountryFilter = countryFilter val activeCountryFilter = countryFilter
val selectedGuid = MmkvManager.getSelectServer().orEmpty()
for (guid in serverList) { for (guid in serverList) {
val profile = MmkvManager.decodeServerConfig(guid) ?: continue val profile = MmkvManager.decodeServerConfig(guid) ?: continue
// Country filter
if (activeCountryFilter.isNotEmpty()) { if (activeCountryFilter.isNotEmpty()) {
val code = CountryDetector.getCountryCode(profile.remarks, profile.server) val codes = CountryDetector.getCountryCodes(profile.remarks, profile.server)
if (code !in activeCountryFilter) continue if (codes.all { it in activeCountryFilter }) continue
} }
val delay = MmkvManager.decodeServerAffiliationInfo(guid)?.testDelayMillis ?: 0L
if (kw.isEmpty()) { if (kw.isEmpty()) {
serversCache.add(ServersCache(guid, profile)) _serversCache.add(ServersCache(guid, profile, delay, guid == selectedGuid))
continue continue
} }
@@ -190,15 +205,49 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|| server.matchesPattern(searchRegex, kw) || server.matchesPattern(searchRegex, kw)
|| protocol.matchesPattern(searchRegex, kw) || protocol.matchesPattern(searchRegex, kw)
) { ) {
serversCache.add(ServersCache(guid, profile)) _serversCache.add(ServersCache(guid, profile, delay, guid == selectedGuid))
} }
} }
publishSnapshot()
} }
/** Sets a new country filter and reloads list. Pass empty set to show all. */ /** Emits an immutable copy of _serversCache to the Flow. Must be called on Main or from @Synchronized blocks. */
fun applyCountryFilter(codes: Set<String>) { private fun publishSnapshot() {
countryFilter = codes _serverListFlow.value = _serversCache.toList()
MmkvManager.setCountryFilter(codes) }
/** Builds a snapshot of ServersCache for the given subId without changing global state. */
fun reloadForSub(subId: String): MutableList<ServersCache>? {
val guids = when {
subId.isEmpty() -> MmkvManager.decodeAllServerList()
subId.startsWith("group_") -> {
val allSubs = MmkvManager.decodeSubscriptions()
val groupSubs = when (subId) {
"group_white" -> allSubs.filter {
it.subscription.remarks.startsWith("БЕЛЫЕ", ignoreCase = true) ||
it.subscription.remarks.startsWith("WHITE", ignoreCase = true)
}
"group_black" -> allSubs.filter {
it.subscription.remarks.startsWith("ЧЕРНЫЕ", ignoreCase = true) ||
it.subscription.remarks.startsWith("BLACK", ignoreCase = true)
}
else -> emptyList()
}
groupSubs.flatMap { MmkvManager.decodeServerList(it.guid) }.toMutableList()
}
else -> MmkvManager.decodeServerList(subId)
}
return guids.mapNotNull { guid ->
val profile = MmkvManager.decodeServerConfig(guid) ?: return@mapNotNull null
val delay = MmkvManager.decodeServerAffiliationInfo(guid)?.testDelayMillis ?: 0L
ServersCache(guid, profile, delay)
}.toMutableList()
}
/** Sets excluded countries and reloads list. Pass empty set to show all. */
fun applyCountryFilter(excludedCodes: Set<String>) {
countryFilter = excludedCodes
MmkvManager.setCountryFilter(excludedCodes)
reloadServerList() reloadServerList()
} }
@@ -211,13 +260,15 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
var hasUnknown = false var hasUnknown = false
for (guid in MmkvManager.decodeAllServerList()) { for (guid in MmkvManager.decodeAllServerList()) {
val profile = MmkvManager.decodeServerConfig(guid) ?: continue val profile = MmkvManager.decodeServerConfig(guid) ?: continue
val code = CountryDetector.getCountryCode(profile.remarks, profile.server) val codes = CountryDetector.getCountryCodes(profile.remarks, profile.server)
for (code in codes) {
if (code == CountryDetector.UNKNOWN) { if (code == CountryDetector.UNKNOWN) {
hasUnknown = true hasUnknown = true
} else { } else {
result[code] = "${CountryDetector.codeToFlag(code)} ${CountryDetector.codeToName(code)}" result[code] = "${CountryDetector.codeToFlag(code)} ${CountryDetector.codeToName(code)}"
} }
} }
}
if (hasUnknown) { if (hasUnknown) {
result[CountryDetector.UNKNOWN] = "🌐 Неизвестно" result[CountryDetector.UNKNOWN] = "🌐 Неизвестно"
} }
@@ -274,9 +325,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
fun exportAllServer(): Int { fun exportAllServer(): Int {
val serverListCopy = val serverListCopy =
if (subscriptionId.isEmpty() && keywordFilter.isEmpty()) { if (subscriptionId.isEmpty() && keywordFilter.isEmpty()) {
serverList serverList.toList()
} else { } else {
serversCache.map { it.guid }.toList() _serversCache.map { it.guid }
} }
val ret = AngConfigManager.shareNonCustomConfigsToClipboard( val ret = AngConfigManager.shareNonCustomConfigsToClipboard(
@@ -292,9 +343,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
fun testAllTcping() { fun testAllTcping() {
tcpingTestScope.coroutineContext[Job]?.cancelChildren() tcpingTestScope.coroutineContext[Job]?.cancelChildren()
SpeedtestManager.closeAllTcpSockets() SpeedtestManager.closeAllTcpSockets()
MmkvManager.clearAllTestDelayResults(serversCache.map { it.guid }.toList()) MmkvManager.clearAllTestDelayResults(_serversCache.map { it.guid })
val serversCopy = serversCache.toList() val serversCopy = _serversCache.toList()
for (item in serversCopy) { for (item in serversCopy) {
item.profile.let { outbound -> item.profile.let { outbound ->
val serverAddress = outbound.server val serverAddress = outbound.server
@@ -304,7 +355,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
val testResult = SpeedtestManager.tcping(serverAddress, serverPort.toInt()) val testResult = SpeedtestManager.tcping(serverAddress, serverPort.toInt())
launch(Dispatchers.Main) { launch(Dispatchers.Main) {
MmkvManager.encodeServerTestDelayMillis(item.guid, testResult) MmkvManager.encodeServerTestDelayMillis(item.guid, testResult)
updateListAction.value = getPosition(item.guid) refreshPingInCache(listOf(item.guid))
} }
} }
} }
@@ -339,15 +390,17 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
if (removed > 0) { if (removed > 0) {
reloadServerList() reloadServerList()
} }
MmkvManager.clearAllTestDelayResults(serversCache.map { it.guid }.toList()) if (!suppressPinSelected) {
updateListAction.value = -1 MmkvManager.clearAllTestDelayResults(_serversCache.map { it.guid })
}
publishSnapshot()
isTesting.value = true isTesting.value = true
viewModelScope.launch(Dispatchers.Default) { viewModelScope.launch(Dispatchers.Default) {
if (serversCache.isEmpty()) { if (_serversCache.isEmpty()) {
withContext(Dispatchers.Main) { reloadServerList() } withContext(Dispatchers.Main) { reloadServerList() }
} }
if (serversCache.isEmpty()) { if (_serversCache.isEmpty()) {
withContext(Dispatchers.Main) { isTesting.value = false } withContext(Dispatchers.Main) { isTesting.value = false }
return@launch return@launch
} }
@@ -360,7 +413,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
key = AppConfig.MSG_MEASURE_CONFIG, key = AppConfig.MSG_MEASURE_CONFIG,
subscriptionId = actualSubId, subscriptionId = actualSubId,
serverGuids = if (keywordFilter.isNotEmpty() || subscriptionId.startsWith("group_")) { serverGuids = if (keywordFilter.isNotEmpty() || subscriptionId.startsWith("group_")) {
serversCache.map { it.guid } _serversCache.map { it.guid }
} else { } else {
emptyList() emptyList()
} }
@@ -472,9 +525,8 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
* @return The position of the server. * @return The position of the server.
*/ */
fun getPosition(guid: String): Int { fun getPosition(guid: String): Int {
serversCache.forEachIndexed { index, it -> _serversCache.forEachIndexed { index, it ->
if (it.guid == guid) if (it.guid == guid) return index
return index
} }
return -1 return -1
} }
@@ -484,7 +536,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
* @return The number of removed servers. * @return The number of removed servers.
*/ */
fun removeDuplicateServer(): Int { fun removeDuplicateServer(): Int {
val serversCacheCopy = serversCache.toList().toMutableList() val serversCacheCopy = _serversCache.toList()
val deleteServer = mutableListOf<String>() val deleteServer = mutableListOf<String>()
serversCacheCopy.forEachIndexed { index, sc -> serversCacheCopy.forEachIndexed { index, sc ->
val profile = sc.profile val profile = sc.profile
@@ -511,9 +563,8 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
*/ */
fun removeDuplicateByIp(): Int { fun removeDuplicateByIp(): Int {
val selectedGuid = MmkvManager.getSelectServer() val selectedGuid = MmkvManager.getSelectServer()
// Group all currently visible servers by their IP address
val byIp = LinkedHashMap<String, MutableList<ServersCache>>() val byIp = LinkedHashMap<String, MutableList<ServersCache>>()
for (sc in serversCache) { for (sc in _serversCache) {
val ip = sc.profile.server?.trim()?.lowercase() ?: continue val ip = sc.profile.server?.trim()?.lowercase() ?: continue
byIp.getOrPut(ip) { mutableListOf() }.add(sc) byIp.getOrPut(ip) { mutableListOf() }.add(sc)
} }
@@ -610,11 +661,11 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
if (subscriptionId.isEmpty() && keywordFilter.isEmpty()) { if (subscriptionId.isEmpty() && keywordFilter.isEmpty()) {
MmkvManager.removeAllServer() MmkvManager.removeAllServer()
} else { } else {
val serversCopy = serversCache.toList() val serversCopy = _serversCache.toList()
for (item in serversCopy) { for (item in serversCopy) {
MmkvManager.removeServer(item.guid) MmkvManager.removeServer(item.guid)
} }
serversCache.toList().count() serversCopy.count()
} }
return count return count
} }
@@ -626,20 +677,43 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
* Sorts serversCache in-place by test delay in real time (during a ping test). * Sorts serversCache in-place by test delay in real time (during a ping test).
* Favorites always come first, then sorted ascending by delay (failed/untested go to bottom). * Favorites always come first, then sorted ascending by delay (failed/untested go to bottom).
*/ */
@Synchronized
fun refreshPingInCache(guids: List<String>) {
val guidSet = guids.toHashSet()
for (i in _serversCache.indices) {
val item = _serversCache[i]
if (item.guid in guidSet) {
val delay = MmkvManager.decodeServerAffiliationInfo(item.guid)?.testDelayMillis ?: 0L
if (item.testDelayMillis != delay) {
_serversCache[i] = item.copy(testDelayMillis = delay)
}
}
}
publishSnapshot()
}
@Synchronized @Synchronized
fun sortServersCacheInPlace() { fun sortServersCacheInPlace() {
serversCache.sortWith(compareBy( for (i in _serversCache.indices) {
val item = _serversCache[i]
val delay = MmkvManager.decodeServerAffiliationInfo(item.guid)?.testDelayMillis ?: 0L
if (item.testDelayMillis != delay) {
_serversCache[i] = item.copy(testDelayMillis = delay)
}
}
_serversCache.sortWith(compareBy(
{ !it.profile.isFavorite }, { !it.profile.isFavorite },
{ {
val delay = MmkvManager.decodeServerAffiliationInfo(it.guid)?.testDelayMillis ?: 0L val delay = it.testDelayMillis
when { when {
delay > 0L -> delay delay > 0L -> delay
delay == 0L -> Long.MAX_VALUE - 1 // untested delay == 0L -> Long.MAX_VALUE - 1
else -> Long.MAX_VALUE // failed else -> Long.MAX_VALUE
} }
} }
)) ))
pinSelectedCacheItemToTop(serversCache) if (!suppressPinSelected) pinSelectedCacheItemToTop(_serversCache)
publishSnapshot()
} }
fun sortByTestResults() { fun sortByTestResults() {
@@ -796,7 +870,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
} }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
reloadServerList() reloadServerList() // rebuilds _serversCache + publishSnapshot
isTesting.value = false isTesting.value = false
liteTestFinished.value = true liteTestFinished.value = true
liteTestFinished.value = false liteTestFinished.value = false
@@ -834,8 +908,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
AppConfig.MSG_MEASURE_CONFIG_SUCCESS -> { AppConfig.MSG_MEASURE_CONFIG_SUCCESS -> {
val resultPair = intent.serializable<Pair<String, Long>>("content") ?: return val resultPair = intent.serializable<Pair<String, Long>>("content") ?: return
MmkvManager.encodeServerTestDelayMillis(resultPair.first, resultPair.second) MmkvManager.encodeServerTestDelayMillis(resultPair.first, resultPair.second)
sortServersCacheInPlace() refreshPingInCache(listOf(resultPair.first))
updateListAction.value = -1 if (!suppressPinSelected) sortServersCacheInPlace()
// publishSnapshot() already called inside refresh/sort above
} }
AppConfig.MSG_MEASURE_CONFIG_BATCH -> { AppConfig.MSG_MEASURE_CONFIG_BATCH -> {
@@ -843,8 +918,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
update.results.forEach { result -> update.results.forEach { result ->
MmkvManager.encodeServerTestDelayMillis(result.guid, result.delay) MmkvManager.encodeServerTestDelayMillis(result.guid, result.delay)
} }
sortServersCacheInPlace() refreshPingInCache(update.results.map { it.guid })
updateListAction.value = -1 if (!suppressPinSelected) sortServersCacheInPlace()
// publishSnapshot() already called inside refresh/sort above
} }
AppConfig.MSG_MEASURE_CONFIG_NOTIFY -> { AppConfig.MSG_MEASURE_CONFIG_NOTIFY -> {
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="?attr/colorSurfaceContainerHighest" />
</shape>
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="oval">
<solid android:color="?attr/colorPrimary" />
<size
android:width="20dp"
android:height="20dp" />
</shape>
</item>
<item
android:drawable="@drawable/ic_check_white"
android:width="14dp"
android:height="14dp"
android:gravity="center" />
</layer-list>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/checkbox_round_checked" android:state_checked="true" />
<item android:drawable="@drawable/checkbox_round_unchecked" />
</selector>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<stroke
android:width="2dp"
android:color="?attr/colorOnSurfaceVariant" />
<size
android:width="20dp"
android:height="20dp" />
</shape>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="14dp"
android:height="14dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@color/md_theme_onPrimary"
android:pathData="M9,16.2L4.8,12l-1.4,1.4L9,19 21,7l-1.4,-1.4L9,16.2z" />
</vector>
@@ -24,7 +24,6 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" android:layout_height="?attr/actionBarSize"
app:title="@string/app_name" app:title="@string/app_name"
app:titleCentered="true"
app:titleTextColor="?attr/colorOnSurface" app:titleTextColor="?attr/colorOnSurface"
app:titleTextAppearance="@style/TextAppearance.Material3.TitleLarge" /> app:titleTextAppearance="@style/TextAppearance.Material3.TitleLarge" />
@@ -145,9 +144,9 @@
android:contentDescription="@string/tasker_start_service" android:contentDescription="@string/tasker_start_service"
app:icon="@drawable/shield_24" app:icon="@drawable/shield_24"
app:iconSize="26dp" app:iconSize="26dp"
app:backgroundTint="?attr/colorPrimary" app:backgroundTint="?attr/colorSecondaryContainer"
app:iconTint="?attr/colorOnPrimary" app:iconTint="?attr/colorOnSecondaryContainer"
app:rippleColor="?attr/colorOnPrimary" app:rippleColor="?attr/colorOnSecondaryContainer"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" /> app:layout_constraintBottom_toBottomOf="parent" />
@@ -175,6 +174,8 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="horizontal" android:orientation="horizontal"
android:gravity="center_vertical" android:gravity="center_vertical"
android:clipChildren="false"
android:clipToPadding="false"
android:paddingStart="16dp" android:paddingStart="16dp"
android:paddingEnd="12dp"> android:paddingEnd="12dp">
@@ -0,0 +1,247 @@
<?xml version="1.0" encoding="utf-8"?>
<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="wrap_content"
android:orientation="vertical"
android:paddingHorizontal="24dp"
android:paddingTop="4dp"
android:paddingBottom="8dp">
<!-- Карта / СБП -->
<com.google.android.material.card.MaterialCardView
android:id="@+id/donate_card_row"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:clickable="true"
android:focusable="true"
app:cardBackgroundColor="?attr/colorSurfaceContainerHigh"
app:cardCornerRadius="14dp"
app:cardElevation="0dp"
app:strokeWidth="0dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="14dp"
android:paddingEnd="10dp"
android:paddingTop="10dp"
android:paddingBottom="10dp">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/donate_label_card"
android:textAppearance="@style/TextAppearance.Material3.TitleSmall"
android:textColor="?attr/colorOnSurface" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:ellipsize="end"
android:maxLines="1"
android:text="@string/donate_card_url"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
android:textColor="?attr/colorOnSurfaceVariant"
android:typeface="monospace" />
</LinearLayout>
<ImageButton
android:id="@+id/donate_card_open"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/donate_open_link"
android:padding="10dp"
android:scaleType="centerInside"
android:src="@drawable/ic_share_24dp"
app:tint="?attr/colorOnSurfaceVariant" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- TON -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
app:cardBackgroundColor="?attr/colorSurfaceContainerHigh"
app:cardCornerRadius="14dp"
app:cardElevation="0dp"
app:strokeWidth="0dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="14dp"
android:paddingEnd="10dp"
android:paddingTop="10dp"
android:paddingBottom="10dp">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/donate_label_ton"
android:textAppearance="@style/TextAppearance.Material3.TitleSmall"
android:textColor="?attr/colorOnSurface" />
<TextView
android:id="@+id/donate_ton_value"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:ellipsize="middle"
android:maxLines="1"
android:text="@string/donate_addr_ton"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
android:textColor="?attr/colorOnSurfaceVariant"
android:typeface="monospace" />
</LinearLayout>
<ImageButton
android:id="@+id/donate_ton_copy"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/donate_copy"
android:padding="10dp"
android:scaleType="centerInside"
android:src="@drawable/ic_copy"
app:tint="?attr/colorOnSurfaceVariant" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- TRC20 -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
app:cardBackgroundColor="?attr/colorSurfaceContainerHigh"
app:cardCornerRadius="14dp"
app:cardElevation="0dp"
app:strokeWidth="0dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="14dp"
android:paddingEnd="10dp"
android:paddingTop="10dp"
android:paddingBottom="10dp">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/donate_label_trc20"
android:textAppearance="@style/TextAppearance.Material3.TitleSmall"
android:textColor="?attr/colorOnSurface" />
<TextView
android:id="@+id/donate_trc_value"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:ellipsize="middle"
android:maxLines="1"
android:text="@string/donate_addr_trc20"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
android:textColor="?attr/colorOnSurfaceVariant"
android:typeface="monospace" />
</LinearLayout>
<ImageButton
android:id="@+id/donate_trc_copy"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/donate_copy"
android:padding="10dp"
android:scaleType="centerInside"
android:src="@drawable/ic_copy"
app:tint="?attr/colorOnSurfaceVariant" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<!-- BTC -->
<com.google.android.material.card.MaterialCardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardBackgroundColor="?attr/colorSurfaceContainerHigh"
app:cardCornerRadius="14dp"
app:cardElevation="0dp"
app:strokeWidth="0dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="14dp"
android:paddingEnd="10dp"
android:paddingTop="10dp"
android:paddingBottom="10dp">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/donate_label_btc"
android:textAppearance="@style/TextAppearance.Material3.TitleSmall"
android:textColor="?attr/colorOnSurface" />
<TextView
android:id="@+id/donate_btc_value"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:ellipsize="middle"
android:maxLines="1"
android:text="@string/donate_addr_btc"
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
android:textColor="?attr/colorOnSurfaceVariant"
android:typeface="monospace" />
</LinearLayout>
<ImageButton
android:id="@+id/donate_btc_copy"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/donate_copy"
android:padding="10dp"
android:scaleType="centerInside"
android:src="@drawable/ic_copy"
app:tint="?attr/colorOnSurfaceVariant" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>
@@ -1,30 +1,29 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingStart="24dp" android:paddingStart="24dp"
android:paddingEnd="8dp" android:paddingEnd="8dp"
android:paddingTop="20dp" android:paddingTop="16dp"
android:paddingBottom="12dp"> android:paddingBottom="8dp">
<TextView <TextView
android:id="@+id/dialog_title_text" android:id="@+id/dialog_title_text"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_centerVertical="true" android:layout_gravity="center_vertical"
android:layout_toStartOf="@+id/dialog_close_btn" android:gravity="start"
android:textAppearance="@style/TextAppearance.Material3.HeadlineSmall" android:paddingEnd="32dp"
android:textAppearance="@style/TextAppearance.Material3.TitleMedium"
android:textColor="?attr/colorOnSurface" /> android:textColor="?attr/colorOnSurface" />
<ImageButton <ImageButton
android:id="@+id/dialog_close_btn" android:id="@+id/dialog_close_btn"
android:layout_width="40dp" android:layout_width="36dp"
android:layout_height="40dp" android:layout_height="36dp"
android:layout_alignParentEnd="true" android:layout_gravity="end|center_vertical"
android:layout_centerVertical="true"
android:background="?attr/selectableItemBackgroundBorderless" android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="Закрыть" android:contentDescription="Закрыть"
android:src="@drawable/ic_close_24dp" android:src="@drawable/ic_close_24dp"
android:tint="?attr/colorOnSurfaceVariant" /> android:tint="?attr/colorOnSurfaceVariant" />
</FrameLayout>
</RelativeLayout>
@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:gravity="center_vertical"
android:minHeight="56dp"
android:paddingHorizontal="24dp"
android:paddingVertical="8dp">
<FrameLayout
android:layout_width="36dp"
android:layout_height="36dp"
android:background="@drawable/bg_country_flag_circle">
<TextView
android:id="@+id/flag"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:includeFontPadding="false"
android:textSize="18sp" />
</FrameLayout>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="16dp"
android:layout_marginEnd="12dp"
android:orientation="vertical">
<TextView
android:id="@+id/text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="?attr/textAppearanceTitleMedium"
android:textColor="?attr/colorOnSurface" />
<TextView
android:id="@+id/code"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:ellipsize="end"
android:maxLines="1"
android:textAllCaps="true"
android:textAppearance="?attr/textAppearanceLabelSmall"
android:textColor="?attr/colorOnSurfaceVariant"
android:letterSpacing="0.08" />
</LinearLayout>
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/check_box"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="false"
android:focusable="false" />
</LinearLayout>
@@ -34,6 +34,7 @@
android:id="@+id/check_box" android:id="@+id/check_box"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:button="@drawable/checkbox_round_selector"
android:clickable="false" android:clickable="false"
android:focusable="false" android:focusable="false"
android:padding="@dimen/padding_spacing_dp8" /> android:padding="@dimen/padding_spacing_dp8" />
@@ -20,9 +20,16 @@
app:strokeWidth="0dp"> app:strokeWidth="0dp">
<LinearLayout <LinearLayout
android:id="@+id/info_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<LinearLayout
android:id="@+id/info_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="center" android:layout_gravity="center"
android:background="?attr/selectableItemBackground" android:background="?attr/selectableItemBackground"
android:clickable="true" android:clickable="true"
@@ -117,11 +124,18 @@
</LinearLayout> </LinearLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>
<LinearLayout <LinearLayout
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="match_parent"
android:gravity="center" android:gravity="center"
android:orientation="vertical"> android:orientation="vertical"
android:paddingEnd="4dp">
<ImageView <ImageView
android:id="@+id/iv_favorite" android:id="@+id/iv_favorite"
@@ -149,10 +163,6 @@
</LinearLayout> </LinearLayout>
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView> </com.google.android.material.card.MaterialCardView>
</LinearLayout> </LinearLayout>
@@ -4,6 +4,7 @@
<color name="colorPingGood">#86D993</color> <color name="colorPingGood">#86D993</color>
<color name="colorPingMedium">#F1C76A</color> <color name="colorPingMedium">#F1C76A</color>
<color name="status_connected">#66BB6A</color> <color name="status_connected">#66BB6A</color>
<color name="status_failure">#EF5350</color>
<color name="colorPingRed">#FFB4AB</color> <color name="colorPingRed">#FFB4AB</color>
<color name="color_fab_active">@color/md_theme_primary</color> <color name="color_fab_active">@color/md_theme_primary</color>
<color name="color_fab_inactive">@color/md_theme_secondaryContainer</color> <color name="color_fab_inactive">@color/md_theme_secondaryContainer</color>
+367 -121
View File
@@ -2,12 +2,18 @@
<resources> <resources>
<string name="app_widget_name">v2rayNG</string> <string name="app_widget_name">v2rayNG</string>
<string name="app_tile_name">v2rayNG</string> <string name="app_tile_name">v2rayNG</string>
<string name="app_tile_first_use">Первое использование этой функции, пожалуйста, используйте приложение, чтобы добавить сервер</string> <string
name="app_tile_first_use"
>Первое использование этой функции, пожалуйста, используйте приложение, чтобы добавить сервер</string>
<string name="navigation_drawer_open">Открыть панель навигации</string> <string name="navigation_drawer_open">Открыть панель навигации</string>
<string name="navigation_drawer_close">Закрыть панель навигации</string> <string name="navigation_drawer_close">Закрыть панель навигации</string>
<string name="migration_success">Успешный перенос данных!</string> <string name="migration_success">Успешный перенос данных!</string>
<string name="drawer_forked_text">forked from <a href="https://github.com/2dust/v2rayng">V2RayNG</a></string> <string name="drawer_forked_text">forked from <a
<string name="drawer_developed_text">developed by developers from <a href="https://t.me/openlibrecommunity">Olc</a></string> href="https://github.com/2dust/v2rayng"
>V2RayNG</a></string>
<string name="drawer_developed_text">developed by developers from <a
href="https://t.me/openlibrecommunity"
>Olc</a></string>
<string name="action_stop_service">Остановить службу</string> <string name="action_stop_service">Остановить службу</string>
<string name="migration_fail">Перенос данных не выполнен!</string> <string name="migration_fail">Перенос данных не выполнен!</string>
<string name="pull_down_to_refresh">Потяните вниз для обновления!</string> <string name="pull_down_to_refresh">Потяните вниз для обновления!</string>
@@ -15,7 +21,9 @@
<!-- Notifications --> <!-- Notifications -->
<string name="notification_action_stop_v2ray">Остановить</string> <string name="notification_action_stop_v2ray">Остановить</string>
<string name="toast_permission_denied">Разрешение не получено</string> <string name="toast_permission_denied">Разрешение не получено</string>
<string name="toast_permission_denied_notification">Разрешение на отображение уведомлений не получено</string> <string
name="toast_permission_denied_notification"
>Разрешение на отображение уведомлений не получено</string>
<string name="notification_action_more">Ещё…</string> <string name="notification_action_more">Ещё…</string>
<string name="toast_services_start">Запуск служб</string> <string name="toast_services_start">Запуск служб</string>
<string name="toast_services_stop">Остановка служб</string> <string name="toast_services_stop">Остановка служб</string>
@@ -29,19 +37,41 @@
<string name="menu_item_edit_config">Изменить профиль</string> <string name="menu_item_edit_config">Изменить профиль</string>
<string name="menu_item_del_config">Удалить профиль</string> <string name="menu_item_del_config">Удалить профиль</string>
<string name="menu_item_import_config_qrcode">Импорт из QR-кода</string> <string name="menu_item_import_config_qrcode">Импорт из QR-кода</string>
<string name="menu_item_import_config_clipboard">Импорт из буфера обмена</string> <string
name="menu_item_import_config_clipboard"
>Импорт из буфера обмена</string>
<string name="menu_item_import_config_local">Импорт из файла</string> <string name="menu_item_import_config_local">Импорт из файла</string>
<string name="menu_item_import_config_policy_group">Добавить группу политик</string> <string
<string name="menu_item_import_config_manually_vmess">Ручной ввод VMess</string> name="menu_item_import_config_policy_group"
<string name="menu_item_import_config_manually_vless">Ручной ввод VLESS</string> >Добавить группу политик</string>
<string name="menu_item_import_config_manually_ss">Ручной ввод Shadowsocks</string> <string
<string name="menu_item_import_config_manually_socks">Ручной ввод SOCKS</string> name="menu_item_import_config_manually_vmess"
<string name="menu_item_import_config_manually_http">Ручной ввод HTTP</string> >Ручной ввод VMess</string>
<string name="menu_item_import_config_manually_trojan">Ручной ввод Trojan</string> <string
<string name="menu_item_import_config_manually_wireguard">Ручной ввод WireGuard</string> name="menu_item_import_config_manually_vless"
<string name="menu_item_import_config_manually_hysteria2">Ручной ввод Hysteria2</string> >Ручной ввод VLESS</string>
<string
name="menu_item_import_config_manually_ss"
>Ручной ввод Shadowsocks</string>
<string
name="menu_item_import_config_manually_socks"
>Ручной ввод SOCKS</string>
<string
name="menu_item_import_config_manually_http"
>Ручной ввод HTTP</string>
<string
name="menu_item_import_config_manually_trojan"
>Ручной ввод Trojan</string>
<string
name="menu_item_import_config_manually_wireguard"
>Ручной ввод WireGuard</string>
<string
name="menu_item_import_config_manually_hysteria2"
>Ручной ввод Hysteria2</string>
<string name="del_config_comfirm">Подтверждаете удаление?</string> <string name="del_config_comfirm">Подтверждаете удаление?</string>
<string name="del_invalid_config_comfirm">Выполните проверку перед удалением! Подтверждаете удаление?</string> <string
name="del_invalid_config_comfirm"
>Выполните проверку перед удалением! Подтверждаете удаление?</string>
<string name="server_lab_remarks">Название</string> <string name="server_lab_remarks">Название</string>
<string name="server_lab_address">Адрес</string> <string name="server_lab_address">Адрес</string>
<string name="server_lab_port">Порт</string> <string name="server_lab_port">Порт</string>
@@ -82,45 +112,73 @@
<string name="server_lab_encryption">Шифрование</string> <string name="server_lab_encryption">Шифрование</string>
<string name="server_lab_flow">Поток</string> <string name="server_lab_flow">Поток</string>
<string name="server_lab_public_key">Открытый ключ</string> <string name="server_lab_public_key">Открытый ключ</string>
<string name="server_lab_preshared_key">Дополнительный ключ шифрования (необязательно)</string> <string
name="server_lab_preshared_key"
>Дополнительный ключ шифрования (необязательно)</string>
<string name="server_lab_short_id">ShortID</string> <string name="server_lab_short_id">ShortID</string>
<string name="server_lab_spider_x">SpiderX</string> <string name="server_lab_spider_x">SpiderX</string>
<string name="server_lab_mldsa65_verify">mldsa65Verify</string> <string name="server_lab_mldsa65_verify">mldsa65Verify</string>
<string name="server_lab_secret_key">Закрытый ключ</string> <string name="server_lab_secret_key">Закрытый ключ</string>
<string name="server_lab_reserved">Reserved (необязательно, через запятую)</string> <string
<string name="server_lab_local_address">Локальный адрес (необязательно, IPv4/IPv6 через запятую)</string> name="server_lab_reserved"
<string name="server_lab_local_mtu">MTU (необязательно, по умолчанию 1420)</string> >Reserved (необязательно, через запятую)</string>
<string
name="server_lab_local_address"
>Локальный адрес (необязательно, IPv4/IPv6 через запятую)</string>
<string
name="server_lab_local_mtu"
>MTU (необязательно, по умолчанию 1420)</string>
<string name="toast_success">Успешно</string> <string name="toast_success">Успешно</string>
<string name="toast_failure">Ошибка</string> <string name="toast_failure">Ошибка</string>
<string name="toast_none_data">Ничего нет</string> <string name="toast_none_data">Ничего нет</string>
<string name="toast_incorrect_protocol">Неправильный протокол</string> <string name="toast_incorrect_protocol">Неправильный протокол</string>
<string name="toast_decoding_failed">Невозможно декодировать</string> <string name="toast_decoding_failed">Невозможно декодировать</string>
<string name="title_file_chooser">Выберите профиль</string> <string name="title_file_chooser">Выберите профиль</string>
<string name="toast_require_file_manager">Установите файловый менеджер</string> <string
name="toast_require_file_manager"
>Установите файловый менеджер</string>
<string name="server_customize_config">Изменить профиль</string> <string name="server_customize_config">Изменить профиль</string>
<string name="toast_config_file_invalid">Неправильный профиль</string> <string name="toast_config_file_invalid">Неправильный профиль</string>
<string name="server_lab_content">Данные</string> <string name="server_lab_content">Данные</string>
<string name="toast_none_data_clipboard">В буфере обмена нет данных</string> <string name="toast_none_data_clipboard">В буфере обмена нет данных</string>
<string name="toast_invalid_url">Неправильный URL</string> <string name="toast_invalid_url">Неправильный URL</string>
<string name="toast_insecure_url_protocol">Не используйте небезопасный HTTP-протокол в адресе подписки</string> <string
<string name="server_lab_need_inbound">Убедитесь, что входящий порт соответствует настройкам</string> name="toast_insecure_url_protocol"
>Не используйте небезопасный HTTP-протокол в адресе подписки</string>
<string
name="server_lab_need_inbound"
>Убедитесь, что входящий порт соответствует настройкам</string>
<string name="toast_malformed_josn">Профиль повреждён</string> <string name="toast_malformed_josn">Профиль повреждён</string>
<string name="server_lab_request_host6">Узел (SNI) (необязательно)</string> <string name="server_lab_request_host6">Узел (SNI) (необязательно)</string>
<string name="toast_action_not_allowed">Это действие запрещено</string> <string name="toast_action_not_allowed">Это действие запрещено</string>
<string name="server_obfs_password">Пароль obfs</string> <string name="server_obfs_password">Пароль obfs</string>
<string name="server_lab_port_hop">Смена портов (переопределяет порт)</string> <string
name="server_lab_port_hop"
>Смена портов (переопределяет порт)</string>
<string name="server_lab_port_hop_interval">Интервал смены портов</string> <string name="server_lab_port_hop_interval">Интервал смены портов</string>
<string name="server_lab_bandwidth_down">Входящая пропускная способность (допускаются: k/m/g/t)</string> <string
<string name="server_lab_bandwidth_up">Исходящая пропускная способность (допускаются: k/m/g/t)</string> name="server_lab_bandwidth_down"
>Входящая пропускная способность (допускаются: k/m/g/t)</string>
<string
name="server_lab_bandwidth_up"
>Исходящая пропускная способность (допускаются: k/m/g/t)</string>
<string name="server_lab_xhttp_mode">Режим XHTTP</string> <string name="server_lab_xhttp_mode">Режим XHTTP</string>
<string name="server_lab_xhttp_extra">Необработанный JSON XHTTP Extra, формат: { XHTTPObject }</string> <string
<string name="server_lab_final_mask">finalMask raw JSON, format: { FinalMaskObject }</string> name="server_lab_xhttp_extra"
>Необработанный JSON XHTTP Extra, формат: { XHTTPObject }</string>
<string
name="server_lab_final_mask"
>finalMask raw JSON, format: { FinalMaskObject }</string>
<string name="server_lab_ech_config_list">EchConfigList</string> <string name="server_lab_ech_config_list">EchConfigList</string>
<string name="server_lab_ech_force_query">EchForceQuery</string> <string name="server_lab_ech_force_query">EchForceQuery</string>
<string name="server_lab_pinned_ca256">Отпечаток сертификата (SHA-256)</string> <string
name="server_lab_pinned_ca256"
>Отпечаток сертификата (SHA-256)</string>
<!-- UserAssetActivity --> <!-- UserAssetActivity -->
<string name="toast_asset_copy_failed">Невозможно скопировать файл, используйте файловый менеджер</string> <string
name="toast_asset_copy_failed"
>Невозможно скопировать файл, используйте файловый менеджер</string>
<string name="menu_item_add_asset">Добавить ресурс</string> <string name="menu_item_add_asset">Добавить ресурс</string>
<string name="menu_item_add_file">Добавить файлы</string> <string name="menu_item_add_file">Добавить файлы</string>
<string name="menu_item_add_url">Добавить URL</string> <string name="menu_item_add_url">Добавить URL</string>
@@ -130,7 +188,9 @@
<string name="title_user_asset_add_url">Добавить URL ресурса</string> <string name="title_user_asset_add_url">Добавить URL ресурса</string>
<string name="msg_file_not_found">Файл не найден</string> <string name="msg_file_not_found">Файл не найден</string>
<string name="msg_remark_is_duplicate">Название уже существует</string> <string name="msg_remark_is_duplicate">Название уже существует</string>
<string name="asset_geo_files_sources">Источник геофайлов (необязательно)</string> <string
name="asset_geo_files_sources"
>Источник геофайлов (необязательно)</string>
<!-- PerAppProxyActivity --> <!-- PerAppProxyActivity -->
<string name="msg_dialog_progress">Загрузка…</string> <string name="msg_dialog_progress">Загрузка…</string>
@@ -144,7 +204,9 @@
<string name="menu_item_export_proxy_app">Экспорт в буфер обмена</string> <string name="menu_item_export_proxy_app">Экспорт в буфер обмена</string>
<string name="menu_item_import_proxy_app">Импорт из буфера обмена</string> <string name="menu_item_import_proxy_app">Импорт из буфера обмена</string>
<string name="per_app_proxy_settings">Раздельное туннелирование</string> <string name="per_app_proxy_settings">Раздельное туннелирование</string>
<string name="per_app_proxy_settings_enable">Раздельное туннелирование</string> <string
name="per_app_proxy_settings_enable"
>Раздельное туннелирование</string>
<!-- Preferences --> <!-- Preferences -->
<string name="title_settings">Настройки</string> <string name="title_settings">Настройки</string>
@@ -152,24 +214,50 @@
<string name="title_core_settings">Настройки ядра</string> <string name="title_core_settings">Настройки ядра</string>
<string name="title_vpn_settings">Настройки VPN</string> <string name="title_vpn_settings">Настройки VPN</string>
<string name="title_pref_per_app_proxy">Раздельное туннелирование</string> <string name="title_pref_per_app_proxy">Раздельное туннелирование</string>
<string name="summary_pref_per_app_proxy">Основной: выбранное приложение соединяется через прокси, не выбранное — напрямую;\nРежим обхода: выбранное приложение соединяется напрямую, не выбранное — через прокси.\nЕсть возможность автоматического выбора проксируемых приложений в меню.</string> <string
name="summary_pref_per_app_proxy"
>Основной: выбранное приложение соединяется через прокси, не выбранное — напрямую;\nРежим обхода: выбранное приложение соединяется напрямую, не выбранное — через прокси.\nЕсть возможность автоматического выбора проксируемых приложений в меню.</string>
<string name="title_pref_is_booted">Автоподключение при запуске</string> <string name="title_pref_is_booted">Автоподключение при запуске</string>
<string name="summary_pref_is_booted">Автоматически подключаться к выбранному серверу при запуске приложения (может оказаться неудачным)</string> <string
name="summary_pref_is_booted"
>Автоматически подключаться к выбранному серверу при запуске приложения (может оказаться неудачным)</string>
<string name="title_pref_auto_sort_after_test">Автосортировка профилей</string> <string
<string name="summary_pref_auto_sort_after_test">Автоматическая сортировка профилей после проверки (результаты проверки могут быть неточными)</string> name="title_pref_auto_sort_after_test"
>Автосортировка профилей</string>
<string
name="summary_pref_auto_sort_after_test"
>Автоматическая сортировка профилей после проверки (результаты проверки могут быть неточными)</string>
<string name="title_pref_show_copy_button">Показывать кнопку копирования</string> <string
<string name="summary_pref_show_copy_button">Показывать кнопку для копирования конфигурации сервера в буфер обмена</string> name="title_pref_show_copy_button"
<string name="title_pref_show_server_ip">Показывать IP / хост сервера</string> >Показывать кнопку копирования</string>
<string name="summary_pref_show_server_ip">Отображать IP-адрес или хост сервера под названием</string> <string
name="summary_pref_show_copy_button"
>Показывать кнопку для копирования конфигурации сервера в буфер обмена</string>
<string
name="title_pref_show_server_ip"
>Показывать IP / хост сервера</string>
<string
name="summary_pref_show_server_ip"
>Отображать IP-адрес или хост сервера под названием</string>
<string name="title_mux_settings">Настройки мультиплексирования</string> <string name="title_mux_settings">Настройки мультиплексирования</string>
<string name="title_pref_mux_enabled">Использовать мультиплексирование</string> <string
<string name="summary_pref_mux_enabled">Быстрее, но это может привести к нестабильному соединению.\nНиже можно настроить обработку TCP, UDP и QUIC.</string> name="title_pref_mux_enabled"
<string name="title_pref_mux_concurency">TCP-соединения (диапазон от 1 до 1024)</string> >Использовать мультиплексирование</string>
<string name="title_pref_mux_xudp_concurency">XUDP-соединения (диапазон от 1 до 1024)</string> <string
<string name="title_pref_mux_xudp_quic">Обработка QUIC в мультиплексном туннеле</string> name="summary_pref_mux_enabled"
>Быстрее, но это может привести к нестабильному соединению.\nНиже можно настроить обработку TCP, UDP и QUIC.</string>
<string
name="title_pref_mux_concurency"
>TCP-соединения (диапазон от 1 до 1024)</string>
<string
name="title_pref_mux_xudp_concurency"
>XUDP-соединения (диапазон от 1 до 1024)</string>
<string
name="title_pref_mux_xudp_quic"
>Обработка QUIC в мультиплексном туннеле</string>
<string-array name="mux_xudp_quic_entries"> <string-array name="mux_xudp_quic_entries">
<item>Отклонять</item> <item>Отклонять</item>
<item>Разрешать</item> <item>Разрешать</item>
@@ -177,49 +265,87 @@
</string-array> </string-array>
<string name="title_pref_speed_enabled">Показывать скорость</string> <string name="title_pref_speed_enabled">Показывать скорость</string>
<string name="summary_pref_speed_enabled">Показывать текущую скорость в уведомлении.\nЗначок будет меняться в зависимости от использования.</string> <string
name="summary_pref_speed_enabled"
>Показывать текущую скорость в уведомлении.\nЗначок будет меняться в зависимости от использования.</string>
<string name="title_pref_sniffing_enabled">Анализировать пакеты</string> <string name="title_pref_sniffing_enabled">Анализировать пакеты</string>
<string name="summary_pref_sniffing_enabled">Пытаться определять доменные имена в пакетах (по умолчанию включено)</string> <string
<string name="title_pref_route_only_enabled">Домен только для маршрутизации</string> name="summary_pref_sniffing_enabled"
<string name="summary_pref_route_only_enabled">Использовать доменное имя только для маршрутизации и сохранять целевой адрес в виде IP.</string> >Пытаться определять доменные имена в пакетах (по умолчанию включено)</string>
<string
name="title_pref_route_only_enabled"
>Домен только для маршрутизации</string>
<string
name="summary_pref_route_only_enabled"
>Использовать доменное имя только для маршрутизации и сохранять целевой адрес в виде IP.</string>
<string name="title_pref_local_dns_enabled">Использовать локальную DNS</string> <string
<string name="summary_pref_local_dns_enabled">Обслуживание выполняется DNS-модулем ядра (в настройках маршрутизации рекомендуется выбрать режим «Все, кроме LAN и Китая»)</string> name="title_pref_local_dns_enabled"
>Использовать локальную DNS</string>
<string
name="summary_pref_local_dns_enabled"
>Обслуживание выполняется DNS-модулем ядра (в настройках маршрутизации рекомендуется выбрать режим «Все, кроме LAN и Китая»)</string>
<string name="title_pref_fake_dns_enabled">Использовать поддельную DNS</string> <string
<string name="summary_pref_fake_dns_enabled">Локальная DNS возвращает поддельные IP-адреса (быстрее, но может не работать с некоторыми приложениями)</string> name="title_pref_fake_dns_enabled"
>Использовать поддельную DNS</string>
<string
name="summary_pref_fake_dns_enabled"
>Локальная DNS возвращает поддельные IP-адреса (быстрее, но может не работать с некоторыми приложениями)</string>
<string name="title_pref_prefer_ipv6">Предпочитать IPv6</string> <string name="title_pref_prefer_ipv6">Предпочитать IPv6</string>
<string name="summary_pref_prefer_ipv6">Использовать маршрутизацию IPv6 предпочитать IPv6-адреса</string> <string
name="summary_pref_prefer_ipv6"
>Использовать маршрутизацию IPv6 предпочитать IPv6-адреса</string>
<string name="title_pref_remote_dns">Удалённая DNS (UDP/TCP/HTTPS/QUIC) (необязательно)</string> <string
name="title_pref_remote_dns"
>Удалённая DNS (UDP/TCP/HTTPS/QUIC) (необязательно)</string>
<string name="summary_pref_remote_dns">DNS</string> <string name="summary_pref_remote_dns">DNS</string>
<string name="title_pref_vpn_dns">VPN DNS (только IPv4/v6)</string> <string name="title_pref_vpn_dns">VPN DNS (только IPv4/v6)</string>
<string name="title_pref_vpn_bypass_lan">VPN обходит LAN</string> <string name="title_pref_vpn_bypass_lan">VPN обходит LAN</string>
<string name="title_pref_vpn_interface_address">Адрес интерфейса VPN</string> <string
name="title_pref_vpn_interface_address"
>Адрес интерфейса VPN</string>
<string name="title_pref_vpn_mtu">VPN MTU (по умолчанию 1500)</string> <string name="title_pref_vpn_mtu">VPN MTU (по умолчанию 1500)</string>
<string name="title_pref_domestic_dns">Внутренняя DNS (необязательно)</string> <string
name="title_pref_domestic_dns"
>Внутренняя DNS (необязательно)</string>
<string name="summary_pref_domestic_dns">DNS</string> <string name="summary_pref_domestic_dns">DNS</string>
<string name="title_pref_dns_hosts">Узлы DNS (формат: домен:адрес,…)</string> <string
name="title_pref_dns_hosts"
>Узлы DNS (формат: домен:адрес,…)</string>
<string name="summary_pref_dns_hosts">домен:адрес,…</string> <string name="summary_pref_dns_hosts">домен:адрес,…</string>
<string name="title_pref_delay_test_url">Сервис проверки задержки</string> <string name="title_pref_delay_test_url">Сервис проверки задержки</string>
<string name="summary_pref_delay_test_url">URL</string> <string name="summary_pref_delay_test_url">URL</string>
<string name="title_pref_ip_api_url">Сервис проверки текущего соединения</string> <string
name="title_pref_ip_api_url"
>Сервис проверки текущего соединения</string>
<string name="summary_pref_ip_api_url">URL</string> <string name="summary_pref_ip_api_url">URL</string>
<string name="title_pref_proxy_sharing_enabled">Разрешать соединения из LAN</string> <string
<string name="summary_pref_proxy_sharing_enabled">Другие устройства могут подключаться, используя ваш IP-адрес, чтобы использовать локальный прокси. Используйте только в доверенной сети, чтобы избежать несанкционированного подключения.</string> name="title_pref_proxy_sharing_enabled"
<string name="toast_warning_pref_proxysharing_short">Доступ из LAN разрешён, убедитесь, что вы находитесь в доверенной сети</string> >Разрешать соединения из LAN</string>
<string
name="summary_pref_proxy_sharing_enabled"
>Другие устройства могут подключаться, используя ваш IP-адрес, чтобы использовать локальный прокси. Используйте только в доверенной сети, чтобы избежать несанкционированного подключения.</string>
<string
name="toast_warning_pref_proxysharing_short"
>Доступ из LAN разрешён, убедитесь, что вы находитесь в доверенной сети</string>
<string name="title_pref_allow_insecure">Разрешать небезопасные соединения</string> <string
<string name="summary_pref_allow_insecure">Для TLS по умолчанию разрешены небезопасные соединения</string> name="title_pref_allow_insecure"
>Разрешать небезопасные соединения</string>
<string
name="summary_pref_allow_insecure"
>Для TLS по умолчанию разрешены небезопасные соединения</string>
<string name="title_pref_socks_port">Порт локального прокси</string> <string name="title_pref_socks_port">Порт локального прокси</string>
<string name="summary_pref_socks_port">Порт локального прокси</string> <string name="summary_pref_socks_port">Порт локального прокси</string>
@@ -227,26 +353,50 @@
<string name="title_pref_local_dns_port">Порт локальной DNS</string> <string name="title_pref_local_dns_port">Порт локальной DNS</string>
<string name="summary_pref_local_dns_port">Порт локальной DNS</string> <string name="summary_pref_local_dns_port">Порт локальной DNS</string>
<string name="title_pref_confirm_remove">Подтверждать удаление профиля</string> <string
<string name="summary_pref_confirm_remove">Обязательное подтверждение удаления профиля</string> name="title_pref_confirm_remove"
>Подтверждать удаление профиля</string>
<string
name="summary_pref_confirm_remove"
>Обязательное подтверждение удаления профиля</string>
<string name="title_pref_start_scan_immediate">Сканировать при запуске</string> <string
<string name="summary_pref_start_scan_immediate">Начинать сканирование сразу при запуске приложения или запускать функцию сканирования камерой или из изображения через панель инструментов</string> name="title_pref_start_scan_immediate"
>Сканировать при запуске</string>
<string
name="summary_pref_start_scan_immediate"
>Начинать сканирование сразу при запуске приложения или запускать функцию сканирования камерой или из изображения через панель инструментов</string>
<string name="title_pref_append_http_proxy">Дополнительный HTTP-прокси</string> <string
<string name="summary_pref_append_http_proxy">HTTP-прокси будет использоваться напрямую (из браузера и других поддерживающих приложений), минуя виртуальный сетевой адаптер (Android 10+)</string> name="title_pref_append_http_proxy"
>Дополнительный HTTP-прокси</string>
<string
name="summary_pref_append_http_proxy"
>HTTP-прокси будет использоваться напрямую (из браузера и других поддерживающих приложений), минуя виртуальный сетевой адаптер (Android 10+)</string>
<string name="title_pref_double_column_display">Профили в два столбца</string> <string
<string name="summary_pref_double_column_display">Список профилей отображается двумя столбцами, что позволяет показать больше информации на экране. Требуется перезапуск приложения.</string> name="title_pref_double_column_display"
>Профили в два столбца</string>
<string
name="summary_pref_double_column_display"
>Список профилей отображается двумя столбцами, что позволяет показать больше информации на экране. Требуется перезапуск приложения.</string>
<string name="title_pref_group_all_display">Общая вкладка групп</string> <string name="title_pref_group_all_display">Общая вкладка групп</string>
<string name="summary_pref_group_all_display">Показывать дополнительную вкладку со всеми профилями групп</string> <string
name="summary_pref_group_all_display"
>Показывать дополнительную вкладку со всеми профилями групп</string>
<!-- AboutActivity --> <!-- AboutActivity -->
<string name="title_pref_feedback">Обратная связь</string> <string name="title_pref_feedback">Обратная связь</string>
<string name="summary_pref_feedback">Предложить улучшение или сообщить об ошибке на GitHub</string> <string
<string name="summary_pref_tg_group">Присоединиться к группе в Telegram</string> name="summary_pref_feedback"
<string name="toast_tg_app_not_found">Приложение Telegram не найдено</string> >Предложить улучшение или сообщить об ошибке на GitHub</string>
<string
name="summary_pref_tg_group"
>Присоединиться к группе в Telegram</string>
<string
name="toast_tg_app_not_found"
>Приложение Telegram не найдено</string>
<string name="title_privacy_policy">Политика конфиденциальности</string> <string name="title_privacy_policy">Политика конфиденциальности</string>
<string name="title_about">О приложении</string> <string name="title_about">О приложении</string>
<string name="title_source_code">Исходный код</string> <string name="title_source_code">Исходный код</string>
@@ -255,33 +405,57 @@
<string name="title_pref_promotion">Содействие</string> <string name="title_pref_promotion">Содействие</string>
<string name="title_pref_auto_update_subscription">Автоматически обновлять подписки</string> <string
<string name="summary_pref_auto_update_subscription">Автоматическое обновление подписок в фоновом режиме с указанным интервалом. В зависимости от устройства эта функция может работать не всегда.</string> name="title_pref_auto_update_subscription"
<string name="title_pref_auto_update_interval">Интервал автообновления (минут, не менее 15)</string> >Автоматически обновлять подписки</string>
<string
name="summary_pref_auto_update_subscription"
>Автоматическое обновление подписок в фоновом режиме с указанным интервалом. В зависимости от устройства эта функция может работать не всегда.</string>
<string
name="title_pref_auto_update_interval"
>Интервал автообновления (минут, не менее 15)</string>
<string name="title_core_loglevel">Подробность ведения журнала</string> <string name="title_core_loglevel">Подробность ведения журнала</string>
<string name="title_outbound_domain_resolve_method">Предопределение исходящего домена</string> <string
name="title_outbound_domain_resolve_method"
>Предопределение исходящего домена</string>
<string name="title_mode">Режим</string> <string name="title_mode">Режим</string>
<string name="title_mode_help">Нажмите для получения дополнительной информации</string> <string
name="title_mode_help"
>Нажмите для получения дополнительной информации</string>
<string name="title_language">Язык</string> <string name="title_language">Язык</string>
<string name="title_ui_settings">Настройки интерфейса</string> <string name="title_ui_settings">Настройки интерфейса</string>
<string name="title_pref_ui_mode_night">Тема интерфейса</string> <string name="title_pref_ui_mode_night">Тема интерфейса</string>
<string name="title_pref_dynamic_colors">Динамические цвета (Material You)</string> <string
<string name="summary_pref_dynamic_colors">Использовать цвета обоев (Android 12+). Требует перезапуска.</string> name="title_pref_dynamic_colors"
>Динамические цвета (Material You)</string>
<string
name="summary_pref_dynamic_colors"
>Использовать цвета обоев (Android 12+). Требует перезапуска.</string>
<string name="restart_required">Требуется перезапуск приложения</string> <string name="restart_required">Требуется перезапуск приложения</string>
<string name="title_pref_subscriptions_bottom">Подписки снизу</string> <string name="title_pref_subscriptions_bottom">Подписки снизу</string>
<string name="summary_pref_subscriptions_bottom">Переместить вкладки подписок под список серверов</string> <string
name="summary_pref_subscriptions_bottom"
>Переместить вкладки подписок под список серверов</string>
<string name="title_pref_use_hev_tunnel">Использовать Hev TUN</string> <string name="title_pref_use_hev_tunnel">Использовать Hev TUN</string>
<string name="summary_pref_use_hev_tunnel">Если включено, TUN будет использовать hev-socks5-tunnel; иначе будет использован xray-core</string> <string
<string name="title_pref_hev_tunnel_loglevel">Подробность журнала HevTun</string> name="summary_pref_use_hev_tunnel"
<string name="title_pref_hev_tunnel_rw_timeout">Ожидание чтения/записи HevTun (секунд, по умолчанию TCP,UDP 300,60)</string> >Если включено, TUN будет использовать hev-socks5-tunnel; иначе будет использован xray-core</string>
<string
name="title_pref_hev_tunnel_loglevel"
>Подробность журнала HevTun</string>
<string
name="title_pref_hev_tunnel_rw_timeout"
>Ожидание чтения/записи HevTun (секунд, по умолчанию TCP,UDP 300,60)</string>
<string name="title_logcat">Журнал</string> <string name="title_logcat">Журнал</string>
<string name="logcat_copy">Копировать</string> <string name="logcat_copy">Копировать</string>
<string name="logcat_clear">Очистить</string> <string name="logcat_clear">Очистить</string>
<string name="title_service_restart">Перезапуск службы</string> <string name="title_service_restart">Перезапуск службы</string>
<string name="title_del_all_config">Удалить профили</string> <string name="title_del_all_config">Удалить профили</string>
<string name="title_del_duplicate_config">Удалить дубликаты профилей</string> <string
name="title_del_duplicate_config"
>Удалить дубликаты профилей</string>
<string name="title_del_invalid_config">Удалить нерабочие профили</string> <string name="title_del_invalid_config">Удалить нерабочие профили</string>
<string name="title_export_all">Экспорт профилей в буфер обмена</string> <string name="title_export_all">Экспорт профилей в буфер обмена</string>
<string name="title_sub_setting">Группы</string> <string name="title_sub_setting">Группы</string>
@@ -291,26 +465,44 @@
<string name="sub_setting_filter">Название фильтра</string> <string name="sub_setting_filter">Название фильтра</string>
<string name="sub_setting_enable">Использовать обновление</string> <string name="sub_setting_enable">Использовать обновление</string>
<string name="sub_auto_update">Использовать автообновление</string> <string name="sub_auto_update">Использовать автообновление</string>
<string name="sub_allow_insecure_url">Разрешать незащищённые HTTP-адреса</string> <string
name="sub_allow_insecure_url"
>Разрешать незащищённые HTTP-адреса</string>
<string name="sub_setting_pre_profile">Предыдущий профиль прокси</string> <string name="sub_setting_pre_profile">Предыдущий профиль прокси</string>
<string name="sub_setting_next_profile">Следующий профиль прокси</string> <string name="sub_setting_next_profile">Следующий профиль прокси</string>
<string name="sub_setting_pre_profile_tip">Профиль должен быть уникальным</string> <string
name="sub_setting_pre_profile_tip"
>Профиль должен быть уникальным</string>
<string name="title_sub_update">Обновить подписку</string> <string name="title_sub_update">Обновить подписку</string>
<string name="title_ping_all_server">Проверить профили</string> <string name="title_ping_all_server">Проверить профили</string>
<string name="title_real_ping_all_server">Проверить задержку профилей</string> <string
name="title_real_ping_all_server"
>Проверить задержку профилей</string>
<string name="title_user_asset_setting">Файлы ресурсов</string> <string name="title_user_asset_setting">Файлы ресурсов</string>
<string name="title_sort_by_test_results">Сортировать по результатам теста</string> <string
name="title_sort_by_test_results"
>Сортировать по результатам теста</string>
<string name="title_filter_config">Фильтр профилей</string> <string name="title_filter_config">Фильтр профилей</string>
<string name="filter_config_all">Все</string> <string name="filter_config_all">Все</string>
<string name="title_del_duplicate_config_count">Удалено дубликатов профилей: %d</string> <string
name="title_del_duplicate_config_count"
>Удалено дубликатов профилей: %d</string>
<string name="title_del_config_count">Удалено профилей: %d</string> <string name="title_del_config_count">Удалено профилей: %d</string>
<string name="title_import_config_count">Импортировано профилей: %d</string> <string name="title_import_config_count">Импортировано профилей: %d</string>
<string name="title_export_config_count">Экспортировано профилей: %d</string> <string
name="title_export_config_count"
>Экспортировано профилей: %d</string>
<string name="title_update_config_count">Обновлено профилей: %d</string> <string name="title_update_config_count">Обновлено профилей: %d</string>
<string name="title_updating">Обновление…</string> <string name="title_updating">Обновление…</string>
<string name="title_update_subscription_result">Обновлено профилей: %1$d (успешно: %2$d, ошибок: %3$d, пропущено: %4$d)</string> <string
<string name="title_update_subscription_no_subscription">Нет подписок</string> name="title_update_subscription_result"
<string name="toast_server_not_found_in_group">Выбранный профиль не найден в текущей группе</string> >Обновлено профилей: %1$d (успешно: %2$d, ошибок: %3$d, пропущено: %4$d)</string>
<string
name="title_update_subscription_no_subscription"
>Нет подписок</string>
<string
name="toast_server_not_found_in_group"
>Выбранный профиль не найден в текущей группе</string>
<string name="toast_fragment_not_available">Фрагмент недоступен</string> <string name="toast_fragment_not_available">Фрагмент недоступен</string>
<string name="title_locate_selected_config">Найти выбранный профиль</string> <string name="title_locate_selected_config">Найти выбранный профиль</string>
@@ -320,17 +512,33 @@
<!-- RoutingSettingActivity --> <!-- RoutingSettingActivity -->
<string name="routing_settings_domain_strategy">Доменная стратегия</string> <string name="routing_settings_domain_strategy">Доменная стратегия</string>
<string name="routing_settings_title">Маршрутизация</string> <string name="routing_settings_title">Маршрутизация</string>
<string name="routing_settings_tips">Введите требуемые домены/IP через запятую</string> <string
name="routing_settings_tips"
>Введите требуемые домены/IP через запятую</string>
<string name="routing_settings_save">Сохранить</string> <string name="routing_settings_save">Сохранить</string>
<string name="routing_settings_delete">Очистить</string> <string name="routing_settings_delete">Очистить</string>
<string name="routing_settings_rule_title">Настройка правил маршрутизации</string> <string
name="routing_settings_rule_title"
>Настройка правил маршрутизации</string>
<string name="routing_settings_add_rule">Добавить правило</string> <string name="routing_settings_add_rule">Добавить правило</string>
<string name="routing_settings_import_predefined_rulesets">Импорт набора правил</string> <string
<string name="routing_settings_import_rulesets_tip">Существующие правила будут удалены. Продолжить?</string> name="routing_settings_import_predefined_rulesets"
<string name="routing_settings_import_rulesets_from_clipboard">Импорт правил из буфера обмена</string> >Импорт набора правил</string>
<string name="routing_settings_import_rulesets_from_qrcode">Импорт правил из QR-кода</string> <string
<string name="routing_settings_export_rulesets_to_clipboard">Экспорт правил в буфер обмена</string> name="routing_settings_import_rulesets_tip"
<string name="routing_settings_locked">Постоянное (сохранится при импорте правил)</string> >Существующие правила будут удалены. Продолжить?</string>
<string
name="routing_settings_import_rulesets_from_clipboard"
>Импорт правил из буфера обмена</string>
<string
name="routing_settings_import_rulesets_from_qrcode"
>Импорт правил из QR-кода</string>
<string
name="routing_settings_export_rulesets_to_clipboard"
>Экспорт правил в буфер обмена</string>
<string
name="routing_settings_locked"
>Постоянное (сохранится при импорте правил)</string>
<string name="routing_settings_domain">Домен</string> <string name="routing_settings_domain">Домен</string>
<string name="routing_settings_ip">IP</string> <string name="routing_settings_ip">IP</string>
<string name="routing_settings_port">Порт</string> <string name="routing_settings_port">Порт</string>
@@ -343,45 +551,71 @@
<string name="connection_test_pending">Проверить соединение</string> <string name="connection_test_pending">Проверить соединение</string>
<string name="connection_test_testing">Проверка…</string> <string name="connection_test_testing">Проверка…</string>
<string name="connection_test_testing_count">Проверка профилей (%d)</string> <string name="connection_test_testing_count">Проверка профилей (%d)</string>
<string name="connection_test_available">Успешно: соединение заняло %d мс</string> <string name="connection_test_available">%d мс</string>
<string name="connection_test_error">Сбой проверки интернет-соединения: %s</string> <string
name="connection_test_error"
>Сбой проверки интернет-соединения: %s</string>
<string name="connection_test_fail">Интернет недоступен</string> <string name="connection_test_fail">Интернет недоступен</string>
<string name="connection_test_error_status_code">Код ошибки: #%d</string> <string name="connection_test_error_status_code">Код ошибки: #%d</string>
<string name="connection_connected">Соединено, нажмите для проверки</string> <string name="connection_connected">нажмите для проверки</string>
<string name="connection_not_connected">Ожидаем действий</string> <string name="connection_not_connected">Не подключено</string>
<string name="connection_updating_profiles">Обновление профилей…</string> <string name="connection_updating_profiles">Обновление профилей…</string>
<string name="connection_runing_task_left">Запущено проверок: %s</string> <string name="connection_runing_task_left">Проверено %s</string>
<string name="import_subscription_success">Подписка импортирована</string> <string name="import_subscription_success">Подписка импортирована</string>
<string name="import_subscription_failure">Невозможно импортировать подписку</string> <string
name="import_subscription_failure"
>Невозможно импортировать подписку</string>
<string name="title_fragment_settings">Настройки фрагментирования</string> <string name="title_fragment_settings">Настройки фрагментирования</string>
<string name="title_pref_fragment_packets">Фрагментирование пакетов</string> <string name="title_pref_fragment_packets">Фрагментирование пакетов</string>
<string name="title_pref_fragment_length">Длина фрагмента (от - до)</string> <string name="title_pref_fragment_length">Длина фрагмента (от - до)</string>
<string name="title_pref_fragment_interval">Интервал фрагментов (от - до)</string> <string
<string name="title_pref_fragment_enabled">Использовать фрагментирование</string> name="title_pref_fragment_interval"
>Интервал фрагментов (от - до)</string>
<string
name="title_pref_fragment_enabled"
>Использовать фрагментирование</string>
<string name="update_check_for_update">Проверить обновление</string> <string name="update_check_for_update">Проверить обновление</string>
<string name="update_already_latest_version">Установлена последняя версия</string> <string
name="update_already_latest_version"
>Установлена последняя версия</string>
<string name="update_new_version_found">Найдена новая версия: %s</string> <string name="update_new_version_found">Найдена новая версия: %s</string>
<string name="update_now">Обновить</string> <string name="update_now">Обновить</string>
<string name="update_check_pre_release">Искать предварительный выпуск</string> <string
name="update_check_pre_release"
>Искать предварительный выпуск</string>
<string name="update_checking_for_update">Проверка обновления…</string> <string name="update_checking_for_update">Проверка обновления…</string>
<string name="title_policy_group_type">Тип группы политик</string> <string name="title_policy_group_type">Тип группы политик</string>
<string name="title_policy_group_subscription_id">Из группы подписки</string> <string
<string name="title_policy_group_subscription_filter">Название фильтра</string> name="title_policy_group_subscription_id"
>Из группы подписки</string>
<string
name="title_policy_group_subscription_filter"
>Название фильтра</string>
<!-- BackupActivity --> <!-- BackupActivity -->
<string name="title_configuration_backup_restore">Резервное копирование</string> <string
<string name="title_configuration_backup">Резервирование конфигурации</string> name="title_configuration_backup_restore"
<string name="title_configuration_restore">Восстановление конфигурации</string> >Резервное копирование</string>
<string
name="title_configuration_backup"
>Резервирование конфигурации</string>
<string
name="title_configuration_restore"
>Восстановление конфигурации</string>
<string name="title_configuration_share">Поделиться конфигурацией</string> <string name="title_configuration_share">Поделиться конфигурацией</string>
<string name="title_webdav_config_setting">Настройки WebDAV</string> <string name="title_webdav_config_setting">Настройки WebDAV</string>
<string name="title_webdav_config_setting_unknown">Необходимо настроить WebDAV</string> <string
name="title_webdav_config_setting_unknown"
>Необходимо настроить WebDAV</string>
<string name="title_webdav_url">URL сервера</string> <string name="title_webdav_url">URL сервера</string>
<string name="title_webdav_user">Пользователь</string> <string name="title_webdav_user">Пользователь</string>
<string name="title_webdav_pass">Пароль</string> <string name="title_webdav_pass">Пароль</string>
<string name="title_webdav_remote_path">Удалённый путь (необязательно)</string> <string
name="title_webdav_remote_path"
>Удалённый путь (необязательно)</string>
<string-array name="share_method"> <string-array name="share_method">
<item>QR-код</item> <item>QR-код</item>
@@ -444,4 +678,16 @@
<item>WebDAV</item> <item>WebDAV</item>
</string-array> </string-array>
<!-- Donate dialog -->
<string name="donate_dialog_title">Поддержите нас</string>
<string name="donate_label_card">Карта</string>
<string name="donate_label_ton">TON</string>
<string name="donate_label_trc20">TRC20</string>
<string name="donate_label_btc">BTC</string>
<string name="donate_copy">Копировать</string>
<string name="donate_open_link">Открыть</string>
<string name="donate_btn_dont_show">Не показывать</string>
<string name="donate_btn_postpone">Позже</string>
<string name="donate_toast_copied">Скопировано</string>
</resources> </resources>
@@ -4,6 +4,7 @@
<color name="colorPingGood">#2E7D32</color> <color name="colorPingGood">#2E7D32</color>
<color name="colorPingMedium">#9A6700</color> <color name="colorPingMedium">#9A6700</color>
<color name="status_connected">#4CAF50</color> <color name="status_connected">#4CAF50</color>
<color name="status_failure">#E53935</color>
<color name="colorPingRed">#BA1A1A</color> <color name="colorPingRed">#BA1A1A</color>
<color name="colorConfigType">#1565C0</color> <color name="colorConfigType">#1565C0</color>
<color name="colorWhite">#FFFFFF</color> <color name="colorWhite">#FFFFFF</color>
+345 -102
View File
@@ -3,20 +3,33 @@
<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_widget_name">Switch</string>
<string name="app_tile_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> <string
name="app_tile_first_use"
>First use of this feature, please use the app to add server</string>
<string name="navigation_drawer_open">Open navigation drawer</string> <string name="navigation_drawer_open">Open navigation drawer</string>
<string name="navigation_drawer_close">Close navigation drawer</string> <string name="navigation_drawer_close">Close navigation drawer</string>
<string name="migration_success">Data migration success!</string> <string name="migration_success">Data migration success!</string>
<string name="drawer_forked_text" translatable="false">forked from <a href="https://github.com/2dust/v2rayng">V2RayNG</a></string> <string name="drawer_forked_text" translatable="false">forked from <a
<string name="drawer_developed_text" translatable="false">developed by developers from <a href="https://t.me/openlibrecommunity">Olc</a></string> href="https://github.com/2dust/v2rayng"
>V2RayNG</a></string>
<string
name="drawer_developed_text"
translatable="false"
>developed by developers from <a
href="https://t.me/openlibrecommunity"
>Olc</a></string>
<string name="action_stop_service">Stop service</string> <string name="action_stop_service">Stop service</string>
<string name="migration_fail">Data migration failed!</string> <string name="migration_fail">Data migration failed!</string>
<string name="pull_down_to_refresh">Please pull down to refresh!</string> <string name="pull_down_to_refresh">Please pull down to refresh!</string>
<!-- Notifications --> <!-- Notifications -->
<string name="notification_action_stop_v2ray">Stop</string> <string name="notification_action_stop_v2ray">Stop</string>
<string name="toast_permission_denied">Unable to obtain the permission</string> <string
<string name="toast_permission_denied_notification">Unable to obtain the notification permission</string> name="toast_permission_denied"
>Unable to obtain the permission</string>
<string
name="toast_permission_denied_notification"
>Unable to obtain the notification permission</string>
<string name="notification_action_more">Click for more</string> <string name="notification_action_more">Click for more</string>
<string name="toast_services_start">Start Services</string> <string name="toast_services_start">Start Services</string>
<string name="toast_services_stop">Stop Services</string> <string name="toast_services_stop">Stop Services</string>
@@ -30,19 +43,31 @@
<string name="menu_item_edit_config">Edit config</string> <string name="menu_item_edit_config">Edit config</string>
<string name="menu_item_del_config">Delete config</string> <string name="menu_item_del_config">Delete config</string>
<string name="menu_item_import_config_qrcode">Import from QRcode</string> <string name="menu_item_import_config_qrcode">Import from QRcode</string>
<string name="menu_item_import_config_clipboard">Import from Clipboard</string> <string
name="menu_item_import_config_clipboard"
>Import from Clipboard</string>
<string name="menu_item_import_config_local">Import from locally</string> <string name="menu_item_import_config_local">Import from locally</string>
<string name="menu_item_import_config_policy_group">Add [Policy group]</string> <string
name="menu_item_import_config_policy_group"
>Add [Policy group]</string>
<string name="menu_item_import_config_manually_vmess">Add [VMess]</string> <string name="menu_item_import_config_manually_vmess">Add [VMess]</string>
<string name="menu_item_import_config_manually_vless">Add [VLESS]</string> <string name="menu_item_import_config_manually_vless">Add [VLESS]</string>
<string name="menu_item_import_config_manually_ss">Add [Shadowsocks]</string> <string
name="menu_item_import_config_manually_ss"
>Add [Shadowsocks]</string>
<string name="menu_item_import_config_manually_socks">Add [SOCKS]</string> <string name="menu_item_import_config_manually_socks">Add [SOCKS]</string>
<string name="menu_item_import_config_manually_http">Add [HTTP]</string> <string name="menu_item_import_config_manually_http">Add [HTTP]</string>
<string name="menu_item_import_config_manually_trojan">Add [Trojan]</string> <string name="menu_item_import_config_manually_trojan">Add [Trojan]</string>
<string name="menu_item_import_config_manually_wireguard">Add [Wireguard]</string> <string
<string name="menu_item_import_config_manually_hysteria2">Add [Hysteria2]</string> name="menu_item_import_config_manually_wireguard"
>Add [Wireguard]</string>
<string
name="menu_item_import_config_manually_hysteria2"
>Add [Hysteria2]</string>
<string name="del_config_comfirm">Confirm delete ?</string> <string name="del_config_comfirm">Confirm delete ?</string>
<string name="del_invalid_config_comfirm">Please test before deleting! Confirm delete ?</string> <string
name="del_invalid_config_comfirm"
>Please test before deleting! Confirm delete ?</string>
<string name="server_lab_remarks">remarks</string> <string name="server_lab_remarks">remarks</string>
<string name="server_lab_address">address</string> <string name="server_lab_address">address</string>
<string name="server_lab_port">port</string> <string name="server_lab_port">port</string>
@@ -70,7 +95,10 @@
<string name="server_lab_path_kcp">kcp seed</string> <string name="server_lab_path_kcp">kcp seed</string>
<string name="server_lab_path_grpc">gRPC serviceName</string> <string name="server_lab_path_grpc">gRPC serviceName</string>
<string name="server_lab_stream_security">TLS</string> <string name="server_lab_stream_security">TLS</string>
<string name="server_lab_stream_fingerprint" translatable="false">Fingerprint</string> <string
name="server_lab_stream_fingerprint"
translatable="false"
>Fingerprint</string>
<string name="server_lab_stream_alpn">Alpn</string> <string name="server_lab_stream_alpn">Alpn</string>
<string name="server_lab_allow_insecure">allowInsecure</string> <string name="server_lab_allow_insecure">allowInsecure</string>
<string name="server_lab_sni">SNI</string> <string name="server_lab_sni">SNI</string>
@@ -88,8 +116,12 @@
<string name="server_lab_spider_x">SpiderX</string> <string name="server_lab_spider_x">SpiderX</string>
<string name="server_lab_mldsa65_verify">Mldsa65Verify</string> <string name="server_lab_mldsa65_verify">Mldsa65Verify</string>
<string name="server_lab_secret_key">SecretKey</string> <string name="server_lab_secret_key">SecretKey</string>
<string name="server_lab_reserved">Reserved(Optional, separated by commas)</string> <string
<string name="server_lab_local_address">Local address (optional IPv4/IPv6, separated by commas)</string> name="server_lab_reserved"
>Reserved(Optional, separated by commas)</string>
<string
name="server_lab_local_address"
>Local address (optional IPv4/IPv6, separated by commas)</string>
<string name="server_lab_local_mtu">Mtu(optional, default 1420)</string> <string name="server_lab_local_mtu">Mtu(optional, default 1420)</string>
<string name="toast_success">Success</string> <string name="toast_success">Success</string>
<string name="toast_failure">Failure</string> <string name="toast_failure">Failure</string>
@@ -97,31 +129,53 @@
<string name="toast_incorrect_protocol">Incorrect protocol</string> <string name="toast_incorrect_protocol">Incorrect protocol</string>
<string name="toast_decoding_failed">Decoding failed</string> <string name="toast_decoding_failed">Decoding failed</string>
<string name="title_file_chooser">Select a config</string> <string name="title_file_chooser">Select a config</string>
<string name="toast_require_file_manager">Please install a File Manager.</string> <string
name="toast_require_file_manager"
>Please install a File Manager.</string>
<string name="server_customize_config">Customize config</string> <string name="server_customize_config">Customize config</string>
<string name="toast_config_file_invalid">Invalid config</string> <string name="toast_config_file_invalid">Invalid config</string>
<string name="server_lab_content">Content</string> <string name="server_lab_content">Content</string>
<string name="toast_none_data_clipboard">There is no data in the clipboard</string> <string
name="toast_none_data_clipboard"
>There is no data in the clipboard</string>
<string name="toast_invalid_url">Invalid URL</string> <string name="toast_invalid_url">Invalid URL</string>
<string name="toast_insecure_url_protocol">Please do not use the insecure HTTP protocol subscription address</string> <string
<string name="server_lab_need_inbound">Ensure inbounds port is consistent with the settings</string> name="toast_insecure_url_protocol"
>Please do not use the insecure HTTP protocol subscription address</string>
<string
name="server_lab_need_inbound"
>Ensure inbounds port is consistent with the settings</string>
<string name="toast_malformed_josn">Config malformed</string> <string name="toast_malformed_josn">Config malformed</string>
<string name="server_lab_request_host6">Host(SNI)(Optional)</string> <string name="server_lab_request_host6">Host(SNI)(Optional)</string>
<string name="toast_action_not_allowed">Action not allowed</string> <string name="toast_action_not_allowed">Action not allowed</string>
<string name="server_obfs_password">Obfs password</string> <string name="server_obfs_password">Obfs password</string>
<string name="server_lab_port_hop">Port Hopping(will override the port)</string> <string
name="server_lab_port_hop"
>Port Hopping(will override the port)</string>
<string name="server_lab_port_hop_interval">Port Hopping Interval</string> <string name="server_lab_port_hop_interval">Port Hopping Interval</string>
<string name="server_lab_bandwidth_down">Bandwidth down (Supported units: k/m/g/t)</string> <string
<string name="server_lab_bandwidth_up">Bandwidth up (Supported units: k/m/g/t)</string> name="server_lab_bandwidth_down"
>Bandwidth down (Supported units: k/m/g/t)</string>
<string
name="server_lab_bandwidth_up"
>Bandwidth up (Supported units: k/m/g/t)</string>
<string name="server_lab_xhttp_mode">XHTTP Mode</string> <string name="server_lab_xhttp_mode">XHTTP Mode</string>
<string name="server_lab_xhttp_extra">XHTTP Extra raw JSON, format: { XHTTPObject }</string> <string
<string name="server_lab_final_mask">finalMask raw JSON, format: { FinalMaskObject }</string> name="server_lab_xhttp_extra"
>XHTTP Extra raw JSON, format: { XHTTPObject }</string>
<string
name="server_lab_final_mask"
>finalMask raw JSON, format: { FinalMaskObject }</string>
<string name="server_lab_ech_config_list">EchConfigList</string> <string name="server_lab_ech_config_list">EchConfigList</string>
<string name="server_lab_ech_force_query">EchForceQuery</string> <string name="server_lab_ech_force_query">EchForceQuery</string>
<string name="server_lab_pinned_ca256">Certificate fingerprint (SHA-256)</string> <string
name="server_lab_pinned_ca256"
>Certificate fingerprint (SHA-256)</string>
<!-- UserAssetActivity --> <!-- UserAssetActivity -->
<string name="toast_asset_copy_failed">File copy failed, please use File Manager</string> <string
name="toast_asset_copy_failed"
>File copy failed, please use File Manager</string>
<string name="menu_item_add_asset">Add asset</string> <string name="menu_item_add_asset">Add asset</string>
<string name="menu_item_add_file">Add files</string> <string name="menu_item_add_file">Add files</string>
<string name="menu_item_add_url">Add URL</string> <string name="menu_item_add_url">Add URL</string>
@@ -146,7 +200,9 @@
<string name="menu_item_import_proxy_app">Import from Clipboard</string> <string name="menu_item_import_proxy_app">Import from Clipboard</string>
<string name="per_app_proxy_settings">Раздельное туннелирование</string> <string name="per_app_proxy_settings">Раздельное туннелирование</string>
<string name="per_app_proxy_settings_enable">Включить</string> <string name="per_app_proxy_settings_enable">Включить</string>
<string name="split_tunneling_description">Выберите приложения, которые будут использовать VPN — остальные приложения, например банки, будут идти напрямую через ваш домашний интернет.\n\nВ режиме обхода всё зеркально — выбранные приложения не используют VPN.</string> <string
name="split_tunneling_description"
>Выберите приложения, которые будут использовать VPN. Остальные (например банки) пойдут напрямую через ваш домашний интернет.\n\nВ режиме обхода всё зеркально: выбранные приложения не используют VPN.</string>
<!-- Preferences --> <!-- Preferences -->
<string name="title_settings">Settings</string> <string name="title_settings">Settings</string>
@@ -154,24 +210,44 @@
<string name="title_core_settings">Core Settings</string> <string name="title_core_settings">Core Settings</string>
<string name="title_vpn_settings">VPN Settings</string> <string name="title_vpn_settings">VPN Settings</string>
<string name="title_pref_per_app_proxy">Per-app proxy</string> <string name="title_pref_per_app_proxy">Per-app proxy</string>
<string name="summary_pref_per_app_proxy">General: Checked apps use proxy, unchecked apps connect directly; \nBypass mode: checked apps connect directly, unchecked apps use proxy. \nThe option to automatically select proxy applications is in the menu</string> <string
name="summary_pref_per_app_proxy"
>General: Checked apps use proxy, unchecked apps connect directly; \nBypass mode: checked apps connect directly, unchecked apps use proxy. \nThe option to automatically select proxy applications is in the menu</string>
<string name="title_pref_is_booted">Auto connect at startup</string> <string name="title_pref_is_booted">Auto connect at startup</string>
<string name="summary_pref_is_booted">Automatically connects to the selected server at startup, which may be unsuccessful</string> <string
name="summary_pref_is_booted"
>Automatically connects to the selected server at startup, which may be unsuccessful</string>
<string name="title_pref_auto_sort_after_test">Auto sort after testing</string> <string
<string name="summary_pref_auto_sort_after_test">Test results may not be accurate;</string> name="title_pref_auto_sort_after_test"
>Auto sort after testing</string>
<string
name="summary_pref_auto_sort_after_test"
>Test results may not be accurate;</string>
<string name="title_pref_show_copy_button">Show copy button</string> <string name="title_pref_show_copy_button">Show copy button</string>
<string name="summary_pref_show_copy_button">Show button to copy server configuration to clipboard</string> <string
name="summary_pref_show_copy_button"
>Show button to copy server configuration to clipboard</string>
<string name="title_pref_show_server_ip">Show server IP / host</string> <string name="title_pref_show_server_ip">Show server IP / host</string>
<string name="summary_pref_show_server_ip">Display the server IP address or host under the server name</string> <string
name="summary_pref_show_server_ip"
>Display the server IP address or host under the server name</string>
<string name="title_mux_settings">Mux Settings</string> <string name="title_mux_settings">Mux Settings</string>
<string name="title_pref_mux_enabled">Enable Mux</string> <string name="title_pref_mux_enabled">Enable Mux</string>
<string name="summary_pref_mux_enabled">Faster, but it may cause unstable connectivity\nCustomize how to handle TCP, UDP and QUIC below</string> <string
<string name="title_pref_mux_concurency">TCP connectionsrange -1 to 1024</string> name="summary_pref_mux_enabled"
<string name="title_pref_mux_xudp_concurency">XUDP connectionsrange -1 to 1024</string> >Faster, but it may cause unstable connectivity\nCustomize how to handle TCP, UDP and QUIC below</string>
<string name="title_pref_mux_xudp_quic">Handling of QUIC in mux tunnel</string> <string
name="title_pref_mux_concurency"
>TCP connectionsrange -1 to 1024</string>
<string
name="title_pref_mux_xudp_concurency"
>XUDP connectionsrange -1 to 1024</string>
<string
name="title_pref_mux_xudp_quic"
>Handling of QUIC in mux tunnel</string>
<string-array name="mux_xudp_quic_entries"> <string-array name="mux_xudp_quic_entries">
<item>reject</item> <item>reject</item>
<item>allow</item> <item>allow</item>
@@ -179,30 +255,46 @@
</string-array> </string-array>
<string name="title_pref_speed_enabled">Enable speed display</string> <string name="title_pref_speed_enabled">Enable speed display</string>
<string name="summary_pref_speed_enabled">Display current speed in the notification.\nNotification icon would change based on <string
name="summary_pref_speed_enabled"
>Display current speed in the notification.\nNotification icon would change based on
usage.</string> usage.</string>
<string name="title_pref_sniffing_enabled">Enable Sniffing</string> <string name="title_pref_sniffing_enabled">Enable Sniffing</string>
<string name="summary_pref_sniffing_enabled">Try sniff domain from the packet (default on)</string> <string
name="summary_pref_sniffing_enabled"
>Try sniff domain from the packet (default on)</string>
<string name="title_pref_route_only_enabled">Enable routeOnly</string> <string name="title_pref_route_only_enabled">Enable routeOnly</string>
<string name="summary_pref_route_only_enabled">Use the sniffed domain name for routing only, and keep the target address as the IP address.</string> <string
name="summary_pref_route_only_enabled"
>Use the sniffed domain name for routing only, and keep the target address as the IP address.</string>
<string name="title_pref_local_dns_enabled">Enable local DNS</string> <string name="title_pref_local_dns_enabled">Enable local DNS</string>
<string name="summary_pref_local_dns_enabled">DNS processed by cores DNS module (Recommended if you need routing bypassing LAN and mainland addresses)</string> <string
name="summary_pref_local_dns_enabled"
>DNS processed by cores DNS module (Recommended if you need routing bypassing LAN and mainland addresses)</string>
<string name="title_pref_fake_dns_enabled">Enable fake DNS</string> <string name="title_pref_fake_dns_enabled">Enable fake DNS</string>
<string name="summary_pref_fake_dns_enabled">Local DNS returns fake IP addresses (faster, but it may not work for some apps)</string> <string
name="summary_pref_fake_dns_enabled"
>Local DNS returns fake IP addresses (faster, but it may not work for some apps)</string>
<string name="title_pref_prefer_ipv6">Prefer IPv6</string> <string name="title_pref_prefer_ipv6">Prefer IPv6</string>
<string name="summary_pref_prefer_ipv6">Enable IPv6 routes and Prefer IPv6 addresses</string> <string
name="summary_pref_prefer_ipv6"
>Enable IPv6 routes and Prefer IPv6 addresses</string>
<string name="title_pref_remote_dns">Remote DNS (udp/tcp/https/quic)(Optional)</string> <string
name="title_pref_remote_dns"
>Remote DNS (udp/tcp/https/quic)(Optional)</string>
<string name="summary_pref_remote_dns">DNS</string> <string name="summary_pref_remote_dns">DNS</string>
<string name="title_pref_vpn_dns">VPN DNS (only IPv4/v6)</string> <string name="title_pref_vpn_dns">VPN DNS (only IPv4/v6)</string>
<string name="title_pref_vpn_bypass_lan">Does VPN bypass LAN</string> <string name="title_pref_vpn_bypass_lan">Does VPN bypass LAN</string>
<string name="title_pref_vpn_interface_address">VPN Interface Address</string> <string
name="title_pref_vpn_interface_address"
>VPN Interface Address</string>
<string name="title_pref_vpn_mtu">VPN MTU (default 1500)</string> <string name="title_pref_vpn_mtu">VPN MTU (default 1500)</string>
@@ -210,21 +302,33 @@
<string name="title_pref_domestic_dns">Domestic DNS (Optional)</string> <string name="title_pref_domestic_dns">Domestic DNS (Optional)</string>
<string name="summary_pref_domestic_dns">DNS</string> <string name="summary_pref_domestic_dns">DNS</string>
<string name="title_pref_dns_hosts">DNS hosts (Format: domain:address,…)</string> <string
name="title_pref_dns_hosts"
>DNS hosts (Format: domain:address,…)</string>
<string name="summary_pref_dns_hosts">domain:address,…</string> <string name="summary_pref_dns_hosts">domain:address,…</string>
<string name="title_pref_delay_test_url">True delay test url </string> <string name="title_pref_delay_test_url">True delay test url </string>
<string name="summary_pref_delay_test_url">Url</string> <string name="summary_pref_delay_test_url">Url</string>
<string name="title_pref_ip_api_url">Current connection info test url</string> <string
name="title_pref_ip_api_url"
>Current connection info test url</string>
<string name="summary_pref_ip_api_url">Url</string> <string name="summary_pref_ip_api_url">Url</string>
<string name="title_pref_proxy_sharing_enabled">Allow connections from the LAN</string> <string
<string name="summary_pref_proxy_sharing_enabled">Other devices can connect to proxy by your IP address through local proxy. Only enable in trusted networks to avoid unauthorized connections</string> name="title_pref_proxy_sharing_enabled"
<string name="toast_warning_pref_proxysharing_short">Allow connections from the LAN. Make sure you are in a trusted network</string> >Allow connections from the LAN</string>
<string
name="summary_pref_proxy_sharing_enabled"
>Other devices can connect to proxy by your IP address through local proxy. Only enable in trusted networks to avoid unauthorized connections</string>
<string
name="toast_warning_pref_proxysharing_short"
>Allow connections from the LAN. Make sure you are in a trusted network</string>
<string name="title_pref_allow_insecure">allowInsecure</string> <string name="title_pref_allow_insecure">allowInsecure</string>
<string name="summary_pref_allow_insecure">When TLS is selected, allow insecure connections by default</string> <string
name="summary_pref_allow_insecure"
>When TLS is selected, allow insecure connections by default</string>
<string name="title_pref_socks_port">Local proxy port</string> <string name="title_pref_socks_port">Local proxy port</string>
<string name="summary_pref_socks_port">Local proxy port</string> <string name="summary_pref_socks_port">Local proxy port</string>
@@ -233,23 +337,41 @@
<string name="summary_pref_local_dns_port">Local DNS port</string> <string name="summary_pref_local_dns_port">Local DNS port</string>
<string name="title_pref_confirm_remove">Delete config confirmation</string> <string name="title_pref_confirm_remove">Delete config confirmation</string>
<string name="summary_pref_confirm_remove">Whether deleting a config requires a second confirmation by the user</string> <string
name="summary_pref_confirm_remove"
>Whether deleting a config requires a second confirmation by the user</string>
<string name="title_pref_start_scan_immediate">Start scanning immediately</string> <string
<string name="summary_pref_start_scan_immediate">Open the camera to scan immediately at startup, otherwise you can choose to scan the code or select a photo in the toolbar</string> name="title_pref_start_scan_immediate"
>Start scanning immediately</string>
<string
name="summary_pref_start_scan_immediate"
>Open the camera to scan immediately at startup, otherwise you can choose to scan the code or select a photo in the toolbar</string>
<string name="title_pref_append_http_proxy">Append HTTP Proxy to VPN</string> <string
<string name="summary_pref_append_http_proxy">HTTP proxy will be used directly from (browser/ some supported apps), without going through the virtual NIC device (Android 10+)</string> name="title_pref_append_http_proxy"
>Append HTTP Proxy to VPN</string>
<string
name="summary_pref_append_http_proxy"
>HTTP proxy will be used directly from (browser/ some supported apps), without going through the virtual NIC device (Android 10+)</string>
<string name="title_pref_double_column_display">Enable double column display</string> <string
<string name="summary_pref_double_column_display">The profile list is displayed in double columns, allowing more content to be displayed on the screen. You need to restart the application to take effect.</string> name="title_pref_double_column_display"
>Enable double column display</string>
<string
name="summary_pref_double_column_display"
>The profile list is displayed in double columns, allowing more content to be displayed on the screen. You need to restart the application to take effect.</string>
<string name="title_pref_group_all_display">Enable show all groups</string> <string name="title_pref_group_all_display">Enable show all groups</string>
<string name="summary_pref_group_all_display">Add an extra "All Tabs" page</string> <string
name="summary_pref_group_all_display"
>Add an extra "All Tabs" page</string>
<!-- AboutActivity --> <!-- AboutActivity -->
<string name="title_pref_feedback">Feedback</string> <string name="title_pref_feedback">Feedback</string>
<string name="summary_pref_feedback">Feedback enhancements or bugs to GitHub</string> <string
name="summary_pref_feedback"
>Feedback enhancements or bugs to GitHub</string>
<string name="summary_pref_tg_group">Join Telegram Group</string> <string name="summary_pref_tg_group">Join Telegram Group</string>
<string name="toast_tg_app_not_found">Telegram app not found</string> <string name="toast_tg_app_not_found">Telegram app not found</string>
<string name="title_privacy_policy">Privacy policy</string> <string name="title_privacy_policy">Privacy policy</string>
@@ -261,25 +383,45 @@
<string name="title_pref_promotion">Promotion</string> <string name="title_pref_promotion">Promotion</string>
<string name="title_pref_auto_update_subscription">Automatic update subscriptions</string> <string
<string name="summary_pref_auto_update_subscription">Update your subscriptions automatically at set intervals in the background. Depending on the device, this feature may not always work</string> name="title_pref_auto_update_subscription"
<string name="title_pref_auto_update_interval">Auto Update Interval (Minutes, Min value 15)</string> >Automatic update subscriptions</string>
<string
name="summary_pref_auto_update_subscription"
>Update your subscriptions automatically at set intervals in the background. Depending on the device, this feature may not always work</string>
<string
name="title_pref_auto_update_interval"
>Auto Update Interval (Minutes, Min value 15)</string>
<string name="title_core_loglevel">Log Level</string> <string name="title_core_loglevel">Log Level</string>
<string name="title_outbound_domain_resolve_method">Outbound domain pre-resolve method</string> <string
name="title_outbound_domain_resolve_method"
>Outbound domain pre-resolve method</string>
<string name="title_mode">Mode</string> <string name="title_mode">Mode</string>
<string name="title_mode_help">Click me for more help</string> <string name="title_mode_help">Click me for more help</string>
<string name="title_language">Language</string> <string name="title_language">Language</string>
<string name="title_ui_settings">UI settings</string> <string name="title_ui_settings">UI settings</string>
<string name="title_pref_ui_mode_night">UI mode settings</string> <string name="title_pref_ui_mode_night">UI mode settings</string>
<string name="title_pref_dynamic_colors">Dynamic colors (Material You)</string> <string
<string name="summary_pref_dynamic_colors">Use wallpaper-based colors (Android 12+). Requires app restart.</string> name="title_pref_dynamic_colors"
<string name="title_pref_subscriptions_bottom">Subscriptions panel at the bottom</string> >Dynamic colors (Material You)</string>
<string name="summary_pref_subscriptions_bottom">Move the subscription tabs below the server list</string> <string
name="summary_pref_dynamic_colors"
>Use wallpaper-based colors (Android 12+). Requires app restart.</string>
<string
name="title_pref_subscriptions_bottom"
>Subscriptions panel at the bottom</string>
<string
name="summary_pref_subscriptions_bottom"
>Move the subscription tabs below the server list</string>
<string name="title_pref_use_hev_tunnel">Enable Hev TUN Feature</string> <string name="title_pref_use_hev_tunnel">Enable Hev TUN Feature</string>
<string name="summary_pref_use_hev_tunnel">When enabled, TUN will use hev-socks5-tunnel; otherwise, it will use xray-core.</string> <string
name="summary_pref_use_hev_tunnel"
>When enabled, TUN will use hev-socks5-tunnel; otherwise, it will use xray-core.</string>
<string name="title_pref_hev_tunnel_loglevel">Hev Tun Log Level</string> <string name="title_pref_hev_tunnel_loglevel">Hev Tun Log Level</string>
<string name="title_pref_hev_tunnel_rw_timeout">Hev Tun read/write timeout (seconds) (tcp,udp default 300,60)</string> <string
name="title_pref_hev_tunnel_rw_timeout"
>Hev Tun read/write timeout (seconds) (tcp,udp default 300,60)</string>
<string name="restart_required">Restart required to apply changes</string> <string name="restart_required">Restart required to apply changes</string>
<string name="title_logcat">Logcat</string> <string name="title_logcat">Logcat</string>
@@ -298,9 +440,13 @@
<string name="sub_setting_enable">Enable update</string> <string name="sub_setting_enable">Enable update</string>
<string name="sub_auto_update">Enable automatic update</string> <string name="sub_auto_update">Enable automatic update</string>
<string name="sub_allow_insecure_url">Allow insecure HTTP address</string> <string name="sub_allow_insecure_url">Allow insecure HTTP address</string>
<string name="sub_setting_pre_profile">Previous proxy config remarks</string> <string
name="sub_setting_pre_profile"
>Previous proxy config remarks</string>
<string name="sub_setting_next_profile">Next proxy config remarks</string> <string name="sub_setting_next_profile">Next proxy config remarks</string>
<string name="sub_setting_pre_profile_tip">The config remarks exist and are unique</string> <string
name="sub_setting_pre_profile_tip"
>The config remarks exist and are unique</string>
<string name="title_sub_update">Update subscription</string> <string name="title_sub_update">Update subscription</string>
<string name="title_ping_all_server">Tcping config</string> <string name="title_ping_all_server">Tcping config</string>
<string name="title_real_ping_all_server">Real delay config</string> <string name="title_real_ping_all_server">Real delay config</string>
@@ -308,17 +454,29 @@
<string name="title_sort_by_test_results">Sorting by test results</string> <string name="title_sort_by_test_results">Sorting by test results</string>
<string name="title_filter_config">Filter config</string> <string name="title_filter_config">Filter config</string>
<string name="filter_config_all">All</string> <string name="filter_config_all">All</string>
<string name="title_del_duplicate_config_count">Delete %d duplicate configs</string> <string
name="title_del_duplicate_config_count"
>Delete %d duplicate configs</string>
<string name="title_del_config_count">Delete %d configs</string> <string name="title_del_config_count">Delete %d configs</string>
<string name="title_import_config_count">Import %d configs</string> <string name="title_import_config_count">Import %d configs</string>
<string name="title_export_config_count">Export %d configs</string> <string name="title_export_config_count">Export %d configs</string>
<string name="title_update_config_count">Update %d configs</string> <string name="title_update_config_count">Update %d configs</string>
<string name="title_updating">Updating…</string> <string name="title_updating">Updating…</string>
<string name="title_update_subscription_result">Updated %1$d configs (%2$d success, %3$d failed, %4$d skipped)</string> <string
<string name="title_update_subscription_no_subscription">No subscriptions</string> name="title_update_subscription_result"
<string name="toast_server_not_found_in_group">Selected server not found in current group</string> >Updated %1$d configs (%2$d success, %3$d failed, %4$d skipped)</string>
<string name="toast_fragment_not_available">Unable to locate current view</string> <string
<string name="title_locate_selected_config">Locate the selected config</string> name="title_update_subscription_no_subscription"
>No subscriptions</string>
<string
name="toast_server_not_found_in_group"
>Selected server not found in current group</string>
<string
name="toast_fragment_not_available"
>Unable to locate current view</string>
<string
name="title_locate_selected_config"
>Locate the selected config</string>
<string name="tasker_start_service">Start Service</string> <string name="tasker_start_service">Start Service</string>
<string name="tasker_setting_confirm">Confirm</string> <string name="tasker_setting_confirm">Confirm</string>
@@ -326,64 +484,117 @@
<!-- RoutingSettingActivity --> <!-- RoutingSettingActivity -->
<string name="routing_settings_domain_strategy">Domain strategy</string> <string name="routing_settings_domain_strategy">Domain strategy</string>
<string name="routing_settings_title">Routing Settings</string> <string name="routing_settings_title">Routing Settings</string>
<string name="routing_settings_tips">Separated by commas(,), choose domain or ip</string> <string
name="routing_settings_tips"
>Separated by commas(,), choose domain or ip</string>
<string name="routing_settings_save">Save</string> <string name="routing_settings_save">Save</string>
<string name="routing_settings_delete">Clear</string> <string name="routing_settings_delete">Clear</string>
<string name="routing_settings_rule_title">Routing Rule Settings</string> <string name="routing_settings_rule_title">Routing Rule Settings</string>
<string name="routing_settings_add_rule">Add rule</string> <string name="routing_settings_add_rule">Add rule</string>
<string name="routing_settings_import_predefined_rulesets">Import predefined rulesets</string> <string
<string name="routing_settings_import_rulesets_tip">Existing rulesets will be deleted, are you sure to continue?</string> name="routing_settings_import_predefined_rulesets"
<string name="routing_settings_import_rulesets_from_clipboard">Import ruleset from clipboard</string> >Import predefined rulesets</string>
<string name="routing_settings_import_rulesets_from_qrcode">Import ruleset from QRcode</string> <string
<string name="routing_settings_export_rulesets_to_clipboard">Export ruleset to clipboard</string> name="routing_settings_import_rulesets_tip"
<string name="routing_settings_locked">Locked, keep this rule when import presets</string> >Existing rulesets will be deleted, are you sure to continue?</string>
<string
name="routing_settings_import_rulesets_from_clipboard"
>Import ruleset from clipboard</string>
<string
name="routing_settings_import_rulesets_from_qrcode"
>Import ruleset from QRcode</string>
<string
name="routing_settings_export_rulesets_to_clipboard"
>Export ruleset to clipboard</string>
<string
name="routing_settings_locked"
>Locked, keep this rule when import presets</string>
<string name="routing_settings_domain" translatable="false">domain</string> <string name="routing_settings_domain" translatable="false">domain</string>
<string name="routing_settings_ip" translatable="false">ip</string> <string name="routing_settings_ip" translatable="false">ip</string>
<string name="routing_settings_port" translatable="false">port</string> <string name="routing_settings_port" translatable="false">port</string>
<string name="routing_settings_protocol" translatable="false">protocol</string> <string
<string name="routing_settings_protocol_tip" translatable="false">[http,tls,bittorrent]</string> name="routing_settings_protocol"
<string name="routing_settings_network" translatable="false">network</string> translatable="false"
<string name="routing_settings_network_tip" translatable="false">[udp|tcp]</string> >protocol</string>
<string name="routing_settings_outbound_tag" translatable="false">outboundTag</string> <string
name="routing_settings_protocol_tip"
translatable="false"
>[http,tls,bittorrent]</string>
<string
name="routing_settings_network"
translatable="false"
>network</string>
<string
name="routing_settings_network_tip"
translatable="false"
>[udp|tcp]</string>
<string
name="routing_settings_outbound_tag"
translatable="false"
>outboundTag</string>
<string name="connection_test_pending">Check Connectivity</string> <string name="connection_test_pending">Check Connectivity</string>
<string name="connection_test_testing">Testing…</string> <string name="connection_test_testing">Testing…</string>
<string name="connection_test_testing_count">Тестирование %d серверов…</string> <string
<string name="connection_test_available">Success: Connection took %dms</string> name="connection_test_testing_count"
<string name="connection_test_error">Fail to detect internet connection: %s</string> >Тестирование %d серверов…</string>
<string
name="connection_test_available"
>Success: Connection took %dms</string>
<string
name="connection_test_error"
>Fail to detect internet connection: %s</string>
<string name="connection_test_fail">Internet Unavailable</string> <string name="connection_test_fail">Internet Unavailable</string>
<string name="connection_test_error_status_code">Error code: #%d</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_connected"
>Connected, tap to check connection</string>
<string name="connection_not_connected">Готово к подключению</string> <string name="connection_not_connected">Готово к подключению</string>
<string name="connection_updating_profiles">Updating profiles…</string> <string name="connection_updating_profiles">Updating profiles…</string>
<string name="connection_runing_task_left">Проверено успешно: %s</string> <string name="connection_runing_task_left">Проверено %s</string>
<string name="import_subscription_success">Subscription imported Successfully</string> <string
<string name="import_subscription_failure">Import subscription failed</string> name="import_subscription_success"
>Subscription imported Successfully</string>
<string
name="import_subscription_failure"
>Import subscription failed</string>
<string name="title_fragment_settings">Fragment Settings</string> <string name="title_fragment_settings">Fragment Settings</string>
<string name="title_pref_fragment_packets">Fragment Packets</string> <string name="title_pref_fragment_packets">Fragment Packets</string>
<string name="title_pref_fragment_length">Fragment Length (min-max)</string> <string name="title_pref_fragment_length">Fragment Length (min-max)</string>
<string name="title_pref_fragment_interval">Fragment Interval (min-max)</string> <string
name="title_pref_fragment_interval"
>Fragment Interval (min-max)</string>
<string name="title_pref_fragment_enabled">Enable Fragment</string> <string name="title_pref_fragment_enabled">Enable Fragment</string>
<string name="update_check_for_update">Check for update</string> <string name="update_check_for_update">Check for update</string>
<string name="update_already_latest_version">Already on the latest version</string> <string
name="update_already_latest_version"
>Already on the latest version</string>
<string name="update_new_version_found">New version found: %s</string> <string name="update_new_version_found">New version found: %s</string>
<string name="update_now">Update now</string> <string name="update_now">Update now</string>
<string name="update_check_pre_release">Check Pre-release</string> <string name="update_check_pre_release">Check Pre-release</string>
<string name="update_checking_for_update">Checking for update…</string> <string name="update_checking_for_update">Checking for update…</string>
<string name="title_policy_group_type">Policy group type</string> <string name="title_policy_group_type">Policy group type</string>
<string name="title_policy_group_subscription_id">From subscription group</string> <string
<string name="title_policy_group_subscription_filter">Remarks regular filter</string> name="title_policy_group_subscription_id"
>From subscription group</string>
<string
name="title_policy_group_subscription_filter"
>Remarks regular filter</string>
<!-- BackupActivity --> <!-- BackupActivity -->
<string name="title_configuration_backup_restore">Backup &amp; Restore</string> <string
name="title_configuration_backup_restore"
>Backup &amp; Restore</string>
<string name="title_configuration_backup">Backup config</string> <string name="title_configuration_backup">Backup config</string>
<string name="title_configuration_restore">Restore config</string> <string name="title_configuration_restore">Restore config</string>
<string name="title_configuration_share">Share config</string> <string name="title_configuration_share">Share config</string>
<string name="title_webdav_config_setting">WebDAV Settings</string> <string name="title_webdav_config_setting">WebDAV Settings</string>
<string name="title_webdav_config_setting_unknown">Please configure WebDAV first.</string> <string
name="title_webdav_config_setting_unknown"
>Please configure WebDAV first.</string>
<string name="title_webdav_url">WebDAV server URL</string> <string name="title_webdav_url">WebDAV server URL</string>
<string name="title_webdav_user">Username</string> <string name="title_webdav_user">Username</string>
<string name="title_webdav_pass">Password</string> <string name="title_webdav_pass">Password</string>
@@ -451,4 +662,36 @@
<item>WebDAV</item> <item>WebDAV</item>
</string-array> </string-array>
<!-- Donate dialog -->
<string name="donate_dialog_title">Поддержите нас</string>
<string name="donate_label_card">Карта</string>
<string name="donate_label_ton">TON</string>
<string name="donate_label_trc20">TRC20</string>
<string name="donate_label_btc">BTC</string>
<string
name="donate_card_url"
translatable="false"
>pay.cloudtips.ru/p/28c476e5</string>
<string
name="donate_addr_ton"
translatable="false"
>UQD_Qc2cxLGe1P4wANi46cKdEvvzyJRrJTYPvGX2KAZDnsDh</string>
<string
name="donate_addr_trc20"
translatable="false"
>TYQqdACH5PrScvsMowSyS8JjaaF5wvFf5Q</string>
<string
name="donate_addr_btc"
translatable="false"
>bc1qvw0ts0jk5e5dfj9fdez76j9ck95lqz04fpf02a</string>
<string name="donate_copy">Копировать</string>
<string name="donate_open_link">Открыть</string>
<string
name="donate_card_link_url"
translatable="false"
>https://pay.cloudtips.ru/p/28c476e5</string>
<string name="donate_btn_dont_show">Не показывать</string>
<string name="donate_btn_postpone">Позже</string>
<string name="donate_toast_copied">Скопировано</string>
</resources> </resources>
@@ -83,6 +83,12 @@
<item name="cornerSize">16dp</item> <item name="cornerSize">16dp</item>
</style> </style>
<!-- Dialog corner shape — flatter than M3 default (28dp) -->
<style name="ShapeAppearance.App.Dialog" parent="ShapeAppearance.Material3.Corner.ExtraLarge">
<item name="cornerFamily">rounded</item>
<item name="cornerSize">16dp</item>
</style>
<style name="RobotoEditTextStyle" parent="Widget.Material3.TextInputEditText.OutlinedBox"> <style name="RobotoEditTextStyle" parent="Widget.Material3.TextInputEditText.OutlinedBox">
<item name="android:fontFamily">sans-serif</item> <item name="android:fontFamily">sans-serif</item>
<item name="android:textSize">16sp</item> <item name="android:textSize">16sp</item>
@@ -92,6 +98,10 @@
<item name="android:fontFamily">sans-serif</item> <item name="android:fontFamily">sans-serif</item>
<item name="fontFamily">sans-serif</item> <item name="fontFamily">sans-serif</item>
<item name="shapeAppearanceMediumComponent">@style/ShapeAppearance.App.MediumComponent</item> <item name="shapeAppearanceMediumComponent">@style/ShapeAppearance.App.MediumComponent</item>
<!-- Flatter corners -->
<item name="shapeAppearanceCornerExtraLarge">@style/ShapeAppearance.App.Dialog</item>
<!-- Make dialog background match app surface instead of standing out -->
<item name="colorSurfaceContainerHigh">?attr/colorSurface</item>
</style> </style>
<!-- Light --> <!-- Light -->
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name" translatable="false">olcNG</string> <string name="app_name" translatable="false">olcng</string>
</resources> </resources>
+8 -7
View File
@@ -2,17 +2,17 @@
agp = "9.1.0" agp = "9.1.0"
desugarJdkLibs = "2.1.5" desugarJdkLibs = "2.1.5"
gradleLicensePlugin = "0.9.8" gradleLicensePlugin = "0.9.8"
kotlin = "2.1.0" kotlin = "2.2.0"
coreKtx = "1.17.0" coreKtx = "1.18.0"
junit = "4.13.2" junit = "4.13.2"
junitVersion = "1.3.0" junitVersion = "1.3.0"
espressoCore = "3.7.0" espressoCore = "3.7.0"
appcompat = "1.7.1" appcompat = "1.7.1"
material = "1.13.0" material = "1.14.0"
activity = "1.12.4" activity = "1.13.0"
constraintlayout = "2.2.1" constraintlayout = "2.2.1"
mmkvStatic = "1.3.16" mmkvStatic = "1.3.16"
gson = "2.13.2" gson = "2.14.0"
okhttp = "5.3.2" okhttp = "5.3.2"
quickieFoss = "1.14.0" quickieFoss = "1.14.0"
kotlinxCoroutinesAndroid = "1.10.2" kotlinxCoroutinesAndroid = "1.10.2"
@@ -21,10 +21,11 @@ swiperefreshlayout = "1.2.0"
toasty = "1.5.2" toasty = "1.5.2"
editorkit = "2.9.0" editorkit = "2.9.0"
core = "3.5.4" core = "3.5.4"
workRuntimeKtx = "2.11.1" workRuntimeKtx = "2.11.2"
lifecycleViewmodelKtx = "2.10.0" lifecycleViewmodelKtx = "2.10.0"
multidex = "2.0.1" multidex = "2.0.1"
mockitoMockitoInline = "5.2.0" mockitoMockitoInline = "5.2.0"
mockitoKotlin = "5.4.0"
flexbox = "3.0.0" flexbox = "3.0.0"
preferenceKtx = "1.2.1" preferenceKtx = "1.2.1"
recyclerview = "1.4.0" recyclerview = "1.4.0"
@@ -60,7 +61,7 @@ lifecycle-livedata-ktx = { module = "androidx.lifecycle:lifecycle-livedata-ktx",
lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycleViewmodelKtx" } lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycleViewmodelKtx" }
multidex = { module = "androidx.multidex:multidex", version.ref = "multidex" } multidex = { module = "androidx.multidex:multidex", version.ref = "multidex" }
org-mockito-mockito-inline = { module = "org.mockito:mockito-inline", version.ref = "mockitoMockitoInline" } org-mockito-mockito-inline = { module = "org.mockito:mockito-inline", version.ref = "mockitoMockitoInline" }
mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version.ref = "mockitoMockitoInline" } mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version.ref = "mockitoKotlin" }
flexbox = { module = "com.google.android.flexbox:flexbox", version.ref = "flexbox" } flexbox = { module = "com.google.android.flexbox:flexbox", version.ref = "flexbox" }
recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview" } recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview" }
preference-ktx = { module = "androidx.preference:preference-ktx", version.ref = "preferenceKtx" } preference-ktx = { module = "androidx.preference:preference-ktx", version.ref = "preferenceKtx" }
Binary file not shown.
+5 -2
View File
@@ -1,6 +1,9 @@
#Thu Nov 14 12:42:51 BDT 2024
distributionBase=GRADLE_USER_HOME distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip
networkTimeout=10000
retries=0
retryBackOffMs=500
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
+179 -116
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env sh #!/bin/sh
# #
# Copyright 2015 the original author or authors. # Copyright © 2015 the original authors.
# #
# Licensed under the Apache License, Version 2.0 (the "License"); # Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License. # you may not use this file except in compliance with the License.
@@ -15,81 +15,114 @@
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
# #
# SPDX-License-Identifier: Apache-2.0
#
############################################################################## ##############################################################################
## #
## Gradle start up script for UN*X # Gradle start up script for POSIX generated by Gradle.
## #
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/3d91ce3b8caaf77ad09f381f43615b715b53f72c/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
############################################################################## ##############################################################################
# Attempt to set APP_HOME # Attempt to set APP_HOME
# Resolve links: $0 may be a link # Resolve links: $0 may be a link
PRG="$0" app_path=$0
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do # Need this for daisy-chained symlinks.
ls=`ls -ld "$PRG"` while
link=`expr "$ls" : '.*-> \(.*\)$'` APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
if expr "$link" : '/.*' > /dev/null; then [ -h "$app_path" ]
PRG="$link" do
else ls=$( ls -ld "$app_path" )
PRG=`dirname "$PRG"`"/$link" link=${ls#*' -> '}
fi case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle" # This is normally unused
APP_BASE_NAME=`basename "$0"` # shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value. # Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum" MAX_FD=maximum
warn () { warn () {
echo "$*" echo "$*"
} } >&2
die () { die () {
echo echo
echo "$*" echo "$*"
echo echo
exit 1 exit 1
} } >&2
# OS specific support (must be 'true' or 'false'). # OS specific support (must be 'true' or 'false').
cygwin=false cygwin=false
msys=false msys=false
darwin=false darwin=false
nonstop=false nonstop=false
case "`uname`" in case "$( uname )" in #(
CYGWIN* ) CYGWIN* ) cygwin=true ;; #(
cygwin=true Darwin* ) darwin=true ;; #(
;; MSYS* | MINGW* ) msys=true ;; #(
Darwin* ) NONSTOP* ) nonstop=true ;;
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM. # Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables # IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java" JAVACMD=$JAVA_HOME/jre/sh/java
else else
JAVACMD="$JAVA_HOME/bin/java" JAVACMD=$JAVA_HOME/bin/java
fi fi
if [ ! -x "$JAVACMD" ] ; then if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@@ -98,88 +131,118 @@ Please set the JAVA_HOME variable in your environment to match the
location of your Java installation." location of your Java installation."
fi fi
else else
JAVACMD="java" JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the Please set the JAVA_HOME variable in your environment to match the
location of your Java installation." location of your Java installation."
fi fi
fi
# Increase the maximum file descriptors if we can. # Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
MAX_FD_LIMIT=`ulimit -H -n` case $MAX_FD in #(
if [ $? -eq 0 ] ; then max*)
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
MAX_FD="$MAX_FD_LIMIT" # shellcheck disable=SC2039,SC3045
fi MAX_FD=$( ulimit -H -n ) ||
ulimit -n $MAX_FD warn "Could not query maximum file descriptor limit"
if [ $? -ne 0 ] ; then esac
warn "Could not set maximum file descriptor limit: $MAX_FD" case $MAX_FD in #(
fi '' | soft) :;; #(
else *)
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
fi # shellcheck disable=SC2039,SC3045
fi ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac esac
fi fi
# Escape application args # Collect all arguments for the java command, stacking in reverse order:
save () { # * args from the command line
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done # * the main class name
echo " " # * -classpath
} # * -D...appname settings
APP_ARGS=`save "$@"` # * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# Collect all arguments for the java command, following the shell quoting and substitution rules # For Cygwin or MSYS, switch paths to Windows format before running java
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@" exec "$JAVACMD" "$@"
+24 -31
View File
@@ -13,6 +13,8 @@
@rem See the License for the specific language governing permissions and @rem See the License for the specific language governing permissions and
@rem limitations under the License. @rem limitations under the License.
@rem @rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off @if "%DEBUG%"=="" @echo off
@rem ########################################################################## @rem ##########################################################################
@@ -21,11 +23,12 @@
@rem @rem
@rem ########################################################################## @rem ##########################################################################
@rem Set local scope for the variables with windows NT shell @rem Set local scope for the variables, and ensure extensions are enabled
if "%OS%"=="Windows_NT" setlocal setlocal EnableExtensions
set DIRNAME=%~dp0 set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=. if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0 set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME% set APP_HOME=%DIRNAME%
@@ -40,15 +43,15 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1 %JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute if %ERRORLEVEL% equ 0 goto execute
echo. echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. echo location of your Java installation. 1>&2
goto fail "%COMSPEC%" /c exit 1
:findJavaFromJavaHome :findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=% set JAVA_HOME=%JAVA_HOME:"=%
@@ -56,34 +59,24 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute if exist "%JAVA_EXE%" goto execute
echo. echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. echo location of your Java installation. 1>&2
goto fail "%COMSPEC%" /c exit 1
:execute :execute
@rem Setup the command line @rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle @rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* @rem endlocal doesn't take effect until after the line is parsed and variables are expanded
@rem which allows us to clear the local environment before executing the java command
endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel
:end :exitWithErrorLevel
@rem End local scope for the variables with windows NT shell @rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
if "%ERRORLEVEL%"=="0" goto mainEnd "%COMSPEC%" /c exit %ERRORLEVEL%
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
+7 -1
View File
@@ -10,8 +10,14 @@ __base="$(basename "${__file}" .sh)"
trap 'echo -e "Aborted, error $? in command: $BASH_COMMAND"; trap ERR; exit 1' ERR INT trap 'echo -e "Aborted, error $? in command: $BASH_COMMAND"; trap ERR; exit 1' ERR INT
export ANDROID_HOME=${ANDROID_HOME:-/opt/android-sdk} export ANDROID_HOME=${ANDROID_HOME:-$HOME/android-sdk}
if [[ ! -d $ANDROID_HOME ]]; then
export ANDROID_HOME=$HOME/android-sdk
fi
export NDK_HOME=${NDK_HOME:-$ANDROID_HOME/ndk/25.2.9519653} export NDK_HOME=${NDK_HOME:-$ANDROID_HOME/ndk/25.2.9519653}
if [[ ! -d $NDK_HOME ]]; then
export NDK_HOME=$ANDROID_HOME/ndk/25.2.9519653
fi
if [[ ! -d $NDK_HOME ]]; then if [[ ! -d $NDK_HOME ]]; then
echo "Android NDK: NDK_HOME not found. please set env \$NDK_HOME" echo "Android NDK: NDK_HOME not found. please set env \$NDK_HOME"