Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ac043da8a2 | |||
| a192fec127 | |||
| b0b09273cf | |||
| 01a943b7ea | |||
| 06a3738b59 |
+1
-1
Submodule AndroidLibXrayLite updated: df33063839...1f10bdfa7f
@@ -12,8 +12,8 @@ android {
|
|||||||
applicationId = "com.v2ray.ang"
|
applicationId = "com.v2ray.ang"
|
||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 705
|
versionCode = 706
|
||||||
versionName = "2.0.5"
|
versionName = "2.0.6"
|
||||||
multiDexEnabled = true
|
multiDexEnabled = true
|
||||||
|
|
||||||
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
|
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
|
||||||
|
|||||||
@@ -1,25 +1,47 @@
|
|||||||
package com.v2ray.ang.ui
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
import androidx.recyclerview.widget.ItemTouchHelper
|
import androidx.recyclerview.widget.ItemTouchHelper
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.databinding.FragmentGroupServerBinding
|
import com.v2ray.ang.databinding.FragmentGroupServerBinding
|
||||||
|
import com.v2ray.ang.databinding.ItemQrcodeBinding
|
||||||
|
import com.v2ray.ang.dto.EConfigType
|
||||||
|
import com.v2ray.ang.dto.ProfileItem
|
||||||
|
import com.v2ray.ang.extension.toast
|
||||||
|
import com.v2ray.ang.extension.toastError
|
||||||
|
import com.v2ray.ang.extension.toastSuccess
|
||||||
|
import com.v2ray.ang.handler.AngConfigManager
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
|
import com.v2ray.ang.helper.SimpleItemTouchHelperCallback
|
||||||
import com.v2ray.ang.viewmodel.MainViewModel
|
import com.v2ray.ang.viewmodel.MainViewModel
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>() {
|
class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>() {
|
||||||
|
private val ownerActivity: MainActivity
|
||||||
|
get() = requireActivity() as MainActivity
|
||||||
private val mainViewModel: MainViewModel by activityViewModels()
|
private val mainViewModel: MainViewModel by activityViewModels()
|
||||||
private lateinit var adapter: MainRecyclerAdapter
|
private lateinit var adapter: MainRecyclerAdapter
|
||||||
private var itemTouchHelper: ItemTouchHelper? = null
|
private var itemTouchHelper: ItemTouchHelper? = null
|
||||||
private val subId: String by lazy { arguments?.getString(ARG_SUB_ID).orEmpty() }
|
private val subId: String by lazy { arguments?.getString(ARG_SUB_ID).orEmpty() }
|
||||||
|
|
||||||
|
private val share_method: Array<out String> by lazy {
|
||||||
|
ownerActivity.resources.getStringArray(R.array.share_method)
|
||||||
|
}
|
||||||
|
private val share_method_more: Array<out String> by lazy {
|
||||||
|
ownerActivity.resources.getStringArray(R.array.share_method_more)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val ARG_SUB_ID = "subscriptionId"
|
private const val ARG_SUB_ID = "subscriptionId"
|
||||||
fun newInstance(subId: String) = GroupServerFragment().apply {
|
fun newInstance(subId: String) = GroupServerFragment().apply {
|
||||||
@@ -32,7 +54,7 @@ class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>() {
|
|||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
|
||||||
adapter = MainRecyclerAdapter(requireActivity() as MainActivity)
|
adapter = MainRecyclerAdapter(mainViewModel, ActivityAdapterListener())
|
||||||
binding.recyclerView.setHasFixedSize(true)
|
binding.recyclerView.setHasFixedSize(true)
|
||||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_DOUBLE_COLUMN_DISPLAY, false)) {
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_DOUBLE_COLUMN_DISPLAY, false)) {
|
||||||
binding.recyclerView.layoutManager = GridLayoutManager(requireContext(), 2)
|
binding.recyclerView.layoutManager = GridLayoutManager(requireContext(), 2)
|
||||||
@@ -52,9 +74,6 @@ class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>() {
|
|||||||
// Log.d(TAG, "GroupServerFragment updateListAction subId=$subId")
|
// Log.d(TAG, "GroupServerFragment updateListAction subId=$subId")
|
||||||
adapter.setData(mainViewModel.serversCache, index)
|
adapter.setData(mainViewModel.serversCache, index)
|
||||||
}
|
}
|
||||||
mainViewModel.isRunning.observe(viewLifecycleOwner) { isRunning ->
|
|
||||||
adapter.isRunning = isRunning
|
|
||||||
}
|
|
||||||
|
|
||||||
// Log.d(TAG, "GroupServerFragment onViewCreated: subId=$subId")
|
// Log.d(TAG, "GroupServerFragment onViewCreated: subId=$subId")
|
||||||
}
|
}
|
||||||
@@ -63,4 +82,191 @@ class GroupServerFragment : BaseFragment<FragmentGroupServerBinding>() {
|
|||||||
super.onResume()
|
super.onResume()
|
||||||
mainViewModel.subscriptionIdChanged(subId)
|
mainViewModel.subscriptionIdChanged(subId)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
/**
|
||||||
|
* Shares server configuration
|
||||||
|
* Displays a dialog with sharing options and executes the selected action
|
||||||
|
* @param guid The server unique identifier
|
||||||
|
* @param profile The server configuration
|
||||||
|
* @param position The position in the list
|
||||||
|
* @param shareOptions The list of share options
|
||||||
|
* @param skip The number of options to skip
|
||||||
|
*/
|
||||||
|
private fun shareServer(guid: String, profile: ProfileItem, position: Int, shareOptions: List<String>, skip: Int) {
|
||||||
|
AlertDialog.Builder(ownerActivity).setItems(shareOptions.toTypedArray()) { _, i ->
|
||||||
|
try {
|
||||||
|
when (i + skip) {
|
||||||
|
0 -> showQRCode(guid)
|
||||||
|
1 -> share2Clipboard(guid)
|
||||||
|
2 -> shareFullContent(guid)
|
||||||
|
3 -> editServer(guid, profile)
|
||||||
|
4 -> removeServer(guid, position)
|
||||||
|
else -> ownerActivity.toast("else")
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(AppConfig.TAG, "Error when sharing server", e)
|
||||||
|
}
|
||||||
|
}.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays QR code for the server configuration
|
||||||
|
* @param guid The server unique identifier
|
||||||
|
*/
|
||||||
|
private fun showQRCode(guid: String) {
|
||||||
|
val ivBinding = ItemQrcodeBinding.inflate(LayoutInflater.from(ownerActivity))
|
||||||
|
ivBinding.ivQcode.setImageBitmap(AngConfigManager.share2QRCode(guid))
|
||||||
|
if (share_method.isNotEmpty()) {
|
||||||
|
ivBinding.ivQcode.contentDescription = share_method[0]
|
||||||
|
} else {
|
||||||
|
ivBinding.ivQcode.contentDescription = "QR Code"
|
||||||
|
}
|
||||||
|
AlertDialog.Builder(ownerActivity).setView(ivBinding.root).show()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shares server configuration to clipboard
|
||||||
|
* @param guid The server unique identifier
|
||||||
|
*/
|
||||||
|
private fun share2Clipboard(guid: String) {
|
||||||
|
if (AngConfigManager.share2Clipboard(ownerActivity, guid) == 0) {
|
||||||
|
ownerActivity.toastSuccess(R.string.toast_success)
|
||||||
|
} else {
|
||||||
|
ownerActivity.toastError(R.string.toast_failure)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shares full server configuration content to clipboard
|
||||||
|
* @param guid The server unique identifier
|
||||||
|
*/
|
||||||
|
private fun shareFullContent(guid: String) {
|
||||||
|
ownerActivity.lifecycleScope.launch(Dispatchers.IO) {
|
||||||
|
val result = AngConfigManager.shareFullContent2Clipboard(ownerActivity, guid)
|
||||||
|
launch(Dispatchers.Main) {
|
||||||
|
if (result == 0) {
|
||||||
|
ownerActivity.toastSuccess(R.string.toast_success)
|
||||||
|
} else {
|
||||||
|
ownerActivity.toastError(R.string.toast_failure)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edits server configuration
|
||||||
|
* Opens appropriate editing interface based on configuration type
|
||||||
|
* @param guid The server unique identifier
|
||||||
|
* @param profile The server configuration
|
||||||
|
*/
|
||||||
|
private fun editServer(guid: String, profile: ProfileItem) {
|
||||||
|
val intent = Intent().putExtra("guid", guid)
|
||||||
|
.putExtra("isRunning", mainViewModel.isRunning.value)
|
||||||
|
.putExtra("createConfigType", profile.configType.value)
|
||||||
|
when (profile.configType) {
|
||||||
|
EConfigType.CUSTOM -> {
|
||||||
|
ownerActivity.startActivity(intent.setClass(ownerActivity, ServerCustomConfigActivity::class.java))
|
||||||
|
}
|
||||||
|
|
||||||
|
EConfigType.POLICYGROUP -> {
|
||||||
|
ownerActivity.startActivity(intent.setClass(ownerActivity, ServerGroupActivity::class.java))
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
ownerActivity.startActivity(intent.setClass(ownerActivity, ServerActivity::class.java))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes server configuration
|
||||||
|
* Handles confirmation dialog and related checks
|
||||||
|
* @param guid The server unique identifier
|
||||||
|
* @param position The position in the list
|
||||||
|
*/
|
||||||
|
private fun removeServer(guid: String, position: Int) {
|
||||||
|
if (guid == MmkvManager.getSelectServer()) {
|
||||||
|
ownerActivity.toast(R.string.toast_action_not_allowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_CONFIRM_REMOVE)) {
|
||||||
|
AlertDialog.Builder(ownerActivity).setMessage(R.string.del_config_comfirm)
|
||||||
|
.setPositiveButton(android.R.string.ok) { _, _ ->
|
||||||
|
removeServerSub(guid, position)
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
||||||
|
//do noting
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
} else {
|
||||||
|
removeServerSub(guid, position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes the actual server removal process
|
||||||
|
* @param guid The server unique identifier
|
||||||
|
* @param position The position in the list
|
||||||
|
*/
|
||||||
|
private fun removeServerSub(guid: String, position: Int) {
|
||||||
|
ownerActivity.mainViewModel.removeServer(guid)
|
||||||
|
adapter.removeServerSub(guid, position)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the selected server
|
||||||
|
* Updates UI and restarts service if needed
|
||||||
|
* @param guid The server unique identifier to select
|
||||||
|
*/
|
||||||
|
private fun setSelectServer(guid: String) {
|
||||||
|
val selected = MmkvManager.getSelectServer()
|
||||||
|
if (guid != selected) {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class ActivityAdapterListener : MainAdapterListener {
|
||||||
|
override fun onEdit(guid: String, position: Int) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onShare(url: String) {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRefreshData() {
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRemove(guid: String, position: Int) {
|
||||||
|
removeServer(guid, position)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onEdit(guid: String, position: Int, profile: ProfileItem) {
|
||||||
|
editServer(guid, profile)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSelectServer(guid: String) {
|
||||||
|
setSelectServer(guid)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onShare(guid: String, profile: ProfileItem, position: Int, more: Boolean) {
|
||||||
|
val isCustom = profile.configType == EConfigType.CUSTOM || profile.configType == EConfigType.POLICYGROUP
|
||||||
|
|
||||||
|
val (shareOptions, skip) = if (more) {
|
||||||
|
val options = if (isCustom) share_method_more.asList().takeLast(3) else share_method_more.asList()
|
||||||
|
options to if (isCustom) 2 else 0
|
||||||
|
} else {
|
||||||
|
val options = if (isCustom) share_method.asList().takeLast(1) else share_method.asList()
|
||||||
|
options to if (isCustom) 2 else 0
|
||||||
|
}
|
||||||
|
|
||||||
|
shareServer(guid, profile, position, shareOptions, skip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
|
import com.v2ray.ang.dto.ProfileItem
|
||||||
|
|
||||||
|
interface MainAdapterListener :BaseAdapterListener {
|
||||||
|
|
||||||
|
fun onEdit(guid: String, position: Int, profile: ProfileItem)
|
||||||
|
|
||||||
|
fun onSelectServer(guid: String)
|
||||||
|
|
||||||
|
fun onShare(guid: String, profile: ProfileItem, position: Int, more: Boolean)
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,60 +1,38 @@
|
|||||||
package com.v2ray.ang.ui
|
package com.v2ray.ang.ui
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Intent
|
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.text.TextUtils
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
import com.v2ray.ang.AngApplication.Companion.application
|
|
||||||
import com.v2ray.ang.AppConfig
|
import com.v2ray.ang.AppConfig
|
||||||
import com.v2ray.ang.R
|
import com.v2ray.ang.R
|
||||||
import com.v2ray.ang.databinding.ItemQrcodeBinding
|
|
||||||
import com.v2ray.ang.databinding.ItemRecyclerFooterBinding
|
import com.v2ray.ang.databinding.ItemRecyclerFooterBinding
|
||||||
import com.v2ray.ang.databinding.ItemRecyclerMainBinding
|
import com.v2ray.ang.databinding.ItemRecyclerMainBinding
|
||||||
import com.v2ray.ang.dto.EConfigType
|
|
||||||
import com.v2ray.ang.dto.ProfileItem
|
import com.v2ray.ang.dto.ProfileItem
|
||||||
import com.v2ray.ang.dto.ServersCache
|
import com.v2ray.ang.dto.ServersCache
|
||||||
import com.v2ray.ang.extension.toast
|
|
||||||
import com.v2ray.ang.extension.toastError
|
|
||||||
import com.v2ray.ang.extension.toastSuccess
|
|
||||||
import com.v2ray.ang.handler.AngConfigManager
|
|
||||||
import com.v2ray.ang.handler.MmkvManager
|
import com.v2ray.ang.handler.MmkvManager
|
||||||
import com.v2ray.ang.helper.ItemTouchHelperAdapter
|
import com.v2ray.ang.helper.ItemTouchHelperAdapter
|
||||||
import com.v2ray.ang.helper.ItemTouchHelperViewHolder
|
import com.v2ray.ang.helper.ItemTouchHelperViewHolder
|
||||||
import kotlinx.coroutines.Dispatchers
|
import com.v2ray.ang.viewmodel.MainViewModel
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
|
|
||||||
class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<MainRecyclerAdapter.BaseViewHolder>(), ItemTouchHelperAdapter {
|
class MainRecyclerAdapter(
|
||||||
|
private val mainViewModel: MainViewModel,
|
||||||
|
private val adapterListener: MainAdapterListener?
|
||||||
|
) : RecyclerView.Adapter<MainRecyclerAdapter.BaseViewHolder>(), ItemTouchHelperAdapter {
|
||||||
companion object {
|
companion object {
|
||||||
private const val VIEW_TYPE_ITEM = 1
|
private const val VIEW_TYPE_ITEM = 1
|
||||||
private const val VIEW_TYPE_FOOTER = 2
|
private const val VIEW_TYPE_FOOTER = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
private var mActivity: MainActivity = activity
|
|
||||||
private val share_method: Array<out String> by lazy {
|
|
||||||
mActivity.resources.getStringArray(R.array.share_method)
|
|
||||||
}
|
|
||||||
private val share_method_more: Array<out String> by lazy {
|
|
||||||
mActivity.resources.getStringArray(R.array.share_method_more)
|
|
||||||
}
|
|
||||||
var isRunning = false
|
|
||||||
private val doubleColumnDisplay = MmkvManager.decodeSettingsBool(AppConfig.PREF_DOUBLE_COLUMN_DISPLAY, false)
|
private val doubleColumnDisplay = MmkvManager.decodeSettingsBool(AppConfig.PREF_DOUBLE_COLUMN_DISPLAY, false)
|
||||||
private var data: MutableList<ServersCache> = mutableListOf()
|
private var data: MutableList<ServersCache> = mutableListOf()
|
||||||
|
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
@SuppressLint("NotifyDataSetChanged")
|
||||||
fun setData(newData: MutableList<ServersCache>?, position: Int = -1) {
|
fun setData(newData: MutableList<ServersCache>?, position: Int = -1) {
|
||||||
if (android.os.Looper.myLooper() != android.os.Looper.getMainLooper()) {
|
|
||||||
mActivity.runOnUiThread { setData(newData, position) }
|
|
||||||
return
|
|
||||||
}
|
|
||||||
data = newData?.toMutableList() ?: mutableListOf()
|
data = newData?.toMutableList() ?: mutableListOf()
|
||||||
|
|
||||||
if (position >= 0 && position in data.indices) {
|
if (position >= 0 && position in data.indices) {
|
||||||
@@ -68,9 +46,9 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
|
|||||||
|
|
||||||
override fun onBindViewHolder(holder: BaseViewHolder, position: Int) {
|
override fun onBindViewHolder(holder: BaseViewHolder, position: Int) {
|
||||||
if (holder is MainViewHolder) {
|
if (holder is MainViewHolder) {
|
||||||
|
val context = holder.itemMainBinding.root.context
|
||||||
val guid = data[position].guid
|
val guid = data[position].guid
|
||||||
val profile = data[position].profile
|
val profile = data[position].profile
|
||||||
val isCustom = profile.configType == EConfigType.CUSTOM || profile.configType == EConfigType.POLICYGROUP
|
|
||||||
|
|
||||||
holder.itemView.setBackgroundColor(Color.TRANSPARENT)
|
holder.itemView.setBackgroundColor(Color.TRANSPARENT)
|
||||||
|
|
||||||
@@ -83,9 +61,9 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
|
|||||||
val aff = MmkvManager.decodeServerAffiliationInfo(guid)
|
val aff = MmkvManager.decodeServerAffiliationInfo(guid)
|
||||||
holder.itemMainBinding.tvTestResult.text = aff?.getTestDelayString().orEmpty()
|
holder.itemMainBinding.tvTestResult.text = aff?.getTestDelayString().orEmpty()
|
||||||
if ((aff?.testDelayMillis ?: 0L) < 0L) {
|
if ((aff?.testDelayMillis ?: 0L) < 0L) {
|
||||||
holder.itemMainBinding.tvTestResult.setTextColor(ContextCompat.getColor(mActivity, R.color.colorPingRed))
|
holder.itemMainBinding.tvTestResult.setTextColor(ContextCompat.getColor(context, R.color.colorPingRed))
|
||||||
} else {
|
} else {
|
||||||
holder.itemMainBinding.tvTestResult.setTextColor(ContextCompat.getColor(mActivity, R.color.colorPing))
|
holder.itemMainBinding.tvTestResult.setTextColor(ContextCompat.getColor(context, R.color.colorPing))
|
||||||
}
|
}
|
||||||
|
|
||||||
//layoutIndicator
|
//layoutIndicator
|
||||||
@@ -107,11 +85,8 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
|
|||||||
holder.itemMainBinding.layoutRemove.visibility = View.GONE
|
holder.itemMainBinding.layoutRemove.visibility = View.GONE
|
||||||
holder.itemMainBinding.layoutMore.visibility = View.VISIBLE
|
holder.itemMainBinding.layoutMore.visibility = View.VISIBLE
|
||||||
|
|
||||||
//share method
|
|
||||||
val shareOptions = if (isCustom) share_method_more.asList().takeLast(3) else share_method_more.asList()
|
|
||||||
|
|
||||||
holder.itemMainBinding.layoutMore.setOnClickListener {
|
holder.itemMainBinding.layoutMore.setOnClickListener {
|
||||||
shareServer(guid, profile, position, shareOptions, if (isCustom) 2 else 0)
|
adapterListener?.onShare(guid, profile, position, true)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
holder.itemMainBinding.layoutShare.visibility = View.VISIBLE
|
holder.itemMainBinding.layoutShare.visibility = View.VISIBLE
|
||||||
@@ -119,34 +94,23 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
|
|||||||
holder.itemMainBinding.layoutRemove.visibility = View.VISIBLE
|
holder.itemMainBinding.layoutRemove.visibility = View.VISIBLE
|
||||||
holder.itemMainBinding.layoutMore.visibility = View.GONE
|
holder.itemMainBinding.layoutMore.visibility = View.GONE
|
||||||
|
|
||||||
//share method
|
|
||||||
val shareOptions = if (isCustom) share_method.asList().takeLast(1) else share_method.asList()
|
|
||||||
|
|
||||||
holder.itemMainBinding.layoutShare.setOnClickListener {
|
holder.itemMainBinding.layoutShare.setOnClickListener {
|
||||||
shareServer(guid, profile, position, shareOptions, if (isCustom) 2 else 0)
|
adapterListener?.onShare(guid, profile, position, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
holder.itemMainBinding.layoutEdit.setOnClickListener {
|
holder.itemMainBinding.layoutEdit.setOnClickListener {
|
||||||
editServer(guid, profile)
|
adapterListener?.onEdit(guid, position, profile)
|
||||||
}
|
}
|
||||||
holder.itemMainBinding.layoutRemove.setOnClickListener {
|
holder.itemMainBinding.layoutRemove.setOnClickListener {
|
||||||
removeServer(guid, position)
|
adapterListener?.onRemove(guid, position)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
holder.itemMainBinding.infoContainer.setOnClickListener {
|
holder.itemMainBinding.infoContainer.setOnClickListener {
|
||||||
setSelectServer(guid)
|
adapterListener?.onSelectServer(guid)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// if (holder is FooterViewHolder) {
|
|
||||||
// if (true) {
|
|
||||||
// holder.itemFooterBinding.layoutEdit.visibility = View.INVISIBLE
|
|
||||||
// } else {
|
|
||||||
// holder.itemFooterBinding.layoutEdit.setOnClickListener {
|
|
||||||
// Utils.openUri(mActivity, "${Utils.decode(AppConfig.PromotionUrl)}?t=${System.currentTimeMillis()}")
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -178,135 +142,14 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
|
|||||||
*/
|
*/
|
||||||
private fun getSubscriptionRemarks(profile: ProfileItem): String {
|
private fun getSubscriptionRemarks(profile: ProfileItem): String {
|
||||||
val subRemarks =
|
val subRemarks =
|
||||||
if (mActivity.mainViewModel.subscriptionId.isEmpty())
|
if (mainViewModel.subscriptionId.isEmpty())
|
||||||
MmkvManager.decodeSubscription(profile.subscriptionId)?.remarks?.firstOrNull()
|
MmkvManager.decodeSubscription(profile.subscriptionId)?.remarks?.firstOrNull()
|
||||||
else
|
else
|
||||||
null
|
null
|
||||||
return subRemarks?.toString() ?: ""
|
return subRemarks?.toString() ?: ""
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
fun removeServerSub(guid: String, position: Int) {
|
||||||
* Shares server configuration
|
|
||||||
* Displays a dialog with sharing options and executes the selected action
|
|
||||||
* @param guid The server unique identifier
|
|
||||||
* @param profile The server configuration
|
|
||||||
* @param position The position in the list
|
|
||||||
* @param shareOptions The list of share options
|
|
||||||
* @param skip The number of options to skip
|
|
||||||
*/
|
|
||||||
private fun shareServer(guid: String, profile: ProfileItem, position: Int, shareOptions: List<String>, skip: Int) {
|
|
||||||
AlertDialog.Builder(mActivity).setItems(shareOptions.toTypedArray()) { _, i ->
|
|
||||||
try {
|
|
||||||
when (i + skip) {
|
|
||||||
0 -> showQRCode(guid)
|
|
||||||
1 -> share2Clipboard(guid)
|
|
||||||
2 -> shareFullContent(guid)
|
|
||||||
3 -> editServer(guid, profile)
|
|
||||||
4 -> removeServer(guid, position)
|
|
||||||
else -> mActivity.toast("else")
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(AppConfig.TAG, "Error when sharing server", e)
|
|
||||||
}
|
|
||||||
}.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Displays QR code for the server configuration
|
|
||||||
* @param guid The server unique identifier
|
|
||||||
*/
|
|
||||||
private fun showQRCode(guid: String) {
|
|
||||||
val ivBinding = ItemQrcodeBinding.inflate(LayoutInflater.from(mActivity))
|
|
||||||
ivBinding.ivQcode.setImageBitmap(AngConfigManager.share2QRCode(guid))
|
|
||||||
if (share_method.isNotEmpty()) {
|
|
||||||
ivBinding.ivQcode.contentDescription = share_method[0]
|
|
||||||
} else {
|
|
||||||
ivBinding.ivQcode.contentDescription = "QR Code"
|
|
||||||
}
|
|
||||||
AlertDialog.Builder(mActivity).setView(ivBinding.root).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shares server configuration to clipboard
|
|
||||||
* @param guid The server unique identifier
|
|
||||||
*/
|
|
||||||
private fun share2Clipboard(guid: String) {
|
|
||||||
if (AngConfigManager.share2Clipboard(mActivity, guid) == 0) {
|
|
||||||
mActivity.toastSuccess(R.string.toast_success)
|
|
||||||
} else {
|
|
||||||
mActivity.toastError(R.string.toast_failure)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shares full server configuration content to clipboard
|
|
||||||
* @param guid The server unique identifier
|
|
||||||
*/
|
|
||||||
private fun shareFullContent(guid: String) {
|
|
||||||
mActivity.lifecycleScope.launch(Dispatchers.IO) {
|
|
||||||
val result = AngConfigManager.shareFullContent2Clipboard(mActivity, guid)
|
|
||||||
launch(Dispatchers.Main) {
|
|
||||||
if (result == 0) {
|
|
||||||
mActivity.toastSuccess(R.string.toast_success)
|
|
||||||
} else {
|
|
||||||
mActivity.toastError(R.string.toast_failure)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Edits server configuration
|
|
||||||
* Opens appropriate editing interface based on configuration type
|
|
||||||
* @param guid The server unique identifier
|
|
||||||
* @param profile The server configuration
|
|
||||||
*/
|
|
||||||
private fun editServer(guid: String, profile: ProfileItem) {
|
|
||||||
val intent = Intent().putExtra("guid", guid)
|
|
||||||
.putExtra("isRunning", isRunning)
|
|
||||||
.putExtra("createConfigType", profile.configType.value)
|
|
||||||
if (profile.configType == EConfigType.CUSTOM) {
|
|
||||||
mActivity.startActivity(intent.setClass(mActivity, ServerCustomConfigActivity::class.java))
|
|
||||||
} else if (profile.configType == EConfigType.POLICYGROUP) {
|
|
||||||
mActivity.startActivity(intent.setClass(mActivity, ServerGroupActivity::class.java))
|
|
||||||
} else {
|
|
||||||
mActivity.startActivity(intent.setClass(mActivity, ServerActivity::class.java))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes server configuration
|
|
||||||
* Handles confirmation dialog and related checks
|
|
||||||
* @param guid The server unique identifier
|
|
||||||
* @param position The position in the list
|
|
||||||
*/
|
|
||||||
private fun removeServer(guid: String, position: Int) {
|
|
||||||
if (guid == MmkvManager.getSelectServer()) {
|
|
||||||
application.toast(R.string.toast_action_not_allowed)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (MmkvManager.decodeSettingsBool(AppConfig.PREF_CONFIRM_REMOVE)) {
|
|
||||||
AlertDialog.Builder(mActivity).setMessage(R.string.del_config_comfirm)
|
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
|
||||||
removeServerSub(guid, position)
|
|
||||||
}
|
|
||||||
.setNegativeButton(android.R.string.cancel) { _, _ ->
|
|
||||||
//do noting
|
|
||||||
}
|
|
||||||
.show()
|
|
||||||
} else {
|
|
||||||
removeServerSub(guid, position)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Executes the actual server removal process
|
|
||||||
* @param guid The server unique identifier
|
|
||||||
* @param position The position in the list
|
|
||||||
*/
|
|
||||||
private fun removeServerSub(guid: String, position: Int) {
|
|
||||||
mActivity.mainViewModel.removeServer(guid)
|
|
||||||
val idx = data.indexOfFirst { it.guid == guid }
|
val idx = data.indexOfFirst { it.guid == guid }
|
||||||
if (idx >= 0) {
|
if (idx >= 0) {
|
||||||
data.removeAt(idx)
|
data.removeAt(idx)
|
||||||
@@ -315,23 +158,9 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
fun setSelectServer(fromPosition: Int, toPosition: Int) {
|
||||||
* Sets the selected server
|
notifyItemChanged(fromPosition)
|
||||||
* Updates UI and restarts service if needed
|
notifyItemChanged(toPosition)
|
||||||
* @param guid The server unique identifier to select
|
|
||||||
*/
|
|
||||||
private fun setSelectServer(guid: String) {
|
|
||||||
val selected = MmkvManager.getSelectServer()
|
|
||||||
if (guid != selected) {
|
|
||||||
MmkvManager.setSelectServer(guid)
|
|
||||||
if (!TextUtils.isEmpty(selected)) {
|
|
||||||
notifyItemChanged(mActivity.mainViewModel.getPosition(selected.orEmpty()))
|
|
||||||
}
|
|
||||||
notifyItemChanged(mActivity.mainViewModel.getPosition(guid))
|
|
||||||
if (isRunning) {
|
|
||||||
mActivity.restartV2Ray()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder {
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder {
|
||||||
@@ -369,7 +198,7 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter<Mai
|
|||||||
BaseViewHolder(itemFooterBinding.root)
|
BaseViewHolder(itemFooterBinding.root)
|
||||||
|
|
||||||
override fun onItemMove(fromPosition: Int, toPosition: Int): Boolean {
|
override fun onItemMove(fromPosition: Int, toPosition: Int): Boolean {
|
||||||
mActivity.mainViewModel.swapServer(fromPosition, toPosition)
|
mainViewModel.swapServer(fromPosition, toPosition)
|
||||||
if (fromPosition < data.size && toPosition < data.size) {
|
if (fromPosition < data.size && toPosition < data.size) {
|
||||||
Collections.swap(data, fromPosition, toPosition)
|
Collections.swap(data, fromPosition, toPosition)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ class PerAppProxyActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
appsAll = apps
|
appsAll = apps
|
||||||
adapter = PerAppProxyAdapter(this@PerAppProxyActivity, apps, viewModel)
|
adapter = PerAppProxyAdapter(apps, viewModel)
|
||||||
binding.recyclerView.adapter = adapter
|
binding.recyclerView.adapter = adapter
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
@@ -292,7 +292,7 @@ class PerAppProxyActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
adapter = PerAppProxyAdapter(this, apps, adapter?.viewModel ?: viewModel)
|
adapter = PerAppProxyAdapter(apps, adapter?.viewModel ?: viewModel)
|
||||||
binding.recyclerView.adapter = adapter
|
binding.recyclerView.adapter = adapter
|
||||||
refreshData()
|
refreshData()
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -8,8 +8,10 @@ import com.v2ray.ang.databinding.ItemRecyclerBypassListBinding
|
|||||||
import com.v2ray.ang.dto.AppInfo
|
import com.v2ray.ang.dto.AppInfo
|
||||||
import com.v2ray.ang.viewmodel.PerAppProxyViewModel
|
import com.v2ray.ang.viewmodel.PerAppProxyViewModel
|
||||||
|
|
||||||
class PerAppProxyAdapter(val activity: BaseActivity, val apps: List<AppInfo>, val viewModel: PerAppProxyViewModel) :
|
class PerAppProxyAdapter(
|
||||||
RecyclerView.Adapter<PerAppProxyAdapter.BaseViewHolder>() {
|
val apps: List<AppInfo>,
|
||||||
|
val viewModel: PerAppProxyViewModel
|
||||||
|
) :RecyclerView.Adapter<PerAppProxyAdapter.BaseViewHolder>() {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val VIEW_TYPE_HEADER = 0
|
private const val VIEW_TYPE_HEADER = 0
|
||||||
@@ -37,6 +39,7 @@ class PerAppProxyAdapter(val activity: BaseActivity, val apps: List<AppInfo>, va
|
|||||||
)
|
)
|
||||||
BaseViewHolder(view)
|
BaseViewHolder(view)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> AppViewHolder(ItemRecyclerBypassListBinding.inflate(LayoutInflater.from(ctx), parent, false))
|
else -> AppViewHolder(ItemRecyclerBypassListBinding.inflate(LayoutInflater.from(ctx), parent, false))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import com.v2ray.ang.handler.MmkvManager
|
|||||||
import com.v2ray.ang.util.QRCodeDecoder
|
import com.v2ray.ang.util.QRCodeDecoder
|
||||||
import io.github.g00fy2.quickie.QRResult
|
import io.github.g00fy2.quickie.QRResult
|
||||||
import io.github.g00fy2.quickie.ScanCustomCode
|
import io.github.g00fy2.quickie.ScanCustomCode
|
||||||
|
import io.github.g00fy2.quickie.config.BarcodeFormat
|
||||||
import io.github.g00fy2.quickie.config.ScannerConfig
|
import io.github.g00fy2.quickie.config.ScannerConfig
|
||||||
|
|
||||||
class ScannerActivity : BaseActivity() {
|
class ScannerActivity : BaseActivity() {
|
||||||
@@ -73,6 +74,7 @@ class ScannerActivity : BaseActivity() {
|
|||||||
setHapticSuccessFeedback(true) // enable (default) or disable haptic feedback when a barcode was detected
|
setHapticSuccessFeedback(true) // enable (default) or disable haptic feedback when a barcode was detected
|
||||||
setShowTorchToggle(true) // show or hide (default) torch/flashlight toggle button
|
setShowTorchToggle(true) // show or hide (default) torch/flashlight toggle button
|
||||||
setShowCloseButton(true) // show or hide (default) close button
|
setShowCloseButton(true) // show or hide (default) close button
|
||||||
|
setBarcodeFormats(listOf(BarcodeFormat.QR_CODE))
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -632,7 +632,7 @@ class ServerActivity : BaseActivity() {
|
|||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
application.toast(R.string.toast_action_not_allowed)
|
toast(R.string.toast_action_not_allowed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -65,9 +65,9 @@ object QRCodeDecoder {
|
|||||||
val qrReader = QRCodeReader()
|
val qrReader = QRCodeReader()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
qrReader.decode(BinaryBitmap(GlobalHistogramBinarizer(source)), mapOf(DecodeHintType.TRY_HARDER to true)).text
|
qrReader.decode(BinaryBitmap(GlobalHistogramBinarizer(source)), HINTS).text
|
||||||
} catch (e: NotFoundException) {
|
} catch (e: NotFoundException) {
|
||||||
qrReader.decode(BinaryBitmap(GlobalHistogramBinarizer(source.invert())), mapOf(DecodeHintType.TRY_HARDER to true)).text
|
qrReader.decode(BinaryBitmap(GlobalHistogramBinarizer(source.invert())), HINTS).text
|
||||||
}
|
}
|
||||||
}.getOrNull()
|
}.getOrNull()
|
||||||
}
|
}
|
||||||
@@ -97,27 +97,9 @@ object QRCodeDecoder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val allFormats: List<BarcodeFormat> = arrayListOf(
|
// Keep decoding hints focused on QR codes and enable TRY_HARDER + UTF-8 charset for better success rate.
|
||||||
BarcodeFormat.AZTEC,
|
HINTS[DecodeHintType.TRY_HARDER] = true
|
||||||
BarcodeFormat.CODABAR,
|
HINTS[DecodeHintType.POSSIBLE_FORMATS] = listOf(BarcodeFormat.QR_CODE)
|
||||||
BarcodeFormat.CODE_39,
|
HINTS[DecodeHintType.CHARACTER_SET] = Charsets.UTF_8.name()
|
||||||
BarcodeFormat.CODE_93,
|
|
||||||
BarcodeFormat.CODE_128,
|
|
||||||
BarcodeFormat.DATA_MATRIX,
|
|
||||||
BarcodeFormat.EAN_8,
|
|
||||||
BarcodeFormat.EAN_13,
|
|
||||||
BarcodeFormat.ITF,
|
|
||||||
BarcodeFormat.MAXICODE,
|
|
||||||
BarcodeFormat.PDF_417,
|
|
||||||
BarcodeFormat.QR_CODE,
|
|
||||||
BarcodeFormat.RSS_14,
|
|
||||||
BarcodeFormat.RSS_EXPANDED,
|
|
||||||
BarcodeFormat.UPC_A,
|
|
||||||
BarcodeFormat.UPC_E,
|
|
||||||
BarcodeFormat.UPC_EAN_EXTENSION
|
|
||||||
)
|
|
||||||
HINTS[DecodeHintType.TRY_HARDER] = BarcodeFormat.QR_CODE
|
|
||||||
HINTS[DecodeHintType.POSSIBLE_FORMATS] = allFormats
|
|
||||||
HINTS[DecodeHintType.CHARACTER_SET] = Charsets.UTF_8
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user