1
0
mirror of https://github.com/fumiama/copymanga.git synced 2026-07-17 10:56:24 +08:00
新增
1. 竖向翻页支持瀑布流
优化
1. 阅读器启动、章节图片流式收集与大章节下载稳定性 (#200) by @xuanli20
2. 同步到最新拷贝网址
3. 代码结构
修复
1. 高版本安卓下按返回键直接退出程序而非回退到上个页面
2. 音量键翻页无法切换到下一章节
This commit is contained in:
源文雨
2026-07-15 20:41:35 +08:00
parent f0b0189c9e
commit 85fb6b3834
23 changed files with 857 additions and 503 deletions

6
.idea/kotlinc.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="KotlinJpsPluginSettings">
<option name="version" value="2.1.0" />
</component>
</project>

View File

@@ -10,8 +10,8 @@ android {
applicationId "top.fumiama.copymangaweb" applicationId "top.fumiama.copymangaweb"
minSdkVersion 23 minSdkVersion 23
targetSdkVersion 36 targetSdkVersion 36
versionCode 16 versionCode 17
versionName '1.5.3' versionName '1.6.0'
resConfigs "zh", "zh-rCN" resConfigs "zh", "zh-rCN"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

View File

@@ -5,6 +5,7 @@
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
<application <application
android:allowBackup="true" android:allowBackup="true"
android:enableOnBackInvokedCallback="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher"

View File

@@ -2,15 +2,61 @@ javascript:
if (typeof (loaded) == "undefined") { if (typeof (loaded) == "undefined") {
var loaded = true; var loaded = true;
function scanChapters(chapter) { function scanChapters(chapter) {
var chapterList = chapter.getElementsByClassName("tab-pane fade show active")[0].getElementsByTagName("ul")[0].getElementsByTagName("a"); var activeTab = chapter.getElementsByClassName("tab-pane fade show active")[0];
var list = activeTab && activeTab.getElementsByTagName("ul")[0];
var chapterList = list ? list.getElementsByTagName("a") : [];
var chapterArr = Array(); var chapterArr = Array();
for (var i = 0; i < chapterList.length; i++) { for (var i = 0; i < chapterList.length; i++) {
chapterArr.push(JSON.constructor()); var name = chapterList[i].title || chapterList[i].innerText.trim();
chapterArr[i]["name"] = chapterList[i].title; var url = chapterList[i].href;
chapterArr[i]["url"] = chapterList[i].href; if (name && url) chapterArr.push({"name": name, "url": url});
} }
return chapterArr; return chapterArr;
} }
var detailScanVersion = 0;
function collectChapterGroups() {
var container = document.getElementsByClassName("upLoop")[0];
if (!container || container.children.length < 2 || container.children.length % 2) return null;
var groups = Array();
for (var i = 0; i < container.children.length; i += 2) {
var name = container.children[i].innerText.trim();
var chapters = scanChapters(container.children[i + 1]);
if (!name || !chapters.length) return null;
groups.push({"name": name, "chapters": chapters});
}
return groups.length ? groups : null;
}
function waitForChapterGroups() {
var version = ++detailScanVersion;
var sourceUrl = location.href;
var lastChapterCount = -1;
var stableChecks = 0;
var startedAt = Date.now();
function scan() {
if (version !== detailScanVersion || location.href !== sourceUrl) return;
var groups = collectChapterGroups();
if (groups) {
var chapterCount = groups.reduce(function (count, group) {
return count + group.chapters.length;
}, 0);
stableChecks = chapterCount === lastChapterCount ? stableChecks + 1 : 0;
lastChapterCount = chapterCount;
if (stableChecks >= 4) {
var titleElement = document.getElementsByTagName("h6")[0];
var comicTitle = titleElement && titleElement.title
? titleElement.title
: document.title;
GM.setFab(JSON.stringify(groups), sourceUrl, comicTitle);
return;
}
} else {
stableChecks = 0;
lastChapterCount = -1;
}
if (Date.now() - startedAt < 120000) setTimeout(scan, 250);
}
scan();
}
function smoothLoadChapter(speed, interval) { function smoothLoadChapter(speed, interval) {
let lastTime = 0; let lastTime = 0;
let ticking = false; let ticking = false;
@@ -156,22 +202,7 @@ if (typeof (loaded) == "undefined") {
GM.setLoadingDialog(true); GM.setLoadingDialog(true);
smoothLoadChapter(320, 16); smoothLoadChapter(320, 16);
} else { } else {
var json = Array(); waitForChapterGroups();
var chapters = document.getElementsByClassName("upLoop")[0].children;
var newObj = null;
for(var i = 0; i < chapters.length; i++) {
if(i % 2) {
newObj["chapters"] = scanChapters(chapters[i]);
json.push(newObj);
newObj = null;
}
else {
newObj = JSON.constructor();
newObj["name"] = chapters[i].innerText;
}
}
GM.setTitle(document.getElementsByTagName("h6")[0].title);
GM.setFab(JSON.stringify(json));
} }
} }
modify(); modify();

View File

@@ -61,13 +61,14 @@ class DlActivity : ToolsBoxActivity() {
setWebViewClient("h.js") setWebViewClient("h.js")
loadJSInterface(JSHidden(onLoadChapter = { onHiddenChapterLoaded(it) })) loadJSInterface(JSHidden(onLoadChapter = { onHiddenChapterLoaded(it) }))
} } } }
handler.sendEmptyMessage(-2) //setLayouts handler.sendEmptyMessage(DlHandler.INITIALIZE_LAYOUTS)
} }
override fun onDestroy() { override fun onDestroy() {
wm?.get()?.saveUrlsOnly = false wm?.get()?.saveUrlsOnly = false
wmdlt?.get()?.exit = true wmdlt?.get()?.exit = true
handler.removeCallbacksAndMessages(null) handler.removeCallbacksAndMessages(null)
mBinding.dwh.destroy()
super.onDestroy() super.onDestroy()
} }
@@ -99,7 +100,7 @@ class DlActivity : ToolsBoxActivity() {
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 ok = false
handler.obtainMessage(-1, i.index, 0).sendToTarget() handler.obtainMessage(DlHandler.CHAPTER_DOWNLOAD_FAILED, i.index, 0).sendToTarget()
} }
} }
return ok && mangaDlTools.waitChapterUrlsReady() return ok && mangaDlTools.waitChapterUrlsReady()
@@ -119,7 +120,7 @@ class DlActivity : ToolsBoxActivity() {
haveDlStarted = false haveDlStarted = false
canDl = false canDl = false
} }
handler.sendEmptyMessage(8) //set dl card color to blue handler.sendEmptyMessage(DlHandler.SET_DOWNLOAD_CARD_BLUE)
} }
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
@@ -150,7 +151,7 @@ class DlActivity : ToolsBoxActivity() {
else { else {
haveDlStarted = true haveDlStarted = true
canDl = true canDl = true
handler.sendEmptyMessage(9) //set dl card color to red handler.sendEmptyMessage(DlHandler.SET_DOWNLOAD_CARD_RED)
Toast.makeText(this@DlActivity, "请耐心等待加载...", Toast.LENGTH_SHORT).show() Toast.makeText(this@DlActivity, "请耐心等待加载...", Toast.LENGTH_SHORT).show()
Thread { Thread {
if (fillChapters()) { if (fillChapters()) {
@@ -158,14 +159,14 @@ class DlActivity : ToolsBoxActivity() {
} else { } else {
canDl = false canDl = false
haveDlStarted = false haveDlStarted = false
handler.sendEmptyMessage(8) handler.sendEmptyMessage(DlHandler.SET_DOWNLOAD_CARD_BLUE)
} }
}.start() }.start()
} }
} }
} }
it.setOnLongClickListener { it.setOnLongClickListener {
handler.sendEmptyMessage(4) handler.sendEmptyMessage(DlHandler.TOGGLE_SELECT_ALL)
return@setOnLongClickListener true return@setOnLongClickListener true
} }
} } } }
@@ -181,7 +182,7 @@ class DlActivity : ToolsBoxActivity() {
} }
private fun analyzeStructure() { private fun analyzeStructure() {
ViewMangaActivity.zipList = arrayOf() ViewMangaActivity.zipList = emptyArray()
Gson().fromJson(json?.reader(), Array<ComicStructure>::class.java)?.let { Gson().fromJson(json?.reader(), Array<ComicStructure>::class.java)?.let {
for (group in it) { for (group in it) {
val tc = layoutInflater.inflate(R.layout.line_caption, mBinding.ldwn, false) val tc = layoutInflater.inflate(R.layout.line_caption, mBinding.ldwn, false)
@@ -224,12 +225,17 @@ class DlActivity : ToolsBoxActivity() {
mangaDlTools.onDownloadedListener = mangaDlTools.onDownloadedListener =
object : MangaDlTools.OnDownloadedListener { object : MangaDlTools.OnDownloadedListener {
override fun handleMessage(succeed: Boolean) { override fun handleMessage(succeed: Boolean) {
handler.obtainMessage(if (succeed) 1 else -1, i.index, 0) handler.obtainMessage(
if (succeed) DlHandler.CHAPTER_DOWNLOAD_SUCCEEDED
else DlHandler.CHAPTER_DOWNLOAD_FAILED,
i.index,
0
)
.sendToTarget() .sendToTarget()
} }
override fun handleMessage(succeed: Boolean, pageNow: Int) { override fun handleMessage(succeed: Boolean, pageNow: Int) {
handler.obtainMessage( handler.obtainMessage(
5, DlHandler.PAGE_DOWNLOAD_FINISHED,
i.index, i.index,
pageNow, pageNow,
succeed succeed
@@ -237,7 +243,7 @@ class DlActivity : ToolsBoxActivity() {
} }
override fun handleMessage(pageNow: Int){ override fun handleMessage(pageNow: Int){
handler.obtainMessage( handler.obtainMessage(
10, DlHandler.PAGE_DOWNLOAD_RETRYING,
i.index, i.index,
pageNow pageNow
).sendToTarget() ).sendToTarget()
@@ -268,14 +274,19 @@ class DlActivity : ToolsBoxActivity() {
tbtnlist += tbvTbtn tbtnlist += tbvTbtn
tbvTbtn.url = url tbvTbtn.url = url
tbtncnt++ tbtncnt++
val zipPosition = ViewMangaActivity.zipList?.size
ViewMangaActivity.zipList = ViewMangaActivity.zipList?.plus("$title.zip")
tbvTbtn.textOff = title tbvTbtn.textOff = title
tbvTbtn.textOn = title tbvTbtn.textOn = title
tbvTbtn.text = title tbvTbtn.text = title
tbvTbtn.hint = caption tbvTbtn.hint = caption
tbvTbtn.layoutParams.width = btnw tbvTbtn.layoutParams.width = btnw
val zipFile = File("${getExternalFilesDir("")}/$comicName/$caption/$title.zip") val zipFile = File("${getExternalFilesDir("")}/$comicName/$caption/$title.zip")
val zipPosition = if (zipFile.exists()) {
ViewMangaActivity.zipList.orEmpty().size.also {
ViewMangaActivity.zipList = (
ViewMangaActivity.zipList?.toList().orEmpty() + zipFile
).toTypedArray()
}
} else null
if (zipFile.exists()) { if (zipFile.exists()) {
tbvTbtn.setBackgroundResource(R.drawable.rndbg_checked) tbvTbtn.setBackgroundResource(R.drawable.rndbg_checked)
tbvTbtn.isChecked = false tbvTbtn.isChecked = false
@@ -301,9 +312,7 @@ class DlActivity : ToolsBoxActivity() {
} else if(tbtn.isChecked) { } else if(tbtn.isChecked) {
tbtn.apply { post { tbtn.apply { post {
isChecked = false isChecked = false
zipPosition?.let { Thread { zipPosition?.let { callVM(title, zipFile, it) }
callVM(title, zipFile, it)
}.start() }
} } } }
} }
} }
@@ -321,7 +330,7 @@ class DlActivity : ToolsBoxActivity() {
text = "$dldChapter/${++checkedChapter}" text = "$dldChapter/${++checkedChapter}"
} } } }
} }
handler.sendEmptyMessage(7) handler.sendEmptyMessage(DlHandler.DELETE_SELECTED_CHAPTERS)
}) })
} }
true true
@@ -338,15 +347,16 @@ class DlActivity : ToolsBoxActivity() {
} }
} }
} }
handler.sendEmptyMessage(6) handler.sendEmptyMessage(DlHandler.UPDATE_CHAPTER_PROGRESS)
} }
private fun callVM(titleText: String, zipFile: File, zipPosition:Int) { private fun callVM(titleText: String, zipFile: File, zipPosition:Int) {
ViewMangaActivity.titleText = titleText ViewMangaActivity.titleText = titleText
ViewMangaActivity.zipFile = zipFile ViewMangaActivity.zipFile = zipFile
//ViewMangaActivity.zipList = zipArrayList
ViewMangaActivity.zipPosition = zipPosition ViewMangaActivity.zipPosition = zipPosition
ViewMangaActivity.cd = zipFile.parentFile ViewMangaActivity.cd = zipFile.parentFile
ViewMangaActivity.nextChapterUrl = null
ViewMangaActivity.previousChapterUrl = null
startActivity(Intent(this@DlActivity, ViewMangaActivity::class.java)) startActivity(Intent(this@DlActivity, ViewMangaActivity::class.java))
} }

