mirror of
https://github.com/MatsuriDayo/NekoBoxForAndroid.git
synced 2025-12-19 22:50:05 +08:00
update share link
This commit is contained in:
parent
6051efd5aa
commit
810058ccd4
@ -97,6 +97,7 @@ class TrafficLooper
|
||||
if (proxy.config.selectorGroupId >= 0L) {
|
||||
itemMain = TrafficUpdater.TrafficLooperData(tag = TAG_PROXY)
|
||||
itemMainBase = TrafficUpdater.TrafficLooperData(tag = TAG_PROXY)
|
||||
tags.add(TAG_PROXY)
|
||||
}
|
||||
//
|
||||
trafficUpdater = TrafficUpdater(
|
||||
|
||||
@ -20,7 +20,6 @@ import io.nekohasekai.sagernet.fmt.socks.SOCKSBean
|
||||
import io.nekohasekai.sagernet.fmt.socks.toUri
|
||||
import io.nekohasekai.sagernet.fmt.ssh.SSHBean
|
||||
import io.nekohasekai.sagernet.fmt.trojan.TrojanBean
|
||||
import io.nekohasekai.sagernet.fmt.trojan.toUri
|
||||
import io.nekohasekai.sagernet.fmt.trojan_go.TrojanGoBean
|
||||
import io.nekohasekai.sagernet.fmt.trojan_go.buildTrojanGoConfig
|
||||
import io.nekohasekai.sagernet.fmt.trojan_go.toUri
|
||||
@ -236,23 +235,18 @@ data class ProxyEntity(
|
||||
}
|
||||
}
|
||||
|
||||
fun toLink(compact: Boolean = false): String? = with(requireBean()) {
|
||||
fun toStdLink(compact: Boolean = false): String = with(requireBean()) {
|
||||
when (this) {
|
||||
is SOCKSBean -> toUri()
|
||||
is HttpBean -> toUri()
|
||||
is ShadowsocksBean -> toUri()
|
||||
is VMessBean -> if (compact) toV2rayN() else toUri()
|
||||
is TrojanBean -> toUri()
|
||||
is VMessBean -> toUriVMessVLESSTrojan(false)
|
||||
is TrojanBean -> toUriVMessVLESSTrojan(true)
|
||||
is TrojanGoBean -> toUri()
|
||||
is NaiveBean -> toUri()
|
||||
is HysteriaBean -> toUri()
|
||||
is SSHBean -> toUniversalLink()
|
||||
is WireGuardBean -> toUniversalLink()
|
||||
is TuicBean -> toUniversalLink()
|
||||
is ShadowTLSBean -> toUniversalLink()
|
||||
is ConfigBean -> toUniversalLink()
|
||||
is NekoBean -> shareLink()
|
||||
else -> null
|
||||
else -> toUniversalLink()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
package io.nekohasekai.sagernet.fmt.trojan
|
||||
|
||||
import io.nekohasekai.sagernet.fmt.v2ray.parseDuckSoft
|
||||
import io.nekohasekai.sagernet.fmt.v2ray.toUri
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
|
||||
fun parseTrojan(server: String): TrojanBean {
|
||||
@ -17,7 +16,3 @@ fun parseTrojan(server: String): TrojanBean {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun TrojanBean.toUri(): String {
|
||||
return toUri(true).replace("vmess://", "trojan://")
|
||||
}
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
package io.nekohasekai.sagernet.fmt.v2ray
|
||||
|
||||
import android.text.TextUtils
|
||||
import com.google.gson.Gson
|
||||
import io.nekohasekai.sagernet.fmt.http.HttpBean
|
||||
import io.nekohasekai.sagernet.fmt.trojan.TrojanBean
|
||||
@ -10,6 +11,24 @@ import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import org.json.JSONObject
|
||||
|
||||
data class VmessQRCode(
|
||||
var v: String = "",
|
||||
var ps: String = "",
|
||||
var add: String = "",
|
||||
var port: String = "",
|
||||
var id: String = "",
|
||||
var aid: String = "0",
|
||||
var scy: String = "",
|
||||
var net: String = "",
|
||||
var type: String = "",
|
||||
var host: String = "",
|
||||
var path: String = "",
|
||||
var tls: String = "",
|
||||
var sni: String = "",
|
||||
var alpn: String = "",
|
||||
var fp: String = "",
|
||||
)
|
||||
|
||||
fun StandardV2RayBean.isTLS(): Boolean {
|
||||
return security == "tls"
|
||||
}
|
||||
@ -104,7 +123,6 @@ fun parseV2Ray(link: String): StandardV2RayBean {
|
||||
}
|
||||
|
||||
// https://github.com/XTLS/Xray-core/issues/91
|
||||
// NO allowInsecure
|
||||
fun StandardV2RayBean.parseDuckSoft(url: HttpUrl) {
|
||||
serverAddress = url.host
|
||||
serverPort = url.port
|
||||
@ -116,27 +134,26 @@ fun StandardV2RayBean.parseDuckSoft(url: HttpUrl) {
|
||||
uuid = url.username
|
||||
}
|
||||
|
||||
// not ducksoft fmt path
|
||||
if (url.pathSegments.size > 1 || url.pathSegments[0].isNotBlank()) {
|
||||
path = url.pathSegments.joinToString("/")
|
||||
}
|
||||
|
||||
type = url.queryParameter("type") ?: "tcp"
|
||||
if (type == "h2") type = "http"
|
||||
|
||||
security = url.queryParameter("security")
|
||||
if (security == null) {
|
||||
security = if (this is TrojanBean) {
|
||||
"tls"
|
||||
} else {
|
||||
"none"
|
||||
}
|
||||
security = if (this is TrojanBean) "tls" else "none"
|
||||
}
|
||||
|
||||
when (security) {
|
||||
"tls" -> {
|
||||
"tls", "reality" -> {
|
||||
url.queryParameter("sni")?.let {
|
||||
sni = it
|
||||
}
|
||||
url.queryParameter("alpn")?.let {
|
||||
alpn = it
|
||||
alpn = it.replace(",", "\n")
|
||||
}
|
||||
url.queryParameter("cert")?.let {
|
||||
certificates = it
|
||||
@ -144,6 +161,15 @@ fun StandardV2RayBean.parseDuckSoft(url: HttpUrl) {
|
||||
}
|
||||
}
|
||||
when (type) {
|
||||
"tcp" -> {
|
||||
// v2rayNG
|
||||
if (url.queryParameter("headerType") == "http") {
|
||||
url.queryParameter("host")?.let {
|
||||
type = "http"
|
||||
host = it
|
||||
}
|
||||
}
|
||||
}
|
||||
"http" -> {
|
||||
url.queryParameter("host")?.let {
|
||||
host = it
|
||||
@ -251,63 +277,43 @@ fun parseV2RayN(link: String): VMessBean {
|
||||
return parseCsvVMess(result)
|
||||
}
|
||||
val bean = VMessBean()
|
||||
val json = JSONObject(result)
|
||||
val vmessQRCode = Gson().fromJson(result, VmessQRCode::class.java)
|
||||
|
||||
bean.serverAddress = json.getStr("add") ?: ""
|
||||
bean.serverPort = json.getIntNya("port") ?: 1080
|
||||
bean.encryption = json.getStr("scy") ?: ""
|
||||
bean.uuid = json.getStr("id") ?: ""
|
||||
bean.alterId = json.getIntNya("aid") ?: 0
|
||||
bean.type = json.getStr("net") ?: ""
|
||||
bean.host = json.getStr("host") ?: ""
|
||||
bean.path = json.getStr("path") ?: ""
|
||||
// Although VmessQRCode fields are non null, looks like Gson may still create null fields
|
||||
if (TextUtils.isEmpty(vmessQRCode.add)
|
||||
|| TextUtils.isEmpty(vmessQRCode.port)
|
||||
|| TextUtils.isEmpty(vmessQRCode.id)
|
||||
|| TextUtils.isEmpty(vmessQRCode.net)
|
||||
) {
|
||||
throw Exception("invalid VmessQRCode")
|
||||
}
|
||||
|
||||
bean.serverAddress = vmessQRCode.add
|
||||
bean.serverPort = vmessQRCode.port.toIntOrNull()
|
||||
bean.encryption = vmessQRCode.scy
|
||||
bean.uuid = vmessQRCode.id
|
||||
bean.alterId = vmessQRCode.aid.toIntOrNull()
|
||||
bean.type = vmessQRCode.type
|
||||
bean.host = vmessQRCode.host
|
||||
bean.path = vmessQRCode.path
|
||||
val headerType = vmessQRCode.type
|
||||
|
||||
when (bean.type) {
|
||||
"grpc" -> {
|
||||
bean.path = bean.path
|
||||
"tcp" -> {
|
||||
if (headerType == "http") {
|
||||
bean.type = "http"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bean.name = json.getStr("ps") ?: ""
|
||||
bean.sni = json.getStr("sni") ?: bean.host
|
||||
bean.security = json.getStr("tls") ?: "none"
|
||||
bean.utlsFingerprint = json.getStr("fp") ?: ""
|
||||
|
||||
if (json.optInt("v", 2) < 2) {
|
||||
when (bean.type) {
|
||||
"ws" -> {
|
||||
var path = ""
|
||||
var host = ""
|
||||
val lstParameter = bean.host.split(";")
|
||||
if (lstParameter.isNotEmpty()) {
|
||||
path = lstParameter[0].trim()
|
||||
}
|
||||
if (lstParameter.size > 1) {
|
||||
path = lstParameter[0].trim()
|
||||
host = lstParameter[1].trim()
|
||||
}
|
||||
bean.path = path
|
||||
bean.host = host
|
||||
}
|
||||
"h2" -> {
|
||||
var path = ""
|
||||
var host = ""
|
||||
val lstParameter = bean.host.split(";")
|
||||
if (lstParameter.isNotEmpty()) {
|
||||
path = lstParameter[0].trim()
|
||||
}
|
||||
if (lstParameter.size > 1) {
|
||||
path = lstParameter[0].trim()
|
||||
host = lstParameter[1].trim()
|
||||
}
|
||||
bean.path = path
|
||||
bean.host = host
|
||||
}
|
||||
}
|
||||
}
|
||||
bean.name = vmessQRCode.ps
|
||||
bean.security = vmessQRCode.tls
|
||||
if (bean.security == "reality") bean.security = "tls"
|
||||
bean.sni = vmessQRCode.sni
|
||||
bean.alpn = vmessQRCode.alpn.replace(",", "\n")
|
||||
bean.utlsFingerprint = vmessQRCode.fp
|
||||
|
||||
return bean
|
||||
|
||||
}
|
||||
|
||||
private fun parseCsvVMess(csv: String): VMessBean {
|
||||
@ -345,61 +351,61 @@ private fun parseCsvVMess(csv: String): VMessBean {
|
||||
|
||||
}
|
||||
|
||||
data class VmessQRCode(
|
||||
var v: String = "",
|
||||
var ps: String = "",
|
||||
var add: String = "",
|
||||
var port: String = "",
|
||||
var id: String = "",
|
||||
var aid: String = "0",
|
||||
var scy: String = "",
|
||||
var net: String = "",
|
||||
var type: String = "",
|
||||
var host: String = "",
|
||||
var path: String = "",
|
||||
var tls: String = "",
|
||||
var sni: String = "",
|
||||
var alpn: String = "",
|
||||
var fp: String = "",
|
||||
)
|
||||
|
||||
fun VMessBean.toV2rayN(): String {
|
||||
if (isVLESS) return toUri(true)
|
||||
val bean = this
|
||||
return "vmess://" + VmessQRCode().apply {
|
||||
v = "2"
|
||||
ps = this@toV2rayN.name
|
||||
add = this@toV2rayN.serverAddress
|
||||
port = this@toV2rayN.serverPort.toString()
|
||||
id = this@toV2rayN.uuid
|
||||
aid = this@toV2rayN.alterId.toString()
|
||||
net = this@toV2rayN.type
|
||||
host = this@toV2rayN.host
|
||||
path = this@toV2rayN.path
|
||||
ps = bean.name
|
||||
add = bean.serverAddress
|
||||
port = bean.serverPort.toString()
|
||||
id = bean.uuid
|
||||
aid = bean.alterId.toString()
|
||||
net = bean.type
|
||||
host = bean.host
|
||||
path = bean.path
|
||||
|
||||
when (net) {
|
||||
"grpc" -> {
|
||||
path = this@toV2rayN.path
|
||||
"http" -> {
|
||||
if (!isTLS()) {
|
||||
type = "http"
|
||||
net = "tcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tls = if (this@toV2rayN.security == "tls") "tls" else ""
|
||||
sni = this@toV2rayN.sni
|
||||
scy = this@toV2rayN.encryption
|
||||
fp = this@toV2rayN.utlsFingerprint
|
||||
if (isTLS()) {
|
||||
tls = "tls"
|
||||
if (bean.realityPubKey.isNotBlank()) {
|
||||
tls = "reality"
|
||||
}
|
||||
}
|
||||
|
||||
scy = bean.encryption
|
||||
sni = bean.sni
|
||||
alpn = bean.alpn.replace("\n", ",")
|
||||
fp = bean.utlsFingerprint
|
||||
}.let {
|
||||
NGUtil.encode(Gson().toJson(it))
|
||||
}
|
||||
}
|
||||
|
||||
fun StandardV2RayBean.toUri(standard: Boolean = true): String {
|
||||
if (this is VMessBean && alterId > 0) return toV2rayN()
|
||||
fun StandardV2RayBean.toUriVMessVLESSTrojan(isTrojan: Boolean): String {
|
||||
// VMess
|
||||
if (this is VMessBean && !isVLESS) {
|
||||
return toV2rayN()
|
||||
}
|
||||
|
||||
// VLESS & Trojan (ducksoft fmt)
|
||||
val builder = linkBuilder()
|
||||
.username(if (this is TrojanBean) password else uuid)
|
||||
.host(serverAddress)
|
||||
.port(serverPort)
|
||||
.addQueryParameter("type", type)
|
||||
if (this !is TrojanBean) builder.addQueryParameter("encryption", encryption)
|
||||
|
||||
if (isVLESS) {
|
||||
builder.addQueryParameter("encryption", "none")
|
||||
if (encryption != "auto") builder.addQueryParameter("flow", encryption)
|
||||
}
|
||||
|
||||
when (type) {
|
||||
"tcp" -> {}
|
||||
@ -408,11 +414,7 @@ fun StandardV2RayBean.toUri(standard: Boolean = true): String {
|
||||
builder.addQueryParameter("host", host)
|
||||
}
|
||||
if (path.isNotBlank()) {
|
||||
if (standard) {
|
||||
builder.addQueryParameter("path", path)
|
||||
} else {
|
||||
builder.encodedPath(path.pathSafe())
|
||||
}
|
||||
}
|
||||
if (type == "ws") {
|
||||
if (wsMaxEarlyData > 0) {
|
||||
@ -422,12 +424,13 @@ fun StandardV2RayBean.toUri(standard: Boolean = true): String {
|
||||
}
|
||||
}
|
||||
} else if (type == "http" && !isTLS()) {
|
||||
return "" // no fmt?
|
||||
builder.setQueryParameter("type", "tcp")
|
||||
builder.addQueryParameter("headerType", "http")
|
||||
}
|
||||
}
|
||||
"grpc" -> {
|
||||
if (path.isNotBlank()) {
|
||||
builder.addQueryParameter("serviceName", path)
|
||||
builder.setQueryParameter("serviceName", path)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -440,7 +443,7 @@ fun StandardV2RayBean.toUri(standard: Boolean = true): String {
|
||||
builder.addQueryParameter("sni", sni)
|
||||
}
|
||||
if (alpn.isNotBlank()) {
|
||||
builder.addQueryParameter("alpn", alpn)
|
||||
builder.addQueryParameter("alpn", alpn.replace("\n", ","))
|
||||
}
|
||||
if (certificates.isNotBlank()) {
|
||||
builder.addQueryParameter("cert", certificates)
|
||||
@ -468,10 +471,7 @@ fun StandardV2RayBean.toUri(standard: Boolean = true): String {
|
||||
builder.encodedFragment(name.urlSafe())
|
||||
}
|
||||
|
||||
// TODO vless flow: bean.encryption != "auto"
|
||||
|
||||
return builder.toLink(if (isVLESS) "vless" else "vmess")
|
||||
|
||||
return builder.toLink(if (isTrojan) "trojan" else "vless")
|
||||
}
|
||||
|
||||
fun buildSingBoxOutboundStreamSettings(bean: StandardV2RayBean): V2RayTransportOptions? {
|
||||
|
||||
@ -43,7 +43,6 @@ import io.nekohasekai.sagernet.databinding.LayoutProfileListBinding
|
||||
import io.nekohasekai.sagernet.databinding.LayoutProgressListBinding
|
||||
import io.nekohasekai.sagernet.fmt.AbstractBean
|
||||
import io.nekohasekai.sagernet.fmt.toUniversalLink
|
||||
import io.nekohasekai.sagernet.fmt.v2ray.toV2rayN
|
||||
import io.nekohasekai.sagernet.group.RawUpdater
|
||||
import io.nekohasekai.sagernet.ktx.*
|
||||
import io.nekohasekai.sagernet.plugin.PluginManager
|
||||
@ -1470,11 +1469,6 @@ class ConfigurationFragment @JvmOverloads constructor(
|
||||
val popup = PopupMenu(requireContext(), anchor)
|
||||
popup.menuInflater.inflate(R.menu.profile_share_menu, popup.menu)
|
||||
|
||||
if (proxyEntity.vmessBean == null || proxyEntity.vmessBean!!.isVLESS) {
|
||||
popup.menu.findItem(R.id.action_group_qr).subMenu?.removeItem(R.id.action_v2rayn_qr)
|
||||
popup.menu.findItem(R.id.action_group_clipboard).subMenu?.removeItem(R.id.action_v2rayn_clipboard)
|
||||
}
|
||||
|
||||
when {
|
||||
!proxyEntity.haveStandardLink() -> {
|
||||
popup.menu.findItem(R.id.action_group_qr).subMenu?.removeItem(R.id.action_standard_qr)
|
||||
@ -1527,14 +1521,12 @@ class ConfigurationFragment @JvmOverloads constructor(
|
||||
try {
|
||||
currentName = entity.displayName()!!
|
||||
when (item.itemId) {
|
||||
R.id.action_standard_qr -> showCode(entity.toLink()!!)
|
||||
R.id.action_standard_clipboard -> export(entity.toLink()!!)
|
||||
R.id.action_standard_qr -> showCode(entity.toStdLink()!!)
|
||||
R.id.action_standard_clipboard -> export(entity.toStdLink()!!)
|
||||
R.id.action_universal_qr -> showCode(entity.requireBean().toUniversalLink())
|
||||
R.id.action_universal_clipboard -> export(
|
||||
entity.requireBean().toUniversalLink()
|
||||
)
|
||||
R.id.action_v2rayn_qr -> showCode(entity.vmessBean!!.toV2rayN())
|
||||
R.id.action_v2rayn_clipboard -> export(entity.vmessBean!!.toV2rayN())
|
||||
R.id.action_config_export_clipboard -> export(entity.exportConfig().first)
|
||||
R.id.action_config_export_file -> {
|
||||
val cfg = entity.exportConfig()
|
||||
|
||||
@ -130,11 +130,12 @@ class GroupFragment : ToolbarFragment(R.layout.layout_group),
|
||||
|
||||
private lateinit var selectedGroup: ProxyGroup
|
||||
|
||||
private val exportProfiles = registerForActivityResult(ActivityResultContracts.CreateDocument()) { data ->
|
||||
private val exportProfiles =
|
||||
registerForActivityResult(ActivityResultContracts.CreateDocument()) { data ->
|
||||
if (data != null) {
|
||||
runOnDefaultDispatcher {
|
||||
val profiles = SagerDatabase.proxyDao.getByGroup(selectedGroup.id)
|
||||
val links = profiles.mapNotNull { it.toLink(compact = true) }.joinToString("\n")
|
||||
val links = profiles.joinToString("\n") { it.toStdLink(compact = true) }
|
||||
try {
|
||||
(requireActivity() as MainActivity).contentResolver.openOutputStream(
|
||||
data
|
||||
@ -310,7 +311,8 @@ class GroupFragment : ToolbarFragment(R.layout.layout_group),
|
||||
undoManager.flush()
|
||||
}
|
||||
|
||||
inner class GroupHolder(binding: LayoutGroupItemBinding) : RecyclerView.ViewHolder(binding.root),
|
||||
inner class GroupHolder(binding: LayoutGroupItemBinding) :
|
||||
RecyclerView.ViewHolder(binding.root),
|
||||
PopupMenu.OnMenuItemClickListener {
|
||||
|
||||
lateinit var proxyGroup: ProxyGroup
|
||||
@ -343,8 +345,7 @@ class GroupFragment : ToolbarFragment(R.layout.layout_group),
|
||||
R.id.action_export_clipboard -> {
|
||||
runOnDefaultDispatcher {
|
||||
val profiles = SagerDatabase.proxyDao.getByGroup(selectedGroup.id)
|
||||
val links = profiles.mapNotNull { it.toLink(compact = true) }
|
||||
.joinToString("\n")
|
||||
val links = profiles.joinToString("\n") { it.toStdLink(compact = true) }
|
||||
onMainDispatcher {
|
||||
SagerNet.trySetPrimaryClip(links)
|
||||
snackbar(getString(R.string.copy_toast_msg)).show()
|
||||
@ -397,7 +398,7 @@ class GroupFragment : ToolbarFragment(R.layout.layout_group),
|
||||
popup.menuInflater.inflate(R.menu.group_action_menu, popup.menu)
|
||||
|
||||
if (proxyGroup.type != GroupType.SUBSCRIPTION) {
|
||||
popup.menu.removeItem(R.id.action_share)
|
||||
popup.menu.removeItem(R.id.action_share_subscription)
|
||||
}
|
||||
popup.setOnMenuItemClickListener(this)
|
||||
popup.show()
|
||||
@ -451,12 +452,12 @@ class GroupFragment : ToolbarFragment(R.layout.layout_group),
|
||||
}
|
||||
groupStatus.setPadding(0)
|
||||
} else if (subscription != null && !subscription.subscriptionUserinfo.isNullOrBlank()) { // Raw
|
||||
var text = "";
|
||||
var text = ""
|
||||
|
||||
fun get(regex: String): String? {
|
||||
return regex.toRegex().findAll(subscription.subscriptionUserinfo).mapNotNull {
|
||||
if (it.groupValues.size > 1) it.groupValues[1] else null
|
||||
}.firstOrNull();
|
||||
}.firstOrNull()
|
||||
}
|
||||
|
||||
var used: Long = 0
|
||||
@ -484,7 +485,7 @@ class GroupFragment : ToolbarFragment(R.layout.layout_group),
|
||||
|
||||
if (text.isNotEmpty()) {
|
||||
groupTraffic.isVisible = true
|
||||
groupTraffic.text = text;
|
||||
groupTraffic.text = text
|
||||
groupStatus.setPadding(0)
|
||||
}
|
||||
} else {
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item
|
||||
android:id="@+id/action_share"
|
||||
android:title="@string/share">
|
||||
android:id="@+id/action_share_subscription"
|
||||
android:title="@string/share_subscription">
|
||||
<menu>
|
||||
<item
|
||||
android:id="@+id/action_universal_clipboard"
|
||||
|
||||
@ -9,10 +9,7 @@
|
||||
android:title="@string/standard" />
|
||||
<item
|
||||
android:id="@+id/action_universal_qr"
|
||||
android:title="@string/app_name" />
|
||||
<item
|
||||
android:id="@+id/action_v2rayn_qr"
|
||||
android:title="@string/v2rayn" />
|
||||
android:title="SN Link" />
|
||||
</menu>
|
||||
</item>
|
||||
<item
|
||||
@ -24,10 +21,7 @@
|
||||
android:title="@string/standard" />
|
||||
<item
|
||||
android:id="@+id/action_universal_clipboard"
|
||||
android:title="@string/app_name" />
|
||||
<item
|
||||
android:id="@+id/action_v2rayn_clipboard"
|
||||
android:title="@string/v2rayn" />
|
||||
android:title="SN Link" />
|
||||
</menu>
|
||||
</item>
|
||||
<item
|
||||
|
||||
@ -466,4 +466,5 @@
|
||||
<string name="front_proxy">前置代理</string>
|
||||
<string name="landing_proxy">落地代理</string>
|
||||
<string name="shadowtls_version">ShadowTLS 版本</string>
|
||||
<string name="share_subscription">分享订阅</string>
|
||||
</resources>
|
||||
@ -507,5 +507,6 @@ Anyone can write advanced plugins, which can control NekoBox. please download an
|
||||
<string name="landing_proxy">Landing Proxy</string>
|
||||
<string name="action_shadowtls" translatable="false">ShadowTLS</string>
|
||||
<string name="shadowtls_version">ShadowTLS Version</string>
|
||||
<string name="share_subscription">Share Subscription</string>
|
||||
|
||||
</resources>
|
||||
Loading…
Reference in New Issue
Block a user