合并上游更改

感谢:@821938089、@zhongerxll、@youke2580c、@Leroy-X、@Dark-Tide、@lhjgege、@3harmony、@mgz0227
This commit is contained in:
HapeLee
2025-09-01 00:57:17 +08:00
parent dccb72adab
commit 8eb4881767
87 changed files with 800 additions and 365 deletions
@@ -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()
)
}
}
}
@@ -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)
}