界面优化

This commit is contained in:
HapeLee
2026-05-17 04:59:39 +08:00
parent 1ac5f6549b
commit 5987e3414c
11 changed files with 283 additions and 157 deletions
@@ -33,13 +33,11 @@ import androidx.compose.material.icons.outlined.FolderZip
import androidx.compose.material.icons.outlined.Image import androidx.compose.material.icons.outlined.Image
import androidx.compose.material.icons.outlined.Settings import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import io.legado.app.ui.widget.components.progressIndicator.AppCircularProgressIndicator
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@@ -48,10 +46,8 @@ 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.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.CornerRadius
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.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.LifecycleEventObserver
@@ -73,7 +69,6 @@ import io.legado.app.ui.book.search.ScopeSelectSheet
import io.legado.app.ui.book.source.edit.BookSourceEditActivity import io.legado.app.ui.book.source.edit.BookSourceEditActivity
import io.legado.app.ui.book.source.manage.BookSourceActivity import io.legado.app.ui.book.source.manage.BookSourceActivity
import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.widget.components.progressIndicator.AppLinearProgressIndicator
import io.legado.app.ui.widget.components.AppTextField import io.legado.app.ui.widget.components.AppTextField
import io.legado.app.ui.widget.components.EmptyMessage import io.legado.app.ui.widget.components.EmptyMessage
import io.legado.app.ui.widget.components.alert.AppAlertDialog import io.legado.app.ui.widget.components.alert.AppAlertDialog
@@ -86,7 +81,8 @@ 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.modalBottomSheet.AppModalBottomSheet import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
import io.legado.app.ui.widget.components.tabRow.AppTabRow import io.legado.app.ui.widget.components.progressIndicator.AppCircularProgressIndicator
import io.legado.app.ui.widget.components.progressIndicator.AppLinearProgressIndicator
import io.legado.app.ui.widget.components.text.AppText import io.legado.app.ui.widget.components.text.AppText
import io.legado.app.utils.StartActivityContract import io.legado.app.utils.StartActivityContract
import io.legado.app.utils.startActivity import io.legado.app.utils.startActivity
@@ -103,7 +99,11 @@ fun WebFileSheet(
) { ) {
AppModalBottomSheet(show = show, onDismissRequest = onDismissRequest, title = title) { AppModalBottomSheet(show = show, onDismissRequest = onDismissRequest, title = title) {
if (files.isEmpty()) { if (files.isEmpty()) {
Box(modifier = Modifier.fillMaxWidth().padding(24.dp), contentAlignment = Alignment.Center) { Box(
modifier = Modifier
.fillMaxWidth()
.padding(24.dp), contentAlignment = Alignment.Center
) {
Text(text = stringResource(R.string.empty)) Text(text = stringResource(R.string.empty))
} }
} else { } else {
@@ -111,7 +111,9 @@ fun WebFileSheet(
items(files, key = { it.name }) { file -> items(files, key = { it.name }) { file ->
GlassCard(onClick = { onSelect(file) }) { GlassCard(onClick = { onSelect(file) }) {
Row( Row(
modifier = Modifier.fillMaxWidth().padding(16.dp), modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp), horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
@@ -269,6 +271,31 @@ fun ChangeCoverSheet(
} }
} }
@Composable
fun ChangeSourceSheet(
data: Book?,
onDismissRequest: () -> Unit,
onReplace: (BookSource, Book, List<BookChapter>, ChangeSourceMigrationOptions) -> Unit,
onAddAsNew: (Book, List<BookChapter>) -> Unit,
) {
var cachedData by remember { mutableStateOf(data) }
if (data != null) {
cachedData = data
}
val currentData = cachedData
if (currentData != null) {
ChangeSourceSheet(
show = data != null,
oldBook = currentData,
onDismissRequest = onDismissRequest,
onReplace = onReplace,
onAddAsNew = onAddAsNew,
)
}
}
@Composable @Composable
fun ChangeSourceSheet( fun ChangeSourceSheet(
show: Boolean, show: Boolean,
@@ -426,7 +453,9 @@ fun ChangeSourceSheet(
if (items.isEmpty()) { if (items.isEmpty()) {
Box( Box(
modifier = Modifier.fillMaxWidth().padding(vertical = 40.dp), modifier = Modifier
.fillMaxWidth()
.padding(vertical = 40.dp),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
EmptyMessage( EmptyMessage(
@@ -10,7 +10,6 @@ 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.layout.ExperimentalLayoutApi 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.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@@ -27,6 +26,7 @@ import androidx.compose.foundation.lazy.rememberLazyListState
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.Bookmarks import androidx.compose.material.icons.filled.Bookmarks
import androidx.compose.material.icons.filled.Clear
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.Info import androidx.compose.material.icons.filled.Info
@@ -35,7 +35,6 @@ import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.SelectAll import androidx.compose.material.icons.filled.SelectAll
import androidx.compose.material.icons.filled.SkipNext
import androidx.compose.material.icons.filled.Stop import androidx.compose.material.icons.filled.Stop
import androidx.compose.material.icons.filled.Upload import androidx.compose.material.icons.filled.Upload
import androidx.compose.material3.Checkbox import androidx.compose.material3.Checkbox
@@ -54,16 +53,19 @@ import androidx.compose.ui.Modifier
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.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import io.legado.app.R 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.BookChapter
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
import io.legado.app.domain.usecase.BatchChangeSourcePreviewStatus import io.legado.app.domain.usecase.BatchChangeSourcePreviewStatus
import io.legado.app.domain.usecase.ChangeSourceMigrationOptions
import io.legado.app.help.book.getExportFileName import io.legado.app.help.book.getExportFileName
import io.legado.app.help.book.isLocal import io.legado.app.help.book.isLocal
import io.legado.app.help.book.tryParesExportFileName import io.legado.app.help.book.tryParesExportFileName
@@ -80,11 +82,9 @@ import io.legado.app.ui.widget.components.AppFloatingActionButtonMenu
import io.legado.app.ui.widget.components.AppTextField import io.legado.app.ui.widget.components.AppTextField
import io.legado.app.ui.widget.components.FabMenuItem import io.legado.app.ui.widget.components.FabMenuItem
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.card.GlassCard import io.legado.app.ui.widget.components.card.GlassCard
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
import io.legado.app.ui.widget.components.card.TextCard import io.legado.app.ui.widget.components.card.TextCard
@@ -805,28 +805,25 @@ private fun BookshelfManageScreen(
} }
} }
singleChangeSourceBook?.let { book -> ChangeSourceSheet(
ChangeSourceSheet( data = singleChangeSourceBook,
show = true, onDismissRequest = { singleChangeSourceBook = null },
oldBook = book, onReplace = { source: BookSource, newBook: Book, toc: List<BookChapter>, options: ChangeSourceMigrationOptions ->
onDismissRequest = { singleChangeSourceBook = null }, viewModel.dispatch(
onReplace = { source, newBook, toc, options -> BookshelfManageScreenIntent.ChangeBookSource(
viewModel.dispatch( oldBookUrl = singleChangeSourceBook?.bookUrl ?: "",
BookshelfManageScreenIntent.ChangeBookSource( source = source,
oldBookUrl = book.bookUrl, book = newBook,
source = source, chapters = toc,
book = newBook, options = options,
chapters = toc,
options = options,
)
) )
singleChangeSourceBook = null )
}, singleChangeSourceBook = null
onAddAsNew = { _, _ -> },
context.toastOnUi("请在书籍详情页添加为新书") onAddAsNew = { _: Book, _: List<BookChapter> ->
}, context.toastOnUi("请在书籍详情页添加为新书")
) },
} )
BookSourcePickerSheet( BookSourcePickerSheet(
show = showBatchSourcePickerSheet, show = showBatchSourcePickerSheet,
@@ -857,8 +854,7 @@ private fun BookshelfManageScreen(
) )
BatchChangePreviewSheet( BatchChangePreviewSheet(
show = state.batchChangePreviewItems.isNotEmpty(), data = state.batchChangePreviewItems.takeIf { it.isNotEmpty() },
items = state.batchChangePreviewItems,
onDismissRequest = { viewModel.dispatch(BookshelfManageScreenIntent.DismissBatchChangePreview) }, onDismissRequest = { viewModel.dispatch(BookshelfManageScreenIntent.DismissBatchChangePreview) },
onOpenBook = { book, inBookshelf -> onOpenBook = { book, inBookshelf ->
viewModel.dispatch(BookshelfManageScreenIntent.OpenBookInfoPreview(book, inBookshelf)) viewModel.dispatch(BookshelfManageScreenIntent.OpenBookInfoPreview(book, inBookshelf))
@@ -871,29 +867,27 @@ private fun BookshelfManageScreen(
}, },
onShowOtherSources = { item -> otherSourcePreviewItem = item }, onShowOtherSources = { item -> otherSourcePreviewItem = item },
onMigrateAll = { viewModel.dispatch(BookshelfManageScreenIntent.MigrateAllPreviewItems) }, onMigrateAll = { viewModel.dispatch(BookshelfManageScreenIntent.MigrateAllPreviewItems) },
onAddAllToShelf = { viewModel.dispatch(BookshelfManageScreenIntent.AddAllPreviewItemsToShelf) },
) )
manualSearchPreviewBook?.let { book -> ChangeSourceSheet(
ChangeSourceSheet( data = manualSearchPreviewBook,
show = true, onDismissRequest = { manualSearchPreviewBook = null },
oldBook = book, onReplace = { source: BookSource, newBook: Book, toc: List<BookChapter>, _ ->
onDismissRequest = { manualSearchPreviewBook = null }, viewModel.dispatch(
onReplace = { source, newBook, toc, _ -> BookshelfManageScreenIntent.UpdatePreviewItem(
viewModel.dispatch( oldBookUrl = manualSearchPreviewBook?.bookUrl ?: "",
BookshelfManageScreenIntent.UpdatePreviewItem( source = source,
oldBookUrl = book.bookUrl, book = newBook,
source = source, chapterCount = toc.size,
book = newBook,
chapterCount = toc.size,
)
) )
manualSearchPreviewBook = null )
}, manualSearchPreviewBook = null
onAddAsNew = { _, _ -> },
context.toastOnUi("请先选择替换候选后再新增至书架") onAddAsNew = { _: Book, _: List<BookChapter> ->
}, context.toastOnUi("请先选择替换候选后再新增至书架")
) },
} )
OtherSourceOptionsSheet( OtherSourceOptionsSheet(
item = otherSourcePreviewItem, item = otherSourcePreviewItem,
@@ -1392,8 +1386,7 @@ private fun SourcePickerItem(
@Composable @Composable
private fun BatchChangePreviewSheet( private fun BatchChangePreviewSheet(
show: Boolean, data: List<BatchChangeSourcePreviewItem>?,
items: List<BatchChangeSourcePreviewItem>,
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
onOpenBook: (Book, Boolean) -> Unit, onOpenBook: (Book, Boolean) -> Unit,
onManualSearch: (Book) -> Unit, onManualSearch: (Book) -> Unit,
@@ -1402,11 +1395,19 @@ private fun BatchChangePreviewSheet(
onAddToShelf: (String) -> Unit, onAddToShelf: (String) -> Unit,
onShowOtherSources: (BatchChangeSourcePreviewItem) -> Unit, onShowOtherSources: (BatchChangeSourcePreviewItem) -> Unit,
onMigrateAll: () -> Unit, onMigrateAll: () -> Unit,
onAddAllToShelf: () -> Unit,
) { ) {
AppModalBottomSheet( AppModalBottomSheet(
show = show, data = data,
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
title = "批量换源预览", title = "批量换源预览",
startAction = {
SmallTonalTextButton(
text = "新增全部",
imageVector = Icons.Default.Add,
onClick = onAddAllToShelf,
)
},
endAction = { endAction = {
SmallTonalTextButton( SmallTonalTextButton(
text = "迁移全部", text = "迁移全部",
@@ -1414,7 +1415,7 @@ private fun BatchChangePreviewSheet(
onClick = onMigrateAll, onClick = onMigrateAll,
) )
} }
) { ) { items ->
LazyColumn( LazyColumn(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -1449,15 +1450,15 @@ private fun BatchChangePreviewRow(
onShowOtherSources: (BatchChangeSourcePreviewItem) -> Unit, onShowOtherSources: (BatchChangeSourcePreviewItem) -> Unit,
) { ) {
val candidate = item.selectedCandidate val candidate = item.selectedCandidate
NormalCard( GlassCard(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
containerColor = LegadoTheme.colorScheme.surfaceContainerLow, containerColor = LegadoTheme.colorScheme.onSheetContent,
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(12.dp), .padding(12.dp),
verticalArrangement = Arrangement.spacedBy(10.dp), verticalArrangement = Arrangement.spacedBy(12.dp),
) { ) {
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@@ -1493,27 +1494,26 @@ private fun BatchChangePreviewRow(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
) )
} }
FlowRow( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.SpaceBetween
verticalArrangement = Arrangement.spacedBy(8.dp),
) { ) {
MediumIconButton( SmallTonalTextButton(
imageVector = Icons.Default.Search, imageVector = Icons.Default.Search,
onClick = { onManualSearch(item.oldBook) }, onClick = { onManualSearch(item.oldBook) },
) )
SmallTonalTextButton( SmallTonalTextButton(
text = "不迁移", text = "不迁移",
imageVector = Icons.Default.SkipNext, imageVector = Icons.Default.Clear,
onClick = { onSkip(item.oldBook.bookUrl) }, onClick = { onSkip(item.oldBook.bookUrl) },
) )
SmallTonalTextButton( SmallTonalTextButton(
text = "开始迁移", text = "迁移",
imageVector = Icons.Default.PlayArrow, imageVector = Icons.Default.PlayArrow,
onClick = { onMigrate(item.oldBook.bookUrl) }, onClick = { onMigrate(item.oldBook.bookUrl) },
) )
SmallTonalTextButton( SmallTonalTextButton(
text = "新增至书架", text = "新增",
imageVector = Icons.Default.Add, imageVector = Icons.Default.Add,
onClick = { onAddToShelf(item.oldBook.bookUrl) }, onClick = { onAddToShelf(item.oldBook.bookUrl) },
) )
@@ -1553,7 +1553,8 @@ private fun PreviewBookInfo(
AppText( AppText(
text = book.name, text = book.name,
style = LegadoTheme.typography.labelMedium, style = LegadoTheme.typography.labelMedium,
maxLines = 1, maxLines = 2,
overflow = TextOverflow.Ellipsis
) )
AppText( AppText(
text = "${book.getRealAuthor()} · ${chapterCount ?: 0}", text = "${book.getRealAuthor()} · ${chapterCount ?: 0}",
@@ -1566,8 +1567,7 @@ private fun PreviewBookInfo(
AppText( AppText(
text = title, text = title,
style = LegadoTheme.typography.labelMedium, style = LegadoTheme.typography.labelMedium,
color = LegadoTheme.colorScheme.error, color = LegadoTheme.colorScheme.error
maxLines = 2,
) )
} }
} }
@@ -1581,41 +1581,38 @@ private fun OtherSourceOptionsSheet(
onOpenBook: (Book) -> Unit, onOpenBook: (Book) -> Unit,
) { ) {
AppModalBottomSheet( AppModalBottomSheet(
show = item != null, data = item,
onDismissRequest = onDismissRequest, onDismissRequest = onDismissRequest,
title = "其他源信息", title = "其他源信息",
) { ) { currentItem ->
val currentItem = item LazyColumn(
if (currentItem != null) { modifier = Modifier
LazyColumn( .fillMaxWidth()
modifier = Modifier .heightIn(max = 560.dp),
.fillMaxWidth() verticalArrangement = Arrangement.spacedBy(8.dp),
.heightIn(max = 560.dp), ) {
verticalArrangement = Arrangement.spacedBy(8.dp), items(currentItem.candidates.indices.toList(), key = { it }) { index ->
) { val candidate = currentItem.candidates[index]
items(currentItem.candidates.indices.toList(), key = { it }) { index -> SelectionItemCard(
val candidate = currentItem.candidates[index] title = candidate.source.bookSourceName,
SelectionItemCard( subtitle = "${candidate.book.name} · ${candidate.chapterCount}",
title = candidate.source.bookSourceName, supportingContent = {
subtitle = "${candidate.book.name} · ${candidate.chapterCount}", AppText(
supportingContent = { text = candidate.book.getRealAuthor(),
AppText( style = LegadoTheme.typography.bodySmall,
text = candidate.book.getRealAuthor(), )
style = LegadoTheme.typography.bodySmall, },
) isSelected = index == currentItem.selectedCandidateIndex,
}, onToggleSelection = { onSelect(currentItem.oldBook.bookUrl, index) },
isSelected = index == currentItem.selectedCandidateIndex, trailingAction = {
onToggleSelection = { onSelect(currentItem.oldBook.bookUrl, index) }, SmallTonalIconButton(
trailingAction = { onClick = { onOpenBook(candidate.book) },
SmallTonalIconButton( imageVector = Icons.Default.Info,
onClick = { onOpenBook(candidate.book) }, contentDescription = null,
imageVector = Icons.Default.Info, )
contentDescription = null, },
) containerColor = LegadoTheme.colorScheme.onSheetContent,
}, )
containerColor = LegadoTheme.colorScheme.onSheetContent,
)
}
} }
} }
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
@@ -3,19 +3,19 @@ package io.legado.app.ui.book.manage
import android.app.Application import android.app.Application
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
import io.legado.app.constant.BookType
import io.legado.app.data.appDb
import io.legado.app.data.dao.BookChapterDao 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.dao.BookGroupDao import io.legado.app.data.dao.BookGroupDao
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.BookChapter import io.legado.app.data.entities.BookChapter
import io.legado.app.data.entities.BookGroup 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.constant.BookType import io.legado.app.domain.usecase.BatchCacheDownloadUseCase
import io.legado.app.domain.usecase.BatchChangeSourceCandidate import io.legado.app.domain.usecase.BatchChangeSourceCandidate
import io.legado.app.domain.usecase.BatchChangeSourcePreviewItem import io.legado.app.domain.usecase.BatchChangeSourcePreviewItem
import io.legado.app.domain.usecase.BatchChangeSourcePreviewStatus import io.legado.app.domain.usecase.BatchChangeSourcePreviewStatus
import io.legado.app.domain.usecase.BatchCacheDownloadUseCase
import io.legado.app.domain.usecase.CacheBookChaptersUseCase import io.legado.app.domain.usecase.CacheBookChaptersUseCase
import io.legado.app.domain.usecase.ChangeBookSourceUseCase import io.legado.app.domain.usecase.ChangeBookSourceUseCase
import io.legado.app.domain.usecase.ChangeSourceMigrationOptions import io.legado.app.domain.usecase.ChangeSourceMigrationOptions
@@ -23,11 +23,10 @@ import io.legado.app.domain.usecase.ClearBookCacheUseCase
import io.legado.app.domain.usecase.DeleteBooksUseCase import io.legado.app.domain.usecase.DeleteBooksUseCase
import io.legado.app.domain.usecase.UpdateBooksGroupUseCase import io.legado.app.domain.usecase.UpdateBooksGroupUseCase
import io.legado.app.help.book.BookHelp import io.legado.app.help.book.BookHelp
import io.legado.app.help.book.isAudio
import io.legado.app.help.book.isLocal import io.legado.app.help.book.isLocal
import io.legado.app.help.book.removeType import io.legado.app.help.book.removeType
import io.legado.app.model.CacheBook
import io.legado.app.help.config.LocalConfig import io.legado.app.help.config.LocalConfig
import io.legado.app.model.CacheBook
import io.legado.app.service.ExportBookService import io.legado.app.service.ExportBookService
import io.legado.app.ui.config.bookshelfConfig.BookshelfConfig import io.legado.app.ui.config.bookshelfConfig.BookshelfConfig
import io.legado.app.ui.config.bookshelfConfig.BookshelfManageScreenConfig import io.legado.app.ui.config.bookshelfConfig.BookshelfManageScreenConfig
@@ -39,17 +38,17 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.ensureActive import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.isActive
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.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.concurrent.ConcurrentHashMap
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
import java.util.concurrent.ConcurrentHashMap
data class BookshelfManageScreenExportConfig( data class BookshelfManageScreenExportConfig(
val exportUseReplace: Boolean = true, val exportUseReplace: Boolean = true,
@@ -123,6 +122,7 @@ sealed interface BookshelfManageScreenIntent {
data class AddPreviewItemToShelf(val oldBookUrl: String) : BookshelfManageScreenIntent data class AddPreviewItemToShelf(val oldBookUrl: String) : BookshelfManageScreenIntent
data class OpenBookInfoPreview(val book: Book, val inBookshelf: Boolean) : BookshelfManageScreenIntent data class OpenBookInfoPreview(val book: Book, val inBookshelf: Boolean) : BookshelfManageScreenIntent
data object MigrateAllPreviewItems : BookshelfManageScreenIntent data object MigrateAllPreviewItems : BookshelfManageScreenIntent
data object AddAllPreviewItemsToShelf : BookshelfManageScreenIntent
data object DismissChangeSourceStatus : BookshelfManageScreenIntent data object DismissChangeSourceStatus : BookshelfManageScreenIntent
data object DismissBatchChangePreview : BookshelfManageScreenIntent data object DismissBatchChangePreview : BookshelfManageScreenIntent
data class SetExportUseReplace(val enabled: Boolean) : BookshelfManageScreenIntent data class SetExportUseReplace(val enabled: Boolean) : BookshelfManageScreenIntent
@@ -233,6 +233,8 @@ class BookshelfManageScreenViewModel(
BookshelfManageScreenIntent.MigrateAllPreviewItems -> migrateAllPreviewItems() BookshelfManageScreenIntent.MigrateAllPreviewItems -> migrateAllPreviewItems()
BookshelfManageScreenIntent.AddAllPreviewItemsToShelf -> addAllPreviewItemsToShelf()
BookshelfManageScreenIntent.DismissChangeSourceStatus -> { BookshelfManageScreenIntent.DismissChangeSourceStatus -> {
_uiState.update { _uiState.update {
it.copy( it.copy(
@@ -958,6 +960,45 @@ class BookshelfManageScreenViewModel(
} }
} }
private fun addAllPreviewItemsToShelf() {
val items = uiState.value.batchChangePreviewItems.filter { it.canMigrate }
if (items.isEmpty()) return
execute {
_uiState.update {
it.copy(isChangingSource = true, changeSourceProgress = "0 / ${items.size}")
}
items.forEachIndexed { index, item ->
_uiState.update {
it.copy(changeSourceProgress = "${index + 1} / ${items.size} ${item.oldBook.name}")
}
val candidate = item.selectedCandidate ?: return@forEachIndexed
val chapters = changeBookSourceUseCase.loadCandidateChapters(
candidate.source,
candidate.book
) ?: return@forEachIndexed
candidate.book.removeType(BookType.notShelf)
if (candidate.book.order == 0) {
candidate.book.order = bookDao.minOrder - 1
}
bookDao.insert(candidate.book)
bookChapterDao.insert(*chapters.toTypedArray())
}
}.onSuccess {
_uiState.update { it.copy(batchChangePreviewItems = emptyList()) }
_effects.tryEmit(BookshelfManageScreenEffect.ShowMessage("批量添加完成"))
}.onError {
_effects.tryEmit(BookshelfManageScreenEffect.ShowMessage("批量添加失败\n${it.localizedMessage}"))
}.onFinally {
_uiState.update {
it.copy(
isChangingSource = false,
changeSourceProgress = null,
cacheVersion = it.cacheVersion + 1
)
}
}
}
private fun clearCacheForBook(book: Book) { private fun clearCacheForBook(book: Book) {
execute { execute {
clearBookCacheUseCase.execute(book.bookUrl) clearBookCacheUseCase.execute(book.bookUrl)
@@ -151,7 +151,7 @@ fun BookshelfItem(
) )
) )
) )
.padding(horizontal = 6.dp, vertical = 6.dp) .padding(all = 4.dp)
) )
} }
} }
@@ -165,7 +165,7 @@ fun BookshelfItem(
textAlign = if (titleCenter) TextAlign.Center else TextAlign.Start, textAlign = if (titleCenter) TextAlign.Center else TextAlign.Start,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(top = 4.dp, start = 4.dp, end = 4.dp, bottom = 8.dp) .padding(start = 4.dp, end = 4.dp, bottom = 4.dp)
) )
} }
} }
@@ -175,7 +175,7 @@ fun BookshelfItem(
NormalCard( NormalCard(
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
.padding(all = 4.dp), .padding(vertical = 4.dp),
cornerRadius = 8.dp, cornerRadius = 8.dp,
containerColor = if (isSelected) { containerColor = if (isSelected) {
LegadoTheme.colorScheme.secondaryContainer LegadoTheme.colorScheme.secondaryContainer
@@ -194,7 +194,6 @@ fun BookshelfItem(
modifier = Modifier modifier = Modifier
.align(Alignment.Top) .align(Alignment.Top)
.width(if (!isCompact) 84.dp else 56.dp) .width(if (!isCompact) 84.dp else 56.dp)
.padding(end = 8.dp)
) { ) {
Box( Box(
modifier = Modifier modifier = Modifier
@@ -215,7 +214,7 @@ fun BookshelfItem(
Column( Column(
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.padding(top = 8.dp, bottom = 8.dp, end = 8.dp), .padding(top = 4.dp, bottom = 4.dp, end = 4.dp),
verticalArrangement = Arrangement.Center verticalArrangement = Arrangement.Center
) { ) {
Row( Row(
@@ -524,13 +523,13 @@ fun BookGroupItemHorizontalCovers(
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
.padding(vertical = 12.dp), .padding(vertical = 4.dp),
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(start = 12.dp, top = 4.dp, bottom = 4.dp), .padding(start = 4.dp, top = 4.dp, bottom = 4.dp),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
@@ -549,7 +548,7 @@ fun BookGroupItemHorizontalCovers(
) )
} }
AppIcon( AppIcon(
modifier = Modifier.padding(end = 6.dp), modifier = Modifier.padding(end = 4.dp),
imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight, imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
contentDescription = "", contentDescription = "",
tint = LegadoTheme.colorScheme.onSurfaceVariant tint = LegadoTheme.colorScheme.onSurfaceVariant
@@ -558,8 +557,8 @@ fun BookGroupItemHorizontalCovers(
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 12.dp), .padding(horizontal = 4.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp) horizontalArrangement = Arrangement.spacedBy(4.dp)
) { ) {
val coverCount = BookshelfConfig.bookshelfGroupCoverCount val coverCount = BookshelfConfig.bookshelfGroupCoverCount
previewBooks.take(coverCount).forEach { bookUi -> previewBooks.take(coverCount).forEach { bookUi ->
@@ -781,7 +781,7 @@ fun BookshelfScreen(
contentPadding = adaptiveContentPaddingBookshelf( contentPadding = adaptiveContentPaddingBookshelf(
top = paddingValues.calculateTopPadding(), top = paddingValues.calculateTopPadding(),
bottom = if (ThemeConfig.useFloatingBottomBar || ThemeConfig.enableBlur) 120.dp else 8.dp, bottom = if (ThemeConfig.useFloatingBottomBar || ThemeConfig.enableBlur) 120.dp else 8.dp,
horizontal = if (isGridMode) 8.dp else 4.dp horizontal = 4.dp
), ),
verticalArrangement = Arrangement.spacedBy(if (isGridMode) 8.dp else 0.dp), verticalArrangement = Arrangement.spacedBy(if (isGridMode) 8.dp else 0.dp),
horizontalArrangement = Arrangement.spacedBy(if (isGridMode) 8.dp else 0.dp), horizontalArrangement = Arrangement.spacedBy(if (isGridMode) 8.dp else 0.dp),
@@ -1361,7 +1361,7 @@ fun BookshelfPage(
contentPadding = adaptiveContentPaddingBookshelf( contentPadding = adaptiveContentPaddingBookshelf(
top = paddingValues.calculateTopPadding(), top = paddingValues.calculateTopPadding(),
bottom = if (ThemeConfig.useFloatingBottomBar || ThemeConfig.enableBlur) 120.dp else 8.dp, bottom = if (ThemeConfig.useFloatingBottomBar || ThemeConfig.enableBlur) 120.dp else 8.dp,
horizontal = if (isGridMode) 8.dp else 4.dp horizontal = 8.dp
), ),
verticalArrangement = Arrangement.spacedBy(if (isGridMode) 8.dp else 0.dp), verticalArrangement = Arrangement.spacedBy(if (isGridMode) 8.dp else 0.dp),
horizontalArrangement = Arrangement.spacedBy(if (isGridMode) 8.dp else 0.dp), horizontalArrangement = Arrangement.spacedBy(if (isGridMode) 8.dp else 0.dp),
@@ -175,6 +175,13 @@ class BookshelfViewModel(
val allBookCount: Int val allBookCount: Int
) )
private data class DataForPreviews(
val groups: List<BookGroup>,
val bookGroupStyle: Int,
val systemCountsMap: Map<Long, Int>,
val allBookCount: Int
)
val groupSelectorState: StateFlow<BookshelfGroupSelectorState> = combine( val groupSelectorState: StateFlow<BookshelfGroupSelectorState> = combine(
groupsFlow, groupsFlow,
groupIdFlow groupIdFlow
@@ -262,16 +269,25 @@ class BookshelfViewModel(
private val groupPreviewsFlow = combine( private val groupPreviewsFlow = combine(
groupsFlow, groupsFlow,
bookGroupStyleFlow, bookGroupStyleFlow,
appDb.bookDao.flowSystemGroupCounts() appDb.bookDao.flowSystemGroupCounts(),
) { groups, bookGroupStyle, systemCounts -> appDb.bookDao.flowAllBookShelfCount()
Triple(groups, bookGroupStyle, systemCounts.associate { it.groupId to it.count }) ) { groups, bookGroupStyle, systemCounts, totalCount ->
}.flatMapLatest { (groups, bookGroupStyle, systemCountsMap) -> DataForPreviews(
groups,
bookGroupStyle,
systemCounts.associate { it.groupId to it.count },
totalCount
)
}.flatMapLatest { data ->
val groups = data.groups
val bookGroupStyle = data.bookGroupStyle
val systemCountsMap = data.systemCountsMap
val allBookCount = data.allBookCount
if (bookGroupStyle !in 2..3) { if (bookGroupStyle !in 2..3) {
appDb.bookDao.flowAllBookShelfCount().map { count -> flowOf(GroupPreviewState(persistentMapOf(), persistentMapOf(), allBookCount))
GroupPreviewState(persistentMapOf(), persistentMapOf(), count)
}
} else if (groups.isEmpty()) { } else if (groups.isEmpty()) {
flowOf(GroupPreviewState(persistentMapOf(), persistentMapOf(), 0)) flowOf(GroupPreviewState(persistentMapOf(), persistentMapOf(), allBookCount))
} else { } else {
val groupFlows = groups.map { group -> val groupFlows = groups.map { group ->
val countFlow: Flow<Int> = if (group.groupId > 0) { val countFlow: Flow<Int> = if (group.groupId > 0) {
@@ -287,11 +303,9 @@ class BookshelfViewModel(
combine(groupFlows) { results -> combine(groupFlows) { results ->
var previews = persistentMapOf<Long, ImmutableList<BookUiItem>>() var previews = persistentMapOf<Long, ImmutableList<BookUiItem>>()
var counts = persistentMapOf<Long, Int>() var counts = persistentMapOf<Long, Int>()
var allBookCount = 0
results.forEach { (groupId, count, preview) -> results.forEach { (groupId, count, preview) ->
counts = counts.put(groupId, count) counts = counts.put(groupId, count)
previews = previews.put(groupId, preview.toImmutableList()) previews = previews.put(groupId, preview.toImmutableList())
if (groupId == BookGroup.IdAll) allBookCount = count
} }
GroupPreviewState(previews, counts, allBookCount) GroupPreviewState(previews, counts, allBookCount)
} }
@@ -480,10 +494,16 @@ class BookshelfViewModel(
bookshelfSort = internal.sortConfig.sort, bookshelfSort = internal.sortConfig.sort,
bookshelfSortOrder = internal.sortConfig.sortOrder, bookshelfSortOrder = internal.sortConfig.sortOrder,
title = title, title = title,
subtitle = if (interaction.isEditMode) { subtitle = when {
context.getString(R.string.bookshelf_total_count, previews.allBookCount) interaction.isEditMode -> {
} else { context.getString(R.string.bookshelf_total_count, previews.allBookCount)
null }
internal.isSearchMode -> {
context.getString(R.string.bookshelf_total_count, filteredBooks.size)
}
else -> null
}, },
currentGroupName = currentGroupName, currentGroupName = currentGroupName,
draggingBooks = interaction.draggingBooks?.toImmutableList(), draggingBooks = interaction.draggingBooks?.toImmutableList(),
@@ -86,7 +86,8 @@ fun adaptiveContentPaddingBookshelf(
horizontal: Dp horizontal: Dp
): PaddingValues { ): PaddingValues {
val adjustedTop = if (ThemeResolver.isMiuixEngine(composeEngine)) top + 12.dp else top + 8.dp val adjustedTop = if (ThemeResolver.isMiuixEngine(composeEngine)) top + 12.dp else top + 8.dp
val horizontal = if (ThemeResolver.isMiuixEngine(composeEngine)) 12.dp + horizontal else 4.dp + horizontal val horizontal =
if (ThemeResolver.isMiuixEngine(composeEngine)) 6.dp + horizontal else 4.dp + horizontal
return PaddingValues( return PaddingValues(
top = adjustedTop, top = adjustedTop,
bottom = bottom, bottom = bottom,
@@ -32,10 +32,10 @@ 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.LegadoTheme.composeEngine import io.legado.app.ui.theme.LegadoTheme.composeEngine
import io.legado.app.ui.theme.ThemeResolver import io.legado.app.ui.theme.ThemeResolver
import top.yukonga.miuix.kmp.theme.MiuixTheme
import top.yukonga.miuix.kmp.basic.Icon as MiuixIcon import top.yukonga.miuix.kmp.basic.Icon as MiuixIcon
import top.yukonga.miuix.kmp.basic.IconButton as MiuixIconButton import top.yukonga.miuix.kmp.basic.IconButton as MiuixIconButton
import top.yukonga.miuix.kmp.basic.Text as MiuixText import top.yukonga.miuix.kmp.basic.Text as MiuixText
import top.yukonga.miuix.kmp.theme.MiuixTheme
private val SmallMiuixButtonSize = 32.dp private val SmallMiuixButtonSize = 32.dp
private val SmallMiuixIconSize = 18.dp private val SmallMiuixIconSize = 18.dp
@@ -74,7 +74,7 @@ fun SmallIconButton(
MiuixIcon( MiuixIcon(
imageVector = imageVector, imageVector = imageVector,
contentDescription = contentDescription, contentDescription = contentDescription,
modifier = Modifier.size(smallIconSize), modifier = Modifier.size(16.dp),
) )
} }
} else { } else {
@@ -4,8 +4,6 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
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.fillMaxWidth
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.layout.width
@@ -24,8 +22,6 @@ import io.legado.app.ui.theme.ThemeResolver
import io.legado.app.ui.widget.components.text.AppText import io.legado.app.ui.widget.components.text.AppText
import top.yukonga.miuix.kmp.basic.Card import top.yukonga.miuix.kmp.basic.Card
import top.yukonga.miuix.kmp.basic.CardDefaults import top.yukonga.miuix.kmp.basic.CardDefaults
import top.yukonga.miuix.kmp.basic.Button as MiuixButton
import top.yukonga.miuix.kmp.basic.ButtonDefaults as MiuixButtonDefaults
import top.yukonga.miuix.kmp.basic.Icon as MiuixIcon import top.yukonga.miuix.kmp.basic.Icon as MiuixIcon
@Composable @Composable
@@ -85,7 +81,7 @@ fun SmallTextButton(
@Composable @Composable
fun SmallTonalTextButton( fun SmallTonalTextButton(
text: String, text: String? = null,
imageVector: ImageVector, imageVector: ImageVector,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onClick: () -> Unit onClick: () -> Unit
@@ -111,10 +107,12 @@ fun SmallTonalTextButton(
contentDescription = null, contentDescription = null,
modifier = Modifier.size(16.dp) modifier = Modifier.size(16.dp)
) )
AppText( if (text != null) {
text = text, AppText(
style = LegadoTheme.typography.labelMedium text = text,
) style = LegadoTheme.typography.labelMedium
)
}
} }
} }
} else { } else {
@@ -129,10 +127,12 @@ fun SmallTonalTextButton(
modifier = Modifier.size(16.dp) modifier = Modifier.size(16.dp)
) )
Spacer(Modifier.width(4.dp)) Spacer(Modifier.width(4.dp))
AppText( if (text != null) {
text = text, AppText(
style = LegadoTheme.typography.labelMedium text = text,
) style = LegadoTheme.typography.labelMedium
)
}
} }
} }
} }
@@ -19,9 +19,12 @@ import androidx.compose.material3.Typography
import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
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.graphics.Color
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalWindowInfo import androidx.compose.ui.platform.LocalWindowInfo
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
@@ -165,3 +168,39 @@ fun AppModalBottomSheet(
} }
} }
} }
/**
* 专为 nullable 数据设计的 AppModalBottomSheet 重载。
* 当 [data] 不为 null 时显示弹窗;当 [data] 变为 null 时,自动缓存最后一次数据并播放退出动画。
*/
@Composable
fun <T> AppModalBottomSheet(
data: T?,
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
title: String? = null,
startAction: @Composable (() -> Unit)? = null,
endAction: @Composable (() -> Unit)? = null,
content: @Composable ColumnScope.(T) -> Unit
) {
var cachedData by remember { mutableStateOf(data) }
if (data != null) {
cachedData = data
}
val currentData = cachedData
AppModalBottomSheet(
show = data != null,
onDismissRequest = onDismissRequest,
modifier = modifier,
title = title,
startAction = startAction,
endAction = endAction,
content = {
if (currentData != null) {
content(currentData)
}
}
)
}
+1 -1
View File
@@ -1,5 +1,5 @@
VERSION_MAJOR=3 VERSION_MAJOR=3
VERSION_MINOR=26 VERSION_MINOR=26
VERSION_PATCH=11 VERSION_PATCH=11
VERSION_SUFFIX=1 VERSION_SUFFIX=0
# 1 = Pre, 0 = Release # 1 = Pre, 0 = Release