[新增] 重写了主页面和书架界面、封面设置界面,现在支持底栏模糊,更多的封面样式、封面阴影、随机默认封面等功能(在高级 - 测试 - 封面设置中应用)。
This commit is contained in:
@@ -228,6 +228,7 @@ dependencies {
|
||||
implementation(libs.nanohttpd.websocket)
|
||||
implementation(libs.zxing.lite)
|
||||
implementation(libs.colorpicker)
|
||||
implementation(libs.colorpicker.compose)
|
||||
implementation(libs.libarchive)
|
||||
implementation(libs.commons.text)
|
||||
implementation(libs.markwon.core)
|
||||
@@ -236,6 +237,7 @@ dependencies {
|
||||
implementation(libs.markwon.html)
|
||||
implementation(libs.quick.chinese.transfer.core)
|
||||
implementation(libs.hutool.crypto)
|
||||
//noinspection GradleDependency
|
||||
implementation(platform(libs.firebase.bom))
|
||||
implementation(libs.firebase.analytics)
|
||||
implementation(libs.firebase.perf)
|
||||
|
||||
@@ -37,4 +37,7 @@ object EventBus {
|
||||
const val REFRESH_BOOK_INFO = "refreshBookInfo"
|
||||
const val REFRESH_BOOK_CONTENT = "refreshBookContent"
|
||||
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 coverShowNameN = "coverShowNameN"
|
||||
const val coverShowAuthorN = "coverShowAuthorN"
|
||||
const val coverInfoOrientation = "coverInfoOrientation"
|
||||
const val remoteServerId = "remoteServerId"
|
||||
const val hideStatusBar = "hideStatusBar"
|
||||
const val clickActionTL = "clickActionTopLeft"
|
||||
@@ -50,6 +51,7 @@ object PreferKey {
|
||||
const val bookshelfLayout = "bookshelfLayout"
|
||||
const val bookshelfLayoutModePortrait = "bookshelfLayoutPortrait"
|
||||
const val bookshelfLayoutModeLandscape = "bookshelf_layout_landscape"
|
||||
const val bookshelfLayoutCompact = "bookshelfLayoutCompact"
|
||||
const val bookshelfSort = "bookshelfSort"
|
||||
const val bookExportFileName = "bookExportFileName"
|
||||
const val bookImportFileName = "bookImportFileName"
|
||||
@@ -222,6 +224,10 @@ object PreferKey {
|
||||
const val sharedElementEnterTransitionEnable = "sharedElementEnterTransitionEnable"
|
||||
const val bookshelfLayoutGridLandscape = "bookshelfLayoutGridLandscape"
|
||||
const val bookshelfLayoutGridPortrait = "bookshelfLayoutGridPortrait"
|
||||
const val bookshelfTitleSmallFont = "bookshelfTitleSmallFont"
|
||||
const val bookshelfTitleCenter = "bookshelfTitleCenter"
|
||||
const val bookshelfTitleMaxLines = "bookshelfTitleMaxLines"
|
||||
const val bookshelfCoverShadow = "bookshelfCoverShadow"
|
||||
|
||||
const val exploreLayoutGridLandscape = "exploreLayoutGridLandscape"
|
||||
|
||||
|
||||
@@ -348,6 +348,9 @@ data class Book(
|
||||
newBook.customIntro = customIntro
|
||||
newBook.customTag = customTag
|
||||
newBook.canUpdate = canUpdate
|
||||
if (config.fixedType) {
|
||||
newBook.type = type
|
||||
}
|
||||
newBook.readConfig = readConfig
|
||||
return newBook
|
||||
}
|
||||
@@ -402,7 +405,9 @@ data class Book(
|
||||
val mangaColorFilter: String? = null,
|
||||
var mangaScrollMode: Int? = null,
|
||||
var webtoonSidePaddingDp: Int? = null,
|
||||
var mangaBackground: String? = null
|
||||
var mangaBackground: String? = null,
|
||||
|
||||
var fixedType: Boolean = false // 固定书籍类型,不随书源更新
|
||||
|
||||
) : Parcelable
|
||||
|
||||
@@ -414,4 +419,4 @@ data class Book(
|
||||
@TypeConverter
|
||||
fun stringToReadConfig(json: String?) = GSON.fromJsonObject<ReadConfig>(json).getOrNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,15 +20,19 @@ import io.legado.app.help.http.okHttpClient
|
||||
import io.legado.app.help.http.okHttpClientManga
|
||||
import io.legado.app.ui.book.bookmark.AllBookmarkViewModel
|
||||
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.info.BookInfoViewModel
|
||||
import io.legado.app.ui.book.readRecord.ReadRecordViewModel
|
||||
import io.legado.app.ui.book.searchContent.SearchContentViewModel
|
||||
import io.legado.app.ui.book.toc.TocViewModel
|
||||
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.readConfig.ReadConfigViewModel
|
||||
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.my.MyViewModel
|
||||
import io.legado.app.ui.main.rss.RssViewModel
|
||||
@@ -76,11 +80,15 @@ val appModule = module {
|
||||
viewModelOf(::ReadRecordViewModel)
|
||||
viewModelOf(::ExploreShowViewModel)
|
||||
viewModelOf(::MyViewModel)
|
||||
viewModelOf(::BookshelfViewModel)
|
||||
viewModelOf(::MainViewModel)
|
||||
viewModelOf(::GroupViewModel)
|
||||
viewModelOf(::ReplaceRuleViewModel)
|
||||
viewModelOf(::AllBookmarkViewModel)
|
||||
viewModelOf(::TxtTocRuleViewModel)
|
||||
viewModelOf(::OtherConfigViewModel)
|
||||
viewModelOf(::ReadConfigViewModel)
|
||||
viewModelOf(::CoverConfigViewModel)
|
||||
viewModelOf(::TocViewModel)
|
||||
viewModelOf(::RemoteBookViewModel)
|
||||
viewModelOf(::BookInfoViewModel)
|
||||
|
||||
@@ -339,6 +339,9 @@ fun Book.updateTo(newBook: Book): Book {
|
||||
newBook.customIntro = customIntro
|
||||
newBook.customTag = customTag
|
||||
newBook.canUpdate = canUpdate
|
||||
if (config.fixedType) {
|
||||
newBook.type = type
|
||||
}
|
||||
newBook.readConfig = readConfig
|
||||
val variableMap = variableMap.toMutableMap()
|
||||
variableMap.keys.removeIf {
|
||||
|
||||
@@ -41,6 +41,7 @@ import io.legado.app.utils.getPrefString
|
||||
import kotlinx.coroutines.currentCoroutineContext
|
||||
import splitties.init.appCtx
|
||||
import java.io.File
|
||||
import kotlin.random.Random
|
||||
|
||||
@Keep
|
||||
object BookCover {
|
||||
@@ -101,16 +102,32 @@ object BookCover {
|
||||
appCtx.getPrefInt(PreferKey.coverShadowColorN, Color.BLACK)
|
||||
|
||||
val key = if (isNightTheme) PreferKey.defaultCoverDark else PreferKey.defaultCover
|
||||
val path = appCtx.getPrefString(key)
|
||||
if (path.isNullOrBlank()) {
|
||||
val paths = appCtx.getPrefString(key)?.split(",")?.filter { it.isNotBlank() }
|
||||
|
||||
if (paths.isNullOrEmpty()) {
|
||||
defaultDrawable = appCtx.resources.getDrawable(R.drawable.image_cover_default, null)
|
||||
return
|
||||
}
|
||||
|
||||
val randomPath = paths[Random.nextInt(paths.size)]
|
||||
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))
|
||||
}
|
||||
|
||||
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,
|
||||
onLoadFinish: (() -> Unit)? = null,
|
||||
): RequestBuilder<Drawable> {
|
||||
val currentDefault = getRandomDefaultDrawable()
|
||||
if (AppConfig.useDefaultCover) {
|
||||
return ImageLoader.load(context, defaultDrawable)
|
||||
return ImageLoader.load(context, currentDefault)
|
||||
.centerCrop()
|
||||
}
|
||||
var options = RequestOptions().set(OkHttpModelLoader.loadOnlyWifiOption, loadOnlyWifi)
|
||||
@@ -156,8 +174,8 @@ object BookCover {
|
||||
}
|
||||
})
|
||||
}
|
||||
return builder.placeholder(defaultDrawable)
|
||||
.error(defaultDrawable)
|
||||
return builder.placeholder(currentDefault)
|
||||
.error(currentDefault)
|
||||
.centerCrop()
|
||||
}
|
||||
|
||||
@@ -222,7 +240,8 @@ object BookCover {
|
||||
loadOnlyWifi: Boolean = false,
|
||||
sourceOrigin: String? = null,
|
||||
): RequestBuilder<Drawable> {
|
||||
val loadBlur = ImageLoader.load(context, defaultDrawable)
|
||||
val currentDefault = getRandomDefaultDrawable()
|
||||
val loadBlur = ImageLoader.load(context, currentDefault)
|
||||
.transform(BlurTransformation(25), CenterCrop())
|
||||
if (AppConfig.useDefaultCover) {
|
||||
return loadBlur
|
||||
@@ -302,4 +321,4 @@ object BookCover {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,8 +162,10 @@ object WebBook {
|
||||
book: Book,
|
||||
canReName: Boolean = true,
|
||||
): Book {
|
||||
book.removeAllBookType()
|
||||
book.addType(bookSource.getBookType())
|
||||
if (!book.config.fixedType) {
|
||||
book.removeAllBookType()
|
||||
book.addType(bookSource.getBookType())
|
||||
}
|
||||
if (!book.infoHtml.isNullOrEmpty()) {
|
||||
BookInfo.analyzeBookInfo(
|
||||
bookSource = bookSource,
|
||||
@@ -235,8 +237,10 @@ object WebBook {
|
||||
book: Book,
|
||||
runPerJs: Boolean = false
|
||||
): Result<List<BookChapter>> {
|
||||
book.removeAllBookType()
|
||||
book.addType(bookSource.getBookType())
|
||||
if (!book.config.fixedType) {
|
||||
book.removeAllBookType()
|
||||
book.addType(bookSource.getBookType())
|
||||
}
|
||||
return kotlin.runCatching {
|
||||
if (runPerJs) {
|
||||
runPreUpdateJs(bookSource, book).getOrThrow()
|
||||
@@ -420,4 +424,4 @@ object WebBook {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,7 +86,6 @@ import io.legado.app.model.BookShelfState
|
||||
import io.legado.app.ui.theme.responsiveHazeEffect
|
||||
import io.legado.app.ui.theme.responsiveHazeSource
|
||||
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.GlassTopAppBarDefaults
|
||||
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.TopbarNavigationButton
|
||||
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.RoundDropdownMenuItem
|
||||
import io.legado.app.ui.widget.components.modalBottomSheet.GlassModalBottomSheet
|
||||
@@ -599,21 +599,22 @@ fun ExploreBookItem(
|
||||
if (!latestChapter.isNullOrEmpty()) {
|
||||
Text(
|
||||
text = " • ",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = Color.Gray,
|
||||
maxLines = 1
|
||||
)
|
||||
|
||||
Text(
|
||||
text = "最新: $latestChapter",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = Color.Gray,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
val intro = book.intro?.replace("\\s+".toRegex(), "") ?: ""
|
||||
if (intro.isNotEmpty()) {
|
||||
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 io.legado.app.R
|
||||
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.readConfig.ReadConfigScreen
|
||||
import io.legado.app.ui.widget.components.GlassMediumFlexibleTopAppBar
|
||||
@@ -38,6 +39,9 @@ class TestConfigActivity : BaseComposeActivity() {
|
||||
@Serializable
|
||||
object ReadConfigRoute
|
||||
|
||||
@Serializable
|
||||
object CoverConfigRoute
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val navController = rememberNavController()
|
||||
@@ -50,7 +54,8 @@ class TestConfigActivity : BaseComposeActivity() {
|
||||
ConfigNavScreen(
|
||||
onBackClick = { finish() },
|
||||
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> {
|
||||
ReadConfigScreen(onBackClick = { navController.popBackStack() })
|
||||
}
|
||||
|
||||
composable<CoverConfigRoute> {
|
||||
CoverConfigScreen(onBackClick = { navController.popBackStack() })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +78,8 @@ class TestConfigActivity : BaseComposeActivity() {
|
||||
fun ConfigNavScreen(
|
||||
onBackClick: () -> Unit,
|
||||
onNavigateToOther: () -> Unit,
|
||||
onNavigateToRead: () -> Unit
|
||||
onNavigateToRead: () -> Unit,
|
||||
onNavigateToCover: () -> Unit
|
||||
) {
|
||||
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
|
||||
|
||||
@@ -101,6 +111,10 @@ class TestConfigActivity : BaseComposeActivity() {
|
||||
title = stringResource(R.string.read_config),
|
||||
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
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.format.DateUtils
|
||||
import android.view.Gravity
|
||||
import android.view.MenuItem
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageView
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.viewModels
|
||||
import androidx.compose.runtime.Composable
|
||||
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.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.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.EventBus
|
||||
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.book.BookHelp
|
||||
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.UpdateDialog
|
||||
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.widget.dialog.TextDialog
|
||||
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.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.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
/**
|
||||
* 主界面
|
||||
*/
|
||||
open class MainActivity : VMBaseActivity<ActivityMainBinding, MainViewModel>(),
|
||||
BottomNavigationView.OnNavigationItemSelectedListener,
|
||||
BottomNavigationView.OnNavigationItemReselectedListener {
|
||||
open class MainActivity : BaseComposeActivity() {
|
||||
|
||||
override val binding by viewBinding(ActivityMainBinding::inflate)
|
||||
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)
|
||||
}
|
||||
private val viewModel by viewModel<MainViewModel>()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
installSplashScreen()
|
||||
super.onCreate(savedInstanceState)
|
||||
toggleSystemBar(AppConfig.showStatusBar)
|
||||
if (checkStartupRoute()) return
|
||||
setExitSharedElementCallback(MaterialContainerTransformSharedElementCallback())
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
pagePosition = savedInstanceState.getInt("currentPagePosition", 0)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S)
|
||||
binding.viewPagerMain.fitsSystemWindows = true
|
||||
// 其他初始化逻辑
|
||||
setupBackCallback()
|
||||
if (checkStartupRoute()) return
|
||||
|
||||
// 智能自启:如果上次是手动开启状态(web_service_auto 为 true),则自启
|
||||
if (AppConfig.webServiceAutoStart) {
|
||||
WebService.startForeground(this)
|
||||
}
|
||||
|
||||
upBottomMenu()
|
||||
initView()
|
||||
upHomePage()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
toggleSystemBar(AppConfig.showStatusBar)
|
||||
}
|
||||
|
||||
override fun onPostCreate(savedInstanceState: Bundle?) {
|
||||
super.onPostCreate(savedInstanceState)
|
||||
lifecycleScope.launch {
|
||||
//版本更新
|
||||
upVersion()
|
||||
@@ -164,51 +63,27 @@ open class MainActivity : VMBaseActivity<ActivityMainBinding, MainViewModel>(),
|
||||
//自动更新书籍
|
||||
val isAutoRefreshedBook = savedInstanceState?.getBoolean("isAutoRefreshedBook") ?: false
|
||||
if (AppConfig.autoRefreshBook && !isAutoRefreshedBook) {
|
||||
binding.viewPagerMain.postDelayed(1000) {
|
||||
viewModel.upAllBookToc()
|
||||
}
|
||||
}
|
||||
binding.viewPagerMain.postDelayed(3000) {
|
||||
viewModel.postLoad()
|
||||
viewModel.upAllBookToc()
|
||||
}
|
||||
viewModel.postLoad()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNavigationItemSelected(item: MenuItem): Boolean {
|
||||
val index = when (item.itemId) {
|
||||
R.id.menu_bookshelf -> 0
|
||||
R.id.menu_discovery -> realPositions.indexOf(idExplore)
|
||||
R.id.menu_rss -> realPositions.indexOf(idRss)
|
||||
R.id.menu_my_config -> realPositions.indexOf(idMy)
|
||||
else -> 0
|
||||
}
|
||||
binding.viewPagerMain.setCurrentItem(index, swipeAnimation)
|
||||
return true
|
||||
}
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val orientation = resources.configuration.orientation
|
||||
val smallestWidthDp = resources.configuration.smallestScreenWidthDp
|
||||
val tabletInterface = AppConfig.tabletInterface
|
||||
|
||||
override fun onNavigationItemReselected(item: MenuItem) {
|
||||
when (item.itemId) {
|
||||
R.id.menu_bookshelf -> {
|
||||
val fragment = fragmentMap[getFragmentId(0)] as? BookshelfFragment2
|
||||
if (fragment != null && fragment.groupId != BookGroup.IdRoot) {
|
||||
fragment.back()
|
||||
} 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()
|
||||
}
|
||||
}
|
||||
val useRail = when (tabletInterface) {
|
||||
"always" -> true
|
||||
"landscape" -> orientation == Configuration.ORIENTATION_LANDSCAPE
|
||||
"off" -> false
|
||||
"auto" -> smallestWidthDp >= 600
|
||||
else -> false
|
||||
}
|
||||
|
||||
MainScreen(useRail = useRail)
|
||||
}
|
||||
|
||||
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() {
|
||||
if (!LocalConfig.appCrash || BuildConfig.DEBUG) {
|
||||
return
|
||||
@@ -316,7 +181,6 @@ open class MainActivity : VMBaseActivity<ActivityMainBinding, MainViewModel>(),
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
outState.putInt("currentPagePosition", pagePosition)
|
||||
if (AppConfig.autoRefreshBook) {
|
||||
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()
|
||||
|
||||
@@ -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
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.recyclerview.widget.RecyclerView.RecycledViewPool
|
||||
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.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.DefaultData
|
||||
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.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.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 io.legado.app.ui.main.my.PrefClickEvent
|
||||
import io.legado.app.utils.eventBus.FlowEventBus
|
||||
import io.legado.app.utils.sendToClip
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.LinkedList
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.concurrent.Executors
|
||||
import kotlin.math.min
|
||||
|
||||
data class MainUiState(
|
||||
val upBooksCount: Int = 0
|
||||
)
|
||||
|
||||
class MainViewModel(application: Application) : BaseViewModel(application) {
|
||||
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>()
|
||||
val onUpBooksLiveData = MutableLiveData<Int>()
|
||||
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)
|
||||
}
|
||||
|
||||
private val upBooksCountFlow = MutableStateFlow(0)
|
||||
|
||||
val state: StateFlow<MainUiState> = upBooksCountFlow
|
||||
.map { count -> MainUiState(count) }
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), MainUiState())
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
FlowEventBus.with<Int>(EventBus.UP_BOOKSHELF_COUNT).collect {
|
||||
upBooksCountFlow.value = it
|
||||
}
|
||||
}
|
||||
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() {
|
||||
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()
|
||||
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)
|
||||
}
|
||||
FlowEventBus.post(EventBus.UP_ALL_BOOK_TOC, Unit)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
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 io.legado.app.R
|
||||
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.data.appDb
|
||||
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.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.coroutine.Coroutine
|
||||
import io.legado.app.help.http.decompressed
|
||||
import io.legado.app.help.http.newCallResponseBody
|
||||
import io.legado.app.help.http.okHttpClient
|
||||
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.service.CacheBookService
|
||||
import io.legado.app.ui.config.bookshelfConfig.BookshelfConfig
|
||||
import io.legado.app.utils.FileUtils
|
||||
import io.legado.app.utils.GSON
|
||||
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.isAbsUrl
|
||||
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.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.withPermit
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
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) {
|
||||
val addBookProgressLiveData = MutableLiveData(-1)
|
||||
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) {
|
||||
var successCount = 0
|
||||
loadingTextFlow.value = "添加中..."
|
||||
addBookJob = execute {
|
||||
val hasBookUrlPattern: List<BookSourcePart> by lazy {
|
||||
appDb.bookSourceDao.hasBookUrlPattern
|
||||
@@ -83,7 +457,7 @@ class BookshelfViewModel(application: Application) : BaseViewModel(application)
|
||||
it.save()
|
||||
}
|
||||
successCount++
|
||||
addBookProgressLiveData.postValue(successCount)
|
||||
loadingTextFlow.value = "添加中... ($successCount)"
|
||||
}
|
||||
}
|
||||
}.onSuccess {
|
||||
@@ -95,7 +469,7 @@ class BookshelfViewModel(application: Application) : BaseViewModel(application)
|
||||
}.onError {
|
||||
AppLog.put("添加网址出错\n${it.localizedMessage}", it, true)
|
||||
}.onFinally {
|
||||
addBookProgressLiveData.postValue(-1)
|
||||
loadingTextFlow.value = null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,6 +528,7 @@ class BookshelfViewModel(application: Application) : BaseViewModel(application)
|
||||
}
|
||||
|
||||
private fun importBookshelfByJson(json: String, groupId: Long) {
|
||||
loadingTextFlow.value = "导入中..."
|
||||
execute {
|
||||
val bookSourceParts = appDb.bookSourceDao.allEnabledPart
|
||||
val semaphore = Semaphore(AppConfig.threadCount)
|
||||
@@ -181,6 +556,7 @@ class BookshelfViewModel(application: Application) : BaseViewModel(application)
|
||||
}.onError {
|
||||
it.printOnDebug()
|
||||
}.onFinally {
|
||||
loadingTextFlow.value = null
|
||||
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
|
||||
)
|
||||
}
|
||||
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
|
||||
@@ -493,8 +491,7 @@ fun ExploreKindItem(
|
||||
OutlinedCard(
|
||||
shape = shape,
|
||||
colors = CardDefaults.outlinedCardColors(
|
||||
containerColor = Color.Transparent,
|
||||
contentColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
|
||||
contentColor = MaterialTheme.colorScheme.primary
|
||||
),
|
||||
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.clearExploreKindsCache
|
||||
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.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
@@ -47,4 +47,18 @@ 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.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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+11
-2
@@ -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 androidx.compose.foundation.layout.Arrangement
|
||||
@@ -41,6 +41,7 @@ fun FilePickerSheet(
|
||||
title: String = stringResource(R.string.select_operation),
|
||||
onSelectSysDir: (() -> Unit)? = null,
|
||||
onSelectSysFile: ((Array<String>) -> Unit)? = null,
|
||||
onSelectSysFiles: ((Array<String>) -> Unit)? = null,
|
||||
onManualInput: (() -> Unit)? = null,
|
||||
onUpload: (() -> Unit)? = 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 {
|
||||
FilePickerOptionCard(
|
||||
icon = Icons.Default.EditNote,
|
||||
@@ -160,4 +169,4 @@ private fun typesOfExtensions(allowExtensions: Array<String>?): Array<String> {
|
||||
}
|
||||
}
|
||||
return types.toTypedArray()
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,6 @@ import io.legado.app.ui.theme.responsiveHazeEffect
|
||||
import io.legado.app.ui.theme.responsiveHazeSource
|
||||
import io.legado.app.ui.widget.components.SelectionActions
|
||||
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
|
||||
|
||||
@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> {
|
||||
val items: List<T>
|
||||
+6
-3
@@ -15,6 +15,8 @@ fun ClickableSettingItem(
|
||||
description: String? = null,
|
||||
option: String? = null,
|
||||
imageVector: ImageVector? = null,
|
||||
onLongClick: (() -> Unit)? = null,
|
||||
trailingContent: (@Composable () -> Unit)? = null,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
SettingItem(
|
||||
@@ -23,13 +25,14 @@ fun ClickableSettingItem(
|
||||
description = description,
|
||||
option = option,
|
||||
imageVector = imageVector,
|
||||
trailingContent = {
|
||||
trailingContent = trailingContent ?: {
|
||||
Icon(
|
||||
imageVector = Icons.Default.ChevronRight,
|
||||
contentDescription = null,
|
||||
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">
|
||||
<item>标签</item>
|
||||
<item>文件夹</item>
|
||||
<item>隐藏标签</item>
|
||||
<item>材料标签</item>
|
||||
<item>文件夹</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="text_suffix">
|
||||
@@ -132,6 +131,14 @@
|
||||
<item>@string/bookshelf_px_5</item>
|
||||
</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">
|
||||
<item>动态取色</item>
|
||||
<item>草野</item>
|
||||
|
||||
@@ -10,9 +10,8 @@
|
||||
|
||||
<string-array name="group_style">
|
||||
<item>標籤</item>
|
||||
<item>文件夾</item>
|
||||
<item>隐藏標籤</item>
|
||||
<item>材料標籤</item>
|
||||
<item>文件夾</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="theme_mode">
|
||||
|
||||
@@ -9,9 +9,8 @@
|
||||
|
||||
<string-array name="group_style">
|
||||
<item>標籤</item>
|
||||
<item>資料夾</item>
|
||||
<item>隐藏標籤</item>
|
||||
<item>材料標籤</item>
|
||||
<item>資料夾</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="text_suffix">
|
||||
|
||||
@@ -14,9 +14,8 @@
|
||||
|
||||
<string-array name="group_style">
|
||||
<item>Tab</item>
|
||||
<item>Folder</item>
|
||||
<item>Hide Tab</item>
|
||||
<item>Material Tab</item>
|
||||
<item>Folder</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="book_sort">
|
||||
@@ -279,6 +278,14 @@
|
||||
<item>@string/bookshelf_px_5</item>
|
||||
</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" >
|
||||
<item>Auto</item>
|
||||
<item>Selected</item>
|
||||
|
||||
@@ -20,6 +20,7 @@ ksp = "2.3.4"
|
||||
agp = "8.13.2"
|
||||
appcompat = "1.7.1"
|
||||
colorpicker = "1.1.0"
|
||||
colorpicker-compose = "1.1.3"
|
||||
commonsText = "1.15.0"
|
||||
constraintlayout = "2.2.1"
|
||||
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-webkit = { module = "androidx.webkit:webkit", version.ref = "webkit" }
|
||||
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" }
|
||||
desugar = { group = "com.android.tools", name = "desugar_jdk_libs_nio", version.ref = "desugar" }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user