1
0
mirror of https://github.com/fumiama/copymanga.git synced 2026-06-10 02:00:25 +08:00

2.0.beta2

1. 修复最多只能加载20话
2. 增加检查更新
This commit is contained in:
fumiama
2021-05-07 17:16:10 +08:00
parent 6f4dd9c34c
commit be810e1408
21 changed files with 480 additions and 63 deletions

View File

@@ -5,6 +5,7 @@
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"

View File

@@ -34,7 +34,9 @@ import kotlinx.android.synthetic.main.app_bar_main.*
import kotlinx.android.synthetic.main.nav_header_main.*
import top.fumiama.dmzj.copymanga.R
import top.fumiama.copymanga.tools.PropertiesTools
import top.fumiama.copymanga.tools.UITools
import top.fumiama.copymanga.ui.download.DownloadFragment
import top.fumiama.copymanga.update.Update
import java.io.File
import java.io.FileInputStream
import java.lang.ref.WeakReference
@@ -88,6 +90,7 @@ class MainActivity : AppCompatActivity() {
override fun onDrawerSlide(drawerView: View, slideOffset: Float) {}
override fun onDrawerStateChanged(newState: Int) {}
})
checkUpdate(false)
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
@@ -228,6 +231,11 @@ class MainActivity : AppCompatActivity() {
)
)
private fun checkUpdate(ignoreSkip: Boolean) {
Thread{
Update.checkUpdate(this, p, UITools(this), ignoreSkip)
}.start()
}
fun showAbout(item: MenuItem) {
val dl = android.app.AlertDialog.Builder(this)
@@ -235,6 +243,9 @@ class MainActivity : AppCompatActivity() {
dl.setTitle(R.string.action_info)
dl.setIcon(R.mipmap.ic_launcher)
dl.setPositiveButton(android.R.string.ok) { _, _ -> }
dl.setNeutralButton(R.string.check_update) {_, _ ->
checkUpdate(true)
}
dl.show()
}

View File

@@ -3,4 +3,5 @@ package top.fumiama.copymanga.json;
public class ThemeStructure {
public String name;
public String path_word;
public int count;
}

View File

@@ -15,4 +15,5 @@ object CMApi {
fun getImgZipFileFromVM(exDir: File?, chapter2Return: Chapter2Return?) = File(exDir, "${chapter2Return?.results?.comic?.name}/${chapter2Return?.results?.chapter?.group_path_word}/${chapter2Return?.results?.chapter?.name}.zip")
fun getZipFile(exDir: File?, manga: String, caption: CharSequence, name: CharSequence) = File(exDir, "$manga/$caption/$name.zip")
fun getApiUrl(id: Int, arg1: String?, arg2: String?) = MainActivity.mainWeakReference?.get()?.getString(id)?.let { String.format(it, arg1, arg2) }
fun getApiUrl(id: Int, arg1: String?, arg2: String?, arg3: Int? = 0) = MainActivity.mainWeakReference?.get()?.getString(id)?.let { String.format(it, arg1, arg2, arg3) }
}

View File

@@ -1,52 +1,63 @@
package top.fumiama.copymanga.tools
//PropertiesTools.kt
//created by fumiama 20200724
import android.util.Log
import java.io.File
import java.io.InputStream
import java.util.*
class PropertiesTools(private val f: File):Properties() {
private val propfile:File
get() {
if(!f.exists()) {
if(f.parentFile?.exists() != true) f.parentFile?.mkdirs()
if(f.parentFile?.canWrite() != true) f.parentFile?.setWritable(true)
createNew(f)
}else if(f.isDirectory) {
if(f.parentFile?.canWrite() != true) f.parentFile?.setWritable(true)
f.delete()
createNew(f)
}
private var cache = hashMapOf<String, String>()
init {
if(!f.exists()) {
if(f.parentFile?.exists() != true) f.parentFile?.mkdirs()
if(f.parentFile?.canWrite() != true) f.parentFile?.setWritable(true)
if(f.parentFile?.canRead() != true) f.parentFile?.setReadable(true)
return f
createNew(f)
} else if(f.isDirectory) {
if(f.parentFile?.canWrite() != true) f.parentFile?.setWritable(true)
f.delete()
createNew(f)
}
private fun createNew(f: File){
if(f.parentFile?.canWrite() != true) f.parentFile?.setWritable(true)
if(f.parentFile?.canRead() != true) f.parentFile?.setReadable(true)
}
private fun createNew(f: File) {
f.createNewFile()
val o = f.outputStream()
this.storeToXML(o, "store")
//Log.d("MyPT", "Generate new prop.")
Log.d("MyPT", "Generate new prop.")
o.close()
}
private fun loadFromXml(`in`: InputStream?): PropertiesTools {
this.loadFromXML(`in`)
return this
}
private fun setProp(key: String?, value: String?): PropertiesTools {
this.setProperty(key, value)
return this
}
operator fun get(key: String): String{
val i = propfile.inputStream()
val re = this.loadFromXml(i).getProperty(key)?:"null"
//Log.d("MyPT", "Get prop: $re")
i.close()
return re
return if(cache.containsKey(key)) cache[key]?:"null"
else {
val i = f.inputStream()
val re = this.loadFromXml(i).getProperty(key)?:"null"
Log.d("MyPT", "Read $key = $re")
i.close()
cache[key] = re
re
}
}
operator fun set(key: String, value: String){
val o = propfile.outputStream()
operator fun set(key: String, value: String) {
cache[key] = value
val o = f.outputStream()
this.setProp(key, value).storeToXML(o, "store")
//Log.d("MyPT", "Set $key = $value")
Log.d("MyPT", "Set $key = $value")
o.close()
}
}

View File

@@ -5,6 +5,7 @@ import android.app.AlertDialog
import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.view.View
import android.widget.Toast
import top.fumiama.dmzj.copymanga.R
import java.lang.ref.WeakReference
@@ -53,6 +54,26 @@ class UITools(that: Context?, w: WeakReference<Activity>? = null) {
txtN?.let { info.setNeutralButton(it) { _, _ -> neutral?.let { it() } } }
info.show()
}
fun buildAlertWithView(
title: String,
view: View,
txtOk: String? = null,
txtN: String? = null,
txtCancel: String? = null,
ok: (() -> Unit)? = null,
neutral: (() -> Unit)? = null,
cancel: (() -> Unit)? = null
): AlertDialog {
val info = AlertDialog.Builder(zis)
info.setIcon(R.drawable.ic_launcher_foreground)
info.setTitle(title)
info.setView(view)
txtOk?.let { info.setPositiveButton(it) { _, _ -> ok?.let { it() } } }
txtCancel?.let { info.setNegativeButton(it) { _, _ -> cancel?.let { it() } } }
txtN?.let { info.setNeutralButton(it) { _, _ -> neutral?.let { it() } } }
return info.show()
}
fun dp2px(dp:Int):Int?{
return zis?.resources?.displayMetrics?.density?.let { (dp * it + 0.5).toInt()}
}
@@ -85,4 +106,14 @@ class UITools(that: Context?, w: WeakReference<Activity>? = null) {
val totalWidth = ((zis?.resources?.displayMetrics?.widthPixels?:1080)-marginPx)/numPerRow
return listOf(numPerRow, w, totalWidth)
}
fun toHexStr(byteArray: ByteArray) =
with(StringBuilder()) {
byteArray.forEach {
val hex = it.toInt() and (0xFF)
val hexStr = Integer.toHexString(hex)
if (hexStr.length == 1) append("0").append(hexStr)
else append(hexStr)
}
toString()
}
}

View File

@@ -61,12 +61,16 @@ class BookFragment:NoBackRefreshFragment(R.layout.fragment_book) {
val groups = bookHandler.book?.results?.groups
var keys = arrayOf<String>()
var gpws = arrayOf<String>()
var cnts = intArrayOf()
groups?.values?.forEach {
keys += it.name
gpws += it.path_word
cnts += it.count
Log.d("MyBF", "Add caption: ${it.name} @ ${it.path_word} of ${it.count}")
}
bundle.putStringArray("group", gpws)
bundle.putStringArray("groupNames", keys)
bundle.putIntArray("count", cnts)
rootView?.let { Navigation.findNavController(it).navigate(R.id.action_nav_book_to_nav_group, bundle) }
}
}

View File

@@ -10,6 +10,7 @@ import android.view.View
import com.google.gson.Gson
import top.fumiama.dmzj.copymanga.R
import top.fumiama.copymanga.MainActivity.Companion.mainWeakReference
import top.fumiama.copymanga.json.ChapterStructure
import top.fumiama.copymanga.json.VolumeStructure
import top.fumiama.copymanga.template.AutoDownloadThread
import top.fumiama.copymanga.template.NoBackRefreshFragment
@@ -32,7 +33,8 @@ class ComicDlFragment:NoBackRefreshFragment(R.layout.fragment_dlcomic) {
}
else -> initComicData(
arguments?.getString("path"),
arguments?.getStringArray("group")
arguments?.getStringArray("group"),
arguments?.getIntArray("count")
)
}
}
@@ -44,7 +46,7 @@ class ComicDlFragment:NoBackRefreshFragment(R.layout.fragment_dlcomic) {
mainWeakReference?.get()?.menuMain?.let { setMenuInvisible(it) }
}*/
fun start2load(volumes: Array<VolumeStructure>, isFromFile: Boolean = false, groupArray: Array<String>? =null){
private fun start2load(volumes: Array<VolumeStructure>, isFromFile: Boolean = false, groupArray: Array<String>? =null){
handler = ComicDlHandler(Looper.myLooper()!!,
WeakReference(this),
volumes,
@@ -85,21 +87,42 @@ class ComicDlFragment:NoBackRefreshFragment(R.layout.fragment_dlcomic) {
menu.findItem(R.id.action_download)?.isVisible = false
}*/
private fun initComicData(pw: String?, gpws: Array<String>?) {
var volumes = arrayOf<VolumeStructure>()
val waitHandler = WaitHandler(WeakReference(this))
private fun initComicData(pw: String?, gpws: Array<String>?, counts: IntArray?) {
var volumes = emptyArray<VolumeStructure>()
if (gpws != null) {
gpws.forEach { gpw ->
gpws.forEachIndexed { i, gpw ->
Log.d("MyCDF", "下载:$gpw")
CMApi.getApiUrl(R.string.groupInfoApiUrl, pw, gpw)?.let {
AutoDownloadThread(it) { result ->
//Log.d("MyCDF", "返回:${result?.decodeToString()}")
volumes += Gson().fromJson(
result?.decodeToString(),
VolumeStructure::class.java
)
}.start()
}
var offset = 0
val re = arrayOfNulls<VolumeStructure>(counts?.get(i)?:1)
do {
counts?.set(i, counts[i] - 100)
CMApi.getApiUrl(R.string.groupInfoApiUrl, pw, gpw, offset)?.let {
AutoDownloadThread(it) { result ->
//Log.d("MyCDF", "返回:${result?.decodeToString()}")
val r = Gson().fromJson(result?.decodeToString(), VolumeStructure::class.java)
re[r.results.offset / 100] = r
}.start()
offset += 100
}
} while ((counts?.get(i) ?: 0) > 0)
Thread {
var c = 0
while (c++ < 80) {
sleep(100)
if(re.all { it != null }) break
}
if(re.size > 1) {
val r = re[0]
var s = emptyArray<ChapterStructure>()
re.forEach {
it?.results?.list?.forEach {
s += it
}
}
r?.results?.list = s
r?.apply { volumes += this }
} else re[0]?.apply { volumes += this }
}.start()
}
Thread {
var c = 0
@@ -109,7 +132,9 @@ class ComicDlFragment:NoBackRefreshFragment(R.layout.fragment_dlcomic) {
c++
}
if (volumes.size == gpws.size) {
waitHandler.obtainMessage(0, volumes).sendToTarget()
mainWeakReference?.get()?.runOnUiThread {
start2load(volumes)
}
}
}.start()
}
@@ -122,15 +147,6 @@ class ComicDlFragment:NoBackRefreshFragment(R.layout.fragment_dlcomic) {
handler?.startLoad()
}
class WaitHandler(private val that: WeakReference<ComicDlFragment>): Handler(Looper.myLooper()!!){
override fun handleMessage(msg: Message) {
super.handleMessage(msg)
when(msg.what){
0 -> that.get()?.start2load(msg.obj as Array<VolumeStructure>)
}
}
}
companion object {
var json: String? = null
}

View File

@@ -0,0 +1,139 @@
package top.fumiama.copymanga.update
import android.util.Log
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.net.Socket
class Client(private val ip: String, private val port: Int) {
//普通数据交互接口
private var sc: Socket? = null
//普通交互流
private var dout: OutputStream? = null
private var din: InputStream? = null
//已连接标记
private val isConnect get() = sc != null && din != null && dout != null
/**
* 初始化普通交互连接
*/
fun initConnect(depth: Int = 0): Boolean{
if(depth > 3) Log.d("MyC", "connect server failed after $depth tries")
else try {
sc = Socket(ip, port) //通过socket连接服务器
din = sc?.getInputStream() //获取输入流并转换为StreamReader约定编码格式
dout = sc?.getOutputStream() //获取输出流
sc?.soTimeout = 2333 //设置连接超时限制
return if (isConnect) {
Log.d("MyC", "connect server successful")
true
} else {
Log.d("MyC", "connect server failed, now retry...")
initConnect(depth + 1)
}
} catch (e: IOException) { //获取输入输出流是可能报IOException的所以必须try-catch
e.printStackTrace()
}
return false
}
/**
* 发送数据至服务器
* @param message 要发送至服务器的字符串
*/
fun sendMessage(message: CharSequence?): Boolean {
try {
if (isConnect) {
if (message != null) { //判断输出流或者消息是否为空为空的话会产生nullpoint错误
dout?.write(message.toString().toByteArray())
dout?.flush()
Log.d("MyC", "Send msg: $message")
return true
} else Log.d("MyC", "The message to be sent is empty")
Log.d("MyC", "send message succeed")
} else Log.d("MyC", "send message failed: no connect")
} catch (e: IOException) {
Log.d("MyC", "send message failed: crash")
e.printStackTrace()
}
return false
}
var buffer = byteArrayOf()
fun receiveRawMessage(totalSize: Int = -1, bufferSize: Int = 1048576, setProgress: Boolean = false) : ByteArray {
if(totalSize == buffer.size) {
val re = buffer
buffer = byteArrayOf()
return re
} else {
var re = byteArrayOf()
try {
if (isConnect) {
Log.d("MyC", "开始接收服务端信息")
val inMessage = ByteArray(bufferSize) //设置接受缓冲,避免接受数据过长占用过多内存
var a: Int
do {
a = din?.read(inMessage)?:0 //a存储返回消息的长度
if(a > 0) {
re += inMessage.copyOf(a)
Log.d("MyC", "reply length:$a")
if(totalSize < 0 && a < bufferSize) break
else if(setProgress && totalSize > 0) progress?.notify(100 * re.size / totalSize)
} else break
} while (totalSize > re.size)
} else Log.d("MyC", "no connect to receive message")
} catch (e: IOException) {
Log.d("MyC", "receive message failed")
e.printStackTrace()
}
if(totalSize > 0 && re.size > totalSize) {
Log.d("MyC", "Reduce re size from ${re.size} to $totalSize")
buffer += re.copyOfRange(totalSize, re.size)
re = re.copyOf(totalSize)
} else if(totalSize > 0 && buffer.isNotEmpty()) {
Log.d("MyC", "Increase re size.")
buffer += re
if(buffer.size > totalSize) {
re = buffer.copyOf(totalSize)
buffer = buffer.copyOfRange(totalSize, buffer.size)
} else {
re = buffer
buffer = byteArrayOf()
}
} else if(totalSize < 0 && buffer.isNotEmpty()) {
re = buffer
buffer = byteArrayOf()
Log.d("MyC", "clear buffer")
}
return re
}
}
//fun receiveMessage() = receiveRawMessage().decodeToString()
/**
* 关闭连接
*/
fun closeConnect() = try {
din?.close()
dout?.close()
sc?.close()
sc = null
din = null
dout = null
true
} catch (e: IOException) {
e.printStackTrace()
false
}
var progress: Progress? = null
interface Progress {
fun notify(progressPercentage: Int)
}
}

View File

@@ -0,0 +1,68 @@
package top.fumiama.copymanga.update
import android.util.Log
class SimpleKanban(private val client: Client, private val pwd: String) { //must run in thread
private val raw: ByteArray?
get() {
var times = 3
var re: ByteArray
var firstRecv: ByteArray
do {
re = byteArrayOf()
if(client.initConnect()) {
client.sendMessage("${pwd}catquit")
client.receiveRawMessage(33) //Welcome to simple kanban server.
try {
firstRecv = client.receiveRawMessage(4) //le
val length = convert2Int(firstRecv)
if(firstRecv.size > 4) re += firstRecv.copyOfRange(4, firstRecv.size)
re += client.receiveRawMessage(length - re.size, setProgress = true)
break
} catch (e: Exception) {
e.printStackTrace()
}
client.closeConnect()
}
} while (times-- > 0)
return if(re.isEmpty()) null else re
}
private fun convert2Int(buffer: ByteArray) =
(buffer[3].toInt() and 0xff shl 24) or
(buffer[2].toInt() and 0xff shl 16) or
(buffer[1].toInt() and 0xff shl 8) or
(buffer[0].toInt() and 0xff)
fun fetchRaw(doOnLoadFailure: ()->Unit = {
Log.d("MySD", "Fetch dict failed")
}, doOnLoadSuccess: (data: ByteArray)->Unit = {
Log.d("MySD", "Fetch dict success")
}) {
raw?.apply {
doOnLoadSuccess(this)
}?:doOnLoadFailure()
}
operator fun get(version: Int): String =
if(client.initConnect()) {
client.sendMessage("${pwd}get${version}quit")
client.receiveRawMessage(36) //Welcome to simple kanban server. get
val r = try {
val firstRecv = client.receiveRawMessage(4)
if(firstRecv.decodeToString() == "null") "null"
else {
val length = convert2Int(firstRecv)
var re = byteArrayOf()
if(firstRecv.size > 4) re += firstRecv.copyOfRange(4, firstRecv.size)
re += client.receiveRawMessage(length - re.size)
if(re.isNotEmpty()) re.decodeToString() else "null"
}
} catch (e: Exception){
e.printStackTrace()
"null"
}
client.closeConnect()
r
} else "null"
}

View File

@@ -0,0 +1,87 @@
package top.fumiama.copymanga.update
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.util.Log
import android.widget.Toast
import androidx.core.content.FileProvider
import kotlinx.android.synthetic.main.dialog_progress.view.*
import top.fumiama.copymanga.tools.PropertiesTools
import top.fumiama.copymanga.tools.UITools
import top.fumiama.dmzj.copymanga.R
import java.io.File
import java.security.MessageDigest
object Update {
fun checkUpdate(activity: Activity, p: PropertiesTools, toolsBox: UITools, ignoreSkip: Boolean = false) = activity.apply{
val client = Client("copymanga.v6.army", 12315)
val progressBar = layoutInflater.inflate(R.layout.dialog_progress, null, false)
val progressHandler = object : Client.Progress{
override fun notify(progressPercentage: Int) {
Log.d("MyUP", "Set progress: $progressPercentage")
progressBar.dpp.progress = progressPercentage
}
}
val kanban = SimpleKanban(client, "fumiama")
val msg = kanban[packageManager.getPackageInfo(packageName, 0).versionCode]
if(msg != "null") {
val verNum = msg.substringBefore('\n').toIntOrNull()
val skipNum = p["skipVersion"].let { if(it != "null") it.toInt() else 0 }
Log.d("MyUP", "Ver:$verNum, skip: $skipNum")
if(verNum != null) {
if(msg.contains("md5:")) {
if(skipNum < verNum || ignoreSkip) runOnUiThread {
toolsBox.buildInfo("看板", msg.substringAfter('\n').substringBeforeLast('\n'), "下载新版", "跳过该版", "取消", {
val info = toolsBox.buildAlertWithView("下载进度", progressBar, "隐藏")
client.progress = progressHandler
Thread {
kanban.fetchRaw({
runOnUiThread {
Toast.makeText(this, "下载失败", Toast.LENGTH_SHORT).show()
client.progress = null
}
}) {
val md5 = msg.substringAfterLast("md5:")
if (md5 == toolsBox.toHexStr(
MessageDigest.getInstance("MD5").digest(it)
)
) {
runOnUiThread {
Toast.makeText(this, "下载成功", Toast.LENGTH_SHORT).show()
info.dismiss()
}
val f = File(externalCacheDir, "new.apk")
f.writeBytes(it)
install(f, activity)
} else runOnUiThread {
Toast.makeText(this, "文件损坏", Toast.LENGTH_SHORT).show()
info.dismiss()
}
client.progress = null
}
}.start()
}, { p["skipVersion"] = verNum.toString() })
}
} else runOnUiThread {
toolsBox.buildInfo("看板", msg.substringAfter('\n'), "知道了")
}
}
} else if(ignoreSkip) runOnUiThread {
Toast.makeText(this, "无更新", Toast.LENGTH_SHORT).show()
}
}
private fun install(apkFile: File, activity: Activity) = activity.apply{
val intent = Intent(Intent.ACTION_VIEW)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
val contentUri: Uri = FileProvider.getUriForFile(this, "$packageName.fileprovider", apkFile)
intent.setDataAndType(contentUri, "application/vnd.android.package-archive")
} else intent.setDataAndType(Uri.fromFile(apkFile), "application/vnd.android.package-archive")
startActivity(intent)
}
}

View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="24dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="24dp"
android:background="@drawable/rndbg_white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:src="@drawable/ic_dere"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<ProgressBar
android:id="@+id/dpp"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:progress="0"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -15,6 +15,7 @@
<string name="page_book">图书详情</string>
<string name="page_chapter">章节内容</string>
<string name="page_group">漫画下载</string>
<string name="check_update">检查更新</string>
<string name="navTextInfo">illust: Hiten(490219)</string>
<string name="navTextInfoInputHint">请设定提示文字内容</string>
@@ -34,7 +35,7 @@
<string name="filterApiUrl">https://api.copymanga.com/api/v3/h5/filterIndex/comic/tags</string>
<string name="sortApiUrl">https://api.copymanga.com/api/v3/comics?limit=21&amp;offset=%1$d&amp;ordering=%2$s&amp;theme=%3$s</string>
<string name="bookInfoApiUrl">https://api.copymanga.com/api/v3/comic2/%1$s</string>
<string name="groupInfoApiUrl">https://api.copymanga.com/api/v3/comic/%1$s/group/%2$s/chapters</string>
<string name="groupInfoApiUrl">https://api.copymanga.com/api/v3/comic/%1$s/group/%2$s/chapters?limit=100&amp;offset=%3$d</string>
<string name="chapterInfoApiUrl">https://api.copymanga.com/api/v3/comic/%1$s/chapter2/%2$s</string>
<string name="chapterTxtUrl">https://nnv3api.dmzj1.com/novel/download/%1$d_%2$d_%3$d.txt</string>

View File

@@ -1,4 +1,5 @@
<paths>
<external-files-path name="ef" path="."/>
<files-path name="if" path="."/>
<external-cache-path name="ec" path="."/>
</paths>