合并上游更改

感谢:@821938089、@zhongerxll、@youke2580c、@Leroy-X、@Dark-Tide、@lhjgege、@3harmony、@mgz0227
This commit is contained in:
HapeLee committed 2025-09-01 00:57:17 +08:00
1 parent dccb72adab
commit 8eb4881767
87 files changed
+800 -365

No files matched your search

+12 -3
View File
@@ -45,7 +45,7 @@
* 调用阅读搜索 * 调用阅读搜索
```js ```js
java.searchBook(bookName: string) java.searchBook(bookName: String)
``` ```
* 添加书架 * 添加书架
@@ -232,6 +232,13 @@ java.s2t(text: String): String
* 时间格式化 * 时间格式化
```js ```js
java.timeFormatUTC(time: Long, format: String, sh: Int): String? java.timeFormatUTC(time: Long, format: String, sh: Int): String?
java.timeFormat(time: Long): String
```
* html格式化
```js
java.htmlFormat(str: String): String
``` ```
* 文件 * 文件
> 所有对于文件的读写删操作都是相对路径,只能操作阅读缓存/android/data/{package}/cache/内的文件 > 所有对于文件的读写删操作都是相对路径,只能操作阅读缓存/android/data/{package}/cache/内的文件
@@ -441,9 +448,11 @@ cache.putFile(key: String, value: String, saveTime: Int)
读取文件内容 读取文件内容
cache.getFile(key: String): String? cache.getFile(key: String): String?
保存到内存 保存到内存
cache.deleteMemory(key: String)
cache.getFromMemory(key: String): Any?
cache.putMemory(key: String, value: Any) cache.putMemory(key: String, value: Any)
读取内存
cache.getFromMemory(key: String): Any?
删除内存
cache.deleteMemory(key: String)
``` ```
@@ -7,6 +7,7 @@ import android.app.NotificationManager
import android.content.Context import android.content.Context
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import android.content.res.Configuration import android.content.res.Configuration
import android.os.Build
import com.github.liuyueyi.quick.transfer.constants.TransType import com.github.liuyueyi.quick.transfer.constants.TransType
import com.google.android.material.color.DynamicColors import com.google.android.material.color.DynamicColors
import com.jeremyliao.liveeventbus.LiveEventBus import com.jeremyliao.liveeventbus.LiveEventBus
@@ -141,6 +142,9 @@ class App : Application() {
* @return * @return
*/ */
private fun installGmsTlsProvider(context: Context) { private fun installGmsTlsProvider(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
return
}
try { try {
val gmsPackageName = "com.google.android.gms" val gmsPackageName = "com.google.android.gms"
val appInfo = packageManager.getApplicationInfo(gmsPackageName, 0) val appInfo = packageManager.getApplicationInfo(gmsPackageName, 0)
@@ -75,9 +75,9 @@ abstract class BaseService : LifecycleService() {
} }
@CallSuper @CallSuper
override fun onTimeout(startId: Int) { override fun onTimeout(startId: Int, fgsType: Int) {
super.onTimeout(startId) super.onTimeout(startId, fgsType)
LogUtils.d(simpleName, "onTimeout startId:$startId") LogUtils.d(simpleName, "onTimeout startId:$startId fgsType:$fgsType")
stopSelf() stopSelf()
} }
@@ -9,7 +9,8 @@ object AppPattern {
val EXP_PATTERN: Pattern = Pattern.compile("\\{\\{([\\w\\W]*?)\\}\\}") val EXP_PATTERN: Pattern = Pattern.compile("\\{\\{([\\w\\W]*?)\\}\\}")
//匹配格式化后的图片格式 //匹配格式化后的图片格式
val imgPattern: Pattern = Pattern.compile("<img[^>]*src=\"([^\"]*(?:\"[^>]+\\})?)\"[^>]*>") val imgPattern: Pattern =
Pattern.compile("<img[^>]*src=['\"]([^'\"]*(?:['\"][^>]+\\})?)['\"][^>]*>")
//dataURL图片类型 //dataURL图片类型
val dataUriRegex = Regex("^data:.*?;base64,(.*)") val dataUriRegex = Regex("^data:.*?;base64,(.*)")
@@ -32,4 +32,5 @@ object EventBus {
const val READ_ALOUD_PLAY = "readAloudPlay" const val READ_ALOUD_PLAY = "readAloudPlay"
const val EXPORT_BOOK = "exportBook" const val EXPORT_BOOK = "exportBook"
const val UP_MANGA_CONFIG = "upMangaConfig" const val UP_MANGA_CONFIG = "upMangaConfig"
const val PLAY_MODE_CHANGED = "playModeChanged"
} }
@@ -170,14 +170,12 @@ object PreferKey {
const val showMangaUi = "showMangaUi" const val showMangaUi = "showMangaUi"
const val disableMangaScale = "disableMangaScale" const val disableMangaScale = "disableMangaScale"
const val firebaseEnabled = "firebaseEnabled" const val firebaseEnabled = "firebaseEnabled"
const val cPrimary = "colorPrimary" const val cPrimary = "colorPrimary"
const val cAccent = "colorAccent" const val cAccent = "colorAccent"
const val cBackground = "colorBackground" const val cBackground = "colorBackground"
const val cBBackground = "colorBottomBackground" const val cBBackground = "colorBottomBackground"
const val bgImage = "backgroundImage" const val bgImage = "backgroundImage"
const val bgImageBlurring = "backgroundImageBlurring" const val bgImageBlurring = "backgroundImageBlurring"
const val cNPrimary = "colorPrimaryNight" const val cNPrimary = "colorPrimaryNight"
const val cNAccent = "colorAccentNight" const val cNAccent = "colorAccentNight"
const val cNBackground = "colorBackgroundNight" const val cNBackground = "colorBackgroundNight"
@@ -187,7 +185,6 @@ object PreferKey {
const val showReadTitleAddition = "showReadTitleAddition" const val showReadTitleAddition = "showReadTitleAddition"
const val readBarStyleFollowPage = "readBarStyleFollowPage" const val readBarStyleFollowPage = "readBarStyleFollowPage"
const val contentSelectSpeakMod = "contentReadAloudMod" const val contentSelectSpeakMod = "contentReadAloudMod"
const val themePref = "themePref" const val themePref = "themePref"
const val bookshelfSortOrder = "bookshelfSortOrder" const val bookshelfSortOrder = "bookshelfSortOrder"
const val showBottomView = "showBottomView" const val showBottomView = "showBottomView"
@@ -208,5 +205,5 @@ object PreferKey {
const val permissionChecked = "permissionChecked" const val permissionChecked = "permissionChecked"
const val notificationsPost = "notificationsPost" const val notificationsPost = "notificationsPost"
const val ignoreBatteryPermission = "ignoreBatteryPermission" const val ignoreBatteryPermission = "ignoreBatteryPermission"
const val paddingDisplayCutouts = "paddingDisplayCutouts"
} }
@@ -5,6 +5,7 @@ import androidx.room.Delete
import androidx.room.Insert import androidx.room.Insert
import androidx.room.OnConflictStrategy import androidx.room.OnConflictStrategy
import androidx.room.Query import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Update import androidx.room.Update
import io.legato.kazusa.constant.BookType import io.legato.kazusa.constant.BookType
import io.legato.kazusa.data.entities.Book import io.legato.kazusa.data.entities.Book
@@ -150,6 +151,12 @@ interface BookDao {
@Delete @Delete
fun delete(vararg book: Book) fun delete(vararg book: Book)
@Transaction
fun replace(oldBook: Book, newBook: Book) {
delete(oldBook)
insert(newBook)
}
@Query("update books set durChapterPos = :pos where bookUrl = :bookUrl") @Query("update books set durChapterPos = :pos where bookUrl = :bookUrl")
fun upProgress(bookUrl: String, pos: Int) fun upProgress(bookUrl: String, pos: Int)
@@ -13,7 +13,6 @@ import io.legato.kazusa.help.config.AppConfig
import io.legato.kazusa.help.crypto.SymmetricCryptoAndroid import io.legato.kazusa.help.crypto.SymmetricCryptoAndroid
import io.legato.kazusa.help.http.CookieStore import io.legato.kazusa.help.http.CookieStore
import io.legato.kazusa.help.source.getShareScope import io.legato.kazusa.help.source.getShareScope
import io.legato.kazusa.model.Debug
import io.legato.kazusa.utils.GSON import io.legato.kazusa.utils.GSON
import io.legato.kazusa.utils.GSONStrict import io.legato.kazusa.utils.GSONStrict
import io.legato.kazusa.utils.fromJsonArray import io.legato.kazusa.utils.fromJsonArray
@@ -116,7 +115,7 @@ interface BaseSource : JsExtensions {
GSONStrict.fromJsonObject<Map<String, String>>(json).getOrNull()?.let { map -> GSONStrict.fromJsonObject<Map<String, String>>(json).getOrNull()?.let { map ->
putAll(map) putAll(map)
} ?: GSON.fromJsonObject<Map<String, String>>(json).getOrNull()?.let { map -> } ?: GSON.fromJsonObject<Map<String, String>>(json).getOrNull()?.let { map ->
Debug.log("≡请求头规则 JSON 格式不规范,请改为规范格式") log("请求头规则 JSON 格式不规范,请改为规范格式")
putAll(map) putAll(map)
} }
} catch (e: Exception) { } catch (e: Exception) {
@@ -101,7 +101,7 @@ object DirectLinkUpload {
@Keep @Keep
data class Rule( data class Rule(
var uploadUrl: String, //上传url var uploadUrl: String, //创建分享链接
var downloadUrlRule: String, //下载链接规则 var downloadUrlRule: String, //下载链接规则
var summary: String, //注释 var summary: String, //注释
var compress: Boolean = false, //是否压缩 var compress: Boolean = false, //是否压缩
@@ -988,6 +988,7 @@ interface JsExtensions : JsEncodeUtils {
// 新增 mimeType 参数,默认为 null(保持兼容性) // 新增 mimeType 参数,默认为 null(保持兼容性)
fun openUrl(url: String, mimeType: String? = null) { fun openUrl(url: String, mimeType: String? = null) {
require(url.length < 64 * 1024) { "openUrl parameter url too long" }
rhinoContext.ensureActive() rhinoContext.ensureActive()
val source = getSource() ?: throw NoStackTraceException("openUrl source cannot be null") val source = getSource() ?: throw NoStackTraceException("openUrl source cannot be null")
appCtx.startActivity<OpenUrlConfirmActivity> { appCtx.startActivity<OpenUrlConfirmActivity> {
@@ -3,6 +3,7 @@ package io.legato.kazusa.help.book
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import com.script.rhino.runScriptWithContext
import io.legato.kazusa.constant.AppLog import io.legato.kazusa.constant.AppLog
import io.legato.kazusa.constant.AppPattern import io.legato.kazusa.constant.AppPattern
import io.legato.kazusa.constant.EventBus import io.legato.kazusa.constant.EventBus
@@ -28,7 +29,6 @@ import io.legato.kazusa.utils.getFile
import io.legato.kazusa.utils.isContentScheme import io.legato.kazusa.utils.isContentScheme
import io.legato.kazusa.utils.onEachParallel import io.legato.kazusa.utils.onEachParallel
import io.legato.kazusa.utils.postEvent import io.legato.kazusa.utils.postEvent
import com.script.rhino.runScriptWithContext
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.ensureActive import kotlinx.coroutines.ensureActive
@@ -157,7 +157,7 @@ object BookHelp {
} }
} }
suspend fun saveContent( fun saveContent(
bookSource: BookSource, bookSource: BookSource,
book: Book, book: Book,
bookChapter: BookChapter, bookChapter: BookChapter,
@@ -404,7 +404,11 @@ object BookHelp {
bookChapter.getFileName() bookChapter.getFileName()
) )
if (file.exists()) { if (file.exists()) {
return file.readText() val string = file.readText()
if (string.isEmpty()) {
return null
}
return string
} }
if (book.isLocal) { if (book.isLocal) {
val string = LocalBook.getContent(book, bookChapter) val string = LocalBook.getContent(book, bookChapter)
@@ -570,6 +570,9 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
val mouseWheelPage val mouseWheelPage
get() = appCtx.getPrefBoolean(PreferKey.mouseWheelPage, true) get() = appCtx.getPrefBoolean(PreferKey.mouseWheelPage, true)
val paddingDisplayCutouts
get() = appCtx.getPrefBoolean(PreferKey.paddingDisplayCutouts, false)
var searchScope: String var searchScope: String
get() = appCtx.getPrefString("searchScope") ?: "" get() = appCtx.getPrefString("searchScope") ?: ""
set(value) { set(value) {
@@ -5,8 +5,8 @@ import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import androidx.annotation.Keep import androidx.annotation.Keep
import androidx.core.graphics.toColorInt
import com.google.android.material.color.MaterialColors import com.google.android.material.color.MaterialColors
import io.legato.kazusa.R
import io.legato.kazusa.constant.AppLog import io.legato.kazusa.constant.AppLog
import io.legato.kazusa.constant.PageAnim import io.legato.kazusa.constant.PageAnim
import io.legato.kazusa.constant.PreferKey import io.legato.kazusa.constant.PreferKey
@@ -30,8 +30,6 @@ import io.legato.kazusa.utils.printOnDebug
import io.legato.kazusa.utils.putPrefBoolean import io.legato.kazusa.utils.putPrefBoolean
import io.legato.kazusa.utils.putPrefInt import io.legato.kazusa.utils.putPrefInt
import io.legato.kazusa.utils.resizeAndRecycle import io.legato.kazusa.utils.resizeAndRecycle
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.withContext
import splitties.init.appCtx import splitties.init.appCtx
import java.io.File import java.io.File
@@ -439,66 +437,55 @@ object ReadBookConfig {
return exportConfig return exportConfig
} }
suspend fun import(byteArray: ByteArray): Result<Config> { fun import(byteArray: ByteArray): Config {
return kotlin.runCatching { val configZipPath = FileUtils.getPath(appCtx.externalCache, "readConfig.zip")
withContext(IO) { FileUtils.delete(configZipPath)
val configZipPath = FileUtils.getPath(appCtx.externalCache, "readConfig.zip") val zipFile = FileUtils.createFileIfNotExist(configZipPath)
FileUtils.delete(configZipPath) zipFile.writeBytes(byteArray)
val zipFile = FileUtils.createFileIfNotExist(configZipPath) val configDir = appCtx.externalCache.getFile("readConfig")
zipFile.writeBytes(byteArray) configDir.createFolderReplace()
val configDir = appCtx.externalCache.getFile("readConfig") ZipUtils.unZipToPath(zipFile, configDir)
configDir.createFolderReplace() val configFile = configDir.getFile(configFileName)
ZipUtils.unZipToPath(zipFile, configDir) val config: Config = GSON.fromJsonObject<Config>(configFile.readText()).getOrThrow()
val configFile = configDir.getFile(configFileName) if (config.textFont.isNotEmpty()) {
val config: Config = GSON.fromJsonObject<Config>(configFile.readText()).getOrThrow() val fontName = FileUtils.getName(config.textFont)
if (config.textFont.isNotEmpty()) { val fontPath =
val fontName = FileUtils.getName(config.textFont) FileUtils.getPath(appCtx.externalFiles, "font", fontName)
val fontPath = if (!FileUtils.exist(fontPath)) {
FileUtils.getPath(appCtx.externalFiles, "font", fontName) configDir.getFile(fontName).copyTo(File(fontPath))
if (!FileUtils.exist(fontPath)) {
configDir.getFile(fontName).copyTo(File(fontPath))
}
config.textFont = fontPath
}
if (config.bgType == 2) {
val bgName = FileUtils.getName(config.bgStr)
config.bgStr = bgName
val bgPath = FileUtils.getPath(appCtx.externalFiles, "bg", bgName)
if (!FileUtils.exist(bgPath)) {
val bgFile = configDir.getFile(bgName)
if (bgFile.exists()) {
bgFile.copyTo(File(bgPath))
}
}
config.bgStr = bgPath
}
if (config.bgTypeNight == 2) {
val bgName = FileUtils.getName(config.bgStrNight)
config.bgStrNight = bgName
val bgPath = FileUtils.getPath(appCtx.externalFiles, "bg", bgName)
if (!FileUtils.exist(bgPath)) {
val bgFile = configDir.getFile(bgName)
if (bgFile.exists()) {
bgFile.copyTo(File(bgPath))
}
}
config.bgStrNight = bgPath
}
if (config.bgTypeEInk == 2) {
val bgName = FileUtils.getName(config.bgStrEInk)
config.bgStrEInk = bgName
val bgPath = FileUtils.getPath(appCtx.externalFiles, "bg", bgName)
if (!FileUtils.exist(bgPath)) {
val bgFile = configDir.getFile(bgName)
if (bgFile.exists()) {
bgFile.copyTo(File(bgPath))
}
}
config.bgStrEInk = bgPath
}
return@withContext config
} }
config.textFont = fontPath
} }
if (config.bgType == 2) {
val bgName = FileUtils.getName(config.bgStr)
config.bgStr = bgName
val bgPath = FileUtils.getPath(appCtx.externalFiles, "bg", bgName)
if (!FileUtils.exist(bgPath)) {
val bgFile = configDir.getFile(bgName)
if (bgFile.exists()) {
bgFile.copyTo(File(bgPath))
}
}
config.bgStrNight = bgPath
} else if (config.bgTypeNight == 0) {
config.bgStrNight.toColorInt()
}
if (config.bgTypeEInk == 2) {
val bgName = FileUtils.getName(config.bgStrEInk)
config.bgStrEInk = bgName
val bgPath = FileUtils.getPath(appCtx.externalFiles, "bg", bgName)
if (!FileUtils.exist(bgPath)) {
val bgFile = configDir.getFile(bgName)
if (bgFile.exists()) {
bgFile.copyTo(File(bgPath))
}
}
config.bgStrEInk = bgPath
} else if (config.bgTypeEInk == 0) {
config.bgStrEInk.toColorInt()
}
config.curTextColor()
return config
} }
@Keep @Keep
@@ -2,19 +2,18 @@ package io.legato.kazusa.help.config
import android.content.Context import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Color
import android.util.DisplayMetrics import android.util.DisplayMetrics
import androidx.annotation.Keep import androidx.annotation.Keep
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.core.graphics.toColorInt
import io.legato.kazusa.R import io.legato.kazusa.R
import io.legato.kazusa.constant.AppLog
import io.legato.kazusa.constant.EventBus import io.legato.kazusa.constant.EventBus
import io.legato.kazusa.constant.PreferKey import io.legato.kazusa.constant.PreferKey
import io.legato.kazusa.constant.Theme import io.legato.kazusa.constant.Theme
import io.legato.kazusa.help.DefaultData import io.legato.kazusa.help.DefaultData
import io.legato.kazusa.lib.theme.ThemeStore
import io.legato.kazusa.model.BookCover import io.legato.kazusa.model.BookCover
import io.legato.kazusa.utils.BitmapUtils import io.legato.kazusa.utils.BitmapUtils
import io.legato.kazusa.utils.ColorUtils
import io.legato.kazusa.utils.FileUtils import io.legato.kazusa.utils.FileUtils
import io.legato.kazusa.utils.GSON import io.legato.kazusa.utils.GSON
import io.legato.kazusa.utils.fromJsonArray import io.legato.kazusa.utils.fromJsonArray
@@ -29,7 +28,6 @@ import io.legato.kazusa.utils.putPrefInt
import io.legato.kazusa.utils.stackBlur import io.legato.kazusa.utils.stackBlur
import splitties.init.appCtx import splitties.init.appCtx
import java.io.File import java.io.File
import androidx.core.graphics.toColorInt
@Keep @Keep
object ThemeConfig { object ThemeConfig {
@@ -117,13 +115,18 @@ object ThemeConfig {
fun addConfig(json: String): Boolean { fun addConfig(json: String): Boolean {
GSON.fromJsonObject<Config>(json.trim { it < ' ' }).getOrNull() GSON.fromJsonObject<Config>(json.trim { it < ' ' }).getOrNull()
?.let { ?.let {
addConfig(it) if (validateConfig(it)) {
return true addConfig(it)
return true
}
} }
return false return false
} }
fun addConfig(newConfig: Config) { fun addConfig(newConfig: Config) {
if (!validateConfig(newConfig)) {
return
}
configList.forEachIndexed { index, config -> configList.forEachIndexed { index, config ->
if (newConfig.themeName == config.themeName) { if (newConfig.themeName == config.themeName) {
configList[index] = newConfig configList[index] = newConfig
@@ -134,6 +137,18 @@ object ThemeConfig {
save() save()
} }
private fun validateConfig(config: Config): Boolean {
try {
config.primaryColor.toColorInt()
config.accentColor.toColorInt()
config.backgroundColor.toColorInt()
config.bottomBackground.toColorInt()
return true
} catch (_: Exception) {
return false
}
}
private fun getConfigs(): List<Config>? { private fun getConfigs(): List<Config>? {
val configFile = File(configFilePath) val configFile = File(configFilePath)
if (configFile.exists()) { if (configFile.exists()) {
@@ -148,23 +163,27 @@ object ThemeConfig {
} }
fun applyConfig(context: Context, config: Config) { fun applyConfig(context: Context, config: Config) {
val primary = config.primaryColor.toColorInt() try {
val accent = config.accentColor.toColorInt() val primary = config.primaryColor.toColorInt()
val background = config.backgroundColor.toColorInt() val accent = config.accentColor.toColorInt()
val bBackground = config.bottomBackground.toColorInt() val background = config.backgroundColor.toColorInt()
if (config.isNightTheme) { val bBackground = config.bottomBackground.toColorInt()
context.putPrefInt(PreferKey.cNPrimary, primary) if (config.isNightTheme) {
context.putPrefInt(PreferKey.cNAccent, accent) context.putPrefInt(PreferKey.cNPrimary, primary)
context.putPrefInt(PreferKey.cNBackground, background) context.putPrefInt(PreferKey.cNAccent, accent)
context.putPrefInt(PreferKey.cNBBackground, bBackground) context.putPrefInt(PreferKey.cNBackground, background)
} else { context.putPrefInt(PreferKey.cNBBackground, bBackground)
context.putPrefInt(PreferKey.cPrimary, primary) } else {
context.putPrefInt(PreferKey.cAccent, accent) context.putPrefInt(PreferKey.cPrimary, primary)
context.putPrefInt(PreferKey.cBackground, background) context.putPrefInt(PreferKey.cAccent, accent)
context.putPrefInt(PreferKey.cBBackground, bBackground) context.putPrefInt(PreferKey.cBackground, background)
context.putPrefInt(PreferKey.cBBackground, bBackground)
}
AppConfig.isNightTheme = config.isNightTheme
applyDayNight(context)
} catch (e: Exception) {
AppLog.put("设置主题出错\n$e", e, true)
} }
AppConfig.isNightTheme = config.isNightTheme
applyDayNight(context)
} }
fun saveDayTheme(context: Context, name: String) { fun saveDayTheme(context: Context, name: String) {
@@ -12,7 +12,7 @@ import java.security.MessageDigest
* @radius: 0..25 * @radius: 0..25
*/ */
class BlurTransformation( class BlurTransformation(
@IntRange(from = 0, to = 25) private val radius: Int @param:IntRange(from = 0, to = 25) private val radius: Int
) : BitmapTransformation() { ) : BitmapTransformation() {
override fun transform( override fun transform(
@@ -2,6 +2,7 @@ package io.legato.kazusa.help.http
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Response import okhttp3.Response
import okhttp3.ResponseBody
import okhttp3.ResponseBody.Companion.asResponseBody import okhttp3.ResponseBody.Companion.asResponseBody
import okhttp3.internal.http.promisesBody import okhttp3.internal.http.promisesBody
import okio.buffer import okio.buffer
@@ -24,7 +25,7 @@ object DecompressInterceptor : Interceptor {
val response = chain.proceed(requestBuilder.build()) val response = chain.proceed(requestBuilder.build())
val body = response.body val body = response.body
if (!transparentDecompress || !response.promisesBody() || body == null) { if (!transparentDecompress || !response.promisesBody() || body == ResponseBody.EMPTY) {
return response return response
} }
@@ -127,7 +127,7 @@ val okHttpClientManga by lazy {
val response = chain.proceed(request) val response = chain.proceed(request)
val url = request.url.toString() val url = request.url.toString()
response.newBuilder() response.newBuilder()
.body(ProgressResponseBody(url, LISTENER, response.body!!)) .body(ProgressResponseBody(url, LISTENER, response.body))
.build() .build()
} }
interceptors.add(1) { chain -> interceptors.add(1) { chain ->
@@ -18,8 +18,6 @@ import okhttp3.Protocol
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody import okhttp3.RequestBody
import okhttp3.Response import okhttp3.Response
import okhttp3.internal.notifyAll
import okhttp3.internal.wait
import okio.Buffer import okio.Buffer
import okio.BufferedSink import okio.BufferedSink
import okio.Pipe import okio.Pipe
@@ -186,7 +184,7 @@ class ObsoleteUrlFactory(private var client: OkHttpClient) : URLStreamHandlerFac
return try { return try {
val response = getResponse(true) val response = getResponse(true)
if (hasBody(response) && response.code >= HTTP_BAD_REQUEST) { if (hasBody(response) && response.code >= HTTP_BAD_REQUEST) {
response.body!!.byteStream() response.body.byteStream()
} else null } else null
} catch (e: IOException) { } catch (e: IOException) {
null null
@@ -258,7 +256,7 @@ class ObsoleteUrlFactory(private var client: OkHttpClient) : URLStreamHandlerFac
} }
val response = getResponse(false) val response = getResponse(false)
if (response.code >= HTTP_BAD_REQUEST) throw FileNotFoundException(url.toString()) if (response.code >= HTTP_BAD_REQUEST) throw FileNotFoundException(url.toString())
return response.body!!.byteStream() return response.body.byteStream()
} }
@Throws(IOException::class) @Throws(IOException::class)
@@ -1174,6 +1172,14 @@ class ObsoleteUrlFactory(private var client: OkHttpClient) : URLStreamHandlerFac
throw AssertionError() throw AssertionError()
} }
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN", "NOTHING_TO_INLINE")
private inline fun Any.wait() = (this as Object).wait()
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN", "NOTHING_TO_INLINE")
private inline fun Any.notify() = (this as Object).notify()
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN", "NOTHING_TO_INLINE")
private inline fun Any.notifyAll() = (this as Object).notifyAll()
@Throws(Exception::class) @Throws(Exception::class)
@JvmStatic @JvmStatic
fun main(args: Array<String>) { fun main(args: Array<String>) {
@@ -29,6 +29,7 @@ object SourceVerificationHelp {
* 获取书源验证结果 * 获取书源验证结果
* 图片验证码 防爬 滑动验证码 点击字符 等等 * 图片验证码 防爬 滑动验证码 点击字符 等等
*/ */
@Synchronized
fun getVerificationResult( fun getVerificationResult(
source: BaseSource?, source: BaseSource?,
url: String, url: String,
@@ -38,9 +39,8 @@ object SourceVerificationHelp {
): String { ): String {
source source
?: throw NoStackTraceException("getVerificationResult parameter source cannot be null") ?: throw NoStackTraceException("getVerificationResult parameter source cannot be null")
if (isMainThread) { require(url.length < 64 * 1024) { "getVerificationResult parameter url too long" }
error("getVerificationResult must be called on a background thread") check(!isMainThread) { "getVerificationResult must be called on a background thread" }
}
clearResult(source.getKey()) clearResult(source.getKey())
@@ -84,6 +84,7 @@ object SourceVerificationHelp {
refetchAfterSuccess: Boolean? = true refetchAfterSuccess: Boolean? = true
) { ) {
source ?: throw NoStackTraceException("startBrowser parameter source cannot be null") source ?: throw NoStackTraceException("startBrowser parameter source cannot be null")
require(url.length < 64 * 1024) { "startBrowser parameter url too long" }
appCtx.startActivity<WebViewActivity> { appCtx.startActivity<WebViewActivity> {
putExtra("title", title) putExtra("title", title)
putExtra("url", url) putExtra("url", url)
@@ -1,6 +1,7 @@
package io.legato.kazusa.help.storage package io.legato.kazusa.help.storage
import android.content.Context import android.content.Context
import android.database.sqlite.SQLiteConstraintException
import android.net.Uri import android.net.Uri
import androidx.core.content.edit import androidx.core.content.edit
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
@@ -106,7 +107,6 @@ object Restore {
.forEach { book -> .forEach { book ->
book.coverUrl = LocalBook.getCoverPath(book) book.coverUrl = LocalBook.getCoverPath(book)
} }
val updateBooks = arrayListOf<Book>()
val newBooks = arrayListOf<Book>() val newBooks = arrayListOf<Book>()
val ignoreLocalBook = BackupConfig.ignoreLocalBook val ignoreLocalBook = BackupConfig.ignoreLocalBook
it.forEach { book -> it.forEach { book ->
@@ -114,12 +114,15 @@ object Restore {
return@forEach return@forEach
} }
if (appDb.bookDao.has(book.bookUrl)) { if (appDb.bookDao.has(book.bookUrl)) {
updateBooks.add(book) try {
appDb.bookDao.update(book)
} catch (_: SQLiteConstraintException) {
appDb.bookDao.insert(book)
}
} else { } else {
newBooks.add(book) newBooks.add(book)
} }
} }
appDb.bookDao.update(*updateBooks.toTypedArray())
appDb.bookDao.insert(*newBooks.toTypedArray()) appDb.bookDao.insert(*newBooks.toTypedArray())
} }
fileToListT<Bookmark>(path, "bookmark.json")?.let { fileToListT<Bookmark>(path, "bookmark.json")?.let {
@@ -9,12 +9,19 @@ import io.legato.kazusa.utils.DebugLog
import io.legato.kazusa.utils.asIOException import io.legato.kazusa.utils.asIOException
import io.legato.kazusa.utils.splitNotBlank import io.legato.kazusa.utils.splitNotBlank
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import okhttp3.* import okhttp3.Call
import okhttp3.Callback
import okhttp3.EventListener import okhttp3.EventListener
import okhttp3.Headers
import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.Protocol
import okhttp3.Request
import okhttp3.Response
import okhttp3.ResponseBody
import okhttp3.ResponseBody.Companion.asResponseBody import okhttp3.ResponseBody.Companion.asResponseBody
import okhttp3.internal.http.HTTP_PERM_REDIRECT
import okhttp3.internal.http.HTTP_TEMP_REDIRECT
import okhttp3.internal.http.HttpMethod import okhttp3.internal.http.HttpMethod
import okhttp3.internal.http.StatusLine
import okio.Buffer import okio.Buffer
import okio.Source import okio.Source
import okio.Timeout import okio.Timeout
@@ -25,7 +32,7 @@ import org.chromium.net.UrlResponseInfo
import java.io.IOException import java.io.IOException
import java.net.ProtocolException import java.net.ProtocolException
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.util.* import java.util.Locale
import java.util.concurrent.ArrayBlockingQueue import java.util.concurrent.ArrayBlockingQueue
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
@@ -98,9 +105,15 @@ abstract class AbsCallBack(
followCount += 1 followCount += 1
urlResponseInfoChain.add(info) urlResponseInfoChain.add(info)
val client = okHttpClient val client = okHttpClient
if (originalRequest.url.isHttps && newLocationUrl.startsWith("http://") && client.followSslRedirects) { if (originalRequest.url.isHttps
&& newLocationUrl.startsWith("http://")
&& client.followSslRedirects
) {
followRedirect = true followRedirect = true
} else if (!originalRequest.url.isHttps && newLocationUrl.startsWith("https://") && client.followSslRedirects) { } else if (!originalRequest.url.isHttps
&& newLocationUrl.startsWith("https://")
&& client.followSslRedirects
) {
followRedirect = true followRedirect = true
} else if (okHttpClient.followRedirects) { } else if (okHttpClient.followRedirects) {
followRedirect = true followRedirect = true
@@ -315,7 +328,7 @@ abstract class AbsCallBack(
contentLength, contentLength,
bodySource bodySource
) )
} } ?: ResponseBody.EMPTY
return Response.Builder() return Response.Builder()
.request(request) .request(request)
@@ -381,9 +394,12 @@ abstract class AbsCallBack(
if (HttpMethod.permitsRequestBody(method)) { if (HttpMethod.permitsRequestBody(method)) {
val responseCode = userResponse.code val responseCode = userResponse.code
val maintainBody = HttpMethod.redirectsWithBody(method) || val maintainBody = HttpMethod.redirectsWithBody(method) ||
responseCode == StatusLine.HTTP_PERM_REDIRECT || responseCode == HTTP_PERM_REDIRECT ||
responseCode == StatusLine.HTTP_TEMP_REDIRECT responseCode == HTTP_TEMP_REDIRECT
if (HttpMethod.redirectsToGet(method) && responseCode != StatusLine.HTTP_PERM_REDIRECT && responseCode != StatusLine.HTTP_TEMP_REDIRECT) { if (HttpMethod.redirectsToGet(method)
&& responseCode != HTTP_PERM_REDIRECT
&& responseCode != HTTP_TEMP_REDIRECT
) {
requestBuilder.method("GET", null) requestBuilder.method("GET", null)
} else { } else {
val requestBody = if (maintainBody) userResponse.request.body else null val requestBody = if (maintainBody) userResponse.request.body else null
@@ -16,11 +16,11 @@ import io.legato.kazusa.lib.mobi.entities.Ptagx
import io.legato.kazusa.lib.mobi.entities.TOC import io.legato.kazusa.lib.mobi.entities.TOC
import io.legato.kazusa.lib.mobi.entities.TagxHeader import io.legato.kazusa.lib.mobi.entities.TagxHeader
import io.legato.kazusa.lib.mobi.entities.TagxTag import io.legato.kazusa.lib.mobi.entities.TagxTag
import io.legato.kazusa.lib.mobi.utils.and
import io.legato.kazusa.lib.mobi.utils.readString import io.legato.kazusa.lib.mobi.utils.readString
import io.legato.kazusa.lib.mobi.utils.readUInt16Array import io.legato.kazusa.lib.mobi.utils.readUInt16Array
import io.legato.kazusa.lib.mobi.utils.readUInt32 import io.legato.kazusa.lib.mobi.utils.readUInt32
import io.legato.kazusa.lib.mobi.utils.readUInt8 import io.legato.kazusa.lib.mobi.utils.readUInt8
import okhttp3.internal.and
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.InputStream import java.io.InputStream
import java.nio.ByteBuffer import java.nio.ByteBuffer
@@ -0,0 +1,7 @@
package io.legato.kazusa.lib.mobi.utils
internal infix fun Byte.and(mask: Int): Int = toInt() and mask
internal infix fun Short.and(mask: Int): Int = toInt() and mask
internal infix fun Int.and(mask: Long): Long = toLong() and mask
@@ -1,6 +1,5 @@
package io.legato.kazusa.lib.mobi.utils package io.legato.kazusa.lib.mobi.utils
import okhttp3.internal.and
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.nio.charset.Charset import java.nio.charset.Charset
@@ -18,6 +18,7 @@ import io.legato.kazusa.utils.toRequestBody
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.ensureActive import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.asRequestBody import okhttp3.RequestBody.Companion.asRequestBody
@@ -92,6 +93,7 @@ open class WebDav(
.replace("+", "%20") .replace("+", "%20")
.replace("%3A", ":") .replace("%3A", ":")
.replace("%2F", "/") .replace("%2F", "/")
raw.toHttpUrl().toString()
}.getOrNull() }.getOrNull()
} }
private val webDavClient by lazy { private val webDavClient by lazy {
@@ -112,7 +114,14 @@ open class WebDav(
build() build()
} }
} }
val host: String? get() = url.host private val host: String?
get() = url.host?.let {
if (it.startsWith("[")) {
it.substring(1, it.lastIndex)
} else {
it
}
}
/** /**
* 获取当前url文件信息 * 获取当前url文件信息
@@ -165,7 +174,7 @@ open class WebDav(
method("PROPFIND", requestBody) method("PROPFIND", requestBody)
}.apply { }.apply {
checkResult(this) checkResult(this)
}.body?.text() }.body.text()
} }
/** /**
@@ -397,8 +406,8 @@ open class WebDav(
url(url) url(url)
}.apply { }.apply {
checkResult(this) checkResult(this)
}.body?.byteStream() }.body.byteStream()
return byteStream ?: throw WebDavException("WebDav下载出错\nNull Exception") return byteStream
} }
/** /**
@@ -425,7 +434,7 @@ open class WebDav(
*/ */
private fun checkResult(response: Response) { private fun checkResult(response: Response) {
if (!response.isSuccessful) { if (!response.isSuccessful) {
val body = response.body?.string() val body = response.body.string()
if (response.code == 401) { if (response.code == 401) {
val headers = response.headers("WWW-Authenticate") val headers = response.headers("WWW-Authenticate")
val supportBasicAuth = headers.any { val supportBasicAuth = headers.any {
@@ -436,7 +445,7 @@ open class WebDav(
} }
} }
if (response.message.isNotBlank() || body.isNullOrBlank()) { if (response.message.isNotBlank() || body.isBlank()) {
throw WebDavException("${url}\n${response.code}:${response.message}") throw WebDavException("${url}\n${response.code}:${response.message}")
} }
val document = Jsoup.parse(body) val document = Jsoup.parse(body)
@@ -31,6 +31,22 @@ import splitties.init.appCtx
@SuppressLint("StaticFieldLeak") @SuppressLint("StaticFieldLeak")
@Suppress("unused") @Suppress("unused")
object AudioPlay : CoroutineScope by MainScope() { object AudioPlay : CoroutineScope by MainScope() {
enum class PlayMode(val iconRes: Int) {
LIST_END_STOP(R.drawable.ic_play_mode_list_end_stop),
RANDOM(R.drawable.ic_play_mode_random),
LIST_LOOP(R.drawable.ic_play_mode_list_loop);
fun next(): PlayMode {
return when (this) {
LIST_END_STOP -> RANDOM
RANDOM -> LIST_LOOP
LIST_LOOP -> LIST_END_STOP
}
}
}
var playMode = PlayMode.LIST_END_STOP
var status = Status.STOP var status = Status.STOP
private var activityContext: Context? = null private var activityContext: Context? = null
private var serviceContext: Context? = null private var serviceContext: Context? = null
@@ -48,6 +64,11 @@ object AudioPlay : CoroutineScope by MainScope() {
var bookSource: BookSource? = null var bookSource: BookSource? = null
val loadingChapters = arrayListOf<Int>() val loadingChapters = arrayListOf<Int>()
fun changePlayMode() {
playMode = playMode.next()
postEvent(EventBus.PLAY_MODE_CHANGED, playMode)
}
fun upData(book: Book) { fun upData(book: Book) {
AudioPlay.book = book AudioPlay.book = book
chapterSize = appDb.bookChapterDao.getChapterCount(book.bookUrl) chapterSize = appDb.bookChapterDao.getChapterCount(book.bookUrl)
@@ -259,12 +280,32 @@ object AudioPlay : CoroutineScope by MainScope() {
fun next() { fun next() {
stopPlay() stopPlay()
if (durChapterIndex + 1 < simulatedChapterSize) { when (playMode) {
durChapterIndex += 1 PlayMode.LIST_END_STOP -> {
durChapterPos = 0 if (durChapterIndex + 1 < simulatedChapterSize) {
durPlayUrl = "" durChapterIndex += 1
saveRead() durChapterPos = 0
loadPlayUrl() durPlayUrl = ""
saveRead()
loadPlayUrl()
}
}
PlayMode.RANDOM -> {
durChapterIndex = (0 until simulatedChapterSize).random()
durChapterPos = 0
durPlayUrl = ""
saveRead()
loadPlayUrl()
}
PlayMode.LIST_LOOP -> {
durChapterIndex = (durChapterIndex + 1) % simulatedChapterSize
durChapterPos = 0
durPlayUrl = ""
saveRead()
loadPlayUrl()
}
} }
} }
@@ -135,7 +135,6 @@ object BookCover {
/** /**
* 加载漫画图片 * 加载漫画图片
*/ */
@SuppressLint("CheckResult")
fun loadManga( fun loadManga(
context: Context, context: Context,
path: String?, path: String?,
@@ -155,8 +154,9 @@ object BookCover {
.skipMemoryCache(true).let { .skipMemoryCache(true).let {
if (transformation != null) { if (transformation != null) {
it.transform(transformation) it.transform(transformation)
} else {
it
} }
it
} }
.transition(DrawableTransitionOptions.withCrossFade()) .transition(DrawableTransitionOptions.withCrossFade())
} }
@@ -11,6 +11,7 @@ import io.legato.kazusa.data.entities.BookSource
import io.legato.kazusa.exception.ConcurrentException import io.legato.kazusa.exception.ConcurrentException
import io.legato.kazusa.help.book.BookHelp import io.legato.kazusa.help.book.BookHelp
import io.legato.kazusa.help.book.isLocal import io.legato.kazusa.help.book.isLocal
import io.legato.kazusa.help.coroutine.CompositeCoroutine
import io.legato.kazusa.help.coroutine.Coroutine import io.legato.kazusa.help.coroutine.Coroutine
import io.legato.kazusa.model.webBook.WebBook import io.legato.kazusa.model.webBook.WebBook
import io.legato.kazusa.service.CacheBookService import io.legato.kazusa.service.CacheBookService
@@ -96,15 +97,11 @@ object CacheBook {
} }
} }
fun clear() {
successDownloadSet.clear()
errorDownloadMap.clear()
}
fun close() { fun close() {
cacheBookMap.forEach { it.value.stop() } cacheBookMap.forEach { it.value.stop() }
cacheBookMap.clear() cacheBookMap.clear()
clear() successDownloadSet.clear()
errorDownloadMap.clear()
} }
val downloadSummary: String val downloadSummary: String
@@ -146,6 +143,7 @@ object CacheBook {
private val waitDownloadSet = linkedSetOf<Int>() private val waitDownloadSet = linkedSetOf<Int>()
private val onDownloadSet = linkedSetOf<Int>() private val onDownloadSet = linkedSetOf<Int>()
private val tasks = CompositeCoroutine()
private var isStopped = false private var isStopped = false
private var waitingRetry = false private var waitingRetry = false
@@ -158,7 +156,7 @@ object CacheBook {
@Synchronized @Synchronized
fun isRun(): Boolean { fun isRun(): Boolean {
return waitDownloadSet.size > 0 || onDownloadSet.size > 0 return waitDownloadSet.isNotEmpty() || onDownloadSet.isNotEmpty()
} }
@Synchronized @Synchronized
@@ -169,6 +167,7 @@ object CacheBook {
@Synchronized @Synchronized
fun stop() { fun stop() {
waitDownloadSet.clear() waitDownloadSet.clear()
tasks.clear()
isStopped = true isStopped = true
postEvent(EventBus.UP_DOWNLOAD, book.bookUrl) postEvent(EventBus.UP_DOWNLOAD, book.bookUrl)
} }
@@ -309,6 +308,8 @@ object CacheBook {
onCancel(chapterIndex) onCancel(chapterIndex)
}.onFinally { }.onFinally {
onFinally() onFinally()
}.apply {
tasks.add(this)
}.start() }.start()
} }
@@ -63,20 +63,8 @@ object ReadBook : CoroutineScope by MainScope() {
var isLocalBook = true var isLocalBook = true
var chapterChanged = false var chapterChanged = false
var prevTextChapter: TextChapter? = null var prevTextChapter: TextChapter? = null
set(value) {
field?.cancelLayout()
field = value
}
var curTextChapter: TextChapter? = null var curTextChapter: TextChapter? = null
set(value) {
field?.cancelLayout()
field = value
}
var nextTextChapter: TextChapter? = null var nextTextChapter: TextChapter? = null
set(value) {
field?.cancelLayout()
field = value
}
var bookSource: BookSource? = null var bookSource: BookSource? = null
var msg: String? = null var msg: String? = null
private val loadingChapters = arrayListOf<Int>() private val loadingChapters = arrayListOf<Int>()
@@ -222,6 +210,9 @@ object ReadBook : CoroutineScope by MainScope() {
} }
fun clearTextChapter() { fun clearTextChapter() {
prevTextChapter?.cancelLayout()
curTextChapter?.cancelLayout()
nextTextChapter?.cancelLayout()
prevTextChapter = null prevTextChapter = null
curTextChapter = null curTextChapter = null
nextTextChapter = null nextTextChapter = null
@@ -333,6 +324,7 @@ object ReadBook : CoroutineScope by MainScope() {
if (durChapterIndex < simulatedChapterSize - 1) { if (durChapterIndex < simulatedChapterSize - 1) {
durChapterPos = 0 durChapterPos = 0
durChapterIndex++ durChapterIndex++
prevTextChapter?.cancelLayout()
prevTextChapter = curTextChapter prevTextChapter = curTextChapter
curTextChapter = nextTextChapter curTextChapter = nextTextChapter
nextTextChapter = null nextTextChapter = null
@@ -363,6 +355,7 @@ object ReadBook : CoroutineScope by MainScope() {
if (durChapterIndex < simulatedChapterSize - 1) { if (durChapterIndex < simulatedChapterSize - 1) {
durChapterPos = 0 durChapterPos = 0
durChapterIndex++ durChapterIndex++
prevTextChapter?.cancelLayout()
prevTextChapter = curTextChapter prevTextChapter = curTextChapter
curTextChapter = nextTextChapter curTextChapter = nextTextChapter
nextTextChapter = null nextTextChapter = null
@@ -394,6 +387,7 @@ object ReadBook : CoroutineScope by MainScope() {
if (durChapterIndex > 0) { if (durChapterIndex > 0) {
durChapterPos = if (toLast) prevTextChapter?.lastReadLength ?: Int.MAX_VALUE else 0 durChapterPos = if (toLast) prevTextChapter?.lastReadLength ?: Int.MAX_VALUE else 0
durChapterIndex-- durChapterIndex--
nextTextChapter?.cancelLayout()
nextTextChapter = curTextChapter nextTextChapter = curTextChapter
curTextChapter = prevTextChapter curTextChapter = prevTextChapter
prevTextChapter = null prevTextChapter = null
@@ -703,6 +697,7 @@ object ReadBook : CoroutineScope by MainScope() {
) )
when (val offset = chapter.index - durChapterIndex) { when (val offset = chapter.index - durChapterIndex) {
0 -> { 0 -> {
curTextChapter?.cancelLayout()
curTextChapter = textChapter curTextChapter = textChapter
callBack?.upMenuView() callBack?.upMenuView()
var available = false var available = false
@@ -727,12 +722,14 @@ object ReadBook : CoroutineScope by MainScope() {
} }
-1 -> { -1 -> {
prevTextChapter?.cancelLayout()
prevTextChapter = textChapter prevTextChapter = textChapter
textChapter.layoutChannel.receiveAsFlow().collect() textChapter.layoutChannel.receiveAsFlow().collect()
if (upContent) callBack?.upContent(offset, resetPageOffset) if (upContent) callBack?.upContent(offset, resetPageOffset)
} }
1 -> { 1 -> {
nextTextChapter?.cancelLayout()
nextTextChapter = textChapter nextTextChapter = textChapter
for (page in textChapter.layoutChannel) { for (page in textChapter.layoutChannel) {
if (page.index > 1) { if (page.index > 1) {
@@ -776,6 +773,7 @@ object ReadBook : CoroutineScope by MainScope() {
) )
when (val offset = chapter.index - durChapterIndex) { when (val offset = chapter.index - durChapterIndex) {
0 -> { 0 -> {
curTextChapter?.cancelLayout()
curTextChapter = textChapter curTextChapter = textChapter
callBack?.upMenuView() callBack?.upMenuView()
var available = false var available = false
@@ -800,12 +798,14 @@ object ReadBook : CoroutineScope by MainScope() {
} }
-1 -> { -1 -> {
prevTextChapter?.cancelLayout()
prevTextChapter = textChapter prevTextChapter = textChapter
textChapter.layoutChannel.receiveAsFlow().collect() textChapter.layoutChannel.receiveAsFlow().collect()
if (upContent) callBack?.upContent(offset, resetPageOffset) if (upContent) callBack?.upContent(offset, resetPageOffset)
} }
1 -> { 1 -> {
nextTextChapter?.cancelLayout()
nextTextChapter = textChapter nextTextChapter = textChapter
for (page in textChapter.layoutChannel) { for (page in textChapter.layoutChannel) {
if (page.index > 1) { if (page.index > 1) {
@@ -858,21 +858,25 @@ object ReadBook : CoroutineScope by MainScope() {
fun saveRead(pageChanged: Boolean = false) { fun saveRead(pageChanged: Boolean = false) {
executor.execute { executor.execute {
val book = book ?: return@execute kotlin.runCatching {
book.lastCheckCount = 0 val book = book ?: return@execute
book.durChapterTime = System.currentTimeMillis() book.lastCheckCount = 0
val chapterChanged = book.durChapterIndex != durChapterIndex book.durChapterTime = System.currentTimeMillis()
book.durChapterIndex = durChapterIndex val chapterChanged = book.durChapterIndex != durChapterIndex
book.durChapterPos = durChapterPos book.durChapterIndex = durChapterIndex
if (!pageChanged || chapterChanged) { book.durChapterPos = durChapterPos
appDb.bookChapterDao.getChapter(book.bookUrl, durChapterIndex)?.let { if (!pageChanged || chapterChanged) {
book.durChapterTitle = it.getDisplayTitle( appDb.bookChapterDao.getChapter(book.bookUrl, durChapterIndex)?.let {
ContentProcessor.get(book.name, book.origin).getTitleReplaceRules(), book.durChapterTitle = it.getDisplayTitle(
book.getUseReplaceRule() ContentProcessor.get(book.name, book.origin).getTitleReplaceRules(),
) book.getUseReplaceRule()
)
}
} }
appDb.bookDao.update(book)
}.onFailure {
AppLog.put("保存书籍阅读进度信息出错\n$it", it)
} }
appDb.bookDao.update(book)
} }
} }
@@ -327,21 +327,25 @@ object ReadManga : CoroutineScope by MainScope() {
fun saveRead(pageChanged: Boolean = false) { fun saveRead(pageChanged: Boolean = false) {
executor.execute { executor.execute {
val book = book ?: return@execute kotlin.runCatching {
book.lastCheckCount = 0 val book = book ?: return@execute
book.durChapterTime = System.currentTimeMillis() book.lastCheckCount = 0
val chapterChanged = book.durChapterIndex != durChapterIndex book.durChapterTime = System.currentTimeMillis()
book.durChapterIndex = durChapterIndex val chapterChanged = book.durChapterIndex != durChapterIndex
book.durChapterPos = durChapterPos book.durChapterIndex = durChapterIndex
if (!pageChanged || chapterChanged) { book.durChapterPos = durChapterPos
appDb.bookChapterDao.getChapter(book.bookUrl, durChapterIndex)?.let { if (!pageChanged || chapterChanged) {
book.durChapterTitle = it.getDisplayTitle( appDb.bookChapterDao.getChapter(book.bookUrl, durChapterIndex)?.let {
ContentProcessor.get(book.name, book.origin).getTitleReplaceRules(), book.durChapterTitle = it.getDisplayTitle(
book.getUseReplaceRule() ContentProcessor.get(book.name, book.origin).getTitleReplaceRules(),
) book.getUseReplaceRule()
)
}
} }
appDb.bookDao.update(book)
}.onFailure {
AppLog.put("保存漫画阅读进度信息出错\n$it", it)
} }
appDb.bookDao.update(book)
} }
} }
@@ -39,7 +39,6 @@ import io.legato.kazusa.help.http.postForm
import io.legato.kazusa.help.http.postJson import io.legato.kazusa.help.http.postJson
import io.legato.kazusa.help.http.postMultipart import io.legato.kazusa.help.http.postMultipart
import io.legato.kazusa.help.source.getShareScope import io.legato.kazusa.help.source.getShareScope
import io.legato.kazusa.model.Debug
import io.legato.kazusa.utils.EncoderUtils import io.legato.kazusa.utils.EncoderUtils
import io.legato.kazusa.utils.GSON import io.legato.kazusa.utils.GSON
import io.legato.kazusa.utils.GSONStrict import io.legato.kazusa.utils.GSONStrict
@@ -225,32 +224,32 @@ class AnalyzeUrl(
if (urlOption == null) { if (urlOption == null) {
urlOption = GSON.fromJsonObject<UrlOption>(urlOptionStr).getOrNull() urlOption = GSON.fromJsonObject<UrlOption>(urlOptionStr).getOrNull()
if (urlOption != null) { if (urlOption != null) {
Debug.log("≡链接参数 JSON 格式不规范,请改为规范格式") log("链接参数 JSON 格式不规范,请改为规范格式")
} }
} }
urlOption?.let { option -> urlOption?.let { option ->
option.getMethod()?.let { option.getMethod()?.let {
if (it.equals("POST", true)) method = RequestMethod.POST if (it.equals("POST", true)) method = RequestMethod.POST
} }
option.getHeaderMap()?.forEach { entry -> option.getHeaderMap()?.forEach { entry ->
headerMap[entry.key.toString()] = entry.value.toString() headerMap[entry.key.toString()] = entry.value.toString()
} }
option.getBody()?.let { option.getBody()?.let {
body = it body = it
} }
type = option.getType() type = option.getType()
charset = option.getCharset() charset = option.getCharset()
retry = option.getRetry() retry = option.getRetry()
useWebView = option.useWebView() useWebView = option.useWebView()
webJs = option.getWebJs() webJs = option.getWebJs()
option.getJs()?.let { jsStr -> option.getJs()?.let { jsStr ->
evalJS(jsStr, url)?.toString()?.let { evalJS(jsStr, url)?.toString()?.let {
url = it url = it
}
} }
serverID = option.getServerID() serverID = option.getServerID()
webViewDelayTime = max(0, option.getWebViewDelayTime() ?: 0) webViewDelayTime = max(0, option.getWebViewDelayTime() ?: 0)
} }
}
} }
urlNoQuery = url urlNoQuery = url
when (method) { when (method) {
@@ -458,7 +457,7 @@ class AnalyzeUrl(
else -> get(urlNoQuery, encodedQuery) else -> get(urlNoQuery, encodedQuery)
} }
}.let { }.let {
val isXml = it.raw.body?.contentType()?.toString() val isXml = it.raw.body.contentType()?.toString()
?.matches(AppPattern.xmlContentTypeRegex) == true ?.matches(AppPattern.xmlContentTypeRegex) == true
if (isXml && it.body?.trim()?.startsWith("<?xml", true) == false) { if (isXml && it.body?.trim()?.startsWith("<?xml", true) == false) {
StrResponse(it.raw, "<?xml version=\"1.0\"?>" + it.body) StrResponse(it.raw, "<?xml version=\"1.0\"?>" + it.body)
@@ -553,7 +552,7 @@ class AnalyzeUrl(
getByteArrayIfDataUri()?.let { getByteArrayIfDataUri()?.let {
return it return it
} }
return getResponseAwait().body!!.bytes() return getResponseAwait().body.bytes()
} }
fun getByteArray(): ByteArray { fun getByteArray(): ByteArray {
@@ -569,7 +568,7 @@ class AnalyzeUrl(
getByteArrayIfDataUri()?.let { getByteArrayIfDataUri()?.let {
return ByteArrayInputStream(it) return ByteArrayInputStream(it)
} }
return getResponseAwait().body!!.byteStream() return getResponseAwait().body.byteStream()
} }
fun getInputStream(): InputStream { fun getInputStream(): InputStream {
@@ -2,6 +2,7 @@ package io.legato.kazusa.model.localBook
import android.net.Uri import android.net.Uri
import android.util.Base64 import android.util.Base64
import androidx.core.net.toUri
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import com.script.ScriptBindings import com.script.ScriptBindings
import com.script.rhino.RhinoScriptEngine import com.script.rhino.RhinoScriptEngine
@@ -103,7 +104,7 @@ object LocalBook {
fun getLastModified(book: Book): Result<Long> { fun getLastModified(book: Book): Result<Long> {
return kotlin.runCatching { return kotlin.runCatching {
val uri = Uri.parse(book.bookUrl) val uri = book.bookUrl.toUri()
if (uri.isContentScheme()) { if (uri.isContentScheme()) {
return@runCatching DocumentFile.fromSingleUri(appCtx, uri)!!.lastModified() return@runCatching DocumentFile.fromSingleUri(appCtx, uri)!!.lastModified()
} }
@@ -144,6 +145,9 @@ object LocalBook {
val list = ArrayList(LinkedHashSet(chapters)) val list = ArrayList(LinkedHashSet(chapters))
list.forEachIndexed { index, bookChapter -> list.forEachIndexed { index, bookChapter ->
bookChapter.index = index bookChapter.index = index
if (bookChapter.title.isEmpty()) {
bookChapter.title = "无标题章节"
}
} }
val replaceRules = ContentProcessor.get(book).getTitleReplaceRules() val replaceRules = ContentProcessor.get(book).getTitleReplaceRules()
book.durChapterTitle = list.getOrElse(book.durChapterIndex) { list.last() } book.durChapterTitle = list.getOrElse(book.durChapterIndex) { list.last() }
@@ -191,6 +195,9 @@ object LocalBook {
return StringEscapeUtils.unescapeHtml4(content) return StringEscapeUtils.unescapeHtml4(content)
} }
} }
if (content.isNullOrEmpty()) {
return null
}
return content return content
} }
@@ -374,7 +381,7 @@ object LocalBook {
} }
if (deleteOriginal) { if (deleteOriginal) {
if (book.bookUrl.isContentScheme()) { if (book.bookUrl.isContentScheme()) {
val uri = Uri.parse(book.bookUrl) val uri = book.bookUrl.toUri()
DocumentFile.fromSingleUri(appCtx, uri)?.delete() DocumentFile.fromSingleUri(appCtx, uri)?.delete()
} else { } else {
FileUtils.delete(book.bookUrl) FileUtils.delete(book.bookUrl)
@@ -419,7 +426,7 @@ object LocalBook {
inputStream.use { inputStream.use {
val defaultBookTreeUri = AppConfig.defaultBookTreeUri val defaultBookTreeUri = AppConfig.defaultBookTreeUri
if (defaultBookTreeUri.isNullOrBlank()) throw NoBooksDirException() if (defaultBookTreeUri.isNullOrBlank()) throw NoBooksDirException()
val treeUri = Uri.parse(defaultBookTreeUri) val treeUri = defaultBookTreeUri.toUri()
return if (treeUri.isContentScheme()) { return if (treeUri.isContentScheme()) {
val treeDoc = DocumentFile.fromTreeUri(appCtx, treeUri) val treeDoc = DocumentFile.fromTreeUri(appCtx, treeUri)
var doc = treeDoc!!.findFile(fileName) var doc = treeDoc!!.findFile(fileName)
@@ -229,7 +229,7 @@ class MobiFile(var book: Book) {
break break
} }
section = section.next ?: break section = section.next ?: break
if (section.linear) { if (!section.linear) {
continue continue
} }
if (section.href == nextSectionHref) { if (section.href == nextSectionHref) {
@@ -215,6 +215,7 @@ class TextFile(private var book: Book) {
val curChapter = BookChapter() val curChapter = BookChapter()
curChapter.title = matcher.group() curChapter.title = matcher.group()
curChapter.start = curOffset + chapterLength curChapter.start = curOffset + chapterLength
curChapter.end = curChapter.start
toc.add(curChapter) toc.add(curChapter)
lastChapterWordCount = 0 lastChapterWordCount = 0
} else if (seekPos == 0 && chapterStart != 0) { } else if (seekPos == 0 && chapterStart != 0) {
@@ -242,6 +243,7 @@ class TextFile(private var book: Book) {
val curChapter = BookChapter() val curChapter = BookChapter()
curChapter.title = matcher.group() curChapter.title = matcher.group()
curChapter.start = curOffset + chapterLength curChapter.start = curOffset + chapterLength
curChapter.end = curChapter.start
toc.add(curChapter) toc.add(curChapter)
} else { //否则就block分割之后,上一个章节的剩余内容 } else { //否则就block分割之后,上一个章节的剩余内容
//获取上一章节 //获取上一章节
@@ -257,6 +259,7 @@ class TextFile(private var book: Book) {
val curChapter = BookChapter() val curChapter = BookChapter()
curChapter.title = matcher.group() curChapter.title = matcher.group()
curChapter.start = lastChapter.end curChapter.start = lastChapter.end
curChapter.end = curChapter.start
toc.add(curChapter) toc.add(curChapter)
} }
bookWordCount += chapterContent.length bookWordCount += chapterContent.length
@@ -275,6 +278,7 @@ class TextFile(private var book: Book) {
val curChapter = BookChapter() val curChapter = BookChapter()
curChapter.title = matcher.group() curChapter.title = matcher.group()
curChapter.start = lastChapter.end curChapter.start = lastChapter.end
curChapter.end = curChapter.start
toc.add(curChapter) toc.add(curChapter)
} else { //如果章节不存在则创建章节 } else { //如果章节不存在则创建章节
val curChapter = BookChapter() val curChapter = BookChapter()
@@ -17,8 +17,10 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.ensureActive import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
@@ -39,7 +41,7 @@ class SearchModel(private val scope: CoroutineScope, private val callBack: CallB
private var bookSourceParts = emptyList<BookSourcePart>() private var bookSourceParts = emptyList<BookSourcePart>()
private var searchBooks = arrayListOf<SearchBook>() private var searchBooks = arrayListOf<SearchBook>()
private var searchJob: Job? = null private var searchJob: Job? = null
private var workingState = MutableStateFlow(true)
private fun initSearchPool() { private fun initSearchPool() {
searchPool?.close() searchPool?.close()
@@ -82,6 +84,7 @@ class SearchModel(private val scope: CoroutineScope, private val callBack: CallB
bs.getBookSource()?.let { bs.getBookSource()?.let {
emit(it) emit(it)
} }
workingState.first { it }
} }
}.onStart { }.onStart {
callBack.onSearchStart() callBack.onSearchStart()
@@ -191,6 +194,14 @@ class SearchModel(private val scope: CoroutineScope, private val callBack: CallB
mSearchId = 0L mSearchId = 0L
} }
fun pause() {
workingState.value = false
}
fun resume() {
workingState.value = true
}
interface CallBack { interface CallBack {
fun getSearchScope(): SearchScope fun getSearchScope(): SearchScope
fun onSearchStart() fun onSearchStart()
@@ -27,6 +27,7 @@ import io.legato.kazusa.R
import io.legato.kazusa.base.BaseService import io.legato.kazusa.base.BaseService
import io.legato.kazusa.constant.AppConst import io.legato.kazusa.constant.AppConst
import io.legato.kazusa.constant.AppLog import io.legato.kazusa.constant.AppLog
import io.legato.kazusa.constant.AppPattern
import io.legato.kazusa.constant.EventBus import io.legato.kazusa.constant.EventBus
import io.legato.kazusa.constant.IntentAction import io.legato.kazusa.constant.IntentAction
import io.legato.kazusa.constant.NotificationId import io.legato.kazusa.constant.NotificationId
@@ -45,9 +46,7 @@ import io.legato.kazusa.ui.book.read.ReadBookActivity
import io.legato.kazusa.ui.book.read.page.entities.TextChapter import io.legato.kazusa.ui.book.read.page.entities.TextChapter
import io.legato.kazusa.utils.LogUtils import io.legato.kazusa.utils.LogUtils
import io.legato.kazusa.utils.activityPendingIntent import io.legato.kazusa.utils.activityPendingIntent
import io.legato.kazusa.utils.broadcastPendingIntent
import io.legato.kazusa.utils.getPrefBoolean import io.legato.kazusa.utils.getPrefBoolean
import io.legato.kazusa.utils.isVivoDevice
import io.legato.kazusa.utils.observeEvent import io.legato.kazusa.utils.observeEvent
import io.legato.kazusa.utils.observeSharedPreferences import io.legato.kazusa.utils.observeSharedPreferences
import io.legato.kazusa.utils.postEvent import io.legato.kazusa.utils.postEvent
@@ -326,9 +325,11 @@ abstract class BaseReadAloudService : BaseService(),
private fun prevP() { private fun prevP() {
if (nowSpeak > 0) { if (nowSpeak > 0) {
playStop() playStop()
nowSpeak-- do {
readAloudNumber -= contentList[nowSpeak].length + 1 + paragraphStartPos nowSpeak--
paragraphStartPos = 0 readAloudNumber -= contentList[nowSpeak].length + 1 + paragraphStartPos
paragraphStartPos = 0
} while (contentList[nowSpeak].matches(AppPattern.notReadAloudRegex))
textChapter?.let { textChapter?.let {
if (readAloudByPage) { if (readAloudByPage) {
val paragraphs = it.getParagraphs(true) val paragraphs = it.getParagraphs(true)
@@ -358,7 +359,9 @@ abstract class BaseReadAloudService : BaseService(),
val paragraphs = it.getParagraphs(true) val paragraphs = it.getParagraphs(true)
if (!paragraphs[nowSpeak].isParagraphEnd) readAloudNumber-- if (!paragraphs[nowSpeak].isParagraphEnd) readAloudNumber--
} }
if (readAloudNumber >= it.getReadLength(pageIndex + 1)) { if (pageIndex + 1 < it.pageSize
&& readAloudNumber >= it.getReadLength(pageIndex + 1)
) {
pageIndex++ pageIndex++
ReadBook.moveToNextPage() ReadBook.moveToNextPage()
} }
@@ -443,24 +446,75 @@ abstract class BaseReadAloudService : BaseService(),
PlaybackStateCompat.Builder() PlaybackStateCompat.Builder()
.setActions(MediaHelp.MEDIA_SESSION_ACTIONS) .setActions(MediaHelp.MEDIA_SESSION_ACTIONS)
.setState(state, nowSpeak.toLong(), 1f) .setState(state, nowSpeak.toLong(), 1f)
// 为系统媒体控件添加定时按钮
.addCustomAction(
PlaybackStateCompat.CustomAction.Builder(
"ACTION_ADD_TIMER",
getString(R.string.set_timer),
R.drawable.ic_time_add_24dp
).build()
)
.build() .build()
) )
} }
/**
* 初始化MediaSession, 注册多媒体按钮
*/
/** /**
* 初始化MediaSession, 注册多媒体按钮 * 初始化MediaSession, 注册多媒体按钮
*/ */
@SuppressLint("UnspecifiedImmutableFlag") @SuppressLint("UnspecifiedImmutableFlag")
private fun initMediaSession() { private fun initMediaSession() {
mediaSessionCompat.setCallback(object : MediaSessionCompat.Callback() { if (getPrefBoolean("systemMediaControlCompatibilityChange")) {
override fun onMediaButtonEvent(mediaButtonEvent: Intent): Boolean { mediaSessionCompat.setCallback(object : MediaSessionCompat.Callback() {
return MediaButtonReceiver.handleIntent(this@BaseReadAloudService, mediaButtonEvent) override fun onPlay() {
} resumeReadAloud()
}) }
mediaSessionCompat.setMediaButtonReceiver(
broadcastPendingIntent<MediaButtonReceiver>(Intent.ACTION_MEDIA_BUTTON) override fun onPause() {
) pauseReadAloud()
mediaSessionCompat.isActive = true }
override fun onSkipToNext() {
if (getPrefBoolean("mediaButtonPerNext", false)) {
nextChapter()
} else {
nextP()
}
}
override fun onSkipToPrevious() {
if (getPrefBoolean("mediaButtonPerNext", false)) {
prevChapter()
} else {
prevP()
}
}
override fun onStop() {
stopSelf()
}
override fun onCustomAction(action: String, extras: Bundle?) {
if (action == "ACTION_ADD_TIMER") addTimer()
}
override fun onMediaButtonEvent(mediaButtonEvent: Intent): Boolean {
return MediaButtonReceiver.handleIntent(
this@BaseReadAloudService, mediaButtonEvent
)
}
})
} else {
mediaSessionCompat.setCallback(object : MediaSessionCompat.Callback() {
override fun onMediaButtonEvent(mediaButtonEvent: Intent): Boolean {
return MediaButtonReceiver.handleIntent(
this@BaseReadAloudService, mediaButtonEvent
)
}
})
}
} }
/** /**
@@ -523,7 +577,7 @@ abstract class BaseReadAloudService : BaseService(),
private fun choiceMediaStyle(): androidx.media.app.NotificationCompat.MediaStyle { private fun choiceMediaStyle(): androidx.media.app.NotificationCompat.MediaStyle {
val mediaStyle = androidx.media.app.NotificationCompat.MediaStyle() val mediaStyle = androidx.media.app.NotificationCompat.MediaStyle()
.setShowActionsInCompactView(1, 2, 4) .setShowActionsInCompactView(1, 2, 4)
if (isVivoDevice) { if (getPrefBoolean("systemMediaControlCompatibilityChange")) {
//fix #4090 android 14 can not show play control in lock screen //fix #4090 android 14 can not show play control in lock screen
mediaStyle.setMediaSession(mediaSessionCompat.sessionToken) mediaStyle.setMediaSession(mediaSessionCompat.sessionToken)
} }
@@ -65,7 +65,6 @@ class CacheBookService : BaseService() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
isRun = true isRun = true
CacheBook.clear()
lifecycleScope.launch { lifecycleScope.launch {
while (isActive) { while (isActive) {
delay(1000) delay(1000)
@@ -3,6 +3,7 @@ package io.legato.kazusa.service
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.PendingIntent import android.app.PendingIntent
import android.net.Uri import android.net.Uri
import androidx.core.net.toUri
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.media3.common.C import androidx.media3.common.C
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
@@ -306,7 +307,7 @@ class HttpReadAloudService : BaseReadAloudService(),
} }
private fun createDownloader(factory: CacheDataSource.Factory, fileName: String): Downloader { private fun createDownloader(factory: CacheDataSource.Factory, fileName: String): Downloader {
val uri = Uri.parse(fileName) val uri = fileName.toUri()
val request = DownloadRequest.Builder(fileName, uri).build() val request = DownloadRequest.Builder(fileName, uri).build()
return DefaultDownloaderFactory(factory, okHttpClient.dispatcher.executorService) return DefaultDownloaderFactory(factory, okHttpClient.dispatcher.executorService)
.createDownloader(request) .createDownloader(request)
@@ -343,17 +344,17 @@ class HttpReadAloudService : BaseReadAloudService(),
val contentType = contentType.substringBefore(";") val contentType = contentType.substringBefore(";")
val ct = httpTts.contentType val ct = httpTts.contentType
if (contentType == "application/json" || contentType.startsWith("text/")) { if (contentType == "application/json" || contentType.startsWith("text/")) {
throw NoStackTraceException(response.body!!.string()) throw NoStackTraceException(response.body.string())
} else if (ct?.isNotBlank() == true) { } else if (ct?.isNotBlank() == true) {
if (!contentType.matches(ct.toRegex())) { if (!contentType.matches(ct.toRegex())) {
throw NoStackTraceException( throw NoStackTraceException(
"TTS服务器返回错误:" + response.body!!.string() "TTS服务器返回错误:" + response.body.string()
) )
} }
} }
} }
coroutineContext.ensureActive() coroutineContext.ensureActive()
response.body!!.byteStream().let { stream -> response.body.byteStream().let { stream ->
downloadErrorNo = 0 downloadErrorNo = 0
return stream return stream
} }
@@ -477,12 +478,12 @@ class HttpReadAloudService : BaseReadAloudService(),
val sleep = exoPlayer.duration / speakTextLength val sleep = exoPlayer.duration / speakTextLength
val start = speakTextLength * exoPlayer.currentPosition / exoPlayer.duration val start = speakTextLength * exoPlayer.currentPosition / exoPlayer.duration
for (i in start..contentList[nowSpeak].length) { for (i in start..contentList[nowSpeak].length) {
if (readAloudNumber + i > textChapter.getReadLength(pageIndex + 1)) { if (pageIndex + 1 < textChapter.pageSize
&& readAloudNumber + i > textChapter.getReadLength(pageIndex + 1)
) {
pageIndex++ pageIndex++
if (pageIndex < textChapter.pageSize) { ReadBook.moveToNextPage()
ReadBook.moveToNextPage() upTtsProgress(readAloudNumber + i.toInt())
upTtsProgress(readAloudNumber + i.toInt())
}
} }
delay(sleep) delay(sleep)
} }
@@ -195,7 +195,9 @@ class TTSReadAloudService : BaseReadAloudService(), TextToSpeech.OnInitListener
if (contentList[nowSpeak].matches(AppPattern.notReadAloudRegex)) { if (contentList[nowSpeak].matches(AppPattern.notReadAloudRegex)) {
nextParagraph() nextParagraph()
} }
if (readAloudNumber + 1 > it.getReadLength(pageIndex + 1)) { if (pageIndex + 1 < it.pageSize
&& readAloudNumber + 1 > it.getReadLength(pageIndex + 1)
) {
pageIndex++ pageIndex++
ReadBook.moveToNextPage() ReadBook.moveToNextPage()
} }
@@ -214,7 +216,9 @@ class TTSReadAloudService : BaseReadAloudService(), TextToSpeech.OnInitListener
"onRangeStart nowSpeak:$nowSpeak pageIndex:$pageIndex utteranceId:$utteranceId start:$start end:$end frame:$frame" "onRangeStart nowSpeak:$nowSpeak pageIndex:$pageIndex utteranceId:$utteranceId start:$start end:$end frame:$frame"
LogUtils.d(TAG, msg) LogUtils.d(TAG, msg)
textChapter?.let { textChapter?.let {
if (readAloudNumber + start > it.getReadLength(pageIndex + 1)) { if (pageIndex + 1 < it.pageSize
&& readAloudNumber + start > it.getReadLength(pageIndex + 1)
) {
pageIndex++ pageIndex++
ReadBook.moveToNextPage() ReadBook.moveToNextPage()
upTtsProgress(readAloudNumber + start) upTtsProgress(readAloudNumber + start)
@@ -56,7 +56,7 @@ class OnLineImportViewModel(app: Application) : BaseAssociationViewModel(app) {
fun importReadConfig(bytes: ByteArray, finally: (title: String, msg: String) -> Unit) { fun importReadConfig(bytes: ByteArray, finally: (title: String, msg: String) -> Unit) {
execute { execute {
val config = ReadBookConfig.import(bytes).getOrThrow() val config = ReadBookConfig.import(bytes)
ReadBookConfig.configList.forEachIndexed { index, c -> ReadBookConfig.configList.forEachIndexed { index, c ->
if (c.name == config.name) { if (c.name == config.name) {
ReadBookConfig.configList[index] = config ReadBookConfig.configList[index] = config
@@ -64,6 +64,7 @@ class AudioPlayActivity :
override val viewModel by viewModels<AudioPlayViewModel>() override val viewModel by viewModels<AudioPlayViewModel>()
private val timerSliderPopup by lazy { TimerSliderPopup(this) } private val timerSliderPopup by lazy { TimerSliderPopup(this) }
private var adjustProgress = false private var adjustProgress = false
private var playMode = AudioPlay.PlayMode.LIST_END_STOP
private val progressTimeFormat by lazy { private val progressTimeFormat by lazy {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
@@ -140,6 +141,14 @@ class AudioPlayActivity :
} }
private fun initView() { private fun initView() {
binding.ivPlayMode.setOnClickListener {
AudioPlay.changePlayMode()
}
observeEventSticky<AudioPlay.PlayMode>(EventBus.PLAY_MODE_CHANGED) {
playMode = it
updatePlayModeIcon()
}
binding.fabPlayStop.setOnClickListener { binding.fabPlayStop.setOnClickListener {
playButton() playButton()
} }
@@ -207,6 +216,10 @@ class AudioPlayActivity :
binding.llPlayMenu.applyNavigationBarPadding() binding.llPlayMenu.applyNavigationBarPadding()
} }
private fun updatePlayModeIcon() {
binding.ivPlayMode.setImageResource(playMode.iconRes)
}
private fun upCover(path: String?) { private fun upCover(path: String?) {
BookCover.load(this, path, sourceOrigin = AudioPlay.bookSource?.bookSourceUrl) { BookCover.load(this, path, sourceOrigin = AudioPlay.bookSource?.bookSourceUrl) {
BookCover.loadBlur(this, path, sourceOrigin = AudioPlay.bookSource?.bookSourceUrl) BookCover.loadBlur(this, path, sourceOrigin = AudioPlay.bookSource?.bookSourceUrl)
@@ -26,7 +26,7 @@ class AudioPlayViewModel(application: Application) : BaseViewModel(application)
fun initData(intent: Intent) = AudioPlay.apply { fun initData(intent: Intent) = AudioPlay.apply {
execute { execute {
val bookUrl = intent.getStringExtra("bookUrl") ?: return@execute val bookUrl = intent.getStringExtra("bookUrl") ?: book?.bookUrl ?: return@execute
val book = appDb.bookDao.getBook(bookUrl) ?: return@execute val book = appDb.bookDao.getBook(bookUrl) ?: return@execute
inBookshelf = intent.getBooleanExtra("inBookshelf", true) inBookshelf = intent.getBooleanExtra("inBookshelf", true)
initBook(book) initBook(book)
@@ -71,7 +71,7 @@ class AudioPlayViewModel(application: Application) : BaseViewModel(application)
if (oldBook.bookUrl == book.bookUrl) { if (oldBook.bookUrl == book.bookUrl) {
appDb.bookDao.update(book) appDb.bookDao.update(book)
} else { } else {
appDb.bookDao.insert(book) appDb.bookDao.replace(oldBook, book)
} }
appDb.bookChapterDao.delByBook(book.bookUrl) appDb.bookChapterDao.delByBook(book.bookUrl)
appDb.bookChapterDao.insert(*cList.toTypedArray()) appDb.bookChapterDao.insert(*cList.toTypedArray())
@@ -739,7 +739,7 @@ class BookInfoActivity :
else -> readBookResult.launch( else -> readBookResult.launch(
Intent( Intent(
this, this,
if (book.isImage && AppConfig.showMangaUi) ReadMangaActivity::class.java if (!book.isLocal && book.isImage && AppConfig.showMangaUi) ReadMangaActivity::class.java
else ReadBookActivity::class.java else ReadBookActivity::class.java
) )
.putExtra("bookUrl", book.bookUrl) .putExtra("bookUrl", book.bookUrl)
@@ -235,7 +235,7 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
WebBook.getChapterList(scope, bookSource, book, runPreUpdateJs) WebBook.getChapterList(scope, bookSource, book, runPreUpdateJs)
.onSuccess(IO) { .onSuccess(IO) {
if (inBookshelf) { if (inBookshelf) {
book.save() appDb.bookDao.replace(oldBook, book)
/** /**
* runPreUpdateJs 有可能会修改 book 的 bookUrl * runPreUpdateJs 有可能会修改 book 的 bookUrl
*/ */
@@ -1,8 +1,10 @@
package io.legato.kazusa.ui.book.info.edit package io.legato.kazusa.ui.book.info.edit
import android.app.Application import android.app.Application
import android.database.sqlite.SQLiteConstraintException
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import io.legato.kazusa.base.BaseViewModel import io.legato.kazusa.base.BaseViewModel
import io.legato.kazusa.constant.AppLog
import io.legato.kazusa.data.appDb import io.legato.kazusa.data.appDb
import io.legato.kazusa.data.entities.Book import io.legato.kazusa.data.entities.Book
import io.legato.kazusa.model.ReadBook import io.legato.kazusa.model.ReadBook
@@ -28,6 +30,12 @@ class BookInfoEditViewModel(application: Application) : BaseViewModel(applicatio
appDb.bookDao.update(book) appDb.bookDao.update(book)
}.onSuccess { }.onSuccess {
success?.invoke() success?.invoke()
}.onError {
if (it is SQLiteConstraintException) {
AppLog.put("书籍信息保存失败,存在相同书名作者书籍\n$it", it, true)
} else {
AppLog.put("书籍信息保存失败\n$it", it, true)
}
} }
} }
} }
@@ -119,7 +119,7 @@ class ReadMangaViewModel(application: Application) : BaseViewModel(application)
if (oldBook.bookUrl == book.bookUrl) { if (oldBook.bookUrl == book.bookUrl) {
appDb.bookDao.update(book) appDb.bookDao.update(book)
} else { } else {
appDb.bookDao.insert(book) appDb.bookDao.replace(oldBook, book)
BookHelp.updateCacheFolder(oldBook, book) BookHelp.updateCacheFolder(oldBook, book)
} }
appDb.bookChapterDao.delByBook(oldBook.bookUrl) appDb.bookChapterDao.delByBook(oldBook.bookUrl)
@@ -5,7 +5,6 @@ import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import android.content.res.Configuration import android.content.res.Configuration
import android.graphics.Color import android.graphics.Color
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.Gravity import android.view.Gravity
import android.view.HapticFeedbackConstants import android.view.HapticFeedbackConstants
@@ -19,6 +18,7 @@ import androidx.activity.addCallback
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.PopupMenu
import androidx.core.net.toUri
import androidx.core.view.doOnPreDraw import androidx.core.view.doOnPreDraw
import androidx.core.view.get import androidx.core.view.get
import androidx.core.view.size import androidx.core.view.size
@@ -906,10 +906,11 @@ class ReadBookActivity : BaseReadBookActivity(),
ReadBook.bookSource?.bookSourceUrl?.let { ReadBook.bookSource?.bookSourceUrl?.let {
scopes.add(it) scopes.add(it)
} }
val text = selectedText.lineSequence().map { it.trim() }.joinToString("\n")
replaceActivity.launch( replaceActivity.launch(
ReplaceEditActivity.startIntent( ReplaceEditActivity.startIntent(
this, this,
pattern = selectedText, pattern = text,
scope = scopes.joinToString(";") scope = scopes.joinToString(";")
) )
) )
@@ -1421,7 +1422,7 @@ class ReadBookActivity : BaseReadBookActivity(),
value = src value = src
} }
} else { } else {
viewModel.saveImage(src, Uri.parse(path)) viewModel.saveImage(src, path.toUri())
} }
} }
@@ -214,7 +214,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
if (oldBook.bookUrl == book.bookUrl) { if (oldBook.bookUrl == book.bookUrl) {
appDb.bookDao.update(book) appDb.bookDao.update(book)
} else { } else {
appDb.bookDao.insert(book) appDb.bookDao.replace(oldBook, book)
BookHelp.updateCacheFolder(oldBook, book) BookHelp.updateCacheFolder(oldBook, book)
} }
appDb.bookChapterDao.delByBook(oldBook.bookUrl) appDb.bookChapterDao.delByBook(oldBook.bookUrl)
@@ -1,16 +1,14 @@
package io.legato.kazusa.ui.book.read.config package io.legato.kazusa.ui.book.read.config
//import io.legado.app.lib.theme.bottomBackground
//import io.legado.app.lib.theme.getPrimaryTextColor
//import io.legado.app.lib.theme.getSecondaryTextColor
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.DialogInterface import android.content.DialogInterface
import android.graphics.Color
import android.graphics.PorterDuff import android.graphics.PorterDuff
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import androidx.appcompat.widget.TooltipCompat import androidx.appcompat.widget.TooltipCompat
import androidx.core.graphics.toColorInt
import androidx.core.view.isGone
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import com.google.android.material.slider.Slider import com.google.android.material.slider.Slider
import com.jaredrummler.android.colorpicker.ColorPickerDialog import com.jaredrummler.android.colorpicker.ColorPickerDialog
@@ -22,12 +20,14 @@ import io.legato.kazusa.databinding.DialogEditTextBinding
import io.legato.kazusa.databinding.DialogReadBgTextBinding import io.legato.kazusa.databinding.DialogReadBgTextBinding
import io.legato.kazusa.databinding.ItemBgImageBinding import io.legato.kazusa.databinding.ItemBgImageBinding
import io.legato.kazusa.help.DefaultData import io.legato.kazusa.help.DefaultData
import io.legato.kazusa.help.book.isImage
import io.legato.kazusa.help.config.ReadBookConfig import io.legato.kazusa.help.config.ReadBookConfig
import io.legato.kazusa.help.http.newCallResponseBody import io.legato.kazusa.help.http.newCallResponseBody
import io.legato.kazusa.help.http.okHttpClient import io.legato.kazusa.help.http.okHttpClient
import io.legato.kazusa.lib.dialogs.SelectItem import io.legato.kazusa.lib.dialogs.SelectItem
import io.legato.kazusa.lib.dialogs.alert import io.legato.kazusa.lib.dialogs.alert
import io.legato.kazusa.lib.dialogs.selector import io.legato.kazusa.lib.dialogs.selector
import io.legato.kazusa.model.ReadBook
import io.legato.kazusa.ui.book.read.ReadBookActivity import io.legato.kazusa.ui.book.read.ReadBookActivity
import io.legato.kazusa.ui.file.HandleFileContract import io.legato.kazusa.ui.file.HandleFileContract
import io.legato.kazusa.utils.FileUtils import io.legato.kazusa.utils.FileUtils
@@ -119,6 +119,7 @@ class BgTextConfigDialog : BaseBottomSheetDialogFragment(R.layout.dialog_read_bg
tvName.text = getString(R.string.select_image) tvName.text = getString(R.string.select_image)
ivBg.setImageResource(R.drawable.ic_image) ivBg.setImageResource(R.drawable.ic_image)
ivBg.setColorFilter(primaryTextColor, PorterDuff.Mode.SRC_IN) ivBg.setColorFilter(primaryTextColor, PorterDuff.Mode.SRC_IN)
swUnderline.isGone = ReadBook.book?.isImage == true
root.setOnClickListener { root.setOnClickListener {
selectBgImage.launch() selectBgImage.launch()
} }
@@ -184,8 +185,8 @@ class BgTextConfigDialog : BaseBottomSheetDialogFragment(R.layout.dialog_read_bg
} }
binding.tvBgColor.setOnClickListener { binding.tvBgColor.setOnClickListener {
val bgColor = val bgColor =
if (curBgType() == 0) Color.parseColor(curBgStr()) if (curBgType() == 0) curBgStr().toColorInt()
else Color.parseColor("#015A86") else "#015A86".toColorInt()
ColorPickerDialog.newBuilder() ColorPickerDialog.newBuilder()
.setColor(bgColor) .setColor(bgColor)
.setShowAlphaSlider(false) .setShowAlphaSlider(false)
@@ -355,7 +356,7 @@ class BgTextConfigDialog : BaseBottomSheetDialogFragment(R.layout.dialog_read_bg
private fun importConfig(byteArray: ByteArray) { private fun importConfig(byteArray: ByteArray) {
execute { execute {
ReadBookConfig.import(byteArray).getOrThrow() ReadBookConfig.import(byteArray)
}.onSuccess { }.onSuccess {
ReadBookConfig.durConfig = it ReadBookConfig.durConfig = it
postEvent(EventBus.UP_CONFIG, arrayListOf(1, 2, 5)) postEvent(EventBus.UP_CONFIG, arrayListOf(1, 2, 5))
@@ -36,7 +36,8 @@ class ClickActionConfigDialog : BaseDialogFragment(R.layout.dialog_click_action_
Pair(9, getString(R.string.replace_state_change)), Pair(9, getString(R.string.replace_state_change)),
Pair(10, getString(R.string.chapter_list)), Pair(10, getString(R.string.chapter_list)),
Pair(11, getString(R.string.search_content)), Pair(11, getString(R.string.search_content)),
Pair(12, getString(R.string.sync_book_progress_t)) Pair(12, getString(R.string.sync_book_progress_t)),
Pair(13, getString(R.string.read_aloud_pause_resume))
) )
} }
@@ -130,6 +130,10 @@ class MoreConfigDialog : BaseBottomSheetDialogFragment(R.layout.dialog_more_conf
ReadBook.callBack?.upPageAnim(true) ReadBook.callBack?.upPageAnim(true)
ReadBook.loadContent(false) ReadBook.loadContent(false)
} }
PreferKey.paddingDisplayCutouts -> {
postEvent(EventBus.UP_CONFIG, arrayListOf(2))
}
} }
} }
@@ -14,12 +14,12 @@ import io.legato.kazusa.utils.themeColor
/** /**
* 自动翻页 * 自动翻页
*/ */
class AutoPager( class AutoPager(private val readView: ReadView) : Runnable {
private val readView: ReadView) {
private var progress = 0 private var progress = 0
var isRunning = false var isRunning = false
private set private set
private var isPausing = false private var isPausing = false
private var isEInkMode = false
private var scrollOffsetRemain = 0.0 private var scrollOffsetRemain = 0.0
private var scrollOffset = 0 private var scrollOffset = 0
private var lastTimeMillis = 0L private var lastTimeMillis = 0L
@@ -29,10 +29,15 @@ class AutoPager(
fun start() { fun start() {
isRunning = true isRunning = true
paint.color = readView.context.themeColor(androidx.appcompat.R.attr.colorPrimary) isEInkMode = AppConfig.isEInkMode
lastTimeMillis = SystemClock.uptimeMillis()
readView.curPage.upSelectAble(false) readView.curPage.upSelectAble(false)
readView.invalidate() if (isEInkMode) {
readView.postDelayed(this, ReadBookConfig.autoReadSpeed * 1000L)
} else {
paint.color = readView.context.themeColor(androidx.appcompat.R.attr.colorPrimary)
lastTimeMillis = SystemClock.uptimeMillis()
readView.invalidate()
}
} }
fun stop() { fun stop() {
@@ -41,6 +46,8 @@ class AutoPager(
} }
isRunning = false isRunning = false
isPausing = false isPausing = false
isEInkMode = false
readView.removeCallbacks(this)
readView.curPage.upSelectAble(AppConfig.textSelectAble) readView.curPage.upSelectAble(AppConfig.textSelectAble)
readView.invalidate() readView.invalidate()
reset() reset()
@@ -52,6 +59,7 @@ class AutoPager(
return return
} }
isPausing = true isPausing = true
readView.removeCallbacks(this)
} }
fun resume() { fun resume() {
@@ -59,15 +67,24 @@ class AutoPager(
return return
} }
isPausing = false isPausing = false
lastTimeMillis = SystemClock.uptimeMillis() if (isEInkMode) {
readView.invalidate() readView.postDelayed(this, ReadBookConfig.autoReadSpeed * 1000L)
} else {
lastTimeMillis = SystemClock.uptimeMillis()
readView.invalidate()
}
} }
fun reset() { fun reset() {
progress = 0 if (isEInkMode) {
scrollOffsetRemain = 0.0 readView.removeCallbacks(this)
scrollOffset = 0 readView.postDelayed(this, ReadBookConfig.autoReadSpeed * 1000L)
canvasRecorder.invalidate() } else {
progress = 0
scrollOffsetRemain = 0.0
scrollOffset = 0
canvasRecorder.invalidate()
}
} }
fun upRecorder() { fun upRecorder() {
@@ -76,12 +93,15 @@ class AutoPager(
} }
fun onDraw(canvas: Canvas) { fun onDraw(canvas: Canvas) {
if (!isRunning) { if (!isRunning || isEInkMode) {
return return
} }
if (readView.isScroll) { if (readView.isScroll) {
if (!isPausing) readView.curPage.scroll(-scrollOffset) if (!isPausing) {
readView.curPage.scroll(-scrollOffset)
scrollOffset = 0
}
} else { } else {
val bottom = progress val bottom = progress
val width = readView.width val width = readView.width
@@ -104,7 +124,7 @@ class AutoPager(
} }
fun computeOffset() { fun computeOffset() {
if (!isRunning || isPausing) { if (!isRunning || isPausing || isEInkMode) {
return return
} }
@@ -132,4 +152,15 @@ class AutoPager(
} }
} }
override fun run() {
if (!isRunning || isPausing) {
return
}
if (!readView.fillPage(PageDirection.NEXT)) {
stop()
} else {
readView.postDelayed(this, ReadBookConfig.autoReadSpeed * 1000L)
}
}
} }
@@ -2,15 +2,20 @@ package io.legato.kazusa.ui.book.read.page
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.graphics.drawable.LayerDrawable
import android.view.LayoutInflater import android.view.LayoutInflater
import android.widget.FrameLayout import android.widget.FrameLayout
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toDrawable
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isGone import androidx.core.view.isGone
import androidx.core.view.isInvisible import androidx.core.view.isInvisible
import io.legato.kazusa.R import io.legato.kazusa.R
import io.legato.kazusa.constant.AppConst.timeFormat import io.legato.kazusa.constant.AppConst.timeFormat
import io.legato.kazusa.data.entities.Bookmark import io.legato.kazusa.data.entities.Bookmark
import io.legato.kazusa.databinding.ViewBookPageBinding import io.legato.kazusa.databinding.ViewBookPageBinding
import io.legato.kazusa.help.config.AppConfig
import io.legato.kazusa.help.config.ReadBookConfig import io.legato.kazusa.help.config.ReadBookConfig
import io.legato.kazusa.help.config.ReadTipConfig import io.legato.kazusa.help.config.ReadTipConfig
import io.legato.kazusa.model.ReadBook import io.legato.kazusa.model.ReadBook
@@ -25,6 +30,7 @@ import io.legato.kazusa.utils.applyNavigationBarPadding
import io.legato.kazusa.utils.applyStatusBarPadding import io.legato.kazusa.utils.applyStatusBarPadding
import io.legato.kazusa.utils.dpToPx import io.legato.kazusa.utils.dpToPx
import io.legato.kazusa.utils.gone import io.legato.kazusa.utils.gone
import io.legato.kazusa.utils.setOnApplyWindowInsetsListenerCompat
import io.legato.kazusa.utils.setTextIfNotEqual import io.legato.kazusa.utils.setTextIfNotEqual
import io.legato.kazusa.utils.statusBarHeight import io.legato.kazusa.utils.statusBarHeight
import splitties.views.backgroundColor import splitties.views.backgroundColor
@@ -56,7 +62,7 @@ class PageView(context: Context) : FrameLayout(context) {
get() { get() {
val h1 = if (binding.vwStatusBar.isGone) 0 else binding.vwStatusBar.height val h1 = if (binding.vwStatusBar.isGone) 0 else binding.vwStatusBar.height
val h2 = if (binding.llHeader.isGone) 0 else binding.llHeader.height val h2 = if (binding.llHeader.isGone) 0 else binding.llHeader.height
return h1 + h2 return h1 + h2 + binding.vwRoot.paddingTop
} }
init { init {
@@ -94,6 +100,7 @@ class PageView(context: Context) : FrameLayout(context) {
vwBottomDivider.backgroundColor = tipDividerColor vwBottomDivider.backgroundColor = tipDividerColor
upStatusBar() upStatusBar()
upNavigationBar() upNavigationBar()
upPaddingDisplayCutouts()
llHeader.setPadding( llHeader.setPadding(
it.headerPaddingLeft.dpToPx(), it.headerPaddingLeft.dpToPx(),
it.headerPaddingTop.dpToPx(), it.headerPaddingTop.dpToPx(),
@@ -125,6 +132,24 @@ class PageView(context: Context) : FrameLayout(context) {
binding.vwNavigationBar.isGone = ReadBookConfig.hideNavigationBar binding.vwNavigationBar.isGone = ReadBookConfig.hideNavigationBar
} }
fun upPaddingDisplayCutouts() {
if (AppConfig.paddingDisplayCutouts) {
binding.vwRoot.setOnApplyWindowInsetsListenerCompat { _, windowInsets ->
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.displayCutout())
binding.vwRoot.setPadding(
insets.left,
if (binding.vwStatusBar.isGone) insets.top else 0,
insets.right,
insets.bottom
)
windowInsets
}
} else {
ViewCompat.setOnApplyWindowInsetsListener(binding.vwRoot, null)
binding.vwRoot.setPadding(0, 0, 0, 0)
}
}
/** /**
* 更新阅读信息 * 更新阅读信息
*/ */
@@ -239,8 +264,12 @@ class PageView(context: Context) : FrameLayout(context) {
* 更新背景 * 更新背景
*/ */
fun upBg() { fun upBg() {
binding.vwRoot.backgroundColor = ReadBookConfig.bgMeanColor binding.vwRoot.background = LayerDrawable(
binding.vwBg.background = ReadBookConfig.bg arrayOf(
ReadBookConfig.bgMeanColor.toDrawable(),
ReadBookConfig.bg
)
)
upBgAlpha() upBgAlpha()
} }
@@ -248,7 +277,8 @@ class PageView(context: Context) : FrameLayout(context) {
* 更新背景透明度 * 更新背景透明度
*/ */
fun upBgAlpha() { fun upBgAlpha() {
binding.vwBg.alpha = ReadBookConfig.bgAlpha / 100f ReadBookConfig.bg?.alpha = (ReadBookConfig.bgAlpha / 100f * 255).toInt()
binding.vwRoot.invalidate()
} }
/** /**
@@ -17,6 +17,7 @@ import io.legato.kazusa.help.config.AppConfig
import io.legato.kazusa.help.config.ReadBookConfig import io.legato.kazusa.help.config.ReadBookConfig
import io.legato.kazusa.model.ReadAloud import io.legato.kazusa.model.ReadAloud
import io.legato.kazusa.model.ReadBook import io.legato.kazusa.model.ReadBook
import io.legato.kazusa.service.BaseReadAloudService
import io.legato.kazusa.ui.book.read.ContentEditDialog import io.legato.kazusa.ui.book.read.ContentEditDialog
import io.legato.kazusa.ui.book.read.page.api.DataSource import io.legato.kazusa.ui.book.read.page.api.DataSource
import io.legato.kazusa.ui.book.read.page.delegate.CoverPageDelegate import io.legato.kazusa.ui.book.read.page.delegate.CoverPageDelegate
@@ -456,6 +457,13 @@ class ReadView(context: Context, attrs: AttributeSet) :
{ progress -> callBack.sureNewProgress(progress) }, { progress -> callBack.sureNewProgress(progress) },
{ context.longToastOnUi(context.getString(R.string.upload_book_success)) }, { context.longToastOnUi(context.getString(R.string.upload_book_success)) },
{ context.longToastOnUi(context.getString(R.string.sync_book_progress_success)) }) { context.longToastOnUi(context.getString(R.string.sync_book_progress_success)) })
13 -> {
if (BaseReadAloudService.isPlay()) {
ReadAloud.pause(context)
} else {
ReadAloud.resume(context)
}
}
} }
} }
@@ -160,6 +160,15 @@ data class TextLine(
columns[i].draw(view, canvas) columns[i].draw(view, canvas)
} }
} }
// 墨水屏模式下的朗读和搜索下划线
if (AppConfig.isEInkMode && (isReadAloud || searchResultColumnCount > 0)) {
val underlinePaint = PaintPool.obtain()
underlinePaint.set(ChapterProvider.contentPaint)
underlinePaint.strokeWidth = 1.dpToPx().toFloat()
val lineY = height - 1.dpToPx()
canvas.drawLine(lineStart + indentWidth, lineY, lineEnd, lineY, underlinePaint)
PaintPool.recycle(underlinePaint)
}
if (ReadBookConfig.underline && !isImage && ReadBook.book?.isImage != true) { if (ReadBookConfig.underline && !isImage && ReadBook.book?.isImage != true) {
drawUnderline(canvas) drawUnderline(canvas)
} }
@@ -34,6 +34,7 @@ import io.legato.kazusa.utils.postEvent
import io.legato.kazusa.utils.spToPx import io.legato.kazusa.utils.spToPx
import io.legato.kazusa.utils.splitNotBlank import io.legato.kazusa.utils.splitNotBlank
import io.legato.kazusa.utils.textHeight import io.legato.kazusa.utils.textHeight
import io.legato.kazusa.utils.toastOnUi
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import splitties.init.appCtx import splitties.init.appCtx
import java.util.LinkedList import java.util.LinkedList
@@ -1001,20 +1002,8 @@ object ChapterProvider {
} }
} }
if (viewWidth > 0 && viewHeight > 0) { if (viewWidth <= 0 || viewHeight <= 0) {
paddingLeft = ReadBookConfig.paddingLeft.dpToPx() return
paddingTop = ReadBookConfig.paddingTop.dpToPx()
paddingRight = ReadBookConfig.paddingRight.dpToPx()
paddingBottom = ReadBookConfig.paddingBottom.dpToPx()
visibleWidth = if (doublePage) {
viewWidth / 2 - paddingLeft - paddingRight
} else {
viewWidth - paddingLeft - paddingRight
}
//留1dp画最后一行下划线
visibleHeight = viewHeight - paddingTop - paddingBottom
visibleRight = viewWidth - paddingRight
visibleBottom = paddingTop + visibleHeight
} }
visibleRect.set( visibleRect.set(
@@ -1024,6 +1013,37 @@ object ChapterProvider {
visibleBottom.toFloat() visibleBottom.toFloat()
) )
paddingLeft = ReadBookConfig.paddingLeft.dpToPx()
paddingTop = ReadBookConfig.paddingTop.dpToPx()
paddingRight = ReadBookConfig.paddingRight.dpToPx()
paddingBottom = ReadBookConfig.paddingBottom.dpToPx()
visibleWidth = if (doublePage) {
viewWidth / 2 - paddingLeft - paddingRight
} else {
viewWidth - paddingLeft - paddingRight
}
//留1dp画最后一行下划线
visibleHeight = viewHeight - paddingTop - paddingBottom
visibleRight = viewWidth - paddingRight
visibleBottom = paddingTop + visibleHeight
if (paddingLeft >= visibleRight || paddingTop >= visibleBottom) {
appCtx.toastOnUi("边距设置过大,请重新设置")
visibleRect.set(
0f,
0f,
viewWidth.toFloat(),
viewHeight.toFloat()
)
} else {
visibleRect.set(
paddingLeft.toFloat(),
paddingTop.toFloat(),
visibleRight.toFloat(),
visibleBottom.toFloat()
)
}
} }
} }
@@ -20,7 +20,6 @@ import io.legato.kazusa.ui.book.read.page.entities.TextChapter
import io.legato.kazusa.ui.book.read.page.entities.TextLine import io.legato.kazusa.ui.book.read.page.entities.TextLine
import io.legato.kazusa.ui.book.read.page.entities.TextPage import io.legato.kazusa.ui.book.read.page.entities.TextPage
import io.legato.kazusa.ui.book.read.page.entities.column.ImageColumn import io.legato.kazusa.ui.book.read.page.entities.column.ImageColumn
import io.legato.kazusa.ui.book.read.page.entities.column.ReviewColumn
import io.legato.kazusa.ui.book.read.page.entities.column.TextColumn import io.legato.kazusa.ui.book.read.page.entities.column.TextColumn
import io.legato.kazusa.utils.dpToPx import io.legato.kazusa.utils.dpToPx
import io.legato.kazusa.utils.fastSum import io.legato.kazusa.utils.fastSum
@@ -214,13 +213,13 @@ class TextChapterLayout(
emptyContent = contents.isEmpty(), emptyContent = contents.isEmpty(),
isVolumeTitle = bookChapter.isVolume isVolumeTitle = bookChapter.isVolume
) )
pendingTextPage.lines.last().isParagraphEnd = true
stringBuilder.append("\n")
} }
pendingTextPage.lines.last().isParagraphEnd = true
stringBuilder.append("\n")
durY += titleBottomSpacing durY += titleBottomSpacing
// 如果是单图模式且当前页有内容,强制分页 // 如果是单图模式且当前页有内容,强制分页
if (isSingleImageStyle && pendingTextPage.lines.isNotEmpty()) { if (isSingleImageStyle && pendingTextPage.lines.isNotEmpty() && contents.isNotEmpty()) {
prepareNextPageIfNeed() prepareNextPageIfNeed()
} }
} }
@@ -708,13 +707,13 @@ class TextChapterLayout(
) )
} }
isLineEnd && char == ChapterProvider.reviewChar -> { // isLineEnd && char == ChapterProvider.reviewChar -> {
ReviewColumn( // ReviewColumn(
start = absStartX + xStart, // start = absStartX + xStart,
end = absStartX + xEnd, // end = absStartX + xEnd,
count = 100 // count = 100
) // )
} // }
else -> { else -> {
TextColumn( TextColumn(
@@ -12,12 +12,13 @@ import androidx.activity.viewModels
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.widget.doAfterTextChanged import androidx.core.widget.doAfterTextChanged
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.DefaultItemAnimator import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.flexbox.FlexboxLayoutManager import com.google.android.flexbox.FlexboxLayoutManager
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.search.SearchBar import com.google.android.material.search.SearchBar
import com.google.android.material.search.SearchView import com.google.android.material.search.SearchView
import io.legato.kazusa.R import io.legato.kazusa.R
@@ -45,6 +46,7 @@ import io.legato.kazusa.utils.viewbindingdelegate.viewBinding
import io.legato.kazusa.utils.visible import io.legato.kazusa.utils.visible
import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.conflate import kotlinx.coroutines.flow.conflate
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
@@ -183,21 +185,19 @@ class SearchActivity : VMBaseActivity<ActivityBookSearchBinding, SearchViewModel
binding.appBar.post { binding.appBar.post {
val maxPaddingTop = binding.appBar.paddingTop val maxPaddingTop = binding.appBar.paddingTop
binding.appBar.addOnOffsetChangedListener( binding.appBar.addOnOffsetChangedListener { appBarLayout, verticalOffset ->
AppBarLayout.OnOffsetChangedListener { appBarLayout, verticalOffset -> val totalScrollRange = appBarLayout.totalScrollRange
val totalScrollRange = appBarLayout.totalScrollRange val scrollRatio =
val scrollRatio = (1f - (abs(verticalOffset).toFloat() / totalScrollRange)).coerceIn(0f, 1f)
(1f - (abs(verticalOffset).toFloat() / totalScrollRange)).coerceIn(0f, 1f)
appBarLayout.alpha = scrollRatio appBarLayout.alpha = scrollRatio
appBarLayout.setPadding( appBarLayout.setPadding(
appBarLayout.paddingLeft, appBarLayout.paddingLeft,
(maxPaddingTop * scrollRatio).toInt(), (maxPaddingTop * scrollRatio).toInt(),
appBarLayout.paddingRight, appBarLayout.paddingRight,
appBarLayout.paddingBottom appBarLayout.paddingBottom
) )
} }
)
} }
searchBar.setOnMenuItemClickListener { item -> searchBar.setOnMenuItemClickListener { item ->
@@ -343,6 +343,16 @@ class SearchActivity : VMBaseActivity<ActivityBookSearchBinding, SearchViewModel
groups = it groups = it
} }
} }
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.RESUMED) {
viewModel.resume()
try {
awaitCancellation()
} finally {
viewModel.pause()
}
}
}
} }
/** /**
@@ -123,6 +123,14 @@ class SearchViewModel(application: Application) : BaseViewModel(application) {
searchModel.cancelSearch() searchModel.cancelSearch()
} }
fun pause() {
searchModel.pause()
}
fun resume() {
searchModel.resume()
}
/** /**
* 保存搜索关键字 * 保存搜索关键字
*/ */
@@ -2,6 +2,7 @@ package io.legato.kazusa.ui.book.searchContent
import android.text.Spanned import android.text.Spanned
import androidx.core.text.HtmlCompat import androidx.core.text.HtmlCompat
import io.legato.kazusa.help.config.AppConfig
data class SearchResult( data class SearchResult(
val resultCount: Int = 0, val resultCount: Int = 0,
@@ -22,19 +23,34 @@ data class SearchResult(
val leftString = resultText.substring(0, queryIndexInSurrounding) val leftString = resultText.substring(0, queryIndexInSurrounding)
val rightString = val rightString =
resultText.substring(queryIndexInSurrounding + query.length, resultText.length) resultText.substring(queryIndexInSurrounding + query.length, resultText.length)
val html = buildString { // 检查是否为墨水屏模式
append(chapterTitle.colorTextForHtml(accentColor)) val html = if (AppConfig.isEInkMode) {
append("<br>") // 墨水屏模式:使用下划线
append(leftString.colorTextForHtml(textColor)) buildString {
append(query.colorTextForHtml(accentColor)) append("<u>${chapterTitle}</u>")
append(rightString.colorTextForHtml(textColor)) append("<br>")
append(leftString)
append("<u>${query}</u>")
append(rightString)
}
} else {
// 普通模式:使用颜色
buildString {
append(chapterTitle.colorTextForHtml(accentColor))
append("<br>")
append(leftString.colorTextForHtml(textColor))
append(query.colorTextForHtml(accentColor))
append(rightString.colorTextForHtml(textColor))
}
} }
HtmlCompat.fromHtml(html, HtmlCompat.FROM_HTML_MODE_LEGACY) HtmlCompat.fromHtml(html, HtmlCompat.FROM_HTML_MODE_LEGACY)
} else { } else {
HtmlCompat.fromHtml( val html = if (AppConfig.isEInkMode) {
resultText.colorTextForHtml(textColor), resultText
HtmlCompat.FROM_HTML_MODE_LEGACY } else {
) resultText.colorTextForHtml(textColor)
}
HtmlCompat.fromHtml(html, HtmlCompat.FROM_HTML_MODE_LEGACY)
} }
} }
@@ -65,8 +65,19 @@ class DictDialog() : BaseBottomSheetDialogFragment(R.layout.dialog_dict) {
tag = it tag = it
}) })
} }
setupTabLayoutMode(it.size)
} }
}
//根据已启用词典数动态选取布局
private fun setupTabLayoutMode(dictCount: Int) {
if (dictCount <= 4) {
binding.tabLayout.tabMode = TabLayout.MODE_FIXED
binding.tabLayout.tabGravity = TabLayout.GRAVITY_FILL
} else {
binding.tabLayout.tabMode = TabLayout.MODE_SCROLLABLE
binding.tabLayout.tabGravity = TabLayout.GRAVITY_CENTER
}
} }
} }
@@ -17,9 +17,11 @@ class SourceLoginActivity : VMBaseActivity<ActivitySourceLoginBinding, SourceLog
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
viewModel.initData(intent) { source -> viewModel.initData(intent, success = { source ->
initView(source) initView(source)
} }, error = {
finish()
})
} }
private fun initView(source: BaseSource) { private fun initView(source: BaseSource) {
@@ -2,11 +2,12 @@ package io.legato.kazusa.ui.login
import android.app.Application import android.app.Application
import android.content.Intent import android.content.Intent
import com.script.rhino.runScriptWithContext
import io.legato.kazusa.base.BaseViewModel import io.legato.kazusa.base.BaseViewModel
import io.legato.kazusa.constant.AppLog
import io.legato.kazusa.data.appDb import io.legato.kazusa.data.appDb
import io.legato.kazusa.data.entities.BaseSource import io.legato.kazusa.data.entities.BaseSource
import io.legato.kazusa.exception.NoStackTraceException import io.legato.kazusa.exception.NoStackTraceException
import com.script.rhino.runScriptWithContext
import io.legato.kazusa.utils.toastOnUi import io.legato.kazusa.utils.toastOnUi
class SourceLoginViewModel(application: Application) : BaseViewModel(application) { class SourceLoginViewModel(application: Application) : BaseViewModel(application) {
@@ -14,7 +15,7 @@ class SourceLoginViewModel(application: Application) : BaseViewModel(application
var source: BaseSource? = null var source: BaseSource? = null
var headerMap: Map<String, String> = emptyMap() var headerMap: Map<String, String> = emptyMap()
fun initData(intent: Intent, success: (bookSource: BaseSource) -> Unit) { fun initData(intent: Intent, success: (bookSource: BaseSource) -> Unit, error: () -> Unit) {
execute { execute {
val sourceKey = intent.getStringExtra("key") val sourceKey = intent.getStringExtra("key")
?: throw NoStackTraceException("没有参数") ?: throw NoStackTraceException("没有参数")
@@ -33,6 +34,9 @@ class SourceLoginViewModel(application: Application) : BaseViewModel(application
} else { } else {
context.toastOnUi("未找到书源") context.toastOnUi("未找到书源")
} }
}.onError {
error.invoke()
AppLog.put("登录 UI 初始化失败\n$it", it, true)
} }
} }
@@ -167,7 +167,7 @@ class MainViewModel(application: Application) : BaseViewModel(application) {
if (book.bookUrl == bookUrl) { if (book.bookUrl == bookUrl) {
appDb.bookDao.update(book) appDb.bookDao.update(book)
} else { } else {
appDb.bookDao.insert(book) appDb.bookDao.replace(oldBook, book)
BookHelp.updateCacheFolder(oldBook, book) BookHelp.updateCacheFolder(oldBook, book)
} }
appDb.bookChapterDao.delByBook(bookUrl) appDb.bookChapterDao.delByBook(bookUrl)
@@ -1,7 +1,6 @@
package io.legato.kazusa.ui.replace package io.legato.kazusa.ui.replace
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
@@ -25,8 +24,6 @@ import io.legato.kazusa.help.DirectLinkUpload
import io.legato.kazusa.help.book.ContentProcessor import io.legato.kazusa.help.book.ContentProcessor
import io.legato.kazusa.help.coroutine.Coroutine import io.legato.kazusa.help.coroutine.Coroutine
import io.legato.kazusa.lib.dialogs.alert import io.legato.kazusa.lib.dialogs.alert
//import io.legado.app.lib.theme.primaryColor
//import io.legado.app.lib.theme.primaryTextColor
import io.legato.kazusa.ui.association.ImportReplaceRuleDialog import io.legato.kazusa.ui.association.ImportReplaceRuleDialog
import io.legato.kazusa.ui.file.HandleFileContract import io.legato.kazusa.ui.file.HandleFileContract
import io.legato.kazusa.ui.qrcode.QrCodeResult import io.legato.kazusa.ui.qrcode.QrCodeResult
@@ -72,7 +69,7 @@ class ReplaceRuleActivity : VMBaseActivity<ActivityReplaceRuleBinding, ReplaceRu
private val searchView: SearchView by lazy { private val searchView: SearchView by lazy {
binding.titleBar.findViewById(R.id.search_view) binding.titleBar.findViewById(R.id.search_view)
} }
private var groups = hashSetOf<String>() private var groups = arrayListOf<String>()
private var groupMenu: SubMenu? = null private var groupMenu: SubMenu? = null
private var replaceRuleFlowJob: Job? = null private var replaceRuleFlowJob: Job? = null
private var dataInit = false private var dataInit = false
@@ -224,7 +221,7 @@ class ReplaceRuleActivity : VMBaseActivity<ActivityReplaceRuleBinding, ReplaceRu
AppLog.put("替换规则管理界面更新数据出错", it) AppLog.put("替换规则管理界面更新数据出错", it)
}.flowOn(IO).conflate().collect { }.flowOn(IO).conflate().collect {
if (dataInit) { if (dataInit) {
setResult(Activity.RESULT_OK) setResult(RESULT_OK)
} }
adapter.setItems(it, adapter.diffItemCallBack) adapter.setItems(it, adapter.diffItemCallBack)
dataInit = true dataInit = true
@@ -20,8 +20,6 @@ import io.legato.kazusa.databinding.ActivityRssSourceBinding
import io.legato.kazusa.databinding.DialogEditTextBinding import io.legato.kazusa.databinding.DialogEditTextBinding
import io.legato.kazusa.help.DirectLinkUpload import io.legato.kazusa.help.DirectLinkUpload
import io.legato.kazusa.lib.dialogs.alert import io.legato.kazusa.lib.dialogs.alert
//import io.legado.app.lib.theme.primaryColor
//import io.legado.app.lib.theme.primaryTextColor
import io.legato.kazusa.ui.association.ImportRssSourceDialog import io.legato.kazusa.ui.association.ImportRssSourceDialog
import io.legato.kazusa.ui.file.HandleFileContract import io.legato.kazusa.ui.file.HandleFileContract
import io.legato.kazusa.ui.qrcode.QrCodeResult import io.legato.kazusa.ui.qrcode.QrCodeResult
@@ -70,7 +68,7 @@ class RssSourceActivity : VMBaseActivity<ActivityRssSourceBinding, RssSourceView
binding.titleBar.findViewById(R.id.search_view) binding.titleBar.findViewById(R.id.search_view)
} }
private var sourceFlowJob: Job? = null private var sourceFlowJob: Job? = null
private var groups = hashSetOf<String>() private var groups = arrayListOf<String>()
private var groupMenu: SubMenu? = null private var groupMenu: SubMenu? = null
private val qrCodeResult = registerForActivityResult(QrCodeResult()) { private val qrCodeResult = registerForActivityResult(QrCodeResult()) {
it ?: return@registerForActivityResult it ?: return@registerForActivityResult
@@ -35,6 +35,7 @@ import androidx.annotation.DrawableRes
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.core.content.edit import androidx.core.content.edit
import androidx.core.net.toUri
import androidx.datastore.preferences.preferencesDataStore import androidx.datastore.preferences.preferencesDataStore
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
@@ -44,6 +45,7 @@ import io.legato.kazusa.data.entities.Book
import io.legato.kazusa.help.IntentHelp import io.legato.kazusa.help.IntentHelp
import io.legato.kazusa.help.book.isAudio import io.legato.kazusa.help.book.isAudio
import io.legato.kazusa.help.book.isImage import io.legato.kazusa.help.book.isImage
import io.legato.kazusa.help.book.isLocal
import io.legato.kazusa.help.config.AppConfig import io.legato.kazusa.help.config.AppConfig
import io.legato.kazusa.ui.book.audio.AudioPlayActivity import io.legato.kazusa.ui.book.audio.AudioPlayActivity
import io.legato.kazusa.ui.book.manga.ReadMangaActivity import io.legato.kazusa.ui.book.manga.ReadMangaActivity
@@ -68,7 +70,7 @@ fun Context.startActivityForBook(
) { ) {
val cls = when { val cls = when {
book.isAudio -> AudioPlayActivity::class.java book.isAudio -> AudioPlayActivity::class.java
book.isImage && AppConfig.showMangaUi -> ReadMangaActivity::class.java !book.isLocal && book.isImage && AppConfig.showMangaUi -> ReadMangaActivity::class.java
else -> ReadBookActivity::class.java else -> ReadBookActivity::class.java
} }
val intent = Intent(this, cls) val intent = Intent(this, cls)
@@ -323,7 +325,7 @@ fun Context.getClipText(): String? {
fun Context.sendMail(mail: String) { fun Context.sendMail(mail: String) {
try { try {
val intent = Intent(Intent.ACTION_SENDTO) val intent = Intent(Intent.ACTION_SENDTO)
intent.data = Uri.parse("mailto:$mail") intent.data = "mailto:$mail".toUri()
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(intent) startActivity(intent)
} catch (e: Exception) { } catch (e: Exception) {
@@ -17,6 +17,7 @@ import io.legato.kazusa.R
import io.legato.kazusa.data.entities.Book import io.legato.kazusa.data.entities.Book
import io.legato.kazusa.help.book.isAudio import io.legato.kazusa.help.book.isAudio
import io.legato.kazusa.help.book.isImage import io.legato.kazusa.help.book.isImage
import io.legato.kazusa.help.book.isLocal
import io.legato.kazusa.help.config.AppConfig import io.legato.kazusa.help.config.AppConfig
import io.legato.kazusa.ui.book.audio.AudioPlayActivity import io.legato.kazusa.ui.book.audio.AudioPlayActivity
import io.legato.kazusa.ui.book.manga.ReadMangaActivity import io.legato.kazusa.ui.book.manga.ReadMangaActivity
@@ -93,7 +94,7 @@ fun Fragment.startActivityForBook(
) { ) {
val cls = when { val cls = when {
book.isAudio -> AudioPlayActivity::class.java book.isAudio -> AudioPlayActivity::class.java
book.isImage && AppConfig.showMangaUi -> ReadMangaActivity::class.java !book.isLocal && book.isImage && AppConfig.showMangaUi -> ReadMangaActivity::class.java
else -> ReadBookActivity::class.java else -> ReadBookActivity::class.java
} }
val intent = Intent(requireActivity(), cls) val intent = Intent(requireActivity(), cls)
@@ -14,7 +14,7 @@ object HtmlFormatter {
private val notImgHtmlRegex = "</?(?!img)[a-zA-Z]+(?=[ >])[^<>]*>".toRegex() private val notImgHtmlRegex = "</?(?!img)[a-zA-Z]+(?=[ >])[^<>]*>".toRegex()
private val otherHtmlRegex = "</?[a-zA-Z]+(?=[ >])[^<>]*>".toRegex() private val otherHtmlRegex = "</?[a-zA-Z]+(?=[ >])[^<>]*>".toRegex()
private val formatImagePattern = Pattern.compile( private val formatImagePattern = Pattern.compile(
"<img[^>]*\\ssrc\\s*=\\s*\"([^\"{>]*\\{(?:[^{}]|\\{[^}>]+\\})+\\})\"[^>]*>|<img[^>]*\\sdata-[^=>]*=\\s*\"([^\">]*)\"[^>]*>|<img[^>]*\\ssrc\\s*=\\s*\"([^\">]*)\"[^>]*>", "<img[^>]*\\ssrc\\s*=\\s*['\"]([^'\"{>]*\\{(?:[^{}]|\\{[^}>]+\\})+\\})['\"][^>]*>|<img[^>]*\\s(?:data-src|src)\\s*=\\s*['\"]([^'\">]+)['\"][^>]*>|<img[^>]*\\sdata-[^=>]*=\\s*['\"]([^'\">]*)['\"][^>]*>",
Pattern.CASE_INSENSITIVE Pattern.CASE_INSENSITIVE
) )
private val indent1Regex = "\\s*\\n+\\s*".toRegex() private val indent1Regex = "\\s*\\n+\\s*".toRegex()
@@ -1,7 +0,0 @@
package io.legato.kazusa.utils
import android.os.Build
val isVivoDevice by lazy {
Build.MANUFACTURER.equals("vivo", ignoreCase = true)
}
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M460,600L460,420L400,420L400,360L520,360L520,600L460,600ZM280,880L120,720L280,560L336,618L274,680L680,680L680,520L760,520L760,760L274,760L336,822L280,880ZM200,440L200,200L686,200L624,138L680,80L840,240L680,400L624,342L686,280L280,280L280,440L200,440Z" />
</vector>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M280,880L120,720L280,560L336,618L274,680L680,680L680,520L760,520L760,760L274,760L336,822L280,880ZM200,440L200,200L686,200L624,138L680,80L840,240L680,400L624,342L686,280L280,280L280,440L200,440Z" />
</vector>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M560,800L560,720L664,720L537,593L594,536L720,662L720,560L800,560L800,800L560,800ZM216,800L160,744L664,240L560,240L560,160L800,160L800,400L720,400L720,296L216,800ZM367,423L160,216L216,160L423,367L367,423Z" />
</vector>
@@ -125,6 +125,20 @@
android:paddingHorizontal="6dp" android:paddingHorizontal="6dp"
app:layout_constraintBottom_toBottomOf="parent"> app:layout_constraintBottom_toBottomOf="parent">
<io.legato.kazusa.ui.widget.image.ImageButton
android:id="@+id/iv_play_mode"
android:layout_width="46dp"
android:layout_height="46dp"
android:layout_marginStart="16dp"
android:background="@drawable/selector_circle_btn_bg"
android:contentDescription="@string/play_mode"
android:padding="5dp"
android:src="@drawable/ic_play_mode_list_end_stop"
app:layout_constraintBottom_toTopOf="@+id/tv_sub_title"
app:layout_constraintStart_toStartOf="parent"
app:tint="@color/md_white_1000"
tools:ignore="ImageContrastCheck" />
<io.legato.kazusa.ui.widget.image.ImageButton <io.legato.kazusa.ui.widget.image.ImageButton
android:id="@+id/iv_timer" android:id="@+id/iv_timer"
android:layout_width="46dp" android:layout_width="46dp"
+1 -1
View File
@@ -42,7 +42,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:stepSize="1.0" android:stepSize="1.0"
android:valueFrom="1.0" android:valueFrom="1.0"
android:valueTo="60.0" android:valueTo="120.0"
tools:ignore="UnusedAttribute" /> tools:ignore="UnusedAttribute" />
<LinearLayout <LinearLayout
@@ -7,11 +7,6 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"> android:orientation="vertical">
<View
android:id="@+id/vw_bg"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<FrameLayout <FrameLayout
android:id="@+id/vw_status_bar" android:id="@+id/vw_status_bar"
android:layout_width="match_parent" android:layout_width="match_parent"
+1 -1
View File
@@ -886,7 +886,7 @@
<string name="pref_cronet_summary">使用Cronet网络组件</string> <string name="pref_cronet_summary">使用Cronet网络组件</string>
<string name="anti_alias">Anti-Aliasing</string> <string name="anti_alias">Anti-Aliasing</string>
<string name="pref_anti_alias_summary">Anti-Aliasing when draw picture</string> <string name="pref_anti_alias_summary">Anti-Aliasing when draw picture</string>
<string name="upload_url">上传URL</string> <string name="upload_url">创建分享链接</string>
<string name="download_url_rule">downloadUrlRule(downloadUrls)</string> <string name="download_url_rule">downloadUrlRule(downloadUrls)</string>
<string name="sort_by_respondTime">Ordenar por tiempo de respuesta</string> <string name="sort_by_respondTime">Ordenar por tiempo de respuesta</string>
<string name="respondTime">tiempo de respuesta: %1$d ms</string> <string name="respondTime">tiempo de respuesta: %1$d ms</string>
+1 -1
View File
@@ -889,7 +889,7 @@
<string name="pref_cronet_summary">使用Cronet网络组件</string> <string name="pref_cronet_summary">使用Cronet网络组件</string>
<string name="anti_alias">Anti-Aliasing</string> <string name="anti_alias">Anti-Aliasing</string>
<string name="pref_anti_alias_summary">Anti-Aliasing when draw picture</string> <string name="pref_anti_alias_summary">Anti-Aliasing when draw picture</string>
<string name="upload_url">上传URL</string> <string name="upload_url">创建分享链接</string>
<string name="download_url_rule">downloadUrlRule(downloadUrls)</string> <string name="download_url_rule">downloadUrlRule(downloadUrls)</string>
<string name="sort_by_respondTime">Sort by respond time</string> <string name="sort_by_respondTime">Sort by respond time</string>
<string name="respondTime">respondTime: %1$d ms</string> <string name="respondTime">respondTime: %1$d ms</string>
+1 -1
View File
@@ -887,7 +887,7 @@
<string name="pref_cronet_summary">Usando componentes de rede Cronet</string> <string name="pref_cronet_summary">Usando componentes de rede Cronet</string>
<string name="anti_alias">Anti-Aliasing</string> <string name="anti_alias">Anti-Aliasing</string>
<string name="pref_anti_alias_summary">Anti-Aliasing when draw picture</string> <string name="pref_anti_alias_summary">Anti-Aliasing when draw picture</string>
<string name="upload_url">上传URL</string> <string name="upload_url">创建分享链接</string>
<string name="download_url_rule">downloadUrlRule(downloadUrls)</string> <string name="download_url_rule">downloadUrlRule(downloadUrls)</string>
<string name="sort_by_respondTime">Classificar por tempo de resposta</string> <string name="sort_by_respondTime">Classificar por tempo de resposta</string>
<string name="respondTime">tempo de resposta: %1$d ms</string> <string name="respondTime">tempo de resposta: %1$d ms</string>
+1 -1
View File
@@ -12,7 +12,7 @@
<item>@string/screen_portrait</item> <item>@string/screen_portrait</item>
<item>@string/screen_landscape</item> <item>@string/screen_landscape</item>
<item>@string/screen_sensor</item> <item>@string/screen_sensor</item>
<item>Đảo ngược màn hình dọc</item> <item>@string/screen_portrait_reversed</item>
</string-array> </string-array>
<string-array name="group_style"> <string-array name="group_style">
<item>Nhãn</item> <item>Nhãn</item>
@@ -1210,4 +1210,5 @@
<string name="manga_color_filter">滤镜</string> <string name="manga_color_filter">滤镜</string>
<string name="hide_manga_title">隐藏漫画列表标题</string> <string name="hide_manga_title">隐藏漫画列表标题</string>
<string name="refresh_explore">刷新发现</string> <string name="refresh_explore">刷新发现</string>
<string name="padding_display_cutouts">填充刘海区域</string>
</resources> </resources>
@@ -1212,4 +1212,5 @@
<string name="manga_color_filter">滤镜</string> <string name="manga_color_filter">滤镜</string>
<string name="hide_manga_title">隐藏漫画列表标题</string> <string name="hide_manga_title">隐藏漫画列表标题</string>
<string name="refresh_explore">刷新发现</string> <string name="refresh_explore">刷新发现</string>
<string name="padding_display_cutouts">填充刘海区域</string>
</resources> </resources>
+5 -2
View File
@@ -1234,9 +1234,12 @@
<string name="list">列表</string> <string name="list">列表</string>
<string name="grid">网格</string> <string name="grid">网格</string>
<string name="descending_order">降序</string> <string name="descending_order">降序</string>
<string name="checking_update">检查更新中...</string> <string name="checking_update">检查更新中…</string>
<string name="search_empty">没有内容哦</string> <string name="search_empty">没有内容哦</string>
<string name="firebase_enable_title">匿名统计</string> <string name="firebase_enable_title">匿名统计</string>
<string name="locate_current">定位至当前</string> <string name="locate_current">定位至当前</string>
<string name="padding_display_cutouts">填充刘海区域</string>
<string name="system_media_control_compatibility_change">系统媒体控件兼容性更改</string>
<string name="system_media_control_compatibility_change_summary">当锁屏不显示系统媒体控件时可以尝试开启,比如 OneUI 7.0或 OriginOS 等</string>
<string name="read_aloud_pause_resume">朗读暂停/继续</string>
</resources> </resources>
+5 -1
View File
@@ -1242,5 +1242,9 @@
<string name="firebase_enable_title">匿名统计</string> <string name="firebase_enable_title">匿名统计</string>
<string name="firebase_enable_summary">允许应用使用Google Firebase进行崩溃和性能统计</string> <string name="firebase_enable_summary">允许应用使用Google Firebase进行崩溃和性能统计</string>
<string name="locate_current">定位至当前</string> <string name="locate_current">定位至当前</string>
<string name="padding_display_cutouts">填充刘海区域</string>
<string name="system_media_control_compatibility_change">系统媒体控件兼容性更改</string>
<string name="system_media_control_compatibility_change_summary">当锁屏不显示系统媒体控件时可以尝试开启,比如 OneUI 7.0或 OriginOS 等</string>
<string name="read_aloud_pause_resume">朗读暂停/继续</string>
<string name="play_mode">播放模式</string>
</resources> </resources>
@@ -41,6 +41,13 @@
android:key="readAloudByPage" android:key="readAloudByPage"
app:iconSpaceReserved="false" /> app:iconSpaceReserved="false" />
<io.legato.kazusa.lib.prefs.SwitchPreference
android:defaultValue="false"
android:key="systemMediaControlCompatibilityChange"
android:summary="@string/system_media_control_compatibility_change_summary"
android:title="@string/system_media_control_compatibility_change"
app:iconSpaceReserved="false" />
<io.legato.kazusa.lib.prefs.SwitchPreference <io.legato.kazusa.lib.prefs.SwitchPreference
android:defaultValue="false" android:defaultValue="false"
android:title="@string/stream_read_aloud_audio" android:title="@string/stream_read_aloud_audio"
+9 -2
View File
@@ -35,6 +35,13 @@
app:iconSpaceReserved="false" app:iconSpaceReserved="false"
app:isBottomBackground="true" /> app:isBottomBackground="true" />
<io.legato.kazusa.lib.prefs.SwitchPreference
android:defaultValue="false"
android:key="hideNavigationBar"
android:title="@string/pt_hide_navigation_bar"
app:iconSpaceReserved="false"
app:isBottomBackground="true" />
<io.legato.kazusa.lib.prefs.SwitchPreference <io.legato.kazusa.lib.prefs.SwitchPreference
android:defaultValue="true" android:defaultValue="true"
android:key="readBodyToLh" android:key="readBodyToLh"
@@ -44,8 +51,8 @@
<io.legato.kazusa.lib.prefs.SwitchPreference <io.legato.kazusa.lib.prefs.SwitchPreference
android:defaultValue="false" android:defaultValue="false"
android:key="hideNavigationBar" android:key="paddingDisplayCutouts"
android:title="@string/pt_hide_navigation_bar" android:title="@string/padding_display_cutouts"
app:iconSpaceReserved="false" app:iconSpaceReserved="false"
app:isBottomBackground="true" /> app:isBottomBackground="true" />
+1 -1
View File
@@ -32,7 +32,7 @@ material = "1.14.0-alpha04"
media = "1.7.1" media = "1.7.1"
media3 = "1.8.0" media3 = "1.8.0"
nanoHttpd = "2.3.1" nanoHttpd = "2.3.1"
okhttp = "4.12.0" okhttp = "5.1.0"
palette = "1.0.0" palette = "1.0.0"
preference = "1.2.1" preference = "1.2.1"
protobufJavalite = "4.26.1" protobufJavalite = "4.26.1"