界面优化
This commit is contained in:
@@ -82,7 +82,8 @@
|
|||||||
"Bash(findstr /i \"miuix.*\\\\.kt\")",
|
"Bash(findstr /i \"miuix.*\\\\.kt\")",
|
||||||
"Bash(findstr /v \"\\\\\\\\build\\\\\\\\\")",
|
"Bash(findstr /v \"\\\\\\\\build\\\\\\\\\")",
|
||||||
"Bash(dir /s /b \"D:\\\\AndroidPrj\\\\legado-with-MD3\\\\.gradle\\\\*miuix*\")",
|
"Bash(dir /s /b \"D:\\\\AndroidPrj\\\\legado-with-MD3\\\\.gradle\\\\*miuix*\")",
|
||||||
"Skill(legado-compose-review)"
|
"Skill(legado-compose-review)",
|
||||||
|
"Bash(cmd /c \".\\\\gradlew.bat :app:compileAppDebugKotlin\")"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,12 +5,17 @@ import io.legado.app.data.dao.BookChapterDao
|
|||||||
import io.legado.app.data.dao.BookDao
|
import io.legado.app.data.dao.BookDao
|
||||||
import io.legado.app.data.entities.Book
|
import io.legado.app.data.entities.Book
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class BookRepository(
|
class BookRepository(
|
||||||
private val bookDao: BookDao,
|
private val bookDao: BookDao,
|
||||||
private val bookChapterDao: BookChapterDao
|
private val bookChapterDao: BookChapterDao
|
||||||
) {
|
) {
|
||||||
|
fun getAllBooks(): Flow<List<Book>> {
|
||||||
|
return bookDao.flowAll()
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun getBookCoverByNameAndAuthor(bookName: String, bookAuthor: String): String? {
|
suspend fun getBookCoverByNameAndAuthor(bookName: String, bookAuthor: String): String? {
|
||||||
return withContext(Dispatchers.IO) {
|
return withContext(Dispatchers.IO) {
|
||||||
bookDao.getBook(bookName, bookAuthor)?.getDisplayCover()
|
bookDao.getBook(bookName, bookAuthor)?.getDisplayCover()
|
||||||
|
|||||||
+17
-2
@@ -1,5 +1,6 @@
|
|||||||
package io.legado.app.domain.usecase.readRecord
|
package io.legado.app.domain.usecase.readRecord
|
||||||
|
|
||||||
|
import io.legado.app.data.entities.Book
|
||||||
import io.legado.app.data.entities.readRecord.ReadRecord
|
import io.legado.app.data.entities.readRecord.ReadRecord
|
||||||
import io.legado.app.data.entities.readRecord.ReadRecordDetail
|
import io.legado.app.data.entities.readRecord.ReadRecordDetail
|
||||||
import io.legado.app.ui.book.readRecord.ReadBookRanking
|
import io.legado.app.ui.book.readRecord.ReadBookRanking
|
||||||
@@ -16,7 +17,8 @@ class GetReadRecordOverviewUseCase {
|
|||||||
period: ReadPeriod,
|
period: ReadPeriod,
|
||||||
refDate: LocalDate,
|
refDate: LocalDate,
|
||||||
details: List<ReadRecordDetail>,
|
details: List<ReadRecordDetail>,
|
||||||
latestRecords: List<ReadRecord>
|
latestRecords: List<ReadRecord>,
|
||||||
|
allBooks: List<Book>
|
||||||
): ReadRecordOverviewUiState {
|
): ReadRecordOverviewUiState {
|
||||||
val (startDate, endDate) = getPeriodRange(period, refDate)
|
val (startDate, endDate) = getPeriodRange(period, refDate)
|
||||||
|
|
||||||
@@ -41,11 +43,24 @@ class GetReadRecordOverviewUseCase {
|
|||||||
val totalBooks = periodBooks.size
|
val totalBooks = periodBooks.size
|
||||||
|
|
||||||
val shelfBooksMap = latestRecords.associateBy { it.bookName to it.bookAuthor }
|
val shelfBooksMap = latestRecords.associateBy { it.bookName to it.bookAuthor }
|
||||||
|
val allShelfBooksMap = allBooks.associateBy { it.name to it.author }
|
||||||
|
|
||||||
var readingCount = 0
|
var readingCount = 0
|
||||||
|
var finishedCount = 0
|
||||||
|
|
||||||
periodBooks.keys.forEach { key ->
|
periodBooks.keys.forEach { key ->
|
||||||
if (shelfBooksMap.containsKey(key)) {
|
if (shelfBooksMap.containsKey(key)) {
|
||||||
readingCount++
|
readingCount++
|
||||||
}
|
}
|
||||||
|
// 判断是否读完
|
||||||
|
allShelfBooksMap[key]?.let { book ->
|
||||||
|
if (book.totalChapterNum > 0 &&
|
||||||
|
book.durChapterIndex >= book.totalChapterNum - 1 &&
|
||||||
|
book.durChapterPos != 0
|
||||||
|
) {
|
||||||
|
finishedCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val topBooks = periodBooks.map { (key, details) ->
|
val topBooks = periodBooks.map { (key, details) ->
|
||||||
@@ -122,7 +137,7 @@ class GetReadRecordOverviewUseCase {
|
|||||||
totalTime = totalTime,
|
totalTime = totalTime,
|
||||||
readingDays = readingDays,
|
readingDays = readingDays,
|
||||||
totalBooks = totalBooks,
|
totalBooks = totalBooks,
|
||||||
finishedBooks = 0, // Placeholder
|
finishedBooks = finishedCount,
|
||||||
readingBooks = readingCount,
|
readingBooks = readingCount,
|
||||||
totalWords = totalWords,
|
totalWords = totalWords,
|
||||||
dailyTimeData = dailyTimeData,
|
dailyTimeData = dailyTimeData,
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ sealed interface BookInfoIntent {
|
|||||||
data object ChangeSourceClick : BookInfoIntent
|
data object ChangeSourceClick : BookInfoIntent
|
||||||
data object ReadRecordClick : BookInfoIntent
|
data object ReadRecordClick : BookInfoIntent
|
||||||
data object RemarkClick : BookInfoIntent
|
data object RemarkClick : BookInfoIntent
|
||||||
|
data class SaveCover(val path: String) : BookInfoIntent
|
||||||
data class ConfirmDelete(val deleteOriginal: Boolean) : BookInfoIntent
|
data class ConfirmDelete(val deleteOriginal: Boolean) : BookInfoIntent
|
||||||
data class UpdateRemark(val remark: String) : BookInfoIntent
|
data class UpdateRemark(val remark: String) : BookInfoIntent
|
||||||
data class SelectGroup(val groupId: Long) : BookInfoIntent
|
data class SelectGroup(val groupId: Long) : BookInfoIntent
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ import io.legado.app.ui.theme.LocalHazeState
|
|||||||
import io.legado.app.ui.theme.ProvideThemeOverride
|
import io.legado.app.ui.theme.ProvideThemeOverride
|
||||||
import io.legado.app.ui.theme.ThemeOverrideState
|
import io.legado.app.ui.theme.ThemeOverrideState
|
||||||
import io.legado.app.ui.theme.ThemeResolver
|
import io.legado.app.ui.theme.ThemeResolver
|
||||||
|
import io.legado.app.ui.theme.fadingEdge
|
||||||
import io.legado.app.ui.theme.rememberImageSeedColor
|
import io.legado.app.ui.theme.rememberImageSeedColor
|
||||||
import io.legado.app.ui.theme.rememberThemeOverride
|
import io.legado.app.ui.theme.rememberThemeOverride
|
||||||
import io.legado.app.ui.theme.responsiveHazeEffectFixedStyle
|
import io.legado.app.ui.theme.responsiveHazeEffectFixedStyle
|
||||||
@@ -737,8 +738,12 @@ private fun BookInfoHeader(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (kindLabels.isNotEmpty() || !groupNames.isNullOrBlank()) {
|
if (kindLabels.isNotEmpty() || !groupNames.isNullOrBlank()) {
|
||||||
|
val kindListState = rememberLazyListState()
|
||||||
LazyRow(
|
LazyRow(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
state = kindListState,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.fadingEdge(kindListState),
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
) {
|
) {
|
||||||
groupNames?.takeIf { it.isNotBlank() }?.let {
|
groupNames?.takeIf { it.isNotBlank() }?.let {
|
||||||
@@ -963,101 +968,106 @@ private fun BookInfoDialogs(
|
|||||||
var deleteOriginal by remember(dialog, state.deleteOriginal) { mutableStateOf(state.deleteOriginal) }
|
var deleteOriginal by remember(dialog, state.deleteOriginal) { mutableStateOf(state.deleteOriginal) }
|
||||||
var remarkText by remember(dialog) { mutableStateOf((dialog as? BookInfoDialog.EditRemark)?.remark.orEmpty()) }
|
var remarkText by remember(dialog) { mutableStateOf((dialog as? BookInfoDialog.EditRemark)?.remark.orEmpty()) }
|
||||||
|
|
||||||
if (dialog is BookInfoDialog.DeleteBook) {
|
AppAlertDialog(
|
||||||
AppAlertDialog(
|
data = dialog as? BookInfoDialog.DeleteBook,
|
||||||
show = true,
|
onDismissRequest = { onIntent(BookInfoIntent.DismissDialog) },
|
||||||
onDismissRequest = { onIntent(BookInfoIntent.DismissDialog) },
|
title = stringResource(R.string.draw),
|
||||||
title = stringResource(R.string.draw),
|
text = stringResource(R.string.sure_del),
|
||||||
text = stringResource(R.string.sure_del),
|
confirmText = stringResource(android.R.string.ok),
|
||||||
confirmText = stringResource(android.R.string.ok),
|
onConfirm = {
|
||||||
onConfirm = {
|
onIntent(BookInfoIntent.ConfirmDelete(deleteOriginal))
|
||||||
onIntent(BookInfoIntent.ConfirmDelete(deleteOriginal))
|
},
|
||||||
},
|
dismissText = stringResource(android.R.string.cancel),
|
||||||
dismissText = stringResource(android.R.string.cancel),
|
onDismiss = { onIntent(BookInfoIntent.DismissDialog) },
|
||||||
onDismiss = { onIntent(BookInfoIntent.DismissDialog) },
|
content = { d ->
|
||||||
content = {
|
if (d.isLocal) {
|
||||||
if (dialog.isLocal) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
androidx.compose.material3.Checkbox(
|
||||||
androidx.compose.material3.Checkbox(
|
checked = deleteOriginal,
|
||||||
checked = deleteOriginal,
|
onCheckedChange = { deleteOriginal = it },
|
||||||
onCheckedChange = { deleteOriginal = it },
|
colors = androidx.compose.material3.CheckboxDefaults.colors(
|
||||||
colors = androidx.compose.material3.CheckboxDefaults.colors(
|
checkedColor = LegadoTheme.colorScheme.primary,
|
||||||
checkedColor = LegadoTheme.colorScheme.primary,
|
checkmarkColor = LegadoTheme.colorScheme.onPrimary,
|
||||||
checkmarkColor = LegadoTheme.colorScheme.onPrimary,
|
uncheckedColor = LegadoTheme.colorScheme.onSurfaceVariant,
|
||||||
uncheckedColor = LegadoTheme.colorScheme.onSurfaceVariant,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
Text(text = stringResource(R.string.delete_book_file))
|
)
|
||||||
}
|
Text(text = stringResource(R.string.delete_book_file))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
}
|
)
|
||||||
|
|
||||||
if (dialog is BookInfoDialog.EditRemark) {
|
AppAlertDialog(
|
||||||
AppAlertDialog(
|
data = dialog as? BookInfoDialog.EditRemark,
|
||||||
show = true,
|
onDismissRequest = { onIntent(BookInfoIntent.DismissDialog) },
|
||||||
onDismissRequest = { onIntent(BookInfoIntent.DismissDialog) },
|
title = stringResource(R.string.edit_remark),
|
||||||
title = stringResource(R.string.edit_remark),
|
confirmText = stringResource(android.R.string.ok),
|
||||||
confirmText = stringResource(android.R.string.ok),
|
onConfirm = { onIntent(BookInfoIntent.UpdateRemark(remarkText)) },
|
||||||
onConfirm = { onIntent(BookInfoIntent.UpdateRemark(remarkText)) },
|
dismissText = stringResource(android.R.string.cancel),
|
||||||
dismissText = stringResource(android.R.string.cancel),
|
onDismiss = { onIntent(BookInfoIntent.DismissDialog) },
|
||||||
onDismiss = { onIntent(BookInfoIntent.DismissDialog) },
|
content = {
|
||||||
content = {
|
AppTextField(
|
||||||
AppTextField(
|
value = remarkText,
|
||||||
value = remarkText,
|
onValueChange = { remarkText = it },
|
||||||
onValueChange = { remarkText = it },
|
label = stringResource(R.string.book_remark),
|
||||||
label = "备注",
|
modifier = Modifier.fillMaxWidth(),
|
||||||
modifier = Modifier.fillMaxWidth(),
|
)
|
||||||
)
|
}
|
||||||
}
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dialog is BookInfoDialog.UnsupportedWebFile) {
|
val unsupportedWebFile = dialog as? BookInfoDialog.UnsupportedWebFile
|
||||||
AppAlertDialog(
|
AppAlertDialog(
|
||||||
show = true,
|
data = unsupportedWebFile,
|
||||||
onDismissRequest = { onIntent(BookInfoIntent.DismissDialog) },
|
onDismissRequest = { onIntent(BookInfoIntent.DismissDialog) },
|
||||||
title = stringResource(R.string.draw),
|
title = stringResource(R.string.draw),
|
||||||
text = stringResource(R.string.file_not_supported, dialog.webFile.name),
|
text = unsupportedWebFile?.let {
|
||||||
confirmText = stringResource(R.string.open_fun),
|
stringResource(
|
||||||
onConfirm = { onIntent(BookInfoIntent.OpenUnsupportedWebFile(dialog.webFile)) },
|
R.string.file_not_supported,
|
||||||
dismissText = stringResource(android.R.string.cancel),
|
it.webFile.name
|
||||||
onDismiss = { onIntent(BookInfoIntent.DismissDialog) },
|
)
|
||||||
)
|
},
|
||||||
}
|
confirmText = stringResource(R.string.open_fun),
|
||||||
|
onConfirm = { onIntent(BookInfoIntent.OpenUnsupportedWebFile(it.webFile)) },
|
||||||
|
dismissText = stringResource(android.R.string.cancel),
|
||||||
|
onDismiss = { onIntent(BookInfoIntent.DismissDialog) },
|
||||||
|
)
|
||||||
|
|
||||||
if (dialog is BookInfoDialog.PhotoPreview) {
|
AppAlertDialog(
|
||||||
AppAlertDialog(
|
data = dialog as? BookInfoDialog.PhotoPreview,
|
||||||
show = true,
|
onDismissRequest = { onIntent(BookInfoIntent.DismissDialog) },
|
||||||
onDismissRequest = { onIntent(BookInfoIntent.DismissDialog) },
|
title = stringResource(R.string.img_cover),
|
||||||
title = stringResource(R.string.img_cover),
|
confirmText = "保存到相册",
|
||||||
confirmText = stringResource(android.R.string.ok),
|
onConfirm = { d ->
|
||||||
onConfirm = { onIntent(BookInfoIntent.DismissDialog) },
|
onIntent(BookInfoIntent.SaveCover(d.path))
|
||||||
content = {
|
onIntent(BookInfoIntent.DismissDialog)
|
||||||
AsyncImage(
|
},
|
||||||
model = dialog.path,
|
dismissText = stringResource(android.R.string.cancel),
|
||||||
contentDescription = null,
|
onDismiss = { onIntent(BookInfoIntent.DismissDialog) },
|
||||||
|
content = { d ->
|
||||||
|
Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||||
|
CoilBookCover(
|
||||||
|
name = state.book?.name,
|
||||||
|
author = state.book?.author,
|
||||||
|
path = d.path,
|
||||||
|
sourceOrigin = state.book?.origin,
|
||||||
|
ignoreUseDefaultCover = true,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.heightIn(max = 420.dp)
|
||||||
.heightIn(max = 420.dp),
|
.fillMaxWidth(0.6f)
|
||||||
contentScale = ContentScale.Fit,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
}
|
)
|
||||||
|
|
||||||
if (state.isBusy) {
|
AppAlertDialog(
|
||||||
AppAlertDialog(
|
show = state.isBusy,
|
||||||
show = true,
|
onDismissRequest = {},
|
||||||
onDismissRequest = {},
|
content = {
|
||||||
content = {
|
Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
||||||
Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.Center) {
|
AppCircularProgressIndicator()
|
||||||
AppCircularProgressIndicator()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
}
|
)
|
||||||
|
|
||||||
AppLogSheet(show = state.showAppLogSheet, onDismissRequest = { onIntent(BookInfoIntent.DismissAppLogSheet) })
|
AppLogSheet(show = state.showAppLogSheet, onDismissRequest = { onIntent(BookInfoIntent.DismissAppLogSheet) })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,12 @@ package io.legado.app.ui.book.info
|
|||||||
import android.app.Activity.RESULT_OK
|
import android.app.Activity.RESULT_OK
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.graphics.Bitmap
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import androidx.core.graphics.drawable.toBitmap
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import coil.imageLoader
|
||||||
|
import coil.request.SuccessResult
|
||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
import io.legado.app.base.BaseViewModel
|
import io.legado.app.base.BaseViewModel
|
||||||
import io.legado.app.constant.AppLog
|
import io.legado.app.constant.AppLog
|
||||||
@@ -16,11 +20,11 @@ import io.legado.app.data.entities.Book
|
|||||||
import io.legado.app.data.entities.BookChapter
|
import io.legado.app.data.entities.BookChapter
|
||||||
import io.legado.app.data.entities.BookSource
|
import io.legado.app.data.entities.BookSource
|
||||||
import io.legado.app.data.entities.readRecord.ReadRecordTimelineDay
|
import io.legado.app.data.entities.readRecord.ReadRecordTimelineDay
|
||||||
import io.legado.app.domain.usecase.ChangeBookSourceUseCase
|
import io.legado.app.data.repository.BookGroupRepository
|
||||||
import io.legado.app.domain.usecase.ChangeSourceMigrationOptions
|
|
||||||
import io.legado.app.data.repository.ReadRecordRepository
|
import io.legado.app.data.repository.ReadRecordRepository
|
||||||
import io.legado.app.data.repository.RemoteBookRepository
|
import io.legado.app.data.repository.RemoteBookRepository
|
||||||
import io.legado.app.data.repository.BookGroupRepository
|
import io.legado.app.domain.usecase.ChangeBookSourceUseCase
|
||||||
|
import io.legado.app.domain.usecase.ChangeSourceMigrationOptions
|
||||||
import io.legado.app.domain.usecase.ClearBookCacheUseCase
|
import io.legado.app.domain.usecase.ClearBookCacheUseCase
|
||||||
import io.legado.app.exception.NoBooksDirException
|
import io.legado.app.exception.NoBooksDirException
|
||||||
import io.legado.app.exception.NoStackTraceException
|
import io.legado.app.exception.NoStackTraceException
|
||||||
@@ -33,7 +37,6 @@ import io.legado.app.help.book.isSameNameAuthor
|
|||||||
import io.legado.app.help.book.isWebFile
|
import io.legado.app.help.book.isWebFile
|
||||||
import io.legado.app.help.book.removeType
|
import io.legado.app.help.book.removeType
|
||||||
import io.legado.app.help.book.updateTo
|
import io.legado.app.help.book.updateTo
|
||||||
import io.legado.app.help.config.AppConfig
|
|
||||||
import io.legado.app.help.config.LocalConfig
|
import io.legado.app.help.config.LocalConfig
|
||||||
import io.legado.app.help.coroutine.Coroutine
|
import io.legado.app.help.coroutine.Coroutine
|
||||||
import io.legado.app.lib.webdav.ObjectNotFoundException
|
import io.legado.app.lib.webdav.ObjectNotFoundException
|
||||||
@@ -45,24 +48,29 @@ import io.legado.app.model.SourceCallBack
|
|||||||
import io.legado.app.model.analyzeRule.AnalyzeUrl
|
import io.legado.app.model.analyzeRule.AnalyzeUrl
|
||||||
import io.legado.app.model.localBook.LocalBook
|
import io.legado.app.model.localBook.LocalBook
|
||||||
import io.legado.app.model.webBook.WebBook
|
import io.legado.app.model.webBook.WebBook
|
||||||
|
import io.legado.app.ui.config.coverConfig.CoverConfig
|
||||||
|
import io.legado.app.ui.widget.components.cover.buildCoverImageRequest
|
||||||
import io.legado.app.utils.ArchiveUtils
|
import io.legado.app.utils.ArchiveUtils
|
||||||
import io.legado.app.utils.ConvertUtils
|
import io.legado.app.utils.ConvertUtils
|
||||||
import io.legado.app.utils.FileDoc
|
import io.legado.app.utils.FileDoc
|
||||||
import io.legado.app.utils.GSON
|
import io.legado.app.utils.GSON
|
||||||
|
import io.legado.app.utils.ImageSaveUtils
|
||||||
import io.legado.app.utils.UrlUtil
|
import io.legado.app.utils.UrlUtil
|
||||||
import io.legado.app.utils.postEvent
|
import io.legado.app.utils.postEvent
|
||||||
|
import io.legado.app.utils.splitNotBlank
|
||||||
import io.legado.app.utils.toastOnUi
|
import io.legado.app.utils.toastOnUi
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.combine
|
|
||||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asSharedFlow
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
|
||||||
class BookInfoViewModel(
|
class BookInfoViewModel(
|
||||||
application: Application,
|
application: Application,
|
||||||
@@ -167,6 +175,9 @@ class BookInfoViewModel(
|
|||||||
BookInfoIntent.ChangeSourceClick -> setSheet(BookInfoSheet.SourcePicker)
|
BookInfoIntent.ChangeSourceClick -> setSheet(BookInfoSheet.SourcePicker)
|
||||||
BookInfoIntent.ReadRecordClick -> setSheet(BookInfoSheet.ReadRecord)
|
BookInfoIntent.ReadRecordClick -> setSheet(BookInfoSheet.ReadRecord)
|
||||||
BookInfoIntent.RemarkClick -> showDialog(BookInfoDialog.EditRemark(currentBook?.remark))
|
BookInfoIntent.RemarkClick -> showDialog(BookInfoDialog.EditRemark(currentBook?.remark))
|
||||||
|
is BookInfoIntent.SaveCover -> {
|
||||||
|
saveCoverToGallery(intent.path)
|
||||||
|
}
|
||||||
is BookInfoIntent.ConfirmDelete -> {
|
is BookInfoIntent.ConfirmDelete -> {
|
||||||
dismissDialog()
|
dismissDialog()
|
||||||
deleteBook(intent.deleteOriginal)
|
deleteBook(intent.deleteOriginal)
|
||||||
@@ -445,6 +456,41 @@ class BookInfoViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun saveCoverToGallery(path: String) {
|
||||||
|
val book = currentBook
|
||||||
|
val sourceOrigin = if (book?.getDisplayCover() == path) book.origin else null
|
||||||
|
execute {
|
||||||
|
setBusy(true)
|
||||||
|
val request = buildCoverImageRequest(
|
||||||
|
context = context,
|
||||||
|
data = path,
|
||||||
|
sourceOrigin = sourceOrigin,
|
||||||
|
loadOnlyWifi = CoverConfig.loadCoverOnlyWifi,
|
||||||
|
crossfade = false
|
||||||
|
)
|
||||||
|
val result = context.imageLoader.execute(request)
|
||||||
|
if (result is SuccessResult) {
|
||||||
|
val bitmap = result.drawable.toBitmap()
|
||||||
|
val outputStream = ByteArrayOutputStream()
|
||||||
|
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
|
||||||
|
val byteArray = outputStream.toByteArray()
|
||||||
|
ImageSaveUtils.saveImageToGallery(context, byteArray, "Cover_")
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}.onSuccess { success ->
|
||||||
|
if (success) {
|
||||||
|
context.toastOnUi("保存成功")
|
||||||
|
} else {
|
||||||
|
context.toastOnUi("保存失败")
|
||||||
|
}
|
||||||
|
}.onFinally {
|
||||||
|
setBusy(false)
|
||||||
|
}.onError {
|
||||||
|
context.toastOnUi("保存出错: ${it.localizedMessage}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun saveRemark(remark: String, success: (() -> Unit)? = null) {
|
fun saveRemark(remark: String, success: (() -> Unit)? = null) {
|
||||||
currentBook?.let { book ->
|
currentBook?.let { book ->
|
||||||
execute {
|
execute {
|
||||||
@@ -713,27 +759,22 @@ class BookInfoViewModel(
|
|||||||
|
|
||||||
private fun refreshMeta(book: Book) {
|
private fun refreshMeta(book: Book) {
|
||||||
execute {
|
execute {
|
||||||
val allKinds = book.getKindList()
|
val fileSizePattern = Regex("""^\d[\d,.]*\s*(b|kb|M|G|T)$""", RegexOption.IGNORE_CASE)
|
||||||
val customKinds = allKinds.filter { it.startsWith("#") }
|
val kinds = (book.kind?.splitNotBlank(",", "\n").orEmpty().toList())
|
||||||
val sourceKinds = allKinds.filter { !it.startsWith("#") }.toMutableList()
|
.filter { !fileSizePattern.matches(it.trim()) }
|
||||||
|
.toMutableList()
|
||||||
if (book.isLocal) {
|
if (book.isLocal) {
|
||||||
val size = FileDoc.fromFile(book.bookUrl).size
|
val size = FileDoc.fromFile(book.bookUrl).size
|
||||||
if (size > 0) {
|
if (size > 0) {
|
||||||
sourceKinds.add(ConvertUtils.formatFileSize(size))
|
kinds.add(ConvertUtils.formatFileSize(size))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val mergedKinds = (customKinds + sourceKinds).distinct()
|
val finalKinds = kinds.distinct()
|
||||||
val userGroupIds = appDb.bookGroupDao.idsSum
|
val userGroupIds = appDb.bookGroupDao.idsSum
|
||||||
val groupAnd = userGroupIds and book.group
|
val groupAnd = userGroupIds and book.group
|
||||||
val hasCustomGroup = book.group > 0L && groupAnd != 0L
|
val hasCustomGroup = book.group > 0L && groupAnd != 0L
|
||||||
val groupNames = appDb.bookGroupDao.getGroupNames(book.group).joinToString(",")
|
val groupNames = appDb.bookGroupDao.getGroupNames(book.group).joinToString(",")
|
||||||
val normalizedGroupNames = groupNames.ifBlank { null }
|
val normalizedGroupNames = groupNames.ifBlank { null }
|
||||||
val persistedCustomKinds = currentKindLabels.filter { it.startsWith("#") }
|
|
||||||
val finalKinds = if (persistedCustomKinds.isNotEmpty()) {
|
|
||||||
(persistedCustomKinds + sourceKinds).distinct()
|
|
||||||
} else {
|
|
||||||
mergedKinds
|
|
||||||
}
|
|
||||||
book.kind = finalKinds.joinToString(",")
|
book.kind = finalKinds.joinToString(",")
|
||||||
appDb.bookDao.update(book)
|
appDb.bookDao.update(book)
|
||||||
Triple(finalKinds, normalizedGroupNames, hasCustomGroup)
|
Triple(finalKinds, normalizedGroupNames, hasCustomGroup)
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package io.legado.app.ui.book.info.edit
|
package io.legado.app.ui.book.info.edit
|
||||||
|
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.horizontalScroll
|
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
@@ -13,14 +13,16 @@ import androidx.compose.foundation.layout.height
|
|||||||
import androidx.compose.foundation.layout.imePadding
|
import androidx.compose.foundation.layout.imePadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.foundation.text.input.TextFieldLineLimits
|
import androidx.compose.foundation.text.input.TextFieldLineLimits
|
||||||
import androidx.compose.foundation.text.input.rememberTextFieldState
|
import androidx.compose.foundation.text.input.rememberTextFieldState
|
||||||
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
|
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.material.icons.filled.Close
|
|
||||||
import androidx.compose.material.icons.filled.FolderOpen
|
import androidx.compose.material.icons.filled.FolderOpen
|
||||||
import androidx.compose.material.icons.filled.ImageSearch
|
import androidx.compose.material.icons.filled.ImageSearch
|
||||||
import androidx.compose.material.icons.filled.Replay
|
import androidx.compose.material.icons.filled.Replay
|
||||||
@@ -30,8 +32,7 @@ import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
|||||||
import androidx.compose.material3.ExposedDropdownMenuAnchorType
|
import androidx.compose.material3.ExposedDropdownMenuAnchorType
|
||||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||||
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -41,27 +42,37 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.zIndex
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
import io.legado.app.ui.book.changecover.ChangeCoverDialog
|
import io.legado.app.ui.book.changecover.ChangeCoverDialog
|
||||||
|
import io.legado.app.ui.config.themeConfig.ThemeConfig
|
||||||
|
import io.legado.app.ui.theme.LegadoTheme
|
||||||
|
import io.legado.app.ui.theme.fadingEdge
|
||||||
import io.legado.app.ui.widget.components.AppScaffold
|
import io.legado.app.ui.widget.components.AppScaffold
|
||||||
import io.legado.app.ui.widget.components.AppTextField
|
import io.legado.app.ui.widget.components.AppTextField
|
||||||
|
import io.legado.app.ui.widget.components.alert.AppAlertDialog
|
||||||
import io.legado.app.ui.widget.components.button.MediumOutlinedIconButton
|
import io.legado.app.ui.widget.components.button.MediumOutlinedIconButton
|
||||||
import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
|
|
||||||
import io.legado.app.ui.widget.components.cover.CoilBookCover
|
import io.legado.app.ui.widget.components.cover.CoilBookCover
|
||||||
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
|
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
|
||||||
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
|
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
|
||||||
import io.legado.app.ui.widget.components.settingItem.SwitchSettingItem
|
import io.legado.app.ui.widget.components.settingItem.SwitchSettingItem
|
||||||
|
import io.legado.app.ui.widget.components.text.AnimatedTextLine
|
||||||
import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
|
import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
|
||||||
import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
|
import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
|
||||||
import io.legado.app.ui.config.themeConfig.ThemeConfig
|
import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
|
||||||
import io.legado.app.utils.SelectImageContract
|
import io.legado.app.utils.SelectImageContract
|
||||||
import io.legado.app.utils.launch
|
import io.legado.app.utils.launch
|
||||||
import io.legado.app.utils.showDialogFragment
|
import io.legado.app.utils.showDialogFragment
|
||||||
|
import sh.calvin.reorderable.ReorderableItem
|
||||||
|
import sh.calvin.reorderable.rememberReorderableLazyListState
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -214,6 +225,7 @@ fun BookInfoEditContent(
|
|||||||
KindEditor(
|
KindEditor(
|
||||||
kindList = uiState.kindList,
|
kindList = uiState.kindList,
|
||||||
onKindListChange = { viewModel.onKindListChange(it) },
|
onKindListChange = { viewModel.onKindListChange(it) },
|
||||||
|
onReset = { viewModel.resetKinds() },
|
||||||
backgroundColor = inputBackgroundColor
|
backgroundColor = inputBackgroundColor
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
@@ -298,89 +310,146 @@ fun BookTypeDropdown(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun KindEditor(
|
fun KindEditor(
|
||||||
kindList: List<String>,
|
kindList: List<String>,
|
||||||
onKindListChange: (List<String>) -> Unit,
|
onKindListChange: (List<String>) -> Unit,
|
||||||
|
onReset: () -> Unit,
|
||||||
backgroundColor: Color
|
backgroundColor: Color
|
||||||
) {
|
) {
|
||||||
var newKindText by remember { mutableStateOf("") }
|
var editingIndex by remember { mutableStateOf<Int?>(null) }
|
||||||
|
var editText by remember { mutableStateOf("") }
|
||||||
|
val hapticFeedback = LocalHapticFeedback.current
|
||||||
|
|
||||||
Column(
|
val listState = rememberLazyListState()
|
||||||
modifier = Modifier.fillMaxWidth()
|
val reorderableState = rememberReorderableLazyListState(listState) { from, to ->
|
||||||
|
val mutable = kindList.toMutableList()
|
||||||
|
mutable.add(to.index, mutable.removeAt(from.index))
|
||||||
|
onKindListChange(mutable)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Row(
|
LazyRow(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
state = listState,
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.fadingEdge(listState),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
AppTextField(
|
items(kindList.size, key = { kindList[it] }) { index ->
|
||||||
value = newKindText,
|
val kind = kindList[index]
|
||||||
onValueChange = { newKindText = it },
|
ReorderableItem(reorderableState, key = kind) { isDragging ->
|
||||||
label = "标签",
|
KindChip(
|
||||||
backgroundColor = backgroundColor,
|
text = kind,
|
||||||
modifier = Modifier.weight(1f)
|
isDragging = isDragging,
|
||||||
)
|
onClick = {
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
editingIndex = index
|
||||||
MediumOutlinedIconButton(
|
editText = kind
|
||||||
onClick = {
|
},
|
||||||
val trimmed = newKindText.trim()
|
modifier = Modifier
|
||||||
val tag = if (trimmed.startsWith("#")) trimmed else "#$trimmed"
|
.longPressDraggableHandle(
|
||||||
if (tag.isNotBlank() && tag !in kindList) {
|
onDragStarted = {
|
||||||
val sortedList = (kindList + tag).sortedWith(
|
hapticFeedback.performHapticFeedback(HapticFeedbackType.GestureThresholdActivate)
|
||||||
compareBy<String> { !it.startsWith("#") }.thenBy { it }
|
},
|
||||||
)
|
onDragStopped = {
|
||||||
onKindListChange(sortedList)
|
hapticFeedback.performHapticFeedback(HapticFeedbackType.GestureEnd)
|
||||||
newKindText = ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
imageVector = Icons.Default.Add
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(4.dp))
|
|
||||||
if (kindList.isNotEmpty()) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.horizontalScroll(rememberScrollState()),
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
kindList.forEach { kind ->
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
AppTextField(
|
|
||||||
value = kind,
|
|
||||||
onValueChange = { newValue ->
|
|
||||||
val index = kindList.indexOf(kind)
|
|
||||||
if (index >= 0) {
|
|
||||||
val newList = kindList.toMutableList()
|
|
||||||
newList[index] = newValue
|
|
||||||
onKindListChange(newList)
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
label = null,
|
|
||||||
backgroundColor = backgroundColor,
|
|
||||||
singleLine = true,
|
|
||||||
maxLines = 1,
|
|
||||||
modifier = Modifier.width(80.dp)
|
|
||||||
)
|
|
||||||
IconButton(
|
|
||||||
onClick = {
|
|
||||||
onKindListChange(kindList - kind)
|
|
||||||
},
|
|
||||||
modifier = Modifier.padding(0.dp)
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.Close,
|
|
||||||
contentDescription = "删除标签",
|
|
||||||
tint = Color.Red.copy(alpha = 0.7f),
|
|
||||||
modifier = Modifier.padding(4.dp)
|
|
||||||
)
|
)
|
||||||
}
|
.animateItem()
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
MediumOutlinedIconButton(
|
||||||
|
onClick = {
|
||||||
|
onReset()
|
||||||
|
},
|
||||||
|
imageVector = Icons.Default.Replay
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
MediumOutlinedIconButton(
|
||||||
|
onClick = {
|
||||||
|
editingIndex = -1
|
||||||
|
editText = ""
|
||||||
|
},
|
||||||
|
imageVector = Icons.Default.Add
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (editingIndex != null) {
|
||||||
|
val isAdding = editingIndex == -1
|
||||||
|
AppAlertDialog(
|
||||||
|
show = true,
|
||||||
|
onDismissRequest = { editingIndex = null },
|
||||||
|
title = if (isAdding) "添加标签" else "编辑标签",
|
||||||
|
content = {
|
||||||
|
AppTextField(
|
||||||
|
value = editText,
|
||||||
|
onValueChange = { editText = it },
|
||||||
|
label = "标签",
|
||||||
|
backgroundColor = backgroundColor,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
},
|
||||||
|
confirmText = stringResource(android.R.string.ok),
|
||||||
|
onConfirm = {
|
||||||
|
val trimmed = editText.trim()
|
||||||
|
if (trimmed.isNotBlank()) {
|
||||||
|
val mutable = kindList.toMutableList()
|
||||||
|
if (isAdding) {
|
||||||
|
if (trimmed !in kindList) mutable.add(trimmed)
|
||||||
|
} else {
|
||||||
|
mutable[editingIndex!!] = trimmed
|
||||||
|
}
|
||||||
|
onKindListChange(mutable)
|
||||||
|
}
|
||||||
|
editingIndex = null
|
||||||
|
},
|
||||||
|
dismissText = if (isAdding) stringResource(android.R.string.cancel) else stringResource(
|
||||||
|
R.string.delete
|
||||||
|
),
|
||||||
|
onDismiss = if (isAdding) {
|
||||||
|
{ editingIndex = null }
|
||||||
|
} else {
|
||||||
|
{
|
||||||
|
onKindListChange(kindList.toMutableList().apply { removeAt(editingIndex!!) })
|
||||||
|
editingIndex = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
|
@Composable
|
||||||
|
private fun KindChip(
|
||||||
|
text: String,
|
||||||
|
isDragging: Boolean,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
color = LegadoTheme.colorScheme.surfaceContainer,
|
||||||
|
contentColor = LegadoTheme.colorScheme.onSurface,
|
||||||
|
shadowElevation = if (isDragging) 8.dp else 0.dp,
|
||||||
|
modifier = modifier
|
||||||
|
.zIndex(if (isDragging) 1f else 0f)
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
) {
|
||||||
|
AnimatedTextLine(
|
||||||
|
text = text,
|
||||||
|
style = LegadoTheme.typography.labelLargeEmphasized,
|
||||||
|
color = LegadoTheme.colorScheme.onSurface,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import io.legado.app.model.ReadBook
|
|||||||
import io.legado.app.utils.FileUtils
|
import io.legado.app.utils.FileUtils
|
||||||
import io.legado.app.utils.MD5Utils
|
import io.legado.app.utils.MD5Utils
|
||||||
import io.legado.app.utils.inputStream
|
import io.legado.app.utils.inputStream
|
||||||
import io.legado.app.utils.splitNotBlank
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
@@ -33,6 +32,7 @@ data class BookInfoEditUiState(
|
|||||||
val intro: String? = null,
|
val intro: String? = null,
|
||||||
val remark: String? = null,
|
val remark: String? = null,
|
||||||
val kindList: List<String> = emptyList(),
|
val kindList: List<String> = emptyList(),
|
||||||
|
val originalKindList: List<String> = emptyList(),
|
||||||
val selectedType: String = "文本",
|
val selectedType: String = "文本",
|
||||||
val bookTypes: List<String> = listOf("文本", "音频", "图片"),
|
val bookTypes: List<String> = listOf("文本", "音频", "图片"),
|
||||||
val fixedType: Boolean = false,
|
val fixedType: Boolean = false,
|
||||||
@@ -53,13 +53,16 @@ class BookInfoEditViewModel(application: Application) : BaseViewModel(applicatio
|
|||||||
it.isAudio -> 1
|
it.isAudio -> 1
|
||||||
else -> 0
|
else -> 0
|
||||||
}
|
}
|
||||||
|
val kinds =
|
||||||
|
it.kind?.split(",", "\n")?.filter { kind -> kind.isNotBlank() }.orEmpty()
|
||||||
_uiState.value = BookInfoEditUiState(
|
_uiState.value = BookInfoEditUiState(
|
||||||
name = it.name,
|
name = it.name,
|
||||||
author = it.author,
|
author = it.author,
|
||||||
coverUrl = it.getDisplayCover(),
|
coverUrl = it.getDisplayCover(),
|
||||||
intro = it.getDisplayIntro(),
|
intro = it.getDisplayIntro(),
|
||||||
remark = it.remark,
|
remark = it.remark,
|
||||||
kindList = it.kind?.split(",", "\n")?.filter { kind -> kind.isNotBlank() }.orEmpty(),
|
kindList = kinds,
|
||||||
|
originalKindList = kinds,
|
||||||
selectedType = _uiState.value.bookTypes[selectedTypeIndex],
|
selectedType = _uiState.value.bookTypes[selectedTypeIndex],
|
||||||
fixedType = it.config.fixedType,
|
fixedType = it.config.fixedType,
|
||||||
book = it
|
book = it
|
||||||
@@ -68,6 +71,10 @@ class BookInfoEditViewModel(application: Application) : BaseViewModel(applicatio
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun resetKinds() {
|
||||||
|
_uiState.value = _uiState.value.copy(kindList = _uiState.value.originalKindList.toList())
|
||||||
|
}
|
||||||
|
|
||||||
fun onNameChange(name: String) {
|
fun onNameChange(name: String) {
|
||||||
_uiState.value = _uiState.value.copy(name = name)
|
_uiState.value = _uiState.value.copy(name = name)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,20 @@
|
|||||||
package io.legado.app.ui.book.manage
|
package io.legado.app.ui.book.manage
|
||||||
|
|
||||||
import androidx.compose.ui.focus.focusRequester
|
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
|
||||||
import androidx.compose.foundation.layout.FlowRow
|
|
||||||
import androidx.compose.foundation.layout.offset
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.heightIn
|
import androidx.compose.foundation.layout.heightIn
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
@@ -26,11 +23,11 @@ import androidx.compose.foundation.lazy.items
|
|||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.MenuOpen
|
import androidx.compose.material.icons.automirrored.filled.MenuOpen
|
||||||
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.material.icons.filled.Bookmarks
|
import androidx.compose.material.icons.filled.Bookmarks
|
||||||
import androidx.compose.material.icons.filled.Close
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
import androidx.compose.material.icons.filled.Download
|
import androidx.compose.material.icons.filled.Download
|
||||||
import androidx.compose.material.icons.filled.Add
|
|
||||||
import androidx.compose.material.icons.filled.Info
|
import androidx.compose.material.icons.filled.Info
|
||||||
import androidx.compose.material.icons.filled.MoreVert
|
import androidx.compose.material.icons.filled.MoreVert
|
||||||
import androidx.compose.material.icons.filled.PlayArrow
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
@@ -44,7 +41,6 @@ import androidx.compose.material3.Checkbox
|
|||||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
import androidx.compose.material3.FloatingActionButtonMenu
|
import androidx.compose.material3.FloatingActionButtonMenu
|
||||||
import androidx.compose.material3.FloatingActionButtonMenuItem
|
import androidx.compose.material3.FloatingActionButtonMenuItem
|
||||||
import io.legado.app.ui.widget.components.progressIndicator.AppCircularProgressIndicator
|
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.RadioButton
|
import androidx.compose.material3.RadioButton
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
@@ -61,9 +57,10 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.rememberUpdatedState
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -72,7 +69,6 @@ import io.legado.app.R
|
|||||||
import io.legado.app.constant.IntentAction
|
import io.legado.app.constant.IntentAction
|
||||||
import io.legado.app.data.appDb
|
import io.legado.app.data.appDb
|
||||||
import io.legado.app.data.entities.Book
|
import io.legado.app.data.entities.Book
|
||||||
import io.legado.app.data.entities.BookGroup
|
|
||||||
import io.legado.app.data.entities.BookSource
|
import io.legado.app.data.entities.BookSource
|
||||||
import io.legado.app.data.entities.BookSourcePart
|
import io.legado.app.data.entities.BookSourcePart
|
||||||
import io.legado.app.domain.usecase.BatchChangeSourcePreviewItem
|
import io.legado.app.domain.usecase.BatchChangeSourcePreviewItem
|
||||||
@@ -87,12 +83,12 @@ import io.legado.app.ui.book.info.ChangeSourceSheet
|
|||||||
import io.legado.app.ui.book.info.GroupSelectSheet
|
import io.legado.app.ui.book.info.GroupSelectSheet
|
||||||
import io.legado.app.ui.theme.LegadoTheme
|
import io.legado.app.ui.theme.LegadoTheme
|
||||||
import io.legado.app.ui.theme.adaptiveContentPadding
|
import io.legado.app.ui.theme.adaptiveContentPadding
|
||||||
import io.legado.app.ui.widget.components.progressIndicator.AppLinearProgressIndicator
|
import io.legado.app.ui.theme.adaptiveHorizontalPadding
|
||||||
import io.legado.app.ui.widget.components.AppTextField
|
import io.legado.app.ui.widget.components.AppTextField
|
||||||
import io.legado.app.ui.widget.components.alert.AppAlertDialog
|
import io.legado.app.ui.widget.components.alert.AppAlertDialog
|
||||||
|
import io.legado.app.ui.widget.components.button.MediumIconButton
|
||||||
import io.legado.app.ui.widget.components.button.SmallTonalIconButton
|
import io.legado.app.ui.widget.components.button.SmallTonalIconButton
|
||||||
import io.legado.app.ui.widget.components.button.SmallTonalTextButton
|
import io.legado.app.ui.widget.components.button.SmallTonalTextButton
|
||||||
import io.legado.app.ui.widget.components.topbar.TopBarActionButton
|
|
||||||
import io.legado.app.ui.widget.components.card.NormalCard
|
import io.legado.app.ui.widget.components.card.NormalCard
|
||||||
import io.legado.app.ui.widget.components.card.ReorderableSelectionItem
|
import io.legado.app.ui.widget.components.card.ReorderableSelectionItem
|
||||||
import io.legado.app.ui.widget.components.card.SelectionItemCard
|
import io.legado.app.ui.widget.components.card.SelectionItemCard
|
||||||
@@ -100,26 +96,25 @@ import io.legado.app.ui.widget.components.card.TextCard
|
|||||||
import io.legado.app.ui.widget.components.cover.CoilBookCover
|
import io.legado.app.ui.widget.components.cover.CoilBookCover
|
||||||
import io.legado.app.ui.widget.components.divider.PillDivider
|
import io.legado.app.ui.widget.components.divider.PillDivider
|
||||||
import io.legado.app.ui.widget.components.filePicker.FilePickerSheet
|
import io.legado.app.ui.widget.components.filePicker.FilePickerSheet
|
||||||
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.icon.AppIcons
|
import io.legado.app.ui.widget.components.icon.AppIcons
|
||||||
import io.legado.app.ui.widget.components.list.ListScaffold
|
import io.legado.app.ui.widget.components.list.ListScaffold
|
||||||
import io.legado.app.ui.widget.components.list.ListUiState
|
import io.legado.app.ui.widget.components.list.ListUiState
|
||||||
import io.legado.app.ui.widget.components.modalBottomSheet.OptionCard
|
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.AppModalBottomSheet
|
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
|
||||||
|
import io.legado.app.ui.widget.components.modalBottomSheet.OptionCard
|
||||||
import io.legado.app.ui.widget.components.modalBottomSheet.OptionSheet
|
import io.legado.app.ui.widget.components.modalBottomSheet.OptionSheet
|
||||||
|
import io.legado.app.ui.widget.components.progressIndicator.AppCircularProgressIndicator
|
||||||
import io.legado.app.ui.widget.components.text.AppText
|
import io.legado.app.ui.widget.components.text.AppText
|
||||||
|
import io.legado.app.ui.widget.components.topbar.TopBarActionButton
|
||||||
import io.legado.app.utils.ACache
|
import io.legado.app.utils.ACache
|
||||||
import io.legado.app.utils.FileDoc
|
import io.legado.app.utils.FileDoc
|
||||||
import io.legado.app.utils.checkWrite
|
import io.legado.app.utils.checkWrite
|
||||||
import io.legado.app.utils.isContentScheme
|
import io.legado.app.utils.isContentScheme
|
||||||
import io.legado.app.utils.move
|
import io.legado.app.utils.move
|
||||||
import io.legado.app.utils.startActivity
|
|
||||||
import io.legado.app.utils.startService
|
import io.legado.app.utils.startService
|
||||||
import io.legado.app.utils.toastOnUi
|
import io.legado.app.utils.toastOnUi
|
||||||
import io.legado.app.utils.verificationField
|
import io.legado.app.utils.verificationField
|
||||||
import io.legado.app.ui.theme.adaptiveHorizontalPadding
|
|
||||||
import io.legado.app.ui.widget.components.button.MediumIconButton
|
|
||||||
import org.koin.androidx.compose.koinViewModel
|
import org.koin.androidx.compose.koinViewModel
|
||||||
import sh.calvin.reorderable.ReorderableItem
|
import sh.calvin.reorderable.ReorderableItem
|
||||||
import sh.calvin.reorderable.rememberReorderableLazyListState
|
import sh.calvin.reorderable.rememberReorderableLazyListState
|
||||||
@@ -399,14 +394,14 @@ private fun BookshelfManageScreen(
|
|||||||
Icons.Default.SelectAll,
|
Icons.Default.SelectAll,
|
||||||
stringResource(R.string.select_all)
|
stringResource(R.string.select_all)
|
||||||
) {
|
) {
|
||||||
selectedBookUrls = state.books.mapTo(hashSetOf()) { it.bookUrl }
|
selectedBookUrls = filteredBooks.mapTo(hashSetOf()) { it.bookUrl }
|
||||||
},
|
},
|
||||||
BookshelfManageFabAction(
|
BookshelfManageFabAction(
|
||||||
Icons.Default.Refresh,
|
Icons.Default.Refresh,
|
||||||
stringResource(R.string.revert_selection)
|
stringResource(R.string.revert_selection)
|
||||||
) {
|
) {
|
||||||
val visibleBookUrls = booksByUrl.keys
|
val filteredUrls = filteredBooks.map { it.bookUrl }.toSet()
|
||||||
selectedBookUrls = visibleBookUrls - selectedBookUrls
|
selectedBookUrls = (selectedBookUrls - filteredUrls) + (filteredUrls - selectedBookUrls)
|
||||||
},
|
},
|
||||||
BookshelfManageFabAction(
|
BookshelfManageFabAction(
|
||||||
Icons.Default.Download,
|
Icons.Default.Download,
|
||||||
|
|||||||
@@ -1,20 +1,47 @@
|
|||||||
package io.legado.app.ui.book.readRecord
|
package io.legado.app.ui.book.readRecord
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedContent
|
||||||
|
import androidx.compose.animation.SizeTransform
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.slideInHorizontally
|
||||||
|
import androidx.compose.animation.slideOutHorizontally
|
||||||
|
import androidx.compose.animation.togetherWith
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
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.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
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.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowLeft
|
import androidx.compose.material.icons.automirrored.filled.ArrowLeft
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowRight
|
import androidx.compose.material.icons.automirrored.filled.ArrowRight
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material.icons.filled.CalendarMonth
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.material.icons.filled.GridOn
|
||||||
|
import androidx.compose.material.icons.filled.Leaderboard
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
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.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
@@ -31,6 +58,7 @@ import io.legado.app.ui.widget.components.button.MediumIconButton
|
|||||||
import io.legado.app.ui.widget.components.card.GlassCard
|
import io.legado.app.ui.widget.components.card.GlassCard
|
||||||
import io.legado.app.ui.widget.components.cover.CoilBookCover
|
import io.legado.app.ui.widget.components.cover.CoilBookCover
|
||||||
import io.legado.app.ui.widget.components.heatmap.HeatmapMode
|
import io.legado.app.ui.widget.components.heatmap.HeatmapMode
|
||||||
|
import io.legado.app.ui.widget.components.tabRow.AppTabRow
|
||||||
import io.legado.app.ui.widget.components.text.AppText
|
import io.legado.app.ui.widget.components.text.AppText
|
||||||
import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
|
import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
|
||||||
import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
|
import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
|
||||||
@@ -83,8 +111,11 @@ fun ReadRecordOverviewScreen(
|
|||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentPadding = PaddingValues(bottom = 16.dp)
|
contentPadding = PaddingValues(bottom = 16.dp)
|
||||||
) {
|
) {
|
||||||
item {
|
|
||||||
HeatmapCard(state)
|
if (state.period != ReadPeriod.ALL && state.dailyTimeData.isNotEmpty()) {
|
||||||
|
item {
|
||||||
|
ReadingTimeBarChartCard(data = state.dailyTimeData, period = state.period)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
item {
|
item {
|
||||||
@@ -99,10 +130,8 @@ fun ReadRecordOverviewScreen(
|
|||||||
StatsGridCard(title = "阅读数据", items = stats)
|
StatsGridCard(title = "阅读数据", items = stats)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.period != ReadPeriod.ALL && state.dailyTimeData.isNotEmpty()) {
|
item {
|
||||||
item {
|
HeatmapCard(state)
|
||||||
ReadingTimeBarChartCard(data = state.dailyTimeData, period = state.period)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.topBooks.isNotEmpty()) {
|
if (state.topBooks.isNotEmpty()) {
|
||||||
@@ -124,27 +153,22 @@ fun PeriodSelector(
|
|||||||
selectedPeriod: ReadPeriod,
|
selectedPeriod: ReadPeriod,
|
||||||
onPeriodSelected: (ReadPeriod) -> Unit
|
onPeriodSelected: (ReadPeriod) -> Unit
|
||||||
) {
|
) {
|
||||||
val periods = listOf(
|
val periods = remember {
|
||||||
ReadPeriod.DAY to "日",
|
listOf(
|
||||||
ReadPeriod.WEEK to "周",
|
ReadPeriod.DAY to "日",
|
||||||
ReadPeriod.MONTH to "月",
|
ReadPeriod.WEEK to "周",
|
||||||
ReadPeriod.YEAR to "年",
|
ReadPeriod.MONTH to "月",
|
||||||
ReadPeriod.ALL to "总"
|
ReadPeriod.YEAR to "年",
|
||||||
)
|
ReadPeriod.ALL to "总"
|
||||||
|
)
|
||||||
PrimaryTabRow(
|
|
||||||
selectedTabIndex = periods.indexOfFirst { it.first == selectedPeriod },
|
|
||||||
containerColor = Color.Transparent,
|
|
||||||
divider = {}
|
|
||||||
) {
|
|
||||||
periods.forEach { (period, label) ->
|
|
||||||
Tab(
|
|
||||||
selected = selectedPeriod == period,
|
|
||||||
onClick = { onPeriodSelected(period) },
|
|
||||||
text = { AppText(label) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AppTabRow(
|
||||||
|
tabTitles = periods.map { it.second },
|
||||||
|
selectedTabIndex = periods.indexOfFirst { it.first == selectedPeriod },
|
||||||
|
onTabSelected = { index -> onPeriodSelected(periods[index].first) },
|
||||||
|
isScrollable = false
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -156,34 +180,50 @@ fun DateNavigator(
|
|||||||
) {
|
) {
|
||||||
if (period == ReadPeriod.ALL) return
|
if (period == ReadPeriod.ALL) return
|
||||||
|
|
||||||
val dateText = when (period) {
|
|
||||||
ReadPeriod.DAY -> referenceDate.format(DateTimeFormatter.ofPattern("yyyy年M月d日"))
|
|
||||||
ReadPeriod.WEEK -> {
|
|
||||||
val start = referenceDate.with(java.time.DayOfWeek.MONDAY)
|
|
||||||
val end = referenceDate.with(java.time.DayOfWeek.SUNDAY)
|
|
||||||
"${start.format(DateTimeFormatter.ofPattern("M.d"))} - ${end.format(DateTimeFormatter.ofPattern("M.d"))}"
|
|
||||||
}
|
|
||||||
ReadPeriod.MONTH -> referenceDate.format(DateTimeFormatter.ofPattern("yyyy年M月"))
|
|
||||||
ReadPeriod.YEAR -> referenceDate.format(DateTimeFormatter.ofPattern("yyyy年"))
|
|
||||||
ReadPeriod.ALL -> ""
|
|
||||||
}
|
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(vertical = 8.dp),
|
.padding(vertical = 8.dp, horizontal = 12.dp),
|
||||||
horizontalArrangement = Arrangement.Center,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
MediumIconButton(
|
MediumIconButton(
|
||||||
onClick = onPrevClick,
|
onClick = onPrevClick,
|
||||||
imageVector = Icons.AutoMirrored.Filled.ArrowLeft
|
imageVector = Icons.AutoMirrored.Filled.ArrowLeft
|
||||||
)
|
)
|
||||||
AppText(
|
AnimatedContent(
|
||||||
text = dateText,
|
targetState = referenceDate,
|
||||||
style = LegadoTheme.typography.titleMedium,
|
transitionSpec = {
|
||||||
modifier = Modifier.padding(horizontal = 16.dp)
|
if (targetState.isAfter(initialState)) {
|
||||||
)
|
(slideInHorizontally { it / 2 } + fadeIn()).togetherWith(slideOutHorizontally { -it / 2 } + fadeOut())
|
||||||
|
} else {
|
||||||
|
(slideInHorizontally { -it / 2 } + fadeIn()).togetherWith(slideOutHorizontally { it / 2 } + fadeOut())
|
||||||
|
}.using(SizeTransform(clip = false))
|
||||||
|
},
|
||||||
|
label = "DateNavigator"
|
||||||
|
) { targetDate ->
|
||||||
|
val text = when (period) {
|
||||||
|
ReadPeriod.DAY -> targetDate.format(DateTimeFormatter.ofPattern("yyyy年M月d日"))
|
||||||
|
ReadPeriod.WEEK -> {
|
||||||
|
val start = targetDate.with(java.time.DayOfWeek.MONDAY)
|
||||||
|
val end = targetDate.with(java.time.DayOfWeek.SUNDAY)
|
||||||
|
"${start.format(DateTimeFormatter.ofPattern("M.d"))} - ${
|
||||||
|
end.format(
|
||||||
|
DateTimeFormatter.ofPattern("M.d")
|
||||||
|
)
|
||||||
|
}"
|
||||||
|
}
|
||||||
|
|
||||||
|
ReadPeriod.MONTH -> targetDate.format(DateTimeFormatter.ofPattern("yyyy年M月"))
|
||||||
|
ReadPeriod.YEAR -> targetDate.format(DateTimeFormatter.ofPattern("yyyy年"))
|
||||||
|
ReadPeriod.ALL -> ""
|
||||||
|
}
|
||||||
|
AppText(
|
||||||
|
text = text,
|
||||||
|
style = LegadoTheme.typography.titleMedium,
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
MediumIconButton(
|
MediumIconButton(
|
||||||
onClick = onNextClick,
|
onClick = onNextClick,
|
||||||
imageVector = Icons.AutoMirrored.Filled.ArrowRight
|
imageVector = Icons.AutoMirrored.Filled.ArrowRight
|
||||||
@@ -199,7 +239,16 @@ fun HeatmapCard(state: ReadRecordOverviewUiState) {
|
|||||||
.adaptiveHorizontalPadding(vertical = 8.dp)
|
.adaptiveHorizontalPadding(vertical = 8.dp)
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.padding(16.dp)) {
|
Column(modifier = Modifier.padding(16.dp)) {
|
||||||
AppText("阅读热力图", style = LegadoTheme.typography.titleMedium)
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.GridOn,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = LegadoTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
AppText("阅读热力图", style = LegadoTheme.typography.titleMedium)
|
||||||
|
}
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
HeatmapCalendarSection(
|
HeatmapCalendarSection(
|
||||||
dailyReadCounts = state.allReadCounts,
|
dailyReadCounts = state.allReadCounts,
|
||||||
@@ -224,11 +273,22 @@ fun TopReadingListCard(
|
|||||||
.adaptiveHorizontalPadding(vertical = 8.dp)
|
.adaptiveHorizontalPadding(vertical = 8.dp)
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.padding(vertical = 16.dp)) {
|
Column(modifier = Modifier.padding(vertical = 16.dp)) {
|
||||||
AppText(
|
Row(
|
||||||
modifier = Modifier.padding(horizontal = 16.dp),
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
text = "阅读时长榜",
|
verticalAlignment = Alignment.CenterVertically
|
||||||
style = LegadoTheme.typography.titleMedium
|
) {
|
||||||
)
|
Icon(
|
||||||
|
imageVector = Icons.Default.Leaderboard,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = LegadoTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
AppText(
|
||||||
|
text = "阅读时长榜",
|
||||||
|
style = LegadoTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
|
||||||
topBooks.forEachIndexed { index, book ->
|
topBooks.forEachIndexed { index, book ->
|
||||||
@@ -247,7 +307,7 @@ fun TopReadingListCard(
|
|||||||
AppText(
|
AppText(
|
||||||
text = "${index + 1}",
|
text = "${index + 1}",
|
||||||
style = LegadoTheme.typography.titleMedium,
|
style = LegadoTheme.typography.titleMedium,
|
||||||
modifier = Modifier.width(32.dp),
|
modifier = Modifier.padding(start = 12.dp, end = 16.dp),
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
color = if (index < 3) LegadoTheme.colorScheme.primary else LegadoTheme.colorScheme.onSurfaceVariant
|
color = if (index < 3) LegadoTheme.colorScheme.primary else LegadoTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
@@ -259,6 +319,12 @@ fun TopReadingListCard(
|
|||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(12.dp))
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
Column(modifier = Modifier.weight(1f)) {
|
Column(modifier = Modifier.weight(1f)) {
|
||||||
|
AppText(
|
||||||
|
modifier = Modifier.padding(end = 8.dp),
|
||||||
|
text = ReadRecordFormatter.formatDuration(book.readTime),
|
||||||
|
style = LegadoTheme.typography.bodySmall,
|
||||||
|
color = LegadoTheme.colorScheme.primary
|
||||||
|
)
|
||||||
AppText(
|
AppText(
|
||||||
text = book.bookName,
|
text = book.bookName,
|
||||||
style = LegadoTheme.typography.bodyMedium,
|
style = LegadoTheme.typography.bodyMedium,
|
||||||
@@ -272,12 +338,6 @@ fun TopReadingListCard(
|
|||||||
color = LegadoTheme.colorScheme.onSurfaceVariant
|
color = LegadoTheme.colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
AppText(
|
|
||||||
modifier = Modifier.padding(end = 8.dp),
|
|
||||||
text = ReadRecordFormatter.formatDuration(book.readTime),
|
|
||||||
style = LegadoTheme.typography.bodySmall,
|
|
||||||
color = LegadoTheme.colorScheme.primary
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -300,7 +360,16 @@ fun ReadingCalendarCard(
|
|||||||
.adaptiveHorizontalPadding(vertical = 8.dp)
|
.adaptiveHorizontalPadding(vertical = 8.dp)
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.padding(16.dp)) {
|
Column(modifier = Modifier.padding(16.dp)) {
|
||||||
AppText("读书日历", style = LegadoTheme.typography.titleMedium)
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.CalendarMonth,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = LegadoTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
AppText("读书日历", style = LegadoTheme.typography.titleMedium)
|
||||||
|
}
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
Row(modifier = Modifier.fillMaxWidth()) {
|
Row(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
|||||||
@@ -52,9 +52,10 @@ class ReadRecordOverviewViewModel(
|
|||||||
_period,
|
_period,
|
||||||
_referenceDate,
|
_referenceDate,
|
||||||
repository.getAllRecordDetails(""),
|
repository.getAllRecordDetails(""),
|
||||||
repository.getLatestReadRecords("")
|
repository.getLatestReadRecords(""),
|
||||||
) { period, refDate, details, latestRecords ->
|
bookRepository.getAllBooks()
|
||||||
getReadRecordOverviewUseCase(period, refDate, details, latestRecords)
|
) { period, refDate, details, latestRecords, allBooks ->
|
||||||
|
getReadRecordOverviewUseCase(period, refDate, details, latestRecords, allBooks)
|
||||||
}.stateIn(
|
}.stateIn(
|
||||||
scope = viewModelScope,
|
scope = viewModelScope,
|
||||||
started = SharingStarted.WhileSubscribed(5000),
|
started = SharingStarted.WhileSubscribed(5000),
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ package io.legado.app.ui.book.readRecord
|
|||||||
|
|
||||||
import androidx.compose.animation.AnimatedContent
|
import androidx.compose.animation.AnimatedContent
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
|
||||||
import androidx.compose.animation.core.tween
|
|
||||||
import androidx.compose.foundation.basicMarquee
|
import androidx.compose.foundation.basicMarquee
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
@@ -49,14 +47,9 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.drawBehind
|
import androidx.compose.ui.draw.drawBehind
|
||||||
import androidx.compose.ui.draw.drawWithContent
|
|
||||||
import androidx.compose.ui.draw.rotate
|
import androidx.compose.ui.draw.rotate
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.BlendMode
|
|
||||||
import androidx.compose.ui.graphics.Brush
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.CompositingStrategy
|
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.text.SpanStyle
|
import androidx.compose.ui.text.SpanStyle
|
||||||
import androidx.compose.ui.text.buildAnnotatedString
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
@@ -69,17 +62,16 @@ import cn.hutool.core.date.DateUtil
|
|||||||
import io.legado.app.data.entities.readRecord.ReadRecord
|
import io.legado.app.data.entities.readRecord.ReadRecord
|
||||||
import io.legado.app.data.entities.readRecord.ReadRecordDetail
|
import io.legado.app.data.entities.readRecord.ReadRecordDetail
|
||||||
import io.legado.app.data.entities.readRecord.ReadRecordSession
|
import io.legado.app.data.entities.readRecord.ReadRecordSession
|
||||||
import io.legado.app.ui.book.readRecord.component.*
|
|
||||||
import io.legado.app.ui.theme.LegadoTheme
|
import io.legado.app.ui.theme.LegadoTheme
|
||||||
import io.legado.app.ui.theme.adaptiveContentPaddingOnlyVertical
|
import io.legado.app.ui.theme.adaptiveContentPaddingOnlyVertical
|
||||||
import io.legado.app.ui.theme.adaptiveHorizontalPadding
|
import io.legado.app.ui.theme.adaptiveHorizontalPadding
|
||||||
|
import io.legado.app.ui.theme.fadingEdge
|
||||||
import io.legado.app.ui.widget.CollapsibleHeader
|
import io.legado.app.ui.widget.CollapsibleHeader
|
||||||
import io.legado.app.ui.widget.components.AppScaffold
|
import io.legado.app.ui.widget.components.AppScaffold
|
||||||
import io.legado.app.ui.widget.components.EmptyMessage
|
import io.legado.app.ui.widget.components.EmptyMessage
|
||||||
import io.legado.app.ui.widget.components.SearchBar
|
import io.legado.app.ui.widget.components.SearchBar
|
||||||
import io.legado.app.ui.widget.components.alert.AppAlertDialog
|
import io.legado.app.ui.widget.components.alert.AppAlertDialog
|
||||||
import io.legado.app.ui.widget.components.button.AppIconButton
|
import io.legado.app.ui.widget.components.button.AppIconButton
|
||||||
import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
|
|
||||||
import io.legado.app.ui.widget.components.card.GlassCard
|
import io.legado.app.ui.widget.components.card.GlassCard
|
||||||
import io.legado.app.ui.widget.components.card.TextCard
|
import io.legado.app.ui.widget.components.card.TextCard
|
||||||
import io.legado.app.ui.widget.components.checkBox.CheckboxItem
|
import io.legado.app.ui.widget.components.checkBox.CheckboxItem
|
||||||
@@ -104,8 +96,9 @@ import io.legado.app.ui.widget.components.swipe.SwipeActionContainer
|
|||||||
import io.legado.app.ui.widget.components.text.AppText
|
import io.legado.app.ui.widget.components.text.AppText
|
||||||
import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
|
import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
|
||||||
import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
|
import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
|
||||||
import io.legado.app.utils.formatReadDuration
|
import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
|
||||||
import io.legado.app.utils.StringUtils.formatFriendlyDate
|
import io.legado.app.utils.StringUtils.formatFriendlyDate
|
||||||
|
import io.legado.app.utils.formatReadDuration
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.androidx.compose.koinViewModel
|
import org.koin.androidx.compose.koinViewModel
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
@@ -516,29 +509,6 @@ fun HeatmapCalendarSection(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val showLeftGradient by remember {
|
|
||||||
derivedStateOf {
|
|
||||||
listState.firstVisibleItemIndex > 0 || listState.firstVisibleItemScrollOffset > 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val showRightGradient by remember {
|
|
||||||
derivedStateOf {
|
|
||||||
listState.canScrollForward
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val leftAlpha by animateFloatAsState(
|
|
||||||
targetValue = if (showLeftGradient) 1f else 0f,
|
|
||||||
animationSpec = tween(durationMillis = 200),
|
|
||||||
label = "LeftGradientAlpha"
|
|
||||||
)
|
|
||||||
|
|
||||||
val rightAlpha by animateFloatAsState(
|
|
||||||
targetValue = if (showRightGradient) 1f else 0f,
|
|
||||||
animationSpec = tween(durationMillis = 200),
|
|
||||||
label = "RightGradientAlpha"
|
|
||||||
)
|
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -556,31 +526,7 @@ fun HeatmapCalendarSection(
|
|||||||
horizontalArrangement = Arrangement.spacedBy(config.cellSpacing),
|
horizontalArrangement = Arrangement.spacedBy(config.cellSpacing),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen)
|
.fadingEdge(listState, config.gradientWidth)
|
||||||
.drawWithContent {
|
|
||||||
drawContent()
|
|
||||||
|
|
||||||
val width = size.width
|
|
||||||
val gradientWidthPx = config.gradientWidth.toPx()
|
|
||||||
val leftStop = gradientWidthPx / width
|
|
||||||
val rightStop = 1f - (gradientWidthPx / width)
|
|
||||||
|
|
||||||
val colorStops = arrayOf(
|
|
||||||
0f to Color.Black.copy(alpha = 1f - leftAlpha),
|
|
||||||
leftStop to Color.Black,
|
|
||||||
rightStop to Color.Black,
|
|
||||||
1f to Color.Black.copy(alpha = 1f - rightAlpha)
|
|
||||||
)
|
|
||||||
|
|
||||||
drawRect(
|
|
||||||
brush = Brush.horizontalGradient(
|
|
||||||
colorStops = colorStops,
|
|
||||||
startX = 0f,
|
|
||||||
endX = width
|
|
||||||
),
|
|
||||||
blendMode = BlendMode.DstIn
|
|
||||||
)
|
|
||||||
}
|
|
||||||
) {
|
) {
|
||||||
val firstReadDate = listOfNotNull(
|
val firstReadDate = listOfNotNull(
|
||||||
dailyReadCounts.filterValues { it > 0 }.keys.minOrNull(),
|
dailyReadCounts.filterValues { it > 0 }.keys.minOrNull(),
|
||||||
|
|||||||
@@ -1,15 +1,35 @@
|
|||||||
package io.legado.app.ui.book.readRecord.component
|
package io.legado.app.ui.book.readRecord.component
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.*
|
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.fillMaxHeight
|
||||||
|
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.layout.width
|
||||||
|
import androidx.compose.foundation.layout.widthIn
|
||||||
|
import androidx.compose.foundation.layout.wrapContentWidth
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.BarChart
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import io.legado.app.ui.book.readRecord.ReadPeriod
|
import io.legado.app.ui.book.readRecord.ReadPeriod
|
||||||
|
import io.legado.app.ui.book.readRecord.ReadRecordFormatter
|
||||||
import io.legado.app.ui.theme.LegadoTheme
|
import io.legado.app.ui.theme.LegadoTheme
|
||||||
import io.legado.app.ui.theme.adaptiveHorizontalPadding
|
import io.legado.app.ui.theme.adaptiveHorizontalPadding
|
||||||
import io.legado.app.ui.widget.components.card.GlassCard
|
import io.legado.app.ui.widget.components.card.GlassCard
|
||||||
@@ -22,7 +42,16 @@ fun ReadingTimeBarChartCard(
|
|||||||
period: ReadPeriod,
|
period: ReadPeriod,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val maxTime = data.maxOfOrNull { it.second }?.coerceAtLeast(1L) ?: 1L
|
val rawMaxTime = data.maxOfOrNull { it.second }?.coerceAtLeast(1L) ?: 1L
|
||||||
|
|
||||||
|
// 向上取整逻辑:根据时长跨度选择合适的对齐单位
|
||||||
|
val roundedMaxTime = when {
|
||||||
|
rawMaxTime < 60_000 -> 60_000L // 不足1分钟取1分钟
|
||||||
|
rawMaxTime < 10 * 60_000 -> ((rawMaxTime + 59_999) / 60_000) * 60_000L // 10分钟内按1分钟对齐
|
||||||
|
rawMaxTime < 60 * 60_000 -> ((rawMaxTime + 5 * 60_000 - 1) / (5 * 60_000)) * 5 * 60_000L // 1小时内按5分钟对齐
|
||||||
|
rawMaxTime < 12 * 3600_000 -> ((rawMaxTime + 3600_000 - 1) / 3600_000) * 3600_000L // 12小时内按1小时对齐
|
||||||
|
else -> ((rawMaxTime + 4 * 3600_000 - 1) / (4 * 3600_000)) * 4 * 3600_000L // 超过12小时按4小时对齐
|
||||||
|
}
|
||||||
|
|
||||||
GlassCard(
|
GlassCard(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
@@ -30,7 +59,16 @@ fun ReadingTimeBarChartCard(
|
|||||||
.adaptiveHorizontalPadding(vertical = 8.dp)
|
.adaptiveHorizontalPadding(vertical = 8.dp)
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.padding(16.dp)) {
|
Column(modifier = Modifier.padding(16.dp)) {
|
||||||
AppText("阅读时长分布", style = LegadoTheme.typography.titleMedium)
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.BarChart,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = LegadoTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
AppText("阅读时长分布", style = LegadoTheme.typography.titleMedium)
|
||||||
|
}
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
@@ -41,13 +79,14 @@ fun ReadingTimeBarChartCard(
|
|||||||
// Y-Axis
|
// Y-Axis
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.width(32.dp)
|
||||||
.fillMaxHeight()
|
.fillMaxHeight()
|
||||||
.padding(bottom = 20.dp),
|
.padding(bottom = 20.dp),
|
||||||
verticalArrangement = Arrangement.SpaceBetween,
|
verticalArrangement = Arrangement.SpaceBetween,
|
||||||
horizontalAlignment = Alignment.End
|
horizontalAlignment = Alignment.End
|
||||||
) {
|
) {
|
||||||
AppText(
|
AppText(
|
||||||
text = formatChartDuration(maxTime),
|
text = ReadRecordFormatter.formatDuration(roundedMaxTime),
|
||||||
style = LegadoTheme.typography.labelSmall,
|
style = LegadoTheme.typography.labelSmall,
|
||||||
fontSize = 8.sp,
|
fontSize = 8.sp,
|
||||||
color = LegadoTheme.colorScheme.onSurfaceVariant
|
color = LegadoTheme.colorScheme.onSurfaceVariant
|
||||||
@@ -71,7 +110,12 @@ fun ReadingTimeBarChartCard(
|
|||||||
horizontalArrangement = Arrangement.SpaceEvenly
|
horizontalArrangement = Arrangement.SpaceEvenly
|
||||||
) {
|
) {
|
||||||
data.forEachIndexed { index, (date, time) ->
|
data.forEachIndexed { index, (date, time) ->
|
||||||
val heightFactor = time.toFloat() / maxTime
|
val targetHeightFactor = time.toFloat() / roundedMaxTime
|
||||||
|
val heightFactor by animateFloatAsState(
|
||||||
|
targetValue = targetHeightFactor,
|
||||||
|
animationSpec = tween(durationMillis = 320, delayMillis = index * 20),
|
||||||
|
label = "BarHeight"
|
||||||
|
)
|
||||||
|
|
||||||
val showLabel = when (period) {
|
val showLabel = when (period) {
|
||||||
ReadPeriod.DAY -> true
|
ReadPeriod.DAY -> true
|
||||||
@@ -98,6 +142,7 @@ fun ReadingTimeBarChartCard(
|
|||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
|
.widthIn(max = 16.dp)
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
contentAlignment = Alignment.BottomCenter
|
contentAlignment = Alignment.BottomCenter
|
||||||
) {
|
) {
|
||||||
@@ -109,7 +154,7 @@ fun ReadingTimeBarChartCard(
|
|||||||
.clip(RoundedCornerShape(topStart = 4.dp, topEnd = 4.dp))
|
.clip(RoundedCornerShape(topStart = 4.dp, topEnd = 4.dp))
|
||||||
.background(
|
.background(
|
||||||
if (time > 0) LegadoTheme.colorScheme.primary
|
if (time > 0) LegadoTheme.colorScheme.primary
|
||||||
else LegadoTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)
|
else LegadoTheme.colorScheme.surfaceVariant
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -120,7 +165,10 @@ fun ReadingTimeBarChartCard(
|
|||||||
text = labelText,
|
text = labelText,
|
||||||
style = LegadoTheme.typography.labelSmall,
|
style = LegadoTheme.typography.labelSmall,
|
||||||
fontSize = 8.sp,
|
fontSize = 8.sp,
|
||||||
color = LegadoTheme.colorScheme.onSurfaceVariant
|
color = LegadoTheme.colorScheme.onSurfaceVariant,
|
||||||
|
softWrap = false,
|
||||||
|
overflow = TextOverflow.Visible,
|
||||||
|
modifier = Modifier.wrapContentWidth(unbounded = true)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -131,10 +179,3 @@ fun ReadingTimeBarChartCard(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun formatChartDuration(millis: Long): String {
|
|
||||||
val totalMinutes = millis / (1000 * 60)
|
|
||||||
val hours = totalMinutes / 60
|
|
||||||
val minutes = totalMinutes % 60
|
|
||||||
return if (hours > 0) "${hours}h${if (minutes > 0) "${minutes}m" else ""}" else "${minutes}m"
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,22 @@
|
|||||||
package io.legado.app.ui.book.readRecord.component
|
package io.legado.app.ui.book.readRecord.component
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.animation.AnimatedContent
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.slideInVertically
|
||||||
|
import androidx.compose.animation.slideOutVertically
|
||||||
|
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.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.layout.width
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Equalizer
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
@@ -10,6 +26,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import io.legado.app.ui.theme.LegadoTheme
|
import io.legado.app.ui.theme.LegadoTheme
|
||||||
import io.legado.app.ui.theme.adaptiveHorizontalPadding
|
import io.legado.app.ui.theme.adaptiveHorizontalPadding
|
||||||
import io.legado.app.ui.widget.components.card.GlassCard
|
import io.legado.app.ui.widget.components.card.GlassCard
|
||||||
|
import io.legado.app.ui.widget.components.icon.AppIcon
|
||||||
import io.legado.app.ui.widget.components.text.AppText
|
import io.legado.app.ui.widget.components.text.AppText
|
||||||
|
|
||||||
data class StatItem(val label: String, val value: String)
|
data class StatItem(val label: String, val value: String)
|
||||||
@@ -26,12 +43,24 @@ fun StatsGridCard(
|
|||||||
.adaptiveHorizontalPadding(vertical = 8.dp)
|
.adaptiveHorizontalPadding(vertical = 8.dp)
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.padding(16.dp)) {
|
Column(modifier = Modifier.padding(16.dp)) {
|
||||||
AppText(title, style = LegadoTheme.typography.titleMedium)
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
AppIcon(
|
||||||
|
imageVector = Icons.Default.Equalizer,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = LegadoTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
AppText(title, style = LegadoTheme.typography.titleMedium)
|
||||||
|
}
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
for (i in items.indices step 2) {
|
for (i in items.indices step 2) {
|
||||||
Row(modifier = Modifier.fillMaxWidth()) {
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
StatCell(items[i], Modifier.weight(1f))
|
StatCell(items[i], Modifier.weight(1f))
|
||||||
if (i + 1 < items.size) {
|
if (i + 1 < items.size) {
|
||||||
StatCell(items[i + 1], Modifier.weight(1f))
|
StatCell(items[i + 1], Modifier.weight(1f))
|
||||||
@@ -55,18 +84,26 @@ private fun StatCell(
|
|||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
horizontalAlignment = Alignment.Start
|
horizontalAlignment = Alignment.Start
|
||||||
) {
|
) {
|
||||||
AppText(
|
|
||||||
text = item.value,
|
|
||||||
style = LegadoTheme.typography.titleLarge,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
color = LegadoTheme.colorScheme.primary,
|
|
||||||
textAlign = TextAlign.Start
|
|
||||||
)
|
|
||||||
AppText(
|
AppText(
|
||||||
text = item.label,
|
text = item.label,
|
||||||
style = LegadoTheme.typography.labelSmall,
|
style = LegadoTheme.typography.labelSmall,
|
||||||
color = LegadoTheme.colorScheme.onSurfaceVariant,
|
color = LegadoTheme.colorScheme.onSurfaceVariant,
|
||||||
textAlign = TextAlign.Start
|
textAlign = TextAlign.Start
|
||||||
)
|
)
|
||||||
|
AnimatedContent(
|
||||||
|
targetState = item.value,
|
||||||
|
transitionSpec = {
|
||||||
|
(fadeIn() + slideInVertically { it / 2 }).togetherWith(fadeOut() + slideOutVertically { -it / 2 })
|
||||||
|
},
|
||||||
|
label = "StatValue"
|
||||||
|
) { targetValue ->
|
||||||
|
AppText(
|
||||||
|
text = targetValue,
|
||||||
|
style = LegadoTheme.typography.titleLarge,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = LegadoTheme.colorScheme.primary,
|
||||||
|
textAlign = TextAlign.Start
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,6 +128,17 @@ object MainNavigator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MainRouteRssFavorites,
|
||||||
|
MainRouteRuleSub -> {
|
||||||
|
if (currentRoute == MainRouteHome) {
|
||||||
|
backStack.add(route)
|
||||||
|
} else {
|
||||||
|
backStack.clear()
|
||||||
|
backStack.add(MainRouteHome)
|
||||||
|
backStack.add(route)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
MainRouteReadRecord -> {
|
MainRouteReadRecord -> {
|
||||||
if (currentRoute == MainRouteHome) {
|
if (currentRoute == MainRouteHome) {
|
||||||
backStack.add(route)
|
backStack.add(route)
|
||||||
|
|||||||
@@ -5,13 +5,16 @@ import androidx.compose.foundation.combinedClickable
|
|||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
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.GridCells
|
||||||
|
import androidx.compose.foundation.lazy.grid.GridItemSpan
|
||||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
import androidx.compose.foundation.lazy.grid.items
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
@@ -51,14 +54,15 @@ import io.legado.app.ui.theme.LegadoTheme
|
|||||||
import io.legado.app.ui.theme.adaptiveContentPadding
|
import io.legado.app.ui.theme.adaptiveContentPadding
|
||||||
import io.legado.app.ui.widget.components.SourceIcon
|
import io.legado.app.ui.widget.components.SourceIcon
|
||||||
import io.legado.app.ui.widget.components.alert.AppAlertDialog
|
import io.legado.app.ui.widget.components.alert.AppAlertDialog
|
||||||
|
import io.legado.app.ui.widget.components.card.GlassCard
|
||||||
import io.legado.app.ui.widget.components.divider.PillDivider
|
import io.legado.app.ui.widget.components.divider.PillDivider
|
||||||
import io.legado.app.ui.widget.components.divider.PillHeaderDivider
|
import io.legado.app.ui.widget.components.divider.PillHeaderDivider
|
||||||
|
import io.legado.app.ui.widget.components.icon.AppIcon
|
||||||
import io.legado.app.ui.widget.components.list.ListScaffold
|
import io.legado.app.ui.widget.components.list.ListScaffold
|
||||||
import io.legado.app.ui.widget.components.menuItem.MenuItemIcon
|
import io.legado.app.ui.widget.components.menuItem.MenuItemIcon
|
||||||
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
|
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
|
||||||
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
|
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
|
||||||
import io.legado.app.ui.widget.components.text.AppText
|
import io.legado.app.ui.widget.components.text.AppText
|
||||||
import io.legado.app.ui.widget.components.topbar.TopBarActionButton
|
|
||||||
import io.legado.app.utils.openUrl
|
import io.legado.app.utils.openUrl
|
||||||
import io.legado.app.utils.startActivity
|
import io.legado.app.utils.startActivity
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
@@ -136,18 +140,6 @@ fun RssScreen(
|
|||||||
onSearchToggle = { viewModel.toggleSearchVisible(it) },
|
onSearchToggle = { viewModel.toggleSearchVisible(it) },
|
||||||
onSearchQueryChange = { viewModel.search(it) },
|
onSearchQueryChange = { viewModel.search(it) },
|
||||||
searchPlaceholder = stringResource(R.string.search_rss_source),
|
searchPlaceholder = stringResource(R.string.search_rss_source),
|
||||||
topBarActions = {
|
|
||||||
TopBarActionButton(
|
|
||||||
onClick = { viewModel.openRuleSub() },
|
|
||||||
imageVector = Icons.Default.Subscriptions,
|
|
||||||
contentDescription = stringResource(R.string.rule_subscription)
|
|
||||||
)
|
|
||||||
TopBarActionButton(
|
|
||||||
onClick = { viewModel.openFavorites() },
|
|
||||||
imageVector = Icons.Default.Star,
|
|
||||||
contentDescription = stringResource(R.string.favorite)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
dropDownMenuContent = { dismiss ->
|
dropDownMenuContent = { dismiss ->
|
||||||
RoundDropdownMenuItem(
|
RoundDropdownMenuItem(
|
||||||
onClick = {
|
onClick = {
|
||||||
@@ -185,6 +177,58 @@ fun RssScreen(
|
|||||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
) {
|
) {
|
||||||
|
item(span = { GridItemSpan(maxLineSpan) }) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
GlassCard(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
onClick = { viewModel.openRuleSub() }
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(all = 12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
AppIcon(
|
||||||
|
imageVector = Icons.Default.Subscriptions,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
AppText(
|
||||||
|
text = stringResource(R.string.rule_subscription),
|
||||||
|
style = LegadoTheme.typography.labelMediumEmphasized
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
GlassCard(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
onClick = { viewModel.openFavorites() }
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(all = 12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
AppIcon(
|
||||||
|
imageVector = Icons.Default.Star,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(12.dp))
|
||||||
|
AppText(
|
||||||
|
text = stringResource(R.string.favorite),
|
||||||
|
style = LegadoTheme.typography.labelMediumEmphasized
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
items(uiState.items, key = { it.sourceUrl }) { source ->
|
items(uiState.items, key = { it.sourceUrl }) { source ->
|
||||||
RssSourceGridItem(
|
RssSourceGridItem(
|
||||||
modifier = Modifier.animateItem(),
|
modifier = Modifier.animateItem(),
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import androidx.appcompat.app.AppCompatActivity
|
|||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
@@ -86,11 +84,7 @@ fun RuleSubScreen(
|
|||||||
RoundDropdownMenuItem(
|
RoundDropdownMenuItem(
|
||||||
text = stringResource(R.string.sort),
|
text = stringResource(R.string.sort),
|
||||||
leadingIcon = {
|
leadingIcon = {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Icon(Icons.AutoMirrored.Filled.Sort, null, modifier = Modifier.size(18.dp))
|
||||||
Icon(Icons.AutoMirrored.Filled.Sort, null, modifier = Modifier.size(18.dp))
|
|
||||||
Spacer(Modifier.size(12.dp))
|
|
||||||
AppText(stringResource(R.string.sort))
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
viewModel.resetOrder()
|
viewModel.resetOrder()
|
||||||
@@ -231,12 +225,14 @@ fun RuleSubEditDialog(
|
|||||||
value = name,
|
value = name,
|
||||||
onValueChange = { name = it },
|
onValueChange = { name = it },
|
||||||
label = stringResource(R.string.name),
|
label = stringResource(R.string.name),
|
||||||
|
backgroundColor = LegadoTheme.colorScheme.onSheetContent,
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
AppTextField(
|
AppTextField(
|
||||||
value = url,
|
value = url,
|
||||||
onValueChange = { url = it },
|
onValueChange = { url = it },
|
||||||
label = "URL",
|
label = "URL",
|
||||||
|
backgroundColor = LegadoTheme.colorScheme.onSheetContent,
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package io.legado.app.ui.theme
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.drawWithContent
|
||||||
|
import androidx.compose.ui.graphics.BlendMode
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.CompositingStrategy
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies a horizontal fading edge effect to a scrollable container.
|
||||||
|
*
|
||||||
|
* @param leftAlpha 0f = fully faded, 1f = fully visible
|
||||||
|
* @param rightAlpha 0f = fully faded, 1f = fully visible
|
||||||
|
* @param gradientWidth width of the fade gradient on each side
|
||||||
|
*/
|
||||||
|
fun Modifier.fadingEdge(
|
||||||
|
leftAlpha: Float,
|
||||||
|
rightAlpha: Float,
|
||||||
|
gradientWidth: Dp = 24.dp
|
||||||
|
): Modifier = graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen)
|
||||||
|
.drawWithContent {
|
||||||
|
drawContent()
|
||||||
|
val width = size.width
|
||||||
|
val gradientWidthPx = gradientWidth.toPx()
|
||||||
|
val leftStop = gradientWidthPx / width
|
||||||
|
val rightStop = 1f - (gradientWidthPx / width)
|
||||||
|
drawRect(
|
||||||
|
brush = Brush.horizontalGradient(
|
||||||
|
colorStops = arrayOf(
|
||||||
|
0f to Color.Black.copy(alpha = 1f - leftAlpha),
|
||||||
|
leftStop to Color.Black,
|
||||||
|
rightStop to Color.Black,
|
||||||
|
1f to Color.Black.copy(alpha = 1f - rightAlpha)
|
||||||
|
),
|
||||||
|
startX = 0f,
|
||||||
|
endX = width
|
||||||
|
),
|
||||||
|
blendMode = BlendMode.DstIn
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience overload that derives fade alphas from a [LazyListState].
|
||||||
|
* Left fade appears when scrolled past the start; right fade appears when more content is available.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun Modifier.fadingEdge(
|
||||||
|
listState: LazyListState,
|
||||||
|
gradientWidth: Dp = 24.dp
|
||||||
|
): Modifier {
|
||||||
|
val showLeft by remember {
|
||||||
|
derivedStateOf {
|
||||||
|
listState.firstVisibleItemIndex > 0 || listState.firstVisibleItemScrollOffset > 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val showRight by remember {
|
||||||
|
derivedStateOf { listState.canScrollForward }
|
||||||
|
}
|
||||||
|
val leftAlpha by animateFloatAsState(
|
||||||
|
targetValue = if (showLeft) 1f else 0f,
|
||||||
|
animationSpec = tween(200),
|
||||||
|
label = "LeftFadeAlpha"
|
||||||
|
)
|
||||||
|
val rightAlpha by animateFloatAsState(
|
||||||
|
targetValue = if (showRight) 1f else 0f,
|
||||||
|
animationSpec = tween(200),
|
||||||
|
label = "RightFadeAlpha"
|
||||||
|
)
|
||||||
|
return fadingEdge(leftAlpha, rightAlpha, gradientWidth)
|
||||||
|
}
|
||||||
@@ -93,7 +93,8 @@ fun MediumOutlinedIconButton(
|
|||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = imageVector,
|
imageVector = imageVector,
|
||||||
contentDescription = contentDescription
|
contentDescription = contentDescription,
|
||||||
|
tint = LegadoTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
package io.legado.app.ui.widget.components.tabRow
|
package io.legado.app.ui.widget.components.tabRow
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.PrimaryScrollableTabRow
|
import androidx.compose.material3.PrimaryScrollableTabRow
|
||||||
|
import androidx.compose.material3.PrimaryTabRow
|
||||||
import androidx.compose.material3.Tab
|
import androidx.compose.material3.Tab
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
@@ -12,6 +11,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.legado.app.ui.theme.LegadoTheme
|
import io.legado.app.ui.theme.LegadoTheme
|
||||||
import io.legado.app.ui.theme.ThemeResolver
|
import io.legado.app.ui.theme.ThemeResolver
|
||||||
|
import io.legado.app.ui.widget.components.text.AppText
|
||||||
import top.yukonga.miuix.kmp.basic.TabRowDefaults
|
import top.yukonga.miuix.kmp.basic.TabRowDefaults
|
||||||
import top.yukonga.miuix.kmp.basic.TabRowWithContour
|
import top.yukonga.miuix.kmp.basic.TabRowWithContour
|
||||||
|
|
||||||
@@ -20,7 +20,8 @@ fun AppTabRow(
|
|||||||
tabTitles: List<String>,
|
tabTitles: List<String>,
|
||||||
selectedTabIndex: Int,
|
selectedTabIndex: Int,
|
||||||
onTabSelected: (Int) -> Unit,
|
onTabSelected: (Int) -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier,
|
||||||
|
isScrollable: Boolean = true
|
||||||
) {
|
) {
|
||||||
val composeEngine = LegadoTheme.composeEngine
|
val composeEngine = LegadoTheme.composeEngine
|
||||||
|
|
||||||
@@ -36,29 +37,59 @@ fun AppTabRow(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
PrimaryScrollableTabRow(
|
if (isScrollable) {
|
||||||
selectedTabIndex = selectedTabIndex,
|
PrimaryScrollableTabRow(
|
||||||
edgePadding = 0.dp,
|
selectedTabIndex = selectedTabIndex,
|
||||||
divider = { },
|
edgePadding = 0.dp,
|
||||||
containerColor = Color.Transparent,
|
divider = { },
|
||||||
minTabWidth = 0.dp,
|
containerColor = Color.Transparent,
|
||||||
modifier = modifier
|
minTabWidth = 0.dp,
|
||||||
) {
|
modifier = modifier
|
||||||
tabTitles.forEachIndexed { index, title ->
|
) {
|
||||||
Tab(
|
tabTitles.forEachIndexed { index, title ->
|
||||||
selected = selectedTabIndex == index,
|
AppTab(
|
||||||
onClick = { onTabSelected(index) },
|
selected = selectedTabIndex == index,
|
||||||
text = {
|
onClick = { onTabSelected(index) },
|
||||||
Text(
|
title = title
|
||||||
text = title,
|
)
|
||||||
maxLines = 1,
|
}
|
||||||
overflow = TextOverflow.Ellipsis,
|
}
|
||||||
modifier = Modifier.padding(horizontal = 8.dp),
|
} else {
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
PrimaryTabRow(
|
||||||
)
|
selectedTabIndex = selectedTabIndex,
|
||||||
}
|
divider = { },
|
||||||
)
|
containerColor = Color.Transparent,
|
||||||
|
modifier = modifier
|
||||||
|
) {
|
||||||
|
tabTitles.forEachIndexed { index, title ->
|
||||||
|
AppTab(
|
||||||
|
selected = selectedTabIndex == index,
|
||||||
|
onClick = { onTabSelected(index) },
|
||||||
|
title = title
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AppTab(
|
||||||
|
selected: Boolean,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
title: String
|
||||||
|
) {
|
||||||
|
Tab(
|
||||||
|
selected = selected,
|
||||||
|
onClick = onClick,
|
||||||
|
text = {
|
||||||
|
AppText(
|
||||||
|
text = title,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.padding(horizontal = 8.dp),
|
||||||
|
color = if (selected) LegadoTheme.colorScheme.primary else LegadoTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
@@ -90,7 +90,7 @@ object ConvertUtils {
|
|||||||
//计算单位的,原理是利用lg,公式是 lg(1024^n) = nlg(1024),最后 nlg(1024)/lg(1024) = n。
|
//计算单位的,原理是利用lg,公式是 lg(1024^n) = nlg(1024),最后 nlg(1024)/lg(1024) = n。
|
||||||
val digitGroups = (log10(length.toDouble()) / log10(1024.0)).toInt()
|
val digitGroups = (log10(length.toDouble()) / log10(1024.0)).toInt()
|
||||||
//计算原理是,size/单位值。单位值指的是:比如说b = 1024,KB = 1024^2
|
//计算原理是,size/单位值。单位值指的是:比如说b = 1024,KB = 1024^2
|
||||||
return DecimalFormat("#,##0.##").format(length / 1024.0.pow(digitGroups.toDouble())) + " " + units[digitGroups]
|
return DecimalFormat("#.##").format(length / 1024.0.pow(digitGroups.toDouble())) + " " + units[digitGroups]
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmOverloads
|
@JvmOverloads
|
||||||
|
|||||||
Reference in New Issue
Block a user