1
0
mirror of https://github.com/fumiama/copymanga.git synced 2026-06-13 12:20:26 +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

@@ -5,6 +5,7 @@
<w>azurewebsites</w> <w>azurewebsites</w>
<w>comancry</w> <w>comancry</w>
<w>comandy</w> <w>comandy</w>
<w>deviceinfo</w>
<w>downloaders</w> <w>downloaders</w>
<w>grps</w> <w>grps</w>
<w>hotmanga</w> <w>hotmanga</w>
@@ -20,6 +21,7 @@
<w>reclass</w> <w>reclass</w>
<w>reilia</w> <w>reilia</w>
<w>systembar</w> <w>systembar</w>
<w>umstring</w>
</words> </words>
</dictionary> </dictionary>
</component> </component>

View File

@@ -10,9 +10,10 @@ android {
compileSdk 34 compileSdk 34
applicationId 'top.fumiama.copymanga' applicationId 'top.fumiama.copymanga'
minSdkVersion 23 minSdkVersion 23
//noinspection OldTargetApi
targetSdkVersion 34 targetSdkVersion 34
versionCode 72 versionCode 73
versionName '2.5.1' versionName '2.5.2'
resourceConfigurations += ['zh', 'zh-rCN'] resourceConfigurations += ['zh', 'zh-rCN']
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -47,7 +48,7 @@ android {
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release signingConfig signingConfigs.release
} }
/*winrelease { /*winrelease {r
minifyEnabled true minifyEnabled true
shrinkResources true shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
@@ -98,7 +99,7 @@ dependencies {
//noinspection GradleDependency //noinspection GradleDependency
implementation 'androidx.core:core-ktx:1.12.0' 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 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'com.google.android.material:material:1.12.0' implementation 'com.google.android.material:material:1.12.0'
implementation 'androidx.constraintlayout:constraintlayout:2.2.1' implementation 'androidx.constraintlayout:constraintlayout:2.2.1'

View File

@@ -157,6 +157,9 @@ class MainActivity : AppCompatActivity() {
isMenuWaiting = true isMenuWaiting = true
Log.d("MyMain", "start menu waiting") Log.d("MyMain", "start menu waiting")
lifecycleScope.launch { lifecycleScope.launch {
withContext(Dispatchers.IO) {
Config.myHostApiUrl.init()
}
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
delay(1000) delay(1000)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
@@ -430,7 +433,7 @@ class MainActivity : AppCompatActivity() {
dl.setMessage("${getString(R.string.app_description)}\n" + dl.setMessage("${getString(R.string.app_description)}\n" +
"\n$comandy\n" + "\n$comandy\n" +
"$comancry\n\n"+ File("/proc/self/cmdline").readText() + "\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.setTitle("${getString(R.string.action_info)} ${BuildConfig.VERSION_NAME}")
dl.setIcon(R.mipmap.ic_launcher) dl.setIcon(R.mipmap.ic_launcher)
dl.setPositiveButton(android.R.string.ok) { _, _ -> } dl.setPositiveButton(android.R.string.ok) { _, _ -> }

View File

@@ -1,14 +1,8 @@
package top.fumiama.copymanga.api package top.fumiama.copymanga.api
import android.util.Log
import com.bumptech.glide.load.model.LazyHeaders 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.MainActivity
import top.fumiama.copymanga.json.NetworkStructure import top.fumiama.copymanga.api.network.Api
import top.fumiama.copymanga.net.DownloadTools
import top.fumiama.copymanga.net.Proxy import top.fumiama.copymanga.net.Proxy
import top.fumiama.copymanga.net.Resolution import top.fumiama.copymanga.net.Resolution
import top.fumiama.copymanga.storage.PreferenceBoolean import top.fumiama.copymanga.storage.PreferenceBoolean
@@ -61,49 +55,9 @@ object Config {
} }
val proxyUrl = MainActivity.mainWeakReference?.get()?.getString(R.string.proxyUrl)!! val proxyUrl = MainActivity.mainWeakReference?.get()?.getString(R.string.proxyUrl)!!
private val reverseProxyUrl = PreferenceString(R.string.reverseProxyKeyID) val reverseProxyUrl = PreferenceString(R.string.reverseProxyKeyID)
private val networkApiUrl = PreferenceString("settings_cat_net_et_api_url", R.string.hostUrl) val networkApiUrl = PreferenceString("settings_cat_net_et_api_url", R.string.hostUrl)
private var mHostApiUrls: Array<String> = arrayOf() val myHostApiUrl = Api()
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 navTextInfo = UserPreferenceString("navTextInfo", R.string.navTextInfo) val navTextInfo = UserPreferenceString("navTextInfo", R.string.navTextInfo)
val proxy_key = PreferenceString(R.string.imgProxyCodeKeyID) val proxy_key = PreferenceString(R.string.imgProxyCodeKeyID)
val app_ver = PreferenceString("settings_cat_general_et_app_version", R.string.app_ver) 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) 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_use_api_proxy = PreferenceBoolean("settings_cat_net_sw_use_api_proxy", false)
val net_img_resolution = PreferenceString(R.string.imgResolutionKeyID) 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_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) 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) = fun getChapterInfoApiUrl(path: String?, uuid: String?, version: Int) =
MainActivity.mainWeakReference?.get()?.getString(R.string.chapterInfoApiUrl) 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 package top.fumiama.copymanga.api.manga
import android.util.Log import android.util.Log
import android.widget.Toast
import com.google.gson.Gson import com.google.gson.Gson
import kotlinx.android.synthetic.main.card_book.* import kotlinx.android.synthetic.main.card_book.*
import kotlinx.android.synthetic.main.line_booktandb.* import kotlinx.android.synthetic.main.line_booktandb.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import top.fumiama.copymanga.MainActivity
import top.fumiama.copymanga.json.BookInfoStructure import top.fumiama.copymanga.json.BookInfoStructure
import top.fumiama.copymanga.json.ThemeStructure import top.fumiama.copymanga.json.ThemeStructure
import top.fumiama.copymanga.json.VolumeStructure import top.fumiama.copymanga.json.VolumeStructure
@@ -15,8 +17,7 @@ import top.fumiama.dmzj.copymanga.R
import java.io.File 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) { 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 mBookApiUrl = getString(R.string.bookInfoApiUrl).format(path, Config.platform.value)
private val mUserAgent = getString(R.string.pc_ua).format(Config.app_ver.value)
private var mBook: BookInfoStructure? = null private var mBook: BookInfoStructure? = null
private var mGroupPathWords = arrayOf<String>() private var mGroupPathWords = arrayOf<String>()
private var mKeys = 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) { suspend fun updateInfo() = withContext(Dispatchers.IO) {
try { try {
var isDownload = false var isDownload = false
val data: ByteArray = if (loadCache) { val data: String = if (loadCache) {
name?.let { loadInfo(it) } ?: run { name?.let { loadInfo(it) } ?: run {
isDownload = true isDownload = true
Config.apiProxy?.comancry(mBookApiUrl) { url -> Config.myHostApiUrl.get(mBookApiUrl)
DownloadTools.getHttpContent(url, null, mUserAgent)
}?:DownloadTools.getHttpContent(mBookApiUrl, null, mUserAgent)
} }
} else { } else {
isDownload = true isDownload = true
Config.apiProxy?.comancry(mBookApiUrl) { url -> Config.myHostApiUrl.get(mBookApiUrl)
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)
} }
mBook = Gson().fromJson(data, BookInfoStructure::class.java)
if (isDownload) saveInfo(data) if (isDownload) saveInfo(data)
mGroupPathWords = arrayOf() mGroupPathWords = arrayOf()
mKeys = arrayOf() mKeys = arrayOf()
@@ -88,6 +83,9 @@ class Book(val path: String, private val getString: (Int) -> String, private val
} }
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() 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 }?:false
} }
private suspend fun saveInfo(data: ByteArray) = withContext(Dispatchers.IO) { private suspend fun saveInfo(data: String) = withContext(Dispatchers.IO) {
name?.let { name -> name?.let { name ->
val mangaFolder = File(exDir, name) val mangaFolder = File(exDir, name)
if(!mangaFolder.exists()) mangaFolder.mkdirs() 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) val mangaFolder = File(exDir, name)
if(!mangaFolder.exists()) mangaFolder.mkdirs() if(!mangaFolder.exists()) mangaFolder.mkdirs()
val f = File(mangaFolder, "meta.json") val f = File(mangaFolder, "meta.json")
if (!f.exists()) return@withContext null 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 com.google.gson.Gson
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import top.fumiama.copymanga.api.Config
import top.fumiama.copymanga.json.BookQueryStructure import top.fumiama.copymanga.json.BookQueryStructure
import top.fumiama.copymanga.json.ReturnBase import top.fumiama.copymanga.json.ReturnBase
import top.fumiama.copymanga.api.Config
import top.fumiama.copymanga.net.DownloadTools
import top.fumiama.dmzj.copymanga.R import top.fumiama.dmzj.copymanga.R
class Shelf(private val getString: (Int) -> String) { 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 queryApiUrlTemplate = getString(R.string.bookUserQueryApiUrl)
private val addApiUrl get() = "$apiUrl?platform=${Config.platform.value}" private val addApiUrl get() = "$apiUrl?platform=${Config.platform.value}"
private val delApiUrl get() = "${apiUrl}s?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("")
append(Config.token.value) 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 { 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 Gson().fromJson(re, ReturnBase::class.java).message
} catch (e: Exception) { } 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("authorization=Token+")
append(Config.token.value) 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 { 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 Gson().fromJson(re, ReturnBase::class.java).message
} catch (e: Exception) { } catch (e: Exception) {
"$re ${e.message}" e.message?:e::class.simpleName?:e.toString()
} }
} }
suspend fun query(pathWord: String): BookQueryStructure? = withContext(Dispatchers.IO) { suspend fun query(pathWord: String): BookQueryStructure? = withContext(Dispatchers.IO) {
try { val queryUrl = queryApiUrlTemplate.format(pathWord, Config.platform.value)
val queryUrl = queryApiUrlTemplate.format(Config.myHostApiUrl.random(), pathWord, Config.platform.value) Config.myHostApiUrl.get(queryUrl).let {
(Config.apiProxy?.comancry(queryUrl) { url -> Gson().fromJson(it, BookQueryStructure::class.java)
DownloadTools.getHttpContent(url, Config.referer)
}?:DownloadTools.getHttpContent(queryUrl, Config.referer)).let {
Gson().fromJson(it.decodeToString(), BookQueryStructure::class.java)
}
} catch (e: Exception) {
e.printStackTrace()
null
} }
} }
} }

View File

@@ -39,7 +39,7 @@ class Volume(private val path: String, private val groupPathWord: String, getStr
return@withContext mVolume 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) { private suspend fun download(re: Array<VolumeStructure?>, offset: Int, c: Int) = withContext(Dispatchers.IO) {
Log.d("MyV", "下载偏移: $offset") Log.d("MyV", "下载偏移: $offset")
getApiUrl(offset).let { 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.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import top.fumiama.copymanga.api.Config import top.fumiama.copymanga.api.Config
import top.fumiama.copymanga.json.ComandyCapsule
import top.fumiama.copymanga.json.LoginInfoStructure 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 top.fumiama.dmzj.copymanga.R
import java.net.URLEncoder import java.net.URLEncoder
import java.nio.charset.Charset 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 val hasLogin: Boolean get() = Config.token.value?.isNotEmpty() ?: false
suspend fun login(username: String, pwd: String, salt: Int): LoginInfoStructure = suspend fun login(username: String, pwd: String, salt: Int): LoginInfoStructure =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
var err = "" return@withContext try {
(if (!Config.net_use_api_proxy.value && Comandy.instance.enabled) saveInfo(postLogin(username, pwd, salt))
postComandyLogin(username, pwd, salt) } catch (e: Exception) {
else postLogin(username, pwd, salt))?.let { data -> val l = LoginInfoStructure()
try { l.code = 400
return@withContext saveInfo(data) l.message = e.message.toString()
} catch (e: Exception) { l
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 登录态 * @return 登录态
@@ -49,24 +39,21 @@ class Member(private val getString: (Int) -> String) {
} }
try { try {
val u = getString(R.string.memberInfoApiUrl) val u = getString(R.string.memberInfoApiUrl)
.format(Config.myHostApiUrl.random(), Config.platform.value) .format(Config.platform.value)
val data = (Config.apiProxy?.comancry(u) {
DownloadTools.getHttpContent(it)
}?:DownloadTools.getHttpContent(u)).decodeToString()
try { 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 if (l.code == 200) Config.avatar.value = l.results.avatar
l l
} catch (e: Exception) { } catch (e: Exception) {
val l = LoginInfoStructure() val l = LoginInfoStructure()
l.code = 450 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 l
} }
} catch (e: Exception) { } catch (e: Exception) {
val l = LoginInfoStructure() val l = LoginInfoStructure()
l.code = 450 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 l
} }
} }
@@ -96,69 +83,17 @@ class Member(private val getString: (Int) -> String) {
} }
} }
private suspend fun postLogin(username: String, pwd: String, salt: Int): ByteArray? = private suspend fun postLogin(username: String, pwd: String, salt: Int): ByteArray =
getString(R.string.loginApiUrl).format(Config.myHostApiUrl.random(), Config.platform.value).let { u -> getString(R.string.loginApiUrl).format(Config.platform.value).let { u ->
val use: suspend (String) -> ByteArray? = { it: String -> val r = if (!Config.net_use_foreign.value) "1" else "0"
DownloadTools.getApiConnection(it, "POST").let { c -> val pwdEncoded =
c.doOutput = true Base64.encode("$pwd-$salt".toByteArray(), Base64.DEFAULT).decodeToString()
c.setRequestProperty( Config.myHostApiUrl.request(u, "username=${
"content-type", URLEncoder.encode(
"application/x-www-form-urlencoded;charset=utf-8" username,
) Charset.defaultCharset().name()
c.setRequestProperty("platform", Config.platform.value) )
c.setRequestProperty("accept", "application/json") }&password=$pwdEncoded&salt=$salt&platform=${Config.platform.value}&authorization=Token+&version=${Config.app_ver.value}&source=copyApp&region=$r&webp=1".encodeToByteArray(),
val r = if (!Config.net_use_foreign.value) "1" else "0" "POST", "application/x-www-form-urlencoded;charset=utf-8")
val pwdEncoded = }.encodeToByteArray()
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)
}
} }

View File

@@ -3,27 +3,18 @@ package top.fumiama.copymanga.lib
import android.util.Log import android.util.Log
import top.fumiama.copymanga.api.Config import top.fumiama.copymanga.api.Config
import top.fumiama.copymanga.lib.template.LazyLibrary import top.fumiama.copymanga.lib.template.LazyLibrary
import top.fumiama.copymanga.net.DownloadTools
class Comandy: LazyLibrary<ComandyMethods>( class Comandy: LazyLibrary<ComandyMethods>(
ComandyMethods::class.java, "libcomandy.so", "网络增强", ComandyMethods::class.java, "libcomandy.so", "网络增强",
Config.net_use_comandy, Config.comandy_version Config.net_use_comandy, Config.comandy_version
) { ) {
private var mEnabled: Boolean? = null
val enabled: Boolean val enabled: Boolean
get() { get() {
if (isInInit.get()) { if (isInInit.get()) {
Log.d("MyComandy", "$name block enabled for isInInit") Log.d("MyComandy", "$name block enabled for isInInit")
return false return false
} }
if (mEnabled != true && DownloadTools.failTimes.get() >= 16) { return isInUse.value
mEnabled = true
return true
}
if (mEnabled != null) return mEnabled!!
val v = isInUse.value
mEnabled = v
return v
} }
val status: String get() = if(enabled) { val status: String get() = if(enabled) {
if (isInUse.value) "生效(手动)" else "生效(自动)" if (isInUse.value) "生效(手动)" else "生效(自动)"

View File

@@ -6,11 +6,11 @@ import com.google.gson.Gson
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import top.fumiama.copymanga.MainActivity
import top.fumiama.copymanga.api.Config import top.fumiama.copymanga.api.Config
import top.fumiama.copymanga.api.Config.proxyUrl import top.fumiama.copymanga.api.Config.proxyUrl
import top.fumiama.copymanga.json.ComandyCapsule import top.fumiama.copymanga.json.ComandyCapsule
import top.fumiama.copymanga.lib.Comandy import top.fumiama.copymanga.lib.Comandy
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
@@ -21,61 +21,63 @@ import java.text.SimpleDateFormat
import java.util.* import java.util.*
import java.util.concurrent.Callable import java.util.concurrent.Callable
import java.util.concurrent.FutureTask import java.util.concurrent.FutureTask
import java.util.concurrent.atomic.AtomicInteger import java.util.zip.GZIPInputStream
object DownloadTools { object DownloadTools {
val failTimes = AtomicInteger(0) private fun getApiConnection(url: String, method: String = "GET", timeout: Int = 20000): HttpURLConnection {
fun getApiConnection(url: String, method: String = "GET", refer: String? = null, ua: String? = null, timeout: Int = 20000): HttpURLConnection {
val connection = URL(url).openConnection() as HttpURLConnection val connection = URL(url).openConnection() as HttpURLConnection
connection.requestMethod = method connection.requestMethod = method
connection.connectTimeout = timeout connection.connectTimeout = timeout
connection.readTimeout = timeout connection.readTimeout = timeout
connection.apply { connection.apply {
/*setRequestProperty("host", if (url.startsWith("https://copymanga.azurewebsites.net")) { setRequestProperty("user-agent", Config.pc_ua)
Uri.parse(url).getQueryParameter("url")?.substringAfter("://")?.substringBefore("/")?:""
} else {
url.substringAfter("://").substringBefore("/")
})*/
ua?.let { setRequestProperty("user-agent", it) }
refer?.let { setRequestProperty("referer", it) }
setRequestProperty("source", "copyApp") setRequestProperty("source", "copyApp")
// deviceinfo
setRequestProperty("webp", "1") 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)) setRequestProperty("dt", SimpleDateFormat("yyyy.MM.dd", Locale.getDefault()).format(Calendar.getInstance().time))
Config.token.value?.let { tk -> setRequestProperty("accept-encoding", "gzip")
setRequestProperty("authorization", "Token $tk") setRequestProperty("authorization", "Token${Config.token.value?.let { tk ->
} if (tk.isNotEmpty()) " $tk" else ""
}}")
setRequestProperty("platform", Config.platform.value) 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")}") Log.d("MyDT", "getConnection: $url\n${connection.requestProperties.map { "${it.key}: ${it.value}" }.joinToString("\n")}")
return connection return connection
} }
fun getComandyApiConnection(url: String, method: String = "GET", refer: String? = null, ua: String? = null) = private fun getComandyApiConnection(url: String, method: String = "GET") =
run { run {
val capsule = ComandyCapsule() val capsule = ComandyCapsule()
capsule.url = url capsule.url = url
capsule.method = method capsule.method = method
capsule.headers = hashMapOf() capsule.headers = hashMapOf()
/*capsule.headers["host"] = if (url.startsWith("https://copymanga.azurewebsites.net")) { capsule.headers["user-agent"] = Config.pc_ua
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["source"] = "copyApp" capsule.headers["source"] = "copyApp"
// deviceinfo
capsule.headers["webp"] = "1" 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["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")}") Log.d("MyDT", "getComandyConnection: $url\n${capsule.headers.map { "${it.key}: ${it.value}" }.joinToString("\n")}")
capsule capsule
} }
@@ -123,16 +125,31 @@ object DownloadTools {
return bytesCopied 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 { private fun InputStream.readBytesWithProgress(sz: Int, p: Client.Progress): ByteArray {
val buffer = ByteArrayOutputStream(maxOf(DEFAULT_BUFFER_SIZE, this.available())) val buffer = ByteArrayOutputStream(maxOf(DEFAULT_BUFFER_SIZE, this.available()))
copyToWithProgress(buffer, sz, p) copyToWithProgress(buffer, sz, p)
return buffer.toByteArray() 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) { withContext(Dispatchers.IO) {
if (!u.startsWith("https://$proxyUrl") && Comandy.instance.enabled) { 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) val para = Gson().toJson(capsule)
//Log.d("MyDT", "comandy request: $para") //Log.d("MyDT", "comandy request: $para")
Comandy.instance.getInstance()?.let { ins -> Comandy.instance.getInstance()?.let { ins ->
@@ -154,6 +171,7 @@ object DownloadTools {
Log.d("MyDT", "quit comandy get progress, completed: $completed for url $u") Log.d("MyDT", "quit comandy get progress, completed: $completed for url $u")
}.start() }.start()
} }
var coding: String? = null
val r = ins.request(para)?.let { result -> val r = ins.request(para)?.let { result ->
completed = true completed = true
p?.notify(100) p?.notify(100)
@@ -162,26 +180,29 @@ object DownloadTools {
if (it.code != 200) throw IllegalArgumentException("HTTP${it.code} ${ if (it.code != 200) throw IllegalArgumentException("HTTP${it.code} ${
it.data?.let { d -> Base64.decode(d, Base64.DEFAULT).decodeToString() } it.data?.let { d -> Base64.decode(d, Base64.DEFAULT).decodeToString() }
}") }")
coding = it.headers["Content-Encoding"] as String?
Base64.decode(it.data, Base64.DEFAULT) Base64.decode(it.data, Base64.DEFAULT)
} }
} }
completed = true completed = true
p?.notify(100) p?.notify(100)
r r?.let { ret -> coding?.let { decodeBody(ret, it) } }
} }
}.let { if(it?.isNotEmpty() == true ) return@withContext it } }.let { if(it?.isNotEmpty() == true ) return@withContext it }
failTimes.incrementAndGet()
} }
getApiConnection(u, "GET", refer, ua).let { getApiConnection(u, "GET").let { conn ->
val sz = it.getHeaderFieldInt("Content-Length", 0) val sz = conn.getHeaderFieldInt("Content-Length", 0)
val ret = if (sz > 0 && p != null) { val ret = if (sz > 0 && p != null) {
it.inputStream.readBytesWithProgress(sz, p) conn.inputStream.readBytesWithProgress(sz, p)
} else { } else {
it.inputStream.readBytes() conn.inputStream.readBytes()
} }
it.disconnect() conn.disconnect()
Log.d("MyDT", "getHttpContent: ${ret.size} bytes") 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? { fun requestApiWithBody(url: String, method: String, body: ByteArray, contentType: String): ByteArray {
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? {
Log.d("MyDT", "$method Http: $url") Log.d("MyDT", "$method Http: $url")
var ret: ByteArray? = null return if(!url.startsWith("https://$proxyUrl") && Comandy.instance.enabled) {
val task = FutureTask(if(!url.startsWith("https://$proxyUrl") && Comandy.instance.enabled) Callable{ val capsule = getComandyApiConnection(url, method)
try { capsule.headers["content-type"] = contentType
val capsule = getComandyApiConnection(url, method, refer, ua) capsule.data = body.decodeToString()
contentType?.let { capsule.headers["content-type"] = it } runBlocking { Comandy.instance.getInstance() }?.request(Gson().toJson(capsule))?.let { result ->
capsule.data = body.decodeToString() Gson().fromJson(result, ComandyCapsule::class.java)?.let { c ->
runBlocking { Comandy.instance.getInstance() }?.request(Gson().toJson(capsule))?.let { result -> c.data?.let { d ->
Gson().fromJson(result, ComandyCapsule::class.java)?.let { Base64.decode(d, Base64.DEFAULT).let {
it.data?.let { d -> Base64.decode(d, Base64.DEFAULT) }?:"empty comandy data".encodeToByteArray() (c.headers["Content-Encoding"] as String?)?.let { coding ->
} decodeBody(it, coding)
}?:it
}
}?: throw IllegalStateException("empty comandy data")
} }
} catch (ex: Exception) { }?: throw IllegalStateException("no comandy")
ex.printStackTrace() } else {
failTimes.incrementAndGet() var ret: ByteArray
ex.message?.encodeToByteArray() var coding = ""
getApiConnection(url, method).apply {
outputStream.write(body)
ret = inputStream.readBytes()
disconnect()
coding = getHeaderField("Content-Encoding")?:""
} }
} decodeBody(ret, coding)
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
} }
} }
} }

View File

@@ -12,9 +12,8 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import top.fumiama.copymanga.MainActivity.Companion.mainWeakReference import top.fumiama.copymanga.MainActivity.Companion.mainWeakReference
import top.fumiama.copymanga.json.ReturnBase
import top.fumiama.copymanga.api.Config import top.fumiama.copymanga.api.Config
import top.fumiama.copymanga.net.DownloadTools import top.fumiama.copymanga.json.ReturnBase
import java.io.File import java.io.File
import java.security.MessageDigest import java.security.MessageDigest
@@ -77,16 +76,12 @@ open class AutoDownloadHandler(
var cnt = 0 var cnt = 0
while (cnt++ <= 3) { while (cnt++ <= 3) {
try { try {
val data = Config.apiProxy?.comancry(url()) { val data = Config.myHostApiUrl.get(url())
DownloadTools.getHttpContent(it)
}?:DownloadTools.getHttpContent(url())
if(exit) return@withContext if(exit) return@withContext
val fi = data.inputStream() val pass = setGsonItem(Gson().fromJson(data, jsonClass))
val pass = setGsonItem(Gson().fromJson(fi.reader(), jsonClass))
if (pass && loadFromCache) { if (pass && loadFromCache) {
cacheFile?.writeBytes(data) cacheFile?.writeText(data)
} }
fi.close()
if(!pass) { if(!pass) {
delay(2000) delay(2000)
continue continue

View File

@@ -5,7 +5,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import top.fumiama.copymanga.api.Config import top.fumiama.copymanga.api.Config
import top.fumiama.copymanga.net.DownloadTools
import kotlin.random.Random 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) { 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 var c = 0
while (!exit && c++ < 3) { while (!exit && c++ < 3) {
try { try {
val data = (if (isApi) Config.apiProxy?.comancry(url) { val data = Config.myHostApiUrl.get(url)
DownloadTools.getHttpContent(it, Config.referer) whenFinish?.let { it(data.encodeToByteArray()) }
} else null)?:DownloadTools.getHttpContent(url, Config.referer)
whenFinish?.let { it(data) }
return@withContext true return@withContext true
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() 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( constructor(key: Int, default: String?, defaultID: Int): this(
MainActivity.mainWeakReference?.get()?.getString(key) ?:"", default, defaultID) MainActivity.mainWeakReference?.get()?.getString(key) ?:"", default, defaultID)
constructor(key: String, default: Int): this(key, null, default) 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) constructor(key: Int): this(key, "", 0)
private val defaultField: String private val defaultField: String

View File

@@ -153,6 +153,7 @@ class BookFragment: NoBackRefreshFragment(R.layout.fragment_book) {
if (getBoolean("loadJson")) { if (getBoolean("loadJson")) {
getString("name")?.let { name -> getString("name")?.let { name ->
try { try {
Log.d("MyBF", "loadFromCache name $name")
book = Book(name, { book = Book(name, {
return@Book getString(it) return@Book getString(it)
}, activity?.getExternalFilesDir("")!!) }, activity?.getExternalFilesDir("")!!)
@@ -184,11 +185,17 @@ class BookFragment: NoBackRefreshFragment(R.layout.fragment_book) {
} }
private suspend fun queryCollect() { private suspend fun queryCollect() {
MainActivity.shelf?.query(book?.path!!)?.let { b -> try {
mBookHandler?.collect = b.results?.collect?:-2 MainActivity.shelf?.query(book?.path!!)?.let { b ->
Log.d("MyBF", "get collect of ${book?.path} = ${mBookHandler?.collect}") mBookHandler?.collect = b.results?.collect?:-2
tic.text = b.results?.browse?.chapter_name?.let { name -> Log.d("MyBF", "get collect of ${book?.path} = ${mBookHandler?.collect}")
getString(R.string.text_format_cloud_read_to).format(Chinese.fixEncodingIfNeeded(name)) 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 -> book?.uuid?.let { uuid ->
this@BookFragment.lbbsub.setOnClickListener { this@BookFragment.lbbsub?.setOnClickListener {
lifecycleScope.launch clickLaunch@ { lifecycleScope.launch clickLaunch@ {
if (this@BookFragment.lbbsub.text != getString(R.string.button_sub)) { if (this@BookFragment.lbbsub.text != getString(R.string.button_sub)) {
mBookHandler?.collect?.let { collect -> mBookHandler?.collect?.let { collect ->

View File

@@ -11,7 +11,7 @@ import top.fumiama.dmzj.copymanga.R
@OptIn(ExperimentalStdlibApi::class) @OptIn(ExperimentalStdlibApi::class)
class HistoryFragment : InfoCardLoader(R.layout.fragment_history, R.id.action_nav_history_to_nav_book, isHistoryBook = true) { class HistoryFragment : InfoCardLoader(R.layout.fragment_history, R.id.action_nav_history_to_nav_book, isHistoryBook = true) {
override fun getApiUrl() = 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?) { override fun onCreate(savedInstanceState: Bundle?) {
if (MainActivity.member?.hasLogin != true) { if (MainActivity.member?.hasLogin != true) {

View File

@@ -7,5 +7,5 @@ import top.fumiama.dmzj.copymanga.R
@ExperimentalStdlibApi @ExperimentalStdlibApi
class NewestFragment : InfoCardLoader(R.layout.fragment_newest, R.id.action_nav_newest_to_nav_book, true) { class NewestFragment : InfoCardLoader(R.layout.fragment_newest, R.id.action_nav_newest_to_nav_book, true) {
override fun getApiUrl() = 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() = override fun getApiUrl() =
getString(R.string.rankApiUrl).format( getString(R.string.rankApiUrl).format(
Config.myHostApiUrl.random(),
page * 21, page * 21,
sortWay[sortValue], sortWay[sortValue],
audienceWay[audience], audienceWay[audience],

View File

@@ -7,5 +7,5 @@ import top.fumiama.dmzj.copymanga.R
@ExperimentalStdlibApi @ExperimentalStdlibApi
class RecFragment : InfoCardLoader(R.layout.fragment_recommend, R.id.action_nav_recommend_to_nav_book, true) { class RecFragment : InfoCardLoader(R.layout.fragment_recommend, R.id.action_nav_recommend_to_nav_book, true) {
override fun getApiUrl() = 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 query: String? = null
private var type: String? = null private var type: String? = null
override fun getApiUrl() = 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)

View File

@@ -26,7 +26,6 @@ class ShelfFragment : InfoCardLoader(R.layout.fragment_shelf, R.id.action_nav_su
override fun getApiUrl() = override fun getApiUrl() =
getString(R.string.shelfApiUrl).format( getString(R.string.shelfApiUrl).format(
Config.myHostApiUrl.random(),
page * 21, page * 21,
sortWay[sortValue], sortWay[sortValue],
Config.platform.value, 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() = override fun getApiUrl() =
getString(R.string.sortApiUrl).format( getString(R.string.sortApiUrl).format(
Config.myHostApiUrl.random(),
page * 21, page * 21,
sortWay[sortValue], sortWay[sortValue],
if(theme >= 0 && theme < (filter?.results?.theme?.size ?: 0)) (filter?.results?.theme?.get(theme)?.path_word ?: "") else "", 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() super.setListeners()
lifecycleScope.launch { lifecycleScope.launch {
setProgress(5) 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 if(ad?.exit == true) return@PausableDownloader
it.let { it.let {
it.inputStream().use { i -> 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) { class TopicFragment : InfoCardLoader(R.layout.fragment_topic, R.id.action_nav_topic_to_nav_book) {
private var type = 1 private var type = 1
override fun getApiUrl() = 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
lifecycleScope.launch { lifecycleScope.launch {
setProgress(5) 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) setProgress(10)
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
if(ad?.exit == true) return@withContext if(ad?.exit == true) return@withContext

View File

@@ -118,7 +118,6 @@ class DownloadFragment: NoBackRefreshFragment(R.layout.fragment_download) {
bundle.putBoolean("callFromOldDL", true) bundle.putBoolean("callFromOldDL", true)
} }
bundle.putString("name", jsonFile.parentFile?.name?:"Null") bundle.putString("name", jsonFile.parentFile?.name?:"Null")
Log.d("MyDF", "root view: $rootView")
Log.d("MyDF", "action_nav_download_to_nav_group") Log.d("MyDF", "action_nav_download_to_nav_group")
Navigate.safeNavigateTo(findNavController(), R.id.action_nav_download_to_nav_group, bundle) 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("title", title)
bundle.putString("file", file.absolutePath) bundle.putString("file", file.absolutePath)
Log.d("MyDF", "Call self to $title") Log.d("MyDF", "Call self to $title")
Log.d("MyDF", "root view: $rootView")
Log.d("MyDF", "action_nav_download_self") Log.d("MyDF", "action_nav_download_self")
Navigate.safeNavigateTo(findNavController(), R.id.action_nav_download_self, bundle) 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.") Log.d("MyNDF", "Call dl and is new.")
bundle.putString("loadJson", File(File(extDir, name), "info.json").readText()) bundle.putString("loadJson", File(File(extDir, name), "info.json").readText())
bundle.putString("name", name) bundle.putString("name", name)
Log.d("MyNDF", "root view: $rootView")
Log.d("MyNDF", "action_nav_new_download_to_nav_group") Log.d("MyNDF", "action_nav_new_download_to_nav_group")
Navigate.safeNavigateTo(findNavController(), R.id.action_nav_new_download_to_nav_group, bundle) 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 val netInfo = tb.netInfo
if(netInfo != tb.transportStringNull && netInfo != tb.transportStringError) if(netInfo != tb.transportStringNull && netInfo != tb.transportStringError)
MainActivity.member?.apply { lifecycleScope.launch { MainActivity.member?.apply { lifecycleScope.launch {
Config.myHostApiUrl.init()
info().let { l -> info().let { l ->
if (l.code != 200 && l.code != 449) { if (l.code != 200 && l.code != 449) {
Toast.makeText(context, l.message, Toast.LENGTH_SHORT).show() 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) { suspend fun refresh(q: CharSequence) = withContext(Dispatchers.IO) {
query = q.toString() query = q.toString()
activity?.apply { 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)) { URLEncoder.encode(q.toString(), Charset.defaultCharset().name()), type, Config.platform.value)) {
results = Gson().fromJson(it.decodeToString(), BookListStructure::class.java) results = Gson().fromJson(it.decodeToString(), BookListStructure::class.java)
count = results?.results?.total?:0 count = results?.results?.total?:0

View File

@@ -40,7 +40,7 @@ import java.lang.ref.WeakReference
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
class HomeHandler(private val that: WeakReference<HomeFragment>) : AutoDownloadHandler({ 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, IndexStructure::class.java,
that.get() that.get()

View File

@@ -25,6 +25,7 @@ open class NoBackRefreshFragment(private val layoutToLoad: Int): Fragment() {
container: ViewGroup?, container: ViewGroup?,
savedInstanceState: Bundle? savedInstanceState: Bundle?
): View? { ): View? {
//TODO: 支持自动重建
if(_rootView == null) { if(_rootView == null) {
isFirstInflate = true isFirstInflate = true
_rootView = inflater.inflate(layoutToLoad, container, false) _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() = override fun getApiUrl() =
getString(api).format( getString(api).format(
Config.myHostApiUrl.random(),
page * 21, page * 21,
sortWay[sortValue], sortWay[sortValue],
Config.platform.value, Config.platform.value,

View File

@@ -12,7 +12,6 @@ open class ThemeCardFlow(private val api: Int, nav: Int) : StatusCardFlow(0, nav
private var theme = "" private var theme = ""
override fun getApiUrl() = override fun getApiUrl() =
getString(api).format( getString(api).format(
Config.myHostApiUrl.random(),
page * 21, page * 21,
sortWay[sortValue], sortWay[sortValue],
theme, theme,

View File

@@ -3,7 +3,7 @@
<!ENTITY hosturl "api.copy-manga.com"> <!ENTITY hosturl "api.copy-manga.com">
<!ENTITY appver "2.3.0"> <!ENTITY appver "2.3.0">
<!ENTITY proxyurl "copymanga.azurewebsites.net"> <!ENTITY proxyurl "copymanga.azurewebsites.net">
<!ENTITY platform "1"> <!ENTITY platform "3">
]> ]>
<resources> <resources>
<string name="app_name">拷贝漫画</string> <string name="app_name">拷贝漫画</string>
@@ -64,31 +64,30 @@
<string name="touch_img_error">预载图片头失败</string> <string name="touch_img_error">预载图片头失败</string>
<string name="analyze_img_size_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="networkApiUrl">/api/v3/system/network2?platform=%1$s</string>
<string name="mainPageApiUrl">https://%1$s/api/v3/h5/homeIndex?platform=%2$s</string> <string name="mainPageApiUrl">/api/v3/h5/homeIndex?platform=%1$s</string>
<string name="referUrl">https://%1$s</string>
<string name="hostUrl">&hosturl;</string> <string name="hostUrl">&hosturl;</string>
<string name="proxyUrl">&proxyurl;</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="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">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="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">https://%1$s/api/v3/h5/filter/comic/tags?platform=%2$s</string> <string name="filterApiUrl">/api/v3/h5/filter/comic/tags?platform=%1$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="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">https://%1$s/api/v3/comic2/%2$s?platform=%3$s</string> <string name="bookInfoApiUrl">/api/v3/comic2/%1$s?platform=%2$s</string>
<string name="bookUserQueryApiUrl">https://%1$s/api/v3/comic2/%2$s/query?platform=%3$s</string> <string name="bookUserQueryApiUrl">/api/v3/comic2/%1$s/query?platform=%2$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="groupInfoApiUrl">/api/v3/comic/%1$s/group/%2$s/chapters?limit=100&amp;offset=%3$d&amp;platform=%4$s</string>
<string name="chapterInfoApiUrl">https://%1$s/api/v3/comic/%2$s/chapter%3$s/%4$s?platform=%5$s</string> <string name="chapterInfoApiUrl">/api/v3/comic/%1$s/chapter%2$s/%3$s?platform=%4$s</string>
<string name="topicApiUrl">https://%1$s/api/v3/topic/%2$s?platform=%3$s</string> <string name="topicApiUrl">/api/v3/topic/%1$s?platform=%2$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="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">https://%1$s/api/v3/recs?pos=3200102&amp;limit=21&amp;offset=%2$d&amp;platform=%3$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">https://%1$s/api/v3/update/newest?limit=21&amp;offset=%2$d&amp;platform=%3$s</string> <string name="newestApiUrl">/api/v3/update/newest?limit=21&amp;offset=%1$d&amp;platform=%2$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="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">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="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">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="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">https://%1$s/api/v3/login?platform=%2$s</string> <string name="loginApiUrl">/api/v3/login?platform=%1$s</string>
<string name="memberInfoApiUrl">https://%1$s/api/v3/member/info?platform=%2$s</string> <string name="memberInfoApiUrl">/api/v3/member/info?platform=%1$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="historyApiUrl">/api/v3/member/browse/comics?limit=21&amp;offset=%1$d&amp;platform=%2$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="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">https://%1$s/api/v3/member/collect/comic</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="imgProxyApiUrl">https://&proxyurl;/api/img?code=%1$s&amp;url=%2$s</string>
<string name="imgProxyCodeKeyID">settings_cat_net_et_img_proxy_code</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_summary_api_url">一般无需更改,除非拷贝漫画官方更改网址,默认:&hosturl;</string>
<string name="settings_cat_net_et_title_reverse_proxy">反向代理</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_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_sw_use_api_proxy">使用API代理需要密钥</string>
<string name="settings_cat_net_sm_use_api_proxy">作者自建的API代理可缓解国内图书详情加载问题但不保证100%解决,也不保证一直可用</string> <string name="settings_cat_net_sm_use_api_proxy">作者自建的API代理可缓解国内图书详情加载问题但不保证100%解决,也不保证一直可用</string>
<string name="settings_cat_net_sw_use_img_proxy">使用图床代理(需要密钥)</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_username">用户名为空</string>
<string name="login_null_pwd">密码为空</string> <string name="login_null_pwd">密码为空</string>
<string name="login_get_conn_failed">登录失败</string>
<string name="login_parse_json_error">解析返回数据失败</string> <string name="login_parse_json_error">解析返回数据失败</string>
<string name="login_get_avatar_failed">获取用户信息失败</string> <string name="login_get_avatar_failed">获取用户信息失败</string>
<string name="login_restart_to_apply">重启应用以彻底退出登录</string> <string name="login_restart_to_apply">重启应用以彻底退出登录</string>

View File

@@ -120,6 +120,16 @@
app:enableCopying="true" app:enableCopying="true"
app:iconSpaceReserved="false" app:iconSpaceReserved="false"
app:key="@string/reverseProxyKeyID" /> 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 <SwitchPreferenceCompat
app:iconSpaceReserved="false" app:iconSpaceReserved="false"
app:key="@string/apiProxyKeyID" app:key="@string/apiProxyKeyID"