View File

@@ -4,21 +4,21 @@ import android.app.Activity
import android.app.AlertDialog import android.app.AlertDialog
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.os.Looper import android.util.Log
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.Toast 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.tool.InsetsTools import top.fumiama.copymangaweb.tool.InsetsTools
import java.io.File import java.io.File
import java.util.concurrent.Executors
import java.util.regex.Pattern import java.util.regex.Pattern
import java.util.zip.ZipInputStream import java.util.zip.ZipInputStream
class DlListActivity: Activity() { class DlListActivity : Activity() {
private lateinit var mBinding: ActivityDlistBinding private lateinit var mBinding: ActivityDlistBinding
private var nullZipDirStr = emptyArray<String>() private var nullZipDirStr = emptyArray<String>()
private var handler: DlLHandler? = null private val fileExecutor = Executors.newSingleThreadExecutor()
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -26,79 +26,97 @@ class DlListActivity: Activity() {
setContentView(mBinding.root) setContentView(mBinding.root)
InsetsTools.applySafeContentInsets(this, 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) loadDirectory(currentDir)
handler?.obtainMessage(3, currentDir)?.sendToTarget() //call scanFile
} }
override fun onDestroy() { override fun onDestroy() {
fileExecutor.shutdownNow()
super.onDestroy() super.onDestroy()
handler?.removeCallbacksAndMessages(null)
handler = null
} }
fun scanFile(cd: File?){ private fun loadDirectory(directory: File?) {
val isRoot = cd == getExternalFilesDir("") fileExecutor.execute {
val jsonFile = File(cd, "info.bin") val isRoot = directory == getExternalFilesDir("")
if(isRoot || !jsonFile.exists()) cd?.list()?.sortedArrayWith { o1, o2 -> val jsonFile = File(directory, "info.bin")
if(o1.endsWith(".zip") && o2.endsWith(".zip")) (10000*getFloat(o1) - 10000*getFloat(o2) + 0.5).toInt() val entries = if (isRoot || !jsonFile.exists()) {
else o1[0] - o2[0] directory?.list()?.sortedWith(::compareFileNames)
}?.let { } else null
mBinding.mylv.adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, it) runOnUiThread {
mBinding.mylv.setOnItemClickListener { _, _, position, _ -> if (!isFinishing && !isDestroyed && entries != null) {
val chosenFile = File(cd, it[position]) showDirectory(directory, entries)
val chosenJson = File(chosenFile, "info.bin")
//Toast.makeText(this, "进入$chosenFile", Toast.LENGTH_SHORT).show()
when {
chosenJson.exists() -> callDownloadActivity(chosenJson)
chosenFile.isDirectory -> {
currentDir = chosenFile
startActivity(
Intent(
this,
DlListActivity::class.java
).putExtra("title", it[position])
)
}
chosenFile.name.endsWith(".zip") -> {
Toast.makeText(this, "加载中...", Toast.LENGTH_SHORT).show()
ViewMangaActivity.zipFile = chosenFile
ViewMangaActivity.titleText = it[position]
ViewMangaActivity.zipPosition = position
ViewMangaActivity.zipList = it.toList().toTypedArray()
ViewMangaActivity.cd = cd
startActivity(Intent(this, ViewMangaActivity::class.java))
}
} }
} }
mBinding.mylv.setOnItemLongClickListener { _, _, position, _ ->
val chosenFile = File(cd, it[position])
AlertDialog.Builder(this)
.setIcon(R.drawable.ic_launcher_foreground).setMessage("在此执行删除/查错?")
.setTitle("提示").setPositiveButton("删除"){ _, _ ->
if(chosenFile.exists()) handler?.obtainMessage(2, chosenFile)?.sendToTarget() //call rmrf
handler?.obtainMessage(3, cd)?.sendToTarget() //call scanFile
}.setNegativeButton(android.R.string.cancel){_, _ ->}
.setNeutralButton("查错"){_, _ -> handler?.obtainMessage(1, chosenFile)?.sendToTarget()} //call checkDir
.show()
true
}
} }
} }
fun rmrf(f: File) { private fun showDirectory(directory: File?, entries: List<String>) {
mBinding.mylv.adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, entries)
mBinding.mylv.setOnItemClickListener { _, _, position, _ ->
val chosenFile = File(directory, entries[position])
val chosenJson = File(chosenFile, "info.bin")
when {
chosenJson.exists() -> callDownloadActivity(chosenJson)
chosenFile.isDirectory -> {
currentDir = chosenFile
startActivity(
Intent(this, DlListActivity::class.java)
.putExtra("title", entries[position])
)
}
chosenFile.name.endsWith(".zip", ignoreCase = true) -> {
Toast.makeText(this, "加载中...", Toast.LENGTH_SHORT).show()
val zipFiles = entries
.map { File(directory, it) }
.filter { it.isFile && it.extension.equals("zip", ignoreCase = true) }
ViewMangaActivity.zipFile = chosenFile
ViewMangaActivity.titleText = entries[position]
ViewMangaActivity.zipPosition = zipFiles.indexOf(chosenFile)
ViewMangaActivity.zipList = zipFiles.toTypedArray()
ViewMangaActivity.cd = directory
ViewMangaActivity.nextChapterUrl = null
ViewMangaActivity.previousChapterUrl = null
startActivity(Intent(this, ViewMangaActivity::class.java))
}
}
}
mBinding.mylv.setOnItemLongClickListener { _, _, position, _ ->
val chosenFile = File(directory, entries[position])
AlertDialog.Builder(this)
.setIcon(R.drawable.ic_launcher_foreground)
.setMessage("在此执行删除/查错?")
.setTitle("提示")
.setPositiveButton("删除") { _, _ ->
fileExecutor.execute {
if (chosenFile.exists()) deleteRecursively(chosenFile)
loadDirectory(directory)
}
}
.setNegativeButton(android.R.string.cancel, null)
.setNeutralButton("查错") { _, _ -> checkDirectory(chosenFile) }
.show()
true
}
}
private fun deleteRecursively(f: File) {
if (f.isDirectory) f.listFiles()?.let { if (f.isDirectory) f.listFiles()?.let {
for (i in it) for (i in it)
if (i.isDirectory) rmrf(i) if (i.isDirectory) deleteRecursively(i)
else i.delete() else i.delete()
} }
f.delete() f.delete()
} }
fun checkDir(f: File){ private fun checkDirectory(directory: File) {
nullZipDirStr = emptyArray() fileExecutor.execute {
findNullWebpZipFileInDir(f) val invalidFiles = findInvalidZipFiles(directory)
if(nullZipDirStr.isNotEmpty()) showErrorZip(nullZipDirStr.joinToString("\n")) runOnUiThread {
else Toast.makeText(this, "未发现错误", Toast.LENGTH_SHORT).show() if (isFinishing || isDestroyed) return@runOnUiThread
nullZipDirStr = invalidFiles.toTypedArray()
if (invalidFiles.isNotEmpty()) showErrorZip(invalidFiles.joinToString("\n"))
else Toast.makeText(this, "未发现错误", Toast.LENGTH_SHORT).show()
}
}
} }
private fun callDownloadActivity(jsonFile: File){ private fun callDownloadActivity(jsonFile: File){
@@ -110,54 +128,64 @@ class DlListActivity: Activity() {
) )
} }
private fun findNullWebpZipFileInDir(f: File){ private fun findInvalidZipFiles(file: File): List<String> {
if (f.isDirectory) f.listFiles()?.let { val invalidFiles = mutableListOf<String>()
for (i in it) if (file.isDirectory) file.listFiles()?.forEach { child ->
if (i.isDirectory) findNullWebpZipFileInDir(i) if (child.isDirectory) invalidFiles += findInvalidZipFiles(child)
else if(!checkZip(i)) nullZipDirStr += i.path.substringAfterLast(getExternalFilesDir("").toString()) else if (child.extension.equals("zip", ignoreCase = true) && !checkZip(child)) {
invalidFiles += child.path.substringAfterLast(getExternalFilesDir("").toString())
}
} }
return invalidFiles
} }
private fun checkZip(f: File): Boolean{ private fun checkZip(f: File): Boolean {
return try { return try {
val exist = f.exists() val exist = f.exists()
if (!exist) true if (!exist) true
else { else {
var re = true var re = true
val zip = ZipInputStream(f.inputStream().buffered()) ZipInputStream(f.inputStream().buffered()).use { zip ->
var entry = zip.nextEntry var entry = zip.nextEntry
while (entry != null) { while (entry != null) {
if (!entry.isDirectory){ if (!entry.isDirectory && zip.read() == -1 && entry.size == 0L) {
if(zip.read() == -1 && entry.size == 0L){
re = false re = false
break break
} }
zip.closeEntry()
entry = zip.nextEntry
} }
entry = zip.nextEntry
} }
zip.closeEntry()
zip.close()
re re
} }
} catch (e: Exception) { } catch (e: Exception) {
Toast.makeText(this, "读取${f.name}错误!", Toast.LENGTH_SHORT).show() Log.e("DlListActivity", "读取 ${f.name} 失败", e)
true false
} }
} }
private fun compareFileNames(first: String, second: String): Int {
return if (first.endsWith(".zip") && second.endsWith(".zip")) {
(10000 * getFloat(first) - 10000 * getFloat(second) + 0.5).toInt()
} else first.compareTo(second)
}
private fun showErrorZip(msg: CharSequence) = AlertDialog.Builder(this) private fun showErrorZip(msg: CharSequence) = AlertDialog.Builder(this)
.setIcon(R.drawable.ic_launcher_foreground) .setIcon(R.drawable.ic_launcher_foreground)
.setTitle("找到以下错误文件,是否删除?") .setTitle("找到以下错误文件,是否删除?")
.setMessage(msg) .setMessage(msg)
.setPositiveButton(android.R.string.ok){_, _ -> deleteErrorZip()} .setPositiveButton(android.R.string.ok) { _, _ -> deleteErrorZip() }
.setNegativeButton(android.R.string.cancel){_, _ ->} .setNegativeButton(android.R.string.cancel, null)
.show() .show()
private fun deleteErrorZip(){ private fun deleteErrorZip() {
val exf = getExternalFilesDir("") val exf = getExternalFilesDir("")
for(i in nullZipDirStr){ fileExecutor.execute {
val f = File(exf, i) for (path in nullZipDirStr) {
if(f.exists()) f.delete() val file = File(exf, path)
if (file.exists()) file.delete()
}
loadDirectory(currentDir)
} }
} }
@@ -165,16 +193,14 @@ class DlListActivity: Activity() {
val newString = StringBuffer() val newString = StringBuffer()
var matcher = Pattern.compile("\\d+.+\\d+").matcher(oldString) var matcher = Pattern.compile("\\d+.+\\d+").matcher(oldString)
while (matcher.find()) newString.append(matcher.group()) while (matcher.find()) newString.append(matcher.group())
//Log.d("MyDLL1", newString.toString()) if (newString.isEmpty()) {
if(newString.isEmpty()){
matcher = Pattern.compile("\\d").matcher(oldString) matcher = Pattern.compile("\\d").matcher(oldString)
while (matcher.find()) newString.append(matcher.group()) while (matcher.find()) newString.append(matcher.group())
} }
//Log.d("MyDLL2", newString.toString().toFloat().toString()) return newString.toString().toFloatOrNull() ?: 0f
return if(newString.isEmpty()) 0f else newString.toString().toFloat()
} }
companion object{ companion object {
var currentDir: File? = null var currentDir: File? = null
} }
} }

