1
0
mirror of https://github.com/fumiama/copymanga.git synced 2026-06-04 23:10:23 +08:00
优化
1. 主页加载速度
2. 默认UA判定
3. TCP Client
This commit is contained in:
源文雨
2025-06-16 23:57:33 +09:00
parent e53db6762f
commit 036fdac4a9
24 changed files with 260 additions and 392 deletions

View File

@@ -3,6 +3,7 @@
<words>
<w>alphae</w>
<w>azurewebsites</w>
<w>catquit</w>
<w>comancry</w>
<w>comandy</w>
<w>deviceinfo</w>

View File

@@ -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'
}

View File

@@ -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()

View File

@@ -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)

View File

@@ -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)
}

View File

@@ -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"
}

View File

@@ -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)
)
) {

View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -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"

View File

@@ -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)

View File

@@ -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,

View File

@@ -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

View File

@@ -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 {

View File

@@ -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) {

View File

@@ -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
}
}

View File

@@ -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

View File

@@ -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()

View File

@@ -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>

View File

@@ -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"

View File

@@ -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.

View File

@@ -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.

View File

@@ -1,7 +1,9 @@
拷贝漫画的第三方 APP优化阅读/下载体验
拷贝漫画的第三方APP仅提供基础功能,更多丰富功能请移步官方版本
<h3>功能</h3>
1. 浏览主页、分类、排行、我的下载、标签、作者。
2. 查看、搜索漫画并直接阅读;记录漫画与章节的阅读进度。
1. 浏览主页、分类、排行、我的下载、我的订阅、浏览历史、标签、作者。
2. 查看、搜索漫画并直接阅读;在本地和云端记录漫画与章节的阅读进度 (云端不能精确到页)
3. 下载漫画。但是由于不可抗力,下载速度较慢且容易出错,这绝对不是优化的原因,绝对不是。
4. 阅读下载的漫画。
5. 检查更新。
4. 阅读、删除下载的漫画,或从我的下载页面直接导航到漫画详情页
5. 检查更新。
6. 登录,注销。
7. 订阅、取消订阅。

View File

@@ -1 +1 @@
拷贝漫画的第三方 APP优化阅读/下载体验
拷贝漫画的第三方APP仅提供基础功能,更多丰富功能请移步官方版本