1
0
mirror of https://github.com/fumiama/copymanga.git synced 2026-07-17 10:56:24 +08:00

optimize: improve chapter reader streaming and download reliability (#200)

This commit is contained in:
xuanli20
2026-07-13 17:29:14 +08:00
committed by GitHub
parent dadcb1e729
commit f0b0189c9e
11 changed files with 622 additions and 117 deletions

View File

@@ -12,44 +12,142 @@ if (typeof (loaded) == "undefined") {
return chapterArr;
}
function smoothLoadChapter(speed, interval) {
let prevHeight = document.body.scrollHeight;
let lastTime = 0;
let ticking = false;
let lastHeight = 0;
let lastImageCount = 0;
let stableCount = 0;
let finished = false;
let declaredCount = 0;
let lastProgressNotify = 0;
let lastPushNotify = 0;
const pushed = Object.create(null);
const startTime = Date.now();
function getTextByClass(name) {
const item = document.getElementsByClassName(name)[0];
return item ? item.innerText : "0";
}
function getImages() {
const content = document.getElementsByClassName("container-fluid comicContent")[0];
return content ? content.getElementsByTagName("li") : [];
}
function safeHref(className, index) {
try {
const items = document.getElementsByClassName(className);
const links = items[index].getElementsByTagName("a");
return links[0].href;
} catch(e) {
return "null";
}
}
function getChapterHeader() {
var nextChapter = safeHref("comicContent-next", 0);
var prevChapter = safeHref("comicContent-prev", 1);
if(nextChapter == location.href) nextChapter = "null";
if(prevChapter == location.href) prevChapter = "null";
return document.title.split(" - ")[1] + " " + location.href.substring(location.href.lastIndexOf("/")+1) + "\n" + nextChapter + "\n" + prevChapter;
}
function notifyMeta() {
try { GM.setChapterMeta(getChapterHeader()); } catch(e) {}
}
function imageSrcOf(li) {
const img = li.getElementsByTagName("img")[0];
if (!img) return "";
return img.dataset.src || img.getAttribute("data-src") || img.src || "";
}
function notifyCount(count) {
if (count > 0 && count !== declaredCount) {
declaredCount = count;
try { GM.setChapterCount(count.toString()); } catch(e) {}
}
}
function pushNewImages(force) {
const images = getImages();
const batch = [];
for(var i = 0; i < images.length; i++) {
const src = imageSrcOf(images[i]);
if (src && !pushed[src]) {
pushed[src] = true;
batch.push(src);
}
}
if (batch.length > 0) {
try { GM.appendChapterImages(batch.join("\n")); } catch(e) {}
} else if (force) {
try { GM.appendChapterImages(""); } catch(e) {}
}
}
function finish() {
if (finished) return;
finished = true;
notifyMeta();
pushNewImages(true);
var images = getImages();
var result = getChapterHeader();
for(var i = 0; i < images.length; i++) {
var src = imageSrcOf(images[i]);
if (src) result += "\n" + src;
}
try { GM.finishStreamingChapter(); } catch(e) {}
GM.setLoadingDialog(false);
GM.loadChapter(result);
}
function requestTick() {
if (!ticking) {
if (!ticking && !finished) {
ticking = true;
requestAnimationFrame(step);
}
}
function step(timestamp) {
if (!lastTime) lastTime = timestamp;
const elapsed = timestamp - lastTime;
if (elapsed >= interval) {
const index = document.getElementsByClassName("comicIndex")[0].innerText;
const count = document.getElementsByClassName("comicCount")[0].innerText;
GM.setLoadingDialogProgress(index, count);
const index = parseInt(getTextByClass("comicIndex")) || 0;
const count = parseInt(getTextByClass("comicCount")) || 0;
const images = getImages();
notifyCount(count);
if (timestamp - lastProgressNotify >= 200) {
lastProgressNotify = timestamp;
try { GM.setLoadingDialogProgress(index.toString(), count.toString()); } catch(e) {}
}
if (timestamp - lastPushNotify >= 80) {
lastPushNotify = timestamp;
pushNewImages(false);
}
window.scrollBy(0, speed);
lastTime = timestamp;
const currentHeight = document.body.scrollHeight;
if (Math.round(window.innerHeight+window.scrollY+0.5) >= currentHeight) { /*避免小数不符无法触发*/
if (currentHeight === prevHeight) {
var images = document.getElementsByClassName("container-fluid comicContent")[0].getElementsByTagName("li");
var nextChapter = document.getElementsByClassName("comicContent-next")[0].getElementsByTagName("a")[0].href;
var prevChapter = document.getElementsByClassName("comicContent-prev")[1].getElementsByTagName("a")[0].href;
if(nextChapter == location.href) nextChapter = "null";
if(prevChapter == location.href) prevChapter = "null";
var result = document.title.split(" - ")[1] + " " + location.href.substring(location.href.lastIndexOf("/")+1) + "\n" + nextChapter + "\n" + prevChapter;
for(var i = 0; i < images.length; i++) result += "\n" + images[i].getElementsByTagName("img")[0].dataset.src;
GM.setLoadingDialog(false);
GM.loadChapter(result);
return;
}
prevHeight = currentHeight;
const height = document.body.scrollHeight;
const imageCount = images.length;
const atBottom = Math.round(window.innerHeight + window.scrollY + 1) >= height;
if (height === lastHeight && imageCount === lastImageCount) stableCount++;
else stableCount = 0;
lastHeight = height;
lastImageCount = imageCount;
if ((count > 0 && imageCount >= count && (index >= count || atBottom) && stableCount >= 2) ||
(atBottom && stableCount >= 10) ||
(Date.now() - startTime > 180000)) {
finish();
return;
}
}
ticking = false;
requestTick();
}
notifyMeta();
pushNewImages(false);
requestTick();
}
function modify() {

View File

@@ -19,6 +19,7 @@ import top.fumiama.copymangaweb.activity.template.ToolsBoxActivity
import top.fumiama.copymangaweb.data.ComicStructure
import top.fumiama.copymangaweb.databinding.ActivityDlBinding
import top.fumiama.copymangaweb.handler.DlHandler
import top.fumiama.copymangaweb.tool.InsetsTools
import top.fumiama.copymangaweb.tool.MangaDlTools
import top.fumiama.copymangaweb.tool.MangaDlTools.Companion.wmdlt
import top.fumiama.copymangaweb.view.ChapterToggleButton
@@ -51,13 +52,14 @@ class DlActivity : ToolsBoxActivity() {
super.onCreate(savedInstanceState)
mBinding = ActivityDlBinding.inflate(layoutInflater)
setContentView(mBinding.root)
InsetsTools.applySafeContentInsets(this, mBinding.root)
wm?.get()?.saveUrlsOnly = true
mangaDlTools = MangaDlTools(this)
mBinding.dwh.apply { post {
settings.userAgentString = getString(R.string.pc_ua)
webChromeClient = WebChromeClient()
setWebViewClient("h.js")
loadJSInterface(JSHidden())
loadJSInterface(JSHidden(onLoadChapter = { onHiddenChapterLoaded(it) }))
} }
handler.sendEmptyMessage(-2) //setLayouts
}
@@ -91,11 +93,16 @@ class DlActivity : ToolsBoxActivity() {
).start()
}
private fun fillChapters() {
private fun fillChapters(): Boolean {
mangaDlTools.allocateChapterUrls(checkedChapter)
var ok = true
for (i in tbtnlist) {
if (i.isChecked) mangaDlTools.dlChapterUrl(i.url.toString())
if (i.isChecked && !mangaDlTools.dlChapterUrl(i.url.toString())) {
ok = false
handler.obtainMessage(-1, i.index, 0).sendToTarget()
}
}
return ok && mangaDlTools.waitChapterUrlsReady()
}
private fun dlThread(dlMethod: (i: ChapterToggleButton) -> Unit) {
@@ -146,8 +153,13 @@ class DlActivity : ToolsBoxActivity() {
handler.sendEmptyMessage(9) //set dl card color to red
Toast.makeText(this@DlActivity, "请耐心等待加载...", Toast.LENGTH_SHORT).show()
Thread {
fillChapters()
dlThread { downloadChapterPages(it) }
if (fillChapters()) {
dlThread { downloadChapterPages(it) }
} else {
canDl = false
haveDlStarted = false
handler.sendEmptyMessage(8)
}
}.start()
}
}
@@ -200,6 +212,14 @@ class DlActivity : ToolsBoxActivity() {
if(!(jsonFile.exists() && intent.getBooleanExtra("callFromDlList", false))) json?.let { jsonFile.writeText(it) }
}
private fun onHiddenChapterLoaded(content: String) {
val listChapter = content.split('\n')
val hash = listChapter.getOrNull(0)?.substringAfterLast(' ')
if (hash.isNullOrBlank()) return
val imgs = listChapter.drop(3).filter { it.isNotBlank() }.toTypedArray()
mangaDlTools.setChapterImages(hash, imgs)
}
private fun downloadChapterPages(i: ChapterToggleButton) {
mangaDlTools.onDownloadedListener =
object : MangaDlTools.OnDownloadedListener {

View File

@@ -10,6 +10,7 @@ import android.widget.Toast
import top.fumiama.copymangaweb.R
import top.fumiama.copymangaweb.databinding.ActivityDlistBinding
import top.fumiama.copymangaweb.handler.DlLHandler
import top.fumiama.copymangaweb.tool.InsetsTools
import java.io.File
import java.util.regex.Pattern
import java.util.zip.ZipInputStream
@@ -23,6 +24,7 @@ class DlListActivity: Activity() {
super.onCreate(savedInstanceState)
mBinding = ActivityDlistBinding.inflate(layoutInflater)
setContentView(mBinding.root)
InsetsTools.applySafeContentInsets(this, mBinding.root)
mBinding.myt.ttitle.text = intent.getStringExtra("title")
handler = DlLHandler(Looper.myLooper()!!, this)
handler?.obtainMessage(3, currentDir)?.sendToTarget() //call scanFile

View File

@@ -20,6 +20,7 @@ import top.fumiama.copymangaweb.activity.template.ToolsBoxActivity
import top.fumiama.copymangaweb.activity.viewmodel.MainViewModel
import top.fumiama.copymangaweb.databinding.ActivityMainBinding
import top.fumiama.copymangaweb.handler.MainHandler
import top.fumiama.copymangaweb.tool.InsetsTools
import top.fumiama.copymangaweb.tool.MangaDlTools.Companion.wmdlt
import top.fumiama.copymangaweb.tool.SetDraggable
import top.fumiama.copymangaweb.tool.Updater
@@ -41,6 +42,7 @@ class MainActivity: ToolsBoxActivity() {
mBinding.mainViewModel = mViewModel
mBinding.lifecycleOwner = this
setContentView(mBinding.root)
InsetsTools.applySafeContentInsets(this, mBinding.root)
wm = WeakReference(this)
mh = MainHandler(Looper.myLooper()!!)
@@ -125,6 +127,16 @@ class MainActivity: ToolsBoxActivity() {
lifecycleScope.launch { mViewModel.updateLoadProgress(p) }
}
fun openStreamingManga(url: String) {
ViewMangaActivity.streamChapterUrl = url
ViewMangaActivity.titleText = "加载中..."
ViewMangaActivity.nextChapterUrl = null
ViewMangaActivity.previousChapterUrl = null
ViewMangaActivity.imgUrls = arrayOf()
ViewMangaActivity.zipFile = null
startActivity(Intent(this, ViewMangaActivity::class.java))
}
fun setFab(content: String) {
json = content
lifecycleScope.launch {

View File

@@ -28,10 +28,14 @@ import top.fumiama.copymangaweb.handler.TimeThread
import top.fumiama.copymangaweb.tool.PropertiesTools
import top.fumiama.copymangaweb.tool.ToolsBox
import top.fumiama.copymangaweb.view.ScaleImageView
import top.fumiama.copymangaweb.web.JSHidden
import top.fumiama.copymangaweb.web.WebChromeClient
import java.io.File
import java.lang.ref.WeakReference
import java.text.SimpleDateFormat
import java.util.Date
import kotlin.math.max
import kotlin.math.min
import java.util.zip.ZipFile
import java.util.zip.ZipInputStream
@@ -52,6 +56,12 @@ class ViewMangaActivity : ToolsBoxActivity() {
private var notUseVP = true
private var mangaZip = zipFile
val dlZip2View = mangaZip != null
private var streamUrl = streamChapterUrl
private var readerPrepared = false
private var streamFinished = false
private var streamDeclaredCount = 0
private val streamSeenUrls = LinkedHashSet<String>()
private var vpAdapter: RecyclerView.Adapter<ViewData>? = null
private val volTurnPage get() = p["volturn"] == "true"
var pageNum = 1
get() {
@@ -91,8 +101,9 @@ class ViewMangaActivity : ToolsBoxActivity() {
show()
}
mBinding.oneinfo.inftitle.ttitle.apply { post { text = titleText } }
Log.d("MyVM", "dlZip2View: $dlZip2View, mangaZip: $mangaZip")
Log.d("MyVM", "dlZip2View: $dlZip2View, mangaZip: $mangaZip, streamUrl: $streamUrl")
if(dlZip2View && mangaZip?.exists() != true) toolsBox.toastError("已经到头了~")
else if(!dlZip2View && !streamUrl.isNullOrBlank()) startStreamingCollector(streamUrl!!)
else Thread {
try {
count = if (dlZip2View) countZipItems() else imgUrls.size
@@ -100,25 +111,172 @@ class ViewMangaActivity : ToolsBoxActivity() {
e.printStackTrace()
runOnUiThread { toolsBox.toastError("分析图片url错误") }
}
runOnUiThread {
try {
prepareItems()
if(pn > 0) {
pageNum = pn
pn = -1
} else if(pn == -2){
pageNum = count
pn = -1
}
} catch (e: Exception) {
e.printStackTrace()
toolsBox.toastError("准备控件错误")
} finally {
dialog?.dismiss()
dialog = null
runOnUiThread { prepareReaderIfNeeded() }
}.start()
}
private fun startStreamingCollector(url: String) {
Log.d("CopymangaDL", "reader collector load url=$url")
mBinding.wcollector.apply { post {
settings.userAgentString = getString(R.string.pc_ua)
webChromeClient = WebChromeClient()
setWebViewClient("h.js")
loadJSInterface(
JSHidden(
onLoadChapter = { onStreamingChapterFinished(it) },
onChapterMeta = { onStreamingMeta(it) },
onAppendImages = { appendStreamingImages(it) },
onFinishStreaming = { onStreamingFinished() },
onChapterCount = { onStreamingCount(it) },
enableLoadingDialog = false
)
)
loadUrl(url)
} }
}
private fun onStreamingMeta(headerString: String) {
val lines = headerString.split('\n')
if (lines.size < 3) return
titleText = lines[0].substringBeforeLast(' ')
nextChapterUrl = lines[1].let { if(it == "null") null else it }
previousChapterUrl = lines[2].let { if(it == "null") null else it }
runOnUiThread { mBinding.oneinfo.inftitle.ttitle.text = titleText }
}
private fun setEarlyTapLayerEnabled(enabled: Boolean) {
mBinding.onec.apply { post {
isClickable = enabled
isFocusable = false
setOnClickListener(if (enabled) View.OnClickListener {
if (clicked) hideSettings() else showSettings()
} else null)
} }
}
private fun seekProgressToPage(progress: Int): Int {
if (count <= 1) return 1
return ((progress.coerceIn(0, 100) * (count - 1) + 50) / 100 + 1).coerceIn(1, count)
}
private fun onStreamingCount(total: Int) {
if (total <= 0) return
runOnUiThread {
val oldCount = count
streamDeclaredCount = max(streamDeclaredCount, total)
count = max(count, streamDeclaredCount)
if (!readerPrepared) {
prepareReaderIfNeeded()
} else {
if (!notUseVP && count > oldCount) {
vpAdapter?.notifyItemRangeInserted(oldCount, count - oldCount)
}
syncSeekBarOnly()
}
}
}
private fun onStreamingChapterFinished(content: String) {
val listChapter = content.split('\n')
if (listChapter.size >= 3) onStreamingMeta(listChapter.take(3).joinToString("\n"))
appendStreamingImages(listChapter.drop(3).filter { it.isNotBlank() }.toTypedArray())
onStreamingFinished()
}
private fun onStreamingFinished() {
streamFinished = true
Log.d("CopymangaDL", "reader stream finished pages=$count")
runOnUiThread {
dialog?.dismiss()
dialog = null
syncSeekBarOnly()
}
}
private fun appendStreamingImages(urls: Array<String>) {
runOnUiThread {
val oldCount = count
val oldImageCount = imgUrls.size
var added = 0
for (url in urls) {
if (streamSeenUrls.add(url)) {
imgUrls += url
added++
}
}
}.start()
if (added <= 0) return@runOnUiThread
count = max(max(streamDeclaredCount, imgUrls.size), count)
Log.d("CopymangaDL", "reader appended images added=$added total=$count images=${imgUrls.size}")
if (!readerPrepared) prepareReaderIfNeeded()
else {
if (notUseVP) {
if (currentItem in oldImageCount until imgUrls.size) loadOneImg(hidePanel = false)
else syncSeekBarOnly()
} else {
if (count > oldCount) vpAdapter?.notifyItemRangeInserted(oldCount, count - oldCount)
notifyVisiblePagesIfArrived(oldImageCount, imgUrls.size)
syncSeekBarOnly()
}
preloadAround(getLogicalCurrentItem())
}
}
}
private fun prepareReaderIfNeeded() {
try {
if (count <= 0) return
prepareItems()
if(pn > 0) {
pageNum = pn
pn = -1
} else if(pn == -2){
pageNum = count
pn = -1
}
} catch (e: Exception) {
e.printStackTrace()
toolsBox.toastError("准备控件错误")
} finally {
if (streamUrl == null || count > 0) {
dialog?.dismiss()
dialog = null
}
}
}
private fun preloadAround(position: Int) {
if (dlZip2View || position < 0 || imgUrls.isEmpty()) return
val end = min(imgUrls.size - 1, position + 4)
for (i in position + 1..end) {
imgUrls.getOrNull(i)?.let { url ->
Glide.with(this@ViewMangaActivity)
.load(toolsBox.resolution.wrap(url))
.preload()
}
}
}
private fun getLogicalCurrentItem(): Int {
return if (notUseVP) currentItem
else if (r2l) count - mBinding.vp.currentItem - 1
else mBinding.vp.currentItem
}
private fun logicalToPagerPosition(logicalPosition: Int): Int {
return if (r2l) count - logicalPosition - 1 else logicalPosition
}
private fun notifyVisiblePagesIfArrived(oldImageCount: Int, newImageCount: Int) {
if (notUseVP || oldImageCount >= newImageCount || count <= 0) return
val center = getLogicalCurrentItem().coerceIn(0, count - 1)
val from = max(0, center - 1)
val to = min(newImageCount - 1, center + 1)
for (logicalPosition in from..to) {
if (logicalPosition >= oldImageCount) {
val pagerPosition = logicalToPagerPosition(logicalPosition)
if (pagerPosition in 0 until count) vpAdapter?.notifyItemChanged(pagerPosition)
}
}
}
override fun onWindowFocusChanged(hasFocus: Boolean) {
@@ -155,20 +313,47 @@ class ViewMangaActivity : ToolsBoxActivity() {
private fun getImgBitmap(position: Int): Bitmap? {
if (position >= count || position < 0) return null
else {
val zip = ZipFile(mangaZip)
return BitmapFactory.decodeStream(zip.getInputStream(zip.getEntry("${position}.webp")))
val zipPath = mangaZip ?: return null
return try {
ZipFile(zipPath).use { zip ->
// Older downloaded zips use 0.webp, 1.webp...
// The large-chapter fix may use 000.webp, 001.webp... for stable sorting.
// Support both formats and fall back to the sorted entry list.
val entry = zip.getEntry("${position}.webp")
?: zip.getEntry("%03d.webp".format(position))
?: zip.entries().asSequence()
.filter { !it.isDirectory }
.sortedBy { it.name }
.elementAtOrNull(position)
?: run {
Log.e("CopymangaDL", "zip entry missing position=$position file=$zipPath")
return null
}
zip.getInputStream(entry).use { input ->
BitmapFactory.decodeStream(input)
}
}
} catch (e: Exception) {
Log.e("CopymangaDL", "decode zip image failed position=$position file=$zipPath", e)
null
}
}
private fun loadOneImg() {
private fun loadOneImg(hidePanel: Boolean = true) {
if(dlZip2View) mBinding.vone.onei.apply { post { setImageBitmap(getImgBitmap(currentItem)) } }
else Glide.with(this@ViewMangaActivity)
.load(toolsBox.resolution.wrap(imgUrls[currentItem]))
.placeholder(R.drawable.ic_dl)
.dontAnimate()
.into(mBinding.vone.onei)
updateSeekBar()
else {
val url = imgUrls.getOrNull(currentItem)
if (url.isNullOrBlank()) {
mBinding.vone.onei.apply { post { setImageResource(R.drawable.ic_dl) } }
} else {
Glide.with(this@ViewMangaActivity)
.load(toolsBox.resolution.wrap(url))
.placeholder(R.drawable.ic_dl)
.dontAnimate()
.into(mBinding.vone.onei)
}
}
updateSeekBar(hidePanel)
}
private fun setIdPosition(position: Int) {
@@ -178,6 +363,7 @@ class ViewMangaActivity : ToolsBoxActivity() {
@SuppressLint("SetTextI18n")
private fun prepareItems() {
if (count <= 0) return
prepareVP()
prepareInfoBar(count)
if (notUseVP) loadOneImg() else prepareIdBtVH()
@@ -185,6 +371,7 @@ class ViewMangaActivity : ToolsBoxActivity() {
prepareIdBtVolTurn()
prepareIdBtVP()
prepareIdBtLR()
readerPrepared = true
}
private fun prepareIdBtLR() {
@@ -216,9 +403,12 @@ class ViewMangaActivity : ToolsBoxActivity() {
} else {
mBinding.vp.apply { post {
visibility = View.VISIBLE
adapter = ViewData(this).RecyclerViewAdapter()
vpAdapter = ViewData(this).RecyclerViewAdapter()
adapter = vpAdapter
registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) {
val pos = if (r2l) count - position - 1 else position
preloadAround(pos)
updateSeekBar()
super.onPageSelected(position)
}
@@ -229,8 +419,12 @@ class ViewMangaActivity : ToolsBoxActivity() {
}
}
private fun updateSeekBar() {
if (!isInSeek) hideSettings()
private fun updateSeekBar(hidePanel: Boolean = true) {
if (hidePanel && !isInSeek) hideSettings()
syncSeekBarOnly()
}
private fun syncSeekBarOnly() {
updateSeekText()
updateSeekProgress()
}
@@ -242,9 +436,10 @@ class ViewMangaActivity : ToolsBoxActivity() {
visibility = View.INVISIBLE
setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(p0: SeekBar?, p1: Int, isHuman: Boolean) {
if (isHuman) {
if (p1 >= (pageNum + 1) * 100 / size) scrollForward()
else if (p1 < (pageNum - 1) * 100 / size) scrollBack()
if (isHuman && count > 0) {
val targetPage = seekProgressToPage(p1)
if (targetPage != pageNum) pageNum = targetPage
updateSeekText()
}
}
@@ -253,7 +448,12 @@ class ViewMangaActivity : ToolsBoxActivity() {
}
override fun onStopTrackingTouch(p0: SeekBar?) {
p0?.let {
val targetPage = seekProgressToPage(it.progress)
if (targetPage != pageNum) pageNum = targetPage
}
isInSeek = false
updateSeekBar()
}
})
} }
@@ -312,11 +512,12 @@ class ViewMangaActivity : ToolsBoxActivity() {
}
fun scrollBack() {
pageNum--
if (pageNum > 1) pageNum--
}
fun scrollForward() {
pageNum++
if (pageNum < count) pageNum++
else if (!streamFinished && streamUrl != null) Toast.makeText(this, "后续页面仍在后台加载", Toast.LENGTH_SHORT).show()
}
@SuppressLint("SetTextI18n")
@@ -325,7 +526,10 @@ class ViewMangaActivity : ToolsBoxActivity() {
}
private fun updateSeekProgress() {
mBinding.oneinfo.infseek.apply { post { progress = pageNum * 100 / count } }
if (isInSeek || count <= 0) return
mBinding.oneinfo.infseek.apply { post {
progress = if (count <= 1) 0 else ((pageNum - 1) * 100 / (count - 1)).coerceIn(0, 100)
} }
}
@Deprecated("Deprecated in Java")
@@ -338,6 +542,7 @@ class ViewMangaActivity : ToolsBoxActivity() {
override fun onDestroy() {
tt.canDo = false
handler.removeCallbacksAndMessages(null)
if (streamUrl != null) streamChapterUrl = null
super.onDestroy()
}
@@ -359,10 +564,13 @@ class ViewMangaActivity : ToolsBoxActivity() {
//Glide.with(this@ViewMangaActivity).load(it).placeholder(R.drawable.bg_comment).into(holder.itemView.onei)
oneImage.setImageBitmap(it)
}
else Glide.with(this@ViewMangaActivity)
.load(toolsBox.resolution.wrap(imgUrls[pos])).placeholder(R.drawable.ic_dl)
.dontAnimate().timeout(10000)
.into(oneImage)
else imgUrls.getOrNull(pos)?.let { url ->
Glide.with(this@ViewMangaActivity)
.load(toolsBox.resolution.wrap(url)).placeholder(R.drawable.ic_dl)
.dontAnimate().timeout(10000)
.into(oneImage)
preloadAround(pos)
} ?: oneImage.setImageResource(R.drawable.ic_dl)
}
}
@@ -466,5 +674,6 @@ class ViewMangaActivity : ToolsBoxActivity() {
var zipList: Array<String>? = null
var cd: File? = null
var pn = -1
var streamChapterUrl: String? = null
}
}

View File

@@ -0,0 +1,47 @@
package top.fumiama.copymangaweb.tool
import android.app.Activity
import android.view.View
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import kotlin.math.max
object InsetsTools {
fun applySafeContentInsets(activity: Activity, root: View) {
WindowCompat.setDecorFitsSystemWindows(activity.window, false)
val startLeft = root.paddingLeft
val startTop = root.paddingTop
val startRight = root.paddingRight
val startBottom = root.paddingBottom
ViewCompat.setOnApplyWindowInsetsListener(root) { view, insets ->
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
val displayCutout = insets.getInsets(WindowInsetsCompat.Type.displayCutout())
val captionBar = insets.getInsets(WindowInsetsCompat.Type.captionBar())
view.updatePadding(
left = startLeft + max(systemBars.left, displayCutout.left),
top = startTop + maxOf(systemBars.top, displayCutout.top, captionBar.top),
right = startRight + max(systemBars.right, displayCutout.right),
bottom = startBottom + max(systemBars.bottom, displayCutout.bottom)
)
insets
}
if (ViewCompat.isAttachedToWindow(root)) {
ViewCompat.requestApplyInsets(root)
} else {
root.addOnAttachStateChangeListener(object : View.OnAttachStateChangeListener {
override fun onViewAttachedToWindow(v: View) {
v.removeOnAttachStateChangeListener(this)
ViewCompat.requestApplyInsets(v)
}
override fun onViewDetachedFromWindow(v: View) = Unit
})
}
}
}

View File

@@ -1,11 +1,13 @@
package top.fumiama.copymangaweb.tool
import android.util.Log
import top.fumiama.copymangaweb.R
import top.fumiama.copymangaweb.activity.DlActivity
import java.io.File
import java.lang.Thread.sleep
import java.lang.ref.WeakReference
import java.util.concurrent.Semaphore
import java.util.concurrent.TimeUnit
import java.util.zip.CRC32
import java.util.zip.CheckedOutputStream
import java.util.zip.ZipEntry
@@ -25,7 +27,7 @@ class MangaDlTools(activity: DlActivity) {
}
fun getImgsCountByHash(hash: String): Int?{
return imgUrlsList?.get(p[hash].toInt())?.size
return p[hash].toIntOrNull()?.let { imgUrlsList?.getOrNull(it)?.size }
}
fun allocateChapterUrls(count: Int){
@@ -33,50 +35,118 @@ class MangaDlTools(activity: DlActivity) {
chaptersCount = 0
}
fun dlChapterUrl(url: String){
sem.acquire()
fun dlChapterUrl(url: String): Boolean {
Log.d("CopymangaDL", "waiting chapter semaphore url=$url")
if (!sem.tryAcquire(3, TimeUnit.MINUTES)) {
Log.e("CopymangaDL", "timeout waiting previous chapter url collection")
return false
}
var started = false
da.get()?.apply {
p[url.substringAfterLast("/")] = (chaptersCount++).toString()
Log.d("CopymangaDL", "load chapter url=$url hash=${url.substringAfterLast("/")}")
runOnUiThread { mBinding.dwh.apply { post { loadUrl(url) } } }
started = true
}
if (!started) sem.release()
return started
}
fun waitChapterUrlsReady(): Boolean {
Log.d("CopymangaDL", "waiting final chapter url collection")
return if (sem.tryAcquire(5, TimeUnit.MINUTES)) {
sem.release()
Log.d("CopymangaDL", "all chapter urls collected")
true
} else {
Log.e("CopymangaDL", "timeout waiting final chapter url collection")
false
}
}
fun setChapterImages(hash: String, imgUrls: Array<String>){
imgUrlsList?.set(p[hash].toInt(), imgUrls)
val index = p[hash].toIntOrNull()
if (index == null) {
Log.e("CopymangaDL", "missing chapter hash=$hash, images=${imgUrls.size}")
sem.release()
return
}
Log.d("CopymangaDL", "chapter images collected hash=$hash index=$index images=${imgUrls.size}")
imgUrlsList?.set(index, imgUrls)
sem.release()
}
fun dlChapterAndPackIntoZip(zipf: File, hash: String){
imgUrlsList?.get(p[hash].toInt())?.let { images ->
val dl = DownloadTools()
zipf.parentFile?.let { if (!it.exists()) it.mkdirs() }
if (zipf.exists()) zipf.delete()
zipf.createNewFile()
val zip = ZipOutputStream(CheckedOutputStream(zipf.outputStream(), CRC32()))
zip.setLevel(9)
var succeed = true
for (i in images.indices) {
zip.putNextEntry(ZipEntry("$i.webp"))
var tryTimes = 3
var s = false
while (!s && tryTimes-- > 0){
s = d?.toolsBox?.resolution?.wrap(images[i])?.let { u ->
dl.getHttpContent(u, d?.getString(R.string.web_home_www),
d?.getString(R.string.pc_ua)
)?.let { zip.write(it); true } ?: false
} ?: false
if (!s) {
onDownloadedListener?.handleMessage(i + 1)
sleep(2000)
val idx = p[hash].toIntOrNull()
val images = idx?.let { imgUrlsList?.getOrNull(it) }
if (images.isNullOrEmpty()) {
Log.e("CopymangaDL", "no images to zip hash=$hash idx=$idx")
onDownloadedListener?.handleMessage(false)
return
}
val dl = DownloadTools()
zipf.parentFile?.let { if (!it.exists()) it.mkdirs() }
val tmpZip = File(zipf.absolutePath + ".tmp")
if (tmpZip.exists()) tmpZip.delete()
Log.d("CopymangaDL", "start zip file=${zipf.absolutePath} pages=${images.size}")
var succeed = true
try {
ZipOutputStream(CheckedOutputStream(tmpZip.outputStream().buffered(), CRC32())).use { zip ->
zip.setLevel(9)
for (i in images.indices) {
if (i % 10 == 0) Log.d("CopymangaDL", "zipping page ${i + 1}/${images.size}")
var tryTimes = 3
var data: ByteArray? = null
while (data == null && tryTimes-- > 0) {
data = d?.toolsBox?.resolution?.wrap(images[i])?.let { u ->
dl.getHttpContent(
u,
d?.getString(R.string.web_home_www),
d?.getString(R.string.pc_ua)
)
}
if (data == null) {
Log.w("CopymangaDL", "retry page=${i + 1} left=$tryTimes url=${images[i]}")
onDownloadedListener?.handleMessage(i + 1)
sleep(2000)
}
}
if (data == null) {
succeed = false
Log.e("CopymangaDL", "download failed page=${i + 1} url=${images[i]}")
onDownloadedListener?.handleMessage(false, i + 1)
} else {
zip.putNextEntry(ZipEntry("%03d.webp".format(i)))
zip.write(data)
zip.closeEntry()
onDownloadedListener?.handleMessage(true, i + 1)
}
zip.flush()
if (exit) {
succeed = false
Log.w("CopymangaDL", "zip canceled")
break
}
}
if(!s && tryTimes <= 0) succeed = false
onDownloadedListener?.handleMessage(s, i + 1)
zip.flush()
if (exit) break
}
zip.close()
if (succeed) {
if (zipf.exists()) zipf.delete()
if (!tmpZip.renameTo(zipf)) {
tmpZip.copyTo(zipf, overwrite = true)
tmpZip.delete()
}
} else tmpZip.delete()
Log.d("CopymangaDL", "zip finished succeed=$succeed file=${zipf.absolutePath}")
onDownloadedListener?.handleMessage(succeed)
} catch (e: Exception) {
Log.e("CopymangaDL", "zip exception", e)
tmpZip.delete()
onDownloadedListener?.handleMessage(false)
}
}

View File

@@ -14,7 +14,8 @@ class JS {
else -> ""
}
Log.d("MyJS", "Load comic: $u")
wm?.get()?.loadHiddenUrl(u)
if (u.contains("/chapter/")) wm?.get()?.openStreamingManga(u)
else wm?.get()?.loadHiddenUrl(u)
}
@JavascriptInterface
fun hideFab() {

View File

@@ -7,11 +7,58 @@ import top.fumiama.copymangaweb.activity.MainActivity.Companion.mh
import top.fumiama.copymangaweb.activity.MainActivity.Companion.wm
import top.fumiama.copymangaweb.handler.MainHandler
class JSHidden {
class JSHidden(
private val onLoadChapter: ((String) -> Unit)? = null,
private val onChapterMeta: ((String) -> Unit)? = null,
private val onAppendImages: ((Array<String>) -> Unit)? = null,
private val onFinishStreaming: (() -> Unit)? = null,
private val onChapterCount: ((Int) -> Unit)? = null,
private val enableLoadingDialog: Boolean = true,
) {
@JavascriptInterface
fun loadChapter(listString: String){
wm?.get()?.callViewManga(listString)
Log.d("CopymangaDL", "JS loadChapter called, length=${listString.length}, lines=${listString.count { it == '\n' } + 1}")
val callback = onLoadChapter
if (callback != null) callback(listString)
else {
val main = wm?.get()
if (main == null) Log.e("CopymangaDL", "MainActivity reference lost, dropped chapter result")
else main.callViewManga(listString)
}
}
@JavascriptInterface
fun setChapterMeta(headerString: String) {
Log.d("CopymangaDL", "JS setChapterMeta called, length=${headerString.length}")
onChapterMeta?.invoke(headerString)
}
@JavascriptInterface
fun appendChapterImages(urlsString: String) {
val urls = urlsString
.split('\n')
.map { it.trim() }
.filter { it.isNotEmpty() }
.toTypedArray()
Log.d("CopymangaDL", "JS appendChapterImages called, images=${urls.size}")
if (urls.isNotEmpty()) onAppendImages?.invoke(urls)
}
@JavascriptInterface
fun setChapterCount(countString: String) {
val c = countString.toIntOrNull() ?: return
if (c > 0) {
Log.d("CopymangaDL", "JS setChapterCount called, count=$c")
onChapterCount?.invoke(c)
}
}
@JavascriptInterface
fun finishStreamingChapter() {
Log.d("CopymangaDL", "JS finishStreamingChapter called")
onFinishStreaming?.invoke()
}
@JavascriptInterface
fun setTitle(title:String){
Log.d("MyJSH", "Set title: $title")
@@ -23,10 +70,10 @@ class JSHidden {
}
@JavascriptInterface
fun setLoadingDialog(display: Boolean) {
mh?.sendEmptyMessage(if (display) MainHandler.SHOW_LOADING_DIALOG else MainHandler.HIDE_LOADING_DIALOG)
if (enableLoadingDialog) mh?.sendEmptyMessage(if (display) MainHandler.SHOW_LOADING_DIALOG else MainHandler.HIDE_LOADING_DIALOG)
}
@JavascriptInterface
fun setLoadingDialogProgress(index: String, count: String) {
mh?.obtainMessage(MainHandler.SET_LOADING_DIALOG_TEXT, "$index/$count")?.sendToTarget()
if (enableLoadingDialog) mh?.obtainMessage(MainHandler.SET_LOADING_DIALOG_TEXT, "$index/$count")?.sendToTarget()
}
}

View File

@@ -2,19 +2,15 @@ package top.fumiama.copymangaweb.web
import android.content.Context
import android.graphics.Bitmap
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.Toast
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import top.fumiama.copymangaweb.R
import top.fumiama.copymangaweb.activity.MainActivity.Companion.wm
class WebViewClient(private val context: Context, jsFileName: String):WebViewClient() {
private val js = context.assets.open(jsFileName).readBytes().decodeToString()
@@ -30,16 +26,11 @@ class WebViewClient(private val context: Context, jsFileName: String):WebViewCli
}
override fun onPageFinished(view: WebView?, url: String?) {
wm?.get()?.lifecycleScope?.launch {
withContext(Dispatchers.IO) {
delay(500)
withContext(Dispatchers.Main) {
view?.loadUrl(js)
Log.d("MyWC", "Inject JS into: $url")
super.onPageFinished(view, url)
}
}
}
Handler(Looper.getMainLooper()).postDelayed({
view?.loadUrl(js)
Log.d("MyWC", "Inject JS into: $url")
}, 500)
super.onPageFinished(view, url)
}
override fun shouldInterceptRequest(

View File

@@ -6,6 +6,14 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<top.fumiama.copymangaweb.view.JSWebView
android:id="@+id/wcollector"
android:layout_width="1dp"
android:layout_height="1dp"
android:alpha="0.01"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<include
android:id="@+id/vone"
layout="@layout/page_imgview"