mirror of
https://github.com/fumiama/copymanga.git
synced 2026-06-11 02:50:28 +08:00
v2.5.2
优化 1. API访问流程 2. 更多访问报错信息 升级 1. appcompat -> 1.7.1
This commit is contained in:
@@ -10,9 +10,10 @@ android {
|
||||
compileSdk 34
|
||||
applicationId 'top.fumiama.copymanga'
|
||||
minSdkVersion 23
|
||||
//noinspection OldTargetApi
|
||||
targetSdkVersion 34
|
||||
versionCode 72
|
||||
versionName '2.5.1'
|
||||
versionCode 73
|
||||
versionName '2.5.2'
|
||||
resourceConfigurations += ['zh', 'zh-rCN']
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
@@ -47,7 +48,7 @@ android {
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
signingConfig signingConfigs.release
|
||||
}
|
||||
/*winrelease {
|
||||
/*winrelease {r
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
@@ -98,7 +99,7 @@ dependencies {
|
||||
|
||||
//noinspection GradleDependency
|
||||
implementation 'androidx.core:core-ktx:1.12.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.7.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.7.1'
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
implementation 'com.google.android.material:material:1.12.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
|
||||
|
||||
@@ -157,6 +157,9 @@ class MainActivity : AppCompatActivity() {
|
||||
isMenuWaiting = true
|
||||
Log.d("MyMain", "start menu waiting")
|
||||
lifecycleScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
Config.myHostApiUrl.init()
|
||||
}
|
||||
withContext(Dispatchers.IO) {
|
||||
delay(1000)
|
||||
withContext(Dispatchers.Main) {
|
||||
@@ -430,7 +433,7 @@ class MainActivity : AppCompatActivity() {
|
||||
dl.setMessage("${getString(R.string.app_description)}\n" +
|
||||
"\n$comandy\n" +
|
||||
"$comancry\n\n"+ File("/proc/self/cmdline").readText() + "\n" +
|
||||
"当前API: ${Config.myHostApiUrl.joinToString(", ")}")
|
||||
"当前API: ${Config.myHostApiUrl.getApis().joinToString(", ")}")
|
||||
dl.setTitle("${getString(R.string.action_info)} ${BuildConfig.VERSION_NAME}")
|
||||
dl.setIcon(R.mipmap.ic_launcher)
|
||||
dl.setPositiveButton(android.R.string.ok) { _, _ -> }
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
package top.fumiama.copymanga.api
|
||||
|
||||
import android.util.Log
|
||||
import com.bumptech.glide.load.model.LazyHeaders
|
||||
import com.google.gson.Gson
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import top.fumiama.copymanga.MainActivity
|
||||
import top.fumiama.copymanga.json.NetworkStructure
|
||||
import top.fumiama.copymanga.net.DownloadTools
|
||||
import top.fumiama.copymanga.api.network.Api
|
||||
import top.fumiama.copymanga.net.Proxy
|
||||
import top.fumiama.copymanga.net.Resolution
|
||||
import top.fumiama.copymanga.storage.PreferenceBoolean
|
||||
@@ -61,49 +55,9 @@ object Config {
|
||||
}
|
||||
|
||||
val proxyUrl = MainActivity.mainWeakReference?.get()?.getString(R.string.proxyUrl)!!
|
||||
private val reverseProxyUrl = PreferenceString(R.string.reverseProxyKeyID)
|
||||
private val networkApiUrl = PreferenceString("settings_cat_net_et_api_url", R.string.hostUrl)
|
||||
private var mHostApiUrls: Array<String> = arrayOf()
|
||||
private var mHostApiUrlsMutex = Mutex()
|
||||
val myHostApiUrl: Array<String>
|
||||
get() {
|
||||
if (mHostApiUrls.isNotEmpty()) return mHostApiUrls
|
||||
if (reverseProxyUrl.value.isNotEmpty() && reverseProxyUrl.value != proxyUrl) {
|
||||
mHostApiUrls = arrayOf(reverseProxyUrl.value)
|
||||
Log.d("MyC", "myHostApiUrl set reverse proxy to ${mHostApiUrls[0]}")
|
||||
return mHostApiUrls
|
||||
}
|
||||
MainActivity.mainWeakReference?.get()?.apply {
|
||||
runBlocking {
|
||||
mHostApiUrlsMutex.withLock {
|
||||
if (mHostApiUrls.isNotEmpty()) return@runBlocking
|
||||
try {
|
||||
val u = getString(R.string.networkApiUrl).format(networkApiUrl.value, platform.value)
|
||||
val r = Gson().fromJson((apiProxy?.comancry(u) {
|
||||
DownloadTools.getHttpContent(it, referer, pc_ua)
|
||||
}?:DownloadTools.getHttpContent(u, referer, pc_ua)).decodeToString(), NetworkStructure::class.java)
|
||||
if (r != null) {
|
||||
Log.d("MyC", "myHostApiUrl get code ${r.code} msg ${r.message}")
|
||||
if (r.code == 200 && r.results != null) {
|
||||
// need header umstring
|
||||
// r.results.api.forEach { it.forEach { api -> if (!api.isNullOrEmpty() && api !in field) field += api } }
|
||||
r.results.share.forEach { api -> if (!api.isNullOrEmpty() && api !in mHostApiUrls) mHostApiUrls += api }
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
mHostApiUrls = arrayOf(networkApiUrl.value)
|
||||
}
|
||||
if (mHostApiUrls.isEmpty()) {
|
||||
mHostApiUrls = arrayOf(networkApiUrl.value)
|
||||
Log.d("MyC", "myHostApiUrl set default ${mHostApiUrls[0]}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Log.d("MyC", "myHostApiUrl get hosts ${mHostApiUrls.joinToString(", ")}")
|
||||
return mHostApiUrls
|
||||
}
|
||||
val reverseProxyUrl = PreferenceString(R.string.reverseProxyKeyID)
|
||||
val networkApiUrl = PreferenceString("settings_cat_net_et_api_url", R.string.hostUrl)
|
||||
val myHostApiUrl = Api()
|
||||
val navTextInfo = UserPreferenceString("navTextInfo", R.string.navTextInfo)
|
||||
val proxy_key = PreferenceString(R.string.imgProxyCodeKeyID)
|
||||
val app_ver = PreferenceString("settings_cat_general_et_app_version", R.string.app_ver)
|
||||
@@ -130,6 +84,7 @@ object Config {
|
||||
private val net_use_img_proxy = PreferenceBoolean("settings_cat_net_sw_use_img_proxy", false)
|
||||
val net_use_api_proxy = PreferenceBoolean("settings_cat_net_sw_use_api_proxy", false)
|
||||
val net_img_resolution = PreferenceString(R.string.imgResolutionKeyID)
|
||||
val net_umstring = PreferenceString("settings_cat_net_et_umstring")
|
||||
|
||||
val view_manga_inverse_chapters = PreferenceBoolean("settings_cat_vm_sw_inverse_chapters", false)
|
||||
val view_manga_always_dark_bg = PreferenceBoolean("settings_cat_vm_sw_always_dark_bg", false)
|
||||
@@ -144,5 +99,5 @@ object Config {
|
||||
|
||||
fun getChapterInfoApiUrl(path: String?, uuid: String?, version: Int) =
|
||||
MainActivity.mainWeakReference?.get()?.getString(R.string.chapterInfoApiUrl)
|
||||
?.format(myHostApiUrl.random(), path, if (version >= 2) "$version" else "" , uuid, platform.value)
|
||||
?.format(path, if (version >= 2) "$version" else "" , uuid, platform.value)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
package top.fumiama.copymanga.api.manga
|
||||
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import com.google.gson.Gson
|
||||
import kotlinx.android.synthetic.main.card_book.*
|
||||
import kotlinx.android.synthetic.main.line_booktandb.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import top.fumiama.copymanga.MainActivity
|
||||
import top.fumiama.copymanga.json.BookInfoStructure
|
||||
import top.fumiama.copymanga.json.ThemeStructure
|
||||
import top.fumiama.copymanga.json.VolumeStructure
|
||||
@@ -15,8 +17,7 @@ import top.fumiama.dmzj.copymanga.R
|
||||
import java.io.File
|
||||
|
||||
class Book(val path: String, private val getString: (Int) -> String, private val exDir: File, private val loadCache: Boolean = false, private val mPassName: String? = null) {
|
||||
private val mBookApiUrl = getString(R.string.bookInfoApiUrl).format(Config.myHostApiUrl.random(), path, Config.platform.value)
|
||||
private val mUserAgent = getString(R.string.pc_ua).format(Config.app_ver.value)
|
||||
private val mBookApiUrl = getString(R.string.bookInfoApiUrl).format(path, Config.platform.value)
|
||||
private var mBook: BookInfoStructure? = null
|
||||
private var mGroupPathWords = arrayOf<String>()
|
||||
private var mKeys = arrayOf<String>()
|
||||
@@ -57,22 +58,16 @@ class Book(val path: String, private val getString: (Int) -> String, private val
|
||||
suspend fun updateInfo() = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
var isDownload = false
|
||||
val data: ByteArray = if (loadCache) {
|
||||
val data: String = if (loadCache) {
|
||||
name?.let { loadInfo(it) } ?: run {
|
||||
isDownload = true
|
||||
Config.apiProxy?.comancry(mBookApiUrl) { url ->
|
||||
DownloadTools.getHttpContent(url, null, mUserAgent)
|
||||
}?:DownloadTools.getHttpContent(mBookApiUrl, null, mUserAgent)
|
||||
Config.myHostApiUrl.get(mBookApiUrl)
|
||||
}
|
||||
} else {
|
||||
isDownload = true
|
||||
Config.apiProxy?.comancry(mBookApiUrl) { url ->
|
||||
DownloadTools.getHttpContent(url, null, mUserAgent)
|
||||
}?:DownloadTools.getHttpContent(mBookApiUrl, null, mUserAgent)
|
||||
}?:DownloadTools.getHttpContent(mBookApiUrl, null, mUserAgent)
|
||||
mBook = data.inputStream().use {
|
||||
Gson().fromJson(it.reader(), BookInfoStructure::class.java)
|
||||
Config.myHostApiUrl.get(mBookApiUrl)
|
||||
}
|
||||
mBook = Gson().fromJson(data, BookInfoStructure::class.java)
|
||||
if (isDownload) saveInfo(data)
|
||||
mGroupPathWords = arrayOf()
|
||||
mKeys = arrayOf()
|
||||
@@ -88,6 +83,9 @@ class Book(val path: String, private val getString: (Int) -> String, private val
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
MainActivity.mainWeakReference?.get()?.apply {
|
||||
Toast.makeText(this, e.message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,19 +160,19 @@ class Book(val path: String, private val getString: (Int) -> String, private val
|
||||
}?:false
|
||||
}
|
||||
|
||||
private suspend fun saveInfo(data: ByteArray) = withContext(Dispatchers.IO) {
|
||||
private suspend fun saveInfo(data: String) = withContext(Dispatchers.IO) {
|
||||
name?.let { name ->
|
||||
val mangaFolder = File(exDir, name)
|
||||
if(!mangaFolder.exists()) mangaFolder.mkdirs()
|
||||
File(mangaFolder, "meta.json").writeBytes(data)
|
||||
File(mangaFolder, "meta.json").writeText(data)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadInfo(name: String): ByteArray? = withContext(Dispatchers.IO) {
|
||||
private suspend fun loadInfo(name: String): String? = withContext(Dispatchers.IO) {
|
||||
val mangaFolder = File(exDir, name)
|
||||
if(!mangaFolder.exists()) mangaFolder.mkdirs()
|
||||
val f = File(mangaFolder, "meta.json")
|
||||
if (!f.exists()) return@withContext null
|
||||
return@withContext f.readBytes()
|
||||
return@withContext f.readBytes().decodeToString()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,14 +3,13 @@ package top.fumiama.copymanga.api.manga
|
||||
import com.google.gson.Gson
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import top.fumiama.copymanga.api.Config
|
||||
import top.fumiama.copymanga.json.BookQueryStructure
|
||||
import top.fumiama.copymanga.json.ReturnBase
|
||||
import top.fumiama.copymanga.api.Config
|
||||
import top.fumiama.copymanga.net.DownloadTools
|
||||
import top.fumiama.dmzj.copymanga.R
|
||||
|
||||
class Shelf(private val getString: (Int) -> String) {
|
||||
private val apiUrl: String get() = getString(R.string.shelfOperateApiUrl).format(Config.myHostApiUrl.random())
|
||||
private val apiUrl: String get() = getString(R.string.shelfOperateApiUrl)
|
||||
private val queryApiUrlTemplate = getString(R.string.bookUserQueryApiUrl)
|
||||
private val addApiUrl get() = "$apiUrl?platform=${Config.platform.value}"
|
||||
private val delApiUrl get() = "${apiUrl}s?platform=${Config.platform.value}"
|
||||
@@ -25,17 +24,13 @@ class Shelf(private val getString: (Int) -> String) {
|
||||
append("")
|
||||
append(Config.token.value)
|
||||
}
|
||||
val re = (Config.apiProxy?.comancry(addApiUrl) { url ->
|
||||
DownloadTools.requestWithBody(
|
||||
url, "POST", body.encodeToByteArray()
|
||||
)
|
||||
}?:DownloadTools.requestWithBody(
|
||||
addApiUrl, "POST", body.encodeToByteArray()
|
||||
))?.decodeToString() ?: return@withContext "空回应"
|
||||
return@withContext try {
|
||||
val re = Config.myHostApiUrl.request(
|
||||
addApiUrl, body.encodeToByteArray(), "POST",
|
||||
"application/x-www-form-urlencoded;charset=utf-8")
|
||||
Gson().fromJson(re, ReturnBase::class.java).message
|
||||
} catch (e: Exception) {
|
||||
"$re ${e.message}"
|
||||
e.message?:e::class.simpleName?:e.toString()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,31 +47,20 @@ class Shelf(private val getString: (Int) -> String) {
|
||||
append("authorization=Token+")
|
||||
append(Config.token.value)
|
||||
}
|
||||
val re = (Config.apiProxy?.comancry(delApiUrl) { url ->
|
||||
DownloadTools.requestWithBody(
|
||||
url, "DELETE", body.encodeToByteArray()
|
||||
)
|
||||
}?:DownloadTools.requestWithBody(
|
||||
delApiUrl, "DELETE", body.encodeToByteArray()
|
||||
))?.decodeToString() ?: return@withContext "空回应"
|
||||
return@withContext try {
|
||||
val re = Config.myHostApiUrl.request(
|
||||
delApiUrl, body.encodeToByteArray(),
|
||||
"DELETE", "application/x-www-form-urlencoded;charset=utf-8")
|
||||
Gson().fromJson(re, ReturnBase::class.java).message
|
||||
} catch (e: Exception) {
|
||||
"$re ${e.message}"
|
||||
e.message?:e::class.simpleName?:e.toString()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun query(pathWord: String): BookQueryStructure? = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val queryUrl = queryApiUrlTemplate.format(Config.myHostApiUrl.random(), pathWord, Config.platform.value)
|
||||
(Config.apiProxy?.comancry(queryUrl) { url ->
|
||||
DownloadTools.getHttpContent(url, Config.referer)
|
||||
}?:DownloadTools.getHttpContent(queryUrl, Config.referer)).let {
|
||||
Gson().fromJson(it.decodeToString(), BookQueryStructure::class.java)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
val queryUrl = queryApiUrlTemplate.format(pathWord, Config.platform.value)
|
||||
Config.myHostApiUrl.get(queryUrl).let {
|
||||
Gson().fromJson(it, BookQueryStructure::class.java)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ class Volume(private val path: String, private val groupPathWord: String, getStr
|
||||
return@withContext mVolume
|
||||
}
|
||||
|
||||
private fun getApiUrl(offset: Int) = mGroupInfoApiUrlTemplate.format(Config.myHostApiUrl.random(), path, groupPathWord, offset, Config.platform.value)
|
||||
private fun getApiUrl(offset: Int) = mGroupInfoApiUrlTemplate.format(path, groupPathWord, offset, Config.platform.value)
|
||||
private suspend fun download(re: Array<VolumeStructure?>, offset: Int, c: Int) = withContext(Dispatchers.IO) {
|
||||
Log.d("MyV", "下载偏移: $offset")
|
||||
getApiUrl(offset).let {
|
||||
|
||||
112
app/src/main/java/top/fumiama/copymanga/api/network/Api.kt
Normal file
112
app/src/main/java/top/fumiama/copymanga/api/network/Api.kt
Normal file
@@ -0,0 +1,112 @@
|
||||
package top.fumiama.copymanga.api.network
|
||||
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import com.google.gson.Gson
|
||||
import top.fumiama.copymanga.MainActivity
|
||||
import top.fumiama.copymanga.api.Config.apiProxy
|
||||
import top.fumiama.copymanga.api.Config.networkApiUrl
|
||||
import top.fumiama.copymanga.api.Config.platform
|
||||
import top.fumiama.copymanga.api.Config.proxyUrl
|
||||
import top.fumiama.copymanga.api.Config.reverseProxyUrl
|
||||
import top.fumiama.copymanga.json.NetworkStructure
|
||||
import top.fumiama.copymanga.json.ReturnBase
|
||||
import top.fumiama.copymanga.net.DownloadTools
|
||||
import top.fumiama.dmzj.copymanga.R
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock
|
||||
import kotlin.concurrent.read
|
||||
import kotlin.concurrent.write
|
||||
|
||||
class Api {
|
||||
private var mHostApiUrls = mutableListOf<String>()
|
||||
private var mu = ReentrantReadWriteLock()
|
||||
|
||||
fun getApis(): Array<String> {
|
||||
return mHostApiUrls.toTypedArray()
|
||||
}
|
||||
|
||||
suspend fun init() {
|
||||
if (mHostApiUrls.isNotEmpty()) return
|
||||
if (reverseProxyUrl.value.isNotEmpty() && reverseProxyUrl.value != proxyUrl) {
|
||||
mu.write { mHostApiUrls = mutableListOf(reverseProxyUrl.value) }
|
||||
Log.d("MyApi", "myHostApiUrl set reverse proxy to ${reverseProxyUrl.value}")
|
||||
return
|
||||
}
|
||||
MainActivity.mainWeakReference?.get()?.apply {
|
||||
mu.write {
|
||||
if (mHostApiUrls.isNotEmpty()) return
|
||||
try {
|
||||
val d = get(getString(R.string.networkApiUrl).format(platform.value), networkApiUrl.value)
|
||||
val r = Gson().fromJson(d, NetworkStructure::class.java)
|
||||
if (r != null) {
|
||||
Log.d("MyApi", "myHostApiUrl get code ${r.code} msg ${r.message}")
|
||||
if (r.results != null) {
|
||||
r.results.api.forEach { it.forEach { api -> if (!api.isNullOrEmpty() && api !in mHostApiUrls) mHostApiUrls += api } }
|
||||
r.results.share.forEach { api -> if (!api.isNullOrEmpty() && api !in mHostApiUrls) mHostApiUrls += api }
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
mHostApiUrls = mutableListOf(networkApiUrl.value)
|
||||
Toast.makeText(this, e.message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
if (mHostApiUrls.isEmpty()) {
|
||||
mHostApiUrls = mutableListOf(networkApiUrl.value)
|
||||
Log.d("MyApi", "myHostApiUrl set default ${mHostApiUrls[0]}")
|
||||
Toast.makeText(this, "无法获取API列表", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
Log.d("MyApi", "myHostApiUrl get hosts ${mHostApiUrls.joinToString(", ")}")
|
||||
}
|
||||
// get throw error on non-json or non-200 or empty apis, path: /api/v3/xxx, return json string
|
||||
suspend fun get(path: String, forceApi: String? = null): String {
|
||||
val apis = if (forceApi == null) mu.read { mHostApiUrls } else mutableListOf(forceApi)
|
||||
if (apis.isEmpty()) {
|
||||
throw NoSuchElementException("API列表为空")
|
||||
}
|
||||
var r: ReturnBase? = null
|
||||
apis.forEach { api ->
|
||||
val u = "https://$api$path"
|
||||
try {
|
||||
val ret = (apiProxy?.comancry(u) {
|
||||
DownloadTools.getApiContent(it)
|
||||
}?: DownloadTools.getApiContent(u)).decodeToString()
|
||||
r = Gson().fromJson(ret, ReturnBase::class.java)
|
||||
if (r!!.code != 200) {
|
||||
mu.write { mHostApiUrls.remove(api) }
|
||||
} else {
|
||||
return ret
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
mu.write { mHostApiUrls.remove(api) }
|
||||
}
|
||||
}
|
||||
throw IllegalStateException("错误码${r!!.code}, 信息: ${r!!.message}")
|
||||
}
|
||||
// request throw error on non-json or non-200 or empty apis, path: /api/v3/xxx, return json string
|
||||
suspend fun request(path: String, body: ByteArray, method: String, contentType: String, forceApi: String? = null): String {
|
||||
val apis = if (forceApi == null) mu.read { mHostApiUrls } else mutableListOf(forceApi)
|
||||
if (apis.isEmpty()) {
|
||||
throw NoSuchElementException("API列表为空")
|
||||
}
|
||||
var r: ReturnBase? = null
|
||||
apis.forEach { api ->
|
||||
val u = "https://$api$path"
|
||||
try {
|
||||
val ret = (apiProxy?.comancry(u) {
|
||||
DownloadTools.requestApiWithBody(u, method, body, contentType)
|
||||
}?: DownloadTools.requestApiWithBody(u, method, body, contentType)).decodeToString()
|
||||
r = Gson().fromJson(ret, ReturnBase::class.java)
|
||||
if (r!!.code != 200) {
|
||||
mu.write { mHostApiUrls.remove(api) }
|
||||
} else {
|
||||
return ret
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
mu.write { mHostApiUrls.remove(api) }
|
||||
}
|
||||
}
|
||||
throw IllegalStateException("错误码${r!!.code}, 信息: ${r!!.message}")
|
||||
}
|
||||
}
|
||||
@@ -5,10 +5,7 @@ import com.google.gson.Gson
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import top.fumiama.copymanga.api.Config
|
||||
import top.fumiama.copymanga.json.ComandyCapsule
|
||||
import top.fumiama.copymanga.json.LoginInfoStructure
|
||||
import top.fumiama.copymanga.lib.Comandy
|
||||
import top.fumiama.copymanga.net.DownloadTools
|
||||
import top.fumiama.dmzj.copymanga.R
|
||||
import java.net.URLEncoder
|
||||
import java.nio.charset.Charset
|
||||
@@ -17,23 +14,16 @@ class Member(private val getString: (Int) -> String) {
|
||||
val hasLogin: Boolean get() = Config.token.value?.isNotEmpty() ?: false
|
||||
suspend fun login(username: String, pwd: String, salt: Int): LoginInfoStructure =
|
||||
withContext(Dispatchers.IO) {
|
||||
var err = ""
|
||||
(if (!Config.net_use_api_proxy.value && Comandy.instance.enabled)
|
||||
postComandyLogin(username, pwd, salt)
|
||||
else postLogin(username, pwd, salt))?.let { data ->
|
||||
try {
|
||||
return@withContext saveInfo(data)
|
||||
} catch (e: Exception) {
|
||||
err = e.message.toString()
|
||||
}
|
||||
} ?: run { err = getString(R.string.login_get_conn_failed) }
|
||||
val l = LoginInfoStructure()
|
||||
l.code = 400
|
||||
l.message = err
|
||||
return@withContext l
|
||||
return@withContext try {
|
||||
saveInfo(postLogin(username, pwd, salt))
|
||||
} catch (e: Exception) {
|
||||
val l = LoginInfoStructure()
|
||||
l.code = 400
|
||||
l.message = e.message.toString()
|
||||
l
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获得登录信息并更新头像
|
||||
* @return 登录态
|
||||
@@ -49,24 +39,21 @@ class Member(private val getString: (Int) -> String) {
|
||||
}
|
||||
try {
|
||||
val u = getString(R.string.memberInfoApiUrl)
|
||||
.format(Config.myHostApiUrl.random(), Config.platform.value)
|
||||
val data = (Config.apiProxy?.comancry(u) {
|
||||
DownloadTools.getHttpContent(it)
|
||||
}?:DownloadTools.getHttpContent(u)).decodeToString()
|
||||
.format(Config.platform.value)
|
||||
try {
|
||||
val l = Gson().fromJson(data, LoginInfoStructure::class.java)
|
||||
val l = Gson().fromJson(Config.myHostApiUrl.get(u), LoginInfoStructure::class.java)
|
||||
if (l.code == 200) Config.avatar.value = l.results.avatar
|
||||
l
|
||||
} catch (e: Exception) {
|
||||
val l = LoginInfoStructure()
|
||||
l.code = 450
|
||||
l.message = "${getString(R.string.login_get_avatar_failed)}: $data"
|
||||
l.message = "${getString(R.string.login_get_avatar_failed)}: ${e.message}"
|
||||
l
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
val l = LoginInfoStructure()
|
||||
l.code = 450
|
||||
l.message = "${getString(R.string.login_get_avatar_failed)}: $e"
|
||||
l.message = "${getString(R.string.login_get_avatar_failed)}: ${e.message}"
|
||||
l
|
||||
}
|
||||
}
|
||||
@@ -96,69 +83,17 @@ class Member(private val getString: (Int) -> String) {
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun postLogin(username: String, pwd: String, salt: Int): ByteArray? =
|
||||
getString(R.string.loginApiUrl).format(Config.myHostApiUrl.random(), Config.platform.value).let { u ->
|
||||
val use: suspend (String) -> ByteArray? = { it: String ->
|
||||
DownloadTools.getApiConnection(it, "POST").let { c ->
|
||||
c.doOutput = true
|
||||
c.setRequestProperty(
|
||||
"content-type",
|
||||
"application/x-www-form-urlencoded;charset=utf-8"
|
||||
)
|
||||
c.setRequestProperty("platform", Config.platform.value)
|
||||
c.setRequestProperty("accept", "application/json")
|
||||
val r = if (!Config.net_use_foreign.value) "1" else "0"
|
||||
val pwdEncoded =
|
||||
Base64.encode("$pwd-$salt".toByteArray(), Base64.DEFAULT).decodeToString()
|
||||
c.outputStream.write(
|
||||
"username=${
|
||||
URLEncoder.encode(
|
||||
username,
|
||||
Charset.defaultCharset().name()
|
||||
)
|
||||
}&password=$pwdEncoded&salt=$salt&platform=${Config.platform.value}&authorization=Token+&version=${Config.app_ver.value}&source=copyApp®ion=$r&webp=1".toByteArray()
|
||||
)
|
||||
c.outputStream.close()
|
||||
val b = c.inputStream.readBytes()
|
||||
c.inputStream.close()
|
||||
b
|
||||
}
|
||||
}
|
||||
Config.apiProxy?.comancry(u, use)?:use(u)
|
||||
}
|
||||
|
||||
|
||||
private suspend fun postComandyLogin(username: String, pwd: String, salt: Int) =
|
||||
getString(R.string.loginApiUrl).format(Config.myHostApiUrl.random(), Config.platform.value).let { u ->
|
||||
val use: suspend (String) -> ByteArray? = { it: String ->
|
||||
DownloadTools.getComandyApiConnection(it, "POST", null, Config.pc_ua).apply {
|
||||
headers["content-type"] = "application/x-www-form-urlencoded;charset=utf-8"
|
||||
headers["platform"] = Config.platform.value
|
||||
headers["accept"] = "application/json"
|
||||
val r = if (!Config.net_use_foreign.value) "1" else "0"
|
||||
val pwdEncoded =
|
||||
Base64.encode("$pwd-$salt".toByteArray(), Base64.DEFAULT).decodeToString()
|
||||
data = "username=${
|
||||
URLEncoder.encode(
|
||||
username,
|
||||
Charset.defaultCharset().name()
|
||||
)
|
||||
}&password=$pwdEncoded&salt=$salt&platform=${Config.platform.value}&authorization=Token+&version=${Config.app_ver.value}&source=copyApp®ion=$r&webp=1"
|
||||
}.let { capsule ->
|
||||
try {
|
||||
val para = Gson().toJson(capsule)
|
||||
Comandy.instance.getInstance()?.request(para)?.let { result ->
|
||||
Gson().fromJson(result, ComandyCapsule::class.java)!!.let {
|
||||
if (it.code != 200) null
|
||||
else Base64.decode(it.data, Base64.DEFAULT)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
Config.apiProxy?.comancry(u, use)?:use(u)
|
||||
}
|
||||
private suspend fun postLogin(username: String, pwd: String, salt: Int): ByteArray =
|
||||
getString(R.string.loginApiUrl).format(Config.platform.value).let { u ->
|
||||
val r = if (!Config.net_use_foreign.value) "1" else "0"
|
||||
val pwdEncoded =
|
||||
Base64.encode("$pwd-$salt".toByteArray(), Base64.DEFAULT).decodeToString()
|
||||
Config.myHostApiUrl.request(u, "username=${
|
||||
URLEncoder.encode(
|
||||
username,
|
||||
Charset.defaultCharset().name()
|
||||
)
|
||||
}&password=$pwdEncoded&salt=$salt&platform=${Config.platform.value}&authorization=Token+&version=${Config.app_ver.value}&source=copyApp®ion=$r&webp=1".encodeToByteArray(),
|
||||
"POST", "application/x-www-form-urlencoded;charset=utf-8")
|
||||
}.encodeToByteArray()
|
||||
}
|
||||
|
||||
@@ -3,27 +3,18 @@ package top.fumiama.copymanga.lib
|
||||
import android.util.Log
|
||||
import top.fumiama.copymanga.api.Config
|
||||
import top.fumiama.copymanga.lib.template.LazyLibrary
|
||||
import top.fumiama.copymanga.net.DownloadTools
|
||||
|
||||
class Comandy: LazyLibrary<ComandyMethods>(
|
||||
ComandyMethods::class.java, "libcomandy.so", "网络增强",
|
||||
Config.net_use_comandy, Config.comandy_version
|
||||
) {
|
||||
private var mEnabled: Boolean? = null
|
||||
val enabled: Boolean
|
||||
get() {
|
||||
if (isInInit.get()) {
|
||||
Log.d("MyComandy", "$name block enabled for isInInit")
|
||||
return false
|
||||
}
|
||||
if (mEnabled != true && DownloadTools.failTimes.get() >= 16) {
|
||||
mEnabled = true
|
||||
return true
|
||||
}
|
||||
if (mEnabled != null) return mEnabled!!
|
||||
val v = isInUse.value
|
||||
mEnabled = v
|
||||
return v
|
||||
return isInUse.value
|
||||
}
|
||||
val status: String get() = if(enabled) {
|
||||
if (isInUse.value) "生效(手动)" else "生效(自动)"
|
||||
|
||||
@@ -6,11 +6,11 @@ import com.google.gson.Gson
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import top.fumiama.copymanga.MainActivity
|
||||
import top.fumiama.copymanga.api.Config
|
||||
import top.fumiama.copymanga.api.Config.proxyUrl
|
||||
import top.fumiama.copymanga.json.ComandyCapsule
|
||||
import top.fumiama.copymanga.lib.Comandy
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
@@ -21,61 +21,63 @@ import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.concurrent.Callable
|
||||
import java.util.concurrent.FutureTask
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import java.util.zip.GZIPInputStream
|
||||
|
||||
object DownloadTools {
|
||||
val failTimes = AtomicInteger(0)
|
||||
fun getApiConnection(url: String, method: String = "GET", refer: String? = null, ua: String? = null, timeout: Int = 20000): HttpURLConnection {
|
||||
private fun getApiConnection(url: String, method: String = "GET", timeout: Int = 20000): HttpURLConnection {
|
||||
val connection = URL(url).openConnection() as HttpURLConnection
|
||||
connection.requestMethod = method
|
||||
connection.connectTimeout = timeout
|
||||
connection.readTimeout = timeout
|
||||
connection.apply {
|
||||
/*setRequestProperty("host", if (url.startsWith("https://copymanga.azurewebsites.net")) {
|
||||
Uri.parse(url).getQueryParameter("url")?.substringAfter("://")?.substringBefore("/")?:""
|
||||
} else {
|
||||
url.substringAfter("://").substringBefore("/")
|
||||
})*/
|
||||
ua?.let { setRequestProperty("user-agent", it) }
|
||||
refer?.let { setRequestProperty("referer", it) }
|
||||
setRequestProperty("user-agent", Config.pc_ua)
|
||||
setRequestProperty("source", "copyApp")
|
||||
// deviceinfo
|
||||
setRequestProperty("webp", "1")
|
||||
setRequestProperty("region", if(!Config.net_use_foreign.value) "1" else "0")
|
||||
setRequestProperty("version", Config.app_ver.value)
|
||||
setRequestProperty("dt", SimpleDateFormat("yyyy.MM.dd", Locale.getDefault()).format(Calendar.getInstance().time))
|
||||
Config.token.value?.let { tk ->
|
||||
setRequestProperty("authorization", "Token $tk")
|
||||
}
|
||||
setRequestProperty("accept-encoding", "gzip")
|
||||
setRequestProperty("authorization", "Token${Config.token.value?.let { tk ->
|
||||
if (tk.isNotEmpty()) " $tk" else ""
|
||||
}}")
|
||||
setRequestProperty("platform", Config.platform.value)
|
||||
setRequestProperty("referer", Config.referer)
|
||||
setRequestProperty("accept", "application/json")
|
||||
setRequestProperty("version", Config.app_ver.value)
|
||||
setRequestProperty("region", if(!Config.net_use_foreign.value) "1" else "0")
|
||||
// device
|
||||
// host
|
||||
Config.net_umstring.value.let { if (it.isNotEmpty()) setRequestProperty("umstring", it) }
|
||||
setRequestProperty("connection", "close")
|
||||
}
|
||||
Log.d("MyDT", "getConnection: $url\n${connection.requestProperties.map { "${it.key}: ${it.value}" }.joinToString("\n")}")
|
||||
return connection
|
||||
}
|
||||
|
||||
fun getComandyApiConnection(url: String, method: String = "GET", refer: String? = null, ua: String? = null) =
|
||||
private fun getComandyApiConnection(url: String, method: String = "GET") =
|
||||
run {
|
||||
val capsule = ComandyCapsule()
|
||||
capsule.url = url
|
||||
capsule.method = method
|
||||
capsule.headers = hashMapOf()
|
||||
/*capsule.headers["host"] = if (url.startsWith("https://copymanga.azurewebsites.net")) {
|
||||
Uri.parse(url).getQueryParameter("url")?.substringAfter("://")?.substringBefore("/")?:""
|
||||
} else {
|
||||
url.substringAfter("://").substringBefore("/")
|
||||
}*/
|
||||
ua?.let { capsule.headers["user-agent"] = it }
|
||||
refer?.let { capsule.headers["referer"] = it }
|
||||
capsule.headers["user-agent"] = Config.pc_ua
|
||||
capsule.headers["source"] = "copyApp"
|
||||
// deviceinfo
|
||||
capsule.headers["webp"] = "1"
|
||||
MainActivity.mainWeakReference?.get()?.let {
|
||||
capsule.headers["region"] = if(!Config.net_use_foreign.value) "1" else "0"
|
||||
capsule.headers["version"] = Config.app_ver.value
|
||||
Config.token.value?.let { tk ->
|
||||
capsule.headers["authorization"] = "Token $tk"
|
||||
}
|
||||
}
|
||||
capsule.headers["platform"] = Config.platform.value
|
||||
capsule.headers["dt"] = SimpleDateFormat("yyyy.MM.dd", Locale.getDefault()).format(Calendar.getInstance().time)
|
||||
capsule.headers["accept-encoding"] = "gzip"
|
||||
capsule.headers["authorization"] = "Token${Config.token.value?.let { tk ->
|
||||
if (tk.isNotEmpty()) " $tk" else ""
|
||||
}}"
|
||||
capsule.headers["platform"] = Config.platform.value
|
||||
capsule.headers["referer"] = Config.referer
|
||||
capsule.headers["accept"] = "application/json"
|
||||
capsule.headers["version"] = Config.app_ver.value
|
||||
capsule.headers["region"] = if(!Config.net_use_foreign.value) "1" else "0"
|
||||
// device
|
||||
// host
|
||||
Config.net_umstring.value.let { if (it.isNotEmpty()) capsule.headers["umstring"] = it }
|
||||
capsule.headers["connection"] = "close"
|
||||
|
||||
Log.d("MyDT", "getComandyConnection: $url\n${capsule.headers.map { "${it.key}: ${it.value}" }.joinToString("\n")}")
|
||||
capsule
|
||||
}
|
||||
@@ -123,16 +125,31 @@ object DownloadTools {
|
||||
return bytesCopied
|
||||
}
|
||||
|
||||
private fun decodeBody(ret: ByteArray, coding: String) : ByteArray {
|
||||
return if (coding == "gzip") ByteArrayInputStream(ret).use { byteIn ->
|
||||
GZIPInputStream(byteIn).use useGzip@ { gzipIn ->
|
||||
ByteArrayOutputStream().use { byteOut ->
|
||||
val buffer = ByteArray(4096)
|
||||
var len: Int
|
||||
while (gzipIn.read(buffer).also { len = it } != -1) {
|
||||
byteOut.write(buffer, 0, len)
|
||||
}
|
||||
return@useGzip byteOut.toByteArray()
|
||||
}
|
||||
}
|
||||
} else ret
|
||||
}
|
||||
|
||||
private fun InputStream.readBytesWithProgress(sz: Int, p: Client.Progress): ByteArray {
|
||||
val buffer = ByteArrayOutputStream(maxOf(DEFAULT_BUFFER_SIZE, this.available()))
|
||||
copyToWithProgress(buffer, sz, p)
|
||||
return buffer.toByteArray()
|
||||
}
|
||||
|
||||
suspend fun getHttpContent(u: String, refer: String? = null, ua: String? = Config.pc_ua, p: Client.Progress? = null): ByteArray =
|
||||
suspend fun getApiContent(u: String, p: Client.Progress? = null): ByteArray =
|
||||
withContext(Dispatchers.IO) {
|
||||
if (!u.startsWith("https://$proxyUrl") && Comandy.instance.enabled) {
|
||||
getComandyApiConnection(u, "GET", refer, ua).let { capsule ->
|
||||
getComandyApiConnection(u, "GET").let { capsule ->
|
||||
val para = Gson().toJson(capsule)
|
||||
//Log.d("MyDT", "comandy request: $para")
|
||||
Comandy.instance.getInstance()?.let { ins ->
|
||||
@@ -154,6 +171,7 @@ object DownloadTools {
|
||||
Log.d("MyDT", "quit comandy get progress, completed: $completed for url $u")
|
||||
}.start()
|
||||
}
|
||||
var coding: String? = null
|
||||
val r = ins.request(para)?.let { result ->
|
||||
completed = true
|
||||
p?.notify(100)
|
||||
@@ -162,26 +180,29 @@ object DownloadTools {
|
||||
if (it.code != 200) throw IllegalArgumentException("HTTP${it.code} ${
|
||||
it.data?.let { d -> Base64.decode(d, Base64.DEFAULT).decodeToString() }
|
||||
}")
|
||||
coding = it.headers["Content-Encoding"] as String?
|
||||
Base64.decode(it.data, Base64.DEFAULT)
|
||||
}
|
||||
}
|
||||
completed = true
|
||||
p?.notify(100)
|
||||
r
|
||||
r?.let { ret -> coding?.let { decodeBody(ret, it) } }
|
||||
}
|
||||
}.let { if(it?.isNotEmpty() == true ) return@withContext it }
|
||||
failTimes.incrementAndGet()
|
||||
}
|
||||
getApiConnection(u, "GET", refer, ua).let {
|
||||
val sz = it.getHeaderFieldInt("Content-Length", 0)
|
||||
getApiConnection(u, "GET").let { conn ->
|
||||
val sz = conn.getHeaderFieldInt("Content-Length", 0)
|
||||
val ret = if (sz > 0 && p != null) {
|
||||
it.inputStream.readBytesWithProgress(sz, p)
|
||||
conn.inputStream.readBytesWithProgress(sz, p)
|
||||
} else {
|
||||
it.inputStream.readBytes()
|
||||
conn.inputStream.readBytes()
|
||||
}
|
||||
it.disconnect()
|
||||
conn.disconnect()
|
||||
Log.d("MyDT", "getHttpContent: ${ret.size} bytes")
|
||||
ret
|
||||
if (conn.getHeaderField("Content-type") != "application/json") {
|
||||
throw IllegalStateException("请求错误: ${ret.decodeToString()}")
|
||||
}
|
||||
decodeBody(ret, conn.getHeaderField("Content-Encoding")?:"")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -263,50 +284,33 @@ object DownloadTools {
|
||||
})
|
||||
}
|
||||
|
||||
/*private fun replaceChineseCharacters(string: String?) : String? {
|
||||
if (android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.M) return string
|
||||
else return string?.replace(Regex("(?<=/)[\\w\\s\\d\\u4e00-\\u9fa5.-]+(?=/?)")) { match ->
|
||||
return@replace URLEncoder.encode(match.value, "UTF-8")
|
||||
}
|
||||
}*/
|
||||
|
||||
fun requestWithBody(url: String, method: String, body: ByteArray, refer: String? = Config.referer, ua: String? = Config.pc_ua, contentType: String? = "application/x-www-form-urlencoded;charset=utf-8"): ByteArray? {
|
||||
fun requestApiWithBody(url: String, method: String, body: ByteArray, contentType: String): ByteArray {
|
||||
Log.d("MyDT", "$method Http: $url")
|
||||
var ret: ByteArray? = null
|
||||
val task = FutureTask(if(!url.startsWith("https://$proxyUrl") && Comandy.instance.enabled) Callable{
|
||||
try {
|
||||
val capsule = getComandyApiConnection(url, method, refer, ua)
|
||||
contentType?.let { capsule.headers["content-type"] = it }
|
||||
capsule.data = body.decodeToString()
|
||||
runBlocking { Comandy.instance.getInstance() }?.request(Gson().toJson(capsule))?.let { result ->
|
||||
Gson().fromJson(result, ComandyCapsule::class.java)?.let {
|
||||
it.data?.let { d -> Base64.decode(d, Base64.DEFAULT) }?:"empty comandy data".encodeToByteArray()
|
||||
}
|
||||
return if(!url.startsWith("https://$proxyUrl") && Comandy.instance.enabled) {
|
||||
val capsule = getComandyApiConnection(url, method)
|
||||
capsule.headers["content-type"] = contentType
|
||||
capsule.data = body.decodeToString()
|
||||
runBlocking { Comandy.instance.getInstance() }?.request(Gson().toJson(capsule))?.let { result ->
|
||||
Gson().fromJson(result, ComandyCapsule::class.java)?.let { c ->
|
||||
c.data?.let { d ->
|
||||
Base64.decode(d, Base64.DEFAULT).let {
|
||||
(c.headers["Content-Encoding"] as String?)?.let { coding ->
|
||||
decodeBody(it, coding)
|
||||
}?:it
|
||||
}
|
||||
}?: throw IllegalStateException("empty comandy data")
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
ex.printStackTrace()
|
||||
failTimes.incrementAndGet()
|
||||
ex.message?.encodeToByteArray()
|
||||
}?: throw IllegalStateException("no comandy")
|
||||
} else {
|
||||
var ret: ByteArray
|
||||
var coding = ""
|
||||
getApiConnection(url, method).apply {
|
||||
outputStream.write(body)
|
||||
ret = inputStream.readBytes()
|
||||
disconnect()
|
||||
coding = getHeaderField("Content-Encoding")?:""
|
||||
}
|
||||
}
|
||||
else Callable {
|
||||
try {
|
||||
getApiConnection(url, method, refer, ua).apply {
|
||||
outputStream.write(body)
|
||||
ret = inputStream.readBytes()
|
||||
disconnect()
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
ex.printStackTrace()
|
||||
}
|
||||
return@Callable ret
|
||||
})
|
||||
Thread(task).start()
|
||||
return try {
|
||||
task.get()
|
||||
} catch (ex: Exception) {
|
||||
ex.printStackTrace()
|
||||
null
|
||||
decodeBody(ret, coding)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,9 +12,8 @@ import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import top.fumiama.copymanga.MainActivity.Companion.mainWeakReference
|
||||
import top.fumiama.copymanga.json.ReturnBase
|
||||
import top.fumiama.copymanga.api.Config
|
||||
import top.fumiama.copymanga.net.DownloadTools
|
||||
import top.fumiama.copymanga.json.ReturnBase
|
||||
import java.io.File
|
||||
import java.security.MessageDigest
|
||||
|
||||
@@ -77,16 +76,12 @@ open class AutoDownloadHandler(
|
||||
var cnt = 0
|
||||
while (cnt++ <= 3) {
|
||||
try {
|
||||
val data = Config.apiProxy?.comancry(url()) {
|
||||
DownloadTools.getHttpContent(it)
|
||||
}?:DownloadTools.getHttpContent(url())
|
||||
val data = Config.myHostApiUrl.get(url())
|
||||
if(exit) return@withContext
|
||||
val fi = data.inputStream()
|
||||
val pass = setGsonItem(Gson().fromJson(fi.reader(), jsonClass))
|
||||
val pass = setGsonItem(Gson().fromJson(data, jsonClass))
|
||||
if (pass && loadFromCache) {
|
||||
cacheFile?.writeBytes(data)
|
||||
cacheFile?.writeText(data)
|
||||
}
|
||||
fi.close()
|
||||
if(!pass) {
|
||||
delay(2000)
|
||||
continue
|
||||
|
||||
@@ -5,7 +5,6 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.withContext
|
||||
import top.fumiama.copymanga.api.Config
|
||||
import top.fumiama.copymanga.net.DownloadTools
|
||||
import kotlin.random.Random
|
||||
|
||||
class PausableDownloader(private val url: String, private val waitMilliseconds: Long = 0, private val isApi: Boolean = true, private val whenFinish: (suspend (result: ByteArray)->Unit)? = null) {
|
||||
@@ -14,10 +13,8 @@ class PausableDownloader(private val url: String, private val waitMilliseconds:
|
||||
var c = 0
|
||||
while (!exit && c++ < 3) {
|
||||
try {
|
||||
val data = (if (isApi) Config.apiProxy?.comancry(url) {
|
||||
DownloadTools.getHttpContent(it, Config.referer)
|
||||
} else null)?:DownloadTools.getHttpContent(url, Config.referer)
|
||||
whenFinish?.let { it(data) }
|
||||
val data = Config.myHostApiUrl.get(url)
|
||||
whenFinish?.let { it(data.encodeToByteArray()) }
|
||||
return@withContext true
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
|
||||
@@ -8,6 +8,8 @@ data class PreferenceString(private val key: String, private var default: String
|
||||
constructor(key: Int, default: String?, defaultID: Int): this(
|
||||
MainActivity.mainWeakReference?.get()?.getString(key) ?:"", default, defaultID)
|
||||
constructor(key: String, default: Int): this(key, null, default)
|
||||
constructor(key: String, default: String): this(key, default, 0)
|
||||
constructor(key: String): this(key, "")
|
||||
constructor(key: Int): this(key, "", 0)
|
||||
|
||||
private val defaultField: String
|
||||
|
||||
@@ -153,6 +153,7 @@ class BookFragment: NoBackRefreshFragment(R.layout.fragment_book) {
|
||||
if (getBoolean("loadJson")) {
|
||||
getString("name")?.let { name ->
|
||||
try {
|
||||
Log.d("MyBF", "loadFromCache name $name")
|
||||
book = Book(name, {
|
||||
return@Book getString(it)
|
||||
}, activity?.getExternalFilesDir("")!!)
|
||||
@@ -184,11 +185,17 @@ class BookFragment: NoBackRefreshFragment(R.layout.fragment_book) {
|
||||
}
|
||||
|
||||
private suspend fun queryCollect() {
|
||||
MainActivity.shelf?.query(book?.path!!)?.let { b ->
|
||||
mBookHandler?.collect = b.results?.collect?:-2
|
||||
Log.d("MyBF", "get collect of ${book?.path} = ${mBookHandler?.collect}")
|
||||
tic.text = b.results?.browse?.chapter_name?.let { name ->
|
||||
getString(R.string.text_format_cloud_read_to).format(Chinese.fixEncodingIfNeeded(name))
|
||||
try {
|
||||
MainActivity.shelf?.query(book?.path!!)?.let { b ->
|
||||
mBookHandler?.collect = b.results?.collect?:-2
|
||||
Log.d("MyBF", "get collect of ${book?.path} = ${mBookHandler?.collect}")
|
||||
tic.text = b.results?.browse?.chapter_name?.let { name ->
|
||||
getString(R.string.text_format_cloud_read_to).format(Chinese.fixEncodingIfNeeded(name))
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
activity?.runOnUiThread {
|
||||
Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -208,7 +215,7 @@ class BookFragment: NoBackRefreshFragment(R.layout.fragment_book) {
|
||||
}
|
||||
}
|
||||
book?.uuid?.let { uuid ->
|
||||
this@BookFragment.lbbsub.setOnClickListener {
|
||||
this@BookFragment.lbbsub?.setOnClickListener {
|
||||
lifecycleScope.launch clickLaunch@ {
|
||||
if (this@BookFragment.lbbsub.text != getString(R.string.button_sub)) {
|
||||
mBookHandler?.collect?.let { collect ->
|
||||
|
||||
@@ -11,7 +11,7 @@ import top.fumiama.dmzj.copymanga.R
|
||||
@OptIn(ExperimentalStdlibApi::class)
|
||||
class HistoryFragment : InfoCardLoader(R.layout.fragment_history, R.id.action_nav_history_to_nav_book, isHistoryBook = true) {
|
||||
override fun getApiUrl() =
|
||||
getString(R.string.historyApiUrl).format(Config.myHostApiUrl.random(), page * 21, Config.platform.value)
|
||||
getString(R.string.historyApiUrl).format(page * 21, Config.platform.value)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
if (MainActivity.member?.hasLogin != true) {
|
||||
|
||||
@@ -7,5 +7,5 @@ import top.fumiama.dmzj.copymanga.R
|
||||
@ExperimentalStdlibApi
|
||||
class NewestFragment : InfoCardLoader(R.layout.fragment_newest, R.id.action_nav_newest_to_nav_book, true) {
|
||||
override fun getApiUrl() =
|
||||
getString(R.string.newestApiUrl).format(Config.myHostApiUrl.random(), page * 21, Config.platform.value)
|
||||
getString(R.string.newestApiUrl).format(page * 21, Config.platform.value)
|
||||
}
|
||||
|
||||
@@ -48,7 +48,6 @@ class RankFragment : InfoCardLoader(R.layout.fragment_rank, R.id.action_nav_rank
|
||||
|
||||
override fun getApiUrl() =
|
||||
getString(R.string.rankApiUrl).format(
|
||||
Config.myHostApiUrl.random(),
|
||||
page * 21,
|
||||
sortWay[sortValue],
|
||||
audienceWay[audience],
|
||||
|
||||
@@ -7,5 +7,5 @@ import top.fumiama.dmzj.copymanga.R
|
||||
@ExperimentalStdlibApi
|
||||
class RecFragment : InfoCardLoader(R.layout.fragment_recommend, R.id.action_nav_recommend_to_nav_book, true) {
|
||||
override fun getApiUrl() =
|
||||
getString(R.string.recommendApiUrl).format(Config.myHostApiUrl.random(), page * 21, Config.platform.value)
|
||||
getString(R.string.recommendApiUrl).format(page * 21, Config.platform.value)
|
||||
}
|
||||
@@ -13,7 +13,7 @@ class SearchFragment : InfoCardLoader(R.layout.fragment_search, R.id.action_nav_
|
||||
private var query: String? = null
|
||||
private var type: String? = null
|
||||
override fun getApiUrl() =
|
||||
getString(R.string.searchApiUrl).format(Config.myHostApiUrl.random(), page * 21, query, type, Config.platform.value)
|
||||
getString(R.string.searchApiUrl).format(page * 21, query, type, Config.platform.value)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
@@ -26,7 +26,6 @@ class ShelfFragment : InfoCardLoader(R.layout.fragment_shelf, R.id.action_nav_su
|
||||
|
||||
override fun getApiUrl() =
|
||||
getString(R.string.shelfApiUrl).format(
|
||||
Config.myHostApiUrl.random(),
|
||||
page * 21,
|
||||
sortWay[sortValue],
|
||||
Config.platform.value,
|
||||
|
||||
@@ -25,7 +25,6 @@ class SortFragment : StatusCardFlow(0, R.id.action_nav_sort_to_nav_book, R.layou
|
||||
|
||||
override fun getApiUrl() =
|
||||
getString(R.string.sortApiUrl).format(
|
||||
Config.myHostApiUrl.random(),
|
||||
page * 21,
|
||||
sortWay[sortValue],
|
||||
if(theme >= 0 && theme < (filter?.results?.theme?.size ?: 0)) (filter?.results?.theme?.get(theme)?.path_word ?: "") else "",
|
||||
@@ -43,7 +42,7 @@ class SortFragment : StatusCardFlow(0, R.id.action_nav_sort_to_nav_book, R.layou
|
||||
super.setListeners()
|
||||
lifecycleScope.launch {
|
||||
setProgress(5)
|
||||
PausableDownloader(getString(R.string.filterApiUrl).format(Config.myHostApiUrl.random(), Config.platform.value)) {
|
||||
PausableDownloader(getString(R.string.filterApiUrl).format(Config.platform.value)) {
|
||||
if(ad?.exit == true) return@PausableDownloader
|
||||
it.let {
|
||||
it.inputStream().use { i ->
|
||||
|
||||
@@ -18,13 +18,13 @@ import top.fumiama.dmzj.copymanga.R
|
||||
class TopicFragment : InfoCardLoader(R.layout.fragment_topic, R.id.action_nav_topic_to_nav_book) {
|
||||
private var type = 1
|
||||
override fun getApiUrl() =
|
||||
getString(R.string.topicContentApiUrl).format(Config.myHostApiUrl.random(), arguments?.getString("path"), type, offset, Config.platform.value)
|
||||
getString(R.string.topicContentApiUrl).format(arguments?.getString("path"), type, offset, Config.platform.value)
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
lifecycleScope.launch {
|
||||
setProgress(5)
|
||||
PausableDownloader(getString(R.string.topicApiUrl).format(Config.myHostApiUrl.random(), arguments?.getString("path"), Config.platform.value)) { data ->
|
||||
PausableDownloader(getString(R.string.topicApiUrl).format(arguments?.getString("path"), Config.platform.value)) { data ->
|
||||
setProgress(10)
|
||||
withContext(Dispatchers.IO) {
|
||||
if(ad?.exit == true) return@withContext
|
||||
|
||||
@@ -118,7 +118,6 @@ class DownloadFragment: NoBackRefreshFragment(R.layout.fragment_download) {
|
||||
bundle.putBoolean("callFromOldDL", true)
|
||||
}
|
||||
bundle.putString("name", jsonFile.parentFile?.name?:"Null")
|
||||
Log.d("MyDF", "root view: $rootView")
|
||||
Log.d("MyDF", "action_nav_download_to_nav_group")
|
||||
Navigate.safeNavigateTo(findNavController(), R.id.action_nav_download_to_nav_group, bundle)
|
||||
}
|
||||
@@ -128,7 +127,6 @@ class DownloadFragment: NoBackRefreshFragment(R.layout.fragment_download) {
|
||||
bundle.putString("title", title)
|
||||
bundle.putString("file", file.absolutePath)
|
||||
Log.d("MyDF", "Call self to $title")
|
||||
Log.d("MyDF", "root view: $rootView")
|
||||
Log.d("MyDF", "action_nav_download_self")
|
||||
Navigate.safeNavigateTo(findNavController(), R.id.action_nav_download_self, bundle)
|
||||
}
|
||||
|
||||
@@ -244,7 +244,6 @@ class NewDownloadFragment: MangaPagesFragmentTemplate(R.layout.fragment_newdownl
|
||||
Log.d("MyNDF", "Call dl and is new.")
|
||||
bundle.putString("loadJson", File(File(extDir, name), "info.json").readText())
|
||||
bundle.putString("name", name)
|
||||
Log.d("MyNDF", "root view: $rootView")
|
||||
Log.d("MyNDF", "action_nav_new_download_to_nav_group")
|
||||
Navigate.safeNavigateTo(findNavController(), R.id.action_nav_new_download_to_nav_group, bundle)
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ class HomeFragment : NoBackRefreshFragment(R.layout.fragment_home) {
|
||||
val netInfo = tb.netInfo
|
||||
if(netInfo != tb.transportStringNull && netInfo != tb.transportStringError)
|
||||
MainActivity.member?.apply { lifecycleScope.launch {
|
||||
Config.myHostApiUrl.init()
|
||||
info().let { l ->
|
||||
if (l.code != 200 && l.code != 449) {
|
||||
Toast.makeText(context, l.message, Toast.LENGTH_SHORT).show()
|
||||
@@ -298,7 +299,7 @@ class HomeFragment : NoBackRefreshFragment(R.layout.fragment_home) {
|
||||
suspend fun refresh(q: CharSequence) = withContext(Dispatchers.IO) {
|
||||
query = q.toString()
|
||||
activity?.apply {
|
||||
PausableDownloader(getString(R.string.searchApiUrl).format(Config.myHostApiUrl.random(), 0,
|
||||
PausableDownloader(getString(R.string.searchApiUrl).format(0,
|
||||
URLEncoder.encode(q.toString(), Charset.defaultCharset().name()), type, Config.platform.value)) {
|
||||
results = Gson().fromJson(it.decodeToString(), BookListStructure::class.java)
|
||||
count = results?.results?.total?:0
|
||||
|
||||
@@ -40,7 +40,7 @@ import java.lang.ref.WeakReference
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
class HomeHandler(private val that: WeakReference<HomeFragment>) : AutoDownloadHandler({
|
||||
that.get()?.getString(R.string.mainPageApiUrl)!!.format(Config.myHostApiUrl.random(), Config.platform.value)
|
||||
that.get()?.getString(R.string.mainPageApiUrl)!!.format(Config.platform.value)
|
||||
},
|
||||
IndexStructure::class.java,
|
||||
that.get()
|
||||
|
||||
@@ -25,6 +25,7 @@ open class NoBackRefreshFragment(private val layoutToLoad: Int): Fragment() {
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
//TODO: 支持自动重建
|
||||
if(_rootView == null) {
|
||||
isFirstInflate = true
|
||||
_rootView = inflater.inflate(layoutToLoad, container, false)
|
||||
|
||||
@@ -18,7 +18,6 @@ open class StatusCardFlow(private val api: Int, nav: Int, inflateRes: Int,
|
||||
|
||||
override fun getApiUrl() =
|
||||
getString(api).format(
|
||||
Config.myHostApiUrl.random(),
|
||||
page * 21,
|
||||
sortWay[sortValue],
|
||||
Config.platform.value,
|
||||
|
||||
@@ -12,7 +12,6 @@ open class ThemeCardFlow(private val api: Int, nav: Int) : StatusCardFlow(0, nav
|
||||
private var theme = ""
|
||||
override fun getApiUrl() =
|
||||
getString(api).format(
|
||||
Config.myHostApiUrl.random(),
|
||||
page * 21,
|
||||
sortWay[sortValue],
|
||||
theme,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<!ENTITY hosturl "api.copy-manga.com">
|
||||
<!ENTITY appver "2.3.0">
|
||||
<!ENTITY proxyurl "copymanga.azurewebsites.net">
|
||||
<!ENTITY platform "1">
|
||||
<!ENTITY platform "3">
|
||||
]>
|
||||
<resources>
|
||||
<string name="app_name">拷贝漫画</string>
|
||||
@@ -64,31 +64,30 @@
|
||||
<string name="touch_img_error">预载图片头失败</string>
|
||||
<string name="analyze_img_size_error">读取图片大小失败</string>
|
||||
|
||||
<string name="networkApiUrl">https://%1$s/api/v3/system/network2?platform=%2$s</string>
|
||||
<string name="mainPageApiUrl">https://%1$s/api/v3/h5/homeIndex?platform=%2$s</string>
|
||||
<string name="referUrl">https://%1$s</string>
|
||||
<string name="networkApiUrl">/api/v3/system/network2?platform=%1$s</string>
|
||||
<string name="mainPageApiUrl">/api/v3/h5/homeIndex?platform=%1$s</string>
|
||||
<string name="hostUrl">&hosturl;</string>
|
||||
<string name="proxyUrl">&proxyurl;</string>
|
||||
<string name="rankApiUrl">https://%1$s/api/v3/ranks?limit=21&offset=%2$d&date_type=%3$s&audience_type=%4$s&platform=%5$s</string>
|
||||
<string name="searchApiUrl">https://%1$s/api/v3/search/comic?limit=21&offset=%2$d&q=%3$s&q_type=%4$s&platform=%5$s</string>
|
||||
<string name="filterApiUrl">https://%1$s/api/v3/h5/filter/comic/tags?platform=%2$s</string>
|
||||
<string name="sortApiUrl">https://%1$s/api/v3/comics?limit=21&offset=%2$d&ordering=%3$s&theme=%4$s&top=%5$s&platform=%6$s</string>
|
||||
<string name="bookInfoApiUrl">https://%1$s/api/v3/comic2/%2$s?platform=%3$s</string>
|
||||
<string name="bookUserQueryApiUrl">https://%1$s/api/v3/comic2/%2$s/query?platform=%3$s</string>
|
||||
<string name="groupInfoApiUrl">https://%1$s/api/v3/comic/%2$s/group/%3$s/chapters?limit=100&offset=%4$d&platform=%5$s</string>
|
||||
<string name="chapterInfoApiUrl">https://%1$s/api/v3/comic/%2$s/chapter%3$s/%4$s?platform=%5$s</string>
|
||||
<string name="topicApiUrl">https://%1$s/api/v3/topic/%2$s?platform=%3$s</string>
|
||||
<string name="topicContentApiUrl">https://%1$s/api/v3/topic/%2$s/contents?type=%3$d&limit=21&offset=%4$d&platform=%5$s</string>
|
||||
<string name="recommendApiUrl">https://%1$s/api/v3/recs?pos=3200102&limit=21&offset=%2$d&platform=%3$s</string>
|
||||
<string name="newestApiUrl">https://%1$s/api/v3/update/newest?limit=21&offset=%2$d&platform=%3$s</string>
|
||||
<string name="finishApiUrl">https://%1$s/api/v3/comics?limit=21&offset=%2$d&ordering=%3$s&top=finish&platform=%4$s</string>
|
||||
<string name="authorApiUrl">https://%1$s/api/v3/comics?limit=21&offset=%2$d&ordering=%3$s&author=%4$s&platform=%5$s</string>
|
||||
<string name="captionApiUrl">https://%1$s/api/v3/comics?limit=21&offset=%2$d&ordering=%3$s&theme=%4$s&platform=%5$s</string>
|
||||
<string name="loginApiUrl">https://%1$s/api/v3/login?platform=%2$s</string>
|
||||
<string name="memberInfoApiUrl">https://%1$s/api/v3/member/info?platform=%2$s</string>
|
||||
<string name="historyApiUrl">https://%1$s/api/v3/member/browse/comics?limit=21&offset=%2$d&platform=%3$s</string>
|
||||
<string name="shelfApiUrl">https://%1$s/api/v3/member/collect/comics?limit=21&offset=%2$d&free_type=1&ordering=%3$s&platform=%4$s</string>
|
||||
<string name="shelfOperateApiUrl">https://%1$s/api/v3/member/collect/comic</string>
|
||||
<string name="rankApiUrl">/api/v3/ranks?limit=21&offset=%1$d&date_type=%2$s&audience_type=%3$s&platform=%4$s</string>
|
||||
<string name="searchApiUrl">/api/v3/search/comic?limit=21&offset=%1$d&q=%2$s&q_type=%3$s&platform=%4$s</string>
|
||||
<string name="filterApiUrl">/api/v3/h5/filter/comic/tags?platform=%1$s</string>
|
||||
<string name="sortApiUrl">/api/v3/comics?limit=21&offset=%1$d&ordering=%2$s&theme=%3$s&top=%4$s&platform=%5$s</string>
|
||||
<string name="bookInfoApiUrl">/api/v3/comic2/%1$s?platform=%2$s</string>
|
||||
<string name="bookUserQueryApiUrl">/api/v3/comic2/%1$s/query?platform=%2$s</string>
|
||||
<string name="groupInfoApiUrl">/api/v3/comic/%1$s/group/%2$s/chapters?limit=100&offset=%3$d&platform=%4$s</string>
|
||||
<string name="chapterInfoApiUrl">/api/v3/comic/%1$s/chapter%2$s/%3$s?platform=%4$s</string>
|
||||
<string name="topicApiUrl">/api/v3/topic/%1$s?platform=%2$s</string>
|
||||
<string name="topicContentApiUrl">/api/v3/topic/%1$s/contents?type=%2$d&limit=21&offset=%3$d&platform=%4$s</string>
|
||||
<string name="recommendApiUrl">/api/v3/recs?pos=3200102&limit=21&offset=%1$d&platform=%2$s</string>
|
||||
<string name="newestApiUrl">/api/v3/update/newest?limit=21&offset=%1$d&platform=%2$s</string>
|
||||
<string name="finishApiUrl">/api/v3/comics?limit=21&offset=%1$d&ordering=%2$s&top=finish&platform=%3$s</string>
|
||||
<string name="authorApiUrl">/api/v3/comics?limit=21&offset=%1$d&ordering=%2$s&author=%3$s&platform=%4$s</string>
|
||||
<string name="captionApiUrl">/api/v3/comics?limit=21&offset=%1$d&ordering=%2$s&theme=%3$s&platform=%4$s</string>
|
||||
<string name="loginApiUrl">/api/v3/login?platform=%1$s</string>
|
||||
<string name="memberInfoApiUrl">/api/v3/member/info?platform=%1$s</string>
|
||||
<string name="historyApiUrl">/api/v3/member/browse/comics?limit=21&offset=%1$d&platform=%2$s</string>
|
||||
<string name="shelfApiUrl">/api/v3/member/collect/comics?limit=21&offset=%1$d&free_type=1&ordering=%2$s&platform=%3$s</string>
|
||||
<string name="shelfOperateApiUrl">/api/v3/member/collect/comic</string>
|
||||
|
||||
<string name="imgProxyApiUrl">https://&proxyurl;/api/img?code=%1$s&url=%2$s</string>
|
||||
<string name="imgProxyCodeKeyID">settings_cat_net_et_img_proxy_code</string>
|
||||
@@ -178,6 +177,8 @@
|
||||
<string name="settings_cat_net_et_summary_api_url">一般无需更改,除非拷贝漫画官方更改网址,默认:&hosturl;</string>
|
||||
<string name="settings_cat_net_et_title_reverse_proxy">反向代理</string>
|
||||
<string name="settings_cat_net_et_summary_reverse_proxy">您可以自建反向代理并填写在此处以解决网络不畅</string>
|
||||
<string name="settings_cat_net_et_title_umstring">友盟ID</string>
|
||||
<string name="settings_cat_net_et_summary_umstring">填写您分配到的友盟ID</string>
|
||||
<string name="settings_cat_net_sw_use_api_proxy">使用API代理(需要密钥)</string>
|
||||
<string name="settings_cat_net_sm_use_api_proxy">作者自建的API代理,可缓解国内图书详情加载问题,但不保证100%解决,也不保证一直可用</string>
|
||||
<string name="settings_cat_net_sw_use_img_proxy">使用图床代理(需要密钥)</string>
|
||||
@@ -207,7 +208,6 @@
|
||||
|
||||
<string name="login_null_username">用户名为空</string>
|
||||
<string name="login_null_pwd">密码为空</string>
|
||||
<string name="login_get_conn_failed">登录失败</string>
|
||||
<string name="login_parse_json_error">解析返回数据失败</string>
|
||||
<string name="login_get_avatar_failed">获取用户信息失败</string>
|
||||
<string name="login_restart_to_apply">重启应用以彻底退出登录</string>
|
||||
|
||||
@@ -120,6 +120,16 @@
|
||||
app:enableCopying="true"
|
||||
app:iconSpaceReserved="false"
|
||||
app:key="@string/reverseProxyKeyID" />
|
||||
<EditTextPreference
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:selectAllOnFocus="false"
|
||||
android:singleLine="true"
|
||||
android:title="@string/settings_cat_net_et_title_umstring"
|
||||
android:summary="@string/settings_cat_net_et_summary_umstring"
|
||||
app:enableCopying="true"
|
||||
app:iconSpaceReserved="false"
|
||||
app:key="settings_cat_net_et_umstring" />
|
||||
<SwitchPreferenceCompat
|
||||
app:iconSpaceReserved="false"
|
||||
app:key="@string/apiProxyKeyID"
|
||||
|
||||
Reference in New Issue
Block a user