1
0
mirror of https://github.com/fumiama/copymanga.git synced 2026-06-11 02:50:28 +08:00
优化
1. API访问流程
2. 更多访问报错信息
升级
1. appcompat -> 1.7.1
This commit is contained in:
源文雨
2025-06-05 18:34:42 +09:00
parent d04e031328
commit 257e5fbb87
32 changed files with 337 additions and 347 deletions

View File

@@ -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'

View File

@@ -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) { _, _ -> }

View File

@@ -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)
}

View File

@@ -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()
}
}

View File

@@ -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)
}
}
}

View File

@@ -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 {

View 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}")
}
}

View File

@@ -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&region=$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&region=$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&region=$r&webp=1".encodeToByteArray(),
"POST", "application/x-www-form-urlencoded;charset=utf-8")
}.encodeToByteArray()
}

View File

@@ -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 "生效(自动)"

View File

@@ -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)
}
}
}

View File

@@ -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

View File

@@ -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()

View File

@@ -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

View File

@@ -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 ->

View File

@@ -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) {

View File

@@ -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)
}

View File

@@ -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],

View File

@@ -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)
}

View File

@@ -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)

View File

@@ -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,

View File

@@ -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 ->

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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()

View File

@@ -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)

View File

@@ -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,

View File

@@ -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,

View File

@@ -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&amp;offset=%2$d&amp;date_type=%3$s&amp;audience_type=%4$s&amp;platform=%5$s</string>
<string name="searchApiUrl">https://%1$s/api/v3/search/comic?limit=21&amp;offset=%2$d&amp;q=%3$s&amp;q_type=%4$s&amp;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&amp;offset=%2$d&amp;ordering=%3$s&amp;theme=%4$s&amp;top=%5$s&amp;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&amp;offset=%4$d&amp;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&amp;limit=21&amp;offset=%4$d&amp;platform=%5$s</string>
<string name="recommendApiUrl">https://%1$s/api/v3/recs?pos=3200102&amp;limit=21&amp;offset=%2$d&amp;platform=%3$s</string>
<string name="newestApiUrl">https://%1$s/api/v3/update/newest?limit=21&amp;offset=%2$d&amp;platform=%3$s</string>
<string name="finishApiUrl">https://%1$s/api/v3/comics?limit=21&amp;offset=%2$d&amp;ordering=%3$s&amp;top=finish&amp;platform=%4$s</string>
<string name="authorApiUrl">https://%1$s/api/v3/comics?limit=21&amp;offset=%2$d&amp;ordering=%3$s&amp;author=%4$s&amp;platform=%5$s</string>
<string name="captionApiUrl">https://%1$s/api/v3/comics?limit=21&amp;offset=%2$d&amp;ordering=%3$s&amp;theme=%4$s&amp;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&amp;offset=%2$d&amp;platform=%3$s</string>
<string name="shelfApiUrl">https://%1$s/api/v3/member/collect/comics?limit=21&amp;offset=%2$d&amp;free_type=1&amp;ordering=%3$s&amp;platform=%4$s</string>
<string name="shelfOperateApiUrl">https://%1$s/api/v3/member/collect/comic</string>
<string name="rankApiUrl">/api/v3/ranks?limit=21&amp;offset=%1$d&amp;date_type=%2$s&amp;audience_type=%3$s&amp;platform=%4$s</string>
<string name="searchApiUrl">/api/v3/search/comic?limit=21&amp;offset=%1$d&amp;q=%2$s&amp;q_type=%3$s&amp;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&amp;offset=%1$d&amp;ordering=%2$s&amp;theme=%3$s&amp;top=%4$s&amp;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&amp;offset=%3$d&amp;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&amp;limit=21&amp;offset=%3$d&amp;platform=%4$s</string>
<string name="recommendApiUrl">/api/v3/recs?pos=3200102&amp;limit=21&amp;offset=%1$d&amp;platform=%2$s</string>
<string name="newestApiUrl">/api/v3/update/newest?limit=21&amp;offset=%1$d&amp;platform=%2$s</string>
<string name="finishApiUrl">/api/v3/comics?limit=21&amp;offset=%1$d&amp;ordering=%2$s&amp;top=finish&amp;platform=%3$s</string>
<string name="authorApiUrl">/api/v3/comics?limit=21&amp;offset=%1$d&amp;ordering=%2$s&amp;author=%3$s&amp;platform=%4$s</string>
<string name="captionApiUrl">/api/v3/comics?limit=21&amp;offset=%1$d&amp;ordering=%2$s&amp;theme=%3$s&amp;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&amp;offset=%1$d&amp;platform=%2$s</string>
<string name="shelfApiUrl">/api/v3/member/collect/comics?limit=21&amp;offset=%1$d&amp;free_type=1&amp;ordering=%2$s&amp;platform=%3$s</string>
<string name="shelfOperateApiUrl">/api/v3/member/collect/comic</string>
<string name="imgProxyApiUrl">https://&proxyurl;/api/img?code=%1$s&amp;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>

View File

@@ -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"