diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/GroupServerFragment.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/GroupServerFragment.kt index 2a4d7ec5..7bac30af 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/GroupServerFragment.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/GroupServerFragment.kt @@ -1,25 +1,47 @@ package com.v2ray.ang.ui +import android.content.Intent import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.appcompat.app.AlertDialog import androidx.fragment.app.activityViewModels +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.ItemTouchHelper import com.v2ray.ang.AppConfig import com.v2ray.ang.R 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.helper.SimpleItemTouchHelperCallback import com.v2ray.ang.viewmodel.MainViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch class GroupServerFragment : BaseFragment() { + private val ownerActivity: MainActivity + get() = requireActivity() as MainActivity private val mainViewModel: MainViewModel by activityViewModels() private lateinit var adapter: MainRecyclerAdapter private var itemTouchHelper: ItemTouchHelper? = null private val subId: String by lazy { arguments?.getString(ARG_SUB_ID).orEmpty() } + private val share_method: Array by lazy { + ownerActivity.resources.getStringArray(R.array.share_method) + } + private val share_method_more: Array by lazy { + ownerActivity.resources.getStringArray(R.array.share_method_more) + } + companion object { private const val ARG_SUB_ID = "subscriptionId" fun newInstance(subId: String) = GroupServerFragment().apply { @@ -32,7 +54,7 @@ class GroupServerFragment : BaseFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - adapter = MainRecyclerAdapter(requireActivity() as MainActivity) + adapter = MainRecyclerAdapter(mainViewModel, ActivityAdapterListener()) binding.recyclerView.setHasFixedSize(true) if (MmkvManager.decodeSettingsBool(AppConfig.PREF_DOUBLE_COLUMN_DISPLAY, false)) { binding.recyclerView.layoutManager = GridLayoutManager(requireContext(), 2) @@ -52,9 +74,6 @@ class GroupServerFragment : BaseFragment() { // Log.d(TAG, "GroupServerFragment updateListAction subId=$subId") adapter.setData(mainViewModel.serversCache, index) } - mainViewModel.isRunning.observe(viewLifecycleOwner) { isRunning -> - adapter.isRunning = isRunning - } // Log.d(TAG, "GroupServerFragment onViewCreated: subId=$subId") } @@ -63,4 +82,191 @@ class GroupServerFragment : BaseFragment() { super.onResume() 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, 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) + } + } +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainAdapterListener.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainAdapterListener.kt new file mode 100644 index 00000000..2e097443 --- /dev/null +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainAdapterListener.kt @@ -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) + +} \ No newline at end of file diff --git a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainRecyclerAdapter.kt b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainRecyclerAdapter.kt index 400bb6fc..f5ad4a9a 100644 --- a/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainRecyclerAdapter.kt +++ b/V2rayNG/app/src/main/java/com/v2ray/ang/ui/MainRecyclerAdapter.kt @@ -1,60 +1,38 @@ package com.v2ray.ang.ui import android.annotation.SuppressLint -import android.content.Intent import android.graphics.Color -import android.text.TextUtils -import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.appcompat.app.AlertDialog import androidx.core.content.ContextCompat -import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.RecyclerView -import com.v2ray.ang.AngApplication.Companion.application import com.v2ray.ang.AppConfig import com.v2ray.ang.R -import com.v2ray.ang.databinding.ItemQrcodeBinding import com.v2ray.ang.databinding.ItemRecyclerFooterBinding import com.v2ray.ang.databinding.ItemRecyclerMainBinding -import com.v2ray.ang.dto.EConfigType import com.v2ray.ang.dto.ProfileItem 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.helper.ItemTouchHelperAdapter import com.v2ray.ang.helper.ItemTouchHelperViewHolder -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch +import com.v2ray.ang.viewmodel.MainViewModel import java.util.Collections -class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter(), ItemTouchHelperAdapter { +class MainRecyclerAdapter( + private val mainViewModel: MainViewModel, + private val adapterListener: MainAdapterListener? +) : RecyclerView.Adapter(), ItemTouchHelperAdapter { companion object { private const val VIEW_TYPE_ITEM = 1 private const val VIEW_TYPE_FOOTER = 2 } - private var mActivity: MainActivity = activity - private val share_method: Array by lazy { - mActivity.resources.getStringArray(R.array.share_method) - } - private val share_method_more: Array 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 var data: MutableList = mutableListOf() @SuppressLint("NotifyDataSetChanged") fun setData(newData: MutableList?, position: Int = -1) { - if (android.os.Looper.myLooper() != android.os.Looper.getMainLooper()) { - mActivity.runOnUiThread { setData(newData, position) } - return - } data = newData?.toMutableList() ?: mutableListOf() if (position >= 0 && position in data.indices) { @@ -68,9 +46,9 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter, 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) + fun removeServerSub(guid: String, position: Int) { val idx = data.indexOfFirst { it.guid == guid } if (idx >= 0) { data.removeAt(idx) @@ -315,23 +158,9 @@ class MainRecyclerAdapter(val activity: MainActivity) : RecyclerView.Adapter