mirror of
https://github.com/fumiama/copymanga.git
synced 2026-06-04 23:10:23 +08:00
v2.5.8
优化 1. 主页加载速度 2. 默认UA判定 3. TCP Client
This commit is contained in:
1
.idea/dictionaries/fumiama.xml
generated
1
.idea/dictionaries/fumiama.xml
generated
@@ -3,6 +3,7 @@
|
||||
<words>
|
||||
<w>alphae</w>
|
||||
<w>azurewebsites</w>
|
||||
<w>catquit</w>
|
||||
<w>comancry</w>
|
||||
<w>comandy</w>
|
||||
<w>deviceinfo</w>
|
||||
|
||||
@@ -12,8 +12,8 @@ android {
|
||||
minSdkVersion 23
|
||||
//noinspection OldTargetApi
|
||||
targetSdkVersion 34
|
||||
versionCode 80
|
||||
versionName '2.5.7'
|
||||
versionCode 81
|
||||
versionName '2.5.8'
|
||||
resourceConfigurations += ['zh', 'zh-rCN']
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
@@ -124,4 +124,5 @@ dependencies {
|
||||
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'
|
||||
implementation 'com.airbnb.android:lottie:6.6.6'
|
||||
implementation 'net.java.dev.jna:jna:5.17.0@aar'
|
||||
implementation 'top.fumiama:sdict:0.1.0'
|
||||
}
|
||||
|
||||
@@ -63,7 +63,7 @@ import top.fumiama.copymanga.api.update.Update
|
||||
import top.fumiama.copymanga.api.user.Member
|
||||
import top.fumiama.copymanga.lib.Comancry
|
||||
import top.fumiama.copymanga.lib.Comandy
|
||||
import top.fumiama.copymanga.storage.DataLoader
|
||||
import top.fumiama.copymanga.storage.ConfigLoader
|
||||
import top.fumiama.copymanga.strings.Base16384
|
||||
import top.fumiama.dmzj.copymanga.BuildConfig
|
||||
import top.fumiama.dmzj.copymanga.R
|
||||
@@ -225,7 +225,7 @@ class MainActivity : AppCompatActivity() {
|
||||
toolsBox.buildInfo("备份管理", "可选择导出或导入base16384格式配置项",
|
||||
"导出", "导入", "取消", { // ok
|
||||
MaterialDialog(this).show {
|
||||
input(prefill = Base16384.encode(DataLoader().toByteArray()))
|
||||
input(prefill = Base16384.encode(ConfigLoader().toByteArray()))
|
||||
positiveButton(android.R.string.ok)
|
||||
title(null, "请复制配置文本并保存")
|
||||
}
|
||||
@@ -233,7 +233,7 @@ class MainActivity : AppCompatActivity() {
|
||||
MaterialDialog(this).show {
|
||||
input { _, c ->
|
||||
try {
|
||||
DataLoader(Base16384.decode(c.toString())).settings.export()
|
||||
ConfigLoader(Base16384.decode(c.toString())).settings.export()
|
||||
navController?.apply {
|
||||
currentDestination?.id?.let {
|
||||
popBackStack()
|
||||
|
||||
@@ -58,6 +58,7 @@ object Config {
|
||||
|
||||
val proxyUrl = MainActivity.mainWeakReference?.get()?.getString(R.string.proxyUrl)!!
|
||||
val pc_ua get() = MainActivity.mainWeakReference?.get()?.getString(R.string.pc_ua)?.format(app_ver.value)?:""
|
||||
val default_ua get() = MainActivity.mainWeakReference?.get()?.getString(R.string.default_ua)?:""
|
||||
val referer get() = MainActivity.mainWeakReference?.get()?.getString(R.string.referer)?.format(app_ver.value)?:""
|
||||
|
||||
val navTextInfo = UserPreferenceString("navTextInfo", R.string.navTextInfo)
|
||||
@@ -95,7 +96,7 @@ object Config {
|
||||
val net_img_resolution = PreferenceString(R.string.imgResolutionKeyID)
|
||||
val net_umstring = PreferenceString("settings_cat_net_et_umstring")
|
||||
val net_source = PreferenceString("settings_cat_net_et_source", R.string.source)
|
||||
val net_ua = PreferenceString("settings_cat_net_et_ua", "__default_ua__")
|
||||
val net_ua = PreferenceString("settings_cat_net_et_ua", R.string.default_ua)
|
||||
|
||||
val view_manga_inverse_chapters = PreferenceBoolean("settings_cat_vm_sw_inverse_chapters", false)
|
||||
val view_manga_always_dark_bg = PreferenceBoolean("settings_cat_vm_sw_always_dark_bg", false)
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
package top.fumiama.copymanga.api.update
|
||||
//Fumiama 20210601
|
||||
//ByteArrayQueue.kt
|
||||
//FIFO队列
|
||||
class ByteArrayQueue {
|
||||
private var elements = byteArrayOf()
|
||||
val size get() = elements.size
|
||||
fun append(items: ByteArray) {
|
||||
elements += items
|
||||
}
|
||||
fun pop(num: Int = 1): ByteArray? {
|
||||
return if(num <= elements.size) {
|
||||
val re = elements.copyOfRange(0, num)
|
||||
elements = elements.copyOfRange(num, elements.size)
|
||||
re
|
||||
} else null
|
||||
}
|
||||
fun clear() {
|
||||
elements = byteArrayOf()
|
||||
}
|
||||
fun popAll(): ByteArray {
|
||||
val re = elements
|
||||
clear()
|
||||
return re
|
||||
}
|
||||
operator fun plusAssign(items: ByteArray) = append(items)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package top.fumiama.copymanga.api.update
|
||||
|
||||
import android.util.Log
|
||||
import top.fumiama.copymanga.net.Client
|
||||
import top.fumiama.sdict.io.Client
|
||||
|
||||
class SimpleKanban(private val client: Client, private val pwd: String) { //must run in thread
|
||||
private val raw: ByteArray?
|
||||
@@ -51,13 +51,13 @@ class SimpleKanban(private val client: Client, private val pwd: String) { //mu
|
||||
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"
|
||||
val firstReceive = client.receiveRawMessage(4)
|
||||
if(firstReceive.decodeToString() == "null") "null"
|
||||
else {
|
||||
val length = convert2Int(firstRecv)
|
||||
val length = convert2Int(firstReceive)
|
||||
Log.d("MySK", "Msg len: $length")
|
||||
var re = byteArrayOf()
|
||||
if(firstRecv.size > 4) re += firstRecv.copyOfRange(4, firstRecv.size)
|
||||
if(firstReceive.size > 4) re += firstReceive.copyOfRange(4, firstReceive.size)
|
||||
re += client.receiveRawMessage(length - re.size)
|
||||
if(re.isNotEmpty()) re.decodeToString() else "null"
|
||||
}
|
||||
|
||||
@@ -16,10 +16,11 @@ import kotlinx.android.synthetic.main.dialog_progress.view.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import top.fumiama.copymanga.net.Client
|
||||
import top.fumiama.sdict.io.Client
|
||||
import top.fumiama.copymanga.view.interaction.UITools
|
||||
import top.fumiama.dmzj.copymanga.BuildConfig
|
||||
import top.fumiama.dmzj.copymanga.R
|
||||
import top.fumiama.sdict.utils.Utils.toHexStr
|
||||
import java.io.File
|
||||
import java.security.MessageDigest
|
||||
|
||||
@@ -63,7 +64,7 @@ object Update {
|
||||
fetch(client, kanban, this@apply) {
|
||||
lifecycleScope.launch {
|
||||
val md5 = msg.substringAfterLast("md5:")
|
||||
if (md5 == UITools.toHexStr(
|
||||
if (md5 == toHexStr(
|
||||
MessageDigest.getInstance("MD5").digest(it)
|
||||
)
|
||||
) {
|
||||
|
||||
@@ -10,7 +10,7 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import top.fumiama.copymanga.MainActivity
|
||||
import top.fumiama.copymanga.json.ComandyVersion
|
||||
import top.fumiama.copymanga.net.Client
|
||||
import top.fumiama.sdict.io.Client
|
||||
import top.fumiama.copymanga.net.DownloadTools
|
||||
import top.fumiama.copymanga.storage.PreferenceBoolean
|
||||
import top.fumiama.copymanga.storage.UserPreferenceInt
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
package top.fumiama.copymanga.net
|
||||
//Fumiama 20210601
|
||||
//Client.kt
|
||||
import android.util.Log
|
||||
import top.fumiama.copymanga.api.update.ByteArrayQueue
|
||||
import java.io.*
|
||||
import java.lang.Thread.sleep
|
||||
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 = 10000 //设置连接超时限制
|
||||
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: String?): Boolean = sendMessage(message?.toByteArray())
|
||||
|
||||
fun sendMessage(message: ByteArray?): Boolean {
|
||||
try {
|
||||
if (isConnect) {
|
||||
if (message != null) { //判断输出流或者消息是否为空,为空的话会产生null pointer错误
|
||||
dout?.write(message)
|
||||
dout?.flush()
|
||||
Log.d("MyC", "Send msg: ${message.decodeToString()}")
|
||||
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
|
||||
}
|
||||
|
||||
fun read(): Char? = din?.read()?.toChar()
|
||||
|
||||
private var buffer = ByteArrayQueue()
|
||||
private val receiveBuffer = ByteArray(65536)
|
||||
|
||||
fun receiveRawMessage(totalSize: Int, setProgress: Boolean = false) : ByteArray {
|
||||
if(totalSize == buffer.size) return buffer.popAll()
|
||||
else {
|
||||
try {
|
||||
if (isConnect) {
|
||||
Log.d("MyC", "开始接收服务端信息")
|
||||
var prevP = 0
|
||||
while(totalSize > buffer.size) {
|
||||
val count = din?.read(receiveBuffer)?:0
|
||||
if(count > 0) {
|
||||
buffer += receiveBuffer.copyOfRange(0, count)
|
||||
Log.d("MyC", "reply length:$count")
|
||||
val p = 100 * buffer.size / totalSize
|
||||
if(setProgress && totalSize > 0 && prevP != p) {
|
||||
progress?.notify(p)
|
||||
prevP = p
|
||||
}
|
||||
} else sleep(10)
|
||||
}
|
||||
} else Log.d("MyC", "no connect to receive message")
|
||||
} catch (e: IOException) {
|
||||
Log.d("MyC", "receive message failed")
|
||||
e.printStackTrace()
|
||||
}
|
||||
return if(totalSize > 0) buffer.pop(totalSize)?:byteArrayOf() else buffer.popAll()
|
||||
}
|
||||
}
|
||||
|
||||
fun receiveMessage(totalSize: Int) = receiveRawMessage(totalSize).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)
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.json.JSONObject
|
||||
import top.fumiama.sdict.io.Client
|
||||
import top.fumiama.copymanga.api.Config
|
||||
import top.fumiama.copymanga.api.Config.proxyUrl
|
||||
import top.fumiama.copymanga.json.ComandyCapsule
|
||||
@@ -34,7 +35,7 @@ object DownloadTools {
|
||||
connection.apply {
|
||||
Config.net_ua.value.let {
|
||||
if (it.isEmpty()) return@let
|
||||
setRequestProperty("user-agent", if (it == "__default_ua__") Config.pc_ua else it)
|
||||
setRequestProperty("user-agent", if (it == Config.default_ua) Config.pc_ua else it)
|
||||
}
|
||||
Config.net_source.value.let { if(it.isNotEmpty()) setRequestProperty("source", it) }
|
||||
// deviceinfo
|
||||
@@ -43,7 +44,7 @@ object DownloadTools {
|
||||
if (Config.net_use_gzip.value) setRequestProperty("accept-encoding", "gzip")
|
||||
setRequestProperty("authorization", "Token${Config.token.value?.let { tk ->
|
||||
if (tk.isNotEmpty()) " $tk" else ""
|
||||
}}")
|
||||
}?:""}")
|
||||
if (Config.net_platform.value) setRequestProperty("platform", Config.platform.value)
|
||||
if (Config.net_referer.value) setRequestProperty("referer", Config.referer)
|
||||
if (Config.net_use_json.value) setRequestProperty("accept", "application/json")
|
||||
@@ -66,7 +67,7 @@ object DownloadTools {
|
||||
capsule.headers = hashMapOf()
|
||||
Config.net_ua.value.let {
|
||||
if (it.isEmpty()) return@let
|
||||
capsule.headers["user-agent"] = if (it == "__default_ua__") Config.pc_ua else it
|
||||
capsule.headers["user-agent"] = if (it == Config.default_ua) Config.pc_ua else it
|
||||
}
|
||||
Config.net_source.value.let { if(it.isNotEmpty()) capsule.headers["source"] = it }
|
||||
// deviceinfo
|
||||
@@ -75,7 +76,7 @@ object DownloadTools {
|
||||
if (Config.net_use_gzip.value) capsule.headers["accept-encoding"] = "gzip"
|
||||
capsule.headers["authorization"] = "Token${Config.token.value?.let { tk ->
|
||||
if (tk.isNotEmpty()) " $tk" else ""
|
||||
}}"
|
||||
}?:""}"
|
||||
if (Config.net_platform.value) capsule.headers["platform"] = Config.platform.value
|
||||
if (Config.net_referer.value) capsule.headers["referer"] = Config.referer
|
||||
if (Config.net_use_json.value) capsule.headers["accept"] = "application/json"
|
||||
|
||||
@@ -11,11 +11,9 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import top.fumiama.copymanga.MainActivity.Companion.mainWeakReference
|
||||
import top.fumiama.copymanga.api.Config
|
||||
import top.fumiama.copymanga.json.ReturnBase
|
||||
import java.io.File
|
||||
import java.security.MessageDigest
|
||||
|
||||
open class AutoDownloadHandler(
|
||||
private val url: () -> String, private val jsonClass: Class<*>,
|
||||
@@ -24,6 +22,7 @@ open class AutoDownloadHandler(
|
||||
private val customCacheFile: File? = null): Handler(Looper.myLooper()!!) {
|
||||
private var checkTimes = 0
|
||||
var exit = false
|
||||
var raw: String? = null
|
||||
override fun handleMessage(msg: Message) {
|
||||
super.handleMessage(msg)
|
||||
when(msg.what){
|
||||
@@ -45,26 +44,16 @@ open class AutoDownloadHandler(
|
||||
downloadCoroutine()
|
||||
check()
|
||||
}
|
||||
private 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()
|
||||
}
|
||||
private suspend fun downloadCoroutine() = withContext(Dispatchers.IO) {
|
||||
val cacheName = toHexStr(MessageDigest.getInstance("MD5").digest(url().encodeToByteArray()))
|
||||
val cacheFile = customCacheFile?:(mainWeakReference?.get()?.externalCacheDir?.let { File(it, cacheName) })
|
||||
if(loadFromCache) {
|
||||
cacheFile?.let {
|
||||
customCacheFile?.let {
|
||||
if (it.exists()) {
|
||||
var pass = true
|
||||
it.inputStream().use { fi->
|
||||
try {
|
||||
pass = setGsonItem(Gson().fromJson(fi.reader(), jsonClass))
|
||||
val data = fi.readBytes().decodeToString()
|
||||
raw = data
|
||||
pass = setGsonItem(Gson().fromJson(data, jsonClass))
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
@@ -77,10 +66,11 @@ open class AutoDownloadHandler(
|
||||
while (cnt++ <= 3) {
|
||||
try {
|
||||
val data = Config.api.get(url())
|
||||
raw = data
|
||||
if(exit) return@withContext
|
||||
val pass = setGsonItem(Gson().fromJson(data, jsonClass))
|
||||
if (pass && loadFromCache) {
|
||||
cacheFile?.writeText(data)
|
||||
customCacheFile?.writeText(data)
|
||||
}
|
||||
if(!pass) {
|
||||
delay(2000)
|
||||
|
||||
@@ -4,7 +4,7 @@ import top.fumiama.copymanga.api.Config
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.zip.CRC32
|
||||
|
||||
class DataLoader {
|
||||
class ConfigLoader {
|
||||
data class Settings(
|
||||
var appVer: String,
|
||||
var platform: String,
|
||||
@@ -21,7 +21,7 @@ import top.fumiama.copymanga.MainActivity
|
||||
import top.fumiama.copymanga.api.Config.manga_dl_show_0m_manga
|
||||
import top.fumiama.copymanga.api.manga.Downloader
|
||||
import top.fumiama.copymanga.api.manga.Reader
|
||||
import top.fumiama.copymanga.net.Client
|
||||
import top.fumiama.sdict.io.Client
|
||||
import top.fumiama.copymanga.storage.FileUtils
|
||||
import top.fumiama.copymanga.storage.FileUtils.compressToUserFile
|
||||
import top.fumiama.copymanga.view.interaction.Navigate
|
||||
|
||||
@@ -10,6 +10,7 @@ import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.ImageButton
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
@@ -24,7 +25,6 @@ import kotlinx.android.synthetic.main.fragment_home.*
|
||||
import kotlinx.android.synthetic.main.line_word.view.*
|
||||
import kotlinx.android.synthetic.main.viewpage_horizonal.view.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import top.fumiama.copymanga.MainActivity
|
||||
@@ -40,150 +40,176 @@ import java.lang.ref.WeakReference
|
||||
import java.net.URLEncoder
|
||||
import java.nio.charset.Charset
|
||||
|
||||
class HomeFragment : NoBackRefreshFragment(R.layout.fragment_home) {
|
||||
class HomeFragment : NoBackRefreshFragment(R.layout.fragment_home, true) {
|
||||
lateinit var homeHandler: HomeHandler
|
||||
val vm: HomeViewModel by viewModels()
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
if(isFirstInflate) {
|
||||
val tb = (activity as MainActivity).toolsBox
|
||||
val netInfo = tb.netInfo
|
||||
if(netInfo != tb.transportStringNull && netInfo != tb.transportStringError)
|
||||
MainActivity.member?.apply { lifecycleScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
Config.api.init()
|
||||
try {
|
||||
info()
|
||||
} catch (e: Exception) {
|
||||
Snackbar
|
||||
.make(view, "${e::class.simpleName} ${e.message}", Snackbar.LENGTH_LONG)
|
||||
.setTextMaxLines(10)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
} }
|
||||
homeHandler = HomeHandler(WeakReference(this))
|
||||
|
||||
val theme = resources.newTheme()
|
||||
swiperefresh?.setColorSchemeColors(
|
||||
resources.getColor(R.color.colorAccent, theme),
|
||||
resources.getColor(R.color.colorBlue2, theme),
|
||||
resources.getColor(R.color.colorGreen, theme))
|
||||
swiperefresh?.isEnabled = true
|
||||
val theme = resources.newTheme()
|
||||
swiperefresh?.setColorSchemeColors(
|
||||
resources.getColor(R.color.colorAccent, theme),
|
||||
resources.getColor(R.color.colorBlue2, theme),
|
||||
resources.getColor(R.color.colorGreen, theme)
|
||||
)
|
||||
|
||||
fhl?.setPadding(0, 0, 0, navBarHeight)
|
||||
fhl?.setPadding(0, 0, 0, navBarHeight)
|
||||
|
||||
fhs?.apply {
|
||||
isNestedScrollingEnabled = true
|
||||
val recyclerView = findViewById<RecyclerView>(R.id.search_recycler_view)
|
||||
recyclerView.isNestedScrollingEnabled = true
|
||||
recyclerView.setPadding(0, 0, 0, navBarHeight)
|
||||
setAdapterLayoutManager(LinearLayoutManager(context))
|
||||
val adapter = ListViewHolder(recyclerView).RecyclerViewAdapter()
|
||||
setAdapter(adapter)
|
||||
navigationIconSupport = SearchLayout.NavigationIconSupport.SEARCH
|
||||
setMicIconImageResource(R.drawable.ic_setting_search)
|
||||
val micView = findViewById<ImageButton>(R.id.search_image_view_mic)
|
||||
setClearFocusOnBackPressed(true)
|
||||
setOnNavigationClickListener(object : SearchLayout.OnNavigationClickListener {
|
||||
override fun onNavigationClick(hasFocus: Boolean) {
|
||||
if (hasFocus()) {
|
||||
clearFocus()
|
||||
}
|
||||
else requestFocus()
|
||||
}
|
||||
})
|
||||
setTextHint(android.R.string.search_go)
|
||||
|
||||
var lastSearch = ""
|
||||
setOnQueryTextListener(object : SearchLayout.OnQueryTextListener {
|
||||
var lastChangeTime = 0L
|
||||
override fun onQueryTextChange(newText: CharSequence): Boolean {
|
||||
if (newText.contentEquals("__notice_focus_change__") || newText.contentEquals(lastSearch)) return true
|
||||
lastSearch = newText.toString()
|
||||
postDelayed({
|
||||
lifecycleScope.launch {
|
||||
if (!newText.contentEquals(lastSearch)) return@launch
|
||||
val diff = System.currentTimeMillis() - lastChangeTime
|
||||
if(diff > 500) {
|
||||
if (newText.isNotEmpty()) {
|
||||
Log.d("MyHF", "new text: $newText")
|
||||
adapter.refresh(newText)
|
||||
lifecycleScope.launch {
|
||||
withContext(Dispatchers.Main) {
|
||||
vm.indexStructure.observe(viewLifecycleOwner) {
|
||||
Log.d("MyHF", "get observed: $it")
|
||||
if (it == null) { // init
|
||||
val tb = (activity as MainActivity).toolsBox
|
||||
val netInfo = tb.netInfo
|
||||
lifecycleScope.launch net@ {
|
||||
if (netInfo == tb.transportStringNull || netInfo == tb.transportStringError) {
|
||||
(activity as MainActivity).toolsBox.toastError(getString(R.string.web_error))
|
||||
fhov?.swipeRefreshLayout = swiperefresh
|
||||
swiperefresh.isRefreshing = false
|
||||
swiperefresh.setOnRefreshListener {
|
||||
Log.d("MyHFH", "Refresh items.")
|
||||
vm.saveIndexStructure(null)
|
||||
}
|
||||
hideKanban()
|
||||
return@net
|
||||
}
|
||||
MainActivity.member?.apply {
|
||||
withContext(Dispatchers.IO) {
|
||||
Config.api.init()
|
||||
try {
|
||||
info()
|
||||
} catch (e: Exception) {
|
||||
withContext(Dispatchers.Main) {
|
||||
Snackbar
|
||||
.make(
|
||||
view,
|
||||
"${e::class.simpleName} ${e.message}",
|
||||
Snackbar.LENGTH_LONG
|
||||
)
|
||||
.setTextMaxLines(10)
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 1024)
|
||||
lastChangeTime = System.currentTimeMillis()
|
||||
return true
|
||||
}
|
||||
} }
|
||||
|
||||
override fun onQueryTextSubmit(query: CharSequence): Boolean {
|
||||
/*if(query.isNotEmpty()) {
|
||||
val key = query.toString()
|
||||
Toast.makeText(context, key, Toast.LENGTH_SHORT).show()
|
||||
}*/
|
||||
Log.d("MyHF", "recover text: $lastSearch")
|
||||
setTextQuery(lastSearch, false)
|
||||
return true
|
||||
}
|
||||
})
|
||||
homeHandler = HomeHandler(WeakReference(this@HomeFragment))
|
||||
homeHandler.obtainMessage(-1, true).sendToTarget()
|
||||
homeHandler.startLoad()
|
||||
|
||||
setOnMicClickListener(object : SearchLayout.OnMicClickListener {
|
||||
val types = arrayOf("", "name", "author", "local")
|
||||
var i = 0
|
||||
override fun onMicClick() {
|
||||
val typeNames = resources.getStringArray(R.array.search_types)
|
||||
AlertDialog.Builder(context)
|
||||
.setTitle(R.string.set_search_types)
|
||||
.setIcon(R.mipmap.ic_launcher)
|
||||
.setSingleChoiceItems(ArrayAdapter(context, R.layout.line_choice_list, typeNames), i){ d, p ->
|
||||
adapter.type = types[p]
|
||||
i = p
|
||||
d.cancel()
|
||||
}.show()
|
||||
return@observe
|
||||
}
|
||||
})
|
||||
if(homeHandler.exit) return@observe
|
||||
|
||||
var isInFocusWaiting = false
|
||||
setOnFocusChangeListener(object : SearchLayout.OnFocusChangeListener {
|
||||
override fun onFocusChange(hasFocus: Boolean) {
|
||||
Log.d("MyHF", "fhs onFocusChange: $hasFocus")
|
||||
if (isInFocusWaiting) return
|
||||
isInFocusWaiting = true
|
||||
postDelayed({
|
||||
navigationIconSupport = if (hasFocus) {
|
||||
setTextQuery("__notice_focus_change__", true)
|
||||
SearchLayout.NavigationIconSupport.ARROW
|
||||
}
|
||||
else {
|
||||
if (lastSearch.isNotEmpty()) {
|
||||
micView?.visibility = View.VISIBLE
|
||||
swiperefresh?.isEnabled = true
|
||||
|
||||
fhs?.apply {
|
||||
isNestedScrollingEnabled = true
|
||||
val recyclerView = findViewById<RecyclerView>(R.id.search_recycler_view)
|
||||
recyclerView.isNestedScrollingEnabled = true
|
||||
recyclerView.setPadding(0, 0, 0, navBarHeight)
|
||||
setAdapterLayoutManager(LinearLayoutManager(context))
|
||||
val adapter = ListViewHolder(recyclerView).RecyclerViewAdapter()
|
||||
setAdapter(adapter)
|
||||
navigationIconSupport = SearchLayout.NavigationIconSupport.SEARCH
|
||||
setMicIconImageResource(R.drawable.ic_setting_search)
|
||||
val micView = findViewById<ImageButton>(R.id.search_image_view_mic)
|
||||
setClearFocusOnBackPressed(true)
|
||||
setOnNavigationClickListener(object : SearchLayout.OnNavigationClickListener {
|
||||
override fun onNavigationClick(hasFocus: Boolean) {
|
||||
if (hasFocus()) {
|
||||
clearFocus()
|
||||
}
|
||||
SearchLayout.NavigationIconSupport.SEARCH
|
||||
else requestFocus()
|
||||
}
|
||||
isInFocusWaiting = false
|
||||
}, 300)
|
||||
}
|
||||
})
|
||||
})
|
||||
setTextHint(android.R.string.search_go)
|
||||
|
||||
setOnTouchListener { _, e ->
|
||||
Log.d("MyHF", "fhns on touch")
|
||||
if (e.action == MotionEvent.ACTION_UP && mSearchEditText?.text?.isNotEmpty() == true) {
|
||||
ime?.hideSoftInputFromWindow(activity?.window?.decorView?.windowToken, 0)
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
var lastSearch = ""
|
||||
setOnQueryTextListener(object : SearchLayout.OnQueryTextListener {
|
||||
var lastChangeTime = 0L
|
||||
override fun onQueryTextChange(newText: CharSequence): Boolean {
|
||||
if (newText.contentEquals("__notice_focus_change__") || newText.contentEquals(lastSearch)) return true
|
||||
lastSearch = newText.toString()
|
||||
postDelayed({
|
||||
lifecycleScope.launch {
|
||||
if (!newText.contentEquals(lastSearch)) return@launch
|
||||
val diff = System.currentTimeMillis() - lastChangeTime
|
||||
if(diff > 500) {
|
||||
if (newText.isNotEmpty()) {
|
||||
Log.d("MyHF", "new text: $newText")
|
||||
adapter.refresh(newText)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 1024)
|
||||
lastChangeTime = System.currentTimeMillis()
|
||||
return true
|
||||
}
|
||||
|
||||
lifecycleScope.launch{
|
||||
withContext(Dispatchers.IO) {
|
||||
homeHandler.obtainMessage(-1, true).sendToTarget()
|
||||
while(!MainActivity.isDrawerClosed) delay(233)
|
||||
//homeHandler.sendEmptyMessage(6) //removeAllViews
|
||||
//homeHandler.fhib = null
|
||||
delay(300)
|
||||
homeHandler.startLoad()
|
||||
override fun onQueryTextSubmit(query: CharSequence): Boolean {
|
||||
/*if(query.isNotEmpty()) {
|
||||
val key = query.toString()
|
||||
Toast.makeText(context, key, Toast.LENGTH_SHORT).show()
|
||||
}*/
|
||||
Log.d("MyHF", "recover text: $lastSearch")
|
||||
setTextQuery(lastSearch, false)
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
setOnMicClickListener(object : SearchLayout.OnMicClickListener {
|
||||
val types = arrayOf("", "name", "author", "local")
|
||||
var i = 0
|
||||
override fun onMicClick() {
|
||||
val typeNames = resources.getStringArray(R.array.search_types)
|
||||
AlertDialog.Builder(context)
|
||||
.setTitle(R.string.set_search_types)
|
||||
.setIcon(R.mipmap.ic_launcher)
|
||||
.setSingleChoiceItems(ArrayAdapter(context, R.layout.line_choice_list, typeNames), i){ d, p ->
|
||||
adapter.type = types[p]
|
||||
i = p
|
||||
d.cancel()
|
||||
}.show()
|
||||
}
|
||||
})
|
||||
|
||||
var isInFocusWaiting = false
|
||||
setOnFocusChangeListener(object : SearchLayout.OnFocusChangeListener {
|
||||
override fun onFocusChange(hasFocus: Boolean) {
|
||||
Log.d("MyHF", "fhs onFocusChange: $hasFocus")
|
||||
if (isInFocusWaiting) return
|
||||
isInFocusWaiting = true
|
||||
postDelayed({
|
||||
navigationIconSupport = if (hasFocus) {
|
||||
setTextQuery("__notice_focus_change__", true)
|
||||
SearchLayout.NavigationIconSupport.ARROW
|
||||
}
|
||||
else {
|
||||
if (lastSearch.isNotEmpty()) {
|
||||
micView?.visibility = View.VISIBLE
|
||||
}
|
||||
SearchLayout.NavigationIconSupport.SEARCH
|
||||
}
|
||||
isInFocusWaiting = false
|
||||
}, 300)
|
||||
}
|
||||
})
|
||||
|
||||
setOnTouchListener { _, e ->
|
||||
Log.d("MyHF", "fhns on touch")
|
||||
if (e.action == MotionEvent.ACTION_UP && mSearchEditText?.text?.isNotEmpty() == true) {
|
||||
ime?.hideSoftInputFromWindow(activity?.window?.decorView?.windowToken, 0)
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
homeHandler.sendEmptyMessage(2) //setSwipe
|
||||
homeHandler.sendEmptyMessage(7) //inflateBanner
|
||||
homeHandler.sendEmptyMessage(1) //inflateCardLines
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -300,6 +326,7 @@ class HomeFragment : NoBackRefreshFragment(R.layout.fragment_home) {
|
||||
|
||||
override fun getItemCount() = (results?.results?.list?.size?:0) + if (query?.isNotEmpty() == true) 1 else 0
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
suspend fun refresh(q: CharSequence) = withContext(Dispatchers.IO) {
|
||||
query = q.toString()
|
||||
activity?.apply {
|
||||
|
||||
@@ -8,7 +8,6 @@ import android.os.Message
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.view.ViewTreeObserver
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.Toast
|
||||
import androidx.cardview.widget.CardView
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
@@ -28,13 +27,13 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import top.fumiama.copymanga.api.Config
|
||||
import top.fumiama.copymanga.json.ComicStructure
|
||||
import top.fumiama.copymanga.json.IndexStructure
|
||||
import top.fumiama.copymanga.net.template.AutoDownloadHandler
|
||||
import top.fumiama.copymanga.api.Config
|
||||
import top.fumiama.copymanga.view.operation.GlideHideLottieViewListener
|
||||
import top.fumiama.copymanga.view.interaction.Navigate
|
||||
import top.fumiama.copymanga.view.interaction.UITools
|
||||
import top.fumiama.copymanga.view.operation.GlideHideLottieViewListener
|
||||
import top.fumiama.dmzj.copymanga.R
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
@@ -47,7 +46,7 @@ class HomeHandler(private val that: WeakReference<HomeFragment>) : AutoDownloadH
|
||||
) {
|
||||
private val homeF get() = that.get()
|
||||
var index: IndexStructure? = null
|
||||
var fhib: Banner? = null
|
||||
private var fhib: Banner? = null
|
||||
get() {
|
||||
Log.d("MyHH", "Get fhib.")
|
||||
if (field == null) {
|
||||
@@ -56,7 +55,6 @@ class HomeHandler(private val that: WeakReference<HomeFragment>) : AutoDownloadH
|
||||
}
|
||||
return field
|
||||
}
|
||||
private var indexLines = arrayOf<View>()
|
||||
|
||||
override fun handleMessage(msg: Message) {
|
||||
super.handleMessage(msg)
|
||||
@@ -72,16 +70,6 @@ class HomeHandler(private val that: WeakReference<HomeFragment>) : AutoDownloadH
|
||||
2 -> homeF?.swiperefresh?.let { setSwipe(it) }
|
||||
3 -> setBanner(fhib!!)
|
||||
5 -> setBannerInfo(msg.obj as Banner)
|
||||
6 -> {
|
||||
homeF?.fhl?.let {
|
||||
val oa = ObjectAnimator.ofFloat(it, "alpha", 1f, 0f).setDuration(233)
|
||||
oa.doOnEnd { _ ->
|
||||
it.removeAllViews()
|
||||
it.alpha = 1f
|
||||
}
|
||||
oa.start()
|
||||
}
|
||||
}
|
||||
7 -> inflateBanner()
|
||||
}
|
||||
}
|
||||
@@ -108,15 +96,22 @@ class HomeHandler(private val that: WeakReference<HomeFragment>) : AutoDownloadH
|
||||
Toast.makeText(homeF?.context, R.string.web_error, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
override suspend fun doWhenFinishDownload() = withContext(Dispatchers.IO) {
|
||||
override suspend fun doWhenFinishDownload(): Unit = withContext(Dispatchers.IO) {
|
||||
super.doWhenFinishDownload()
|
||||
if(exit) return@withContext
|
||||
sendEmptyMessage(2) //setSwipe
|
||||
sendEmptyMessage(7) //inflateBanner
|
||||
sendEmptyMessage(1) //inflateCardLines
|
||||
raw?.let {
|
||||
Log.d("MyHFH", "save raw: $it")
|
||||
homeF?.apply { activity?.runOnUiThread {
|
||||
vm.saveIndexStructure(it)
|
||||
} }
|
||||
}
|
||||
}
|
||||
|
||||
private fun inflateBanner() = homeF?.fhl?.addView(fhib)
|
||||
private fun inflateBanner() {
|
||||
homeF?.fhl?.let { it.post {
|
||||
fhib = null
|
||||
it.addView(fhib)
|
||||
} }
|
||||
}
|
||||
|
||||
private suspend fun inflateTopics() {
|
||||
index?.results?.topics?.list?.let {
|
||||
@@ -242,29 +237,13 @@ class HomeHandler(private val that: WeakReference<HomeFragment>) : AutoDownloadH
|
||||
private fun inflateCardLines() {
|
||||
homeF?.lifecycleScope?.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
if (indexLines.isNotEmpty()) indexLines = arrayOf()
|
||||
inflateRec()
|
||||
inflateTopics()
|
||||
inflateHot()
|
||||
inflateNew()
|
||||
inflateFinish()
|
||||
inflateRank()
|
||||
homeF?.fhl?.apply { post {
|
||||
for (i in indexLines.indices) {
|
||||
try {
|
||||
addView(indexLines[i])
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
(indexLines[i].parent as LinearLayout).apply {
|
||||
post {
|
||||
removeAllViews()
|
||||
homeF?.fhl?.addView(indexLines[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
obtainMessage(-1, false).sendToTarget() //closeLoad
|
||||
} }
|
||||
obtainMessage(-1, false).sendToTarget() //closeLoad
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -306,14 +285,24 @@ class HomeHandler(private val that: WeakReference<HomeFragment>) : AutoDownloadH
|
||||
homeF?.lifecycleScope?.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
homeF?.showKanban()
|
||||
fhib?.isAutoPlay = false
|
||||
index = null
|
||||
fhib?.adapter?.notifyDataSetChanged()
|
||||
fhib?.let {
|
||||
it.isAutoPlay = false
|
||||
index = null
|
||||
it.adapter?.notifyDataSetChanged()
|
||||
}
|
||||
fhib = null
|
||||
indexLines = arrayOf()
|
||||
this@HomeHandler.sendEmptyMessage(6) //removeAllViews
|
||||
delay(300)
|
||||
this@HomeHandler.sendEmptyMessage(0) //setLayouts
|
||||
withContext(Dispatchers.Main) {
|
||||
homeF?.fhl?.let {
|
||||
val oa = ObjectAnimator.ofFloat(it, "alpha", 1f, 0f).setDuration(233)
|
||||
oa.doOnEnd { _ ->
|
||||
it.removeAllViews()
|
||||
it.alpha = 1f
|
||||
homeF?.vm?.saveIndexStructure(null) // reload
|
||||
}
|
||||
oa.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -322,15 +311,14 @@ class HomeHandler(private val that: WeakReference<HomeFragment>) : AutoDownloadH
|
||||
private suspend fun allocateLine(
|
||||
title: String, iconResId: Int, comics: Array<ComicStructure>,
|
||||
finish: Boolean = false, isTopic: Boolean = false, onClick: (() -> Unit)? = null
|
||||
): Int = withContext(Dispatchers.IO) {
|
||||
val p = indexLines.size
|
||||
): Unit = withContext(Dispatchers.IO) {
|
||||
val c = comics.size / 3
|
||||
homeF?.layoutInflater?.inflate(
|
||||
when(c){
|
||||
1 -> R.layout.line_1bookline
|
||||
2 -> R.layout.line_2bookline
|
||||
3 -> R.layout.line_3bookline
|
||||
else -> return@withContext -1
|
||||
else -> return@withContext
|
||||
}, null, false)?.apply {
|
||||
withContext(Dispatchers.Main) {
|
||||
scanCards(this@apply, comics, finish, isTopic)
|
||||
@@ -341,9 +329,9 @@ class HomeHandler(private val that: WeakReference<HomeFragment>) : AutoDownloadH
|
||||
if(onClick != null) setOnClickListener { onClick() }
|
||||
}
|
||||
}
|
||||
indexLines += this
|
||||
homeF?.fhl?.let { it.post { it.addView(this) } }
|
||||
}
|
||||
return@withContext p
|
||||
return@withContext
|
||||
}
|
||||
|
||||
private suspend fun scanCards(v: View, comics: Array<ComicStructure>, finish: Boolean, isTopic: Boolean) = withContext(Dispatchers.IO) {
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package top.fumiama.copymanga.ui.home
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
||||
class HomeViewModel : ViewModel() {
|
||||
val indexStructure = MutableLiveData<String?>(null)
|
||||
|
||||
fun saveIndexStructure(index: String?) {
|
||||
indexStructure.value = index
|
||||
}
|
||||
}
|
||||
@@ -123,16 +123,6 @@ class UITools(that: Context?, w: WeakReference<Activity>? = null) {
|
||||
return listOf(numPerRow, w, totalWidth)
|
||||
}
|
||||
companion object {
|
||||
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()
|
||||
}
|
||||
@SuppressLint("DiscouragedApi", "InternalInsetResource")
|
||||
fun getNavigationBarHeight(context: Context): Int {
|
||||
val resources = context.resources
|
||||
|
||||
@@ -14,9 +14,8 @@ import top.fumiama.copymanga.api.Config
|
||||
import top.fumiama.copymanga.view.interaction.UITools
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
open class NoBackRefreshFragment(private val layoutToLoad: Int): Fragment() {
|
||||
open class NoBackRefreshFragment(private val layoutToLoad: Int, private val noCacheView: Boolean = false): Fragment() {
|
||||
private var _rootView: View? = null
|
||||
val rootView: View get() = _rootView!!
|
||||
var isFirstInflate = true
|
||||
var navBarHeight = 0
|
||||
private val disableAnimation get() = Config.general_disable_kanban_animation.value
|
||||
@@ -25,7 +24,10 @@ open class NoBackRefreshFragment(private val layoutToLoad: Int): Fragment() {
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
//TODO: 支持自动重建
|
||||
navBarHeight = context?.let { UITools.getNavigationBarHeight(it) } ?: 0
|
||||
|
||||
if (noCacheView) return inflater.inflate(layoutToLoad, container, false)
|
||||
|
||||
if(_rootView == null) {
|
||||
isFirstInflate = true
|
||||
_rootView = inflater.inflate(layoutToLoad, container, false)
|
||||
@@ -34,8 +36,7 @@ open class NoBackRefreshFragment(private val layoutToLoad: Int): Fragment() {
|
||||
isFirstInflate = false
|
||||
Log.d("MyNBRF", "not first inflate")
|
||||
}
|
||||
navBarHeight = context?.let { UITools.getNavigationBarHeight(it) } ?: 0
|
||||
return rootView
|
||||
return _rootView!!
|
||||
}
|
||||
override fun onDestroy() {
|
||||
hideKanban()
|
||||
|
||||
@@ -115,6 +115,7 @@
|
||||
<string name="TRANSPORT_ERROR">网络错误</string>
|
||||
|
||||
<string name="pc_ua">COPY/%1$s</string>
|
||||
<string name="default_ua">__default_ua__</string>
|
||||
<string name="app_ver">&appver;</string>
|
||||
<string name="referer">com.copymanga.app-%1$s</string>
|
||||
<string name="platform">&platform;</string>
|
||||
|
||||
@@ -185,6 +185,7 @@
|
||||
<EditTextPreference
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:defaultValue="@string/default_ua"
|
||||
android:selectAllOnFocus="false"
|
||||
android:singleLine="true"
|
||||
android:title="@string/settings_cat_net_et_title_ua"
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
Third-party client for copymanga with better reading/downloading experience.
|
||||
A third-party app for CopyManga, offering only basic features. For a more complete experience, please refer to the official version.
|
||||
<h3>Features</h3>
|
||||
1. Browse homepage, category, rank, my downloads, tag, and author.
|
||||
2. View, search and read manga; log reading progress.
|
||||
3. Download manga. But due to force majeure, the downloading is slow and error-prone. This is not due to bad optimization, absolutely not.
|
||||
4. Read downloaded manga.
|
||||
5. Check update.
|
||||
1. Browse the homepage, categories, rankings, downloads, subscriptions, reading history, tags, and authors.
|
||||
2. View and search for manga, and read them directly. Reading progress is saved both locally and in the cloud (cloud sync does not track the exact page).
|
||||
3. Download manga. However, due to circumstances beyond our control, download speeds may be slow and errors can occur—definitely not because of optimization, absolutely not.
|
||||
4. Read and delete downloaded manga, or jump to the manga detail page directly from your downloads.
|
||||
5. Check for updates.
|
||||
6. Log in and log out.
|
||||
7. Subscribe or unsubscribe to manga.
|
||||
|
||||
@@ -1 +1 @@
|
||||
Third-party client for copymanga with better reading/downloading experience
|
||||
A third-party app for CopyManga, offering only basic features. For a more complete experience, please refer to the official version.
|
||||
@@ -1,7 +1,9 @@
|
||||
拷贝漫画的第三方 APP,优化阅读/下载体验
|
||||
拷贝漫画的第三方APP,仅提供基础功能,更多丰富功能请移步官方版本
|
||||
<h3>功能</h3>
|
||||
1. 浏览主页、分类、排行、我的下载、标签、作者。
|
||||
2. 查看、搜索漫画并直接阅读;记录漫画与章节的阅读进度。
|
||||
1. 浏览主页、分类、排行、我的下载、我的订阅、浏览历史、标签、作者。
|
||||
2. 查看、搜索漫画并直接阅读;在本地和云端记录漫画与章节的阅读进度 (云端不能精确到页)。
|
||||
3. 下载漫画。但是由于不可抗力,下载速度较慢且容易出错,这绝对不是优化的原因,绝对不是。
|
||||
4. 阅读下载的漫画。
|
||||
5. 检查更新。
|
||||
4. 阅读、删除下载的漫画,或从我的下载页面直接导航到漫画详情页。
|
||||
5. 检查更新。
|
||||
6. 登录,注销。
|
||||
7. 订阅、取消订阅。
|
||||
@@ -1 +1 @@
|
||||
拷贝漫画的第三方 APP,优化阅读/下载体验
|
||||
拷贝漫画的第三方APP,仅提供基础功能,更多丰富功能请移步官方版本
|
||||
Reference in New Issue
Block a user