diff --git a/V2rayNG/app/src/main/AndroidManifest.xml b/V2rayNG/app/src/main/AndroidManifest.xml
index de4dad9bc..9efa35471 100644
--- a/V2rayNG/app/src/main/AndroidManifest.xml
+++ b/V2rayNG/app/src/main/AndroidManifest.xml
@@ -93,6 +93,9 @@
+
{
+ val intent = Intent(this, UserAssetUrlActivity::class.java)
+ startActivity(intent)
+ true
+ }
R.id.download_file -> {
downloadGeoFiles()
true
@@ -104,13 +118,27 @@ class UserAssetActivity : BaseActivity() {
}
private val chooseFile =
- registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
+ registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { it ->
val uri = it.data?.data
if (it.resultCode == RESULT_OK && uri != null) {
+ val assetId = Utils.getUuid()
try {
+ val assetItem = AssetUrlItem(
+ getCursorName(uri) ?: uri.toString(),
+ "file"
+ )
+
+ // check remarks unique
+ val assetList = MmkvManager.decodeAssetUrls()
+ if (assetList.any { it.second.remarks == assetItem.remarks && it.first != assetId }) {
+ toast(R.string.msg_remark_is_duplicate)
+ return@registerForActivityResult
+ }
+ assetStorage?.encode(assetId, Gson().toJson(assetItem))
copyFile(uri)
} catch (e: Exception) {
toast(R.string.toast_asset_copy_failed)
+ MmkvManager.removeAssetUrl(assetId)
}
}
}
@@ -143,31 +171,33 @@ class UserAssetActivity : BaseActivity() {
val httpPort = Utils.parseInt(settingsStorage?.decodeString(AppConfig.PREF_HTTP_PORT), AppConfig.PORT_HTTP.toInt())
toast(R.string.msg_downloading_content)
- geofiles.forEach {
+ var assets = MmkvManager.decodeAssetUrls()
+ assets = addBuiltInGeoItems(assets)
+
+ assets.forEach {
//toast(getString(R.string.msg_downloading_content) + it)
lifecycleScope.launch(Dispatchers.IO) {
- val result = downloadGeo(it, 60000, httpPort)
+ val result = downloadGeo(it.second, 60000, httpPort)
launch(Dispatchers.Main) {
if (result) {
- toast(getString(R.string.toast_success) + " " + it)
+ toast(getString(R.string.toast_success) + " " + it.second.remarks)
binding.recyclerView.adapter?.notifyDataSetChanged()
} else {
- toast(getString(R.string.toast_failure) + " " + it)
+ toast(getString(R.string.toast_failure) + " " + it.second.remarks)
}
}
}
}
}
- private fun downloadGeo(name: String, timeout: Int, httpPort: Int): Boolean {
- val url = AppConfig.geoUrl + name
- val targetTemp = File(extDir, name + "_temp")
- val target = File(extDir, name)
+ private fun downloadGeo(item: AssetUrlItem, timeout: Int, httpPort: Int): Boolean {
+ val targetTemp = File(extDir, item.remarks + "_temp")
+ val target = File(extDir, item.remarks)
var conn: HttpURLConnection? = null
//Log.d(AppConfig.ANG_PACKAGE, url)
try {
- conn = URL(url).openConnection(
+ conn = URL(item.url).openConnection(
Proxy(
Proxy.Type.HTTP,
InetSocketAddress("127.0.0.1", httpPort)
@@ -192,33 +222,74 @@ class UserAssetActivity : BaseActivity() {
conn?.disconnect()
}
}
+ private fun addBuiltInGeoItems(assets: List>): List> {
+ val list = mutableListOf>()
+ builtInGeoFiles.forEach {
+ list.add(Utils.getUuid() to AssetUrlItem(
+ it,
+ AppConfig.geoUrl + it
+ )
+ )
+ }
+
+ return list + assets
+ }
inner class UserAssetAdapter : RecyclerView.Adapter() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): UserAssetViewHolder {
- return UserAssetViewHolder(ItemRecyclerUserAssetBinding.inflate(LayoutInflater.from(parent.context), parent, false))
+ return UserAssetViewHolder(
+ ItemRecyclerUserAssetBinding.inflate(
+ LayoutInflater.from(parent.context),
+ parent,
+ false)
+ )
}
@SuppressLint("SetTextI18n")
override fun onBindViewHolder(holder: UserAssetViewHolder, position: Int) {
- val file = extDir.listFiles()?.getOrNull(position) ?: return
- holder.itemUserAssetBinding.assetName.text = file.name
- val dateFormat = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM)
- holder.itemUserAssetBinding.assetProperties.text = "${file.length().toTrafficString()} • ${dateFormat.format(Date(file.lastModified()))}"
- if (file.name in geofiles) {
+ var assets = MmkvManager.decodeAssetUrls();
+ assets = addBuiltInGeoItems(assets);
+ val item = assets.getOrNull(position) ?: return
+// file with name == item.second.remarks
+ val file = extDir.listFiles()?.find { it.name == item.second.remarks }
+
+ holder.itemUserAssetBinding.assetName.text = item.second.remarks
+
+ if (file != null) {
+ val dateFormat = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM)
+ holder.itemUserAssetBinding.assetProperties.text =
+ "${file.length().toTrafficString()} • ${dateFormat.format(Date(file.lastModified()))}"
+ } else {
+ holder.itemUserAssetBinding.assetProperties.text = getString(R.string.msg_file_not_found)
+ }
+
+ if (item.second.remarks in builtInGeoFiles) {
+ holder.itemUserAssetBinding.layoutEdit.visibility = GONE
holder.itemUserAssetBinding.layoutRemove.visibility = GONE
} else {
+ holder.itemUserAssetBinding.layoutEdit.visibility = item.second.url.let { if (it == "file") GONE else VISIBLE }
holder.itemUserAssetBinding.layoutRemove.visibility = VISIBLE
}
+
+ holder.itemUserAssetBinding.layoutEdit.setOnClickListener {
+ val intent = Intent(this@UserAssetActivity, UserAssetUrlActivity::class.java)
+ intent.putExtra("assetId", item.first)
+ startActivity(intent)
+ }
holder.itemUserAssetBinding.layoutRemove.setOnClickListener {
- file.delete()
+ file?.delete()
+ MmkvManager.removeAssetUrl(item.first)
binding.recyclerView.adapter?.notifyItemRemoved(position)
}
}
override fun getItemCount(): Int {
- return extDir.listFiles()?.size ?: 0
+ var assets = MmkvManager.decodeAssetUrls();
+ assets = addBuiltInGeoItems(assets);
+ return assets.size
}
}
- class UserAssetViewHolder(val itemUserAssetBinding: ItemRecyclerUserAssetBinding) : RecyclerView.ViewHolder(itemUserAssetBinding.root)
+ class UserAssetViewHolder(val itemUserAssetBinding: ItemRecyclerUserAssetBinding) :
+ RecyclerView.ViewHolder(itemUserAssetBinding.root)
}
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/UserAssetUrlActivity.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/UserAssetUrlActivity.kt
new file mode 100644
index 000000000..c032bef3f
--- /dev/null
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/ui/UserAssetUrlActivity.kt
@@ -0,0 +1,145 @@
+package com.v2ray.ang.ui
+
+import android.os.Bundle
+import android.text.TextUtils
+import android.view.Menu
+import android.view.MenuItem
+import androidx.appcompat.app.AlertDialog
+import com.google.gson.Gson
+import com.tencent.mmkv.MMKV
+import com.v2ray.ang.R
+import com.v2ray.ang.databinding.ActivityUserAssetUrlBinding
+import com.v2ray.ang.dto.AssetUrlItem
+import com.v2ray.ang.extension.toast
+import com.v2ray.ang.util.MmkvManager
+import com.v2ray.ang.util.Utils
+import java.io.File
+
+class UserAssetUrlActivity : BaseActivity() {
+ private lateinit var binding: ActivityUserAssetUrlBinding
+
+ var del_config: MenuItem? = null
+ var save_config: MenuItem? = null
+
+ val extDir by lazy { File(Utils.userAssetPath(this)) }
+ private val assetStorage by lazy { MMKV.mmkvWithID(MmkvManager.ID_ASSET, MMKV.MULTI_PROCESS_MODE) }
+ private val editAssetId by lazy { intent.getStringExtra("assetId").orEmpty() }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityUserAssetUrlBinding.inflate(layoutInflater)
+ val view = binding.root
+ setContentView(view)
+ title = getString(R.string.title_user_asset_add_url)
+
+ val json = assetStorage?.decodeString(editAssetId)
+ if (!json.isNullOrBlank()) {
+ bindingAsset(Gson().fromJson(json, AssetUrlItem::class.java))
+ } else {
+ clearAsset()
+ }
+ }
+
+ /**
+ * bingding seleced asset config
+ */
+ private fun bindingAsset(assetItem: AssetUrlItem): Boolean {
+ binding.etRemarks.text = Utils.getEditable(assetItem.remarks)
+ binding.etUrl.text = Utils.getEditable(assetItem.url)
+ return true
+ }
+
+ /**
+ * clear or init asset config
+ */
+ private fun clearAsset(): Boolean {
+ binding.etRemarks.text = null
+ binding.etUrl.text = null
+ return true
+ }
+
+ /**
+ * save asset config
+ */
+ private fun saveServer(): Boolean {
+ val assetItem: AssetUrlItem
+ val json = assetStorage?.decodeString(editAssetId)
+ var assetId = editAssetId
+ if (!json.isNullOrBlank()) {
+ assetItem = Gson().fromJson(json, AssetUrlItem::class.java)
+
+ // remove file associated with the asset
+ val file = extDir.resolve(assetItem.remarks)
+ if (file.exists()) {
+ file.delete()
+ }
+ } else {
+ assetId = Utils.getUuid()
+ assetItem = AssetUrlItem()
+ }
+
+ assetItem.remarks = binding.etRemarks.text.toString()
+ assetItem.url = binding.etUrl.text.toString()
+
+ // check remarks unique
+ val assetList = MmkvManager.decodeAssetUrls()
+ if (assetList.any { it.second.remarks == assetItem.remarks && it.first != assetId }) {
+ toast(R.string.msg_remark_is_duplicate)
+ return false
+ }
+
+
+ if (TextUtils.isEmpty(assetItem.remarks)) {
+ toast(R.string.sub_setting_remarks)
+ return false
+ }
+ if (TextUtils.isEmpty(assetItem.url)) {
+ toast(R.string.title_url)
+ return false
+ }
+
+ assetStorage?.encode(assetId, Gson().toJson(assetItem))
+ toast(R.string.toast_success)
+ finish()
+ return true
+ }
+
+ /**
+ * save server config
+ */
+ private fun deleteServer(): Boolean {
+ if (editAssetId.isNotEmpty()) {
+ AlertDialog.Builder(this).setMessage(R.string.del_config_comfirm)
+ .setPositiveButton(android.R.string.ok) { _, _ ->
+ MmkvManager.removeAssetUrl(editAssetId)
+ finish()
+ }
+ .show()
+ }
+ return true
+ }
+
+ override fun onCreateOptionsMenu(menu: Menu): Boolean {
+ menuInflater.inflate(R.menu.action_server, menu)
+ del_config = menu.findItem(R.id.del_config)
+ save_config = menu.findItem(R.id.save_config)
+
+ if (editAssetId.isEmpty()) {
+ del_config?.isVisible = false
+ }
+
+ return super.onCreateOptionsMenu(menu)
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
+ R.id.del_config -> {
+ deleteServer()
+ true
+ }
+ R.id.save_config -> {
+ saveServer()
+ true
+ }
+ else -> super.onOptionsItemSelected(item)
+ }
+}
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/MmkvManager.kt b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/MmkvManager.kt
index 68bd9f42b..f00ab0e84 100644
--- a/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/MmkvManager.kt
+++ b/V2rayNG/app/src/main/kotlin/com/v2ray/ang/util/MmkvManager.kt
@@ -2,6 +2,7 @@ package com.v2ray.ang.util
import com.google.gson.Gson
import com.tencent.mmkv.MMKV
+import com.v2ray.ang.dto.AssetUrlItem
import com.v2ray.ang.dto.ServerAffiliationInfo
import com.v2ray.ang.dto.ServerConfig
import com.v2ray.ang.dto.SubscriptionItem
@@ -12,6 +13,7 @@ object MmkvManager {
const val ID_SERVER_RAW = "SERVER_RAW"
const val ID_SERVER_AFF = "SERVER_AFF"
const val ID_SUB = "SUB"
+ const val ID_ASSET = "ASSET"
const val ID_SETTING = "SETTING"
const val KEY_SELECTED_SERVER = "SELECTED_SERVER"
const val KEY_ANG_CONFIGS = "ANG_CONFIGS"
@@ -20,6 +22,7 @@ object MmkvManager {
private val serverStorage by lazy { MMKV.mmkvWithID(ID_SERVER_CONFIG, MMKV.MULTI_PROCESS_MODE) }
private val serverAffStorage by lazy { MMKV.mmkvWithID(ID_SERVER_AFF, MMKV.MULTI_PROCESS_MODE) }
private val subStorage by lazy { MMKV.mmkvWithID(ID_SUB, MMKV.MULTI_PROCESS_MODE) }
+ private val assetStorage by lazy { MMKV.mmkvWithID(ID_ASSET, MMKV.MULTI_PROCESS_MODE) }
fun decodeServerList(): MutableList {
val json = mainStorage?.decodeString(KEY_ANG_CONFIGS)
@@ -142,6 +145,22 @@ object MmkvManager {
removeServerViaSubid(subid)
}
+ fun decodeAssetUrls(): List> {
+ val assetUrlItems = mutableListOf>()
+ assetStorage?.allKeys()?.forEach { key ->
+ val json = assetStorage?.decodeString(key)
+ if (!json.isNullOrBlank()) {
+ assetUrlItems.add(Pair(key, Gson().fromJson(json, AssetUrlItem::class.java)))
+ }
+ }
+ assetUrlItems.sortedBy { (_, value) -> value.addedTime }
+ return assetUrlItems
+ }
+
+ fun removeAssetUrl(assetid: String) {
+ assetStorage?.remove(assetid)
+ }
+
fun removeAllServer() {
mainStorage?.clearAll()
serverStorage?.clearAll()
diff --git a/V2rayNG/app/src/main/res/layout/activity_user_asset_url.xml b/V2rayNG/app/src/main/res/layout/activity_user_asset_url.xml
new file mode 100644
index 000000000..bc3780093
--- /dev/null
+++ b/V2rayNG/app/src/main/res/layout/activity_user_asset_url.xml
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/V2rayNG/app/src/main/res/layout/item_recycler_user_asset.xml b/V2rayNG/app/src/main/res/layout/item_recycler_user_asset.xml
index 7ae979126..7677d3ff1 100644
--- a/V2rayNG/app/src/main/res/layout/item_recycler_user_asset.xml
+++ b/V2rayNG/app/src/main/res/layout/item_recycler_user_asset.xml
@@ -19,27 +19,61 @@
android:background="@color/colorPrimary"
android:foreground="?attr/selectableItemBackground"
android:padding="@dimen/nav_header_vertical_spacing">
-
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+ android:title="@string/menu_item_add_asset"
+ app:showAsAction="ifRoom" >
+
+
- المستخدم (اختياري)
التشفير
التدفق
- مفتاح عام
- ShortId
- SpiderX
Reserved (اختياري)
العنوان المحلي IPv4(اختياري)
Mtu(optional, default 1420)
@@ -82,6 +79,9 @@
إضافة ملفات
تحميل الملفات
+ أضف عنوان URL للأصل
+ لم يتم العثور على الملف
+ الملاحظات موجودة بالفعل
جار التحميل
بحث
تحديد الكل
@@ -221,4 +221,6 @@
- VPN
- الوكيل فقط
+ يضيف
+ إضافة رابط
diff --git a/V2rayNG/app/src/main/res/values-fa/strings.xml b/V2rayNG/app/src/main/res/values-fa/strings.xml
index 3d7332e16..6ba420f2f 100644
--- a/V2rayNG/app/src/main/res/values-fa/strings.xml
+++ b/V2rayNG/app/src/main/res/values-fa/strings.xml
@@ -82,6 +82,9 @@
دانلود فایلها
+ URL را اضافه کنید
+ فایل پیدا نشد
+ نام قبلاً وجود دارد
بارگذاری
جستجو
انتخاب همه
@@ -253,5 +256,7 @@
- VPN
- فقط پروکسی
+ افزودن
+ افزودن لینک
diff --git a/V2rayNG/app/src/main/res/values-ru/strings.xml b/V2rayNG/app/src/main/res/values-ru/strings.xml
index b6d9ee5fb..354cac8f4 100644
--- a/V2rayNG/app/src/main/res/values-ru/strings.xml
+++ b/V2rayNG/app/src/main/res/values-ru/strings.xml
@@ -49,8 +49,6 @@
Запрос узла (WS/H2) / Шифрование QUIC
Путь (WS/H2) / Ключ QUIC / Сид KCP / Сервис gRPC
TLS
- Fingerprint
- Alpn
Разрешать небезопасные
SNI
Адрес
@@ -84,6 +82,9 @@
Загрузить файлы
+ Добавить URL ресурса
+ Файл не найден
+ Замечания уже есть
Загрузка…
Поиск
Выбрать все
@@ -256,5 +257,7 @@
- VPN
- Только прокси
+ Добавлять
+ Добавить ссылку
diff --git a/V2rayNG/app/src/main/res/values-vi/strings.xml b/V2rayNG/app/src/main/res/values-vi/strings.xml
index 4a186f4be..0b4d8bcdd 100644
--- a/V2rayNG/app/src/main/res/values-vi/strings.xml
+++ b/V2rayNG/app/src/main/res/values-vi/strings.xml
@@ -82,6 +82,9 @@
Tải xuống tệp tin
+ Thêm URL nội dung
+ Không tìm thấy tập tin
+ Nhận xét đã tồn tại
Đang tải...
Tìm kiếm
Chọn tất cả
@@ -255,5 +258,7 @@
- Chế độ VPN
- Chế độ Proxy
+ Thêm vào
+ Thêm liên kết
diff --git a/V2rayNG/app/src/main/res/values-zh-rCN/strings.xml b/V2rayNG/app/src/main/res/values-zh-rCN/strings.xml
index 956c6dba9..00d0f539f 100644
--- a/V2rayNG/app/src/main/res/values-zh-rCN/strings.xml
+++ b/V2rayNG/app/src/main/res/values-zh-rCN/strings.xml
@@ -83,6 +83,9 @@
+ 添加资产网址
+ 文件未找到
+ 备注已经存在
正在加载
搜索
全选
@@ -254,5 +257,7 @@
订阅导入成功
导入订阅失败
+ 添加
+ 添加链接
diff --git a/V2rayNG/app/src/main/res/values-zh-rTW/strings.xml b/V2rayNG/app/src/main/res/values-zh-rTW/strings.xml
index 1efdf66b3..c81b140e3 100644
--- a/V2rayNG/app/src/main/res/values-zh-rTW/strings.xml
+++ b/V2rayNG/app/src/main/res/values-zh-rTW/strings.xml
@@ -82,6 +82,9 @@
下載檔案
+ 新增資產網址
+ 文件未找到
+ 備註已經存在
載入
搜尋
全選
@@ -253,5 +256,7 @@
訂閱導入成功
導入訂閱失敗
+ 添加
+ 添加連結
diff --git a/V2rayNG/app/src/main/res/values/strings.xml b/V2rayNG/app/src/main/res/values/strings.xml
index eccbbb56b..a518741ca 100644
--- a/V2rayNG/app/src/main/res/values/strings.xml
+++ b/V2rayNG/app/src/main/res/values/strings.xml
@@ -85,8 +85,15 @@
Config malformed
Host(SNI)(Optional)
File copy failed, please use File Manager
+ Add asset
Add files
+ Add URL
+ URL
Download files
+ Add asset URL
+ File not found
+ The remarks already exists
+
Loading