View File

@@ -3,12 +3,14 @@ package top.fumiama.copymangaweb.activity
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Looper import android.os.Looper
import android.view.View import android.view.View
import android.view.WindowManager
import android.webkit.ValueCallback import android.webkit.ValueCallback
import android.webkit.WebView import android.webkit.WebView
import android.window.OnBackInvokedCallback
import android.window.OnBackInvokedDispatcher
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -34,6 +36,9 @@ class MainActivity: ToolsBoxActivity() {
var saveUrlsOnly = false var saveUrlsOnly = false
lateinit var mBinding: ActivityMainBinding lateinit var mBinding: ActivityMainBinding
private val mViewModel = MainViewModel() private val mViewModel = MainViewModel()
private var backInvokedCallback: OnBackInvokedCallback? = null
@Volatile
private var requestedDetailsUrl: String? = null
@SuppressLint("JavascriptInterface") @SuppressLint("JavascriptInterface")
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -43,6 +48,7 @@ class MainActivity: ToolsBoxActivity() {
mBinding.lifecycleOwner = this mBinding.lifecycleOwner = this
setContentView(mBinding.root) setContentView(mBinding.root)
InsetsTools.applySafeContentInsets(this, mBinding.root) InsetsTools.applySafeContentInsets(this, mBinding.root)
registerBackCallback()
wm = WeakReference(this) wm = WeakReference(this)
mh = MainHandler(Looper.myLooper()!!) mh = MainHandler(Looper.myLooper()!!)
@@ -58,7 +64,7 @@ class MainActivity: ToolsBoxActivity() {
} }
} }
WebView.setWebContentsDebuggingEnabled(true) WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG)
mBinding.w.apply { post { mBinding.w.apply { post {
setWebViewClient("i.js") setWebViewClient("i.js")
webChromeClient = WebChromeClient() webChromeClient = WebChromeClient()
@@ -78,36 +84,38 @@ class MainActivity: ToolsBoxActivity() {
@Deprecated("Deprecated in Java") @Deprecated("Deprecated in Java")
override fun onBackPressed() { override fun onBackPressed() {
if(mBinding.w.canGoBack()) mBinding.w.goBack() navigateBack()
else super.onBackPressed() }
private fun registerBackCallback() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return
backInvokedCallback = OnBackInvokedCallback(::navigateBack).also {
onBackInvokedDispatcher.registerOnBackInvokedCallback(
OnBackInvokedDispatcher.PRIORITY_DEFAULT,
it
)
}
}
private fun navigateBack() {
if (mBinding.w.canGoBack()) mBinding.w.goBack()
else finishAfterTransition()
} }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data) super.onActivityResult(requestCode, resultCode, data)
if (requestCode == FILE_CHOOSER_RESULT_CODE) { //处理返回的图片,并进行上传 if (requestCode != FILE_CHOOSER_RESULT_CODE) return
if (uploadMessageAboveL == null || resultCode != RESULT_OK) return
data?.let { val callback = uploadMessageAboveL ?: return
onActivityResultAboveL(requestCode, resultCode, it) uploadMessageAboveL = null
} callback.onReceiveValue(if (resultCode == RESULT_OK) data?.selectedUris() else null)
}
} }
private fun onActivityResultAboveL(requestCode: Int, resultCode: Int, intent: Intent) { private fun Intent.selectedUris(): Array<Uri>? {
if (requestCode != FILE_CHOOSER_RESULT_CODE || val uris = clipData?.let { clips ->
uploadMessageAboveL == null || Array(clips.itemCount) { index -> clips.getItemAt(index).uri }
resultCode != RESULT_OK } ?: data?.let { arrayOf(it) }
) return return uris?.takeIf { it.isNotEmpty() }
intent.clipData?.let { clipData ->
var results = arrayOf<Uri>()
for (i in 0..clipData.itemCount) {
val item = clipData.getItemAt(i)
results += item.uri
}
if (intent.dataString != null) {
uploadMessageAboveL?.onReceiveValue(results)
uploadMessageAboveL = null
}
}
} }
private suspend fun goCheckUpdate(ignoreSkip: Boolean) { private suspend fun goCheckUpdate(ignoreSkip: Boolean) {
@@ -120,6 +128,7 @@ class MainActivity: ToolsBoxActivity() {
} }
fun loadHiddenUrl(u: String) { fun loadHiddenUrl(u: String) {
requestedDetailsUrl = u
mBinding.wh.apply { post { loadUrl(u) } } mBinding.wh.apply { post { loadUrl(u) } }
} }
@@ -137,7 +146,9 @@ class MainActivity: ToolsBoxActivity() {
startActivity(Intent(this, ViewMangaActivity::class.java)) startActivity(Intent(this, ViewMangaActivity::class.java))
} }
fun setFab(content: String) { fun setFab(content: String, sourceUrl: String, comicTitle: String) {
if (content.isBlank() || content == "[]" || !isRequestedDetailsPage(sourceUrl)) return
DlActivity.comicName = comicTitle
json = content json = content
lifecycleScope.launch { lifecycleScope.launch {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
@@ -148,6 +159,7 @@ class MainActivity: ToolsBoxActivity() {
} }
fun setFab2DlList() { fun setFab2DlList() {
requestedDetailsUrl = null
lifecycleScope.launch { lifecycleScope.launch {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
mViewModel.showDlList.value = true mViewModel.showDlList.value = true
@@ -157,9 +169,20 @@ class MainActivity: ToolsBoxActivity() {
} }
fun hideFab() { fun hideFab() {
requestedDetailsUrl = null
lifecycleScope.launch { mViewModel.setFabVisibility(false) } lifecycleScope.launch { mViewModel.setFabVisibility(false) }
} }
private fun isRequestedDetailsPage(sourceUrl: String): Boolean {
val requestedPath = requestedDetailsUrl
?.let(Uri::parse)
?.path
?.trimEnd('/')
?: return false
val sourcePath = Uri.parse(sourceUrl).path?.trimEnd('/') ?: return false
return requestedPath == sourcePath
}
fun onFabClicked(v: View) { fun onFabClicked(v: View) {
DlListActivity.currentDir = getExternalFilesDir("") DlListActivity.currentDir = getExternalFilesDir("")
startActivity( startActivity(
@@ -168,13 +191,16 @@ class MainActivity: ToolsBoxActivity() {
) )
} }
fun openImageChooserActivity() { fun openImageChooserActivity(callback: ValueCallback<Array<Uri>>) {
// 调用自己的图库 uploadMessageAboveL?.onReceiveValue(null)
uploadMessageAboveL = callback
startActivityForResult( startActivityForResult(
Intent.createChooser( Intent.createChooser(
Intent(Intent.ACTION_GET_CONTENT) Intent(Intent.ACTION_GET_CONTENT)
.addCategory(Intent.CATEGORY_OPENABLE) .addCategory(Intent.CATEGORY_OPENABLE)
.setType("image/*"), "Image Chooser" .setType("image/*")
.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, false),
"Image Chooser"
), FILE_CHOOSER_RESULT_CODE ), FILE_CHOOSER_RESULT_CODE
) )
} }
@@ -182,25 +208,40 @@ class MainActivity: ToolsBoxActivity() {
fun callViewManga(content: String) { fun callViewManga(content: String) {
lifecycleScope.launch { withContext(Dispatchers.IO) { lifecycleScope.launch { withContext(Dispatchers.IO) {
val listChapter = content.split('\n') val listChapter = content.split('\n')
if (listChapter.size < CHAPTER_METADATA_LINE_COUNT) return@withContext
val images = listChapter.drop(CHAPTER_METADATA_LINE_COUNT).toTypedArray()
if(!saveUrlsOnly) { if(!saveUrlsOnly) {
ViewMangaActivity.titleText = listChapter[0].substringBeforeLast(' ') ViewMangaActivity.titleText = listChapter[0].substringBeforeLast(' ')
ViewMangaActivity.nextChapterUrl = listChapter[1].let { if(it == "null") null else it } ViewMangaActivity.nextChapterUrl = listChapter[1].let { if(it == "null") null else it }
ViewMangaActivity.previousChapterUrl = listChapter[2].let { if(it == "null") null else it } ViewMangaActivity.previousChapterUrl = listChapter[2].let { if(it == "null") null else it }
ViewMangaActivity.imgUrls = arrayOf() ViewMangaActivity.imgUrls = images
for(i in 3 until listChapter.size) ViewMangaActivity.imgUrls += listChapter[i]
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
startActivity(Intent(this@MainActivity, ViewMangaActivity::class.java)) startActivity(Intent(this@MainActivity, ViewMangaActivity::class.java))
} }
} else { } else {
var imgs = arrayOf<String>() wmdlt?.get()?.setChapterImages(listChapter[0].substringAfterLast(' '), images)
for(i in 3 until listChapter.size) imgs += listChapter[i]
wmdlt?.get()?.setChapterImages(listChapter[0].substringAfterLast(' '), imgs)
} }
} } } }
} }
override fun onDestroy() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
backInvokedCallback?.let(onBackInvokedDispatcher::unregisterOnBackInvokedCallback)
}
backInvokedCallback = null
uploadMessageAboveL?.onReceiveValue(null)
uploadMessageAboveL = null
mBinding.w.destroy()
mBinding.wh.destroy()
mh?.dispose()
mh = null
if (wm?.get() === this) wm = null
super.onDestroy()
}
companion object { companion object {
const val FILE_CHOOSER_RESULT_CODE = 1 private const val FILE_CHOOSER_RESULT_CODE = 1
private const val CHAPTER_METADATA_LINE_COUNT = 3
var wm: WeakReference<MainActivity>? = null var wm: WeakReference<MainActivity>? = null
var mh: MainHandler? = null var mh: MainHandler? = null
} }

View File

@@ -7,41 +7,37 @@ import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.os.Message
import android.util.Log import android.util.Log
import android.view.KeyEvent import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import android.widget.SeekBar import android.widget.SeekBar
import android.widget.Toast import android.widget.Toast
import android.window.OnBackInvokedCallback
import android.window.OnBackInvokedDispatcher
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.viewpager2.widget.ViewPager2 import androidx.viewpager2.widget.ViewPager2
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import top.fumiama.copymangaweb.R import top.fumiama.copymangaweb.R
import top.fumiama.copymangaweb.activity.reader.ContinuousMangaAdapter
import top.fumiama.copymangaweb.activity.reader.PagedMangaAdapter
import top.fumiama.copymangaweb.activity.reader.ReaderOverlayController
import top.fumiama.copymangaweb.activity.MainActivity.Companion.wm import top.fumiama.copymangaweb.activity.MainActivity.Companion.wm
import top.fumiama.copymangaweb.activity.template.ToolsBoxActivity import top.fumiama.copymangaweb.activity.template.ToolsBoxActivity
import top.fumiama.copymangaweb.databinding.ActivityViewmangaBinding import top.fumiama.copymangaweb.databinding.ActivityViewmangaBinding
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.PagesManager
import top.fumiama.copymangaweb.view.ScaleImageView import top.fumiama.copymangaweb.view.ScaleImageView
import top.fumiama.copymangaweb.web.JSHidden import top.fumiama.copymangaweb.web.JSHidden
import top.fumiama.copymangaweb.web.WebChromeClient 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.util.concurrent.Executors
import java.util.Date
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
import java.util.zip.ZipFile import java.util.zip.ZipFile
import java.util.zip.ZipInputStream
class ViewMangaActivity : ToolsBoxActivity() { class ViewMangaActivity : ToolsBoxActivity() {
lateinit var handler: Handler
lateinit var tt: TimeThread
lateinit var mBinding: ActivityViewmangaBinding lateinit var mBinding: ActivityViewmangaBinding
var count = 0 var count = 0
@@ -54,32 +50,40 @@ class ViewMangaActivity : ToolsBoxActivity() {
private var isInSeek = false private var isInSeek = false
private var currentItem = 0 private var currentItem = 0
private var notUseVP = true private var notUseVP = true
private var verticalReading = false
private var mangaZip = zipFile private var mangaZip = zipFile
val dlZip2View = mangaZip != null val dlZip2View = mangaZip != null
private var streamUrl = streamChapterUrl private var streamUrl = streamChapterUrl
private var readerPrepared = false private var readerPrepared = false
private var streamFinished = false private var streamFinished = false
private var streamDeclaredCount = 0 private var streamDeclaredCount = 0
private var userRequestedExit = false
private var backInvokedCallback: OnBackInvokedCallback? = null
private val streamSeenUrls = LinkedHashSet<String>() private val streamSeenUrls = LinkedHashSet<String>()
private var vpAdapter: RecyclerView.Adapter<ViewData>? = null private var pagedAdapter: PagedMangaAdapter? = null
private var continuousAdapter: ContinuousMangaAdapter? = null
private val imageExecutor = Executors.newFixedThreadPool(2)
private lateinit var overlayController: ReaderOverlayController
private val pagesManager by lazy { PagesManager(WeakReference(this)) }
private val volTurnPage get() = p["volturn"] == "true" private val volTurnPage get() = p["volturn"] == "true"
var pageNum = 1 private val readerMode: ReaderMode
get() { get() = when {
field = getPageNumber() verticalReading -> ReaderMode.CONTINUOUS
return field notUseVP -> ReaderMode.SINGLE_PAGE
else -> ReaderMode.PAGED
} }
var pageNum: Int
get() = getPageNumber()
set(value) { set(value) {
setPageNumber(value) setPageNumber(value, smoothScroll = true)
if (notUseVP) { if (readerMode == ReaderMode.SINGLE_PAGE) {
//currentItem += delta
try { try {
loadOneImg() loadOneImg()
} catch (e: java.lang.Exception) { } catch (e: java.lang.Exception) {
e.printStackTrace() e.printStackTrace()
toolsBox.toastError("页数${currentItem}不合法") toolsBox.toastError("页数${currentItem}不合法")
} }
}// else vp.currentItem += delta }
field = getPageNumber()
} }
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
@@ -87,14 +91,18 @@ class ViewMangaActivity : ToolsBoxActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
mBinding = ActivityViewmangaBinding.inflate(layoutInflater) mBinding = ActivityViewmangaBinding.inflate(layoutInflater)
setContentView(mBinding.root) setContentView(mBinding.root)
registerBackCallback()
va = WeakReference(this) va = WeakReference(this)
p = PropertiesTools(File("$filesDir/settings.properties")) p = PropertiesTools(File("$filesDir/settings.properties"))
r2l = p["r2l"] == "true" r2l = p["r2l"] == "true"
notUseVP = p["noAnimation"] == "true" notUseVP = p["noAnimation"] == "true"
handler = MyHandler(toolsBox) verticalReading = p["vertical"] == "true"
tt = TimeThread(handler, 22) overlayController = ReaderOverlayController(
tt.canDo = true activity = this,
tt.start() binding = mBinding,
toolsBox = toolsBox,
drawerOffset = { infoDrawerDelta }
).also { it.start() }
dialog = Dialog(this) dialog = Dialog(this)
dialog?.apply { dialog?.apply {
setContentView(R.layout.dialog_unzipping) setContentView(R.layout.dialog_unzipping)
@@ -105,13 +113,18 @@ class ViewMangaActivity : ToolsBoxActivity() {
if(dlZip2View && mangaZip?.exists() != true) toolsBox.toastError("已经到头了~") if(dlZip2View && mangaZip?.exists() != true) toolsBox.toastError("已经到头了~")
else if(!dlZip2View && !streamUrl.isNullOrBlank()) startStreamingCollector(streamUrl!!) else if(!dlZip2View && !streamUrl.isNullOrBlank()) startStreamingCollector(streamUrl!!)
else Thread { else Thread {
try { val initialCount = try {
count = if (dlZip2View) countZipItems() else imgUrls.size if (dlZip2View) countZipItems() else imgUrls.size
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
runOnUiThread { toolsBox.toastError("分析图片url错误") } 0
}
runOnUiThread {
if (isFinishing || isDestroyed) return@runOnUiThread
count = initialCount
if (initialCount == 0) toolsBox.toastError("分析图片url错误")
prepareReaderIfNeeded()
} }
runOnUiThread { prepareReaderIfNeeded() }
}.start() }.start()
} }
@@ -162,14 +175,15 @@ class ViewMangaActivity : ToolsBoxActivity() {
private fun onStreamingCount(total: Int) { private fun onStreamingCount(total: Int) {
if (total <= 0) return if (total <= 0) return
runOnUiThread { runOnUiThread {
if (isFinishing || isDestroyed) return@runOnUiThread
val oldCount = count val oldCount = count
streamDeclaredCount = max(streamDeclaredCount, total) streamDeclaredCount = max(streamDeclaredCount, total)
count = max(count, streamDeclaredCount) count = max(count, streamDeclaredCount)
if (!readerPrepared) { if (!readerPrepared) {
prepareReaderIfNeeded() prepareReaderIfNeeded()
} else { } else {
if (!notUseVP && count > oldCount) { if (readerMode != ReaderMode.SINGLE_PAGE && count > oldCount) {
vpAdapter?.notifyItemRangeInserted(oldCount, count - oldCount) notifyPagesInserted(oldCount, count - oldCount)
} }
syncSeekBarOnly() syncSeekBarOnly()
} }
@@ -184,17 +198,25 @@ class ViewMangaActivity : ToolsBoxActivity() {
} }
private fun onStreamingFinished() { private fun onStreamingFinished() {
streamFinished = true
Log.d("CopymangaDL", "reader stream finished pages=$count")
runOnUiThread { runOnUiThread {
dialog?.dismiss() if (isFinishing || isDestroyed) return@runOnUiThread
dialog = null streamFinished = true
syncSeekBarOnly() Log.d(
"CopymangaDL",
"reader stream finished declared=$streamDeclaredCount received=${imgUrls.size}"
)
prepareReaderIfNeeded()
if (readerPrepared) {
dialog?.dismiss()
dialog = null
syncSeekBarOnly()
}
} }
} }
private fun appendStreamingImages(urls: Array<String>) { private fun appendStreamingImages(urls: Array<String>) {
runOnUiThread { runOnUiThread {
if (isFinishing || isDestroyed) return@runOnUiThread
val oldCount = count val oldCount = count
val oldImageCount = imgUrls.size val oldImageCount = imgUrls.size
var added = 0 var added = 0
@@ -209,12 +231,16 @@ class ViewMangaActivity : ToolsBoxActivity() {
Log.d("CopymangaDL", "reader appended images added=$added total=$count images=${imgUrls.size}") Log.d("CopymangaDL", "reader appended images added=$added total=$count images=${imgUrls.size}")
if (!readerPrepared) prepareReaderIfNeeded() if (!readerPrepared) prepareReaderIfNeeded()
else { else {
if (notUseVP) { if (readerMode == ReaderMode.SINGLE_PAGE) {
if (currentItem in oldImageCount until imgUrls.size) loadOneImg(hidePanel = false) if (currentItem in oldImageCount until imgUrls.size) loadOneImg(hidePanel = false)
else syncSeekBarOnly() else syncSeekBarOnly()
} else { } else {
if (count > oldCount) vpAdapter?.notifyItemRangeInserted(oldCount, count - oldCount) if (count > oldCount) notifyPagesInserted(oldCount, count - oldCount)
notifyVisiblePagesIfArrived(oldImageCount, imgUrls.size) notifyVisiblePagesIfArrived(oldImageCount, imgUrls.size)
continuousAdapter?.notifyItemRangeChanged(
oldImageCount,
imgUrls.size - oldImageCount
)
syncSeekBarOnly() syncSeekBarOnly()
} }
preloadAround(getLogicalCurrentItem()) preloadAround(getLogicalCurrentItem())
@@ -223,17 +249,21 @@ class ViewMangaActivity : ToolsBoxActivity() {
} }
private fun prepareReaderIfNeeded() { private fun prepareReaderIfNeeded() {
if (
readerPrepared ||
count <= 0 ||
isFinishing ||
isDestroyed ||
shouldWaitForLastStreamingPage()
) return
readerPrepared = true
try { try {
if (count <= 0) return
prepareItems() prepareItems()
if(pn > 0) { setPageNumber(consumeRequestedPage(), smoothScroll = false)
pageNum = pn if (readerMode == ReaderMode.SINGLE_PAGE) loadOneImg()
pn = -1 else syncSeekBarOnly()
} else if(pn == -2){
pageNum = count
pn = -1
}
} catch (e: Exception) { } catch (e: Exception) {
readerPrepared = false
e.printStackTrace() e.printStackTrace()
toolsBox.toastError("准备控件错误") toolsBox.toastError("准备控件错误")
} finally { } finally {
@@ -244,6 +274,23 @@ class ViewMangaActivity : ToolsBoxActivity() {
} }
} }
private fun shouldWaitForLastStreamingPage(): Boolean {
if (streamUrl == null || pn != LAST_PAGE) return false
val allDeclaredPagesReceived =
streamDeclaredCount <= 0 || imgUrls.size >= streamDeclaredCount
return !streamFinished || !allDeclaredPagesReceived
}
private fun consumeRequestedPage(): Int {
val requestedPage = when {
pn == LAST_PAGE -> count
pn > 0 -> pn.coerceAtMost(count)
else -> 1
}
pn = FIRST_PAGE
return requestedPage
}
private fun preloadAround(position: Int) { private fun preloadAround(position: Int) {
if (dlZip2View || position < 0 || imgUrls.isEmpty()) return if (dlZip2View || position < 0 || imgUrls.isEmpty()) return
val end = min(imgUrls.size - 1, position + 4) val end = min(imgUrls.size - 1, position + 4)
@@ -257,24 +304,20 @@ class ViewMangaActivity : ToolsBoxActivity() {
} }
private fun getLogicalCurrentItem(): Int { private fun getLogicalCurrentItem(): Int {
return if (notUseVP) currentItem return when (readerMode) {
else if (r2l) count - mBinding.vp.currentItem - 1 ReaderMode.SINGLE_PAGE, ReaderMode.CONTINUOUS -> currentItem
else mBinding.vp.currentItem ReaderMode.PAGED -> mBinding.vp.currentItem
} }
private fun logicalToPagerPosition(logicalPosition: Int): Int {
return if (r2l) count - logicalPosition - 1 else logicalPosition
} }
private fun notifyVisiblePagesIfArrived(oldImageCount: Int, newImageCount: Int) { private fun notifyVisiblePagesIfArrived(oldImageCount: Int, newImageCount: Int) {
if (notUseVP || oldImageCount >= newImageCount || count <= 0) return if (readerMode != ReaderMode.PAGED || oldImageCount >= newImageCount || count <= 0) return
val center = getLogicalCurrentItem().coerceIn(0, count - 1) val center = getLogicalCurrentItem().coerceIn(0, count - 1)
val from = max(0, center - 1) val from = max(0, center - 1)
val to = min(newImageCount - 1, center + 1) val to = min(newImageCount - 1, center + 1)
for (logicalPosition in from..to) { for (logicalPosition in from..to) {
if (logicalPosition >= oldImageCount) { if (logicalPosition >= oldImageCount) {
val pagerPosition = logicalToPagerPosition(logicalPosition) pagedAdapter?.notifyItemChanged(logicalPosition)
if (pagerPosition in 0 until count) vpAdapter?.notifyItemChanged(pagerPosition)
} }
} }
} }
@@ -287,28 +330,57 @@ class ViewMangaActivity : ToolsBoxActivity() {
} }
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
var flag = false if (volTurnPage) {
if(volTurnPage) when(keyCode) { when (keyCode) {
KeyEvent.KEYCODE_VOLUME_UP -> { KeyEvent.KEYCODE_VOLUME_UP -> pagesManager.goBackward()
scrollBack() KeyEvent.KEYCODE_VOLUME_DOWN -> pagesManager.goForward()
flag = true else -> return super.onKeyDown(keyCode, event)
}
KeyEvent.KEYCODE_VOLUME_DOWN -> {
scrollForward()
flag = true
} }
return true
} }
return if(flag) true else super.onKeyDown(keyCode, event) return super.onKeyDown(keyCode, event)
}
@Deprecated("Deprecated in Java")
override fun onBackPressed() {
exitByUserRequest()
}
private fun registerBackCallback() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return
backInvokedCallback = OnBackInvokedCallback(::exitByUserRequest).also { callback ->
onBackInvokedDispatcher.registerOnBackInvokedCallback(
OnBackInvokedDispatcher.PRIORITY_DEFAULT,
callback
)
}
}
private fun exitByUserRequest() {
userRequestedExit = true
finishAfterTransition()
} }
private fun getPageNumber(): Int { private fun getPageNumber(): Int {
return if (r2l && !notUseVP) count - mBinding.vp.currentItem return when (readerMode) {
else (if (notUseVP) currentItem else mBinding.vp.currentItem) + 1 ReaderMode.SINGLE_PAGE, ReaderMode.CONTINUOUS -> currentItem + 1
ReaderMode.PAGED -> mBinding.vp.currentItem + 1
}
} }
private fun setPageNumber(num: Int) { private fun setPageNumber(num: Int, smoothScroll: Boolean) {
if (r2l && !notUseVP) mBinding.vp.apply { post { currentItem = count - num } } val target = (num - 1).coerceIn(0, (count - 1).coerceAtLeast(0))
else if (notUseVP) currentItem = num - 1 else mBinding.vp.currentItem = num - 1 when (readerMode) {
ReaderMode.SINGLE_PAGE -> currentItem = target
ReaderMode.CONTINUOUS -> {
currentItem = target
mBinding.continuousPages.scrollToPosition(target)
}
ReaderMode.PAGED -> mBinding.vp.setCurrentItem(
target,
smoothScroll
)
}
} }
private fun getImgBitmap(position: Int): Bitmap? { private fun getImgBitmap(position: Int): Bitmap? {
@@ -340,6 +412,7 @@ class ViewMangaActivity : ToolsBoxActivity() {
} }
private fun loadOneImg(hidePanel: Boolean = true) { private fun loadOneImg(hidePanel: Boolean = true) {
mBinding.vone.onei.resetImageTransform()
if(dlZip2View) mBinding.vone.onei.apply { post { setImageBitmap(getImgBitmap(currentItem)) } } if(dlZip2View) mBinding.vone.onei.apply { post { setImageBitmap(getImgBitmap(currentItem)) } }
else { else {
val url = imgUrls.getOrNull(currentItem) val url = imgUrls.getOrNull(currentItem)
@@ -364,14 +437,14 @@ class ViewMangaActivity : ToolsBoxActivity() {
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
private fun prepareItems() { private fun prepareItems() {
if (count <= 0) return if (count <= 0) return
mBinding.vone.onei.setOnTapRegionListener(::onImageTapped)
prepareVP() prepareVP()
prepareInfoBar(count) prepareInfoBar(count)
if (notUseVP) loadOneImg() else prepareIdBtVH()
toolsBox.dp2px(67)?.let { setIdPosition(it) } toolsBox.dp2px(67)?.let { setIdPosition(it) }
prepareIdBtVolTurn() prepareIdBtVolTurn()
prepareIdBtVH()
prepareIdBtVP() prepareIdBtVP()
prepareIdBtLR() prepareIdBtLR()
readerPrepared = true
} }
private fun prepareIdBtLR() { private fun prepareIdBtLR() {
@@ -388,6 +461,7 @@ class ViewMangaActivity : ToolsBoxActivity() {
private fun prepareIdBtVP() { private fun prepareIdBtVP() {
mBinding.infcard.idtbvp.apply { post { mBinding.infcard.idtbvp.apply { post {
isChecked = notUseVP isChecked = notUseVP
isEnabled = !verticalReading
setOnClickListener { setOnClickListener {
if (mBinding.infcard.idtbvp.isChecked) p["noAnimation"] = "true" if (mBinding.infcard.idtbvp.isChecked) p["noAnimation"] = "true"
else p["noAnimation"] = "false" else p["noAnimation"] = "false"
@@ -397,25 +471,107 @@ class ViewMangaActivity : ToolsBoxActivity() {
} }
private fun prepareVP() { private fun prepareVP() {
if (notUseVP) { mBinding.vone.root.visibility = if (readerMode == ReaderMode.SINGLE_PAGE) View.VISIBLE else View.GONE
mBinding.vp.apply { post { visibility = View.INVISIBLE } } mBinding.vp.visibility = if (readerMode == ReaderMode.PAGED) View.VISIBLE else View.GONE
mBinding.vone.root.apply { post { visibility = View.VISIBLE } } mBinding.continuousPages.visibility = if (readerMode == ReaderMode.CONTINUOUS) View.VISIBLE else View.GONE
} else {
mBinding.vp.apply { post { when (readerMode) {
visibility = View.VISIBLE ReaderMode.SINGLE_PAGE -> Unit
vpAdapter = ViewData(this).RecyclerViewAdapter() ReaderMode.PAGED -> mBinding.vp.apply {
adapter = vpAdapter layoutDirection = if (r2l) {
View.LAYOUT_DIRECTION_RTL
} else {
View.LAYOUT_DIRECTION_LTR
}
pagedAdapter = PagedMangaAdapter(
itemCountProvider = { count },
bindImage = ::bindPageImage
)
adapter = pagedAdapter
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(position)
preloadAround(pos)
updateSeekBar() updateSeekBar()
super.onPageSelected(position) super.onPageSelected(position)
} }
}) })
if (r2l) currentItem = count - 1 }
} } ReaderMode.CONTINUOUS -> prepareContinuousReader()
mBinding.vone.root.apply { post { visibility = View.INVISIBLE } } }
}
private fun prepareContinuousReader() {
continuousAdapter = ContinuousMangaAdapter(
itemCountProvider = { count },
bindImage = ::bindPageImage
)
mBinding.continuousPages.apply {
adapter = continuousAdapter
itemAnimator = null
addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
val manager = recyclerView.layoutManager as? LinearLayoutManager ?: return
val firstVisible = manager.findFirstVisibleItemPosition()
if (firstVisible != RecyclerView.NO_POSITION) {
currentItem = firstVisible
syncSeekBarOnly()
preloadAround(firstVisible)
}
}
})
}
}
private fun notifyPagesInserted(positionStart: Int, itemCount: Int) {
if (itemCount <= 0) return
pagedAdapter?.notifyItemRangeInserted(positionStart, itemCount)
continuousAdapter?.notifyItemRangeInserted(positionStart, itemCount)
}
private fun bindPageImage(imageView: ScaleImageView, position: Int) {
imageView.tag = position
imageView.setOnTapRegionListener(::onImageTapped)
if (dlZip2View) loadZipImage(imageView, position)
else loadNetworkImage(imageView, position)
}
private fun onImageTapped(region: ScaleImageView.TapRegion) {
when (region) {
ScaleImageView.TapRegion.PREVIOUS -> {
if (readerMode == ReaderMode.CONTINUOUS) pagesManager.goBackward()
else pagesManager.toPreviousPage()
}
ScaleImageView.TapRegion.CENTER -> pagesManager.manageInfo()
ScaleImageView.TapRegion.NEXT -> {
if (readerMode == ReaderMode.CONTINUOUS) pagesManager.goForward()
else pagesManager.toNextPage()
}
}
}
private fun loadNetworkImage(imageView: ScaleImageView, position: Int) {
val url = imgUrls.getOrNull(position)
if (url.isNullOrBlank()) {
imageView.setImageResource(R.drawable.ic_dl)
return
}
Glide.with(this)
.load(toolsBox.resolution.wrap(url))
.placeholder(R.drawable.ic_dl)
.dontAnimate()
.into(imageView)
preloadAround(position)
}
private fun loadZipImage(imageView: ScaleImageView, position: Int) {
imageView.setImageResource(R.drawable.ic_dl)
imageExecutor.execute {
val bitmap = getImgBitmap(position)
imageView.post {
if (!isFinishing && !isDestroyed && imageView.tag == position) {
imageView.setImageBitmap(bitmap)
}
}
} }
} }
@@ -459,28 +615,18 @@ class ViewMangaActivity : ToolsBoxActivity() {
} } } }
mBinding.oneinfo.inftitle.isearch.apply { post { mBinding.oneinfo.inftitle.isearch.apply { post {
visibility = View.INVISIBLE visibility = View.INVISIBLE
setOnClickListener { setOnClickListener { overlayController.toggleDrawer() }
this@ViewMangaActivity.handler.sendEmptyMessage(3)
}
} } } }
mBinding.oneinfo.inftxtprogress.apply { post { text = "$pageNum/$size" } } mBinding.oneinfo.inftxtprogress.apply { post { text = "$pageNum/$size" } }
} }
private fun prepareIdBtVH() { private fun prepareIdBtVH() {
mBinding.infcard.idtbvh.apply { post { mBinding.infcard.idtbvh.apply { post {
isChecked = p["vertical"] == "true" isChecked = verticalReading
setOnClickListener { setOnClickListener {
if (mBinding.infcard.idtbvh.isChecked) { p["vertical"] = isChecked.toString()
mBinding.vp.apply { post { orientation = ViewPager2.ORIENTATION_VERTICAL } } Toast.makeText(this@ViewMangaActivity, "下次浏览生效", Toast.LENGTH_SHORT).show()
p["vertical"] = "true"
} else {
mBinding.vp.apply { post { orientation = ViewPager2.ORIENTATION_HORIZONTAL } }
p["vertical"] = "false"
}
} }
if (isChecked) mBinding.vp.apply { post {
orientation = ViewPager2.ORIENTATION_VERTICAL
} }
} } } }
} }
@@ -532,52 +678,24 @@ class ViewMangaActivity : ToolsBoxActivity() {
} } } }
} }
@Deprecated("Deprecated in Java")
override fun onBackPressed() {
tt.canDo = false
wm?.get()?.mBinding?.w?.goBack()
super.onBackPressed()
}
override fun onDestroy() { override fun onDestroy() {
tt.canDo = false if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
handler.removeCallbacksAndMessages(null) backInvokedCallback?.let(onBackInvokedDispatcher::unregisterOnBackInvokedCallback)
if (streamUrl != null) streamChapterUrl = null
super.onDestroy()
}
inner class ViewData(itemView: View) : RecyclerView.ViewHolder(itemView) {
inner class RecyclerViewAdapter :
RecyclerView.Adapter<ViewData>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewData {
return ViewData(
LayoutInflater.from(parent.context)
.inflate(R.layout.page_imgview, parent, false)
)
}
@SuppressLint("ClickableViewAccessibility", "SetTextI18n")
override fun onBindViewHolder(holder: ViewData, position: Int) {
val pos = if (r2l) count - position - 1 else position
holder.itemView.findViewById<ScaleImageView>(R.id.onei)?.let { oneImage ->
if(dlZip2View) getImgBitmap(pos)?.let {
//Glide.with(this@ViewMangaActivity).load(it).placeholder(R.drawable.bg_comment).into(holder.itemView.onei)
oneImage.setImageBitmap(it)
}
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)
}
}
override fun getItemCount(): Int {
return count
}
} }
backInvokedCallback = null
overlayController.close()
imageExecutor.shutdownNow()
pagedAdapter = null
continuousAdapter = null
mBinding.vp.adapter = null
mBinding.continuousPages.adapter = null
dialog?.dismiss()
dialog = null
mBinding.wcollector.destroy()
if (userRequestedExit && !dlZip2View) wm?.get()?.mBinding?.w?.goBack()
if (streamUrl != null) streamChapterUrl = null
if (va?.get() === this) va = null
super.onDestroy()
} }
fun showSettings() { fun showSettings() {
@@ -603,62 +721,17 @@ class ViewMangaActivity : ToolsBoxActivity() {
).setDuration(233).start() ).setDuration(233).start()
clicked = false clicked = false
mBinding.oneinfo.infseek.postDelayed({ mBinding.oneinfo.infseek.postDelayed({
if (isFinishing || isDestroyed) return@postDelayed
mBinding.oneinfo.infseek.visibility = View.INVISIBLE mBinding.oneinfo.infseek.visibility = View.INVISIBLE
mBinding.oneinfo.inftitle.isearch.visibility = View.INVISIBLE mBinding.oneinfo.inftitle.isearch.visibility = View.INVISIBLE
}, 300) }, 300)
handler.sendEmptyMessage(1) overlayController.hideDrawer()
}
class MyHandler(
private val toolsBox: ToolsBox
) : Handler(Looper.myLooper()!!) {
private var infoShown = false
private var delta = -1f
get() {
if (field < 0) field = va?.get()?.infoDrawerDelta ?: 0f
return field
}
@SuppressLint("SimpleDateFormat", "SetTextI18n")
override fun handleMessage(msg: Message) {
super.handleMessage(msg)
when (msg.what) {
1 -> if (infoShown) {
hideInfCard(); infoShown = false
}
2 -> if (!infoShown) {
showInfCard(); infoShown = true
}
3 -> infoShown = if (infoShown) {
hideInfCard(); false
} else {
showInfCard(); true
}
22 -> (toolsBox.zis as? ViewMangaActivity)?.mBinding?.infcard?.idtime?.apply { post {
text = SimpleDateFormat("HH:mm")
.format(Date()) + toolsBox.week + toolsBox.netInfo
} }
}
}
private fun showInfCard() {
Log.d("MyVM", "showInfCard delta $delta")
va?.get()?.mBinding?.infcard?.apply {
ObjectAnimator.ofFloat(idc, "alpha", 0.3F, 0.8F).setDuration(233).start()
ObjectAnimator.ofFloat(root, "translationY", delta, 0F).setDuration(233).start()
}
}
private fun hideInfCard() {
Log.d("MyVM", "hideInfCard delta $delta")
va?.get()?.mBinding?.infcard?.apply {
ObjectAnimator.ofFloat(idc, "alpha", 0.8F, 0.3F).setDuration(233).start()
ObjectAnimator.ofFloat(root, "translationY", 0F, delta).setDuration(233).start()
}
}
} }
companion object { companion object {
const val FIRST_PAGE = -1
const val LAST_PAGE = -2
var va: WeakReference<ViewMangaActivity>? = null var va: WeakReference<ViewMangaActivity>? = null
var imgUrls = arrayOf<String>() var imgUrls = arrayOf<String>()
var zipFile: File? = null var zipFile: File? = null
@@ -671,9 +744,15 @@ class ViewMangaActivity : ToolsBoxActivity() {
var nextChapterUrl: String? = null var nextChapterUrl: String? = null
var previousChapterUrl: String? = null var previousChapterUrl: String? = null
var zipPosition = 0 var zipPosition = 0
var zipList: Array<String>? = null var zipList: Array<File>? = null
var cd: File? = null var cd: File? = null
var pn = -1 var pn = FIRST_PAGE
var streamChapterUrl: String? = null var streamChapterUrl: String? = null
} }
private enum class ReaderMode {
SINGLE_PAGE,
PAGED,
CONTINUOUS,
}
} }

View File

@@ -0,0 +1,66 @@
package top.fumiama.copymangaweb.activity.reader
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import top.fumiama.copymangaweb.R
import top.fumiama.copymangaweb.view.ScaleImageView
class PagedMangaAdapter(
private val itemCountProvider: () -> Int,
private val bindImage: (ScaleImageView, Int) -> Unit,
) : RecyclerView.Adapter<PagedMangaAdapter.PageViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PageViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.page_imgview, parent, false)
return PageViewHolder(view)
}
override fun onBindViewHolder(holder: PageViewHolder, position: Int) {
holder.image.resetImageTransform()
bindImage(holder.image, position)
}
override fun onViewRecycled(holder: PageViewHolder) {
holder.image.setOnTapRegionListener(null)
holder.image.setImageDrawable(null)
super.onViewRecycled(holder)
}
override fun getItemCount(): Int = itemCountProvider()
class PageViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val image: ScaleImageView = itemView.findViewById(R.id.onei)
}
}
class ContinuousMangaAdapter(
private val itemCountProvider: () -> Int,
private val bindImage: (ScaleImageView, Int) -> Unit,
) : RecyclerView.Adapter<ContinuousMangaAdapter.PageViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PageViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.page_img_continuous, parent, false)
return PageViewHolder(view)
}
override fun onBindViewHolder(holder: PageViewHolder, position: Int) {
holder.image.resetImageTransform()
bindImage(holder.image, position)
}
override fun onViewRecycled(holder: PageViewHolder) {
holder.image.setOnTapRegionListener(null)
holder.image.setImageDrawable(null)
super.onViewRecycled(holder)
}
override fun getItemCount(): Int = itemCountProvider()
class PageViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val image: ScaleImageView = itemView.findViewById(R.id.pageImage)
}
}

View File

@@ -0,0 +1,70 @@
package top.fumiama.copymangaweb.activity.reader
import android.animation.ObjectAnimator
import android.annotation.SuppressLint
import android.os.Handler
import android.os.Looper
import android.text.format.DateFormat
import android.util.Log
import top.fumiama.copymangaweb.activity.ViewMangaActivity
import top.fumiama.copymangaweb.databinding.ActivityViewmangaBinding
import top.fumiama.copymangaweb.tool.ToolsBox
import java.util.Date
class ReaderOverlayController(
private val activity: ViewMangaActivity,
private val binding: ActivityViewmangaBinding,
private val toolsBox: ToolsBox,
private val drawerOffset: () -> Float,
) {
private val handler = Handler(Looper.getMainLooper())
private val clockFormat = DateFormat.getTimeFormat(activity)
private var drawerVisible = false
private val updateStatus = object : Runnable {
@SuppressLint("SetTextI18n")
override fun run() {
if (activity.isFinishing || activity.isDestroyed) return
binding.infcard.idtime.text = "${clockFormat.format(Date())}${toolsBox.week}${toolsBox.netInfo}"
handler.postDelayed(this, STATUS_UPDATE_INTERVAL_MS)
}
}
fun start() {
handler.post(updateStatus)
}
fun toggleDrawer() {
if (drawerVisible) hideDrawer() else showDrawer()
}
fun hideDrawer() {
if (!drawerVisible) return
val offset = drawerOffset()
Log.d("ReaderOverlay", "hide drawer offset=$offset")
binding.infcard.apply {
ObjectAnimator.ofFloat(idc, "alpha", idc.alpha, 0.3f).setDuration(ANIMATION_DURATION_MS).start()
ObjectAnimator.ofFloat(root, "translationY", root.translationY, offset).setDuration(ANIMATION_DURATION_MS).start()
}
drawerVisible = false
}
private fun showDrawer() {
val offset = drawerOffset()
Log.d("ReaderOverlay", "show drawer offset=$offset")
binding.infcard.apply {
ObjectAnimator.ofFloat(idc, "alpha", idc.alpha, 0.8f).setDuration(ANIMATION_DURATION_MS).start()
ObjectAnimator.ofFloat(root, "translationY", root.translationY, 0f).setDuration(ANIMATION_DURATION_MS).start()
}
drawerVisible = true
}
fun close() {
handler.removeCallbacksAndMessages(null)
}
companion object {
private const val STATUS_UPDATE_INTERVAL_MS = 3_000L
private const val ANIMATION_DURATION_MS = 233L
}
}

View File

@@ -16,7 +16,6 @@ open class ToolsBoxActivity: Activity(), LifecycleOwner {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
lifecycleRegistry = LifecycleRegistry(this) lifecycleRegistry = LifecycleRegistry(this)
lifecycleRegistry.currentState = Lifecycle.State.CREATED lifecycleRegistry.currentState = Lifecycle.State.CREATED
lifecycleRegistry.currentState = Lifecycle.State.STARTED
toolsBox = ToolsBox(WeakReference(this)) toolsBox = ToolsBox(WeakReference(this))
} }

