diff --git a/app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt b/app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt index 81ec6555f..12997ea1f 100644 --- a/app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt @@ -16,13 +16,11 @@ import android.graphics.drawable.TransitionDrawable import android.net.Uri import android.os.Build import android.os.Bundle -import android.text.method.ScrollingMovementMethod import android.view.Menu import android.view.MenuItem import android.view.View import android.widget.CheckBox import android.widget.LinearLayout -import androidx.activity.OnBackPressedCallback import androidx.activity.addCallback import androidx.activity.viewModels import androidx.core.app.ActivityCompat @@ -54,7 +52,6 @@ import io.legado.app.help.book.isAudio import io.legado.app.help.book.isImage import io.legado.app.help.book.isLocal import io.legado.app.help.book.isLocalTxt -import io.legado.app.help.book.isNotShelf import io.legado.app.help.book.isWebFile import io.legado.app.help.book.removeType import io.legado.app.help.config.AppConfig @@ -62,7 +59,6 @@ import io.legado.app.help.config.LocalConfig import io.legado.app.lib.dialogs.alert import io.legado.app.lib.dialogs.selector import io.legado.app.model.BookCover -import io.legado.app.model.ReadBook import io.legado.app.model.remote.RemoteBookWebDav import io.legado.app.ui.about.AppLogDialog import io.legado.app.ui.book.audio.AudioPlayActivity @@ -158,7 +154,6 @@ class BookInfoActivity : viewModel.refreshBook(book) } } - private lateinit var backCallback: OnBackPressedCallback private var surfaceFinalColor: Int = 0 private var surfaceContainerFinalColor: Int = 0 private var secondaryFinalColor: Int = 0 @@ -178,7 +173,6 @@ class BookInfoActivity : @SuppressLint("PrivateResource") override fun onCreate(savedInstanceState: Bundle?) { - postponeEnterTransition() setEnterSharedElementCallback(MaterialContainerTransformSharedElementCallback()) setExitSharedElementCallback(MaterialContainerTransformSharedElementCallback()) val transform = MaterialContainerTransform().apply { @@ -187,7 +181,6 @@ class BookInfoActivity : } window.sharedElementEnterTransition = transform window.sharedElementReturnTransition = transform - window.sharedElementsUseOverlay = false super.onCreate(savedInstanceState) setupBackCallback() surfaceFinalColor = @@ -389,7 +382,7 @@ class BookInfoActivity : private fun setupBackCallback() { onBackPressedDispatcher.addCallback(this) { - if (!viewModel.inBookshelf) { + if (!viewModel.inBookshelf && AppConfig.showAddToShelfAlert) { viewModel.getBook()?.let { book -> alert(title = getString(R.string.add_to_bookshelf)) { setMessage(getString(R.string.check_add_bookshelf, book.name)) @@ -442,7 +435,7 @@ class BookInfoActivity : addColorScheme(binding.ivCover.drawable) tvName.text = book.name tvRemark.text = book.remark - if (book.remark == null) + if (book.remark.isNullOrEmpty()) cdRemark.gone() else cdRemark.visible() diff --git a/app/src/main/java/io/legado/app/ui/book/info/edit/BookInfoEditActivity.kt b/app/src/main/java/io/legado/app/ui/book/info/edit/BookInfoEditActivity.kt index 49bc1a852..82ea19645 100644 --- a/app/src/main/java/io/legado/app/ui/book/info/edit/BookInfoEditActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/info/edit/BookInfoEditActivity.kt @@ -19,7 +19,12 @@ class BookInfoEditActivity : BaseComposeActivity(), ChangeCoverDialog.CallBack { BookInfoEditScreen( viewModel = viewModel, onBack = { finish() }, - onSave = ::saveData + onSave = { + viewModel.save { + setResult(RESULT_OK) + finish() + } + } ) } } @@ -32,16 +37,9 @@ class BookInfoEditActivity : BaseComposeActivity(), ChangeCoverDialog.CallBack { } } - private fun saveData() { - viewModel.saveBook { - setResult(RESULT_OK) - finish() - } - } - override fun coverChangeTo(coverUrl: String) { // 更新封面 URL - viewModel.updateCoverUrl(coverUrl) + viewModel.onCoverUrlChange(coverUrl) } -} \ No newline at end of file +} diff --git a/app/src/main/java/io/legado/app/ui/book/info/edit/BookInfoEditScreen.kt b/app/src/main/java/io/legado/app/ui/book/info/edit/BookInfoEditScreen.kt index 103d2de62..efc041507 100644 --- a/app/src/main/java/io/legado/app/ui/book/info/edit/BookInfoEditScreen.kt +++ b/app/src/main/java/io/legado/app/ui/book/info/edit/BookInfoEditScreen.kt @@ -13,6 +13,8 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.FolderOpen +import androidx.compose.material.icons.filled.ImageSearch import androidx.compose.material.icons.filled.Replay import androidx.compose.material.icons.filled.Save import androidx.compose.material3.DropdownMenuItem @@ -21,16 +23,16 @@ import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.ExposedDropdownMenuAnchorType import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.ExposedDropdownMenuDefaults -import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.FilledTonalIconButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.MediumTopAppBar import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults 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 @@ -43,9 +45,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.legado.app.R -import io.legado.app.data.entities.Book -import io.legado.app.help.book.isAudio -import io.legado.app.help.book.isImage import io.legado.app.ui.book.changecover.ChangeCoverDialog import io.legado.app.ui.widget.components.Cover import io.legado.app.utils.SelectImageContract @@ -59,7 +58,7 @@ fun BookInfoEditScreen( onBack: () -> Unit, onSave: () -> Unit ) { - val book by viewModel.bookData.collectAsStateWithLifecycle() + val uiState by viewModel.uiState.collectAsStateWithLifecycle() val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() Scaffold( @@ -76,7 +75,9 @@ fun BookInfoEditScreen( } }, actions = { - FilledTonalButton(onClick = onSave) { + FilledTonalIconButton( + onClick = { viewModel.save(onSave) }, + shapes = IconButtonDefaults.shapes()) { Icon( imageVector = Icons.Default.Save, contentDescription = stringResource(id = R.string.action_save) @@ -87,13 +88,13 @@ fun BookInfoEditScreen( ) }, content = { paddingValues -> - book?.let { + uiState.book?.let { BookInfoEditContent( modifier = Modifier .padding(paddingValues) .fillMaxSize() .verticalScroll(rememberScrollState()), - book = it, + uiState = uiState, viewModel = viewModel ) } @@ -101,59 +102,56 @@ fun BookInfoEditScreen( ) } +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun BookInfoEditContent( modifier: Modifier = Modifier, - book: Book, + uiState: BookInfoEditUiState, viewModel: BookInfoEditViewModel ) { val context = LocalContext.current - var name by remember(book.name) { mutableStateOf(book.name) } - var author by remember(book.author) { mutableStateOf(book.author) } - var coverUrl by remember(book.customCoverUrl) { mutableStateOf(book.getDisplayCover()) } - var intro by remember(book.customIntro) { mutableStateOf(book.getDisplayIntro()) } - var remark by remember(book.remark) { mutableStateOf(book.remark) } - - val bookTypes = arrayOf("文本", "音频", "图片") - val selectedTypeIndex = when { - book.isImage -> 2 - book.isAudio -> 1 - else -> 0 - } - var selectedType by remember { mutableStateOf(bookTypes[selectedTypeIndex]) } val selectCover = rememberLauncherForActivityResult(SelectImageContract()) { it.uri?.let { uri -> - viewModel.coverChangeTo(context, uri) { newCoverUrl -> - coverUrl = newCoverUrl - } + viewModel.coverChangeTo(context, uri) } } - LaunchedEffect(coverUrl) { - viewModel.book?.customCoverUrl = coverUrl - } - Column( modifier = modifier.padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally ) { Cover( - path = coverUrl, + path = uiState.coverUrl, modifier = Modifier .fillMaxWidth(0.3f) ) Spacer(modifier = Modifier.height(16.dp)) Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { - FilledTonalButton(onClick = { - (context as? BookInfoEditActivity)?.showDialogFragment(ChangeCoverDialog(book.name, book.author)) - }) { - Text("网络搜索") + FilledTonalIconButton( + onClick = { + (context as? BookInfoEditActivity)?.showDialogFragment( + ChangeCoverDialog( + uiState.name, + uiState.author + ) + ) + }, + shapes = IconButtonDefaults.shapes()) { + Icon( + Icons.Default.ImageSearch, + contentDescription = stringResource(id = R.string.default_cover) + ) } - FilledTonalButton(onClick = { selectCover.launch() }) { - Text("本地选择") + FilledTonalIconButton(onClick = { selectCover.launch() }, + shapes = IconButtonDefaults.shapes()) { + Icon( + Icons.Default.FolderOpen, + contentDescription = stringResource(id = R.string.default_cover) + ) } - FilledTonalButton(onClick = { coverUrl = book.coverUrl ?: "" }) { + FilledTonalIconButton(onClick = { viewModel.resetCover() }, + shapes = IconButtonDefaults.shapes()) { Icon( Icons.Default.Replay, contentDescription = stringResource(id = R.string.default_cover) @@ -162,57 +160,53 @@ fun BookInfoEditContent( } Spacer(modifier = Modifier.height(16.dp)) OutlinedTextField( - value = name, - onValueChange = { name = it }, + value = uiState.name, + onValueChange = { viewModel.onNameChange(it) }, label = { Text("书名") }, modifier = Modifier.fillMaxWidth() ) Spacer(modifier = Modifier.height(8.dp)) OutlinedTextField( - value = author, - onValueChange = { author = it }, + value = uiState.author, + onValueChange = { viewModel.onAuthorChange(it) }, label = { Text("作者") }, modifier = Modifier.fillMaxWidth() ) Spacer(modifier = Modifier.height(8.dp)) BookTypeDropdown( - bookTypes = bookTypes, - selectedType = selectedType, - onTypeSelected = { selectedType = it } + bookTypes = uiState.bookTypes, + selectedType = uiState.selectedType, + onTypeSelected = { viewModel.onBookTypeChange(it) } ) Spacer(modifier = Modifier.height(8.dp)) OutlinedTextField( - value = coverUrl ?: "", - onValueChange = { coverUrl = it }, + value = uiState.coverUrl ?: "", + onValueChange = { viewModel.onCoverUrlChange(it) }, label = { Text("封面链接") }, modifier = Modifier.fillMaxWidth() ) Spacer(modifier = Modifier.height(8.dp)) OutlinedTextField( - value = intro ?: "", - onValueChange = { intro = it }, + value = uiState.intro ?: "", + onValueChange = { viewModel.onIntroChange(it) }, label = { Text("简介") }, modifier = Modifier.fillMaxWidth(), maxLines = 5 ) Spacer(modifier = Modifier.height(8.dp)) OutlinedTextField( - value = remark ?: "", - onValueChange = { remark = it }, + value = uiState.remark ?: "", + onValueChange = { viewModel.onRemarkChange(it) }, label = { Text("备注") }, modifier = Modifier.fillMaxWidth() ) } - - LaunchedEffect(name, author, selectedType, coverUrl, intro, remark) { - viewModel.updateBookState(name, author, selectedType, bookTypes, coverUrl, intro, remark) - } } @OptIn(ExperimentalMaterial3Api::class) @Composable fun BookTypeDropdown( - bookTypes: Array, + bookTypes: List, selectedType: String, onTypeSelected: (String) -> Unit ) { @@ -223,10 +217,12 @@ fun BookTypeDropdown( onExpandedChange = { expanded = !expanded } ) { OutlinedTextField( - modifier = Modifier.fillMaxWidth().menuAnchor( - ExposedDropdownMenuAnchorType.PrimaryEditable, - true - ), + modifier = Modifier + .fillMaxWidth() + .menuAnchor( + ExposedDropdownMenuAnchorType.PrimaryEditable, + true + ), readOnly = true, value = selectedType, onValueChange = {}, diff --git a/app/src/main/java/io/legado/app/ui/book/info/edit/BookInfoEditViewModel.kt b/app/src/main/java/io/legado/app/ui/book/info/edit/BookInfoEditViewModel.kt index bd952323d..2b9704729 100644 --- a/app/src/main/java/io/legado/app/ui/book/info/edit/BookInfoEditViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/info/edit/BookInfoEditViewModel.kt @@ -11,6 +11,8 @@ import io.legado.app.data.appDb import io.legado.app.data.entities.Book import io.legado.app.help.book.BookHelp import io.legado.app.help.book.addType +import io.legado.app.help.book.isAudio +import io.legado.app.help.book.isImage import io.legado.app.help.book.isLocal import io.legado.app.help.book.removeType import io.legado.app.model.ReadBook @@ -23,30 +25,99 @@ import kotlinx.coroutines.flow.asStateFlow import java.io.File import java.io.FileOutputStream +data class BookInfoEditUiState( + val name: String = "", + val author: String = "", + val coverUrl: String? = null, + val intro: String? = null, + val remark: String? = null, + val selectedType: String = "文本", + val bookTypes: List = listOf("文本", "音频", "图片"), + val book: Book? = null, +) + class BookInfoEditViewModel(application: Application) : BaseViewModel(application) { var book: Book? = null - private val _bookData = MutableStateFlow(null) - val bookData: StateFlow = _bookData.asStateFlow() + private val _uiState = MutableStateFlow(BookInfoEditUiState()) + val uiState: StateFlow = _uiState.asStateFlow() fun loadBook(bookUrl: String) { execute { book = appDb.bookDao.getBook(bookUrl) book?.let { - _bookData.value = it + val selectedTypeIndex = when { + it.isImage -> 2 + it.isAudio -> 1 + else -> 0 + } + _uiState.value = BookInfoEditUiState( + name = it.name, + author = it.author, + coverUrl = it.getDisplayCover(), + intro = it.getDisplayIntro(), + remark = it.remark, + selectedType = _uiState.value.bookTypes[selectedTypeIndex], + book = it + ) } } } - fun saveBook(success: (() -> Unit)?) { + fun onNameChange(name: String) { + _uiState.value = _uiState.value.copy(name = name) + } + + fun onAuthorChange(author: String) { + _uiState.value = _uiState.value.copy(author = author) + } + + fun onCoverUrlChange(coverUrl: String) { + _uiState.value = _uiState.value.copy(coverUrl = coverUrl) + } + + fun onIntroChange(intro: String) { + _uiState.value = _uiState.value.copy(intro = intro) + } + + fun onRemarkChange(remark: String) { + _uiState.value = _uiState.value.copy(remark = remark) + } + + fun onBookTypeChange(bookType: String) { + _uiState.value = _uiState.value.copy(selectedType = bookType) + } + + fun resetCover() { + _uiState.value = _uiState.value.copy(coverUrl = book?.coverUrl ?: "") + } + + fun save(onSuccess: () -> Unit) { execute { + val currentState = _uiState.value book?.let { book -> + val oldBook = book.copy() + book.name = currentState.name + book.author = currentState.author + book.remark = currentState.remark + val local = if (book.isLocal) BookType.local else 0 + val bookType = when (currentState.selectedType) { + currentState.bookTypes[2] -> BookType.image or local + currentState.bookTypes[1] -> BookType.audio or local + else -> BookType.text or local + } + book.removeType(BookType.local, BookType.image, BookType.audio, BookType.text) + book.addType(bookType) + book.customCoverUrl = if (currentState.coverUrl == book.coverUrl) null else currentState.coverUrl + book.customIntro = if (currentState.intro == book.intro) null else currentState.intro + BookHelp.updateCacheFolder(oldBook, book) + if (ReadBook.book?.bookUrl == book.bookUrl) { ReadBook.book = book } appDb.bookDao.update(book) } }.onSuccess { - success?.invoke() + onSuccess.invoke() }.onError { if (it is SQLiteConstraintException) { AppLog.put("书籍信息保存失败,存在相同书名作者书籍\n$it", it, true) @@ -56,35 +127,7 @@ class BookInfoEditViewModel(application: Application) : BaseViewModel(applicatio } } - fun updateBookState( - name: String, - author: String, - selectedType: String, - bookTypes: Array, - coverUrl: String?, - intro: String?, - remark: String? - ) { - book?.let { book -> - val oldBook = book.copy() - book.name = name - book.author = author - book.remark = remark - val local = if (book.isLocal) BookType.local else 0 - val bookType = when (selectedType) { - bookTypes[2] -> BookType.image or local - bookTypes[1] -> BookType.audio or local - else -> BookType.text or local - } - book.removeType(BookType.local, BookType.image, BookType.audio, BookType.text) - book.addType(bookType) - book.customCoverUrl = if (coverUrl == book.coverUrl) null else coverUrl - book.customIntro = if (intro == book.intro) null else intro - BookHelp.updateCacheFolder(oldBook, book) - } - } - - fun coverChangeTo(context: Context, uri: Uri, onFinally: (coverUrl: String) -> Unit) { + fun coverChangeTo(context: Context, uri: Uri) { execute { runCatching { context.externalCacheDir?.let { externalCacheDir -> @@ -97,7 +140,7 @@ class BookInfoEditViewModel(application: Application) : BaseViewModel(applicatio inputStream.copyTo(outputStream) } } - onFinally(coverFile.absolutePath) + _uiState.value = _uiState.value.copy(coverUrl = coverFile.absolutePath) } ?: run { AppLog.put("External cache directory is null", Throwable("Null directory"), true) } @@ -106,13 +149,4 @@ class BookInfoEditViewModel(application: Application) : BaseViewModel(applicatio } } } - - - fun updateCoverUrl(coverUrl: String) { - book?.let { - val updatedBook = it.copy(customCoverUrl = coverUrl) - _bookData.value = updatedBook - } - } - } diff --git a/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt b/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt index 06e13e52d..12467efe4 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt @@ -286,8 +286,6 @@ class ReadBookActivity : BaseReadBookActivity(), @SuppressLint("ClickableViewAccessibility") override fun onCreate(savedInstanceState: Bundle?) { - - setEnterSharedElementCallback(MaterialContainerTransformSharedElementCallback()) setExitSharedElementCallback(MaterialContainerTransformSharedElementCallback()) val transform = MaterialContainerTransform().apply { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index acf06a2f3..0388136ab 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -41,8 +41,7 @@ markdownCompose = "0.5.0" markwon = "4.6.2" material = "1.14.0-alpha08" material3 = "1.5.0-alpha11" -material3IconsExtended = "1.2.0" -materialIconsExtended = "1.7.8" +material3IconsExtended = "1.7.8" media = "1.7.1" media3 = "1.8.0" nanoHttpd = "2.3.1" @@ -90,9 +89,8 @@ androidx-compose-animation = { module = "androidx.compose.animation:animation", androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" } androidx-compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "foundation" } androidx-compose-material = { module = "androidx.compose.material:material" } -compose-materialIcons = { group = "androidx.compose.material", name = "material-icons-extended", version = "1.7.8" } +compose-materialIcons = { group = "androidx.compose.material", name = "material-icons-extended", version.ref = "material3IconsExtended" } androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" } -androidx-compose-material3-icons-extended = { module = "androidx.compose.material3:material3-icons-extended", version.ref = "material3IconsExtended" } androidx-compose-ui = { module = "androidx.compose.ui:ui" } androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } androidx-compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }