add ProxyList
This commit is contained in:
parent
d371a68382
commit
e1f8e971ec
@ -3,25 +3,35 @@ package xfk233.GenshinProxy
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.AlertDialog
|
||||
import android.app.Application
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.content.res.XModuleResources
|
||||
import android.graphics.Color
|
||||
import android.graphics.PixelFormat
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.widget.EditText
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.Switch
|
||||
import android.widget.Toast
|
||||
import android.util.TypedValue
|
||||
import android.view.Gravity
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import android.widget.*
|
||||
import com.github.kyuubiran.ezxhelper.init.EzXHelperInit
|
||||
import com.github.kyuubiran.ezxhelper.utils.findConstructor
|
||||
import com.github.kyuubiran.ezxhelper.utils.findMethod
|
||||
import com.github.kyuubiran.ezxhelper.utils.findMethodOrNull
|
||||
import com.github.kyuubiran.ezxhelper.utils.hookBefore
|
||||
import com.github.kyuubiran.ezxhelper.utils.*
|
||||
import de.robv.android.xposed.IXposedHookLoadPackage
|
||||
import de.robv.android.xposed.IXposedHookZygoteInit
|
||||
import de.robv.android.xposed.XC_MethodHook
|
||||
import de.robv.android.xposed.XposedBridge
|
||||
import de.robv.android.xposed.callbacks.XC_LoadPackage
|
||||
import org.json.JSONObject
|
||||
import xfk233.GenshinProxy.Utils.dp2px
|
||||
import xfk233.GenshinProxy.Utils.isInit
|
||||
import java.io.BufferedReader
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.InputStreamReader
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import java.net.URLEncoder
|
||||
import java.util.regex.Pattern
|
||||
|
||||
|
||||
@ -31,33 +41,75 @@ class MainHook : IXposedHookLoadPackage, IXposedHookZygoteInit {
|
||||
private var forceUrl = false
|
||||
private lateinit var modulePath: String
|
||||
private lateinit var moduleRes: XModuleResources
|
||||
private lateinit var windowManager: WindowManager
|
||||
private lateinit var activity: Activity
|
||||
private var proxyList = false
|
||||
private val proxyListRegex = arrayListOf(
|
||||
"api-os-takumi.mihoyo.com",
|
||||
"hk4e-api-os-static.mihoyo.com",
|
||||
"hk4e-sdk-os.mihoyo.com",
|
||||
"dispatchosglobal.yuanshen.com",
|
||||
"osusadispatch.yuanshen.com",
|
||||
"account.mihoyo.com",
|
||||
"log-upload-os.mihoyo.com",
|
||||
"dispatchcntest.yuanshen.com",
|
||||
"devlog-upload.mihoyo.com",
|
||||
"webstatic.mihoyo.com",
|
||||
"log-upload.mihoyo.com",
|
||||
"hk4e-sdk.mihoyo.com",
|
||||
"api-beta-sdk.mihoyo.com",
|
||||
"api-beta-sdk-os.mihoyo.com",
|
||||
"cnbeta01dispatch.yuanshen.com",
|
||||
"dispatchcnglobal.yuanshen.com",
|
||||
"cnbeta02dispatch.yuanshen.com",
|
||||
"sdk-os-static.mihoyo.com",
|
||||
"webstatic-sea.mihoyo.com",
|
||||
"webstatic-sea.hoyoverse.com",
|
||||
"hk4e-sdk-os-static.hoyoverse.com",
|
||||
"sdk-os-static.hoyoverse.com",
|
||||
"api-account-os.hoyoverse.com",
|
||||
"hk4e-sdk-os.hoyoverse.com",
|
||||
"overseauspider.yuanshen.com",
|
||||
"gameapi-account.mihoyo.com",
|
||||
"minor-api.mihoyo.com",
|
||||
"public-data-api.mihoyo.com",
|
||||
"uspider.yuanshen.com",
|
||||
"sdk-static.mihoyo.com",
|
||||
"minor-api-os.hoyoverse.com",
|
||||
"log-upload-os.hoyoverse.com"
|
||||
)
|
||||
|
||||
override fun initZygote(startupParam: IXposedHookZygoteInit.StartupParam) {
|
||||
modulePath = startupParam.modulePath
|
||||
moduleRes = XModuleResources.createInstance(modulePath, null)
|
||||
}
|
||||
|
||||
@SuppressLint("WrongConstant", "ClickableViewAccessibility")
|
||||
override fun handleLoadPackage(lpparam: XC_LoadPackage.LoadPackageParam) {
|
||||
if (lpparam.packageName != "com.miHoYo.GenshinImpact") return
|
||||
EzXHelperInit.initHandleLoadPackage(lpparam)
|
||||
findMethod(Application::class.java, true) { name == "attachBaseContext" }.hookBefore {
|
||||
findMethod("com.combosdk.openapi.ComboApplication") { name == "attachBaseContext" }.hookBefore {
|
||||
val context = it.args[0] as Context
|
||||
val sp = context.getSharedPreferences("serverConfig", 0)
|
||||
forceUrl = sp.getBoolean("forceUrl", false)
|
||||
server = sp.getString("serverip", "") ?: ""
|
||||
proxyList = sp.getBoolean("ProxyList", false)
|
||||
}
|
||||
sslHook(lpparam)
|
||||
hook()
|
||||
findMethod(Activity::class.java, true) { name == "onCreate" }.hookBefore { param ->
|
||||
activity = param.thisObject as Activity
|
||||
}
|
||||
findMethod("com.miHoYo.GetMobileInfo.MainActivity") { name == "onCreate" }.hookBefore { param ->
|
||||
val context = param.thisObject as Activity
|
||||
val sp = context.getSharedPreferences("serverConfig", 0)
|
||||
AlertDialog.Builder(context).apply {
|
||||
activity = param.thisObject as Activity
|
||||
val sp = activity.getSharedPreferences("serverConfig", 0)
|
||||
AlertDialog.Builder(activity).apply {
|
||||
setCancelable(false)
|
||||
setTitle(moduleRes.getString(R.string.SelectServer))
|
||||
setMessage(moduleRes.getString(R.string.Tips))
|
||||
setView(LinearLayout(context).apply {
|
||||
setView(LinearLayout(activity).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
addView(EditText(context).apply {
|
||||
addView(EditText(activity).apply {
|
||||
hint = "http(s)://server.com:1234"
|
||||
val str = sp.getString("serverip", "") ?: ""
|
||||
setText(str.toCharArray(), 0, str.length)
|
||||
@ -74,7 +126,7 @@ class MainHook : IXposedHookLoadPackage, IXposedHookZygoteInit {
|
||||
}
|
||||
})
|
||||
})
|
||||
addView(Switch(context).apply {
|
||||
addView(Switch(activity).apply {
|
||||
text = moduleRes.getString(R.string.ForcedMode)
|
||||
isChecked = sp.getBoolean("forceUrl", false)
|
||||
setOnClickListener {
|
||||
@ -85,15 +137,28 @@ class MainHook : IXposedHookLoadPackage, IXposedHookZygoteInit {
|
||||
forceUrl = (it as Switch).isChecked
|
||||
}
|
||||
})
|
||||
addView(Switch(activity).apply {
|
||||
text = moduleRes.getString(R.string.ProxyList)
|
||||
isChecked = sp.getBoolean("ProxyList", false)
|
||||
setOnClickListener {
|
||||
sp.edit().run {
|
||||
putBoolean("ProxyList", (it as Switch).isChecked)
|
||||
apply()
|
||||
}
|
||||
proxyList = (it as Switch).isChecked
|
||||
}
|
||||
})
|
||||
setNegativeButton(moduleRes.getString(R.string.CustomServer)) { _, _ ->
|
||||
})
|
||||
setPositiveButton(moduleRes.getString(R.string.CustomServer)) { _, _ ->
|
||||
val ip = sp.getString("serverip", "") ?: ""
|
||||
if (ip == "") {
|
||||
Toast.makeText(context, moduleRes.getString(R.string.ServerAddressError), Toast.LENGTH_LONG).show()
|
||||
context.finish()
|
||||
Toast.makeText(activity, moduleRes.getString(R.string.ServerAddressError), Toast.LENGTH_LONG).show()
|
||||
activity.finish()
|
||||
} else {
|
||||
server = ip
|
||||
forceUrl = true
|
||||
// TODO: The input box cannot enter content
|
||||
// gmTool()
|
||||
}
|
||||
}
|
||||
setNeutralButton(moduleRes.getString(R.string.OfficialServer)) { _, _ ->
|
||||
@ -104,6 +169,249 @@ class MainHook : IXposedHookLoadPackage, IXposedHookZygoteInit {
|
||||
}
|
||||
}
|
||||
|
||||
inner class MoveOnTouchListener : View.OnTouchListener {
|
||||
private var originalXPos = 0
|
||||
private var originalYPos = 0
|
||||
|
||||
private var offsetX = 0f
|
||||
private var offsetY = 0f
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onTouch(v: View, event: MotionEvent): Boolean {
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
val x = event.rawX
|
||||
val y = event.rawY
|
||||
|
||||
val location = IntArray(2)
|
||||
v.getLocationOnScreen(location)
|
||||
|
||||
originalXPos = location[0]
|
||||
originalYPos = location[1]
|
||||
|
||||
offsetX = x - originalXPos
|
||||
offsetY = y - originalYPos
|
||||
}
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
val onScreen = IntArray(2)
|
||||
v.getLocationOnScreen(onScreen)
|
||||
|
||||
val x = event.rawX
|
||||
val y = event.rawY
|
||||
|
||||
val params: WindowManager.LayoutParams =
|
||||
v.layoutParams as WindowManager.LayoutParams
|
||||
|
||||
val newX = (x - offsetX).toInt()
|
||||
val newY = (y - offsetY).toInt()
|
||||
|
||||
if (newX == originalXPos && newY == originalYPos) {
|
||||
return false
|
||||
}
|
||||
|
||||
params.x = newX
|
||||
params.y = newY
|
||||
|
||||
windowManager.updateViewLayout(v, params)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private lateinit var imageView: ImageView
|
||||
private lateinit var mainView: ScrollView
|
||||
private fun gmTool() {
|
||||
if (this::mainView.isInitialized) return
|
||||
if (this::imageView.isInitialized) return
|
||||
if (isInit) return
|
||||
isInit = true
|
||||
mainView = ScrollView(activity).apply {
|
||||
visibility = View.GONE
|
||||
addView(LinearLayout(activity).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT)
|
||||
setBackgroundColor(Color.parseColor("#5F000000"))
|
||||
addView(LinearLayout(activity).apply {
|
||||
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)
|
||||
setBackgroundColor(Color.parseColor("#8F000000"))
|
||||
addView(TextView(activity).apply {
|
||||
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
|
||||
setTextColor(Color.BLUE)
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_SP, 18f)
|
||||
text = "Tools"
|
||||
})
|
||||
addView(TextView(activity).apply {
|
||||
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT)
|
||||
setTextColor(Color.BLUE)
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_SP, 18f)
|
||||
text = "X"
|
||||
setOnClickListener {
|
||||
mainView.visibility = View.GONE
|
||||
imageView.visibility = View.VISIBLE
|
||||
}
|
||||
})
|
||||
})
|
||||
addView(LinearLayout(activity).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.MATCH_PARENT)
|
||||
var userEdit: EditText
|
||||
var passEdit: EditText
|
||||
addView(LinearLayout(activity).apply {
|
||||
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)
|
||||
addView(TextView(activity).apply {
|
||||
setTextColor(Color.BLUE)
|
||||
text = "(Check server stats)"
|
||||
setOnClickListener {
|
||||
Thread() {
|
||||
try {
|
||||
XposedBridge.log("$server/authentication/type")
|
||||
(URL("$server/authentication/type").openConnection() as HttpURLConnection).apply {
|
||||
requestMethod = "GET"
|
||||
readTimeout = 8000
|
||||
connectTimeout = 8000
|
||||
val reader = BufferedReader(InputStreamReader(inputStream))
|
||||
if (responseCode == 200) {
|
||||
val response = StringBuilder()
|
||||
var line = ""
|
||||
while (reader.readLine()?.also { line = it } != null) {
|
||||
response.append(line)
|
||||
}
|
||||
runOnMainThread {
|
||||
text = if (response.toString() == "me.exzork.gcauth.handler.GCAuthAuthenticationHandler") "Server stats: GcAuth" else "Server stats: GcAuth not install"
|
||||
}
|
||||
} else {
|
||||
runOnMainThread {
|
||||
text = "Server stats: Get server stats error. "
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
runOnMainThread {
|
||||
text = "Server stats: Get server stats error. $e"
|
||||
}
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
})
|
||||
})
|
||||
addView(LinearLayout(activity).apply {
|
||||
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)
|
||||
addView(TextView(activity).apply {
|
||||
setTextColor(Color.BLUE)
|
||||
text = "User:"
|
||||
})
|
||||
addView(EditText(activity).apply {
|
||||
userEdit = this
|
||||
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)
|
||||
})
|
||||
})
|
||||
addView(LinearLayout(activity).apply {
|
||||
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)
|
||||
addView(TextView(activity).apply {
|
||||
setTextColor(Color.BLUE)
|
||||
text = "Password:"
|
||||
})
|
||||
addView(EditText(activity).apply {
|
||||
passEdit = this
|
||||
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)
|
||||
})
|
||||
})
|
||||
addView(LinearLayout(activity).apply {
|
||||
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)
|
||||
addView(Button(activity).apply {
|
||||
text = "Login"
|
||||
layoutParams = LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT)
|
||||
setOnClickListener {
|
||||
Thread() {
|
||||
try {
|
||||
(URL("$server/authentication/login").openConnection() as HttpURLConnection).apply {
|
||||
requestMethod = "POST"
|
||||
readTimeout = 8000
|
||||
connectTimeout = 8000
|
||||
doOutput = true
|
||||
doInput = true
|
||||
useCaches = false
|
||||
|
||||
outputStream.apply {
|
||||
write(URLEncoder.encode("{\"username\":\"${userEdit.text}\",\"password\":\"${passEdit.text}\"}", "UTF-8").toByteArray())
|
||||
flush()
|
||||
}
|
||||
if (responseCode == 200) {
|
||||
val input = inputStream
|
||||
val message = ByteArrayOutputStream()
|
||||
|
||||
var len: Int
|
||||
val buffer = ByteArray(1024)
|
||||
while (input.read(buffer).also { len = it } != -1) {
|
||||
message.write(buffer, 0, len)
|
||||
}
|
||||
input.close()
|
||||
message.close()
|
||||
|
||||
val json = JSONObject(String(message.toByteArray()))
|
||||
if (json.optBoolean("success", false)) {
|
||||
val token = json.optString("jwt", "")
|
||||
runOnMainThread {
|
||||
Toast.makeText(activity, "Login success\n${token}", Toast.LENGTH_LONG).show()
|
||||
(activity.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager).text = token
|
||||
}
|
||||
} else {
|
||||
runOnMainThread {
|
||||
Toast.makeText(activity, "Login failed, ${json.optString("message", "")}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
runOnMainThread {
|
||||
Toast.makeText(activity, "Login error, $e", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
windowManager = activity.windowManager
|
||||
windowManager.addView(mainView, WindowManager.LayoutParams(
|
||||
dp2px(activity, 200f),
|
||||
WindowManager.LayoutParams.MATCH_PARENT,
|
||||
WindowManager.LayoutParams.TYPE_APPLICATION,
|
||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL,
|
||||
PixelFormat.TRANSLUCENT
|
||||
).apply {
|
||||
gravity = Gravity.START or Gravity.TOP
|
||||
x = 0
|
||||
y = 0
|
||||
})
|
||||
|
||||
val layoutParams = WindowManager.LayoutParams(
|
||||
WindowManager.LayoutParams.WRAP_CONTENT,
|
||||
WindowManager.LayoutParams.WRAP_CONTENT,
|
||||
WindowManager.LayoutParams.TYPE_APPLICATION,
|
||||
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL,
|
||||
PixelFormat.TRANSLUCENT
|
||||
).apply {
|
||||
gravity = Gravity.START or Gravity.TOP
|
||||
x = 0
|
||||
y = 0
|
||||
}
|
||||
imageView = ImageView(activity).apply {
|
||||
background = moduleRes.getDrawable(R.drawable.ic_android_black_24dp).also { it.alpha = 50 }
|
||||
this.layoutParams = layoutParams
|
||||
setOnTouchListener(MoveOnTouchListener())
|
||||
setOnClickListener {
|
||||
mainView.visibility = View.VISIBLE
|
||||
it.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
windowManager.addView(imageView, layoutParams)
|
||||
}
|
||||
|
||||
private fun sslHook(lpparam: XC_LoadPackage.LoadPackageParam) {
|
||||
findMethodOrNull("com.combosdk.lib.third.okhttp3.internal.tls.OkHostnameVerifier") { name == "verify" }?.hookBefore {
|
||||
it.result = true
|
||||
@ -141,15 +449,23 @@ class MainHook : IXposedHookLoadPackage, IXposedHookZygoteInit {
|
||||
}
|
||||
|
||||
private fun replaceUrl(method: XC_MethodHook.MethodHookParam, args: Int) {
|
||||
if (!forceUrl) return
|
||||
if (!forceUrl && !proxyList) return
|
||||
if (!this::server.isInitialized) return
|
||||
if (server == "") return
|
||||
|
||||
if (BuildConfig.DEBUG) XposedBridge.log("old: " + method.args[args].toString())
|
||||
if (proxyList) {
|
||||
for (list in proxyListRegex) {
|
||||
for (head in arrayListOf("http://", "https://")) {
|
||||
method.args[args] = method.args[args].toString().replace(head + list, server)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val m = regex.matcher(method.args[args].toString())
|
||||
if (m.find()) {
|
||||
method.args[args] = m.replaceAll(server)
|
||||
}
|
||||
}
|
||||
if (BuildConfig.DEBUG) XposedBridge.log("new: " + method.args[args].toString())
|
||||
}
|
||||
}
|
||||
|
12
app/src/main/java/xfk233/GenshinProxy/Utils.kt
Normal file
12
app/src/main/java/xfk233/GenshinProxy/Utils.kt
Normal file
@ -0,0 +1,12 @@
|
||||
package xfk233.GenshinProxy
|
||||
|
||||
import android.content.Context
|
||||
|
||||
object Utils {
|
||||
var isInit = false
|
||||
|
||||
fun dp2px(context: Context, dpValue: Float): Int {
|
||||
val scale: Float = context.resources.displayMetrics.density
|
||||
return (dpValue * scale + 0.5f).toInt()
|
||||
}
|
||||
}
|
5
app/src/main/res/drawable/ic_android_black_24dp.xml
Normal file
5
app/src/main/res/drawable/ic_android_black_24dp.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#213150"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M17.6,11.48 L19.44,8.3a0.63,0.63 0,0 0,-1.09 -0.63l-1.88,3.24a11.43,11.43 0,0 0,-8.94 0L5.65,7.67a0.63,0.63 0,0 0,-1.09 0.63L6.4,11.48A10.81,10.81 0,0 0,1 20L23,20A10.81,10.81 0,0 0,17.6 11.48ZM7,17.25A1.25,1.25 0,1 1,8.25 16,1.25 1.25,0 0,1 7,17.25ZM17,17.25A1.25,1.25 0,1 1,18.25 16,1.25 1.25,0 0,1 17,17.25Z"/>
|
||||
</vector>
|
@ -7,4 +7,5 @@
|
||||
<string name="Tips">第一次使用请输入好服务器地址后进入官方服务器下载资源,之后便可进入私服。\n强制模式: 强制替换全部域名\n\n请输入服务器地址: </string>
|
||||
<string name="ForcedMode">强制模式 (需重启)</string>
|
||||
<string name="ServerAddressError">服务器地址错误</string>
|
||||
<string name="ProxyList">只代理列表内的地址 (需重启)</string>
|
||||
</resources>
|
@ -6,4 +6,5 @@
|
||||
<string name="Tips">For the first time, please enter the server address and enter the official server to download resources. Then you can enter the private server.\nForced mode: Forced replacement of all domain names\n\nInput server address: </string>
|
||||
<string name="ForcedMode">Forced mode (restart required)</string>
|
||||
<string name="ServerAddressError">Server address error.</string>
|
||||
<string name="ProxyList">Only addresses in the proxy list (restart required)</string>
|
||||
</resources>
|
Loading…
Reference in New Issue
Block a user