1
0
mirror of https://github.com/fumiama/copymanga.git synced 2026-06-04 23:10:23 +08:00
新增
1. 增强型数据访问选项
2. 漫画下载数量并发限制
修复
1. 同时下载过多漫画时失败 (fix #67)
2. 主页滑动横幅后刷新闪退 (fix #65)
优化
1. 网络不佳时自动打开增强型数据访问
2. 不再反复读取代理状态
This commit is contained in:
源文雨
2024-04-16 23:03:08 +09:00
parent 43de1640f1
commit 05a774d542
20 changed files with 501 additions and 91 deletions

View File

@@ -2,10 +2,12 @@
<dictionary name="fumiama">
<words>
<w>alphae</w>
<w>comandy</w>
<w>downloaders</w>
<w>grps</w>
<w>imgs</w>
<w>kohima</w>
<w>libcomandy</w>
<w>lowpan</w>
<w>mangacopy</w>
<w>mangafuna</w>

View File

@@ -1,6 +1,7 @@
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
android {
defaultConfig {
@@ -8,8 +9,8 @@ android {
applicationId 'top.fumiama.copymanga'
minSdkVersion 23
targetSdkVersion 34
versionCode 57
versionName '2.2.9'
versionCode 58
versionName '2.3.0'
resourceConfigurations += ['zh', 'zh-rCN']
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -46,6 +47,11 @@ android {
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}*/
}
sourceSets {
main {
jniLibs.srcDirs = ['libs']
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
@@ -78,7 +84,7 @@ dependencies {
implementation 'com.github.yalantis:ucrop:2.2.6'
implementation 'com.to.aboomy:pager2banner:1.0.1'
implementation 'com.github.bumptech.glide:glide:4.16.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0'
kapt 'com.github.bumptech.glide:compiler:4.16.0'
implementation 'com.google.code.gson:gson:2.10.1'
implementation 'com.github.vovaksenov99:OverscrollableScrollView:1.0'
implementation 'com.liaoinstan.springview:library:1.7.0'
@@ -86,4 +92,5 @@ dependencies {
implementation 'com.lapism:search:2.4.1@aar'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
implementation 'com.airbnb.android:lottie:6.4.0'
implementation 'net.java.dev.jna:jna:5.14.0'
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,11 @@
package top.fumiama.copymanga.json;
import java.util.HashMap;
public class ComandyCapsule {
public int code;
public String method;
public String url;
public HashMap<String, Object> headers;
public String data;
}

View File

@@ -0,0 +1,5 @@
package top.fumiama.copymanga.json;
public class ComandyVersion {
public String body;
}

View File

@@ -10,9 +10,7 @@ import androidx.core.animation.doOnEnd
import androidx.fragment.app.Fragment
import androidx.preference.PreferenceManager
import kotlinx.android.synthetic.main.content_main.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import top.fumiama.copymanga.MainActivity
import top.fumiama.copymanga.tools.ui.UITools
import java.util.concurrent.atomic.AtomicBoolean
@@ -48,8 +46,8 @@ open class NoBackRefreshFragment(private val layoutToLoad: Int): Fragment() {
isFirstInflate = true
Log.d("MyNBRF", "destroyed")
}
suspend fun showKanban() = withContext(Dispatchers.Main) {
if (disableAnimation) return@withContext
fun showKanban() {
if (disableAnimation) return
(activity?:(MainActivity.mainWeakReference?.get()))?.apply {cmaini?.post {
if(cmaini?.visibility == View.GONE) {
Log.d("MyNBRF", "show: start, set h: ${window?.decorView?.height}")
@@ -62,8 +60,8 @@ open class NoBackRefreshFragment(private val layoutToLoad: Int): Fragment() {
Log.d("MyNBRF", "show: end")
}
private var isHideRunning = AtomicBoolean()
suspend fun hideKanban() = withContext(Dispatchers.Main) {
if (disableAnimation) return@withContext
fun hideKanban() {
if (disableAnimation) return
(activity?:(MainActivity.mainWeakReference?.get()))?.apply { cmaini?.post {
if(!isHideRunning.get() && cmaini?.visibility == View.VISIBLE) {
isHideRunning.set(true)

View File

@@ -0,0 +1,117 @@
package top.fumiama.copymanga.tools.http
import android.os.Build
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import com.google.gson.Gson
import com.sun.jna.Library
import com.sun.jna.Native
import top.fumiama.copymanga.MainActivity
import top.fumiama.copymanga.json.ComandyVersion
import top.fumiama.dmzj.copymanga.R
import java.io.ByteArrayInputStream
import java.io.File
import java.util.concurrent.atomic.AtomicBoolean
import java.util.zip.GZIPInputStream
interface Comandy : Library {
// fun add_dns(para: String?, is_ipv6: Int): String?
fun request(para: String): String?
companion object {
private var isInInit = AtomicBoolean(false)
var instance: Comandy? = null
get() {
//Log.d("MyComandy", "get instance @$field")
if (field != null) return field
field = libraryFile?.absolutePath?.let { Native.load(it, Comandy::class.java) }?:return null
//Log.d("MyComandy", "init instance @$field")
return field
}
private var mUseComandy: Boolean? = null
val useComandy: Boolean
get() {
if (isInInit.get()) {
Log.d("MyComandy", "block useComandy for isInInit")
return false
}
if (mUseComandy != true && DownloadTools.failTimes.get() >= 2) {
mUseComandy = true
return true
}
if (mUseComandy != null) return mUseComandy!!
MainActivity.mainWeakReference?.get()?.let {
PreferenceManager.getDefaultSharedPreferences(it).apply {
val b = getBoolean("settings_cat_net_sw_use_comandy", false)
Log.d("MyComandy", "use comandy: $b")
mUseComandy = b
return b
}
}
mUseComandy = false
return false
}
private val libraryFile: File?
get() {
if (isInInit.get()) return null
isInInit.set(true)
Log.d("MyComandy", "start to download/check lib")
val prefix = when (Build.SUPPORTED_ABIS[0]) {
"arm64-v8a" -> "aarch64"
"armeabi-v7a" -> "armv7a"
"x86_64" -> "x86_64"
"x86" -> "i686"
else -> null
}?:return null
Log.d("MyComandy", "arch: $prefix")
return MainActivity.mainWeakReference?.get()?.let { ma ->
var f = File(ma.filesDir, "libs")
if (!f.exists()) f.mkdirs()
f = File(f, "libcomandy.so")
var remoteVersion = 0
if (f.exists()) {
DownloadTools.getHttpContent(ma.getString(R.string.comandy_version_url), -1)?.let dataLet@{
try {
val body = Gson().fromJson(it.decodeToString(), ComandyVersion::class.java)?.body
if (body?.startsWith("Version: ") == true) {
remoteVersion = body.substring(9).toInt()
}
} catch (e: Exception) {
e.printStackTrace()
}
val myVersion = ma.getPreferences(AppCompatActivity.MODE_PRIVATE).getInt("comandy_version", 0)
if (myVersion >= remoteVersion) {
Log.d("MyComandy", "lib version $myVersion is latest")
isInInit.set(false)
return@let f
}
Log.d("MyComandy", "lib version $myVersion <= latest $remoteVersion, update...")
}
}
DownloadTools.getHttpContent(ma.getString(R.string.comandy_download_url).format(prefix), -1)?.let {
if(f.exists()) f.delete()
try {
GZIPInputStream(ByteArrayInputStream(it)).use { dataIn ->
f.outputStream().use { dataOut ->
dataIn.copyTo(dataOut)
}
}
if (remoteVersion > 0) ma.getPreferences(AppCompatActivity.MODE_PRIVATE).edit {
putInt("comandy_version", remoteVersion)
apply()
}
Log.d("MyComandy", "update success")
isInInit.set(false)
} catch (e: Exception) {
e.printStackTrace()
if(f.exists()) f.delete()
}
}
return@let if(f.exists()) f else null
}
}
}
}

View File

@@ -0,0 +1,129 @@
package top.fumiama.copymanga.tools.http
import android.content.Context
import android.util.Base64
import android.util.Log
import com.bumptech.glide.Glide
import com.bumptech.glide.Priority
import com.bumptech.glide.Registry
import com.bumptech.glide.annotation.GlideModule
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.Options
import com.bumptech.glide.load.data.DataFetcher
import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.load.model.ModelLoader
import com.bumptech.glide.load.model.ModelLoaderFactory
import com.bumptech.glide.load.model.MultiModelLoaderFactory
import com.bumptech.glide.module.AppGlideModule
import com.bumptech.glide.signature.ObjectKey
import com.google.gson.Gson
import top.fumiama.copymanga.MainActivity
import top.fumiama.copymanga.json.ComandyCapsule
import java.nio.ByteBuffer
@GlideModule
class ComandyGlideModule: AppGlideModule() {
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
super.registerComponents(context, glide, registry)
registry.prepend(GlideUrl::class.java, ByteBuffer::class.java, ComandyGlideUrlFactory())
registry.prepend(String::class.java, ByteBuffer::class.java, ComandyStringFactory())
}
inner class ComandyDataFetcher(private val model: GlideUrl): DataFetcher<ByteBuffer> {
constructor(model: String): this(GlideUrl(model))
override fun loadData(
priority: Priority,
callback: DataFetcher.DataCallback<in ByteBuffer>
) {
val capsule = ComandyCapsule()
capsule.url = model.toStringUrl()
capsule.method = "GET"
if (model.headers.isNotEmpty()) {
capsule.headers = hashMapOf()
model.headers.forEach { (k, v) -> capsule.headers[k] = v }
}
try {
val para = Gson().toJson(capsule)
MainActivity.mainWeakReference?.get()?.runOnUiThread {
Log.d("MyCGM", "request: $para")
}
Comandy.instance!!.request(para).let { result ->
Gson().fromJson(result, ComandyCapsule::class.java)!!.let {
if (it.code != 200) {
callback.onLoadFailed(IllegalArgumentException("HTTP${it.code} ${
it.data?.let { d -> Base64.decode(d, Base64.DEFAULT).decodeToString() }
}"))
return
}
callback.onDataReady(ByteBuffer.wrap(Base64.decode(it.data, Base64.DEFAULT)))
}
}
} catch (e: Exception) {
callback.onLoadFailed(e)
}
}
override fun cleanup() { }
override fun cancel() { }
override fun getDataClass(): Class<ByteBuffer> {
return ByteBuffer::class.java
}
override fun getDataSource(): DataSource {
return DataSource.REMOTE
}
}
inner class ComandyGlideUrlModelLoader: ModelLoader<GlideUrl, ByteBuffer> {
override fun buildLoadData(
model: GlideUrl,
width: Int,
height: Int,
options: Options
): ModelLoader.LoadData<ByteBuffer> {
return ModelLoader.LoadData(ObjectKey(model), ComandyDataFetcher(model))
}
override fun handles(model: GlideUrl): Boolean {
return Comandy.useComandy && Comandy.instance != null && model.toURL().let { it.protocol == "https" }
}
}
inner class ComandyStringModelLoader: ModelLoader<String, ByteBuffer> {
override fun buildLoadData(
model: String,
width: Int,
height: Int,
options: Options
): ModelLoader.LoadData<ByteBuffer> {
return ModelLoader.LoadData(ObjectKey(model), ComandyDataFetcher(model))
}
override fun handles(model: String): Boolean {
return Comandy.useComandy && Comandy.instance != null && model.startsWith("https://")
}
}
inner class ComandyGlideUrlFactory: ModelLoaderFactory<GlideUrl, ByteBuffer> {
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<GlideUrl, ByteBuffer> {
return ComandyGlideUrlModelLoader()
}
override fun teardown() { }
}
inner class ComandyStringFactory: ModelLoaderFactory<String, ByteBuffer> {
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<String, ByteBuffer> {
return ComandyStringModelLoader()
}
override fun teardown() { }
}
}

View File

@@ -1,16 +1,21 @@
package top.fumiama.copymanga.tools.http
import android.content.Context
import android.util.Base64
import android.util.Log
import androidx.preference.PreferenceManager
import com.google.gson.Gson
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import top.fumiama.copymanga.MainActivity
import top.fumiama.copymanga.json.ComandyCapsule
import top.fumiama.dmzj.copymanga.R
import java.lang.IllegalArgumentException
import java.net.HttpURLConnection
import java.net.URL
import java.util.concurrent.Callable
import java.util.concurrent.FutureTask
import java.util.concurrent.atomic.AtomicInteger
object DownloadTools {
val app_ver = MainActivity.mainWeakReference?.get()?.let { main ->
@@ -20,6 +25,7 @@ object DownloadTools {
}!!
val pc_ua = MainActivity.mainWeakReference?.get()!!.getString(R.string.pc_ua).format(app_ver)
val referer = MainActivity.mainWeakReference?.get()!!.getString(R.string.referer).format(app_ver)
val failTimes = AtomicInteger(0)
fun getApiConnection(url: String, method: String = "GET", refer: String? = null, ua: String? = null, timeout: Int = 20000) =
url.let {
val connection = URL(url).openConnection() as HttpURLConnection
@@ -45,10 +51,37 @@ object DownloadTools {
}
setRequestProperty("platform", "3")
}
Log.d("Mydl", "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")}")
connection
}
fun getComandyApiConnection(url: String, method: String = "GET", refer: String? = null, ua: String? = null) =
run {
val capsule = ComandyCapsule()
capsule.url = url
capsule.method = method
capsule.headers = hashMapOf()
capsule.headers["host"] = url.substringAfter("://").substringBefore("/")
ua?.let { capsule.headers["user-agent"] = it }
refer?.let { capsule.headers["referer"] = it }
capsule.headers["source"] = "copyApp"
capsule.headers["webp"] = "1"
MainActivity.mainWeakReference?.get()?.let {
PreferenceManager.getDefaultSharedPreferences(it).apply {
capsule.headers["region"] = if(!getBoolean("settings_cat_net_sw_use_foreign", false)) "1" else "0"
}
it.getPreferences(Context.MODE_PRIVATE).apply {
capsule.headers["version"] = app_ver
getString("token", "")?.let { tk ->
capsule.headers["authorization"] = "Token $tk"
}
}
}
capsule.headers["platform"] = "3"
Log.d("MyDT", "getComandyConnection: $url\n${capsule.headers.map { "${it.key}: ${it.value}" }.joinToString("\n")}")
capsule
}
private fun getNormalConnection(url: String, method: String = "GET", ua: String? = null, timeout: Int = 20000) =
url.let {
val connection = URL(url).openConnection() as HttpURLConnection
@@ -61,20 +94,75 @@ object DownloadTools {
}
}
private fun getComandyNormalConnection(url: String, method: String = "GET", ua: String? = null) =
run {
val capsule = ComandyCapsule()
capsule.url = url
capsule.method = method
capsule.headers = hashMapOf()
capsule.headers["host"] = url.substringAfter("://").substringBefore("/")
ua?.let { capsule.headers["user-agent"] = it }
capsule
}
suspend fun getHttpContent(u: String, refer: String? = null, ua: String? = null): ByteArray =
withContext(Dispatchers.IO) {
if (Comandy.useComandy) {
getComandyApiConnection(u, "GET", refer, ua).let { capsule ->
val para = Gson().toJson(capsule)
//Log.d("MyDT", "comandy request: $para")
Comandy.instance?.request(para)?.let { result ->
//Log.d("MyDT", "comandy reply: $result")
Gson().fromJson(result, ComandyCapsule::class.java)!!.let {
if (it.code != 200) throw IllegalArgumentException("HTTP${it.code} ${
it.data?.let { d -> Base64.decode(d, Base64.DEFAULT).decodeToString() }
}")
Base64.decode(it.data, Base64.DEFAULT)
}
}
}.let { if(it?.isNotEmpty() == true ) return@withContext it }
}
failTimes.incrementAndGet()
getApiConnection(u, "GET", refer, ua).let {
val ret = it.inputStream.readBytes()
it.disconnect()
Log.d("Mydl", "getHttpContent: ${ret.size} bytes")
Log.d("MyDT", "getHttpContent: ${ret.size} bytes")
failTimes.decrementAndGet()
ret
}
}
fun getHttpContent(u: String, readSize: Int): ByteArray? {
Log.d("Mydl", "getHttp: $u")
var ret: ByteArray? = null
val task = FutureTask(Callable {
Log.d("MyDT", "getHttp: $u")
val task = prepare(u, readSize)
Thread(task).start()
return try {
task.get()
} catch (ex: Exception) {
ex.printStackTrace()
if (Comandy.useComandy) failTimes.incrementAndGet()
null
}
}
fun prepare(u: String, readSize: Int = -1) = run {
Log.d("MyDT", "prepareHttp: $u")
FutureTask(if (Comandy.useComandy) Callable{
try {
Comandy.instance?.request(Gson().toJson(
getComandyNormalConnection(u, "GET"))
)?.let { result ->
Gson().fromJson(result, ComandyCapsule::class.java)?.let {
if (it.code != 200) null
else it.data?.let { d -> Base64.decode(d, Base64.DEFAULT) }
}
}
} catch (ex: Exception) {
ex.printStackTrace()
null
}
} else Callable {
var ret: ByteArray? = null
try {
val connection = getNormalConnection(u, "GET")
val ci = connection.inputStream
@@ -87,57 +175,10 @@ object DownloadTools {
} catch (ex: Exception) {
ex.printStackTrace()
}
return@Callable ret
ret
})
Thread(task).start()
return try {
task.get()
} catch (ex: Exception) {
ex.printStackTrace()
null
}
}
/*fun touch(url: String?): FutureTask<ByteArray?>? =
url?.let {
Log.d("Mydl", "touchHttp: $it")
var ret: ByteArray? = null
val task = FutureTask(Callable {
try {
val connection = getNormalConnection(it, "GET")
val ci = connection?.inputStream
ret = ci?.readBytes()
ci?.close()
connection?.disconnect()
} catch (ex: Exception) {
ex.printStackTrace()
}
return@Callable ret
})
Thread(task).start()
task
}*/
fun prepare(url: String?): FutureTask<ByteArray?>? =
url?.let {
Log.d("Mydl", "prepareHttp: $it")
val task = FutureTask(Callable {
var ret: ByteArray? = null
try {
val connection = getNormalConnection(it, "GET")
connection.inputStream?.use { ci ->
ret = ci.readBytes()
}
connection.disconnect()
} catch (ex: Exception) {
ex.printStackTrace()
}
return@Callable ret
})
task
}
/*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 ->
@@ -146,14 +187,31 @@ object DownloadTools {
}*/
fun requestWithBody(url: String, method: String, body: ByteArray, refer: String? = null, ua: String? = null): ByteArray? {
Log.d("Mydl", "$method Http: $url")
Log.d("MyDT", "$method Http: $url")
var ret: ByteArray? = null
val task = FutureTask(Callable {
val task = FutureTask(if(Comandy.useComandy) Callable{
try {
getApiConnection(url, method, refer, ua)?.apply {
val capsule = getComandyApiConnection(url, method, refer, ua)
capsule.data = body.decodeToString()
Comandy.instance?.request(Gson().toJson(capsule))?.let { result ->
Gson().fromJson(result, ComandyCapsule::class.java)?.let {
if (it.code != 200) null
else it.data?.let { d -> Base64.decode(d, Base64.DEFAULT) }
}
}
} catch (ex: Exception) {
ex.printStackTrace()
null
}
}
else Callable {
failTimes.incrementAndGet()
try {
getApiConnection(url, method, refer, ua).apply {
outputStream.write(body)
ret = inputStream.readBytes()
disconnect()
failTimes.decrementAndGet()
}
} catch (ex: Exception) {
ex.printStackTrace()

View File

@@ -30,26 +30,34 @@ class Proxy(id: Int, private val apiRegex: Regex, keyID: Int? = null) {
}
companion object {
private var mUseImageProxy: Boolean? = null
val useImageProxy: Boolean
get() {
if (mUseImageProxy != null) return mUseImageProxy!!
MainActivity.mainWeakReference?.get()?.let {
PreferenceManager.getDefaultSharedPreferences(it).apply {
val b = getBoolean("settings_cat_net_sw_use_img_proxy", false)
Log.d("MyProxy", "use image proxy: $b")
mUseImageProxy = b
return b
}
}
mUseImageProxy = false
return false
}
private var mUseApiProxy: Boolean? = null
val useApiProxy: Boolean
get() {
if (mUseApiProxy != null) return mUseApiProxy!!
MainActivity.mainWeakReference?.get()?.let {
PreferenceManager.getDefaultSharedPreferences(it).apply {
val b = getBoolean("settings_cat_net_sw_use_api_proxy", false)
Log.d("MyProxy", "use api proxy: $b")
mUseApiProxy = b
return b
}
}
mUseApiProxy = false
return false
}
}

View File

@@ -11,6 +11,7 @@ import android.view.ViewGroup
import android.view.ViewTreeObserver
import android.widget.Toast
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager
import com.google.gson.Gson
import kotlinx.android.synthetic.main.button_tbutton.*
import kotlinx.android.synthetic.main.button_tbutton.view.*
@@ -21,6 +22,7 @@ import kotlinx.android.synthetic.main.line_chapter.view.*
import kotlinx.android.synthetic.main.line_horizonal_empty.view.*
import kotlinx.android.synthetic.main.widget_downloadbar.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import top.fumiama.copymanga.json.ChapterStructure
@@ -38,6 +40,7 @@ import top.fumiama.copymanga.views.LazyScrollView
import top.fumiama.dmzj.copymanga.R
import java.io.File
import java.lang.ref.WeakReference
import java.util.concurrent.atomic.AtomicInteger
class ComicDlHandler(looper: Looper, private val th: WeakReference<ComicDlFragment>, private val vols: Array<VolumeStructure>, private val comicName: String, private val groupNames: Array<String>?):Handler(looper) {
constructor(looper: Looper, th: WeakReference<ComicDlFragment>, comicName: String) : this(looper, th, arrayOf(), comicName, null) {
@@ -63,6 +66,7 @@ class ComicDlHandler(looper: Looper, private val th: WeakReference<ComicDlFragme
var downloading = false
private var urlArray = arrayOf<String>()
private var uuidArray = arrayOf<String>()
private val maxBatch = that?.activity?.let { PreferenceManager.getDefaultSharedPreferences(it) }?.getInt("settings_cat_md_sb_max_batch", 16)?:16
@SuppressLint("SetTextI18n")
override fun handleMessage(msg: Message) {
@@ -265,12 +269,15 @@ class ComicDlHandler(looper: Looper, private val th: WeakReference<ComicDlFragme
}
private suspend fun downloadChapterPages() = withContext(Dispatchers.IO) {
val totalInDownload = AtomicInteger(0)
tbtnlist.forEach { i ->
if(i.isChecked) {
withContext(Dispatchers.Main) {
i.isEnabled = false
}
i.url?.let {
while (totalInDownload.get() >= maxBatch) delay(1000)
totalInDownload.incrementAndGet()
launch {
mangaDlTools.downloadChapterInVol(
it,
@@ -278,7 +285,7 @@ class ComicDlHandler(looper: Looper, private val th: WeakReference<ComicDlFragme
i.caption?:"null",
i.index
)
}
}.invokeOnCompletion { totalInDownload.decrementAndGet() }
}
}
}

View File

@@ -63,7 +63,7 @@ class HomeHandler(private val that: WeakReference<HomeFragment>) : AutoDownloadH
-1 -> {
homeF?.apply {
swiperefresh?.isRefreshing = msg.obj as Boolean
lifecycleScope.launch { if(msg.obj as Boolean) showKanban() else hideKanban() }
if(msg.obj as Boolean) showKanban() else hideKanban()
}
}
//0 -> setLayouts()
@@ -281,8 +281,8 @@ class HomeHandler(private val that: WeakReference<HomeFragment>) : AutoDownloadH
withContext(Dispatchers.IO) {
homeF?.showKanban()
fhib?.isAutoPlay = false
fhib?.adapter?.notifyDataSetChanged()
index = null
fhib?.adapter?.notifyDataSetChanged()
fhib = null
indexLines = arrayOf()
this@HomeHandler.sendEmptyMessage(6) //removeAllViews

View File

@@ -5,8 +5,10 @@ import android.util.Base64
import com.google.gson.Gson
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import top.fumiama.copymanga.json.ComandyCapsule
import top.fumiama.copymanga.json.LoginInfoStructure
import top.fumiama.copymanga.tools.api.CMApi
import top.fumiama.copymanga.tools.http.Comandy
import top.fumiama.copymanga.tools.http.DownloadTools
import top.fumiama.dmzj.copymanga.R
import java.net.URLEncoder
@@ -16,29 +18,37 @@ class Member(private val pref: SharedPreferences, private val getString: (Int) -
val hasLogin: Boolean get() = pref.getString("token", "")?.isNotEmpty()?:false
suspend fun login(username: String, pwd: String, salt: Int): LoginInfoStructure = withContext(Dispatchers.IO) {
var err = ""
getLoginConnection(username, pwd, salt).apply {
if (Comandy.useComandy) getComandyLoginConnection(username, pwd, salt).let { capsule ->
try {
Comandy.instance?.request(Gson().toJson(capsule))?.let { result ->
Gson().fromJson(result, ComandyCapsule::class.java)!!.let {
if (it.code != 200) {
val l = LoginInfoStructure()
l.code = it.code
l.message = it.data?.let { d -> Base64.decode(d, Base64.DEFAULT).decodeToString() }
return@withContext l
}
Base64.decode(it.data, Base64.DEFAULT)
}
}
} catch (e: Exception) {
err = e.message.toString()
null
}
}?.let {
try {
saveInfo(it)
} catch (e: Exception) {
err = e.message.toString()
}
}
else getLoginConnection(username, pwd, salt).apply {
inputStream.use {
it?.readBytes()?.let { data ->
data.inputStream().use { dataIn ->
try {
Gson().fromJson<LoginInfoStructure>(
dataIn.reader(), LoginInfoStructure::class.java
)?.let { info ->
if(info.code == 200) {
pref.edit()?.apply {
putString("token", info.results?.token)
putString("user_id", info.results?.user_id)
putString("username", info.results?.username)
putString("nickname", info.results?.nickname)
apply()
return@withContext info()
}
}
return@withContext info
}?: run { err = getString(R.string.login_parse_json_error) }
} catch (e: Exception) {
err = data.decodeToString()
}
try {
saveInfo(data)
} catch (e: Exception) {
err = e.message.toString()
}
}?: run { err = getString(R.string.login_get_conn_failed) }
}
@@ -101,6 +111,26 @@ class Member(private val pref: SharedPreferences, private val getString: (Int) -
}
}
private suspend fun saveInfo(data: ByteArray) = data.inputStream().use { dataIn ->
try {
Gson().fromJson(dataIn.reader(), LoginInfoStructure::class.java)?.let { l ->
if(l.code == 200) {
pref.edit()?.apply {
putString("token", l.results?.token)
putString("user_id", l.results?.user_id)
putString("username", l.results?.username)
putString("nickname", l.results?.nickname)
apply()
return@use info()
}
}
return@use l
}?: throw Exception(getString(R.string.login_parse_json_error))
} catch (e: Exception) {
throw Exception(data.decodeToString(), e)
}
}
private fun getLoginConnection(username: String, pwd: String, salt: Int) =
getString(R.string.loginApiUrl).format(CMApi.myHostApiUrl).let {
CMApi.apiProxy?.wrap(it)?:it
@@ -117,4 +147,20 @@ class Member(private val pref: SharedPreferences, private val getString: (Int) -
}
}
}
private fun getComandyLoginConnection(username: String, pwd: String, salt: Int) =
getString(R.string.loginApiUrl).format(CMApi.myHostApiUrl).let {
CMApi.apiProxy?.wrap(it)?:it
}.let {
DownloadTools.getComandyApiConnection(it, "POST").apply {
pref.apply {
headers["content-type"] = "application/x-www-form-urlencoded;charset=utf-8"
headers["platform"] = "3"
headers["accept"] = "application/json"
val r = if(!getBoolean("settings_cat_net_sw_use_foreign", false)) "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=3&authorization=Token+&version=1.4.4&source=copyApp&region=$r&webp=1"
}
}
}
}

View File

@@ -89,6 +89,9 @@
<string name="apiProxyApiUrl">https://copymanga.azurewebsites.net/api/api?code=%1$s&amp;url=%2$s</string>
<string name="imgResolutionKeyID">settings_cat_net_sb_image_resolution</string>
<string name="comandy_version_url">https://gitea.seku.su/api/v1/repos/fumiama/comandy/releases/tags/default</string>
<string name="comandy_download_url">"https://gitea.seku.su/fumiama/comandy/releases/download/default/%1$s_libcomandy.so.gz"</string>
<string name="complete">已完结</string>
<string name="TRANSPORT_WIFI">WIFI</string>
@@ -151,6 +154,8 @@
<string name="settings_cat_net_sm_use_cellar">打开后不再在开始阅读时提示</string>
<string name="settings_cat_net_sw_use_foreign">使用海外线路</string>
<string name="settings_cat_net_sm_use_foreign">不管使用什么线路, API访问均是海外, 只有图片CDN可能会变化也可能不变, 请酌情选择使用</string>
<string name="settings_cat_net_sw_use_comandy">增强型数据访问</string>
<string name="settings_cat_net_sm_use_comandy">使用经过优化的请求方法访问服务器</string>
<string name="settings_cat_net_et_title_api_url">请求API网址</string>
<string name="settings_cat_net_et_summary_api_url">一般无需更改,除非拷贝漫画官方更改网址,默认:&hosturl;</string>
<string name="settings_cat_net_sw_use_api_proxy">使用API代理重启生效</string>
@@ -175,6 +180,8 @@
<string name="settings_cat_md">漫画下载</string>
<string name="settings_cat_md_sw_show_0m_manga">显示未下载漫画</string>
<string name="settings_cat_md_sm_show_0m_manga">打开后将在我的下载显示所有浏览过详情页的漫画,旧版下载永远全部显示</string>
<string name="settings_cat_md_sb_max_batch">漫画下载并发限制</string>
<string name="settings_cat_md_sm_max_batch">默认为16</string>
<string name="login_null_username">用户名为空</string>
<string name="login_null_pwd">密码为空</string>

View File

@@ -44,6 +44,12 @@
<PreferenceCategory
app:iconSpaceReserved="false"
app:title="@string/settings_cat_net">
<SwitchPreferenceCompat
app:iconSpaceReserved="false"
app:key="settings_cat_net_sw_use_comandy"
app:selectable="true"
app:summary="@string/settings_cat_net_sm_use_comandy"
app:title="@string/settings_cat_net_sw_use_comandy" />
<ListPreference
android:max="1500"
app:iconSpaceReserved="false"
@@ -146,5 +152,14 @@
app:selectable="true"
app:summary="@string/settings_cat_md_sm_show_0m_manga"
app:title="@string/settings_cat_md_sw_show_0m_manga" />
<SeekBarPreference
android:defaultValue="16"
android:max="32"
app:iconSpaceReserved="false"
app:key="settings_cat_md_sb_max_batch"
app:min="1"
app:showSeekBarValue="true"
app:summary="@string/settings_cat_md_sm_max_batch"
app:title="@string/settings_cat_md_sb_max_batch" />
</PreferenceCategory>
</PreferenceScreen>

View File

@@ -10,7 +10,7 @@ buildscript {
maven { url "https://jitpack.io" }
}
dependencies {
classpath 'com.android.tools.build:gradle:8.2.2'
classpath 'com.android.tools.build:gradle:8.3.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong

View File

@@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip