1
0
mirror of https://github.com/fumiama/copymanga.git synced 2026-07-01 00:00:25 +08:00
新增
1. 导入/导出设置
优化
1. 错误显示
This commit is contained in:
源文雨
2025-06-11 17:29:05 +09:00
parent cd68a5056d
commit 74c0b165ed
27 changed files with 597 additions and 225 deletions

View File

@@ -12,8 +12,8 @@ android {
minSdkVersion 23 minSdkVersion 23
//noinspection OldTargetApi //noinspection OldTargetApi
targetSdkVersion 34 targetSdkVersion 34
versionCode 78 versionCode 79
versionName '2.5.5' versionName '2.5.6'
resourceConfigurations += ['zh', 'zh-rCN'] resourceConfigurations += ['zh', 'zh-rCN']
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

View File

@@ -35,8 +35,8 @@ class LoginActivity : AppCompatActivity() {
).show() ).show()
return@launch return@launch
} }
val pwd = altpwd.text?.toString() val pwd = altpwd.text?.toString()?:""
if (pwd.isNullOrEmpty()) { if (!isLogout && pwd.isEmpty()) {
Toast.makeText(this@LoginActivity, R.string.login_null_pwd, Toast.LENGTH_SHORT) Toast.makeText(this@LoginActivity, R.string.login_null_pwd, Toast.LENGTH_SHORT)
.show() .show()
return@launch return@launch
@@ -66,7 +66,7 @@ class LoginActivity : AppCompatActivity() {
Toast.makeText( Toast.makeText(
this@LoginActivity, this@LoginActivity,
"${e::class.simpleName} ${e.message}", "${e::class.simpleName} ${e.message}",
Toast.LENGTH_SHORT Toast.LENGTH_LONG
).show() ).show()
} }
} }

View File

