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; return chapterArr;
} }
function smoothLoadChapter(speed, interval) { function smoothLoadChapter(speed, interval) {
let prevHeight = document.body.scrollHeight;
let lastTime = 0; let lastTime = 0;
let ticking = false; 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() { function requestTick() {
if (!ticking) { if (!ticking && !finished) {
ticking = true; ticking = true;
requestAnimationFrame(step); requestAnimationFrame(step);
} }
} }
function step(timestamp) { function step(timestamp) {
if (!lastTime) lastTime = timestamp; if (!lastTime) lastTime = timestamp;
const elapsed = timestamp - lastTime; const elapsed = timestamp - lastTime;
if (elapsed >= interval) { if (elapsed >= interval) {
const index = document.getElementsByClassName("comicIndex")[0].innerText; const index = parseInt(getTextByClass("comicIndex")) || 0;
const count = document.getElementsByClassName("comicCount")[0].innerText; const count = parseInt(getTextByClass("comicCount")) || 0;
GM.setLoadingDialogProgress(index, count); 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); window.scrollBy(0, speed);
lastTime = timestamp; lastTime = timestamp;
const currentHeight = document.body.scrollHeight;
if (Math.round(window.innerHeight+window.scrollY+0.5) >= currentHeight) { /*避免小数不符无法触发*/ const height = document.body.scrollHeight;
if (currentHeight === prevHeight) { const imageCount = images.length;
var images = document.getElementsByClassName("container-fluid comicContent")[0].getElementsByTagName("li"); const atBottom = Math.round(window.innerHeight + window.scrollY + 1) >= height;
var nextChapter = document.getElementsByClassName("comicContent-next")[0].getElementsByTagName("a")[0].href; if (height === lastHeight && imageCount === lastImageCount) stableCount++;
var prevChapter = document.getElementsByClassName("comicContent-prev")[1].getElementsByTagName("a")[0].href; else stableCount = 0;
if(nextChapter == location.href) nextChapter = "null"; lastHeight = height;
if(prevChapter == location.href) prevChapter = "null"; lastImageCount = imageCount;
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; if ((count > 0 && imageCount >= count && (index >= count || atBottom) && stableCount >= 2) ||
GM.setLoadingDialog(false); (atBottom && stableCount >= 10) ||
GM.loadChapter(result); (Date.now() - startTime > 180000)) {
finish();
return; return;
} }
prevHeight = currentHeight;
}
} }
ticking = false; ticking = false;
requestTick(); requestTick();
} }
notifyMeta();
pushNewImages(false);
requestTick(); requestTick();
} }
function modify() { 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.data.ComicStructure
import top.fumiama.copymangaweb.databinding.ActivityDlBinding import top.fumiama.copymangaweb.databinding.ActivityDlBinding
import top.fumiama.copymangaweb.handler.DlHandler import top.fumiama.copymangaweb.handler.DlHandler
import top.fumiama.copymangaweb.tool.InsetsTools
import top.fumiama.copymangaweb.tool.MangaDlTools import top.fumiama.copymangaweb.tool.MangaDlTools
import top.fumiama.copymangaweb.tool.MangaDlTools.Companion.wmdlt import top.fumiama.copymangaweb.tool.MangaDlTools.Companion.wmdlt
import top.fumiama.copymangaweb.view.ChapterToggleButton import top.fumiama.copymangaweb.view.ChapterToggleButton
@@ -51,13 +52,14 @@ class DlActivity : ToolsBoxActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
mBinding = ActivityDlBinding.inflate(layoutInflater) mBinding = ActivityDlBinding.inflate(layoutInflater)
setContentView(mBinding.root) setContentView(mBinding.root)
InsetsTools.applySafeContentInsets(this, mBinding.root)
wm?.get()?.saveUrlsOnly = true wm?.get()?.saveUrlsOnly = true
mangaDlTools = MangaDlTools(this) mangaDlTools = MangaDlTools(this)
mBinding.dwh.apply { post { mBinding.dwh.apply { post {
settings.userAgentString = getString(R.string.pc_ua) settings.userAgentString = getString(R.string.pc_ua)
webChromeClient = WebChromeClient() webChromeClient = WebChromeClient()
setWebViewClient("h.js") setWebViewClient("h.js")
loadJSInterface(JSHidden()) loadJSInterface(JSHidden(onLoadChapter = { onHiddenChapterLoaded(it) }))
} } } }
handler.sendEmptyMessage(-2) //setLayouts handler.sendEmptyMessage(-2) //setLayouts
} }
@@ -91,12 +93,17 @@ class DlActivity : ToolsBoxActivity() {
).start() ).start()
} }
private fun fillChapters() { private fun fillChapters(): Boolean {
mangaDlTools.allocateChapterUrls(checkedChapter) mangaDlTools.allocateChapterUrls(checkedChapter)
var ok = true
for (i in tbtnlist) { 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) { private fun dlThread(dlMethod: (i: ChapterToggleButton) -> Unit) {
sleep(10000) sleep(10000)
@@ -146,8 +153,13 @@ class DlActivity : ToolsBoxActivity() {
handler.sendEmptyMessage(9) //set dl card color to red handler.sendEmptyMessage(9) //set dl card color to red
Toast.makeText(this@DlActivity, "请耐心等待加载...", Toast.LENGTH_SHORT).show() Toast.makeText(this@DlActivity, "请耐心等待加载...", Toast.LENGTH_SHORT).show()
Thread { Thread {
fillChapters() if (fillChapters()) {
dlThread { downloadChapterPages(it) } dlThread { downloadChapterPages(it) }
} else {
canDl = false
haveDlStarted = false
handler.sendEmptyMessage(8)
}
}.start() }.start()
} }
} }
@@ -200,6 +212,14 @@ class DlActivity : ToolsBoxActivity() {
if(!(jsonFile.exists() && intent.getBooleanExtra("callFromDlList", false))) json?.let { jsonFile.writeText(it) } 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) { private fun downloadChapterPages(i: ChapterToggleButton) {
mangaDlTools.onDownloadedListener = mangaDlTools.onDownloadedListener =
object : MangaDlTools.OnDownloadedListener { object : MangaDlTools.OnDownloadedListener {

View File

@@ -10,6 +10,7 @@ import android.widget.Toast
import top.fumiama.copymangaweb.R import top.fumiama.copymangaweb.R
import top.fumiama.copymangaweb.databinding.ActivityDlistBinding import top.fumiama.copymangaweb.databinding.ActivityDlistBinding
import top.fumiama.copymangaweb.handler.DlLHandler import top.fumiama.copymangaweb.handler.DlLHandler
import top.fumiama.copymangaweb.tool.InsetsTools
import java.io.File import java.io.File
import java.util.regex.Pattern import java.util.regex.Pattern
import java.util.zip.ZipInputStream import java.util.zip.ZipInputStream
@@ -23,6 +24,7 @@ class DlListActivity: Activity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
mBinding = ActivityDlistBinding.inflate(layoutInflater) mBinding = ActivityDlistBinding.inflate(layoutInflater)
setContentView(mBinding.root) setContentView(mBinding.root)
InsetsTools.applySafeContentInsets(this, mBinding.root)
mBinding.myt.ttitle.text = intent.getStringExtra("title") mBinding.myt.ttitle.text = intent.getStringExtra("title")
handler = DlLHandler(Looper.myLooper()!!, this) handler = DlLHandler(Looper.myLooper()!!, this)
handler?.obtainMessage(3, currentDir)?.sendToTarget() //call scanFile 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.activity.viewmodel.MainViewModel
import top.fumiama.copymangaweb.databinding.ActivityMainBinding import top.fumiama.copymangaweb.databinding.ActivityMainBinding
import top.fumiama.copymangaweb.handler.MainHandler 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.MangaDlTools.Companion.wmdlt
import top.fumiama.copymangaweb.tool.SetDraggable import top.fumiama.copymangaweb.tool.SetDraggable
import top.fumiama.copymangaweb.tool.Updater import top.fumiama.copymangaweb.tool.Updater
@@ -41,6 +42,7 @@ class MainActivity: ToolsBoxActivity() {
mBinding.mainViewModel = mViewModel mBinding.mainViewModel = mViewModel
mBinding.lifecycleOwner = this mBinding.lifecycleOwner = this
setContentView(mBinding.root) setContentView(mBinding.root)
InsetsTools.applySafeContentInsets(this, mBinding.root)
wm = WeakReference(this) wm = WeakReference(this)
mh = MainHandler(Looper.myLooper()!!) mh = MainHandler(Looper.myLooper()!!)
@@ -125,6 +127,16 @@ class MainActivity: ToolsBoxActivity() {
lifecycleScope.launch { mViewModel.updateLoadProgress(p) } 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) { fun setFab(content: String) {
json = content json = content
lifecycleScope.launch { 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.PropertiesTools
import top.fumiama.copymangaweb.tool.ToolsBox import top.fumiama.copymangaweb.tool.ToolsBox
import top.fumiama.copymangaweb.view.ScaleImageView import top.fumiama.copymangaweb.view.ScaleImageView
import top.fumiama.copymangaweb.web.JSHidden
import top.fumiama.copymangaweb.web.WebChromeClient
import java.io.File import java.io.File
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import kotlin.math.max
import kotlin.math.min
import java.util.zip.ZipFile import java.util.zip.ZipFile
import java.util.zip.ZipInputStream import java.util.zip.ZipInputStream
@@ -52,6 +56,12 @@ class ViewMangaActivity : ToolsBoxActivity() {
private var notUseVP = true private var notUseVP = true
private var mangaZip = zipFile private var mangaZip = zipFile
val dlZip2View = mangaZip != null 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" private val volTurnPage get() = p["volturn"] == "true"
var pageNum = 1 var pageNum = 1
get() { get() {
@@ -91,8 +101,9 @@ class ViewMangaActivity : ToolsBoxActivity() {
show() show()
} }
mBinding.oneinfo.inftitle.ttitle.apply { post { text = titleText } } 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("已经到头了~") if(dlZip2View && mangaZip?.exists() != true) toolsBox.toastError("已经到头了~")
else if(!dlZip2View && !streamUrl.isNullOrBlank()) startStreamingCollector(streamUrl!!)
else Thread { else Thread {
try { try {
count = if (dlZip2View) countZipItems() else imgUrls.size count = if (dlZip2View) countZipItems() else imgUrls.size
@@ -100,8 +111,120 @@ class ViewMangaActivity : ToolsBoxActivity() {
e.printStackTrace() e.printStackTrace()
runOnUiThread { toolsBox.toastError("分析图片url错误") } runOnUiThread { toolsBox.toastError("分析图片url错误") }
} }
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 { 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++
}
}
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 { try {
if (count <= 0) return
prepareItems() prepareItems()
if(pn > 0) { if(pn > 0) {
pageNum = pn pageNum = pn
@@ -114,11 +237,46 @@ class ViewMangaActivity : ToolsBoxActivity() {
e.printStackTrace() e.printStackTrace()
toolsBox.toastError("准备控件错误") toolsBox.toastError("准备控件错误")
} finally { } finally {
if (streamUrl == null || count > 0) {
dialog?.dismiss() dialog?.dismiss()
dialog = null dialog = null
} }
} }
}.start() }
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) { override fun onWindowFocusChanged(hasFocus: Boolean) {
@@ -155,20 +313,47 @@ class ViewMangaActivity : ToolsBoxActivity() {
private fun getImgBitmap(position: Int): Bitmap? { private fun getImgBitmap(position: Int): Bitmap? {
if (position >= count || position < 0) return null if (position >= count || position < 0) return null
else { val zipPath = mangaZip ?: return null
val zip = ZipFile(mangaZip) return try {
return BitmapFactory.decodeStream(zip.getInputStream(zip.getEntry("${position}.webp"))) 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)) } } if(dlZip2View) mBinding.vone.onei.apply { post { setImageBitmap(getImgBitmap(currentItem)) } }
else Glide.with(this@ViewMangaActivity) else {
.load(toolsBox.resolution.wrap(imgUrls[currentItem])) 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) .placeholder(R.drawable.ic_dl)
.dontAnimate() .dontAnimate()
.into(mBinding.vone.onei) .into(mBinding.vone.onei)
updateSeekBar() }
}
updateSeekBar(hidePanel)
} }
private fun setIdPosition(position: Int) { private fun setIdPosition(position: Int) {
@@ -178,6 +363,7 @@ class ViewMangaActivity : ToolsBoxActivity() {
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
private fun prepareItems() { private fun prepareItems() {
if (count <= 0) return
prepareVP() prepareVP()
prepareInfoBar(count) prepareInfoBar(count)
if (notUseVP) loadOneImg() else prepareIdBtVH() if (notUseVP) loadOneImg() else prepareIdBtVH()
@@ -185,6 +371,7 @@ class ViewMangaActivity : ToolsBoxActivity() {
prepareIdBtVolTurn() prepareIdBtVolTurn()
prepareIdBtVP() prepareIdBtVP()
prepareIdBtLR() prepareIdBtLR()
readerPrepared = true
} }
private fun prepareIdBtLR() { private fun prepareIdBtLR() {
@@ -216,9 +403,12 @@ class ViewMangaActivity : ToolsBoxActivity() {
} else { } else {
mBinding.vp.apply { post { mBinding.vp.apply { post {
visibility = View.VISIBLE visibility = View.VISIBLE
adapter = ViewData(this).RecyclerViewAdapter() vpAdapter = ViewData(this).RecyclerViewAdapter()
adapter = vpAdapter
registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageSelected(position: Int) { override fun onPageSelected(position: Int) {
val pos = if (r2l) count - position - 1 else position
preloadAround(pos)
updateSeekBar() updateSeekBar()
super.onPageSelected(position) super.onPageSelected(position)
} }
@@ -229,8 +419,12 @@ class ViewMangaActivity : ToolsBoxActivity() {
} }
} }
private fun updateSeekBar() { private fun updateSeekBar(hidePanel: Boolean = true) {
if (!isInSeek) hideSettings() if (hidePanel && !isInSeek) hideSettings()
syncSeekBarOnly()
}
private fun syncSeekBarOnly() {
updateSeekText() updateSeekText()
updateSeekProgress() updateSeekProgress()
} }
@@ -242,9 +436,10 @@ class ViewMangaActivity : ToolsBoxActivity() {
visibility = View.INVISIBLE visibility = View.INVISIBLE
setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(p0: SeekBar?, p1: Int, isHuman: Boolean) { override fun onProgressChanged(p0: SeekBar?, p1: Int, isHuman: Boolean) {
if (isHuman) { if (isHuman && count > 0) {
if (p1 >= (pageNum + 1) * 100 / size) scrollForward() val targetPage = seekProgressToPage(p1)
else if (p1 < (pageNum - 1) * 100 / size) scrollBack() if (targetPage != pageNum) pageNum = targetPage
updateSeekText()
} }
} }
@@ -253,7 +448,12 @@ class ViewMangaActivity : ToolsBoxActivity() {
} }
override fun onStopTrackingTouch(p0: SeekBar?) { override fun onStopTrackingTouch(p0: SeekBar?) {
p0?.let {
val targetPage = seekProgressToPage(it.progress)
if (targetPage != pageNum) pageNum = targetPage
}
isInSeek = false isInSeek = false
updateSeekBar()
} }
}) })
} } } }
@@ -312,11 +512,12 @@ class ViewMangaActivity : ToolsBoxActivity() {
} }
fun scrollBack() { fun scrollBack() {
pageNum-- if (pageNum > 1) pageNum--
} }
fun scrollForward() { fun scrollForward() {
pageNum++ if (pageNum < count) pageNum++
else if (!streamFinished && streamUrl != null) Toast.makeText(this, "后续页面仍在后台加载", Toast.LENGTH_SHORT).show()
} }
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
@@ -325,7 +526,10 @@ class ViewMangaActivity : ToolsBoxActivity() {
} }
private fun updateSeekProgress() { 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") @Deprecated("Deprecated in Java")
@@ -338,6 +542,7 @@ class ViewMangaActivity : ToolsBoxActivity() {
override fun onDestroy() { override fun onDestroy() {
tt.canDo = false tt.canDo = false
handler.removeCallbacksAndMessages(null) handler.removeCallbacksAndMessages(null)
if (streamUrl != null) streamChapterUrl = null
super.onDestroy() super.onDestroy()
} }
@@ -359,10 +564,13 @@ class ViewMangaActivity : ToolsBoxActivity() {
//Glide.with(this@ViewMangaActivity).load(it).placeholder(R.drawable.bg_comment).into(holder.itemView.onei) //Glide.with(this@ViewMangaActivity).load(it).placeholder(R.drawable.bg_comment).into(holder.itemView.onei)
oneImage.setImageBitmap(it) oneImage.setImageBitmap(it)
} }
else Glide.with(this@ViewMangaActivity) else imgUrls.getOrNull(pos)?.let { url ->
.load(toolsBox.resolution.wrap(imgUrls[pos])).placeholder(R.drawable.ic_dl) Glide.with(this@ViewMangaActivity)
.load(toolsBox.resolution.wrap(url)).placeholder(R.drawable.ic_dl)
.dontAnimate().timeout(10000) .dontAnimate().timeout(10000)
.into(oneImage) .into(oneImage)
preloadAround(pos)
} ?: oneImage.setImageResource(R.drawable.ic_dl)
} }
} }
@@ -466,5 +674,6 @@ class ViewMangaActivity : ToolsBoxActivity() {
var zipList: Array<String>? = null var zipList: Array<String>? = null
var cd: File? = null var cd: File? = null
var pn = -1 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 package top.fumiama.copymangaweb.tool
import android.util.Log
import top.fumiama.copymangaweb.R import top.fumiama.copymangaweb.R
import top.fumiama.copymangaweb.activity.DlActivity import top.fumiama.copymangaweb.activity.DlActivity
import java.io.File import java.io.File
import java.lang.Thread.sleep import java.lang.Thread.sleep
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import java.util.concurrent.Semaphore import java.util.concurrent.Semaphore
import java.util.concurrent.TimeUnit
import java.util.zip.CRC32 import java.util.zip.CRC32
import java.util.zip.CheckedOutputStream import java.util.zip.CheckedOutputStream
import java.util.zip.ZipEntry import java.util.zip.ZipEntry
@@ -25,7 +27,7 @@ class MangaDlTools(activity: DlActivity) {
} }
fun getImgsCountByHash(hash: String): Int?{ 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){ fun allocateChapterUrls(count: Int){
@@ -33,50 +35,118 @@ class MangaDlTools(activity: DlActivity) {
chaptersCount = 0 chaptersCount = 0
} }
fun dlChapterUrl(url: String){ fun dlChapterUrl(url: String): Boolean {
sem.acquire() 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 { da.get()?.apply {
p[url.substringAfterLast("/")] = (chaptersCount++).toString() p[url.substringAfterLast("/")] = (chaptersCount++).toString()
Log.d("CopymangaDL", "load chapter url=$url hash=${url.substringAfterLast("/")}")
runOnUiThread { mBinding.dwh.apply { post { loadUrl(url) } } } 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>){ 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() sem.release()
} }
fun dlChapterAndPackIntoZip(zipf: File, hash: String){ fun dlChapterAndPackIntoZip(zipf: File, hash: String){
imgUrlsList?.get(p[hash].toInt())?.let { images -> 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() val dl = DownloadTools()
zipf.parentFile?.let { if (!it.exists()) it.mkdirs() } zipf.parentFile?.let { if (!it.exists()) it.mkdirs() }
if (zipf.exists()) zipf.delete() val tmpZip = File(zipf.absolutePath + ".tmp")
zipf.createNewFile() if (tmpZip.exists()) tmpZip.delete()
val zip = ZipOutputStream(CheckedOutputStream(zipf.outputStream(), CRC32()))
zip.setLevel(9) Log.d("CopymangaDL", "start zip file=${zipf.absolutePath} pages=${images.size}")
var succeed = true var succeed = true
try {
ZipOutputStream(CheckedOutputStream(tmpZip.outputStream().buffered(), CRC32())).use { zip ->
zip.setLevel(9)
for (i in images.indices) { for (i in images.indices) {
zip.putNextEntry(ZipEntry("$i.webp")) if (i % 10 == 0) Log.d("CopymangaDL", "zipping page ${i + 1}/${images.size}")
var tryTimes = 3 var tryTimes = 3
var s = false var data: ByteArray? = null
while (!s && tryTimes-- > 0){ while (data == null && tryTimes-- > 0) {
s = d?.toolsBox?.resolution?.wrap(images[i])?.let { u -> data = d?.toolsBox?.resolution?.wrap(images[i])?.let { u ->
dl.getHttpContent(u, d?.getString(R.string.web_home_www), dl.getHttpContent(
u,
d?.getString(R.string.web_home_www),
d?.getString(R.string.pc_ua) d?.getString(R.string.pc_ua)
)?.let { zip.write(it); true } ?: false )
} ?: false }
if (!s) { if (data == null) {
Log.w("CopymangaDL", "retry page=${i + 1} left=$tryTimes url=${images[i]}")
onDownloadedListener?.handleMessage(i + 1) onDownloadedListener?.handleMessage(i + 1)
sleep(2000) sleep(2000)
} }
} }
if(!s && tryTimes <= 0) succeed = false
onDownloadedListener?.handleMessage(s, i + 1) if (data == null) {
zip.flush() succeed = false
if (exit) break 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.close() zip.flush()
if (exit) {
succeed = false
Log.w("CopymangaDL", "zip canceled")
break
}
}
}
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) 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 -> "" else -> ""
} }
Log.d("MyJS", "Load comic: $u") Log.d("MyJS", "Load comic: $u")
wm?.get()?.loadHiddenUrl(u) if (u.contains("/chapter/")) wm?.get()?.openStreamingManga(u)
else wm?.get()?.loadHiddenUrl(u)
} }
@JavascriptInterface @JavascriptInterface
fun hideFab() { 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.activity.MainActivity.Companion.wm
import top.fumiama.copymangaweb.handler.MainHandler 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 @JavascriptInterface
fun loadChapter(listString: String){ 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 @JavascriptInterface
fun setTitle(title:String){ fun setTitle(title:String){
Log.d("MyJSH", "Set title: $title") Log.d("MyJSH", "Set title: $title")
@@ -23,10 +70,10 @@ class JSHidden {
} }
@JavascriptInterface @JavascriptInterface
fun setLoadingDialog(display: Boolean) { 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 @JavascriptInterface
fun setLoadingDialogProgress(index: String, count: String) { 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.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.os.Handler
import android.os.Looper
import android.util.Log import android.util.Log
import android.webkit.WebResourceRequest import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse import android.webkit.WebResourceResponse
import android.webkit.WebView import android.webkit.WebView
import android.webkit.WebViewClient import android.webkit.WebViewClient
import android.widget.Toast 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.R
import top.fumiama.copymangaweb.activity.MainActivity.Companion.wm
class WebViewClient(private val context: Context, jsFileName: String):WebViewClient() { class WebViewClient(private val context: Context, jsFileName: String):WebViewClient() {
private val js = context.assets.open(jsFileName).readBytes().decodeToString() private val js = context.assets.open(jsFileName).readBytes().decodeToString()
@@ -30,17 +26,12 @@ class WebViewClient(private val context: Context, jsFileName: String):WebViewCli
} }
override fun onPageFinished(view: WebView?, url: String?) { override fun onPageFinished(view: WebView?, url: String?) {
wm?.get()?.lifecycleScope?.launch { Handler(Looper.getMainLooper()).postDelayed({
withContext(Dispatchers.IO) {
delay(500)
withContext(Dispatchers.Main) {
view?.loadUrl(js) view?.loadUrl(js)
Log.d("MyWC", "Inject JS into: $url") Log.d("MyWC", "Inject JS into: $url")
}, 500)
super.onPageFinished(view, url) super.onPageFinished(view, url)
} }
}
}
}
override fun shouldInterceptRequest( override fun shouldInterceptRequest(
view: WebView?, view: WebView?,

View File

@@ -6,6 +6,14 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="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 <include
android:id="@+id/vone" android:id="@+id/vone"
layout="@layout/page_imgview" layout="@layout/page_imgview"