diff --git a/app/src/main/assets/h.js b/app/src/main/assets/h.js index df6922e..7c1dc7b 100644 --- a/app/src/main/assets/h.js +++ b/app/src/main/assets/h.js @@ -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() { diff --git a/app/src/main/java/top/fumiama/copymangaweb/activity/DlActivity.kt b/app/src/main/java/top/fumiama/copymangaweb/activity/DlActivity.kt index 19cfce3..837f071 100644 --- a/app/src/main/java/top/fumiama/copymangaweb/activity/DlActivity.kt +++ b/app/src/main/java/top/fumiama/copymangaweb/activity/DlActivity.kt @@ -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 { diff --git a/app/src/main/java/top/fumiama/copymangaweb/activity/DlListActivity.kt b/app/src/main/java/top/fumiama/copymangaweb/activity/DlListActivity.kt index 08afbf4..b090aab 100644 --- a/app/src/main/java/top/fumiama/copymangaweb/activity/DlListActivity.kt +++ b/app/src/main/java/top/fumiama/copymangaweb/activity/DlListActivity.kt @@ -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 diff --git a/app/src/main/java/top/fumiama/copymangaweb/activity/MainActivity.kt b/app/src/main/java/top/fumiama/copymangaweb/activity/MainActivity.kt index eeef245..384bfcf 100644 --- a/app/src/main/java/top/fumiama/copymangaweb/activity/MainActivity.kt +++ b/app/src/main/java/top/fumiama/copymangaweb/activity/MainActivity.kt @@ -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 { diff --git a/app/src/main/java/top/fumiama/copymangaweb/activity/ViewMangaActivity.kt b/app/src/main/java/top/fumiama/copymangaweb/activity/ViewMangaActivity.kt index 8a566a1..91b5f64 100644 --- a/app/src/main/java/top/fumiama/copymangaweb/activity/ViewMangaActivity.kt +++ b/app/src/main/java/top/fumiama/copymangaweb/activity/ViewMangaActivity.kt @@ -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() + private var vpAdapter: RecyclerView.Adapter? = 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) { + 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? = null var cd: File? = null var pn = -1 + var streamChapterUrl: String? = null } } \ No newline at end of file diff --git a/app/src/main/java/top/fumiama/copymangaweb/tool/InsetsTools.kt b/app/src/main/java/top/fumiama/copymangaweb/tool/InsetsTools.kt new file mode 100644 index 0000000..2893d47 --- /dev/null +++ b/app/src/main/java/top/fumiama/copymangaweb/tool/InsetsTools.kt @@ -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 + }) + } + } +} diff --git a/app/src/main/java/top/fumiama/copymangaweb/tool/MangaDlTools.kt b/app/src/main/java/top/fumiama/copymangaweb/tool/MangaDlTools.kt index 48c8398..9657590 100644 --- a/app/src/main/java/top/fumiama/copymangaweb/tool/MangaDlTools.kt +++ b/app/src/main/java/top/fumiama/copymangaweb/tool/MangaDlTools.kt @@ -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){ - 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) } } diff --git a/app/src/main/java/top/fumiama/copymangaweb/web/JS.kt b/app/src/main/java/top/fumiama/copymangaweb/web/JS.kt index 2ae5933..3254f46 100644 --- a/app/src/main/java/top/fumiama/copymangaweb/web/JS.kt +++ b/app/src/main/java/top/fumiama/copymangaweb/web/JS.kt @@ -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() { diff --git a/app/src/main/java/top/fumiama/copymangaweb/web/JSHidden.kt b/app/src/main/java/top/fumiama/copymangaweb/web/JSHidden.kt index 7fa60f3..881227d 100644 --- a/app/src/main/java/top/fumiama/copymangaweb/web/JSHidden.kt +++ b/app/src/main/java/top/fumiama/copymangaweb/web/JSHidden.kt @@ -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) -> 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() } } \ No newline at end of file diff --git a/app/src/main/java/top/fumiama/copymangaweb/web/WebViewClient.kt b/app/src/main/java/top/fumiama/copymangaweb/web/WebViewClient.kt index 04a5f12..88f767e 100644 --- a/app/src/main/java/top/fumiama/copymangaweb/web/WebViewClient.kt +++ b/app/src/main/java/top/fumiama/copymangaweb/web/WebViewClient.kt @@ -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( diff --git a/app/src/main/res/layout/activity_viewmanga.xml b/app/src/main/res/layout/activity_viewmanga.xml index 960c6e0..4946aea 100644 --- a/app/src/main/res/layout/activity_viewmanga.xml +++ b/app/src/main/res/layout/activity_viewmanga.xml @@ -6,6 +6,14 @@ android:layout_width="match_parent" android:layout_height="match_parent"> + +