@@ -63,6 +63,8 @@ import top.fumiama.copymanga.api.update.Update
import top.fumiama.copymanga.api.user.Member import top.fumiama.copymanga.api.user.Member
import top.fumiama.copymanga.lib.Comancry import top.fumiama.copymanga.lib.Comancry
import top.fumiama.copymanga.lib.Comandy import top.fumiama.copymanga.lib.Comandy
import top.fumiama.copymanga.storage.DataLoader
import top.fumiama.copymanga.strings.Base16384
import top.fumiama.dmzj.copymanga.BuildConfig import top.fumiama.dmzj.copymanga.BuildConfig
import top.fumiama.dmzj.copymanga.R import top.fumiama.dmzj.copymanga.R
import java.io.File import java.io.File
@@ -158,7 +160,7 @@ class MainActivity : AppCompatActivity() {
Log.d("MyMain", "start menu waiting") Log.d("MyMain", "start menu waiting")
lifecycleScope.launch { lifecycleScope.launch {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
Config.myHostApiUrl.init() Config.api.init()
} }
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
delay(1000) delay(1000)
@@ -206,6 +208,7 @@ class MainActivity : AppCompatActivity() {
return true return true
} }
@SuppressLint("CheckResult")
@OptIn(ExperimentalStdlibApi::class) @OptIn(ExperimentalStdlibApi::class)
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) { return when (item.itemId) {
@@ -214,10 +217,39 @@ class MainActivity : AppCompatActivity() {
true true
} }
R.id.action_download -> { R.id.action_download -> {
if (NewDownloadFragment.wn != null) { when (navController?.currentDestination?.id) {
//TODO: fill it R.id.nav_new_download -> {
} else { //TODO: fill it
bookHandler.get()?.sendEmptyMessage(BookHandler.NAVIGATE_TO_DOWNLOAD) }
R.id.nav_settings -> {
toolsBox.buildInfo("备份管理", "可选择导出或导入base16384格式配置项",
"导出", "导入", "取消", { // ok
MaterialDialog(this).show {
input(prefill = Base16384.encode(DataLoader().toByteArray()))
positiveButton(android.R.string.ok)
title(null, "请复制配置文本并保存")
}
}, { // neutral
MaterialDialog(this).show {
input { _, c ->
try {
DataLoader(Base16384.decode(c.toString())).settings.export()
navController?.apply {
currentDestination?.id?.let {
popBackStack()
navigate(it)
}
}
} catch (e: Exception) {
Toast.makeText(this@MainActivity, e.message?:e::class.simpleName, Toast.LENGTH_SHORT).show()
}
}
positiveButton(android.R.string.ok)
title(null, "请粘贴配置文本")
}
})
}
else -> bookHandler.get()?.sendEmptyMessage(BookHandler.NAVIGATE_TO_DOWNLOAD)
} }
true true
} }
@@ -338,6 +370,13 @@ class MainActivity : AppCompatActivity() {
menuMain?.findItem(R.id.action_sort)?.isVisible = false menuMain?.findItem(R.id.action_sort)?.isVisible = false
menuMain?.findItem(R.id.action_del)?.isVisible = true menuMain?.findItem(R.id.action_del)?.isVisible = true
} }
R.id.nav_settings -> {
Log.d("MyMain", "enter settings")
menuMain?.findItem(R.id.action_info)?.isVisible = false
menuMain?.findItem(R.id.action_download)?.isVisible = true
menuMain?.findItem(R.id.action_sort)?.isVisible = false
menuMain?.findItem(R.id.action_del)?.isVisible = false
}
else -> { else -> {
Log.d("MyMain", "enter others") Log.d("MyMain", "enter others")
menuMain?.findItem(R.id.action_info)?.isVisible = false menuMain?.findItem(R.id.action_info)?.isVisible = false
@@ -433,7 +472,7 @@ class MainActivity : AppCompatActivity() {
dl.setMessage("${getString(R.string.app_description)}\n" + dl.setMessage("${getString(R.string.app_description)}\n" +
"\n$comandy\n" + "\n$comandy\n" +
"$comancry\n\n"+ File("/proc/self/cmdline").readText() + "\n" + "$comancry\n\n"+ File("/proc/self/cmdline").readText() + "\n" +
"当前API: ${Config.myHostApiUrl.getApis().joinToString(", ")}") "当前API: ${Config.api.getApis().joinToString(", ")}")
dl.setTitle("${getString(R.string.action_info)} ${BuildConfig.VERSION_NAME}") dl.setTitle("${getString(R.string.action_info)} ${BuildConfig.VERSION_NAME}")
dl.setIcon(R.mipmap.ic_launcher) dl.setIcon(R.mipmap.ic_launcher)
dl.setPositiveButton(android.R.string.ok) { _, _ -> } dl.setPositiveButton(android.R.string.ok) { _, _ -> }

View File

@@ -14,6 +14,8 @@ import top.fumiama.dmzj.copymanga.R
import java.io.File import java.io.File
object Config { object Config {
val api = Api()
var imageProxy: Proxy? = null var imageProxy: Proxy? = null
get() { get() {
if (!net_use_img_proxy.value) return null if (!net_use_img_proxy.value) return null
@@ -55,16 +57,11 @@ object Config {
} }
val proxyUrl = MainActivity.mainWeakReference?.get()?.getString(R.string.proxyUrl)!! val proxyUrl = MainActivity.mainWeakReference?.get()?.getString(R.string.proxyUrl)!!
val reverseProxyUrl = PreferenceString(R.string.reverseProxyKeyID)
val networkApiUrl = PreferenceString("settings_cat_net_et_api_url", R.string.hostUrl)
val myHostApiUrl = Api()
val navTextInfo = UserPreferenceString("navTextInfo", R.string.navTextInfo)
val proxy_key = PreferenceString(R.string.imgProxyCodeKeyID)
val app_ver = PreferenceString("settings_cat_general_et_app_version", R.string.app_ver)
val platform = PreferenceString("settings_cat_general_et_platform", R.string.platform)
val token = UserPreferenceString("token", "", null)
val pc_ua get() = MainActivity.mainWeakReference?.get()?.getString(R.string.pc_ua)?.format(app_ver.value)?:"" val pc_ua get() = MainActivity.mainWeakReference?.get()?.getString(R.string.pc_ua)?.format(app_ver.value)?:""
val referer get() = MainActivity.mainWeakReference?.get()?.getString(R.string.referer)?.format(app_ver.value)?:"" val referer get() = MainActivity.mainWeakReference?.get()?.getString(R.string.referer)?.format(app_ver.value)?:""
val navTextInfo = UserPreferenceString("navTextInfo", R.string.navTextInfo)
val token = UserPreferenceString("token", "", null)
val comandy_version = UserPreferenceInt("comandy_version", 0) val comandy_version = UserPreferenceInt("comandy_version", 0)
val comancry_version = UserPreferenceInt("comancry_version", 0) val comancry_version = UserPreferenceInt("comancry_version", 0)
val user_id = UserPreferenceString("user_id") val user_id = UserPreferenceString("user_id")
@@ -72,6 +69,8 @@ object Config {
val nickname = UserPreferenceString("nickname") val nickname = UserPreferenceString("nickname")
val avatar = UserPreferenceString("avatar") val avatar = UserPreferenceString("avatar")
val app_ver = PreferenceString("settings_cat_general_et_app_version", R.string.app_ver)
val platform = PreferenceString("settings_cat_general_et_platform", R.string.platform)
val general_enable_transparent_system_bar = PreferenceBoolean("settings_cat_general_sw_enable_transparent_systembar", false) val general_enable_transparent_system_bar = PreferenceBoolean("settings_cat_general_sw_enable_transparent_systembar", false)
val general_disable_kanban_animation = PreferenceBoolean("settings_cat_general_sw_disable_kanban_animation", false) val general_disable_kanban_animation = PreferenceBoolean("settings_cat_general_sw_disable_kanban_animation", false)
val general_card_per_row = PreferenceInt("settings_cat_general_sb_card_per_row", 0) val general_card_per_row = PreferenceInt("settings_cat_general_sb_card_per_row", 0)
@@ -79,6 +78,9 @@ object Config {
val manga_dl_max_batch = PreferenceInt("settings_cat_md_sb_max_batch", 16) val manga_dl_max_batch = PreferenceInt("settings_cat_md_sb_max_batch", 16)
val manga_dl_show_0m_manga = PreferenceBoolean("settings_cat_md_sw_show_0m_manga", false) val manga_dl_show_0m_manga = PreferenceBoolean("settings_cat_md_sw_show_0m_manga", false)
val reverseProxyUrl = PreferenceString(R.string.reverseProxyKeyID)
val networkApiUrl = PreferenceString("settings_cat_net_et_api_url", R.string.hostUrl)
val proxy_key = PreferenceString(R.string.imgProxyCodeKeyID)
val net_use_gzip = PreferenceBoolean("settings_cat_net_sw_use_gzip", false) val net_use_gzip = PreferenceBoolean("settings_cat_net_sw_use_gzip", false)
val net_use_json = PreferenceBoolean("settings_cat_net_sw_use_json", false) val net_use_json = PreferenceBoolean("settings_cat_net_sw_use_json", false)
val net_platform = PreferenceBoolean("settings_cat_net_sw_platform", false) val net_platform = PreferenceBoolean("settings_cat_net_sw_platform", false)
@@ -88,7 +90,7 @@ object Config {
val net_no_webp = PreferenceBoolean("settings_cat_net_no_webp", false) val net_no_webp = PreferenceBoolean("settings_cat_net_no_webp", false)
val net_use_comandy = PreferenceBoolean("settings_cat_net_sw_use_comandy", false) val net_use_comandy = PreferenceBoolean("settings_cat_net_sw_use_comandy", false)
val net_use_foreign = PreferenceBoolean("settings_cat_net_sw_use_foreign", false) val net_use_foreign = PreferenceBoolean("settings_cat_net_sw_use_foreign", false)
private val net_use_img_proxy = PreferenceBoolean("settings_cat_net_sw_use_img_proxy", false) val net_use_img_proxy = PreferenceBoolean("settings_cat_net_sw_use_img_proxy", false)
val net_use_api_proxy = PreferenceBoolean("settings_cat_net_sw_use_api_proxy", false) val net_use_api_proxy = PreferenceBoolean("settings_cat_net_sw_use_api_proxy", false)
val net_img_resolution = PreferenceString(R.string.imgResolutionKeyID) val net_img_resolution = PreferenceString(R.string.imgResolutionKeyID)
val net_umstring = PreferenceString("settings_cat_net_et_umstring") val net_umstring = PreferenceString("settings_cat_net_et_umstring")

View File

@@ -2,7 +2,9 @@ package top.fumiama.copymanga.api.manga
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import com.google.android.material.snackbar.Snackbar
import com.google.gson.Gson import com.google.gson.Gson
import kotlinx.android.synthetic.main.app_bar_main.*
import kotlinx.android.synthetic.main.card_book.* import kotlinx.android.synthetic.main.card_book.*
import kotlinx.android.synthetic.main.line_booktandb.* import kotlinx.android.synthetic.main.line_booktandb.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -56,38 +58,29 @@ class Book(val path: String, private val getString: (Int) -> String, private val
* 更新云端最新图书信息并缓存到本地 * 更新云端最新图书信息并缓存到本地
*/ */
suspend fun updateInfo() = withContext(Dispatchers.IO) { suspend fun updateInfo() = withContext(Dispatchers.IO) {
try { var isDownload = false
var isDownload = false val data: String = if (loadCache) {
val data: String = if (loadCache) { name?.let { loadInfo(it) } ?: run {
name?.let { loadInfo(it) } ?: run {
isDownload = true
Config.myHostApiUrl.get(mBookApiUrl)
}
} else {
isDownload = true isDownload = true
Config.myHostApiUrl.get(mBookApiUrl) Config.api.get(mBookApiUrl)
} }
mBook = Gson().fromJson(data, BookInfoStructure::class.java) } else {
if (isDownload) saveInfo(data) isDownload = true
mGroupPathWords = arrayOf() Config.api.get(mBookApiUrl)
mKeys = arrayOf() }
mCounts = intArrayOf() mBook = Gson().fromJson(data, BookInfoStructure::class.java)
mBook?.results?.groups?.values?.forEach { if (isDownload) saveInfo(data)
mKeys += it.name mGroupPathWords = arrayOf()
mGroupPathWords += it.path_word mKeys = arrayOf()
if (it.count == 0) { mCounts = intArrayOf()
it.count = 1 mBook?.results?.groups?.values?.forEach {
} mKeys += it.name
mCounts += it.count mGroupPathWords += it.path_word
Log.d("MyB", "Add caption: ${it.name} @ ${it.path_word} of ${it.count}") if (it.count == 0) {
} it.count = 1
} catch (e: Exception) {
e.printStackTrace()
MainActivity.mainWeakReference?.get()?.apply {
withContext(Dispatchers.Main) {
runOnUiThread { Toast.makeText(this@apply, "${e::class.simpleName} ${e.message}", Toast.LENGTH_SHORT).show() }
}
} }
mCounts += it.count
Log.d("MyB", "Add caption: ${it.name} @ ${it.path_word} of ${it.count}")
} }
} }

View File

@@ -24,7 +24,7 @@ class Shelf(private val getString: (Int) -> String) {
append("") append("")
append(Config.token.value) append(Config.token.value)
} }
val re = Config.myHostApiUrl.request( val re = Config.api.request(
addApiUrl, body.encodeToByteArray(), "POST", addApiUrl, body.encodeToByteArray(), "POST",
"application/x-www-form-urlencoded;charset=utf-8") "application/x-www-form-urlencoded;charset=utf-8")
Gson().fromJson(re, ReturnBase::class.java).message Gson().fromJson(re, ReturnBase::class.java).message
@@ -43,7 +43,7 @@ class Shelf(private val getString: (Int) -> String) {
append("authorization=Token+") append("authorization=Token+")
append(Config.token.value) append(Config.token.value)
} }
val re = Config.myHostApiUrl.request( val re = Config.api.request(
delApiUrl, body.encodeToByteArray(), delApiUrl, body.encodeToByteArray(),
"DELETE", "application/x-www-form-urlencoded;charset=utf-8") "DELETE", "application/x-www-form-urlencoded;charset=utf-8")
Gson().fromJson(re, ReturnBase::class.java).message Gson().fromJson(re, ReturnBase::class.java).message
@@ -51,7 +51,7 @@ class Shelf(private val getString: (Int) -> String) {
suspend fun query(pathWord: String): BookQueryStructure? = withContext(Dispatchers.IO) { suspend fun query(pathWord: String): BookQueryStructure? = withContext(Dispatchers.IO) {
val queryUrl = queryApiUrlTemplate.format(pathWord, Config.platform.value) val queryUrl = queryApiUrlTemplate.format(pathWord, Config.platform.value)
Config.myHostApiUrl.get(queryUrl).let { Config.api.get(queryUrl).let {
Gson().fromJson(it, BookQueryStructure::class.java) Gson().fromJson(it, BookQueryStructure::class.java)
} }
} }

View File

@@ -50,18 +50,14 @@ class Api {
e.printStackTrace() e.printStackTrace()
mHostApiUrls = mutableListOf(networkApiUrl.value) mHostApiUrls = mutableListOf(networkApiUrl.value)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
runOnUiThread { Toast.makeText(this@apply, "${e::class.simpleName} ${e.message}", Toast.LENGTH_LONG).show()
Toast.makeText(this@apply, "${e::class.simpleName} ${e.message}", Toast.LENGTH_SHORT).show()
}
} }
} }
if (mHostApiUrls.isEmpty()) { if (mHostApiUrls.isEmpty()) {
mHostApiUrls = mutableListOf(networkApiUrl.value) mHostApiUrls = mutableListOf(networkApiUrl.value)
Log.d("MyApi", "myHostApiUrl set default ${mHostApiUrls[0]}") Log.d("MyApi", "myHostApiUrl set default ${mHostApiUrls[0]}")
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
runOnUiThread { Toast.makeText(this@apply, "无法获取API列表", Toast.LENGTH_SHORT).show()
Toast.makeText(this@apply, "无法获取API列表", Toast.LENGTH_SHORT).show()
}
} }
} }
} }
@@ -77,22 +73,12 @@ class Api {
var r: ReturnBase? = null var r: ReturnBase? = null
apis.forEachIndexed { i, api -> apis.forEachIndexed { i, api ->
val u = "https://$api$path" val u = "https://$api$path"
var ret = ""
try { try {
val ret = (apiProxy?.comancry(u) { ret = (apiProxy?.comancry(u) {
DownloadTools.getApiContent(it) DownloadTools.getApiContent(it)
}?: DownloadTools.getApiContent(u)).decodeToString() }?: DownloadTools.getApiContent(u)).decodeToString()
r = Gson().fromJson(ret, ReturnBase::class.java) r = Gson().fromJson(ret, ReturnBase::class.java)
if (r!!.code != 200) {
withContext(Dispatchers.Main) {
MainActivity.mainWeakReference?.get()?.apply {
runOnUiThread {
Toast.makeText(this, "错误码${r?.code?:-1}, 信息: ${r?.message?:"空"}", Toast.LENGTH_SHORT).show()
}
}
}
} else {
return ret
}
} catch (e: Exception) { } catch (e: Exception) {
mu.withLock { mu.withLock {
if (mHostApiUrls.size <= 1) return@withLock if (mHostApiUrls.size <= 1) return@withLock
@@ -102,11 +88,19 @@ class Api {
throw e throw e
} }
} }
r?.let {
if (it.code != 200) {
throw IllegalArgumentException("错误码${it.code}, 信息: ${it.message?:"空"}")
} else {
return ret
}
}
} }
throw IllegalStateException("错误码${r?.code?:-1}, 信息: ${r?.message?:"空"}") throw NoSuchElementException("无可用API")
} }
// request throw error on non-json or non-200 or empty apis, path: /api/v3/xxx, return json string // request throw error on non-json or non-200 or empty apis, path: /api/v3/xxx, return json string
suspend fun request(path: String, body: ByteArray, method: String, contentType: String, forceApi: String? = null): String { suspend fun request(path: String, body: ByteArray, method: String, contentType: String, forceApi: String? = null): String {
val apis = if (forceApi == null) mu.withLock { mHostApiUrls } else mutableListOf(forceApi) val apis = if (forceApi == null) mu.withLock { mHostApiUrls } else mutableListOf(forceApi)
if (apis.isEmpty()) { if (apis.isEmpty()) {
throw NoSuchElementException("API列表为空") throw NoSuchElementException("API列表为空")
@@ -114,22 +108,12 @@ class Api {
var r: ReturnBase? = null var r: ReturnBase? = null
apis.forEachIndexed { i, api -> apis.forEachIndexed { i, api ->
val u = "https://$api$path" val u = "https://$api$path"
var ret = ""
try { try {
val ret = (apiProxy?.comancry(u) { ret = (apiProxy?.comancry(u) {
DownloadTools.requestApiWithBody(u, method, body, contentType) DownloadTools.requestApiWithBody(u, method, body, contentType)
}?: DownloadTools.requestApiWithBody(u, method, body, contentType)).decodeToString() }?: DownloadTools.requestApiWithBody(u, method, body, contentType)).decodeToString()
r = Gson().fromJson(ret, ReturnBase::class.java) r = Gson().fromJson(ret, ReturnBase::class.java)
if (r!!.code != 200) {
withContext(Dispatchers.Main) {
MainActivity.mainWeakReference?.get()?.apply {
runOnUiThread {
Toast.makeText(this, "错误码${r?.code?:-1}, 信息: ${r?.message?:"空"}", Toast.LENGTH_SHORT).show()
}
}
}
} else {
return ret
}
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
mu.withLock { mu.withLock {
@@ -140,7 +124,14 @@ class Api {
throw e throw e
} }
} }
r?.let {
if (it.code != 200) {
throw IllegalArgumentException("错误码${it.code}, 信息: ${it.message?:"空"}")
} else {
return ret
}
}
} }
throw IllegalStateException("错误码${r?.code?:-1}, 信息: ${r?.message?:"空"}") throw NoSuchElementException("无可用API")
} }
} }

View File

@@ -24,15 +24,15 @@ class Member(private val getString: (Int) -> String) {
* - **code**: 449: 未登录, 450: 有 Exception * - **code**: 449: 未登录, 450: 有 Exception
* - **message**: 可以 toast 的信息 * - **message**: 可以 toast 的信息
*/ */
suspend fun info(): LoginInfoStructure = withContext(Dispatchers.IO) { suspend fun info(): LoginInfoStructure {
if (!hasLogin) { if (!hasLogin) {
throw IllegalArgumentException(getString(R.string.noLogin)) throw IllegalArgumentException(getString(R.string.noLogin))
} }
val u = getString(R.string.memberInfoApiUrl) val u = getString(R.string.memberInfoApiUrl)
.format(Config.platform.value) .format(Config.platform.value)
val l = Gson().fromJson(Config.myHostApiUrl.get(u), LoginInfoStructure::class.java) val l = Gson().fromJson(Config.api.get(u), LoginInfoStructure::class.java)
if (l.code == 200) Config.avatar.value = l.results.avatar if (l.code == 200) Config.avatar.value = l.results.avatar // must be true
l return l
} }
suspend fun logout() = withContext(Dispatchers.IO) { suspend fun logout() = withContext(Dispatchers.IO) {
@@ -43,7 +43,7 @@ class Member(private val getString: (Int) -> String) {
Config.avatar.value = null Config.avatar.value = null
} }
private suspend fun saveInfo(data: ByteArray) = data.inputStream().use { dataIn -> private fun saveInfo(data: ByteArray) = data.inputStream().use { dataIn ->
try { try {
Gson().fromJson(dataIn.reader(), LoginInfoStructure::class.java)?.let { l -> Gson().fromJson(dataIn.reader(), LoginInfoStructure::class.java)?.let { l ->
if (l.code == 200) { if (l.code == 200) {
@@ -51,9 +51,8 @@ class Member(private val getString: (Int) -> String) {
Config.user_id.value = l.results?.user_id Config.user_id.value = l.results?.user_id
Config.username.value = l.results?.username Config.username.value = l.results?.username
Config.nickname.value = l.results.nickname Config.nickname.value = l.results.nickname
return@use info()
} }
return@use l l
} ?: throw Exception(getString(R.string.login_parse_json_error)) } ?: throw Exception(getString(R.string.login_parse_json_error))
} catch (e: JsonSyntaxException) { } catch (e: JsonSyntaxException) {
throw JsonSyntaxException(data.decodeToString(), e) throw JsonSyntaxException(data.decodeToString(), e)
@@ -65,7 +64,7 @@ class Member(private val getString: (Int) -> String) {
val r = if (!Config.net_use_foreign.value) "1" else "0" val r = if (!Config.net_use_foreign.value) "1" else "0"
val pwdEncoded = val pwdEncoded =
Base64.encode("$pwd-$salt".toByteArray(), Base64.DEFAULT).decodeToString() Base64.encode("$pwd-$salt".toByteArray(), Base64.DEFAULT).decodeToString()
Config.myHostApiUrl.request(u, "username=${ Config.api.request(u, "username=${
URLEncoder.encode( URLEncoder.encode(
username, username,
Charset.defaultCharset().name() Charset.defaultCharset().name()

View File

@@ -6,10 +6,12 @@ import com.google.gson.Gson
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.json.JSONObject
import top.fumiama.copymanga.api.Config import top.fumiama.copymanga.api.Config
import top.fumiama.copymanga.api.Config.proxyUrl import top.fumiama.copymanga.api.Config.proxyUrl
import top.fumiama.copymanga.json.ComandyCapsule import top.fumiama.copymanga.json.ComandyCapsule
import top.fumiama.copymanga.lib.Comandy import top.fumiama.copymanga.lib.Comandy
import java.io.BufferedReader
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.InputStream import java.io.InputStream
@@ -146,6 +148,19 @@ object DownloadTools {
} else ret } else ret
} }
private fun parseErrorResponse(body: String): String {
return try {
val json = JSONObject(body)
if (json.has("detail")) {
json.getString("detail")
} else {
body // 原始 JSON 返回
}
} catch (e: Exception) {
"非JSON返回: $body"
}
}
private fun InputStream.readBytesWithProgress(sz: Int, p: Client.Progress): ByteArray { private fun InputStream.readBytesWithProgress(sz: Int, p: Client.Progress): ByteArray {
val buffer = ByteArrayOutputStream(maxOf(DEFAULT_BUFFER_SIZE, this.available())) val buffer = ByteArrayOutputStream(maxOf(DEFAULT_BUFFER_SIZE, this.available()))
copyToWithProgress(buffer, sz, p) copyToWithProgress(buffer, sz, p)
@@ -184,7 +199,7 @@ object DownloadTools {
//Log.d("MyDT", "comandy reply: $result") //Log.d("MyDT", "comandy reply: $result")
Gson().fromJson(result, ComandyCapsule::class.java)!!.let { Gson().fromJson(result, ComandyCapsule::class.java)!!.let {
if (it.code != 200) throw IllegalArgumentException("HTTP${it.code} ${ if (it.code != 200) throw IllegalArgumentException("HTTP${it.code} ${
it.data?.let { d -> Base64.decode(d, Base64.DEFAULT).decodeToString() } it.data?.let { d -> parseErrorResponse(Base64.decode(d, Base64.DEFAULT).decodeToString()) }
}") }")
coding = it.headers["Content-Encoding"] as String? coding = it.headers["Content-Encoding"] as String?
Base64.decode(it.data, Base64.DEFAULT) Base64.decode(it.data, Base64.DEFAULT)
@@ -198,10 +213,14 @@ object DownloadTools {
} }
getApiConnection(u, "GET").let { conn -> getApiConnection(u, "GET").let { conn ->
val sz = conn.getHeaderFieldInt("Content-Length", 0) val sz = conn.getHeaderFieldInt("Content-Length", 0)
if (conn.responseCode >= 400)
throw IllegalArgumentException("HTTP${conn.responseCode} " +
parseErrorResponse(conn.errorStream.bufferedReader().use(BufferedReader::readText))
)
val ret = if (sz > 0 && p != null) { val ret = if (sz > 0 && p != null) {
conn.inputStream.readBytesWithProgress(sz, p) conn.inputStream.use { it.readBytesWithProgress(sz, p) }
} else { } else {
conn.inputStream.readBytes() conn.inputStream.use(InputStream::readBytes)
} }
conn.disconnect() conn.disconnect()
Log.d("MyDT", "getHttpContent: ${ret.size} bytes") Log.d("MyDT", "getHttpContent: ${ret.size} bytes")
@@ -255,7 +274,9 @@ object DownloadTools {
completed = true completed = true
p?.notify(100) p?.notify(100)
Gson().fromJson(result, ComandyCapsule::class.java)?.let { Gson().fromJson(result, ComandyCapsule::class.java)?.let {
if (it.code != 200) null if (it.code != 200) throw IllegalArgumentException("HTTP${it.code} ${
it.data?.let { d -> Base64.decode(d, Base64.DEFAULT).decodeToString() }
}")
else it.data?.let { d -> Base64.decode(d, Base64.DEFAULT) } else it.data?.let { d -> Base64.decode(d, Base64.DEFAULT) }
} }
} }
@@ -272,17 +293,17 @@ object DownloadTools {
var ret: ByteArray? = null var ret: ByteArray? = null
try { try {
val connection = getNormalConnection(u, "GET", Config.pc_ua) val connection = getNormalConnection(u, "GET", Config.pc_ua)
val ci = connection.inputStream
val sz = connection.getHeaderFieldInt("Content-Length", 0) val sz = connection.getHeaderFieldInt("Content-Length", 0)
if(readSize > 0) { connection.inputStream.use { ci ->
ret = ByteArray(readSize) if(readSize > 0) {
ci?.read(ret, 0, readSize) ret = ByteArray(readSize)
} else ret = if (sz > 0 && p != null) { ci.read(ret, 0, readSize)
ci.readBytesWithProgress(sz, p) } else ret = if (sz > 0 && p != null) {
} else { ci.readBytesWithProgress(sz, p)
ci.readBytes() } else {
ci.readBytes()
}
} }
ci?.close()
connection.disconnect() connection.disconnect()
} catch (ex: Exception) { } catch (ex: Exception) {
ex.printStackTrace() ex.printStackTrace()
@@ -313,7 +334,11 @@ object DownloadTools {
var coding = "" var coding = ""
getApiConnection(url, method).apply { getApiConnection(url, method).apply {
outputStream.write(body) outputStream.write(body)
ret = inputStream.readBytes() if (responseCode >= 400)
throw IllegalArgumentException("HTTP${responseCode} " +
parseErrorResponse(errorStream.bufferedReader().use(BufferedReader::readText))
)
ret = inputStream.use(InputStream::readBytes)
disconnect() disconnect()
coding = getHeaderField("Content-Encoding")?:"" coding = getHeaderField("Content-Encoding")?:""
} }

View File

@@ -76,7 +76,7 @@ open class AutoDownloadHandler(
var cnt = 0 var cnt = 0
while (cnt++ <= 3) { while (cnt++ <= 3) {
try { try {
val data = Config.myHostApiUrl.get(url()) val data = Config.api.get(url())
if(exit) return@withContext if(exit) return@withContext
val pass = setGsonItem(Gson().fromJson(data, jsonClass)) val pass = setGsonItem(Gson().fromJson(data, jsonClass))
if (pass && loadFromCache) { if (pass && loadFromCache) {

View File

@@ -13,7 +13,7 @@ class PausableDownloader(private val url: String, private val waitMilliseconds:
var c = 0 var c = 0
while (!exit && c++ < 3) { while (!exit && c++ < 3) {
try { try {
val data = Config.myHostApiUrl.get(url) val data = Config.api.get(url)
whenFinish?.let { it(data.encodeToByteArray()) } whenFinish?.let { it(data.encodeToByteArray()) }
return@withContext true return@withContext true
} catch (e: Exception) { } catch (e: Exception) {

View File

@@ -0,0 +1,290 @@
package top.fumiama.copymanga.storage
import top.fumiama.copymanga.api.Config
import java.nio.ByteBuffer
import java.util.zip.CRC32
class DataLoader {
data class Settings(
var appVer: String,
var platform: String,
var generalEnableTransparentSystemBar: Boolean,
var generalDisableKanbanAnimation: Boolean,
var generalCardPerRow: Int,
var mangaDlMaxBatch: Int,
var mangaDlShow0mManga: Boolean,
var reverseProxyUrl: String,
var networkApiUrl: String,
var proxyKey: String,
var netUseGzip: Boolean,
var netUseJson: Boolean,
var netPlatform: Boolean,
var netReferer: Boolean,
var netVersion: Boolean,
var netRegion: Boolean,
var netNoWebp: Boolean,
var netUseComandy: Boolean,
var netUseForeign: Boolean,
var netUseImgProxy: Boolean,
var netUseApiProxy: Boolean,
var netImgResolution: String,
var netUmstring: String,
var netSource: String,
var netUa: String,
var viewMangaInverseChapters: Boolean,
var viewMangaAlwaysDarkBg: Boolean,
var viewMangaVerticalMax: Int,
var viewMangaQuality: Int,
var viewMangaVolTurn: Boolean,
var viewMangaUseCellar: Boolean,
var viewMangaHideInfo: Boolean,
) {
fun export() {
Config.app_ver.value = appVer
Config.platform.value = platform
Config.general_enable_transparent_system_bar.value = generalEnableTransparentSystemBar
Config.general_disable_kanban_animation.value = generalDisableKanbanAnimation
Config.general_card_per_row.value = generalCardPerRow
Config.manga_dl_max_batch.value = mangaDlMaxBatch
Config.manga_dl_show_0m_manga.value = mangaDlShow0mManga
Config.reverseProxyUrl.value = reverseProxyUrl
Config.networkApiUrl.value = networkApiUrl
Config.proxy_key.value = proxyKey
Config.net_use_gzip.value = netUseGzip
Config.net_use_json.value = netUseJson
Config.net_platform.value = netPlatform
Config.net_referer.value = netReferer
Config.net_version.value = netVersion
Config.net_region.value = netRegion
Config.net_no_webp.value = netNoWebp
Config.net_use_comandy.value = netUseComandy
Config.net_use_foreign.value = netUseForeign
Config.net_use_img_proxy.value = netUseImgProxy
Config.net_use_api_proxy.value = netUseApiProxy
Config.net_img_resolution.value = netImgResolution
Config.net_umstring.value = netUmstring
Config.net_source.value = netSource
Config.net_ua.value = netUa
Config.view_manga_inverse_chapters.value = viewMangaInverseChapters
Config.view_manga_always_dark_bg.value = viewMangaAlwaysDarkBg
Config.view_manga_vertical_max.value = viewMangaVerticalMax
Config.view_manga_quality.value = viewMangaQuality
Config.view_manga_vol_turn.value = viewMangaVolTurn
Config.view_manga_use_cellar.value = viewMangaUseCellar
Config.view_manga_hide_info.value = viewMangaHideInfo
}
}
val settings: Settings
constructor(): this(Settings(
appVer = Config.app_ver.value,
platform = Config.platform.value,
generalEnableTransparentSystemBar = Config.general_enable_transparent_system_bar.value,
generalDisableKanbanAnimation = Config.general_disable_kanban_animation.value,
generalCardPerRow = Config.general_card_per_row.value,
mangaDlMaxBatch = Config.manga_dl_max_batch.value,
mangaDlShow0mManga = Config.manga_dl_show_0m_manga.value,
reverseProxyUrl = Config.reverseProxyUrl.value,
networkApiUrl = Config.networkApiUrl.value,
proxyKey = Config.proxy_key.value,
netUseGzip = Config.net_use_gzip.value,
netUseJson = Config.net_use_json.value,
netPlatform = Config.net_platform.value,
netReferer = Config.net_referer.value,
netVersion = Config.net_version.value,
netRegion = Config.net_region.value,
netNoWebp = Config.net_no_webp.value,
netUseComandy = Config.net_use_comandy.value,
netUseForeign = Config.net_use_foreign.value,
netUseImgProxy = Config.net_use_img_proxy.value,
netUseApiProxy = Config.net_use_api_proxy.value,
netImgResolution = Config.net_img_resolution.value,
netUmstring = Config.net_umstring.value,
netSource = Config.net_source.value,
netUa = Config.net_ua.value,
viewMangaInverseChapters = Config.view_manga_inverse_chapters.value,
viewMangaAlwaysDarkBg = Config.view_manga_always_dark_bg.value,
viewMangaVerticalMax = Config.view_manga_vertical_max.value,
viewMangaQuality = Config.view_manga_quality.value,
viewMangaVolTurn = Config.view_manga_vol_turn.value,
viewMangaUseCellar = Config.view_manga_use_cellar.value,
viewMangaHideInfo = Config.view_manga_hide_info.value,
))
constructor(settings: Settings) {
this.settings = settings
}
constructor(data: ByteArray) {
if (data.size < 4) throw IllegalArgumentException("Data too short for CRC")
val payload = data.copyOfRange(0, data.size - 4)
val expectedCrc = ByteBuffer.wrap(data, data.size - 4, 4).int
val actualCrc = CRC32().apply { update(payload) }.value.toInt()
if (expectedCrc != actualCrc) throw IllegalArgumentException("CRC32 check failed")
val buffer = ByteBuffer.wrap(payload)
val netFlags = buffer.short.toInt() and 0xFFFF
val miscFlags = buffer.get().toInt() and 0xFF
fun bit(value: Int, pos: Int) = ((value shr pos) and 1) != 0
val generalCardPerRow = buffer.int
val mangaDlMaxBatch = buffer.int
val viewMangaVerticalMax = buffer.int
val viewMangaQuality = buffer.int
fun readString(): String {
val len = buffer.short.toInt()
val bytes = ByteArray(len)
buffer.get(bytes)
return String(bytes, Charsets.UTF_8)
}
settings = Settings(
appVer = readString(),
platform = readString(),
generalEnableTransparentSystemBar = bit(miscFlags, 7),
generalDisableKanbanAnimation = bit(miscFlags, 6),
generalCardPerRow = generalCardPerRow,
mangaDlMaxBatch = mangaDlMaxBatch,
mangaDlShow0mManga = bit(miscFlags, 5),
reverseProxyUrl = readString(),
networkApiUrl = readString(),
proxyKey = readString(),
netUseGzip = bit(netFlags, 15),
netUseJson = bit(netFlags, 14),
netPlatform = bit(netFlags, 13),
netReferer = bit(netFlags, 12),
netVersion = bit(netFlags, 11),
netRegion = bit(netFlags, 10),
netNoWebp = bit(netFlags, 9),
netUseComandy = bit(netFlags, 8),
netUseForeign = bit(netFlags, 7),
netUseImgProxy = bit(netFlags, 6),
netUseApiProxy = bit(netFlags, 5),
netImgResolution = readString(),
netUmstring = readString(),
netSource = readString(),
netUa = readString(),
viewMangaInverseChapters = bit(miscFlags, 4),
viewMangaAlwaysDarkBg = bit(miscFlags, 3),
viewMangaVerticalMax = viewMangaVerticalMax,
viewMangaQuality = viewMangaQuality,
viewMangaVolTurn = bit(miscFlags, 2),
viewMangaUseCellar = bit(miscFlags, 1),
viewMangaHideInfo = bit(miscFlags, 0),
)
}
fun toByteArray(): ByteArray {
val strFields = listOf(
settings.appVer, settings.platform,
settings.reverseProxyUrl, settings.networkApiUrl, settings.proxyKey,
settings.netImgResolution, settings.netUmstring, settings.netSource, settings.netUa
)
val strBytes = strFields.map { it.toByteArray(Charsets.UTF_8) }
val netFlags = wrapBooleans(
settings.netUseGzip,
settings.netUseJson,
settings.netPlatform,
settings.netReferer,
settings.netVersion,
settings.netRegion,
settings.netNoWebp,
settings.netUseComandy,
settings.netUseForeign,
settings.netUseImgProxy,
settings.netUseApiProxy,
false, false, false, false, false
)
val miscFlags = wrapBooleans(
settings.generalEnableTransparentSystemBar,
settings.generalDisableKanbanAnimation,
settings.mangaDlShow0mManga,
settings.viewMangaInverseChapters,
settings.viewMangaAlwaysDarkBg,
settings.viewMangaVolTurn,
settings.viewMangaUseCellar,
settings.viewMangaHideInfo
)
val strTotal = strBytes.sumOf { 2 + it.size }
val buffer = ByteBuffer.allocate(
2 + // netFlags: UShort (16 bits)
1 + // miscFlags: UByte (8 bits)
4 * 4 + // Ints: 4 fields x 4 bytes
strTotal + // all string fields with 2-byte length prefix
4 // CRC32
)
buffer.putShort(netFlags.toShort())
buffer.put(miscFlags.toByte())
buffer.putInt(settings.generalCardPerRow)
buffer.putInt(settings.mangaDlMaxBatch)
buffer.putInt(settings.viewMangaVerticalMax)
buffer.putInt(settings.viewMangaQuality)
for (b in strBytes) {
buffer.putShort(b.size.toShort())
buffer.put(b)
}
val payload = buffer.array().copyOfRange(0, buffer.position())
val crc = CRC32().apply { update(payload) }.value
val finalBuffer = ByteBuffer.allocate(payload.size + 4)
finalBuffer.put(payload)
finalBuffer.putInt(crc.toInt())
return finalBuffer.array()
}
private fun wrapBooleans(
b0: Boolean, b1: Boolean, b2: Boolean, b3: Boolean,
b4: Boolean, b5: Boolean, b6: Boolean, b7: Boolean
): UByte {
var result: UByte = 0u
val flags = listOf(b0, b1, b2, b3, b4, b5, b6, b7)
for ((i, flag) in flags.withIndex()) {
if (flag) result = result or (1u shl (7 - i)).toUByte()
}
return result
}
private fun wrapBooleans(
b0: Boolean, b1: Boolean, b2: Boolean, b3: Boolean,
b4: Boolean, b5: Boolean, b6: Boolean, b7: Boolean,
b8: Boolean, b9: Boolean, b10: Boolean, b11: Boolean,
b12: Boolean, b13: Boolean, b14: Boolean, b15: Boolean
): UShort {
var result: UShort = 0u
val flags = listOf(
b0, b1, b2, b3, b4, b5, b6, b7,
b8, b9, b10, b11, b12, b13, b14, b15
)
for ((i, flag) in flags.withIndex()) {
if (flag) result = result or (1u shl (15 - i)).toUShort()
}
return result
}
}

View File

@@ -1,20 +1,30 @@
package top.fumiama.copymanga.storage package top.fumiama.copymanga.storage
import android.util.Log import android.util.Log
import androidx.core.content.edit
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import top.fumiama.copymanga.MainActivity import top.fumiama.copymanga.MainActivity
data class PreferenceBoolean(private val key: String, private var default: Boolean) { data class PreferenceBoolean(private val key: String, private var default: Boolean) {
val value: Boolean var value: Boolean = default
get() { get() {
MainActivity.mainWeakReference?.get()?.let { MainActivity.mainWeakReference?.get()?.let {
PreferenceManager.getDefaultSharedPreferences(it).apply { PreferenceManager.getDefaultSharedPreferences(it).apply {
getBoolean(key, default).let { v -> getBoolean(key, field).let { v ->
Log.d("MyPB", "get key $key value $v") Log.d("MyPB", "get key $key value $v")
return v return v
} }
} }
} }
return default return field
}
set(value) {
field = value
Log.d("MyUPI", "set key $key value $value")
MainActivity.mainWeakReference?.get()?.let {
PreferenceManager.getDefaultSharedPreferences(it).apply {
edit(commit = true) { putBoolean(key, value) }
}
}
} }
} }

View File

@@ -1,20 +1,30 @@
package top.fumiama.copymanga.storage package top.fumiama.copymanga.storage
import android.util.Log import android.util.Log
import androidx.core.content.edit
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import top.fumiama.copymanga.MainActivity import top.fumiama.copymanga.MainActivity
data class PreferenceInt(private val key: String, private var default: Int) { data class PreferenceInt(private val key: String, private var default: Int) {
val value: Int var value: Int = default
get() { get() {
MainActivity.mainWeakReference?.get()?.let { MainActivity.mainWeakReference?.get()?.let {
PreferenceManager.getDefaultSharedPreferences(it).apply { PreferenceManager.getDefaultSharedPreferences(it).apply {
getInt(key, default).let { v -> getInt(key, field).let { v ->
Log.d("MyPI", "get key $key value $v") Log.d("MyPI", "get key $key value $v")
return v return v
} }
} }
} }
return default return field
}
set(value) {
field = value
Log.d("MyUPI", "set key $key value $value")
MainActivity.mainWeakReference?.get()?.let {
PreferenceManager.getDefaultSharedPreferences(it).apply {
edit(commit = true) { putInt(key, value) }
}
}
} }
} }

View File

@@ -1,6 +1,7 @@
package top.fumiama.copymanga.storage package top.fumiama.copymanga.storage
import android.util.Log import android.util.Log
import androidx.core.content.edit
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import top.fumiama.copymanga.MainActivity import top.fumiama.copymanga.MainActivity
@@ -20,7 +21,7 @@ data class PreferenceString(private val key: String, private var default: String
} }
return default?:"" return default?:""
} }
val value: String var value: String = defaultField
get() { get() {
MainActivity.mainWeakReference?.get()?.let { MainActivity.mainWeakReference?.get()?.let {
PreferenceManager.getDefaultSharedPreferences(it).apply { PreferenceManager.getDefaultSharedPreferences(it).apply {
@@ -32,7 +33,16 @@ data class PreferenceString(private val key: String, private var default: String
} }
} }
} }
Log.d("MyPS", "get default key $key value $defaultField") Log.d("MyPS", "get default key $key value $field")
return defaultField return field
}
set(value) {
field = value
Log.d("MyUPI", "set key $key value $value")
MainActivity.mainWeakReference?.get()?.let {
PreferenceManager.getDefaultSharedPreferences(it).apply {
edit(commit = true) { putString(key, value) }
}
}
} }
} }

View File

@@ -1,63 +0,0 @@
package top.fumiama.copymanga.storage
//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 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)
createNew(f)
} else if(f.isDirectory) {
if(f.parentFile?.canWrite() != true) f.parentFile?.setWritable(true)
f.delete()
createNew(f)
}
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()
f.outputStream().use { o ->
this.storeToXML(o, "store")
}
Log.d("MyPT", "Generate new prop.")
}
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{
return if(cache.containsKey(key)) cache[key]?:"null"
else {
f.inputStream().use { i ->
val re = this.loadFromXml(i).getProperty(key)?:"null"
Log.d("MyPT", "Read $key = $re")
cache[key] = re
re
}
}
}
operator fun set(key: String, value: String) {
cache[key] = value
f.outputStream().use { o ->
this.setProp(key, value).storeToXML(o, "store")
}
Log.d("MyPT", "Set $key = $value")
}
}

View File

@@ -0,0 +1,73 @@
package top.fumiama.copymanga.strings
import android.util.Base64
import android.util.Log
object Base16384 {
private const val BASE = 16384
private const val FIRST_CHAR = 0x4E00
private const val PAD_START = 0x3D00
fun encode(input: ByteArray): String {
val sb = StringBuilder()
var buffer = 0
var bitCount = 0
for (b in input) {
buffer = (buffer shl 8) or (b.toInt() and 0xFF)
bitCount += 8
while (bitCount >= 14) {
bitCount -= 14
val index = (buffer shr bitCount) and (BASE - 1)
sb.append(Char(FIRST_CHAR + index))
buffer = buffer and ((1 shl bitCount) - 1)
}
}
if (bitCount in 1..6) {
buffer = (buffer shl (14 - bitCount))
val index = buffer and (BASE - 1)
sb.append(Char(FIRST_CHAR + index))
val padIndex = PAD_START + bitCount
sb.append(Char(padIndex))
}
Log.d("MyB14", "encode bitCount $bitCount, len ${input.size}, data: ${Base64.encode(input, Base64.DEFAULT).decodeToString()}")
return sb.toString()
}
fun decode(input: String): ByteArray {
val bits = mutableListOf<Boolean>()
var tailBits = 0
for (c in input) {
when (val code = c.code) {
in FIRST_CHAR until FIRST_CHAR + BASE -> {
val idx = code - FIRST_CHAR
for (i in 13 downTo 0) {
bits.add(((idx shr i) and 1) == 1)
}
}
in (PAD_START + 1)..(PAD_START + 6) -> {
tailBits = code - PAD_START
break
}
else -> throw IllegalArgumentException("Invalid base16384 char: $c")
}
}
val totalBits = bits.size - (14 - tailBits)
val byteCount = totalBits / 8
val out = ByteArray(byteCount)
for (i in 0 until byteCount) {
var byteVal = 0
for (j in 0 until 8) {
if (bits[i * 8 + j]) byteVal = byteVal or (1 shl (7 - j))
}
out[i] = byteVal.toByte()
}
Log.d("MyB14", "decode totalBits $totalBits, tailBits $tailBits, len ${out.size}, data: ${Base64.encode(out, Base64.DEFAULT).decodeToString()}")
return out
}
}

View File

@@ -28,9 +28,8 @@ object Chinese {
/** /**
* 简单检测字符串是否包含常见 CJK中日韩汉字。 * 简单检测字符串是否包含常见 CJK中日韩汉字。
*/ */
fun containsChinese(text: String): Boolean { private fun containsChinese(text: String): Boolean {
val regex = Regex("[\u4E00-\u9FFF]") val regex = Regex("[\u4E00-\u9FFF]")
return regex.containsMatchIn(text) return regex.containsMatchIn(text)
} }
} }

View File

@@ -11,6 +11,7 @@ import android.widget.Toast
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import com.google.android.material.color.MaterialColors import com.google.android.material.color.MaterialColors
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.app_bar_main.* import kotlinx.android.synthetic.main.app_bar_main.*
import kotlinx.android.synthetic.main.card_book.* import kotlinx.android.synthetic.main.card_book.*
import kotlinx.android.synthetic.main.fragment_book.* import kotlinx.android.synthetic.main.fragment_book.*
@@ -57,8 +58,9 @@ class BookFragment: NoBackRefreshFragment(R.layout.fragment_book) {
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
if(mBookHandler?.exit != false) return@launch if(mBookHandler?.exit != false) return@launch
Toast.makeText(context, R.string.null_book, Toast.LENGTH_SHORT).show() Snackbar.make(view, "${e::class.simpleName} ${e.message}", Snackbar.LENGTH_LONG).setTextMaxLines(10).setDuration(10000).show()
findNavController().popBackStack() // Toast.makeText(context, R.string.null_book, Toast.LENGTH_SHORT).show()
// findNavController().popBackStack()
return@launch return@launch
} }
Log.d("MyBF", "read path: ${book?.path}") Log.d("MyBF", "read path: ${book?.path}")
@@ -196,9 +198,7 @@ class BookFragment: NoBackRefreshFragment(R.layout.fragment_book) {
} }
} catch (e: Exception) { } catch (e: Exception) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
activity?.runOnUiThread { Toast.makeText(context, "${e::class.simpleName} ${e.message}", Toast.LENGTH_LONG).show()
Toast.makeText(context, "${e::class.simpleName} ${e.message}", Toast.LENGTH_SHORT).show()
}
} }
} }
} }
@@ -239,7 +239,7 @@ class BookFragment: NoBackRefreshFragment(R.layout.fragment_book) {
} }
} catch (e: Exception) { } catch (e: Exception) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
Toast.makeText(context, "${e::class.simpleName} ${e.message}", Toast.LENGTH_SHORT).show() Toast.makeText(context, "${e::class.simpleName} ${e.message}", Toast.LENGTH_LONG).show()
} }
} }
} }
@@ -258,7 +258,7 @@ class BookFragment: NoBackRefreshFragment(R.layout.fragment_book) {
} }
} catch (e: Exception) { } catch (e: Exception) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
Toast.makeText(context, "${e::class.simpleName} ${e.message}", Toast.LENGTH_SHORT).show() Toast.makeText(context, "${e::class.simpleName} ${e.message}", Toast.LENGTH_LONG).show()
} }
} }
} }