View File

@@ -20,10 +20,12 @@ class DlHandler(activity: DlActivity, looper: Looper) : Handler(looper) {
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
override fun handleMessage(msg: Message) { override fun handleMessage(msg: Message) {
super.handleMessage(msg) super.handleMessage(msg)
val chapterIndex = msg.arg1
val pageNumber = msg.arg2
when (msg.what) { when (msg.what) {
-2 -> d?.setLayouts() INITIALIZE_LAYOUTS -> d?.setLayouts()
1 -> { CHAPTER_DOWNLOAD_SUCCEEDED -> {
d?.tbtnlist?.get(msg.arg1)?.apply { post { d?.tbtnlist?.get(chapterIndex)?.apply { post {
setBackgroundResource(R.drawable.rndbg_checked) setBackgroundResource(R.drawable.rndbg_checked)
isChecked = false isChecked = false
d?.updateProgressBar() d?.updateProgressBar()
@@ -39,15 +41,15 @@ class DlHandler(activity: DlActivity, looper: Looper) : Handler(looper) {
} }
} } } }
} }
-1 -> { CHAPTER_DOWNLOAD_FAILED -> {
d?.tbtnlist?.get(msg.arg1)?.apply { post { d?.tbtnlist?.get(chapterIndex)?.apply { post {
setBackgroundResource(R.drawable.rndbg_error) setBackgroundResource(R.drawable.rndbg_error)
d!!.dldChapter-- d!!.dldChapter--
Toast.makeText(d, "下载${d?.tbtnlist?.get(msg.arg1)?.textOn}失败", Toast.LENGTH_SHORT).show() Toast.makeText(d, "下载${d?.tbtnlist?.get(chapterIndex)?.textOn}失败", Toast.LENGTH_SHORT).show()
d?.updateProgressBar() d?.updateProgressBar()
} } } }
} }
4 -> { TOGGLE_SELECT_ALL -> {
d?.mBinding?.dldlbar?.pdwn?.apply { post { progress = 0 } } d?.mBinding?.dldlbar?.pdwn?.apply { post { progress = 0 } }
val selectDownloaded = d?.multiSelect?:false val selectDownloaded = d?.multiSelect?:false
if (d?.haveSElectAll == true) { if (d?.haveSElectAll == true) {
@@ -81,27 +83,27 @@ class DlHandler(activity: DlActivity, looper: Looper) : Handler(looper) {
text = "${d?.dldChapter}/${d?.checkedChapter}" text = "${d?.dldChapter}/${d?.checkedChapter}"
} } } }
} }
5 -> { PAGE_DOWNLOAD_FINISHED -> {
setSize(msg.arg2, msg.arg1) setSize(pageNumber, chapterIndex)
d?.updateProgressBar(msg.arg2, size) d?.updateProgressBar(pageNumber, size)
if (!(msg.obj as Boolean)) { if (msg.obj != true) {
Toast.makeText(d, "下载${d?.tbtnlist?.get(msg.arg1)?.textOn}的第${msg.arg2}页失败", Toast.LENGTH_SHORT).show() Toast.makeText(d, "下载${d?.tbtnlist?.get(chapterIndex)?.textOn}的第${pageNumber}页失败", Toast.LENGTH_SHORT).show()
}else{ }else{
val progressTxt = d?.mBinding?.dldlbar?.tdwn?.text.toString() val progressTxt = d?.mBinding?.dldlbar?.tdwn?.text.toString()
d?.mBinding?.dldlbar?.tdwn?.apply { post { d?.mBinding?.dldlbar?.tdwn?.apply { post {
text = "${progressTxt.substringBefore(' ')}${msg.arg2}/${size}" text = "${progressTxt.substringBefore(' ')}${pageNumber}/${size}"
} } } }
} }
} }
6 -> d?.mBinding?.dldlbar?.tdwn?.apply { post { text = "${d?.dldChapter}/${d?.checkedChapter}" } } UPDATE_CHAPTER_PROGRESS -> d?.mBinding?.dldlbar?.tdwn?.apply { post { text = "${d?.dldChapter}/${d?.checkedChapter}" } }
7 -> d?.deleteChapters() DELETE_SELECTED_CHAPTERS -> d?.deleteChapters()
8 -> d?.resources?.getColor(R.color.colorBlue)?.let { d?.mBinding?.dldlbar?.cdwn?.apply { post { SET_DOWNLOAD_CARD_BLUE -> d?.resources?.getColor(R.color.colorBlue)?.let { d?.mBinding?.dldlbar?.cdwn?.apply { post {
setCardBackgroundColor(it) setCardBackgroundColor(it)
} } } } } }
9 -> d?.resources?.getColor(R.color.colorRed)?.let { d?.mBinding?.dldlbar?.cdwn?.apply { post { SET_DOWNLOAD_CARD_RED -> d?.resources?.getColor(R.color.colorRed)?.let { d?.mBinding?.dldlbar?.cdwn?.apply { post {
setCardBackgroundColor(it) setCardBackgroundColor(it)
} } } } } }
10 -> Toast.makeText(d, "下载${d?.tbtnlist?.get(msg.arg1)?.textOn}的第${msg.arg2}页失败,尝试重新下载...", Toast.LENGTH_SHORT).show() PAGE_DOWNLOAD_RETRYING -> Toast.makeText(d, "下载${d?.tbtnlist?.get(chapterIndex)?.textOn}的第${pageNumber}页失败,尝试重新下载...", Toast.LENGTH_SHORT).show()
} }
} }
private fun setSize(pageNow: Int, tbtnNo: Int){ private fun setSize(pageNow: Int, tbtnNo: Int){
@@ -110,4 +112,17 @@ class DlHandler(activity: DlActivity, looper: Looper) : Handler(looper) {
refreshSize = false refreshSize = false
}else if(pageNow == size) refreshSize = true }else if(pageNow == size) refreshSize = true
} }
companion object {
const val INITIALIZE_LAYOUTS = -2
const val CHAPTER_DOWNLOAD_FAILED = -1
const val CHAPTER_DOWNLOAD_SUCCEEDED = 1
const val TOGGLE_SELECT_ALL = 4
const val PAGE_DOWNLOAD_FINISHED = 5
const val UPDATE_CHAPTER_PROGRESS = 6
const val DELETE_SELECTED_CHAPTERS = 7
const val SET_DOWNLOAD_CARD_BLUE = 8
const val SET_DOWNLOAD_CARD_RED = 9
const val PAGE_DOWNLOAD_RETRYING = 10
}
} }

