[新增] 重写了主页面和书架界面、封面设置界面,现在支持底栏模糊,更多的封面样式、封面阴影、随机默认封面等功能(在高级 - 测试 - 封面设置中应用)。
This commit is contained in:
@@ -228,6 +228,7 @@ dependencies {
|
|||||||
implementation(libs.nanohttpd.websocket)
|
implementation(libs.nanohttpd.websocket)
|
||||||
implementation(libs.zxing.lite)
|
implementation(libs.zxing.lite)
|
||||||
implementation(libs.colorpicker)
|
implementation(libs.colorpicker)
|
||||||
|
implementation(libs.colorpicker.compose)
|
||||||
implementation(libs.libarchive)
|
implementation(libs.libarchive)
|
||||||
implementation(libs.commons.text)
|
implementation(libs.commons.text)
|
||||||
implementation(libs.markwon.core)
|
implementation(libs.markwon.core)
|
||||||
@@ -236,6 +237,7 @@ dependencies {
|
|||||||
implementation(libs.markwon.html)
|
implementation(libs.markwon.html)
|
||||||
implementation(libs.quick.chinese.transfer.core)
|
implementation(libs.quick.chinese.transfer.core)
|
||||||
implementation(libs.hutool.crypto)
|
implementation(libs.hutool.crypto)
|
||||||
|
//noinspection GradleDependency
|
||||||
implementation(platform(libs.firebase.bom))
|
implementation(platform(libs.firebase.bom))
|
||||||
implementation(libs.firebase.analytics)
|
implementation(libs.firebase.analytics)
|
||||||
implementation(libs.firebase.perf)
|
implementation(libs.firebase.perf)
|
||||||
|
|||||||
@@ -37,4 +37,7 @@ object EventBus {
|
|||||||
const val REFRESH_BOOK_INFO = "refreshBookInfo"
|
const val REFRESH_BOOK_INFO = "refreshBookInfo"
|
||||||
const val REFRESH_BOOK_CONTENT = "refreshBookContent"
|
const val REFRESH_BOOK_CONTENT = "refreshBookContent"
|
||||||
const val UP_TOC = "upToc"
|
const val UP_TOC = "upToc"
|
||||||
|
|
||||||
|
const val UP_BOOKSHELF_COUNT = "upBookshelfCount"
|
||||||
|
const val UP_ALL_BOOK_TOC = "upAllBookToc"
|
||||||
}
|
}
|
||||||
@@ -23,6 +23,7 @@ object PreferKey {
|
|||||||
const val coverDefaultColor = "coverDefaultColor"
|
const val coverDefaultColor = "coverDefaultColor"
|
||||||
const val coverShowNameN = "coverShowNameN"
|
const val coverShowNameN = "coverShowNameN"
|
||||||
const val coverShowAuthorN = "coverShowAuthorN"
|
const val coverShowAuthorN = "coverShowAuthorN"
|
||||||
|
const val coverInfoOrientation = "coverInfoOrientation"
|
||||||
const val remoteServerId = "remoteServerId"
|
const val remoteServerId = "remoteServerId"
|
||||||
const val hideStatusBar = "hideStatusBar"
|
const val hideStatusBar = "hideStatusBar"
|
||||||
const val clickActionTL = "clickActionTopLeft"
|
const val clickActionTL = "clickActionTopLeft"
|
||||||
@@ -50,6 +51,7 @@ object PreferKey {
|
|||||||
const val bookshelfLayout = "bookshelfLayout"
|
const val bookshelfLayout = "bookshelfLayout"
|
||||||
const val bookshelfLayoutModePortrait = "bookshelfLayoutPortrait"
|
const val bookshelfLayoutModePortrait = "bookshelfLayoutPortrait"
|
||||||
const val bookshelfLayoutModeLandscape = "bookshelf_layout_landscape"
|
const val bookshelfLayoutModeLandscape = "bookshelf_layout_landscape"
|
||||||
|
const val bookshelfLayoutCompact = "bookshelfLayoutCompact"
|
||||||
const val bookshelfSort = "bookshelfSort"
|
const val bookshelfSort = "bookshelfSort"
|
||||||
const val bookExportFileName = "bookExportFileName"
|
const val bookExportFileName = "bookExportFileName"
|
||||||
const val bookImportFileName = "bookImportFileName"
|
const val bookImportFileName = "bookImportFileName"
|
||||||
@@ -222,6 +224,10 @@ object PreferKey {
|
|||||||
const val sharedElementEnterTransitionEnable = "sharedElementEnterTransitionEnable"
|
const val sharedElementEnterTransitionEnable = "sharedElementEnterTransitionEnable"
|
||||||
const val bookshelfLayoutGridLandscape = "bookshelfLayoutGridLandscape"
|
const val bookshelfLayoutGridLandscape = "bookshelfLayoutGridLandscape"
|
||||||
const val bookshelfLayoutGridPortrait = "bookshelfLayoutGridPortrait"
|
const val bookshelfLayoutGridPortrait = "bookshelfLayoutGridPortrait"
|
||||||
|
const val bookshelfTitleSmallFont = "bookshelfTitleSmallFont"
|
||||||
|
const val bookshelfTitleCenter = "bookshelfTitleCenter"
|
||||||
|
const val bookshelfTitleMaxLines = "bookshelfTitleMaxLines"
|
||||||
|
const val bookshelfCoverShadow = "bookshelfCoverShadow"
|
||||||
|
|
||||||
const val exploreLayoutGridLandscape = "exploreLayoutGridLandscape"
|
const val exploreLayoutGridLandscape = "exploreLayoutGridLandscape"
|
||||||
|
|
||||||
|
|||||||
@@ -348,6 +348,9 @@ data class Book(
|
|||||||
newBook.customIntro = customIntro
|
newBook.customIntro = customIntro
|
||||||
newBook.customTag = customTag
|
newBook.customTag = customTag
|
||||||
newBook.canUpdate = canUpdate
|
newBook.canUpdate = canUpdate
|
||||||
|
if (config.fixedType) {
|
||||||
|
newBook.type = type
|
||||||
|
}
|
||||||
newBook.readConfig = readConfig
|
newBook.readConfig = readConfig
|
||||||
return newBook
|
return newBook
|
||||||
}
|
}
|
||||||
@@ -402,7 +405,9 @@ data class Book(
|
|||||||
val mangaColorFilter: String? = null,
|
val mangaColorFilter: String? = null,
|
||||||
var mangaScrollMode: Int? = null,
|
var mangaScrollMode: Int? = null,
|
||||||
var webtoonSidePaddingDp: Int? = null,
|
var webtoonSidePaddingDp: Int? = null,
|
||||||
var mangaBackground: String? = null
|
var mangaBackground: String? = null,
|
||||||
|
|
||||||
|
var fixedType: Boolean = false // 固定书籍类型,不随书源更新
|
||||||
|
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
|
|
||||||
|
|||||||
@@ -20,15 +20,19 @@ import io.legado.app.help.http.okHttpClient
|
|||||||
import io.legado.app.help.http.okHttpClientManga
|
import io.legado.app.help.http.okHttpClientManga
|
||||||
import io.legado.app.ui.book.bookmark.AllBookmarkViewModel
|
import io.legado.app.ui.book.bookmark.AllBookmarkViewModel
|
||||||
import io.legado.app.ui.book.explore.ExploreShowViewModel
|
import io.legado.app.ui.book.explore.ExploreShowViewModel
|
||||||
|
import io.legado.app.ui.book.group.GroupViewModel
|
||||||
import io.legado.app.ui.book.import.remote.RemoteBookViewModel
|
import io.legado.app.ui.book.import.remote.RemoteBookViewModel
|
||||||
import io.legado.app.ui.book.info.BookInfoViewModel
|
import io.legado.app.ui.book.info.BookInfoViewModel
|
||||||
import io.legado.app.ui.book.readRecord.ReadRecordViewModel
|
import io.legado.app.ui.book.readRecord.ReadRecordViewModel
|
||||||
import io.legado.app.ui.book.searchContent.SearchContentViewModel
|
import io.legado.app.ui.book.searchContent.SearchContentViewModel
|
||||||
import io.legado.app.ui.book.toc.TocViewModel
|
import io.legado.app.ui.book.toc.TocViewModel
|
||||||
import io.legado.app.ui.book.toc.rule.TxtTocRuleViewModel
|
import io.legado.app.ui.book.toc.rule.TxtTocRuleViewModel
|
||||||
|
import io.legado.app.ui.config.coverConfig.CoverConfigViewModel
|
||||||
import io.legado.app.ui.config.otherConfig.OtherConfigViewModel
|
import io.legado.app.ui.config.otherConfig.OtherConfigViewModel
|
||||||
import io.legado.app.ui.config.readConfig.ReadConfigViewModel
|
import io.legado.app.ui.config.readConfig.ReadConfigViewModel
|
||||||
import io.legado.app.ui.dict.rule.DictRuleViewModel
|
import io.legado.app.ui.dict.rule.DictRuleViewModel
|
||||||
|
import io.legado.app.ui.main.MainViewModel
|
||||||
|
import io.legado.app.ui.main.bookshelf.BookshelfViewModel
|
||||||
import io.legado.app.ui.main.explore.ExploreViewModel
|
import io.legado.app.ui.main.explore.ExploreViewModel
|
||||||
import io.legado.app.ui.main.my.MyViewModel
|
import io.legado.app.ui.main.my.MyViewModel
|
||||||
import io.legado.app.ui.main.rss.RssViewModel
|
import io.legado.app.ui.main.rss.RssViewModel
|
||||||
@@ -76,11 +80,15 @@ val appModule = module {
|
|||||||
viewModelOf(::ReadRecordViewModel)
|
viewModelOf(::ReadRecordViewModel)
|
||||||
viewModelOf(::ExploreShowViewModel)
|
viewModelOf(::ExploreShowViewModel)
|
||||||
viewModelOf(::MyViewModel)
|
viewModelOf(::MyViewModel)
|
||||||
|
viewModelOf(::BookshelfViewModel)
|
||||||
|
viewModelOf(::MainViewModel)
|
||||||
|
viewModelOf(::GroupViewModel)
|
||||||
viewModelOf(::ReplaceRuleViewModel)
|
viewModelOf(::ReplaceRuleViewModel)
|
||||||
viewModelOf(::AllBookmarkViewModel)
|
viewModelOf(::AllBookmarkViewModel)
|
||||||
viewModelOf(::TxtTocRuleViewModel)
|
viewModelOf(::TxtTocRuleViewModel)
|
||||||
viewModelOf(::OtherConfigViewModel)
|
viewModelOf(::OtherConfigViewModel)
|
||||||
viewModelOf(::ReadConfigViewModel)
|
viewModelOf(::ReadConfigViewModel)
|
||||||
|
viewModelOf(::CoverConfigViewModel)
|
||||||
viewModelOf(::TocViewModel)
|
viewModelOf(::TocViewModel)
|
||||||
viewModelOf(::RemoteBookViewModel)
|
viewModelOf(::RemoteBookViewModel)
|
||||||
viewModelOf(::BookInfoViewModel)
|
viewModelOf(::BookInfoViewModel)
|
||||||
|
|||||||
@@ -339,6 +339,9 @@ fun Book.updateTo(newBook: Book): Book {
|
|||||||
newBook.customIntro = customIntro
|
newBook.customIntro = customIntro
|
||||||
newBook.customTag = customTag
|
newBook.customTag = customTag
|
||||||
newBook.canUpdate = canUpdate
|
newBook.canUpdate = canUpdate
|
||||||
|
if (config.fixedType) {
|
||||||
|
newBook.type = type
|
||||||
|
}
|
||||||
newBook.readConfig = readConfig
|
newBook.readConfig = readConfig
|
||||||
val variableMap = variableMap.toMutableMap()
|
val variableMap = variableMap.toMutableMap()
|
||||||
variableMap.keys.removeIf {
|
variableMap.keys.removeIf {
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import io.legado.app.utils.getPrefString
|
|||||||
import kotlinx.coroutines.currentCoroutineContext
|
import kotlinx.coroutines.currentCoroutineContext
|
||||||
import splitties.init.appCtx
|
import splitties.init.appCtx
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
object BookCover {
|
object BookCover {
|
||||||
@@ -101,16 +102,32 @@ object BookCover {
|
|||||||
appCtx.getPrefInt(PreferKey.coverShadowColorN, Color.BLACK)
|
appCtx.getPrefInt(PreferKey.coverShadowColorN, Color.BLACK)
|
||||||
|
|
||||||
val key = if (isNightTheme) PreferKey.defaultCoverDark else PreferKey.defaultCover
|
val key = if (isNightTheme) PreferKey.defaultCoverDark else PreferKey.defaultCover
|
||||||
val path = appCtx.getPrefString(key)
|
val paths = appCtx.getPrefString(key)?.split(",")?.filter { it.isNotBlank() }
|
||||||
if (path.isNullOrBlank()) {
|
|
||||||
|
if (paths.isNullOrEmpty()) {
|
||||||
defaultDrawable = appCtx.resources.getDrawable(R.drawable.image_cover_default, null)
|
defaultDrawable = appCtx.resources.getDrawable(R.drawable.image_cover_default, null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val randomPath = paths[Random.nextInt(paths.size)]
|
||||||
defaultDrawable = kotlin.runCatching {
|
defaultDrawable = kotlin.runCatching {
|
||||||
BitmapUtils.decodeBitmap(path, 600, 900)!!.toDrawable(appCtx.resources)
|
BitmapUtils.decodeBitmap(randomPath, 600, 900)!!.toDrawable(appCtx.resources)
|
||||||
}.getOrDefault(appCtx.resources.getDrawable(R.drawable.image_cover_default, null))
|
}.getOrDefault(appCtx.resources.getDrawable(R.drawable.image_cover_default, null))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getRandomDefaultDrawable(seed: Any? = null): Drawable {
|
||||||
|
val isNightTheme = AppConfig.isNightTheme
|
||||||
|
val key = if (isNightTheme) PreferKey.defaultCoverDark else PreferKey.defaultCover
|
||||||
|
val paths = appCtx.getPrefString(key)?.split(",")?.filter { it.isNotBlank() }
|
||||||
|
if (paths.isNullOrEmpty()) {
|
||||||
|
return appCtx.resources.getDrawable(R.drawable.image_cover_default, null)
|
||||||
|
}
|
||||||
|
val random = if (seed != null) Random(seed.hashCode()) else Random
|
||||||
|
val randomPath = paths[random.nextInt(paths.size)]
|
||||||
|
return kotlin.runCatching {
|
||||||
|
BitmapUtils.decodeBitmap(randomPath, 600, 900)!!.toDrawable(appCtx.resources)
|
||||||
|
}.getOrDefault(appCtx.resources.getDrawable(R.drawable.image_cover_default, null))
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 加载封面
|
* 加载封面
|
||||||
@@ -122,8 +139,9 @@ object BookCover {
|
|||||||
sourceOrigin: String? = null,
|
sourceOrigin: String? = null,
|
||||||
onLoadFinish: (() -> Unit)? = null,
|
onLoadFinish: (() -> Unit)? = null,
|
||||||
): RequestBuilder<Drawable> {
|
): RequestBuilder<Drawable> {
|
||||||
|
val currentDefault = getRandomDefaultDrawable()
|
||||||
if (AppConfig.useDefaultCover) {
|
if (AppConfig.useDefaultCover) {
|
||||||
return ImageLoader.load(context, defaultDrawable)
|
return ImageLoader.load(context, currentDefault)
|
||||||
.centerCrop()
|
.centerCrop()
|
||||||
}
|
}
|
||||||
var options = RequestOptions().set(OkHttpModelLoader.loadOnlyWifiOption, loadOnlyWifi)
|
var options = RequestOptions().set(OkHttpModelLoader.loadOnlyWifiOption, loadOnlyWifi)
|
||||||
@@ -156,8 +174,8 @@ object BookCover {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return builder.placeholder(defaultDrawable)
|
return builder.placeholder(currentDefault)
|
||||||
.error(defaultDrawable)
|
.error(currentDefault)
|
||||||
.centerCrop()
|
.centerCrop()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,7 +240,8 @@ object BookCover {
|
|||||||
loadOnlyWifi: Boolean = false,
|
loadOnlyWifi: Boolean = false,
|
||||||
sourceOrigin: String? = null,
|
sourceOrigin: String? = null,
|
||||||
): RequestBuilder<Drawable> {
|
): RequestBuilder<Drawable> {
|
||||||
val loadBlur = ImageLoader.load(context, defaultDrawable)
|
val currentDefault = getRandomDefaultDrawable()
|
||||||
|
val loadBlur = ImageLoader.load(context, currentDefault)
|
||||||
.transform(BlurTransformation(25), CenterCrop())
|
.transform(BlurTransformation(25), CenterCrop())
|
||||||
if (AppConfig.useDefaultCover) {
|
if (AppConfig.useDefaultCover) {
|
||||||
return loadBlur
|
return loadBlur
|
||||||
|
|||||||
@@ -162,8 +162,10 @@ object WebBook {
|
|||||||
book: Book,
|
book: Book,
|
||||||
canReName: Boolean = true,
|
canReName: Boolean = true,
|
||||||
): Book {
|
): Book {
|
||||||
book.removeAllBookType()
|
if (!book.config.fixedType) {
|
||||||
book.addType(bookSource.getBookType())
|
book.removeAllBookType()
|
||||||
|
book.addType(bookSource.getBookType())
|
||||||
|
}
|
||||||
if (!book.infoHtml.isNullOrEmpty()) {
|
if (!book.infoHtml.isNullOrEmpty()) {
|
||||||
BookInfo.analyzeBookInfo(
|
BookInfo.analyzeBookInfo(
|
||||||
bookSource = bookSource,
|
bookSource = bookSource,
|
||||||
@@ -235,8 +237,10 @@ object WebBook {
|
|||||||
book: Book,
|
book: Book,
|
||||||
runPerJs: Boolean = false
|
runPerJs: Boolean = false
|
||||||
): Result<List<BookChapter>> {
|
): Result<List<BookChapter>> {
|
||||||
book.removeAllBookType()
|
if (!book.config.fixedType) {
|
||||||
book.addType(bookSource.getBookType())
|
book.removeAllBookType()
|
||||||
|
book.addType(bookSource.getBookType())
|
||||||
|
}
|
||||||
return kotlin.runCatching {
|
return kotlin.runCatching {
|
||||||
if (runPerJs) {
|
if (runPerJs) {
|
||||||
runPreUpdateJs(bookSource, book).getOrThrow()
|
runPreUpdateJs(bookSource, book).getOrThrow()
|
||||||
|
|||||||
@@ -86,7 +86,6 @@ import io.legado.app.model.BookShelfState
|
|||||||
import io.legado.app.ui.theme.responsiveHazeEffect
|
import io.legado.app.ui.theme.responsiveHazeEffect
|
||||||
import io.legado.app.ui.theme.responsiveHazeSource
|
import io.legado.app.ui.theme.responsiveHazeSource
|
||||||
import io.legado.app.ui.widget.components.AnimatedTextLine
|
import io.legado.app.ui.widget.components.AnimatedTextLine
|
||||||
import io.legado.app.ui.widget.components.Cover
|
|
||||||
import io.legado.app.ui.widget.components.GlassMediumFlexibleTopAppBar
|
import io.legado.app.ui.widget.components.GlassMediumFlexibleTopAppBar
|
||||||
import io.legado.app.ui.widget.components.GlassTopAppBarDefaults
|
import io.legado.app.ui.widget.components.GlassTopAppBarDefaults
|
||||||
import io.legado.app.ui.widget.components.SearchBarSection
|
import io.legado.app.ui.widget.components.SearchBarSection
|
||||||
@@ -94,6 +93,7 @@ import io.legado.app.ui.widget.components.button.AnimatedTextButton
|
|||||||
import io.legado.app.ui.widget.components.button.TopBarActionButton
|
import io.legado.app.ui.widget.components.button.TopBarActionButton
|
||||||
import io.legado.app.ui.widget.components.button.TopbarNavigationButton
|
import io.legado.app.ui.widget.components.button.TopbarNavigationButton
|
||||||
import io.legado.app.ui.widget.components.card.TextCard
|
import io.legado.app.ui.widget.components.card.TextCard
|
||||||
|
import io.legado.app.ui.widget.components.cover.Cover
|
||||||
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
|
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
|
||||||
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
|
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
|
||||||
import io.legado.app.ui.widget.components.modalBottomSheet.GlassModalBottomSheet
|
import io.legado.app.ui.widget.components.modalBottomSheet.GlassModalBottomSheet
|
||||||
@@ -599,21 +599,22 @@ fun ExploreBookItem(
|
|||||||
if (!latestChapter.isNullOrEmpty()) {
|
if (!latestChapter.isNullOrEmpty()) {
|
||||||
Text(
|
Text(
|
||||||
text = " • ",
|
text = " • ",
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = Color.Gray,
|
color = Color.Gray,
|
||||||
maxLines = 1
|
maxLines = 1
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = "最新: $latestChapter",
|
text = "最新: $latestChapter",
|
||||||
style = MaterialTheme.typography.labelSmall,
|
style = MaterialTheme.typography.bodySmall,
|
||||||
color = Color.Gray,
|
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
|
||||||
val intro = book.intro?.replace("\\s+".toRegex(), "") ?: ""
|
val intro = book.intro?.replace("\\s+".toRegex(), "") ?: ""
|
||||||
if (intro.isNotEmpty()) {
|
if (intro.isNotEmpty()) {
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
@@ -0,0 +1,249 @@
|
|||||||
|
package io.legado.app.ui.book.group
|
||||||
|
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Delete
|
||||||
|
import androidx.compose.material.icons.filled.Restore
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedButton
|
||||||
|
import androidx.compose.material3.OutlinedIconButton
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringArrayResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.legado.app.R
|
||||||
|
import io.legado.app.data.entities.BookGroup
|
||||||
|
import io.legado.app.lib.dialogs.alert
|
||||||
|
import io.legado.app.ui.widget.components.cover.Cover
|
||||||
|
import io.legado.app.ui.widget.components.modalBottomSheet.GlassModalBottomSheet
|
||||||
|
import io.legado.app.ui.widget.components.settingItem.DropdownListSettingItem
|
||||||
|
import io.legado.app.ui.widget.components.settingItem.SettingItem
|
||||||
|
import io.legado.app.utils.FileUtils
|
||||||
|
import io.legado.app.utils.MD5Utils
|
||||||
|
import io.legado.app.utils.SelectImageContract
|
||||||
|
import io.legado.app.utils.externalFiles
|
||||||
|
import io.legado.app.utils.launch
|
||||||
|
import io.legado.app.utils.toastOnUi
|
||||||
|
import org.koin.androidx.compose.koinViewModel
|
||||||
|
import splitties.init.appCtx
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun GroupEditSheet(
|
||||||
|
group: BookGroup? = null,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
viewModel: GroupViewModel = koinViewModel()
|
||||||
|
) {
|
||||||
|
GlassModalBottomSheet(onDismissRequest = onDismissRequest) {
|
||||||
|
GroupEditContent(
|
||||||
|
group = group,
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
viewModel = viewModel
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun GroupEditContent(
|
||||||
|
group: BookGroup? = null,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
viewModel: GroupViewModel = koinViewModel()
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
var groupName by remember(group) { mutableStateOf(group?.groupName ?: "") }
|
||||||
|
var coverPath by remember(group) { mutableStateOf(group?.cover) }
|
||||||
|
var enableRefresh by remember(group) { mutableStateOf(group?.enableRefresh ?: true) }
|
||||||
|
var selectedSortIndex by remember(group) { mutableIntStateOf(group?.bookSort ?: -1) }
|
||||||
|
|
||||||
|
val sortOptions = stringArrayResource(R.array.book_sort)
|
||||||
|
val sortEntryValues = remember(sortOptions) {
|
||||||
|
Array(sortOptions.size) { (it - 1).toString() }
|
||||||
|
}
|
||||||
|
|
||||||
|
val selectImage = rememberLauncherForActivityResult(SelectImageContract()) { result ->
|
||||||
|
result.uri?.let { uri ->
|
||||||
|
try {
|
||||||
|
val inputStream = context.contentResolver.openInputStream(uri)
|
||||||
|
?: return@rememberLauncherForActivityResult
|
||||||
|
inputStream.use { input ->
|
||||||
|
val fileName = MD5Utils.md5Encode(input) + ".png"
|
||||||
|
val file =
|
||||||
|
FileUtils.createFileIfNotExist(context.externalFiles, "covers", fileName)
|
||||||
|
FileOutputStream(file).use { output ->
|
||||||
|
context.contentResolver.openInputStream(uri)?.use { it.copyTo(output) }
|
||||||
|
}
|
||||||
|
coverPath = file.absolutePath
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
appCtx.toastOnUi(e.localizedMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.padding(bottom = 16.dp)
|
||||||
|
.verticalScroll(rememberScrollState()),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.group_edit),
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
modifier = Modifier.padding(vertical = 16.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.Top,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
Cover(
|
||||||
|
path = coverPath,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(width = 90.dp, height = 130.dp)
|
||||||
|
.clickable { selectImage.launch() }
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
OutlinedTextField(
|
||||||
|
value = groupName,
|
||||||
|
onValueChange = { groupName = it },
|
||||||
|
label = { Text(stringResource(R.string.group_name)) },
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
singleLine = true
|
||||||
|
)
|
||||||
|
|
||||||
|
DropdownListSettingItem(
|
||||||
|
title = stringResource(R.string.sort),
|
||||||
|
selectedValue = selectedSortIndex.toString(),
|
||||||
|
displayEntries = sortOptions,
|
||||||
|
entryValues = sortEntryValues,
|
||||||
|
onValueChange = {
|
||||||
|
selectedSortIndex = it.toInt()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
|
SettingItem(
|
||||||
|
title = stringResource(R.string.allow_drop_down_refresh),
|
||||||
|
trailingContent = {
|
||||||
|
Switch(
|
||||||
|
checked = enableRefresh,
|
||||||
|
onCheckedChange = { enableRefresh = it }
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = { enableRefresh = !enableRefresh }
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
if (group != null && (group.groupId > 0 || group.groupId == Long.MIN_VALUE)) {
|
||||||
|
OutlinedIconButton(onClick = {
|
||||||
|
context.alert(R.string.delete, R.string.sure_del) {
|
||||||
|
yesButton {
|
||||||
|
viewModel.delGroup(group) {
|
||||||
|
onDismissRequest()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
noButton()
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Delete,
|
||||||
|
contentDescription = stringResource(R.string.delete)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
OutlinedIconButton(onClick = {
|
||||||
|
if (group != null) {
|
||||||
|
viewModel.clearCover(group) {
|
||||||
|
coverPath = null
|
||||||
|
appCtx.toastOnUi("封面已重置")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
coverPath = null
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Icon(Icons.Default.Restore, contentDescription = stringResource(R.string.reset))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
|
||||||
|
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
OutlinedButton(onClick = onDismissRequest) {
|
||||||
|
Text(stringResource(R.string.cancel))
|
||||||
|
}
|
||||||
|
Button(onClick = {
|
||||||
|
if (groupName.isEmpty()) {
|
||||||
|
appCtx.toastOnUi("分组名称不能为空")
|
||||||
|
} else {
|
||||||
|
if (group != null) {
|
||||||
|
viewModel.upGroup(
|
||||||
|
group.copy(
|
||||||
|
groupName = groupName,
|
||||||
|
cover = coverPath,
|
||||||
|
bookSort = selectedSortIndex,
|
||||||
|
enableRefresh = enableRefresh
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
onDismissRequest()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
viewModel.addGroup(
|
||||||
|
groupName,
|
||||||
|
selectedSortIndex,
|
||||||
|
enableRefresh,
|
||||||
|
coverPath
|
||||||
|
) {
|
||||||
|
onDismissRequest()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Text(stringResource(R.string.ok))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ import androidx.navigation.compose.composable
|
|||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
import io.legado.app.base.BaseComposeActivity
|
import io.legado.app.base.BaseComposeActivity
|
||||||
|
import io.legado.app.ui.config.coverConfig.CoverConfigScreen
|
||||||
import io.legado.app.ui.config.otherConfig.OtherConfigScreen
|
import io.legado.app.ui.config.otherConfig.OtherConfigScreen
|
||||||
import io.legado.app.ui.config.readConfig.ReadConfigScreen
|
import io.legado.app.ui.config.readConfig.ReadConfigScreen
|
||||||
import io.legado.app.ui.widget.components.GlassMediumFlexibleTopAppBar
|
import io.legado.app.ui.widget.components.GlassMediumFlexibleTopAppBar
|
||||||
@@ -38,6 +39,9 @@ class TestConfigActivity : BaseComposeActivity() {
|
|||||||
@Serializable
|
@Serializable
|
||||||
object ReadConfigRoute
|
object ReadConfigRoute
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
object CoverConfigRoute
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun Content() {
|
override fun Content() {
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
@@ -50,7 +54,8 @@ class TestConfigActivity : BaseComposeActivity() {
|
|||||||
ConfigNavScreen(
|
ConfigNavScreen(
|
||||||
onBackClick = { finish() },
|
onBackClick = { finish() },
|
||||||
onNavigateToOther = { navController.navigate(OtherConfigRoute) },
|
onNavigateToOther = { navController.navigate(OtherConfigRoute) },
|
||||||
onNavigateToRead = { navController.navigate(ReadConfigRoute) }
|
onNavigateToRead = { navController.navigate(ReadConfigRoute) },
|
||||||
|
onNavigateToCover = { navController.navigate(CoverConfigRoute) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,6 +66,10 @@ class TestConfigActivity : BaseComposeActivity() {
|
|||||||
composable<ReadConfigRoute> {
|
composable<ReadConfigRoute> {
|
||||||
ReadConfigScreen(onBackClick = { navController.popBackStack() })
|
ReadConfigScreen(onBackClick = { navController.popBackStack() })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
composable<CoverConfigRoute> {
|
||||||
|
CoverConfigScreen(onBackClick = { navController.popBackStack() })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,7 +78,8 @@ class TestConfigActivity : BaseComposeActivity() {
|
|||||||
fun ConfigNavScreen(
|
fun ConfigNavScreen(
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
onNavigateToOther: () -> Unit,
|
onNavigateToOther: () -> Unit,
|
||||||
onNavigateToRead: () -> Unit
|
onNavigateToRead: () -> Unit,
|
||||||
|
onNavigateToCover: () -> Unit
|
||||||
) {
|
) {
|
||||||
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
|
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
|
||||||
|
|
||||||
@@ -101,6 +111,10 @@ class TestConfigActivity : BaseComposeActivity() {
|
|||||||
title = stringResource(R.string.read_config),
|
title = stringResource(R.string.read_config),
|
||||||
onClick = onNavigateToRead
|
onClick = onNavigateToRead
|
||||||
)
|
)
|
||||||
|
ClickableSettingItem(
|
||||||
|
title = stringResource(R.string.cover_config),
|
||||||
|
onClick = onNavigateToCover
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
package io.legado.app.ui.config.bookshelfConfig
|
||||||
|
|
||||||
|
import io.legado.app.constant.EventBus
|
||||||
|
import io.legado.app.constant.PreferKey
|
||||||
|
import io.legado.app.ui.config.prefDelegate
|
||||||
|
import io.legado.app.utils.postEvent
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 书架配置
|
||||||
|
*/
|
||||||
|
object BookshelfConfig {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 书架分组样式: 0: Tab, 1:隐藏Tab, 2:文件夹
|
||||||
|
*/
|
||||||
|
var bookGroupStyle by prefDelegate(PreferKey.bookGroupStyle, 0) {
|
||||||
|
postEvent(EventBus.NOTIFY_MAIN, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 书架排序方式
|
||||||
|
*/
|
||||||
|
var bookshelfSort by prefDelegate(PreferKey.bookshelfSort, 0)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 书架排序顺序
|
||||||
|
*/
|
||||||
|
var bookshelfSortOrder by prefDelegate(PreferKey.bookshelfSortOrder, 1)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否显示未读标志
|
||||||
|
*/
|
||||||
|
var showUnread by prefDelegate(PreferKey.showUnread, true)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否显示新未读标志(小圆点)
|
||||||
|
*/
|
||||||
|
var showUnreadNew by prefDelegate(PreferKey.showUnreadNew, true)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否显示更新提示
|
||||||
|
*/
|
||||||
|
var showTip by prefDelegate(PreferKey.showTip, false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否在列表中显示最后更新时间
|
||||||
|
*/
|
||||||
|
var showLastUpdateTime by prefDelegate(PreferKey.showLastUpdateTime, false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否显示等待更新的书籍数量
|
||||||
|
*/
|
||||||
|
var showWaitUpCount by prefDelegate(PreferKey.showWaitUpCount, false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否显示书架快速滚动条
|
||||||
|
*/
|
||||||
|
var showBookshelfFastScroller by prefDelegate(PreferKey.showBookshelfFastScroller, false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否在书架Tab处显示分类展开按钮
|
||||||
|
*/
|
||||||
|
var shouldShowExpandButton by prefDelegate(PreferKey.shouldShowExpandButton, false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 书架同时更新书籍数量限制 (0为无限制)
|
||||||
|
*/
|
||||||
|
var bookshelfRefreshingLimit by prefDelegate(PreferKey.bookshelfRefreshingLimit, 0)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 竖屏书架布局模式: 0:列表, 1:网格
|
||||||
|
*/
|
||||||
|
var bookshelfLayoutModePortrait by prefDelegate(PreferKey.bookshelfLayoutModePortrait, 1)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 竖屏书架网格列数
|
||||||
|
*/
|
||||||
|
var bookshelfLayoutGridPortrait by prefDelegate(PreferKey.bookshelfLayoutGridPortrait, 3)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 横屏书架布局模式: 0:列表, 1:网格
|
||||||
|
*/
|
||||||
|
var bookshelfLayoutModeLandscape by prefDelegate(PreferKey.bookshelfLayoutModeLandscape, 1)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 横屏书架网格列数
|
||||||
|
*/
|
||||||
|
var bookshelfLayoutGridLandscape by prefDelegate(PreferKey.bookshelfLayoutGridLandscape, 7)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 紧凑模式
|
||||||
|
*/
|
||||||
|
var bookshelfLayoutCompact by prefDelegate(PreferKey.bookshelfLayoutCompact, false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 书架标题小字体
|
||||||
|
*/
|
||||||
|
var bookshelfTitleSmallFont by prefDelegate(PreferKey.bookshelfTitleSmallFont, false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 书架标题居中
|
||||||
|
*/
|
||||||
|
var bookshelfTitleCenter by prefDelegate(PreferKey.bookshelfTitleCenter, true)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 书架标题最大行数
|
||||||
|
*/
|
||||||
|
var bookshelfTitleMaxLines by prefDelegate(PreferKey.bookshelfTitleMaxLines, 2)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 书架封面阴影
|
||||||
|
*/
|
||||||
|
var bookshelfCoverShadow by prefDelegate(PreferKey.bookshelfCoverShadow, false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动时自动刷新书架
|
||||||
|
*/
|
||||||
|
var autoRefreshBook by prefDelegate(PreferKey.autoRefresh, false)
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
package io.legado.app.ui.config.coverConfig
|
||||||
|
|
||||||
|
import io.legado.app.constant.PreferKey
|
||||||
|
import io.legado.app.ui.config.prefDelegate
|
||||||
|
|
||||||
|
object CoverConfig {
|
||||||
|
|
||||||
|
var loadCoverOnlyWifi by prefDelegate(
|
||||||
|
PreferKey.loadCoverOnlyWifi,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
|
var useDefaultCover by prefDelegate(
|
||||||
|
PreferKey.useDefaultCover,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
|
var coverShowShadow by prefDelegate(
|
||||||
|
PreferKey.coverShowShadow,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
|
||||||
|
var coverShowStroke by prefDelegate(
|
||||||
|
PreferKey.coverShowStroke,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
|
||||||
|
var coverDefaultColor by prefDelegate(
|
||||||
|
PreferKey.coverDefaultColor,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
|
||||||
|
var defaultCover by prefDelegate(
|
||||||
|
PreferKey.defaultCover,
|
||||||
|
""
|
||||||
|
)
|
||||||
|
|
||||||
|
var coverTextColor by prefDelegate(
|
||||||
|
PreferKey.coverTextColor,
|
||||||
|
-16777216 // This might need a better default or a color resource lookup
|
||||||
|
)
|
||||||
|
|
||||||
|
var coverShadowColor by prefDelegate(
|
||||||
|
PreferKey.coverShadowColor,
|
||||||
|
-16777216
|
||||||
|
)
|
||||||
|
|
||||||
|
var coverShowName by prefDelegate(
|
||||||
|
PreferKey.coverShowName,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
|
||||||
|
var coverShowAuthor by prefDelegate(
|
||||||
|
PreferKey.coverShowAuthor,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
|
||||||
|
var defaultCoverDark by prefDelegate(
|
||||||
|
PreferKey.defaultCoverDark,
|
||||||
|
""
|
||||||
|
)
|
||||||
|
|
||||||
|
var coverTextColorN by prefDelegate(
|
||||||
|
PreferKey.coverTextColorN,
|
||||||
|
-1
|
||||||
|
)
|
||||||
|
|
||||||
|
var coverShadowColorN by prefDelegate(
|
||||||
|
PreferKey.coverShadowColorN,
|
||||||
|
-1
|
||||||
|
)
|
||||||
|
|
||||||
|
var coverShowNameN by prefDelegate(
|
||||||
|
PreferKey.coverShowNameN,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
|
||||||
|
var coverShowAuthorN by prefDelegate(
|
||||||
|
PreferKey.coverShowAuthorN,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
|
||||||
|
var coverInfoOrientation by prefDelegate(
|
||||||
|
PreferKey.coverInfoOrientation,
|
||||||
|
"0" // 0: vertical, 1: horizontal
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,285 @@
|
|||||||
|
package io.legado.app.ui.config.coverConfig
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.legado.app.R
|
||||||
|
import io.legado.app.constant.PreferKey
|
||||||
|
import io.legado.app.ui.widget.components.GlassMediumFlexibleTopAppBar
|
||||||
|
import io.legado.app.ui.widget.components.SplicedColumnGroup
|
||||||
|
import io.legado.app.ui.widget.components.button.TopbarNavigationButton
|
||||||
|
import io.legado.app.ui.widget.components.dialog.ColorPickerSheet
|
||||||
|
import io.legado.app.ui.widget.components.settingItem.ClickableSettingItem
|
||||||
|
import io.legado.app.ui.widget.components.settingItem.DropdownListSettingItem
|
||||||
|
import io.legado.app.ui.widget.components.settingItem.SwitchSettingItem
|
||||||
|
import org.koin.androidx.compose.koinViewModel
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun CoverConfigScreen(
|
||||||
|
onBackClick: () -> Unit,
|
||||||
|
viewModel: CoverConfigViewModel = koinViewModel()
|
||||||
|
) {
|
||||||
|
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
|
||||||
|
var showCoverRuleSheet by remember { mutableStateOf(false) }
|
||||||
|
var manageKey by remember { mutableStateOf<String?>(null) }
|
||||||
|
var showColorPickerByField by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
|
topBar = {
|
||||||
|
GlassMediumFlexibleTopAppBar(
|
||||||
|
title = { Text(stringResource(R.string.cover_config)) },
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
navigationIcon = {
|
||||||
|
TopbarNavigationButton(onClick = onBackClick)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { paddingValues ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(paddingValues)
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
SplicedColumnGroup {
|
||||||
|
SwitchSettingItem(
|
||||||
|
title = stringResource(R.string.only_wifi),
|
||||||
|
description = stringResource(R.string.only_wifi_summary),
|
||||||
|
checked = CoverConfig.loadCoverOnlyWifi,
|
||||||
|
onCheckedChange = { CoverConfig.loadCoverOnlyWifi = it }
|
||||||
|
)
|
||||||
|
|
||||||
|
ClickableSettingItem(
|
||||||
|
title = stringResource(R.string.cover_rule),
|
||||||
|
description = stringResource(R.string.cover_rule_summary),
|
||||||
|
onClick = { showCoverRuleSheet = true }
|
||||||
|
)
|
||||||
|
|
||||||
|
SwitchSettingItem(
|
||||||
|
title = stringResource(R.string.use_default_cover),
|
||||||
|
description = stringResource(R.string.use_default_cover_s),
|
||||||
|
checked = CoverConfig.useDefaultCover,
|
||||||
|
onCheckedChange = { CoverConfig.useDefaultCover = it }
|
||||||
|
)
|
||||||
|
|
||||||
|
SwitchSettingItem(
|
||||||
|
title = stringResource(R.string.cover_show_shadow),
|
||||||
|
checked = CoverConfig.coverShowShadow,
|
||||||
|
onCheckedChange = {
|
||||||
|
CoverConfig.coverShowShadow = it
|
||||||
|
viewModel.updateCoverStyle()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
SwitchSettingItem(
|
||||||
|
title = stringResource(R.string.cover_show_stroke),
|
||||||
|
checked = CoverConfig.coverShowStroke,
|
||||||
|
onCheckedChange = {
|
||||||
|
CoverConfig.coverShowStroke = it
|
||||||
|
viewModel.updateCoverStyle()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
SwitchSettingItem(
|
||||||
|
title = stringResource(R.string.default_color),
|
||||||
|
checked = CoverConfig.coverDefaultColor,
|
||||||
|
onCheckedChange = {
|
||||||
|
CoverConfig.coverDefaultColor = it
|
||||||
|
viewModel.updateCoverStyle()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
SplicedColumnGroup {
|
||||||
|
DropdownListSettingItem(
|
||||||
|
title = "封面信息方向",
|
||||||
|
selectedValue = CoverConfig.coverInfoOrientation,
|
||||||
|
displayEntries = arrayOf(
|
||||||
|
stringResource(R.string.screen_portrait),
|
||||||
|
stringResource(R.string.screen_landscape)
|
||||||
|
),
|
||||||
|
entryValues = arrayOf("0", "1"),
|
||||||
|
onValueChange = {
|
||||||
|
CoverConfig.coverInfoOrientation = it
|
||||||
|
viewModel.updateCoverStyle()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
SplicedColumnGroup(title = stringResource(R.string.day)) {
|
||||||
|
val coverCount = CoverConfig.defaultCover.split(",").filter { it.isNotBlank() }.size
|
||||||
|
ClickableSettingItem(
|
||||||
|
title = stringResource(R.string.default_cover),
|
||||||
|
description = if (coverCount > 0) "已选择 $coverCount 张图片" else stringResource(
|
||||||
|
R.string.select_image
|
||||||
|
),
|
||||||
|
onClick = { manageKey = PreferKey.defaultCover }
|
||||||
|
)
|
||||||
|
|
||||||
|
ClickableSettingItem(
|
||||||
|
title = stringResource(R.string.text_color),
|
||||||
|
option = "#${Integer.toHexString(CoverConfig.coverTextColor).uppercase()}",
|
||||||
|
onClick = { showColorPickerByField = "coverTextColor" },
|
||||||
|
trailingContent = {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(28.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(Color(CoverConfig.coverTextColor))
|
||||||
|
.border(1.dp, MaterialTheme.colorScheme.outlineVariant, CircleShape)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
ClickableSettingItem(
|
||||||
|
title = stringResource(R.string.text_shadow_color),
|
||||||
|
option = "#${Integer.toHexString(CoverConfig.coverShadowColor).uppercase()}",
|
||||||
|
onClick = { showColorPickerByField = "coverShadowColor" },
|
||||||
|
trailingContent = {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(28.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(Color(CoverConfig.coverShadowColor))
|
||||||
|
.border(1.dp, MaterialTheme.colorScheme.outlineVariant, CircleShape)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
SwitchSettingItem(
|
||||||
|
title = stringResource(R.string.cover_show_name),
|
||||||
|
description = stringResource(R.string.cover_show_name_summary),
|
||||||
|
checked = CoverConfig.coverShowName,
|
||||||
|
onCheckedChange = { viewModel.updateShowName(it) }
|
||||||
|
)
|
||||||
|
|
||||||
|
SwitchSettingItem(
|
||||||
|
title = stringResource(R.string.cover_show_author),
|
||||||
|
description = stringResource(R.string.cover_show_author_summary),
|
||||||
|
checked = CoverConfig.coverShowAuthor,
|
||||||
|
enabled = CoverConfig.coverShowName,
|
||||||
|
onCheckedChange = { viewModel.updateShowAuthor(it) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
SplicedColumnGroup(title = stringResource(R.string.night)) {
|
||||||
|
val coverCount =
|
||||||
|
CoverConfig.defaultCoverDark.split(",").filter { it.isNotBlank() }.size
|
||||||
|
ClickableSettingItem(
|
||||||
|
title = stringResource(R.string.default_cover),
|
||||||
|
description = if (coverCount > 0) "已选择 $coverCount 张图片" else stringResource(
|
||||||
|
R.string.select_image
|
||||||
|
),
|
||||||
|
onClick = { manageKey = PreferKey.defaultCoverDark }
|
||||||
|
)
|
||||||
|
|
||||||
|
ClickableSettingItem(
|
||||||
|
title = stringResource(R.string.text_color),
|
||||||
|
option = "#${Integer.toHexString(CoverConfig.coverTextColorN).uppercase()}",
|
||||||
|
onClick = { showColorPickerByField = "coverTextColorN" },
|
||||||
|
trailingContent = {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(28.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(Color(CoverConfig.coverTextColorN))
|
||||||
|
.border(1.dp, MaterialTheme.colorScheme.outlineVariant, CircleShape)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
ClickableSettingItem(
|
||||||
|
title = stringResource(R.string.text_shadow_color),
|
||||||
|
option = "#${Integer.toHexString(CoverConfig.coverShadowColorN).uppercase()}",
|
||||||
|
onClick = { showColorPickerByField = "coverShadowColorN" },
|
||||||
|
trailingContent = {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(28.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(Color(CoverConfig.coverShadowColorN))
|
||||||
|
.border(1.dp, MaterialTheme.colorScheme.outlineVariant, CircleShape)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
SwitchSettingItem(
|
||||||
|
title = stringResource(R.string.cover_show_name),
|
||||||
|
description = stringResource(R.string.cover_show_name_summary),
|
||||||
|
checked = CoverConfig.coverShowNameN,
|
||||||
|
onCheckedChange = { viewModel.updateShowName(it, true) }
|
||||||
|
)
|
||||||
|
|
||||||
|
SwitchSettingItem(
|
||||||
|
title = stringResource(R.string.cover_show_author),
|
||||||
|
description = stringResource(R.string.cover_show_author_summary),
|
||||||
|
checked = CoverConfig.coverShowAuthorN,
|
||||||
|
enabled = CoverConfig.coverShowNameN,
|
||||||
|
onCheckedChange = { viewModel.updateShowAuthor(it, true) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showCoverRuleSheet) {
|
||||||
|
CoverRuleConfigSheet(onDismissRequest = { showCoverRuleSheet = false })
|
||||||
|
}
|
||||||
|
|
||||||
|
manageKey?.let { key ->
|
||||||
|
CoverManageSheet(
|
||||||
|
preferenceKey = key,
|
||||||
|
onDismissRequest = { manageKey = null },
|
||||||
|
viewModel = viewModel
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
showColorPickerByField?.let { field ->
|
||||||
|
val initialColor = when (field) {
|
||||||
|
"coverTextColor" -> CoverConfig.coverTextColor
|
||||||
|
"coverShadowColor" -> CoverConfig.coverShadowColor
|
||||||
|
"coverTextColorN" -> CoverConfig.coverTextColorN
|
||||||
|
"coverShadowColorN" -> CoverConfig.coverShadowColorN
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
|
||||||
|
ColorPickerSheet(
|
||||||
|
initialColor = initialColor,
|
||||||
|
onDismissRequest = { showColorPickerByField = null },
|
||||||
|
onColorSelected = { color ->
|
||||||
|
when (field) {
|
||||||
|
"coverTextColor" -> CoverConfig.coverTextColor = color
|
||||||
|
"coverShadowColor" -> CoverConfig.coverShadowColor = color
|
||||||
|
"coverTextColorN" -> CoverConfig.coverTextColorN = color
|
||||||
|
"coverShadowColorN" -> CoverConfig.coverShadowColorN = color
|
||||||
|
}
|
||||||
|
viewModel.updateCoverStyle()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,139 @@
|
|||||||
|
package io.legado.app.ui.config.coverConfig
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import io.legado.app.constant.AppLog
|
||||||
|
import io.legado.app.constant.PreferKey
|
||||||
|
import io.legado.app.exception.NoStackTraceException
|
||||||
|
import io.legado.app.lib.permission.Permissions
|
||||||
|
import io.legado.app.lib.permission.PermissionsCompat
|
||||||
|
import io.legado.app.model.BookCover
|
||||||
|
import io.legado.app.utils.FileDoc
|
||||||
|
import io.legado.app.utils.FileUtils
|
||||||
|
import io.legado.app.utils.MD5Utils
|
||||||
|
import io.legado.app.utils.RealPathUtil
|
||||||
|
import io.legado.app.utils.externalFiles
|
||||||
|
import io.legado.app.utils.isContentScheme
|
||||||
|
import io.legado.app.utils.printOnDebug
|
||||||
|
import io.legado.app.utils.toastOnUi
|
||||||
|
import splitties.init.appCtx
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileInputStream
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
class CoverConfigViewModel : ViewModel() {
|
||||||
|
|
||||||
|
fun addCoverFromUri(preferenceKey: String, uris: List<Uri>) {
|
||||||
|
uris.forEach { uri ->
|
||||||
|
readUri(appCtx, uri) { fileDoc, _ ->
|
||||||
|
kotlin.runCatching {
|
||||||
|
var file = appCtx.externalFiles
|
||||||
|
val suffix = fileDoc.name.substringAfterLast(".")
|
||||||
|
val inputStreamForMd5 = appCtx.contentResolver.openInputStream(uri)
|
||||||
|
?: throw NoStackTraceException("无法打开输入流")
|
||||||
|
val fileName = MD5Utils.md5Encode(inputStreamForMd5) + ".$suffix"
|
||||||
|
file = FileUtils.createFileIfNotExist(file, "covers", fileName)
|
||||||
|
FileOutputStream(file).use {
|
||||||
|
appCtx.contentResolver.openInputStream(uri)?.use { input ->
|
||||||
|
input.copyTo(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val currentCovers = if (preferenceKey == PreferKey.defaultCover) {
|
||||||
|
CoverConfig.defaultCover
|
||||||
|
} else {
|
||||||
|
CoverConfig.defaultCoverDark
|
||||||
|
}
|
||||||
|
val newList =
|
||||||
|
currentCovers.split(",").filter { it.isNotBlank() }.toMutableList()
|
||||||
|
if (!newList.contains(file.absolutePath)) {
|
||||||
|
newList.add(file.absolutePath)
|
||||||
|
}
|
||||||
|
val newCovers = newList.joinToString(",")
|
||||||
|
if (preferenceKey == PreferKey.defaultCover) {
|
||||||
|
CoverConfig.defaultCover = newCovers
|
||||||
|
} else {
|
||||||
|
CoverConfig.defaultCoverDark = newCovers
|
||||||
|
}
|
||||||
|
BookCover.upDefaultCover()
|
||||||
|
}.onFailure {
|
||||||
|
appCtx.toastOnUi(it.localizedMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun readUri(
|
||||||
|
context: Context,
|
||||||
|
uri: Uri?,
|
||||||
|
success: (fileDoc: FileDoc, inputStream: InputStream) -> Unit
|
||||||
|
) {
|
||||||
|
uri ?: return
|
||||||
|
try {
|
||||||
|
if (uri.isContentScheme()) {
|
||||||
|
val doc = DocumentFile.fromSingleUri(context, uri)
|
||||||
|
doc ?: throw NoStackTraceException("未获取到文件")
|
||||||
|
val fileDoc = FileDoc.fromDocumentFile(doc)
|
||||||
|
context.contentResolver.openInputStream(uri)!!.use { inputStream ->
|
||||||
|
success.invoke(fileDoc, inputStream)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
PermissionsCompat.Builder()
|
||||||
|
.addPermissions(*Permissions.Group.STORAGE)
|
||||||
|
.onGranted {
|
||||||
|
RealPathUtil.getPath(context, uri)?.let { path ->
|
||||||
|
val file = File(path)
|
||||||
|
val fileDoc = FileDoc.fromFile(file)
|
||||||
|
FileInputStream(file).use { inputStream ->
|
||||||
|
success.invoke(fileDoc, inputStream)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.request()
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printOnDebug()
|
||||||
|
AppLog.put("读取Uri出错\n$e", e, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeCover(preferenceKey: String, path: String) {
|
||||||
|
val currentCovers = if (preferenceKey == PreferKey.defaultCover) {
|
||||||
|
CoverConfig.defaultCover
|
||||||
|
} else {
|
||||||
|
CoverConfig.defaultCoverDark
|
||||||
|
}
|
||||||
|
val newList = currentCovers.split(",").filter { it.isNotBlank() && it != path }
|
||||||
|
val newCovers = newList.joinToString(",")
|
||||||
|
if (preferenceKey == PreferKey.defaultCover) {
|
||||||
|
CoverConfig.defaultCover = newCovers
|
||||||
|
} else {
|
||||||
|
CoverConfig.defaultCoverDark = newCovers
|
||||||
|
}
|
||||||
|
BookCover.upDefaultCover()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateShowName(show: Boolean, isNight: Boolean = false) {
|
||||||
|
if (isNight) {
|
||||||
|
CoverConfig.coverShowNameN = show
|
||||||
|
} else {
|
||||||
|
CoverConfig.coverShowName = show
|
||||||
|
}
|
||||||
|
BookCover.upDefaultCover()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateShowAuthor(show: Boolean, isNight: Boolean = false) {
|
||||||
|
if (isNight) {
|
||||||
|
CoverConfig.coverShowAuthorN = show
|
||||||
|
} else {
|
||||||
|
CoverConfig.coverShowAuthor = show
|
||||||
|
}
|
||||||
|
BookCover.upDefaultCover()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateCoverStyle() {
|
||||||
|
BookCover.upDefaultCover()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
package io.legado.app.ui.config.coverConfig
|
||||||
|
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import coil.compose.AsyncImage
|
||||||
|
import io.legado.app.R
|
||||||
|
import io.legado.app.constant.PreferKey
|
||||||
|
import io.legado.app.ui.widget.components.filePicker.FilePickerSheet
|
||||||
|
import io.legado.app.ui.widget.components.modalBottomSheet.GlassModalBottomSheet
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun CoverManageSheet(
|
||||||
|
preferenceKey: String,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
viewModel: CoverConfigViewModel
|
||||||
|
) {
|
||||||
|
var showFilePicker by remember { mutableStateOf(false) }
|
||||||
|
val selectImages =
|
||||||
|
rememberLauncherForActivityResult(ActivityResultContracts.GetMultipleContents()) { uris ->
|
||||||
|
if (uris.isNotEmpty()) {
|
||||||
|
viewModel.addCoverFromUri(preferenceKey, uris)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val currentCovers = if (preferenceKey == PreferKey.defaultCover) {
|
||||||
|
CoverConfig.defaultCover
|
||||||
|
} else {
|
||||||
|
CoverConfig.defaultCoverDark
|
||||||
|
}
|
||||||
|
val coverList = currentCovers.split(",").filter { it.isNotBlank() }
|
||||||
|
|
||||||
|
GlassModalBottomSheet(
|
||||||
|
onDismissRequest = onDismissRequest
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(bottom = 24.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.default_cover),
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
modifier = Modifier.padding(16.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
LazyVerticalGrid(
|
||||||
|
columns = GridCells.Fixed(3),
|
||||||
|
contentPadding = PaddingValues(16.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
items(coverList) { path ->
|
||||||
|
Box {
|
||||||
|
Surface(
|
||||||
|
shape = MaterialTheme.shapes.medium,
|
||||||
|
tonalElevation = 2.dp
|
||||||
|
) {
|
||||||
|
AsyncImage(
|
||||||
|
model = path,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.aspectRatio(0.7f),
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
}
|
||||||
|
IconButton(
|
||||||
|
onClick = { viewModel.removeCover(preferenceKey, path) },
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.TopEnd)
|
||||||
|
.padding(4.dp)
|
||||||
|
.size(24.dp)
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
shape = CircleShape,
|
||||||
|
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.7f)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Close,
|
||||||
|
contentDescription = "Remove",
|
||||||
|
modifier = Modifier.size(16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
Surface(
|
||||||
|
onClick = { showFilePicker = true },
|
||||||
|
shape = MaterialTheme.shapes.medium,
|
||||||
|
color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.aspectRatio(0.7f)
|
||||||
|
) {
|
||||||
|
Box(contentAlignment = Alignment.Center) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Add,
|
||||||
|
contentDescription = "Add",
|
||||||
|
modifier = Modifier.size(32.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showFilePicker) {
|
||||||
|
FilePickerSheet(
|
||||||
|
onDismissRequest = { showFilePicker = false },
|
||||||
|
onSelectSysFiles = {
|
||||||
|
selectImages.launch("image/*")
|
||||||
|
showFilePicker = false
|
||||||
|
},
|
||||||
|
allowExtensions = arrayOf("jpg", "jpeg", "png", "webp")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
package io.legado.app.ui.config.coverConfig
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.Checkbox
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedButton
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.legado.app.R
|
||||||
|
import io.legado.app.model.BookCover
|
||||||
|
import io.legado.app.ui.widget.components.modalBottomSheet.GlassModalBottomSheet
|
||||||
|
import io.legado.app.ui.widget.components.settingItem.InputSettingItem
|
||||||
|
import io.legado.app.utils.toastOnUi
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import splitties.init.appCtx
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun CoverRuleConfigSheet(
|
||||||
|
onDismissRequest: () -> Unit
|
||||||
|
) {
|
||||||
|
var enable by remember { mutableStateOf(false) }
|
||||||
|
var searchUrl by remember { mutableStateOf("") }
|
||||||
|
var coverRule by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
val rule = withContext(Dispatchers.IO) {
|
||||||
|
BookCover.getCoverRule()
|
||||||
|
}
|
||||||
|
enable = rule.enable
|
||||||
|
searchUrl = rule.searchUrl
|
||||||
|
coverRule = rule.coverRule
|
||||||
|
}
|
||||||
|
|
||||||
|
GlassModalBottomSheet(
|
||||||
|
onDismissRequest = onDismissRequest
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.cover_rule),
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
modifier = Modifier.padding(bottom = 16.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Checkbox(
|
||||||
|
checked = enable,
|
||||||
|
onCheckedChange = { enable = it }
|
||||||
|
)
|
||||||
|
Text(text = stringResource(R.string.enable))
|
||||||
|
}
|
||||||
|
|
||||||
|
InputSettingItem(
|
||||||
|
title = "搜索URL",
|
||||||
|
value = searchUrl,
|
||||||
|
onConfirm = { searchUrl = it }
|
||||||
|
)
|
||||||
|
|
||||||
|
InputSettingItem(
|
||||||
|
title = "封面规则",
|
||||||
|
value = coverRule,
|
||||||
|
onConfirm = { coverRule = it }
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
TextButton(onClick = {
|
||||||
|
BookCover.delCoverRule()
|
||||||
|
onDismissRequest()
|
||||||
|
}) {
|
||||||
|
Text(stringResource(R.string.delete), color = MaterialTheme.colorScheme.error)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
|
||||||
|
OutlinedButton(onClick = onDismissRequest) {
|
||||||
|
Text(stringResource(R.string.cancel))
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.padding(horizontal = 4.dp))
|
||||||
|
|
||||||
|
Button(onClick = {
|
||||||
|
if (searchUrl.isBlank() || coverRule.isBlank()) {
|
||||||
|
appCtx.toastOnUi("搜索url和cover规则不能为空")
|
||||||
|
} else {
|
||||||
|
BookCover.CoverRule(enable, searchUrl, coverRule).let { config ->
|
||||||
|
BookCover.saveCoverRule(config)
|
||||||
|
}
|
||||||
|
onDismissRequest()
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Text(stringResource(R.string.ok))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
package io.legado.app.ui.config.mainConfig
|
||||||
|
|
||||||
|
import io.legado.app.constant.PreferKey
|
||||||
|
import io.legado.app.ui.config.prefDelegate
|
||||||
|
|
||||||
|
object MainConfig {
|
||||||
|
var showDiscovery by prefDelegate(PreferKey.showDiscovery, true)
|
||||||
|
var showRSS by prefDelegate(PreferKey.showRss, true)
|
||||||
|
var showBottomView by prefDelegate(PreferKey.showBottomView, true)
|
||||||
|
var defaultHomePage by prefDelegate(PreferKey.defaultHomePage, "bookshelf")
|
||||||
|
var tabletInterface by prefDelegate(PreferKey.tabletInterface, "auto")
|
||||||
|
var labelVisibilityMode by prefDelegate(PreferKey.labelVisibilityMode, "auto")
|
||||||
|
var swipeAnimation by prefDelegate(PreferKey.swipeAnimation, true)
|
||||||
|
var navExtended by prefDelegate("navExtended", false)
|
||||||
|
var webServiceAutoStart by prefDelegate(PreferKey.webServiceAutoStart, false)
|
||||||
|
var autoRefreshBook by prefDelegate(PreferKey.autoRefresh, false)
|
||||||
|
var autoCheckNewBackup by prefDelegate(PreferKey.autoCheckNewBackup, true)
|
||||||
|
var showStatusBar by prefDelegate(PreferKey.showStatusBar, true)
|
||||||
|
}
|
||||||
@@ -1,39 +1,16 @@
|
|||||||
@file:Suppress("DEPRECATION")
|
|
||||||
|
|
||||||
package io.legado.app.ui.main
|
package io.legado.app.ui.main
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.format.DateUtils
|
import android.text.format.DateUtils
|
||||||
import android.view.Gravity
|
import androidx.compose.runtime.Composable
|
||||||
import android.view.MenuItem
|
|
||||||
import android.widget.FrameLayout
|
|
||||||
import android.widget.ImageView
|
|
||||||
import androidx.activity.OnBackPressedCallback
|
|
||||||
import androidx.activity.viewModels
|
|
||||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
import androidx.core.view.get
|
|
||||||
import androidx.core.view.postDelayed
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.fragment.app.FragmentActivity
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
|
||||||
import androidx.viewpager2.widget.ViewPager2
|
|
||||||
import com.google.android.material.bottomnavigation.BottomNavigationView
|
|
||||||
import com.google.android.material.bottomnavigation.LabelVisibilityMode
|
|
||||||
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
|
||||||
import com.google.android.material.navigation.NavigationBarView
|
|
||||||
import com.google.android.material.transition.platform.MaterialContainerTransformSharedElementCallback
|
|
||||||
import io.legado.app.BuildConfig
|
import io.legado.app.BuildConfig
|
||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
import io.legado.app.base.VMBaseActivity
|
import io.legado.app.base.BaseComposeActivity
|
||||||
import io.legado.app.constant.AppConst.appInfo
|
import io.legado.app.constant.AppConst.appInfo
|
||||||
import io.legado.app.constant.EventBus
|
|
||||||
import io.legado.app.constant.PreferKey
|
import io.legado.app.constant.PreferKey
|
||||||
import io.legado.app.data.entities.BookGroup
|
|
||||||
import io.legado.app.databinding.ActivityMainBinding
|
|
||||||
import io.legado.app.help.AppWebDav
|
import io.legado.app.help.AppWebDav
|
||||||
import io.legado.app.help.book.BookHelp
|
import io.legado.app.help.book.BookHelp
|
||||||
import io.legado.app.help.config.AppConfig
|
import io.legado.app.help.config.AppConfig
|
||||||
@@ -46,114 +23,36 @@ import io.legado.app.service.WebService
|
|||||||
import io.legado.app.ui.about.CrashLogsDialog
|
import io.legado.app.ui.about.CrashLogsDialog
|
||||||
import io.legado.app.ui.about.UpdateDialog
|
import io.legado.app.ui.about.UpdateDialog
|
||||||
import io.legado.app.ui.book.read.ReadBookActivity
|
import io.legado.app.ui.book.read.ReadBookActivity
|
||||||
import io.legado.app.ui.book.search.SearchActivity
|
|
||||||
import io.legado.app.ui.main.bookshelf.BaseBookshelfFragment
|
|
||||||
import io.legado.app.ui.main.bookshelf.books.BookshelfFragment1
|
|
||||||
import io.legado.app.ui.main.bookshelf.books.BookshelfFragment2
|
|
||||||
import io.legado.app.ui.main.bookshelf.books.BookshelfFragment3
|
|
||||||
import io.legado.app.ui.main.bookshelf.books.BookshelfFragment4
|
|
||||||
import io.legado.app.ui.main.explore.ExploreFragment
|
|
||||||
import io.legado.app.ui.main.my.MyFragment
|
|
||||||
import io.legado.app.ui.main.rss.RssFragment
|
|
||||||
import io.legado.app.ui.welcome.WelcomeActivity
|
import io.legado.app.ui.welcome.WelcomeActivity
|
||||||
import io.legado.app.ui.widget.dialog.TextDialog
|
import io.legado.app.ui.widget.dialog.TextDialog
|
||||||
import io.legado.app.utils.getPrefBoolean
|
import io.legado.app.utils.getPrefBoolean
|
||||||
import io.legado.app.utils.gone
|
|
||||||
import io.legado.app.utils.observeEvent
|
|
||||||
import io.legado.app.utils.setNavigationBarColorAuto
|
|
||||||
import io.legado.app.utils.showDialogFragment
|
import io.legado.app.utils.showDialogFragment
|
||||||
import io.legado.app.utils.startActivity
|
import io.legado.app.utils.startActivity
|
||||||
import io.legado.app.utils.themeColor
|
|
||||||
import io.legado.app.utils.toggleSystemBar
|
|
||||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
|
||||||
import io.legado.app.utils.visible
|
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
import kotlin.coroutines.suspendCoroutine
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 主界面
|
* 主界面
|
||||||
*/
|
*/
|
||||||
open class MainActivity : VMBaseActivity<ActivityMainBinding, MainViewModel>(),
|
open class MainActivity : BaseComposeActivity() {
|
||||||
BottomNavigationView.OnNavigationItemSelectedListener,
|
|
||||||
BottomNavigationView.OnNavigationItemReselectedListener {
|
|
||||||
|
|
||||||
override val binding by viewBinding(ActivityMainBinding::inflate)
|
private val viewModel by viewModel<MainViewModel>()
|
||||||
override val viewModel by viewModels<MainViewModel>()
|
|
||||||
private val idBookshelf = 0
|
|
||||||
private val idBookshelf1 = 11
|
|
||||||
private val idBookshelf2 = 12
|
|
||||||
private val idBookshelf3 = 13
|
|
||||||
private val idBookshelf4 = 14
|
|
||||||
private val idExplore = 1
|
|
||||||
private val idRss = 2
|
|
||||||
private val idMy = 3
|
|
||||||
private var bookshelfReselected: Long = 0
|
|
||||||
private var exploreReselected: Long = 0
|
|
||||||
private var pagePosition = 0
|
|
||||||
private val fragmentMap = hashMapOf<Int, Fragment>()
|
|
||||||
private var bottomMenuCount = 4
|
|
||||||
private val realPositions = arrayOf(idBookshelf, idExplore, idRss, idMy)
|
|
||||||
private val swipeAnimation = AppConfig.swipeAnimation
|
|
||||||
private val adapter by lazy {
|
|
||||||
TabFragmentPageAdapter(this)
|
|
||||||
}
|
|
||||||
private lateinit var backCallback: OnBackPressedCallback
|
|
||||||
private val badge by lazy {
|
|
||||||
getNavigationBarView().getOrCreateBadge(R.id.menu_bookshelf)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
|
||||||
super.onConfigurationChanged(newConfig)
|
|
||||||
recreate()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupBackCallback() {
|
|
||||||
backCallback = object : OnBackPressedCallback(false) {
|
|
||||||
override fun handleOnBackPressed() {
|
|
||||||
if (pagePosition != 0) {
|
|
||||||
binding.viewPagerMain.currentItem = 0
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onBackPressedDispatcher.addCallback(this, backCallback)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
installSplashScreen()
|
installSplashScreen()
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
toggleSystemBar(AppConfig.showStatusBar)
|
|
||||||
if (checkStartupRoute()) return
|
|
||||||
setExitSharedElementCallback(MaterialContainerTransformSharedElementCallback())
|
|
||||||
|
|
||||||
if (savedInstanceState != null) {
|
if (checkStartupRoute()) return
|
||||||
pagePosition = savedInstanceState.getInt("currentPagePosition", 0)
|
|
||||||
}
|
|
||||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S)
|
|
||||||
binding.viewPagerMain.fitsSystemWindows = true
|
|
||||||
// 其他初始化逻辑
|
|
||||||
setupBackCallback()
|
|
||||||
|
|
||||||
// 智能自启:如果上次是手动开启状态(web_service_auto 为 true),则自启
|
// 智能自启:如果上次是手动开启状态(web_service_auto 为 true),则自启
|
||||||
if (AppConfig.webServiceAutoStart) {
|
if (AppConfig.webServiceAutoStart) {
|
||||||
WebService.startForeground(this)
|
WebService.startForeground(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
upBottomMenu()
|
|
||||||
initView()
|
|
||||||
upHomePage()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
toggleSystemBar(AppConfig.showStatusBar)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPostCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onPostCreate(savedInstanceState)
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
//版本更新
|
//版本更新
|
||||||
upVersion()
|
upVersion()
|
||||||
@@ -164,51 +63,27 @@ open class MainActivity : VMBaseActivity<ActivityMainBinding, MainViewModel>(),
|
|||||||
//自动更新书籍
|
//自动更新书籍
|
||||||
val isAutoRefreshedBook = savedInstanceState?.getBoolean("isAutoRefreshedBook") ?: false
|
val isAutoRefreshedBook = savedInstanceState?.getBoolean("isAutoRefreshedBook") ?: false
|
||||||
if (AppConfig.autoRefreshBook && !isAutoRefreshedBook) {
|
if (AppConfig.autoRefreshBook && !isAutoRefreshedBook) {
|
||||||
binding.viewPagerMain.postDelayed(1000) {
|
viewModel.upAllBookToc()
|
||||||
viewModel.upAllBookToc()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
binding.viewPagerMain.postDelayed(3000) {
|
|
||||||
viewModel.postLoad()
|
|
||||||
}
|
}
|
||||||
|
viewModel.postLoad()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNavigationItemSelected(item: MenuItem): Boolean {
|
@Composable
|
||||||
val index = when (item.itemId) {
|
override fun Content() {
|
||||||
R.id.menu_bookshelf -> 0
|
val orientation = resources.configuration.orientation
|
||||||
R.id.menu_discovery -> realPositions.indexOf(idExplore)
|
val smallestWidthDp = resources.configuration.smallestScreenWidthDp
|
||||||
R.id.menu_rss -> realPositions.indexOf(idRss)
|
val tabletInterface = AppConfig.tabletInterface
|
||||||
R.id.menu_my_config -> realPositions.indexOf(idMy)
|
|
||||||
else -> 0
|
|
||||||
}
|
|
||||||
binding.viewPagerMain.setCurrentItem(index, swipeAnimation)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNavigationItemReselected(item: MenuItem) {
|
val useRail = when (tabletInterface) {
|
||||||
when (item.itemId) {
|
"always" -> true
|
||||||
R.id.menu_bookshelf -> {
|
"landscape" -> orientation == Configuration.ORIENTATION_LANDSCAPE
|
||||||
val fragment = fragmentMap[getFragmentId(0)] as? BookshelfFragment2
|
"off" -> false
|
||||||
if (fragment != null && fragment.groupId != BookGroup.IdRoot) {
|
"auto" -> smallestWidthDp >= 600
|
||||||
fragment.back()
|
else -> false
|
||||||
} else {
|
|
||||||
if (System.currentTimeMillis() - bookshelfReselected > 300) {
|
|
||||||
bookshelfReselected = System.currentTimeMillis()
|
|
||||||
} else {
|
|
||||||
fragment?.gotoTop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.menu_discovery -> {
|
|
||||||
if (System.currentTimeMillis() - exploreReselected > 300) {
|
|
||||||
exploreReselected = System.currentTimeMillis()
|
|
||||||
} else {
|
|
||||||
(fragmentMap[1] as? ExploreFragment)?.compressExplore()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MainScreen(useRail = useRail)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun checkStartupRoute(): Boolean {
|
private fun checkStartupRoute(): Boolean {
|
||||||
@@ -226,12 +101,6 @@ open class MainActivity : VMBaseActivity<ActivityMainBinding, MainViewModel>(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initView() = binding.run {
|
|
||||||
viewPagerMain.offscreenPageLimit = 3
|
|
||||||
viewPagerMain.adapter = adapter
|
|
||||||
viewPagerMain.registerOnPageChangeCallback(PageChangeCallback())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 版本更新日志
|
* 版本更新日志
|
||||||
*/
|
*/
|
||||||
@@ -275,10 +144,6 @@ open class MainActivity : VMBaseActivity<ActivityMainBinding, MainViewModel>(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置本地密码
|
|
||||||
*/
|
|
||||||
|
|
||||||
private fun notifyAppCrash() {
|
private fun notifyAppCrash() {
|
||||||
if (!LocalConfig.appCrash || BuildConfig.DEBUG) {
|
if (!LocalConfig.appCrash || BuildConfig.DEBUG) {
|
||||||
return
|
return
|
||||||
@@ -316,7 +181,6 @@ open class MainActivity : VMBaseActivity<ActivityMainBinding, MainViewModel>(),
|
|||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
override fun onSaveInstanceState(outState: Bundle) {
|
||||||
super.onSaveInstanceState(outState)
|
super.onSaveInstanceState(outState)
|
||||||
outState.putInt("currentPagePosition", pagePosition)
|
|
||||||
if (AppConfig.autoRefreshBook) {
|
if (AppConfig.autoRefreshBook) {
|
||||||
outState.putBoolean("isAutoRefreshedBook", true)
|
outState.putBoolean("isAutoRefreshedBook", true)
|
||||||
}
|
}
|
||||||
@@ -332,268 +196,6 @@ open class MainActivity : VMBaseActivity<ActivityMainBinding, MainViewModel>(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 如果重启太快fragment不会重建,这里更新一下书架的排序
|
|
||||||
*/
|
|
||||||
override fun recreate() {
|
|
||||||
(fragmentMap[getFragmentId(0)] as? BaseBookshelfFragment)?.run {
|
|
||||||
upSort()
|
|
||||||
}
|
|
||||||
super.recreate()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun observeLiveBus() {
|
|
||||||
super.observeLiveBus()
|
|
||||||
viewModel.onUpBooksLiveData.observe(this) {
|
|
||||||
badge.isVisible = it > 0
|
|
||||||
badge.number = it
|
|
||||||
}
|
|
||||||
|
|
||||||
observeEvent<Boolean>(EventBus.NOTIFY_MAIN) {
|
|
||||||
binding.apply {
|
|
||||||
upBottomMenu()
|
|
||||||
updateBookshelfIcon(true)
|
|
||||||
if (it) {
|
|
||||||
viewPagerMain.setCurrentItem(bottomMenuCount - 1, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
observeEvent<String>(PreferKey.threadCount) {
|
|
||||||
viewModel.upPool()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getNavigationBarView(): NavigationBarView {
|
|
||||||
val tabletInterface = AppConfig.tabletInterface
|
|
||||||
val orientation = resources.configuration.orientation
|
|
||||||
val smallestWidthDp = resources.configuration.smallestScreenWidthDp
|
|
||||||
|
|
||||||
val useRail = when (tabletInterface) {
|
|
||||||
"always" -> true
|
|
||||||
"landscape" -> orientation == Configuration.ORIENTATION_LANDSCAPE
|
|
||||||
"off" -> false
|
|
||||||
"auto" -> smallestWidthDp >= 600
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
|
|
||||||
return if (useRail) binding.navigationRailView else binding.bottomNavigationView
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
|
||||||
private fun upBottomMenu() {
|
|
||||||
val navView = getNavigationBarView()
|
|
||||||
val menu = navView.menu
|
|
||||||
|
|
||||||
menu.findItem(R.id.menu_discovery).isVisible = AppConfig.showDiscovery
|
|
||||||
menu.findItem(R.id.menu_rss).isVisible = AppConfig.showRSS
|
|
||||||
|
|
||||||
var index = 0
|
|
||||||
if (AppConfig.showDiscovery) {
|
|
||||||
index++
|
|
||||||
realPositions[index] = idExplore
|
|
||||||
}
|
|
||||||
if (AppConfig.showRSS) {
|
|
||||||
index++
|
|
||||||
realPositions[index] = idRss
|
|
||||||
}
|
|
||||||
index++
|
|
||||||
realPositions[index] = idMy
|
|
||||||
bottomMenuCount = index + 1
|
|
||||||
|
|
||||||
binding.viewPagerMain.adapter?.notifyDataSetChanged()
|
|
||||||
|
|
||||||
if (AppConfig.showBottomView) {
|
|
||||||
window.setNavigationBarColorAuto(themeColor(com.google.android.material.R.attr.colorSurfaceContainer))
|
|
||||||
val navView = getNavigationBarView()
|
|
||||||
navView.visible()
|
|
||||||
if (navView === binding.bottomNavigationView) {
|
|
||||||
binding.navigationRailView.gone()
|
|
||||||
} else {
|
|
||||||
binding.bottomNavigationView.gone()
|
|
||||||
}
|
|
||||||
|
|
||||||
navView.labelVisibilityMode = when (AppConfig.labelVisibilityMode) {
|
|
||||||
"auto" -> LabelVisibilityMode.LABEL_VISIBILITY_AUTO
|
|
||||||
"selected" -> LabelVisibilityMode.LABEL_VISIBILITY_SELECTED
|
|
||||||
"labeled" -> LabelVisibilityMode.LABEL_VISIBILITY_LABELED
|
|
||||||
"unlabeled" -> LabelVisibilityMode.LABEL_VISIBILITY_UNLABELED
|
|
||||||
else -> LabelVisibilityMode.LABEL_VISIBILITY_AUTO
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
window.setNavigationBarColorAuto(themeColor(com.google.android.material.R.attr.colorSurface))
|
|
||||||
binding.bottomNavigationView.gone()
|
|
||||||
binding.navigationRailView.gone()
|
|
||||||
}
|
|
||||||
|
|
||||||
val lp =
|
|
||||||
binding.navigationRailView.headerView!!.layoutParams as FrameLayout.LayoutParams
|
|
||||||
lp.gravity = Gravity.START
|
|
||||||
|
|
||||||
binding.navigationRailView.headerView!!
|
|
||||||
.setPadding(
|
|
||||||
binding.navigationRailView.itemActiveIndicatorExpandedMarginHorizontal,
|
|
||||||
0,
|
|
||||||
binding.navigationRailView.itemActiveIndicatorExpandedMarginHorizontal,
|
|
||||||
0)
|
|
||||||
|
|
||||||
val efab =
|
|
||||||
binding.navigationRailView.headerView!!.findViewById<ExtendedFloatingActionButton>(R.id.nav_fab)
|
|
||||||
val button =
|
|
||||||
binding.navigationRailView.headerView!!.findViewById<ImageView>(R.id.nav_botton)
|
|
||||||
|
|
||||||
efab.let {
|
|
||||||
if (LocalConfig.navExtended) {
|
|
||||||
it.isExtended = true
|
|
||||||
binding.navigationRailView.expand()
|
|
||||||
button.setImageResource(R.drawable.ic_menu_open)
|
|
||||||
} else {
|
|
||||||
it.isExtended = false
|
|
||||||
binding.navigationRailView.collapse()
|
|
||||||
button.setImageResource(R.drawable.ic_menu)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
efab.setOnClickListener {
|
|
||||||
startActivity<SearchActivity>()
|
|
||||||
}
|
|
||||||
|
|
||||||
button.setOnClickListener {
|
|
||||||
efab.let { it1 ->
|
|
||||||
if (it1.isExtended) {
|
|
||||||
efab.isExtended = false
|
|
||||||
binding.navigationRailView.collapse()
|
|
||||||
button.setImageResource(R.drawable.ic_menu)
|
|
||||||
} else {
|
|
||||||
efab.isExtended = true
|
|
||||||
binding.navigationRailView.expand()
|
|
||||||
button.setImageResource(R.drawable.ic_menu_open)
|
|
||||||
}
|
|
||||||
LocalConfig.navExtended = it1.isExtended
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
navView.setOnItemSelectedListener { onNavigationItemSelected(it) }
|
|
||||||
navView.setOnItemReselectedListener { onNavigationItemReselected(it) }
|
|
||||||
|
|
||||||
binding.viewPagerMain.post {
|
|
||||||
val currentPosition = if (pagePosition < bottomMenuCount) pagePosition else 0
|
|
||||||
binding.viewPagerMain.setCurrentItem(currentPosition, false)
|
|
||||||
getNavigationBarView().menu[currentPosition].isChecked = true
|
|
||||||
updateBackCallbackState()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateBookshelfIcon(isRoot: Boolean) {
|
|
||||||
val navView = getNavigationBarView()
|
|
||||||
val bookshelfMenuItem = navView.menu.findItem(R.id.menu_bookshelf) ?: return
|
|
||||||
|
|
||||||
if (isRoot) {
|
|
||||||
bookshelfMenuItem.setIcon(R.drawable.ic_bottom_books)
|
|
||||||
} else {
|
|
||||||
val currentFragment = fragmentMap[getFragmentId(0)]
|
|
||||||
if (currentFragment is BookshelfFragment2) {
|
|
||||||
bookshelfMenuItem.setIcon(R.drawable.ic_arrow_back)
|
|
||||||
} else {
|
|
||||||
bookshelfMenuItem.setIcon(R.drawable.ic_bottom_books)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun upHomePage() {
|
|
||||||
when (AppConfig.defaultHomePage) {
|
|
||||||
"bookshelf" -> {}
|
|
||||||
"explore" -> if (AppConfig.showDiscovery && AppConfig.showBottomView) {
|
|
||||||
binding.viewPagerMain.setCurrentItem(realPositions.indexOf(idExplore), false)
|
|
||||||
}
|
|
||||||
|
|
||||||
"rss" -> if (AppConfig.showRSS && AppConfig.showBottomView) {
|
|
||||||
binding.viewPagerMain.setCurrentItem(realPositions.indexOf(idRss), false)
|
|
||||||
}
|
|
||||||
|
|
||||||
"my" -> if (AppConfig.showBottomView) {
|
|
||||||
binding.viewPagerMain.setCurrentItem(realPositions.indexOf(idMy), false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getFragmentId(position: Int): Int {
|
|
||||||
val id = realPositions[position]
|
|
||||||
if (id == idBookshelf) {
|
|
||||||
return when (AppConfig.bookGroupStyle) {
|
|
||||||
0 -> idBookshelf1
|
|
||||||
1 -> idBookshelf2
|
|
||||||
2 -> idBookshelf3
|
|
||||||
3 -> idBookshelf4
|
|
||||||
else -> idBookshelf1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return id
|
|
||||||
}
|
|
||||||
|
|
||||||
private inner class PageChangeCallback : ViewPager2.OnPageChangeCallback() {
|
|
||||||
override fun onPageSelected(position: Int) {
|
|
||||||
pagePosition = position
|
|
||||||
getNavigationBarView().selectedItemId = when (realPositions[position]) {
|
|
||||||
idBookshelf -> R.id.menu_bookshelf
|
|
||||||
idExplore -> R.id.menu_discovery
|
|
||||||
idRss -> R.id.menu_rss
|
|
||||||
idMy -> R.id.menu_my_config
|
|
||||||
else -> R.id.menu_bookshelf
|
|
||||||
}
|
|
||||||
if (position == 0) {
|
|
||||||
val fragment = fragmentMap[getFragmentId(0)] as? BookshelfFragment2
|
|
||||||
updateBookshelfIcon(fragment?.groupId == BookGroup.IdRoot)
|
|
||||||
} else {
|
|
||||||
updateBookshelfIcon(true)
|
|
||||||
}
|
|
||||||
updateBackCallbackState()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateBackCallbackState() {
|
|
||||||
backCallback.isEnabled = (pagePosition != 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
private inner class TabFragmentPageAdapter(
|
|
||||||
activity: FragmentActivity
|
|
||||||
) : FragmentStateAdapter(activity) {
|
|
||||||
|
|
||||||
override fun getItemCount(): Int = bottomMenuCount
|
|
||||||
|
|
||||||
override fun createFragment(position: Int): Fragment {
|
|
||||||
val fragment = when (getFragmentId(position)) {
|
|
||||||
idBookshelf1 -> BookshelfFragment1(position)
|
|
||||||
idBookshelf2 -> BookshelfFragment2(position).apply {
|
|
||||||
this.onGroupIdChangedListener = { isRoot ->
|
|
||||||
if (pagePosition == 0) {
|
|
||||||
updateBookshelfIcon(isRoot)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
idBookshelf3 -> BookshelfFragment3(position)
|
|
||||||
idBookshelf4 -> BookshelfFragment4(position)
|
|
||||||
idExplore -> ExploreFragment(position)
|
|
||||||
idRss -> RssFragment(position)
|
|
||||||
else -> MyFragment.newInstance(position)
|
|
||||||
}
|
|
||||||
|
|
||||||
fragmentMap[getFragmentId(position)] = fragment
|
|
||||||
return fragment
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemId(position: Int): Long {
|
|
||||||
return getFragmentId(position).toLong()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun containsItem(itemId: Long): Boolean {
|
|
||||||
return (0 until bottomMenuCount).any { getItemId(it) == itemId }
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class LauncherW : MainActivity()
|
class LauncherW : MainActivity()
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package io.legado.app.ui.main
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.LibraryBooks
|
||||||
|
import androidx.compose.material.icons.automirrored.outlined.LibraryBooks
|
||||||
|
import androidx.compose.material.icons.filled.Explore
|
||||||
|
import androidx.compose.material.icons.filled.Person
|
||||||
|
import androidx.compose.material.icons.filled.RssFeed
|
||||||
|
import androidx.compose.material.icons.outlined.Explore
|
||||||
|
import androidx.compose.material.icons.outlined.Person
|
||||||
|
import androidx.compose.material.icons.outlined.RssFeed
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import io.legado.app.R
|
||||||
|
|
||||||
|
sealed class MainDestination(
|
||||||
|
val route: String,
|
||||||
|
@StringRes val labelId: Int,
|
||||||
|
val icon: ImageVector,
|
||||||
|
val selectedIcon: ImageVector
|
||||||
|
) {
|
||||||
|
object Bookshelf : MainDestination(
|
||||||
|
"bookshelf",
|
||||||
|
R.string.bookshelf,
|
||||||
|
Icons.AutoMirrored.Outlined.LibraryBooks,
|
||||||
|
Icons.AutoMirrored.Filled.LibraryBooks
|
||||||
|
)
|
||||||
|
|
||||||
|
object Explore : MainDestination(
|
||||||
|
"explore",
|
||||||
|
R.string.screen_find,
|
||||||
|
Icons.Outlined.Explore,
|
||||||
|
Icons.Default.Explore
|
||||||
|
)
|
||||||
|
|
||||||
|
object Rss : MainDestination(
|
||||||
|
"rss",
|
||||||
|
R.string.rss,
|
||||||
|
Icons.Outlined.RssFeed,
|
||||||
|
Icons.Default.RssFeed
|
||||||
|
)
|
||||||
|
|
||||||
|
object My : MainDestination(
|
||||||
|
"my",
|
||||||
|
R.string.my,
|
||||||
|
Icons.Outlined.Person,
|
||||||
|
Icons.Default.Person
|
||||||
|
)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val mainDestinations = listOf(Bookshelf, Explore, Rss, My)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
package io.legado.app.ui.main
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.MenuOpen
|
||||||
|
import androidx.compose.material.icons.filled.Menu
|
||||||
|
import androidx.compose.material.icons.filled.Search
|
||||||
|
import androidx.compose.material3.Badge
|
||||||
|
import androidx.compose.material3.BadgedBox
|
||||||
|
import androidx.compose.material3.BottomAppBarDefaults
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
|
import androidx.compose.material3.ExtendedFloatingActionButton
|
||||||
|
import androidx.compose.material3.FlexibleBottomAppBar
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.NavigationBarItem
|
||||||
|
import androidx.compose.material3.NavigationBarItemDefaults
|
||||||
|
import androidx.compose.material3.NavigationRail
|
||||||
|
import androidx.compose.material3.NavigationRailItem
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import dev.chrisbanes.haze.HazeState
|
||||||
|
import dev.chrisbanes.haze.hazeSource
|
||||||
|
import io.legado.app.R
|
||||||
|
import io.legado.app.ui.book.info.BookInfoActivity
|
||||||
|
import io.legado.app.ui.book.read.ReadBookActivity
|
||||||
|
import io.legado.app.ui.book.search.SearchActivity
|
||||||
|
import io.legado.app.ui.config.mainConfig.MainConfig
|
||||||
|
import io.legado.app.ui.main.bookshelf.BookshelfScreen
|
||||||
|
import io.legado.app.ui.main.explore.ExploreScreen
|
||||||
|
import io.legado.app.ui.main.my.MyScreen
|
||||||
|
import io.legado.app.ui.main.rss.RssScreen
|
||||||
|
import io.legado.app.ui.theme.regularHazeEffect
|
||||||
|
import io.legado.app.ui.widget.components.GlassDefaults
|
||||||
|
import io.legado.app.utils.startActivity
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.koin.androidx.compose.koinViewModel
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||||
|
@Composable
|
||||||
|
fun MainScreen(
|
||||||
|
viewModel: MainViewModel = koinViewModel(),
|
||||||
|
useRail: Boolean
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val uiState by viewModel.state.collectAsStateWithLifecycle()
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
val hazeState = remember { HazeState() }
|
||||||
|
val destinations = remember(MainConfig.showDiscovery, MainConfig.showRSS) {
|
||||||
|
MainDestination.mainDestinations.filter {
|
||||||
|
when (it) {
|
||||||
|
MainDestination.Explore -> MainConfig.showDiscovery
|
||||||
|
MainDestination.Rss -> MainConfig.showRSS
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val initialPage = remember(destinations) {
|
||||||
|
val index = destinations.indexOfFirst { it.route == MainConfig.defaultHomePage }
|
||||||
|
if (index != -1) index else 0
|
||||||
|
}
|
||||||
|
val pagerState = rememberPagerState(initialPage = initialPage) { destinations.size }
|
||||||
|
|
||||||
|
Row(modifier = Modifier.fillMaxSize()) {
|
||||||
|
if (useRail && MainConfig.showBottomView) {
|
||||||
|
NavigationRail(
|
||||||
|
header = {
|
||||||
|
IconButton(onClick = { MainConfig.navExtended = !MainConfig.navExtended }) {
|
||||||
|
Icon(
|
||||||
|
if (MainConfig.navExtended) Icons.AutoMirrored.Filled.MenuOpen else Icons.Default.Menu,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ExtendedFloatingActionButton(
|
||||||
|
onClick = { context.startActivity<SearchActivity>() },
|
||||||
|
expanded = MainConfig.navExtended,
|
||||||
|
icon = { Icon(Icons.Default.Search, contentDescription = null) },
|
||||||
|
text = { Text(text = stringResource(R.string.search)) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
destinations.forEachIndexed { index, destination ->
|
||||||
|
val selected = pagerState.currentPage == index
|
||||||
|
NavigationRailItem(
|
||||||
|
selected = selected,
|
||||||
|
onClick = {
|
||||||
|
coroutineScope.launch {
|
||||||
|
pagerState.animateScrollToPage(index)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon = {
|
||||||
|
NavigationIcon(destination, selected, uiState.upBooksCount)
|
||||||
|
},
|
||||||
|
label = { Text(stringResource(destination.labelId)) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
bottomBar = {
|
||||||
|
if (!useRail && MainConfig.showBottomView) {
|
||||||
|
FlexibleBottomAppBar(
|
||||||
|
modifier = Modifier.regularHazeEffect(state = hazeState),
|
||||||
|
containerColor = GlassDefaults.glassColor(
|
||||||
|
noBlurColor = BottomAppBarDefaults.containerColor,
|
||||||
|
blurAlpha = GlassDefaults.DefaultBlurAlpha
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
destinations.forEachIndexed { index, destination ->
|
||||||
|
val selected = pagerState.currentPage == index
|
||||||
|
NavigationBarItem(
|
||||||
|
selected = selected,
|
||||||
|
onClick = {
|
||||||
|
coroutineScope.launch {
|
||||||
|
pagerState.animateScrollToPage(index)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon = {
|
||||||
|
NavigationIcon(destination, selected, uiState.upBooksCount)
|
||||||
|
},
|
||||||
|
colors = NavigationBarItemDefaults.colors(
|
||||||
|
indicatorColor = GlassDefaults.glassColor(
|
||||||
|
noBlurColor = MaterialTheme.colorScheme.primaryContainer,
|
||||||
|
blurAlpha = GlassDefaults.ThickBlurAlpha
|
||||||
|
),
|
||||||
|
),
|
||||||
|
label = { Text(stringResource(destination.labelId)) },
|
||||||
|
alwaysShowLabel = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
contentWindowInsets = WindowInsets(0)
|
||||||
|
) { _ ->
|
||||||
|
Box(modifier = Modifier.hazeSource(hazeState)) {
|
||||||
|
HorizontalPager(
|
||||||
|
state = pagerState,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
userScrollEnabled = true,
|
||||||
|
beyondViewportPageCount = 1
|
||||||
|
) { page ->
|
||||||
|
when (destinations[page]) {
|
||||||
|
MainDestination.Bookshelf -> BookshelfScreen(
|
||||||
|
onBookClick = { book ->
|
||||||
|
context.startActivity<ReadBookActivity> {
|
||||||
|
putExtra("bookUrl", book.bookUrl)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onBookLongClick = { book ->
|
||||||
|
context.startActivity<BookInfoActivity> {
|
||||||
|
putExtra("name", book.name)
|
||||||
|
putExtra("author", book.author)
|
||||||
|
putExtra("bookUrl", book.bookUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
MainDestination.Explore -> ExploreScreen()
|
||||||
|
MainDestination.Rss -> RssScreen()
|
||||||
|
MainDestination.My -> MyScreen(
|
||||||
|
viewModel = koinViewModel(),
|
||||||
|
onNavigate = { event ->
|
||||||
|
viewModel.onPrefClickEvent(context, event)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun NavigationIcon(
|
||||||
|
destination: MainDestination,
|
||||||
|
selected: Boolean,
|
||||||
|
upBooksCount: Int
|
||||||
|
) {
|
||||||
|
val icon = if (selected) destination.selectedIcon else destination.icon
|
||||||
|
if (destination == MainDestination.Bookshelf && upBooksCount > 0) {
|
||||||
|
BadgedBox(badge = { Badge { Text(upBooksCount.toString()) } }) {
|
||||||
|
Icon(icon, contentDescription = null)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Icon(icon, contentDescription = null)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,251 +1,47 @@
|
|||||||
package io.legado.app.ui.main
|
package io.legado.app.ui.main
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import androidx.lifecycle.MutableLiveData
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import androidx.recyclerview.widget.RecyclerView.RecycledViewPool
|
|
||||||
import io.legado.app.base.BaseViewModel
|
import io.legado.app.base.BaseViewModel
|
||||||
import io.legado.app.constant.AppConst
|
|
||||||
import io.legado.app.constant.AppLog
|
|
||||||
import io.legado.app.constant.BookType
|
|
||||||
import io.legado.app.constant.EventBus
|
import io.legado.app.constant.EventBus
|
||||||
import io.legado.app.data.appDb
|
import io.legado.app.data.appDb
|
||||||
import io.legado.app.data.entities.Book
|
|
||||||
import io.legado.app.data.entities.BookSource
|
|
||||||
import io.legado.app.help.AppWebDav
|
import io.legado.app.help.AppWebDav
|
||||||
import io.legado.app.help.DefaultData
|
import io.legado.app.help.DefaultData
|
||||||
import io.legado.app.help.book.BookHelp
|
import io.legado.app.ui.main.my.PrefClickEvent
|
||||||
import io.legado.app.help.book.addType
|
import io.legado.app.utils.eventBus.FlowEventBus
|
||||||
import io.legado.app.help.book.isLocal
|
import io.legado.app.utils.sendToClip
|
||||||
import io.legado.app.help.book.isUpError
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import io.legado.app.help.book.removeType
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import io.legado.app.help.book.sync
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import io.legado.app.help.config.AppConfig
|
import kotlinx.coroutines.flow.map
|
||||||
import io.legado.app.model.CacheBook
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import io.legado.app.model.ReadBook
|
|
||||||
import io.legado.app.model.SourceCallBack
|
|
||||||
import io.legado.app.model.webBook.WebBook
|
|
||||||
import io.legado.app.service.CacheBookService
|
|
||||||
import io.legado.app.utils.onEachParallel
|
|
||||||
import io.legado.app.utils.postEvent
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.asCoroutineDispatcher
|
|
||||||
import kotlinx.coroutines.currentCoroutineContext
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.ensureActive
|
|
||||||
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.isActive
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.util.LinkedList
|
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
data class MainUiState(
|
||||||
import java.util.concurrent.Executors
|
val upBooksCount: Int = 0
|
||||||
import kotlin.math.min
|
)
|
||||||
|
|
||||||
class MainViewModel(application: Application) : BaseViewModel(application) {
|
class MainViewModel(application: Application) : BaseViewModel(application) {
|
||||||
private var threadCount = AppConfig.threadCount
|
|
||||||
private var poolSize = min(threadCount, AppConst.MAX_THREAD)
|
private val upBooksCountFlow = MutableStateFlow(0)
|
||||||
private var upTocPool = Executors.newFixedThreadPool(poolSize).asCoroutineDispatcher()
|
|
||||||
private val waitUpTocBooks = LinkedList<String>()
|
val state: StateFlow<MainUiState> = upBooksCountFlow
|
||||||
private val onUpTocBooks = ConcurrentHashMap.newKeySet<String>()
|
.map { count -> MainUiState(count) }
|
||||||
val onUpBooksLiveData = MutableLiveData<Int>()
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), MainUiState())
|
||||||
private var upTocJob: Job? = null
|
|
||||||
private var cacheBookJob: Job? = null
|
|
||||||
private val eventListenerSource = ConcurrentHashMap<BookSource, Boolean>()
|
|
||||||
val booksListRecycledViewPool = RecycledViewPool().apply {
|
|
||||||
setMaxRecycledViews(0, 30)
|
|
||||||
}
|
|
||||||
val booksGridRecycledViewPool = RecycledViewPool().apply {
|
|
||||||
setMaxRecycledViews(0, 100)
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
|
viewModelScope.launch {
|
||||||
|
FlowEventBus.with<Int>(EventBus.UP_BOOKSHELF_COUNT).collect {
|
||||||
|
upBooksCountFlow.value = it
|
||||||
|
}
|
||||||
|
}
|
||||||
deleteNotShelfBook()
|
deleteNotShelfBook()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
|
||||||
super.onCleared()
|
|
||||||
upTocPool.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun upPool() {
|
|
||||||
threadCount = AppConfig.threadCount
|
|
||||||
if (upTocJob?.isActive == true || cacheBookJob?.isActive == true) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val newPoolSize = min(threadCount, AppConst.MAX_THREAD)
|
|
||||||
if (poolSize == newPoolSize) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
poolSize = newPoolSize
|
|
||||||
upTocPool.close()
|
|
||||||
upTocPool = Executors.newFixedThreadPool(poolSize).asCoroutineDispatcher()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isUpdate(bookUrl: String): Boolean {
|
|
||||||
return onUpTocBooks.contains(bookUrl)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun upAllBookToc() {
|
fun upAllBookToc() {
|
||||||
execute {
|
FlowEventBus.post(EventBus.UP_ALL_BOOK_TOC, Unit)
|
||||||
addToWaitUp(appDb.bookDao.hasUpdateBooks)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun upToc(books: List<Book>) {
|
|
||||||
execute(context = upTocPool) {
|
|
||||||
books.filter {
|
|
||||||
!it.isLocal && it.canUpdate
|
|
||||||
}.let {
|
|
||||||
addToWaitUp(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
private fun addToWaitUp(books: List<Book>) {
|
|
||||||
books.forEach { book ->
|
|
||||||
if (!waitUpTocBooks.contains(book.bookUrl) && !onUpTocBooks.contains(book.bookUrl)) {
|
|
||||||
waitUpTocBooks.add(book.bookUrl)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (upTocJob == null) {
|
|
||||||
startUpTocJob()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startUpTocJob() {
|
|
||||||
upPool()
|
|
||||||
postUpBooksLiveData()
|
|
||||||
upTocJob = viewModelScope.launch(upTocPool) {
|
|
||||||
flow {
|
|
||||||
while (true) {
|
|
||||||
emit(waitUpTocBooks.poll() ?: break)
|
|
||||||
}
|
|
||||||
}.onEachParallel(threadCount) {
|
|
||||||
onUpTocBooks.add(it)
|
|
||||||
postEvent(EventBus.UP_BOOKSHELF, it)
|
|
||||||
updateToc(it)
|
|
||||||
}.onEach {
|
|
||||||
onUpTocBooks.remove(it)
|
|
||||||
postEvent(EventBus.UP_BOOKSHELF, it)
|
|
||||||
postUpBooksLiveData()
|
|
||||||
}.onCompletion {
|
|
||||||
upTocJob = null
|
|
||||||
if (waitUpTocBooks.isNotEmpty()) {
|
|
||||||
startUpTocJob()
|
|
||||||
}
|
|
||||||
if (it == null && cacheBookJob == null && !CacheBookService.isRun) {
|
|
||||||
//所有目录更新完再开始缓存章节
|
|
||||||
cacheBook()
|
|
||||||
}
|
|
||||||
}.catch {
|
|
||||||
AppLog.put("更新目录出错\n${it.localizedMessage}", it)
|
|
||||||
}.collect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun updateToc(bookUrl: String) {
|
|
||||||
val book = appDb.bookDao.getBook(bookUrl) ?: return
|
|
||||||
val source = appDb.bookSourceDao.getBookSource(book.origin)
|
|
||||||
if (source == null) {
|
|
||||||
if (!book.isUpError) {
|
|
||||||
book.addType(BookType.updateError)
|
|
||||||
appDb.bookDao.update(book)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (source.eventListener) {
|
|
||||||
// 使用 putIfAbsent 确保只添加一次
|
|
||||||
if (eventListenerSource.putIfAbsent(source, true) == null) {
|
|
||||||
// 通知监听事件的书源,书架刷新开始
|
|
||||||
SourceCallBack.callBackSource(
|
|
||||||
viewModelScope,
|
|
||||||
SourceCallBack.START_SHELF_REFRESH,
|
|
||||||
source
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
kotlin.runCatching {
|
|
||||||
val oldBook = book.copy()
|
|
||||||
if (book.tocUrl.isBlank()) {
|
|
||||||
WebBook.getBookInfoAwait(source, book)
|
|
||||||
} else {
|
|
||||||
WebBook.runPreUpdateJs(source, book)
|
|
||||||
}
|
|
||||||
val toc = WebBook.getChapterListAwait(source, book).getOrThrow()
|
|
||||||
book.sync(oldBook)
|
|
||||||
book.removeType(BookType.updateError)
|
|
||||||
if (book.bookUrl == bookUrl) {
|
|
||||||
appDb.bookDao.update(book)
|
|
||||||
} else {
|
|
||||||
appDb.bookDao.replace(oldBook, book)
|
|
||||||
BookHelp.updateCacheFolder(oldBook, book)
|
|
||||||
}
|
|
||||||
appDb.bookChapterDao.delByBook(bookUrl)
|
|
||||||
appDb.bookChapterDao.insert(*toc.toTypedArray())
|
|
||||||
ReadBook.onChapterListUpdated(book)
|
|
||||||
addDownload(source, book)
|
|
||||||
}.onFailure {
|
|
||||||
currentCoroutineContext().ensureActive()
|
|
||||||
AppLog.put("${book.name} 更新目录失败\n${it.localizedMessage}", it)
|
|
||||||
//这里可能因为时间太长书籍信息已经更改,所以重新获取
|
|
||||||
appDb.bookDao.getBook(book.bookUrl)?.let { book ->
|
|
||||||
book.addType(BookType.updateError)
|
|
||||||
appDb.bookDao.update(book)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun postUpBooksLiveData(reset: Boolean = false) {
|
|
||||||
if (AppConfig.showWaitUpCount) {
|
|
||||||
onUpBooksLiveData.postValue(waitUpTocBooks.size + onUpTocBooks.size)
|
|
||||||
} else if (reset) {
|
|
||||||
onUpBooksLiveData.postValue(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
private fun addDownload(source: BookSource, book: Book) {
|
|
||||||
if (AppConfig.preDownloadNum == 0) return
|
|
||||||
val endIndex = min(
|
|
||||||
book.totalChapterNum - 1,
|
|
||||||
book.durChapterIndex.plus(AppConfig.preDownloadNum)
|
|
||||||
)
|
|
||||||
val cacheBook = CacheBook.getOrCreate(source, book)
|
|
||||||
cacheBook.addDownload(book.durChapterIndex, endIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 缓存书籍
|
|
||||||
*/
|
|
||||||
private fun cacheBook() {
|
|
||||||
//开始缓存前,通知监听事件的书源,书架刷新已完成
|
|
||||||
eventListenerSource.toList().forEach {
|
|
||||||
SourceCallBack.callBackSource(
|
|
||||||
viewModelScope,
|
|
||||||
SourceCallBack.END_SHELF_REFRESH,
|
|
||||||
it.first
|
|
||||||
)
|
|
||||||
}
|
|
||||||
eventListenerSource.clear()
|
|
||||||
if (AppConfig.preDownloadNum == 0) return
|
|
||||||
cacheBookJob?.cancel()
|
|
||||||
cacheBookJob = viewModelScope.launch(upTocPool) {
|
|
||||||
launch {
|
|
||||||
while (isActive && CacheBook.isRun) {
|
|
||||||
val isOnUpTocBooksEmpty =
|
|
||||||
onUpTocBooks.isEmpty()
|
|
||||||
//有目录更新是不缓存,优先更新目录,现在更多网站限制并发
|
|
||||||
CacheBook.setWorkingState(waitUpTocBooks.isEmpty() && isOnUpTocBooksEmpty)
|
|
||||||
delay(1000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CacheBook.startProcessJob(upTocPool)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun postLoad() {
|
fun postLoad() {
|
||||||
@@ -270,4 +66,34 @@ class MainViewModel(application: Application) : BaseViewModel(application) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onPrefClickEvent(context: Context, event: PrefClickEvent) {
|
||||||
|
when (event) {
|
||||||
|
is PrefClickEvent.OpenUrl -> context.startActivity(
|
||||||
|
Intent(
|
||||||
|
Intent.ACTION_VIEW,
|
||||||
|
android.net.Uri.parse(event.url)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
is PrefClickEvent.CopyUrl -> context.sendToClip(event.url)
|
||||||
|
is PrefClickEvent.ShowMd -> {
|
||||||
|
// Handle showing MD dialog
|
||||||
|
}
|
||||||
|
|
||||||
|
is PrefClickEvent.StartActivity -> {
|
||||||
|
context.startActivity(Intent(context, event.destination).apply {
|
||||||
|
event.configTag?.let { putExtra("configTag", it) }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
PrefClickEvent.ExitApp -> {
|
||||||
|
if (context is androidx.activity.ComponentActivity) {
|
||||||
|
context.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,179 +0,0 @@
|
|||||||
package io.legado.app.ui.main.bookshelf
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.view.Menu
|
|
||||||
import android.view.MenuItem
|
|
||||||
import androidx.fragment.app.activityViewModels
|
|
||||||
import androidx.fragment.app.viewModels
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import io.legado.app.R
|
|
||||||
import io.legado.app.base.VMBaseFragment
|
|
||||||
import io.legado.app.data.appDb
|
|
||||||
import io.legado.app.data.entities.Book
|
|
||||||
import io.legado.app.data.entities.BookGroup
|
|
||||||
import io.legado.app.databinding.DialogEditTextBinding
|
|
||||||
import io.legado.app.help.DirectLinkUpload
|
|
||||||
import io.legado.app.lib.dialogs.alert
|
|
||||||
import io.legado.app.ui.about.AppLogDialog
|
|
||||||
import io.legado.app.ui.book.cache.CacheActivity
|
|
||||||
import io.legado.app.ui.book.group.GroupManageDialog
|
|
||||||
import io.legado.app.ui.book.import.local.ImportBookActivity
|
|
||||||
import io.legado.app.ui.book.import.remote.RemoteBookActivity
|
|
||||||
import io.legado.app.ui.book.manage.BookshelfManageActivity
|
|
||||||
import io.legado.app.ui.book.search.SearchActivity
|
|
||||||
import io.legado.app.ui.file.HandleFileContract
|
|
||||||
import io.legado.app.ui.main.MainFragmentInterface
|
|
||||||
import io.legado.app.ui.main.MainViewModel
|
|
||||||
import io.legado.app.ui.widget.dialog.WaitDialog
|
|
||||||
import io.legado.app.utils.isAbsUrl
|
|
||||||
import io.legado.app.utils.readText
|
|
||||||
import io.legado.app.utils.sendToClip
|
|
||||||
import io.legado.app.utils.showDialogFragment
|
|
||||||
import io.legado.app.utils.startActivity
|
|
||||||
import io.legado.app.utils.toastOnUi
|
|
||||||
|
|
||||||
abstract class BaseBookshelfFragment(layoutId: Int) : VMBaseFragment<BookshelfViewModel>(layoutId),
|
|
||||||
MainFragmentInterface {
|
|
||||||
|
|
||||||
override val position: Int? get() = arguments?.getInt("position")
|
|
||||||
|
|
||||||
val activityViewModel by activityViewModels<MainViewModel>()
|
|
||||||
override val viewModel by viewModels<BookshelfViewModel>()
|
|
||||||
|
|
||||||
private val importBookshelf = registerForActivityResult(HandleFileContract()) {
|
|
||||||
kotlin.runCatching {
|
|
||||||
it.uri?.readText(requireContext())?.let { text ->
|
|
||||||
viewModel.importBookshelf(text, groupId)
|
|
||||||
}
|
|
||||||
}.onFailure {
|
|
||||||
toastOnUi(it.localizedMessage ?: "ERROR")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private val exportResult = registerForActivityResult(HandleFileContract()) {
|
|
||||||
it.uri?.let { uri ->
|
|
||||||
alert(R.string.export_success) {
|
|
||||||
if (uri.toString().isAbsUrl()) {
|
|
||||||
setMessage(DirectLinkUpload.getSummary())
|
|
||||||
}
|
|
||||||
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
|
|
||||||
editView.hint = getString(R.string.path)
|
|
||||||
editView.setText(uri.toString())
|
|
||||||
}
|
|
||||||
customView { alertBinding.root }
|
|
||||||
okButton {
|
|
||||||
requireContext().sendToClip(uri.toString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
abstract val groupId: Long
|
|
||||||
abstract val books: List<Book>
|
|
||||||
private var groupsLiveData: LiveData<List<BookGroup>>? = null
|
|
||||||
private val waitDialog by lazy {
|
|
||||||
WaitDialog(requireContext()).apply {
|
|
||||||
setOnCancelListener {
|
|
||||||
viewModel.addBookJob?.cancel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract fun gotoTop()
|
|
||||||
|
|
||||||
override fun onCompatCreateOptionsMenu(menu: Menu) {
|
|
||||||
menuInflater.inflate(R.menu.main_bookshelf, menu)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCompatOptionsItemSelected(item: MenuItem) {
|
|
||||||
super.onCompatOptionsItemSelected(item)
|
|
||||||
when (item.itemId) {
|
|
||||||
R.id.menu_remote -> startActivity<RemoteBookActivity>()
|
|
||||||
R.id.menu_search -> startActivity<SearchActivity>()
|
|
||||||
R.id.menu_update_toc -> activityViewModel.upToc(books)
|
|
||||||
R.id.menu_bookshelf_layout -> showDialogFragment<BookshelfConfigBottomSheet>()
|
|
||||||
R.id.menu_group_manage -> showDialogFragment<GroupManageDialog>()
|
|
||||||
R.id.menu_add_local -> startActivity<ImportBookActivity>()
|
|
||||||
R.id.menu_add_url -> showAddBookByUrlAlert()
|
|
||||||
R.id.menu_bookshelf_manage -> startActivity<BookshelfManageActivity> {
|
|
||||||
putExtra("groupId", groupId)
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.menu_download -> startActivity<CacheActivity> {
|
|
||||||
putExtra("groupId", groupId)
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.menu_export_bookshelf -> viewModel.exportBookshelf(books) { file ->
|
|
||||||
exportResult.launch {
|
|
||||||
mode = HandleFileContract.EXPORT
|
|
||||||
fileData =
|
|
||||||
HandleFileContract.FileData("bookshelf.json", file, "application/json")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.menu_import_bookshelf -> importBookshelfAlert(groupId)
|
|
||||||
R.id.menu_log -> showDialogFragment<AppLogDialog>()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected fun initBookGroupData() {
|
|
||||||
groupsLiveData?.removeObservers(viewLifecycleOwner)
|
|
||||||
groupsLiveData = appDb.bookGroupDao.show.apply {
|
|
||||||
observe(viewLifecycleOwner) {
|
|
||||||
upGroup(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract fun upGroup(data: List<BookGroup>)
|
|
||||||
|
|
||||||
abstract fun upSort()
|
|
||||||
|
|
||||||
override fun observeLiveBus() {
|
|
||||||
viewModel.addBookProgressLiveData.observe(this) { count ->
|
|
||||||
if (count < 0) {
|
|
||||||
waitDialog.dismiss()
|
|
||||||
} else {
|
|
||||||
waitDialog.setText("添加中... ($count)")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("InflateParams")
|
|
||||||
fun showAddBookByUrlAlert() {
|
|
||||||
alert(titleResource = R.string.add_book_url) {
|
|
||||||
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
|
|
||||||
editView.hint = "url"
|
|
||||||
}
|
|
||||||
customView { alertBinding.root }
|
|
||||||
okButton {
|
|
||||||
alertBinding.editView.text?.toString()?.let {
|
|
||||||
waitDialog.setText("添加中...")
|
|
||||||
waitDialog.show()
|
|
||||||
viewModel.addBookByUrl(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cancelButton()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun importBookshelfAlert(groupId: Long) {
|
|
||||||
alert(titleResource = R.string.import_bookshelf) {
|
|
||||||
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
|
|
||||||
editView.hint = "url/json"
|
|
||||||
}
|
|
||||||
customView { alertBinding.root }
|
|
||||||
okButton {
|
|
||||||
alertBinding.editView.text?.toString()?.let {
|
|
||||||
viewModel.importBookshelf(it, groupId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cancelButton()
|
|
||||||
neutralButton(R.string.select_file) {
|
|
||||||
importBookshelf.launch {
|
|
||||||
mode = HandleFileContract.FILE
|
|
||||||
allowExtensions = arrayOf("txt", "json")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,325 @@
|
|||||||
|
package io.legado.app.ui.main.bookshelf
|
||||||
|
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.RowScope
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.Shadow
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.legado.app.data.entities.Book
|
||||||
|
import io.legado.app.data.entities.BookGroup
|
||||||
|
import io.legado.app.ui.widget.components.cover.BookCover
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用的书架条目布局组件
|
||||||
|
* 支持 列表/网格 模式及 紧凑/常规 样式
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun BookshelfItem(
|
||||||
|
isGrid: Boolean,
|
||||||
|
isCompact: Boolean,
|
||||||
|
cover: @Composable (Modifier) -> Unit,
|
||||||
|
title: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
subTitle: String? = null,
|
||||||
|
desc: String? = null,
|
||||||
|
extra: @Composable (RowScope.() -> Unit)? = null,
|
||||||
|
titleSmallFont: Boolean = false,
|
||||||
|
titleCenter: Boolean = true,
|
||||||
|
titleMaxLines: Int = 2,
|
||||||
|
coverShadow: Boolean = false,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
onLongClick: () -> Unit
|
||||||
|
) {
|
||||||
|
if (isGrid) {
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.clip(MaterialTheme.shapes.small)
|
||||||
|
.combinedClickable(
|
||||||
|
onClick = onClick,
|
||||||
|
onLongClick = onLongClick
|
||||||
|
)
|
||||||
|
.padding(4.dp)
|
||||||
|
) {
|
||||||
|
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
|
val coverModifier = if (coverShadow) {
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.shadow(4.dp, RoundedCornerShape(4.dp))
|
||||||
|
} else {
|
||||||
|
Modifier.fillMaxWidth()
|
||||||
|
}
|
||||||
|
|
||||||
|
Box {
|
||||||
|
cover(coverModifier)
|
||||||
|
if (isCompact) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = (if (titleSmallFont) MaterialTheme.typography.labelSmall else MaterialTheme.typography.labelMedium).copy(
|
||||||
|
color = Color.White,
|
||||||
|
shadow = Shadow(
|
||||||
|
color = Color.Black.copy(alpha = 0.5f),
|
||||||
|
blurRadius = 4f
|
||||||
|
)
|
||||||
|
),
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomStart)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(
|
||||||
|
Brush.verticalGradient(
|
||||||
|
colors = listOf(
|
||||||
|
Color.Transparent,
|
||||||
|
Color.Black.copy(alpha = 0.7f)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.padding(horizontal = 6.dp, vertical = 6.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isCompact) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = if (titleSmallFont) MaterialTheme.typography.labelSmall else MaterialTheme.typography.labelMedium,
|
||||||
|
maxLines = titleMaxLines,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
textAlign = if (titleCenter) TextAlign.Center else TextAlign.Start,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 列表布局
|
||||||
|
Column {
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.combinedClickable(
|
||||||
|
onClick = onClick,
|
||||||
|
onLongClick = onLongClick
|
||||||
|
)
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
cover(
|
||||||
|
Modifier
|
||||||
|
.width(if (!isCompact) 70.dp else 55.dp)
|
||||||
|
.padding(end = 12.dp)
|
||||||
|
)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.Bold),
|
||||||
|
maxLines = if (!isCompact) 2 else 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
subTitle?.let {
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.padding(top = 2.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (!isCompact) {
|
||||||
|
desc?.let {
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.padding(top = 2.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
extra?.let {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(top = 2.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
content = it
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!isCompact) HorizontalDivider(
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
|
thickness = 0.5.dp,
|
||||||
|
color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BookGroupCover(
|
||||||
|
books: List<Book>,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.aspectRatio(5f / 7f)
|
||||||
|
.clip(RoundedCornerShape(4.dp))
|
||||||
|
.background(MaterialTheme.colorScheme.surfaceVariant)
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.fillMaxSize()) {
|
||||||
|
Row(modifier = Modifier.weight(1f)) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.fillMaxHeight()
|
||||||
|
.padding(1.dp)
|
||||||
|
) {
|
||||||
|
books.getOrNull(0)?.let {
|
||||||
|
BookCover(
|
||||||
|
name = it.name,
|
||||||
|
author = it.author,
|
||||||
|
path = it.getDisplayCover(),
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.fillMaxHeight()
|
||||||
|
.padding(1.dp)
|
||||||
|
) {
|
||||||
|
books.getOrNull(1)?.let {
|
||||||
|
BookCover(
|
||||||
|
name = it.name,
|
||||||
|
author = it.author,
|
||||||
|
path = it.getDisplayCover(),
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Row(modifier = Modifier.weight(1f)) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.fillMaxHeight()
|
||||||
|
.padding(1.dp)
|
||||||
|
) {
|
||||||
|
books.getOrNull(2)?.let {
|
||||||
|
BookCover(
|
||||||
|
name = it.name,
|
||||||
|
author = it.author,
|
||||||
|
path = it.getDisplayCover(),
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.fillMaxHeight()
|
||||||
|
.padding(1.dp)
|
||||||
|
) {
|
||||||
|
books.getOrNull(3)?.let {
|
||||||
|
BookCover(
|
||||||
|
name = it.name,
|
||||||
|
author = it.author,
|
||||||
|
path = it.getDisplayCover(),
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BookGroupItemGrid(
|
||||||
|
group: BookGroup,
|
||||||
|
previewBooks: List<Book>,
|
||||||
|
isCompact: Boolean = false,
|
||||||
|
titleSmallFont: Boolean = false,
|
||||||
|
titleCenter: Boolean = true,
|
||||||
|
titleMaxLines: Int = 2,
|
||||||
|
coverShadow: Boolean = false,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
onLongClick: () -> Unit
|
||||||
|
) {
|
||||||
|
BookshelfItem(
|
||||||
|
isGrid = true,
|
||||||
|
isCompact = isCompact,
|
||||||
|
cover = { BookGroupCover(books = previewBooks, modifier = it) },
|
||||||
|
title = group.groupName,
|
||||||
|
modifier = modifier,
|
||||||
|
titleSmallFont = titleSmallFont,
|
||||||
|
titleCenter = titleCenter,
|
||||||
|
titleMaxLines = titleMaxLines,
|
||||||
|
coverShadow = coverShadow,
|
||||||
|
onClick = onClick,
|
||||||
|
onLongClick = onLongClick
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BookGroupItemList(
|
||||||
|
group: BookGroup,
|
||||||
|
previewBooks: List<Book>,
|
||||||
|
isCompact: Boolean = false,
|
||||||
|
titleSmallFont: Boolean = false,
|
||||||
|
titleCenter: Boolean = true,
|
||||||
|
titleMaxLines: Int = 2,
|
||||||
|
coverShadow: Boolean = false,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
onLongClick: () -> Unit
|
||||||
|
) {
|
||||||
|
BookshelfItem(
|
||||||
|
isGrid = false,
|
||||||
|
isCompact = isCompact,
|
||||||
|
cover = { BookGroupCover(books = previewBooks, modifier = it) },
|
||||||
|
title = group.groupName,
|
||||||
|
titleSmallFont = titleSmallFont,
|
||||||
|
titleCenter = titleCenter,
|
||||||
|
titleMaxLines = titleMaxLines,
|
||||||
|
coverShadow = coverShadow,
|
||||||
|
modifier = modifier,
|
||||||
|
subTitle = "${previewBooks.size} 本书籍",
|
||||||
|
desc = "点击打开文件夹",
|
||||||
|
onClick = onClick,
|
||||||
|
onLongClick = onLongClick
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,228 +0,0 @@
|
|||||||
package io.legado.app.ui.main.bookshelf
|
|
||||||
|
|
||||||
import android.content.res.Configuration
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import androidx.fragment.app.activityViewModels
|
|
||||||
import com.google.android.material.chip.Chip
|
|
||||||
import io.legado.app.R
|
|
||||||
import io.legado.app.base.BaseBottomSheetDialogFragment
|
|
||||||
import io.legado.app.constant.EventBus
|
|
||||||
import io.legado.app.databinding.DialogBookshelfConfigBinding
|
|
||||||
import io.legado.app.help.config.AppConfig
|
|
||||||
import io.legado.app.ui.main.MainViewModel
|
|
||||||
import io.legado.app.ui.widget.number.NumberPickerDialog
|
|
||||||
import io.legado.app.utils.bookshelfLayoutGrid
|
|
||||||
import io.legado.app.utils.postEvent
|
|
||||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
|
||||||
|
|
||||||
class BookshelfConfigBottomSheet : BaseBottomSheetDialogFragment(R.layout.dialog_bookshelf_config) {
|
|
||||||
|
|
||||||
private val binding by viewBinding(DialogBookshelfConfigBinding::bind)
|
|
||||||
private val activityViewModel by activityViewModels<MainViewModel>()
|
|
||||||
|
|
||||||
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
|
|
||||||
val orientation = requireContext().resources.configuration.orientation
|
|
||||||
|
|
||||||
val bookshelfLayout = if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
|
||||||
AppConfig.bookshelfLayoutModeLandscape
|
|
||||||
} else {
|
|
||||||
AppConfig.bookshelfLayoutModePortrait
|
|
||||||
}
|
|
||||||
|
|
||||||
val columnCount = requireContext().bookshelfLayoutGrid.takeIf { it > 0 } ?: 1
|
|
||||||
|
|
||||||
val bookshelfSort = AppConfig.bookshelfSort
|
|
||||||
|
|
||||||
binding.apply {
|
|
||||||
|
|
||||||
resources.getStringArray(R.array.group_style).forEachIndexed { index, label ->
|
|
||||||
val chip = Chip(context).apply {
|
|
||||||
text = label
|
|
||||||
isCheckable = true
|
|
||||||
isClickable = true
|
|
||||||
id = View.generateViewId()
|
|
||||||
}
|
|
||||||
chipGroupStyle.addView(chip)
|
|
||||||
if (index == AppConfig.bookGroupStyle) {
|
|
||||||
chipGroupStyle.check(chip.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resources.getStringArray(R.array.bookshelf_px_array).forEachIndexed { index, label ->
|
|
||||||
val chip = Chip(context).apply {
|
|
||||||
text = label
|
|
||||||
isCheckable = true
|
|
||||||
isClickable = true
|
|
||||||
id = index
|
|
||||||
}
|
|
||||||
chipGroupSort.addView(chip)
|
|
||||||
if (index == AppConfig.bookshelfSort) {
|
|
||||||
chipGroupSort.check(chip.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
swShowUnread.isChecked = AppConfig.showUnread
|
|
||||||
swShowUnreadNew.isChecked = AppConfig.showUnreadNew
|
|
||||||
swShowTip.isChecked = AppConfig.showTip
|
|
||||||
swShowLastUpdateTime.isChecked = AppConfig.showLastUpdateTime
|
|
||||||
swShowWaitUpBooks.isChecked = AppConfig.showWaitUpCount
|
|
||||||
swShowBookshelfFastScroller.isChecked = AppConfig.showBookshelfFastScroller
|
|
||||||
swShowBookshelfTabMenu.isChecked = AppConfig.shouldShowExpandButton
|
|
||||||
|
|
||||||
when (bookshelfLayout) {
|
|
||||||
0 -> chipGroupLayout.check(R.id.chip_list)
|
|
||||||
1 -> chipGroupLayout.check(R.id.chip_grid)
|
|
||||||
2 -> chipGroupLayout.check(R.id.chip_grid_compact)
|
|
||||||
3 -> chipGroupLayout.check(R.id.chip_grid_cover)
|
|
||||||
4 -> chipGroupLayout.check(R.id.chip_list_compact)
|
|
||||||
}
|
|
||||||
|
|
||||||
sliderGridCount.progress = columnCount
|
|
||||||
|
|
||||||
updateControlsEnableState()
|
|
||||||
|
|
||||||
chipGroupLayout.setOnCheckedStateChangeListener { group, checkedIds ->
|
|
||||||
updateControlsEnableState()
|
|
||||||
}
|
|
||||||
|
|
||||||
chipGroupStyle.setOnCheckedStateChangeListener { group, checkedIds ->
|
|
||||||
updateControlsEnableState()
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.tvValue.text = if (AppConfig.bookshelfRefreshingLimit <= 0) "无限制"
|
|
||||||
else "${AppConfig.bookshelfRefreshingLimit} 本"
|
|
||||||
|
|
||||||
binding.tvValue.setOnClickListener {
|
|
||||||
showNumberPicker()
|
|
||||||
}
|
|
||||||
binding.layoutRefreshLimit.setOnClickListener {
|
|
||||||
showNumberPicker()
|
|
||||||
}
|
|
||||||
|
|
||||||
when (AppConfig.bookshelfSortOrder) {
|
|
||||||
0 -> binding.chipGroupOrder.check(binding.chipAsc.id)
|
|
||||||
1 -> binding.chipGroupOrder.check(binding.chipDesc.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
btnOk.setOnClickListener {
|
|
||||||
var notifyMain = false
|
|
||||||
var recreate = false
|
|
||||||
|
|
||||||
if (AppConfig.bookGroupStyle != chipGroupStyle.checkedChipId) {
|
|
||||||
AppConfig.bookGroupStyle = chipGroupStyle.indexOfChild(
|
|
||||||
chipGroupStyle.findViewById(chipGroupStyle.checkedChipId)
|
|
||||||
)
|
|
||||||
notifyMain = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (AppConfig.showUnread != swShowUnread.isChecked) {
|
|
||||||
AppConfig.showUnread = swShowUnread.isChecked
|
|
||||||
postEvent(EventBus.BOOKSHELF_REFRESH, "")
|
|
||||||
}
|
|
||||||
if (AppConfig.showUnreadNew != swShowUnreadNew.isChecked) {
|
|
||||||
AppConfig.showUnreadNew = swShowUnreadNew.isChecked
|
|
||||||
postEvent(EventBus.BOOKSHELF_REFRESH, "")
|
|
||||||
}
|
|
||||||
if (AppConfig.showTip != swShowTip.isChecked) {
|
|
||||||
AppConfig.showTip = swShowTip.isChecked
|
|
||||||
postEvent(EventBus.BOOKSHELF_REFRESH, "")
|
|
||||||
}
|
|
||||||
if (AppConfig.showLastUpdateTime != swShowLastUpdateTime.isChecked) {
|
|
||||||
AppConfig.showLastUpdateTime = swShowLastUpdateTime.isChecked
|
|
||||||
postEvent(EventBus.BOOKSHELF_REFRESH, "")
|
|
||||||
}
|
|
||||||
if (AppConfig.showWaitUpCount != swShowWaitUpBooks.isChecked) {
|
|
||||||
AppConfig.showWaitUpCount = swShowWaitUpBooks.isChecked
|
|
||||||
activityViewModel.postUpBooksLiveData(true)
|
|
||||||
}
|
|
||||||
if (AppConfig.showBookshelfFastScroller != swShowBookshelfFastScroller.isChecked) {
|
|
||||||
AppConfig.showBookshelfFastScroller = swShowBookshelfFastScroller.isChecked
|
|
||||||
postEvent(EventBus.BOOKSHELF_REFRESH, "")
|
|
||||||
}
|
|
||||||
if (AppConfig.shouldShowExpandButton != swShowBookshelfTabMenu.isChecked) {
|
|
||||||
AppConfig.shouldShowExpandButton = swShowBookshelfTabMenu.isChecked
|
|
||||||
postEvent(EventBus.BOOKSHELF_REFRESH, "")
|
|
||||||
}
|
|
||||||
if (bookshelfSort != chipGroupSort.checkedChipId) {
|
|
||||||
AppConfig.bookshelfSort = chipGroupSort.checkedChipId
|
|
||||||
(requireParentFragment() as? BaseBookshelfFragment)?.upSort()
|
|
||||||
}
|
|
||||||
|
|
||||||
val selectedColumn = sliderGridCount.progress
|
|
||||||
val newLayout = when (chipGroupLayout.checkedChipId) {
|
|
||||||
R.id.chip_list -> 0
|
|
||||||
R.id.chip_grid -> 1
|
|
||||||
R.id.chip_grid_compact -> 2
|
|
||||||
R.id.chip_grid_cover -> 3
|
|
||||||
R.id.chip_list_compact -> 4
|
|
||||||
else -> 0
|
|
||||||
}
|
|
||||||
|
|
||||||
val oldColumn = requireContext().bookshelfLayoutGrid
|
|
||||||
if (bookshelfLayout != newLayout || oldColumn != selectedColumn) {
|
|
||||||
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
|
||||||
AppConfig.bookshelfLayoutModeLandscape = newLayout
|
|
||||||
AppConfig.bookshelfLayoutGridLandscape = selectedColumn
|
|
||||||
} else {
|
|
||||||
AppConfig.bookshelfLayoutModePortrait = newLayout
|
|
||||||
AppConfig.bookshelfLayoutGridPortrait = selectedColumn
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newLayout == 0 || newLayout == 4) {
|
|
||||||
activityViewModel.booksGridRecycledViewPool.clear()
|
|
||||||
} else {
|
|
||||||
activityViewModel.booksListRecycledViewPool.clear()
|
|
||||||
}
|
|
||||||
recreate = true
|
|
||||||
}
|
|
||||||
|
|
||||||
val newOrder = when (binding.chipGroupOrder.checkedChipId) {
|
|
||||||
binding.chipAsc.id -> 0
|
|
||||||
binding.chipDesc.id -> 1
|
|
||||||
else -> 1
|
|
||||||
}
|
|
||||||
if (AppConfig.bookshelfSortOrder != newOrder) {
|
|
||||||
AppConfig.bookshelfSortOrder = newOrder
|
|
||||||
(requireParentFragment() as? BaseBookshelfFragment)?.upSort()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (recreate) {
|
|
||||||
dismiss()
|
|
||||||
postEvent(EventBus.RECREATE, "")
|
|
||||||
} else if (notifyMain) {
|
|
||||||
dismiss()
|
|
||||||
postEvent(EventBus.NOTIFY_MAIN, false)
|
|
||||||
}
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
|
|
||||||
btnCancel.setOnClickListener {
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private fun showNumberPicker() {
|
|
||||||
NumberPickerDialog(requireContext())
|
|
||||||
.setTitle("书架更新数量限制")
|
|
||||||
.setMinValue(0) // 0 表示无限制
|
|
||||||
.setMaxValue(500) // 可根据需求设定最大值
|
|
||||||
.setValue(AppConfig.bookshelfRefreshingLimit)
|
|
||||||
.show { selected ->
|
|
||||||
AppConfig.bookshelfRefreshingLimit = selected
|
|
||||||
binding.tvValue.text = if (selected <= 0) "无限制" else "$selected 本"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateControlsEnableState() {
|
|
||||||
val checkedLayoutId = binding.chipGroupLayout.checkedChipId
|
|
||||||
val isListMode = checkedLayoutId == R.id.chip_list || checkedLayoutId == R.id.chip_list_compact
|
|
||||||
binding.swShowLastUpdateTime.isEnabled = isListMode
|
|
||||||
|
|
||||||
|
|
||||||
val checkedStyleChip = binding.chipGroupStyle.findViewById<Chip>(binding.chipGroupStyle.checkedChipId)
|
|
||||||
val isGroupStyle0 = binding.chipGroupStyle.indexOfChild(checkedStyleChip) == 0
|
|
||||||
binding.swShowBookshelfTabMenu.isEnabled = isGroupStyle0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,228 @@
|
|||||||
|
package io.legado.app.ui.main.bookshelf
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.Sort
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ViewList
|
||||||
|
import androidx.compose.material.icons.filled.BlurOn
|
||||||
|
import androidx.compose.material.icons.filled.History
|
||||||
|
import androidx.compose.material.icons.filled.Info
|
||||||
|
import androidx.compose.material.icons.filled.Menu
|
||||||
|
import androidx.compose.material.icons.filled.Notifications
|
||||||
|
import androidx.compose.material.icons.filled.SortByAlpha
|
||||||
|
import androidx.compose.material.icons.filled.Style
|
||||||
|
import androidx.compose.material.icons.filled.TextFormat
|
||||||
|
import androidx.compose.material.icons.filled.ViewCompact
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
|
import androidx.compose.ui.res.stringArrayResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.legado.app.R
|
||||||
|
import io.legado.app.ui.config.bookshelfConfig.BookshelfConfig
|
||||||
|
import io.legado.app.ui.widget.components.modalBottomSheet.GlassModalBottomSheet
|
||||||
|
import io.legado.app.ui.widget.components.settingItem.CompactDropdownSettingItem
|
||||||
|
import io.legado.app.ui.widget.components.settingItem.CompactSliderSettingItem
|
||||||
|
import io.legado.app.ui.widget.components.settingItem.SwitchSettingItem
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun BookshelfConfigSheet(
|
||||||
|
onDismissRequest: () -> Unit
|
||||||
|
) {
|
||||||
|
val configuration = LocalConfiguration.current
|
||||||
|
val isLandscape = configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
|
||||||
|
|
||||||
|
GlassModalBottomSheet(onDismissRequest = onDismissRequest) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.padding(bottom = 32.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.bookshelf_layout),
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
modifier = Modifier.padding(vertical = 16.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Group Style
|
||||||
|
CompactDropdownSettingItem(
|
||||||
|
title = stringResource(R.string.group_style),
|
||||||
|
selectedValue = BookshelfConfig.bookGroupStyle.toString(),
|
||||||
|
displayEntries = stringArrayResource(R.array.group_style),
|
||||||
|
entryValues = Array(stringArrayResource(R.array.group_style).size) { it.toString() },
|
||||||
|
imageVector = Icons.Default.Style,
|
||||||
|
onValueChange = { BookshelfConfig.bookGroupStyle = it.toInt() }
|
||||||
|
)
|
||||||
|
|
||||||
|
// Sort
|
||||||
|
CompactDropdownSettingItem(
|
||||||
|
title = stringResource(R.string.sort),
|
||||||
|
selectedValue = BookshelfConfig.bookshelfSort.toString(),
|
||||||
|
displayEntries = stringArrayResource(R.array.bookshelf_px_array),
|
||||||
|
entryValues = Array(stringArrayResource(R.array.bookshelf_px_array).size) { it.toString() },
|
||||||
|
imageVector = Icons.AutoMirrored.Filled.Sort,
|
||||||
|
onValueChange = { BookshelfConfig.bookshelfSort = it.toInt() }
|
||||||
|
)
|
||||||
|
|
||||||
|
// Sort Order
|
||||||
|
CompactDropdownSettingItem(
|
||||||
|
title = "排序方向",
|
||||||
|
selectedValue = BookshelfConfig.bookshelfSortOrder.toString(),
|
||||||
|
displayEntries = arrayOf(
|
||||||
|
stringResource(R.string.ascending_order),
|
||||||
|
stringResource(R.string.descending_order)
|
||||||
|
),
|
||||||
|
entryValues = arrayOf("0", "1"),
|
||||||
|
imageVector = Icons.Default.SortByAlpha,
|
||||||
|
onValueChange = { BookshelfConfig.bookshelfSortOrder = it.toInt() }
|
||||||
|
)
|
||||||
|
|
||||||
|
// Layout Mode
|
||||||
|
val layoutMode =
|
||||||
|
if (isLandscape) BookshelfConfig.bookshelfLayoutModeLandscape else BookshelfConfig.bookshelfLayoutModePortrait
|
||||||
|
CompactDropdownSettingItem(
|
||||||
|
title = "布局模式",
|
||||||
|
description = stringResource(if (isLandscape) R.string.screen_landscape else R.string.screen_portrait),
|
||||||
|
selectedValue = layoutMode.toString(),
|
||||||
|
displayEntries = arrayOf("列表", "网格"),
|
||||||
|
entryValues = arrayOf("0", "1"),
|
||||||
|
imageVector = Icons.AutoMirrored.Filled.ViewList,
|
||||||
|
onValueChange = {
|
||||||
|
if (isLandscape) BookshelfConfig.bookshelfLayoutModeLandscape = it.toInt()
|
||||||
|
else BookshelfConfig.bookshelfLayoutModePortrait = it.toInt()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
SwitchSettingItem(
|
||||||
|
title = "紧凑模式",
|
||||||
|
checked = BookshelfConfig.bookshelfLayoutCompact,
|
||||||
|
imageVector = Icons.Default.ViewCompact,
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
onCheckedChange = { BookshelfConfig.bookshelfLayoutCompact = it }
|
||||||
|
)
|
||||||
|
|
||||||
|
// Grid Count
|
||||||
|
val gridCount =
|
||||||
|
if (isLandscape) BookshelfConfig.bookshelfLayoutGridLandscape else BookshelfConfig.bookshelfLayoutGridPortrait
|
||||||
|
CompactSliderSettingItem(
|
||||||
|
title = stringResource(R.string.number_rows_columns),
|
||||||
|
value = gridCount.toFloat(),
|
||||||
|
valueRange = 1f..15f,
|
||||||
|
steps = 14,
|
||||||
|
onValueChange = {
|
||||||
|
if (isLandscape) BookshelfConfig.bookshelfLayoutGridLandscape = it.toInt()
|
||||||
|
else BookshelfConfig.bookshelfLayoutGridPortrait = it.toInt()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
SwitchSettingItem(
|
||||||
|
title = "标题小字体",
|
||||||
|
checked = BookshelfConfig.bookshelfTitleSmallFont,
|
||||||
|
imageVector = Icons.Default.TextFormat,
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
onCheckedChange = { BookshelfConfig.bookshelfTitleSmallFont = it }
|
||||||
|
)
|
||||||
|
|
||||||
|
SwitchSettingItem(
|
||||||
|
title = "标题居中",
|
||||||
|
checked = BookshelfConfig.bookshelfTitleCenter,
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
onCheckedChange = { BookshelfConfig.bookshelfTitleCenter = it }
|
||||||
|
)
|
||||||
|
|
||||||
|
CompactSliderSettingItem(
|
||||||
|
title = "标题最大行数",
|
||||||
|
value = BookshelfConfig.bookshelfTitleMaxLines.toFloat(),
|
||||||
|
valueRange = 1f..5f,
|
||||||
|
steps = 4,
|
||||||
|
onValueChange = { BookshelfConfig.bookshelfTitleMaxLines = it.toInt() }
|
||||||
|
)
|
||||||
|
|
||||||
|
SwitchSettingItem(
|
||||||
|
title = "封面阴影",
|
||||||
|
checked = BookshelfConfig.bookshelfCoverShadow,
|
||||||
|
imageVector = Icons.Default.BlurOn,
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
onCheckedChange = { BookshelfConfig.bookshelfCoverShadow = it }
|
||||||
|
)
|
||||||
|
|
||||||
|
// Switches
|
||||||
|
SwitchSettingItem(
|
||||||
|
title = stringResource(R.string.show_unread),
|
||||||
|
checked = BookshelfConfig.showUnread,
|
||||||
|
imageVector = Icons.Default.Notifications,
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
onCheckedChange = { BookshelfConfig.showUnread = it }
|
||||||
|
)
|
||||||
|
|
||||||
|
SwitchSettingItem(
|
||||||
|
title = stringResource(R.string.show_unread_new),
|
||||||
|
checked = BookshelfConfig.showUnreadNew,
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
onCheckedChange = { BookshelfConfig.showUnreadNew = it }
|
||||||
|
)
|
||||||
|
|
||||||
|
SwitchSettingItem(
|
||||||
|
title = stringResource(R.string.show_tip),
|
||||||
|
checked = BookshelfConfig.showTip,
|
||||||
|
imageVector = Icons.Default.Info,
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
onCheckedChange = { BookshelfConfig.showTip = it }
|
||||||
|
)
|
||||||
|
|
||||||
|
SwitchSettingItem(
|
||||||
|
title = stringResource(R.string.show_last_update_time),
|
||||||
|
checked = BookshelfConfig.showLastUpdateTime,
|
||||||
|
imageVector = Icons.Default.History,
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
onCheckedChange = { BookshelfConfig.showLastUpdateTime = it }
|
||||||
|
)
|
||||||
|
|
||||||
|
SwitchSettingItem(
|
||||||
|
title = stringResource(R.string.show_wait_up_count),
|
||||||
|
checked = BookshelfConfig.showWaitUpCount,
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
onCheckedChange = { BookshelfConfig.showWaitUpCount = it }
|
||||||
|
)
|
||||||
|
|
||||||
|
SwitchSettingItem(
|
||||||
|
title = stringResource(R.string.show_bookshelf_fast_scroller),
|
||||||
|
checked = BookshelfConfig.showBookshelfFastScroller,
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
onCheckedChange = { BookshelfConfig.showBookshelfFastScroller = it }
|
||||||
|
)
|
||||||
|
|
||||||
|
SwitchSettingItem(
|
||||||
|
title = stringResource(R.string.show_bookshelf_tab_menu),
|
||||||
|
checked = BookshelfConfig.shouldShowExpandButton,
|
||||||
|
imageVector = Icons.Default.Menu,
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
onCheckedChange = { BookshelfConfig.shouldShowExpandButton = it }
|
||||||
|
)
|
||||||
|
|
||||||
|
// Refresh Limit
|
||||||
|
CompactSliderSettingItem(
|
||||||
|
title = stringResource(R.string.bookshelf_update_limit),
|
||||||
|
description = if (BookshelfConfig.bookshelfRefreshingLimit <= 0) "无限制" else "${BookshelfConfig.bookshelfRefreshingLimit} 本",
|
||||||
|
value = BookshelfConfig.bookshelfRefreshingLimit.toFloat(),
|
||||||
|
valueRange = 0f..100f,
|
||||||
|
steps = 100,
|
||||||
|
onValueChange = { BookshelfConfig.bookshelfRefreshingLimit = it.toInt() }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,534 @@
|
|||||||
|
package io.legado.app.ui.main.bookshelf
|
||||||
|
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Download
|
||||||
|
import androidx.compose.material.icons.filled.Edit
|
||||||
|
import androidx.compose.material.icons.filled.FileOpen
|
||||||
|
import androidx.compose.material.icons.filled.GridView
|
||||||
|
import androidx.compose.material.icons.filled.History
|
||||||
|
import androidx.compose.material.icons.filled.ImportExport
|
||||||
|
import androidx.compose.material.icons.filled.Link
|
||||||
|
import androidx.compose.material.icons.filled.Refresh
|
||||||
|
import androidx.compose.material.icons.filled.Save
|
||||||
|
import androidx.compose.material.icons.filled.Settings
|
||||||
|
import androidx.compose.material.icons.filled.Wifi
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.SecondaryScrollableTabRow
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Tab
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||||
|
import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults
|
||||||
|
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.runtime.snapshotFlow
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import io.legado.app.R
|
||||||
|
import io.legado.app.data.entities.Book
|
||||||
|
import io.legado.app.help.book.isLocal
|
||||||
|
import io.legado.app.ui.book.cache.CacheActivity
|
||||||
|
import io.legado.app.ui.book.import.local.ImportBookActivity
|
||||||
|
import io.legado.app.ui.book.import.remote.RemoteBookActivity
|
||||||
|
import io.legado.app.ui.book.manage.BookshelfManageActivity
|
||||||
|
import io.legado.app.ui.book.search.SearchActivity
|
||||||
|
import io.legado.app.ui.config.bookshelfConfig.BookshelfConfig
|
||||||
|
import io.legado.app.ui.file.HandleFileContract
|
||||||
|
import io.legado.app.ui.widget.components.cover.BookCoverWithProgress
|
||||||
|
import io.legado.app.ui.widget.components.filePicker.FilePickerSheet
|
||||||
|
import io.legado.app.ui.widget.components.importComponents.SourceInputDialog
|
||||||
|
import io.legado.app.ui.widget.components.list.ListScaffold
|
||||||
|
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
|
||||||
|
import io.legado.app.utils.readText
|
||||||
|
import io.legado.app.utils.startActivity
|
||||||
|
import io.legado.app.utils.toTimeAgo
|
||||||
|
import io.legado.app.utils.toastOnUi
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.koin.androidx.compose.koinViewModel
|
||||||
|
|
||||||
|
@OptIn(
|
||||||
|
ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class,
|
||||||
|
ExperimentalMaterial3ExpressiveApi::class
|
||||||
|
)
|
||||||
|
@Composable
|
||||||
|
fun BookshelfScreen(
|
||||||
|
viewModel: BookshelfViewModel = koinViewModel(),
|
||||||
|
onBookClick: (Book) -> Unit,
|
||||||
|
onBookLongClick: (Book) -> Unit
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val uiState by viewModel.uiState.collectAsState()
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
var showAddUrlDialog by remember { mutableStateOf(false) }
|
||||||
|
var showImportSheet by remember { mutableStateOf(false) }
|
||||||
|
var showConfigSheet by remember { mutableStateOf(false) }
|
||||||
|
var showGroupManageSheet by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val importLauncher = rememberLauncherForActivityResult(HandleFileContract()) {
|
||||||
|
runCatching {
|
||||||
|
it.uri?.readText(context)?.let { text ->
|
||||||
|
val groupId = uiState.groups.getOrNull(uiState.selectedGroupIndex)?.groupId ?: -1L
|
||||||
|
viewModel.importBookshelf(text, groupId)
|
||||||
|
}
|
||||||
|
}.onFailure {
|
||||||
|
context.toastOnUi(it.localizedMessage ?: "ERROR")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val pagerState = rememberPagerState(
|
||||||
|
initialPage = uiState.selectedGroupIndex,
|
||||||
|
pageCount = { uiState.groups.size }
|
||||||
|
)
|
||||||
|
|
||||||
|
LaunchedEffect(pagerState) {
|
||||||
|
snapshotFlow { pagerState.currentPage }.collect { page ->
|
||||||
|
if (uiState.groups.isNotEmpty() && page < uiState.groups.size) {
|
||||||
|
viewModel.changeGroup(uiState.groups[page].groupId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(uiState.selectedGroupIndex) {
|
||||||
|
if (uiState.selectedGroupIndex != pagerState.currentPage && uiState.selectedGroupIndex < pagerState.pageCount) {
|
||||||
|
pagerState.scrollToPage(uiState.selectedGroupIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val bookGroupStyle = BookshelfConfig.bookGroupStyle
|
||||||
|
// 控制是否处于“文件夹列表”根视图,还是“文件夹内部”书籍视图
|
||||||
|
var isInFolderRoot by remember(bookGroupStyle) { mutableStateOf(bookGroupStyle == 2) }
|
||||||
|
val title = if (bookGroupStyle == 2 && uiState.groups.isNotEmpty()) {
|
||||||
|
if (isInFolderRoot) "书架"
|
||||||
|
else uiState.groups.getOrNull(pagerState.currentPage)?.groupName ?: "书架"
|
||||||
|
} else {
|
||||||
|
"书架"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bookGroupStyle == 2 && !isInFolderRoot) {
|
||||||
|
BackHandler {
|
||||||
|
isInFolderRoot = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ListScaffold(
|
||||||
|
title = title,
|
||||||
|
state = uiState,
|
||||||
|
onSearchToggle = { context.startActivity<SearchActivity>() },
|
||||||
|
onSearchQueryChange = { viewModel.setSearchKey(it) },
|
||||||
|
topBarActions = { },
|
||||||
|
dropDownMenuContent = { dismiss ->
|
||||||
|
RoundDropdownMenuItem(
|
||||||
|
text = { Text(stringResource(R.string.add_remote_book)) },
|
||||||
|
onClick = { context.startActivity<RemoteBookActivity>(); dismiss() },
|
||||||
|
leadingIcon = { Icon(Icons.Default.Wifi, null) }
|
||||||
|
)
|
||||||
|
RoundDropdownMenuItem(
|
||||||
|
text = { Text(stringResource(R.string.book_local)) },
|
||||||
|
onClick = { context.startActivity<ImportBookActivity>(); dismiss() },
|
||||||
|
leadingIcon = { Icon(Icons.Default.Save, null) }
|
||||||
|
)
|
||||||
|
RoundDropdownMenuItem(
|
||||||
|
text = { Text(stringResource(R.string.update_toc)) },
|
||||||
|
onClick = { viewModel.upToc(uiState.items); dismiss() },
|
||||||
|
leadingIcon = { Icon(Icons.Default.Refresh, null) }
|
||||||
|
)
|
||||||
|
RoundDropdownMenuItem(
|
||||||
|
text = { Text("布局设置") },
|
||||||
|
onClick = { showConfigSheet = true; dismiss() },
|
||||||
|
leadingIcon = { Icon(Icons.Default.GridView, null) }
|
||||||
|
)
|
||||||
|
RoundDropdownMenuItem(
|
||||||
|
text = { Text(stringResource(R.string.group_manage)) },
|
||||||
|
onClick = { showGroupManageSheet = true; dismiss() },
|
||||||
|
leadingIcon = { Icon(Icons.Default.Edit, null) }
|
||||||
|
)
|
||||||
|
RoundDropdownMenuItem(
|
||||||
|
text = { Text(stringResource(R.string.add_url)) },
|
||||||
|
onClick = { showAddUrlDialog = true; dismiss() },
|
||||||
|
leadingIcon = { Icon(Icons.Default.Link, null) }
|
||||||
|
)
|
||||||
|
RoundDropdownMenuItem(
|
||||||
|
text = { Text(stringResource(R.string.bookshelf_management)) },
|
||||||
|
onClick = {
|
||||||
|
val groupId =
|
||||||
|
uiState.groups.getOrNull(uiState.selectedGroupIndex)?.groupId ?: -1L
|
||||||
|
context.startActivity<BookshelfManageActivity> {
|
||||||
|
putExtra("groupId", groupId)
|
||||||
|
}
|
||||||
|
dismiss()
|
||||||
|
},
|
||||||
|
leadingIcon = { Icon(Icons.Default.Settings, null) }
|
||||||
|
)
|
||||||
|
RoundDropdownMenuItem(
|
||||||
|
text = { Text(stringResource(R.string.cache_export)) },
|
||||||
|
onClick = {
|
||||||
|
val groupId =
|
||||||
|
uiState.groups.getOrNull(uiState.selectedGroupIndex)?.groupId ?: -1L
|
||||||
|
context.startActivity<CacheActivity> {
|
||||||
|
putExtra("groupId", groupId)
|
||||||
|
}
|
||||||
|
dismiss()
|
||||||
|
},
|
||||||
|
leadingIcon = { Icon(Icons.Default.Download, null) }
|
||||||
|
)
|
||||||
|
RoundDropdownMenuItem(
|
||||||
|
text = { Text(stringResource(R.string.export_bookshelf)) },
|
||||||
|
onClick = {
|
||||||
|
dismiss()
|
||||||
|
},
|
||||||
|
leadingIcon = { Icon(Icons.Default.ImportExport, null) }
|
||||||
|
)
|
||||||
|
RoundDropdownMenuItem(
|
||||||
|
text = { Text(stringResource(R.string.import_bookshelf)) },
|
||||||
|
onClick = { showImportSheet = true; dismiss() },
|
||||||
|
leadingIcon = { Icon(Icons.Default.FileOpen, null) }
|
||||||
|
)
|
||||||
|
RoundDropdownMenuItem(
|
||||||
|
text = { Text(stringResource(R.string.log)) },
|
||||||
|
onClick = {
|
||||||
|
dismiss()
|
||||||
|
},
|
||||||
|
leadingIcon = { Icon(Icons.Default.History, null) }
|
||||||
|
)
|
||||||
|
},
|
||||||
|
bottomContent = if (bookGroupStyle == 0) {
|
||||||
|
{
|
||||||
|
if (uiState.groups.isNotEmpty()) {
|
||||||
|
SecondaryScrollableTabRow(
|
||||||
|
selectedTabIndex = pagerState.currentPage,
|
||||||
|
edgePadding = 16.dp,
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
divider = {}
|
||||||
|
) {
|
||||||
|
uiState.groups.forEachIndexed { index, group ->
|
||||||
|
Tab(
|
||||||
|
selected = pagerState.currentPage == index,
|
||||||
|
onClick = {
|
||||||
|
scope.launch { pagerState.animateScrollToPage(index) }
|
||||||
|
},
|
||||||
|
text = { Text(group.groupName) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else null
|
||||||
|
) { paddingValues ->
|
||||||
|
var isRefreshing by remember { mutableStateOf(false) }
|
||||||
|
val pullToRefreshState = rememberPullToRefreshState()
|
||||||
|
|
||||||
|
PullToRefreshBox(
|
||||||
|
isRefreshing = isRefreshing,
|
||||||
|
onRefresh = {
|
||||||
|
scope.launch {
|
||||||
|
isRefreshing = true
|
||||||
|
viewModel.upAllBookToc()
|
||||||
|
delay(1000)
|
||||||
|
isRefreshing = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
state = pullToRefreshState,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
indicator = {
|
||||||
|
PullToRefreshDefaults.LoadingIndicator(
|
||||||
|
state = pullToRefreshState,
|
||||||
|
isRefreshing = isRefreshing,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.TopCenter)
|
||||||
|
.padding(top = paddingValues.calculateTopPadding())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
if (bookGroupStyle == 2 && isInFolderRoot) {
|
||||||
|
|
||||||
|
if (uiState.bookshelfLayoutMode == 0) {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentPadding = PaddingValues(
|
||||||
|
top = paddingValues.calculateTopPadding(),
|
||||||
|
bottom = 56.dp
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
items(uiState.groups) { group ->
|
||||||
|
BookGroupItemList(
|
||||||
|
group = group,
|
||||||
|
previewBooks = uiState.groupPreviews[group.groupId] ?: emptyList(),
|
||||||
|
isCompact = uiState.bookshelfLayoutCompact,
|
||||||
|
titleSmallFont = uiState.titleSmallFont,
|
||||||
|
titleCenter = uiState.titleCenter,
|
||||||
|
titleMaxLines = uiState.titleMaxLines,
|
||||||
|
onClick = {
|
||||||
|
val index = uiState.groups.indexOf(group)
|
||||||
|
if (index != -1) {
|
||||||
|
scope.launch { pagerState.scrollToPage(index) }
|
||||||
|
isInFolderRoot = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLongClick = { showGroupManageSheet = true }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LazyVerticalGrid(
|
||||||
|
columns = GridCells.Fixed(uiState.bookshelfLayoutGrid),
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentPadding = PaddingValues(
|
||||||
|
top = paddingValues.calculateTopPadding(),
|
||||||
|
bottom = paddingValues.calculateBottomPadding() + 16.dp,
|
||||||
|
start = 12.dp,
|
||||||
|
end = 12.dp
|
||||||
|
),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
items(uiState.groups) { group ->
|
||||||
|
|
||||||
|
BookGroupItemGrid(
|
||||||
|
group = group,
|
||||||
|
previewBooks = uiState.groupPreviews[group.groupId] ?: emptyList(),
|
||||||
|
isCompact = uiState.bookshelfLayoutCompact,
|
||||||
|
titleSmallFont = uiState.titleSmallFont,
|
||||||
|
titleCenter = uiState.titleCenter,
|
||||||
|
titleMaxLines = uiState.titleMaxLines,
|
||||||
|
coverShadow = uiState.coverShadow,
|
||||||
|
onClick = {
|
||||||
|
val index = uiState.groups.indexOf(group)
|
||||||
|
if (index != -1) {
|
||||||
|
scope.launch { pagerState.scrollToPage(index) }
|
||||||
|
isInFolderRoot = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLongClick = { showGroupManageSheet = true }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
HorizontalPager(
|
||||||
|
state = pagerState,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize(),
|
||||||
|
beyondViewportPageCount = 1
|
||||||
|
) {
|
||||||
|
BookshelfPage(
|
||||||
|
paddingValues = paddingValues,
|
||||||
|
uiState = uiState,
|
||||||
|
onBookClick = onBookClick,
|
||||||
|
onBookLongClick = onBookLongClick
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showConfigSheet) {
|
||||||
|
BookshelfConfigSheet(onDismissRequest = { showConfigSheet = false })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showGroupManageSheet) {
|
||||||
|
GroupManageSheet(onDismissRequest = { showGroupManageSheet = false })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showAddUrlDialog) {
|
||||||
|
SourceInputDialog(
|
||||||
|
title = stringResource(R.string.add_book_url),
|
||||||
|
onDismissRequest = { showAddUrlDialog = false },
|
||||||
|
onConfirm = { url ->
|
||||||
|
viewModel.addBookByUrl(url)
|
||||||
|
showAddUrlDialog = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showImportSheet) {
|
||||||
|
FilePickerSheet(
|
||||||
|
onDismissRequest = { showImportSheet = false },
|
||||||
|
title = stringResource(R.string.import_bookshelf),
|
||||||
|
onSelectSysFile = { types ->
|
||||||
|
importLauncher.launch {
|
||||||
|
mode = HandleFileContract.FILE
|
||||||
|
allowExtensions = types
|
||||||
|
}
|
||||||
|
showImportSheet = false
|
||||||
|
},
|
||||||
|
onManualInput = {
|
||||||
|
showAddUrlDialog = true
|
||||||
|
showImportSheet = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uiState.isLoading) {
|
||||||
|
Dialog(onDismissRequest = {}) {
|
||||||
|
Surface(
|
||||||
|
shape = MaterialTheme.shapes.medium,
|
||||||
|
color = MaterialTheme.colorScheme.surfaceContainerHigh
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(24.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator()
|
||||||
|
uiState.loadingText?.let {
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
modifier = Modifier.padding(top = 16.dp),
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BookshelfPage(
|
||||||
|
paddingValues: PaddingValues,
|
||||||
|
uiState: BookshelfUiState,
|
||||||
|
onBookClick: (Book) -> Unit,
|
||||||
|
onBookLongClick: (Book) -> Unit
|
||||||
|
) {
|
||||||
|
if (uiState.bookshelfLayoutMode == 0) {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentPadding = PaddingValues(
|
||||||
|
top = paddingValues.calculateTopPadding(),
|
||||||
|
bottom = 56.dp
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
items(uiState.items, key = { it.bookUrl }) { book ->
|
||||||
|
BookItem(
|
||||||
|
book = book,
|
||||||
|
layoutMode = uiState.bookshelfLayoutMode,
|
||||||
|
isCompact = uiState.bookshelfLayoutCompact,
|
||||||
|
isUpdating = uiState.updatingBooks.contains(book.bookUrl),
|
||||||
|
onClick = { onBookClick(book) },
|
||||||
|
onLongClick = { onBookLongClick(book) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LazyVerticalGrid(
|
||||||
|
columns = GridCells.Fixed(uiState.bookshelfLayoutGrid),
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentPadding = PaddingValues(
|
||||||
|
top = paddingValues.calculateTopPadding(),
|
||||||
|
bottom = paddingValues.calculateBottomPadding() + 16.dp,
|
||||||
|
start = 12.dp,
|
||||||
|
end = 12.dp
|
||||||
|
),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
items(uiState.items, key = { it.bookUrl }) { book ->
|
||||||
|
BookItem(
|
||||||
|
book = book,
|
||||||
|
layoutMode = uiState.bookshelfLayoutMode,
|
||||||
|
isCompact = uiState.bookshelfLayoutCompact,
|
||||||
|
isUpdating = uiState.updatingBooks.contains(book.bookUrl),
|
||||||
|
titleSmallFont = uiState.titleSmallFont,
|
||||||
|
titleCenter = uiState.titleCenter,
|
||||||
|
titleMaxLines = uiState.titleMaxLines,
|
||||||
|
coverShadow = uiState.coverShadow,
|
||||||
|
onClick = { onBookClick(book) },
|
||||||
|
onLongClick = { onBookLongClick(book) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BookItem(
|
||||||
|
book: Book,
|
||||||
|
layoutMode: Int,
|
||||||
|
isCompact: Boolean = false,
|
||||||
|
isUpdating: Boolean = false,
|
||||||
|
titleSmallFont: Boolean = false,
|
||||||
|
titleCenter: Boolean = true,
|
||||||
|
titleMaxLines: Int = 2,
|
||||||
|
coverShadow: Boolean = false,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
onLongClick: () -> Unit
|
||||||
|
) {
|
||||||
|
val unreadCount = (book.totalChapterNum - book.durChapterIndex).coerceAtLeast(0)
|
||||||
|
|
||||||
|
BookshelfItem(
|
||||||
|
isGrid = layoutMode != 0,
|
||||||
|
isCompact = isCompact,
|
||||||
|
cover = { modifier ->
|
||||||
|
BookCoverWithProgress(
|
||||||
|
name = book.name,
|
||||||
|
author = book.author,
|
||||||
|
path = book.getDisplayCover(),
|
||||||
|
isUpdating = isUpdating,
|
||||||
|
modifier = modifier
|
||||||
|
)
|
||||||
|
},
|
||||||
|
title = book.name,
|
||||||
|
subTitle = if (isCompact && layoutMode == 0) {
|
||||||
|
stringResource(R.string.author_read, book.author, unreadCount)
|
||||||
|
} else {
|
||||||
|
book.author
|
||||||
|
},
|
||||||
|
desc = stringResource(R.string.read_dur_progress, book.durChapterTitle ?: ""),
|
||||||
|
extra = {
|
||||||
|
if (BookshelfConfig.showLastUpdateTime && !book.isLocal) {
|
||||||
|
Text(
|
||||||
|
text = book.latestChapterTime.toTimeAgo(),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = if (!isCompact) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.secondary,
|
||||||
|
modifier = Modifier.padding(end = 4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = book.latestChapterTitle ?: "",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
titleSmallFont = titleSmallFont,
|
||||||
|
titleCenter = titleCenter,
|
||||||
|
titleMaxLines = titleMaxLines,
|
||||||
|
coverShadow = coverShadow,
|
||||||
|
onClick = onClick,
|
||||||
|
onLongClick = onLongClick
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package io.legado.app.ui.main.bookshelf
|
||||||
|
|
||||||
|
import io.legado.app.data.entities.Book
|
||||||
|
import io.legado.app.data.entities.BookGroup
|
||||||
|
import io.legado.app.ui.config.bookshelfConfig.BookshelfConfig
|
||||||
|
import io.legado.app.ui.widget.components.list.ListUiState
|
||||||
|
|
||||||
|
data class BookshelfUiState(
|
||||||
|
override val items: List<Book> = emptyList(),
|
||||||
|
override val selectedIds: Set<Any> = emptySet(),
|
||||||
|
override val searchKey: String = "",
|
||||||
|
override val isSearch: Boolean = false,
|
||||||
|
override val isLoading: Boolean = false,
|
||||||
|
val groups: List<BookGroup> = emptyList(),
|
||||||
|
val groupPreviews: Map<Long, List<Book>> = emptyMap(),
|
||||||
|
val selectedGroupIndex: Int = 0,
|
||||||
|
val loadingText: String? = null,
|
||||||
|
val bookshelfLayoutMode: Int = 1,
|
||||||
|
val bookshelfLayoutGrid: Int = 3,
|
||||||
|
val bookshelfLayoutCompact: Boolean = BookshelfConfig.bookshelfLayoutCompact,
|
||||||
|
val titleSmallFont: Boolean = BookshelfConfig.bookshelfTitleSmallFont,
|
||||||
|
val titleCenter: Boolean = BookshelfConfig.bookshelfTitleCenter,
|
||||||
|
val titleMaxLines: Int = BookshelfConfig.bookshelfTitleMaxLines,
|
||||||
|
val coverShadow: Boolean = BookshelfConfig.bookshelfCoverShadow,
|
||||||
|
val updatingBooks: Set<String> = emptySet()
|
||||||
|
) : ListUiState<Book>
|
||||||
@@ -1,42 +1,416 @@
|
|||||||
package io.legado.app.ui.main.bookshelf
|
package io.legado.app.ui.main.bookshelf
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import androidx.lifecycle.MutableLiveData
|
import android.content.res.Configuration
|
||||||
|
import androidx.lifecycle.asFlow
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import com.google.gson.stream.JsonWriter
|
import com.google.gson.stream.JsonWriter
|
||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
import io.legado.app.base.BaseViewModel
|
import io.legado.app.base.BaseViewModel
|
||||||
|
import io.legado.app.constant.AppConst
|
||||||
import io.legado.app.constant.AppLog
|
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.appDb
|
||||||
import io.legado.app.data.entities.Book
|
import io.legado.app.data.entities.Book
|
||||||
|
import io.legado.app.data.entities.BookGroup
|
||||||
|
import io.legado.app.data.entities.BookSource
|
||||||
import io.legado.app.data.entities.BookSourcePart
|
import io.legado.app.data.entities.BookSourcePart
|
||||||
import io.legado.app.exception.NoStackTraceException
|
import io.legado.app.exception.NoStackTraceException
|
||||||
|
import io.legado.app.help.book.BookHelp
|
||||||
|
import io.legado.app.help.book.addType
|
||||||
|
import io.legado.app.help.book.isLocal
|
||||||
|
import io.legado.app.help.book.isUpError
|
||||||
|
import io.legado.app.help.book.removeType
|
||||||
|
import io.legado.app.help.book.sync
|
||||||
import io.legado.app.help.config.AppConfig
|
import io.legado.app.help.config.AppConfig
|
||||||
import io.legado.app.help.coroutine.Coroutine
|
import io.legado.app.help.coroutine.Coroutine
|
||||||
import io.legado.app.help.http.decompressed
|
import io.legado.app.help.http.decompressed
|
||||||
import io.legado.app.help.http.newCallResponseBody
|
import io.legado.app.help.http.newCallResponseBody
|
||||||
import io.legado.app.help.http.okHttpClient
|
import io.legado.app.help.http.okHttpClient
|
||||||
import io.legado.app.help.http.text
|
import io.legado.app.help.http.text
|
||||||
|
import io.legado.app.model.CacheBook
|
||||||
|
import io.legado.app.model.ReadBook
|
||||||
|
import io.legado.app.model.SourceCallBack
|
||||||
import io.legado.app.model.webBook.WebBook
|
import io.legado.app.model.webBook.WebBook
|
||||||
|
import io.legado.app.service.CacheBookService
|
||||||
|
import io.legado.app.ui.config.bookshelfConfig.BookshelfConfig
|
||||||
import io.legado.app.utils.FileUtils
|
import io.legado.app.utils.FileUtils
|
||||||
import io.legado.app.utils.GSON
|
import io.legado.app.utils.GSON
|
||||||
import io.legado.app.utils.NetworkUtils
|
import io.legado.app.utils.NetworkUtils
|
||||||
|
import io.legado.app.utils.cnCompare
|
||||||
|
import io.legado.app.utils.eventBus.FlowEventBus
|
||||||
import io.legado.app.utils.fromJsonArray
|
import io.legado.app.utils.fromJsonArray
|
||||||
import io.legado.app.utils.isAbsUrl
|
import io.legado.app.utils.isAbsUrl
|
||||||
import io.legado.app.utils.isJsonArray
|
import io.legado.app.utils.isJsonArray
|
||||||
|
import io.legado.app.utils.onEachParallel
|
||||||
|
import io.legado.app.utils.postEvent
|
||||||
import io.legado.app.utils.printOnDebug
|
import io.legado.app.utils.printOnDebug
|
||||||
import io.legado.app.utils.toastOnUi
|
import io.legado.app.utils.toastOnUi
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.asCoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.currentCoroutineContext
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.ensureActive
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.catch
|
||||||
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.onCompletion
|
||||||
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.sync.Semaphore
|
import kotlinx.coroutines.sync.Semaphore
|
||||||
import kotlinx.coroutines.sync.withPermit
|
import kotlinx.coroutines.sync.withPermit
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
import java.io.OutputStreamWriter
|
import java.io.OutputStreamWriter
|
||||||
|
import java.util.LinkedList
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
class BookshelfViewModel(application: Application) : BaseViewModel(application) {
|
class BookshelfViewModel(application: Application) : BaseViewModel(application) {
|
||||||
val addBookProgressLiveData = MutableLiveData(-1)
|
|
||||||
var addBookJob: Coroutine<*>? = null
|
var addBookJob: Coroutine<*>? = null
|
||||||
|
|
||||||
|
private val groupIdFlow = MutableStateFlow(BookGroup.IdAll)
|
||||||
|
private val searchKeyFlow = MutableStateFlow("")
|
||||||
|
private val refreshTrigger = MutableStateFlow(0)
|
||||||
|
private val loadingTextFlow = MutableStateFlow<String?>(null)
|
||||||
|
|
||||||
|
// 更新相关
|
||||||
|
private var threadCount = AppConfig.threadCount
|
||||||
|
private var poolSize = min(threadCount, AppConst.MAX_THREAD)
|
||||||
|
private var upTocPool = Executors.newFixedThreadPool(poolSize).asCoroutineDispatcher()
|
||||||
|
private val waitUpTocBooks = LinkedList<String>()
|
||||||
|
private val onUpTocBooks = ConcurrentHashMap.newKeySet<String>()
|
||||||
|
private val updatingBooksFlow = MutableStateFlow<Set<String>>(emptySet())
|
||||||
|
private var upTocJob: Job? = null
|
||||||
|
private var cacheBookJob: Job? = null
|
||||||
|
private val eventListenerSource = ConcurrentHashMap<BookSource, Boolean>()
|
||||||
|
|
||||||
|
val scrollTrigger = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
|
||||||
|
|
||||||
|
val groupsFlow = appDb.bookGroupDao.show.asFlow()
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
val booksFlow = combine(groupIdFlow, refreshTrigger) { groupId, _ -> groupId }
|
||||||
|
.flatMapLatest { groupId ->
|
||||||
|
appDb.bookDao.flowByGroup(groupId).map { list ->
|
||||||
|
sortBooks(list, groupId)
|
||||||
|
}
|
||||||
|
}.flowOn(Dispatchers.Default)
|
||||||
|
|
||||||
|
private val internalStateFlow = combine(
|
||||||
|
groupIdFlow,
|
||||||
|
searchKeyFlow,
|
||||||
|
loadingTextFlow,
|
||||||
|
updatingBooksFlow
|
||||||
|
) { groupId, searchKey, loadingText, updatingBooks ->
|
||||||
|
InternalState(groupId, searchKey, loadingText, updatingBooks)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class InternalState(
|
||||||
|
val groupId: Long,
|
||||||
|
val searchKey: String,
|
||||||
|
val loadingText: String?,
|
||||||
|
val updatingBooks: Set<String>
|
||||||
|
)
|
||||||
|
|
||||||
|
val uiState: StateFlow<BookshelfUiState> = combine(
|
||||||
|
booksFlow,
|
||||||
|
groupsFlow,
|
||||||
|
internalStateFlow
|
||||||
|
) { books, groups, internal ->
|
||||||
|
val filteredBooks = if (internal.searchKey.isEmpty()) {
|
||||||
|
books
|
||||||
|
} else {
|
||||||
|
books.filter {
|
||||||
|
it.name.contains(
|
||||||
|
internal.searchKey,
|
||||||
|
true
|
||||||
|
) || it.author.contains(internal.searchKey, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val currentGroups = groups ?: emptyList()
|
||||||
|
val isLandscape =
|
||||||
|
context.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
|
||||||
|
|
||||||
|
// 计算分组预览图
|
||||||
|
val previews = mutableMapOf<Long, List<Book>>()
|
||||||
|
if (BookshelfConfig.bookGroupStyle in 2..3) {
|
||||||
|
currentGroups.forEach { group ->
|
||||||
|
val groupBooks = if (group.groupId == BookGroup.IdAll) {
|
||||||
|
books.take(4)
|
||||||
|
} else {
|
||||||
|
books.filter { (it.group and group.groupId) != 0L }.take(4)
|
||||||
|
}
|
||||||
|
previews[group.groupId] = groupBooks
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BookshelfUiState(
|
||||||
|
items = filteredBooks,
|
||||||
|
groups = currentGroups,
|
||||||
|
groupPreviews = previews,
|
||||||
|
selectedGroupIndex = currentGroups.indexOfFirst { it.groupId == internal.groupId }
|
||||||
|
.coerceAtLeast(0),
|
||||||
|
searchKey = internal.searchKey,
|
||||||
|
isSearch = internal.searchKey.isNotEmpty(),
|
||||||
|
isLoading = internal.loadingText != null,
|
||||||
|
loadingText = internal.loadingText,
|
||||||
|
bookshelfLayoutMode = if (isLandscape) BookshelfConfig.bookshelfLayoutModeLandscape else BookshelfConfig.bookshelfLayoutModePortrait,
|
||||||
|
bookshelfLayoutGrid = if (isLandscape) BookshelfConfig.bookshelfLayoutGridLandscape else BookshelfConfig.bookshelfLayoutGridPortrait,
|
||||||
|
bookshelfLayoutCompact = BookshelfConfig.bookshelfLayoutCompact,
|
||||||
|
titleSmallFont = BookshelfConfig.bookshelfTitleSmallFont,
|
||||||
|
titleCenter = BookshelfConfig.bookshelfTitleCenter,
|
||||||
|
titleMaxLines = BookshelfConfig.bookshelfTitleMaxLines,
|
||||||
|
coverShadow = BookshelfConfig.bookshelfCoverShadow,
|
||||||
|
updatingBooks = internal.updatingBooks
|
||||||
|
)
|
||||||
|
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), BookshelfUiState())
|
||||||
|
|
||||||
|
init {
|
||||||
|
viewModelScope.launch {
|
||||||
|
FlowEventBus.with<Unit>(EventBus.UP_ALL_BOOK_TOC).collect {
|
||||||
|
upAllBookToc()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
viewModelScope.launch {
|
||||||
|
FlowEventBus.with<Unit>(EventBus.BOOKSHELF_REFRESH).collect {
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (BookshelfConfig.autoRefreshBook) {
|
||||||
|
upAllBookToc()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
super.onCleared()
|
||||||
|
upTocPool.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun sortBooks(list: List<Book>, groupId: Long): List<Book> {
|
||||||
|
val group = appDb.bookGroupDao.getByID(groupId)
|
||||||
|
val bookSort = group?.getRealBookSort() ?: AppConfig.bookshelfSort
|
||||||
|
val isDescending = AppConfig.bookshelfSortOrder == 1
|
||||||
|
|
||||||
|
return when (bookSort) {
|
||||||
|
1 -> if (isDescending) list.sortedByDescending { it.latestChapterTime }
|
||||||
|
else list.sortedBy { it.latestChapterTime }
|
||||||
|
|
||||||
|
2 -> if (isDescending)
|
||||||
|
list.sortedWith { o1, o2 -> o2.name.cnCompare(o1.name) }
|
||||||
|
else
|
||||||
|
list.sortedWith { o1, o2 -> o1.name.cnCompare(o2.name) }
|
||||||
|
|
||||||
|
3 -> if (isDescending) list.sortedByDescending { it.order }
|
||||||
|
else list.sortedBy { it.order }
|
||||||
|
|
||||||
|
4 -> if (isDescending) list.sortedByDescending {
|
||||||
|
max(
|
||||||
|
it.latestChapterTime,
|
||||||
|
it.durChapterTime
|
||||||
|
)
|
||||||
|
}
|
||||||
|
else list.sortedBy { max(it.latestChapterTime, it.durChapterTime) }
|
||||||
|
|
||||||
|
5 -> if (isDescending)
|
||||||
|
list.sortedWith { o1, o2 -> o2.author.cnCompare(o1.author) }
|
||||||
|
else
|
||||||
|
list.sortedWith { o1, o2 -> o1.author.cnCompare(o2.author) }
|
||||||
|
|
||||||
|
else -> if (isDescending) list.sortedByDescending { it.durChapterTime }
|
||||||
|
else list.sortedBy { it.durChapterTime }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun changeGroup(groupId: Long) {
|
||||||
|
groupIdFlow.value = groupId
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setSearchKey(key: String) {
|
||||||
|
searchKeyFlow.value = key
|
||||||
|
}
|
||||||
|
|
||||||
|
fun refresh() {
|
||||||
|
refreshTrigger.value++
|
||||||
|
}
|
||||||
|
|
||||||
|
fun gotoTop() {
|
||||||
|
scrollTrigger.tryEmit(Unit)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新逻辑移入
|
||||||
|
fun upAllBookToc() {
|
||||||
|
execute {
|
||||||
|
addToWaitUp(appDb.bookDao.hasUpdateBooks)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun upToc(books: List<Book>) {
|
||||||
|
execute(context = upTocPool) {
|
||||||
|
books.filter {
|
||||||
|
!it.isLocal && it.canUpdate
|
||||||
|
}.let {
|
||||||
|
addToWaitUp(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
private fun addToWaitUp(books: List<Book>) {
|
||||||
|
books.forEach { book ->
|
||||||
|
if (!waitUpTocBooks.contains(book.bookUrl) && !onUpTocBooks.contains(book.bookUrl)) {
|
||||||
|
waitUpTocBooks.add(book.bookUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (upTocJob == null) {
|
||||||
|
startUpTocJob()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startUpTocJob() {
|
||||||
|
upPool()
|
||||||
|
postUpBooksCount()
|
||||||
|
upTocJob = viewModelScope.launch(upTocPool) {
|
||||||
|
flow {
|
||||||
|
while (true) {
|
||||||
|
emit(waitUpTocBooks.poll() ?: break)
|
||||||
|
}
|
||||||
|
}.onEachParallel(threadCount) {
|
||||||
|
onUpTocBooks.add(it)
|
||||||
|
updatingBooksFlow.value = onUpTocBooks.toSet()
|
||||||
|
postEvent(EventBus.UP_BOOKSHELF, it)
|
||||||
|
updateToc(it)
|
||||||
|
}.onEach {
|
||||||
|
onUpTocBooks.remove(it)
|
||||||
|
updatingBooksFlow.value = onUpTocBooks.toSet()
|
||||||
|
postEvent(EventBus.UP_BOOKSHELF, it)
|
||||||
|
postUpBooksCount()
|
||||||
|
}.onCompletion {
|
||||||
|
upTocJob = null
|
||||||
|
if (waitUpTocBooks.isNotEmpty()) {
|
||||||
|
startUpTocJob()
|
||||||
|
}
|
||||||
|
if (it == null && cacheBookJob == null && !CacheBookService.isRun) {
|
||||||
|
cacheBook()
|
||||||
|
}
|
||||||
|
}.catch {
|
||||||
|
AppLog.put("更新目录出错\n${it.localizedMessage}", it)
|
||||||
|
}.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun upPool() {
|
||||||
|
threadCount = AppConfig.threadCount
|
||||||
|
val newPoolSize = min(threadCount, AppConst.MAX_THREAD)
|
||||||
|
if (poolSize == newPoolSize) return
|
||||||
|
poolSize = newPoolSize
|
||||||
|
upTocPool.close()
|
||||||
|
upTocPool = Executors.newFixedThreadPool(poolSize).asCoroutineDispatcher()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun updateToc(bookUrl: String) {
|
||||||
|
val book = appDb.bookDao.getBook(bookUrl) ?: return
|
||||||
|
val source = appDb.bookSourceDao.getBookSource(book.origin)
|
||||||
|
if (source == null) {
|
||||||
|
if (!book.isUpError) {
|
||||||
|
book.addType(BookType.updateError)
|
||||||
|
appDb.bookDao.update(book)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (source.eventListener) {
|
||||||
|
if (eventListenerSource.putIfAbsent(source, true) == null) {
|
||||||
|
SourceCallBack.callBackSource(
|
||||||
|
viewModelScope,
|
||||||
|
SourceCallBack.START_SHELF_REFRESH,
|
||||||
|
source
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
kotlin.runCatching {
|
||||||
|
val oldBook = book.copy()
|
||||||
|
if (book.tocUrl.isBlank()) {
|
||||||
|
WebBook.getBookInfoAwait(source, book)
|
||||||
|
} else {
|
||||||
|
WebBook.runPreUpdateJs(source, book)
|
||||||
|
}
|
||||||
|
val toc = WebBook.getChapterListAwait(source, book).getOrThrow()
|
||||||
|
book.sync(oldBook)
|
||||||
|
book.removeType(BookType.updateError)
|
||||||
|
if (book.bookUrl == bookUrl) {
|
||||||
|
appDb.bookDao.update(book)
|
||||||
|
} else {
|
||||||
|
appDb.bookDao.replace(oldBook, book)
|
||||||
|
BookHelp.updateCacheFolder(oldBook, book)
|
||||||
|
}
|
||||||
|
appDb.bookChapterDao.delByBook(bookUrl)
|
||||||
|
appDb.bookChapterDao.insert(*toc.toTypedArray())
|
||||||
|
ReadBook.onChapterListUpdated(book)
|
||||||
|
addDownload(source, book)
|
||||||
|
}.onFailure {
|
||||||
|
currentCoroutineContext().ensureActive()
|
||||||
|
AppLog.put("${book.name} 更新目录失败\n${it.localizedMessage}", it)
|
||||||
|
appDb.bookDao.getBook(book.bookUrl)?.let { book ->
|
||||||
|
book.addType(BookType.updateError)
|
||||||
|
appDb.bookDao.update(book)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun postUpBooksCount() {
|
||||||
|
val count = if (AppConfig.showWaitUpCount) waitUpTocBooks.size + onUpTocBooks.size else 0
|
||||||
|
FlowEventBus.post(EventBus.UP_BOOKSHELF_COUNT, count)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addDownload(source: BookSource, book: Book) {
|
||||||
|
if (AppConfig.preDownloadNum == 0) return
|
||||||
|
val endIndex =
|
||||||
|
min(book.totalChapterNum - 1, book.durChapterIndex + AppConfig.preDownloadNum)
|
||||||
|
val cacheBook = CacheBook.getOrCreate(source, book)
|
||||||
|
cacheBook.addDownload(book.durChapterIndex, endIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cacheBook() {
|
||||||
|
eventListenerSource.toList().forEach {
|
||||||
|
SourceCallBack.callBackSource(
|
||||||
|
viewModelScope,
|
||||||
|
SourceCallBack.END_SHELF_REFRESH,
|
||||||
|
it.first
|
||||||
|
)
|
||||||
|
}
|
||||||
|
eventListenerSource.clear()
|
||||||
|
if (AppConfig.preDownloadNum == 0) return
|
||||||
|
cacheBookJob?.cancel()
|
||||||
|
cacheBookJob = viewModelScope.launch(upTocPool) {
|
||||||
|
launch {
|
||||||
|
while (isActive && CacheBook.isRun) {
|
||||||
|
CacheBook.setWorkingState(waitUpTocBooks.isEmpty() && onUpTocBooks.isEmpty())
|
||||||
|
delay(1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CacheBook.startProcessJob(upTocPool)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun addBookByUrl(bookUrls: String) {
|
fun addBookByUrl(bookUrls: String) {
|
||||||
var successCount = 0
|
var successCount = 0
|
||||||
|
loadingTextFlow.value = "添加中..."
|
||||||
addBookJob = execute {
|
addBookJob = execute {
|
||||||
val hasBookUrlPattern: List<BookSourcePart> by lazy {
|
val hasBookUrlPattern: List<BookSourcePart> by lazy {
|
||||||
appDb.bookSourceDao.hasBookUrlPattern
|
appDb.bookSourceDao.hasBookUrlPattern
|
||||||
@@ -83,7 +457,7 @@ class BookshelfViewModel(application: Application) : BaseViewModel(application)
|
|||||||
it.save()
|
it.save()
|
||||||
}
|
}
|
||||||
successCount++
|
successCount++
|
||||||
addBookProgressLiveData.postValue(successCount)
|
loadingTextFlow.value = "添加中... ($successCount)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.onSuccess {
|
}.onSuccess {
|
||||||
@@ -95,7 +469,7 @@ class BookshelfViewModel(application: Application) : BaseViewModel(application)
|
|||||||
}.onError {
|
}.onError {
|
||||||
AppLog.put("添加网址出错\n${it.localizedMessage}", it, true)
|
AppLog.put("添加网址出错\n${it.localizedMessage}", it, true)
|
||||||
}.onFinally {
|
}.onFinally {
|
||||||
addBookProgressLiveData.postValue(-1)
|
loadingTextFlow.value = null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,6 +528,7 @@ class BookshelfViewModel(application: Application) : BaseViewModel(application)
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun importBookshelfByJson(json: String, groupId: Long) {
|
private fun importBookshelfByJson(json: String, groupId: Long) {
|
||||||
|
loadingTextFlow.value = "导入中..."
|
||||||
execute {
|
execute {
|
||||||
val bookSourceParts = appDb.bookSourceDao.allEnabledPart
|
val bookSourceParts = appDb.bookSourceDao.allEnabledPart
|
||||||
val semaphore = Semaphore(AppConfig.threadCount)
|
val semaphore = Semaphore(AppConfig.threadCount)
|
||||||
@@ -181,6 +556,7 @@ class BookshelfViewModel(application: Application) : BaseViewModel(application)
|
|||||||
}.onError {
|
}.onError {
|
||||||
it.printOnDebug()
|
it.printOnDebug()
|
||||||
}.onFinally {
|
}.onFinally {
|
||||||
|
loadingTextFlow.value = null
|
||||||
context.toastOnUi(R.string.success)
|
context.toastOnUi(R.string.success)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
package io.legado.app.ui.main.bookshelf
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedContent
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.togetherWith
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material.icons.filled.Edit
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.legado.app.R
|
||||||
|
import io.legado.app.data.entities.BookGroup
|
||||||
|
import io.legado.app.ui.book.group.GroupEditContent
|
||||||
|
import io.legado.app.ui.book.group.GroupViewModel
|
||||||
|
import io.legado.app.ui.widget.components.modalBottomSheet.GlassModalBottomSheet
|
||||||
|
import io.legado.app.ui.widget.components.settingItem.SettingItem
|
||||||
|
import org.koin.androidx.compose.koinViewModel
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun GroupManageSheet(
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
viewModel: GroupViewModel = koinViewModel(),
|
||||||
|
bookshelfViewModel: BookshelfViewModel = koinViewModel()
|
||||||
|
) {
|
||||||
|
val groups by bookshelfViewModel.groupsFlow.collectAsState(initial = emptyList())
|
||||||
|
var editingGroup by remember { mutableStateOf<BookGroup?>(null) }
|
||||||
|
var isEditing by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
GlassModalBottomSheet(onDismissRequest = onDismissRequest) {
|
||||||
|
AnimatedContent(
|
||||||
|
targetState = isEditing,
|
||||||
|
transitionSpec = {
|
||||||
|
fadeIn().togetherWith(fadeOut())
|
||||||
|
},
|
||||||
|
label = "GroupManageTransition"
|
||||||
|
) { editing ->
|
||||||
|
if (editing) {
|
||||||
|
GroupEditContent(
|
||||||
|
group = editingGroup,
|
||||||
|
onDismissRequest = { isEditing = false },
|
||||||
|
viewModel = viewModel
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.padding(bottom = 32.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 8.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.group_manage),
|
||||||
|
style = MaterialTheme.typography.titleLarge
|
||||||
|
)
|
||||||
|
IconButton(onClick = {
|
||||||
|
editingGroup = null
|
||||||
|
isEditing = true
|
||||||
|
}) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Add,
|
||||||
|
contentDescription = stringResource(R.string.add)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
items(groups, key = { it.groupId }) { group ->
|
||||||
|
GroupItem(
|
||||||
|
group = group,
|
||||||
|
onEdit = {
|
||||||
|
editingGroup = group
|
||||||
|
isEditing = true
|
||||||
|
},
|
||||||
|
onToggle = { isChecked ->
|
||||||
|
viewModel.upGroup(group.copy(show = isChecked))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun GroupItem(
|
||||||
|
group: BookGroup,
|
||||||
|
onEdit: () -> Unit,
|
||||||
|
onToggle: (Boolean) -> Unit
|
||||||
|
) {
|
||||||
|
SettingItem(
|
||||||
|
title = group.groupName,
|
||||||
|
onClick = onEdit,
|
||||||
|
trailingContent = {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
IconButton(onClick = onEdit) {
|
||||||
|
Icon(Icons.Default.Edit, contentDescription = stringResource(R.string.edit))
|
||||||
|
}
|
||||||
|
Switch(
|
||||||
|
checked = group.show,
|
||||||
|
onCheckedChange = onToggle
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,375 +0,0 @@
|
|||||||
package io.legado.app.ui.main.bookshelf.books
|
|
||||||
|
|
||||||
//import io.legado.app.lib.theme.accentColor
|
|
||||||
//import io.legado.app.lib.theme.primaryColor
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewConfiguration
|
|
||||||
import androidx.core.app.ActivityOptionsCompat
|
|
||||||
import androidx.core.view.ViewCompat
|
|
||||||
import androidx.core.view.WindowInsetsCompat
|
|
||||||
import androidx.core.view.isGone
|
|
||||||
import androidx.fragment.app.activityViewModels
|
|
||||||
import androidx.lifecycle.Lifecycle
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import androidx.recyclerview.widget.RecyclerView.Adapter.StateRestorationPolicy
|
|
||||||
import io.legado.app.R
|
|
||||||
import io.legado.app.base.BaseFragment
|
|
||||||
import io.legado.app.constant.AppLog
|
|
||||||
import io.legado.app.constant.EventBus
|
|
||||||
import io.legado.app.data.AppDatabase
|
|
||||||
import io.legado.app.data.appDb
|
|
||||||
import io.legado.app.data.entities.Book
|
|
||||||
import io.legado.app.data.entities.BookGroup
|
|
||||||
import io.legado.app.databinding.FragmentBooksBinding
|
|
||||||
import io.legado.app.help.book.isAudio
|
|
||||||
import io.legado.app.help.book.isImage
|
|
||||||
import io.legado.app.help.config.AppConfig
|
|
||||||
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.ui.main.bookshelf.books.styleDefalut.BaseBooksAdapter
|
|
||||||
import io.legado.app.ui.main.bookshelf.books.styleDefalut.BooksAdapterGrid
|
|
||||||
import io.legado.app.ui.main.bookshelf.books.styleDefalut.BooksAdapterGridCompact
|
|
||||||
import io.legado.app.ui.main.bookshelf.books.styleDefalut.BooksAdapterGridCover
|
|
||||||
import io.legado.app.ui.main.bookshelf.books.styleDefalut.BooksAdapterList
|
|
||||||
import io.legado.app.ui.main.bookshelf.books.styleDefalut.BooksAdapterListCompact
|
|
||||||
import io.legado.app.utils.bookshelfLayoutGrid
|
|
||||||
import io.legado.app.utils.bookshelfLayoutMode
|
|
||||||
import io.legado.app.utils.cnCompare
|
|
||||||
import io.legado.app.utils.flowWithLifecycleAndDatabaseChangeFirst
|
|
||||||
import io.legado.app.utils.observeEvent
|
|
||||||
import io.legado.app.utils.startActivity
|
|
||||||
import io.legado.app.utils.startActivityForBook
|
|
||||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.flow.catch
|
|
||||||
import kotlinx.coroutines.flow.conflate
|
|
||||||
import kotlinx.coroutines.flow.flowOn
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.coroutines.isActive
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlin.math.max
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 书架界面
|
|
||||||
*/
|
|
||||||
class BooksFragment() : BaseFragment(R.layout.fragment_books),
|
|
||||||
BaseBooksAdapter.CallBack {
|
|
||||||
|
|
||||||
constructor(position: Int, group: BookGroup) : this() {
|
|
||||||
val bundle = Bundle()
|
|
||||||
bundle.putInt("position", position)
|
|
||||||
bundle.putLong("groupId", group.groupId)
|
|
||||||
bundle.putInt("bookSort", group.getRealBookSort())
|
|
||||||
bundle.putBoolean("enableRefresh", group.enableRefresh)
|
|
||||||
arguments = bundle
|
|
||||||
}
|
|
||||||
|
|
||||||
private val binding by viewBinding(FragmentBooksBinding::bind)
|
|
||||||
private val activityViewModel by activityViewModels<MainViewModel>()
|
|
||||||
|
|
||||||
private val bookshelfLayoutMode by lazy { requireContext().bookshelfLayoutMode }
|
|
||||||
|
|
||||||
private val bookshelfLayoutGrid by lazy { requireContext().bookshelfLayoutGrid }
|
|
||||||
|
|
||||||
private val booksAdapter: BaseBooksAdapter<*> by lazy {
|
|
||||||
when (bookshelfLayoutMode) {
|
|
||||||
0 -> {
|
|
||||||
BooksAdapterList(requireContext(), this, this, viewLifecycleOwner.lifecycle)
|
|
||||||
}
|
|
||||||
|
|
||||||
1 -> {
|
|
||||||
BooksAdapterGrid(requireContext(), this)
|
|
||||||
}
|
|
||||||
|
|
||||||
2 -> {
|
|
||||||
BooksAdapterGridCompact(requireContext(), this)
|
|
||||||
}
|
|
||||||
|
|
||||||
3 -> {
|
|
||||||
BooksAdapterGridCover(requireContext(), this)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
BooksAdapterListCompact(requireContext(), this, this, viewLifecycleOwner.lifecycle)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private var booksFlowJob: Job? = null
|
|
||||||
var position = 0
|
|
||||||
private set
|
|
||||||
var groupId = -1L
|
|
||||||
private set
|
|
||||||
var bookSort = 0
|
|
||||||
private set
|
|
||||||
private var upLastUpdateTimeJob: Job? = null
|
|
||||||
private var enableRefresh = true
|
|
||||||
|
|
||||||
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
arguments?.let {
|
|
||||||
position = it.getInt("position", 0)
|
|
||||||
groupId = it.getLong("groupId", -1)
|
|
||||||
bookSort = it.getInt("bookSort", 0)
|
|
||||||
enableRefresh = it.getBoolean("enableRefresh", true)
|
|
||||||
binding.refreshLayout.isEnabled = enableRefresh
|
|
||||||
}
|
|
||||||
initRecyclerView()
|
|
||||||
upRecyclerData()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initRecyclerView() {
|
|
||||||
//binding.rvBookshelf.setEdgeEffectColor(primaryColor)
|
|
||||||
upFastScrollerBar()
|
|
||||||
//binding.refreshLayout.setColorSchemeColors(accentColor)
|
|
||||||
binding.refreshLayout.setOnRefreshListener {
|
|
||||||
val books = booksAdapter.getItems()
|
|
||||||
val refreshList = if (AppConfig.bookshelfRefreshingLimit > 0) {
|
|
||||||
books.take(AppConfig.bookshelfRefreshingLimit)
|
|
||||||
} else {
|
|
||||||
books
|
|
||||||
}
|
|
||||||
binding.refreshLayout.isRefreshing = false
|
|
||||||
activityViewModel.upToc(refreshList)
|
|
||||||
}
|
|
||||||
binding.rvBookshelf.layoutManager = GridLayoutManager(context, bookshelfLayoutGrid)
|
|
||||||
binding.rvBookshelf.setRecycledViewPool(activityViewModel.booksGridRecycledViewPool)
|
|
||||||
booksAdapter.stateRestorationPolicy = StateRestorationPolicy.PREVENT_WHEN_EMPTY
|
|
||||||
binding.rvBookshelf.adapter = booksAdapter
|
|
||||||
booksAdapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
|
|
||||||
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
|
||||||
val layoutManager = binding.rvBookshelf.layoutManager
|
|
||||||
if (positionStart == 0 && itemCount == 1 && layoutManager is LinearLayoutManager) {
|
|
||||||
val scrollTo = layoutManager.findFirstVisibleItemPosition() - itemCount
|
|
||||||
binding.rvBookshelf.scrollToPosition(max(0, scrollTo))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
|
|
||||||
val layoutManager = binding.rvBookshelf.layoutManager
|
|
||||||
if (toPosition == 0 && itemCount == 1 && layoutManager is LinearLayoutManager) {
|
|
||||||
val scrollTo = layoutManager.findFirstVisibleItemPosition() - itemCount
|
|
||||||
binding.rvBookshelf.scrollToPosition(max(0, scrollTo))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(binding.rvBookshelf) { view, insets ->
|
|
||||||
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
|
||||||
view.setPadding(
|
|
||||||
view.paddingLeft,
|
|
||||||
view.paddingTop,
|
|
||||||
view.paddingRight,
|
|
||||||
systemBars.bottom
|
|
||||||
)
|
|
||||||
WindowInsetsCompat.CONSUMED
|
|
||||||
}
|
|
||||||
startLastUpdateTimeJob()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun upFastScrollerBar() {
|
|
||||||
val showBookshelfFastScroller = AppConfig.showBookshelfFastScroller
|
|
||||||
binding.rvBookshelf.setFastScrollEnabled(showBookshelfFastScroller)
|
|
||||||
if (showBookshelfFastScroller) {
|
|
||||||
binding.rvBookshelf.scrollBarSize = 0
|
|
||||||
} else {
|
|
||||||
binding.rvBookshelf.scrollBarSize =
|
|
||||||
ViewConfiguration.get(requireContext()).scaledScrollBarSize
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun upBookSort(sort: Int) {
|
|
||||||
binding.root.post {
|
|
||||||
if (!isAdded || view == null) return@post
|
|
||||||
arguments?.putInt("bookSort", sort)
|
|
||||||
bookSort = sort
|
|
||||||
upRecyclerData()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setEnableRefresh(enable: Boolean) {
|
|
||||||
enableRefresh = enable
|
|
||||||
binding.refreshLayout.isEnabled = enable
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新书籍列表信息
|
|
||||||
*/
|
|
||||||
private fun upRecyclerData() {
|
|
||||||
booksFlowJob?.cancel()
|
|
||||||
booksFlowJob = viewLifecycleOwner.lifecycleScope.launch {
|
|
||||||
appDb.bookDao.flowByGroup(groupId).map { list ->
|
|
||||||
//排序
|
|
||||||
|
|
||||||
val isDescending = AppConfig.bookshelfSortOrder == 1
|
|
||||||
|
|
||||||
when (bookSort) {
|
|
||||||
1 -> if (isDescending) list.sortedByDescending { it.latestChapterTime }
|
|
||||||
else list.sortedBy { it.latestChapterTime }
|
|
||||||
|
|
||||||
2 -> if (isDescending)
|
|
||||||
list.sortedWith { o1, o2 -> o2.name.cnCompare(o1.name) }
|
|
||||||
else
|
|
||||||
list.sortedWith { o1, o2 -> o1.name.cnCompare(o2.name) }
|
|
||||||
|
|
||||||
3 -> if (isDescending) list.sortedByDescending { it.order }
|
|
||||||
else list.sortedBy { it.order }
|
|
||||||
|
|
||||||
4 -> if (isDescending) list.sortedByDescending {
|
|
||||||
max(
|
|
||||||
it.latestChapterTime,
|
|
||||||
it.durChapterTime
|
|
||||||
)
|
|
||||||
}
|
|
||||||
else list.sortedBy { max(it.latestChapterTime, it.durChapterTime) }
|
|
||||||
|
|
||||||
5 -> if (isDescending)
|
|
||||||
list.sortedWith { o1, o2 -> o2.author.cnCompare(o1.author) }
|
|
||||||
else
|
|
||||||
list.sortedWith { o1, o2 -> o1.author.cnCompare(o2.author) }
|
|
||||||
|
|
||||||
else -> if (isDescending) list.sortedByDescending { it.durChapterTime }
|
|
||||||
else list.sortedBy { it.durChapterTime }
|
|
||||||
}
|
|
||||||
}.flowWithLifecycleAndDatabaseChangeFirst(
|
|
||||||
viewLifecycleOwner.lifecycle,
|
|
||||||
Lifecycle.State.RESUMED,
|
|
||||||
AppDatabase.BOOK_TABLE_NAME
|
|
||||||
).catch {
|
|
||||||
AppLog.put("书架更新出错", it)
|
|
||||||
}.conflate().flowOn(Dispatchers.Default).collect { list ->
|
|
||||||
if (view == null) return@collect
|
|
||||||
binding.emptyView.isGone = list.isNotEmpty()
|
|
||||||
binding.refreshLayout.isEnabled = enableRefresh && list.isNotEmpty()
|
|
||||||
booksAdapter.setItems(list)
|
|
||||||
delay(500)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun startLastUpdateTimeJob() {
|
|
||||||
upLastUpdateTimeJob?.cancel()
|
|
||||||
if (!AppConfig.showLastUpdateTime || (bookshelfLayoutMode != 0 && bookshelfLayoutMode != 4)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
upLastUpdateTimeJob = viewLifecycleOwner.lifecycleScope.launch {
|
|
||||||
repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
|
||||||
while (isActive) {
|
|
||||||
booksAdapter.upLastUpdateTime()
|
|
||||||
delay(30 * 1000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getBooks(): List<Book> {
|
|
||||||
return booksAdapter.getItems()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun gotoTop() {
|
|
||||||
if (AppConfig.isEInkMode) {
|
|
||||||
binding.rvBookshelf.scrollToPosition(0)
|
|
||||||
} else {
|
|
||||||
binding.rvBookshelf.smoothScrollToPosition(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getBooksCount(): Int {
|
|
||||||
return booksAdapter.itemCount
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
super.onDestroyView()
|
|
||||||
/**
|
|
||||||
* 将 RecyclerView 中的视图全部回收到 RecycledViewPool 中
|
|
||||||
*/
|
|
||||||
upLastUpdateTimeJob?.cancel()
|
|
||||||
booksFlowJob?.cancel()
|
|
||||||
binding.rvBookshelf.setItemViewCacheSize(0)
|
|
||||||
binding.rvBookshelf.adapter = null
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun open(book: Book, sharedView: View) {
|
|
||||||
if (AppConfig.sharedElementEnterTransitionEnable){
|
|
||||||
val transitionName = "book_${book.bookUrl}"
|
|
||||||
sharedView.transitionName = transitionName
|
|
||||||
|
|
||||||
val cls = when {
|
|
||||||
book.isAudio -> AudioPlayActivity::class.java
|
|
||||||
book.isImage && AppConfig.showMangaUi -> ReadMangaActivity::class.java
|
|
||||||
else -> ReadBookActivity::class.java
|
|
||||||
}
|
|
||||||
|
|
||||||
val intent = Intent(requireContext(), cls).apply {
|
|
||||||
putExtra("bookUrl", book.bookUrl)
|
|
||||||
putExtra("transitionName", transitionName)
|
|
||||||
}
|
|
||||||
|
|
||||||
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(
|
|
||||||
requireActivity(),
|
|
||||||
sharedView,
|
|
||||||
transitionName
|
|
||||||
)
|
|
||||||
|
|
||||||
startActivity(intent, options.toBundle())
|
|
||||||
} else {
|
|
||||||
startActivityForBook(book)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun openBookInfo(book: Book, sharedView: View) {
|
|
||||||
if (AppConfig.sharedElementEnterTransitionEnable){
|
|
||||||
val intent = Intent(requireContext(), BookInfoActivity::class.java).apply {
|
|
||||||
putExtra("name", book.name)
|
|
||||||
putExtra("author", book.author)
|
|
||||||
putExtra("bookUrl", book.bookUrl)
|
|
||||||
putExtra("transitionName", "book_${book.bookUrl}")
|
|
||||||
}
|
|
||||||
|
|
||||||
sharedView.transitionName = "book_${book.bookUrl}"
|
|
||||||
|
|
||||||
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(
|
|
||||||
requireActivity(),
|
|
||||||
sharedView,
|
|
||||||
sharedView.transitionName
|
|
||||||
)
|
|
||||||
|
|
||||||
startActivity(intent, options.toBundle())
|
|
||||||
} else {
|
|
||||||
startActivity<BookInfoActivity> {
|
|
||||||
putExtra("name", book.name)
|
|
||||||
putExtra("author", book.author)
|
|
||||||
putExtra("bookUrl", book.bookUrl)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
override fun isUpdate(bookUrl: String): Boolean {
|
|
||||||
return activityViewModel.isUpdate(bookUrl)
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
|
||||||
override fun observeLiveBus() {
|
|
||||||
super.observeLiveBus()
|
|
||||||
observeEvent<String>(EventBus.UP_BOOKSHELF) {
|
|
||||||
booksAdapter.notification(it)
|
|
||||||
}
|
|
||||||
observeEvent<String>(EventBus.BOOKSHELF_REFRESH) {
|
|
||||||
booksAdapter.notifyDataSetChanged()
|
|
||||||
startLastUpdateTimeJob()
|
|
||||||
upFastScrollerBar()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,229 +0,0 @@
|
|||||||
@file:Suppress("DEPRECATION")
|
|
||||||
|
|
||||||
package io.legado.app.ui.main.bookshelf.books
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.graphics.Color
|
|
||||||
import android.graphics.drawable.GradientDrawable
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import androidx.appcompat.widget.PopupMenu
|
|
||||||
import androidx.appcompat.widget.SearchView
|
|
||||||
import androidx.core.graphics.ColorUtils
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
|
||||||
import com.google.android.material.appbar.AppBarLayout
|
|
||||||
import com.google.android.material.tabs.TabLayout
|
|
||||||
import com.google.android.material.tabs.TabLayoutMediator
|
|
||||||
import io.legado.app.R
|
|
||||||
import io.legado.app.data.appDb
|
|
||||||
import io.legado.app.data.entities.Book
|
|
||||||
import io.legado.app.data.entities.BookGroup
|
|
||||||
import io.legado.app.databinding.FragmentBookshelf1Binding
|
|
||||||
import io.legado.app.help.config.AppConfig
|
|
||||||
import io.legado.app.ui.book.group.GroupEditDialog
|
|
||||||
import io.legado.app.ui.book.search.SearchActivity
|
|
||||||
import io.legado.app.ui.config.themeConfig.ThemeConfig
|
|
||||||
import io.legado.app.ui.main.bookshelf.BaseBookshelfFragment
|
|
||||||
import io.legado.app.utils.showDialogFragment
|
|
||||||
import io.legado.app.utils.themeColor
|
|
||||||
import io.legado.app.utils.toastOnUi
|
|
||||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
|
||||||
import kotlin.collections.set
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 书架界面
|
|
||||||
*/
|
|
||||||
class BookshelfFragment1() : BaseBookshelfFragment(R.layout.fragment_bookshelf1),
|
|
||||||
TabLayout.OnTabSelectedListener,
|
|
||||||
SearchView.OnQueryTextListener {
|
|
||||||
|
|
||||||
constructor(position: Int) : this() {
|
|
||||||
val bundle = Bundle()
|
|
||||||
bundle.putInt("position", position)
|
|
||||||
arguments = bundle
|
|
||||||
}
|
|
||||||
|
|
||||||
private val binding by viewBinding(FragmentBookshelf1Binding::bind)
|
|
||||||
|
|
||||||
private val bookGroups = mutableListOf<BookGroup>()
|
|
||||||
private val fragmentMap = hashMapOf<Long, BooksFragment>()
|
|
||||||
override val groupId: Long get() = selectedGroup?.groupId ?: 0
|
|
||||||
|
|
||||||
private lateinit var adapter: TabFragmentPageAdapter
|
|
||||||
private var tabFadeListener: AppBarLayout.OnOffsetChangedListener? = null
|
|
||||||
|
|
||||||
override val books: List<Book>
|
|
||||||
get() {
|
|
||||||
val fragment = fragmentMap[groupId]
|
|
||||||
return fragment?.getBooks() ?: emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
setSupportToolbar(binding.topBar)
|
|
||||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.S)
|
|
||||||
binding.titleBar.fitsSystemWindows = true
|
|
||||||
initView()
|
|
||||||
initBookGroupData()
|
|
||||||
setupExpandButton()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val selectedGroup: BookGroup?
|
|
||||||
get() = bookGroups.getOrNull(binding.viewPagerBookshelf.currentItem)
|
|
||||||
|
|
||||||
private fun initView() {
|
|
||||||
adapter = TabFragmentPageAdapter(this)
|
|
||||||
binding.viewPagerBookshelf.adapter = adapter
|
|
||||||
binding.viewPagerBookshelf.offscreenPageLimit = 3
|
|
||||||
|
|
||||||
TabLayoutMediator(binding.tabLayout, binding.viewPagerBookshelf) { tab, position ->
|
|
||||||
tab.text = bookGroups[position].groupName
|
|
||||||
}.attach()
|
|
||||||
|
|
||||||
updateTabFadeEffect()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupExpandButton() {
|
|
||||||
binding.btnExpandTabs.setOnClickListener {
|
|
||||||
showTabSelectionMenu()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateTabFadeEffect() {
|
|
||||||
tabFadeListener?.let { binding.titleBar.removeOnOffsetChangedListener(it) }
|
|
||||||
val listener = AppBarLayout.OnOffsetChangedListener { appBarLayout, verticalOffset ->
|
|
||||||
val drawable = binding.tabRightFade.background as? GradientDrawable
|
|
||||||
val opacity = if (ThemeConfig.enableBlur) ThemeConfig.containerOpacity / 100f else 1f
|
|
||||||
val baseColor = if (-verticalOffset >= appBarLayout.totalScrollRange) {
|
|
||||||
requireContext().themeColor(com.google.android.material.R.attr.colorSurfaceContainer)
|
|
||||||
} else {
|
|
||||||
requireContext().themeColor(com.google.android.material.R.attr.colorSurface)
|
|
||||||
}
|
|
||||||
val newAlpha = (Color.alpha(baseColor) * opacity).toInt()
|
|
||||||
drawable?.colors = intArrayOf(Color.TRANSPARENT, ColorUtils.setAlphaComponent(baseColor, newAlpha))
|
|
||||||
}
|
|
||||||
tabFadeListener = listener
|
|
||||||
binding.titleBar.addOnOffsetChangedListener(listener)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showTabSelectionMenu() {
|
|
||||||
val popup = PopupMenu(requireContext(), binding.btnExpandTabs)
|
|
||||||
|
|
||||||
bookGroups.forEachIndexed { index, bookGroup ->
|
|
||||||
popup.menu.add(0, index, index, bookGroup.groupName)
|
|
||||||
}
|
|
||||||
|
|
||||||
popup.setOnMenuItemClickListener { item ->
|
|
||||||
val position = item.itemId
|
|
||||||
if (position in bookGroups.indices) {
|
|
||||||
binding.viewPagerBookshelf.setCurrentItem(position, true)
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
popup.setOnDismissListener {
|
|
||||||
binding.btnExpandTabs.isChecked = false
|
|
||||||
}
|
|
||||||
|
|
||||||
popup.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateExpandButtonVisibility() {
|
|
||||||
binding.btnExpandTabs.visibility = if (AppConfig.shouldShowExpandButton) {
|
|
||||||
View.VISIBLE
|
|
||||||
} else {
|
|
||||||
View.GONE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onQueryTextSubmit(query: String?): Boolean {
|
|
||||||
SearchActivity.start(requireContext(), query)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onQueryTextChange(newText: String?): Boolean {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
|
||||||
@Synchronized
|
|
||||||
override fun upGroup(data: List<BookGroup>) {
|
|
||||||
if (data.isEmpty()) {
|
|
||||||
appDb.bookGroupDao.enableGroup(BookGroup.IdAll)
|
|
||||||
} else {
|
|
||||||
if (data != bookGroups) {
|
|
||||||
bookGroups.clear()
|
|
||||||
bookGroups.addAll(data)
|
|
||||||
adapter.notifyDataSetChanged()
|
|
||||||
adapter = TabFragmentPageAdapter(this)
|
|
||||||
binding.viewPagerBookshelf.adapter = adapter
|
|
||||||
|
|
||||||
TabLayoutMediator(binding.tabLayout, binding.viewPagerBookshelf) { tab, position ->
|
|
||||||
tab.text = bookGroups[position].groupName
|
|
||||||
}.attach()
|
|
||||||
|
|
||||||
updateExpandButtonVisibility()
|
|
||||||
updateTabFadeEffect()
|
|
||||||
selectLastTab()
|
|
||||||
|
|
||||||
binding.tabLayout.post {
|
|
||||||
for (i in 0 until bookGroups.size) {
|
|
||||||
binding.tabLayout.getTabAt(i)?.view?.setOnLongClickListener {
|
|
||||||
showDialogFragment(GroupEditDialog(bookGroups[i]))
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun upSort() {
|
|
||||||
childFragmentManager.fragments.forEach {
|
|
||||||
if (it is BooksFragment) {
|
|
||||||
val position = it.position
|
|
||||||
val group = bookGroups.getOrNull(position) ?: return@forEach
|
|
||||||
it.setEnableRefresh(group.enableRefresh)
|
|
||||||
it.upBookSort(group.getRealBookSort())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun selectLastTab() {
|
|
||||||
binding.tabLayout.post {
|
|
||||||
binding.tabLayout.removeOnTabSelectedListener(this)
|
|
||||||
binding.tabLayout.getTabAt(AppConfig.saveTabPosition)?.select()
|
|
||||||
binding.tabLayout.addOnTabSelectedListener(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTabReselected(tab: TabLayout.Tab) {
|
|
||||||
selectedGroup?.let { group ->
|
|
||||||
fragmentMap[group.groupId]?.let {
|
|
||||||
toastOnUi("${group.groupName}(${it.getBooksCount()})")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTabUnselected(tab: TabLayout.Tab) = Unit
|
|
||||||
|
|
||||||
override fun onTabSelected(tab: TabLayout.Tab) {
|
|
||||||
AppConfig.saveTabPosition = tab.position
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun gotoTop() {
|
|
||||||
fragmentMap[groupId]?.gotoTop()
|
|
||||||
}
|
|
||||||
|
|
||||||
private inner class TabFragmentPageAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) {
|
|
||||||
override fun getItemCount(): Int = bookGroups.size
|
|
||||||
|
|
||||||
override fun createFragment(position: Int): Fragment {
|
|
||||||
val group = bookGroups[position]
|
|
||||||
val fragment = BooksFragment(position, group)
|
|
||||||
fragmentMap[group.groupId] = fragment
|
|
||||||
return fragment
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,467 +0,0 @@
|
|||||||
package io.legado.app.ui.main.bookshelf.books
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewConfiguration
|
|
||||||
import androidx.activity.OnBackPressedCallback
|
|
||||||
import androidx.appcompat.widget.SearchView
|
|
||||||
import androidx.core.app.ActivityOptionsCompat
|
|
||||||
import androidx.core.view.ViewCompat
|
|
||||||
import androidx.core.view.WindowInsetsCompat
|
|
||||||
import androidx.core.view.isGone
|
|
||||||
import androidx.lifecycle.Lifecycle
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
|
||||||
import androidx.recyclerview.widget.DefaultItemAnimator
|
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import io.legado.app.R
|
|
||||||
import io.legado.app.constant.AppLog
|
|
||||||
import io.legado.app.constant.EventBus
|
|
||||||
import io.legado.app.data.AppDatabase
|
|
||||||
import io.legado.app.data.appDb
|
|
||||||
import io.legado.app.data.entities.Book
|
|
||||||
import io.legado.app.data.entities.BookGroup
|
|
||||||
import io.legado.app.databinding.FragmentBookshelf2Binding
|
|
||||||
import io.legado.app.help.book.isAudio
|
|
||||||
import io.legado.app.help.book.isImage
|
|
||||||
import io.legado.app.help.config.AppConfig
|
|
||||||
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
|
|
||||||
import io.legado.app.ui.main.bookshelf.books.styleFold.BaseBooksAdapter
|
|
||||||
import io.legado.app.ui.main.bookshelf.books.styleFold.BooksAdapterGrid
|
|
||||||
import io.legado.app.ui.main.bookshelf.books.styleFold.BooksAdapterGridCompact
|
|
||||||
import io.legado.app.ui.main.bookshelf.books.styleFold.BooksAdapterGridCover
|
|
||||||
import io.legado.app.ui.main.bookshelf.books.styleFold.BooksAdapterList
|
|
||||||
import io.legado.app.ui.main.bookshelf.books.styleFold.BooksAdapterListCompact
|
|
||||||
import io.legado.app.utils.bookshelfLayoutGrid
|
|
||||||
import io.legado.app.utils.bookshelfLayoutMode
|
|
||||||
import io.legado.app.utils.cnCompare
|
|
||||||
import io.legado.app.utils.flowWithLifecycleAndDatabaseChangeFirst
|
|
||||||
import io.legado.app.utils.observeEvent
|
|
||||||
import io.legado.app.utils.showDialogFragment
|
|
||||||
import io.legado.app.utils.startActivity
|
|
||||||
import io.legado.app.utils.startActivityForBook
|
|
||||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.flow.catch
|
|
||||||
import kotlinx.coroutines.flow.conflate
|
|
||||||
import kotlinx.coroutines.flow.flowOn
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlin.math.max
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 书架界面
|
|
||||||
*/
|
|
||||||
class BookshelfFragment2() : BaseBookshelfFragment(R.layout.fragment_bookshelf2),
|
|
||||||
SearchView.OnQueryTextListener,
|
|
||||||
BaseBooksAdapter.CallBack {
|
|
||||||
|
|
||||||
fun interface OnGroupIdChangeListener {
|
|
||||||
fun onGroupIdChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
constructor(position: Int) : this() {
|
|
||||||
val bundle = Bundle()
|
|
||||||
bundle.putInt("position", position)
|
|
||||||
arguments = bundle
|
|
||||||
}
|
|
||||||
|
|
||||||
private val binding by viewBinding(FragmentBookshelf2Binding::bind)
|
|
||||||
|
|
||||||
private val bookshelfLayoutMode by lazy { requireContext().bookshelfLayoutMode }
|
|
||||||
|
|
||||||
private val bookshelfLayoutGrid by lazy { requireContext().bookshelfLayoutGrid }
|
|
||||||
|
|
||||||
private var groupIdChangeListener: OnGroupIdChangeListener? = null
|
|
||||||
|
|
||||||
private val booksAdapter: BaseBooksAdapter<*> by lazy {
|
|
||||||
when (bookshelfLayoutMode) {
|
|
||||||
0 -> {
|
|
||||||
BooksAdapterList(requireContext(), this)
|
|
||||||
}
|
|
||||||
|
|
||||||
1 -> {
|
|
||||||
BooksAdapterGrid(requireContext(), this)
|
|
||||||
}
|
|
||||||
|
|
||||||
2 -> {
|
|
||||||
BooksAdapterGridCompact(requireContext(), this)
|
|
||||||
}
|
|
||||||
|
|
||||||
3 -> {
|
|
||||||
BooksAdapterGridCover(requireContext(), this)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
BooksAdapterListCompact(requireContext(), this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var onGroupIdChangedListener: ((isRoot: Boolean) -> Unit)? = null
|
|
||||||
|
|
||||||
private var bookGroups: List<BookGroup> = emptyList()
|
|
||||||
private var booksFlowJob: Job? = null
|
|
||||||
override var groupId = BookGroup.Companion.IdRoot
|
|
||||||
private var allBooks: List<Book> = emptyList()
|
|
||||||
override var books: List<Book> = emptyList()
|
|
||||||
private var enableRefresh = true
|
|
||||||
|
|
||||||
private lateinit var backCallback: OnBackPressedCallback
|
|
||||||
|
|
||||||
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
setSupportToolbar(binding.topBar)
|
|
||||||
initRecyclerView()
|
|
||||||
initBookGroupData()
|
|
||||||
initAllBooksData()
|
|
||||||
initBooksData()
|
|
||||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.S)
|
|
||||||
binding.titleBar.fitsSystemWindows = true
|
|
||||||
backCallback = object : OnBackPressedCallback(false) { // 初始禁用
|
|
||||||
override fun handleOnBackPressed() {
|
|
||||||
if (back()) {
|
|
||||||
updateBackCallbackState()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, backCallback)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
onGroupIdChangedListener?.invoke(true)
|
|
||||||
super.onDestroyView()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateBackCallbackState() {
|
|
||||||
val isRoot = groupId == BookGroup.IdRoot
|
|
||||||
backCallback.isEnabled = !isRoot
|
|
||||||
onGroupIdChangedListener?.invoke(isRoot)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
fun BaseBooksAdapter<*>.getBookItems(): List<Book> {
|
|
||||||
return getItems() as List<Book>
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initRecyclerView() {
|
|
||||||
upFastScrollerBar()
|
|
||||||
binding.refreshLayout.setOnRefreshListener {
|
|
||||||
val books = booksAdapter.getBookItems()
|
|
||||||
val refreshList = if (AppConfig.bookshelfRefreshingLimit > 0) {
|
|
||||||
books.take(AppConfig.bookshelfRefreshingLimit)
|
|
||||||
} else {
|
|
||||||
books
|
|
||||||
}
|
|
||||||
binding.refreshLayout.isRefreshing = false
|
|
||||||
activityViewModel.upToc(refreshList)
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.rvBookshelf.layoutManager = GridLayoutManager(context, bookshelfLayoutGrid)
|
|
||||||
binding.rvBookshelf.itemAnimator = null
|
|
||||||
binding.rvBookshelf.adapter = booksAdapter
|
|
||||||
booksAdapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
|
|
||||||
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
|
||||||
val layoutManager = binding.rvBookshelf.layoutManager
|
|
||||||
if (positionStart == 0 && layoutManager is LinearLayoutManager) {
|
|
||||||
val scrollTo = layoutManager.findFirstVisibleItemPosition() - itemCount
|
|
||||||
binding.rvBookshelf.scrollToPosition(max(0, scrollTo))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
|
|
||||||
val layoutManager = binding.rvBookshelf.layoutManager
|
|
||||||
if (toPosition == 0 && layoutManager is LinearLayoutManager) {
|
|
||||||
val scrollTo = layoutManager.findFirstVisibleItemPosition() - itemCount
|
|
||||||
binding.rvBookshelf.scrollToPosition(max(0, scrollTo))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
binding.rvBookshelf.itemAnimator = DefaultItemAnimator()
|
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(binding.rvBookshelf) { view, insets ->
|
|
||||||
val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
|
|
||||||
view.setPadding(
|
|
||||||
view.paddingLeft,
|
|
||||||
view.paddingTop,
|
|
||||||
view.paddingRight,
|
|
||||||
systemBars.bottom
|
|
||||||
)
|
|
||||||
WindowInsetsCompat.CONSUMED
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun upFastScrollerBar() {
|
|
||||||
val showBookshelfFastScroller = AppConfig.showBookshelfFastScroller
|
|
||||||
binding.rvBookshelf.setFastScrollEnabled(showBookshelfFastScroller)
|
|
||||||
if (showBookshelfFastScroller) {
|
|
||||||
binding.rvBookshelf.scrollBarSize = 0
|
|
||||||
} else {
|
|
||||||
binding.rvBookshelf.scrollBarSize =
|
|
||||||
ViewConfiguration.get(requireContext()).scaledScrollBarSize
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun upGroup(data: List<BookGroup>) {
|
|
||||||
if (data != bookGroups) {
|
|
||||||
bookGroups = data
|
|
||||||
booksAdapter.updateItems()
|
|
||||||
binding.emptyView.isGone = getItemCount() > 0
|
|
||||||
binding.refreshLayout.isEnabled = enableRefresh && getItemCount() > 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun upSort() {
|
|
||||||
initAllBooksData()
|
|
||||||
initBooksData()
|
|
||||||
}
|
|
||||||
|
|
||||||
private var allBooksFlowJob: Job? = null
|
|
||||||
|
|
||||||
private fun initAllBooksData() {
|
|
||||||
allBooksFlowJob?.cancel()
|
|
||||||
allBooksFlowJob = viewLifecycleOwner.lifecycleScope.launch {
|
|
||||||
appDb.bookDao.flowAll().map { list ->
|
|
||||||
val sortType = BookGroup(BookGroup.Companion.IdRoot, "").getRealBookSort()
|
|
||||||
when (sortType) {
|
|
||||||
1 -> list.sortedByDescending { it.latestChapterTime }
|
|
||||||
2 -> list.sortedWith { o1, o2 -> o1.name.cnCompare(o2.name) }
|
|
||||||
3 -> list.sortedBy { it.order }
|
|
||||||
4 -> list.sortedByDescending { max(it.latestChapterTime, it.durChapterTime) }
|
|
||||||
5 -> list.sortedWith { o1, o2 -> o1.author.cnCompare(o2.author) }
|
|
||||||
else -> list.sortedByDescending { it.durChapterTime }
|
|
||||||
}
|
|
||||||
}.flowWithLifecycleAndDatabaseChangeFirst(
|
|
||||||
viewLifecycleOwner.lifecycle,
|
|
||||||
Lifecycle.State.RESUMED,
|
|
||||||
AppDatabase.Companion.BOOK_TABLE_NAME
|
|
||||||
).catch {
|
|
||||||
AppLog.put("所有书籍更新出错", it)
|
|
||||||
}.conflate().flowOn(Dispatchers.Default).collect { list ->
|
|
||||||
allBooks = list
|
|
||||||
booksAdapter.setAllBooks(allBooks)
|
|
||||||
booksAdapter.updateItems()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initBooksData() {
|
|
||||||
if (groupId == BookGroup.IdRoot) {
|
|
||||||
if (isAdded) {
|
|
||||||
binding.collTopBar.title = getString(R.string.bookshelf)
|
|
||||||
binding.refreshLayout.isEnabled = true
|
|
||||||
enableRefresh = true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
bookGroups.firstOrNull { groupId == it.groupId }?.let {
|
|
||||||
binding.collTopBar.title = it.groupName
|
|
||||||
binding.refreshLayout.isEnabled = it.enableRefresh
|
|
||||||
enableRefresh = it.enableRefresh
|
|
||||||
}
|
|
||||||
}
|
|
||||||
booksFlowJob?.cancel()
|
|
||||||
booksFlowJob = viewLifecycleOwner.lifecycleScope.launch {
|
|
||||||
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
|
||||||
appDb.bookDao.flowByGroup(groupId)
|
|
||||||
.map { list ->
|
|
||||||
val isDescending = AppConfig.bookshelfSortOrder == 1
|
|
||||||
val sortType = AppConfig.getBookSortByGroupId(groupId)
|
|
||||||
|
|
||||||
when (sortType) {
|
|
||||||
1 -> if (isDescending) list.sortedByDescending { it.latestChapterTime }
|
|
||||||
else list.sortedBy { it.latestChapterTime }
|
|
||||||
|
|
||||||
2 -> if (isDescending)
|
|
||||||
list.sortedWith { o1, o2 -> o2.name.cnCompare(o1.name) }
|
|
||||||
else
|
|
||||||
list.sortedWith { o1, o2 -> o1.name.cnCompare(o2.name) }
|
|
||||||
|
|
||||||
3 -> if (isDescending) list.sortedByDescending { it.order }
|
|
||||||
else list.sortedBy { it.order }
|
|
||||||
|
|
||||||
4 -> if (isDescending)
|
|
||||||
list.sortedByDescending { max(it.latestChapterTime, it.durChapterTime) }
|
|
||||||
else
|
|
||||||
list.sortedBy { max(it.latestChapterTime, it.durChapterTime) }
|
|
||||||
|
|
||||||
5 -> if (isDescending)
|
|
||||||
list.sortedWith { o1, o2 -> o2.author.cnCompare(o1.author) }
|
|
||||||
else
|
|
||||||
list.sortedWith { o1, o2 -> o1.author.cnCompare(o2.author) }
|
|
||||||
|
|
||||||
else ->
|
|
||||||
if (isDescending) list.sortedByDescending { it.durChapterTime }
|
|
||||||
else list.sortedBy { it.durChapterTime }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.flowOn(Dispatchers.Default)
|
|
||||||
.catch { AppLog.put("书架更新出错", it) }
|
|
||||||
.collect { list ->
|
|
||||||
|
|
||||||
books = list
|
|
||||||
booksAdapter.updateItems()
|
|
||||||
val empty = getItemCount() > 0
|
|
||||||
binding.emptyView.isGone = empty
|
|
||||||
binding.refreshLayout.isEnabled = enableRefresh && empty
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun back(): Boolean {
|
|
||||||
if (groupId != BookGroup.Companion.IdRoot) {
|
|
||||||
groupId = BookGroup.Companion.IdRoot
|
|
||||||
initBooksData()
|
|
||||||
groupIdChangeListener?.onGroupIdChanged()
|
|
||||||
updateBackCallbackState()
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
//需要提前设置,因为在返回时触发
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onQueryTextSubmit(query: String?): Boolean {
|
|
||||||
SearchActivity.Companion.start(requireContext(), query)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onQueryTextChange(newText: String?): Boolean {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun gotoTop() {
|
|
||||||
if (AppConfig.isEInkMode) {
|
|
||||||
binding.rvBookshelf.scrollToPosition(0)
|
|
||||||
} else {
|
|
||||||
binding.rvBookshelf.smoothScrollToPosition(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onItemClick(item: Any, sharedView: View) {
|
|
||||||
if (AppConfig.sharedElementEnterTransitionEnable){
|
|
||||||
when (item) {
|
|
||||||
is Book -> {
|
|
||||||
val transitionName = "book_${item.bookUrl}"
|
|
||||||
sharedView.transitionName = transitionName
|
|
||||||
|
|
||||||
val cls = when {
|
|
||||||
item.isAudio -> AudioPlayActivity::class.java
|
|
||||||
item.isImage && AppConfig.showMangaUi -> ReadMangaActivity::class.java
|
|
||||||
else -> ReadBookActivity::class.java
|
|
||||||
}
|
|
||||||
|
|
||||||
val intent = Intent(requireContext(), cls).apply {
|
|
||||||
putExtra("bookUrl", item.bookUrl)
|
|
||||||
putExtra("transitionName", transitionName)
|
|
||||||
}
|
|
||||||
|
|
||||||
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(
|
|
||||||
requireActivity(),
|
|
||||||
sharedView,
|
|
||||||
transitionName
|
|
||||||
)
|
|
||||||
|
|
||||||
startActivity(intent, options.toBundle())
|
|
||||||
}
|
|
||||||
|
|
||||||
is BookGroup -> {
|
|
||||||
groupId = item.groupId
|
|
||||||
initBooksData()
|
|
||||||
groupIdChangeListener?.onGroupIdChanged()
|
|
||||||
updateBackCallbackState()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
when (item) {
|
|
||||||
is Book -> startActivityForBook(item)
|
|
||||||
|
|
||||||
is BookGroup -> {
|
|
||||||
groupId = item.groupId
|
|
||||||
initBooksData()
|
|
||||||
groupIdChangeListener?.onGroupIdChanged()
|
|
||||||
updateBackCallbackState()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onItemLongClick(item: Any, sharedView: View) {
|
|
||||||
if (AppConfig.sharedElementEnterTransitionEnable){
|
|
||||||
when (item) {
|
|
||||||
is Book -> {
|
|
||||||
val intent = Intent(requireContext(), BookInfoActivity::class.java).apply {
|
|
||||||
putExtra("name", item.name)
|
|
||||||
putExtra("author", item.author)
|
|
||||||
putExtra("bookUrl", item.bookUrl)
|
|
||||||
putExtra("transitionName", "book_${item.bookUrl}") // 给共享元素唯一标识
|
|
||||||
}
|
|
||||||
|
|
||||||
sharedView.transitionName = "book_${item.bookUrl}"
|
|
||||||
|
|
||||||
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(
|
|
||||||
requireActivity(),
|
|
||||||
sharedView,
|
|
||||||
sharedView.transitionName
|
|
||||||
)
|
|
||||||
|
|
||||||
startActivity(intent, options.toBundle())
|
|
||||||
}
|
|
||||||
|
|
||||||
is BookGroup -> showDialogFragment(GroupEditDialog(item))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
when (item) {
|
|
||||||
is Book -> startActivity<BookInfoActivity> {
|
|
||||||
putExtra("name", item.name)
|
|
||||||
putExtra("author", item.author)
|
|
||||||
putExtra("bookUrl", item.bookUrl)
|
|
||||||
}
|
|
||||||
|
|
||||||
is BookGroup -> showDialogFragment(GroupEditDialog(item))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun isUpdate(bookUrl: String): Boolean {
|
|
||||||
return activityViewModel.isUpdate(bookUrl)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getItemCount(): Int {
|
|
||||||
return if (groupId == BookGroup.Companion.IdRoot) {
|
|
||||||
bookGroups.size + books.size
|
|
||||||
} else {
|
|
||||||
books.size
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItems(): List<Any> {
|
|
||||||
return if (groupId == BookGroup.Companion.IdRoot) {
|
|
||||||
bookGroups + books
|
|
||||||
} else {
|
|
||||||
books
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
|
||||||
override fun observeLiveBus() {
|
|
||||||
super.observeLiveBus()
|
|
||||||
observeEvent<String>(EventBus.UP_BOOKSHELF) {
|
|
||||||
booksAdapter.notification(it)
|
|
||||||
}
|
|
||||||
observeEvent<String>(EventBus.BOOKSHELF_REFRESH) {
|
|
||||||
booksAdapter.notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
@file:Suppress("DEPRECATION")
|
|
||||||
|
|
||||||
package io.legado.app.ui.main.bookshelf.books
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import androidx.appcompat.widget.SearchView
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
|
||||||
import androidx.viewpager2.widget.ViewPager2
|
|
||||||
import com.google.android.material.tabs.TabLayout
|
|
||||||
import io.legado.app.R
|
|
||||||
import io.legado.app.data.appDb
|
|
||||||
import io.legado.app.data.entities.Book
|
|
||||||
import io.legado.app.data.entities.BookGroup
|
|
||||||
import io.legado.app.databinding.FragmentBookshelf3Binding
|
|
||||||
import io.legado.app.help.config.AppConfig
|
|
||||||
import io.legado.app.ui.book.search.SearchActivity
|
|
||||||
import io.legado.app.ui.main.bookshelf.BaseBookshelfFragment
|
|
||||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
|
||||||
import kotlin.collections.set
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 书架界面
|
|
||||||
*/
|
|
||||||
class BookshelfFragment3() : BaseBookshelfFragment(R.layout.fragment_bookshelf3),
|
|
||||||
SearchView.OnQueryTextListener {
|
|
||||||
|
|
||||||
constructor(position: Int) : this() {
|
|
||||||
val bundle = Bundle()
|
|
||||||
bundle.putInt("position", position)
|
|
||||||
arguments = bundle
|
|
||||||
}
|
|
||||||
|
|
||||||
private val binding by viewBinding(FragmentBookshelf3Binding::bind)
|
|
||||||
|
|
||||||
private val bookGroups = mutableListOf<BookGroup>()
|
|
||||||
private val fragmentMap = hashMapOf<Long, BooksFragment>()
|
|
||||||
override val groupId: Long get() = selectedGroup?.groupId ?: 0
|
|
||||||
|
|
||||||
private lateinit var adapter: TabFragmentPageAdapter
|
|
||||||
|
|
||||||
override val books: List<Book>
|
|
||||||
get() {
|
|
||||||
val fragment = fragmentMap[groupId]
|
|
||||||
return fragment?.getBooks() ?: emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
setSupportToolbar(binding.topBar)
|
|
||||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.S)
|
|
||||||
binding.titleBar.fitsSystemWindows = true
|
|
||||||
initView()
|
|
||||||
initBookGroupData()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val selectedGroup: BookGroup?
|
|
||||||
get() = bookGroups.getOrNull(binding.viewPagerBookshelf.currentItem)
|
|
||||||
|
|
||||||
private fun initView() {
|
|
||||||
adapter = TabFragmentPageAdapter(this)
|
|
||||||
binding.viewPagerBookshelf.adapter = adapter
|
|
||||||
binding.viewPagerBookshelf.offscreenPageLimit = 3
|
|
||||||
|
|
||||||
binding.viewPagerBookshelf.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
|
|
||||||
override fun onPageSelected(position: Int) {
|
|
||||||
super.onPageSelected(position)
|
|
||||||
updateToolbarTitle(position)
|
|
||||||
AppConfig.saveTabPosition = position
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateToolbarTitle(position: Int) {
|
|
||||||
if (bookGroups.isNotEmpty() && position in 0 until bookGroups.size) {
|
|
||||||
binding.collTopBar.title = bookGroups[position].groupName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onQueryTextSubmit(query: String?): Boolean {
|
|
||||||
SearchActivity.start(requireContext(), query)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onQueryTextChange(newText: String?): Boolean {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
|
||||||
@Synchronized
|
|
||||||
override fun upGroup(data: List<BookGroup>) {
|
|
||||||
if (data.isEmpty()) {
|
|
||||||
appDb.bookGroupDao.enableGroup(BookGroup.IdAll)
|
|
||||||
} else {
|
|
||||||
if (data != bookGroups) {
|
|
||||||
bookGroups.clear()
|
|
||||||
bookGroups.addAll(data)
|
|
||||||
adapter.notifyDataSetChanged()
|
|
||||||
adapter = TabFragmentPageAdapter(this)
|
|
||||||
binding.viewPagerBookshelf.adapter = adapter
|
|
||||||
updateToolbarTitle(binding.viewPagerBookshelf.currentItem)
|
|
||||||
selectLastTab()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun upSort() {
|
|
||||||
childFragmentManager.fragments.forEach {
|
|
||||||
if (it is BooksFragment) {
|
|
||||||
val position = it.position
|
|
||||||
val group = bookGroups.getOrNull(position) ?: return@forEach
|
|
||||||
it.setEnableRefresh(group.enableRefresh)
|
|
||||||
it.upBookSort(group.getRealBookSort())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun gotoTop() {
|
|
||||||
fragmentMap[groupId]?.gotoTop()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun selectLastTab() {
|
|
||||||
val lastPosition = AppConfig.saveTabPosition
|
|
||||||
if (lastPosition in 0 until bookGroups.size) {
|
|
||||||
binding.viewPagerBookshelf.post {
|
|
||||||
binding.viewPagerBookshelf.setCurrentItem(lastPosition, false)
|
|
||||||
updateToolbarTitle(lastPosition)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
AppConfig.saveTabPosition = 0
|
|
||||||
updateToolbarTitle(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private inner class TabFragmentPageAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) {
|
|
||||||
override fun getItemCount(): Int = bookGroups.size
|
|
||||||
|
|
||||||
override fun createFragment(position: Int): Fragment {
|
|
||||||
val group = bookGroups[position]
|
|
||||||
val fragment = BooksFragment(position, group)
|
|
||||||
fragmentMap[group.groupId] = fragment
|
|
||||||
return fragment
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
@file:Suppress("DEPRECATION")
|
|
||||||
|
|
||||||
package io.legado.app.ui.main.bookshelf.books
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import androidx.appcompat.widget.SearchView
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
|
||||||
import com.google.android.material.tabs.TabLayout
|
|
||||||
import com.google.android.material.tabs.TabLayoutMediator
|
|
||||||
import io.legado.app.R
|
|
||||||
import io.legado.app.data.appDb
|
|
||||||
import io.legado.app.data.entities.Book
|
|
||||||
import io.legado.app.data.entities.BookGroup
|
|
||||||
import io.legado.app.databinding.FragmentBookshelf1Binding
|
|
||||||
import io.legado.app.databinding.FragmentBookshelf4Binding
|
|
||||||
import io.legado.app.help.config.AppConfig
|
|
||||||
import io.legado.app.ui.book.group.GroupEditDialog
|
|
||||||
import io.legado.app.ui.book.search.SearchActivity
|
|
||||||
import io.legado.app.ui.main.bookshelf.BaseBookshelfFragment
|
|
||||||
import io.legado.app.utils.showDialogFragment
|
|
||||||
import io.legado.app.utils.toastOnUi
|
|
||||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
|
||||||
import kotlin.collections.set
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 书架界面
|
|
||||||
*/
|
|
||||||
class BookshelfFragment4() : BaseBookshelfFragment(R.layout.fragment_bookshelf4),
|
|
||||||
TabLayout.OnTabSelectedListener,
|
|
||||||
SearchView.OnQueryTextListener {
|
|
||||||
|
|
||||||
constructor(position: Int) : this() {
|
|
||||||
val bundle = Bundle()
|
|
||||||
bundle.putInt("position", position)
|
|
||||||
arguments = bundle
|
|
||||||
}
|
|
||||||
|
|
||||||
private val binding by viewBinding(FragmentBookshelf4Binding::bind)
|
|
||||||
|
|
||||||
private val bookGroups = mutableListOf<BookGroup>()
|
|
||||||
private val fragmentMap = hashMapOf<Long, BooksFragment>()
|
|
||||||
override val groupId: Long get() = selectedGroup?.groupId ?: 0
|
|
||||||
|
|
||||||
private lateinit var adapter: TabFragmentPageAdapter
|
|
||||||
|
|
||||||
override val books: List<Book>
|
|
||||||
get() {
|
|
||||||
val fragment = fragmentMap[groupId]
|
|
||||||
return fragment?.getBooks() ?: emptyList()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
setSupportToolbar(binding.topBar)
|
|
||||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.S)
|
|
||||||
binding.titleBar.fitsSystemWindows = true
|
|
||||||
initView()
|
|
||||||
initBookGroupData()
|
|
||||||
}
|
|
||||||
|
|
||||||
private val selectedGroup: BookGroup?
|
|
||||||
get() = bookGroups.getOrNull(binding.viewPagerBookshelf.currentItem)
|
|
||||||
|
|
||||||
private fun initView() {
|
|
||||||
adapter = TabFragmentPageAdapter(this)
|
|
||||||
binding.viewPagerBookshelf.adapter = adapter
|
|
||||||
binding.viewPagerBookshelf.offscreenPageLimit = 3
|
|
||||||
|
|
||||||
TabLayoutMediator(binding.tabLayout, binding.viewPagerBookshelf) { tab, position ->
|
|
||||||
tab.text = bookGroups[position].groupName
|
|
||||||
}.attach()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onQueryTextSubmit(query: String?): Boolean {
|
|
||||||
SearchActivity.start(requireContext(), query)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onQueryTextChange(newText: String?): Boolean {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("NotifyDataSetChanged")
|
|
||||||
@Synchronized
|
|
||||||
override fun upGroup(data: List<BookGroup>) {
|
|
||||||
if (data.isEmpty()) {
|
|
||||||
appDb.bookGroupDao.enableGroup(BookGroup.IdAll)
|
|
||||||
} else {
|
|
||||||
if (data != bookGroups) {
|
|
||||||
bookGroups.clear()
|
|
||||||
bookGroups.addAll(data)
|
|
||||||
adapter.notifyDataSetChanged()
|
|
||||||
adapter = TabFragmentPageAdapter(this)
|
|
||||||
binding.viewPagerBookshelf.adapter = adapter
|
|
||||||
|
|
||||||
TabLayoutMediator(binding.tabLayout, binding.viewPagerBookshelf) { tab, position ->
|
|
||||||
tab.text = bookGroups[position].groupName
|
|
||||||
}.attach()
|
|
||||||
|
|
||||||
selectLastTab()
|
|
||||||
|
|
||||||
binding.tabLayout.post {
|
|
||||||
for (i in 0 until bookGroups.size) {
|
|
||||||
binding.tabLayout.getTabAt(i)?.view?.setOnLongClickListener {
|
|
||||||
showDialogFragment(GroupEditDialog(bookGroups[i]))
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun upSort() {
|
|
||||||
childFragmentManager.fragments.forEach {
|
|
||||||
if (it is BooksFragment) {
|
|
||||||
val position = it.position
|
|
||||||
val group = bookGroups.getOrNull(position) ?: return@forEach
|
|
||||||
it.setEnableRefresh(group.enableRefresh)
|
|
||||||
it.upBookSort(group.getRealBookSort())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun selectLastTab() {
|
|
||||||
binding.tabLayout.post {
|
|
||||||
binding.tabLayout.removeOnTabSelectedListener(this)
|
|
||||||
binding.tabLayout.getTabAt(AppConfig.saveTabPosition)?.select()
|
|
||||||
binding.tabLayout.addOnTabSelectedListener(this)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTabReselected(tab: TabLayout.Tab) {
|
|
||||||
selectedGroup?.let { group ->
|
|
||||||
fragmentMap[group.groupId]?.let {
|
|
||||||
toastOnUi("${group.groupName}(${it.getBooksCount()})")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTabUnselected(tab: TabLayout.Tab) = Unit
|
|
||||||
|
|
||||||
override fun onTabSelected(tab: TabLayout.Tab) {
|
|
||||||
AppConfig.saveTabPosition = tab.position
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun gotoTop() {
|
|
||||||
fragmentMap[groupId]?.gotoTop()
|
|
||||||
}
|
|
||||||
|
|
||||||
private inner class TabFragmentPageAdapter(fragment: Fragment) : FragmentStateAdapter(fragment) {
|
|
||||||
override fun getItemCount(): Int = bookGroups.size
|
|
||||||
|
|
||||||
override fun createFragment(position: Int): Fragment {
|
|
||||||
val group = bookGroups[position]
|
|
||||||
val fragment = BooksFragment(position, group)
|
|
||||||
fragmentMap[group.groupId] = fragment
|
|
||||||
return fragment
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-102
@@ -1,102 +0,0 @@
|
|||||||
package io.legado.app.ui.main.bookshelf.books.styleDefalut
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.view.View
|
|
||||||
import androidx.core.os.bundleOf
|
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
|
||||||
import androidx.viewbinding.ViewBinding
|
|
||||||
import io.legado.app.base.adapter.DiffRecyclerAdapter
|
|
||||||
import io.legado.app.base.adapter.ItemViewHolder
|
|
||||||
import io.legado.app.data.entities.Book
|
|
||||||
|
|
||||||
abstract class BaseBooksAdapter<VB : ViewBinding>(context: Context) :
|
|
||||||
DiffRecyclerAdapter<Book, VB>(context) {
|
|
||||||
|
|
||||||
init {
|
|
||||||
setHasStableIds(true)
|
|
||||||
}
|
|
||||||
override fun getItemId(position: Int): Long {
|
|
||||||
return getItems()[position].bookUrl.hashCode().toLong()
|
|
||||||
}
|
|
||||||
|
|
||||||
override val keepScrollPosition = true
|
|
||||||
|
|
||||||
override val diffItemCallback: DiffUtil.ItemCallback<Book> =
|
|
||||||
object : DiffUtil.ItemCallback<Book>() {
|
|
||||||
|
|
||||||
override fun areItemsTheSame(oldItem: Book, newItem: Book): Boolean {
|
|
||||||
return oldItem.bookUrl == newItem.bookUrl
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItem: Book, newItem: Book): Boolean {
|
|
||||||
return when {
|
|
||||||
oldItem.durChapterTime != newItem.durChapterTime -> false
|
|
||||||
oldItem.name != newItem.name -> false
|
|
||||||
oldItem.author != newItem.author -> false
|
|
||||||
oldItem.durChapterTitle != newItem.durChapterTitle -> false
|
|
||||||
oldItem.latestChapterTitle != newItem.latestChapterTitle -> false
|
|
||||||
oldItem.lastCheckCount != newItem.lastCheckCount -> false
|
|
||||||
oldItem.getDisplayCover() != newItem.getDisplayCover() -> false
|
|
||||||
oldItem.getUnreadChapterNum() != newItem.getUnreadChapterNum() -> false
|
|
||||||
else -> true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getChangePayload(oldItem: Book, newItem: Book): Any? {
|
|
||||||
val bundle = bundleOf()
|
|
||||||
if (oldItem.name != newItem.name) {
|
|
||||||
bundle.putString("name", newItem.name)
|
|
||||||
}
|
|
||||||
if (oldItem.author != newItem.author) {
|
|
||||||
bundle.putString("author", newItem.author)
|
|
||||||
}
|
|
||||||
if (oldItem.durChapterTitle != newItem.durChapterTitle) {
|
|
||||||
bundle.putString("dur", newItem.durChapterTitle)
|
|
||||||
}
|
|
||||||
if (oldItem.latestChapterTitle != newItem.latestChapterTitle) {
|
|
||||||
bundle.putString("last", newItem.latestChapterTitle)
|
|
||||||
}
|
|
||||||
if (oldItem.getDisplayCover() != newItem.getDisplayCover()) {
|
|
||||||
bundle.putString("cover", newItem.getDisplayCover())
|
|
||||||
}
|
|
||||||
if (oldItem.lastCheckCount != newItem.lastCheckCount
|
|
||||||
|| oldItem.durChapterTime != newItem.durChapterTime
|
|
||||||
|| oldItem.getUnreadChapterNum() != newItem.getUnreadChapterNum()
|
|
||||||
|| oldItem.lastCheckCount != newItem.lastCheckCount
|
|
||||||
) {
|
|
||||||
bundle.putBoolean("refresh", true)
|
|
||||||
}
|
|
||||||
if (oldItem.latestChapterTime != newItem.latestChapterTime) {
|
|
||||||
bundle.putBoolean("lastUpdateTime", true)
|
|
||||||
}
|
|
||||||
if (bundle.isEmpty) return null
|
|
||||||
return bundle
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewRecycled(holder: ItemViewHolder) {
|
|
||||||
super.onViewRecycled(holder)
|
|
||||||
holder.itemView.setOnClickListener(null)
|
|
||||||
holder.itemView.setOnLongClickListener(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun notification(bookUrl: String) {
|
|
||||||
getItems().forEachIndexed { i, it ->
|
|
||||||
if (it.bookUrl == bookUrl) {
|
|
||||||
notifyItemChanged(i, bundleOf("refresh" to true, "lastUpdateTime" to true))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun upLastUpdateTime() {
|
|
||||||
notifyItemRangeChanged(0, itemCount, bundleOf(Pair("lastUpdateTime", null)))
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CallBack {
|
|
||||||
fun open(book: Book, sharedView: View)
|
|
||||||
fun openBookInfo(book: Book, sharedView: View)
|
|
||||||
fun isUpdate(bookUrl: String): Boolean
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-93
@@ -1,93 +0,0 @@
|
|||||||
package io.legado.app.ui.main.bookshelf.books.styleDefalut
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import io.legado.app.base.adapter.ItemViewHolder
|
|
||||||
import io.legado.app.data.entities.Book
|
|
||||||
import io.legado.app.databinding.ItemBookshelfGridBinding
|
|
||||||
import io.legado.app.help.book.getBookTypeName
|
|
||||||
import io.legado.app.help.book.isLocal
|
|
||||||
import io.legado.app.help.config.AppConfig
|
|
||||||
import io.legado.app.utils.gone
|
|
||||||
import io.legado.app.utils.visible
|
|
||||||
|
|
||||||
class BooksAdapterGrid(context: Context, private val callBack: CallBack) :
|
|
||||||
BaseBooksAdapter<ItemBookshelfGridBinding>(context) {
|
|
||||||
|
|
||||||
override fun getViewBinding(parent: ViewGroup): ItemBookshelfGridBinding {
|
|
||||||
return ItemBookshelfGridBinding.inflate(inflater, parent, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun convert(
|
|
||||||
holder: ItemViewHolder,
|
|
||||||
binding: ItemBookshelfGridBinding,
|
|
||||||
item: Book,
|
|
||||||
payloads: MutableList<Any>
|
|
||||||
) = binding.run {
|
|
||||||
if (payloads.isEmpty()) {
|
|
||||||
cdCover.transitionName = "book_${item.bookUrl}"
|
|
||||||
tvName.text = item.name
|
|
||||||
ivCover.load(item.getDisplayCover(), item.name, item.author, false, item.origin)
|
|
||||||
upRefresh(binding, item)
|
|
||||||
} else {
|
|
||||||
for (i in payloads.indices) {
|
|
||||||
val bundle = payloads[i] as Bundle
|
|
||||||
bundle.keySet().forEach {
|
|
||||||
when (it) {
|
|
||||||
"name" -> tvName.text = item.name
|
|
||||||
"cover" -> ivCover.load(item.getDisplayCover(), item.name, item.author, false, item.origin)
|
|
||||||
"refresh" -> upRefresh(binding, item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun upRefresh(binding: ItemBookshelfGridBinding, item: Book) {
|
|
||||||
if (!item.isLocal && callBack.isUpdate(item.bookUrl)) {
|
|
||||||
binding.cdUnread.gone()
|
|
||||||
binding.cdTip.visible()
|
|
||||||
binding.rlLoading.visible()
|
|
||||||
} else {
|
|
||||||
binding.rlLoading.gone()
|
|
||||||
if (AppConfig.showUnread) {
|
|
||||||
val unreadCount = item.getUnreadChapterNum()
|
|
||||||
if (unreadCount > 0) {
|
|
||||||
binding.cdUnread.visible()
|
|
||||||
binding.tvUnread.text = unreadCount.toString()
|
|
||||||
if (AppConfig.showUnreadNew)
|
|
||||||
binding.newChapter.isVisible = item.lastCheckCount > 0
|
|
||||||
} else {
|
|
||||||
binding.cdUnread.gone()
|
|
||||||
binding.newChapter.gone()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
binding.cdUnread.gone()
|
|
||||||
}
|
|
||||||
if (AppConfig.showTip){
|
|
||||||
binding.cdTip.visible()
|
|
||||||
binding.tvTip.text = item.getBookTypeName()
|
|
||||||
} else {
|
|
||||||
binding.cdTip.gone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun registerListener(holder: ItemViewHolder, binding: ItemBookshelfGridBinding) {
|
|
||||||
|
|
||||||
binding.cvContent.setOnClickListener {
|
|
||||||
getItem(holder.layoutPosition)?.let {
|
|
||||||
callBack.open(it, binding.cdCover)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.cvContent.setOnLongClickListener {
|
|
||||||
getItem(holder.layoutPosition)?.let {
|
|
||||||
callBack.openBookInfo(it, binding.cdCover)
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-103
@@ -1,103 +0,0 @@
|
|||||||
package io.legado.app.ui.main.bookshelf.books.styleDefalut
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import io.legado.app.base.adapter.ItemViewHolder
|
|
||||||
import io.legado.app.data.entities.Book
|
|
||||||
import io.legado.app.databinding.ItemBookshelfGridCompactBinding
|
|
||||||
import io.legado.app.help.book.getBookTypeName
|
|
||||||
import io.legado.app.help.book.isLocal
|
|
||||||
import io.legado.app.help.config.AppConfig
|
|
||||||
import io.legado.app.utils.gone
|
|
||||||
import io.legado.app.utils.visible
|
|
||||||
|
|
||||||
class BooksAdapterGridCompact(context: Context, private val callBack: CallBack) :
|
|
||||||
BaseBooksAdapter<ItemBookshelfGridCompactBinding>(context) {
|
|
||||||
|
|
||||||
override fun getViewBinding(parent: ViewGroup): ItemBookshelfGridCompactBinding {
|
|
||||||
return ItemBookshelfGridCompactBinding.inflate(inflater, parent, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun convert(
|
|
||||||
holder: ItemViewHolder,
|
|
||||||
binding: ItemBookshelfGridCompactBinding,
|
|
||||||
item: Book,
|
|
||||||
payloads: MutableList<Any>
|
|
||||||
) = binding.run {
|
|
||||||
if (payloads.isEmpty()) {
|
|
||||||
cvContent.transitionName = "book_${item.bookUrl}"
|
|
||||||
tvName.text = item.name
|
|
||||||
ivCover.load(item.getDisplayCover(), item.name, item.author, false, item.origin)
|
|
||||||
upRefresh(binding, item)
|
|
||||||
} else {
|
|
||||||
for (i in payloads.indices) {
|
|
||||||
val bundle = payloads[i] as Bundle
|
|
||||||
bundle.keySet().forEach {
|
|
||||||
when (it) {
|
|
||||||
"name" -> tvName.text = item.name
|
|
||||||
"cover" -> ivCover.load(
|
|
||||||
item.getDisplayCover(),
|
|
||||||
item.name,
|
|
||||||
item.author,
|
|
||||||
false,
|
|
||||||
item.origin
|
|
||||||
)
|
|
||||||
|
|
||||||
"refresh" -> upRefresh(binding, item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun upRefresh(binding: ItemBookshelfGridCompactBinding, item: Book) {
|
|
||||||
if (!item.isLocal && callBack.isUpdate(item.bookUrl)) {
|
|
||||||
binding.cdUnread.visibility = View.GONE
|
|
||||||
binding.rlLoading.visibility = View.VISIBLE
|
|
||||||
} else {
|
|
||||||
binding.rlLoading.visibility = View.GONE
|
|
||||||
if (AppConfig.showUnread) {
|
|
||||||
val unreadCount = item.getUnreadChapterNum()
|
|
||||||
if (unreadCount > 0) {
|
|
||||||
binding.cdUnread.visibility = View.VISIBLE
|
|
||||||
binding.tvUnread.text = unreadCount.toString()
|
|
||||||
if (AppConfig.showUnreadNew)
|
|
||||||
binding.newChapter.isVisible = item.lastCheckCount > 0
|
|
||||||
} else {
|
|
||||||
binding.cdUnread.visibility = View.GONE
|
|
||||||
binding.newChapter.gone()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
binding.cdUnread.visibility = View.GONE
|
|
||||||
}
|
|
||||||
if (AppConfig.showTip){
|
|
||||||
binding.cdTip.visible()
|
|
||||||
binding.tvTip.text = item.getBookTypeName()
|
|
||||||
} else {
|
|
||||||
binding.cdTip.gone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun registerListener(
|
|
||||||
holder: ItemViewHolder,
|
|
||||||
binding: ItemBookshelfGridCompactBinding
|
|
||||||
) {
|
|
||||||
|
|
||||||
binding.cvContent.setOnClickListener {
|
|
||||||
getItem(holder.layoutPosition)?.let {
|
|
||||||
callBack.open(it, binding.cvContent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.cvContent.setOnLongClickListener {
|
|
||||||
getItem(holder.layoutPosition)?.let {
|
|
||||||
callBack.openBookInfo(it, binding.cvContent)
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-103
@@ -1,103 +0,0 @@
|
|||||||
package io.legado.app.ui.main.bookshelf.books.styleDefalut
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import io.legado.app.base.adapter.ItemViewHolder
|
|
||||||
import io.legado.app.data.entities.Book
|
|
||||||
import io.legado.app.databinding.ItemBookshelfGridCompactBinding
|
|
||||||
import io.legado.app.help.book.getBookTypeName
|
|
||||||
import io.legado.app.help.book.isLocal
|
|
||||||
import io.legado.app.help.config.AppConfig
|
|
||||||
import io.legado.app.utils.gone
|
|
||||||
import io.legado.app.utils.visible
|
|
||||||
|
|
||||||
class BooksAdapterGridCover(context: Context, private val callBack: CallBack) :
|
|
||||||
BaseBooksAdapter<ItemBookshelfGridCompactBinding>(context) {
|
|
||||||
|
|
||||||
override fun getViewBinding(parent: ViewGroup): ItemBookshelfGridCompactBinding {
|
|
||||||
return ItemBookshelfGridCompactBinding.inflate(inflater, parent, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun convert(
|
|
||||||
holder: ItemViewHolder,
|
|
||||||
binding: ItemBookshelfGridCompactBinding,
|
|
||||||
item: Book,
|
|
||||||
payloads: MutableList<Any>
|
|
||||||
) = binding.run {
|
|
||||||
if (payloads.isEmpty()) {
|
|
||||||
cvContent.transitionName = "book_${item.bookUrl}"
|
|
||||||
tvName.gone()
|
|
||||||
ivCover.load(item.getDisplayCover(), item.name, item.author, false, item.origin)
|
|
||||||
upRefresh(binding, item)
|
|
||||||
} else {
|
|
||||||
for (i in payloads.indices) {
|
|
||||||
val bundle = payloads[i] as Bundle
|
|
||||||
bundle.keySet().forEach {
|
|
||||||
when (it) {
|
|
||||||
"name" -> tvName.gone()
|
|
||||||
"cover" -> ivCover.load(
|
|
||||||
item.getDisplayCover(),
|
|
||||||
item.name,
|
|
||||||
item.author,
|
|
||||||
false,
|
|
||||||
item.origin
|
|
||||||
)
|
|
||||||
|
|
||||||
"refresh" -> upRefresh(binding, item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun upRefresh(binding: ItemBookshelfGridCompactBinding, item: Book) {
|
|
||||||
if (!item.isLocal && callBack.isUpdate(item.bookUrl)) {
|
|
||||||
binding.cdUnread.visibility = View.GONE
|
|
||||||
binding.rlLoading.visibility = View.VISIBLE
|
|
||||||
} else {
|
|
||||||
binding.rlLoading.visibility = View.GONE
|
|
||||||
if (AppConfig.showUnread) {
|
|
||||||
val unreadCount = item.getUnreadChapterNum()
|
|
||||||
if (unreadCount > 0) {
|
|
||||||
binding.cdUnread.visibility = View.VISIBLE
|
|
||||||
binding.tvUnread.text = unreadCount.toString()
|
|
||||||
if (AppConfig.showUnreadNew)
|
|
||||||
binding.newChapter.isVisible = item.lastCheckCount > 0
|
|
||||||
} else {
|
|
||||||
binding.cdUnread.visibility = View.GONE
|
|
||||||
binding.newChapter.gone()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
binding.cdUnread.visibility = View.GONE
|
|
||||||
}
|
|
||||||
if (AppConfig.showTip){
|
|
||||||
binding.cdTip.visible()
|
|
||||||
binding.tvTip.text = item.getBookTypeName()
|
|
||||||
} else {
|
|
||||||
binding.cdTip.gone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun registerListener(
|
|
||||||
holder: ItemViewHolder,
|
|
||||||
binding: ItemBookshelfGridCompactBinding
|
|
||||||
) {
|
|
||||||
|
|
||||||
binding.cvContent.setOnClickListener {
|
|
||||||
getItem(holder.layoutPosition)?.let {
|
|
||||||
callBack.open(it, binding.cvContent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.cvContent.setOnLongClickListener {
|
|
||||||
getItem(holder.layoutPosition)?.let {
|
|
||||||
callBack.openBookInfo(it, binding.cvContent)
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-129
@@ -1,129 +0,0 @@
|
|||||||
package io.legado.app.ui.main.bookshelf.books.styleDefalut
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.lifecycle.Lifecycle
|
|
||||||
import io.legado.app.base.adapter.ItemViewHolder
|
|
||||||
import io.legado.app.data.entities.Book
|
|
||||||
import io.legado.app.databinding.ItemBookshelfListBinding
|
|
||||||
import io.legado.app.help.book.getBookTypeName
|
|
||||||
import io.legado.app.help.book.isLocal
|
|
||||||
import io.legado.app.help.config.AppConfig
|
|
||||||
import io.legado.app.utils.gone
|
|
||||||
import io.legado.app.utils.toTimeAgo
|
|
||||||
import io.legado.app.utils.visible
|
|
||||||
import splitties.views.onLongClick
|
|
||||||
|
|
||||||
class BooksAdapterList(
|
|
||||||
context: Context,
|
|
||||||
private val fragment: Fragment,
|
|
||||||
private val callBack: CallBack,
|
|
||||||
private val lifecycle: Lifecycle
|
|
||||||
) : BaseBooksAdapter<ItemBookshelfListBinding>(context) {
|
|
||||||
|
|
||||||
override fun getViewBinding(parent: ViewGroup): ItemBookshelfListBinding {
|
|
||||||
return ItemBookshelfListBinding.inflate(inflater, parent, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun convert(
|
|
||||||
holder: ItemViewHolder,
|
|
||||||
binding: ItemBookshelfListBinding,
|
|
||||||
item: Book,
|
|
||||||
payloads: MutableList<Any>
|
|
||||||
) = binding.run {
|
|
||||||
if (payloads.isEmpty()) {
|
|
||||||
cdCover.transitionName = "book_${item.bookUrl}"
|
|
||||||
tvName.text = item.name
|
|
||||||
tvAuthor.text = item.author
|
|
||||||
tvRead.text = item.durChapterTitle
|
|
||||||
tvLast.text = item.latestChapterTitle
|
|
||||||
ivCover.load(item.getDisplayCover(), item.name, item.author, false, item.origin)
|
|
||||||
upRefresh(binding, item)
|
|
||||||
upLastUpdateTime(binding, item)
|
|
||||||
} else {
|
|
||||||
for (i in payloads.indices) {
|
|
||||||
val bundle = payloads[i] as Bundle
|
|
||||||
bundle.keySet().forEach {
|
|
||||||
when (it) {
|
|
||||||
"name" -> tvName.text = item.name
|
|
||||||
"author" -> tvAuthor.text = item.author
|
|
||||||
"dur" -> tvRead.text = item.durChapterTitle
|
|
||||||
"last" -> tvLast.text = item.latestChapterTitle
|
|
||||||
"cover" -> ivCover.load(
|
|
||||||
item.getDisplayCover(),
|
|
||||||
item.name,
|
|
||||||
item.author,
|
|
||||||
false,
|
|
||||||
item.origin,
|
|
||||||
fragment,
|
|
||||||
lifecycle
|
|
||||||
)
|
|
||||||
|
|
||||||
"refresh" -> upRefresh(binding, item)
|
|
||||||
"lastUpdateTime" -> upLastUpdateTime(binding, item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun upRefresh(binding: ItemBookshelfListBinding, item: Book) {
|
|
||||||
if (!item.isLocal && callBack.isUpdate(item.bookUrl)) {
|
|
||||||
binding.cdUnread.gone()
|
|
||||||
binding.rlLoading.visible()
|
|
||||||
} else {
|
|
||||||
binding.rlLoading.gone()
|
|
||||||
if (AppConfig.showUnread) {
|
|
||||||
val unreadCount = item.getUnreadChapterNum()
|
|
||||||
if (unreadCount > 0) {
|
|
||||||
binding.cdUnread.visible()
|
|
||||||
binding.tvUnread.text = unreadCount.toString()
|
|
||||||
if (AppConfig.showUnreadNew)
|
|
||||||
binding.newChapter.isVisible = item.lastCheckCount > 0
|
|
||||||
} else {
|
|
||||||
binding.cdUnread.gone()
|
|
||||||
binding.newChapter.gone()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
binding.cdUnread.gone()
|
|
||||||
}
|
|
||||||
if (AppConfig.showTip){
|
|
||||||
binding.cdTip.visible()
|
|
||||||
binding.tvTip.text = item.getBookTypeName()
|
|
||||||
} else {
|
|
||||||
binding.cdTip.gone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun upLastUpdateTime(binding: ItemBookshelfListBinding, item: Book) {
|
|
||||||
if (AppConfig.showLastUpdateTime && !item.isLocal) {
|
|
||||||
val time = item.latestChapterTime.toTimeAgo()
|
|
||||||
if (binding.tvLastUpdateTime.text != time) {
|
|
||||||
binding.tvLastUpdateTime.text = time
|
|
||||||
binding.tvLastUpdateTime.isVisible = true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
binding.tvLastUpdateTime.isVisible = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun registerListener(holder: ItemViewHolder, binding: ItemBookshelfListBinding) {
|
|
||||||
holder.itemView.apply {
|
|
||||||
binding.cvContent.setOnClickListener {
|
|
||||||
getItem(holder.layoutPosition)?.let {
|
|
||||||
callBack.open(it, binding.cdCover)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.cvContent.onLongClick {
|
|
||||||
getItem(holder.layoutPosition)?.let {
|
|
||||||
callBack.openBookInfo(it, binding.cdCover)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-133
@@ -1,133 +0,0 @@
|
|||||||
package io.legado.app.ui.main.bookshelf.books.styleDefalut
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import androidx.lifecycle.Lifecycle
|
|
||||||
import io.legado.app.R
|
|
||||||
import io.legado.app.base.adapter.ItemViewHolder
|
|
||||||
import io.legado.app.data.entities.Book
|
|
||||||
import io.legado.app.databinding.ItemBookshelfListCompactBinding
|
|
||||||
import io.legado.app.help.book.getBookTypeName
|
|
||||||
import io.legado.app.help.book.isLocal
|
|
||||||
import io.legado.app.help.config.AppConfig
|
|
||||||
import io.legado.app.utils.gone
|
|
||||||
import io.legado.app.utils.toTimeAgo
|
|
||||||
import io.legado.app.utils.visible
|
|
||||||
import splitties.views.onLongClick
|
|
||||||
|
|
||||||
class BooksAdapterListCompact(
|
|
||||||
context: Context,
|
|
||||||
private val fragment: Fragment,
|
|
||||||
private val callBack: CallBack,
|
|
||||||
private val lifecycle: Lifecycle
|
|
||||||
) : BaseBooksAdapter<ItemBookshelfListCompactBinding>(context) {
|
|
||||||
|
|
||||||
override fun getViewBinding(parent: ViewGroup): ItemBookshelfListCompactBinding {
|
|
||||||
return ItemBookshelfListCompactBinding.inflate(inflater, parent, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun convert(
|
|
||||||
holder: ItemViewHolder,
|
|
||||||
binding: ItemBookshelfListCompactBinding,
|
|
||||||
item: Book,
|
|
||||||
payloads: MutableList<Any>
|
|
||||||
) = binding.run {
|
|
||||||
if (payloads.isEmpty()) {
|
|
||||||
cdCover.transitionName = "book_${item.bookUrl}"
|
|
||||||
tvName.text = item.name
|
|
||||||
tvAuthor.text =
|
|
||||||
context.getString(
|
|
||||||
R.string.author_read,
|
|
||||||
item.author,
|
|
||||||
(item.totalChapterNum - item.durChapterIndex).coerceAtLeast(0)
|
|
||||||
)
|
|
||||||
tvLast.text = item.latestChapterTitle
|
|
||||||
ivCover.load(item.getDisplayCover(), item.name, item.author, false, item.origin)
|
|
||||||
upRefresh(binding, item)
|
|
||||||
upLastUpdateTime(binding, item)
|
|
||||||
} else {
|
|
||||||
for (i in payloads.indices) {
|
|
||||||
val bundle = payloads[i] as Bundle
|
|
||||||
bundle.keySet().forEach {
|
|
||||||
when (it) {
|
|
||||||
"name" -> tvName.text = item.name
|
|
||||||
"author" -> tvAuthor.text = item.author
|
|
||||||
"last" -> tvLast.text = item.latestChapterTitle
|
|
||||||
"cover" -> ivCover.load(
|
|
||||||
item.getDisplayCover(),
|
|
||||||
item.name,
|
|
||||||
item.author,
|
|
||||||
false,
|
|
||||||
item.origin,
|
|
||||||
fragment,
|
|
||||||
lifecycle
|
|
||||||
)
|
|
||||||
|
|
||||||
"refresh" -> upRefresh(binding, item)
|
|
||||||
"lastUpdateTime" -> upLastUpdateTime(binding, item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun upRefresh(binding: ItemBookshelfListCompactBinding, item: Book) {
|
|
||||||
if (!item.isLocal && callBack.isUpdate(item.bookUrl)) {
|
|
||||||
binding.cdUnread.gone()
|
|
||||||
binding.rlLoading.visible()
|
|
||||||
} else {
|
|
||||||
binding.rlLoading.gone()
|
|
||||||
if (AppConfig.showUnread) {
|
|
||||||
val unreadCount = item.getUnreadChapterNum()
|
|
||||||
if (unreadCount > 0) {
|
|
||||||
binding.cdUnread.visible()
|
|
||||||
binding.tvUnread.text = unreadCount.toString()
|
|
||||||
if (AppConfig.showUnreadNew)
|
|
||||||
binding.newChapter.isVisible = item.lastCheckCount > 0
|
|
||||||
} else {
|
|
||||||
binding.cdUnread.gone()
|
|
||||||
binding.newChapter.gone()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
binding.cdUnread.gone()
|
|
||||||
}
|
|
||||||
if (AppConfig.showTip){
|
|
||||||
binding.cdTip.visible()
|
|
||||||
binding.tvTip.text = item.getBookTypeName()
|
|
||||||
} else {
|
|
||||||
binding.cdTip.gone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun upLastUpdateTime(binding: ItemBookshelfListCompactBinding, item: Book) {
|
|
||||||
if (AppConfig.showLastUpdateTime && !item.isLocal) {
|
|
||||||
val time = item.latestChapterTime.toTimeAgo()
|
|
||||||
if (binding.tvLastUpdateTime.text != time) {
|
|
||||||
binding.tvLastUpdateTime.text = time
|
|
||||||
binding.tvLastUpdateTime.isVisible = true
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
binding.tvLastUpdateTime.isVisible = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun registerListener(holder: ItemViewHolder, binding: ItemBookshelfListCompactBinding) {
|
|
||||||
holder.itemView.apply {
|
|
||||||
binding.cvContent.setOnClickListener {
|
|
||||||
getItem(holder.layoutPosition)?.let {
|
|
||||||
callBack.open(it, binding.cdCover)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.cvContent.onLongClick {
|
|
||||||
getItem(holder.layoutPosition)?.let {
|
|
||||||
callBack.openBookInfo(it, binding.cdCover)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-146
@@ -1,146 +0,0 @@
|
|||||||
package io.legado.app.ui.main.bookshelf.books.styleFold
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import androidx.core.os.bundleOf
|
|
||||||
import androidx.recyclerview.widget.AsyncListDiffer
|
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import io.legado.app.data.entities.Book
|
|
||||||
import io.legado.app.data.entities.BookGroup
|
|
||||||
|
|
||||||
abstract class BaseBooksAdapter<VH : RecyclerView.ViewHolder>(
|
|
||||||
val context: Context,
|
|
||||||
val callBack: CallBack
|
|
||||||
) : RecyclerView.Adapter<VH>() {
|
|
||||||
|
|
||||||
protected val inflater: LayoutInflater = LayoutInflater.from(context)
|
|
||||||
|
|
||||||
private val diffItemCallback = object : DiffUtil.ItemCallback<Any>() {
|
|
||||||
|
|
||||||
override fun areItemsTheSame(oldItem: Any, newItem: Any): Boolean {
|
|
||||||
return when {
|
|
||||||
oldItem is Book && newItem is Book -> {
|
|
||||||
oldItem.bookUrl == newItem.bookUrl
|
|
||||||
}
|
|
||||||
|
|
||||||
oldItem is BookGroup && newItem is BookGroup -> {
|
|
||||||
oldItem.groupId == newItem.groupId
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItem: Any, newItem: Any): Boolean {
|
|
||||||
return when {
|
|
||||||
oldItem is Book && newItem is Book -> {
|
|
||||||
oldItem.durChapterTime == newItem.durChapterTime &&
|
|
||||||
oldItem.name == newItem.name &&
|
|
||||||
oldItem.author == newItem.author &&
|
|
||||||
oldItem.durChapterTitle == newItem.durChapterTitle &&
|
|
||||||
oldItem.latestChapterTitle == newItem.latestChapterTitle &&
|
|
||||||
oldItem.lastCheckCount == newItem.lastCheckCount &&
|
|
||||||
oldItem.getDisplayCover() == newItem.getDisplayCover() &&
|
|
||||||
oldItem.getUnreadChapterNum() == newItem.getUnreadChapterNum()
|
|
||||||
}
|
|
||||||
|
|
||||||
oldItem is BookGroup && newItem is BookGroup -> false
|
|
||||||
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getChangePayload(oldItem: Any, newItem: Any): Any? {
|
|
||||||
val bundle = bundleOf()
|
|
||||||
when {
|
|
||||||
oldItem is Book && newItem is Book -> {
|
|
||||||
if (oldItem.name != newItem.name) {
|
|
||||||
bundle.putString("name", newItem.name)
|
|
||||||
}
|
|
||||||
if (oldItem.author != newItem.author) {
|
|
||||||
bundle.putString("author", newItem.author)
|
|
||||||
}
|
|
||||||
if (oldItem.durChapterTitle != newItem.durChapterTitle) {
|
|
||||||
bundle.putString("dur", newItem.durChapterTitle)
|
|
||||||
}
|
|
||||||
if (oldItem.latestChapterTitle != newItem.latestChapterTitle) {
|
|
||||||
bundle.putString("last", newItem.latestChapterTitle)
|
|
||||||
}
|
|
||||||
if (oldItem.getDisplayCover() != newItem.getDisplayCover()) {
|
|
||||||
bundle.putString("cover", newItem.getDisplayCover())
|
|
||||||
}
|
|
||||||
if (oldItem.lastCheckCount != newItem.lastCheckCount
|
|
||||||
|| oldItem.durChapterTime != newItem.durChapterTime
|
|
||||||
|| oldItem.getUnreadChapterNum() != newItem.getUnreadChapterNum()
|
|
||||||
|| oldItem.lastCheckCount != newItem.lastCheckCount
|
|
||||||
) {
|
|
||||||
bundle.putBoolean("refresh", true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
oldItem is BookGroup && newItem is BookGroup -> {
|
|
||||||
if (oldItem.groupName != newItem.groupName) {
|
|
||||||
bundle.putString("groupName", newItem.groupName)
|
|
||||||
}
|
|
||||||
if (oldItem.cover != newItem.cover) {
|
|
||||||
bundle.putString("cover", newItem.cover)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (bundle.isEmpty) return null
|
|
||||||
return bundle
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val asyncListDiffer by lazy {
|
|
||||||
AsyncListDiffer(this, diffItemCallback)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateItems() {
|
|
||||||
asyncListDiffer.submitList(callBack.getItems())
|
|
||||||
}
|
|
||||||
|
|
||||||
fun notification(bookUrl: String) {
|
|
||||||
for (i in 0 until itemCount) {
|
|
||||||
getItem(i).let {
|
|
||||||
if (it is Book && it.bookUrl == bookUrl) {
|
|
||||||
notifyItemChanged(i, bundleOf(Pair("refresh", null)))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getItems() = asyncListDiffer.currentList
|
|
||||||
|
|
||||||
fun getItem(position: Int) = getItems().getOrNull(position)
|
|
||||||
|
|
||||||
internal var allBooks: List<Book> = emptyList()
|
|
||||||
|
|
||||||
fun setAllBooks(all: List<Book>) {
|
|
||||||
this.allBooks = all
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
|
||||||
return getItems().size
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemViewType(position: Int): Int {
|
|
||||||
if (getItem(position) is BookGroup) {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
final override fun onBindViewHolder(holder: VH, position: Int) {}
|
|
||||||
|
|
||||||
|
|
||||||
interface CallBack {
|
|
||||||
fun onItemClick(item: Any, sharedView: View)
|
|
||||||
fun onItemLongClick(item: Any, sharedView: View)
|
|
||||||
fun isUpdate(bookUrl: String): Boolean
|
|
||||||
fun getItems(): List<Any>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-195
@@ -1,195 +0,0 @@
|
|||||||
package io.legado.app.ui.main.bookshelf.books.styleFold
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import io.legado.app.R
|
|
||||||
import io.legado.app.data.entities.Book
|
|
||||||
import io.legado.app.data.entities.BookGroup
|
|
||||||
import io.legado.app.databinding.ItemBookshelfGridBinding
|
|
||||||
import io.legado.app.databinding.ItemBookshelfGridGroupBinding
|
|
||||||
import io.legado.app.help.book.getBookTypeName
|
|
||||||
import io.legado.app.help.book.isLocal
|
|
||||||
import io.legado.app.help.config.AppConfig
|
|
||||||
import io.legado.app.utils.gone
|
|
||||||
import io.legado.app.utils.visible
|
|
||||||
import splitties.views.onLongClick
|
|
||||||
|
|
||||||
@Suppress("UNUSED_PARAMETER")
|
|
||||||
class BooksAdapterGrid(context: Context, callBack: CallBack) :
|
|
||||||
BaseBooksAdapter<RecyclerView.ViewHolder>(context, callBack) {
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(
|
|
||||||
parent: ViewGroup,
|
|
||||||
viewType: Int
|
|
||||||
): RecyclerView.ViewHolder {
|
|
||||||
return when (viewType) {
|
|
||||||
1 -> GroupViewHolder(ItemBookshelfGridGroupBinding.inflate(inflater, parent, false))
|
|
||||||
else -> BookViewHolder(ItemBookshelfGridBinding.inflate(inflater, parent, false))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(
|
|
||||||
holder: RecyclerView.ViewHolder,
|
|
||||||
position: Int,
|
|
||||||
payloads: MutableList<Any>
|
|
||||||
) {
|
|
||||||
|
|
||||||
when (holder) {
|
|
||||||
is BookViewHolder -> (getItem(position) as? Book)?.let {
|
|
||||||
holder.registerListener(it)
|
|
||||||
holder.onBind(it, payloads)
|
|
||||||
}
|
|
||||||
|
|
||||||
is GroupViewHolder -> (getItem(position) as? BookGroup)?.let {
|
|
||||||
holder.registerListener(it)
|
|
||||||
holder.onBind(it, payloads)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getBooksForGroup(group: BookGroup): List<Book> {
|
|
||||||
return if (group.groupId == -1L) {
|
|
||||||
allBooks.take(4)
|
|
||||||
} else {
|
|
||||||
allBooks.filter { (it.group and group.groupId) != 0L }.take(4)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class BookViewHolder(val binding: ItemBookshelfGridBinding) :
|
|
||||||
RecyclerView.ViewHolder(binding.root) {
|
|
||||||
|
|
||||||
fun onBind(item: Book) = binding.run {
|
|
||||||
cvContent.transitionName = "book_${item.bookUrl}"
|
|
||||||
tvName.text = item.name
|
|
||||||
ivCover.load(item.getDisplayCover(), item.name, item.author, false, item.origin)
|
|
||||||
upRefresh(this, item)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onBind(item: Book, payloads: MutableList<Any>) = binding.run {
|
|
||||||
if (payloads.isEmpty()) {
|
|
||||||
onBind(item)
|
|
||||||
} else {
|
|
||||||
for (i in payloads.indices) {
|
|
||||||
val bundle = payloads[i] as Bundle
|
|
||||||
bundle.keySet().forEach {
|
|
||||||
when (it) {
|
|
||||||
"name" -> tvName.text = item.name
|
|
||||||
"cover" -> ivCover.load(
|
|
||||||
item.getDisplayCover(),
|
|
||||||
item.name,
|
|
||||||
item.author,
|
|
||||||
false,
|
|
||||||
item.origin
|
|
||||||
)
|
|
||||||
|
|
||||||
"refresh" -> upRefresh(this, item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun registerListener(item: Any) {
|
|
||||||
binding.cvContent.setOnClickListener {
|
|
||||||
callBack.onItemClick(item, binding.cvContent)
|
|
||||||
}
|
|
||||||
binding.cvContent.onLongClick {
|
|
||||||
callBack.onItemLongClick(item, binding.cvContent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun upRefresh(binding: ItemBookshelfGridBinding, item: Book) {
|
|
||||||
if (!item.isLocal && callBack.isUpdate(item.bookUrl)) {
|
|
||||||
binding.cdUnread.visibility = View.GONE
|
|
||||||
binding.rlLoading.visibility = View.VISIBLE
|
|
||||||
} else {
|
|
||||||
binding.rlLoading.visibility = View.GONE
|
|
||||||
if (AppConfig.showUnread) {
|
|
||||||
val unreadCount = item.getUnreadChapterNum()
|
|
||||||
if (unreadCount > 0) {
|
|
||||||
binding.cdUnread.visibility = View.VISIBLE
|
|
||||||
binding.tvUnread.text = unreadCount.toString()
|
|
||||||
if (AppConfig.showUnreadNew)
|
|
||||||
binding.newChapter.isVisible = item.lastCheckCount > 0
|
|
||||||
} else {
|
|
||||||
binding.cdUnread.visibility = View.GONE
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
binding.cdUnread.visibility = View.GONE
|
|
||||||
}
|
|
||||||
if (AppConfig.showTip){
|
|
||||||
binding.cdTip.visible()
|
|
||||||
binding.tvTip.text = item.getBookTypeName()
|
|
||||||
} else {
|
|
||||||
binding.cdTip.gone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class GroupViewHolder(val binding: ItemBookshelfGridGroupBinding) :
|
|
||||||
RecyclerView.ViewHolder(binding.root) {
|
|
||||||
|
|
||||||
fun onBind(item: BookGroup) = binding.run {
|
|
||||||
tvName.text = item.groupName
|
|
||||||
loadGroupCover(item)
|
|
||||||
cdUnread.gone()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onBind(item: BookGroup, payloads: MutableList<Any>) = binding.run {
|
|
||||||
if (payloads.isEmpty()) {
|
|
||||||
onBind(item)
|
|
||||||
} else {
|
|
||||||
for (i in payloads.indices) {
|
|
||||||
val bundle = payloads[i] as Bundle
|
|
||||||
bundle.keySet().forEach {
|
|
||||||
when (it) {
|
|
||||||
"groupName" -> tvName.text = item.groupName
|
|
||||||
"cover" -> loadGroupCover(item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun registerListener(item: Any) {
|
|
||||||
binding.cvContent.setOnClickListener {
|
|
||||||
callBack.onItemClick(item, binding.cdCover)
|
|
||||||
}
|
|
||||||
binding.cvContent.onLongClick {
|
|
||||||
callBack.onItemLongClick(item, binding.cdCover)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun loadGroupCover(item: BookGroup) = binding.run {
|
|
||||||
|
|
||||||
binding.ivCover.setImageDrawable(
|
|
||||||
ContextCompat.getDrawable(binding.root.context, R.drawable.bg_surface_variant)
|
|
||||||
)
|
|
||||||
binding.ivCover1.setImageDrawable(null)
|
|
||||||
binding.ivCover2.setImageDrawable(null)
|
|
||||||
binding.ivCover3.setImageDrawable(null)
|
|
||||||
binding.ivCover4.setImageDrawable(null)
|
|
||||||
if (!item.cover.isNullOrEmpty()) {
|
|
||||||
ivCover.load(item.cover)
|
|
||||||
} else {
|
|
||||||
|
|
||||||
val books = getBooksForGroup(item)
|
|
||||||
|
|
||||||
if (books.size > 0) binding.ivCover1.load(books[0].getDisplayCover())
|
|
||||||
if (books.size > 1) binding.ivCover2.load(books[1].getDisplayCover())
|
|
||||||
if (books.size > 2) binding.ivCover3.load(books[2].getDisplayCover())
|
|
||||||
if (books.size > 3) binding.ivCover4.load(books[3].getDisplayCover())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
-202
@@ -1,202 +0,0 @@
|
|||||||
package io.legado.app.ui.main.bookshelf.books.styleFold
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import io.legado.app.R
|
|
||||||
import io.legado.app.data.entities.Book
|
|
||||||
import io.legado.app.data.entities.BookGroup
|
|
||||||
import io.legado.app.databinding.ItemBookshelfGridCompactBinding
|
|
||||||
import io.legado.app.databinding.ItemBookshelfGridCompactGroupBinding
|
|
||||||
import io.legado.app.help.book.getBookTypeName
|
|
||||||
import io.legado.app.help.book.isLocal
|
|
||||||
import io.legado.app.help.config.AppConfig
|
|
||||||
import io.legado.app.utils.gone
|
|
||||||
import io.legado.app.utils.visible
|
|
||||||
import splitties.views.onLongClick
|
|
||||||
|
|
||||||
@Suppress("UNUSED_PARAMETER")
|
|
||||||
class BooksAdapterGridCompact(context: Context, callBack: CallBack) :
|
|
||||||
BaseBooksAdapter<RecyclerView.ViewHolder>(context, callBack) {
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(
|
|
||||||
parent: ViewGroup,
|
|
||||||
viewType: Int
|
|
||||||
): RecyclerView.ViewHolder {
|
|
||||||
return when (viewType) {
|
|
||||||
1 -> GroupViewHolder(
|
|
||||||
ItemBookshelfGridCompactGroupBinding.inflate(
|
|
||||||
inflater,
|
|
||||||
parent,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
else -> BookViewHolder(ItemBookshelfGridCompactBinding.inflate(inflater, parent, false))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(
|
|
||||||
holder: RecyclerView.ViewHolder,
|
|
||||||
position: Int,
|
|
||||||
payloads: MutableList<Any>
|
|
||||||
) {
|
|
||||||
|
|
||||||
when (holder) {
|
|
||||||
is BookViewHolder -> (getItem(position) as? Book)?.let {
|
|
||||||
holder.registerListener(it)
|
|
||||||
holder.onBind(it, payloads)
|
|
||||||
}
|
|
||||||
|
|
||||||
is GroupViewHolder -> (getItem(position) as? BookGroup)?.let {
|
|
||||||
holder.registerListener(it)
|
|
||||||
holder.onBind(it, payloads)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getBooksForGroup(group: BookGroup): List<Book> {
|
|
||||||
return if (group.groupId == -1L) {
|
|
||||||
allBooks.take(4)
|
|
||||||
} else {
|
|
||||||
allBooks.filter { (it.group and group.groupId) != 0L }.take(4)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class BookViewHolder(val binding: ItemBookshelfGridCompactBinding) :
|
|
||||||
RecyclerView.ViewHolder(binding.root) {
|
|
||||||
|
|
||||||
fun onBind(item: Book) = binding.run {
|
|
||||||
cvContent.transitionName = "book_${item.bookUrl}"
|
|
||||||
tvName.text = item.name
|
|
||||||
ivCover.load(item.getDisplayCover(), item.name, item.author, false, item.origin)
|
|
||||||
upRefresh(this, item)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onBind(item: Book, payloads: MutableList<Any>) = binding.run {
|
|
||||||
if (payloads.isEmpty()) {
|
|
||||||
onBind(item)
|
|
||||||
} else {
|
|
||||||
for (i in payloads.indices) {
|
|
||||||
val bundle = payloads[i] as Bundle
|
|
||||||
bundle.keySet().forEach {
|
|
||||||
when (it) {
|
|
||||||
"name" -> tvName.text = item.name
|
|
||||||
"cover" -> ivCover.load(
|
|
||||||
item.getDisplayCover(),
|
|
||||||
item.name,
|
|
||||||
item.author,
|
|
||||||
false,
|
|
||||||
item.origin
|
|
||||||
)
|
|
||||||
|
|
||||||
"refresh" -> upRefresh(this, item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun registerListener(item: Any) {
|
|
||||||
binding.cvContent.setOnClickListener {
|
|
||||||
callBack.onItemClick(item, binding.cvContent)
|
|
||||||
}
|
|
||||||
binding.cvContent.onLongClick {
|
|
||||||
callBack.onItemLongClick(item, binding.cvContent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun upRefresh(binding: ItemBookshelfGridCompactBinding, item: Book) {
|
|
||||||
if (!item.isLocal && callBack.isUpdate(item.bookUrl)) {
|
|
||||||
binding.cdUnread.visibility = View.GONE
|
|
||||||
binding.rlLoading.visibility = View.VISIBLE
|
|
||||||
} else {
|
|
||||||
binding.rlLoading.visibility = View.GONE
|
|
||||||
if (AppConfig.showUnread) {
|
|
||||||
val unreadCount = item.getUnreadChapterNum()
|
|
||||||
if (unreadCount > 0) {
|
|
||||||
binding.cdUnread.visibility = View.VISIBLE
|
|
||||||
binding.tvUnread.text = unreadCount.toString()
|
|
||||||
if (AppConfig.showUnreadNew)
|
|
||||||
binding.newChapter.isVisible = item.lastCheckCount > 0
|
|
||||||
} else {
|
|
||||||
binding.cdUnread.visibility = View.GONE
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
binding.cdUnread.visibility = View.GONE
|
|
||||||
}
|
|
||||||
if (AppConfig.showTip){
|
|
||||||
binding.cdTip.visible()
|
|
||||||
binding.tvTip.text = item.getBookTypeName()
|
|
||||||
} else {
|
|
||||||
binding.cdTip.gone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class GroupViewHolder(val binding: ItemBookshelfGridCompactGroupBinding) :
|
|
||||||
RecyclerView.ViewHolder(binding.root) {
|
|
||||||
|
|
||||||
fun onBind(item: BookGroup) = binding.run {
|
|
||||||
tvName.text = item.groupName
|
|
||||||
loadGroupCover(item)
|
|
||||||
cdUnread.gone()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onBind(item: BookGroup, payloads: MutableList<Any>) = binding.run {
|
|
||||||
if (payloads.isEmpty()) {
|
|
||||||
onBind(item)
|
|
||||||
} else {
|
|
||||||
for (i in payloads.indices) {
|
|
||||||
val bundle = payloads[i] as Bundle
|
|
||||||
bundle.keySet().forEach {
|
|
||||||
when (it) {
|
|
||||||
"groupName" -> tvName.text = item.groupName
|
|
||||||
"cover" -> loadGroupCover(item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun registerListener(item: Any) {
|
|
||||||
binding.cvContent.setOnClickListener {
|
|
||||||
callBack.onItemClick(item, binding.cvContent)
|
|
||||||
}
|
|
||||||
binding.cvContent.onLongClick {
|
|
||||||
callBack.onItemLongClick(item, binding.cvContent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun loadGroupCover(item: BookGroup) = binding.run {
|
|
||||||
|
|
||||||
binding.ivCover.setImageDrawable(
|
|
||||||
ContextCompat.getDrawable(binding.root.context, R.drawable.bg_surface_variant)
|
|
||||||
)
|
|
||||||
binding.ivCover1.setImageDrawable(null)
|
|
||||||
binding.ivCover2.setImageDrawable(null)
|
|
||||||
binding.ivCover3.setImageDrawable(null)
|
|
||||||
binding.ivCover4.setImageDrawable(null)
|
|
||||||
if (!item.cover.isNullOrEmpty()) {
|
|
||||||
ivCover.load(item.cover)
|
|
||||||
} else {
|
|
||||||
|
|
||||||
val books = getBooksForGroup(item)
|
|
||||||
|
|
||||||
if (books.size > 0) binding.ivCover1.load(books[0].getDisplayCover())
|
|
||||||
if (books.size > 1) binding.ivCover2.load(books[1].getDisplayCover())
|
|
||||||
if (books.size > 2) binding.ivCover3.load(books[2].getDisplayCover())
|
|
||||||
if (books.size > 3) binding.ivCover4.load(books[3].getDisplayCover())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
-202
@@ -1,202 +0,0 @@
|
|||||||
package io.legado.app.ui.main.bookshelf.books.styleFold
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import io.legado.app.R
|
|
||||||
import io.legado.app.data.entities.Book
|
|
||||||
import io.legado.app.data.entities.BookGroup
|
|
||||||
import io.legado.app.databinding.ItemBookshelfGridCompactBinding
|
|
||||||
import io.legado.app.databinding.ItemBookshelfGridCompactGroupBinding
|
|
||||||
import io.legado.app.help.book.getBookTypeName
|
|
||||||
import io.legado.app.help.book.isLocal
|
|
||||||
import io.legado.app.help.config.AppConfig
|
|
||||||
import io.legado.app.utils.gone
|
|
||||||
import io.legado.app.utils.visible
|
|
||||||
import splitties.views.onLongClick
|
|
||||||
|
|
||||||
@Suppress("UNUSED_PARAMETER")
|
|
||||||
class BooksAdapterGridCover(context: Context, callBack: CallBack) :
|
|
||||||
BaseBooksAdapter<RecyclerView.ViewHolder>(context, callBack) {
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(
|
|
||||||
parent: ViewGroup,
|
|
||||||
viewType: Int
|
|
||||||
): RecyclerView.ViewHolder {
|
|
||||||
return when (viewType) {
|
|
||||||
1 -> GroupViewHolder(
|
|
||||||
ItemBookshelfGridCompactGroupBinding.inflate(
|
|
||||||
inflater,
|
|
||||||
parent,
|
|
||||||
false
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
else -> BookViewHolder(ItemBookshelfGridCompactBinding.inflate(inflater, parent, false))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(
|
|
||||||
holder: RecyclerView.ViewHolder,
|
|
||||||
position: Int,
|
|
||||||
payloads: MutableList<Any>
|
|
||||||
) {
|
|
||||||
|
|
||||||
when (holder) {
|
|
||||||
is BookViewHolder -> (getItem(position) as? Book)?.let {
|
|
||||||
holder.registerListener(it)
|
|
||||||
holder.onBind(it, payloads)
|
|
||||||
}
|
|
||||||
|
|
||||||
is GroupViewHolder -> (getItem(position) as? BookGroup)?.let {
|
|
||||||
holder.registerListener(it)
|
|
||||||
holder.onBind(it, payloads)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getBooksForGroup(group: BookGroup): List<Book> {
|
|
||||||
return if (group.groupId == -1L) {
|
|
||||||
allBooks.take(4)
|
|
||||||
} else {
|
|
||||||
allBooks.filter { (it.group and group.groupId) != 0L }.take(4)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class BookViewHolder(val binding: ItemBookshelfGridCompactBinding) :
|
|
||||||
RecyclerView.ViewHolder(binding.root) {
|
|
||||||
|
|
||||||
fun onBind(item: Book) = binding.run {
|
|
||||||
cvContent.transitionName = "book_${item.bookUrl}"
|
|
||||||
tvName.gone()
|
|
||||||
ivCover.load(item.getDisplayCover(), item.name, item.author, false, item.origin)
|
|
||||||
upRefresh(this, item)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onBind(item: Book, payloads: MutableList<Any>) = binding.run {
|
|
||||||
if (payloads.isEmpty()) {
|
|
||||||
onBind(item)
|
|
||||||
} else {
|
|
||||||
for (i in payloads.indices) {
|
|
||||||
val bundle = payloads[i] as Bundle
|
|
||||||
bundle.keySet().forEach {
|
|
||||||
when (it) {
|
|
||||||
"name" -> tvName.gone()
|
|
||||||
"cover" -> ivCover.load(
|
|
||||||
item.getDisplayCover(),
|
|
||||||
item.name,
|
|
||||||
item.author,
|
|
||||||
false,
|
|
||||||
item.origin
|
|
||||||
)
|
|
||||||
|
|
||||||
"refresh" -> upRefresh(this, item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun registerListener(item: Any) {
|
|
||||||
binding.cvContent.setOnClickListener {
|
|
||||||
callBack.onItemClick(item, binding.cvContent)
|
|
||||||
}
|
|
||||||
binding.cvContent.onLongClick {
|
|
||||||
callBack.onItemLongClick(item, binding.cvContent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun upRefresh(binding: ItemBookshelfGridCompactBinding, item: Book) {
|
|
||||||
if (!item.isLocal && callBack.isUpdate(item.bookUrl)) {
|
|
||||||
binding.cdUnread.visibility = View.GONE
|
|
||||||
binding.rlLoading.visibility = View.VISIBLE
|
|
||||||
} else {
|
|
||||||
binding.rlLoading.visibility = View.GONE
|
|
||||||
if (AppConfig.showUnread) {
|
|
||||||
val unreadCount = item.getUnreadChapterNum()
|
|
||||||
if (unreadCount > 0) {
|
|
||||||
binding.cdUnread.visibility = View.VISIBLE
|
|
||||||
binding.tvUnread.text = unreadCount.toString()
|
|
||||||
if (AppConfig.showUnreadNew)
|
|
||||||
binding.newChapter.isVisible = item.lastCheckCount > 0
|
|
||||||
} else {
|
|
||||||
binding.cdUnread.visibility = View.GONE
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
binding.cdUnread.visibility = View.GONE
|
|
||||||
}
|
|
||||||
if (AppConfig.showTip){
|
|
||||||
binding.cdTip.visible()
|
|
||||||
binding.tvTip.text = item.getBookTypeName()
|
|
||||||
} else {
|
|
||||||
binding.cdTip.gone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class GroupViewHolder(val binding: ItemBookshelfGridCompactGroupBinding) :
|
|
||||||
RecyclerView.ViewHolder(binding.root) {
|
|
||||||
|
|
||||||
fun onBind(item: BookGroup) = binding.run {
|
|
||||||
tvName.gone()
|
|
||||||
loadGroupCover(item)
|
|
||||||
cdUnread.gone()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onBind(item: BookGroup, payloads: MutableList<Any>) = binding.run {
|
|
||||||
if (payloads.isEmpty()) {
|
|
||||||
onBind(item)
|
|
||||||
} else {
|
|
||||||
for (i in payloads.indices) {
|
|
||||||
val bundle = payloads[i] as Bundle
|
|
||||||
bundle.keySet().forEach {
|
|
||||||
when (it) {
|
|
||||||
"groupName" -> tvName.gone()
|
|
||||||
"cover" -> loadGroupCover(item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun registerListener(item: Any) {
|
|
||||||
binding.cvContent.setOnClickListener {
|
|
||||||
callBack.onItemClick(item, binding.cvContent)
|
|
||||||
}
|
|
||||||
binding.cvContent.onLongClick {
|
|
||||||
callBack.onItemLongClick(item, binding.cvContent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun loadGroupCover(item: BookGroup) = binding.run {
|
|
||||||
|
|
||||||
binding.ivCover.setImageDrawable(
|
|
||||||
ContextCompat.getDrawable(binding.root.context, R.drawable.bg_surface_variant)
|
|
||||||
)
|
|
||||||
binding.ivCover1.setImageDrawable(null)
|
|
||||||
binding.ivCover2.setImageDrawable(null)
|
|
||||||
binding.ivCover3.setImageDrawable(null)
|
|
||||||
binding.ivCover4.setImageDrawable(null)
|
|
||||||
if (!item.cover.isNullOrEmpty()) {
|
|
||||||
ivCover.load(item.cover)
|
|
||||||
} else {
|
|
||||||
|
|
||||||
val books = getBooksForGroup(item)
|
|
||||||
|
|
||||||
if (books.size > 0) binding.ivCover1.load(books[0].getDisplayCover())
|
|
||||||
if (books.size > 1) binding.ivCover2.load(books[1].getDisplayCover())
|
|
||||||
if (books.size > 2) binding.ivCover3.load(books[2].getDisplayCover())
|
|
||||||
if (books.size > 3) binding.ivCover4.load(books[3].getDisplayCover())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
-215
@@ -1,215 +0,0 @@
|
|||||||
package io.legado.app.ui.main.bookshelf.books.styleFold
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import io.legado.app.R
|
|
||||||
import io.legado.app.data.entities.Book
|
|
||||||
import io.legado.app.data.entities.BookGroup
|
|
||||||
import io.legado.app.databinding.ItemBookshelfListBinding
|
|
||||||
import io.legado.app.databinding.ItemBookshelfListGroupBinding
|
|
||||||
import io.legado.app.help.book.getBookTypeName
|
|
||||||
import io.legado.app.help.book.isLocal
|
|
||||||
import io.legado.app.help.config.AppConfig
|
|
||||||
import io.legado.app.utils.gone
|
|
||||||
import io.legado.app.utils.toTimeAgo
|
|
||||||
import io.legado.app.utils.visible
|
|
||||||
import splitties.views.onLongClick
|
|
||||||
|
|
||||||
@Suppress("UNUSED_PARAMETER")
|
|
||||||
class BooksAdapterList(context: Context, callBack: CallBack) :
|
|
||||||
BaseBooksAdapter<RecyclerView.ViewHolder>(context, callBack) {
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
|
||||||
return when (viewType) {
|
|
||||||
1 -> GroupViewHolder(ItemBookshelfListGroupBinding.inflate(inflater, parent, false))
|
|
||||||
else -> BookViewHolder(ItemBookshelfListBinding.inflate(inflater, parent, false))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(
|
|
||||||
holder: RecyclerView.ViewHolder,
|
|
||||||
position: Int,
|
|
||||||
payloads: MutableList<Any>
|
|
||||||
) {
|
|
||||||
when (holder) {
|
|
||||||
is BookViewHolder -> (getItem(position) as? Book)?.let {
|
|
||||||
holder.registerListener(it)
|
|
||||||
holder.onBind(it, payloads)
|
|
||||||
}
|
|
||||||
|
|
||||||
is GroupViewHolder -> (getItem(position) as? BookGroup)?.let {
|
|
||||||
holder.registerListener(it)
|
|
||||||
holder.onBind(it, payloads)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getBooksForGroup(group: BookGroup): List<Book> {
|
|
||||||
return if (group.groupId == -1L) {
|
|
||||||
allBooks.take(4)
|
|
||||||
} else {
|
|
||||||
allBooks.filter { (it.group and group.groupId) != 0L }.take(4)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class BookViewHolder(val binding: ItemBookshelfListBinding) :
|
|
||||||
RecyclerView.ViewHolder(binding.root) {
|
|
||||||
|
|
||||||
fun onBind(item: Book) = binding.run {
|
|
||||||
cdCover.transitionName = "book_${item.bookUrl}"
|
|
||||||
tvName.text = item.name
|
|
||||||
tvAuthor.text = item.author
|
|
||||||
tvRead.text = item.durChapterTitle
|
|
||||||
tvLast.text = item.latestChapterTitle
|
|
||||||
ivCover.load(item.getDisplayCover(), item.name, item.author, false, item.origin)
|
|
||||||
cdUnread.visible()
|
|
||||||
// ivAuthor.visible()
|
|
||||||
// ivLast.visible()
|
|
||||||
// ivRead.visible()
|
|
||||||
upRefresh(this, item)
|
|
||||||
upLastUpdateTime(binding, item)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onBind(item: Book, payloads: MutableList<Any>) = binding.run {
|
|
||||||
if (payloads.isEmpty()) {
|
|
||||||
onBind(item)
|
|
||||||
} else {
|
|
||||||
for (i in payloads.indices) {
|
|
||||||
val bundle = payloads[i] as Bundle
|
|
||||||
bundle.keySet().forEach {
|
|
||||||
when (it) {
|
|
||||||
"name" -> tvName.text = item.name
|
|
||||||
"author" -> tvAuthor.text = item.author
|
|
||||||
"dur" -> tvRead.text = item.durChapterTitle
|
|
||||||
"last" -> tvLast.text = item.latestChapterTitle
|
|
||||||
"cover" -> ivCover.load(
|
|
||||||
item.getDisplayCover(),
|
|
||||||
item.name,
|
|
||||||
item.author,
|
|
||||||
false,
|
|
||||||
item.origin
|
|
||||||
)
|
|
||||||
|
|
||||||
"refresh" -> upRefresh(this, item)
|
|
||||||
"lastUpdateTime" -> upLastUpdateTime(binding, item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun upLastUpdateTime(binding: ItemBookshelfListBinding, item: Book) {
|
|
||||||
if (AppConfig.showLastUpdateTime && !item.isLocal) {
|
|
||||||
val time = item.latestChapterTime.toTimeAgo()
|
|
||||||
if (binding.tvLastUpdateTime.text != time) {
|
|
||||||
binding.tvLastUpdateTime.text = time
|
|
||||||
binding.tvLastUpdateTime.visible()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
binding.tvLastUpdateTime.gone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun registerListener(item: Any) {
|
|
||||||
binding.cvContent.setOnClickListener {
|
|
||||||
callBack.onItemClick(item, binding.cdCover)
|
|
||||||
}
|
|
||||||
binding.cvContent.onLongClick {
|
|
||||||
callBack.onItemLongClick(item, binding.cdCover)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun upRefresh(binding: ItemBookshelfListBinding, item: Book) {
|
|
||||||
if (!item.isLocal && callBack.isUpdate(item.bookUrl)) {
|
|
||||||
binding.cdUnread.gone()
|
|
||||||
binding.rlLoading.visible()
|
|
||||||
} else {
|
|
||||||
binding.rlLoading.gone()
|
|
||||||
if (AppConfig.showUnread) {
|
|
||||||
val unreadCount = item.getUnreadChapterNum()
|
|
||||||
if (unreadCount > 0) {
|
|
||||||
binding.cdUnread.visible()
|
|
||||||
binding.tvUnread.text = unreadCount.toString()
|
|
||||||
if (AppConfig.showUnreadNew)
|
|
||||||
binding.newChapter.isVisible = item.lastCheckCount > 0
|
|
||||||
} else {
|
|
||||||
binding.cdUnread.gone()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
binding.cdUnread.gone()
|
|
||||||
}
|
|
||||||
if (AppConfig.showTip){
|
|
||||||
binding.cdTip.visible()
|
|
||||||
binding.tvTip.text = item.getBookTypeName()
|
|
||||||
} else {
|
|
||||||
binding.cdTip.gone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class GroupViewHolder(val binding: ItemBookshelfListGroupBinding) :
|
|
||||||
RecyclerView.ViewHolder(binding.root) {
|
|
||||||
|
|
||||||
fun onBind(item: BookGroup) = binding.run {
|
|
||||||
tvName.text = item.groupName
|
|
||||||
loadGroupCover(item)
|
|
||||||
cdUnread.gone()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun onBind(item: BookGroup, payloads: MutableList<Any>) = binding.run {
|
|
||||||
if (payloads.isEmpty()) {
|
|
||||||
onBind(item)
|
|
||||||
} else {
|
|
||||||
for (i in payloads.indices) {
|
|
||||||
val bundle = payloads[i] as Bundle
|
|
||||||
bundle.keySet().forEach {
|
|
||||||
when (it) {
|
|
||||||
"groupName" -> tvName.text = item.groupName
|
|
||||||
"cover" -> loadGroupCover(item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun registerListener(item: Any) {
|
|
||||||
binding.cvContent.setOnClickListener {
|
|
||||||
callBack.onItemClick(item, binding.ivCover)
|
|
||||||
}
|
|
||||||
binding.cvContent.onLongClick {
|
|
||||||
callBack.onItemLongClick(item, binding.ivCover)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun loadGroupCover(item: BookGroup) = binding.run {
|
|
||||||
|
|
||||||
binding.ivCover.setImageDrawable(
|
|
||||||
ContextCompat.getDrawable(binding.root.context, R.drawable.bg_surface_variant)
|
|
||||||
)
|
|
||||||
binding.ivCover1.setImageDrawable(null)
|
|
||||||
binding.ivCover2.setImageDrawable(null)
|
|
||||||
binding.ivCover3.setImageDrawable(null)
|
|
||||||
binding.ivCover4.setImageDrawable(null)
|
|
||||||
if (!item.cover.isNullOrEmpty()) {
|
|
||||||
ivCover.load(item.cover)
|
|
||||||
} else {
|
|
||||||
|
|
||||||
val books = getBooksForGroup(item)
|
|
||||||
|
|
||||||
if (books.size > 0) binding.ivCover1.load(books[0].getDisplayCover())
|
|
||||||
if (books.size > 1) binding.ivCover2.load(books[1].getDisplayCover())
|
|
||||||
if (books.size > 2) binding.ivCover3.load(books[2].getDisplayCover())
|
|
||||||
if (books.size > 3) binding.ivCover4.load(books[3].getDisplayCover())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
-209
@@ -1,209 +0,0 @@
|
|||||||
package io.legado.app.ui.main.bookshelf.books.styleFold
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import io.legado.app.R
|
|
||||||
import io.legado.app.data.entities.Book
|
|
||||||
import io.legado.app.data.entities.BookGroup
|
|
||||||
import io.legado.app.databinding.ItemBookshelfListCompactBinding
|
|
||||||
import io.legado.app.databinding.ItemBookshelfListCompactGroupBinding
|
|
||||||
import io.legado.app.help.book.getBookTypeName
|
|
||||||
import io.legado.app.help.book.isLocal
|
|
||||||
import io.legado.app.help.config.AppConfig
|
|
||||||
import io.legado.app.utils.gone
|
|
||||||
import io.legado.app.utils.toTimeAgo
|
|
||||||
import io.legado.app.utils.visible
|
|
||||||
import splitties.views.onLongClick
|
|
||||||
|
|
||||||
@Suppress("UNUSED_PARAMETER")
|
|
||||||
class BooksAdapterListCompact(context: Context, callBack: CallBack) :
|
|
||||||
BaseBooksAdapter<RecyclerView.ViewHolder>(context, callBack) {
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
|
||||||
return when (viewType) {
|
|
||||||
1 -> GroupViewHolder(ItemBookshelfListCompactGroupBinding.inflate(inflater, parent, false))
|
|
||||||
else -> BookViewHolder(ItemBookshelfListCompactBinding.inflate(inflater, parent, false))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(
|
|
||||||
holder: RecyclerView.ViewHolder,
|
|
||||||
position: Int,
|
|
||||||
payloads: MutableList<Any>
|
|
||||||
) {
|
|
||||||
when (holder) {
|
|
||||||
is BookViewHolder -> (getItem(position) as? Book)?.let {
|
|
||||||
holder.registerListener(it)
|
|
||||||
holder.onBind(it, payloads)
|
|
||||||
}
|
|
||||||
|
|
||||||
is GroupViewHolder -> (getItem(position) as? BookGroup)?.let {
|
|
||||||
holder.registerListener(it)
|
|
||||||
holder.onBind(it, payloads)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getBooksForGroup(group: BookGroup): List<Book> {
|
|
||||||
return if (group.groupId == -1L) {
|
|
||||||
allBooks.take(4)
|
|
||||||
} else {
|
|
||||||
allBooks.filter { (it.group and group.groupId) != 0L }.take(4)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class BookViewHolder(val binding: ItemBookshelfListCompactBinding) :
|
|
||||||
RecyclerView.ViewHolder(binding.root) {
|
|
||||||
|
|
||||||
fun onBind(item: Book) = binding.run {
|
|
||||||
cdCover.transitionName = "book_${item.bookUrl}"
|
|
||||||
tvName.text = item.name
|
|
||||||
tvAuthor.text = item.author
|
|
||||||
tvLast.text = item.latestChapterTitle
|
|
||||||
ivCover.load(item.getDisplayCover(), item.name, item.author, false, item.origin)
|
|
||||||
cdUnread.visible()
|
|
||||||
// ivAuthor.visible()
|
|
||||||
// ivLast.visible()
|
|
||||||
// ivRead.visible()
|
|
||||||
upRefresh(this, item)
|
|
||||||
upLastUpdateTime(binding, item)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onBind(item: Book, payloads: MutableList<Any>) = binding.run {
|
|
||||||
if (payloads.isEmpty()) {
|
|
||||||
onBind(item)
|
|
||||||
} else {
|
|
||||||
for (i in payloads.indices) {
|
|
||||||
val bundle = payloads[i] as Bundle
|
|
||||||
bundle.keySet().forEach {
|
|
||||||
when (it) {
|
|
||||||
"name" -> tvName.text = item.name
|
|
||||||
"author" -> tvAuthor.text = item.author
|
|
||||||
"last" -> tvLast.text = item.latestChapterTitle
|
|
||||||
"cover" -> ivCover.load(
|
|
||||||
item.getDisplayCover(),
|
|
||||||
item.name,
|
|
||||||
item.author,
|
|
||||||
false,
|
|
||||||
item.origin
|
|
||||||
)
|
|
||||||
|
|
||||||
"refresh" -> upRefresh(this, item)
|
|
||||||
"lastUpdateTime" -> upLastUpdateTime(binding, item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun upLastUpdateTime(binding: ItemBookshelfListCompactBinding, item: Book) {
|
|
||||||
if (AppConfig.showLastUpdateTime && !item.isLocal) {
|
|
||||||
val time = item.latestChapterTime.toTimeAgo()
|
|
||||||
if (binding.tvLastUpdateTime.text != time) {
|
|
||||||
binding.tvLastUpdateTime.text = time
|
|
||||||
binding.tvLastUpdateTime.visible()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
binding.tvLastUpdateTime.gone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun registerListener(item: Any) {
|
|
||||||
binding.cvContent.setOnClickListener {
|
|
||||||
callBack.onItemClick(item, binding.cdCover)
|
|
||||||
}
|
|
||||||
binding.cvContent.onLongClick {
|
|
||||||
callBack.onItemLongClick(item, binding.cdCover)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun upRefresh(binding: ItemBookshelfListCompactBinding, item: Book) {
|
|
||||||
if (!item.isLocal && callBack.isUpdate(item.bookUrl)) {
|
|
||||||
binding.cdUnread.gone()
|
|
||||||
binding.rlLoading.visible()
|
|
||||||
} else {
|
|
||||||
binding.rlLoading.gone()
|
|
||||||
if (AppConfig.showUnread) {
|
|
||||||
val unreadCount = item.getUnreadChapterNum()
|
|
||||||
if (unreadCount > 0) {
|
|
||||||
binding.cdUnread.visible()
|
|
||||||
binding.tvUnread.text = unreadCount.toString()
|
|
||||||
if (AppConfig.showUnreadNew)
|
|
||||||
binding.newChapter.isVisible = item.lastCheckCount > 0
|
|
||||||
} else {
|
|
||||||
binding.cdUnread.gone()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
binding.cdUnread.gone()
|
|
||||||
}
|
|
||||||
if (AppConfig.showTip){
|
|
||||||
binding.cdTip.visible()
|
|
||||||
binding.tvTip.text = item.getBookTypeName()
|
|
||||||
} else {
|
|
||||||
binding.cdTip.gone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class GroupViewHolder(val binding: ItemBookshelfListCompactGroupBinding) :
|
|
||||||
RecyclerView.ViewHolder(binding.root) {
|
|
||||||
|
|
||||||
fun onBind(item: BookGroup) = binding.run {
|
|
||||||
tvName.text = item.groupName
|
|
||||||
loadGroupCover(item)
|
|
||||||
cdUnread.gone()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun onBind(item: BookGroup, payloads: MutableList<Any>) = binding.run {
|
|
||||||
if (payloads.isEmpty()) {
|
|
||||||
onBind(item)
|
|
||||||
} else {
|
|
||||||
for (i in payloads.indices) {
|
|
||||||
val bundle = payloads[i] as Bundle
|
|
||||||
bundle.keySet().forEach {
|
|
||||||
when (it) {
|
|
||||||
"groupName" -> tvName.text = item.groupName
|
|
||||||
"cover" -> loadGroupCover(item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun registerListener(item: Any) {
|
|
||||||
binding.cvContent.setOnClickListener {
|
|
||||||
callBack.onItemClick(item, binding.ivCover)
|
|
||||||
}
|
|
||||||
binding.cvContent.onLongClick {
|
|
||||||
callBack.onItemLongClick(item, binding.ivCover)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun loadGroupCover(item: BookGroup) = binding.run {
|
|
||||||
|
|
||||||
binding.ivCover.setImageDrawable(
|
|
||||||
ContextCompat.getDrawable(binding.root.context, R.drawable.bg_surface_variant)
|
|
||||||
)
|
|
||||||
binding.ivCover1.setImageDrawable(null)
|
|
||||||
binding.ivCover2.setImageDrawable(null)
|
|
||||||
if (!item.cover.isNullOrEmpty()) {
|
|
||||||
ivCover.load(item.cover)
|
|
||||||
} else {
|
|
||||||
|
|
||||||
val books = getBooksForGroup(item)
|
|
||||||
|
|
||||||
if (books.size > 0) binding.ivCover1.load(books[0].getDisplayCover())
|
|
||||||
if (books.size > 1) binding.ivCover2.load(books[1].getDisplayCover())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,225 +0,0 @@
|
|||||||
package io.legado.app.ui.main.explore
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.PopupMenu
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.core.view.children
|
|
||||||
import com.google.android.flexbox.FlexboxLayout
|
|
||||||
import com.google.android.material.card.MaterialCardView
|
|
||||||
import io.legado.app.R
|
|
||||||
import io.legado.app.base.adapter.ItemViewHolder
|
|
||||||
import io.legado.app.base.adapter.RecyclerAdapter
|
|
||||||
import io.legado.app.data.entities.BookSourcePart
|
|
||||||
import io.legado.app.data.entities.rule.ExploreKind
|
|
||||||
import io.legado.app.databinding.ItemFilletTextBinding
|
|
||||||
import io.legado.app.databinding.ItemFindBookBinding
|
|
||||||
import io.legado.app.help.coroutine.Coroutine
|
|
||||||
import io.legado.app.help.source.clearExploreKindsCache
|
|
||||||
import io.legado.app.help.source.exploreKinds
|
|
||||||
import io.legado.app.ui.login.SourceLoginActivity
|
|
||||||
import io.legado.app.ui.widget.dialog.TextDialog
|
|
||||||
import io.legado.app.utils.activity
|
|
||||||
import io.legado.app.utils.gone
|
|
||||||
import io.legado.app.utils.removeLastElement
|
|
||||||
import io.legado.app.utils.showDialogFragment
|
|
||||||
import io.legado.app.utils.startActivity
|
|
||||||
import io.legado.app.utils.visible
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import splitties.views.onLongClick
|
|
||||||
|
|
||||||
class ExploreAdapter(
|
|
||||||
context: Context,
|
|
||||||
private val callBack: CallBack
|
|
||||||
) : RecyclerAdapter<BookSourcePart, ItemFindBookBinding>(context) {
|
|
||||||
|
|
||||||
private val recycler = arrayListOf<View>()
|
|
||||||
private var expandedId: String? = null
|
|
||||||
private var scrollToId: String? = null
|
|
||||||
|
|
||||||
init {
|
|
||||||
setHasStableIds(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemId(position: Int): Long {
|
|
||||||
return getItem(position)?.bookSourceUrl.hashCode().toLong()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getViewBinding(parent: ViewGroup): ItemFindBookBinding {
|
|
||||||
return ItemFindBookBinding.inflate(inflater, parent, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun convert(
|
|
||||||
holder: ItemViewHolder,
|
|
||||||
binding: ItemFindBookBinding,
|
|
||||||
item: BookSourcePart,
|
|
||||||
payloads: MutableList<Any>
|
|
||||||
) {
|
|
||||||
binding.run {
|
|
||||||
if (payloads.isEmpty()) {
|
|
||||||
tvName.text = item.bookSourceName
|
|
||||||
}
|
|
||||||
|
|
||||||
val isExpanded = (item.bookSourceUrl == expandedId)
|
|
||||||
|
|
||||||
if (isExpanded) {
|
|
||||||
ivStatus.isChecked = true
|
|
||||||
rotateLoading.visible()
|
|
||||||
if (scrollToId == item.bookSourceUrl) {
|
|
||||||
val pos = holder.bindingAdapterPosition
|
|
||||||
if (pos >= 0) callBack.scrollTo(pos)
|
|
||||||
scrollToId = null
|
|
||||||
}
|
|
||||||
|
|
||||||
Coroutine.async(callBack.scope) {
|
|
||||||
item.exploreKinds()
|
|
||||||
}.onSuccess { kindList ->
|
|
||||||
upKindList(flexbox, item.bookSourceUrl, kindList)
|
|
||||||
}.onFinally {
|
|
||||||
rotateLoading.gone()
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
ivStatus.isChecked = false
|
|
||||||
recyclerFlexbox(flexbox)
|
|
||||||
flexbox.gone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun upKindList(flexbox: FlexboxLayout, sourceUrl: String, kinds: List<ExploreKind>) {
|
|
||||||
if (kinds.isNotEmpty()) kotlin.runCatching {
|
|
||||||
recyclerFlexbox(flexbox)
|
|
||||||
flexbox.visible()
|
|
||||||
kinds.forEach { kind ->
|
|
||||||
val cardView = getFlexboxChild(flexbox)
|
|
||||||
val textView = cardView.findViewById<TextView>(R.id.text_view)
|
|
||||||
flexbox.addView(cardView)
|
|
||||||
|
|
||||||
textView.text = kind.title
|
|
||||||
kind.style().apply(cardView)
|
|
||||||
|
|
||||||
if (kind.url.isNullOrBlank()) {
|
|
||||||
cardView.setOnClickListener(null)
|
|
||||||
} else {
|
|
||||||
cardView.setOnClickListener {
|
|
||||||
if (kind.title.startsWith("ERROR:")) {
|
|
||||||
it.activity?.showDialogFragment(TextDialog("ERROR", kind.url))
|
|
||||||
} else {
|
|
||||||
callBack.openExplore(sourceUrl, kind.title, kind.url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
private fun getFlexboxChild(flexbox: FlexboxLayout): MaterialCardView {
|
|
||||||
val cardView = if (recycler.isEmpty()) {
|
|
||||||
ItemFilletTextBinding.inflate(inflater, flexbox, false).root
|
|
||||||
} else {
|
|
||||||
recycler.removeLastElement() as MaterialCardView
|
|
||||||
}
|
|
||||||
val lp = FlexboxLayout.LayoutParams(
|
|
||||||
ViewGroup.LayoutParams.WRAP_CONTENT,
|
|
||||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
||||||
)
|
|
||||||
cardView.layoutParams = lp
|
|
||||||
cardView.forceLayout()
|
|
||||||
val tv = cardView.findViewById<TextView>(R.id.text_view)
|
|
||||||
tv.text = ""
|
|
||||||
tv.requestLayout()
|
|
||||||
return cardView
|
|
||||||
}
|
|
||||||
|
|
||||||
@Synchronized
|
|
||||||
private fun recyclerFlexbox(flexbox: FlexboxLayout) {
|
|
||||||
recycler.addAll(flexbox.children)
|
|
||||||
flexbox.removeAllViews()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun registerListener(holder: ItemViewHolder, binding: ItemFindBookBinding) {
|
|
||||||
binding.apply {
|
|
||||||
llTitle.setOnClickListener {
|
|
||||||
toggleExpandState(holder)
|
|
||||||
}
|
|
||||||
ivStatus.setOnClickListener {
|
|
||||||
toggleExpandState(holder)
|
|
||||||
}
|
|
||||||
llTitle.onLongClick {
|
|
||||||
showMenu(ivStatus, holder.bindingAdapterPosition)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun toggleExpandState(holder: ItemViewHolder) {
|
|
||||||
val item = getItem(holder.bindingAdapterPosition) ?: return
|
|
||||||
val oldId = expandedId
|
|
||||||
expandedId = if (expandedId == item.bookSourceUrl) null else item.bookSourceUrl
|
|
||||||
notifyItemChangedById(oldId)
|
|
||||||
notifyItemChangedById(expandedId)
|
|
||||||
|
|
||||||
if (expandedId != null) {
|
|
||||||
scrollToId = expandedId
|
|
||||||
callBack.scrollTo(holder.bindingAdapterPosition)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun compressExplore(): Boolean {
|
|
||||||
return if (expandedId == null) {
|
|
||||||
false
|
|
||||||
} else {
|
|
||||||
val oldId = expandedId
|
|
||||||
expandedId = null
|
|
||||||
notifyItemChangedById(oldId)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private fun notifyItemChangedById(id: String?) {
|
|
||||||
id ?: return
|
|
||||||
for (pos in 0 until itemCount) {
|
|
||||||
val item = getItem(pos) ?: continue
|
|
||||||
if (item.bookSourceUrl == id) {
|
|
||||||
notifyItemChanged(pos)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private fun showMenu(view: View, position: Int) {
|
|
||||||
val source = getItem(position) ?: return
|
|
||||||
val popupMenu = PopupMenu(context, view)
|
|
||||||
popupMenu.inflate(R.menu.explore_item)
|
|
||||||
popupMenu.menu.findItem(R.id.menu_login).isVisible = source.hasLoginUrl
|
|
||||||
popupMenu.setOnMenuItemClickListener {
|
|
||||||
when (it.itemId) {
|
|
||||||
R.id.menu_edit -> callBack.editSource(source.bookSourceUrl)
|
|
||||||
R.id.menu_top -> callBack.toTop(source)
|
|
||||||
R.id.menu_search -> callBack.searchBook(source)
|
|
||||||
R.id.menu_login -> context.startActivity<SourceLoginActivity> {
|
|
||||||
putExtra("type", "bookSource")
|
|
||||||
putExtra("key", source.bookSourceUrl)
|
|
||||||
}
|
|
||||||
R.id.menu_refresh -> Coroutine.async(callBack.scope) {
|
|
||||||
source.clearExploreKindsCache()
|
|
||||||
}.onSuccess {
|
|
||||||
notifyItemChanged(position)
|
|
||||||
}
|
|
||||||
R.id.menu_del -> callBack.deleteSource(source)
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
popupMenu.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CallBack {
|
|
||||||
val scope: CoroutineScope
|
|
||||||
fun scrollTo(pos: Int)
|
|
||||||
fun openExplore(sourceUrl: String, title: String, exploreUrl: String?)
|
|
||||||
fun editSource(sourceUrl: String)
|
|
||||||
fun toTop(source: BookSourcePart)
|
|
||||||
fun deleteSource(source: BookSourcePart)
|
|
||||||
fun searchBook(bookSource: BookSourcePart)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package io.legado.app.ui.main.explore
|
|
||||||
|
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
|
||||||
import io.legado.app.data.entities.BookSourcePart
|
|
||||||
|
|
||||||
|
|
||||||
class ExploreDiffItemCallBack : DiffUtil.ItemCallback<BookSourcePart>() {
|
|
||||||
|
|
||||||
override fun areItemsTheSame(oldItem: BookSourcePart, newItem: BookSourcePart): Boolean {
|
|
||||||
return oldItem == newItem
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItem: BookSourcePart, newItem: BookSourcePart): Boolean {
|
|
||||||
return oldItem.bookSourceName == newItem.bookSourceName
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
package io.legado.app.ui.main.explore
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.compose.ui.platform.ComposeView
|
|
||||||
import androidx.compose.ui.platform.ViewCompositionStrategy
|
|
||||||
import androidx.fragment.app.Fragment
|
|
||||||
import io.legado.app.ui.main.MainFragmentInterface
|
|
||||||
import io.legado.app.ui.theme.AppTheme
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 发现界面
|
|
||||||
*/
|
|
||||||
class ExploreFragment() : Fragment(), MainFragmentInterface {
|
|
||||||
|
|
||||||
constructor(position: Int) : this() {
|
|
||||||
arguments = Bundle().apply {
|
|
||||||
putInt("position", position)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override val position: Int
|
|
||||||
get() = arguments?.getInt("position") ?: 0
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
return ComposeView(requireContext()).apply {
|
|
||||||
setViewCompositionStrategy(
|
|
||||||
ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed
|
|
||||||
)
|
|
||||||
setContent {
|
|
||||||
AppTheme {
|
|
||||||
ExploreScreen()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun compressExplore() {
|
|
||||||
// Compose version handled by ViewModel state
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -397,56 +397,54 @@ fun ExploreSourceHeader(
|
|||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
RoundDropdownMenu(expanded = showMenu, onDismissRequest = { showMenu = false }) {
|
||||||
|
PillHeaderDivider(title = item.bookSourceName)
|
||||||
|
RoundDropdownMenuItem(
|
||||||
|
leadingIcon = { MenuItemIcon(Icons.Default.VerticalAlignTop) },
|
||||||
|
text = { Text(stringResource(R.string.to_top)) },
|
||||||
|
onClick = { onTop(); showMenu = false }
|
||||||
|
)
|
||||||
|
RoundDropdownMenuItem(
|
||||||
|
leadingIcon = { MenuItemIcon(Icons.Default.Edit) },
|
||||||
|
text = { Text(stringResource(R.string.edit)) },
|
||||||
|
onClick = { onEdit(); showMenu = false }
|
||||||
|
)
|
||||||
|
RoundDropdownMenuItem(
|
||||||
|
leadingIcon = { MenuItemIcon(Icons.Default.Search) },
|
||||||
|
text = { Text(stringResource(R.string.search)) },
|
||||||
|
onClick = { onSearch(); showMenu = false }
|
||||||
|
)
|
||||||
|
if (item.hasLoginUrl) {
|
||||||
|
RoundDropdownMenuItem(
|
||||||
|
leadingIcon = { MenuItemIcon(Icons.AutoMirrored.Filled.Login) },
|
||||||
|
text = { Text(stringResource(R.string.login)) },
|
||||||
|
onClick = { onLogin(); showMenu = false }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
RoundDropdownMenuItem(
|
||||||
|
leadingIcon = { MenuItemIcon(Icons.Default.Refresh) },
|
||||||
|
text = { Text(stringResource(R.string.refresh)) },
|
||||||
|
onClick = { onRefresh(); showMenu = false }
|
||||||
|
)
|
||||||
|
RoundDropdownMenuItem(
|
||||||
|
leadingIcon = {
|
||||||
|
MenuItemIcon(
|
||||||
|
Icons.Default.Delete,
|
||||||
|
tint = MaterialTheme.colorScheme.error
|
||||||
|
)
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.delete),
|
||||||
|
color = MaterialTheme.colorScheme.error
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = { onDelete(); showMenu = false }
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
RoundDropdownMenu(expanded = showMenu, onDismissRequest = { showMenu = false }) {
|
|
||||||
PillHeaderDivider(title = item.bookSourceName)
|
|
||||||
RoundDropdownMenuItem(
|
|
||||||
leadingIcon = { MenuItemIcon(Icons.Default.VerticalAlignTop) },
|
|
||||||
text = { Text(stringResource(R.string.to_top)) },
|
|
||||||
onClick = { onTop(); showMenu = false }
|
|
||||||
)
|
|
||||||
RoundDropdownMenuItem(
|
|
||||||
leadingIcon = { MenuItemIcon(Icons.Default.Edit) },
|
|
||||||
text = { Text(stringResource(R.string.edit)) },
|
|
||||||
onClick = { onEdit(); showMenu = false }
|
|
||||||
)
|
|
||||||
RoundDropdownMenuItem(
|
|
||||||
leadingIcon = { MenuItemIcon(Icons.Default.Search) },
|
|
||||||
text = { Text(stringResource(R.string.search)) },
|
|
||||||
onClick = { onSearch(); showMenu = false }
|
|
||||||
)
|
|
||||||
if (item.hasLoginUrl) {
|
|
||||||
RoundDropdownMenuItem(
|
|
||||||
leadingIcon = { MenuItemIcon(Icons.AutoMirrored.Filled.Login) },
|
|
||||||
text = { Text(stringResource(R.string.login)) },
|
|
||||||
onClick = { onLogin(); showMenu = false }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
RoundDropdownMenuItem(
|
|
||||||
leadingIcon = { MenuItemIcon(Icons.Default.Refresh) },
|
|
||||||
text = { Text(stringResource(R.string.refresh)) },
|
|
||||||
onClick = { onRefresh(); showMenu = false }
|
|
||||||
)
|
|
||||||
RoundDropdownMenuItem(
|
|
||||||
leadingIcon = {
|
|
||||||
MenuItemIcon(
|
|
||||||
Icons.Default.Delete,
|
|
||||||
tint = MaterialTheme.colorScheme.error
|
|
||||||
)
|
|
||||||
},
|
|
||||||
text = {
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.delete),
|
|
||||||
color = MaterialTheme.colorScheme.error
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onClick = { onDelete(); showMenu = false }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -493,8 +491,7 @@ fun ExploreKindItem(
|
|||||||
OutlinedCard(
|
OutlinedCard(
|
||||||
shape = shape,
|
shape = shape,
|
||||||
colors = CardDefaults.outlinedCardColors(
|
colors = CardDefaults.outlinedCardColors(
|
||||||
containerColor = Color.Transparent,
|
contentColor = MaterialTheme.colorScheme.primary
|
||||||
contentColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
|
|
||||||
),
|
),
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import io.legado.app.data.entities.rule.ExploreKind
|
|||||||
import io.legado.app.help.source.SourceHelp
|
import io.legado.app.help.source.SourceHelp
|
||||||
import io.legado.app.help.source.clearExploreKindsCache
|
import io.legado.app.help.source.clearExploreKindsCache
|
||||||
import io.legado.app.help.source.exploreKinds
|
import io.legado.app.help.source.exploreKinds
|
||||||
import io.legado.app.ui.widget.components.rules.ListUiState
|
import io.legado.app.ui.widget.components.list.ListUiState
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
|||||||
@@ -48,3 +48,17 @@ fun Modifier.responsiveHazeEffect(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 仅判断 enableBlur 的简单 HazeEffect
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalHazeMaterialsApi::class)
|
||||||
|
@Composable
|
||||||
|
fun Modifier.regularHazeEffect(state: HazeState): Modifier {
|
||||||
|
if (!ThemeConfig.enableBlur) return this
|
||||||
|
|
||||||
|
return this.hazeEffect(
|
||||||
|
state = state,
|
||||||
|
style = HazeMaterials.ultraThin()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package io.legado.app.ui.widget.components
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import io.legado.app.ui.config.themeConfig.ThemeConfig
|
||||||
|
|
||||||
|
object GlassDefaults {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统一的玻璃颜色处理方法
|
||||||
|
* @param noBlurColor 未开启模糊时使用的颜色
|
||||||
|
* @param blurAlpha 开启模糊时应用的透明度
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun glassColor(noBlurColor: Color, blurAlpha: Float): Color {
|
||||||
|
return if (ThemeConfig.enableBlur) {
|
||||||
|
noBlurColor.copy(alpha = blurAlpha)
|
||||||
|
} else {
|
||||||
|
noBlurColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认的玻璃效果透明度值,方便全局修改
|
||||||
|
val DefaultBlurAlpha = 0.36f
|
||||||
|
val ThickBlurAlpha = 0.72f
|
||||||
|
val TransparentAlpha = 0f
|
||||||
|
}
|
||||||
@@ -0,0 +1,292 @@
|
|||||||
|
package io.legado.app.ui.widget.components.cover
|
||||||
|
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.graphics.Typeface
|
||||||
|
import android.text.Layout
|
||||||
|
import android.text.StaticLayout
|
||||||
|
import android.text.TextPaint
|
||||||
|
import android.text.TextUtils
|
||||||
|
import androidx.compose.foundation.Canvas
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.RowScope
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
|
||||||
|
import androidx.compose.ui.graphics.nativeCanvas
|
||||||
|
import androidx.compose.ui.graphics.toArgb
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.graphics.withSave
|
||||||
|
import coil.compose.AsyncImage
|
||||||
|
import coil.request.ImageRequest
|
||||||
|
import io.legado.app.help.config.AppConfig
|
||||||
|
import io.legado.app.model.BookCover
|
||||||
|
import io.legado.app.ui.config.coverConfig.CoverConfig
|
||||||
|
import org.koin.compose.koinInject
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BookCover(
|
||||||
|
name: String?,
|
||||||
|
author: String?,
|
||||||
|
path: String?,
|
||||||
|
modifier: Modifier = Modifier.width(64.dp),
|
||||||
|
badgeContent: (@Composable RowScope.() -> Unit)? = null,
|
||||||
|
sourceOrigin: String? = null,
|
||||||
|
onLoadFinish: (() -> Unit)? = null
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val isNight = isSystemInDarkTheme()
|
||||||
|
|
||||||
|
val useDefault = AppConfig.useDefaultCover
|
||||||
|
val finalPath = if (useDefault) null else path
|
||||||
|
|
||||||
|
// 为每个 BookCover 组件实例基于其书名或路径生成一个随机封面
|
||||||
|
// 这样书架上的每个默认封面都会不同,但对于同一本书又是固定的
|
||||||
|
val randomDefault = remember(name, author, path) {
|
||||||
|
BookCover.getRandomDefaultDrawable(seed = name ?: author ?: path ?: "")
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.aspectRatio(5f / 7f)
|
||||||
|
.then(
|
||||||
|
if (CoverConfig.coverShowShadow) {
|
||||||
|
Modifier.shadow(4.dp, RoundedCornerShape(4.dp))
|
||||||
|
} else Modifier
|
||||||
|
)
|
||||||
|
.clip(RoundedCornerShape(4.dp))
|
||||||
|
.background(MaterialTheme.colorScheme.surfaceContainerLow)
|
||||||
|
.then(
|
||||||
|
if (CoverConfig.coverShowStroke) {
|
||||||
|
Modifier.border(
|
||||||
|
0.5.dp,
|
||||||
|
MaterialTheme.colorScheme.outlineVariant,
|
||||||
|
RoundedCornerShape(4.dp)
|
||||||
|
)
|
||||||
|
} else Modifier
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// 1. 封面底图
|
||||||
|
AsyncImage(
|
||||||
|
model = ImageRequest.Builder(context)
|
||||||
|
.data(finalPath ?: randomDefault)
|
||||||
|
.placeholder(randomDefault)
|
||||||
|
.error(randomDefault)
|
||||||
|
.crossfade(true)
|
||||||
|
.setParameter("sourceOrigin", sourceOrigin)
|
||||||
|
.setParameter("loadOnlyWifi", CoverConfig.loadCoverOnlyWifi)
|
||||||
|
.build(),
|
||||||
|
contentDescription = null,
|
||||||
|
imageLoader = koinInject(),
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
onSuccess = { onLoadFinish?.invoke() },
|
||||||
|
onError = { onLoadFinish?.invoke() }
|
||||||
|
)
|
||||||
|
|
||||||
|
// 2. 文字叠加层(始终尝试渲染,由 CoverConfig 内部控制具体显示逻辑)
|
||||||
|
CoverTextOverlay(
|
||||||
|
name = name,
|
||||||
|
author = author,
|
||||||
|
isNight = isNight
|
||||||
|
)
|
||||||
|
|
||||||
|
// 3. 角标 (Badge)
|
||||||
|
if (badgeContent != null) {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.TopEnd)
|
||||||
|
.padding(2.dp),
|
||||||
|
shape = RoundedCornerShape(2.dp),
|
||||||
|
color = MaterialTheme.colorScheme.secondaryContainer.copy(alpha = 0.9f),
|
||||||
|
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(horizontal = 4.dp, vertical = 1.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
content = badgeContent
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun CoverTextOverlay(
|
||||||
|
name: String?,
|
||||||
|
author: String?,
|
||||||
|
isNight: Boolean
|
||||||
|
) {
|
||||||
|
val showName = if (isNight) CoverConfig.coverShowNameN else CoverConfig.coverShowName
|
||||||
|
val showAuthor =
|
||||||
|
(if (isNight) CoverConfig.coverShowAuthorN else CoverConfig.coverShowAuthor) && showName
|
||||||
|
|
||||||
|
// 如果都不显示,直接返回
|
||||||
|
if (!showName && !showAuthor) return
|
||||||
|
|
||||||
|
val secondaryColor = MaterialTheme.colorScheme.secondary.toArgb()
|
||||||
|
val textColor = if (CoverConfig.coverDefaultColor) {
|
||||||
|
secondaryColor
|
||||||
|
} else {
|
||||||
|
if (isNight) CoverConfig.coverTextColorN else CoverConfig.coverTextColor
|
||||||
|
}
|
||||||
|
val shadowColor = if (isNight) CoverConfig.coverShadowColorN else CoverConfig.coverShadowColor
|
||||||
|
val isHorizontal = CoverConfig.coverInfoOrientation == "1"
|
||||||
|
|
||||||
|
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||||
|
val viewWidth = size.width
|
||||||
|
val viewHeight = size.height
|
||||||
|
|
||||||
|
drawIntoCanvas { canvas ->
|
||||||
|
val nativeCanvas = canvas.nativeCanvas
|
||||||
|
|
||||||
|
// 绘制书名
|
||||||
|
if (showName && !name.isNullOrBlank()) {
|
||||||
|
val paint = Paint().apply {
|
||||||
|
isAntiAlias = true
|
||||||
|
textAlign = Paint.Align.CENTER
|
||||||
|
typeface = Typeface.DEFAULT_BOLD
|
||||||
|
textSize = viewWidth / 8f
|
||||||
|
color = textColor
|
||||||
|
if (CoverConfig.coverShowShadow) {
|
||||||
|
setShadowLayer(4f, 2f, 2f, shadowColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isHorizontal) {
|
||||||
|
val maxWidth = (viewWidth * 0.8f).toInt()
|
||||||
|
val textPaint = TextPaint(paint).apply {
|
||||||
|
textAlign = Paint.Align.LEFT
|
||||||
|
}
|
||||||
|
|
||||||
|
val layout = StaticLayout.Builder
|
||||||
|
.obtain(name, 0, name.length, textPaint, maxWidth)
|
||||||
|
.setAlignment(Layout.Alignment.ALIGN_CENTER)
|
||||||
|
.setMaxLines(3)
|
||||||
|
.setEllipsize(TextUtils.TruncateAt.END)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
nativeCanvas.withSave {
|
||||||
|
val textX = (viewWidth - maxWidth) / 2f
|
||||||
|
val textY = viewHeight * 0.08f
|
||||||
|
|
||||||
|
translate(textX, textY)
|
||||||
|
|
||||||
|
if (CoverConfig.coverShowStroke) {
|
||||||
|
textPaint.style = Paint.Style.STROKE
|
||||||
|
textPaint.strokeWidth = textPaint.textSize / 12
|
||||||
|
val originalColor = textPaint.color
|
||||||
|
textPaint.color = Color.White.toArgb()
|
||||||
|
textPaint.clearShadowLayer()
|
||||||
|
layout.draw(this)
|
||||||
|
|
||||||
|
// 还原
|
||||||
|
textPaint.style = Paint.Style.FILL
|
||||||
|
textPaint.color = originalColor
|
||||||
|
if (CoverConfig.coverShowShadow) {
|
||||||
|
textPaint.setShadowLayer(4f, 2f, 2f, shadowColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
layout.draw(this)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var startX = viewWidth * 0.16f
|
||||||
|
var startY = viewHeight * 0.16f
|
||||||
|
val fm = paint.fontMetrics
|
||||||
|
val charHeight = fm.bottom - fm.top
|
||||||
|
|
||||||
|
name.forEach { char ->
|
||||||
|
if (CoverConfig.coverShowStroke) {
|
||||||
|
val strokePaint = Paint(paint).apply {
|
||||||
|
color = Color.White.toArgb()
|
||||||
|
style = Paint.Style.STROKE
|
||||||
|
strokeWidth = paint.textSize / 10
|
||||||
|
clearShadowLayer()
|
||||||
|
}
|
||||||
|
nativeCanvas.drawText(char.toString(), startX, startY, strokePaint)
|
||||||
|
}
|
||||||
|
nativeCanvas.drawText(char.toString(), startX, startY, paint)
|
||||||
|
startY += charHeight
|
||||||
|
if (startY > viewHeight * 0.8f) {
|
||||||
|
startX += paint.textSize * 1.2f
|
||||||
|
startY = viewHeight * 0.2f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 绘制作者
|
||||||
|
if (showAuthor && !author.isNullOrBlank()) {
|
||||||
|
val paint = Paint().apply {
|
||||||
|
isAntiAlias = true
|
||||||
|
textAlign = Paint.Align.CENTER
|
||||||
|
textSize = viewWidth / 12f
|
||||||
|
color = textColor
|
||||||
|
if (CoverConfig.coverShowShadow) {
|
||||||
|
setShadowLayer(4f, 1f, 1f, shadowColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isHorizontal) {
|
||||||
|
val authorText = TextUtils.ellipsize(
|
||||||
|
author,
|
||||||
|
TextPaint(paint),
|
||||||
|
viewWidth * 0.9f,
|
||||||
|
TextUtils.TruncateAt.END
|
||||||
|
)
|
||||||
|
|
||||||
|
if (CoverConfig.coverShowStroke) {
|
||||||
|
val strokePaint = Paint(paint).apply {
|
||||||
|
color = Color.White.toArgb()
|
||||||
|
style = Paint.Style.STROKE
|
||||||
|
strokeWidth = paint.textSize / 10
|
||||||
|
clearShadowLayer()
|
||||||
|
}
|
||||||
|
nativeCanvas.drawText(
|
||||||
|
authorText.toString(),
|
||||||
|
viewWidth / 2,
|
||||||
|
viewHeight * 0.75f,
|
||||||
|
strokePaint
|
||||||
|
)
|
||||||
|
}
|
||||||
|
nativeCanvas.drawText(
|
||||||
|
authorText.toString(),
|
||||||
|
viewWidth / 2,
|
||||||
|
viewHeight * 0.75f,
|
||||||
|
paint
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val startX = viewWidth * 0.84f
|
||||||
|
val fm = paint.fontMetrics
|
||||||
|
val charHeight = fm.bottom - fm.top
|
||||||
|
var startY = viewHeight * 0.16f - (author.length * charHeight)
|
||||||
|
startY = startY.coerceAtLeast(viewHeight * 0.2f)
|
||||||
|
|
||||||
|
author.forEach { char ->
|
||||||
|
nativeCanvas.drawText(char.toString(), startX, startY, paint)
|
||||||
|
startY += charHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package io.legado.app.ui.widget.components.cover
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.RowScope
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.StrokeCap
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BookCoverWithProgress(
|
||||||
|
name: String?,
|
||||||
|
author: String?,
|
||||||
|
path: String?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
isUpdating: Boolean = false,
|
||||||
|
badgeContent: (@Composable RowScope.() -> Unit)? = null,
|
||||||
|
sourceOrigin: String? = null,
|
||||||
|
onLoadFinish: (() -> Unit)? = null
|
||||||
|
) {
|
||||||
|
Box(modifier = modifier) {
|
||||||
|
BookCover(
|
||||||
|
name = name,
|
||||||
|
author = author,
|
||||||
|
path = path,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
badgeContent = badgeContent,
|
||||||
|
sourceOrigin = sourceOrigin,
|
||||||
|
onLoadFinish = onLoadFinish
|
||||||
|
)
|
||||||
|
|
||||||
|
if (isUpdating) {
|
||||||
|
LinearProgressIndicator(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 4.dp, vertical = 6.dp)
|
||||||
|
.height(3.dp),
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
trackColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.2f),
|
||||||
|
strokeCap = StrokeCap.Round
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package io.legado.app.ui.widget.components
|
package io.legado.app.ui.widget.components.cover
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
package io.legado.app.ui.widget.components.dialog
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.toArgb
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.github.skydoves.colorpicker.compose.AlphaSlider
|
||||||
|
import com.github.skydoves.colorpicker.compose.BrightnessSlider
|
||||||
|
import com.github.skydoves.colorpicker.compose.HsvColorPicker
|
||||||
|
import com.github.skydoves.colorpicker.compose.rememberColorPickerController
|
||||||
|
import io.legado.app.R
|
||||||
|
import io.legado.app.ui.widget.components.modalBottomSheet.GlassModalBottomSheet
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun ColorPickerSheet(
|
||||||
|
initialColor: Int,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
onColorSelected: (Int) -> Unit
|
||||||
|
) {
|
||||||
|
val controller = rememberColorPickerController()
|
||||||
|
var currentColor by remember { mutableStateOf(Color(initialColor)) }
|
||||||
|
|
||||||
|
val presets = remember {
|
||||||
|
listOf(
|
||||||
|
Color(0xFFF44336),
|
||||||
|
Color(0xFFE91E63),
|
||||||
|
Color(0xFF9C27B0),
|
||||||
|
Color(0xFF673AB7),
|
||||||
|
Color(0xFF3F51B5),
|
||||||
|
Color(0xFF2196F3),
|
||||||
|
Color(0xFF03A9F4),
|
||||||
|
Color(0xFF00BCD4),
|
||||||
|
Color(0xFF009688),
|
||||||
|
Color(0xFF4CAF50),
|
||||||
|
Color(0xFF8BC34A),
|
||||||
|
Color(0xFFCDDC39),
|
||||||
|
Color(0xFFFFEB3B),
|
||||||
|
Color(0xFFFFC107),
|
||||||
|
Color(0xFFFF9800),
|
||||||
|
Color(0xFFFF5722),
|
||||||
|
Color(0xFF795548),
|
||||||
|
Color(0xFF9E9E9E),
|
||||||
|
Color(0xFF607D8B),
|
||||||
|
Color(0xFF000000),
|
||||||
|
Color(0xFFFFFFFF)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
GlassModalBottomSheet(onDismissRequest = onDismissRequest) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "颜色选择",
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
modifier = Modifier.padding(bottom = 16.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
HsvColorPicker(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(250.dp)
|
||||||
|
.padding(10.dp),
|
||||||
|
controller = controller,
|
||||||
|
initialColor = Color(initialColor),
|
||||||
|
onColorChanged = { envelope ->
|
||||||
|
currentColor = envelope.color
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
AlphaSlider(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(10.dp)
|
||||||
|
.height(35.dp),
|
||||||
|
controller = controller,
|
||||||
|
)
|
||||||
|
|
||||||
|
BrightnessSlider(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(10.dp)
|
||||||
|
.height(35.dp),
|
||||||
|
controller = controller,
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 10.dp)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(40.dp)
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.background(currentColor)
|
||||||
|
.border(
|
||||||
|
1.dp,
|
||||||
|
MaterialTheme.colorScheme.outlineVariant,
|
||||||
|
RoundedCornerShape(8.dp)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
Text(
|
||||||
|
text = "#${Integer.toHexString(currentColor.toArgb()).uppercase()}",
|
||||||
|
style = MaterialTheme.typography.bodyLarge
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
|
LazyVerticalGrid(
|
||||||
|
columns = GridCells.Adaptive(36.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
modifier = Modifier
|
||||||
|
.heightIn(max = 80.dp)
|
||||||
|
.padding(horizontal = 10.dp)
|
||||||
|
) {
|
||||||
|
items(presets) { color ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(36.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(color)
|
||||||
|
.border(
|
||||||
|
if (color.toArgb() == currentColor.toArgb()) 2.dp else 0.dp,
|
||||||
|
MaterialTheme.colorScheme.primary,
|
||||||
|
CircleShape
|
||||||
|
)
|
||||||
|
.clickable {
|
||||||
|
controller.selectByColor(color, true)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.height(24.dp))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.End
|
||||||
|
) {
|
||||||
|
TextButton(onClick = onDismissRequest) {
|
||||||
|
Text(stringResource(R.string.cancel))
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
Button(onClick = {
|
||||||
|
onColorSelected(currentColor.toArgb())
|
||||||
|
onDismissRequest()
|
||||||
|
}) {
|
||||||
|
Text(stringResource(R.string.ok))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+10
-1
@@ -1,4 +1,4 @@
|
|||||||
package io.legado.app.ui.widget.components.exportComponents
|
package io.legado.app.ui.widget.components.filePicker
|
||||||
|
|
||||||
import android.webkit.MimeTypeMap
|
import android.webkit.MimeTypeMap
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
@@ -41,6 +41,7 @@ fun FilePickerSheet(
|
|||||||
title: String = stringResource(R.string.select_operation),
|
title: String = stringResource(R.string.select_operation),
|
||||||
onSelectSysDir: (() -> Unit)? = null,
|
onSelectSysDir: (() -> Unit)? = null,
|
||||||
onSelectSysFile: ((Array<String>) -> Unit)? = null,
|
onSelectSysFile: ((Array<String>) -> Unit)? = null,
|
||||||
|
onSelectSysFiles: ((Array<String>) -> Unit)? = null,
|
||||||
onManualInput: (() -> Unit)? = null,
|
onManualInput: (() -> Unit)? = null,
|
||||||
onUpload: (() -> Unit)? = null,
|
onUpload: (() -> Unit)? = null,
|
||||||
allowExtensions: Array<String>? = null,
|
allowExtensions: Array<String>? = null,
|
||||||
@@ -84,6 +85,14 @@ fun FilePickerSheet(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onSelectSysFiles?.let {
|
||||||
|
FilePickerOptionCard(
|
||||||
|
icon = Icons.AutoMirrored.Filled.InsertDriveFile,
|
||||||
|
text = "系统多选",
|
||||||
|
onClick = { it(typesOfExtensions(allowExtensions)) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
onManualInput?.let {
|
onManualInput?.let {
|
||||||
FilePickerOptionCard(
|
FilePickerOptionCard(
|
||||||
icon = Icons.Default.EditNote,
|
icon = Icons.Default.EditNote,
|
||||||
@@ -42,7 +42,6 @@ import io.legado.app.ui.theme.responsiveHazeEffect
|
|||||||
import io.legado.app.ui.theme.responsiveHazeSource
|
import io.legado.app.ui.theme.responsiveHazeSource
|
||||||
import io.legado.app.ui.widget.components.SelectionActions
|
import io.legado.app.ui.widget.components.SelectionActions
|
||||||
import io.legado.app.ui.widget.components.SelectionBottomBar
|
import io.legado.app.ui.widget.components.SelectionBottomBar
|
||||||
import io.legado.app.ui.widget.components.rules.ListUiState
|
|
||||||
import io.legado.app.ui.widget.components.topbar.DynamicTopAppBar
|
import io.legado.app.ui.widget.components.topbar.DynamicTopAppBar
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package io.legado.app.ui.widget.components.rules
|
package io.legado.app.ui.widget.components.list
|
||||||
|
|
||||||
interface ListUiState<T> {
|
interface ListUiState<T> {
|
||||||
val items: List<T>
|
val items: List<T>
|
||||||
+5
-2
@@ -15,6 +15,8 @@ fun ClickableSettingItem(
|
|||||||
description: String? = null,
|
description: String? = null,
|
||||||
option: String? = null,
|
option: String? = null,
|
||||||
imageVector: ImageVector? = null,
|
imageVector: ImageVector? = null,
|
||||||
|
onLongClick: (() -> Unit)? = null,
|
||||||
|
trailingContent: (@Composable () -> Unit)? = null,
|
||||||
onClick: () -> Unit
|
onClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
SettingItem(
|
SettingItem(
|
||||||
@@ -23,13 +25,14 @@ fun ClickableSettingItem(
|
|||||||
description = description,
|
description = description,
|
||||||
option = option,
|
option = option,
|
||||||
imageVector = imageVector,
|
imageVector = imageVector,
|
||||||
trailingContent = {
|
trailingContent = trailingContent ?: {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.ChevronRight,
|
imageVector = Icons.Default.ChevronRight,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onClick = onClick
|
onClick = onClick,
|
||||||
|
onLongClick = onLongClick
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
+153
@@ -0,0 +1,153 @@
|
|||||||
|
package io.legado.app.ui.widget.components.settingItem
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material.icons.filled.Check
|
||||||
|
import androidx.compose.material.icons.filled.Remove
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Slider
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.Shape
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.legado.app.ui.widget.components.button.SmallOutlinedIconButton
|
||||||
|
import io.legado.app.ui.widget.components.card.TextCard
|
||||||
|
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
|
@Composable
|
||||||
|
fun CompactDropdownSettingItem(
|
||||||
|
title: String,
|
||||||
|
selectedValue: String,
|
||||||
|
displayEntries: Array<String>,
|
||||||
|
entryValues: Array<String>,
|
||||||
|
description: String? = null,
|
||||||
|
imageVector: ImageVector? = null,
|
||||||
|
color: Color? = MaterialTheme.colorScheme.surface,
|
||||||
|
shape: Shape = MaterialTheme.shapes.small,
|
||||||
|
onValueChange: (String) -> Unit
|
||||||
|
) {
|
||||||
|
val currentEntry = displayEntries.getOrNull(entryValues.indexOf(selectedValue)) ?: selectedValue
|
||||||
|
|
||||||
|
SettingItem(
|
||||||
|
title = title,
|
||||||
|
description = description,
|
||||||
|
imageVector = imageVector,
|
||||||
|
color = color,
|
||||||
|
shape = shape,
|
||||||
|
trailingContent = {
|
||||||
|
TextCard(
|
||||||
|
cornerRadius = 8.dp,
|
||||||
|
horizontalPadding = 8.dp,
|
||||||
|
verticalPadding = 4.dp,
|
||||||
|
text = currentEntry,
|
||||||
|
backgroundColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
},
|
||||||
|
dropdownMenu = { onDismiss ->
|
||||||
|
displayEntries.forEachIndexed { index, display ->
|
||||||
|
RoundDropdownMenuItem(
|
||||||
|
text = { Text(display) },
|
||||||
|
onClick = {
|
||||||
|
onValueChange(entryValues[index])
|
||||||
|
onDismiss()
|
||||||
|
},
|
||||||
|
trailingIcon = if (selectedValue == entryValues[index]) {
|
||||||
|
{
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Check,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(18.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
|
@Composable
|
||||||
|
fun CompactSliderSettingItem(
|
||||||
|
title: String,
|
||||||
|
value: Float,
|
||||||
|
valueRange: ClosedFloatingPointRange<Float>,
|
||||||
|
steps: Int = 0,
|
||||||
|
description: String? = null,
|
||||||
|
imageVector: ImageVector? = null,
|
||||||
|
color: Color? = MaterialTheme.colorScheme.surface,
|
||||||
|
shape: Shape = MaterialTheme.shapes.small,
|
||||||
|
onValueChange: (Float) -> Unit
|
||||||
|
) {
|
||||||
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
var sliderValue by remember(value) { mutableFloatStateOf(value) }
|
||||||
|
|
||||||
|
SettingItem(
|
||||||
|
title = title,
|
||||||
|
description = description,
|
||||||
|
imageVector = imageVector,
|
||||||
|
color = color,
|
||||||
|
shape = shape,
|
||||||
|
expanded = expanded,
|
||||||
|
onExpandChange = { expanded = it },
|
||||||
|
trailingContent = {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
SmallOutlinedIconButton(
|
||||||
|
onClick = {
|
||||||
|
val newValue = (value.toInt() - 1).toFloat().coerceIn(valueRange)
|
||||||
|
onValueChange(newValue)
|
||||||
|
},
|
||||||
|
icon = Icons.Default.Remove
|
||||||
|
)
|
||||||
|
TextCard(
|
||||||
|
cornerRadius = 8.dp,
|
||||||
|
horizontalPadding = 8.dp,
|
||||||
|
verticalPadding = 4.dp,
|
||||||
|
text = value.toInt().toString(),
|
||||||
|
backgroundColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
SmallOutlinedIconButton(
|
||||||
|
onClick = {
|
||||||
|
val newValue = (value.toInt() + 1).toFloat().coerceIn(valueRange)
|
||||||
|
onValueChange(newValue)
|
||||||
|
},
|
||||||
|
icon = Icons.Default.Add
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
expandContent = {
|
||||||
|
Slider(
|
||||||
|
value = sliderValue,
|
||||||
|
onValueChange = {
|
||||||
|
sliderValue = it
|
||||||
|
onValueChange(it)
|
||||||
|
},
|
||||||
|
valueRange = valueRange,
|
||||||
|
steps = steps,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -9,9 +9,8 @@
|
|||||||
|
|
||||||
<string-array name="group_style">
|
<string-array name="group_style">
|
||||||
<item>标签</item>
|
<item>标签</item>
|
||||||
<item>文件夹</item>
|
|
||||||
<item>隐藏标签</item>
|
<item>隐藏标签</item>
|
||||||
<item>材料标签</item>
|
<item>文件夹</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
<string-array name="text_suffix">
|
<string-array name="text_suffix">
|
||||||
@@ -132,6 +131,14 @@
|
|||||||
<item>@string/bookshelf_px_5</item>
|
<item>@string/bookshelf_px_5</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
|
<string-array name="bookshelf_layout">
|
||||||
|
<item>@string/list</item>
|
||||||
|
<item>@string/compact_list</item>
|
||||||
|
<item>@string/grid</item>
|
||||||
|
<item>@string/compact_grid</item>
|
||||||
|
<item>@string/cover_grid</item>
|
||||||
|
</string-array>
|
||||||
|
|
||||||
<string-array name="themes_item">
|
<string-array name="themes_item">
|
||||||
<item>动态取色</item>
|
<item>动态取色</item>
|
||||||
<item>草野</item>
|
<item>草野</item>
|
||||||
|
|||||||
@@ -10,9 +10,8 @@
|
|||||||
|
|
||||||
<string-array name="group_style">
|
<string-array name="group_style">
|
||||||
<item>標籤</item>
|
<item>標籤</item>
|
||||||
<item>文件夾</item>
|
|
||||||
<item>隐藏標籤</item>
|
<item>隐藏標籤</item>
|
||||||
<item>材料標籤</item>
|
<item>文件夾</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
<string-array name="theme_mode">
|
<string-array name="theme_mode">
|
||||||
|
|||||||
@@ -9,9 +9,8 @@
|
|||||||
|
|
||||||
<string-array name="group_style">
|
<string-array name="group_style">
|
||||||
<item>標籤</item>
|
<item>標籤</item>
|
||||||
<item>資料夾</item>
|
|
||||||
<item>隐藏標籤</item>
|
<item>隐藏標籤</item>
|
||||||
<item>材料標籤</item>
|
<item>資料夾</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
<string-array name="text_suffix">
|
<string-array name="text_suffix">
|
||||||
|
|||||||
@@ -14,9 +14,8 @@
|
|||||||
|
|
||||||
<string-array name="group_style">
|
<string-array name="group_style">
|
||||||
<item>Tab</item>
|
<item>Tab</item>
|
||||||
<item>Folder</item>
|
|
||||||
<item>Hide Tab</item>
|
<item>Hide Tab</item>
|
||||||
<item>Material Tab</item>
|
<item>Folder</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
<string-array name="book_sort">
|
<string-array name="book_sort">
|
||||||
@@ -279,6 +278,14 @@
|
|||||||
<item>@string/bookshelf_px_5</item>
|
<item>@string/bookshelf_px_5</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
|
<string-array name="bookshelf_layout">
|
||||||
|
<item>@string/list</item>
|
||||||
|
<item>@string/compact_list</item>
|
||||||
|
<item>@string/grid</item>
|
||||||
|
<item>@string/compact_grid</item>
|
||||||
|
<item>@string/cover_grid</item>
|
||||||
|
</string-array>
|
||||||
|
|
||||||
<string-array name="label_vis_mode" >
|
<string-array name="label_vis_mode" >
|
||||||
<item>Auto</item>
|
<item>Auto</item>
|
||||||
<item>Selected</item>
|
<item>Selected</item>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ ksp = "2.3.4"
|
|||||||
agp = "8.13.2"
|
agp = "8.13.2"
|
||||||
appcompat = "1.7.1"
|
appcompat = "1.7.1"
|
||||||
colorpicker = "1.1.0"
|
colorpicker = "1.1.0"
|
||||||
|
colorpicker-compose = "1.1.3"
|
||||||
commonsText = "1.15.0"
|
commonsText = "1.15.0"
|
||||||
constraintlayout = "2.2.1"
|
constraintlayout = "2.2.1"
|
||||||
core = "1.17.0"
|
core = "1.17.0"
|
||||||
@@ -209,6 +210,7 @@ androidx-recyclerview = { module = "androidx.recyclerview:recyclerview", version
|
|||||||
androidx-viewpager2 = { module = "androidx.viewpager2:viewpager2", version.ref = "viewpager2" }
|
androidx-viewpager2 = { module = "androidx.viewpager2:viewpager2", version.ref = "viewpager2" }
|
||||||
androidx-webkit = { module = "androidx.webkit:webkit", version.ref = "webkit" }
|
androidx-webkit = { module = "androidx.webkit:webkit", version.ref = "webkit" }
|
||||||
colorpicker = { module = "com.jaredrummler:colorpicker", version.ref = "colorpicker" }
|
colorpicker = { module = "com.jaredrummler:colorpicker", version.ref = "colorpicker" }
|
||||||
|
colorpicker-compose = { module = "com.github.skydoves:colorpicker-compose", version.ref = "colorpicker-compose" }
|
||||||
commons-text = { module = "org.apache.commons:commons-text", version.ref = "commonsText" }
|
commons-text = { module = "org.apache.commons:commons-text", version.ref = "commonsText" }
|
||||||
desugar = { group = "com.android.tools", name = "desugar_jdk_libs_nio", version.ref = "desugar" }
|
desugar = { group = "com.android.tools", name = "desugar_jdk_libs_nio", version.ref = "desugar" }
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user