View File

@@ -10,13 +10,13 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.ImageButton import android.widget.ImageButton
import android.widget.Toast
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.load.model.GlideUrl import com.bumptech.glide.load.model.GlideUrl
import com.google.android.material.snackbar.Snackbar
import com.google.gson.Gson import com.google.gson.Gson
import com.lapism.search.internal.SearchLayout import com.lapism.search.internal.SearchLayout
import kotlinx.android.synthetic.main.card_book_plain.view.* import kotlinx.android.synthetic.main.card_book_plain.view.*
@@ -29,12 +29,12 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import top.fumiama.copymanga.MainActivity import top.fumiama.copymanga.MainActivity
import top.fumiama.copymanga.MainActivity.Companion.ime import top.fumiama.copymanga.MainActivity.Companion.ime
import top.fumiama.copymanga.json.BookListStructure
import top.fumiama.copymanga.view.template.NoBackRefreshFragment
import top.fumiama.copymanga.net.template.PausableDownloader
import top.fumiama.copymanga.api.Config import top.fumiama.copymanga.api.Config
import top.fumiama.copymanga.view.operation.GlideHideLottieViewListener import top.fumiama.copymanga.json.BookListStructure
import top.fumiama.copymanga.net.template.PausableDownloader
import top.fumiama.copymanga.view.interaction.Navigate import top.fumiama.copymanga.view.interaction.Navigate
import top.fumiama.copymanga.view.operation.GlideHideLottieViewListener
import top.fumiama.copymanga.view.template.NoBackRefreshFragment
import top.fumiama.dmzj.copymanga.R import top.fumiama.dmzj.copymanga.R
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import java.net.URLEncoder import java.net.URLEncoder
@@ -51,18 +51,15 @@ class HomeFragment : NoBackRefreshFragment(R.layout.fragment_home) {
val netInfo = tb.netInfo val netInfo = tb.netInfo
if(netInfo != tb.transportStringNull && netInfo != tb.transportStringError) if(netInfo != tb.transportStringNull && netInfo != tb.transportStringError)
MainActivity.member?.apply { lifecycleScope.launch { MainActivity.member?.apply { lifecycleScope.launch {
Config.myHostApiUrl.init() withContext(Dispatchers.IO) {
try { Config.api.init()
info().let { l -> try {
if (l.code != 200 && l.code != 449) { info()
Toast.makeText(context, l.message, Toast.LENGTH_SHORT).show() } catch (e: Exception) {
logout() Snackbar
} .make(view, "${e::class.simpleName} ${e.message}", Snackbar.LENGTH_LONG)
} .setTextMaxLines(10)
} catch (e: Exception) { .show()
e.printStackTrace()
withContext(Dispatchers.Main) {
Toast.makeText(context, "${e::class.simpleName} ${e.message}", Toast.LENGTH_SHORT).show()
} }
} }
} } } }

View File

@@ -148,9 +148,7 @@ class VMHandler(activity: ViewMangaActivity, private val chapterUrl: String, pri
override suspend fun onError() { override suspend fun onError() {
super.onError() super.onError()
if(exit) return if(exit) return
withContext(Dispatchers.Main) { wv.get()?.toolsBox?.toastErrorAndFinish(R.string.download_chapter_info_failed)
wv.get()?.toolsBox?.toastError(R.string.download_chapter_info_failed)
}
} }
override suspend fun doWhenFinishDownload() { override suspend fun doWhenFinishDownload() {
@@ -187,7 +185,7 @@ class VMHandler(activity: ViewMangaActivity, private val chapterUrl: String, pri
true true
} catch (e: Exception){ } catch (e: Exception){
e.printStackTrace() e.printStackTrace()
wv.get()?.toolsBox?.toastError(R.string.load_local_chapter_info_failed) wv.get()?.toolsBox?.toastErrorAndFinish(R.string.load_local_chapter_info_failed)
false false
} }
} }

View File

@@ -78,7 +78,6 @@ class ViewMangaActivity : TitleActivityTemplate() {
var clicked = 0 var clicked = 0
private var isInSeek = false private var isInSeek = false
private var isInScroll = true private var isInScroll = true
//private var progressLog: PropertiesTools? = null
var scrollImages = arrayOf<ScaleImageView>() var scrollImages = arrayOf<ScaleImageView>()
var scrollButtons = arrayOf<Button>() var scrollButtons = arrayOf<Button>()
var scrollPositions = arrayOf<Int>() var scrollPositions = arrayOf<Int>()
@@ -192,7 +191,7 @@ class ViewMangaActivity : TitleActivityTemplate() {
} else prepareImgFromWeb() } else prepareImgFromWeb()
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
toolsBox.toastError(R.string.load_manga_error) toolsBox.toastErrorAndFinish(R.string.load_manga_error)
} }
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
startPostponedEnterTransition() startPostponedEnterTransition()
@@ -350,7 +349,7 @@ class ViewMangaActivity : TitleActivityTemplate() {
Log.d("MyVM", "[$i] 分析: ${it.name}, cut: $useCut") Log.d("MyVM", "[$i] 分析: ${it.name}, cut: $useCut")
} }
} catch (e: Exception) { } catch (e: Exception) {
withContext(Dispatchers.Main) { toolsBox.toastError(R.string.count_zip_entries_error) } toolsBox.toastErrorAndFinish(R.string.count_zip_entries_error)
} }
Log.d("MyVM", "开始加载控件") Log.d("MyVM", "开始加载控件")
doWhenFinish(count) doWhenFinish(count)
@@ -377,9 +376,8 @@ class ViewMangaActivity : TitleActivityTemplate() {
loadOneImg() loadOneImg()
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
withContext(Dispatchers.Main) { toolsBox.toastError(getString(R.string.load_page_number_error).format(currentItem))
toolsBox.toastError(getString(R.string.load_page_number_error).format(currentItem)) finish()
}
} }
} }
} else { } else {
@@ -637,7 +635,7 @@ class ViewMangaActivity : TitleActivityTemplate() {
}*/ }*/
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
toolsBox.toastError(R.string.load_chapter_error) toolsBox.toastErrorAndFinish(R.string.load_chapter_error)
finish() finish()
} }
} }

