重做漫画功能,不影响现有逻辑漫画功能 (#4685)
* 增加漫画UI * ... * ... * ... * ... * ... * ... * ... * ... * ... * 完成加载图片人机验证问题 * 完成加载框 * ... * ... * .... * ... * ... * ... * ... * ... * ... * ... * ... * ... * 剔除重复章节 * ... * .... * ... * ... * ... * ... * .... * ... * ... * ... * .... * ... * ... * ... * .... * ... * ... * ... * .... * ... * ... * ... * ... * ... * ... * ... * ... * ... * ... * .... * ... * ... * ... * ... * ... * ... * ... * ... * ... * ... * .... * ... * ... * ... * ... * ... * ... * ... * ... * ... * .... * ... * ... * ... * ... * ... * ... * ... * ... * ... * ... * ...
This commit is contained in:
@@ -238,6 +238,7 @@ dependencies {
|
||||
|
||||
//Glide
|
||||
implementation(libs.glide.glide)
|
||||
implementation(libs.glide.okhttp)
|
||||
ksp(libs.glide.ksp)
|
||||
|
||||
//Svg
|
||||
@@ -279,6 +280,8 @@ dependencies {
|
||||
implementation platform(libs.firebase.bom)
|
||||
implementation libs.firebase.analytics
|
||||
implementation libs.firebase.perf
|
||||
implementation libs.zoom.imageview
|
||||
implementation libs.glide.recyclerview
|
||||
|
||||
//LeakCanary, 内存泄露检测
|
||||
//debugImplementation('com.squareup.leakcanary:leakcanary-android:2.7')
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
<application
|
||||
android:name=".App"
|
||||
android:allowBackup="true"
|
||||
android:largeHeap="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="${app_name}"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
@@ -134,6 +135,21 @@
|
||||
android:name="com.samsung.android.support.REMOTE_ACTION"
|
||||
android:resource="@xml/spen_remote_actions" />
|
||||
</activity>
|
||||
|
||||
<!-- 阅读漫画界面 -->
|
||||
<activity
|
||||
android:name=".ui.book.manga.ReadMangaActivity"
|
||||
android:configChanges="locale|keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTask">
|
||||
<intent-filter>
|
||||
<action android:name="com.samsung.android.support.REMOTE_ACTION" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="com.samsung.android.support.REMOTE_ACTION"
|
||||
android:resource="@xml/spen_remote_actions" />
|
||||
</activity>
|
||||
<!-- 书籍详情页 -->
|
||||
<activity
|
||||
android:name=".ui.book.info.BookInfoActivity"
|
||||
|
||||
@@ -475,7 +475,7 @@ abstract class RecyclerAdapter<ITEM, VB : ViewBinding>(protected val context: Co
|
||||
|
||||
companion object {
|
||||
private const val TYPE_HEADER_VIEW = Int.MIN_VALUE
|
||||
private const val TYPE_FOOTER_VIEW = Int.MAX_VALUE - 999
|
||||
const val TYPE_FOOTER_VIEW = Int.MAX_VALUE - 999
|
||||
private val handler by lazy { buildMainHandler() }
|
||||
}
|
||||
|
||||
|
||||
@@ -153,6 +153,8 @@ object PreferKey {
|
||||
const val streamReadAloudAudio = "streamReadAloudAudio"
|
||||
const val pauseReadAloudWhilePhoneCalls = "pauseReadAloudWhilePhoneCalls"
|
||||
const val readAloudByMediaButton = "readAloudByMediaButton"
|
||||
const val showMangaUi="showMangaUi"
|
||||
const val disableMangaScaling="disableMangaScaling"
|
||||
|
||||
const val cPrimary = "colorPrimary"
|
||||
const val cAccent = "colorAccent"
|
||||
|
||||
@@ -604,5 +604,13 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
appCtx.toastOnUi("当前没有配置菜单区域,自动恢复中间区域为菜单.")
|
||||
}
|
||||
}
|
||||
|
||||
//跳转到漫画界面不使用富文本模式
|
||||
val showMangaUi: Boolean
|
||||
get() = appCtx.getPrefBoolean(PreferKey.showMangaUi, true)
|
||||
|
||||
//禁用漫画缩放
|
||||
val disableMangaScaling: Boolean
|
||||
get() = appCtx.getPrefBoolean(PreferKey.disableMangaScaling, true)
|
||||
}
|
||||
|
||||
|
||||
@@ -2,10 +2,16 @@ package io.legado.app.help.glide
|
||||
|
||||
import android.content.Context
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.GlideBuilder
|
||||
import com.bumptech.glide.Registry
|
||||
import com.bumptech.glide.annotation.GlideModule
|
||||
import com.bumptech.glide.load.DecodeFormat
|
||||
import com.bumptech.glide.load.engine.bitmap_recycle.LruBitmapPool
|
||||
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory
|
||||
import com.bumptech.glide.load.engine.cache.LruResourceCache
|
||||
import com.bumptech.glide.load.model.GlideUrl
|
||||
import com.bumptech.glide.module.AppGlideModule
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import java.io.InputStream
|
||||
|
||||
|
||||
@@ -13,7 +19,6 @@ import java.io.InputStream
|
||||
@GlideModule
|
||||
class LegadoGlideModule : AppGlideModule() {
|
||||
|
||||
|
||||
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
|
||||
registry.replace(
|
||||
GlideUrl::class.java,
|
||||
@@ -22,4 +27,19 @@ class LegadoGlideModule : AppGlideModule() {
|
||||
)
|
||||
}
|
||||
|
||||
override fun applyOptions(context: Context, builder: GlideBuilder) {
|
||||
super.applyOptions(context, builder)
|
||||
builder.setMemoryCache(LruResourceCache(1024 * 1024 * 500))
|
||||
.setBitmapPool(LruBitmapPool(1024 * 1024 * 200))
|
||||
.setDiskCache(
|
||||
InternalCacheDiskCacheFactory(
|
||||
context,
|
||||
1024 * 1024 * 1000
|
||||
)
|
||||
)
|
||||
.setDefaultRequestOptions {
|
||||
RequestOptions().format(DecodeFormat.PREFER_RGB_565)
|
||||
.encodeQuality(90)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,11 @@ import com.bumptech.glide.load.model.GlideUrl
|
||||
import com.bumptech.glide.load.model.ModelLoader
|
||||
import com.bumptech.glide.load.model.ModelLoaderFactory
|
||||
import com.bumptech.glide.load.model.MultiModelLoaderFactory
|
||||
import okhttp3.Call
|
||||
import java.io.InputStream
|
||||
|
||||
|
||||
object OkHttpModeLoaderFactory : ModelLoaderFactory<GlideUrl?, InputStream?> {
|
||||
object OkHttpModeLoaderFactory: ModelLoaderFactory<GlideUrl?, InputStream?> {
|
||||
|
||||
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<GlideUrl?, InputStream?> {
|
||||
return OkHttpModelLoader
|
||||
|
||||
@@ -6,26 +6,29 @@ import com.bumptech.glide.load.model.GlideUrl
|
||||
import com.bumptech.glide.load.model.ModelLoader
|
||||
import io.legado.app.model.analyzeRule.AnalyzeUrl
|
||||
import io.legado.app.utils.isAbsUrl
|
||||
|
||||
import java.io.InputStream
|
||||
|
||||
object OkHttpModelLoader : ModelLoader<GlideUrl?, InputStream?> {
|
||||
|
||||
val loadOnlyWifiOption = Option.memory("loadOnlyWifi", false)
|
||||
val sourceOriginOption = Option.memory<String>("sourceOrigin")
|
||||
val mangaOption = Option.memory<Boolean>("manga",false)
|
||||
|
||||
override fun buildLoadData(
|
||||
model: GlideUrl,
|
||||
width: Int,
|
||||
height: Int,
|
||||
options: Options
|
||||
options: Options,
|
||||
): ModelLoader.LoadData<InputStream?> {
|
||||
val cacheKey = model.toString()
|
||||
var modelWithHeader = model
|
||||
if (cacheKey.isAbsUrl()) {
|
||||
modelWithHeader = AnalyzeUrl(cacheKey).getGlideUrl()
|
||||
}
|
||||
return ModelLoader.LoadData(modelWithHeader, OkHttpStreamFetcher(modelWithHeader, options))
|
||||
return ModelLoader.LoadData(
|
||||
modelWithHeader,
|
||||
OkHttpStreamFetcher(modelWithHeader, options)
|
||||
)
|
||||
}
|
||||
|
||||
override fun handles(model: GlideUrl): Boolean {
|
||||
|
||||
@@ -14,6 +14,7 @@ import io.legado.app.help.http.CookieManager.cookieJarHeader
|
||||
import io.legado.app.help.http.addHeaders
|
||||
import io.legado.app.help.http.okHttpClient
|
||||
import io.legado.app.help.source.SourceHelp
|
||||
import io.legado.app.model.ReadMange
|
||||
import io.legado.app.utils.ImageUtils
|
||||
import io.legado.app.utils.isWifiConnect
|
||||
import okhttp3.Call
|
||||
@@ -26,7 +27,10 @@ import java.io.IOException
|
||||
import java.io.InputStream
|
||||
|
||||
|
||||
class OkHttpStreamFetcher(private val url: GlideUrl, private val options: Options) :
|
||||
class OkHttpStreamFetcher(
|
||||
private val url: GlideUrl,
|
||||
private val options: Options,
|
||||
) :
|
||||
DataFetcher<InputStream>, okhttp3.Callback {
|
||||
private var stream: InputStream? = null
|
||||
private var responseBody: ResponseBody? = null
|
||||
@@ -95,15 +99,31 @@ class OkHttpStreamFetcher(private val url: GlideUrl, private val options: Option
|
||||
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
responseBody = response.body
|
||||
if (response.isSuccessful) {
|
||||
val decodeResult = ImageUtils.decode(
|
||||
val manga = options.get(OkHttpModelLoader.mangaOption) == true
|
||||
val decodeResult = if (manga) {
|
||||
ImageUtils.decode(
|
||||
url.toStringUrl(),
|
||||
responseBody!!.byteStream().readBytes(),
|
||||
isCover = false,
|
||||
source,
|
||||
ReadMange.book
|
||||
).let {
|
||||
ByteArrayInputStream(it)
|
||||
}
|
||||
} else {
|
||||
ImageUtils.decode(
|
||||
url.toStringUrl(), responseBody!!.byteStream(),
|
||||
isCover = true, source
|
||||
)
|
||||
}
|
||||
if (response.isSuccessful) {
|
||||
|
||||
if (decodeResult == null) {
|
||||
callback?.onLoadFailed(NoStackTraceException("封面二次解密失败"))
|
||||
} else {
|
||||
val contentLength: Long = if (decodeResult is ByteArrayInputStream) decodeResult.available().toLong() else Preconditions.checkNotNull(responseBody).contentLength()
|
||||
val contentLength: Long =
|
||||
if (decodeResult is ByteArrayInputStream) decodeResult.available()
|
||||
.toLong() else Preconditions.checkNotNull(responseBody).contentLength()
|
||||
stream = ContentLengthInputStream.obtain(decodeResult, contentLength)
|
||||
callback?.onDataReady(stream)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
package io.legado.app.help.glide.progress
|
||||
|
||||
typealias OnProgressListener = ((isComplete: Boolean, percentage: Int, bytesRead: Long, totalBytes: Long) -> Unit)?
|
||||
@@ -0,0 +1,51 @@
|
||||
package io.legado.app.help.glide.progress
|
||||
|
||||
import android.text.TextUtils
|
||||
import io.legado.app.utils.runOnUI
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
/**
|
||||
* 进度监听器管理类
|
||||
* 加入图片加载进度监听,加入Https支持
|
||||
*/
|
||||
object ProgressManager {
|
||||
private val listenersMap = ConcurrentHashMap<String, OnProgressListener>()
|
||||
|
||||
val LISTENER = object : ProgressResponseBody.InternalProgressListener {
|
||||
override fun onProgress(url: String, bytesRead: Long, totalBytes: Long) {
|
||||
getProgressListener(url)?.let {
|
||||
var percentage = (bytesRead * 1f / totalBytes * 100f).toInt()
|
||||
var isComplete = percentage >= 100
|
||||
if (percentage <= -100) {
|
||||
percentage = 0
|
||||
isComplete = true
|
||||
}
|
||||
runOnUI {
|
||||
it.invoke(isComplete, percentage, bytesRead, totalBytes)
|
||||
}
|
||||
if (isComplete) {
|
||||
removeListener(url)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun addListener(url: String, listener: OnProgressListener) {
|
||||
if (!TextUtils.isEmpty(url) && listener != null) {
|
||||
listenersMap[url] = listener
|
||||
listener.invoke(false, 1, 0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeListener(url: String) {
|
||||
if (!TextUtils.isEmpty(url)) {
|
||||
listenersMap.remove(url)
|
||||
}
|
||||
}
|
||||
|
||||
fun getProgressListener(url: String?): OnProgressListener {
|
||||
return if (TextUtils.isEmpty(url) || listenersMap.size == 0) {
|
||||
null
|
||||
} else listenersMap[url]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package io.legado.app.help.glide.progress
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.ResponseBody
|
||||
import okio.*
|
||||
import java.io.IOException
|
||||
import kotlin.jvm.Throws
|
||||
|
||||
class ProgressResponseBody internal constructor(private val url: String, private val internalProgressListener: InternalProgressListener?, private val responseBody: ResponseBody) : ResponseBody() {
|
||||
private var bufferedSource: BufferedSource? = null
|
||||
override fun contentType(): MediaType? {
|
||||
return responseBody.contentType()
|
||||
}
|
||||
|
||||
override fun contentLength(): Long {
|
||||
return responseBody.contentLength()
|
||||
}
|
||||
|
||||
override fun source(): BufferedSource {
|
||||
if (bufferedSource == null) {
|
||||
bufferedSource = source(responseBody.source()).buffer()
|
||||
}
|
||||
return bufferedSource!!
|
||||
}
|
||||
|
||||
private fun source(source: Source): Source {
|
||||
return object : ForwardingSource(source) {
|
||||
var totalBytesRead: Long = 0
|
||||
var lastTotalBytesRead: Long = 0
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun read(sink: Buffer, byteCount: Long): Long {
|
||||
val bytesRead = super.read(sink, byteCount)
|
||||
totalBytesRead += if (bytesRead == -1L) 0 else bytesRead
|
||||
if (internalProgressListener != null && lastTotalBytesRead != totalBytesRead) {
|
||||
lastTotalBytesRead = totalBytesRead
|
||||
mainThreadHandler.post { internalProgressListener.onProgress(url, totalBytesRead, contentLength()) }
|
||||
}
|
||||
return bytesRead
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface InternalProgressListener {
|
||||
fun onProgress(url: String, bytesRead: Long, totalBytes: Long)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val mainThreadHandler = Handler(Looper.getMainLooper())
|
||||
}
|
||||
|
||||
}
|
||||
@@ -3,6 +3,9 @@ package io.legado.app.help.http
|
||||
import io.legado.app.constant.AppConst
|
||||
import io.legado.app.help.CacheManager
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.help.glide.progress.ProgressManager
|
||||
import io.legado.app.help.glide.progress.ProgressManager.LISTENER
|
||||
import io.legado.app.help.glide.progress.ProgressResponseBody
|
||||
import io.legado.app.help.http.CookieManager.cookieJarHeader
|
||||
import io.legado.app.utils.NetworkUtils
|
||||
import okhttp3.ConnectionSpec
|
||||
@@ -93,7 +96,15 @@ val okHttpClient: OkHttpClient by lazy {
|
||||
if (enableCookieJar) {
|
||||
CookieManager.saveResponse(networkResponse)
|
||||
}
|
||||
networkResponse
|
||||
networkResponse.newBuilder()
|
||||
.body(
|
||||
ProgressResponseBody(
|
||||
request.url.toString(),
|
||||
LISTENER,
|
||||
networkResponse.body!!
|
||||
)
|
||||
)
|
||||
.build()
|
||||
}
|
||||
if (AppConfig.isCronet) {
|
||||
if (Cronet.loader?.install() == true) {
|
||||
|
||||
@@ -6,9 +6,11 @@ import android.graphics.drawable.BitmapDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import androidx.annotation.Keep
|
||||
import com.bumptech.glide.RequestBuilder
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.bumptech.glide.load.resource.bitmap.CenterCrop
|
||||
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import com.bumptech.glide.request.target.Target.SIZE_ORIGINAL
|
||||
import io.legado.app.R
|
||||
import io.legado.app.constant.PreferKey
|
||||
import io.legado.app.data.entities.BaseSource
|
||||
@@ -21,7 +23,11 @@ import io.legado.app.help.glide.ImageLoader
|
||||
import io.legado.app.help.glide.OkHttpModelLoader
|
||||
import io.legado.app.model.analyzeRule.AnalyzeRule
|
||||
import io.legado.app.model.analyzeRule.AnalyzeUrl
|
||||
import io.legado.app.utils.*
|
||||
import io.legado.app.utils.BitmapUtils
|
||||
import io.legado.app.utils.GSON
|
||||
import io.legado.app.utils.fromJsonObject
|
||||
import io.legado.app.utils.getPrefBoolean
|
||||
import io.legado.app.utils.getPrefString
|
||||
import splitties.init.appCtx
|
||||
|
||||
@Keep
|
||||
@@ -71,7 +77,7 @@ object BookCover {
|
||||
context: Context,
|
||||
path: String?,
|
||||
loadOnlyWifi: Boolean = false,
|
||||
sourceOrigin: String? = null
|
||||
sourceOrigin: String? = null,
|
||||
): RequestBuilder<Drawable> {
|
||||
if (AppConfig.useDefaultCover) {
|
||||
return ImageLoader.load(context, defaultDrawable)
|
||||
@@ -88,6 +94,31 @@ object BookCover {
|
||||
.centerCrop()
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载漫画图片
|
||||
*/
|
||||
|
||||
fun loadManga(
|
||||
context: Context,
|
||||
path: String?,
|
||||
loadOnlyWifi: Boolean = false,
|
||||
sourceOrigin: String? = null,
|
||||
manga: Boolean = false,
|
||||
useDefaultCover: Drawable? = null,
|
||||
): RequestBuilder<Drawable> {
|
||||
var options = RequestOptions().set(OkHttpModelLoader.loadOnlyWifiOption, loadOnlyWifi)
|
||||
.set(OkHttpModelLoader.mangaOption, manga)
|
||||
if (sourceOrigin != null) {
|
||||
options = options.set(OkHttpModelLoader.sourceOriginOption, sourceOrigin)
|
||||
}
|
||||
return ImageLoader.load(context, path)
|
||||
.apply(options)
|
||||
.override(context.resources.displayMetrics.widthPixels, SIZE_ORIGINAL)
|
||||
.placeholder(useDefaultCover)
|
||||
.error(useDefaultCover)
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载模糊封面
|
||||
*/
|
||||
@@ -95,7 +126,7 @@ object BookCover {
|
||||
context: Context,
|
||||
path: String?,
|
||||
loadOnlyWifi: Boolean = false,
|
||||
sourceOrigin: String? = null
|
||||
sourceOrigin: String? = null,
|
||||
): RequestBuilder<Drawable> {
|
||||
val loadBlur = ImageLoader.load(context, defaultDrawable)
|
||||
.transform(BlurTransformation(25), CenterCrop())
|
||||
|
||||
@@ -0,0 +1,638 @@
|
||||
package io.legado.app.model
|
||||
|
||||
import io.legado.app.constant.AppLog
|
||||
import io.legado.app.constant.AppPattern
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.data.entities.Book
|
||||
import io.legado.app.data.entities.BookChapter
|
||||
import io.legado.app.data.entities.BookProgress
|
||||
import io.legado.app.data.entities.BookSource
|
||||
import io.legado.app.data.entities.ReadRecord
|
||||
import io.legado.app.help.AppWebDav
|
||||
import io.legado.app.help.book.BookHelp
|
||||
import io.legado.app.help.book.ContentProcessor
|
||||
import io.legado.app.help.book.isLocal
|
||||
import io.legado.app.help.book.readSimulating
|
||||
import io.legado.app.help.book.simulatedTotalChapterNum
|
||||
import io.legado.app.help.book.update
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.help.coroutine.Coroutine
|
||||
import io.legado.app.help.globalExecutor
|
||||
import io.legado.app.model.recyclerView.MangeContent
|
||||
import io.legado.app.model.recyclerView.ReaderLoading
|
||||
import io.legado.app.model.webBook.WebBook
|
||||
import io.legado.app.utils.NetworkUtils
|
||||
import io.legado.app.utils.mapIndexed
|
||||
import io.legado.app.utils.runOnUI
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.CoroutineStart
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.asCoroutineDispatcher
|
||||
import kotlinx.coroutines.cancelChildren
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.flow.distinctUntilChangedBy
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.min
|
||||
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
object ReadMange : CoroutineScope by MainScope() {
|
||||
var inBookshelf = false
|
||||
var tocChanged = false
|
||||
var chapterChanged = false
|
||||
var book: Book? = null
|
||||
val executor = globalExecutor
|
||||
var durChapterPagePos = 0 //章节位置
|
||||
var durChapterPageCount = 0//总章节
|
||||
var durChapterCount = 0
|
||||
var durChapterPos = 0
|
||||
var bookSource: BookSource? = null
|
||||
var chapterTitle: String = ""
|
||||
var readStartTime: Long = System.currentTimeMillis()
|
||||
private val readRecord = ReadRecord()
|
||||
private val loadingChapters = arrayListOf<Int>()
|
||||
var simulatedChapterSize = 0
|
||||
var mCallback: Callback? = null
|
||||
var mFirstLoading = false
|
||||
var gameOver = false
|
||||
var mTopChapter: BookChapter? = null
|
||||
var preDownloadTask: Job? = null
|
||||
val downloadedChapters = hashSetOf<Int>()
|
||||
val downloadFailChapters = hashMapOf<Int, Int>()
|
||||
private val downloadLoadingChapters = arrayListOf<Int>()
|
||||
var isMangaMode = false
|
||||
val downloadScope = CoroutineScope(SupervisorJob() + IO)
|
||||
|
||||
fun saveRead(pageChanged: Boolean = false) {
|
||||
executor.execute {
|
||||
val book = ReadMange.book ?: return@execute
|
||||
book.lastCheckCount = 0
|
||||
book.durChapterTime = System.currentTimeMillis()
|
||||
val chapterChanged = book.durChapterIndex != durChapterPagePos
|
||||
book.durChapterIndex = durChapterPagePos
|
||||
book.durChapterPos = durChapterPos
|
||||
if (!pageChanged || chapterChanged) {
|
||||
appDb.bookChapterDao.getChapter(book.bookUrl, durChapterPagePos)?.let {
|
||||
book.durChapterTitle = it.getDisplayTitle(
|
||||
ContentProcessor.get(book.name, book.origin).getTitleReplaceRules(),
|
||||
book.getUseReplaceRule()
|
||||
)
|
||||
}
|
||||
}
|
||||
appDb.bookDao.update(book)
|
||||
}
|
||||
}
|
||||
|
||||
fun upData(book: Book) {
|
||||
ReadMange.book = book
|
||||
isMangaMode = true
|
||||
durChapterPageCount = appDb.bookChapterDao.getChapterCount(book.bookUrl)
|
||||
simulatedChapterSize = if (book.readSimulating()) {
|
||||
book.simulatedTotalChapterNum()
|
||||
} else {
|
||||
durChapterPageCount
|
||||
}
|
||||
|
||||
if (durChapterPagePos != book.durChapterIndex || tocChanged) {
|
||||
durChapterPagePos = book.durChapterIndex
|
||||
durChapterPos = book.durChapterPos
|
||||
}
|
||||
upWebBook(book)
|
||||
|
||||
synchronized(this) {
|
||||
loadingChapters.clear()
|
||||
}
|
||||
}
|
||||
|
||||
fun resetData(book: Book) {
|
||||
ReadMange.book = book
|
||||
isMangaMode = true
|
||||
readRecord.bookName = book.name
|
||||
readRecord.readTime = appDb.readRecordDao.getReadTime(book.name) ?: 0
|
||||
durChapterPageCount = appDb.bookChapterDao.getChapterCount(book.bookUrl)
|
||||
simulatedChapterSize = if (book.readSimulating()) {
|
||||
book.simulatedTotalChapterNum()
|
||||
} else {
|
||||
durChapterPageCount
|
||||
}
|
||||
durChapterPagePos = book.durChapterIndex
|
||||
durChapterPos = book.durChapterPos
|
||||
upWebBook(book)
|
||||
synchronized(this) {
|
||||
loadingChapters.clear()
|
||||
}
|
||||
}
|
||||
|
||||
fun upWebBook(book: Book) {
|
||||
appDb.bookSourceDao.getBookSource(book.origin)?.let {
|
||||
bookSource = it
|
||||
} ?: let {
|
||||
bookSource = null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//每次切换章节更新阅读记录
|
||||
fun upReadTime() {
|
||||
executor.execute {
|
||||
if (!AppConfig.enableReadRecord) {
|
||||
return@execute
|
||||
}
|
||||
readRecord.readTime = readRecord.readTime + System.currentTimeMillis() - readStartTime
|
||||
readStartTime = System.currentTimeMillis()
|
||||
readRecord.lastRead = System.currentTimeMillis()
|
||||
appDb.readRecordDao.insert(readRecord)
|
||||
}
|
||||
}
|
||||
|
||||
private fun addLoading(index: Int): Boolean {
|
||||
synchronized(this) {
|
||||
if (loadingChapters.contains(index)) return false
|
||||
loadingChapters.add(index)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private fun addDownloadLoading(index: Int): Boolean {
|
||||
synchronized(this) {
|
||||
if (downloadLoadingChapters.contains(index)) return false
|
||||
downloadLoadingChapters.add(index)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun removeDownloadLoading(index: Int) {
|
||||
synchronized(this) {
|
||||
downloadLoadingChapters.remove(index)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeLoading(index: Int) {
|
||||
synchronized(this) {
|
||||
loadingChapters.remove(index)
|
||||
}
|
||||
}
|
||||
|
||||
fun loading(index: Int): Boolean {
|
||||
synchronized(this) {
|
||||
return loadingChapters.contains(index)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取正文
|
||||
*/
|
||||
private suspend fun getContent(
|
||||
scope: CoroutineScope,
|
||||
chapter: BookChapter,
|
||||
) {
|
||||
val book = ReadMange.book ?: return removeLoading(chapter.index)
|
||||
val bookSource = ReadMange.bookSource
|
||||
if (bookSource != null) {
|
||||
getContent(bookSource, scope, chapter, book)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun loadContent() {
|
||||
loadContent(durChapterPagePos)
|
||||
}
|
||||
|
||||
fun loadContent(
|
||||
index: Int,
|
||||
) {
|
||||
if (addLoading(index)) {
|
||||
Coroutine.async {
|
||||
val book = ReadMange.book!!
|
||||
appDb.bookChapterDao.getChapter(book.bookUrl, index)?.let { chapter ->
|
||||
getContent(
|
||||
downloadScope,
|
||||
chapter,
|
||||
)
|
||||
} ?: removeLoading(index)
|
||||
}.onError {
|
||||
removeLoading(index)
|
||||
AppLog.put("加载正文出错\n${it.localizedMessage}")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 注册回调
|
||||
*/
|
||||
fun register(cb: Callback) {
|
||||
mCallback = cb
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消注册回调
|
||||
*/
|
||||
fun unregister() {
|
||||
inBookshelf = false
|
||||
tocChanged = false
|
||||
chapterChanged = false
|
||||
book = null
|
||||
durChapterPagePos = 0
|
||||
isMangaMode = false
|
||||
durChapterPageCount = 0
|
||||
durChapterPos = 0
|
||||
durChapterCount = 0
|
||||
bookSource = null
|
||||
chapterTitle = ""
|
||||
loadingChapters.clear()
|
||||
simulatedChapterSize = 0
|
||||
mCallback = null
|
||||
mFirstLoading = false
|
||||
gameOver = false
|
||||
mTopChapter = null
|
||||
preDownloadTask?.cancel()
|
||||
preDownloadTask = null
|
||||
downloadedChapters.clear()
|
||||
downloadFailChapters.clear()
|
||||
downloadLoadingChapters.clear()
|
||||
executor.asCoroutineDispatcher().cancelChildren()
|
||||
downloadScope.coroutineContext.cancelChildren()
|
||||
coroutineContext.cancelChildren()
|
||||
}
|
||||
|
||||
/**
|
||||
* 内容加载完成
|
||||
*/
|
||||
suspend fun contentLoadFinish(
|
||||
chapter: BookChapter,
|
||||
content: String,
|
||||
book: Book
|
||||
) {
|
||||
if (mTopChapter != null && mTopChapter!!.title != chapterTitle && BookHelp.hasContent(
|
||||
book,
|
||||
mTopChapter!!
|
||||
)
|
||||
) {
|
||||
BookHelp.delContent(book, chapter)
|
||||
}
|
||||
mTopChapter = chapter
|
||||
chapterTitle = chapter.title
|
||||
if (chapter.index !in durChapterPagePos - 1..durChapterPagePos + 1) {
|
||||
return
|
||||
}
|
||||
if (content.isNotEmpty()) {
|
||||
val list = flow {
|
||||
val matcher = AppPattern.imgPattern.matcher(content)
|
||||
while (matcher.find()) {
|
||||
val src = matcher.group(1) ?: continue
|
||||
val mSrc = NetworkUtils.getAbsoluteURL(chapter.url, src)
|
||||
emit(mSrc)
|
||||
}
|
||||
}.distinctUntilChangedBy {
|
||||
it
|
||||
}.mapIndexed { index, src ->
|
||||
MangeContent(
|
||||
mChapterPageCount = durChapterPageCount,
|
||||
mChapterPagePos = durChapterPagePos,
|
||||
mChapterNextPagePos = durChapterPagePos.plus(1),
|
||||
mImageUrl = src,
|
||||
mDurChapterPos = index.plus(1)
|
||||
)
|
||||
}.toList().apply {
|
||||
this.forEach {
|
||||
it.mDurChapterCount = this.size
|
||||
}
|
||||
}
|
||||
val contentList = mutableListOf<Any>()
|
||||
contentList.add(
|
||||
ReaderLoading(
|
||||
durChapterPagePos,
|
||||
"阅读 ${chapter.title}",
|
||||
mNextChapterIndex = durChapterPagePos.plus(1)
|
||||
)
|
||||
)
|
||||
contentList.addAll(list)
|
||||
durChapterCount = contentList.size
|
||||
contentList.add(
|
||||
ReaderLoading(
|
||||
durChapterPagePos,
|
||||
"已读完 ${chapter.title}",
|
||||
mNextChapterIndex = durChapterPagePos.plus(1)
|
||||
)
|
||||
)
|
||||
runOnUI {
|
||||
mCallback?.loadContentFinish(contentList)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载下一章
|
||||
*/
|
||||
fun moveToNextChapter(index: Int) {
|
||||
|
||||
if (loading(index)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (index > durChapterPageCount - 1) {
|
||||
upToc(index)
|
||||
return
|
||||
}
|
||||
if (durChapterPagePos < simulatedChapterSize - 1) {
|
||||
durChapterPos = 0
|
||||
durChapterPagePos = index
|
||||
saveRead()
|
||||
loadContent(durChapterPagePos)
|
||||
AppLog.putDebug("moveToNextChapter-curPageChanged()")
|
||||
curPageChanged()
|
||||
} else {
|
||||
AppLog.putDebug("跳转下一章失败,没有下一章")
|
||||
}
|
||||
}
|
||||
|
||||
fun curPageChanged() {
|
||||
upReadTime()
|
||||
preDownload()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun upToc(index: Int) {
|
||||
val bookSource = bookSource ?: return
|
||||
val book = book ?: return
|
||||
if (!book.canUpdate) return
|
||||
if (System.currentTimeMillis() - book.lastCheckTime < 600000) return
|
||||
book.lastCheckTime = System.currentTimeMillis()
|
||||
WebBook.getChapterList(this, bookSource, book).onSuccess(IO) { cList ->
|
||||
if (book.bookUrl == ReadMange.book?.bookUrl
|
||||
&& cList.size > durChapterPageCount
|
||||
) {
|
||||
appDb.bookChapterDao.delByBook(book.bookUrl)
|
||||
appDb.bookChapterDao.insert(*cList.toTypedArray())
|
||||
saveRead()
|
||||
durChapterPos = 0
|
||||
durChapterPagePos = index
|
||||
durChapterPageCount = cList.size
|
||||
simulatedChapterSize = book.simulatedTotalChapterNum()
|
||||
loadContent(durChapterPagePos)
|
||||
} else {
|
||||
durChapterPagePos = durChapterPagePos.minus(1)
|
||||
saveRead()
|
||||
gameOver = true
|
||||
runOnUI {
|
||||
mCallback?.noData()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getContent(
|
||||
bookSource: BookSource,
|
||||
scope: CoroutineScope,
|
||||
chapter: BookChapter,
|
||||
book: Book,
|
||||
) {
|
||||
if (BookHelp.hasContent(book, chapter)) {
|
||||
BookHelp.getContent(book, chapter)?.apply {
|
||||
contentLoadFinish(chapter, this, book)
|
||||
runOnUI {
|
||||
mCallback?.loadComplete()
|
||||
}
|
||||
if (durChapterPagePos >= durChapterPageCount.minus(1)) {
|
||||
gameOver = true
|
||||
runOnUI {
|
||||
mCallback?.noData()
|
||||
}
|
||||
}
|
||||
} ?: downloadNetworkContent(bookSource, scope, chapter, book, success = {
|
||||
contentLoadFinish(chapter, it, book)
|
||||
runOnUI {
|
||||
mCallback?.loadComplete()
|
||||
}
|
||||
}, error = {
|
||||
removeLoading(chapter.index)
|
||||
runOnUI {
|
||||
mCallback?.loadFail("加载内容失败")
|
||||
}
|
||||
})
|
||||
} else {
|
||||
downloadNetworkContent(bookSource, scope, chapter, book, success = {
|
||||
contentLoadFinish(chapter, it, book)
|
||||
runOnUI {
|
||||
mCallback?.loadComplete()
|
||||
}
|
||||
}, error = {
|
||||
removeLoading(chapter.index)
|
||||
runOnUI {
|
||||
mCallback?.loadFail("加载内容失败")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private fun downloadNetworkContent(
|
||||
bookSource: BookSource,
|
||||
scope: CoroutineScope,
|
||||
chapter: BookChapter,
|
||||
book: Book,
|
||||
success: suspend (String) -> Unit = {},
|
||||
error: suspend () -> Unit = {},
|
||||
) {
|
||||
WebBook.getContent(
|
||||
scope,
|
||||
bookSource,
|
||||
book,
|
||||
chapter,
|
||||
start = CoroutineStart.LAZY,
|
||||
executeContext = IO,
|
||||
needSave = true
|
||||
).onSuccess { content ->
|
||||
success.invoke(content)
|
||||
}.onError {
|
||||
error.invoke()
|
||||
}.start()
|
||||
}
|
||||
|
||||
private fun preDownload() {
|
||||
if (book?.isLocal == true) return
|
||||
executor.execute {
|
||||
if (AppConfig.preDownloadNum < 2) {
|
||||
return@execute
|
||||
}
|
||||
preDownloadTask?.cancel()
|
||||
preDownloadTask = launch(IO) {
|
||||
//预下载
|
||||
launch {
|
||||
val maxChapterIndex =
|
||||
min(durChapterPagePos + AppConfig.preDownloadNum, durChapterPageCount)
|
||||
for (i in durChapterPagePos.plus(2)..maxChapterIndex) {
|
||||
if (downloadedChapters.contains(i)) continue
|
||||
if ((downloadFailChapters[i] ?: 0) >= 3) continue
|
||||
downloadIndex(i)
|
||||
}
|
||||
}
|
||||
launch {
|
||||
val minChapterIndex = durChapterPagePos - min(5, AppConfig.preDownloadNum)
|
||||
for (i in durChapterPagePos.minus(2) downTo minChapterIndex) {
|
||||
if (downloadedChapters.contains(i)) continue
|
||||
if ((downloadFailChapters[i] ?: 0) >= 3) continue
|
||||
downloadIndex(i)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun downloadIndex(index: Int) {
|
||||
if (index < 0) return
|
||||
if (index > durChapterPageCount - 1) {
|
||||
upToc()
|
||||
return
|
||||
}
|
||||
val book = book ?: return
|
||||
if (addDownloadLoading(index)) {
|
||||
try {
|
||||
appDb.bookChapterDao.getChapter(book.bookUrl, index)?.let { chapter ->
|
||||
if (BookHelp.hasContent(book, chapter)) {
|
||||
removeDownloadLoading(chapter.index)
|
||||
downloadedChapters.add(chapter.index)
|
||||
} else {
|
||||
delay(1000)
|
||||
downloadNetworkContent(
|
||||
bookSource!!,
|
||||
downloadScope,
|
||||
chapter,
|
||||
book,
|
||||
success = {
|
||||
downloadedChapters.add(chapter.index)
|
||||
downloadFailChapters.remove(chapter.index)
|
||||
},
|
||||
error = {
|
||||
downloadFailChapters[chapter.index] =
|
||||
(downloadFailChapters[chapter.index] ?: 0) + 1
|
||||
removeDownloadLoading(chapter.index)
|
||||
})
|
||||
}
|
||||
} ?: removeDownloadLoading(index)
|
||||
} catch (e: Exception) {
|
||||
removeLoading(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun upToc() {
|
||||
val bookSource = bookSource ?: return
|
||||
val book = book ?: return
|
||||
if (!book.canUpdate) return
|
||||
if (System.currentTimeMillis() - book.lastCheckTime < 600000) return
|
||||
book.lastCheckTime = System.currentTimeMillis()
|
||||
WebBook.getChapterList(this, bookSource, book).onSuccess(IO) { cList ->
|
||||
if (book.bookUrl == ReadMange.book?.bookUrl
|
||||
&& cList.size > durChapterPageCount
|
||||
) {
|
||||
appDb.bookChapterDao.delByBook(book.bookUrl)
|
||||
appDb.bookChapterDao.insert(*cList.toTypedArray())
|
||||
saveRead()
|
||||
durChapterPageCount = cList.size
|
||||
simulatedChapterSize = book.simulatedTotalChapterNum()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun uploadProgress(successAction: (() -> Unit)? = null) {
|
||||
book?.let {
|
||||
launch(IO) {
|
||||
AppWebDav.uploadBookProgress(it)
|
||||
ensureActive()
|
||||
it.update()
|
||||
successAction?.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 同步阅读进度
|
||||
* 如果当前进度快于服务器进度或者没有进度进行上传,如果慢与服务器进度则执行传入动作
|
||||
*/
|
||||
fun syncProgress(
|
||||
newProgressAction: ((progress: BookProgress) -> Unit)? = null,
|
||||
uploadSuccessAction: (() -> Unit)? = null,
|
||||
syncSuccessAction: (() -> Unit)? = null
|
||||
) {
|
||||
if (!AppConfig.syncBookProgress) return
|
||||
book?.let {
|
||||
Coroutine.async {
|
||||
AppWebDav.getBookProgress(it)
|
||||
}.onError {
|
||||
AppLog.put("拉取阅读进度失败", it)
|
||||
}.onSuccess { progress ->
|
||||
if (progress == null || progress.durChapterIndex < it.durChapterIndex ||
|
||||
(progress.durChapterIndex == it.durChapterIndex
|
||||
&& progress.durChapterPos < it.durChapterPos)
|
||||
) {
|
||||
// 服务器没有进度或者进度比服务器快,上传现有进度
|
||||
Coroutine.async {
|
||||
AppWebDav.uploadBookProgress(BookProgress(it), uploadSuccessAction)
|
||||
it.update()
|
||||
}
|
||||
} else if (progress.durChapterIndex > it.durChapterIndex ||
|
||||
progress.durChapterPos > it.durChapterPos
|
||||
) {
|
||||
// 进度比服务器慢,执行传入动作
|
||||
newProgressAction?.invoke(progress)
|
||||
} else {
|
||||
syncSuccessAction?.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setProgress(progress: BookProgress) {
|
||||
if (progress.durChapterIndex < durChapterPageCount &&
|
||||
(durChapterPagePos != progress.durChapterIndex
|
||||
|| durChapterPos != progress.durChapterPos)
|
||||
) {
|
||||
chapterChanged = true
|
||||
if (progress.durChapterIndex == durChapterPagePos) {
|
||||
durChapterPos = progress.durChapterPos
|
||||
mCallback?.adjustmentProgress()
|
||||
} else {
|
||||
durChapterPagePos = progress.durChapterIndex
|
||||
durChapterPos = progress.durChapterPos
|
||||
if (addLoading(durChapterPagePos)) {
|
||||
loadContent(durChapterPagePos)
|
||||
} else {
|
||||
Coroutine.async {
|
||||
val book = ReadMange.book!!
|
||||
appDb.bookChapterDao.getChapter(book.bookUrl, durChapterPagePos)
|
||||
?.let { chapter ->
|
||||
getContent(
|
||||
downloadScope,
|
||||
chapter,
|
||||
)
|
||||
}
|
||||
}.onError {
|
||||
AppLog.put("加载正文出错\n${it.localizedMessage}")
|
||||
}
|
||||
}
|
||||
}
|
||||
saveRead()
|
||||
}
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
fun loadContentFinish(list: MutableList<Any>)
|
||||
fun loadComplete()
|
||||
fun loadFail(msg: String)
|
||||
fun noData()
|
||||
fun adjustmentProgress()
|
||||
fun sureNewProgress(progress: BookProgress)
|
||||
val chapterList: MutableList<Any>
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package io.legado.app.model.recyclerView
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
import android.widget.Button
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.TextView
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import com.bumptech.glide.load.DataSource
|
||||
import com.bumptech.glide.load.engine.GlideException
|
||||
import com.bumptech.glide.request.RequestListener
|
||||
import com.bumptech.glide.request.target.Target
|
||||
import com.github.panpf.zoomimage.GlideZoomImageView
|
||||
import io.legado.app.R
|
||||
import io.legado.app.help.glide.progress.OnProgressListener
|
||||
import io.legado.app.help.glide.progress.ProgressManager
|
||||
import io.legado.app.model.BookCover
|
||||
import io.legado.app.model.ReadMange
|
||||
import io.legado.app.utils.getCompatDrawable
|
||||
import io.legado.app.utils.printOnDebug
|
||||
|
||||
open class MangaVH<VB : ViewBinding>(val binding: VB, private val context: Context) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
protected lateinit var mLoading: ProgressBar
|
||||
protected lateinit var mImage: GlideZoomImageView
|
||||
protected lateinit var mProgress: TextView
|
||||
protected lateinit var mFlProgress: FrameLayout
|
||||
protected var mRetry: Button? = null
|
||||
|
||||
fun initComponent(
|
||||
loading: ProgressBar,
|
||||
image: GlideZoomImageView,
|
||||
progress: TextView,
|
||||
button: Button? = null,
|
||||
flProgress: FrameLayout,
|
||||
) {
|
||||
mLoading = loading
|
||||
mImage = image
|
||||
mRetry = button
|
||||
mProgress = progress
|
||||
mFlProgress = flProgress
|
||||
}
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
fun loadImageWithRetry(imageUrl: String) {
|
||||
mFlProgress.isVisible = true
|
||||
mLoading.isVisible = true
|
||||
mRetry?.isGone = true
|
||||
mProgress.isVisible = true
|
||||
ProgressManager.removeListener(imageUrl)
|
||||
ProgressManager.addListener(imageUrl, object : OnProgressListener {
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun invoke(
|
||||
isComplete: Boolean,
|
||||
percentage: Int,
|
||||
bytesRead: Long,
|
||||
totalBytes: Long,
|
||||
) {
|
||||
mProgress.text = "${percentage}%"
|
||||
}
|
||||
})
|
||||
try {
|
||||
mImage.tag = imageUrl
|
||||
BookCover.loadManga(
|
||||
context,
|
||||
imageUrl,
|
||||
sourceOrigin = ReadMange.book?.origin,
|
||||
manga = true,
|
||||
useDefaultCover = context.getCompatDrawable(R.color.book_ant_10)
|
||||
).addListener(object : RequestListener<Drawable> {
|
||||
override fun onLoadFailed(
|
||||
e: GlideException?,
|
||||
model: Any?,
|
||||
target: Target<Drawable>,
|
||||
isFirstResource: Boolean,
|
||||
): Boolean {
|
||||
mFlProgress.isVisible = true
|
||||
mLoading.isGone = true
|
||||
mRetry?.isVisible = true
|
||||
mProgress.isGone = true
|
||||
itemView.updateLayoutParams<ViewGroup.LayoutParams> {
|
||||
height = MATCH_PARENT
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onResourceReady(
|
||||
resource: Drawable,
|
||||
model: Any,
|
||||
target: Target<Drawable>?,
|
||||
dataSource: DataSource,
|
||||
isFirstResource: Boolean,
|
||||
): Boolean {
|
||||
mFlProgress.isGone = true
|
||||
itemView.updateLayoutParams<ViewGroup.LayoutParams> {
|
||||
height = WRAP_CONTENT
|
||||
}
|
||||
return false
|
||||
}
|
||||
}).into(mImage)
|
||||
} catch (e: Exception) {
|
||||
e.printOnDebug()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package io.legado.app.model.recyclerView
|
||||
|
||||
|
||||
data class MangeContent(
|
||||
var mChapterPagePos: Int = 0,//总章节位置
|
||||
var mChapterPageCount:Int,//总章节数量
|
||||
var mChapterNextPagePos: Int = 0,//下一章
|
||||
val mImageUrl: String="",//当前URL
|
||||
var mDurChapterPos:Int=0,//当前章节位置
|
||||
var mDurChapterCount:Int=0//当前章节内容总数
|
||||
)
|
||||
@@ -0,0 +1,8 @@
|
||||
package io.legado.app.model.recyclerView
|
||||
|
||||
data class ReaderLoading(
|
||||
val mChapterPagePos: Int = 0,
|
||||
val mMessage: String? = null,
|
||||
val mNextChapterIndex: Int = 0,
|
||||
var mLoading: Boolean = false,
|
||||
)
|
||||
@@ -19,6 +19,7 @@ import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.help.config.LocalConfig
|
||||
import io.legado.app.lib.dialogs.alert
|
||||
import io.legado.app.lib.theme.primaryTextColor
|
||||
import io.legado.app.ui.book.manga.ReadMangaActivity
|
||||
import io.legado.app.ui.book.read.ReadBookActivity
|
||||
import io.legado.app.ui.book.search.SearchActivity
|
||||
import io.legado.app.utils.applyNavigationBarPadding
|
||||
@@ -26,7 +27,7 @@ import io.legado.app.utils.applyTint
|
||||
import io.legado.app.utils.cnCompare
|
||||
import io.legado.app.utils.getInt
|
||||
import io.legado.app.utils.putInt
|
||||
import io.legado.app.utils.startActivity
|
||||
import io.legado.app.utils.startReadOrMangaActivity
|
||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -76,16 +77,19 @@ class ReadRecordActivity : BaseActivity<ActivityReadRecordBinding>() {
|
||||
item.isChecked = true
|
||||
initData()
|
||||
}
|
||||
|
||||
R.id.menu_sort_read_long -> {
|
||||
sortMode = 1
|
||||
item.isChecked = true
|
||||
initData()
|
||||
}
|
||||
|
||||
R.id.menu_sort_read_time -> {
|
||||
sortMode = 2
|
||||
item.isChecked = true
|
||||
initData()
|
||||
}
|
||||
|
||||
R.id.menu_enable_record -> {
|
||||
AppConfig.enableReadRecord = !item.isChecked
|
||||
}
|
||||
@@ -165,7 +169,7 @@ class ReadRecordActivity : BaseActivity<ActivityReadRecordBinding>() {
|
||||
holder: ItemViewHolder,
|
||||
binding: ItemReadRecordBinding,
|
||||
item: ReadRecordShow,
|
||||
payloads: MutableList<Any>
|
||||
payloads: MutableList<Any>,
|
||||
) {
|
||||
binding.apply {
|
||||
tvBookName.text = item.bookName
|
||||
@@ -189,7 +193,7 @@ class ReadRecordActivity : BaseActivity<ActivityReadRecordBinding>() {
|
||||
if (book == null) {
|
||||
SearchActivity.start(this@ReadRecordActivity, item.bookName)
|
||||
} else {
|
||||
startActivity<ReadBookActivity> {
|
||||
startReadOrMangaActivity<ReadBookActivity, ReadMangaActivity>(book) {
|
||||
putExtra("bookUrl", book.bookUrl)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.lib.dialogs.alert
|
||||
import io.legado.app.lib.permission.Permissions
|
||||
import io.legado.app.lib.permission.PermissionsCompat
|
||||
import io.legado.app.ui.book.manga.ReadMangaActivity
|
||||
import io.legado.app.ui.book.read.ReadBookActivity
|
||||
import io.legado.app.ui.file.HandleFileContract
|
||||
import io.legado.app.utils.FileUtils
|
||||
@@ -26,6 +27,7 @@ import io.legado.app.utils.isContentScheme
|
||||
import io.legado.app.utils.readUri
|
||||
import io.legado.app.utils.showDialogFragment
|
||||
import io.legado.app.utils.startActivity
|
||||
import io.legado.app.utils.startReadOrMangaActivity
|
||||
import io.legado.app.utils.toastOnUi
|
||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
@@ -90,8 +92,8 @@ class FileAssociationActivity :
|
||||
}
|
||||
viewModel.openBookLiveData.observe(this) {
|
||||
binding.rotateLoading.gone()
|
||||
startActivity<ReadBookActivity> {
|
||||
putExtra("bookUrl", it)
|
||||
startReadOrMangaActivity<ReadBookActivity, ReadMangaActivity>(it) {
|
||||
putExtra("bookUrl", it.bookUrl)
|
||||
}
|
||||
finish()
|
||||
}
|
||||
|
||||
@@ -6,13 +6,14 @@ import androidx.lifecycle.MutableLiveData
|
||||
import io.legado.app.constant.AppLog
|
||||
import io.legado.app.constant.AppPattern
|
||||
import io.legado.app.constant.AppPattern.bookFileRegex
|
||||
import io.legado.app.data.entities.Book
|
||||
import io.legado.app.model.localBook.LocalBook
|
||||
import io.legado.app.utils.*
|
||||
|
||||
class FileAssociationViewModel(application: Application) : BaseAssociationViewModel(application) {
|
||||
val importBookLiveData = MutableLiveData<Uri>()
|
||||
val onLineImportLive = MutableLiveData<Uri>()
|
||||
val openBookLiveData = MutableLiveData<String>()
|
||||
val openBookLiveData = MutableLiveData<Book>()
|
||||
val notSupportedLiveData = MutableLiveData<Pair<Uri, String>>()
|
||||
|
||||
fun dispatchIntent(uri: Uri) {
|
||||
@@ -60,6 +61,6 @@ class FileAssociationViewModel(application: Application) : BaseAssociationViewMo
|
||||
|
||||
fun importBook(uri: Uri) {
|
||||
val book = LocalBook.importFile(uri)
|
||||
openBookLiveData.postValue(book.bookUrl)
|
||||
openBookLiveData.postValue(book)
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@ import io.legado.app.model.BookCover
|
||||
import io.legado.app.service.AudioPlayService
|
||||
import io.legado.app.ui.about.AppLogDialog
|
||||
import io.legado.app.ui.book.changesource.ChangeBookSourceDialog
|
||||
import io.legado.app.ui.book.manga.ReadMangaActivity
|
||||
import io.legado.app.ui.book.read.ReadBookActivity
|
||||
import io.legado.app.ui.book.source.edit.BookSourceEditActivity
|
||||
import io.legado.app.ui.book.toc.TocActivityResult
|
||||
@@ -45,6 +46,7 @@ import io.legado.app.utils.observeEventSticky
|
||||
import io.legado.app.utils.sendToClip
|
||||
import io.legado.app.utils.showDialogFragment
|
||||
import io.legado.app.utils.startActivity
|
||||
import io.legado.app.utils.startReadOrMangaActivity
|
||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
||||
import io.legado.app.utils.visible
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
@@ -218,7 +220,7 @@ class AudioPlayActivity :
|
||||
AudioPlay.book?.delete()
|
||||
appDb.bookDao.insert(book)
|
||||
}
|
||||
startActivity<ReadBookActivity> {
|
||||
startReadOrMangaActivity<ReadBookActivity,ReadMangaActivity>(book) {
|
||||
putExtra("bookUrl", book.bookUrl)
|
||||
}
|
||||
finish()
|
||||
|
||||
@@ -2,19 +2,20 @@ package io.legado.app.ui.book.import
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.MotionEvent
|
||||
import android.widget.EditText
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.lifecycle.ViewModel
|
||||
import io.legado.app.R
|
||||
import io.legado.app.base.VMBaseActivity
|
||||
import io.legado.app.constant.AppPattern
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.data.entities.Book
|
||||
import io.legado.app.databinding.ActivityImportBookBinding
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.lib.dialogs.alert
|
||||
import io.legado.app.lib.dialogs.selector
|
||||
import io.legado.app.lib.theme.primaryTextColor
|
||||
import io.legado.app.model.localBook.LocalBook
|
||||
import io.legado.app.ui.book.manga.ReadMangaActivity
|
||||
import io.legado.app.ui.book.read.ReadBookActivity
|
||||
import io.legado.app.ui.file.HandleFileContract
|
||||
import io.legado.app.utils.ArchiveUtils
|
||||
@@ -22,7 +23,7 @@ import io.legado.app.utils.FileDoc
|
||||
import io.legado.app.utils.applyTint
|
||||
import io.legado.app.utils.hideSoftInput
|
||||
import io.legado.app.utils.shouldHideSoftInput
|
||||
import io.legado.app.utils.startActivity
|
||||
import io.legado.app.utils.startReadOrMangaActivity
|
||||
import io.legado.app.utils.toastOnUi
|
||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
||||
import kotlin.coroutines.resume
|
||||
@@ -97,9 +98,9 @@ abstract class BaseImportBookActivity<VM : ViewModel> : VMBaseActivity<ActivityI
|
||||
|
||||
abstract fun onSearchTextChange(newText: String?)
|
||||
|
||||
protected fun startReadBook(bookUrl: String) {
|
||||
startActivity<ReadBookActivity> {
|
||||
putExtra("bookUrl", bookUrl)
|
||||
protected fun startReadBook(book: Book) {
|
||||
startReadOrMangaActivity<ReadBookActivity, ReadMangaActivity>(book) {
|
||||
putExtra("bookUrl", book.bookUrl)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,7 +111,7 @@ abstract class BaseImportBookActivity<VM : ViewModel> : VMBaseActivity<ActivityI
|
||||
if (fileNames.size == 1) {
|
||||
val name = fileNames[0]
|
||||
appDb.bookDao.getBookByFileName(name)?.let {
|
||||
startReadBook(it.bookUrl)
|
||||
startReadBook(it)
|
||||
} ?: showImportAlert(fileDoc, name)
|
||||
} else {
|
||||
showSelectBookReadAlert(fileDoc, fileNames)
|
||||
@@ -127,7 +128,7 @@ abstract class BaseImportBookActivity<VM : ViewModel> : VMBaseActivity<ActivityI
|
||||
fileNames
|
||||
) { _, name, _ ->
|
||||
appDb.bookDao.getBookByFileName(name)?.let {
|
||||
startReadBook(it.bookUrl)
|
||||
startReadBook(it)
|
||||
} ?: showImportAlert(fileDoc, name)
|
||||
}
|
||||
}
|
||||
@@ -136,12 +137,12 @@ abstract class BaseImportBookActivity<VM : ViewModel> : VMBaseActivity<ActivityI
|
||||
private inline fun addArchiveToBookShelf(
|
||||
fileDoc: FileDoc,
|
||||
fileName: String,
|
||||
onSuccess: (String) -> Unit
|
||||
onSuccess: (Book) -> Unit
|
||||
) {
|
||||
LocalBook.importArchiveFile(fileDoc.uri, fileName) {
|
||||
it.contains(fileName)
|
||||
}.firstOrNull()?.run {
|
||||
onSuccess.invoke(bookUrl)
|
||||
onSuccess.invoke(this)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -304,7 +304,7 @@ class ImportBookActivity : BaseImportBookActivity<ImportBookViewModel>(),
|
||||
override fun startRead(fileDoc: FileDoc) {
|
||||
if (!ArchiveUtils.isArchive(fileDoc.name)) {
|
||||
appDb.bookDao.getBookByFileName(fileDoc.name)?.let {
|
||||
startReadBook(it.bookUrl)
|
||||
startReadBook(it)
|
||||
}
|
||||
} else {
|
||||
onArchiveFileClick(fileDoc)
|
||||
|
||||
@@ -228,7 +228,7 @@ class RemoteBookActivity : BaseImportBookActivity<RemoteBookViewModel>(),
|
||||
val downloadFileName = remoteBook.filename
|
||||
if (!ArchiveUtils.isArchive(downloadFileName)) {
|
||||
appDb.bookDao.getBookByFileName(downloadFileName)?.let {
|
||||
startReadBook(it.bookUrl)
|
||||
startReadBook(it)
|
||||
}
|
||||
} else {
|
||||
AppConfig.defaultBookTreeUri ?: return
|
||||
|
||||
@@ -27,6 +27,7 @@ import io.legado.app.help.AppWebDav
|
||||
import io.legado.app.help.book.addType
|
||||
import io.legado.app.help.book.getRemoteUrl
|
||||
import io.legado.app.help.book.isAudio
|
||||
import io.legado.app.help.book.isImage
|
||||
import io.legado.app.help.book.isLocal
|
||||
import io.legado.app.help.book.isLocalTxt
|
||||
import io.legado.app.help.book.isWebFile
|
||||
@@ -47,6 +48,7 @@ import io.legado.app.ui.book.changecover.ChangeCoverDialog
|
||||
import io.legado.app.ui.book.changesource.ChangeBookSourceDialog
|
||||
import io.legado.app.ui.book.group.GroupSelectDialog
|
||||
import io.legado.app.ui.book.info.edit.BookInfoEditActivity
|
||||
import io.legado.app.ui.book.manga.ReadMangaActivity
|
||||
import io.legado.app.ui.book.read.ReadBookActivity
|
||||
import io.legado.app.ui.book.read.ReadBookActivity.Companion.RESULT_DELETED
|
||||
import io.legado.app.ui.book.search.SearchActivity
|
||||
@@ -310,7 +312,7 @@ class BookInfoActivity :
|
||||
|
||||
private fun upLoadBook(
|
||||
book: Book,
|
||||
bookWebDav: RemoteBookWebDav? = AppWebDav.defaultBookWebDav
|
||||
bookWebDav: RemoteBookWebDav? = AppWebDav.defaultBookWebDav,
|
||||
) {
|
||||
lifecycleScope.launch {
|
||||
waitDialog.setText("上传中.....")
|
||||
@@ -604,7 +606,7 @@ class BookInfoActivity :
|
||||
}
|
||||
|
||||
private fun showWebFileDownloadAlert(
|
||||
onClick: ((Book) -> Unit)? = null
|
||||
onClick: ((Book) -> Unit)? = null,
|
||||
) {
|
||||
val webFiles = viewModel.webFiles
|
||||
if (webFiles.isEmpty()) {
|
||||
@@ -653,7 +655,7 @@ class BookInfoActivity :
|
||||
private fun showDecompressFileImportAlert(
|
||||
archiveFileUri: Uri,
|
||||
fileNames: List<String>,
|
||||
success: ((Book) -> Unit)? = null
|
||||
success: ((Book) -> Unit)? = null,
|
||||
) {
|
||||
if (fileNames.isEmpty()) {
|
||||
toastOnUi(R.string.unsupport_archivefile_entry)
|
||||
@@ -693,7 +695,10 @@ class BookInfoActivity :
|
||||
)
|
||||
|
||||
else -> readBookResult.launch(
|
||||
Intent(this, ReadBookActivity::class.java)
|
||||
Intent(
|
||||
this,
|
||||
if (book.isImage&&AppConfig.showMangaUi) ReadMangaActivity::class.java else ReadBookActivity::class.java
|
||||
)
|
||||
.putExtra("bookUrl", book.bookUrl)
|
||||
.putExtra("inBookshelf", viewModel.inBookshelf)
|
||||
.putExtra("tocChanged", tocChanged)
|
||||
|
||||
@@ -0,0 +1,240 @@
|
||||
package io.legado.app.ui.book.manga
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Intent
|
||||
import io.legado.app.R
|
||||
import io.legado.app.base.BaseViewModel
|
||||
import io.legado.app.constant.AppLog
|
||||
import io.legado.app.constant.BookType
|
||||
import io.legado.app.constant.EventBus
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.data.entities.Book
|
||||
import io.legado.app.data.entities.BookChapter
|
||||
import io.legado.app.exception.NoStackTraceException
|
||||
import io.legado.app.help.book.BookHelp
|
||||
import io.legado.app.help.book.isLocal
|
||||
import io.legado.app.help.book.isLocalModified
|
||||
import io.legado.app.help.book.removeType
|
||||
import io.legado.app.help.book.simulatedTotalChapterNum
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.help.coroutine.Coroutine
|
||||
import io.legado.app.model.ReadMange
|
||||
import io.legado.app.model.localBook.LocalBook
|
||||
import io.legado.app.model.webBook.WebBook
|
||||
import io.legado.app.utils.mapParallelSafe
|
||||
import io.legado.app.utils.postEvent
|
||||
import io.legado.app.utils.toastOnUi
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.onCompletion
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.onEmpty
|
||||
import kotlinx.coroutines.flow.onStart
|
||||
import kotlinx.coroutines.flow.take
|
||||
|
||||
class MangaViewModel(application: Application) : BaseViewModel(application) {
|
||||
|
||||
private var changeSourceCoroutine: Coroutine<*>? = null
|
||||
|
||||
/**
|
||||
* 初始化
|
||||
*/
|
||||
fun initData(intent: Intent, success: (() -> Unit)? = null) {
|
||||
execute {
|
||||
ReadMange.inBookshelf = intent.getBooleanExtra("inBookshelf", true)
|
||||
ReadMange.tocChanged = intent.getBooleanExtra("tocChanged", false)
|
||||
val bookUrl = intent.getStringExtra("bookUrl")
|
||||
val book = when {
|
||||
bookUrl.isNullOrEmpty() -> appDb.bookDao.lastReadBook
|
||||
else -> appDb.bookDao.getBook(bookUrl)
|
||||
} ?: ReadMange.book
|
||||
when {
|
||||
book != null -> initMange(book)
|
||||
else -> context.getString(R.string.no_book)//没有找到书
|
||||
}
|
||||
}.onSuccess {
|
||||
success?.invoke()
|
||||
}.onError {
|
||||
val msg = "初始化数据失败\n${it.localizedMessage}"
|
||||
AppLog.put(msg, it)
|
||||
}.onFinally {
|
||||
ReadMange.saveRead()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun initMange(book: Book) {
|
||||
val isSameBook = ReadMange.book?.bookUrl == book.bookUrl
|
||||
if (isSameBook) {
|
||||
ReadMange.upData(book)
|
||||
} else {
|
||||
ReadMange.resetData(book)
|
||||
}
|
||||
if (!book.isLocal && book.tocUrl.isEmpty() && !loadBookInfo(book)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (book.isLocal && !checkLocalBookFileExist(book)) {
|
||||
return
|
||||
}
|
||||
|
||||
if ((ReadMange.durChapterPageCount == 0 || book.isLocalModified()) && !loadChapterListAwait(
|
||||
book
|
||||
)
|
||||
) {
|
||||
return
|
||||
}
|
||||
ensureChapterExist()
|
||||
|
||||
//开始加载内容
|
||||
ReadMange.loadContent()
|
||||
|
||||
//自动换源
|
||||
if (!book.isLocal && ReadMange.bookSource == null) {
|
||||
autoChangeSource(book.name, book.author)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadChapterListAwait(book: Book): Boolean {
|
||||
ReadMange.bookSource?.let {
|
||||
val oldBook = book.copy()
|
||||
WebBook.getChapterListAwait(it, book, true).onSuccess { cList ->
|
||||
if (oldBook.bookUrl == book.bookUrl) {
|
||||
appDb.bookDao.update(book)
|
||||
} else {
|
||||
appDb.bookDao.insert(book)
|
||||
BookHelp.updateCacheFolder(oldBook, book)
|
||||
}
|
||||
appDb.bookChapterDao.delByBook(oldBook.bookUrl)
|
||||
appDb.bookChapterDao.insert(*cList.toTypedArray())
|
||||
ReadMange.durChapterPageCount = cList.size
|
||||
ReadMange.simulatedChapterSize = book.simulatedTotalChapterNum()
|
||||
return true
|
||||
}.onFailure {
|
||||
//加载章节出错
|
||||
ReadMange.mCallback?.loadFail("加载目录失败")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载详情页
|
||||
*/
|
||||
private suspend fun loadBookInfo(book: Book): Boolean {
|
||||
val source = ReadMange.bookSource ?: return true
|
||||
try {
|
||||
WebBook.getBookInfoAwait(source, book, canReName = false)
|
||||
return true
|
||||
} catch (e: Throwable) {
|
||||
// 加载详情页失败
|
||||
// ReadBook.upMsg("详情页出错: ${e.localizedMessage}")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private fun ensureChapterExist() {
|
||||
if (ReadMange.simulatedChapterSize > 0 && ReadMange.durChapterPagePos > ReadMange.simulatedChapterSize - 1) {
|
||||
ReadMange.durChapterPagePos = ReadMange.simulatedChapterSize - 1
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动换源
|
||||
*/
|
||||
private fun autoChangeSource(name: String, author: String) {
|
||||
if (!AppConfig.autoChangeSource) return
|
||||
execute {
|
||||
val sources = appDb.bookSourceDao.allTextEnabledPart
|
||||
flow {
|
||||
for (source in sources) {
|
||||
source.getBookSource()?.let {
|
||||
emit(it)
|
||||
}
|
||||
}
|
||||
}.onStart {
|
||||
// 自动换源
|
||||
|
||||
}.mapParallelSafe(AppConfig.threadCount) { source ->
|
||||
val book = WebBook.preciseSearchAwait(this, source, name, author).getOrThrow()
|
||||
if (book.tocUrl.isEmpty()) {
|
||||
WebBook.getBookInfoAwait(source, book)
|
||||
}
|
||||
val toc = WebBook.getChapterListAwait(source, book).getOrThrow()
|
||||
val chapter = toc.getOrElse(book.durChapterIndex) {
|
||||
toc.last()
|
||||
}
|
||||
val nextChapter = toc.getOrElse(chapter.index) {
|
||||
toc.first()
|
||||
}
|
||||
WebBook.getContentAwait(
|
||||
bookSource = source,
|
||||
book = book,
|
||||
bookChapter = chapter,
|
||||
nextChapterUrl = nextChapter.url
|
||||
)
|
||||
book to toc
|
||||
}.take(1).onEach { (book, toc) ->
|
||||
changeTo(book, toc)
|
||||
}.onEmpty {
|
||||
throw NoStackTraceException("没有合适书源")
|
||||
}.onCompletion {
|
||||
// 换源完成
|
||||
}.catch {
|
||||
AppLog.put("自动换源失败\n${it.localizedMessage}", it)
|
||||
context.toastOnUi("自动换源失败\n${it.localizedMessage}")
|
||||
}.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 换源
|
||||
*/
|
||||
fun changeTo(book: Book, toc: List<BookChapter>) {
|
||||
changeSourceCoroutine?.cancel()
|
||||
changeSourceCoroutine = execute {
|
||||
//换源中
|
||||
ReadMange.book?.migrateTo(book, toc)
|
||||
book.removeType(BookType.updateError)
|
||||
ReadMange.book?.delete()
|
||||
appDb.bookDao.insert(book)
|
||||
appDb.bookChapterDao.insert(*toc.toTypedArray())
|
||||
ReadMange.resetData(book)
|
||||
toc.find { it.title.contains(ReadMange.chapterTitle) }?.run {
|
||||
ReadMange.loadContent(index)
|
||||
} ?: ReadMange.loadContent()
|
||||
}.onError {
|
||||
AppLog.put("换源失败\n$it", it, true)
|
||||
}.onFinally {
|
||||
postEvent(EventBus.SOURCE_CHANGED, book.bookUrl)
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkLocalBookFileExist(book: Book): Boolean {
|
||||
try {
|
||||
LocalBook.getBookInputStream(book)
|
||||
return true
|
||||
} catch (e: Throwable) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
fun openChapter(index: Int, durChapterPos: Int = 0) {
|
||||
if (index < ReadMange.durChapterPageCount) {
|
||||
ReadMange.chapterChanged = true
|
||||
ReadMange.durChapterPagePos = index
|
||||
ReadMange.durChapterPos = durChapterPos
|
||||
ReadMange.saveRead()
|
||||
ReadMange.loadContent(index)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
changeSourceCoroutine?.cancel()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,483 @@
|
||||
package io.legado.app.ui.book.manga
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Looper
|
||||
import android.view.KeyEvent
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.WindowInsets
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader
|
||||
import com.bumptech.glide.request.target.Target.SIZE_ORIGINAL
|
||||
import com.bumptech.glide.util.FixedPreloadSizeProvider
|
||||
import io.legado.app.BuildConfig
|
||||
import io.legado.app.R
|
||||
import io.legado.app.base.VMBaseActivity
|
||||
import io.legado.app.data.entities.Book
|
||||
import io.legado.app.data.entities.BookChapter
|
||||
import io.legado.app.data.entities.BookProgress
|
||||
import io.legado.app.data.entities.BookSource
|
||||
import io.legado.app.databinding.ActivityMangeBinding
|
||||
import io.legado.app.databinding.ViewLoadMoreBinding
|
||||
import io.legado.app.help.book.isImage
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.help.config.ReadBookConfig
|
||||
import io.legado.app.help.storage.Backup
|
||||
import io.legado.app.lib.dialogs.alert
|
||||
import io.legado.app.lib.theme.ThemeStore
|
||||
import io.legado.app.model.ReadMange
|
||||
import io.legado.app.model.ReadMange.mFirstLoading
|
||||
import io.legado.app.model.recyclerView.MangeContent
|
||||
import io.legado.app.model.recyclerView.ReaderLoading
|
||||
import io.legado.app.receiver.NetworkChangedListener
|
||||
import io.legado.app.ui.book.changesource.ChangeBookSourceDialog
|
||||
import io.legado.app.ui.book.info.BookInfoActivity
|
||||
import io.legado.app.ui.book.manga.rv.MangaAdapter
|
||||
import io.legado.app.ui.book.read.MangaMenu
|
||||
import io.legado.app.ui.book.read.ReadBookActivity.Companion.RESULT_DELETED
|
||||
import io.legado.app.ui.book.toc.TocActivityResult
|
||||
import io.legado.app.ui.widget.recycler.LoadMoreView
|
||||
import io.legado.app.utils.ColorUtils
|
||||
import io.legado.app.utils.NetworkUtils
|
||||
import io.legado.app.utils.StartActivityContract
|
||||
import io.legado.app.utils.getCompatColor
|
||||
import io.legado.app.utils.gone
|
||||
import io.legado.app.utils.immersionFullScreen
|
||||
import io.legado.app.utils.printOnDebug
|
||||
import io.legado.app.utils.setLightStatusBar
|
||||
import io.legado.app.utils.setNavigationBarColorAuto
|
||||
import io.legado.app.utils.showDialogFragment
|
||||
import io.legado.app.utils.toastOnUi
|
||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
||||
import io.legado.app.utils.visible
|
||||
|
||||
class ReadMangaActivity : VMBaseActivity<ActivityMangeBinding, MangaViewModel>(),
|
||||
ReadMange.Callback, ChangeBookSourceDialog.CallBack, MangaMenu.CallBack {
|
||||
|
||||
private val menuLayoutIsVisible get() = binding.mangaMenu.isVisible
|
||||
|
||||
private val mLayoutManager by lazy {
|
||||
LinearLayoutManager(this@ReadMangaActivity)
|
||||
}
|
||||
private val mAdapter: MangaAdapter by lazy {
|
||||
MangaAdapter(this@ReadMangaActivity)
|
||||
}
|
||||
private val mSizeProvider by lazy {
|
||||
FixedPreloadSizeProvider<Any>(
|
||||
this@ReadMangaActivity.resources.displayMetrics.widthPixels,
|
||||
SIZE_ORIGINAL
|
||||
)
|
||||
}
|
||||
|
||||
private val mRecyclerViewPreloader by lazy {
|
||||
RecyclerViewPreloader(Glide.with(this), mAdapter, mSizeProvider, 10)
|
||||
}
|
||||
|
||||
private val networkChangedListener by lazy {
|
||||
NetworkChangedListener(this)
|
||||
}
|
||||
|
||||
private var justInitData: Boolean = false
|
||||
private var syncDialog: AlertDialog? = null
|
||||
|
||||
private val loadMoreView by lazy {
|
||||
LoadMoreView(this).apply {
|
||||
setBackgroundColor(getCompatColor(R.color.book_ant_10))
|
||||
getLoading().loadingColor = getCompatColor(R.color.white)
|
||||
getLoadingText().setTextColor(getCompatColor(R.color.white))
|
||||
}
|
||||
}
|
||||
|
||||
//打开目录返回选择章节返回结果
|
||||
private val tocActivity =
|
||||
registerForActivityResult(TocActivityResult()) {
|
||||
it?.let {
|
||||
binding.flLoading.isVisible = true
|
||||
viewModel.openChapter(it.first, it.second)
|
||||
}
|
||||
}
|
||||
private val bookInfoActivity =
|
||||
registerForActivityResult(StartActivityContract(BookInfoActivity::class.java)) {
|
||||
if (it.resultCode == RESULT_OK) {
|
||||
setResult(RESULT_DELETED)
|
||||
super.finish()
|
||||
}
|
||||
}
|
||||
override val binding by viewBinding(ActivityMangeBinding::inflate)
|
||||
override val viewModel by viewModels<MangaViewModel>()
|
||||
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
immersionFullScreen(WindowInsetsControllerCompat(window, binding.root))
|
||||
ReadMange.register(this)
|
||||
binding.mRecyclerMange.run {
|
||||
adapter = mAdapter
|
||||
itemAnimator = null
|
||||
layoutManager = mLayoutManager
|
||||
setHasFixedSize(true)
|
||||
mLayoutManager.initialPrefetchItemCount = 4
|
||||
mLayoutManager.isItemPrefetchEnabled = true
|
||||
setItemViewCacheSize(AppConfig.preDownloadNum)
|
||||
setPreScrollListener { _, _, dy, position ->
|
||||
if (dy > 0 && position + 2 > mAdapter.getCurrentList().size - 3) {
|
||||
if (mAdapter.getCurrentList().last() is ReaderLoading) {
|
||||
val nextIndex =
|
||||
(mAdapter.getCurrentList().last() as ReaderLoading).mNextChapterIndex
|
||||
if (nextIndex != -1) {
|
||||
scrollToBottom(false, nextIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
setNestedPreScrollListener { _, _, _, position ->
|
||||
if (mAdapter.getCurrentList()
|
||||
.isNotEmpty() && position <= mAdapter.getCurrentList().lastIndex
|
||||
) {
|
||||
try {
|
||||
val content = mAdapter.getCurrentList()[position]
|
||||
if (content is MangeContent) {
|
||||
ReadMange.durChapterPos = content.mDurChapterPos.minus(1)
|
||||
upText(
|
||||
content.mChapterPagePos,
|
||||
content.mChapterPageCount,
|
||||
content.mDurChapterPos,
|
||||
content.mDurChapterCount
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printOnDebug()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
addOnScrollListener(mRecyclerViewPreloader)
|
||||
onToucheMiddle {
|
||||
if (!binding.mangaMenu.isVisible) {
|
||||
binding.mangaMenu.runMenuIn()
|
||||
}
|
||||
}
|
||||
}
|
||||
binding.retry.setOnClickListener {
|
||||
binding.llLoading.isVisible = true
|
||||
binding.llRetry.isGone = true
|
||||
mFirstLoading = false
|
||||
ReadMange.loadContent()
|
||||
}
|
||||
|
||||
mAdapter.addFooterView {
|
||||
ViewLoadMoreBinding.bind(loadMoreView)
|
||||
}
|
||||
loadMoreView.setOnClickListener {
|
||||
if (!loadMoreView.isLoading && !ReadMange.gameOver) {
|
||||
scrollToBottom(true, ReadMange.durChapterPagePos)
|
||||
}
|
||||
}
|
||||
loadMoreView.gone()
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
viewModel.initData(intent)
|
||||
}
|
||||
|
||||
private fun scrollToBottom(forceLoad: Boolean = false, index: Int) {
|
||||
if ((loadMoreView.hasMore && !loadMoreView.isLoading) && !ReadMange.gameOver || forceLoad) {
|
||||
loadMoreView.hasMore()
|
||||
ReadMange.moveToNextChapter(index)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPostCreate(savedInstanceState: Bundle?) {
|
||||
super.onPostCreate(savedInstanceState)
|
||||
Looper.myQueue().addIdleHandler {
|
||||
viewModel.initData(intent)
|
||||
false
|
||||
}
|
||||
justInitData = true
|
||||
}
|
||||
|
||||
override fun loadContentFinish(list: MutableList<Any>) {
|
||||
if (!this.isDestroyed) {
|
||||
mAdapter.submitList(list) {
|
||||
if (!mFirstLoading) {
|
||||
if (list.size > 1) {
|
||||
binding.infobar.isVisible = true
|
||||
upText(
|
||||
ReadMange.durChapterPagePos,
|
||||
ReadMange.durChapterPageCount,
|
||||
ReadMange.durChapterPos,
|
||||
ReadMange.durChapterCount
|
||||
)
|
||||
}
|
||||
|
||||
if (ReadMange.durChapterPos + 2 > mAdapter.getCurrentList().size - 3) {
|
||||
val nextIndex =
|
||||
(mAdapter.getCurrentList().last() as ReaderLoading).mNextChapterIndex
|
||||
scrollToBottom(index = nextIndex)
|
||||
} else {
|
||||
binding.mRecyclerMange.scrollToPosition(ReadMange.durChapterPos)
|
||||
}
|
||||
}
|
||||
|
||||
if (ReadMange.chapterChanged) {
|
||||
binding.mRecyclerMange.scrollToPosition(ReadMange.durChapterPos)
|
||||
}
|
||||
|
||||
ReadMange.chapterChanged = false
|
||||
loadMoreView.visible()
|
||||
mFirstLoading = true
|
||||
loadMoreView.stopLoad()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun upText(
|
||||
chapterPagePos: Int, chapterPageCount: Int, chapterPos: Int, chapterCount: Int,
|
||||
) {
|
||||
binding.infobar.update(
|
||||
chapterPagePos,
|
||||
chapterPageCount,
|
||||
chapterPagePos.minus(1f).div(chapterPageCount.minus(1f)),
|
||||
chapterPos,
|
||||
chapterCount
|
||||
)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
networkChangedListener.register()
|
||||
networkChangedListener.onNetworkChanged = {
|
||||
// 当网络是可用状态且无需初始化时同步进度(初始化中已有同步进度逻辑)
|
||||
if (AppConfig.syncBookProgressPlus && NetworkUtils.isAvailable()&&!justInitData) {
|
||||
ReadMange.syncProgress({ progress -> sureNewProgress(progress) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
if (ReadMange.inBookshelf) {
|
||||
ReadMange.saveRead()
|
||||
if (!BuildConfig.DEBUG) {
|
||||
if (AppConfig.syncBookProgressPlus) {
|
||||
ReadMange.syncProgress()
|
||||
} else {
|
||||
ReadMange.uploadProgress()
|
||||
}
|
||||
Backup.autoBack(this)
|
||||
}
|
||||
}
|
||||
networkChangedListener.unRegister()
|
||||
}
|
||||
|
||||
override fun loadComplete() {
|
||||
binding.flLoading.isGone = true
|
||||
}
|
||||
|
||||
override fun loadFail(msg: String) {
|
||||
if (!mFirstLoading || ReadMange.chapterChanged) {
|
||||
binding.llLoading.isGone = true
|
||||
binding.llRetry.isVisible = true
|
||||
binding.tvMsg.text = msg
|
||||
} else {
|
||||
loadMoreView.error(null, "加载失败,点击重试")
|
||||
}
|
||||
}
|
||||
|
||||
override fun noData() {
|
||||
loadMoreView.noMore("暂无章节了!")
|
||||
}
|
||||
|
||||
override fun adjustmentProgress() {
|
||||
if (ReadMange.chapterChanged) {
|
||||
binding.mRecyclerMange.scrollToPosition(ReadMange.durChapterPos)
|
||||
binding.flLoading.isGone = true
|
||||
}
|
||||
}
|
||||
|
||||
override val chapterList: MutableList<Any>
|
||||
get() = mAdapter.getCurrentList()
|
||||
|
||||
override fun onDestroy() {
|
||||
ReadMange.unregister()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onLowMemory() {
|
||||
super.onLowMemory()
|
||||
Glide.get(this).clearMemory()
|
||||
}
|
||||
|
||||
override fun sureNewProgress(progress: BookProgress) {
|
||||
syncDialog?.dismiss()
|
||||
syncDialog = alert(R.string.get_book_progress) {
|
||||
setMessage(R.string.cloud_progress_exceeds_current)
|
||||
okButton {
|
||||
binding.flLoading.isVisible = true
|
||||
ReadMange.setProgress(progress)
|
||||
}
|
||||
noButton()
|
||||
}
|
||||
}
|
||||
|
||||
override val oldBook: Book?
|
||||
get() = ReadMange.book
|
||||
|
||||
override fun changeTo(source: BookSource, book: Book, toc: List<BookChapter>) {
|
||||
if (book.isImage) {
|
||||
binding.flLoading.isVisible = true
|
||||
ReadMange.chapterChanged = true
|
||||
viewModel.changeTo(book, toc)
|
||||
} else {
|
||||
toastOnUi("所选择的源不是漫画源")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onCompatCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.book_manga, menu)
|
||||
return super.onCompatCreateOptionsMenu(menu)
|
||||
}
|
||||
|
||||
/**
|
||||
* 菜单
|
||||
*/
|
||||
override fun onCompatOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.menu_change_source -> {
|
||||
binding.mangaMenu.runMenuOut()
|
||||
ReadMange.book?.let {
|
||||
showDialogFragment(ChangeBookSourceDialog(it.name, it.author))
|
||||
}
|
||||
}
|
||||
|
||||
R.id.menu_catalog -> {
|
||||
ReadMange.book?.let {
|
||||
tocActivity.launch(it.bookUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
return super.onCompatOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
override fun upNavigationBarColor() {
|
||||
when {
|
||||
binding.mangaMenu.isVisible -> super.upNavigationBarColor()
|
||||
!AppConfig.immNavigationBar -> super.upNavigationBarColor()
|
||||
else -> setNavigationBarColorAuto(ReadBookConfig.bgMeanColor)
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("RtlHardcoded")
|
||||
private fun upNavigationBar(value: Boolean) {
|
||||
binding.mangaMenu.isVisible = value
|
||||
}
|
||||
|
||||
override fun openBookInfoActivity() {
|
||||
ReadMange.book?.let {
|
||||
bookInfoActivity.launch {
|
||||
putExtra("name", it.name)
|
||||
putExtra("author", it.author)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun upSystemUiVisibility(value: Boolean) {
|
||||
upSystemUiVisibility(isInMultiWindow, !menuLayoutIsVisible, false)
|
||||
upNavigationBarColor()
|
||||
upNavigationBar(value)
|
||||
if (!value) {
|
||||
immersionFullScreen(WindowInsetsControllerCompat(window, binding.root))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新状态栏,导航栏
|
||||
*/
|
||||
fun upSystemUiVisibility(
|
||||
isInMultiWindow: Boolean,
|
||||
toolBarHide: Boolean = true,
|
||||
useBgMeanColor: Boolean = false,
|
||||
) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
window.insetsController?.run {
|
||||
if (toolBarHide && ReadBookConfig.hideNavigationBar) {
|
||||
hide(WindowInsets.Type.navigationBars())
|
||||
} else {
|
||||
show(WindowInsets.Type.navigationBars())
|
||||
}
|
||||
if (toolBarHide && ReadBookConfig.hideStatusBar) {
|
||||
hide(WindowInsets.Type.statusBars())
|
||||
} else {
|
||||
show(WindowInsets.Type.statusBars())
|
||||
}
|
||||
}
|
||||
}
|
||||
upSystemUiVisibilityO(isInMultiWindow, toolBarHide)
|
||||
if (toolBarHide) {
|
||||
setLightStatusBar(ReadBookConfig.durConfig.curStatusIconDark())
|
||||
} else {
|
||||
val statusBarColor =
|
||||
if (AppConfig.readBarStyleFollowPage
|
||||
&& ReadBookConfig.durConfig.curBgType() == 0
|
||||
|| useBgMeanColor
|
||||
) {
|
||||
ReadBookConfig.bgMeanColor
|
||||
} else {
|
||||
ThemeStore.statusBarColor(this, AppConfig.isTransparentStatusBar)
|
||||
}
|
||||
setLightStatusBar(ColorUtils.isColorLight(statusBarColor))
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun upSystemUiVisibilityO(
|
||||
isInMultiWindow: Boolean,
|
||||
toolBarHide: Boolean = true,
|
||||
) {
|
||||
var flag = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
||||
or View.SYSTEM_UI_FLAG_IMMERSIVE
|
||||
or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY)
|
||||
if (!isInMultiWindow) {
|
||||
flag = flag or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
||||
}
|
||||
if (ReadBookConfig.hideNavigationBar) {
|
||||
flag = flag or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
if (toolBarHide) {
|
||||
flag = flag or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||
}
|
||||
}
|
||||
if (ReadBookConfig.hideStatusBar && toolBarHide) {
|
||||
flag = flag or View.SYSTEM_UI_FLAG_FULLSCREEN
|
||||
}
|
||||
window.decorView.systemUiVisibility = flag
|
||||
}
|
||||
|
||||
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
|
||||
val keyCode = event.keyCode
|
||||
val action = event.action
|
||||
val isDown = action == 0
|
||||
|
||||
if (keyCode == KeyEvent.KEYCODE_MENU) {
|
||||
if (isDown && !binding.mangaMenu.canShowMenu) {
|
||||
binding.mangaMenu.runMenuIn()
|
||||
return true
|
||||
}
|
||||
if (!isDown && !binding.mangaMenu.canShowMenu) {
|
||||
binding.mangaMenu.canShowMenu = true
|
||||
return true
|
||||
}
|
||||
}
|
||||
return super.dispatchKeyEvent(event)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package io.legado.app.ui.book.manga.rv
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.GestureDetector
|
||||
import android.view.MotionEvent
|
||||
import android.view.ViewConfiguration
|
||||
import kotlin.math.abs
|
||||
|
||||
open class GestureDetectorWithLongTap(
|
||||
context: Context,
|
||||
listener: Listener,
|
||||
) : GestureDetector(context, listener) {
|
||||
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
private val slop = ViewConfiguration.get(context).scaledTouchSlop
|
||||
private val longTapTime = ViewConfiguration.getLongPressTimeout().toLong()
|
||||
private val doubleTapTime = ViewConfiguration.getDoubleTapTimeout().toLong()
|
||||
|
||||
private var downX = 0f
|
||||
private var downY = 0f
|
||||
private var lastUp = 0L
|
||||
private var lastDownEvent: MotionEvent? = null
|
||||
|
||||
|
||||
private val longTapFn = Runnable { listener.onLongTapConfirmed(lastDownEvent!!) }
|
||||
|
||||
override fun onTouchEvent(ev: MotionEvent): Boolean {
|
||||
when (ev.actionMasked) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
lastDownEvent?.recycle()
|
||||
lastDownEvent = MotionEvent.obtain(ev)
|
||||
|
||||
if (ev.downTime - lastUp > doubleTapTime) {
|
||||
downX = ev.rawX
|
||||
downY = ev.rawY
|
||||
handler.postDelayed(longTapFn, longTapTime)
|
||||
}
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
if (abs(ev.rawX - downX) > slop || abs(ev.rawY - downY) > slop) {
|
||||
handler.removeCallbacks(longTapFn)
|
||||
}
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_UP -> {
|
||||
lastUp = ev.eventTime
|
||||
handler.removeCallbacks(longTapFn)
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_POINTER_DOWN -> {
|
||||
handler.removeCallbacks(longTapFn)
|
||||
}
|
||||
}
|
||||
return super.onTouchEvent(ev)
|
||||
}
|
||||
|
||||
open class Listener : SimpleOnGestureListener() {
|
||||
open fun onLongTapConfirmed(ev: MotionEvent) {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
package io.legado.app.ui.book.manga.rv
|
||||
|
||||
import android.content.Context
|
||||
import android.util.SparseArray
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||
import androidx.annotation.IntRange
|
||||
import androidx.core.view.updateLayoutParams
|
||||
import androidx.recyclerview.widget.AsyncListDiffer
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.ListPreloader.PreloadModelProvider
|
||||
import com.bumptech.glide.RequestBuilder
|
||||
import com.github.panpf.zoomimage.zoom.GestureType
|
||||
import io.legado.app.R
|
||||
import io.legado.app.base.adapter.ItemViewHolder
|
||||
import io.legado.app.base.adapter.RecyclerAdapter.Companion.TYPE_FOOTER_VIEW
|
||||
import io.legado.app.databinding.BookComicLoadingRvBinding
|
||||
import io.legado.app.databinding.BookComicRvBinding
|
||||
import io.legado.app.help.glide.progress.ProgressManager
|
||||
import io.legado.app.model.BookCover
|
||||
import io.legado.app.model.ReadMange
|
||||
import io.legado.app.model.recyclerView.MangaVH
|
||||
import io.legado.app.model.recyclerView.MangeContent
|
||||
import io.legado.app.model.recyclerView.ReaderLoading
|
||||
import io.legado.app.utils.getCompatDrawable
|
||||
import java.util.Collections
|
||||
|
||||
|
||||
class MangaAdapter(private val context: Context) :
|
||||
RecyclerView.Adapter<RecyclerView.ViewHolder>(), PreloadModelProvider<Any> {
|
||||
|
||||
companion object {
|
||||
private const val LOADING_VIEW = 0
|
||||
private const val CONTENT_VIEW = 1
|
||||
}
|
||||
|
||||
private val mDiffCallback: DiffUtil.ItemCallback<Any> = object : DiffUtil.ItemCallback<Any>() {
|
||||
override fun areItemsTheSame(oldItem: Any, newItem: Any): Boolean {
|
||||
return if (oldItem is ReaderLoading && newItem is ReaderLoading) {
|
||||
newItem.mMessage == oldItem.mMessage
|
||||
} else if (oldItem is MangeContent && newItem is MangeContent) {
|
||||
oldItem.mImageUrl == newItem.mImageUrl
|
||||
} else false
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: Any, newItem: Any): Boolean {
|
||||
return if (oldItem is ReaderLoading && newItem is ReaderLoading) {
|
||||
oldItem == newItem
|
||||
} else if (oldItem is MangeContent && newItem is MangeContent) {
|
||||
oldItem == newItem
|
||||
} else false
|
||||
}
|
||||
}
|
||||
|
||||
private val mDiffer = AsyncListDiffer(this, mDiffCallback)
|
||||
|
||||
private fun getItem(@IntRange(from = 0) position: Int) = mDiffer.currentList[position]
|
||||
|
||||
fun getCurrentList() = mDiffer.currentList
|
||||
|
||||
//全部替换数据
|
||||
fun submitList(contents: MutableList<Any>, runnable: Runnable) {
|
||||
val currentList = mDiffer.currentList.toMutableList()
|
||||
currentList.addAll(contents)
|
||||
if (ReadMange.chapterChanged) {
|
||||
mDiffer.submitList(contents) {
|
||||
runnable.run()
|
||||
}
|
||||
} else {
|
||||
mDiffer.submitList(currentList) {
|
||||
runnable.run()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
inner class PageViewHolder(binding: BookComicRvBinding) :
|
||||
MangaVH<BookComicRvBinding>(binding, context) {
|
||||
|
||||
init {
|
||||
initComponent(
|
||||
binding.loading,
|
||||
binding.image,
|
||||
binding.progress,
|
||||
binding.retry,
|
||||
binding.flProgress
|
||||
)
|
||||
binding.image.zoomable.disabledGestureTypesState.value =
|
||||
GestureType.DOUBLE_TAP_SCALE or GestureType.ONE_FINGER_SCALE or
|
||||
GestureType.TWO_FINGER_SCALE or GestureType.KEYBOARD_DRAG or
|
||||
GestureType.ONE_FINGER_DRAG or GestureType.KEYBOARD_SCALE or GestureType.MOUSE_WHEEL_SCALE
|
||||
binding.retry.setOnClickListener {
|
||||
val item = mDiffer.currentList[layoutPosition]
|
||||
if (item is MangeContent) {
|
||||
loadImageWithRetry(item.mImageUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onBind(item: MangeContent) {
|
||||
loadImageWithRetry(item.mImageUrl)
|
||||
}
|
||||
}
|
||||
|
||||
inner class PageMoreViewHolder(val binding: BookComicLoadingRvBinding) :
|
||||
RecyclerView.ViewHolder(binding.root) {
|
||||
fun onBind(item: ReaderLoading) {
|
||||
val message = item.mMessage
|
||||
binding.text.text = message
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||
|
||||
return when {
|
||||
|
||||
viewType >= TYPE_FOOTER_VIEW -> {
|
||||
ItemViewHolder(footerItems.get(viewType).invoke(parent))
|
||||
}
|
||||
|
||||
viewType == LOADING_VIEW -> PageMoreViewHolder(
|
||||
BookComicLoadingRvBinding.inflate(
|
||||
LayoutInflater.from(
|
||||
parent.context
|
||||
), parent, false
|
||||
)
|
||||
)
|
||||
|
||||
viewType == CONTENT_VIEW -> PageViewHolder(
|
||||
BookComicRvBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
else -> error("Unknown view type!")
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = getActualItemCount() + getFooterCount()
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return when {
|
||||
isFooter(position) -> TYPE_FOOTER_VIEW + position - getActualItemCount()
|
||||
getItem(position) is MangeContent -> CONTENT_VIEW
|
||||
getItem(position) is ReaderLoading -> LOADING_VIEW
|
||||
else -> error("Unknown view type!")
|
||||
}
|
||||
}
|
||||
|
||||
fun getFooterCount() = footerItems.size()
|
||||
|
||||
private fun isFooter(position: Int) = position >= getActualItemCount()
|
||||
|
||||
override fun onViewRecycled(vh: RecyclerView.ViewHolder) {
|
||||
super.onViewRecycled(vh)
|
||||
when (vh) {
|
||||
is PageViewHolder -> {
|
||||
vh.itemView.updateLayoutParams<ViewGroup.LayoutParams> {
|
||||
height = MATCH_PARENT
|
||||
}
|
||||
Glide.with(context).clear(vh.binding.image)
|
||||
if (vh.binding.image.tag is String) {
|
||||
ProgressManager.removeListener(vh.binding.image.tag as String)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(vh: RecyclerView.ViewHolder, position: Int) {
|
||||
when (vh) {
|
||||
is PageViewHolder -> vh.onBind(getItem(position) as MangeContent)
|
||||
is PageMoreViewHolder -> vh.onBind(getItem(position) as ReaderLoading)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private val footerItems: SparseArray<(parent: ViewGroup) -> ViewBinding> by lazy { SparseArray() }
|
||||
|
||||
@Synchronized
|
||||
fun addFooterView(footer: ((parent: ViewGroup) -> ViewBinding)) {
|
||||
kotlin.runCatching {
|
||||
val index = getActualItemCount() + footerItems.size()
|
||||
footerItems.put(TYPE_FOOTER_VIEW + footerItems.size(), footer)
|
||||
notifyItemInserted(index)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 除去header和footer
|
||||
*/
|
||||
fun getActualItemCount() = getCurrentList().size
|
||||
|
||||
@Synchronized
|
||||
fun removeFooterView(footer: ((parent: ViewGroup) -> ViewBinding)) {
|
||||
kotlin.runCatching {
|
||||
val index = footerItems.indexOfValue(footer)
|
||||
if (index >= 0) {
|
||||
footerItems.remove(index)
|
||||
notifyItemRemoved(getActualItemCount() + index - 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getPreloadItems(position: Int): MutableList<Any> {
|
||||
if (getCurrentList().isEmpty()) return Collections.emptyList()
|
||||
if (position >= getCurrentList().size) return Collections.emptyList()
|
||||
return getCurrentList().subList(position, position + 1)
|
||||
}
|
||||
|
||||
override fun getPreloadRequestBuilder(item: Any): RequestBuilder<*>? {
|
||||
if (item is MangeContent) {
|
||||
return BookCover.loadManga(
|
||||
context,
|
||||
item.mImageUrl,
|
||||
sourceOrigin = ReadMange.book?.origin,
|
||||
manga = true,
|
||||
useDefaultCover = context.getCompatDrawable(R.color.book_ant_10)
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package io.legado.app.ui.book.manga.rv
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Rect
|
||||
import android.util.AttributeSet
|
||||
import android.view.GestureDetector
|
||||
import android.view.MotionEvent
|
||||
import android.view.ScaleGestureDetector
|
||||
import android.widget.FrameLayout
|
||||
import io.legado.app.help.config.AppConfig
|
||||
|
||||
class WebtoonFrame : FrameLayout {
|
||||
|
||||
constructor(context: Context) : super(context)
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
|
||||
constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(
|
||||
context,
|
||||
attrs,
|
||||
defStyle
|
||||
)
|
||||
|
||||
private val scaleDetector = ScaleGestureDetector(context, ScaleListener())
|
||||
private val flingDetector = GestureDetector(context, FlingListener())
|
||||
|
||||
var doubleTapZoom = true
|
||||
set(value) {
|
||||
field = value
|
||||
recycler?.doubleTapZoom = value
|
||||
scaleDetector.isQuickScaleEnabled = value
|
||||
}
|
||||
|
||||
private val recycler: WebtoonRecyclerView?
|
||||
get() = getChildAt(0) as? WebtoonRecyclerView
|
||||
|
||||
override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
|
||||
if (!AppConfig.disableMangaScaling) {
|
||||
scaleDetector.onTouchEvent(ev)
|
||||
flingDetector.onTouchEvent(ev)
|
||||
val recyclerRect = Rect()
|
||||
recycler?.getHitRect(recyclerRect) ?: return super.dispatchTouchEvent(ev)
|
||||
recyclerRect.inset(1, 1)
|
||||
|
||||
if (recyclerRect.right < recyclerRect.left || recyclerRect.bottom < recyclerRect.top) {
|
||||
return super.dispatchTouchEvent(ev)
|
||||
}
|
||||
|
||||
ev.setLocation(
|
||||
ev.x.coerceIn(recyclerRect.left.toFloat(), recyclerRect.right.toFloat()),
|
||||
ev.y.coerceIn(recyclerRect.top.toFloat(), recyclerRect.bottom.toFloat()),
|
||||
)
|
||||
}
|
||||
return super.dispatchTouchEvent(ev)
|
||||
}
|
||||
|
||||
inner class ScaleListener : ScaleGestureDetector.SimpleOnScaleGestureListener() {
|
||||
override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {
|
||||
recycler?.onScaleBegin()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onScale(detector: ScaleGestureDetector): Boolean {
|
||||
recycler?.onScale(detector.scaleFactor)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onScaleEnd(detector: ScaleGestureDetector) {
|
||||
recycler?.onScaleEnd()
|
||||
}
|
||||
}
|
||||
|
||||
inner class FlingListener : GestureDetector.SimpleOnGestureListener() {
|
||||
override fun onDown(e: MotionEvent): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onFling(
|
||||
e1: MotionEvent?,
|
||||
e2: MotionEvent,
|
||||
velocityX: Float,
|
||||
velocityY: Float,
|
||||
): Boolean {
|
||||
return recycler?.zoomFling(velocityX.toInt(), velocityY.toInt()) ?: false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,390 @@
|
||||
package io.legado.app.ui.book.manga.rv
|
||||
|
||||
import android.animation.AnimatorSet
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.Context
|
||||
import android.graphics.RectF
|
||||
import android.util.AttributeSet
|
||||
import android.view.HapticFeedbackConstants
|
||||
import android.view.MotionEvent
|
||||
import android.view.ViewConfiguration
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import androidx.core.animation.doOnEnd
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.utils.findCenterViewPosition
|
||||
import kotlin.math.abs
|
||||
|
||||
|
||||
class WebtoonRecyclerView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyle: Int = 0,
|
||||
) : RecyclerView(context, attrs, defStyle) {
|
||||
|
||||
private var isZooming = false
|
||||
private var atLastPosition = false
|
||||
private var atFirstPosition = false
|
||||
private var halfWidth = 0
|
||||
private var halfHeight = 0
|
||||
private var originalHeight = 0
|
||||
private var heightSet = false
|
||||
private var firstVisibleItemPosition = 0
|
||||
private var lastVisibleItemPosition = 0
|
||||
private var currentScale = DEFAULT_RATE
|
||||
private var mLastCenterViewPosition = 0
|
||||
|
||||
private var mPreScrollListener: IComicPreScroll? = null
|
||||
private var mNestedPreScrollListener: IComicPreScroll? = null
|
||||
private val listener = GestureListener()
|
||||
private val detector = Detector()
|
||||
private val mcRect = RectF()
|
||||
private var mToucheMiddle: (() -> Unit)? = null
|
||||
|
||||
//起始点
|
||||
private var startX: Float = 0f
|
||||
private var startY: Float = 0f
|
||||
|
||||
var doubleTapZoom = true
|
||||
|
||||
var tapListener: ((MotionEvent) -> Unit)? = null
|
||||
var longTapListener: ((MotionEvent) -> Boolean)? = null
|
||||
fun onToucheMiddle(init: () -> Unit) = apply { this.mToucheMiddle = init }
|
||||
|
||||
override fun onMeasure(widthSpec: Int, heightSpec: Int) {
|
||||
halfWidth = MeasureSpec.getSize(widthSpec) / 2
|
||||
halfHeight = MeasureSpec.getSize(heightSpec) / 2
|
||||
if (!heightSet) {
|
||||
originalHeight = MeasureSpec.getSize(heightSpec)
|
||||
heightSet = true
|
||||
}
|
||||
super.onMeasure(widthSpec, heightSpec)
|
||||
}
|
||||
|
||||
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
|
||||
detector.onTouchEvent(ev!!)
|
||||
return super.dispatchTouchEvent(ev)
|
||||
}
|
||||
|
||||
override fun onScrolled(dx: Int, dy: Int) {
|
||||
super.onScrolled(dx, dy)
|
||||
val layoutManager = layoutManager
|
||||
lastVisibleItemPosition =
|
||||
(layoutManager as LinearLayoutManager).findLastVisibleItemPosition()
|
||||
firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition()
|
||||
}
|
||||
|
||||
override fun onScrollStateChanged(state: Int) {
|
||||
super.onScrollStateChanged(state)
|
||||
val layoutManager = layoutManager
|
||||
val visibleItemCount = layoutManager?.childCount ?: 0
|
||||
val totalItemCount = layoutManager?.itemCount ?: 0
|
||||
atLastPosition = visibleItemCount > 0 && lastVisibleItemPosition == totalItemCount - 1
|
||||
atFirstPosition = firstVisibleItemPosition == 0
|
||||
}
|
||||
|
||||
override fun dispatchNestedPreScroll(
|
||||
dx: Int,
|
||||
dy: Int,
|
||||
consumed: IntArray?,
|
||||
offsetInWindow: IntArray?,
|
||||
type: Int
|
||||
): Boolean {
|
||||
val position = findCenterViewPosition()
|
||||
if (position != NO_POSITION && position != mLastCenterViewPosition) {
|
||||
mLastCenterViewPosition = position
|
||||
mPreScrollListener?.onPreScrollListener(this, dx, dy, position)
|
||||
}
|
||||
mNestedPreScrollListener?.onPreScrollListener(this, dx, dy, position)
|
||||
return super.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow, type)
|
||||
}
|
||||
|
||||
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
||||
super.onSizeChanged(w, h, oldw, oldh)
|
||||
mcRect.set(width * 0.33f, height * 0.33f, width * 0.66f, height * 0.66f)
|
||||
}
|
||||
|
||||
private fun getPositionX(positionX: Float): Float {
|
||||
if (currentScale < 1) {
|
||||
return 0f
|
||||
}
|
||||
val maxPositionX = halfWidth * (currentScale - 1)
|
||||
return positionX.coerceIn(-maxPositionX, maxPositionX)
|
||||
}
|
||||
|
||||
private fun getPositionY(positionY: Float): Float {
|
||||
if (currentScale < 1) {
|
||||
return (originalHeight / 2 - halfHeight).toFloat()
|
||||
}
|
||||
val maxPositionY = halfHeight * (currentScale - 1)
|
||||
return positionY.coerceIn(-maxPositionY, maxPositionY)
|
||||
}
|
||||
|
||||
private fun zoom(
|
||||
fromRate: Float,
|
||||
toRate: Float,
|
||||
fromX: Float,
|
||||
toX: Float,
|
||||
fromY: Float,
|
||||
toY: Float,
|
||||
) {
|
||||
isZooming = true
|
||||
val animatorSet = AnimatorSet()
|
||||
val translationXAnimator = ValueAnimator.ofFloat(fromX, toX)
|
||||
translationXAnimator.addUpdateListener { animation -> x = animation.animatedValue as Float }
|
||||
|
||||
val translationYAnimator = ValueAnimator.ofFloat(fromY, toY)
|
||||
translationYAnimator.addUpdateListener { animation -> y = animation.animatedValue as Float }
|
||||
|
||||
val scaleAnimator = ValueAnimator.ofFloat(fromRate, toRate)
|
||||
scaleAnimator.addUpdateListener { animation ->
|
||||
currentScale = animation.animatedValue as Float
|
||||
setScaleRate(currentScale)
|
||||
}
|
||||
animatorSet.playTogether(translationXAnimator, translationYAnimator, scaleAnimator)
|
||||
animatorSet.duration = ANIMATOR_DURATION_TIME.toLong()
|
||||
animatorSet.interpolator = DecelerateInterpolator()
|
||||
animatorSet.start()
|
||||
animatorSet.doOnEnd {
|
||||
isZooming = false
|
||||
currentScale = toRate
|
||||
}
|
||||
}
|
||||
|
||||
fun zoomFling(velocityX: Int, velocityY: Int): Boolean {
|
||||
if (currentScale <= 1f) return false
|
||||
|
||||
val distanceTimeFactor = 0.4f
|
||||
val animatorSet = AnimatorSet()
|
||||
|
||||
if (velocityX != 0) {
|
||||
val dx = (distanceTimeFactor * velocityX / 2)
|
||||
val newX = getPositionX(x + dx)
|
||||
val translationXAnimator = ValueAnimator.ofFloat(x, newX)
|
||||
translationXAnimator.addUpdateListener { animation ->
|
||||
x = getPositionX(animation.animatedValue as Float)
|
||||
}
|
||||
animatorSet.play(translationXAnimator)
|
||||
}
|
||||
if (velocityY != 0 && (atFirstPosition || atLastPosition)) {
|
||||
val dy = (distanceTimeFactor * velocityY / 2)
|
||||
val newY = getPositionY(y + dy)
|
||||
val translationYAnimator = ValueAnimator.ofFloat(y, newY)
|
||||
translationYAnimator.addUpdateListener { animation ->
|
||||
y = getPositionY(animation.animatedValue as Float)
|
||||
}
|
||||
animatorSet.play(translationYAnimator)
|
||||
}
|
||||
|
||||
animatorSet.duration = 400
|
||||
animatorSet.interpolator = DecelerateInterpolator()
|
||||
animatorSet.start()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun zoomScrollBy(dx: Int, dy: Int) {
|
||||
if (dx != 0) {
|
||||
x = getPositionX(x + dx)
|
||||
}
|
||||
if (dy != 0) {
|
||||
y = getPositionY(y + dy)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setScaleRate(rate: Float) {
|
||||
scaleX = rate
|
||||
scaleY = rate
|
||||
}
|
||||
|
||||
fun onScale(scaleFactor: Float) {
|
||||
currentScale *= scaleFactor
|
||||
currentScale = currentScale.coerceIn(
|
||||
MIN_RATE,
|
||||
MAX_SCALE_RATE,
|
||||
)
|
||||
|
||||
setScaleRate(currentScale)
|
||||
|
||||
layoutParams.height = if (currentScale < 1) {
|
||||
(originalHeight / currentScale).toInt()
|
||||
} else {
|
||||
originalHeight
|
||||
}
|
||||
halfHeight = layoutParams.height / 2
|
||||
|
||||
if (currentScale != DEFAULT_RATE) {
|
||||
x = getPositionX(x)
|
||||
y = getPositionY(y)
|
||||
} else {
|
||||
x = 0f
|
||||
y = 0f
|
||||
}
|
||||
|
||||
requestLayout()
|
||||
}
|
||||
|
||||
fun onScaleBegin() {
|
||||
if (detector.isDoubleTapping) {
|
||||
detector.isQuickScaling = true
|
||||
}
|
||||
}
|
||||
|
||||
fun onScaleEnd() {
|
||||
if (scaleX < MIN_RATE) {
|
||||
zoom(currentScale, MIN_RATE, x, 0f, y, 0f)
|
||||
}
|
||||
}
|
||||
|
||||
inner class GestureListener : GestureDetectorWithLongTap.Listener() {
|
||||
|
||||
override fun onSingleTapConfirmed(ev: MotionEvent): Boolean {
|
||||
if (mcRect.contains(startX, startY)) {
|
||||
mToucheMiddle?.invoke()
|
||||
} else {
|
||||
tapListener?.invoke(ev)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onDoubleTap(ev: MotionEvent): Boolean {
|
||||
detector.isDoubleTapping = true
|
||||
return false
|
||||
}
|
||||
|
||||
fun onDoubleTapConfirmed(ev: MotionEvent) {
|
||||
if (!isZooming && doubleTapZoom) {
|
||||
if (scaleX != DEFAULT_RATE) {
|
||||
zoom(currentScale, DEFAULT_RATE, x, 0f, y, 0f)
|
||||
} else {
|
||||
val toScale = 2f
|
||||
val toX = (halfWidth - ev.x) * (toScale - 1)
|
||||
val toY = (halfHeight - ev.y) * (toScale - 1)
|
||||
zoom(DEFAULT_RATE, toScale, 0f, toX, 0f, toY)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLongTapConfirmed(ev: MotionEvent) {
|
||||
if (longTapListener?.invoke(ev) == true) {
|
||||
performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner class Detector : GestureDetectorWithLongTap(context, listener) {
|
||||
|
||||
private var scrollPointerId = 0
|
||||
private var downX = 0
|
||||
private var downY = 0
|
||||
private val touchSlop = ViewConfiguration.get(context).scaledTouchSlop
|
||||
private var isZoomDragging = false
|
||||
var isDoubleTapping = false
|
||||
var isQuickScaling = false
|
||||
val disableMangaScaling = AppConfig.disableMangaScaling
|
||||
override fun onTouchEvent(ev: MotionEvent): Boolean {
|
||||
val action = ev.actionMasked
|
||||
val actionIndex = ev.actionIndex
|
||||
|
||||
when (action) {
|
||||
MotionEvent.ACTION_DOWN -> {
|
||||
scrollPointerId = ev.getPointerId(0)
|
||||
val eveX = (ev.x + 0.5f)
|
||||
val eveY = (ev.y + 0.5f)
|
||||
downX = eveX.toInt()
|
||||
downY = eveY.toInt()
|
||||
startX = eveX
|
||||
startY = eveY
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_POINTER_DOWN -> {
|
||||
scrollPointerId = ev.getPointerId(actionIndex)
|
||||
downX = (ev.getX(actionIndex) + 0.5f).toInt()
|
||||
downY = (ev.getY(actionIndex) + 0.5f).toInt()
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_MOVE -> {
|
||||
if (disableMangaScaling) {
|
||||
return false
|
||||
}
|
||||
if (isDoubleTapping && isQuickScaling) {
|
||||
return true
|
||||
}
|
||||
|
||||
val index = ev.findPointerIndex(scrollPointerId)
|
||||
if (index < 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
val x = (ev.getX(index) + 0.5f).toInt()
|
||||
val y = (ev.getY(index) + 0.5f).toInt()
|
||||
var dx = x - downX
|
||||
var dy = if (atFirstPosition || atLastPosition) y - downY else 0
|
||||
|
||||
if (!isZoomDragging && currentScale > 1f) {
|
||||
var startScroll = false
|
||||
|
||||
if (abs(dx) > touchSlop) {
|
||||
if (dx < 0) {
|
||||
dx += touchSlop
|
||||
} else {
|
||||
dx -= touchSlop
|
||||
}
|
||||
startScroll = true
|
||||
}
|
||||
if (abs(dy) > touchSlop) {
|
||||
if (dy < 0) {
|
||||
dy += touchSlop
|
||||
} else {
|
||||
dy -= touchSlop
|
||||
}
|
||||
startScroll = true
|
||||
}
|
||||
|
||||
if (startScroll) {
|
||||
isZoomDragging = true
|
||||
}
|
||||
}
|
||||
|
||||
if (isZoomDragging) {
|
||||
zoomScrollBy(dx, dy)
|
||||
}
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_UP -> {
|
||||
if (isDoubleTapping && !isQuickScaling && !disableMangaScaling) {
|
||||
listener.onDoubleTapConfirmed(ev)
|
||||
}
|
||||
isZoomDragging = false
|
||||
isDoubleTapping = false
|
||||
isQuickScaling = false
|
||||
}
|
||||
|
||||
MotionEvent.ACTION_CANCEL -> {
|
||||
isZoomDragging = false
|
||||
isDoubleTapping = false
|
||||
isQuickScaling = false
|
||||
}
|
||||
}
|
||||
return super.onTouchEvent(ev)
|
||||
}
|
||||
}
|
||||
|
||||
fun setPreScrollListener(iComicPreScroll: IComicPreScroll) {
|
||||
mPreScrollListener = iComicPreScroll
|
||||
}
|
||||
|
||||
fun setNestedPreScrollListener(iComicPreScroll: IComicPreScroll) {
|
||||
mNestedPreScrollListener = iComicPreScroll
|
||||
}
|
||||
|
||||
fun interface IComicPreScroll {
|
||||
fun onPreScrollListener(recyclerView: RecyclerView, dx: Int, dy: Int, position: Int)
|
||||
}
|
||||
}
|
||||
|
||||
private const val ANIMATOR_DURATION_TIME = 200
|
||||
private const val MIN_RATE = 0.5f
|
||||
private const val DEFAULT_RATE = 1f
|
||||
private const val MAX_SCALE_RATE = 3f
|
||||
@@ -0,0 +1,236 @@
|
||||
package io.legado.app.ui.book.read
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.GradientDrawable
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View.OnClickListener
|
||||
import android.view.View.OnLongClickListener
|
||||
import android.view.animation.Animation
|
||||
import android.widget.FrameLayout
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import io.legado.app.R
|
||||
import io.legado.app.databinding.ViewMangaMenuBinding
|
||||
import io.legado.app.help.IntentData
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.help.config.ReadBookConfig
|
||||
import io.legado.app.lib.dialogs.alert
|
||||
import io.legado.app.lib.theme.bottomBackground
|
||||
import io.legado.app.lib.theme.getPrimaryTextColor
|
||||
import io.legado.app.lib.theme.primaryColor
|
||||
import io.legado.app.lib.theme.primaryTextColor
|
||||
import io.legado.app.model.ReadMange
|
||||
import io.legado.app.ui.browser.WebViewActivity
|
||||
import io.legado.app.utils.ColorUtils
|
||||
import io.legado.app.utils.ConstraintModify
|
||||
import io.legado.app.utils.activity
|
||||
import io.legado.app.utils.applyNavigationBarPadding
|
||||
import io.legado.app.utils.dpToPx
|
||||
import io.legado.app.utils.gone
|
||||
import io.legado.app.utils.invisible
|
||||
import io.legado.app.utils.loadAnimation
|
||||
import io.legado.app.utils.modifyBegin
|
||||
import io.legado.app.utils.openUrl
|
||||
import io.legado.app.utils.startActivity
|
||||
import io.legado.app.utils.visible
|
||||
|
||||
class MangaMenu @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
) : FrameLayout(context, attrs) {
|
||||
private val binding = ViewMangaMenuBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
private val callBack: CallBack get() = activity as CallBack
|
||||
var canShowMenu: Boolean = false
|
||||
private val menuTopIn: Animation by lazy {
|
||||
loadAnimation(context, R.anim.anim_readbook_top_in)
|
||||
}
|
||||
private val menuTopOut: Animation by lazy {
|
||||
loadAnimation(context, R.anim.anim_readbook_top_out)
|
||||
}
|
||||
private val menuBottomIn: Animation by lazy {
|
||||
loadAnimation(context, R.anim.anim_readbook_bottom_in)
|
||||
}
|
||||
private val menuBottomOut: Animation by lazy {
|
||||
loadAnimation(context, R.anim.anim_readbook_bottom_out)
|
||||
}
|
||||
private var isMenuOutAnimating = false
|
||||
private val immersiveMenu: Boolean
|
||||
get() = AppConfig.readBarStyleFollowPage && ReadBookConfig.durConfig.curBgType() == 0
|
||||
|
||||
private var bgColor: Int = if (immersiveMenu) {
|
||||
kotlin.runCatching {
|
||||
Color.parseColor(ReadBookConfig.durConfig.curBgStr())
|
||||
}.getOrDefault(context.bottomBackground)
|
||||
} else {
|
||||
context.bottomBackground
|
||||
}
|
||||
|
||||
private var textColor: Int = if (immersiveMenu) {
|
||||
ReadBookConfig.durConfig.curTextColor()
|
||||
} else {
|
||||
context.getPrimaryTextColor(ColorUtils.isColorLight(bgColor))
|
||||
}
|
||||
|
||||
private val menuOutListener = object : Animation.AnimationListener {
|
||||
override fun onAnimationStart(animation: Animation) {
|
||||
isMenuOutAnimating = true
|
||||
binding.vwMenuBg.setOnClickListener(null)
|
||||
}
|
||||
|
||||
override fun onAnimationEnd(animation: Animation) {
|
||||
binding.titleBar.invisible()
|
||||
isMenuOutAnimating = false
|
||||
canShowMenu = false
|
||||
callBack.upSystemUiVisibility(false)
|
||||
}
|
||||
|
||||
override fun onAnimationRepeat(animation: Animation) = Unit
|
||||
}
|
||||
private val menuInListener = object : Animation.AnimationListener {
|
||||
override fun onAnimationStart(animation: Animation) {
|
||||
binding.tvSourceAction.text =
|
||||
ReadMange.bookSource?.bookSourceName ?: context.getString(R.string.book_source)
|
||||
callBack.upSystemUiVisibility(true)
|
||||
binding.tvSourceAction.isGone = false
|
||||
}
|
||||
|
||||
@SuppressLint("RtlHardcoded")
|
||||
override fun onAnimationEnd(animation: Animation) {
|
||||
binding.run {
|
||||
vwMenuBg.setOnClickListener { runMenuOut() }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAnimationRepeat(animation: Animation) = Unit
|
||||
}
|
||||
|
||||
init {
|
||||
initView()
|
||||
bindEvent()
|
||||
}
|
||||
|
||||
private fun initView(reset: Boolean = false) = binding.run {
|
||||
initAnimation()
|
||||
if (immersiveMenu) {
|
||||
val lightTextColor = ColorUtils.withAlpha(ColorUtils.lightenColor(textColor), 0.75f)
|
||||
titleBar.setTextColor(textColor)
|
||||
titleBar.setBackgroundColor(bgColor)
|
||||
titleBar.setColorFilter(textColor)
|
||||
tvChapterName.setTextColor(lightTextColor)
|
||||
tvChapterUrl.setTextColor(lightTextColor)
|
||||
} else if (reset) {
|
||||
val bgColor = context.primaryColor
|
||||
val textColor = context.primaryTextColor
|
||||
titleBar.setTextColor(textColor)
|
||||
titleBar.setBackgroundColor(bgColor)
|
||||
titleBar.setColorFilter(textColor)
|
||||
tvChapterName.setTextColor(textColor)
|
||||
tvChapterUrl.setTextColor(textColor)
|
||||
}
|
||||
val brightnessBackground = GradientDrawable()
|
||||
brightnessBackground.cornerRadius = 5F.dpToPx()
|
||||
brightnessBackground.setColor(ColorUtils.adjustAlpha(bgColor, 0.5f))
|
||||
if (AppConfig.isEInkMode) {
|
||||
titleBar.setBackgroundResource(R.drawable.bg_eink_border_bottom)
|
||||
}
|
||||
if (AppConfig.showReadTitleBarAddition) {
|
||||
titleBarAddition.visible()
|
||||
} else {
|
||||
titleBarAddition.gone()
|
||||
}
|
||||
upBrightnessVwPos()
|
||||
/**
|
||||
* 确保视图不被导航栏遮挡
|
||||
*/
|
||||
applyNavigationBarPadding()
|
||||
}
|
||||
|
||||
private fun upBrightnessVwPos() {
|
||||
if (AppConfig.brightnessVwPos) {
|
||||
binding.root.modifyBegin()
|
||||
.clear(R.id.ll_brightness, ConstraintModify.Anchor.LEFT)
|
||||
.rightToRightOf(R.id.ll_brightness, R.id.vw_menu_root)
|
||||
.commit()
|
||||
} else {
|
||||
binding.root.modifyBegin()
|
||||
.clear(R.id.ll_brightness, ConstraintModify.Anchor.RIGHT)
|
||||
.leftToLeftOf(R.id.ll_brightness, R.id.vw_menu_root)
|
||||
.commit()
|
||||
}
|
||||
}
|
||||
|
||||
private fun initAnimation() {
|
||||
menuTopIn.setAnimationListener(menuInListener)
|
||||
menuTopOut.setAnimationListener(menuOutListener)
|
||||
}
|
||||
|
||||
fun runMenuOut(anim: Boolean = !AppConfig.isEInkMode) {
|
||||
if (isMenuOutAnimating) {
|
||||
return
|
||||
}
|
||||
if (this.isVisible) {
|
||||
if (anim) {
|
||||
binding.titleBar.startAnimation(menuTopOut)
|
||||
} else {
|
||||
menuOutListener.onAnimationStart(menuBottomOut)
|
||||
menuOutListener.onAnimationEnd(menuBottomOut)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun runMenuIn(anim: Boolean = !AppConfig.isEInkMode) {
|
||||
this.visible()
|
||||
binding.titleBar.visible()
|
||||
if (anim) {
|
||||
binding.titleBar.startAnimation(menuTopIn)
|
||||
} else {
|
||||
menuInListener.onAnimationStart(menuBottomIn)
|
||||
menuInListener.onAnimationEnd(menuBottomIn)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun bindEvent() = binding.run {
|
||||
vwMenuBg.setOnClickListener { runMenuOut() }
|
||||
titleBar.toolbar.setOnClickListener {
|
||||
callBack.openBookInfoActivity()
|
||||
}
|
||||
val chapterViewClickListener = OnClickListener {
|
||||
if (AppConfig.readUrlInBrowser) {
|
||||
context.openUrl(tvChapterUrl.text.toString().substringBefore(",{"))
|
||||
} else {
|
||||
context.startActivity<WebViewActivity> {
|
||||
val url = tvChapterUrl.text.toString()
|
||||
putExtra("title", tvChapterName.text)
|
||||
putExtra("url", url)
|
||||
IntentData.put(url, ReadMange.bookSource?.getHeaderMap(true))
|
||||
}
|
||||
}
|
||||
}
|
||||
val chapterViewLongClickListener = OnLongClickListener {
|
||||
context.alert(R.string.open_fun) {
|
||||
setMessage(R.string.use_browser_open)
|
||||
okButton {
|
||||
AppConfig.readUrlInBrowser = true
|
||||
}
|
||||
noButton {
|
||||
AppConfig.readUrlInBrowser = false
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
tvChapterName.setOnClickListener(chapterViewClickListener)
|
||||
tvChapterName.setOnLongClickListener(chapterViewLongClickListener)
|
||||
tvChapterUrl.setOnClickListener(chapterViewClickListener)
|
||||
tvChapterUrl.setOnLongClickListener(chapterViewLongClickListener)
|
||||
}
|
||||
|
||||
interface CallBack {
|
||||
fun openBookInfoActivity()
|
||||
fun upSystemUiVisibility(value:Boolean)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import io.legado.app.lib.theme.accentColor
|
||||
import io.legado.app.lib.theme.primaryColor
|
||||
import io.legado.app.ui.book.audio.AudioPlayActivity
|
||||
import io.legado.app.ui.book.info.BookInfoActivity
|
||||
import io.legado.app.ui.book.manga.ReadMangaActivity
|
||||
import io.legado.app.ui.book.read.ReadBookActivity
|
||||
import io.legado.app.ui.main.MainViewModel
|
||||
import io.legado.app.utils.cnCompare
|
||||
@@ -35,6 +36,7 @@ import io.legado.app.utils.flowWithLifecycleAndDatabaseChangeFirst
|
||||
import io.legado.app.utils.observeEvent
|
||||
import io.legado.app.utils.setEdgeEffectColor
|
||||
import io.legado.app.utils.startActivity
|
||||
import io.legado.app.utils.startReadOrMangaActivity
|
||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
@@ -247,7 +249,7 @@ class BooksFragment() : BaseFragment(R.layout.fragment_books),
|
||||
putExtra("bookUrl", book.bookUrl)
|
||||
}
|
||||
|
||||
else -> startActivity<ReadBookActivity> {
|
||||
else -> startReadOrMangaActivity<ReadBookActivity, ReadMangaActivity>(book) {
|
||||
putExtra("bookUrl", book.bookUrl)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import io.legado.app.lib.theme.primaryColor
|
||||
import io.legado.app.ui.book.audio.AudioPlayActivity
|
||||
import io.legado.app.ui.book.group.GroupEditDialog
|
||||
import io.legado.app.ui.book.info.BookInfoActivity
|
||||
import io.legado.app.ui.book.manga.ReadMangaActivity
|
||||
import io.legado.app.ui.book.read.ReadBookActivity
|
||||
import io.legado.app.ui.book.search.SearchActivity
|
||||
import io.legado.app.ui.main.bookshelf.BaseBookshelfFragment
|
||||
@@ -203,7 +204,7 @@ class BookshelfFragment2() : BaseBookshelfFragment(R.layout.fragment_bookshelf2)
|
||||
startActivity<AudioPlayActivity> {
|
||||
putExtra("bookUrl", item.bookUrl)
|
||||
}
|
||||
else -> startActivity<ReadBookActivity> {
|
||||
else -> startReadOrMangaActivity<ReadBookActivity,ReadMangaActivity>(item) {
|
||||
putExtra("bookUrl", item.bookUrl)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
package io.legado.app.ui.widget
|
||||
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Rect
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import android.view.WindowInsets
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.res.use
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import io.legado.app.R
|
||||
import splitties.dimensions.dp
|
||||
import java.text.DecimalFormat
|
||||
import java.text.NumberFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
class ReaderInfoBarView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
@AttrRes defStyleAttr: Int = 0,
|
||||
) : View(context, attrs, defStyleAttr) {
|
||||
|
||||
private val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||||
private val textBounds = Rect()
|
||||
private val timeFormat = SimpleDateFormat.getTimeInstance(SimpleDateFormat.SHORT)
|
||||
private val timeReceiver = TimeReceiver()
|
||||
private var insetLeft: Int = 0
|
||||
private var insetRight: Int = 0
|
||||
private var insetTop: Int = 0
|
||||
private var cutoutInsetLeft = 0
|
||||
private var cutoutInsetRight = 0
|
||||
private val colorText = ColorUtils.setAlphaComponent(
|
||||
context.obtainStyledAttributes(intArrayOf(materialR.attr.colorOnSurface)).use {
|
||||
it.getColor(0, Color.BLACK)
|
||||
},
|
||||
200,
|
||||
)
|
||||
private val colorOutline = ColorUtils.setAlphaComponent(
|
||||
context.obtainStyledAttributes(intArrayOf(materialR.attr.colorSurface)).use {
|
||||
it.getColor(0, Color.WHITE)
|
||||
},
|
||||
200,
|
||||
)
|
||||
|
||||
private var timeText = timeFormat.format(Date())
|
||||
private var text: String = ""
|
||||
|
||||
private val innerHeight
|
||||
get() = height - paddingTop - paddingBottom - insetTop
|
||||
|
||||
private val innerWidth
|
||||
get() = width - paddingLeft - paddingRight - insetLeft - insetRight
|
||||
|
||||
init {
|
||||
val insetStart = dp(10f).toInt()
|
||||
val insetEnd = dp(10f).toInt()
|
||||
paint.strokeWidth = dp(2f)
|
||||
paint.setShadowLayer(2f, 1f, 1f, Color.GRAY)
|
||||
insetLeft = insetStart
|
||||
insetRight = insetEnd
|
||||
insetTop = minOf(insetLeft, insetRight)
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
val ty = innerHeight / 2f + textBounds.height() / 2f - textBounds.bottom
|
||||
paint.textAlign = Paint.Align.LEFT
|
||||
canvas.drawTextOutline(
|
||||
text,
|
||||
(paddingLeft + insetLeft + cutoutInsetLeft).toFloat(),
|
||||
paddingTop + insetTop + ty,
|
||||
)
|
||||
paint.textAlign = Paint.Align.RIGHT
|
||||
canvas.drawTextOutline(
|
||||
timeText,
|
||||
(width - paddingRight - insetRight - cutoutInsetRight).toFloat(),
|
||||
paddingTop + insetTop + ty,
|
||||
)
|
||||
}
|
||||
|
||||
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
||||
super.onSizeChanged(w, h, oldw, oldh)
|
||||
updateCutoutInsets(ViewCompat.getRootWindowInsets(this))
|
||||
updateTextSize()
|
||||
}
|
||||
|
||||
override fun onApplyWindowInsets(insets: WindowInsets): WindowInsets {
|
||||
updateCutoutInsets(WindowInsetsCompat.toWindowInsetsCompat(insets))
|
||||
return super.onApplyWindowInsets(insets)
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
ContextCompat.registerReceiver(
|
||||
context,
|
||||
timeReceiver,
|
||||
IntentFilter(Intent.ACTION_TIME_TICK),
|
||||
ContextCompat.RECEIVER_EXPORTED,
|
||||
)
|
||||
updateCutoutInsets(ViewCompat.getRootWindowInsets(this))
|
||||
}
|
||||
|
||||
override fun onDetachedFromWindow() {
|
||||
super.onDetachedFromWindow()
|
||||
context.unregisterReceiver(timeReceiver)
|
||||
}
|
||||
|
||||
fun update(
|
||||
currentPage: Int,
|
||||
totalPage: Int,
|
||||
percent: Float,
|
||||
chapterIndex: Int,
|
||||
chapterCount: Int
|
||||
) {
|
||||
text = context.getString(
|
||||
R.string.book_reader_info_bar,
|
||||
chapterIndex,
|
||||
chapterCount,
|
||||
currentPage,
|
||||
totalPage,
|
||||
if (percent in 0f..1f) (percent * 100).format() else ""
|
||||
)
|
||||
updateTextSize()
|
||||
invalidate()
|
||||
}
|
||||
|
||||
private fun Number.format(
|
||||
decimals: Int = 0,
|
||||
decPoint: Char = '.',
|
||||
thousandsSep: Char? = ' '
|
||||
): String {
|
||||
val formatter = NumberFormat.getInstance(Locale.US) as DecimalFormat
|
||||
val symbols = formatter.decimalFormatSymbols
|
||||
if (thousandsSep != null) {
|
||||
symbols.groupingSeparator = thousandsSep
|
||||
formatter.isGroupingUsed = true
|
||||
} else {
|
||||
formatter.isGroupingUsed = false
|
||||
}
|
||||
symbols.decimalSeparator = decPoint
|
||||
formatter.decimalFormatSymbols = symbols
|
||||
formatter.minimumFractionDigits = decimals
|
||||
formatter.maximumFractionDigits = decimals
|
||||
return when (this) {
|
||||
is Float,
|
||||
is Double,
|
||||
-> formatter.format(this.toDouble())
|
||||
|
||||
else -> formatter.format(this.toLong())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun updateTextSize() {
|
||||
val str = text + timeText
|
||||
val testTextSize = 48f
|
||||
paint.textSize = testTextSize
|
||||
paint.getTextBounds(str, 0, str.length, textBounds)
|
||||
paint.textSize = testTextSize * innerHeight / textBounds.height()
|
||||
paint.getTextBounds(str, 0, str.length, textBounds)
|
||||
}
|
||||
|
||||
private fun Canvas.drawTextOutline(text: String, x: Float, y: Float) {
|
||||
paint.color = colorOutline
|
||||
paint.style = Paint.Style.STROKE
|
||||
drawText(text, x, y, paint)
|
||||
paint.color = colorText
|
||||
paint.style = Paint.Style.FILL
|
||||
drawText(text, x, y, paint)
|
||||
}
|
||||
|
||||
private fun updateCutoutInsets(insetsCompat: WindowInsetsCompat?) {
|
||||
val cutouts = (insetsCompat ?: return).displayCutout?.boundingRects.orEmpty()
|
||||
cutoutInsetLeft = 0
|
||||
cutoutInsetRight = 0
|
||||
for (rect in cutouts) {
|
||||
if (rect.left <= paddingLeft) {
|
||||
cutoutInsetLeft += rect.width()
|
||||
}
|
||||
if (rect.right >= width - paddingRight) {
|
||||
cutoutInsetRight += rect.width()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private inner class TimeReceiver : BroadcastReceiver() {
|
||||
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
timeText = timeFormat.format(Date())
|
||||
invalidate()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,14 +70,18 @@ class LoadMoreView(context: Context, attrs: AttributeSet? = null) : FrameLayout(
|
||||
binding.tvText.visible()
|
||||
}
|
||||
|
||||
fun error(msg: String) {
|
||||
fun error(msg: String?, text: String = "") {
|
||||
stopLoad()
|
||||
hasMore = false
|
||||
errorMsg = msg
|
||||
binding.tvText.text = context.getString(R.string.error_load_msg, "点击查看详情")
|
||||
errorMsg = msg ?: ""
|
||||
binding.tvText.text =
|
||||
text.ifEmpty { context.getString(R.string.error_load_msg, "点击查看详情") }
|
||||
binding.tvText.visible()
|
||||
}
|
||||
|
||||
fun getLoading() = binding.rotateLoading
|
||||
fun getLoadingText() = binding.tvText
|
||||
|
||||
private fun showErrorDialog(): Boolean {
|
||||
if (errorMsg.isBlank()) {
|
||||
return false
|
||||
|
||||
@@ -18,6 +18,7 @@ import androidx.annotation.ColorInt
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat.BEHAVIOR_DEFAULT
|
||||
import androidx.core.view.WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
import androidx.fragment.app.DialogFragment
|
||||
@@ -244,3 +245,8 @@ fun AppCompatActivity.showHelp(fileName: String) {
|
||||
val mdText = String(assets.open("web/help/md/${fileName}.md").readBytes())
|
||||
showDialogFragment(TextDialog(getString(R.string.help), mdText, TextDialog.Mode.MD))
|
||||
}
|
||||
|
||||
fun immersionFullScreen(windowInsetsControllerCompat: WindowInsetsControllerCompat) {
|
||||
windowInsetsControllerCompat.systemBarsBehavior = BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
windowInsetsControllerCompat.hide(WindowInsetsCompat.Type.systemBars())
|
||||
}
|
||||
@@ -38,7 +38,10 @@ import androidx.preference.PreferenceManager
|
||||
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
|
||||
import io.legado.app.R
|
||||
import io.legado.app.constant.AppConst
|
||||
import io.legado.app.data.entities.Book
|
||||
import io.legado.app.help.IntentHelp
|
||||
import io.legado.app.help.book.isImage
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import splitties.systemservices.clipboardManager
|
||||
import splitties.systemservices.connectivityManager
|
||||
import splitties.systemservices.uiModeManager
|
||||
@@ -53,6 +56,18 @@ inline fun <reified A : Activity> Context.startActivity(configIntent: Intent.()
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
inline fun <reified A : Activity, reified M : Activity> Context.startReadOrMangaActivity(
|
||||
book: Book,
|
||||
configIntent: Intent.() -> Unit = {},
|
||||
) {
|
||||
val intent =
|
||||
Intent(this, if (book.isImage && AppConfig.showMangaUi) M::class.java else A::class.java)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
intent.apply(configIntent)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
|
||||
inline fun <reified T : Service> Context.startService(configIntent: Intent.() -> Unit = {}) {
|
||||
startService(Intent(this, T::class.java).apply(configIntent))
|
||||
}
|
||||
@@ -65,7 +80,7 @@ inline fun <reified T : Service> Context.stopService() {
|
||||
inline fun <reified T : Service> Context.servicePendingIntent(
|
||||
action: String,
|
||||
requestCode: Int = 0,
|
||||
configIntent: Intent.() -> Unit = {}
|
||||
configIntent: Intent.() -> Unit = {},
|
||||
): PendingIntent? {
|
||||
val intent = Intent(this, T::class.java)
|
||||
intent.action = action
|
||||
@@ -81,7 +96,7 @@ inline fun <reified T : Service> Context.servicePendingIntent(
|
||||
@SuppressLint("UnspecifiedImmutableFlag")
|
||||
fun Context.activityPendingIntent(
|
||||
intent: Intent,
|
||||
action: String
|
||||
action: String,
|
||||
): PendingIntent? {
|
||||
intent.action = action
|
||||
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
@@ -95,7 +110,7 @@ fun Context.activityPendingIntent(
|
||||
@SuppressLint("UnspecifiedImmutableFlag")
|
||||
inline fun <reified T : Activity> Context.activityPendingIntent(
|
||||
action: String,
|
||||
configIntent: Intent.() -> Unit = {}
|
||||
configIntent: Intent.() -> Unit = {},
|
||||
): PendingIntent? {
|
||||
val intent = Intent(this, T::class.java)
|
||||
intent.action = action
|
||||
@@ -111,7 +126,7 @@ inline fun <reified T : Activity> Context.activityPendingIntent(
|
||||
@SuppressLint("UnspecifiedImmutableFlag")
|
||||
inline fun <reified T : BroadcastReceiver> Context.broadcastPendingIntent(
|
||||
action: String,
|
||||
configIntent: Intent.() -> Unit = {}
|
||||
configIntent: Intent.() -> Unit = {},
|
||||
): PendingIntent? {
|
||||
val intent = Intent(this, T::class.java)
|
||||
intent.action = action
|
||||
@@ -161,7 +176,7 @@ fun Context.putPrefString(key: String, value: String?) =
|
||||
|
||||
fun Context.getPrefStringSet(
|
||||
key: String,
|
||||
defValue: MutableSet<String>? = null
|
||||
defValue: MutableSet<String>? = null,
|
||||
): MutableSet<String>? = defaultSharedPreferences.getStringSet(key, defValue)
|
||||
|
||||
fun Context.putPrefStringSet(key: String, value: MutableSet<String>) =
|
||||
@@ -255,7 +270,7 @@ fun Context.share(file: File, type: String = "text/*") {
|
||||
fun Context.shareWithQr(
|
||||
text: String,
|
||||
title: String = getString(R.string.share),
|
||||
errorCorrectionLevel: ErrorCorrectionLevel = ErrorCorrectionLevel.H
|
||||
errorCorrectionLevel: ErrorCorrectionLevel = ErrorCorrectionLevel.H,
|
||||
) {
|
||||
val bitmap = QRCodeUtils.createQRCode(text, errorCorrectionLevel = errorCorrectionLevel)
|
||||
if (bitmap == null) {
|
||||
|
||||
@@ -14,6 +14,9 @@ import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import io.legado.app.R
|
||||
import io.legado.app.data.entities.Book
|
||||
import io.legado.app.help.book.isImage
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.ui.widget.dialog.TextDialog
|
||||
|
||||
inline fun <reified T : DialogFragment> Fragment.showDialogFragment(
|
||||
@@ -81,6 +84,19 @@ inline fun <reified T : Activity> Fragment.startActivity(
|
||||
startActivity(Intent(requireContext(), T::class.java).apply(configIntent))
|
||||
}
|
||||
|
||||
inline fun <reified A : Activity, reified M : Activity> Fragment.startReadOrMangaActivity(
|
||||
book: Book,
|
||||
configIntent: Intent.() -> Unit = {},
|
||||
) {
|
||||
val intent = Intent(
|
||||
requireActivity(),
|
||||
if (book.isImage && AppConfig.showMangaUi) M::class.java else A::class.java
|
||||
)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
intent.apply(configIntent)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
fun Fragment.showHelp(fileName: String) {
|
||||
val mdText = String(requireContext().assets.open("web/help/md/${fileName}.md").readBytes())
|
||||
showDialogFragment(TextDialog(getString(R.string.help), mdText, TextDialog.Mode.MD))
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package io.legado.app.utils
|
||||
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
|
||||
fun RecyclerView.findCenterViewPosition(): Int {
|
||||
return getChildAdapterPosition(findChildViewUnder(width / 2f, height / 2f) ?: return RecyclerView.NO_POSITION)
|
||||
}
|
||||
|
||||
fun RecyclerView.findViewPosition(x: Float, y: Float): Int {
|
||||
return getChildAdapterPosition(findChildViewUnder(x, y) ?: return RecyclerView.NO_POSITION)
|
||||
}
|
||||
|
||||
fun RecyclerView.findFirstVisibleViewPosition(): Int {
|
||||
var pos = -1
|
||||
if (layoutManager is LinearLayoutManager) {
|
||||
pos = (layoutManager as LinearLayoutManager).findFirstVisibleItemPosition()
|
||||
}
|
||||
return pos
|
||||
}
|
||||
|
||||
fun RecyclerView.findLastVisibleViewPosition(): Int {
|
||||
var pos = -1
|
||||
if ( layoutManager is LinearLayoutManager) {
|
||||
pos = (layoutManager as LinearLayoutManager).findLastVisibleItemPosition()
|
||||
}
|
||||
return pos
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<io.legado.app.ui.book.manga.rv.WebtoonFrame xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<io.legado.app.ui.book.manga.rv.WebtoonRecyclerView
|
||||
android:id="@+id/mRecyclerMange"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<io.legado.app.ui.widget.ReaderInfoBarView
|
||||
android:id="@+id/infobar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="20dp"
|
||||
android:layout_gravity="bottom"
|
||||
android:layout_marginBottom="10dp"
|
||||
android:paddingStart="10dp"
|
||||
android:paddingEnd="10dp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<io.legado.app.ui.book.read.MangaMenu
|
||||
android:id="@+id/mangaMenu"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/fl_loading"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/white">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/ll_loading"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ProgressBar
|
||||
style="@style/Widget.Material3.CircularProgressIndicator.Small"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:indeterminate="true"
|
||||
app:indicatorColor="@color/white"
|
||||
app:indicatorSize="36dp"
|
||||
app:trackCornerRadius="99dp"
|
||||
app:trackThickness="3dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_loading_message"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="10dp"
|
||||
android:text="正在加载中..."
|
||||
android:textColor="@color/black" />
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/ll_retry"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:visibility="gone"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_msg"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/black" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/retry"
|
||||
style="@style/Widget.Material3.Button.TonalButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:text="重新加载"
|
||||
android:textSize="18sp"
|
||||
app:cornerRadius="10dp"
|
||||
tools:visibility="visible" />
|
||||
</LinearLayout>
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</io.legado.app.ui.book.manga.rv.WebtoonFrame>
|
||||
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="96dp"
|
||||
android:background="@color/book_ant_10">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:textColor="@android:color/white"
|
||||
android:textStyle="bold"
|
||||
tools:text="上一章节:第一话" />
|
||||
</FrameLayout>
|
||||
@@ -0,0 +1,64 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/rootView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/book_ant_10"
|
||||
android:contentDescription="@null">
|
||||
|
||||
<com.github.panpf.zoomimage.GlideZoomImageView
|
||||
android:id="@+id/image"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:adjustViewBounds="true"
|
||||
app:contentScale="fillWidth" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/fl_progress"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/book_ant_10">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/loading"
|
||||
style="@style/Widget.Material3.CircularProgressIndicator.Small"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:indeterminate="true"
|
||||
android:indeterminateTint="@color/white"
|
||||
android:progressTint="@color/white"
|
||||
app:indicatorColor="@color/white"
|
||||
app:indicatorSize="36dp"
|
||||
app:trackCornerRadius="99dp"
|
||||
app:trackThickness="3dp" />
|
||||
|
||||
|
||||
<Button
|
||||
android:id="@+id/retry"
|
||||
style="@style/Widget.Material3.Button.TonalButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:text="重新加载"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp"
|
||||
android:visibility="gone"
|
||||
app:backgroundTint="@color/white"
|
||||
app:cornerRadius="10dp"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/progress"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:text="0%"
|
||||
android:textColor="@color/white"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
</FrameLayout>
|
||||
|
||||
</FrameLayout>
|
||||
@@ -0,0 +1,73 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/vw_menu_root"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<View
|
||||
android:id="@+id/vw_menu_bg"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:contentDescription="@string/content"
|
||||
tools:layout_editor_absoluteX="0dp"
|
||||
tools:layout_editor_absoluteY="0dp" />
|
||||
|
||||
<io.legado.app.ui.widget.TitleBar
|
||||
android:id="@+id/title_bar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:theme="?attr/actionBarStyle"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/title_bar_addition"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_chapter_name"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingLeft="10dp"
|
||||
android:paddingRight="10dp"
|
||||
android:singleLine="true"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toLeftOf="@+id/tv_source_action"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tv_chapter_url"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingLeft="10dp"
|
||||
android:paddingRight="10dp"
|
||||
android:singleLine="true"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toLeftOf="@+id/tv_source_action"
|
||||
app:layout_constraintTop_toBottomOf="@+id/tv_chapter_name" />
|
||||
|
||||
<io.legado.app.ui.widget.text.AccentBgTextView
|
||||
android:id="@+id/tv_source_action"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="0dp"
|
||||
android:layout_margin="1dp"
|
||||
android:gravity="center"
|
||||
android:maxWidth="120dp"
|
||||
android:paddingLeft="6dp"
|
||||
android:paddingRight="6dp"
|
||||
android:text="@string/book_source"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/tv_chapter_url"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:radius="2dp" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
</io.legado.app.ui.widget.TitleBar>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -0,0 +1,21 @@
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:context=".ui.main.MainActivity"
|
||||
tools:ignore="AlwaysShowAction">
|
||||
|
||||
<group android:id="@+id/menu_group_on_line">
|
||||
<item
|
||||
android:id="@+id/menu_change_source"
|
||||
android:icon="@drawable/ic_exchange"
|
||||
android:title="@string/change_origin"
|
||||
app:showAsAction="always" />
|
||||
|
||||
<item
|
||||
android:id="@+id/menu_catalog"
|
||||
android:icon="@drawable/ic_toc"
|
||||
android:title="@string/chapter_list"
|
||||
app:showAsAction="always" />
|
||||
|
||||
</group>
|
||||
</menu>
|
||||
@@ -1180,6 +1180,9 @@
|
||||
<string name="read_aloud_by_media_button_summary">通过耳机按键来启动朗读</string>
|
||||
<string name="show_same_source">显示重复书源</string>
|
||||
<string name="theme_config">主题配置</string>
|
||||
<string name="show_mange_ui">漫画浏览</string>
|
||||
<string name="disable_manga_scaling">禁用漫画缩放</string>
|
||||
<string name="book_reader_info_bar">页数. %1$d/%2$d 章节. %3$d/%4$d -> %5$s%%</string>
|
||||
<string name="menu_download_after">Descargar el siguiente capítulo</string>
|
||||
<string name="menu_download_all">Descargar todos los capítulos</string>
|
||||
</resources>
|
||||
|
||||
@@ -1183,6 +1183,9 @@
|
||||
<string name="read_aloud_by_media_button_summary">通过耳机按键来启动朗读</string>
|
||||
<string name="show_same_source">显示重复书源</string>
|
||||
<string name="theme_config">主题配置</string>
|
||||
<string name="show_mange_ui">漫画浏览</string>
|
||||
<string name="disable_manga_scaling">禁用漫画缩放</string>
|
||||
<string name="book_reader_info_bar">页数. %1$d/%2$d 章节. %3$d/%4$d -> %5$s%%</string>
|
||||
<string name="menu_download_after">下载之后章节</string>
|
||||
<string name="menu_download_all">下载全部章节</string>
|
||||
</resources>
|
||||
|
||||
@@ -1183,6 +1183,9 @@
|
||||
<string name="read_aloud_by_media_button_summary">通过耳机按键来启动朗读</string>
|
||||
<string name="show_same_source">显示重复书源</string>
|
||||
<string name="theme_config">主题配置</string>
|
||||
<string name="show_mange_ui">漫画浏览</string>
|
||||
<string name="disable_manga_scaling">禁用漫画缩放</string>
|
||||
<string name="book_reader_info_bar">页数. %1$d/%2$d 章节. %3$d/%4$d -> %5$s%%</string>
|
||||
<string name="menu_download_after">Faça o download do próximo capítulo</string>
|
||||
<string name="menu_download_all">Download de todos os capítulos</string>
|
||||
</resources>
|
||||
|
||||
@@ -1179,6 +1179,9 @@ Còn </string>
|
||||
<string name="read_aloud_by_media_button_summary">通过耳机按键来启动朗读</string>
|
||||
<string name="show_same_source">显示重复书源</string>
|
||||
<string name="theme_config">主题配置</string>
|
||||
<string name="show_mange_ui">漫画浏览</string>
|
||||
<string name="disable_manga_scaling">禁用漫画缩放</string>
|
||||
<string name="book_reader_info_bar">页数. %1$d/%2$d 章节. %3$d/%4$d -> %5$s%%</string>
|
||||
<string name="menu_download_after">Tải xuống chương tiếp theo</string>
|
||||
<string name="menu_download_all">Tải xuống tất cả các chương</string>
|
||||
</resources>
|
||||
|
||||
@@ -1180,6 +1180,9 @@
|
||||
<string name="read_aloud_by_media_button_summary">通过耳机按键来启动朗读</string>
|
||||
<string name="show_same_source">显示重复书源</string>
|
||||
<string name="theme_config">主题配置</string>
|
||||
<string name="show_mange_ui">漫画浏览</string>
|
||||
<string name="disable_manga_scaling">禁用漫画缩放</string>
|
||||
<string name="book_reader_info_bar">页数. %1$d/%2$d 章节. %3$d/%4$d -> %5$s%%</string>
|
||||
<string name="menu_download_after">下載之後章節</string>
|
||||
<string name="menu_download_all">下載全部章節</string>
|
||||
</resources>
|
||||
|
||||
@@ -1182,6 +1182,9 @@
|
||||
<string name="read_aloud_by_media_button_summary">通过耳机按键来启动朗读</string>
|
||||
<string name="show_same_source">显示重复书源</string>
|
||||
<string name="theme_config">主题配置</string>
|
||||
<string name="show_mange_ui">漫画浏览</string>
|
||||
<string name="disable_manga_scaling">禁用漫画缩放</string>
|
||||
<string name="book_reader_info_bar">页数. %1$d/%2$d 章节. %3$d/%4$d -> %5$s%%</string>
|
||||
<string name="menu_download_after">下載之後章節</string>
|
||||
<string name="menu_download_all">下載全部章節</string>
|
||||
</resources>
|
||||
|
||||
@@ -1182,6 +1182,9 @@
|
||||
<string name="read_aloud_by_media_button_summary">通过耳机按键来启动朗读</string>
|
||||
<string name="show_same_source">显示重复书源</string>
|
||||
<string name="theme_config">主题配置</string>
|
||||
<string name="show_mange_ui">漫画浏览</string>
|
||||
<string name="disable_manga_scaling">禁用漫画缩放</string>
|
||||
<string name="book_reader_info_bar">页数. %1$d/%2$d 章节. %3$d/%4$d -> %5$s%%</string>
|
||||
<string name="menu_download_after">下载之后章节</string>
|
||||
<string name="menu_download_all">下载全部章节</string>
|
||||
</resources>
|
||||
|
||||
@@ -111,4 +111,6 @@
|
||||
<color name="ate_switch_thumb_disabled_dark">#FF424242</color>
|
||||
<!---->
|
||||
<color name="ate_navigation_drawer_selected_dark">#202020</color>
|
||||
|
||||
<color name="book_ant_10">#141414</color>
|
||||
</resources>
|
||||
|
||||
@@ -1183,6 +1183,9 @@
|
||||
<string name="read_aloud_by_media_button_summary">通过耳机按键来启动朗读</string>
|
||||
<string name="show_same_source">显示重复书源</string>
|
||||
<string name="theme_config">主题配置</string>
|
||||
<string name="show_mange_ui">漫画浏览</string>
|
||||
<string name="disable_manga_scaling">禁用漫画缩放</string>
|
||||
<string name="book_reader_info_bar">页数. %1$d/%2$d 章节. %3$d/%4$d -> %5$s%%</string>
|
||||
<string name="menu_download_after">Download the chapter after</string>
|
||||
<string name="menu_download_all">Download all chapter</string>
|
||||
</resources>
|
||||
|
||||
@@ -42,6 +42,18 @@
|
||||
android:title="@string/show_rss"
|
||||
app:iconSpaceReserved="false" />
|
||||
|
||||
<io.legado.app.lib.prefs.SwitchPreference
|
||||
android:defaultValue="true"
|
||||
android:key="showMangaUi"
|
||||
android:title="@string/show_mange_ui"
|
||||
app:iconSpaceReserved="false" />
|
||||
|
||||
<io.legado.app.lib.prefs.SwitchPreference
|
||||
android:defaultValue="true"
|
||||
android:key="disableMangaScaling"
|
||||
android:title="@string/disable_manga_scaling"
|
||||
app:iconSpaceReserved="false" />
|
||||
|
||||
<io.legado.app.lib.prefs.NameListPreference
|
||||
android:defaultValue="bookshelf"
|
||||
android:key="defaultHomePage"
|
||||
|
||||
@@ -115,6 +115,8 @@ room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
|
||||
room-testing = { module = "androidx.room:room-testing", version.ref = "room" }
|
||||
|
||||
glide-glide = { module = "com.github.bumptech.glide:glide", version.ref = "glide" }
|
||||
glide-okhttp= { module = "com.github.bumptech.glide:okhttp3-integration", version.ref ="glide" }
|
||||
glide-recyclerview={module="com.github.bumptech.glide:recyclerview-integration",version.ref="glide"}
|
||||
glide-compiler = { module = "com.github.bumptech.glide:compiler", version.ref = "glide" }
|
||||
glide-compose = { module = "com.github.bumptech.glide:compose", version = "1.0.0-beta01" }
|
||||
glide-ksp = { module = "com.github.bumptech.glide:ksp", version.ref = "glide" }
|
||||
@@ -159,6 +161,7 @@ mozilla-rhino = { module = "org.mozilla:rhino", version.ref = "rhino" }
|
||||
|
||||
renderscript-intrinsics-replacement-toolkit = { module = "com.github.android:renderscript-intrinsics-replacement-toolkit", version = "b6363490c3" }
|
||||
zxing-lite = { module = "com.github.jenly1314:zxing-lite", version.ref = "zxingLite" }
|
||||
zoom-imageview="io.github.panpf.zoomimage:zoomimage-view-glide:1.1.1"
|
||||
|
||||
[bundles]
|
||||
coroutines = ["kotlinx-coroutines-core", "kotlinx-coroutines-android"]
|
||||
|
||||
Reference in New Issue
Block a user