合并上游更改
感谢:@821938089、@zhongerxll、@youke2580c、@Leroy-X、@Dark-Tide、@lhjgege、@3harmony、@mgz0227
This commit is contained in:
@@ -7,6 +7,7 @@ import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import com.github.liuyueyi.quick.transfer.constants.TransType
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import com.jeremyliao.liveeventbus.LiveEventBus
|
||||
@@ -141,6 +142,9 @@ class App : Application() {
|
||||
* @return
|
||||
*/
|
||||
private fun installGmsTlsProvider(context: Context) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
val gmsPackageName = "com.google.android.gms"
|
||||
val appInfo = packageManager.getApplicationInfo(gmsPackageName, 0)
|
||||
|
||||
@@ -75,9 +75,9 @@ abstract class BaseService : LifecycleService() {
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
override fun onTimeout(startId: Int) {
|
||||
super.onTimeout(startId)
|
||||
LogUtils.d(simpleName, "onTimeout startId:$startId")
|
||||
override fun onTimeout(startId: Int, fgsType: Int) {
|
||||
super.onTimeout(startId, fgsType)
|
||||
LogUtils.d(simpleName, "onTimeout startId:$startId fgsType:$fgsType")
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,8 @@ object AppPattern {
|
||||
val EXP_PATTERN: Pattern = Pattern.compile("\\{\\{([\\w\\W]*?)\\}\\}")
|
||||
|
||||
//匹配格式化后的图片格式
|
||||
val imgPattern: Pattern = Pattern.compile("<img[^>]*src=\"([^\"]*(?:\"[^>]+\\})?)\"[^>]*>")
|
||||
val imgPattern: Pattern =
|
||||
Pattern.compile("<img[^>]*src=['\"]([^'\"]*(?:['\"][^>]+\\})?)['\"][^>]*>")
|
||||
|
||||
//dataURL图片类型
|
||||
val dataUriRegex = Regex("^data:.*?;base64,(.*)")
|
||||
|
||||
@@ -32,4 +32,5 @@ object EventBus {
|
||||
const val READ_ALOUD_PLAY = "readAloudPlay"
|
||||
const val EXPORT_BOOK = "exportBook"
|
||||
const val UP_MANGA_CONFIG = "upMangaConfig"
|
||||
const val PLAY_MODE_CHANGED = "playModeChanged"
|
||||
}
|
||||
@@ -170,14 +170,12 @@ object PreferKey {
|
||||
const val showMangaUi = "showMangaUi"
|
||||
const val disableMangaScale = "disableMangaScale"
|
||||
const val firebaseEnabled = "firebaseEnabled"
|
||||
|
||||
const val cPrimary = "colorPrimary"
|
||||
const val cAccent = "colorAccent"
|
||||
const val cBackground = "colorBackground"
|
||||
const val cBBackground = "colorBottomBackground"
|
||||
const val bgImage = "backgroundImage"
|
||||
const val bgImageBlurring = "backgroundImageBlurring"
|
||||
|
||||
const val cNPrimary = "colorPrimaryNight"
|
||||
const val cNAccent = "colorAccentNight"
|
||||
const val cNBackground = "colorBackgroundNight"
|
||||
@@ -187,7 +185,6 @@ object PreferKey {
|
||||
const val showReadTitleAddition = "showReadTitleAddition"
|
||||
const val readBarStyleFollowPage = "readBarStyleFollowPage"
|
||||
const val contentSelectSpeakMod = "contentReadAloudMod"
|
||||
|
||||
const val themePref = "themePref"
|
||||
const val bookshelfSortOrder = "bookshelfSortOrder"
|
||||
const val showBottomView = "showBottomView"
|
||||
@@ -208,5 +205,5 @@ object PreferKey {
|
||||
const val permissionChecked = "permissionChecked"
|
||||
const val notificationsPost = "notificationsPost"
|
||||
const val ignoreBatteryPermission = "ignoreBatteryPermission"
|
||||
|
||||
const val paddingDisplayCutouts = "paddingDisplayCutouts"
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import androidx.room.Delete
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import androidx.room.Update
|
||||
import io.legato.kazusa.constant.BookType
|
||||
import io.legato.kazusa.data.entities.Book
|
||||
@@ -150,6 +151,12 @@ interface BookDao {
|
||||
@Delete
|
||||
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")
|
||||
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.http.CookieStore
|
||||
import io.legato.kazusa.help.source.getShareScope
|
||||
import io.legato.kazusa.model.Debug
|
||||
import io.legato.kazusa.utils.GSON
|
||||
import io.legato.kazusa.utils.GSONStrict
|
||||
import io.legato.kazusa.utils.fromJsonArray
|
||||
@@ -116,7 +115,7 @@ interface BaseSource : JsExtensions {
|
||||
GSONStrict.fromJsonObject<Map<String, String>>(json).getOrNull()?.let { map ->
|
||||
putAll(map)
|
||||
} ?: GSON.fromJsonObject<Map<String, String>>(json).getOrNull()?.let { map ->
|
||||
Debug.log("≡请求头规则 JSON 格式不规范,请改为规范格式")
|
||||
log("请求头规则 JSON 格式不规范,请改为规范格式")
|
||||
putAll(map)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
||||
@@ -101,7 +101,7 @@ object DirectLinkUpload {
|
||||
|
||||
@Keep
|
||||
data class Rule(
|
||||
var uploadUrl: String, //上传url
|
||||
var uploadUrl: String, //创建分享链接
|
||||
var downloadUrlRule: String, //下载链接规则
|
||||
var summary: String, //注释
|
||||
var compress: Boolean = false, //是否压缩
|
||||
|
||||
@@ -988,6 +988,7 @@ interface JsExtensions : JsEncodeUtils {
|
||||
|
||||
// 新增 mimeType 参数,默认为 null(保持兼容性)
|
||||
fun openUrl(url: String, mimeType: String? = null) {
|
||||
require(url.length < 64 * 1024) { "openUrl parameter url too long" }
|
||||
rhinoContext.ensureActive()
|
||||
val source = getSource() ?: throw NoStackTraceException("openUrl source cannot be null")
|
||||
appCtx.startActivity<OpenUrlConfirmActivity> {
|
||||
|
||||
@@ -3,6 +3,7 @@ package io.legato.kazusa.help.book
|
||||
import android.graphics.BitmapFactory
|
||||
import android.os.ParcelFileDescriptor
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import com.script.rhino.runScriptWithContext
|
||||
import io.legato.kazusa.constant.AppLog
|
||||
import io.legato.kazusa.constant.AppPattern
|
||||
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.onEachParallel
|
||||
import io.legato.kazusa.utils.postEvent
|
||||
import com.script.rhino.runScriptWithContext
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.ensureActive
|
||||
@@ -157,7 +157,7 @@ object BookHelp {
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun saveContent(
|
||||
fun saveContent(
|
||||
bookSource: BookSource,
|
||||
book: Book,
|
||||
bookChapter: BookChapter,
|
||||
@@ -404,7 +404,11 @@ object BookHelp {
|
||||
bookChapter.getFileName()
|
||||
)
|
||||
if (file.exists()) {
|
||||
return file.readText()
|
||||
val string = file.readText()
|
||||
if (string.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
return string
|
||||
}
|
||||
if (book.isLocal) {
|
||||
val string = LocalBook.getContent(book, bookChapter)
|
||||
|
||||
@@ -570,6 +570,9 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
val mouseWheelPage
|
||||
get() = appCtx.getPrefBoolean(PreferKey.mouseWheelPage, true)
|
||||
|
||||
val paddingDisplayCutouts
|
||||
get() = appCtx.getPrefBoolean(PreferKey.paddingDisplayCutouts, false)
|
||||
|
||||
var searchScope: String
|
||||
get() = appCtx.getPrefString("searchScope") ?: ""
|
||||
set(value) {
|
||||
|
||||
@@ -5,8 +5,8 @@ import android.graphics.drawable.BitmapDrawable
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import androidx.annotation.Keep
|
||||
import androidx.core.graphics.toColorInt
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import io.legato.kazusa.R
|
||||
import io.legato.kazusa.constant.AppLog
|
||||
import io.legato.kazusa.constant.PageAnim
|
||||
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.putPrefInt
|
||||
import io.legato.kazusa.utils.resizeAndRecycle
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.withContext
|
||||
import splitties.init.appCtx
|
||||
import java.io.File
|
||||
|
||||
@@ -439,66 +437,55 @@ object ReadBookConfig {
|
||||
return exportConfig
|
||||
}
|
||||
|
||||
suspend fun import(byteArray: ByteArray): Result<Config> {
|
||||
return kotlin.runCatching {
|
||||
withContext(IO) {
|
||||
val configZipPath = FileUtils.getPath(appCtx.externalCache, "readConfig.zip")
|
||||
FileUtils.delete(configZipPath)
|
||||
val zipFile = FileUtils.createFileIfNotExist(configZipPath)
|
||||
zipFile.writeBytes(byteArray)
|
||||
val configDir = appCtx.externalCache.getFile("readConfig")
|
||||
configDir.createFolderReplace()
|
||||
ZipUtils.unZipToPath(zipFile, configDir)
|
||||
val configFile = configDir.getFile(configFileName)
|
||||
val config: Config = GSON.fromJsonObject<Config>(configFile.readText()).getOrThrow()
|
||||
if (config.textFont.isNotEmpty()) {
|
||||
val fontName = FileUtils.getName(config.textFont)
|
||||
val fontPath =
|
||||
FileUtils.getPath(appCtx.externalFiles, "font", fontName)
|
||||
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
|
||||
fun import(byteArray: ByteArray): Config {
|
||||
val configZipPath = FileUtils.getPath(appCtx.externalCache, "readConfig.zip")
|
||||
FileUtils.delete(configZipPath)
|
||||
val zipFile = FileUtils.createFileIfNotExist(configZipPath)
|
||||
zipFile.writeBytes(byteArray)
|
||||
val configDir = appCtx.externalCache.getFile("readConfig")
|
||||
configDir.createFolderReplace()
|
||||
ZipUtils.unZipToPath(zipFile, configDir)
|
||||
val configFile = configDir.getFile(configFileName)
|
||||
val config: Config = GSON.fromJsonObject<Config>(configFile.readText()).getOrThrow()
|
||||
if (config.textFont.isNotEmpty()) {
|
||||
val fontName = FileUtils.getName(config.textFont)
|
||||
val fontPath =
|
||||
FileUtils.getPath(appCtx.externalFiles, "font", fontName)
|
||||
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.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
|
||||
|
||||
@@ -2,19 +2,18 @@ package io.legato.kazusa.help.config
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import android.util.DisplayMetrics
|
||||
import androidx.annotation.Keep
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.graphics.toColorInt
|
||||
import io.legato.kazusa.R
|
||||
import io.legato.kazusa.constant.AppLog
|
||||
import io.legato.kazusa.constant.EventBus
|
||||
import io.legato.kazusa.constant.PreferKey
|
||||
import io.legato.kazusa.constant.Theme
|
||||
import io.legato.kazusa.help.DefaultData
|
||||
import io.legato.kazusa.lib.theme.ThemeStore
|
||||
import io.legato.kazusa.model.BookCover
|
||||
import io.legato.kazusa.utils.BitmapUtils
|
||||
import io.legato.kazusa.utils.ColorUtils
|
||||
import io.legato.kazusa.utils.FileUtils
|
||||
import io.legato.kazusa.utils.GSON
|
||||
import io.legato.kazusa.utils.fromJsonArray
|
||||
@@ -29,7 +28,6 @@ import io.legato.kazusa.utils.putPrefInt
|
||||
import io.legato.kazusa.utils.stackBlur
|
||||
import splitties.init.appCtx
|
||||
import java.io.File
|
||||
import androidx.core.graphics.toColorInt
|
||||
|
||||
@Keep
|
||||
object ThemeConfig {
|
||||
@@ -117,13 +115,18 @@ object ThemeConfig {
|
||||
fun addConfig(json: String): Boolean {
|
||||
GSON.fromJsonObject<Config>(json.trim { it < ' ' }).getOrNull()
|
||||
?.let {
|
||||
addConfig(it)
|
||||
return true
|
||||
if (validateConfig(it)) {
|
||||
addConfig(it)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun addConfig(newConfig: Config) {
|
||||
if (!validateConfig(newConfig)) {
|
||||
return
|
||||
}
|
||||
configList.forEachIndexed { index, config ->
|
||||
if (newConfig.themeName == config.themeName) {
|
||||
configList[index] = newConfig
|
||||
@@ -134,6 +137,18 @@ object ThemeConfig {
|
||||
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>? {
|
||||
val configFile = File(configFilePath)
|
||||
if (configFile.exists()) {
|
||||
@@ -148,23 +163,27 @@ object ThemeConfig {
|
||||
}
|
||||
|
||||
fun applyConfig(context: Context, config: Config) {
|
||||
val primary = config.primaryColor.toColorInt()
|
||||
val accent = config.accentColor.toColorInt()
|
||||
val background = config.backgroundColor.toColorInt()
|
||||
val bBackground = config.bottomBackground.toColorInt()
|
||||
if (config.isNightTheme) {
|
||||
context.putPrefInt(PreferKey.cNPrimary, primary)
|
||||
context.putPrefInt(PreferKey.cNAccent, accent)
|
||||
context.putPrefInt(PreferKey.cNBackground, background)
|
||||
context.putPrefInt(PreferKey.cNBBackground, bBackground)
|
||||
} else {
|
||||
context.putPrefInt(PreferKey.cPrimary, primary)
|
||||
context.putPrefInt(PreferKey.cAccent, accent)
|
||||
context.putPrefInt(PreferKey.cBackground, background)
|
||||
context.putPrefInt(PreferKey.cBBackground, bBackground)
|
||||
try {
|
||||
val primary = config.primaryColor.toColorInt()
|
||||
val accent = config.accentColor.toColorInt()
|
||||
val background = config.backgroundColor.toColorInt()
|
||||
val bBackground = config.bottomBackground.toColorInt()
|
||||
if (config.isNightTheme) {
|
||||
context.putPrefInt(PreferKey.cNPrimary, primary)
|
||||
context.putPrefInt(PreferKey.cNAccent, accent)
|
||||
context.putPrefInt(PreferKey.cNBackground, background)
|
||||
context.putPrefInt(PreferKey.cNBBackground, bBackground)
|
||||
} else {
|
||||
context.putPrefInt(PreferKey.cPrimary, primary)
|
||||
context.putPrefInt(PreferKey.cAccent, accent)
|
||||
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) {
|
||||
|
||||
@@ -12,7 +12,7 @@ import java.security.MessageDigest
|
||||
* @radius: 0..25
|
||||
*/
|
||||
class BlurTransformation(
|
||||
@IntRange(from = 0, to = 25) private val radius: Int
|
||||
@param:IntRange(from = 0, to = 25) private val radius: Int
|
||||
) : BitmapTransformation() {
|
||||
|
||||
override fun transform(
|
||||
|
||||
@@ -2,6 +2,7 @@ package io.legato.kazusa.help.http
|
||||
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import okhttp3.ResponseBody
|
||||
import okhttp3.ResponseBody.Companion.asResponseBody
|
||||
import okhttp3.internal.http.promisesBody
|
||||
import okio.buffer
|
||||
@@ -24,7 +25,7 @@ object DecompressInterceptor : Interceptor {
|
||||
val response = chain.proceed(requestBuilder.build())
|
||||
val body = response.body
|
||||
|
||||
if (!transparentDecompress || !response.promisesBody() || body == null) {
|
||||
if (!transparentDecompress || !response.promisesBody() || body == ResponseBody.EMPTY) {
|
||||
return response
|
||||
}
|
||||
|
||||
|
||||
@@ -127,7 +127,7 @@ val okHttpClientManga by lazy {
|
||||
val response = chain.proceed(request)
|
||||
val url = request.url.toString()
|
||||
response.newBuilder()
|
||||
.body(ProgressResponseBody(url, LISTENER, response.body!!))
|
||||
.body(ProgressResponseBody(url, LISTENER, response.body))
|
||||
.build()
|
||||
}
|
||||
interceptors.add(1) { chain ->
|
||||
|
||||
@@ -18,8 +18,6 @@ import okhttp3.Protocol
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody
|
||||
import okhttp3.Response
|
||||
import okhttp3.internal.notifyAll
|
||||
import okhttp3.internal.wait
|
||||
import okio.Buffer
|
||||
import okio.BufferedSink
|
||||
import okio.Pipe
|
||||
@@ -186,7 +184,7 @@ class ObsoleteUrlFactory(private var client: OkHttpClient) : URLStreamHandlerFac
|
||||
return try {
|
||||
val response = getResponse(true)
|
||||
if (hasBody(response) && response.code >= HTTP_BAD_REQUEST) {
|
||||
response.body!!.byteStream()
|
||||
response.body.byteStream()
|
||||
} else null
|
||||
} catch (e: IOException) {
|
||||
null
|
||||
@@ -258,7 +256,7 @@ class ObsoleteUrlFactory(private var client: OkHttpClient) : URLStreamHandlerFac
|
||||
}
|
||||
val response = getResponse(false)
|
||||
if (response.code >= HTTP_BAD_REQUEST) throw FileNotFoundException(url.toString())
|
||||
return response.body!!.byteStream()
|
||||
return response.body.byteStream()
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
@@ -1174,6 +1172,14 @@ class ObsoleteUrlFactory(private var client: OkHttpClient) : URLStreamHandlerFac
|
||||
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)
|
||||
@JvmStatic
|
||||
fun main(args: Array<String>) {
|
||||
|
||||
@@ -29,6 +29,7 @@ object SourceVerificationHelp {
|
||||
* 获取书源验证结果
|
||||
* 图片验证码 防爬 滑动验证码 点击字符 等等
|
||||
*/
|
||||
@Synchronized
|
||||
fun getVerificationResult(
|
||||
source: BaseSource?,
|
||||
url: String,
|
||||
@@ -38,9 +39,8 @@ object SourceVerificationHelp {
|
||||
): String {
|
||||
source
|
||||
?: throw NoStackTraceException("getVerificationResult parameter source cannot be null")
|
||||
if (isMainThread) {
|
||||
error("getVerificationResult must be called on a background thread")
|
||||
}
|
||||
require(url.length < 64 * 1024) { "getVerificationResult parameter url too long" }
|
||||
check(!isMainThread) { "getVerificationResult must be called on a background thread" }
|
||||
|
||||
clearResult(source.getKey())
|
||||
|
||||
@@ -84,6 +84,7 @@ object SourceVerificationHelp {
|
||||
refetchAfterSuccess: Boolean? = true
|
||||
) {
|
||||
source ?: throw NoStackTraceException("startBrowser parameter source cannot be null")
|
||||
require(url.length < 64 * 1024) { "startBrowser parameter url too long" }
|
||||
appCtx.startActivity<WebViewActivity> {
|
||||
putExtra("title", title)
|
||||
putExtra("url", url)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package io.legato.kazusa.help.storage
|
||||
|
||||
import android.content.Context
|
||||
import android.database.sqlite.SQLiteConstraintException
|
||||
import android.net.Uri
|
||||
import androidx.core.content.edit
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
@@ -106,7 +107,6 @@ object Restore {
|
||||
.forEach { book ->
|
||||
book.coverUrl = LocalBook.getCoverPath(book)
|
||||
}
|
||||
val updateBooks = arrayListOf<Book>()
|
||||
val newBooks = arrayListOf<Book>()
|
||||
val ignoreLocalBook = BackupConfig.ignoreLocalBook
|
||||
it.forEach { book ->
|
||||
@@ -114,12 +114,15 @@ object Restore {
|
||||
return@forEach
|
||||
}
|
||||
if (appDb.bookDao.has(book.bookUrl)) {
|
||||
updateBooks.add(book)
|
||||
try {
|
||||
appDb.bookDao.update(book)
|
||||
} catch (_: SQLiteConstraintException) {
|
||||
appDb.bookDao.insert(book)
|
||||
}
|
||||
} else {
|
||||
newBooks.add(book)
|
||||
}
|
||||
}
|
||||
appDb.bookDao.update(*updateBooks.toTypedArray())
|
||||
appDb.bookDao.insert(*newBooks.toTypedArray())
|
||||
}
|
||||
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.splitNotBlank
|
||||
import kotlinx.coroutines.delay
|
||||
import okhttp3.*
|
||||
import okhttp3.Call
|
||||
import okhttp3.Callback
|
||||
import okhttp3.EventListener
|
||||
import okhttp3.Headers
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
import okhttp3.Protocol
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.ResponseBody
|
||||
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.StatusLine
|
||||
import okio.Buffer
|
||||
import okio.Source
|
||||
import okio.Timeout
|
||||
@@ -25,7 +32,7 @@ import org.chromium.net.UrlResponseInfo
|
||||
import java.io.IOException
|
||||
import java.net.ProtocolException
|
||||
import java.nio.ByteBuffer
|
||||
import java.util.*
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.ArrayBlockingQueue
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
@@ -98,9 +105,15 @@ abstract class AbsCallBack(
|
||||
followCount += 1
|
||||
urlResponseInfoChain.add(info)
|
||||
val client = okHttpClient
|
||||
if (originalRequest.url.isHttps && newLocationUrl.startsWith("http://") && client.followSslRedirects) {
|
||||
if (originalRequest.url.isHttps
|
||||
&& newLocationUrl.startsWith("http://")
|
||||
&& client.followSslRedirects
|
||||
) {
|
||||
followRedirect = true
|
||||
} else if (!originalRequest.url.isHttps && newLocationUrl.startsWith("https://") && client.followSslRedirects) {
|
||||
} else if (!originalRequest.url.isHttps
|
||||
&& newLocationUrl.startsWith("https://")
|
||||
&& client.followSslRedirects
|
||||
) {
|
||||
followRedirect = true
|
||||
} else if (okHttpClient.followRedirects) {
|
||||
followRedirect = true
|
||||
@@ -315,7 +328,7 @@ abstract class AbsCallBack(
|
||||
contentLength,
|
||||
bodySource
|
||||
)
|
||||
}
|
||||
} ?: ResponseBody.EMPTY
|
||||
|
||||
return Response.Builder()
|
||||
.request(request)
|
||||
@@ -381,9 +394,12 @@ abstract class AbsCallBack(
|
||||
if (HttpMethod.permitsRequestBody(method)) {
|
||||
val responseCode = userResponse.code
|
||||
val maintainBody = HttpMethod.redirectsWithBody(method) ||
|
||||
responseCode == StatusLine.HTTP_PERM_REDIRECT ||
|
||||
responseCode == StatusLine.HTTP_TEMP_REDIRECT
|
||||
if (HttpMethod.redirectsToGet(method) && responseCode != StatusLine.HTTP_PERM_REDIRECT && responseCode != StatusLine.HTTP_TEMP_REDIRECT) {
|
||||
responseCode == HTTP_PERM_REDIRECT ||
|
||||
responseCode == HTTP_TEMP_REDIRECT
|
||||
if (HttpMethod.redirectsToGet(method)
|
||||
&& responseCode != HTTP_PERM_REDIRECT
|
||||
&& responseCode != HTTP_TEMP_REDIRECT
|
||||
) {
|
||||
requestBuilder.method("GET", null)
|
||||
} else {
|
||||
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.TagxHeader
|
||||
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.readUInt16Array
|
||||
import io.legato.kazusa.lib.mobi.utils.readUInt32
|
||||
import io.legato.kazusa.lib.mobi.utils.readUInt8
|
||||
import okhttp3.internal.and
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.InputStream
|
||||
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
|
||||
|
||||
import okhttp3.internal.and
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.charset.Charset
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import io.legato.kazusa.utils.toRequestBody
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.RequestBody.Companion.asRequestBody
|
||||
@@ -92,6 +93,7 @@ open class WebDav(
|
||||
.replace("+", "%20")
|
||||
.replace("%3A", ":")
|
||||
.replace("%2F", "/")
|
||||
raw.toHttpUrl().toString()
|
||||
}.getOrNull()
|
||||
}
|
||||
private val webDavClient by lazy {
|
||||
@@ -112,7 +114,14 @@ open class WebDav(
|
||||
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文件信息
|
||||
@@ -165,7 +174,7 @@ open class WebDav(
|
||||
method("PROPFIND", requestBody)
|
||||
}.apply {
|
||||
checkResult(this)
|
||||
}.body?.text()
|
||||
}.body.text()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -397,8 +406,8 @@ open class WebDav(
|
||||
url(url)
|
||||
}.apply {
|
||||
checkResult(this)
|
||||
}.body?.byteStream()
|
||||
return byteStream ?: throw WebDavException("WebDav下载出错\nNull Exception")
|
||||
}.body.byteStream()
|
||||
return byteStream
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -425,7 +434,7 @@ open class WebDav(
|
||||
*/
|
||||
private fun checkResult(response: Response) {
|
||||
if (!response.isSuccessful) {
|
||||
val body = response.body?.string()
|
||||
val body = response.body.string()
|
||||
if (response.code == 401) {
|
||||
val headers = response.headers("WWW-Authenticate")
|
||||
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}")
|
||||
}
|
||||
val document = Jsoup.parse(body)
|
||||
|
||||
@@ -31,6 +31,22 @@ import splitties.init.appCtx
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
@Suppress("unused")
|
||||
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
|
||||
private var activityContext: Context? = null
|
||||
private var serviceContext: Context? = null
|
||||
@@ -48,6 +64,11 @@ object AudioPlay : CoroutineScope by MainScope() {
|
||||
var bookSource: BookSource? = null
|
||||
val loadingChapters = arrayListOf<Int>()
|
||||
|
||||
fun changePlayMode() {
|
||||
playMode = playMode.next()
|
||||
postEvent(EventBus.PLAY_MODE_CHANGED, playMode)
|
||||
}
|
||||
|
||||
fun upData(book: Book) {
|
||||
AudioPlay.book = book
|
||||
chapterSize = appDb.bookChapterDao.getChapterCount(book.bookUrl)
|
||||
@@ -259,12 +280,32 @@ object AudioPlay : CoroutineScope by MainScope() {
|
||||
|
||||
fun next() {
|
||||
stopPlay()
|
||||
if (durChapterIndex + 1 < simulatedChapterSize) {
|
||||
durChapterIndex += 1
|
||||
durChapterPos = 0
|
||||
durPlayUrl = ""
|
||||
saveRead()
|
||||
loadPlayUrl()
|
||||
when (playMode) {
|
||||
PlayMode.LIST_END_STOP -> {
|
||||
if (durChapterIndex + 1 < simulatedChapterSize) {
|
||||
durChapterIndex += 1
|
||||
durChapterPos = 0
|
||||
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(
|
||||
context: Context,
|
||||
path: String?,
|
||||
@@ -155,8 +154,9 @@ object BookCover {
|
||||
.skipMemoryCache(true).let {
|
||||
if (transformation != null) {
|
||||
it.transform(transformation)
|
||||
} else {
|
||||
it
|
||||
}
|
||||
it
|
||||
}
|
||||
.transition(DrawableTransitionOptions.withCrossFade())
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import io.legato.kazusa.data.entities.BookSource
|
||||
import io.legato.kazusa.exception.ConcurrentException
|
||||
import io.legato.kazusa.help.book.BookHelp
|
||||
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.model.webBook.WebBook
|
||||
import io.legato.kazusa.service.CacheBookService
|
||||
@@ -96,15 +97,11 @@ object CacheBook {
|
||||
}
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
successDownloadSet.clear()
|
||||
errorDownloadMap.clear()
|
||||
}
|
||||
|
||||
fun close() {
|
||||
cacheBookMap.forEach { it.value.stop() }
|
||||
cacheBookMap.clear()
|
||||
clear()
|
||||
successDownloadSet.clear()
|
||||
errorDownloadMap.clear()
|
||||
}
|
||||
|
||||
val downloadSummary: String
|
||||
@@ -146,6 +143,7 @@ object CacheBook {
|
||||
|
||||
private val waitDownloadSet = linkedSetOf<Int>()
|
||||
private val onDownloadSet = linkedSetOf<Int>()
|
||||
private val tasks = CompositeCoroutine()
|
||||
private var isStopped = false
|
||||
private var waitingRetry = false
|
||||
|
||||
@@ -158,7 +156,7 @@ object CacheBook {
|
||||
|
||||
@Synchronized
|
||||
fun isRun(): Boolean {
|
||||
return waitDownloadSet.size > 0 || onDownloadSet.size > 0
|
||||
return waitDownloadSet.isNotEmpty() || onDownloadSet.isNotEmpty()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
@@ -169,6 +167,7 @@ object CacheBook {
|
||||
@Synchronized
|
||||
fun stop() {
|
||||
waitDownloadSet.clear()
|
||||
tasks.clear()
|
||||
isStopped = true
|
||||
postEvent(EventBus.UP_DOWNLOAD, book.bookUrl)
|
||||
}
|
||||
@@ -309,6 +308,8 @@ object CacheBook {
|
||||
onCancel(chapterIndex)
|
||||
}.onFinally {
|
||||
onFinally()
|
||||
}.apply {
|
||||
tasks.add(this)
|
||||
}.start()
|
||||
}
|
||||
|
||||
|
||||
@@ -63,20 +63,8 @@ object ReadBook : CoroutineScope by MainScope() {
|
||||
var isLocalBook = true
|
||||
var chapterChanged = false
|
||||
var prevTextChapter: TextChapter? = null
|
||||
set(value) {
|
||||
field?.cancelLayout()
|
||||
field = value
|
||||
}
|
||||
var curTextChapter: TextChapter? = null
|
||||
set(value) {
|
||||
field?.cancelLayout()
|
||||
field = value
|
||||
}
|
||||
var nextTextChapter: TextChapter? = null
|
||||
set(value) {
|
||||
field?.cancelLayout()
|
||||
field = value
|
||||
}
|
||||
var bookSource: BookSource? = null
|
||||
var msg: String? = null
|
||||
private val loadingChapters = arrayListOf<Int>()
|
||||
@@ -222,6 +210,9 @@ object ReadBook : CoroutineScope by MainScope() {
|
||||
}
|
||||
|
||||
fun clearTextChapter() {
|
||||
prevTextChapter?.cancelLayout()
|
||||
curTextChapter?.cancelLayout()
|
||||
nextTextChapter?.cancelLayout()
|
||||
prevTextChapter = null
|
||||
curTextChapter = null
|
||||
nextTextChapter = null
|
||||
@@ -333,6 +324,7 @@ object ReadBook : CoroutineScope by MainScope() {
|
||||
if (durChapterIndex < simulatedChapterSize - 1) {
|
||||
durChapterPos = 0
|
||||
durChapterIndex++
|
||||
prevTextChapter?.cancelLayout()
|
||||
prevTextChapter = curTextChapter
|
||||
curTextChapter = nextTextChapter
|
||||
nextTextChapter = null
|
||||
@@ -363,6 +355,7 @@ object ReadBook : CoroutineScope by MainScope() {
|
||||
if (durChapterIndex < simulatedChapterSize - 1) {
|
||||
durChapterPos = 0
|
||||
durChapterIndex++
|
||||
prevTextChapter?.cancelLayout()
|
||||
prevTextChapter = curTextChapter
|
||||
curTextChapter = nextTextChapter
|
||||
nextTextChapter = null
|
||||
@@ -394,6 +387,7 @@ object ReadBook : CoroutineScope by MainScope() {
|
||||
if (durChapterIndex > 0) {
|
||||
durChapterPos = if (toLast) prevTextChapter?.lastReadLength ?: Int.MAX_VALUE else 0
|
||||
durChapterIndex--
|
||||
nextTextChapter?.cancelLayout()
|
||||
nextTextChapter = curTextChapter
|
||||
curTextChapter = prevTextChapter
|
||||
prevTextChapter = null
|
||||
@@ -703,6 +697,7 @@ object ReadBook : CoroutineScope by MainScope() {
|
||||
)
|
||||
when (val offset = chapter.index - durChapterIndex) {
|
||||
0 -> {
|
||||
curTextChapter?.cancelLayout()
|
||||
curTextChapter = textChapter
|
||||
callBack?.upMenuView()
|
||||
var available = false
|
||||
@@ -727,12 +722,14 @@ object ReadBook : CoroutineScope by MainScope() {
|
||||
}
|
||||
|
||||
-1 -> {
|
||||
prevTextChapter?.cancelLayout()
|
||||
prevTextChapter = textChapter
|
||||
textChapter.layoutChannel.receiveAsFlow().collect()
|
||||
if (upContent) callBack?.upContent(offset, resetPageOffset)
|
||||
}
|
||||
|
||||
1 -> {
|
||||
nextTextChapter?.cancelLayout()
|
||||
nextTextChapter = textChapter
|
||||
for (page in textChapter.layoutChannel) {
|
||||
if (page.index > 1) {
|
||||
@@ -776,6 +773,7 @@ object ReadBook : CoroutineScope by MainScope() {
|
||||
)
|
||||
when (val offset = chapter.index - durChapterIndex) {
|
||||
0 -> {
|
||||
curTextChapter?.cancelLayout()
|
||||
curTextChapter = textChapter
|
||||
callBack?.upMenuView()
|
||||
var available = false
|
||||
@@ -800,12 +798,14 @@ object ReadBook : CoroutineScope by MainScope() {
|
||||
}
|
||||
|
||||
-1 -> {
|
||||
prevTextChapter?.cancelLayout()
|
||||
prevTextChapter = textChapter
|
||||
textChapter.layoutChannel.receiveAsFlow().collect()
|
||||
if (upContent) callBack?.upContent(offset, resetPageOffset)
|
||||
}
|
||||
|
||||
1 -> {
|
||||
nextTextChapter?.cancelLayout()
|
||||
nextTextChapter = textChapter
|
||||
for (page in textChapter.layoutChannel) {
|
||||
if (page.index > 1) {
|
||||
@@ -858,21 +858,25 @@ object ReadBook : CoroutineScope by MainScope() {
|
||||
|
||||
fun saveRead(pageChanged: Boolean = false) {
|
||||
executor.execute {
|
||||
val book = book ?: return@execute
|
||||
book.lastCheckCount = 0
|
||||
book.durChapterTime = System.currentTimeMillis()
|
||||
val chapterChanged = book.durChapterIndex != durChapterIndex
|
||||
book.durChapterIndex = durChapterIndex
|
||||
book.durChapterPos = durChapterPos
|
||||
if (!pageChanged || chapterChanged) {
|
||||
appDb.bookChapterDao.getChapter(book.bookUrl, durChapterIndex)?.let {
|
||||
book.durChapterTitle = it.getDisplayTitle(
|
||||
ContentProcessor.get(book.name, book.origin).getTitleReplaceRules(),
|
||||
book.getUseReplaceRule()
|
||||
)
|
||||
kotlin.runCatching {
|
||||
val book = book ?: return@execute
|
||||
book.lastCheckCount = 0
|
||||
book.durChapterTime = System.currentTimeMillis()
|
||||
val chapterChanged = book.durChapterIndex != durChapterIndex
|
||||
book.durChapterIndex = durChapterIndex
|
||||
book.durChapterPos = durChapterPos
|
||||
if (!pageChanged || chapterChanged) {
|
||||
appDb.bookChapterDao.getChapter(book.bookUrl, durChapterIndex)?.let {
|
||||
book.durChapterTitle = it.getDisplayTitle(
|
||||
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) {
|
||||
executor.execute {
|
||||
val book = book ?: return@execute
|
||||
book.lastCheckCount = 0
|
||||
book.durChapterTime = System.currentTimeMillis()
|
||||
val chapterChanged = book.durChapterIndex != durChapterIndex
|
||||
book.durChapterIndex = durChapterIndex
|
||||
book.durChapterPos = durChapterPos
|
||||
if (!pageChanged || chapterChanged) {
|
||||
appDb.bookChapterDao.getChapter(book.bookUrl, durChapterIndex)?.let {
|
||||
book.durChapterTitle = it.getDisplayTitle(
|
||||
ContentProcessor.get(book.name, book.origin).getTitleReplaceRules(),
|
||||
book.getUseReplaceRule()
|
||||
)
|
||||
kotlin.runCatching {
|
||||
val book = book ?: return@execute
|
||||
book.lastCheckCount = 0
|
||||
book.durChapterTime = System.currentTimeMillis()
|
||||
val chapterChanged = book.durChapterIndex != durChapterIndex
|
||||
book.durChapterIndex = durChapterIndex
|
||||
book.durChapterPos = durChapterPos
|
||||
if (!pageChanged || chapterChanged) {
|
||||
appDb.bookChapterDao.getChapter(book.bookUrl, durChapterIndex)?.let {
|
||||
book.durChapterTitle = it.getDisplayTitle(
|
||||
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.postMultipart
|
||||
import io.legato.kazusa.help.source.getShareScope
|
||||
import io.legato.kazusa.model.Debug
|
||||
import io.legato.kazusa.utils.EncoderUtils
|
||||
import io.legato.kazusa.utils.GSON
|
||||
import io.legato.kazusa.utils.GSONStrict
|
||||
@@ -225,32 +224,32 @@ class AnalyzeUrl(
|
||||
if (urlOption == null) {
|
||||
urlOption = GSON.fromJsonObject<UrlOption>(urlOptionStr).getOrNull()
|
||||
if (urlOption != null) {
|
||||
Debug.log("≡链接参数 JSON 格式不规范,请改为规范格式")
|
||||
log("链接参数 JSON 格式不规范,请改为规范格式")
|
||||
}
|
||||
}
|
||||
urlOption?.let { option ->
|
||||
option.getMethod()?.let {
|
||||
if (it.equals("POST", true)) method = RequestMethod.POST
|
||||
}
|
||||
option.getHeaderMap()?.forEach { entry ->
|
||||
headerMap[entry.key.toString()] = entry.value.toString()
|
||||
}
|
||||
option.getBody()?.let {
|
||||
body = it
|
||||
}
|
||||
type = option.getType()
|
||||
charset = option.getCharset()
|
||||
retry = option.getRetry()
|
||||
useWebView = option.useWebView()
|
||||
webJs = option.getWebJs()
|
||||
option.getJs()?.let { jsStr ->
|
||||
evalJS(jsStr, url)?.toString()?.let {
|
||||
url = it
|
||||
}
|
||||
option.getMethod()?.let {
|
||||
if (it.equals("POST", true)) method = RequestMethod.POST
|
||||
}
|
||||
option.getHeaderMap()?.forEach { entry ->
|
||||
headerMap[entry.key.toString()] = entry.value.toString()
|
||||
}
|
||||
option.getBody()?.let {
|
||||
body = it
|
||||
}
|
||||
type = option.getType()
|
||||
charset = option.getCharset()
|
||||
retry = option.getRetry()
|
||||
useWebView = option.useWebView()
|
||||
webJs = option.getWebJs()
|
||||
option.getJs()?.let { jsStr ->
|
||||
evalJS(jsStr, url)?.toString()?.let {
|
||||
url = it
|
||||
}
|
||||
serverID = option.getServerID()
|
||||
webViewDelayTime = max(0, option.getWebViewDelayTime() ?: 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
urlNoQuery = url
|
||||
when (method) {
|
||||
@@ -458,7 +457,7 @@ class AnalyzeUrl(
|
||||
else -> get(urlNoQuery, encodedQuery)
|
||||
}
|
||||
}.let {
|
||||
val isXml = it.raw.body?.contentType()?.toString()
|
||||
val isXml = it.raw.body.contentType()?.toString()
|
||||
?.matches(AppPattern.xmlContentTypeRegex) == true
|
||||
if (isXml && it.body?.trim()?.startsWith("<?xml", true) == false) {
|
||||
StrResponse(it.raw, "<?xml version=\"1.0\"?>" + it.body)
|
||||
@@ -553,7 +552,7 @@ class AnalyzeUrl(
|
||||
getByteArrayIfDataUri()?.let {
|
||||
return it
|
||||
}
|
||||
return getResponseAwait().body!!.bytes()
|
||||
return getResponseAwait().body.bytes()
|
||||
}
|
||||
|
||||
fun getByteArray(): ByteArray {
|
||||
@@ -569,7 +568,7 @@ class AnalyzeUrl(
|
||||
getByteArrayIfDataUri()?.let {
|
||||
return ByteArrayInputStream(it)
|
||||
}
|
||||
return getResponseAwait().body!!.byteStream()
|
||||
return getResponseAwait().body.byteStream()
|
||||
}
|
||||
|
||||
fun getInputStream(): InputStream {
|
||||
|
||||
@@ -2,6 +2,7 @@ package io.legato.kazusa.model.localBook
|
||||
|
||||
import android.net.Uri
|
||||
import android.util.Base64
|
||||
import androidx.core.net.toUri
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import com.script.ScriptBindings
|
||||
import com.script.rhino.RhinoScriptEngine
|
||||
@@ -103,7 +104,7 @@ object LocalBook {
|
||||
|
||||
fun getLastModified(book: Book): Result<Long> {
|
||||
return kotlin.runCatching {
|
||||
val uri = Uri.parse(book.bookUrl)
|
||||
val uri = book.bookUrl.toUri()
|
||||
if (uri.isContentScheme()) {
|
||||
return@runCatching DocumentFile.fromSingleUri(appCtx, uri)!!.lastModified()
|
||||
}
|
||||
@@ -144,6 +145,9 @@ object LocalBook {
|
||||
val list = ArrayList(LinkedHashSet(chapters))
|
||||
list.forEachIndexed { index, bookChapter ->
|
||||
bookChapter.index = index
|
||||
if (bookChapter.title.isEmpty()) {
|
||||
bookChapter.title = "无标题章节"
|
||||
}
|
||||
}
|
||||
val replaceRules = ContentProcessor.get(book).getTitleReplaceRules()
|
||||
book.durChapterTitle = list.getOrElse(book.durChapterIndex) { list.last() }
|
||||
@@ -191,6 +195,9 @@ object LocalBook {
|
||||
return StringEscapeUtils.unescapeHtml4(content)
|
||||
}
|
||||
}
|
||||
if (content.isNullOrEmpty()) {
|
||||
return null
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
||||
@@ -374,7 +381,7 @@ object LocalBook {
|
||||
}
|
||||
if (deleteOriginal) {
|
||||
if (book.bookUrl.isContentScheme()) {
|
||||
val uri = Uri.parse(book.bookUrl)
|
||||
val uri = book.bookUrl.toUri()
|
||||
DocumentFile.fromSingleUri(appCtx, uri)?.delete()
|
||||
} else {
|
||||
FileUtils.delete(book.bookUrl)
|
||||
@@ -419,7 +426,7 @@ object LocalBook {
|
||||
inputStream.use {
|
||||
val defaultBookTreeUri = AppConfig.defaultBookTreeUri
|
||||
if (defaultBookTreeUri.isNullOrBlank()) throw NoBooksDirException()
|
||||
val treeUri = Uri.parse(defaultBookTreeUri)
|
||||
val treeUri = defaultBookTreeUri.toUri()
|
||||
return if (treeUri.isContentScheme()) {
|
||||
val treeDoc = DocumentFile.fromTreeUri(appCtx, treeUri)
|
||||
var doc = treeDoc!!.findFile(fileName)
|
||||
|
||||
@@ -229,7 +229,7 @@ class MobiFile(var book: Book) {
|
||||
break
|
||||
}
|
||||
section = section.next ?: break
|
||||
if (section.linear) {
|
||||
if (!section.linear) {
|
||||
continue
|
||||
}
|
||||
if (section.href == nextSectionHref) {
|
||||
|
||||
@@ -215,6 +215,7 @@ class TextFile(private var book: Book) {
|
||||
val curChapter = BookChapter()
|
||||
curChapter.title = matcher.group()
|
||||
curChapter.start = curOffset + chapterLength
|
||||
curChapter.end = curChapter.start
|
||||
toc.add(curChapter)
|
||||
lastChapterWordCount = 0
|
||||
} else if (seekPos == 0 && chapterStart != 0) {
|
||||
@@ -242,6 +243,7 @@ class TextFile(private var book: Book) {
|
||||
val curChapter = BookChapter()
|
||||
curChapter.title = matcher.group()
|
||||
curChapter.start = curOffset + chapterLength
|
||||
curChapter.end = curChapter.start
|
||||
toc.add(curChapter)
|
||||
} else { //否则就block分割之后,上一个章节的剩余内容
|
||||
//获取上一章节
|
||||
@@ -257,6 +259,7 @@ class TextFile(private var book: Book) {
|
||||
val curChapter = BookChapter()
|
||||
curChapter.title = matcher.group()
|
||||
curChapter.start = lastChapter.end
|
||||
curChapter.end = curChapter.start
|
||||
toc.add(curChapter)
|
||||
}
|
||||
bookWordCount += chapterContent.length
|
||||
@@ -275,6 +278,7 @@ class TextFile(private var book: Book) {
|
||||
val curChapter = BookChapter()
|
||||
curChapter.title = matcher.group()
|
||||
curChapter.start = lastChapter.end
|
||||
curChapter.end = curChapter.start
|
||||
toc.add(curChapter)
|
||||
} else { //如果章节不存在则创建章节
|
||||
val curChapter = BookChapter()
|
||||
|
||||
@@ -17,8 +17,10 @@ import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.asCoroutineDispatcher
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.onCompletion
|
||||
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 searchBooks = arrayListOf<SearchBook>()
|
||||
private var searchJob: Job? = null
|
||||
|
||||
private var workingState = MutableStateFlow(true)
|
||||
|
||||
private fun initSearchPool() {
|
||||
searchPool?.close()
|
||||
@@ -82,6 +84,7 @@ class SearchModel(private val scope: CoroutineScope, private val callBack: CallB
|
||||
bs.getBookSource()?.let {
|
||||
emit(it)
|
||||
}
|
||||
workingState.first { it }
|
||||
}
|
||||
}.onStart {
|
||||
callBack.onSearchStart()
|
||||
@@ -191,6 +194,14 @@ class SearchModel(private val scope: CoroutineScope, private val callBack: CallB
|
||||
mSearchId = 0L
|
||||
}
|
||||
|
||||
fun pause() {
|
||||
workingState.value = false
|
||||
}
|
||||
|
||||
fun resume() {
|
||||
workingState.value = true
|
||||
}
|
||||
|
||||
interface CallBack {
|
||||
fun getSearchScope(): SearchScope
|
||||
fun onSearchStart()
|
||||
|
||||
@@ -27,6 +27,7 @@ import io.legato.kazusa.R
|
||||
import io.legato.kazusa.base.BaseService
|
||||
import io.legato.kazusa.constant.AppConst
|
||||
import io.legato.kazusa.constant.AppLog
|
||||
import io.legato.kazusa.constant.AppPattern
|
||||
import io.legato.kazusa.constant.EventBus
|
||||
import io.legato.kazusa.constant.IntentAction
|
||||
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.utils.LogUtils
|
||||
import io.legato.kazusa.utils.activityPendingIntent
|
||||
import io.legato.kazusa.utils.broadcastPendingIntent
|
||||
import io.legato.kazusa.utils.getPrefBoolean
|
||||
import io.legato.kazusa.utils.isVivoDevice
|
||||
import io.legato.kazusa.utils.observeEvent
|
||||
import io.legato.kazusa.utils.observeSharedPreferences
|
||||
import io.legato.kazusa.utils.postEvent
|
||||
@@ -326,9 +325,11 @@ abstract class BaseReadAloudService : BaseService(),
|
||||
private fun prevP() {
|
||||
if (nowSpeak > 0) {
|
||||
playStop()
|
||||
nowSpeak--
|
||||
readAloudNumber -= contentList[nowSpeak].length + 1 + paragraphStartPos
|
||||
paragraphStartPos = 0
|
||||
do {
|
||||
nowSpeak--
|
||||
readAloudNumber -= contentList[nowSpeak].length + 1 + paragraphStartPos
|
||||
paragraphStartPos = 0
|
||||
} while (contentList[nowSpeak].matches(AppPattern.notReadAloudRegex))
|
||||
textChapter?.let {
|
||||
if (readAloudByPage) {
|
||||
val paragraphs = it.getParagraphs(true)
|
||||
@@ -358,7 +359,9 @@ abstract class BaseReadAloudService : BaseService(),
|
||||
val paragraphs = it.getParagraphs(true)
|
||||
if (!paragraphs[nowSpeak].isParagraphEnd) readAloudNumber--
|
||||
}
|
||||
if (readAloudNumber >= it.getReadLength(pageIndex + 1)) {
|
||||
if (pageIndex + 1 < it.pageSize
|
||||
&& readAloudNumber >= it.getReadLength(pageIndex + 1)
|
||||
) {
|
||||
pageIndex++
|
||||
ReadBook.moveToNextPage()
|
||||
}
|
||||
@@ -443,24 +446,75 @@ abstract class BaseReadAloudService : BaseService(),
|
||||
PlaybackStateCompat.Builder()
|
||||
.setActions(MediaHelp.MEDIA_SESSION_ACTIONS)
|
||||
.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()
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化MediaSession, 注册多媒体按钮
|
||||
*/
|
||||
/**
|
||||
* 初始化MediaSession, 注册多媒体按钮
|
||||
*/
|
||||
@SuppressLint("UnspecifiedImmutableFlag")
|
||||
private fun initMediaSession() {
|
||||
mediaSessionCompat.setCallback(object : MediaSessionCompat.Callback() {
|
||||
override fun onMediaButtonEvent(mediaButtonEvent: Intent): Boolean {
|
||||
return MediaButtonReceiver.handleIntent(this@BaseReadAloudService, mediaButtonEvent)
|
||||
}
|
||||
})
|
||||
mediaSessionCompat.setMediaButtonReceiver(
|
||||
broadcastPendingIntent<MediaButtonReceiver>(Intent.ACTION_MEDIA_BUTTON)
|
||||
)
|
||||
mediaSessionCompat.isActive = true
|
||||
if (getPrefBoolean("systemMediaControlCompatibilityChange")) {
|
||||
mediaSessionCompat.setCallback(object : MediaSessionCompat.Callback() {
|
||||
override fun onPlay() {
|
||||
resumeReadAloud()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
pauseReadAloud()
|
||||
}
|
||||
|
||||
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 {
|
||||
val mediaStyle = androidx.media.app.NotificationCompat.MediaStyle()
|
||||
.setShowActionsInCompactView(1, 2, 4)
|
||||
if (isVivoDevice) {
|
||||
if (getPrefBoolean("systemMediaControlCompatibilityChange")) {
|
||||
//fix #4090 android 14 can not show play control in lock screen
|
||||
mediaStyle.setMediaSession(mediaSessionCompat.sessionToken)
|
||||
}
|
||||
|
||||
@@ -65,7 +65,6 @@ class CacheBookService : BaseService() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
isRun = true
|
||||
CacheBook.clear()
|
||||
lifecycleScope.launch {
|
||||
while (isActive) {
|
||||
delay(1000)
|
||||
|
||||
@@ -3,6 +3,7 @@ package io.legato.kazusa.service
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.PendingIntent
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.MediaItem
|
||||
@@ -306,7 +307,7 @@ class HttpReadAloudService : BaseReadAloudService(),
|
||||
}
|
||||
|
||||
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()
|
||||
return DefaultDownloaderFactory(factory, okHttpClient.dispatcher.executorService)
|
||||
.createDownloader(request)
|
||||
@@ -343,17 +344,17 @@ class HttpReadAloudService : BaseReadAloudService(),
|
||||
val contentType = contentType.substringBefore(";")
|
||||
val ct = httpTts.contentType
|
||||
if (contentType == "application/json" || contentType.startsWith("text/")) {
|
||||
throw NoStackTraceException(response.body!!.string())
|
||||
throw NoStackTraceException(response.body.string())
|
||||
} else if (ct?.isNotBlank() == true) {
|
||||
if (!contentType.matches(ct.toRegex())) {
|
||||
throw NoStackTraceException(
|
||||
"TTS服务器返回错误:" + response.body!!.string()
|
||||
"TTS服务器返回错误:" + response.body.string()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
coroutineContext.ensureActive()
|
||||
response.body!!.byteStream().let { stream ->
|
||||
response.body.byteStream().let { stream ->
|
||||
downloadErrorNo = 0
|
||||
return stream
|
||||
}
|
||||
@@ -477,12 +478,12 @@ class HttpReadAloudService : BaseReadAloudService(),
|
||||
val sleep = exoPlayer.duration / speakTextLength
|
||||
val start = speakTextLength * exoPlayer.currentPosition / exoPlayer.duration
|
||||
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++
|
||||
if (pageIndex < textChapter.pageSize) {
|
||||
ReadBook.moveToNextPage()
|
||||
upTtsProgress(readAloudNumber + i.toInt())
|
||||
}
|
||||
ReadBook.moveToNextPage()
|
||||
upTtsProgress(readAloudNumber + i.toInt())
|
||||
}
|
||||
delay(sleep)
|
||||
}
|
||||
|
||||
@@ -195,7 +195,9 @@ class TTSReadAloudService : BaseReadAloudService(), TextToSpeech.OnInitListener
|
||||
if (contentList[nowSpeak].matches(AppPattern.notReadAloudRegex)) {
|
||||
nextParagraph()
|
||||
}
|
||||
if (readAloudNumber + 1 > it.getReadLength(pageIndex + 1)) {
|
||||
if (pageIndex + 1 < it.pageSize
|
||||
&& readAloudNumber + 1 > it.getReadLength(pageIndex + 1)
|
||||
) {
|
||||
pageIndex++
|
||||
ReadBook.moveToNextPage()
|
||||
}
|
||||
@@ -214,7 +216,9 @@ class TTSReadAloudService : BaseReadAloudService(), TextToSpeech.OnInitListener
|
||||
"onRangeStart nowSpeak:$nowSpeak pageIndex:$pageIndex utteranceId:$utteranceId start:$start end:$end frame:$frame"
|
||||
LogUtils.d(TAG, msg)
|
||||
textChapter?.let {
|
||||
if (readAloudNumber + start > it.getReadLength(pageIndex + 1)) {
|
||||
if (pageIndex + 1 < it.pageSize
|
||||
&& readAloudNumber + start > it.getReadLength(pageIndex + 1)
|
||||
) {
|
||||
pageIndex++
|
||||
ReadBook.moveToNextPage()
|
||||
upTtsProgress(readAloudNumber + start)
|
||||
|
||||
@@ -56,7 +56,7 @@ class OnLineImportViewModel(app: Application) : BaseAssociationViewModel(app) {
|
||||
|
||||
fun importReadConfig(bytes: ByteArray, finally: (title: String, msg: String) -> Unit) {
|
||||
execute {
|
||||
val config = ReadBookConfig.import(bytes).getOrThrow()
|
||||
val config = ReadBookConfig.import(bytes)
|
||||
ReadBookConfig.configList.forEachIndexed { index, c ->
|
||||
if (c.name == config.name) {
|
||||
ReadBookConfig.configList[index] = config
|
||||
|
||||
@@ -64,6 +64,7 @@ class AudioPlayActivity :
|
||||
override val viewModel by viewModels<AudioPlayViewModel>()
|
||||
private val timerSliderPopup by lazy { TimerSliderPopup(this) }
|
||||
private var adjustProgress = false
|
||||
private var playMode = AudioPlay.PlayMode.LIST_END_STOP
|
||||
|
||||
private val progressTimeFormat by lazy {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
@@ -140,6 +141,14 @@ class AudioPlayActivity :
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
binding.ivPlayMode.setOnClickListener {
|
||||
AudioPlay.changePlayMode()
|
||||
}
|
||||
|
||||
observeEventSticky<AudioPlay.PlayMode>(EventBus.PLAY_MODE_CHANGED) {
|
||||
playMode = it
|
||||
updatePlayModeIcon()
|
||||
}
|
||||
binding.fabPlayStop.setOnClickListener {
|
||||
playButton()
|
||||
}
|
||||
@@ -207,6 +216,10 @@ class AudioPlayActivity :
|
||||
binding.llPlayMenu.applyNavigationBarPadding()
|
||||
}
|
||||
|
||||
private fun updatePlayModeIcon() {
|
||||
binding.ivPlayMode.setImageResource(playMode.iconRes)
|
||||
}
|
||||
|
||||
private fun upCover(path: String?) {
|
||||
BookCover.load(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 {
|
||||
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
|
||||
inBookshelf = intent.getBooleanExtra("inBookshelf", true)
|
||||
initBook(book)
|
||||
@@ -71,7 +71,7 @@ class AudioPlayViewModel(application: Application) : BaseViewModel(application)
|
||||
if (oldBook.bookUrl == book.bookUrl) {
|
||||
appDb.bookDao.update(book)
|
||||
} else {
|
||||
appDb.bookDao.insert(book)
|
||||
appDb.bookDao.replace(oldBook, book)
|
||||
}
|
||||
appDb.bookChapterDao.delByBook(book.bookUrl)
|
||||
appDb.bookChapterDao.insert(*cList.toTypedArray())
|
||||
|
||||
@@ -739,7 +739,7 @@ class BookInfoActivity :
|
||||
else -> readBookResult.launch(
|
||||
Intent(
|
||||
this,
|
||||
if (book.isImage && AppConfig.showMangaUi) ReadMangaActivity::class.java
|
||||
if (!book.isLocal && book.isImage && AppConfig.showMangaUi) ReadMangaActivity::class.java
|
||||
else ReadBookActivity::class.java
|
||||
)
|
||||
.putExtra("bookUrl", book.bookUrl)
|
||||
|
||||
@@ -235,7 +235,7 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
|
||||
WebBook.getChapterList(scope, bookSource, book, runPreUpdateJs)
|
||||
.onSuccess(IO) {
|
||||
if (inBookshelf) {
|
||||
book.save()
|
||||
appDb.bookDao.replace(oldBook, book)
|
||||
/**
|
||||
* runPreUpdateJs 有可能会修改 book 的 bookUrl
|
||||
*/
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package io.legato.kazusa.ui.book.info.edit
|
||||
|
||||
import android.app.Application
|
||||
import android.database.sqlite.SQLiteConstraintException
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import io.legato.kazusa.base.BaseViewModel
|
||||
import io.legato.kazusa.constant.AppLog
|
||||
import io.legato.kazusa.data.appDb
|
||||
import io.legato.kazusa.data.entities.Book
|
||||
import io.legato.kazusa.model.ReadBook
|
||||
@@ -28,6 +30,12 @@ class BookInfoEditViewModel(application: Application) : BaseViewModel(applicatio
|
||||
appDb.bookDao.update(book)
|
||||
}.onSuccess {
|
||||
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) {
|
||||
appDb.bookDao.update(book)
|
||||
} else {
|
||||
appDb.bookDao.insert(book)
|
||||
appDb.bookDao.replace(oldBook, book)
|
||||
BookHelp.updateCacheFolder(oldBook, book)
|
||||
}
|
||||
appDb.bookChapterDao.delByBook(oldBook.bookUrl)
|
||||
|
||||
@@ -5,7 +5,6 @@ import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Color
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.Gravity
|
||||
import android.view.HapticFeedbackConstants
|
||||
@@ -19,6 +18,7 @@ import androidx.activity.addCallback
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.view.doOnPreDraw
|
||||
import androidx.core.view.get
|
||||
import androidx.core.view.size
|
||||
@@ -906,10 +906,11 @@ class ReadBookActivity : BaseReadBookActivity(),
|
||||
ReadBook.bookSource?.bookSourceUrl?.let {
|
||||
scopes.add(it)
|
||||
}
|
||||
val text = selectedText.lineSequence().map { it.trim() }.joinToString("\n")
|
||||
replaceActivity.launch(
|
||||
ReplaceEditActivity.startIntent(
|
||||
this,
|
||||
pattern = selectedText,
|
||||
pattern = text,
|
||||
scope = scopes.joinToString(";")
|
||||
)
|
||||
)
|
||||
@@ -1421,7 +1422,7 @@ class ReadBookActivity : BaseReadBookActivity(),
|
||||
value = src
|
||||
}
|
||||
} 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) {
|
||||
appDb.bookDao.update(book)
|
||||
} else {
|
||||
appDb.bookDao.insert(book)
|
||||
appDb.bookDao.replace(oldBook, book)
|
||||
BookHelp.updateCacheFolder(oldBook, book)
|
||||
}
|
||||
appDb.bookChapterDao.delByBook(oldBook.bookUrl)
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
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.content.DialogInterface
|
||||
import android.graphics.Color
|
||||
import android.graphics.PorterDuff
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.TooltipCompat
|
||||
import androidx.core.graphics.toColorInt
|
||||
import androidx.core.view.isGone
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import com.google.android.material.slider.Slider
|
||||
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.ItemBgImageBinding
|
||||
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.http.newCallResponseBody
|
||||
import io.legato.kazusa.help.http.okHttpClient
|
||||
import io.legato.kazusa.lib.dialogs.SelectItem
|
||||
import io.legato.kazusa.lib.dialogs.alert
|
||||
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.file.HandleFileContract
|
||||
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)
|
||||
ivBg.setImageResource(R.drawable.ic_image)
|
||||
ivBg.setColorFilter(primaryTextColor, PorterDuff.Mode.SRC_IN)
|
||||
swUnderline.isGone = ReadBook.book?.isImage == true
|
||||
root.setOnClickListener {
|
||||
selectBgImage.launch()
|
||||
}
|
||||
@@ -184,8 +185,8 @@ class BgTextConfigDialog : BaseBottomSheetDialogFragment(R.layout.dialog_read_bg
|
||||
}
|
||||
binding.tvBgColor.setOnClickListener {
|
||||
val bgColor =
|
||||
if (curBgType() == 0) Color.parseColor(curBgStr())
|
||||
else Color.parseColor("#015A86")
|
||||
if (curBgType() == 0) curBgStr().toColorInt()
|
||||
else "#015A86".toColorInt()
|
||||
ColorPickerDialog.newBuilder()
|
||||
.setColor(bgColor)
|
||||
.setShowAlphaSlider(false)
|
||||
@@ -355,7 +356,7 @@ class BgTextConfigDialog : BaseBottomSheetDialogFragment(R.layout.dialog_read_bg
|
||||
|
||||
private fun importConfig(byteArray: ByteArray) {
|
||||
execute {
|
||||
ReadBookConfig.import(byteArray).getOrThrow()
|
||||
ReadBookConfig.import(byteArray)
|
||||
}.onSuccess {
|
||||
ReadBookConfig.durConfig = it
|
||||
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(10, getString(R.string.chapter_list)),
|
||||
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.loadContent(false)
|
||||
}
|
||||
|
||||
PreferKey.paddingDisplayCutouts -> {
|
||||
postEvent(EventBus.UP_CONFIG, arrayListOf(2))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,12 +14,12 @@ import io.legato.kazusa.utils.themeColor
|
||||
/**
|
||||
* 自动翻页
|
||||
*/
|
||||
class AutoPager(
|
||||
private val readView: ReadView) {
|
||||
class AutoPager(private val readView: ReadView) : Runnable {
|
||||
private var progress = 0
|
||||
var isRunning = false
|
||||
private set
|
||||
private var isPausing = false
|
||||
private var isEInkMode = false
|
||||
private var scrollOffsetRemain = 0.0
|
||||
private var scrollOffset = 0
|
||||
private var lastTimeMillis = 0L
|
||||
@@ -29,10 +29,15 @@ class AutoPager(
|
||||
|
||||
fun start() {
|
||||
isRunning = true
|
||||
paint.color = readView.context.themeColor(androidx.appcompat.R.attr.colorPrimary)
|
||||
lastTimeMillis = SystemClock.uptimeMillis()
|
||||
isEInkMode = AppConfig.isEInkMode
|
||||
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() {
|
||||
@@ -41,6 +46,8 @@ class AutoPager(
|
||||
}
|
||||
isRunning = false
|
||||
isPausing = false
|
||||
isEInkMode = false
|
||||
readView.removeCallbacks(this)
|
||||
readView.curPage.upSelectAble(AppConfig.textSelectAble)
|
||||
readView.invalidate()
|
||||
reset()
|
||||
@@ -52,6 +59,7 @@ class AutoPager(
|
||||
return
|
||||
}
|
||||
isPausing = true
|
||||
readView.removeCallbacks(this)
|
||||
}
|
||||
|
||||
fun resume() {
|
||||
@@ -59,15 +67,24 @@ class AutoPager(
|
||||
return
|
||||
}
|
||||
isPausing = false
|
||||
lastTimeMillis = SystemClock.uptimeMillis()
|
||||
readView.invalidate()
|
||||
if (isEInkMode) {
|
||||
readView.postDelayed(this, ReadBookConfig.autoReadSpeed * 1000L)
|
||||
} else {
|
||||
lastTimeMillis = SystemClock.uptimeMillis()
|
||||
readView.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
fun reset() {
|
||||
progress = 0
|
||||
scrollOffsetRemain = 0.0
|
||||
scrollOffset = 0
|
||||
canvasRecorder.invalidate()
|
||||
if (isEInkMode) {
|
||||
readView.removeCallbacks(this)
|
||||
readView.postDelayed(this, ReadBookConfig.autoReadSpeed * 1000L)
|
||||
} else {
|
||||
progress = 0
|
||||
scrollOffsetRemain = 0.0
|
||||
scrollOffset = 0
|
||||
canvasRecorder.invalidate()
|
||||
}
|
||||
}
|
||||
|
||||
fun upRecorder() {
|
||||
@@ -76,12 +93,15 @@ class AutoPager(
|
||||
}
|
||||
|
||||
fun onDraw(canvas: Canvas) {
|
||||
if (!isRunning) {
|
||||
if (!isRunning || isEInkMode) {
|
||||
return
|
||||
}
|
||||
|
||||
if (readView.isScroll) {
|
||||
if (!isPausing) readView.curPage.scroll(-scrollOffset)
|
||||
if (!isPausing) {
|
||||
readView.curPage.scroll(-scrollOffset)
|
||||
scrollOffset = 0
|
||||
}
|
||||
} else {
|
||||
val bottom = progress
|
||||
val width = readView.width
|
||||
@@ -104,7 +124,7 @@ class AutoPager(
|
||||
}
|
||||
|
||||
fun computeOffset() {
|
||||
if (!isRunning || isPausing) {
|
||||
if (!isRunning || isPausing || isEInkMode) {
|
||||
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.content.Context
|
||||
import android.graphics.drawable.LayerDrawable
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.FrameLayout
|
||||
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.isInvisible
|
||||
import io.legato.kazusa.R
|
||||
import io.legato.kazusa.constant.AppConst.timeFormat
|
||||
import io.legato.kazusa.data.entities.Bookmark
|
||||
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.ReadTipConfig
|
||||
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.dpToPx
|
||||
import io.legato.kazusa.utils.gone
|
||||
import io.legato.kazusa.utils.setOnApplyWindowInsetsListenerCompat
|
||||
import io.legato.kazusa.utils.setTextIfNotEqual
|
||||
import io.legato.kazusa.utils.statusBarHeight
|
||||
import splitties.views.backgroundColor
|
||||
@@ -56,7 +62,7 @@ class PageView(context: Context) : FrameLayout(context) {
|
||||
get() {
|
||||
val h1 = if (binding.vwStatusBar.isGone) 0 else binding.vwStatusBar.height
|
||||
val h2 = if (binding.llHeader.isGone) 0 else binding.llHeader.height
|
||||
return h1 + h2
|
||||
return h1 + h2 + binding.vwRoot.paddingTop
|
||||
}
|
||||
|
||||
init {
|
||||
@@ -94,6 +100,7 @@ class PageView(context: Context) : FrameLayout(context) {
|
||||
vwBottomDivider.backgroundColor = tipDividerColor
|
||||
upStatusBar()
|
||||
upNavigationBar()
|
||||
upPaddingDisplayCutouts()
|
||||
llHeader.setPadding(
|
||||
it.headerPaddingLeft.dpToPx(),
|
||||
it.headerPaddingTop.dpToPx(),
|
||||
@@ -125,6 +132,24 @@ class PageView(context: Context) : FrameLayout(context) {
|
||||
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() {
|
||||
binding.vwRoot.backgroundColor = ReadBookConfig.bgMeanColor
|
||||
binding.vwBg.background = ReadBookConfig.bg
|
||||
binding.vwRoot.background = LayerDrawable(
|
||||
arrayOf(
|
||||
ReadBookConfig.bgMeanColor.toDrawable(),
|
||||
ReadBookConfig.bg
|
||||
)
|
||||
)
|
||||
upBgAlpha()
|
||||
}
|
||||
|
||||
@@ -248,7 +277,8 @@ class PageView(context: Context) : FrameLayout(context) {
|
||||
* 更新背景透明度
|
||||
*/
|
||||
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.model.ReadAloud
|
||||
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.page.api.DataSource
|
||||
import io.legato.kazusa.ui.book.read.page.delegate.CoverPageDelegate
|
||||
@@ -456,6 +457,13 @@ class ReadView(context: Context, attrs: AttributeSet) :
|
||||
{ progress -> callBack.sureNewProgress(progress) },
|
||||
{ context.longToastOnUi(context.getString(R.string.upload_book_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)
|
||||
}
|
||||
}
|
||||
// 墨水屏模式下的朗读和搜索下划线
|
||||
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) {
|
||||
drawUnderline(canvas)
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ import io.legato.kazusa.utils.postEvent
|
||||
import io.legato.kazusa.utils.spToPx
|
||||
import io.legato.kazusa.utils.splitNotBlank
|
||||
import io.legato.kazusa.utils.textHeight
|
||||
import io.legato.kazusa.utils.toastOnUi
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import splitties.init.appCtx
|
||||
import java.util.LinkedList
|
||||
@@ -1001,20 +1002,8 @@ object ChapterProvider {
|
||||
}
|
||||
}
|
||||
|
||||
if (viewWidth > 0 && viewHeight > 0) {
|
||||
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 (viewWidth <= 0 || viewHeight <= 0) {
|
||||
return
|
||||
}
|
||||
|
||||
visibleRect.set(
|
||||
@@ -1024,6 +1013,37 @@ object ChapterProvider {
|
||||
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()
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+10
-11
@@ -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.TextPage
|
||||
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.utils.dpToPx
|
||||
import io.legato.kazusa.utils.fastSum
|
||||
@@ -214,13 +213,13 @@ class TextChapterLayout(
|
||||
emptyContent = contents.isEmpty(),
|
||||
isVolumeTitle = bookChapter.isVolume
|
||||
)
|
||||
pendingTextPage.lines.last().isParagraphEnd = true
|
||||
stringBuilder.append("\n")
|
||||
}
|
||||
pendingTextPage.lines.last().isParagraphEnd = true
|
||||
stringBuilder.append("\n")
|
||||
durY += titleBottomSpacing
|
||||
|
||||
// 如果是单图模式且当前页有内容,强制分页
|
||||
if (isSingleImageStyle && pendingTextPage.lines.isNotEmpty()) {
|
||||
if (isSingleImageStyle && pendingTextPage.lines.isNotEmpty() && contents.isNotEmpty()) {
|
||||
prepareNextPageIfNeed()
|
||||
}
|
||||
}
|
||||
@@ -708,13 +707,13 @@ class TextChapterLayout(
|
||||
)
|
||||
}
|
||||
|
||||
isLineEnd && char == ChapterProvider.reviewChar -> {
|
||||
ReviewColumn(
|
||||
start = absStartX + xStart,
|
||||
end = absStartX + xEnd,
|
||||
count = 100
|
||||
)
|
||||
}
|
||||
// isLineEnd && char == ChapterProvider.reviewChar -> {
|
||||
// ReviewColumn(
|
||||
// start = absStartX + xStart,
|
||||
// end = absStartX + xEnd,
|
||||
// count = 100
|
||||
// )
|
||||
// }
|
||||
|
||||
else -> {
|
||||
TextColumn(
|
||||
|
||||
@@ -12,12 +12,13 @@ import androidx.activity.viewModels
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.widget.doAfterTextChanged
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.recyclerview.widget.DefaultItemAnimator
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
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.SearchView
|
||||
import io.legato.kazusa.R
|
||||
@@ -45,6 +46,7 @@ import io.legato.kazusa.utils.viewbindingdelegate.viewBinding
|
||||
import io.legato.kazusa.utils.visible
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.awaitCancellation
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.conflate
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
@@ -183,21 +185,19 @@ class SearchActivity : VMBaseActivity<ActivityBookSearchBinding, SearchViewModel
|
||||
|
||||
binding.appBar.post {
|
||||
val maxPaddingTop = binding.appBar.paddingTop
|
||||
binding.appBar.addOnOffsetChangedListener(
|
||||
AppBarLayout.OnOffsetChangedListener { appBarLayout, verticalOffset ->
|
||||
val totalScrollRange = appBarLayout.totalScrollRange
|
||||
val scrollRatio =
|
||||
(1f - (abs(verticalOffset).toFloat() / totalScrollRange)).coerceIn(0f, 1f)
|
||||
binding.appBar.addOnOffsetChangedListener { appBarLayout, verticalOffset ->
|
||||
val totalScrollRange = appBarLayout.totalScrollRange
|
||||
val scrollRatio =
|
||||
(1f - (abs(verticalOffset).toFloat() / totalScrollRange)).coerceIn(0f, 1f)
|
||||
|
||||
appBarLayout.alpha = scrollRatio
|
||||
appBarLayout.setPadding(
|
||||
appBarLayout.paddingLeft,
|
||||
(maxPaddingTop * scrollRatio).toInt(),
|
||||
appBarLayout.paddingRight,
|
||||
appBarLayout.paddingBottom
|
||||
)
|
||||
}
|
||||
)
|
||||
appBarLayout.alpha = scrollRatio
|
||||
appBarLayout.setPadding(
|
||||
appBarLayout.paddingLeft,
|
||||
(maxPaddingTop * scrollRatio).toInt(),
|
||||
appBarLayout.paddingRight,
|
||||
appBarLayout.paddingBottom
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
searchBar.setOnMenuItemClickListener { item ->
|
||||
@@ -343,6 +343,16 @@ class SearchActivity : VMBaseActivity<ActivityBookSearchBinding, SearchViewModel
|
||||
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()
|
||||
}
|
||||
|
||||
fun pause() {
|
||||
searchModel.pause()
|
||||
}
|
||||
|
||||
fun resume() {
|
||||
searchModel.resume()
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存搜索关键字
|
||||
*/
|
||||
|
||||
@@ -2,6 +2,7 @@ package io.legato.kazusa.ui.book.searchContent
|
||||
|
||||
import android.text.Spanned
|
||||
import androidx.core.text.HtmlCompat
|
||||
import io.legato.kazusa.help.config.AppConfig
|
||||
|
||||
data class SearchResult(
|
||||
val resultCount: Int = 0,
|
||||
@@ -22,19 +23,34 @@ data class SearchResult(
|
||||
val leftString = resultText.substring(0, queryIndexInSurrounding)
|
||||
val rightString =
|
||||
resultText.substring(queryIndexInSurrounding + query.length, resultText.length)
|
||||
val html = buildString {
|
||||
append(chapterTitle.colorTextForHtml(accentColor))
|
||||
append("<br>")
|
||||
append(leftString.colorTextForHtml(textColor))
|
||||
append(query.colorTextForHtml(accentColor))
|
||||
append(rightString.colorTextForHtml(textColor))
|
||||
// 检查是否为墨水屏模式
|
||||
val html = if (AppConfig.isEInkMode) {
|
||||
// 墨水屏模式:使用下划线
|
||||
buildString {
|
||||
append("<u>${chapterTitle}</u>")
|
||||
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)
|
||||
} else {
|
||||
HtmlCompat.fromHtml(
|
||||
resultText.colorTextForHtml(textColor),
|
||||
HtmlCompat.FROM_HTML_MODE_LEGACY
|
||||
)
|
||||
val html = if (AppConfig.isEInkMode) {
|
||||
resultText
|
||||
} 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
|
||||
})
|
||||
}
|
||||
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?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
viewModel.initData(intent) { source ->
|
||||
viewModel.initData(intent, success = { source ->
|
||||
initView(source)
|
||||
}
|
||||
}, error = {
|
||||
finish()
|
||||
})
|
||||
}
|
||||
|
||||
private fun initView(source: BaseSource) {
|
||||
|
||||
@@ -2,11 +2,12 @@ package io.legato.kazusa.ui.login
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Intent
|
||||
import com.script.rhino.runScriptWithContext
|
||||
import io.legato.kazusa.base.BaseViewModel
|
||||
import io.legato.kazusa.constant.AppLog
|
||||
import io.legato.kazusa.data.appDb
|
||||
import io.legato.kazusa.data.entities.BaseSource
|
||||
import io.legato.kazusa.exception.NoStackTraceException
|
||||
import com.script.rhino.runScriptWithContext
|
||||
import io.legato.kazusa.utils.toastOnUi
|
||||
|
||||
class SourceLoginViewModel(application: Application) : BaseViewModel(application) {
|
||||
@@ -14,7 +15,7 @@ class SourceLoginViewModel(application: Application) : BaseViewModel(application
|
||||
var source: BaseSource? = null
|
||||
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 {
|
||||
val sourceKey = intent.getStringExtra("key")
|
||||
?: throw NoStackTraceException("没有参数")
|
||||
@@ -33,6 +34,9 @@ class SourceLoginViewModel(application: Application) : BaseViewModel(application
|
||||
} else {
|
||||
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) {
|
||||
appDb.bookDao.update(book)
|
||||
} else {
|
||||
appDb.bookDao.insert(book)
|
||||
appDb.bookDao.replace(oldBook, book)
|
||||
BookHelp.updateCacheFolder(oldBook, book)
|
||||
}
|
||||
appDb.bookChapterDao.delByBook(bookUrl)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package io.legato.kazusa.ui.replace
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
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.coroutine.Coroutine
|
||||
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.file.HandleFileContract
|
||||
import io.legato.kazusa.ui.qrcode.QrCodeResult
|
||||
@@ -72,7 +69,7 @@ class ReplaceRuleActivity : VMBaseActivity<ActivityReplaceRuleBinding, ReplaceRu
|
||||
private val searchView: SearchView by lazy {
|
||||
binding.titleBar.findViewById(R.id.search_view)
|
||||
}
|
||||
private var groups = hashSetOf<String>()
|
||||
private var groups = arrayListOf<String>()
|
||||
private var groupMenu: SubMenu? = null
|
||||
private var replaceRuleFlowJob: Job? = null
|
||||
private var dataInit = false
|
||||
@@ -224,7 +221,7 @@ class ReplaceRuleActivity : VMBaseActivity<ActivityReplaceRuleBinding, ReplaceRu
|
||||
AppLog.put("替换规则管理界面更新数据出错", it)
|
||||
}.flowOn(IO).conflate().collect {
|
||||
if (dataInit) {
|
||||
setResult(Activity.RESULT_OK)
|
||||
setResult(RESULT_OK)
|
||||
}
|
||||
adapter.setItems(it, adapter.diffItemCallBack)
|
||||
dataInit = true
|
||||
|
||||
@@ -20,8 +20,6 @@ import io.legato.kazusa.databinding.ActivityRssSourceBinding
|
||||
import io.legato.kazusa.databinding.DialogEditTextBinding
|
||||
import io.legato.kazusa.help.DirectLinkUpload
|
||||
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.file.HandleFileContract
|
||||
import io.legato.kazusa.ui.qrcode.QrCodeResult
|
||||
@@ -70,7 +68,7 @@ class RssSourceActivity : VMBaseActivity<ActivityRssSourceBinding, RssSourceView
|
||||
binding.titleBar.findViewById(R.id.search_view)
|
||||
}
|
||||
private var sourceFlowJob: Job? = null
|
||||
private var groups = hashSetOf<String>()
|
||||
private var groups = arrayListOf<String>()
|
||||
private var groupMenu: SubMenu? = null
|
||||
private val qrCodeResult = registerForActivityResult(QrCodeResult()) {
|
||||
it ?: return@registerForActivityResult
|
||||
|
||||
@@ -35,6 +35,7 @@ import androidx.annotation.DrawableRes
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.net.toUri
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import androidx.preference.PreferenceManager
|
||||
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.book.isAudio
|
||||
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.ui.book.audio.AudioPlayActivity
|
||||
import io.legato.kazusa.ui.book.manga.ReadMangaActivity
|
||||
@@ -68,7 +70,7 @@ fun Context.startActivityForBook(
|
||||
) {
|
||||
val cls = when {
|
||||
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
|
||||
}
|
||||
val intent = Intent(this, cls)
|
||||
@@ -323,7 +325,7 @@ fun Context.getClipText(): String? {
|
||||
fun Context.sendMail(mail: String) {
|
||||
try {
|
||||
val intent = Intent(Intent.ACTION_SENDTO)
|
||||
intent.data = Uri.parse("mailto:$mail")
|
||||
intent.data = "mailto:$mail".toUri()
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
startActivity(intent)
|
||||
} catch (e: Exception) {
|
||||
|
||||
@@ -17,6 +17,7 @@ import io.legato.kazusa.R
|
||||
import io.legato.kazusa.data.entities.Book
|
||||
import io.legato.kazusa.help.book.isAudio
|
||||
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.ui.book.audio.AudioPlayActivity
|
||||
import io.legato.kazusa.ui.book.manga.ReadMangaActivity
|
||||
@@ -93,7 +94,7 @@ fun Fragment.startActivityForBook(
|
||||
) {
|
||||
val cls = when {
|
||||
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
|
||||
}
|
||||
val intent = Intent(requireActivity(), cls)
|
||||
|
||||
@@ -14,7 +14,7 @@ object HtmlFormatter {
|
||||
private val notImgHtmlRegex = "</?(?!img)[a-zA-Z]+(?=[ >])[^<>]*>".toRegex()
|
||||
private val otherHtmlRegex = "</?[a-zA-Z]+(?=[ >])[^<>]*>".toRegex()
|
||||
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
|
||||
)
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user