View File

@@ -564,11 +564,11 @@ class ScaleImageView : ImageView {
} else { } else {
super.onDraw(canvas) super.onDraw(canvas)
} }
}catch (e:Exception){ } catch (e:Exception){
e.printStackTrace() e.printStackTrace()
ViewMangaActivity.va?.get()?.apply { ViewMangaActivity.va?.get()?.apply {
lifecycleScope.launch { lifecycleScope.launch {
toolsBox.toastError(R.string.show_image_error_try_lower_resolution, false) toolsBox.toastErrorAndFinish(R.string.show_image_error_try_lower_resolution, false)
} }
} }
} }

View File

@@ -43,11 +43,10 @@ class UITools(that: Context?, w: WeakReference<Activity>? = null) {
} }
} ?: transportStringError } ?: transportStringError
} }
suspend fun toastError(s: String, willFinish: Boolean = true) = withContext(Dispatchers.Main) { suspend fun toastError(s: String) = withContext(Dispatchers.Main) {
Toast.makeText(zis, s, Toast.LENGTH_SHORT).show() Toast.makeText(zis, s, Toast.LENGTH_SHORT).show()
if (willFinish) weak?.get()?.finish()
} }
suspend fun toastError(s: Int, willFinish: Boolean = true) = withContext(Dispatchers.Main) { suspend fun toastErrorAndFinish(s: Int, willFinish: Boolean = true) = withContext(Dispatchers.Main) {
Toast.makeText(zis, s, Toast.LENGTH_SHORT).show() Toast.makeText(zis, s, Toast.LENGTH_SHORT).show()
if (willFinish) weak?.get()?.finish() if (willFinish) weak?.get()?.finish()
} }