View File

@@ -1,21 +0,0 @@
package top.fumiama.copymangaweb.handler
import android.os.Handler
import android.os.Looper
import android.os.Message
import top.fumiama.copymangaweb.activity.DlListActivity
import java.io.File
import java.lang.ref.WeakReference
class DlLHandler(looper: Looper, activity: DlListActivity): Handler(looper) {
private val dll = WeakReference(activity)
override fun handleMessage(msg: Message) {
super.handleMessage(msg)
when(msg.what){
1 -> dll.get()?.checkDir(msg.obj as File)
2 -> dll.get()?.rmrf(msg.obj as File)
3 -> dll.get()?.scanFile(msg.obj as File)
}
}
}

View File

@@ -28,12 +28,17 @@ class MainHandler(looper: Looper): Handler(looper) {
} }
SET_LOADING_DIALOG_TEXT -> { SET_LOADING_DIALOG_TEXT -> {
val t = msg.obj as? String?:return val t = msg.obj as? String?:return
dialog?.findViewById<TextView>(R.id.tunz)?.apply { post { dialog?.findViewById<TextView>(R.id.tunz)?.text = t
text = t
} }
} }
} }
} }
fun dispose() {
removeCallbacksAndMessages(null)
dialog?.dismiss()
dialog = null
}
companion object { companion object {
const val SHOW_LOADING_DIALOG = 7 const val SHOW_LOADING_DIALOG = 7
const val HIDE_LOADING_DIALOG = 8 const val HIDE_LOADING_DIALOG = 8

View File

@@ -1,17 +0,0 @@
package top.fumiama.copymangaweb.handler
import android.os.Handler
class TimeThread(private val handler: Handler, private val msg: Int) : Thread() {
var canDo = false
override fun run() {
while (canDo) {
try {
handler.sendEmptyMessage(msg)
sleep(3000)
} catch (e: InterruptedException) {
e.printStackTrace()
}
}
}
}

View File

@@ -3,35 +3,31 @@ package top.fumiama.copymangaweb.tool
import android.util.Log import android.util.Log
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.URL import java.net.URL
import java.util.concurrent.Callable
import java.util.concurrent.FutureTask
class DownloadTools { class DownloadTools {
fun getHttpContent(u: String, refer: String? = null, ua: String? = null): ByteArray? { fun getHttpContent(u: String, refer: String? = null, ua: String? = null): ByteArray? {
Log.d("Mydl", "getHttp: $u") Log.d("Mydl", "getHttp: $u")
var ret: ByteArray? = null val connection = try {
val task = FutureTask(Callable { URL(u).openConnection() as HttpURLConnection
try { } catch (e: Exception) {
val connection = URL(u).openConnection() as HttpURLConnection Log.e("Mydl", "Unable to open connection: $u", e)
connection.requestMethod = "GET" return null
connection.connectTimeout = 10000 }
connection.readTimeout = 10000
refer?.let { connection.setRequestProperty("referer", it) }
ua?.let { connection.setRequestProperty("User-agent", it) }
ret = connection.inputStream.readBytes()
connection.disconnect()
} catch (ex: Exception) {
ex.printStackTrace()
}
return@Callable ret
})
Thread(task).start()
return try { return try {
task.get() connection.run {
} catch (ex: Exception) { requestMethod = "GET"
ex.printStackTrace() connectTimeout = 10000
readTimeout = 10000
refer?.let { setRequestProperty("referer", it) }
ua?.let { setRequestProperty("User-agent", it) }
inputStream.use { it.readBytes() }
}
} catch (e: Exception) {
Log.e("Mydl", "Download failed: $u", e)
null null
} finally {
connection.disconnect()
} }
} }
} }

View File

@@ -4,63 +4,86 @@ import android.content.Intent
import android.widget.Toast import android.widget.Toast
import top.fumiama.copymangaweb.activity.MainActivity.Companion.wm import top.fumiama.copymangaweb.activity.MainActivity.Companion.wm
import top.fumiama.copymangaweb.activity.ViewMangaActivity import top.fumiama.copymangaweb.activity.ViewMangaActivity
import java.io.File
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
class PagesManager(w: WeakReference<ViewMangaActivity>) { class PagesManager(w: WeakReference<ViewMangaActivity>) {
val v = w.get() private val activity = w
private val v get() = activity.get()
private var isEndL = false private var isEndL = false
private var isEndR = false private var isEndR = false
fun toPreviousPage(){ toPage(v?.r2l==true) } fun toPreviousPage(){ toPage(v?.r2l==true) }
fun toNextPage(){ toPage(v?.r2l!=true) } fun toNextPage(){ toPage(v?.r2l!=true) }
fun goBackward() = toPage(false)
fun goForward() = toPage(true)
private fun judgePrevious() = (v?.pageNum ?: 0) > 1 private fun judgePrevious() = (v?.pageNum ?: 0) > 1
private fun judgeNext() = (v?.pageNum ?: 0) < (v?.count ?: 0) private fun judgeNext() = (v?.pageNum ?: 0) < (v?.count ?: 0)
private fun toPage(goNext:Boolean){ private fun toPage(goNext:Boolean){
if (v?.clicked == false) { if (v?.clicked == false) {
if (if(goNext)judgeNext() else judgePrevious()) { if (if(goNext)judgeNext() else judgePrevious()) {
if(goNext) { if(goNext) {
v.scrollForward() v?.scrollForward()
isEndR = false isEndR = false
} else { } else {
v.scrollBack() v?.scrollBack()
isEndL = false isEndL = false
} }
} else { } else {
val chapterUrl = if(goNext) ViewMangaActivity.nextChapterUrl else ViewMangaActivity.previousChapterUrl if (v?.dlZip2View == true) {
if (chapterUrl != null) { switchZipChapter(goNext)
} else {
val chapterUrl = if(goNext) ViewMangaActivity.nextChapterUrl else ViewMangaActivity.previousChapterUrl
if (chapterUrl == null) {
showReachedEnd()
return
}
if (if(goNext)isEndR else isEndL) { if (if(goNext)isEndR else isEndL) {
if(!goNext) ViewMangaActivity.pn = -2 setChapterStartPage(goNext)
wm?.get()?.mBinding?.w?.apply { post { wm?.get()?.mBinding?.w?.apply { post {
loadUrl("javascript:invoke.clickClass(\"comicControlBottomTopClick\",${if(goNext)1 else 0});") loadUrl("javascript:invoke.clickClass(\"comicControlBottomTopClick\",${if(goNext)1 else 0});")
} } } }
v.tt.canDo = false v?.finish()
v.finish()
} else doubleTapToast(goNext) } else doubleTapToast(goNext)
} else {
val newZipPosition = ViewMangaActivity.zipPosition + (if(goNext) 1 else -1)
if(v.dlZip2View && newZipPosition >= 0 && newZipPosition <
(ViewMangaActivity.zipList?.size ?: 0)){
if (if(goNext)isEndR else isEndL){
if(!goNext) ViewMangaActivity.pn = -2
ViewMangaActivity.zipPosition = newZipPosition
ViewMangaActivity.titleText = ViewMangaActivity.zipList?.get(newZipPosition) ?: "null"
ViewMangaActivity.zipFile = File(ViewMangaActivity.cd, ViewMangaActivity.titleText)
v.startActivity(Intent(v, ViewMangaActivity::class.java))
v.tt.canDo = false
v.finish()
}else doubleTapToast(goNext)
}
else Toast.makeText(
v.applicationContext,
"已经到头了~",
Toast.LENGTH_SHORT
).show()
} }
} }
} else v?.hideSettings() } else v?.hideSettings()
} }
private fun switchZipChapter(goNext: Boolean) {
val chapters = ViewMangaActivity.zipList.orEmpty()
val newPosition = ViewMangaActivity.zipPosition + if (goNext) 1 else -1
val chapter = chapters.getOrNull(newPosition)
if (chapter == null) {
showReachedEnd()
return
}
if (!(if (goNext) isEndR else isEndL)) {
doubleTapToast(goNext)
return
}
val reader = v ?: return
setChapterStartPage(goNext)
ViewMangaActivity.zipPosition = newPosition
ViewMangaActivity.titleText = chapter.nameWithoutExtension
ViewMangaActivity.zipFile = chapter
reader.startActivity(Intent(reader, ViewMangaActivity::class.java))
reader.finish()
}
private fun showReachedEnd() {
Toast.makeText(v?.applicationContext, "已经到头了~", Toast.LENGTH_SHORT).show()
}
private fun setChapterStartPage(goNext: Boolean) {
ViewMangaActivity.pn = if (goNext) {
ViewMangaActivity.FIRST_PAGE
} else {
ViewMangaActivity.LAST_PAGE
}
}
fun manageInfo(){ fun manageInfo(){
if (v?.clicked == false) v.showSettings() else v?.hideSettings() if (v?.clicked == false) v?.showSettings() else v?.hideSettings()
} }
private fun doubleTapToast(goNext: Boolean){ private fun doubleTapToast(goNext: Boolean){
val hint = if(goNext) "" else "" val hint = if(goNext) "" else ""

View File

@@ -15,8 +15,6 @@ import android.view.GestureDetector.SimpleOnGestureListener
import android.view.MotionEvent import android.view.MotionEvent
import android.widget.ImageView import android.widget.ImageView
import top.fumiama.copymangaweb.activity.ViewMangaActivity import top.fumiama.copymangaweb.activity.ViewMangaActivity
import top.fumiama.copymangaweb.tool.PagesManager
import java.lang.ref.WeakReference
import java.util.* import java.util.*
import kotlin.math.sqrt import kotlin.math.sqrt
@@ -26,6 +24,28 @@ import kotlin.math.sqrt
* @author clifford * @author clifford
*/ */
class ScaleImageView : ImageView { class ScaleImageView : ImageView {
enum class TapRegion {
PREVIOUS,
CENTER,
NEXT,
}
private var onTapRegionListener: ((TapRegion) -> Unit)? = null
fun setOnTapRegionListener(listener: ((TapRegion) -> Unit)?) {
onTapRegionListener = listener
}
fun resetImageTransform() {
cancelAllAnimator()
mOuterMatrix.reset()
pinchMode = PINCH_MODE_FREE
mLastMovePoint.set(0f, 0f)
mScaleCenter.set(0f, 0f)
mScaleBase = 0f
invalidate()
}
////////////////////////////////监听器//////////////////////////////// ////////////////////////////////监听器////////////////////////////////
/** /**
* 外界点击事件 * 外界点击事件
@@ -672,24 +692,14 @@ class ScaleImageView : ImageView {
return true return true
} }
var v :WeakReference<ViewMangaActivity>? = null
var pm:PagesManager? = null
override fun onSingleTapConfirmed(event: MotionEvent): Boolean { override fun onSingleTapConfirmed(event: MotionEvent): Boolean {
if(v == null) { mOnClickListener?.onClick(this@ScaleImageView)
v = ViewMangaActivity.va val region = when {
v?.let { pm = PagesManager(it) } width <= 0 || event.x <= width / 3f -> TapRegion.PREVIOUS
} event.x <= width * 2f / 3f -> TapRegion.CENTER
//触发点击 else -> TapRegion.NEXT
if (mOnClickListener != null) {
mOnClickListener!!.onClick(this@ScaleImageView)
}
(event.x / width).let {
when {
it <= 1.0 / 3.0 -> pm?.toPreviousPage()
it <= 2.0 / 3.0 -> pm?.manageInfo()
else -> pm?.toNextPage()
}
} }
onTapRegionListener?.invoke(region)
return true return true
} }
}) })
@@ -739,6 +749,7 @@ class ScaleImageView : ImageView {
} else if (action == MotionEvent.ACTION_POINTER_DOWN) { } else if (action == MotionEvent.ACTION_POINTER_DOWN) {
//停止所有动画 //停止所有动画
cancelAllAnimator() cancelAllAnimator()
parent.requestDisallowInterceptTouchEvent(true)
//切换到缩放模式 //切换到缩放模式
pinchMode = PINCH_MODE_SCALE pinchMode = PINCH_MODE_SCALE
//保存缩放的两个手指 //保存缩放的两个手指

View File

@@ -2,7 +2,6 @@ package top.fumiama.copymangaweb.web
import android.util.Log import android.util.Log
import android.webkit.JavascriptInterface import android.webkit.JavascriptInterface
import top.fumiama.copymangaweb.activity.DlActivity
import top.fumiama.copymangaweb.activity.MainActivity.Companion.mh 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
@@ -60,13 +59,8 @@ class JSHidden(
} }
@JavascriptInterface @JavascriptInterface
fun setTitle(title:String){ fun setFab(content: String, sourceUrl: String, comicTitle: String){
Log.d("MyJSH", "Set title: $title") wm?.get()?.setFab(content, sourceUrl, comicTitle)
DlActivity.comicName = title
}
@JavascriptInterface
fun setFab(content: String){
wm?.get()?.setFab(content)
} }
@JavascriptInterface @JavascriptInterface
fun setLoadingDialog(display: Boolean) { fun setLoadingDialog(display: Boolean) {

View File

@@ -51,10 +51,9 @@ class WebChromeClient:WebChromeClient() {
filePathCallback: ValueCallback<Array<Uri>>?, filePathCallback: ValueCallback<Array<Uri>>?,
fileChooserParams: FileChooserParams? fileChooserParams: FileChooserParams?
): Boolean { ): Boolean {
wm?.get()?.apply { val callback = filePathCallback ?: return false
uploadMessageAboveL = filePathCallback val activity = wm?.get() ?: return false
openImageChooserActivity() activity.openImageChooserActivity(callback)
}
return true return true
} }
} }

View File

@@ -25,6 +25,19 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/continuousPages"
android:layout_width="0dp"
android:layout_height="0dp"
android:clipToPadding="false"
android:overScrollMode="never"
android:visibility="gone"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.viewpager2.widget.ViewPager2 <androidx.viewpager2.widget.ViewPager2
android:id="@+id/vp" android:id="@+id/vp"
android:layout_width="0dp" android:layout_width="0dp"

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<top.fumiama.copymangaweb.view.ScaleImageView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/pageImage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:adjustViewBounds="true"
android:contentDescription="@null" />

View File

@@ -1,8 +1,8 @@
<resources> <resources>
<string name="app_name">拷贝Lite</string> <string name="app_name">拷贝Lite</string>
<string name="web_home">https://www.copy20.com</string> <string name="web_home">https://www.copy3000.com</string>
<string name="web_home_www">https://www.copy20.com</string> <string name="web_home_www">https://www.copy3000.com</string>
<string name="web_comic_detail_pc">https://www.copy20.com/comic</string> <string name="web_comic_detail_pc">https://www.copy3000.com/comic</string>
<string name="pc_ua">Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.75 Safari/537.36 Edg/86.0.622.38</string> <string name="pc_ua">Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.75 Safari/537.36 Edg/86.0.622.38</string>