mirror of
https://github.com/openlibrecommunity/olcng.git
synced 2026-06-02 06:23:41 +02:00
Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 29fd0d719d | |||
| abeb58191e | |||
| f55ac36b5a | |||
| 63aec656d8 | |||
| 2062d56fdb | |||
| 0b02534724 | |||
| a75deb1374 | |||
| fbb6da0c42 | |||
| fc9f18858a | |||
| 9df74d3ad2 | |||
| cbefd19abd | |||
| 5dcba978a7 | |||
| b26d1bde77 | |||
| 2878456437 | |||
| d99677ca23 | |||
| 4dff052c35 | |||
| 4f1aca4928 | |||
| 35f2a1079b | |||
| 0adcbfcaf5 | |||
| b81708ff5c | |||
| 4ad24ec2dc | |||
| 4fa5dd4bca | |||
| b2f3415421 | |||
| 6e1c774d86 | |||
| 8e9e709d12 | |||
| 649f305a82 | |||
| 44005dffd3 | |||
| bee7002f54 | |||
| 0363ebaabd | |||
| 88627bbf4f | |||
| ceec94e5db | |||
| b30fc13b0d | |||
| a9f5844b84 | |||
| 518edd096b | |||
| 450542d3b8 |
@@ -66,3 +66,12 @@ Thumbs.db
|
||||
add_subscription_mmkv.py
|
||||
.gitignore
|
||||
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
@@ -3,13 +3,13 @@ module github.com/2dust/AndroidLibXrayLite
|
||||
go 1.26
|
||||
|
||||
require (
|
||||
github.com/xtls/xray-core v1.260327.0
|
||||
golang.org/x/mobile v0.0.0-20260312152759-81488f6aeb60
|
||||
github.com/xtls/xray-core v1.260327.1-0.20260509173629-1bdb488c9ec0
|
||||
golang.org/x/mobile v0.0.0-20260520154334-0e4426e1883d
|
||||
)
|
||||
|
||||
require (
|
||||
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/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 // 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/miekg/dns v1.1.72 // 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/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/sagernet/sing v0.7.13 // 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
|
||||
go.uber.org/mock v0.6.0 // 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/mod v0.34.0 // indirect
|
||||
golang.org/x/net v0.52.0 // indirect
|
||||
golang.org/x/mod v0.36.0 // indirect
|
||||
golang.org/x/net v0.54.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
golang.org/x/sys v0.44.0 // indirect
|
||||
golang.org/x/text v0.37.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/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
||||
google.golang.org/grpc v1.79.3 // indirect
|
||||
golang.zx2c4.com/wireguard/windows v1.0.1 // 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
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 // indirect
|
||||
|
||||
+40
-36
@@ -1,7 +1,7 @@
|
||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||
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.20260217092621-db4786c77a22/go.mod h1:Npbg8qBtAZlsAB3FWmqwlVh5jtVG6a4DlYsOylUpvzA=
|
||||
github.com/apernet/quic-go v0.59.1-0.20260425001925-6c6cc9bcb716 h1:J1O+xpLuJWkdYbw5JPGwBqIHs2J8tiEP7Py9lPqkN2I=
|
||||
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/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
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/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/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4=
|
||||
github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
||||
github.com/pires/go-proxyproto v0.12.0 h1:TTCxD66dU898tahivkqc3hoceZp7P44FnorWyo9d5vM=
|
||||
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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
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/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/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/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
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/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/xray-core v1.260327.0 h1:g4TzxMwyPrxslZh6uD+FiG3lXKTrnNO+b4ky2OhogHE=
|
||||
github.com/xtls/xray-core v1.260327.0/go.mod h1:OXMlhBloFry8mw0KwWLWLd3RQyXJzEYsCGlgsX36h60=
|
||||
github.com/xtls/xray-core v1.260327.1-0.20260509173629-1bdb488c9ec0 h1:ft6HiTHelF1z9i3zZVPG9Q1LcLcvEB5jzBxky/+wljk=
|
||||
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/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/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
||||
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
||||
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
||||
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
||||
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
||||
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
|
||||
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
||||
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/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/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
|
||||
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/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
|
||||
golang.org/x/mobile v0.0.0-20260312152759-81488f6aeb60 h1:MOzyaj0wu2xneBkzkg9LHNYjDBB4W5vP043A2SYQRPA=
|
||||
golang.org/x/mobile v0.0.0-20260312152759-81488f6aeb60/go.mod h1:th6VJvzjMbrYF8SduQY5rpD0HG0GleGxjadkqSxFs3k=
|
||||
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
||||
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
||||
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||
golang.org/x/mobile v0.0.0-20260520154334-0e4426e1883d h1:pWrEKZvKeqE2xPrylgBjgCyJSpPPt3L2WG2DmA+Xccg=
|
||||
golang.org/x/mobile v0.0.0-20260520154334-0e4426e1883d/go.mod h1:ltIbhcRzKgwHa4ZxKJeiv0nyzcXUUYCqMyO0Y+vPmXw=
|
||||
golang.org/x/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4=
|
||||
golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ=
|
||||
golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
|
||||
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/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
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.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
|
||||
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||
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/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
|
||||
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
|
||||
golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8=
|
||||
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/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/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
|
||||
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
golang.zx2c4.com/wireguard/windows v1.0.1 h1:eOxiDVbywPC+ZQqvdCK7x+ZwWXKbYv50TtH8ysFIbw8=
|
||||
golang.zx2c4.com/wireguard/windows v1.0.1/go.mod h1:+fbT3FFdX4zzYDLwJh5+HPEcNN/3HyNdzhNSVsQM+zs=
|
||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
|
||||
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/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<item name="app_name" type="string">olcNG</item>
|
||||
<item name="app_name" type="string">olcng</item>
|
||||
</resources>
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name" translatable="false">olcNG</string>
|
||||
<string name="app_name" translatable="false">olcng</string>
|
||||
</resources>
|
||||
|
||||
@@ -74,9 +74,14 @@ object AppConfig {
|
||||
const val PREF_HEV_TUNNEL_RW_TIMEOUT = "pref_hev_tunnel_rw_timeout_v2"
|
||||
const val PREF_AUTO_SORT_AFTER_TEST = "pref_auto_sort_after_test"
|
||||
const val PREF_SHOW_COPY_BUTTON = "pref_show_copy_button"
|
||||
const val PREF_SHOW_SERVER_IP = "pref_show_server_ip"
|
||||
const val PREF_DYNAMIC_COLORS = "pref_dynamic_colors"
|
||||
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. */
|
||||
const val CACHE_SUBSCRIPTION_ID = "cache_subscription_id"
|
||||
|
||||
|
||||
@@ -3,8 +3,8 @@ package xyz.zarazaex.olc.dto
|
||||
data class ServerAffiliationInfo(var testDelayMillis: Long = 0L) {
|
||||
fun getTestDelayString(): String {
|
||||
return when {
|
||||
testDelayMillis == 0L -> ""
|
||||
testDelayMillis < 0L -> "Error"
|
||||
testDelayMillis == 0L -> "—ms"
|
||||
testDelayMillis < 0L -> "-ms"
|
||||
else -> "${testDelayMillis}ms"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,5 +2,7 @@ package xyz.zarazaex.olc.dto
|
||||
|
||||
data class ServersCache(
|
||||
val guid: String,
|
||||
val profile: ProfileItem
|
||||
val profile: ProfileItem,
|
||||
val testDelayMillis: Long = 0L,
|
||||
val isSelected: Boolean = false
|
||||
)
|
||||
@@ -612,18 +612,19 @@ object AngConfigManager {
|
||||
Log.i(AppConfig.TAG, url)
|
||||
val userAgent = it.subscription.userAgent
|
||||
|
||||
val timeout = if (url.startsWith("https://key.zarazaex.xyz/sub")) 3000 else 6000
|
||||
val proxyTimeout = if (url.startsWith("https://key.zarazaex.xyz/sub")) 3000 else 5000
|
||||
val directTimeout = if (url.startsWith("https://key.zarazaex.xyz/sub")) 3000 else 11000
|
||||
|
||||
var configText = try {
|
||||
val httpPort = SettingsManager.getHttpPort()
|
||||
HttpUtil.getUrlContentWithUserAgent(url, userAgent, timeout, httpPort)
|
||||
HttpUtil.getUrlContentWithUserAgent(url, userAgent, proxyTimeout, httpPort)
|
||||
} catch (e: Exception) {
|
||||
Log.e(AppConfig.ANG_PACKAGE, "Update subscription: proxy not ready or other error", e)
|
||||
""
|
||||
}
|
||||
if (configText.isEmpty()) {
|
||||
configText = try {
|
||||
HttpUtil.getUrlContentWithUserAgent(url, userAgent, timeout)
|
||||
HttpUtil.getUrlContentWithUserAgent(url, userAgent, directTimeout)
|
||||
} catch (e: Exception) {
|
||||
Log.e(AppConfig.TAG, "Update subscription: Failed to get URL content with user agent", e)
|
||||
""
|
||||
|
||||
@@ -16,7 +16,11 @@ object CountryDetector {
|
||||
// ── Emoji flag → ISO 2-letter country code ────────────────────────────────
|
||||
|
||||
/** 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()
|
||||
var i = 0
|
||||
while (i < codePoints.size - 1) {
|
||||
@@ -25,11 +29,13 @@ object CountryDetector {
|
||||
if (cp1 in 0x1F1E6..0x1F1FF && cp2 in 0x1F1E6..0x1F1FF) {
|
||||
val c1 = ('A'.code + (cp1 - 0x1F1E6)).toChar()
|
||||
val c2 = ('A'.code + (cp2 - 0x1F1E6)).toChar()
|
||||
return "$c1$c2"
|
||||
result.add("$c1$c2")
|
||||
i += 2
|
||||
continue
|
||||
}
|
||||
i++
|
||||
}
|
||||
return null
|
||||
return result
|
||||
}
|
||||
|
||||
/** Get best country code for a server (emoji first, then cache). */
|
||||
@@ -41,6 +47,16 @@ object CountryDetector {
|
||||
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 ──────────────────────────────────────────────────
|
||||
|
||||
/** ISO code → flag emoji string */
|
||||
|
||||
@@ -700,9 +700,15 @@ object MmkvManager {
|
||||
/** Persists ISO country code for [ip]. */
|
||||
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). */
|
||||
fun getCountryFilter(): Set<String> =
|
||||
settingsStorage.decodeStringSet("pref_country_filter") ?: emptySet()
|
||||
/** Loads the user's country filter preference (set of ISO codes to EXCLUDE, empty = show all). */
|
||||
fun getCountryFilter(): Set<String> {
|
||||
// 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. */
|
||||
fun setCountryFilter(codes: Set<String>) {
|
||||
|
||||
@@ -305,20 +305,28 @@ object V2RayServiceManager {
|
||||
var time = -1L
|
||||
var errorStr = ""
|
||||
|
||||
try {
|
||||
time = coreController.measureDelay(SettingsManager.getDelayTestUrl())
|
||||
} catch (e: Exception) {
|
||||
Log.e(AppConfig.TAG, "StartCore-Manager: Failed to measure delay", e)
|
||||
errorStr = e.message?.substringAfter("\":") ?: "empty message"
|
||||
}
|
||||
if (time == -1L) {
|
||||
val urls = listOf(
|
||||
SettingsManager.getDelayTestUrl(),
|
||||
SettingsManager.getDelayTestUrl(true)
|
||||
)
|
||||
for (url in urls) {
|
||||
if (time >= 0) break
|
||||
try {
|
||||
time = coreController.measureDelay(SettingsManager.getDelayTestUrl(true))
|
||||
time = coreController.measureDelay(url)
|
||||
} catch (e: Exception) {
|
||||
Log.e(AppConfig.TAG, "StartCore-Manager: Failed to measure delay", e)
|
||||
errorStr = e.message?.substringAfter("\":") ?: "empty message"
|
||||
}
|
||||
}
|
||||
// One more retry after a brief pause to reduce false negatives
|
||||
if (time == -1L) {
|
||||
kotlinx.coroutines.delay(500)
|
||||
try {
|
||||
time = coreController.measureDelay(urls[0])
|
||||
} catch (e: Exception) {
|
||||
errorStr = e.message?.substringAfter("\":") ?: "empty message"
|
||||
}
|
||||
}
|
||||
|
||||
val result = if (time >= 0) {
|
||||
service.getString(R.string.connection_test_available, time)
|
||||
@@ -330,7 +338,7 @@ object V2RayServiceManager {
|
||||
// Only fetch IP info if the delay test was successful
|
||||
if (time >= 0) {
|
||||
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.dto.PingProgressUpdate
|
||||
import xyz.zarazaex.olc.dto.PingResultItem
|
||||
import xyz.zarazaex.olc.handler.MmkvManager
|
||||
import xyz.zarazaex.olc.handler.SettingsManager
|
||||
import xyz.zarazaex.olc.handler.V2RayNativeManager
|
||||
import xyz.zarazaex.olc.handler.V2rayConfigManager
|
||||
@@ -57,8 +58,15 @@ class RealPingWorkerService(
|
||||
|
||||
scope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
// Prepare configurations in parallel for faster startup
|
||||
val shuffledGuids = guids.shuffled()
|
||||
// Prepare configurations in parallel for faster startup.
|
||||
// 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 ->
|
||||
async(Dispatchers.IO) {
|
||||
val configResult = V2rayConfigManager.getV2rayConfig4Speedtest(context, guid)
|
||||
@@ -135,7 +143,7 @@ class RealPingWorkerService(
|
||||
|
||||
private fun sendBatchUpdate(update: PingProgressUpdate) {
|
||||
MessageUtil.sendMsg2UI(context, AppConfig.MSG_MEASURE_CONFIG_BATCH, update)
|
||||
MessageUtil.sendMsg2UI(context, AppConfig.MSG_MEASURE_CONFIG_NOTIFY, "${update.finished} / ${update.total}")
|
||||
MessageUtil.sendMsg2UI(context, AppConfig.MSG_MEASURE_CONFIG_NOTIFY, "${update.finished}/${update.total}")
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
|
||||
@@ -123,6 +123,9 @@ abstract class BaseActivity : AppCompatActivity() {
|
||||
setSupportActionBar(it)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(showHomeAsUp)
|
||||
title?.let { t -> this.title = t }
|
||||
val typedValue = TypedValue()
|
||||
theme.resolveAttribute(com.google.android.material.R.attr.colorOnSurface, typedValue, true)
|
||||
it.setTitleTextColor(typedValue.data)
|
||||
syncStatusBarWithToolbar(it)
|
||||
}
|
||||
progressBar = findViewById(R.id.progress_bar)
|
||||
|
||||
@@ -12,7 +12,6 @@ import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import xyz.zarazaex.olc.AppConfig
|
||||
import xyz.zarazaex.olc.R
|
||||
import xyz.zarazaex.olc.contracts.MainAdapterListener
|
||||
@@ -65,7 +64,7 @@ class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>(),
|
||||
} else {
|
||||
binding.recyclerView.layoutManager = GridLayoutManager(requireContext(), 1)
|
||||
}
|
||||
addCustomDividerToRecyclerView(binding.recyclerView, R.drawable.custom_divider)
|
||||
addCustomDividerToRecyclerView(binding.recyclerView, R.drawable.server_list_divider)
|
||||
binding.recyclerView.adapter = adapter
|
||||
|
||||
itemTouchHelper = ItemTouchHelper(SimpleItemTouchHelperCallback(adapter, allowSwipe = false))
|
||||
@@ -76,19 +75,21 @@ class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>(),
|
||||
// // Set the distance to trigger sync to 160dp
|
||||
// binding.refreshLayout.setDistanceToTriggerSync((160 * resources.displayMetrics.density).toInt())
|
||||
|
||||
mainViewModel.updateListAction.observe(viewLifecycleOwner) { index ->
|
||||
if (mainViewModel.subscriptionId != subId) {
|
||||
return@observe
|
||||
}
|
||||
// Log.d(TAG, "GroupServerFragment updateListAction subId=$subId")
|
||||
adapter.setData(mainViewModel.serversCache, index)
|
||||
// Each fragment subscribes independently to the shared flow and filters its own subId.
|
||||
// No onResume subscription switch needed — the active fragment's subId is always correct.
|
||||
lifecycleScope.launch {
|
||||
mainViewModel.serverListFlow.collect { list ->
|
||||
if (mainViewModel.subscriptionId == subId) {
|
||||
adapter.setData(list)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Log.d(TAG, "GroupServerFragment onViewCreated: subId=$subId")
|
||||
}
|
||||
|
||||
override fun 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)
|
||||
}
|
||||
|
||||
@@ -219,7 +220,7 @@ class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>(),
|
||||
*/
|
||||
private fun removeServerSub(guid: String, position: Int) {
|
||||
mainViewModel.removeServer(guid)
|
||||
adapter.removeServerSub(guid, position)
|
||||
// adapter updates automatically via serverListFlow
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -231,19 +232,13 @@ class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>(),
|
||||
val selected = MmkvManager.getSelectServer()
|
||||
if (guid == selected) {
|
||||
MmkvManager.setSelectServer("")
|
||||
val position = mainViewModel.getPosition(guid)
|
||||
adapter.setSelectServer(position, position)
|
||||
if (mainViewModel.isRunning.value == true) {
|
||||
ownerActivity.restartV2Ray()
|
||||
}
|
||||
} else {
|
||||
MmkvManager.setSelectServer(guid)
|
||||
val fromPosition = mainViewModel.getPosition(selected.orEmpty())
|
||||
val toPosition = mainViewModel.getPosition(guid)
|
||||
adapter.setSelectServer(fromPosition, toPosition)
|
||||
if (mainViewModel.isRunning.value == true) {
|
||||
ownerActivity.restartV2Ray()
|
||||
}
|
||||
}
|
||||
// Republish snapshot so DiffUtil picks up the selection change in card background
|
||||
mainViewModel.reloadServerList()
|
||||
if (mainViewModel.isRunning.value == true) {
|
||||
ownerActivity.restartV2Ray()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,9 +305,7 @@ class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>(),
|
||||
return
|
||||
}
|
||||
|
||||
// Find the position of the selected server
|
||||
val serversCache = mainViewModel.serversCache
|
||||
val position = serversCache.indexOfFirst { it.guid == selectedGuid }
|
||||
val position = mainViewModel.serverListFlow.value.indexOfFirst { it.guid == selectedGuid }
|
||||
val recyclerView = binding.recyclerView
|
||||
|
||||
if (position >= 0) {
|
||||
@@ -333,4 +326,4 @@ class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>(),
|
||||
ownerActivity.toast(R.string.toast_server_not_found_in_group)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package xyz.zarazaex.olc.ui
|
||||
|
||||
import android.animation.ArgbEvaluator
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.Intent
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Color
|
||||
@@ -56,6 +58,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
||||
private var isLiteTesting = false
|
||||
private var easterEggClickCount = 0
|
||||
private var isEasterEggActive = false
|
||||
private var liteActionJob: kotlinx.coroutines.Job? = null
|
||||
/** Был ли VPN уже запущен в предыдущем колбэке — чтобы детектировать момент подключения */
|
||||
private var wasRunning = false
|
||||
|
||||
@@ -110,21 +113,33 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
||||
toggle.syncState()
|
||||
binding.navView.setNavigationItemSelectedListener(this)
|
||||
|
||||
val typedValue = android.util.TypedValue()
|
||||
theme.resolveAttribute(com.google.android.material.R.attr.colorOnSurface, typedValue, true)
|
||||
val onSurface = typedValue.data
|
||||
binding.toolbar.setTitleTextColor(onSurface)
|
||||
// MaterialToolbar с titleCentered рисует отдельный TextView — красим его явно
|
||||
for (i in 0 until binding.toolbar.childCount) {
|
||||
val child = binding.toolbar.getChildAt(i)
|
||||
if (child is android.widget.TextView) {
|
||||
child.setTextColor(onSurface)
|
||||
}
|
||||
}
|
||||
|
||||
ViewCompat.setOnApplyWindowInsetsListener(binding.drawerContentLayout) { v, insets ->
|
||||
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
||||
v.setPadding(0, systemBars.top, 0, systemBars.bottom)
|
||||
insets
|
||||
}
|
||||
|
||||
findViewById<android.widget.TextView>(R.id.drawer_settings)?.setOnClickListener {
|
||||
findViewById<android.view.View>(R.id.drawer_settings)?.setOnClickListener {
|
||||
requestActivityLauncher.launch(Intent(this, SettingsActivity::class.java))
|
||||
binding.drawerLayout.closeDrawer(androidx.core.view.GravityCompat.START)
|
||||
}
|
||||
findViewById<android.widget.TextView>(R.id.drawer_per_app)?.setOnClickListener {
|
||||
findViewById<android.view.View>(R.id.drawer_per_app)?.setOnClickListener {
|
||||
requestActivityLauncher.launch(Intent(this, PerAppProxyActivity::class.java))
|
||||
binding.drawerLayout.closeDrawer(androidx.core.view.GravityCompat.START)
|
||||
}
|
||||
findViewById<android.widget.TextView>(R.id.drawer_check_update)?.setOnClickListener {
|
||||
findViewById<android.view.View>(R.id.drawer_check_update)?.setOnClickListener {
|
||||
startActivity(Intent(this, CheckUpdateActivity::class.java))
|
||||
binding.drawerLayout.closeDrawer(androidx.core.view.GravityCompat.START)
|
||||
}
|
||||
@@ -186,10 +201,17 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
||||
}
|
||||
|
||||
checkForUpdatesOnStartup()
|
||||
showDonateDialogIfNeeded()
|
||||
}
|
||||
|
||||
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 ->
|
||||
if (testing) {
|
||||
@@ -203,40 +225,53 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
||||
// Молния — стоп-кнопка, всегда активна во время теста
|
||||
binding.btnSummaryLite.isEnabled = true
|
||||
binding.btnSummaryLite.alpha = 1.0f
|
||||
binding.btnSummaryLite.setImageResource(R.drawable.ic_stop_24dp)
|
||||
binding.btnSummaryLite.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(this, R.color.color_fab_active))
|
||||
binding.btnSummaryLite.setIconResource(R.drawable.ic_stop_24dp)
|
||||
animateButtonTint(binding.btnSummaryLite,
|
||||
com.google.android.material.color.MaterialColors.getColor(this, com.google.android.material.R.attr.colorSecondaryContainer, 0)
|
||||
)
|
||||
} else {
|
||||
setButtonsEnabled(true)
|
||||
binding.btnSummaryLite.setImageResource(R.drawable.bolt_24)
|
||||
binding.btnSummaryLite.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(this, R.color.color_fab_inactive))
|
||||
binding.btnSummaryLite.setIconResource(R.drawable.bolt_24)
|
||||
animateButtonTint(binding.btnSummaryLite,
|
||||
com.google.android.material.color.MaterialColors.getColor(this, com.google.android.material.R.attr.colorSecondaryContainer, 0)
|
||||
)
|
||||
if (!isLiteTesting) {
|
||||
showStatus("Проверка завершена")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mainViewModel.liteTestFinished.observe(this) { finished ->
|
||||
if (finished && isLiteTesting) {
|
||||
isLiteTesting = false
|
||||
mainViewModel.sortByTestResults()
|
||||
|
||||
val firstReachable = mainViewModel.serversCache.firstOrNull { cache ->
|
||||
(MmkvManager.decodeServerAffiliationInfo(cache.guid)?.testDelayMillis ?: 0L) > 0L
|
||||
}
|
||||
// Ищем лучший сервер ДО сортировки, прямо из текущего cache
|
||||
val firstReachable = mainViewModel.serversCache
|
||||
.filter { (MmkvManager.decodeServerAffiliationInfo(it.guid)?.testDelayMillis ?: 0L) > 0L }
|
||||
.minByOrNull { MmkvManager.decodeServerAffiliationInfo(it.guid)?.testDelayMillis ?: Long.MAX_VALUE }
|
||||
|
||||
if (firstReachable != null) {
|
||||
MmkvManager.setSelectServer(firstReachable.guid)
|
||||
mainViewModel.reloadServerList() // reload AFTER selection so indicator renders correctly
|
||||
}
|
||||
|
||||
mainViewModel.suppressPinSelected = false
|
||||
mainViewModel.sortByTestResults()
|
||||
mainViewModel.reloadServerList()
|
||||
|
||||
if (firstReachable != null) {
|
||||
showStatus("Подключаемся к быстрейшему серверу")
|
||||
// Блокируем кнопки на время подключения
|
||||
setButtonsEnabled(false)
|
||||
applyRunningState(isLoading = true, isRunning = false)
|
||||
startV2RayWithPermission()
|
||||
} else {
|
||||
mainViewModel.reloadServerList()
|
||||
showStatus("Нет доступных серверов!")
|
||||
setButtonsEnabled(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
mainViewModel.isRunning.observe(this) { isRunning ->
|
||||
applyRunningState(false, isRunning)
|
||||
if (!isFabOperationInProgress) {
|
||||
applyRunningState(false, isRunning)
|
||||
}
|
||||
// Как только VPN только что подключился — обновляем подписки через него
|
||||
if (isRunning && !wasRunning) {
|
||||
updateSubsViaVpn()
|
||||
@@ -302,15 +337,19 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
||||
}
|
||||
|
||||
private fun handleFabAction() {
|
||||
// Если идёт подключение (isLoading) — позволяем прервать и остановить сервис
|
||||
if (isFabOperationInProgress) {
|
||||
Log.d(AppConfig.TAG, "FAB: cancel in-progress, stopping service")
|
||||
isFabOperationInProgress = false
|
||||
lifecycleScope.launch {
|
||||
V2RayServiceManager.stopVService(this@MainActivity)
|
||||
}
|
||||
return
|
||||
}
|
||||
isFabOperationInProgress = true
|
||||
|
||||
val isRunning = mainViewModel.isRunning.value == true
|
||||
|
||||
// Блокируем все кнопки сразу
|
||||
setButtonsEnabled(false)
|
||||
applyRunningState(isLoading = true, isRunning = false)
|
||||
|
||||
lifecycleScope.launch {
|
||||
@@ -341,11 +380,21 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
||||
}
|
||||
|
||||
private fun handleLiteAction() {
|
||||
// If testing is in progress - stop it
|
||||
if (mainViewModel.isTesting.value == true) {
|
||||
// Отмена на любом этапе: обновление подписок или тест
|
||||
if (mainViewModel.isTesting.value == true || liteActionJob?.isActive == true) {
|
||||
liteActionJob?.cancel()
|
||||
liteActionJob = null
|
||||
mainViewModel.cancelAllTests()
|
||||
showStatus("Тест остановлен")
|
||||
mainViewModel.suppressPinSelected = false
|
||||
isLiteTesting = false
|
||||
isFabOperationInProgress = false
|
||||
showStatus("Остановлено")
|
||||
setButtonsEnabled(true)
|
||||
binding.btnSummaryLite.setIconResource(R.drawable.bolt_24)
|
||||
animateButtonTint(binding.btnSummaryLite,
|
||||
com.google.android.material.color.MaterialColors.getColor(this, com.google.android.material.R.attr.colorSecondaryContainer, 0)
|
||||
)
|
||||
hideLoading()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -354,10 +403,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
||||
}
|
||||
isFabOperationInProgress = true
|
||||
|
||||
// Блокируем все кнопки сразу при нажатии
|
||||
setButtonsEnabled(false)
|
||||
|
||||
lifecycleScope.launch {
|
||||
liteActionJob = lifecycleScope.launch {
|
||||
try {
|
||||
if (mainViewModel.isRunning.value == true) {
|
||||
V2RayServiceManager.stopVService(this@MainActivity)
|
||||
@@ -366,33 +412,45 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
||||
|
||||
showStatus("Обновление профилей...")
|
||||
showLoading()
|
||||
// Иконка молнии → стоп пока идёт обновление; FAB и меню блокируем
|
||||
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
|
||||
mainViewModel.suppressPinSelected = true
|
||||
|
||||
launch(Dispatchers.IO) {
|
||||
val result = mainViewModel.updateConfigViaSubAll()
|
||||
val removed = mainViewModel.removeDuplicateByIpAll()
|
||||
withContext(Dispatchers.Main) {
|
||||
mainViewModel.reloadServerList()
|
||||
if (result.configCount > 0) {
|
||||
val status = if (removed > 0)
|
||||
"Обновлено ${result.configCount} профилей, удалено $removed дубл. IP. Запуск теста..."
|
||||
else
|
||||
"Обновлено ${result.configCount} профилей. Запуск теста..."
|
||||
showStatus(status)
|
||||
} else {
|
||||
showStatus("Запуск теста...")
|
||||
}
|
||||
hideLoading()
|
||||
val result = withContext(Dispatchers.IO) { mainViewModel.updateConfigViaSubAll() }
|
||||
val removed = withContext(Dispatchers.IO) { mainViewModel.removeDuplicateByIpAll() }
|
||||
|
||||
showStatus("Выполняется замер задержки. Ожидаем завершения...")
|
||||
mainViewModel.testAllRealPing()
|
||||
}
|
||||
mainViewModel.reloadServerList()
|
||||
if (result.configCount > 0) {
|
||||
val status = if (removed > 0)
|
||||
"Обновлено ${result.configCount} профилей, удалено $removed дубл. IP. Запуск теста..."
|
||||
else
|
||||
"Обновлено ${result.configCount} профилей. Запуск теста..."
|
||||
showStatus(status)
|
||||
} else {
|
||||
showStatus("Запуск теста...")
|
||||
}
|
||||
delay(1500)
|
||||
hideLoading()
|
||||
|
||||
showStatus("Выполняется замер задержки. Ожидаем завершения...")
|
||||
mainViewModel.testAllRealPing()
|
||||
} catch (e: kotlinx.coroutines.CancellationException) {
|
||||
// Пользователь нажал стоп — уже обработано выше
|
||||
} catch (e: Exception) {
|
||||
Log.e(AppConfig.TAG, "Error in handleLiteAction", e)
|
||||
isLiteTesting = false
|
||||
hideLoading()
|
||||
} finally {
|
||||
isFabOperationInProgress = false
|
||||
liteActionJob = null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -423,6 +481,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
||||
return
|
||||
}
|
||||
isFabOperationInProgress = true
|
||||
applyRunningState(isLoading = true, isRunning = false)
|
||||
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
@@ -430,12 +489,18 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
||||
V2RayServiceManager.stopVService(this@MainActivity)
|
||||
delay(1000)
|
||||
}
|
||||
if (MmkvManager.getSelectServer().isNullOrEmpty()) {
|
||||
// Сервер был снят с выбора — просто остановились, разблокируем UI
|
||||
applyRunningState(isLoading = false, isRunning = false)
|
||||
return@launch
|
||||
}
|
||||
startV2Ray()
|
||||
delay(1000)
|
||||
} catch (e: Exception) {
|
||||
Log.e(AppConfig.TAG, "Error in restartV2Ray", e)
|
||||
} finally {
|
||||
isFabOperationInProgress = false
|
||||
applyRunningState(isLoading = false, isRunning = mainViewModel.isRunning.value == true)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -450,6 +515,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
||||
private fun showStatus(message: String) {
|
||||
statusResetJob?.cancel()
|
||||
binding.tvTestState.text = message
|
||||
if (isFabOperationInProgress || mainViewModel.isTesting.value == true) return
|
||||
statusResetJob = lifecycleScope.launch {
|
||||
delay(3000)
|
||||
val isRunning = mainViewModel.isRunning.value == true
|
||||
@@ -472,34 +538,102 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
||||
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) {
|
||||
val secContainer = ColorStateList.valueOf(
|
||||
com.google.android.material.color.MaterialColors.getColor(this, com.google.android.material.R.attr.colorSecondaryContainer, 0)
|
||||
)
|
||||
if (isLoading) {
|
||||
// Идёт процесс подключения/отключения — блокируем всё
|
||||
setButtonsEnabled(false)
|
||||
binding.fab.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(this, R.color.color_fab_inactive))
|
||||
// Во время подключения: только FAB доступен для отмены, всё остальное заблокировано
|
||||
binding.fab.isEnabled = true
|
||||
binding.fab.alpha = 1.0f
|
||||
binding.fab.backgroundTintList = secContainer
|
||||
binding.btnSummaryLite.isEnabled = false
|
||||
binding.btnSummaryLite.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 }
|
||||
setStatusDot(DotState.LOADING)
|
||||
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) {
|
||||
// Подключены: только FAB (отключить) активен, остальное блокируем
|
||||
setSecondaryButtonsEnabled(false)
|
||||
binding.fab.isEnabled = true
|
||||
binding.fab.alpha = 1.0f
|
||||
binding.fab.backgroundTintList = accentColor()
|
||||
binding.btnSummaryLite.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(this, R.color.color_fab_inactive))
|
||||
binding.fab.iconTint = onPrimary
|
||||
animateButtonTint(binding.btnSummaryLite, secContainer.defaultColor)
|
||||
binding.fab.contentDescription = getString(R.string.action_stop_service)
|
||||
setTestState(getString(R.string.connection_connected))
|
||||
binding.layoutTest.isFocusable = true
|
||||
setStatusDot(DotState.CONNECTED)
|
||||
} else {
|
||||
setButtonsEnabled(true)
|
||||
binding.fab.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(this, R.color.color_fab_inactive))
|
||||
binding.btnSummaryLite.backgroundTintList = ColorStateList.valueOf(ContextCompat.getColor(this, R.color.color_fab_inactive))
|
||||
binding.fab.backgroundTintList = secContainer
|
||||
binding.fab.iconTint = onSecContainer
|
||||
animateButtonTint(binding.btnSummaryLite, secContainer.defaultColor)
|
||||
binding.fab.contentDescription = getString(R.string.tasker_start_service)
|
||||
setTestState(getString(R.string.connection_not_connected))
|
||||
if (mainViewModel.isTesting.value != true && statusResetJob?.isActive != true) {
|
||||
setTestState(getString(R.string.connection_not_connected))
|
||||
}
|
||||
binding.layoutTest.isFocusable = false
|
||||
setStatusDot(DotState.IDLE)
|
||||
}
|
||||
}
|
||||
|
||||
private enum class DotState { IDLE, CONNECTED, LOADING, FAILURE }
|
||||
|
||||
private fun setStatusDot(state: DotState) {
|
||||
val dot = binding.statusDot
|
||||
dot.animate().cancel()
|
||||
dot.alpha = 1f; dot.scaleX = 1f; dot.scaleY = 1f
|
||||
dot.backgroundTintList = ColorStateList.valueOf(when (state) {
|
||||
DotState.CONNECTED -> ContextCompat.getColor(this, R.color.status_connected)
|
||||
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)
|
||||
})
|
||||
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() {
|
||||
super.onResume()
|
||||
MessageUtil.sendMsg2Service(this, AppConfig.MSG_REGISTER_CLIENT, "")
|
||||
@@ -535,6 +669,8 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
||||
|
||||
searchItem.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
|
||||
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
|
||||
searchView.alpha = 0f
|
||||
searchView.animate().alpha(1f).setDuration(220).start()
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -557,7 +693,7 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
||||
}
|
||||
|
||||
R.id.sub_update -> {
|
||||
setButtonsEnabled(false)
|
||||
setSecondaryButtonsEnabled(false)
|
||||
importConfigViaSub()
|
||||
true
|
||||
}
|
||||
@@ -747,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() {
|
||||
MaterialAlertDialogBuilder(this).setMessage(R.string.del_config_comfirm)
|
||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
@@ -909,27 +1113,39 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
||||
}
|
||||
|
||||
val codes = allCountriesMap.keys.toTypedArray()
|
||||
val labels = allCountriesMap.values.toTypedArray()
|
||||
|
||||
// In exclude mode: checked = should be EXCLUDED
|
||||
// currentFilter stores included set (empty = show all)
|
||||
// Convert to excluded set for UI
|
||||
val allCodes = codes.toSet()
|
||||
val excludedByFilter = if (currentFilter.isEmpty()) emptySet()
|
||||
else allCodes - currentFilter
|
||||
// currentFilter stores excluded set (empty = show all)
|
||||
val checked = BooleanArray(codes.size) { codes[it] in 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)
|
||||
.setTitle("Исключить страны")
|
||||
.setMultiChoiceItems(labels, checked) { _, which, isChecked ->
|
||||
checked[which] = isChecked
|
||||
}
|
||||
.setAdapter(adapter, null)
|
||||
.setPositiveButton("Применить") { _, _ ->
|
||||
val excluded = codes.filterIndexed { i, _ -> checked[i] }.toSet()
|
||||
val included = if (excluded.isEmpty()) emptySet()
|
||||
else allCodes - excluded
|
||||
mainViewModel.applyCountryFilter(included)
|
||||
mainViewModel.applyCountryFilter(excluded)
|
||||
val msg = if (excluded.isEmpty()) "Показаны все страны"
|
||||
else "Скрыто: ${excluded.joinToString { CountryDetector.codeToFlag(it) }}"
|
||||
showStatus(msg)
|
||||
@@ -939,8 +1155,8 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
||||
showStatus("Показаны все страны")
|
||||
}
|
||||
.create()
|
||||
dialog.show()
|
||||
dialog.setCustomTitle(buildDialogTitleWithClose("Исключить страны") { dialog.dismiss() })
|
||||
dialog.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -966,7 +1182,6 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
||||
val message = result.releaseNotes?.let { xyz.zarazaex.olc.util.MarkdownUtil.parseBasic(it) } ?: ""
|
||||
val titleStr = getString(R.string.update_new_version_found, result.latestVersion)
|
||||
val dialog = MaterialAlertDialogBuilder(this)
|
||||
.setTitle(titleStr)
|
||||
.setMessage(message)
|
||||
.setPositiveButton(R.string.update_now) { _, _ ->
|
||||
result.downloadUrl?.let {
|
||||
@@ -974,8 +1189,8 @@ class MainActivity : HelperBaseActivity(), NavigationView.OnNavigationItemSelect
|
||||
}
|
||||
}
|
||||
.create()
|
||||
dialog.show()
|
||||
dialog.setCustomTitle(buildDialogTitleWithClose(titleStr) { dialog.dismiss() })
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
private fun buildDialogTitleWithClose(title: String, onClose: () -> Unit): View {
|
||||
|
||||
@@ -7,6 +7,7 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import java.util.Collections
|
||||
import xyz.zarazaex.olc.AppConfig
|
||||
import xyz.zarazaex.olc.R
|
||||
@@ -35,7 +36,11 @@ class MainRecyclerAdapter(
|
||||
MmkvManager.decodeSettingsBool(AppConfig.PREF_DOUBLE_COLUMN_DISPLAY, false)
|
||||
private val showCopyButton =
|
||||
MmkvManager.decodeSettingsBool(AppConfig.PREF_SHOW_COPY_BUTTON, false)
|
||||
private val showServerIp =
|
||||
MmkvManager.decodeSettingsBool(AppConfig.PREF_SHOW_SERVER_IP, false)
|
||||
private var data: MutableList<ServersCache> = mutableListOf()
|
||||
private var minReachablePing: Long? = null
|
||||
private var maxReachablePing: Long? = null
|
||||
private var recyclerView: RecyclerView? = null
|
||||
|
||||
override fun onAttachedToRecyclerView(rv: RecyclerView) {
|
||||
@@ -54,58 +59,51 @@ class MainRecyclerAdapter(
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun setData(newData: MutableList<ServersCache>?, position: Int = -1) {
|
||||
val parsedNewData = newData?.toList() ?: emptyList()
|
||||
fun setData(newData: List<ServersCache>) {
|
||||
val oldData = data
|
||||
val parsedNewData = newData
|
||||
|
||||
if (data.isEmpty() || parsedNewData.isEmpty() || position >= 0) {
|
||||
if (oldData.isEmpty() || parsedNewData.isEmpty()) {
|
||||
data = parsedNewData.toMutableList()
|
||||
if (position >= 0 && position in data.indices) {
|
||||
notifyItemChanged(position)
|
||||
} else {
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
recomputePingRange()
|
||||
notifyDataSetChanged()
|
||||
return
|
||||
}
|
||||
|
||||
val oldData = data
|
||||
val lm = recyclerView?.layoutManager as? androidx.recyclerview.widget.LinearLayoutManager
|
||||
val firstVisible = lm?.findFirstVisibleItemPosition()?.coerceAtLeast(0) ?: 0
|
||||
val isAtTop = firstVisible == 0 && (lm?.findViewByPosition(0)?.top ?: 0) >= 0
|
||||
val firstVisibleGuid = if (!isAtTop) oldData.getOrNull(firstVisible)?.guid else null
|
||||
|
||||
val diffResult =
|
||||
androidx.recyclerview.widget.DiffUtil.calculateDiff(
|
||||
object : androidx.recyclerview.widget.DiffUtil.Callback() {
|
||||
override fun getOldListSize() = oldData.size
|
||||
override fun getNewListSize() = parsedNewData.size
|
||||
val diffResult = androidx.recyclerview.widget.DiffUtil.calculateDiff(
|
||||
object : androidx.recyclerview.widget.DiffUtil.Callback() {
|
||||
override fun getOldListSize() = oldData.size
|
||||
override fun getNewListSize() = parsedNewData.size
|
||||
|
||||
override fun areItemsTheSame(oldPos: Int, newPos: Int): Boolean {
|
||||
return oldData[oldPos].guid == parsedNewData[newPos].guid
|
||||
}
|
||||
override fun areItemsTheSame(oldPos: Int, newPos: Int) =
|
||||
oldData[oldPos].guid == parsedNewData[newPos].guid
|
||||
|
||||
override fun areContentsTheSame(oldPos: Int, newPos: Int): Boolean {
|
||||
val oldProfile = oldData[oldPos].profile
|
||||
val newProfile = parsedNewData[newPos].profile
|
||||
val oldGuid = oldData[oldPos].guid
|
||||
val newGuid = parsedNewData[newPos].guid
|
||||
return oldProfile == newProfile &&
|
||||
oldProfile.isFavorite == newProfile.isFavorite &&
|
||||
(oldGuid == MmkvManager.getSelectServer()) == (newGuid == MmkvManager.getSelectServer()) &&
|
||||
MmkvManager.decodeServerAffiliationInfo(oldGuid)?.testDelayMillis ==
|
||||
MmkvManager.decodeServerAffiliationInfo(newGuid)?.testDelayMillis
|
||||
}
|
||||
override fun areContentsTheSame(oldPos: Int, newPos: Int): Boolean {
|
||||
val old = oldData[oldPos]
|
||||
val new = parsedNewData[newPos]
|
||||
return old.profile == new.profile &&
|
||||
old.profile.isFavorite == new.profile.isFavorite &&
|
||||
old.isSelected == new.isSelected &&
|
||||
old.testDelayMillis == new.testDelayMillis
|
||||
}
|
||||
|
||||
override fun getChangePayload(oldPos: Int, newPos: Int): Any? {
|
||||
if (oldData[oldPos].profile.isFavorite != parsedNewData[newPos].profile.isFavorite) {
|
||||
return PAYLOAD_FAVORITE
|
||||
}
|
||||
return super.getChangePayload(oldPos, newPos)
|
||||
}
|
||||
},
|
||||
true
|
||||
)
|
||||
override fun getChangePayload(oldPos: Int, newPos: Int): Any? {
|
||||
if (oldData[oldPos].profile.isFavorite != parsedNewData[newPos].profile.isFavorite) {
|
||||
return PAYLOAD_FAVORITE
|
||||
}
|
||||
return super.getChangePayload(oldPos, newPos)
|
||||
}
|
||||
},
|
||||
true
|
||||
)
|
||||
|
||||
data = parsedNewData.toMutableList()
|
||||
recomputePingRange()
|
||||
diffResult.dispatchUpdatesTo(this)
|
||||
|
||||
if (isAtTop) {
|
||||
@@ -162,26 +160,35 @@ class MainRecyclerAdapter(
|
||||
|
||||
// Name address
|
||||
holder.itemMainBinding.tvName.text = profile.remarks
|
||||
holder.itemMainBinding.tvStatistics.text = getAddress(profile)
|
||||
val addressText = getAddress(profile)
|
||||
holder.itemMainBinding.tvStatistics.text = addressText
|
||||
holder.itemMainBinding.tvStatistics.visibility =
|
||||
if (addressText.isEmpty()) View.GONE else View.VISIBLE
|
||||
|
||||
// TestResult
|
||||
val aff = MmkvManager.decodeServerAffiliationInfo(guid)
|
||||
holder.itemMainBinding.tvTestResult.text = aff?.getTestDelayString().orEmpty()
|
||||
if ((aff?.testDelayMillis ?: 0L) < 0L) {
|
||||
holder.itemMainBinding.tvTestResult.setTextColor(
|
||||
ContextCompat.getColor(context, R.color.colorPingRed)
|
||||
)
|
||||
} else {
|
||||
holder.itemMainBinding.tvTestResult.setTextColor(
|
||||
ContextCompat.getColor(context, R.color.colorPing)
|
||||
)
|
||||
val delayMillis = data[position].testDelayMillis
|
||||
holder.itemMainBinding.tvTestResult.text = when {
|
||||
delayMillis == 0L -> "—ms"
|
||||
delayMillis < 0L -> "-ms"
|
||||
else -> "${delayMillis}ms"
|
||||
}
|
||||
holder.itemMainBinding.tvTestResult.setTextColor(
|
||||
getPingColor(context, delayMillis)
|
||||
)
|
||||
(holder.itemMainBinding.tvTestResult.layoutParams as? ViewGroup.MarginLayoutParams)?.marginStart =
|
||||
if (addressText.isEmpty()) 0 else 6.dpToPx(context)
|
||||
|
||||
// layoutIndicator
|
||||
if (guid == MmkvManager.getSelectServer()) {
|
||||
holder.itemMainBinding.layoutIndicator.setBackgroundResource(R.color.colorIndicator)
|
||||
} else {
|
||||
holder.itemMainBinding.layoutIndicator.setBackgroundResource(0)
|
||||
val isSelected = data[position].isSelected
|
||||
holder.itemMainBinding.cardContainer.apply {
|
||||
val selectedColor = MaterialColors.getColor(
|
||||
context,
|
||||
com.google.android.material.R.attr.colorSurfaceContainerHigh,
|
||||
Color.TRANSPARENT
|
||||
)
|
||||
|
||||
setCardBackgroundColor(if (isSelected) selectedColor else Color.TRANSPARENT)
|
||||
strokeWidth = 0
|
||||
strokeColor = Color.TRANSPARENT
|
||||
}
|
||||
|
||||
// subscription remarks
|
||||
@@ -223,9 +230,50 @@ class MainRecyclerAdapter(
|
||||
* @return Formatted address string
|
||||
*/
|
||||
private fun getAddress(profile: ProfileItem): String {
|
||||
if (!showServerIp) {
|
||||
return ""
|
||||
}
|
||||
return AngConfigManager.generateDescription(profile)
|
||||
}
|
||||
|
||||
private fun getPingColor(context: android.content.Context, delayMillis: Long?): Int {
|
||||
val delay = delayMillis ?: return MaterialColors.getColor(
|
||||
context,
|
||||
com.google.android.material.R.attr.colorOnSurfaceVariant,
|
||||
ContextCompat.getColor(context, R.color.colorPing)
|
||||
)
|
||||
if (delay == 0L) {
|
||||
return MaterialColors.getColor(
|
||||
context,
|
||||
com.google.android.material.R.attr.colorOnSurfaceVariant,
|
||||
ContextCompat.getColor(context, R.color.colorPing)
|
||||
)
|
||||
}
|
||||
return when {
|
||||
delay < 0L -> ContextCompat.getColor(context, R.color.colorPingRed)
|
||||
minReachablePing == null || maxReachablePing == null -> ContextCompat.getColor(context, R.color.colorPingGood)
|
||||
minReachablePing == maxReachablePing -> ContextCompat.getColor(context, R.color.colorPingGood)
|
||||
else -> {
|
||||
val min = minReachablePing ?: delay
|
||||
val max = maxReachablePing ?: delay
|
||||
val relative = ((delay - min).toFloat() / (max - min).toFloat()).coerceIn(0f, 1f)
|
||||
when {
|
||||
relative <= 0.33f -> ContextCompat.getColor(context, R.color.colorPingGood)
|
||||
relative <= 0.66f -> ContextCompat.getColor(context, R.color.colorPingMedium)
|
||||
else -> ContextCompat.getColor(context, R.color.colorPingRed)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun recomputePingRange() {
|
||||
val delays = data.mapNotNull { item ->
|
||||
item.testDelayMillis.takeIf { it > 0L }
|
||||
}
|
||||
minReachablePing = delays.minOrNull()
|
||||
maxReachablePing = delays.maxOrNull()
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the subscription remarks information
|
||||
* @param profile The server configuration
|
||||
@@ -241,18 +289,8 @@ class MainRecyclerAdapter(
|
||||
return subRemarks?.toString() ?: ""
|
||||
}
|
||||
|
||||
fun removeServerSub(guid: String, position: Int) {
|
||||
val idx = data.indexOfFirst { it.guid == guid }
|
||||
if (idx >= 0) {
|
||||
data.removeAt(idx)
|
||||
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 {
|
||||
return (this * context.resources.displayMetrics.density).toInt()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder {
|
||||
@@ -301,6 +339,8 @@ class MainRecyclerAdapter(
|
||||
BaseViewHolder(itemFooterBinding.root)
|
||||
|
||||
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)
|
||||
if (fromPosition < data.size && toPosition < data.size) {
|
||||
Collections.swap(data, fromPosition, toPosition)
|
||||
|
||||
@@ -33,6 +33,9 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
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.withContext
|
||||
import java.util.Collections
|
||||
@@ -42,15 +45,24 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
private var serverList = mutableListOf<String>()
|
||||
var subscriptionId: String = MmkvManager.decodeSettingsString(AppConfig.CACHE_SUBSCRIPTION_ID, "").orEmpty()
|
||||
var keywordFilter = ""
|
||||
/** ISO codes to show (empty = show all) */
|
||||
/** ISO codes to EXCLUDE (empty = show all) */
|
||||
var countryFilter: Set<String> = MmkvManager.getCountryFilter()
|
||||
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 updateListAction by lazy { MutableLiveData<Int>() }
|
||||
val updateTestResultAction by lazy { MutableLiveData<String>() }
|
||||
val liteTestFinished = MutableLiveData<Boolean>()
|
||||
val isTesting by lazy { MutableLiveData<Boolean>().also { it.value = false } }
|
||||
var suppressPinSelected = false
|
||||
private val tcpingTestScope by lazy { CoroutineScope(Dispatchers.IO) }
|
||||
|
||||
/**
|
||||
@@ -120,8 +132,8 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
list
|
||||
}
|
||||
|
||||
if (!suppressPinSelected) pinSelectedGuidToTop(serverList)
|
||||
updateCache()
|
||||
updateListAction.value = -1
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -131,10 +143,11 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
fun removeServer(guid: String) {
|
||||
serverList.remove(guid)
|
||||
MmkvManager.removeServer(guid)
|
||||
val index = getPosition(guid)
|
||||
val index = _serversCache.indexOfFirst { it.guid == guid }
|
||||
if (index >= 0) {
|
||||
serversCache.removeAt(index)
|
||||
_serversCache.removeAt(index)
|
||||
}
|
||||
publishSnapshot()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -148,17 +161,18 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
}
|
||||
|
||||
Collections.swap(serverList, fromPosition, toPosition)
|
||||
Collections.swap(serversCache, fromPosition, toPosition)
|
||||
Collections.swap(_serversCache, fromPosition, toPosition)
|
||||
publishSnapshot()
|
||||
|
||||
MmkvManager.encodeServerList(serverList, subscriptionId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the cache of servers.
|
||||
* Rebuilds _serversCache from serverList and publishes a new snapshot to serverListFlow.
|
||||
*/
|
||||
@Synchronized
|
||||
fun updateCache() {
|
||||
serversCache.clear()
|
||||
_serversCache.clear()
|
||||
val kw = keywordFilter.trim()
|
||||
val searchRegex = try {
|
||||
if (kw.isNotEmpty()) Regex(kw, setOf(RegexOption.IGNORE_CASE)) else null
|
||||
@@ -166,17 +180,19 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
null
|
||||
}
|
||||
val activeCountryFilter = countryFilter
|
||||
val selectedGuid = MmkvManager.getSelectServer().orEmpty()
|
||||
for (guid in serverList) {
|
||||
val profile = MmkvManager.decodeServerConfig(guid) ?: continue
|
||||
|
||||
// Country filter
|
||||
if (activeCountryFilter.isNotEmpty()) {
|
||||
val code = CountryDetector.getCountryCode(profile.remarks, profile.server)
|
||||
if (code !in activeCountryFilter) continue
|
||||
val codes = CountryDetector.getCountryCodes(profile.remarks, profile.server)
|
||||
if (codes.all { it in activeCountryFilter }) continue
|
||||
}
|
||||
|
||||
val delay = MmkvManager.decodeServerAffiliationInfo(guid)?.testDelayMillis ?: 0L
|
||||
|
||||
if (kw.isEmpty()) {
|
||||
serversCache.add(ServersCache(guid, profile))
|
||||
_serversCache.add(ServersCache(guid, profile, delay, guid == selectedGuid))
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -189,15 +205,49 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
|| server.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. */
|
||||
fun applyCountryFilter(codes: Set<String>) {
|
||||
countryFilter = codes
|
||||
MmkvManager.setCountryFilter(codes)
|
||||
/** Emits an immutable copy of _serversCache to the Flow. Must be called on Main or from @Synchronized blocks. */
|
||||
private fun publishSnapshot() {
|
||||
_serverListFlow.value = _serversCache.toList()
|
||||
}
|
||||
|
||||
/** 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()
|
||||
}
|
||||
|
||||
@@ -210,11 +260,13 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
var hasUnknown = false
|
||||
for (guid in MmkvManager.decodeAllServerList()) {
|
||||
val profile = MmkvManager.decodeServerConfig(guid) ?: continue
|
||||
val code = CountryDetector.getCountryCode(profile.remarks, profile.server)
|
||||
if (code == CountryDetector.UNKNOWN) {
|
||||
hasUnknown = true
|
||||
} else {
|
||||
result[code] = "${CountryDetector.codeToFlag(code)} ${CountryDetector.codeToName(code)}"
|
||||
val codes = CountryDetector.getCountryCodes(profile.remarks, profile.server)
|
||||
for (code in codes) {
|
||||
if (code == CountryDetector.UNKNOWN) {
|
||||
hasUnknown = true
|
||||
} else {
|
||||
result[code] = "${CountryDetector.codeToFlag(code)} ${CountryDetector.codeToName(code)}"
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hasUnknown) {
|
||||
@@ -273,9 +325,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
fun exportAllServer(): Int {
|
||||
val serverListCopy =
|
||||
if (subscriptionId.isEmpty() && keywordFilter.isEmpty()) {
|
||||
serverList
|
||||
serverList.toList()
|
||||
} else {
|
||||
serversCache.map { it.guid }.toList()
|
||||
_serversCache.map { it.guid }
|
||||
}
|
||||
|
||||
val ret = AngConfigManager.shareNonCustomConfigsToClipboard(
|
||||
@@ -291,9 +343,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
fun testAllTcping() {
|
||||
tcpingTestScope.coroutineContext[Job]?.cancelChildren()
|
||||
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) {
|
||||
item.profile.let { outbound ->
|
||||
val serverAddress = outbound.server
|
||||
@@ -303,7 +355,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
val testResult = SpeedtestManager.tcping(serverAddress, serverPort.toInt())
|
||||
launch(Dispatchers.Main) {
|
||||
MmkvManager.encodeServerTestDelayMillis(item.guid, testResult)
|
||||
updateListAction.value = getPosition(item.guid)
|
||||
refreshPingInCache(listOf(item.guid))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -338,15 +390,17 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
if (removed > 0) {
|
||||
reloadServerList()
|
||||
}
|
||||
MmkvManager.clearAllTestDelayResults(serversCache.map { it.guid }.toList())
|
||||
updateListAction.value = -1
|
||||
if (!suppressPinSelected) {
|
||||
MmkvManager.clearAllTestDelayResults(_serversCache.map { it.guid })
|
||||
}
|
||||
publishSnapshot()
|
||||
isTesting.value = true
|
||||
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
if (serversCache.isEmpty()) {
|
||||
if (_serversCache.isEmpty()) {
|
||||
withContext(Dispatchers.Main) { reloadServerList() }
|
||||
}
|
||||
if (serversCache.isEmpty()) {
|
||||
if (_serversCache.isEmpty()) {
|
||||
withContext(Dispatchers.Main) { isTesting.value = false }
|
||||
return@launch
|
||||
}
|
||||
@@ -359,7 +413,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
key = AppConfig.MSG_MEASURE_CONFIG,
|
||||
subscriptionId = actualSubId,
|
||||
serverGuids = if (keywordFilter.isNotEmpty() || subscriptionId.startsWith("group_")) {
|
||||
serversCache.map { it.guid }
|
||||
_serversCache.map { it.guid }
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
@@ -471,9 +525,8 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
* @return The position of the server.
|
||||
*/
|
||||
fun getPosition(guid: String): Int {
|
||||
serversCache.forEachIndexed { index, it ->
|
||||
if (it.guid == guid)
|
||||
return index
|
||||
_serversCache.forEachIndexed { index, it ->
|
||||
if (it.guid == guid) return index
|
||||
}
|
||||
return -1
|
||||
}
|
||||
@@ -483,7 +536,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
* @return The number of removed servers.
|
||||
*/
|
||||
fun removeDuplicateServer(): Int {
|
||||
val serversCacheCopy = serversCache.toList().toMutableList()
|
||||
val serversCacheCopy = _serversCache.toList()
|
||||
val deleteServer = mutableListOf<String>()
|
||||
serversCacheCopy.forEachIndexed { index, sc ->
|
||||
val profile = sc.profile
|
||||
@@ -510,9 +563,8 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
*/
|
||||
fun removeDuplicateByIp(): Int {
|
||||
val selectedGuid = MmkvManager.getSelectServer()
|
||||
// Group all currently visible servers by their IP address
|
||||
val byIp = LinkedHashMap<String, MutableList<ServersCache>>()
|
||||
for (sc in serversCache) {
|
||||
for (sc in _serversCache) {
|
||||
val ip = sc.profile.server?.trim()?.lowercase() ?: continue
|
||||
byIp.getOrPut(ip) { mutableListOf() }.add(sc)
|
||||
}
|
||||
@@ -609,11 +661,11 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
if (subscriptionId.isEmpty() && keywordFilter.isEmpty()) {
|
||||
MmkvManager.removeAllServer()
|
||||
} else {
|
||||
val serversCopy = serversCache.toList()
|
||||
val serversCopy = _serversCache.toList()
|
||||
for (item in serversCopy) {
|
||||
MmkvManager.removeServer(item.guid)
|
||||
}
|
||||
serversCache.toList().count()
|
||||
serversCopy.count()
|
||||
}
|
||||
return count
|
||||
}
|
||||
@@ -625,19 +677,43 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
* 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).
|
||||
*/
|
||||
@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
|
||||
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 },
|
||||
{
|
||||
val delay = MmkvManager.decodeServerAffiliationInfo(it.guid)?.testDelayMillis ?: 0L
|
||||
val delay = it.testDelayMillis
|
||||
when {
|
||||
delay > 0L -> delay
|
||||
delay == 0L -> Long.MAX_VALUE - 1 // untested
|
||||
else -> Long.MAX_VALUE // failed
|
||||
delay == 0L -> Long.MAX_VALUE - 1
|
||||
else -> Long.MAX_VALUE
|
||||
}
|
||||
}
|
||||
))
|
||||
if (!suppressPinSelected) pinSelectedCacheItemToTop(_serversCache)
|
||||
publishSnapshot()
|
||||
}
|
||||
|
||||
fun sortByTestResults() {
|
||||
@@ -689,6 +765,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
val serversBySubId = allServerDelays.groupBy { it.subId }
|
||||
serversBySubId.forEach { (subId, servers) ->
|
||||
val sortedList = servers.map { it.guid }.toMutableList()
|
||||
pinSelectedGuidToTop(sortedList)
|
||||
MmkvManager.encodeServerList(sortedList, subId)
|
||||
}
|
||||
}
|
||||
@@ -716,11 +793,32 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
serverDelays.sortWith(compareBy({ !it.isFav }, { it.testDelayMillis }))
|
||||
|
||||
val sortedServerList = serverDelays.map { it.guid }.toMutableList()
|
||||
pinSelectedGuidToTop(sortedServerList)
|
||||
|
||||
// Save the sorted list for this subscription
|
||||
MmkvManager.encodeServerList(sortedServerList, subId)
|
||||
}
|
||||
|
||||
private fun pinSelectedGuidToTop(list: MutableList<String>) {
|
||||
val selectedGuid = MmkvManager.getSelectServer().orEmpty()
|
||||
if (selectedGuid.isEmpty()) return
|
||||
val index = list.indexOf(selectedGuid)
|
||||
if (index > 0) {
|
||||
list.removeAt(index)
|
||||
list.add(0, selectedGuid)
|
||||
}
|
||||
}
|
||||
|
||||
private fun pinSelectedCacheItemToTop(list: MutableList<ServersCache>) {
|
||||
val selectedGuid = MmkvManager.getSelectServer().orEmpty()
|
||||
if (selectedGuid.isEmpty()) return
|
||||
val index = list.indexOfFirst { it.guid == selectedGuid }
|
||||
if (index > 0) {
|
||||
val selectedItem = list.removeAt(index)
|
||||
list.add(0, selectedItem)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Initializes assets.
|
||||
@@ -772,7 +870,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
}
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
reloadServerList()
|
||||
reloadServerList() // rebuilds _serversCache + publishSnapshot
|
||||
isTesting.value = false
|
||||
liteTestFinished.value = true
|
||||
liteTestFinished.value = false
|
||||
@@ -804,14 +902,25 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
}
|
||||
|
||||
AppConfig.MSG_MEASURE_DELAY_SUCCESS -> {
|
||||
updateTestResultAction.value = intent.getStringExtra("content")
|
||||
val content = intent.getStringExtra("content")
|
||||
updateTestResultAction.value = content
|
||||
// Save ping for selected server so it shows in the list
|
||||
val guid = MmkvManager.getSelectServer()
|
||||
if (!guid.isNullOrEmpty() && content != null) {
|
||||
val ms = Regex("\\d+").find(content)?.value?.toLongOrNull()
|
||||
if (ms != null && ms > 0) {
|
||||
MmkvManager.encodeServerTestDelayMillis(guid, ms)
|
||||
refreshPingInCache(listOf(guid))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AppConfig.MSG_MEASURE_CONFIG_SUCCESS -> {
|
||||
val resultPair = intent.serializable<Pair<String, Long>>("content") ?: return
|
||||
MmkvManager.encodeServerTestDelayMillis(resultPair.first, resultPair.second)
|
||||
sortServersCacheInPlace()
|
||||
updateListAction.value = -1
|
||||
refreshPingInCache(listOf(resultPair.first))
|
||||
if (!suppressPinSelected) sortServersCacheInPlace()
|
||||
// publishSnapshot() already called inside refresh/sort above
|
||||
}
|
||||
|
||||
AppConfig.MSG_MEASURE_CONFIG_BATCH -> {
|
||||
@@ -819,8 +928,9 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
|
||||
update.results.forEach { result ->
|
||||
MmkvManager.encodeServerTestDelayMillis(result.guid, result.delay)
|
||||
}
|
||||
sortServersCacheInPlace()
|
||||
updateListAction.value = -1
|
||||
refreshPingInCache(update.results.map { it.guid })
|
||||
if (!suppressPinSelected) sortServersCacheInPlace()
|
||||
// publishSnapshot() already called inside refresh/sort above
|
||||
}
|
||||
|
||||
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>
|
||||
@@ -1,7 +1,7 @@
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
<solid android:color="@color/divider_color_light" />
|
||||
<solid android:color="?attr/colorSecondaryContainer" />
|
||||
<size
|
||||
android:width="48dp"
|
||||
android:height="48dp" />
|
||||
</shape>
|
||||
android:width="24dp"
|
||||
android:height="24dp" />
|
||||
</shape>
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:bottom="0dp"
|
||||
android:left="28dp"
|
||||
android:right="28dp"
|
||||
android:top="0dp">
|
||||
<shape>
|
||||
<size android:height="1dp" />
|
||||
<solid android:color="@color/server_list_divider" />
|
||||
</shape>
|
||||
</item>
|
||||
</layer-list>
|
||||
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
<solid android:color="#FF888888" />
|
||||
<size android:width="10dp" android:height="10dp" />
|
||||
</shape>
|
||||
@@ -1,171 +1,228 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<LinearLayout xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<LinearLayout
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="top"
|
||||
android:orientation="vertical">
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
app:cardBackgroundColor="?attr/colorSurfaceContainerLow"
|
||||
app:cardCornerRadius="16dp"
|
||||
app:cardElevation="0dp"
|
||||
app:strokeWidth="0dp">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_soure_ccode"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:gravity="center|start"
|
||||
android:orientation="horizontal"
|
||||
android:padding="@dimen/padding_spacing_dp16">
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="top"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="@dimen/image_size_dp24"
|
||||
android:layout_height="@dimen/image_size_dp24"
|
||||
android:importantForAccessibility="no"
|
||||
app:srcCompat="@drawable/ic_source_code_24dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_soure_ccode"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="@dimen/padding_spacing_dp16"
|
||||
android:text="@string/title_source_code"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:gravity="center|start"
|
||||
android:orientation="horizontal"
|
||||
android:padding="16dp">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:importantForAccessibility="no"
|
||||
app:srcCompat="@drawable/ic_source_code_24dp"
|
||||
app:tint="?attr/colorOnSurfaceVariant" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="16dp"
|
||||
android:text="@string/title_source_code"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyLarge" />
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:background="?attr/colorOutlineVariant" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_oss_licenses"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:gravity="center|start"
|
||||
android:orientation="horizontal"
|
||||
android:padding="16dp">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:importantForAccessibility="no"
|
||||
app:srcCompat="@drawable/license_24px"
|
||||
app:tint="?attr/colorOnSurfaceVariant" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="16dp"
|
||||
android:text="@string/title_oss_license"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyLarge" />
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:background="?attr/colorOutlineVariant" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_feedback"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:gravity="center|start"
|
||||
android:orientation="horizontal"
|
||||
android:padding="16dp">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:importantForAccessibility="no"
|
||||
app:srcCompat="@drawable/ic_feedback_24dp"
|
||||
app:tint="?attr/colorOnSurfaceVariant" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="16dp"
|
||||
android:text="@string/title_pref_feedback"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyLarge" />
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:background="?attr/colorOutlineVariant" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_tg_channel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:gravity="center|start"
|
||||
android:orientation="horizontal"
|
||||
android:padding="16dp">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:importantForAccessibility="no"
|
||||
app:srcCompat="@drawable/ic_telegram_24dp"
|
||||
app:tint="?attr/colorOnSurfaceVariant" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="16dp"
|
||||
android:text="@string/title_tg_channel"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyLarge" />
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:background="?attr/colorOutlineVariant" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_privacy_policy"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:gravity="center|start"
|
||||
android:orientation="horizontal"
|
||||
android:padding="16dp">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:importantForAccessibility="no"
|
||||
app:srcCompat="@drawable/ic_privacy_24dp"
|
||||
app:tint="?attr/colorOnSurfaceVariant" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="16dp"
|
||||
android:text="@string/title_privacy_policy"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyLarge" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_oss_licenses"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:gravity="center|start"
|
||||
android:orientation="horizontal"
|
||||
android:padding="@dimen/padding_spacing_dp16">
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<ImageView
|
||||
android:layout_width="@dimen/image_size_dp24"
|
||||
android:layout_height="@dimen/image_size_dp24"
|
||||
android:importantForAccessibility="no"
|
||||
app:srcCompat="@drawable/license_24px" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="@dimen/padding_spacing_dp16"
|
||||
android:text="@string/title_oss_license"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_feedback"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:gravity="center|start"
|
||||
android:orientation="horizontal"
|
||||
android:padding="@dimen/padding_spacing_dp16">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="@dimen/image_size_dp24"
|
||||
android:layout_height="@dimen/image_size_dp24"
|
||||
android:importantForAccessibility="no"
|
||||
app:srcCompat="@drawable/ic_feedback_24dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="@dimen/padding_spacing_dp16"
|
||||
android:text="@string/title_pref_feedback"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_tg_channel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:gravity="center|start"
|
||||
android:orientation="horizontal"
|
||||
android:padding="@dimen/padding_spacing_dp16">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="@dimen/image_size_dp24"
|
||||
android:layout_height="@dimen/image_size_dp24"
|
||||
android:importantForAccessibility="no"
|
||||
app:srcCompat="@drawable/ic_telegram_24dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="@dimen/padding_spacing_dp16"
|
||||
android:text="@string/title_tg_channel"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_privacy_policy"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:gravity="center|start"
|
||||
android:orientation="horizontal"
|
||||
android:padding="@dimen/padding_spacing_dp16">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="@dimen/image_size_dp24"
|
||||
android:layout_height="@dimen/image_size_dp24"
|
||||
android:importantForAccessibility="no"
|
||||
app:srcCompat="@drawable/ic_privacy_24dp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="@dimen/padding_spacing_dp16"
|
||||
android:text="@string/title_privacy_policy"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
|
||||
</LinearLayout>
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:cardBackgroundColor="?attr/colorSurfaceContainerLowest"
|
||||
app:cardCornerRadius="16dp"
|
||||
app:cardElevation="0dp"
|
||||
app:strokeWidth="0dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/padding_spacing_dp16">
|
||||
android:padding="20dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_version"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/title_about"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small" />
|
||||
android:textAppearance="@style/TextAppearance.Material3.LabelMedium"
|
||||
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_app_id"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:text="@string/title_about"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small" />
|
||||
android:textAppearance="@style/TextAppearance.Material3.LabelSmall"
|
||||
android:textColor="?attr/colorOutline" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
|
||||
@@ -5,33 +5,41 @@
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/app_bar"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/colorSurface"
|
||||
app:elevation="0dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:titleTextAppearance="@style/TextAppearance.AppCompat.Title" />
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
app:titleTextAppearance="@style/TextAppearance.Material3.TitleLarge" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
android:id="@+id/progress_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toBottomOf="@id/toolbar"
|
||||
app:layout_constraintTop_toBottomOf="@id/app_bar"
|
||||
android:indeterminate="true"
|
||||
android:visibility="gone"
|
||||
app:indicatorColor="@color/color_fab_active" />
|
||||
app:indicatorColor="?attr/colorPrimary"
|
||||
app:trackColor="?attr/colorSurfaceVariant" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/content_container"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/toolbar"
|
||||
app:layout_constraintTop_toBottomOf="@id/app_bar"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent">
|
||||
|
||||
</FrameLayout>
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
android:id="@+id/drawer_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/md_theme_surface">
|
||||
android:background="?attr/colorSurface">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
@@ -15,16 +15,17 @@
|
||||
android:id="@+id/app_bar_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/md_theme_surface"
|
||||
app:elevation="0dp">
|
||||
android:background="?attr/colorSurface"
|
||||
app:elevation="0dp"
|
||||
app:liftOnScroll="false">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
app:title="@string/app_name"
|
||||
app:titleCentered="true"
|
||||
app:titleTextColor="?attr/colorOnSurface" />
|
||||
app:titleTextColor="?attr/colorOnSurface"
|
||||
app:titleTextAppearance="@style/TextAppearance.Material3.TitleLarge" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
@@ -61,7 +62,7 @@
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="@color/divider_color_light" />
|
||||
android:background="?attr/colorOutlineVariant" />
|
||||
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/view_pager"
|
||||
@@ -71,16 +72,18 @@
|
||||
android:scrollbars="vertical"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<!-- Bottom container: tab + action card -->
|
||||
<LinearLayout
|
||||
android:id="@+id/bottom_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
android:orientation="vertical"
|
||||
android:background="?attr/colorSurfaceContainer">
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:background="@color/divider_color_light" />
|
||||
android:background="?attr/colorOutlineVariant" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/tab_slot_bottom"
|
||||
@@ -88,106 +91,132 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/tab_group"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:tabIndicatorFullWidth="true"
|
||||
app:tabMode="fixed"
|
||||
app:tabGravity="fill"
|
||||
app:tabMaxWidth="0dp"
|
||||
app:tabRippleColor="@android:color/transparent"
|
||||
app:tabTextAppearance="@style/TabLayoutTextStyle" />
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/tab_group"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="56dp"
|
||||
android:minHeight="56dp"
|
||||
android:background="?attr/colorSurface"
|
||||
app:tabIndicatorFullWidth="true"
|
||||
app:tabIndicatorHeight="3dp"
|
||||
app:tabMode="fixed"
|
||||
app:tabGravity="fill"
|
||||
app:tabMaxWidth="0dp"
|
||||
app:tabRippleColor="@android:color/transparent"
|
||||
app:tabIndicatorColor="?attr/colorPrimary"
|
||||
app:tabSelectedTextColor="?attr/colorPrimary"
|
||||
app:tabTextColor="?attr/colorOnSurfaceVariant"
|
||||
app:tabTextAppearance="@style/TabLayoutTextStyle" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
<!-- Bottom action row -->
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/btn_summary_lite"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:src="@drawable/bolt_24"
|
||||
android:stateListAnimator="@null"
|
||||
app:tint="@color/colorWhite"
|
||||
app:backgroundTint="@color/color_fab_inactive"
|
||||
app:rippleColor="#33FFFFFF"
|
||||
app:fabSize="normal"
|
||||
app:maxImageSize="28dp"
|
||||
app:elevation="0dp"
|
||||
app:pressedTranslationZ="0dp"
|
||||
app:hoveredFocusedTranslationZ="0dp" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/fab"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/tasker_start_service"
|
||||
android:focusable="true"
|
||||
android:nextFocusLeft="@+id/layout_test"
|
||||
android:src="@drawable/shield_24"
|
||||
android:stateListAnimator="@null"
|
||||
app:tint="@color/colorWhite"
|
||||
app:rippleColor="#33FFFFFF"
|
||||
app:fabSize="normal"
|
||||
app:maxImageSize="28dp"
|
||||
app:elevation="0dp"
|
||||
app:pressedTranslationZ="0dp"
|
||||
app:hoveredFocusedTranslationZ="0dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_test"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/connection_test_pending"
|
||||
android:focusable="true"
|
||||
android:nextFocusLeft="@+id/view_pager"
|
||||
android:nextFocusRight="@+id/fab"
|
||||
android:orientation="vertical"
|
||||
android:gravity="center"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingBottom="12dp"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp">
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingBottom="12dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_test_state"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:maxLines="2"
|
||||
android:minLines="1"
|
||||
android:text="@string/connection_test_pending"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small" />
|
||||
<!-- Bolt icon button -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn_summary_lite"
|
||||
style="@style/Widget.ActionSquareButton"
|
||||
android:layout_width="52dp"
|
||||
android:layout_height="52dp"
|
||||
app:icon="@drawable/bolt_24"
|
||||
app:iconSize="22dp"
|
||||
app:backgroundTint="?attr/colorSecondaryContainer"
|
||||
app:iconTint="?attr/colorOnSecondaryContainer"
|
||||
app:rippleColor="?attr/colorOnSecondaryContainer"
|
||||
app:layout_constraintEnd_toStartOf="@+id/fab"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:layout_marginEnd="8dp" />
|
||||
|
||||
<!-- Connect / Disconnect icon button -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/fab"
|
||||
style="@style/Widget.ActionSquareButton"
|
||||
android:layout_width="52dp"
|
||||
android:layout_height="52dp"
|
||||
android:contentDescription="@string/tasker_start_service"
|
||||
app:icon="@drawable/shield_24"
|
||||
app:iconSize="26dp"
|
||||
app:backgroundTint="?attr/colorSecondaryContainer"
|
||||
app:iconTint="?attr/colorOnSecondaryContainer"
|
||||
app:rippleColor="?attr/colorOnSecondaryContainer"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent" />
|
||||
|
||||
<!-- Status pill: dot + text, tappable for ping -->
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/layout_test"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="52dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:contentDescription="@string/connection_test_pending"
|
||||
app:cardBackgroundColor="?attr/colorSurfaceContainerHigh"
|
||||
app:cardCornerRadius="26dp"
|
||||
app:cardElevation="0dp"
|
||||
app:strokeWidth="0dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/btn_summary_lite"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="12dp">
|
||||
|
||||
<View
|
||||
android:id="@+id/status_dot"
|
||||
android:layout_width="10dp"
|
||||
android:layout_height="10dp"
|
||||
android:background="@drawable/status_dot"
|
||||
android:layout_marginEnd="10dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_test_state"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
android:text="@string/connection_test_pending"
|
||||
android:textAppearance="@style/TextAppearance.Material3.LabelLarge"
|
||||
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
</RelativeLayout>
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Navigation Drawer -->
|
||||
<LinearLayout
|
||||
android:id="@+id/drawer_content_layout"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="280dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="start"
|
||||
android:background="?android:attr/windowBackground"
|
||||
android:background="?attr/colorSurfaceContainerLow"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.navigation.NavigationView
|
||||
@@ -195,6 +224,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:background="@android:color/transparent"
|
||||
app:headerLayout="@layout/nav_header" />
|
||||
|
||||
<LinearLayout
|
||||
@@ -203,61 +233,65 @@
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/drawer_settings"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="start|center_vertical"
|
||||
android:drawablePadding="12dp"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingBottom="12dp"
|
||||
android:text="@string/title_settings"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true" />
|
||||
android:textAppearance="@style/TextAppearance.Material3.LabelLarge"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
app:iconGravity="start"
|
||||
app:iconTint="?attr/colorOnSurfaceVariant" />
|
||||
|
||||
<TextView
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/drawer_per_app"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="start|center_vertical"
|
||||
android:drawablePadding="12dp"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingBottom="12dp"
|
||||
android:text="@string/per_app_proxy_settings"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true" />
|
||||
android:textAppearance="@style/TextAppearance.Material3.LabelLarge"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
app:iconGravity="start"
|
||||
app:iconTint="?attr/colorOnSurfaceVariant" />
|
||||
|
||||
<TextView
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/drawer_check_update"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="start|center_vertical"
|
||||
android:drawablePadding="12dp"
|
||||
android:gravity="center_vertical"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingBottom="16dp"
|
||||
android:text="@string/update_check_for_update"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true" />
|
||||
android:textAppearance="@style/TextAppearance.Material3.LabelLarge"
|
||||
android:textColor="?attr/colorOnSurface"
|
||||
app:iconGravity="start"
|
||||
app:iconTint="?attr/colorOnSurfaceVariant" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:background="@color/divider_color_light" />
|
||||
android:background="?attr/colorOutlineVariant" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_forked"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/drawer_forked_text"
|
||||
android:textColor="#9E9E9E"
|
||||
android:textColorLink="#9E9E9E"
|
||||
android:textColor="?attr/colorOnSurfaceVariant"
|
||||
android:textColorLink="?attr/colorPrimary"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
|
||||
android:linksClickable="true"
|
||||
android:textIsSelectable="true" />
|
||||
|
||||
@@ -267,8 +301,9 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="@string/drawer_developed_text"
|
||||
android:textColor="#9E9E9E"
|
||||
android:textColorLink="#9E9E9E"
|
||||
android:textColor="?attr/colorOnSurfaceVariant"
|
||||
android:textColorLink="?attr/colorPrimary"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
|
||||
android:linksClickable="true"
|
||||
android:textIsSelectable="true" />
|
||||
</LinearLayout>
|
||||
|
||||
@@ -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"?>
|
||||
<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_height="wrap_content"
|
||||
android:paddingStart="24dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:paddingTop="20dp"
|
||||
android:paddingBottom="12dp">
|
||||
android:paddingTop="16dp"
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/dialog_title_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_toStartOf="@+id/dialog_close_btn"
|
||||
android:textAppearance="@style/TextAppearance.Material3.HeadlineSmall"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:gravity="start"
|
||||
android:paddingEnd="32dp"
|
||||
android:textAppearance="@style/TextAppearance.Material3.TitleMedium"
|
||||
android:textColor="?attr/colorOnSurface" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/dialog_close_btn"
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_gravity="end|center_vertical"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="Закрыть"
|
||||
android:src="@drawable/ic_close_24dp"
|
||||
android:tint="?attr/colorOnSurfaceVariant" />
|
||||
|
||||
</RelativeLayout>
|
||||
</FrameLayout>
|
||||
|
||||
@@ -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:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:button="@drawable/checkbox_round_selector"
|
||||
android:clickable="false"
|
||||
android:focusable="false"
|
||||
android:padding="@dimen/padding_spacing_dp8" />
|
||||
|
||||
@@ -1,44 +1,46 @@
|
||||
<?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"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/item_bg"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingTop="6dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:paddingBottom="6dp"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/info_container"
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/card_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:gravity="center"
|
||||
android:nextFocusRight="@+id/layout_share"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="@dimen/padding_spacing_dp4"
|
||||
android:paddingTop="@dimen/padding_spacing_dp8"
|
||||
android:paddingEnd="@dimen/padding_spacing_dp4"
|
||||
android:paddingBottom="@dimen/padding_spacing_dp8">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_indicator"
|
||||
android:layout_width="@dimen/padding_spacing_dp4"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:orientation="vertical" />
|
||||
app:cardBackgroundColor="@android:color/transparent"
|
||||
app:cardCornerRadius="14dp"
|
||||
app:cardElevation="0dp"
|
||||
app:strokeWidth="0dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:id="@+id/info_container"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
android:layout_weight="1"
|
||||
android:layout_gravity="center"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:gravity="center_vertical"
|
||||
android:nextFocusRight="@+id/layout_share"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="0dp"
|
||||
android:paddingTop="13dp"
|
||||
android:paddingEnd="4dp"
|
||||
android:paddingBottom="13dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
@@ -47,51 +49,78 @@
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="@dimen/padding_spacing_dp8">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:maxLines="2"
|
||||
android:minLines="1"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
|
||||
android:orientation="horizontal">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingTop="@dimen/padding_spacing_dp8">
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/layout_subscription"
|
||||
android:layout_width="@dimen/image_size_dp24"
|
||||
android:layout_height="@dimen/image_size_dp24"
|
||||
android:layout_gravity="bottom"
|
||||
android:layout_marginEnd="@dimen/padding_spacing_dp4"
|
||||
android:background="@drawable/ic_circle">
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="12dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_subscription"
|
||||
android:id="@+id/tv_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
||||
android:textSize="11sp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
android:maxLines="2"
|
||||
android:minLines="1"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyLarge"
|
||||
android:textColor="?attr/colorOnSurface" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_statistics"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:lines="1"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small" />
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingTop="5dp">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/layout_subscription"
|
||||
android:layout_width="18dp"
|
||||
android:layout_height="18dp"
|
||||
android:layout_gravity="bottom"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:background="@drawable/ic_circle">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_subscription"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@style/TextAppearance.Material3.LabelSmall"
|
||||
android:textSize="10sp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_statistics"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:lines="1"
|
||||
android:textAppearance="@style/TextAppearance.Material3.LabelSmall"
|
||||
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_test_result"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="6dp"
|
||||
android:lines="1"
|
||||
android:textAppearance="@style/TextAppearance.Material3.LabelSmall"
|
||||
android:textColor="@color/colorPing"
|
||||
android:textSize="11sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -99,65 +128,41 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:gravity="end">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_favorite"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:padding="@dimen/padding_spacing_dp8"
|
||||
android:src="@drawable/kid_star_outline_24" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_copy"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:padding="@dimen/padding_spacing_dp8"
|
||||
android:visibility="gone"
|
||||
app:srcCompat="@drawable/ic_copy"
|
||||
app:tint="?attr/colorAccent" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="@dimen/padding_spacing_dp8"
|
||||
android:paddingTop="@dimen/padding_spacing_dp8"
|
||||
android:paddingEnd="@dimen/padding_spacing_dp8">
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:paddingEnd="4dp">
|
||||
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_test_result"
|
||||
<ImageView
|
||||
android:id="@+id/iv_favorite"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:lines="1"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small"
|
||||
android:textColor="@color/colorPing"
|
||||
android:textSize="11sp"
|
||||
tools:text="214ms" />
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:padding="6dp"
|
||||
android:src="@drawable/kid_star_outline_24" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/iv_copy"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:padding="6dp"
|
||||
android:visibility="gone"
|
||||
app:srcCompat="@drawable/ic_copy"
|
||||
app:tint="?attr/colorPrimary" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -4,110 +4,127 @@
|
||||
android:id="@+id/item_bg"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingBottom="4dp"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/info_container"
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:gravity="center"
|
||||
android:nextFocusRight="@+id/layout_edit"
|
||||
android:orientation="horizontal"
|
||||
android:padding="@dimen/padding_spacing_dp8">
|
||||
app:cardBackgroundColor="?attr/colorSurfaceContainerLow"
|
||||
app:cardCornerRadius="16dp"
|
||||
app:cardElevation="0dp"
|
||||
app:strokeWidth="0dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:id="@+id/info_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/padding_spacing_dp8">
|
||||
android:layout_gravity="center"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:gravity="center"
|
||||
android:nextFocusRight="@+id/layout_edit"
|
||||
android:orientation="horizontal"
|
||||
android:padding="8dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
android:padding="8dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/remarks"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyLarge" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/img_locked"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginStart="16dp"
|
||||
android:importantForAccessibility="no"
|
||||
app:srcCompat="@drawable/ic_lock_24dp"
|
||||
app:tint="?attr/colorOnSurfaceVariant" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/remarks"
|
||||
android:id="@+id/domainIp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
|
||||
android:layout_marginTop="6dp"
|
||||
android:lines="1"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
|
||||
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/img_locked"
|
||||
android:layout_width="@dimen/padding_spacing_dp16"
|
||||
android:layout_height="@dimen/padding_spacing_dp16"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginStart="@dimen/padding_spacing_dp16"
|
||||
android:importantForAccessibility="no"
|
||||
app:srcCompat="@drawable/ic_lock_24dp" />
|
||||
<TextView
|
||||
android:id="@+id/outboundTag"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="4dp"
|
||||
android:lines="1"
|
||||
android:textAppearance="@style/TextAppearance.Material3.LabelMedium"
|
||||
android:textColor="?attr/colorPrimary" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/domainIp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/padding_spacing_dp8"
|
||||
android:lines="1"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/outboundTag"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/padding_spacing_dp8"
|
||||
android:lines="1"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/padding_spacing_dp8">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_edit"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/menu_item_edit_config"
|
||||
android:focusable="true"
|
||||
android:gravity="center"
|
||||
android:nextFocusLeft="@+id/info_container"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/padding_spacing_dp8">
|
||||
android:padding="8dp">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="@dimen/image_size_dp24"
|
||||
android:layout_height="@dimen/image_size_dp24"
|
||||
android:importantForAccessibility="no"
|
||||
app:srcCompat="@drawable/ic_edit_24dp" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/padding_spacing_dp8"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/chk_enable"
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_edit"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:theme="@style/BrandedSwitch" />
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/menu_item_edit_config"
|
||||
android:focusable="true"
|
||||
android:gravity="center"
|
||||
android:nextFocusLeft="@+id/info_container"
|
||||
android:orientation="vertical"
|
||||
android:padding="8dp">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:importantForAccessibility="no"
|
||||
app:srcCompat="@drawable/ic_edit_24dp"
|
||||
app:tint="?attr/colorOnSurfaceVariant" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/chk_enable"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:theme="@style/BrandedSwitch" />
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -4,195 +4,214 @@
|
||||
android:id="@+id/item_bg"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingBottom="4dp"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/info_container"
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:gravity="center"
|
||||
android:nextFocusRight="@+id/layout_share"
|
||||
android:orientation="horizontal"
|
||||
android:padding="@dimen/padding_spacing_dp8">
|
||||
app:cardBackgroundColor="?attr/colorSurfaceContainerLow"
|
||||
app:cardCornerRadius="16dp"
|
||||
app:cardElevation="0dp"
|
||||
app:strokeWidth="0dp">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/info_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
android:layout_gravity="center"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:gravity="center"
|
||||
android:nextFocusRight="@+id/layout_share"
|
||||
android:orientation="horizontal"
|
||||
android:padding="8dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="@dimen/padding_spacing_dp8">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:maxLines="2"
|
||||
android:minLines="1"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Subhead" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_share"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/title_configuration_share"
|
||||
android:focusable="true"
|
||||
android:gravity="center"
|
||||
android:nextFocusLeft="@+id/info_container"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/padding_spacing_dp8">
|
||||
android:paddingStart="8dp">
|
||||
|
||||
<ImageView
|
||||
<TextView
|
||||
android:id="@+id/tv_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="@dimen/image_size_dp24"
|
||||
android:importantForAccessibility="no"
|
||||
app:srcCompat="@drawable/ic_share_24dp" />
|
||||
android:layout_height="wrap_content"
|
||||
android:maxLines="2"
|
||||
android:minLines="1"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodyLarge" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_edit"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/menu_item_edit_config"
|
||||
android:focusable="true"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/padding_spacing_dp8">
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="@dimen/image_size_dp24"
|
||||
android:layout_height="@dimen/image_size_dp24"
|
||||
android:importantForAccessibility="no"
|
||||
app:srcCompat="@drawable/ic_edit_24dp" />
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_share"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/title_configuration_share"
|
||||
android:focusable="true"
|
||||
android:gravity="center"
|
||||
android:nextFocusLeft="@+id/info_container"
|
||||
android:orientation="vertical"
|
||||
android:padding="8dp">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="24dp"
|
||||
android:importantForAccessibility="no"
|
||||
app:srcCompat="@drawable/ic_share_24dp"
|
||||
app:tint="?attr/colorOnSurfaceVariant" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_edit"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/menu_item_edit_config"
|
||||
android:focusable="true"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:padding="8dp">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:importantForAccessibility="no"
|
||||
app:srcCompat="@drawable/ic_edit_24dp"
|
||||
app:tint="?attr/colorOnSurfaceVariant" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_remove"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/menu_item_del_config"
|
||||
android:focusable="true"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:padding="8dp">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:importantForAccessibility="no"
|
||||
app:srcCompat="@drawable/ic_delete_24dp"
|
||||
app:tint="?attr/colorError" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_remove"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/menu_item_del_config"
|
||||
android:focusable="true"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:padding="@dimen/padding_spacing_dp8">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="@dimen/image_size_dp24"
|
||||
android:layout_height="@dimen/image_size_dp24"
|
||||
android:importantForAccessibility="no"
|
||||
app:srcCompat="@drawable/ic_delete_24dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_url"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_url"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="@dimen/padding_spacing_dp8"
|
||||
android:paddingEnd="@dimen/padding_spacing_dp8">
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="horizontal"
|
||||
android:paddingTop="6dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_url"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:lines="2"
|
||||
android:textAppearance="@style/TextAppearance.Material3.BodySmall"
|
||||
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/chk_enable"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:theme="@style/BrandedSwitch" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:id="@+id/layout_last_updated"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="horizontal"
|
||||
android:paddingTop="@dimen/padding_spacing_dp8">
|
||||
android:layout_marginTop="6dp"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:paddingBottom="4dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_url"
|
||||
android:id="@+id/tv_last_updated"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:textAppearance="@style/TextAppearance.Material3.LabelSmall"
|
||||
android:textColor="?attr/colorOnSurfaceVariant" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress_bar"
|
||||
style="?android:attr/progressBarStyleSmall"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:visibility="gone" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_update_status"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:lines="2"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Small" />
|
||||
android:layout_marginStart="8dp"
|
||||
android:textAppearance="@style/TextAppearance.Material3.LabelSmall"
|
||||
android:visibility="gone" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/padding_spacing_dp8"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<androidx.appcompat.widget.SwitchCompat
|
||||
android:id="@+id/chk_enable"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:theme="@style/BrandedSwitch" />
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/layout_last_updated"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginTop="@dimen/padding_spacing_dp8"
|
||||
android:paddingStart="@dimen/padding_spacing_dp8"
|
||||
android:paddingEnd="@dimen/padding_spacing_dp8">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_last_updated"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Caption"/>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/progress_bar"
|
||||
style="?android:attr/progressBarStyleSmall"
|
||||
android:layout_width="16dp"
|
||||
android:layout_height="16dp"
|
||||
android:layout_marginStart="@dimen/padding_spacing_dp8"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_update_status"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="@dimen/padding_spacing_dp8"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Caption"
|
||||
android:visibility="gone"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Button xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<com.google.android.material.button.MaterialButton xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
style="@style/Widget.AppCompat.Button.Borderless"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:onClick="onModeHelpClicked"
|
||||
android:text="@string/title_mode_help"
|
||||
android:textAlignment="textStart"
|
||||
android:textStyle="italic"
|
||||
android:textColor="?attr/colorPrimary"
|
||||
app:iconTint="?attr/colorPrimary"
|
||||
tools:ignore="UsingOnClickInXml" />
|
||||
|
||||
@@ -1,29 +1,37 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="color_fab_active">#C0C0C0</color>
|
||||
<color name="color_fab_inactive">#646464</color>
|
||||
<color name="divider_color_light">#424242</color>
|
||||
<color name="colorPing">#90CAF9</color>
|
||||
<color name="colorPingGood">#86D993</color>
|
||||
<color name="colorPingMedium">#F1C76A</color>
|
||||
<color name="status_connected">#66BB6A</color>
|
||||
<color name="status_failure">#EF5350</color>
|
||||
<color name="colorPingRed">#FFB4AB</color>
|
||||
<color name="color_fab_active">@color/md_theme_primary</color>
|
||||
<color name="color_fab_inactive">@color/md_theme_secondaryContainer</color>
|
||||
<color name="divider_color_light">@color/md_theme_outlineVariant</color>
|
||||
<color name="server_list_divider">#2649454F</color>
|
||||
|
||||
<color name="md_theme_primary">#C0C0C0</color>
|
||||
<color name="md_theme_onPrimary">#303030</color>
|
||||
<color name="md_theme_primaryContainer">#474747</color>
|
||||
<color name="md_theme_onPrimaryContainer">#E0E0E0</color>
|
||||
<!-- M3 Dark scheme — Purple/Violet tonal palette -->
|
||||
<color name="md_theme_primary">#D0BCFF</color>
|
||||
<color name="md_theme_onPrimary">#381E72</color>
|
||||
<color name="md_theme_primaryContainer">#4F378B</color>
|
||||
<color name="md_theme_onPrimaryContainer">#EADDFF</color>
|
||||
|
||||
<color name="md_theme_secondary">#90CAF9</color>
|
||||
<color name="md_theme_onSecondary">#FFFFFF</color>
|
||||
<color name="md_theme_secondaryContainer">#6F3800</color>
|
||||
<color name="md_theme_onSecondaryContainer">#FFE8D6</color>
|
||||
<color name="md_theme_secondary">#CCC2DC</color>
|
||||
<color name="md_theme_onSecondary">#332D41</color>
|
||||
<color name="md_theme_secondaryContainer">#4A4458</color>
|
||||
<color name="md_theme_onSecondaryContainer">#E8DEF8</color>
|
||||
|
||||
<color name="md_theme_tertiary">#64B5F6</color>
|
||||
<color name="md_theme_onTertiary">#00382E</color>
|
||||
<color name="md_theme_tertiaryContainer">#005143</color>
|
||||
<color name="md_theme_onTertiaryContainer">#BBDEFB</color>
|
||||
<color name="md_theme_tertiary">#EFB8C8</color>
|
||||
<color name="md_theme_onTertiary">#492532</color>
|
||||
<color name="md_theme_tertiaryContainer">#633B48</color>
|
||||
<color name="md_theme_onTertiaryContainer">#FFD8E4</color>
|
||||
|
||||
<!-- Error colors -->
|
||||
<color name="md_theme_error">#FFB4AB</color>
|
||||
<color name="md_theme_errorContainer">#93000A</color>
|
||||
<color name="md_theme_onError">#690005</color>
|
||||
<color name="md_theme_onErrorContainer">#FFDAD6</color>
|
||||
<color name="md_theme_error">#F2B8B5</color>
|
||||
<color name="md_theme_errorContainer">#8C1D18</color>
|
||||
<color name="md_theme_onError">#601410</color>
|
||||
<color name="md_theme_onErrorContainer">#F9DEDC</color>
|
||||
|
||||
<!-- Background colors -->
|
||||
<color name="md_theme_background">#1C1B1F</color>
|
||||
@@ -35,15 +43,22 @@
|
||||
<color name="md_theme_surfaceVariant">#49454F</color>
|
||||
<color name="md_theme_onSurfaceVariant">#CAC4D0</color>
|
||||
<color name="md_theme_inverseSurface">#E6E1E5</color>
|
||||
<color name="md_theme_inverseOnSurface">#1C1B1F</color>
|
||||
<color name="md_theme_inverseOnSurface">#313033</color>
|
||||
|
||||
<!-- Surface containers — M3 dark elevation tones -->
|
||||
<color name="md_theme_surfaceContainerLowest">#0F0D13</color>
|
||||
<color name="md_theme_surfaceContainerLow">#1D1B20</color>
|
||||
<color name="md_theme_surfaceContainer">#211F26</color>
|
||||
<color name="md_theme_surfaceContainerHigh">#2B2930</color>
|
||||
<color name="md_theme_surfaceContainerHighest">#36343B</color>
|
||||
|
||||
<!-- Outline colors -->
|
||||
<color name="md_theme_outline">#938F99</color>
|
||||
<color name="md_theme_outlineVariant">#49454F</color>
|
||||
|
||||
<!-- Other colors -->
|
||||
<color name="md_theme_inversePrimary">#000000</color>
|
||||
<color name="md_theme_inversePrimary">#6750A4</color>
|
||||
<color name="md_theme_shadow">#000000</color>
|
||||
<color name="md_theme_surfaceTint">#C0C0C0</color>
|
||||
<color name="md_theme_surfaceTint">#D0BCFF</color>
|
||||
<color name="md_theme_scrim">#000000</color>
|
||||
</resources>
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<!-- Night theme: inherit the shared AppThemeBase and only override night-specific items -->
|
||||
<!-- Night theme: inherit shared base, only override night-specific items -->
|
||||
<style name="AppThemeDayNight" parent="AppThemeBase">
|
||||
<!-- Night mode specific overrides -->
|
||||
<item name="android:windowLightStatusBar">false</item>
|
||||
<item name="android:windowLightNavigationBar" tools:targetApi="27">false</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
</resources>
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<resources>
|
||||
<string name="app_widget_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_close">Закрыть панель навигации</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_developed_text">developed by developers from <a href="https://t.me/openlibrecommunity">Olc</a></string>
|
||||
<string name="drawer_forked_text">forked from <a
|
||||
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="migration_fail">Перенос данных не выполнен!</string>
|
||||
<string name="pull_down_to_refresh">Потяните вниз для обновления!</string>
|
||||
@@ -15,7 +21,9 @@
|
||||
<!-- Notifications -->
|
||||
<string name="notification_action_stop_v2ray">Остановить</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="toast_services_start">Запуск служб</string>
|
||||
<string name="toast_services_stop">Остановка служб</string>
|
||||
@@ -29,19 +37,41 @@
|
||||
<string name="menu_item_edit_config">Изменить профиль</string>
|
||||
<string name="menu_item_del_config">Удалить профиль</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_policy_group">Добавить группу политик</string>
|
||||
<string name="menu_item_import_config_manually_vmess">Ручной ввод VMess</string>
|
||||
<string name="menu_item_import_config_manually_vless">Ручной ввод 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="menu_item_import_config_policy_group"
|
||||
>Добавить группу политик</string>
|
||||
<string
|
||||
name="menu_item_import_config_manually_vmess"
|
||||
>Ручной ввод VMess</string>
|
||||
<string
|
||||
name="menu_item_import_config_manually_vless"
|
||||
>Ручной ввод 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_invalid_config_comfirm">Выполните проверку перед удалением! Подтверждаете удаление?</string>
|
||||
<string
|
||||
name="del_invalid_config_comfirm"
|
||||
>Выполните проверку перед удалением! Подтверждаете удаление?</string>
|
||||
<string name="server_lab_remarks">Название</string>
|
||||
<string name="server_lab_address">Адрес</string>
|
||||
<string name="server_lab_port">Порт</string>
|
||||
@@ -82,45 +112,73 @@
|
||||
<string name="server_lab_encryption">Шифрование</string>
|
||||
<string name="server_lab_flow">Поток</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_spider_x">SpiderX</string>
|
||||
<string name="server_lab_mldsa65_verify">mldsa65Verify</string>
|
||||
<string name="server_lab_secret_key">Закрытый ключ</string>
|
||||
<string name="server_lab_reserved">Reserved (необязательно, через запятую)</string>
|
||||
<string name="server_lab_local_address">Локальный адрес (необязательно, IPv4/IPv6 через запятую)</string>
|
||||
<string name="server_lab_local_mtu">MTU (необязательно, по умолчанию 1420)</string>
|
||||
<string
|
||||
name="server_lab_reserved"
|
||||
>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_failure">Ошибка</string>
|
||||
<string name="toast_none_data">Ничего нет</string>
|
||||
<string name="toast_incorrect_protocol">Неправильный протокол</string>
|
||||
<string name="toast_decoding_failed">Невозможно декодировать</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="toast_config_file_invalid">Неправильный профиль</string>
|
||||
<string name="server_lab_content">Данные</string>
|
||||
<string name="toast_none_data_clipboard">В буфере обмена нет данных</string>
|
||||
<string name="toast_invalid_url">Неправильный URL</string>
|
||||
<string name="toast_insecure_url_protocol">Не используйте небезопасный HTTP-протокол в адресе подписки</string>
|
||||
<string name="server_lab_need_inbound">Убедитесь, что входящий порт соответствует настройкам</string>
|
||||
<string
|
||||
name="toast_insecure_url_protocol"
|
||||
>Не используйте небезопасный HTTP-протокол в адресе подписки</string>
|
||||
<string
|
||||
name="server_lab_need_inbound"
|
||||
>Убедитесь, что входящий порт соответствует настройкам</string>
|
||||
<string name="toast_malformed_josn">Профиль повреждён</string>
|
||||
<string name="server_lab_request_host6">Узел (SNI) (необязательно)</string>
|
||||
<string name="toast_action_not_allowed">Это действие запрещено</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_bandwidth_down">Входящая пропускная способность (допускаются: k/m/g/t)</string>
|
||||
<string name="server_lab_bandwidth_up">Исходящая пропускная способность (допускаются: k/m/g/t)</string>
|
||||
<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_extra">Необработанный JSON XHTTP Extra, формат: { XHTTPObject }</string>
|
||||
<string name="server_lab_final_mask">finalMask raw JSON, format: { FinalMaskObject }</string>
|
||||
<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_force_query">EchForceQuery</string>
|
||||
<string name="server_lab_pinned_ca256">Отпечаток сертификата (SHA-256)</string>
|
||||
<string
|
||||
name="server_lab_pinned_ca256"
|
||||
>Отпечаток сертификата (SHA-256)</string>
|
||||
|
||||
<!-- 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_file">Добавить файлы</string>
|
||||
<string name="menu_item_add_url">Добавить URL</string>
|
||||
@@ -130,7 +188,9 @@
|
||||
<string name="title_user_asset_add_url">Добавить URL ресурса</string>
|
||||
<string name="msg_file_not_found">Файл не найден</string>
|
||||
<string name="msg_remark_is_duplicate">Название уже существует</string>
|
||||
<string name="asset_geo_files_sources">Источник геофайлов (необязательно)</string>
|
||||
<string
|
||||
name="asset_geo_files_sources"
|
||||
>Источник геофайлов (необязательно)</string>
|
||||
|
||||
<!-- PerAppProxyActivity -->
|
||||
<string name="msg_dialog_progress">Загрузка…</string>
|
||||
@@ -144,7 +204,9 @@
|
||||
<string name="menu_item_export_proxy_app">Экспорт в буфер обмена</string>
|
||||
<string name="menu_item_import_proxy_app">Импорт из буфера обмена</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 -->
|
||||
<string name="title_settings">Настройки</string>
|
||||
@@ -152,19 +214,50 @@
|
||||
<string name="title_core_settings">Настройки ядра</string>
|
||||
<string name="title_vpn_settings">Настройки VPN</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="summary_pref_is_booted">Автоматически подключаться к выбранному серверу при запуске приложения (может оказаться неудачным)</string>
|
||||
<string
|
||||
name="summary_pref_is_booted"
|
||||
>Автоматически подключаться к выбранному серверу при запуске приложения (может оказаться неудачным)</string>
|
||||
|
||||
<string name="title_pref_auto_sort_after_test">Автосортировка профилей</string>
|
||||
<string name="summary_pref_auto_sort_after_test">Автоматическая сортировка профилей после проверки (результаты проверки могут быть неточными)</string>
|
||||
<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
|
||||
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_pref_mux_enabled">Использовать мультиплексирование</string>
|
||||
<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
|
||||
name="title_pref_mux_enabled"
|
||||
>Использовать мультиплексирование</string>
|
||||
<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">
|
||||
<item>Отклонять</item>
|
||||
<item>Разрешать</item>
|
||||
@@ -172,49 +265,87 @@
|
||||
</string-array>
|
||||
|
||||
<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="summary_pref_sniffing_enabled">Пытаться определять доменные имена в пакетах (по умолчанию включено)</string>
|
||||
<string name="title_pref_route_only_enabled">Домен только для маршрутизации</string>
|
||||
<string name="summary_pref_route_only_enabled">Использовать доменное имя только для маршрутизации и сохранять целевой адрес в виде IP.</string>
|
||||
<string
|
||||
name="summary_pref_sniffing_enabled"
|
||||
>Пытаться определять доменные имена в пакетах (по умолчанию включено)</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 name="summary_pref_local_dns_enabled">Обслуживание выполняется DNS-модулем ядра (в настройках маршрутизации рекомендуется выбрать режим «Все, кроме LAN и Китая»)</string>
|
||||
<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 name="summary_pref_fake_dns_enabled">Локальная DNS возвращает поддельные IP-адреса (быстрее, но может не работать с некоторыми приложениями)</string>
|
||||
<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="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="title_pref_vpn_dns">VPN DNS (только IPv4/v6)</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_domestic_dns">Внутренняя DNS (необязательно)</string>
|
||||
<string
|
||||
name="title_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="title_pref_delay_test_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="title_pref_proxy_sharing_enabled">Разрешать соединения из LAN</string>
|
||||
<string name="summary_pref_proxy_sharing_enabled">Другие устройства могут подключаться, используя ваш IP-адрес, чтобы использовать локальный прокси. Используйте только в доверенной сети, чтобы избежать несанкционированного подключения.</string>
|
||||
<string name="toast_warning_pref_proxysharing_short">Доступ из LAN разрешён, убедитесь, что вы находитесь в доверенной сети</string>
|
||||
<string
|
||||
name="title_pref_proxy_sharing_enabled"
|
||||
>Разрешать соединения из 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 name="summary_pref_allow_insecure">Для TLS по умолчанию разрешены небезопасные соединения</string>
|
||||
<string
|
||||
name="title_pref_allow_insecure"
|
||||
>Разрешать небезопасные соединения</string>
|
||||
<string
|
||||
name="summary_pref_allow_insecure"
|
||||
>Для TLS по умолчанию разрешены небезопасные соединения</string>
|
||||
|
||||
<string name="title_pref_socks_port">Порт локального прокси</string>
|
||||
<string name="summary_pref_socks_port">Порт локального прокси</string>
|
||||
@@ -222,28 +353,50 @@
|
||||
<string name="title_pref_local_dns_port">Порт локальной DNS</string>
|
||||
<string name="summary_pref_local_dns_port">Порт локальной DNS</string>
|
||||
|
||||
<string name="title_pref_confirm_remove">Подтверждать удаление профиля</string>
|
||||
<string name="summary_pref_confirm_remove">Обязательное подтверждение удаления профиля</string>
|
||||
<string
|
||||
name="title_pref_confirm_remove"
|
||||
>Подтверждать удаление профиля</string>
|
||||
<string
|
||||
name="summary_pref_confirm_remove"
|
||||
>Обязательное подтверждение удаления профиля</string>
|
||||
|
||||
<string name="title_pref_start_scan_immediate">Сканировать при запуске</string>
|
||||
<string name="summary_pref_start_scan_immediate">Начинать сканирование сразу при запуске приложения или запускать функцию сканирования камерой или из изображения через панель инструментов</string>
|
||||
<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 name="summary_pref_append_http_proxy">HTTP-прокси будет использоваться напрямую (из браузера и других поддерживающих приложений), минуя виртуальный сетевой адаптер (Android 10+)</string>
|
||||
<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 name="summary_pref_double_column_display">Список профилей отображается двумя столбцами, что позволяет показать больше информации на экране. Требуется перезапуск приложения.</string>
|
||||
<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="summary_pref_group_all_display">Показывать дополнительную вкладку со всеми профилями групп</string>
|
||||
<string
|
||||
name="summary_pref_group_all_display"
|
||||
>Показывать дополнительную вкладку со всеми профилями групп</string>
|
||||
|
||||
<string name="title_pref_show_copy_button">Кнопка копирования</string>
|
||||
<string name="summary_pref_show_copy_button">Показывать кнопку копирования конфигурации сервера в буфер обмена</string>
|
||||
<!-- AboutActivity -->
|
||||
<string name="title_pref_feedback">Обратная связь</string>
|
||||
<string name="summary_pref_feedback">Предложить улучшение или сообщить об ошибке на GitHub</string>
|
||||
<string name="summary_pref_tg_group">Присоединиться к группе в Telegram</string>
|
||||
<string name="toast_tg_app_not_found">Приложение Telegram не найдено</string>
|
||||
<string
|
||||
name="summary_pref_feedback"
|
||||
>Предложить улучшение или сообщить об ошибке на 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_about">О приложении</string>
|
||||
<string name="title_source_code">Исходный код</string>
|
||||
@@ -252,33 +405,57 @@
|
||||
|
||||
<string name="title_pref_promotion">Содействие</string>
|
||||
|
||||
<string name="title_pref_auto_update_subscription">Автоматически обновлять подписки</string>
|
||||
<string name="summary_pref_auto_update_subscription">Автоматическое обновление подписок в фоновом режиме с указанным интервалом. В зависимости от устройства эта функция может работать не всегда.</string>
|
||||
<string name="title_pref_auto_update_interval">Интервал автообновления (минут, не менее 15)</string>
|
||||
<string
|
||||
name="title_pref_auto_update_subscription"
|
||||
>Автоматически обновлять подписки</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_outbound_domain_resolve_method">Предопределение исходящего домена</string>
|
||||
<string
|
||||
name="title_outbound_domain_resolve_method"
|
||||
>Предопределение исходящего домена</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_ui_settings">Настройки интерфейса</string>
|
||||
<string name="title_pref_ui_mode_night">Тема интерфейса</string>
|
||||
<string name="title_pref_dynamic_colors">Динамические цвета (Material You)</string>
|
||||
<string name="summary_pref_dynamic_colors">Использовать цвета обоев (Android 12+). Требует перезапуска.</string>
|
||||
<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="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="summary_pref_use_hev_tunnel">Если включено, 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="summary_pref_use_hev_tunnel"
|
||||
>Если включено, 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="logcat_copy">Копировать</string>
|
||||
<string name="logcat_clear">Очистить</string>
|
||||
<string name="title_service_restart">Перезапуск службы</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_export_all">Экспорт профилей в буфер обмена</string>
|
||||
<string name="title_sub_setting">Группы</string>
|
||||
@@ -288,26 +465,44 @@
|
||||
<string name="sub_setting_filter">Название фильтра</string>
|
||||
<string name="sub_setting_enable">Использовать обновление</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_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_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_sort_by_test_results">Сортировать по результатам теста</string>
|
||||
<string
|
||||
name="title_sort_by_test_results"
|
||||
>Сортировать по результатам теста</string>
|
||||
<string name="title_filter_config">Фильтр профилей</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_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_updating">Обновление…</string>
|
||||
<string name="title_update_subscription_result">Обновлено профилей: %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="title_update_subscription_result"
|
||||
>Обновлено профилей: %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="title_locate_selected_config">Найти выбранный профиль</string>
|
||||
|
||||
@@ -317,17 +512,33 @@
|
||||
<!-- RoutingSettingActivity -->
|
||||
<string name="routing_settings_domain_strategy">Доменная стратегия</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_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_import_predefined_rulesets">Импорт набора правил</string>
|
||||
<string name="routing_settings_import_rulesets_tip">Существующие правила будут удалены. Продолжить?</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_import_predefined_rulesets"
|
||||
>Импорт набора правил</string>
|
||||
<string
|
||||
name="routing_settings_import_rulesets_tip"
|
||||
>Существующие правила будут удалены. Продолжить?</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_ip">IP</string>
|
||||
<string name="routing_settings_port">Порт</string>
|
||||
@@ -340,45 +551,71 @@
|
||||
<string name="connection_test_pending">Проверить соединение</string>
|
||||
<string name="connection_test_testing">Проверка…</string>
|
||||
<string name="connection_test_testing_count">Проверка профилей (%d)</string>
|
||||
<string name="connection_test_available">Успешно: соединение заняло %d мс</string>
|
||||
<string name="connection_test_error">Сбой проверки интернет-соединения: %s</string>
|
||||
<string name="connection_test_available">%d мс</string>
|
||||
<string
|
||||
name="connection_test_error"
|
||||
>Сбой проверки интернет-соединения: %s</string>
|
||||
<string name="connection_test_fail">Интернет недоступен</string>
|
||||
<string name="connection_test_error_status_code">Код ошибки: #%d</string>
|
||||
<string name="connection_connected">Соединено, нажмите для проверки</string>
|
||||
<string name="connection_not_connected">Ожидаем действий</string>
|
||||
<string name="connection_connected">нажмите для проверки</string>
|
||||
<string name="connection_not_connected">Не подключено</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_failure">Невозможно импортировать подписку</string>
|
||||
<string
|
||||
name="import_subscription_failure"
|
||||
>Невозможно импортировать подписку</string>
|
||||
<string name="title_fragment_settings">Настройки фрагментирования</string>
|
||||
<string name="title_pref_fragment_packets">Фрагментирование пакетов</string>
|
||||
<string name="title_pref_fragment_length">Длина фрагмента (от - до)</string>
|
||||
<string name="title_pref_fragment_interval">Интервал фрагментов (от - до)</string>
|
||||
<string name="title_pref_fragment_enabled">Использовать фрагментирование</string>
|
||||
<string
|
||||
name="title_pref_fragment_interval"
|
||||
>Интервал фрагментов (от - до)</string>
|
||||
<string
|
||||
name="title_pref_fragment_enabled"
|
||||
>Использовать фрагментирование</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_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="title_policy_group_type">Тип группы политик</string>
|
||||
<string name="title_policy_group_subscription_id">Из группы подписки</string>
|
||||
<string name="title_policy_group_subscription_filter">Название фильтра</string>
|
||||
<string
|
||||
name="title_policy_group_subscription_id"
|
||||
>Из группы подписки</string>
|
||||
<string
|
||||
name="title_policy_group_subscription_filter"
|
||||
>Название фильтра</string>
|
||||
|
||||
<!-- BackupActivity -->
|
||||
<string name="title_configuration_backup_restore">Резервное копирование</string>
|
||||
<string name="title_configuration_backup">Резервирование конфигурации</string>
|
||||
<string name="title_configuration_restore">Восстановление конфигурации</string>
|
||||
<string
|
||||
name="title_configuration_backup_restore"
|
||||
>Резервное копирование</string>
|
||||
<string
|
||||
name="title_configuration_backup"
|
||||
>Резервирование конфигурации</string>
|
||||
<string
|
||||
name="title_configuration_restore"
|
||||
>Восстановление конфигурации</string>
|
||||
<string name="title_configuration_share">Поделиться конфигурацией</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_user">Пользователь</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">
|
||||
<item>QR-код</item>
|
||||
@@ -441,4 +678,16 @@
|
||||
<item>WebDAV</item>
|
||||
</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>
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
|
||||
<style name="TabLayoutTextStyle" parent="TextAppearance.Design.Tab">
|
||||
<item name="textAllCaps">false</item>
|
||||
</style>
|
||||
</resources>
|
||||
</resources>
|
||||
|
||||
@@ -1,54 +1,67 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="colorPing">#1565C0</color>
|
||||
<color name="colorPingRed">#FF0099</color>
|
||||
<color name="colorConfigType">#1976D2</color>
|
||||
<color name="colorPing">#006494</color>
|
||||
<color name="colorPingGood">#2E7D32</color>
|
||||
<color name="colorPingMedium">#9A6700</color>
|
||||
<color name="status_connected">#4CAF50</color>
|
||||
<color name="status_failure">#E53935</color>
|
||||
<color name="colorPingRed">#BA1A1A</color>
|
||||
<color name="colorConfigType">#1565C0</color>
|
||||
<color name="colorWhite">#FFFFFF</color>
|
||||
<color name="color_fab_active">#000000</color>
|
||||
<color name="color_fab_inactive">#9C9C9C</color>
|
||||
<color name="divider_color_light">#E0E0E0</color>
|
||||
<color name="color_fab_active">@color/md_theme_primary</color>
|
||||
<color name="color_fab_inactive">@color/md_theme_secondaryContainer</color>
|
||||
<color name="divider_color_light">@color/md_theme_outlineVariant</color>
|
||||
<color name="server_list_divider">#22CAC4D0</color>
|
||||
<color name="colorIndicator">@color/md_theme_primary</color>
|
||||
|
||||
<color name="md_theme_primary">#000000</color>
|
||||
<!-- M3 Light scheme — Purple/Violet tonal palette -->
|
||||
<color name="md_theme_primary">#6750A4</color>
|
||||
<color name="md_theme_onPrimary">#FFFFFF</color>
|
||||
<color name="md_theme_primaryContainer">#E0E0E0</color>
|
||||
<color name="md_theme_onPrimaryContainer">#000000</color>
|
||||
<color name="md_theme_primaryContainer">#EADDFF</color>
|
||||
<color name="md_theme_onPrimaryContainer">#21005D</color>
|
||||
|
||||
<color name="md_theme_secondary">#1976D2</color>
|
||||
<color name="md_theme_secondary">#625B71</color>
|
||||
<color name="md_theme_onSecondary">#FFFFFF</color>
|
||||
<color name="md_theme_secondaryContainer">#FFE8D6</color>
|
||||
<color name="md_theme_onSecondaryContainer">#2B1700</color>
|
||||
<color name="md_theme_secondaryContainer">#E8DEF8</color>
|
||||
<color name="md_theme_onSecondaryContainer">#1D192B</color>
|
||||
|
||||
<color name="md_theme_tertiary">#1565C0</color>
|
||||
<color name="md_theme_tertiary">#7D5260</color>
|
||||
<color name="md_theme_onTertiary">#FFFFFF</color>
|
||||
<color name="md_theme_tertiaryContainer">#BBDEFB</color>
|
||||
<color name="md_theme_onTertiaryContainer">#00201A</color>
|
||||
<color name="md_theme_tertiaryContainer">#FFD8E4</color>
|
||||
<color name="md_theme_onTertiaryContainer">#31111D</color>
|
||||
|
||||
<!-- Error colors -->
|
||||
<color name="md_theme_error">#BA1A1A</color>
|
||||
<color name="md_theme_errorContainer">#FFDAD6</color>
|
||||
<color name="md_theme_error">#B3261E</color>
|
||||
<color name="md_theme_errorContainer">#F9DEDC</color>
|
||||
<color name="md_theme_onError">#FFFFFF</color>
|
||||
<color name="md_theme_onErrorContainer">#410002</color>
|
||||
<color name="md_theme_onErrorContainer">#410E0B</color>
|
||||
|
||||
<!-- Background colors -->
|
||||
<color name="md_theme_background">#FFFFFF</color>
|
||||
<color name="md_theme_background">#FFFBFE</color>
|
||||
<color name="md_theme_onBackground">#1C1B1F</color>
|
||||
|
||||
<!-- Surface colors -->
|
||||
<color name="md_theme_surface">#FFFFFF</color>
|
||||
<!-- Surface colors — M3 tonal surface hierarchy -->
|
||||
<color name="md_theme_surface">#FFFBFE</color>
|
||||
<color name="md_theme_onSurface">#1C1B1F</color>
|
||||
<color name="md_theme_surfaceVariant">#E7E0EC</color>
|
||||
<color name="md_theme_onSurfaceVariant">#49454F</color>
|
||||
<color name="md_theme_inverseSurface">#313033</color>
|
||||
<color name="md_theme_inverseOnSurface">#F4EFF4</color>
|
||||
|
||||
<!-- Surface containers — M3 elevation tones -->
|
||||
<color name="md_theme_surfaceContainerLowest">#FFFFFF</color>
|
||||
<color name="md_theme_surfaceContainerLow">#F7F2FA</color>
|
||||
<color name="md_theme_surfaceContainer">#F3EDF7</color>
|
||||
<color name="md_theme_surfaceContainerHigh">#ECE6F0</color>
|
||||
<color name="md_theme_surfaceContainerHighest">#E6E0E9</color>
|
||||
|
||||
<!-- Outline colors -->
|
||||
<color name="md_theme_outline">#79747E</color>
|
||||
<color name="md_theme_outlineVariant">#CAC4D0</color>
|
||||
|
||||
<!-- Other colors -->
|
||||
<color name="md_theme_inversePrimary">#C0C0C0</color>
|
||||
<color name="md_theme_inversePrimary">#D0BCFF</color>
|
||||
<color name="md_theme_shadow">#000000</color>
|
||||
<color name="md_theme_surfaceTint">#000000</color>
|
||||
<color name="md_theme_surfaceTint">#6750A4</color>
|
||||
<color name="md_theme_scrim">#000000</color>
|
||||
</resources>
|
||||
|
||||
@@ -1,22 +1,35 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<resources>
|
||||
<string name="app_name" translatable="false">olcng</string>
|
||||
<string name="app_widget_name">Switch</string>
|
||||
<string name="app_tile_name">Switch</string>
|
||||
<string name="app_tile_first_use">First use of this feature, please use the app to add server</string>
|
||||
<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_close">Close navigation drawer</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_developed_text" translatable="false">developed by developers from <a href="https://t.me/openlibrecommunity">Olc</a></string>
|
||||
<string name="drawer_forked_text" translatable="false">forked from <a
|
||||
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="migration_fail">Data migration failed!</string>
|
||||
<string name="pull_down_to_refresh">Please pull down to refresh!</string>
|
||||
|
||||
<!-- Notifications -->
|
||||
<string name="notification_action_stop_v2ray">Stop</string>
|
||||
<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="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="toast_services_start">Start 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_del_config">Delete config</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_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_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_http">Add [HTTP]</string>
|
||||
<string name="menu_item_import_config_manually_trojan">Add [Trojan]</string>
|
||||
<string name="menu_item_import_config_manually_wireguard">Add [Wireguard]</string>
|
||||
<string name="menu_item_import_config_manually_hysteria2">Add [Hysteria2]</string>
|
||||
<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_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_address">address</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_grpc">gRPC serviceName</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_allow_insecure">allowInsecure</string>
|
||||
<string name="server_lab_sni">SNI</string>
|
||||
@@ -88,8 +116,12 @@
|
||||
<string name="server_lab_spider_x">SpiderX</string>
|
||||
<string name="server_lab_mldsa65_verify">Mldsa65Verify</string>
|
||||
<string name="server_lab_secret_key">SecretKey</string>
|
||||
<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_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="toast_success">Success</string>
|
||||
<string name="toast_failure">Failure</string>
|
||||
@@ -97,31 +129,53 @@
|
||||
<string name="toast_incorrect_protocol">Incorrect protocol</string>
|
||||
<string name="toast_decoding_failed">Decoding failed</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="toast_config_file_invalid">Invalid config</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_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_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="server_lab_request_host6">Host(SNI)(Optional)</string>
|
||||
<string name="toast_action_not_allowed">Action not allowed</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_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_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_extra">XHTTP Extra raw JSON, format: { XHTTPObject }</string>
|
||||
<string name="server_lab_final_mask">finalMask raw JSON, format: { FinalMaskObject }</string>
|
||||
<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_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 -->
|
||||
<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_file">Add files</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="per_app_proxy_settings">Раздельное туннелирование</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 -->
|
||||
<string name="title_settings">Settings</string>
|
||||
@@ -154,22 +210,44 @@
|
||||
<string name="title_core_settings">Core Settings</string>
|
||||
<string name="title_vpn_settings">VPN Settings</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="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 name="summary_pref_auto_sort_after_test">Test results may not be accurate;</string>
|
||||
<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="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="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_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 name="title_pref_mux_concurency">TCP connections(range -1 to 1024)</string>
|
||||
<string name="title_pref_mux_xudp_concurency">XUDP connections(range -1 to 1024)</string>
|
||||
<string name="title_pref_mux_xudp_quic">Handling of QUIC in mux tunnel</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
|
||||
name="title_pref_mux_concurency"
|
||||
>TCP connections(range -1 to 1024)</string>
|
||||
<string
|
||||
name="title_pref_mux_xudp_concurency"
|
||||
>XUDP connections(range -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">
|
||||
<item>reject</item>
|
||||
<item>allow</item>
|
||||
@@ -177,30 +255,46 @@
|
||||
</string-array>
|
||||
|
||||
<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>
|
||||
|
||||
<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="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="summary_pref_local_dns_enabled">DNS processed by core‘s DNS module (Recommended if you need routing bypassing LAN and mainland addresses)</string>
|
||||
<string
|
||||
name="summary_pref_local_dns_enabled"
|
||||
>DNS processed by core‘s 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="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="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="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_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>
|
||||
|
||||
@@ -208,21 +302,33 @@
|
||||
<string name="title_pref_domestic_dns">Domestic DNS (Optional)</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="title_pref_delay_test_url">True delay test 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="title_pref_proxy_sharing_enabled">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_proxy_sharing_enabled"
|
||||
>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="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="summary_pref_socks_port">Local proxy port</string>
|
||||
@@ -231,23 +337,41 @@
|
||||
<string name="summary_pref_local_dns_port">Local DNS port</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 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_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 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_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 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_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="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 -->
|
||||
<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="toast_tg_app_not_found">Telegram app not found</string>
|
||||
<string name="title_privacy_policy">Privacy policy</string>
|
||||
@@ -259,25 +383,45 @@
|
||||
|
||||
<string name="title_pref_promotion">Promotion</string>
|
||||
|
||||
<string name="title_pref_auto_update_subscription">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_pref_auto_update_subscription"
|
||||
>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_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_help">Click me for more help</string>
|
||||
<string name="title_language">Language</string>
|
||||
<string name="title_ui_settings">UI 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 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_dynamic_colors"
|
||||
>Dynamic colors (Material You)</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="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_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="title_logcat">Logcat</string>
|
||||
@@ -296,9 +440,13 @@
|
||||
<string name="sub_setting_enable">Enable 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_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_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_ping_all_server">Tcping config</string>
|
||||
<string name="title_real_ping_all_server">Real delay config</string>
|
||||
@@ -306,17 +454,29 @@
|
||||
<string name="title_sort_by_test_results">Sorting by test results</string>
|
||||
<string name="title_filter_config">Filter config</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_import_config_count">Import %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_updating">Updating…</string>
|
||||
<string name="title_update_subscription_result">Updated %1$d configs (%2$d success, %3$d failed, %4$d skipped)</string>
|
||||
<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="title_update_subscription_result"
|
||||
>Updated %1$d configs (%2$d success, %3$d failed, %4$d skipped)</string>
|
||||
<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_setting_confirm">Confirm</string>
|
||||
@@ -324,64 +484,117 @@
|
||||
<!-- RoutingSettingActivity -->
|
||||
<string name="routing_settings_domain_strategy">Domain strategy</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_delete">Clear</string>
|
||||
<string name="routing_settings_rule_title">Routing Rule Settings</string>
|
||||
<string name="routing_settings_add_rule">Add rule</string>
|
||||
<string name="routing_settings_import_predefined_rulesets">Import predefined rulesets</string>
|
||||
<string name="routing_settings_import_rulesets_tip">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_import_predefined_rulesets"
|
||||
>Import predefined rulesets</string>
|
||||
<string
|
||||
name="routing_settings_import_rulesets_tip"
|
||||
>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_ip" translatable="false">ip</string>
|
||||
<string name="routing_settings_port" translatable="false">port</string>
|
||||
<string name="routing_settings_protocol" translatable="false">protocol</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="routing_settings_protocol"
|
||||
translatable="false"
|
||||
>protocol</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_testing">Testing…</string>
|
||||
<string name="connection_test_testing_count">Тестирование %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_testing_count"
|
||||
>Тестирование %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_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_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 name="import_subscription_failure">Import subscription failed</string>
|
||||
<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_pref_fragment_packets">Fragment Packets</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="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_now">Update now</string>
|
||||
<string name="update_check_pre_release">Check Pre-release</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_subscription_id">From subscription group</string>
|
||||
<string name="title_policy_group_subscription_filter">Remarks regular filter</string>
|
||||
<string
|
||||
name="title_policy_group_subscription_id"
|
||||
>From subscription group</string>
|
||||
<string
|
||||
name="title_policy_group_subscription_filter"
|
||||
>Remarks regular filter</string>
|
||||
|
||||
<!-- BackupActivity -->
|
||||
<string name="title_configuration_backup_restore">Backup & Restore</string>
|
||||
<string
|
||||
name="title_configuration_backup_restore"
|
||||
>Backup & Restore</string>
|
||||
<string name="title_configuration_backup">Backup config</string>
|
||||
<string name="title_configuration_restore">Restore 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_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_user">Username</string>
|
||||
<string name="title_webdav_pass">Password</string>
|
||||
@@ -449,4 +662,36 @@
|
||||
<item>WebDAV</item>
|
||||
</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>
|
||||
|
||||
@@ -1,58 +1,65 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<!-- Common base theme: put all shared items here so day/night can reuse -->
|
||||
<!-- Common base theme -->
|
||||
<style name="AppThemeBase" parent="Theme.Material3.DayNight">
|
||||
<!-- Primary colors - main tone: black -->
|
||||
<!-- Primary -->
|
||||
<item name="colorPrimary">@color/md_theme_primary</item>
|
||||
<item name="colorOnPrimary">@color/md_theme_onPrimary</item>
|
||||
<item name="colorPrimaryContainer">@color/md_theme_primaryContainer</item>
|
||||
<item name="colorOnPrimaryContainer">@color/md_theme_onPrimaryContainer</item>
|
||||
|
||||
<!-- Secondary -->
|
||||
<item name="colorSecondary">@color/md_theme_secondary</item>
|
||||
<item name="colorOnSecondary">@color/md_theme_onSecondary</item>
|
||||
<item name="colorSecondaryContainer">@color/md_theme_secondaryContainer</item>
|
||||
<item name="colorOnSecondaryContainer">@color/md_theme_onSecondaryContainer</item>
|
||||
|
||||
<!-- Tertiary -->
|
||||
<item name="colorTertiary">@color/md_theme_tertiary</item>
|
||||
<item name="colorOnTertiary">@color/md_theme_onTertiary</item>
|
||||
<item name="colorTertiaryContainer">@color/md_theme_tertiaryContainer</item>
|
||||
<item name="colorOnTertiaryContainer">@color/md_theme_onTertiaryContainer</item>
|
||||
|
||||
<!-- Error colors -->
|
||||
<!-- Error -->
|
||||
<item name="colorError">@color/md_theme_error</item>
|
||||
<item name="colorOnError">@color/md_theme_onError</item>
|
||||
<item name="colorErrorContainer">@color/md_theme_errorContainer</item>
|
||||
<item name="colorOnErrorContainer">@color/md_theme_onErrorContainer</item>
|
||||
|
||||
<!-- Surface colors -->
|
||||
<!-- Surface -->
|
||||
<item name="colorSurface">@color/md_theme_surface</item>
|
||||
<item name="colorOnSurface">@color/md_theme_onSurface</item>
|
||||
<item name="colorSurfaceVariant">@color/md_theme_surfaceVariant</item>
|
||||
<item name="colorOnSurfaceVariant">@color/md_theme_onSurfaceVariant</item>
|
||||
<item name="colorSurfaceInverse">@color/md_theme_inverseSurface</item>
|
||||
<item name="colorOnSurfaceInverse">@color/md_theme_inverseOnSurface</item>
|
||||
<item name="colorSurfaceContainer">@color/md_theme_surface</item>
|
||||
<item name="colorSurfaceContainerHigh">@color/md_theme_surface</item>
|
||||
<item name="colorSurfaceContainerHighest">@color/md_theme_surface</item>
|
||||
<item name="colorSurfaceContainerLow">@color/md_theme_surface</item>
|
||||
<item name="colorSurfaceContainerLowest">@color/md_theme_surface</item>
|
||||
<item name="colorSurfaceContainer">@color/md_theme_surfaceContainer</item>
|
||||
<item name="colorSurfaceContainerHigh">@color/md_theme_surfaceContainerHigh</item>
|
||||
<item name="colorSurfaceContainerHighest">@color/md_theme_surfaceContainerHighest</item>
|
||||
<item name="colorSurfaceContainerLow">@color/md_theme_surfaceContainerLow</item>
|
||||
<item name="colorSurfaceContainerLowest">@color/md_theme_surfaceContainerLowest</item>
|
||||
|
||||
<!-- Background colors -->
|
||||
<!-- Background -->
|
||||
<item name="android:colorBackground">@color/md_theme_background</item>
|
||||
<item name="colorOnBackground">@color/md_theme_onBackground</item>
|
||||
|
||||
<!-- Outline colors -->
|
||||
<!-- Outline -->
|
||||
<item name="colorOutline">@color/md_theme_outline</item>
|
||||
<item name="colorOutlineVariant">@color/md_theme_outlineVariant</item>
|
||||
|
||||
<!-- Other colors -->
|
||||
<!-- Misc -->
|
||||
<item name="colorPrimaryInverse">@color/md_theme_inversePrimary</item>
|
||||
|
||||
<!-- Status bar and navigation bar - system bars -->
|
||||
<!-- System bars — transparent so edge-to-edge works -->
|
||||
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||
<item name="android:navigationBarColor">@color/md_theme_surface</item>
|
||||
<item name="android:navigationBarColor">@android:color/transparent</item>
|
||||
|
||||
<!-- Typography: use Roboto (Google's Material font) everywhere -->
|
||||
<!-- Shape — M3 uses larger corner radii -->
|
||||
<item name="shapeAppearanceSmallComponent">@style/ShapeAppearance.App.SmallComponent</item>
|
||||
<item name="shapeAppearanceMediumComponent">@style/ShapeAppearance.App.MediumComponent</item>
|
||||
<item name="shapeAppearanceLargeComponent">@style/ShapeAppearance.App.LargeComponent</item>
|
||||
|
||||
<!-- Typography: Roboto -->
|
||||
<item name="android:fontFamily">sans-serif</item>
|
||||
<item name="fontFamily">sans-serif</item>
|
||||
<item name="android:editTextStyle">@style/RobotoEditTextStyle</item>
|
||||
@@ -60,7 +67,29 @@
|
||||
<item name="android:dialogTheme">@style/RobotoAlertDialogTheme</item>
|
||||
</style>
|
||||
|
||||
<style name="RobotoEditTextStyle" parent="Widget.AppCompat.EditText">
|
||||
<!-- M3 Shape tokens -->
|
||||
<style name="ShapeAppearance.App.SmallComponent" parent="ShapeAppearance.Material3.SmallComponent">
|
||||
<item name="cornerFamily">rounded</item>
|
||||
<item name="cornerSize">8dp</item>
|
||||
</style>
|
||||
|
||||
<style name="ShapeAppearance.App.MediumComponent" parent="ShapeAppearance.Material3.MediumComponent">
|
||||
<item name="cornerFamily">rounded</item>
|
||||
<item name="cornerSize">12dp</item>
|
||||
</style>
|
||||
|
||||
<style name="ShapeAppearance.App.LargeComponent" parent="ShapeAppearance.Material3.LargeComponent">
|
||||
<item name="cornerFamily">rounded</item>
|
||||
<item name="cornerSize">16dp</item>
|
||||
</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">
|
||||
<item name="android:fontFamily">sans-serif</item>
|
||||
<item name="android:textSize">16sp</item>
|
||||
</style>
|
||||
@@ -68,16 +97,19 @@
|
||||
<style name="RobotoAlertDialogTheme" parent="ThemeOverlay.Material3.MaterialAlertDialog">
|
||||
<item name="android:fontFamily">sans-serif</item>
|
||||
<item name="fontFamily">sans-serif</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>
|
||||
|
||||
<!-- Day/Night theme: inherit common values and set light-mode-specific items -->
|
||||
<!-- Light -->
|
||||
<style name="AppThemeDayNight" parent="AppThemeBase">
|
||||
<!-- day/night-specific overrides (light/default) -->
|
||||
<item name="android:windowLightStatusBar">true</item>
|
||||
<item name="android:windowLightNavigationBar" tools:targetApi="27">true</item>
|
||||
</style>
|
||||
|
||||
<!-- Theme without ActionBar -->
|
||||
<style name="AppThemeDayNight.NoActionBar" parent="AppThemeDayNight">
|
||||
<item name="windowActionBar">false</item>
|
||||
<item name="windowNoTitle">true</item>
|
||||
@@ -89,8 +121,40 @@
|
||||
<item name="android:windowIsTranslucent">true</item>
|
||||
</style>
|
||||
|
||||
<!-- Switch uses primary color -->
|
||||
<style name="BrandedSwitch" parent="AppThemeDayNight">
|
||||
<item name="colorPrimary">@color/color_fab_active</item>
|
||||
<item name="colorPrimary">@color/md_theme_primary</item>
|
||||
</style>
|
||||
|
||||
<!-- Rounded-square FAB shape overlay (16dp corners) -->
|
||||
<style name="ShapeAppearance.App.RoundedSquare" parent="ShapeAppearance.Material3.SmallComponent">
|
||||
<item name="cornerFamily">rounded</item>
|
||||
<item name="cornerSize">16dp</item>
|
||||
</style>
|
||||
|
||||
<!-- Square icon button: no insets, no minHeight, no text -->
|
||||
<style name="Widget.ActionSquareButton" parent="Widget.Material3.Button">
|
||||
<item name="android:minWidth">0dp</item>
|
||||
<item name="android:minHeight">0dp</item>
|
||||
<item name="android:insetTop">0dp</item>
|
||||
<item name="android:insetBottom">0dp</item>
|
||||
<item name="android:insetLeft">0dp</item>
|
||||
<item name="android:insetRight">0dp</item>
|
||||
<item name="android:paddingStart">0dp</item>
|
||||
<item name="android:paddingEnd">0dp</item>
|
||||
<item name="android:paddingTop">0dp</item>
|
||||
<item name="android:paddingBottom">0dp</item>
|
||||
<item name="iconGravity">textStart</item>
|
||||
<item name="iconPadding">0dp</item>
|
||||
<item name="android:text"></item>
|
||||
<item name="shapeAppearance">@style/ShapeAppearance.App.RoundedSquare</item>
|
||||
<item name="elevation">0dp</item>
|
||||
<item name="android:stateListAnimator">@null</item>
|
||||
</style>
|
||||
|
||||
<!-- Tab label -->
|
||||
<style name="TabLayoutTextStyle" parent="TextAppearance.Material3.LabelLarge">
|
||||
<item name="android:textSize">14sp</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
||||
@@ -34,6 +34,12 @@
|
||||
android:summary="@string/summary_pref_show_copy_button"
|
||||
android:title="@string/title_pref_show_copy_button" />
|
||||
|
||||
<CheckBoxPreference
|
||||
android:key="pref_show_server_ip"
|
||||
android:defaultValue="false"
|
||||
android:summary="@string/summary_pref_show_server_ip"
|
||||
android:title="@string/title_pref_show_server_ip" />
|
||||
|
||||
<ListPreference
|
||||
android:defaultValue="auto"
|
||||
android:entries="@array/language_select"
|
||||
@@ -310,4 +316,4 @@
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
</PreferenceScreen>
|
||||
</PreferenceScreen>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name" translatable="false">olcNG</string>
|
||||
<string name="app_name" translatable="false">olcng</string>
|
||||
</resources>
|
||||
@@ -2,17 +2,17 @@
|
||||
agp = "9.1.0"
|
||||
desugarJdkLibs = "2.1.5"
|
||||
gradleLicensePlugin = "0.9.8"
|
||||
kotlin = "2.1.0"
|
||||
coreKtx = "1.17.0"
|
||||
kotlin = "2.2.0"
|
||||
coreKtx = "1.18.0"
|
||||
junit = "4.13.2"
|
||||
junitVersion = "1.3.0"
|
||||
espressoCore = "3.7.0"
|
||||
appcompat = "1.7.1"
|
||||
material = "1.13.0"
|
||||
activity = "1.12.4"
|
||||
material = "1.14.0"
|
||||
activity = "1.13.0"
|
||||
constraintlayout = "2.2.1"
|
||||
mmkvStatic = "1.3.16"
|
||||
gson = "2.13.2"
|
||||
gson = "2.14.0"
|
||||
okhttp = "5.3.2"
|
||||
quickieFoss = "1.14.0"
|
||||
kotlinxCoroutinesAndroid = "1.10.2"
|
||||
@@ -21,10 +21,11 @@ swiperefreshlayout = "1.2.0"
|
||||
toasty = "1.5.2"
|
||||
editorkit = "2.9.0"
|
||||
core = "3.5.4"
|
||||
workRuntimeKtx = "2.11.1"
|
||||
workRuntimeKtx = "2.11.2"
|
||||
lifecycleViewmodelKtx = "2.10.0"
|
||||
multidex = "2.0.1"
|
||||
mockitoMockitoInline = "5.2.0"
|
||||
mockitoKotlin = "5.4.0"
|
||||
flexbox = "3.0.0"
|
||||
preferenceKtx = "1.2.1"
|
||||
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" }
|
||||
multidex = { module = "androidx.multidex:multidex", version.ref = "multidex" }
|
||||
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" }
|
||||
recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview" }
|
||||
preference-ktx = { module = "androidx.preference:preference-ktx", version.ref = "preferenceKtx" }
|
||||
|
||||
BIN
Binary file not shown.
+5
-2
@@ -1,6 +1,9 @@
|
||||
#Thu Nov 14 12:42:51 BDT 2024
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
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
|
||||
zipStorePath=wrapper/dists
|
||||
|
||||
Vendored
+179
-116
@@ -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");
|
||||
# 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
|
||||
# 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
|
||||
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
app_path=$0
|
||||
|
||||
# Need this for daisy-chained symlinks.
|
||||
while
|
||||
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
|
||||
[ -h "$app_path" ]
|
||||
do
|
||||
ls=$( ls -ld "$app_path" )
|
||||
link=${ls#*' -> '}
|
||||
case $link in #(
|
||||
/*) app_path=$link ;; #(
|
||||
*) app_path=$APP_HOME$link ;;
|
||||
esac
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# 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"'
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
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.
|
||||
MAX_FD="maximum"
|
||||
MAX_FD=maximum
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
} >&2
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
} >&2
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
case "$( uname )" in #(
|
||||
CYGWIN* ) cygwin=true ;; #(
|
||||
Darwin* ) darwin=true ;; #(
|
||||
MSYS* | MINGW* ) msys=true ;; #(
|
||||
NONSTOP* ) nonstop=true ;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
JAVACMD=$JAVA_HOME/jre/sh/java
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
JAVACMD=$JAVA_HOME/bin/java
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
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."
|
||||
fi
|
||||
else
|
||||
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.
|
||||
JAVACMD=java
|
||||
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
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 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" ;;
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=`save "$@"`
|
||||
# Collect all arguments for the java command, stacking in reverse order:
|
||||
# * args from the command line
|
||||
# * the main class name
|
||||
# * -classpath
|
||||
# * -D...appname settings
|
||||
# * --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
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
# For Cygwin or MSYS, switch paths to Windows format before running java
|
||||
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" "$@"
|
||||
|
||||
Vendored
+82
-89
@@ -1,89 +1,82 @@
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
: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
|
||||
@rem
|
||||
@rem Copyright 2015 the original author or authors.
|
||||
@rem
|
||||
@rem Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@rem you may not use this file except in compliance with the License.
|
||||
@rem You may obtain a copy of the License at
|
||||
@rem
|
||||
@rem https://www.apache.org/licenses/LICENSE-2.0
|
||||
@rem
|
||||
@rem Unless required by applicable law or agreed to in writing, software
|
||||
@rem distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@rem See the License for the specific language governing permissions and
|
||||
@rem limitations under the License.
|
||||
@rem
|
||||
@rem SPDX-License-Identifier: Apache-2.0
|
||||
@rem
|
||||
|
||||
@if "%DEBUG%"=="" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables, and ensure extensions are enabled
|
||||
setlocal EnableExtensions
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
|
||||
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if %ERRORLEVEL% equ 0 goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
"%COMSPEC%" /c exit 1
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto execute
|
||||
|
||||
echo. 1>&2
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||
echo. 1>&2
|
||||
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||
echo location of your Java installation. 1>&2
|
||||
|
||||
"%COMSPEC%" /c exit 1
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
|
||||
|
||||
@rem Execute Gradle
|
||||
@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
|
||||
|
||||
:exitWithErrorLevel
|
||||
@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts
|
||||
"%COMSPEC%" /c exit %ERRORLEVEL%
|
||||
|
||||
@@ -10,8 +10,14 @@ __base="$(basename "${__file}" .sh)"
|
||||
|
||||
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}
|
||||
if [[ ! -d $NDK_HOME ]]; then
|
||||
export NDK_HOME=$ANDROID_HOME/ndk/25.2.9519653
|
||||
fi
|
||||
|
||||
if [[ ! -d $NDK_HOME ]]; then
|
||||
echo "Android NDK: NDK_HOME not found. please set env \$NDK_HOME"
|
||||
|
||||
Reference in New Issue
Block a user