View File

@@ -2,6 +2,7 @@
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" <androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/appbar_coord"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
tools:context="top.fumiama.copymanga.MainActivity"> tools:context="top.fumiama.copymanga.MainActivity">
@@ -31,6 +32,6 @@
layout="@layout/content_main" layout="@layout/content_main"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"/> app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout> </androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@@ -67,7 +67,7 @@
<string name="analyze_img_size_error">读取图片大小失败</string> <string name="analyze_img_size_error">读取图片大小失败</string>
<string name="networkApiUrl">/api/v3/system/network2?platform=%1$s</string> <string name="networkApiUrl">/api/v3/system/network2?platform=%1$s</string>
<string name="mainPageApiUrl">/api/v3/h5/homeIndex?platform=%1$s</string> <string name="mainPageApiUrl">/api/v3/h5/homeIndex2?platform=%1$s</string>
<string name="hostUrl">&hosturl;</string> <string name="hostUrl">&hosturl;</string>
<string name="proxyUrl">&proxyurl;</string> <string name="proxyUrl">&proxyurl;</string>
<string name="rankApiUrl">/api/v3/ranks?limit=21&amp;offset=%1$d&amp;date_type=%2$s&amp;audience_type=%3$s&amp;platform=%4$s</string> <string name="rankApiUrl">/api/v3/ranks?limit=21&amp;offset=%1$d&amp;date_type=%2$s&amp;audience_type=%3$s&amp;platform=%4$s</string>

View File

@@ -2,6 +2,7 @@
buildscript { buildscript {
ext.kotlin_version = "$cm_kotlin_version" ext.kotlin_version = "$cm_kotlin_version"
repositories { repositories {
gradlePluginPortal()
google() google()
mavenCentral() mavenCentral()
maven { url 'https://maven.google.com' } maven { url 'https://maven.google.com' }