[优化] 架构优化

This commit is contained in:
HapeLee
2026-04-26 15:13:50 +08:00
parent 076d16a176
commit fce37cb1a6
90 changed files with 1102 additions and 349 deletions
+63
View File
@@ -0,0 +1,63 @@
Behavioral guidelines to reduce common LLM coding mistakes. Merge with project-specific instructions as needed.
**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.
## 1. Think Before Coding
**Don't assume. Don't hide confusion. Surface tradeoffs.**
Before implementing:
- State your assumptions explicitly. If uncertain, ask.
- If multiple interpretations exist, present them - don't pick silently.
- If a simpler approach exists, say so. Push back when warranted.
- If something is unclear, stop. Name what's confusing. Ask.
## 2. Simplicity First
**Minimum code that solves the problem. Nothing speculative.**
- No features beyond what was asked.
- No abstractions for single-use code.
- No "flexibility" or "configurability" that wasn't requested.
- No error handling for impossible scenarios.
- If you write 200 lines and it could be 50, rewrite it.
Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.
## 3. Surgical Changes
**Touch only what you must. Clean up only your own mess.**
When editing existing code:
- Don't "improve" adjacent code, comments, or formatting.
- Don't refactor things that aren't broken.
- Match existing style, even if you'd do it differently.
- If you notice unrelated dead code, mention it - don't delete it.
When your changes create orphans:
- Remove imports/variables/functions that YOUR changes made unused.
- Don't remove pre-existing dead code unless asked.
The test: Every changed line should trace directly to the user's request.
## 4. Goal-Driven Execution
**Define success criteria. Loop until verified.**
Transform tasks into verifiable goals:
- "Add validation" -> "Write tests for invalid inputs, then make them pass"
- "Fix the bug" -> "Write a test that reproduces it, then make it pass"
- "Refactor X" -> "Ensure tests pass before and after"
For multi-step tasks, state a brief plan:
```
1. [Step] -> verify: [check]
2. [Step] -> verify: [check]
3. [Step] -> verify: [check]
```
Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification.
---
**These guidelines are working if:** fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, and clarifying questions come before implementation rather than after mistakes.
@@ -0,0 +1,20 @@
package io.legado.app.data.repository
import io.legado.app.data.AppDatabase
import io.legado.app.domain.gateway.AppStartupGateway
import io.legado.app.help.DefaultData
class AppStartupRepository(
private val appDatabase: AppDatabase
) : AppStartupGateway {
override suspend fun deleteNotShelfBooks() {
appDatabase.bookDao.deleteNotShelfBook()
}
override suspend fun ensureDefaultHttpTts() {
if (appDatabase.httpTTSDao.count == 0) {
appDatabase.httpTTSDao.insert(*DefaultData.httpTTS.toTypedArray())
}
}
}
@@ -0,0 +1,20 @@
package io.legado.app.data.repository
import io.legado.app.data.dao.BookDao
import io.legado.app.domain.gateway.BookCacheCleanupGateway
import io.legado.app.help.book.BookHelp
class BookCacheCleanupRepository(
private val bookDao: BookDao
) : BookCacheCleanupGateway {
override fun clearAll() {
BookHelp.clearCache()
}
override suspend fun clear(bookUrl: String): Boolean {
val book = bookDao.getBook(bookUrl) ?: return false
BookHelp.clearCache(book)
return true
}
}
@@ -0,0 +1,81 @@
package io.legado.app.data.repository
import io.legado.app.data.dao.BookChapterDao
import io.legado.app.data.dao.BookDao
import io.legado.app.data.entities.Book
import io.legado.app.domain.model.BookGroupAssignment
import io.legado.app.domain.model.CacheableBook
import io.legado.app.domain.model.DeletableBook
import io.legado.app.domain.repository.BookDomainRepository
import io.legado.app.help.book.isAudio
import io.legado.app.help.book.isLocal
class BookDomainRepositoryImpl(
private val bookDao: BookDao,
private val bookChapterDao: BookChapterDao
) : BookDomainRepository {
private suspend fun getBooks(bookUrls: Set<String>): List<Book> {
if (bookUrls.isEmpty()) return emptyList()
return bookUrls.mapNotNull { bookDao.getBook(it) }
}
override suspend fun getCacheableBooks(bookUrls: Set<String>): List<CacheableBook> {
return getBooks(bookUrls).map { book ->
CacheableBook(
bookUrl = book.bookUrl,
isLocal = book.isLocal,
isAudio = book.isAudio,
durChapterIndex = book.durChapterIndex,
lastChapterIndex = book.lastChapterIndex
)
}
}
override suspend fun getDeletableBooks(bookUrls: Set<String>): List<DeletableBook> {
return getBooks(bookUrls).map { book ->
DeletableBook(
bookUrl = book.bookUrl,
origin = book.origin,
isLocal = book.isLocal
)
}
}
override suspend fun getBookGroupAssignments(bookUrls: Set<String>): List<BookGroupAssignment> {
return getBooks(bookUrls).map { book ->
BookGroupAssignment(
bookUrl = book.bookUrl,
group = book.group
)
}
}
override suspend fun updateBookGroups(assignments: List<BookGroupAssignment>) {
if (assignments.isEmpty()) return
val groups = assignments.associateBy { it.bookUrl }
val books = getBooks(groups.keys).mapNotNull { book ->
groups[book.bookUrl]?.let { assignment ->
if (book.group == assignment.group) null else book.copy(group = assignment.group)
}
}
if (books.isNotEmpty()) {
bookDao.update(*books.toTypedArray())
}
}
override suspend fun removeGroupFromBooks(groupId: Long) {
bookDao.removeGroup(groupId)
}
override suspend fun deleteBooks(bookUrls: Set<String>) {
val books = getBooks(bookUrls)
if (books.isNotEmpty()) {
bookDao.delete(*books.toTypedArray())
}
}
override suspend fun deleteChaptersByBook(bookUrl: String) {
bookChapterDao.delByBook(bookUrl)
}
}
@@ -0,0 +1,17 @@
package io.legado.app.data.repository
import io.legado.app.data.dao.BookDao
import io.legado.app.data.dao.BookSourceDao
import io.legado.app.domain.gateway.BookSourceCallbackGateway
import io.legado.app.model.SourceCallBack
class BookSourceCallbackRepository(
private val bookDao: BookDao,
private val bookSourceDao: BookSourceDao
) : BookSourceCallbackGateway {
override suspend fun onDeleteFromShelf(bookUrl: String) {
val book = bookDao.getBook(bookUrl) ?: return
val source = bookSourceDao.getBookSource(book.origin)
SourceCallBack.callBackBook(SourceCallBack.DEL_BOOK_SHELF, source, book)
}
}
@@ -0,0 +1,16 @@
package io.legado.app.data.repository
import io.legado.app.data.dao.BookDao
import io.legado.app.domain.gateway.BookCacheDownloadGateway
import io.legado.app.model.CacheBook
import splitties.init.appCtx
class CacheBookDownloadRepository(
private val bookDao: BookDao
) : BookCacheDownloadGateway {
override suspend fun start(bookUrl: String, chapterIndices: List<Int>) {
val book = bookDao.getBook(bookUrl) ?: return
CacheBook.start(appCtx, book, chapterIndices)
}
}
@@ -0,0 +1,12 @@
package io.legado.app.data.repository
import io.legado.app.data.AppDatabase
import io.legado.app.domain.gateway.DatabaseMaintenanceGateway
class DatabaseMaintenanceRepository(
private val appDatabase: AppDatabase
) : DatabaseMaintenanceGateway {
override fun shrink() {
appDatabase.openHelper.writableDatabase.execSQL("VACUUM")
}
}
@@ -22,6 +22,10 @@ class DictRuleRepository {
return dao.all return dao.all
} }
fun getEnabled(): List<DictRule> {
return dao.enabled
}
suspend fun insert(vararg rule: DictRule) { suspend fun insert(vararg rule: DictRule) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
dao.insert(*rule) dao.insert(*rule)
@@ -0,0 +1,14 @@
package io.legado.app.data.repository
import io.legado.app.data.dao.BookDao
import io.legado.app.domain.gateway.LocalBookGateway
import io.legado.app.model.localBook.LocalBook
class LocalBookRepository(
private val bookDao: BookDao
) : LocalBookGateway {
override suspend fun deleteBook(bookUrl: String, deleteOriginal: Boolean) {
val book = bookDao.getBook(bookUrl) ?: return
LocalBook.deleteBook(book, deleteOriginal)
}
}
@@ -4,6 +4,7 @@ import io.legado.app.constant.AppConst
import io.legado.app.constant.BookType import io.legado.app.constant.BookType
import io.legado.app.data.AppDatabase import io.legado.app.data.AppDatabase
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.data.entities.Server
import io.legado.app.exception.NoStackTraceException import io.legado.app.exception.NoStackTraceException
import io.legado.app.help.AppWebDav import io.legado.app.help.AppWebDav
import io.legado.app.help.book.getRemoteUrl import io.legado.app.help.book.getRemoteUrl
@@ -90,6 +91,10 @@ class RemoteBookRepository(
} }
} }
fun getDefaultBookWebDav(): RemoteBookWebDav? {
return AppWebDav.defaultBookWebDav
}
suspend fun loadBooks( suspend fun loadBooks(
webDav: RemoteBookWebDav, webDav: RemoteBookWebDav,
path: String? path: String?
@@ -120,4 +125,20 @@ class RemoteBookRepository(
return appDb.bookDao.flowLocal() return appDb.bookDao.flowLocal()
} }
fun flowServers(): Flow<List<Server>> {
return appDb.serverDao.observeAll()
}
suspend fun getServer(id: Long): Server? {
return appDb.serverDao.get(id)
}
suspend fun saveServer(server: Server) {
appDb.serverDao.insert(server)
}
suspend fun deleteServer(server: Server) {
appDb.serverDao.delete(server)
}
} }
@@ -4,9 +4,9 @@ import io.legado.app.data.AppDatabase
import io.legado.app.data.entities.BookSourcePart import io.legado.app.data.entities.BookSourcePart
import io.legado.app.data.entities.SearchBook import io.legado.app.data.entities.SearchBook
import io.legado.app.data.entities.SearchKeyword import io.legado.app.data.entities.SearchKeyword
import io.legado.app.domain.usecase.BookShelfKey
import io.legado.app.help.book.isNotShelf import io.legado.app.help.book.isNotShelf
import io.legado.app.model.webBook.SearchModel import io.legado.app.model.webBook.SearchModel
import io.legado.app.ui.book.search.BookKey
import io.legado.app.ui.book.search.SearchScope import io.legado.app.ui.book.search.SearchScope
import io.legado.app.ui.main.bookshelf.BookShelfItem import io.legado.app.ui.main.bookshelf.BookShelfItem
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@@ -23,7 +23,7 @@ import kotlinx.coroutines.withContext
interface SearchRepository { interface SearchRepository {
val enabledGroups: Flow<List<String>> val enabledGroups: Flow<List<String>>
val enabledSources: Flow<List<BookSourcePart>> val enabledSources: Flow<List<BookSourcePart>>
val bookshelfKeys: Flow<Set<BookKey>> val bookshelfKeys: Flow<Set<BookShelfKey>>
fun searchBookshelf(query: String): Flow<List<BookShelfItem>> fun searchBookshelf(query: String): Flow<List<BookShelfItem>>
fun searchHistory(query: String): Flow<List<SearchKeyword>> fun searchHistory(query: String): Flow<List<SearchKeyword>>
@@ -68,9 +68,9 @@ class SearchRepositoryImpl(
override val enabledGroups: Flow<List<String>> = appDb.bookSourceDao.flowEnabledGroups() override val enabledGroups: Flow<List<String>> = appDb.bookSourceDao.flowEnabledGroups()
override val enabledSources: Flow<List<BookSourcePart>> = appDb.bookSourceDao.flowEnabled() override val enabledSources: Flow<List<BookSourcePart>> = appDb.bookSourceDao.flowEnabled()
override val bookshelfKeys: Flow<Set<BookKey>> = appDb.bookDao.flowBookShelf().map { books -> override val bookshelfKeys: Flow<Set<BookShelfKey>> = appDb.bookDao.flowBookShelf().map { books ->
books.filterNot { it.isNotShelf } books.filterNot { it.isNotShelf }
.map { book -> BookKey(book.name, book.author, book.bookUrl) } .map { book -> BookShelfKey(book.name, book.author, book.bookUrl) }
.toSet() .toSet()
} }
@@ -0,0 +1,36 @@
package io.legado.app.data.repository
import io.legado.app.domain.gateway.WebDavBackupGateway
import io.legado.app.domain.model.WebDavBackup
import io.legado.app.help.AppWebDav
class WebDavBackupRepository : WebDavBackupGateway {
override val isJianGuoYun: Boolean
get() = AppWebDav.isJianGuoYun
override suspend fun syncConfig() {
AppWebDav.upConfig()
}
override suspend fun test(): Boolean {
return AppWebDav.testWebDav()
}
override suspend fun getBackupNames(): List<String> {
return AppWebDav.getBackupNames()
}
override suspend fun getLatestBackup(): WebDavBackup? {
return AppWebDav.lastBackUp().getOrNull()?.let {
WebDavBackup(
name = it.displayName,
lastModify = it.lastModify
)
}
}
override suspend fun restore(name: String) {
AppWebDav.restoreWebDav(name)
}
}
@@ -0,0 +1,40 @@
package io.legado.app.data.repository
import io.legado.app.data.entities.BookProgress
import io.legado.app.domain.gateway.ReadingProgressGateway
import io.legado.app.domain.model.ReadingProgress
import io.legado.app.help.AppWebDav
class WebDavReadingProgressRepository : ReadingProgressGateway {
override val isConfigured: Boolean
get() = AppWebDav.isOk
override suspend fun getProgress(name: String, author: String): ReadingProgress? {
return AppWebDav.getBookProgress(name, author)?.let {
ReadingProgress(
name = it.name,
author = it.author,
durChapterIndex = it.durChapterIndex,
durChapterPos = it.durChapterPos,
durChapterTime = it.durChapterTime,
durChapterTitle = it.durChapterTitle
)
}
}
override suspend fun uploadProgress(progress: ReadingProgress): Long? {
val uploadTime = System.currentTimeMillis()
val uploaded = AppWebDav.uploadBookProgress(
BookProgress(
name = progress.name,
author = progress.author,
durChapterIndex = progress.durChapterIndex,
durChapterPos = progress.durChapterPos,
durChapterTime = progress.durChapterTime,
durChapterTitle = progress.durChapterTitle
)
)
return if (uploaded) uploadTime else null
}
}
@@ -6,22 +6,49 @@ import coil.decode.GifDecoder
import coil.decode.ImageDecoderDecoder import coil.decode.ImageDecoderDecoder
import coil.decode.SvgDecoder import coil.decode.SvgDecoder
import io.legado.app.data.AppDatabase import io.legado.app.data.AppDatabase
import io.legado.app.data.repository.AppStartupRepository
import io.legado.app.data.repository.BookCacheCleanupRepository
import io.legado.app.data.repository.BookDomainRepositoryImpl
import io.legado.app.data.repository.BookGroupRepository import io.legado.app.data.repository.BookGroupRepository
import io.legado.app.data.repository.BookRepository import io.legado.app.data.repository.BookRepository
import io.legado.app.data.repository.BookSourceCallbackRepository
import io.legado.app.data.repository.CacheBookDownloadRepository
import io.legado.app.data.repository.DatabaseMaintenanceRepository
import io.legado.app.data.repository.DirectLinkUploadRepository import io.legado.app.data.repository.DirectLinkUploadRepository
import io.legado.app.data.repository.DictRuleRepository
import io.legado.app.data.repository.ExploreRepository import io.legado.app.data.repository.ExploreRepository
import io.legado.app.data.repository.ExploreRepositoryImpl import io.legado.app.data.repository.ExploreRepositoryImpl
import io.legado.app.data.repository.LocalBookRepository
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.SearchRepository import io.legado.app.data.repository.SearchRepository
import io.legado.app.data.repository.SearchRepositoryImpl import io.legado.app.data.repository.SearchRepositoryImpl
import io.legado.app.data.repository.SearchContentRepository import io.legado.app.data.repository.SearchContentRepository
import io.legado.app.data.repository.UploadRepository import io.legado.app.data.repository.UploadRepository
import io.legado.app.data.repository.WebDavBackupRepository
import io.legado.app.data.repository.WebDavReadingProgressRepository
import io.legado.app.domain.gateway.BookCacheCleanupGateway
import io.legado.app.domain.gateway.AppStartupGateway
import io.legado.app.domain.gateway.BookCacheDownloadGateway
import io.legado.app.domain.gateway.BookSourceCallbackGateway
import io.legado.app.domain.gateway.DatabaseMaintenanceGateway
import io.legado.app.domain.gateway.LocalBookGateway
import io.legado.app.domain.gateway.ReadingProgressGateway
import io.legado.app.domain.gateway.WebDavBackupGateway
import io.legado.app.domain.repository.BookDomainRepository
import io.legado.app.domain.usecase.AppStartupMaintenanceUseCase
import io.legado.app.domain.usecase.BatchCacheDownloadUseCase import io.legado.app.domain.usecase.BatchCacheDownloadUseCase
import io.legado.app.domain.usecase.CacheBookChaptersUseCase
import io.legado.app.domain.usecase.ClearBookCacheUseCase 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.ExploreKindUiUseCase import io.legado.app.domain.usecase.GetReadingProgressUseCase
import io.legado.app.domain.usecase.RemoveBookGroupAssignmentUseCase
import io.legado.app.ui.widget.components.explore.ExploreKindUiUseCase
import io.legado.app.domain.usecase.ResolveBookShelfStateUseCase
import io.legado.app.domain.usecase.ShrinkDatabaseUseCase
import io.legado.app.domain.usecase.UpdateBooksGroupUseCase import io.legado.app.domain.usecase.UpdateBooksGroupUseCase
import io.legado.app.domain.usecase.UploadReadingProgressUseCase
import io.legado.app.domain.usecase.WebDavBackupUseCase
import io.legado.app.help.coil.CoverFetcher import io.legado.app.help.coil.CoverFetcher
import io.legado.app.help.coil.CoverInterceptor import io.legado.app.help.coil.CoverInterceptor
import io.legado.app.help.http.okHttpClient import io.legado.app.help.http.okHttpClient
@@ -35,6 +62,8 @@ import io.legado.app.ui.book.explore.ExploreShowViewModel
import io.legado.app.ui.book.group.GroupViewModel import io.legado.app.ui.book.group.GroupViewModel
import io.legado.app.ui.book.import.local.ImportBookViewModel import io.legado.app.ui.book.import.local.ImportBookViewModel
import io.legado.app.ui.book.import.remote.RemoteBookViewModel import io.legado.app.ui.book.import.remote.RemoteBookViewModel
import io.legado.app.ui.book.import.remote.ServerConfigViewModel
import io.legado.app.ui.book.import.remote.ServersViewModel
import io.legado.app.ui.book.info.BookInfoViewModel import io.legado.app.ui.book.info.BookInfoViewModel
import io.legado.app.ui.book.readRecord.ReadRecordViewModel import io.legado.app.ui.book.readRecord.ReadRecordViewModel
import io.legado.app.ui.book.search.SearchViewModel import io.legado.app.ui.book.search.SearchViewModel
@@ -47,6 +76,7 @@ import io.legado.app.ui.config.coverConfig.CoverConfigViewModel
import io.legado.app.ui.config.otherConfig.OtherConfigViewModel import io.legado.app.ui.config.otherConfig.OtherConfigViewModel
import io.legado.app.ui.config.readConfig.ReadConfigViewModel import io.legado.app.ui.config.readConfig.ReadConfigViewModel
import io.legado.app.ui.config.themeConfig.ThemeConfigViewModel import io.legado.app.ui.config.themeConfig.ThemeConfigViewModel
import io.legado.app.ui.dict.DictViewModel
import io.legado.app.ui.dict.rule.DictRuleViewModel import io.legado.app.ui.dict.rule.DictRuleViewModel
import io.legado.app.ui.main.MainViewModel import io.legado.app.ui.main.MainViewModel
import io.legado.app.ui.main.bookshelf.BookshelfViewModel import io.legado.app.ui.main.bookshelf.BookshelfViewModel
@@ -76,16 +106,34 @@ val appModule = module {
singleOf(::ReadRecordRepository) singleOf(::ReadRecordRepository)
singleOf(::BookRepository) singleOf(::BookRepository)
singleOf(::BookGroupRepository) singleOf(::BookGroupRepository)
singleOf(::DictRuleRepository)
singleOf(::SearchContentRepository) singleOf(::SearchContentRepository)
singleOf(::RemoteBookRepository) singleOf(::RemoteBookRepository)
singleOf(::ExploreKindUiUseCase) singleOf(::ExploreKindUiUseCase)
singleOf(::AppStartupMaintenanceUseCase)
singleOf(::BatchCacheDownloadUseCase) singleOf(::BatchCacheDownloadUseCase)
singleOf(::CacheBookChaptersUseCase)
singleOf(::ClearBookCacheUseCase) singleOf(::ClearBookCacheUseCase)
singleOf(::DeleteBooksUseCase) singleOf(::DeleteBooksUseCase)
singleOf(::GetReadingProgressUseCase)
singleOf(::RemoveBookGroupAssignmentUseCase)
singleOf(::UpdateBooksGroupUseCase) singleOf(::UpdateBooksGroupUseCase)
singleOf(::UploadReadingProgressUseCase)
singleOf(::ResolveBookShelfStateUseCase)
singleOf(::ShrinkDatabaseUseCase)
singleOf(::WebDavBackupUseCase)
singleOf(::CacheConfig) singleOf(::CacheConfig)
single<UploadRepository> { DirectLinkUploadRepository() } single<UploadRepository> { DirectLinkUploadRepository() }
single<AppStartupGateway> { AppStartupRepository(get()) }
single<BookCacheDownloadGateway> { CacheBookDownloadRepository(get()) }
single<BookCacheCleanupGateway> { BookCacheCleanupRepository(get()) }
single<BookSourceCallbackGateway> { BookSourceCallbackRepository(get(), get()) }
single<LocalBookGateway> { LocalBookRepository(get()) }
single<DatabaseMaintenanceGateway> { DatabaseMaintenanceRepository(get()) }
single<WebDavBackupGateway> { WebDavBackupRepository() }
single<ReadingProgressGateway> { WebDavReadingProgressRepository() }
single<BookDomainRepository> { BookDomainRepositoryImpl(get(), get()) }
single<ExploreRepository> { ExploreRepositoryImpl(get()) } single<ExploreRepository> { ExploreRepositoryImpl(get()) }
single<SearchRepository> { SearchRepositoryImpl(get()) } single<SearchRepository> { SearchRepositoryImpl(get()) }
@@ -106,6 +154,7 @@ val appModule = module {
} }
viewModelOf(::DictRuleViewModel) viewModelOf(::DictRuleViewModel)
viewModelOf(::DictViewModel)
viewModelOf(::RssSourceViewModel) viewModelOf(::RssSourceViewModel)
viewModelOf(::RssSortViewModel) viewModelOf(::RssSortViewModel)
viewModelOf(::RssArticlesViewModel) viewModelOf(::RssArticlesViewModel)
@@ -127,6 +176,8 @@ val appModule = module {
viewModelOf(::TocViewModel) viewModelOf(::TocViewModel)
viewModelOf(::ImportBookViewModel) viewModelOf(::ImportBookViewModel)
viewModelOf(::RemoteBookViewModel) viewModelOf(::RemoteBookViewModel)
viewModelOf(::ServerConfigViewModel)
viewModelOf(::ServersViewModel)
viewModelOf(::BookInfoViewModel) viewModelOf(::BookInfoViewModel)
viewModelOf(::ChangeCoverViewModel) viewModelOf(::ChangeCoverViewModel)
viewModelOf(::ChangeBookSourceComposeViewModel) viewModelOf(::ChangeBookSourceComposeViewModel)
@@ -142,6 +193,7 @@ val appModule = module {
bookChapterDao = get(), bookChapterDao = get(),
cacheConfig = get(), cacheConfig = get(),
batchCacheDownloadUseCase = get(), batchCacheDownloadUseCase = get(),
cacheBookChaptersUseCase = get(),
clearBookCacheUseCase = get(), clearBookCacheUseCase = get(),
deleteBooksUseCase = get(), deleteBooksUseCase = get(),
updateBooksGroupUseCase = get() updateBooksGroupUseCase = get()
@@ -0,0 +1,6 @@
package io.legado.app.domain.gateway
interface AppStartupGateway {
suspend fun deleteNotShelfBooks()
suspend fun ensureDefaultHttpTts()
}
@@ -0,0 +1,6 @@
package io.legado.app.domain.gateway
interface BookCacheCleanupGateway {
fun clearAll()
suspend fun clear(bookUrl: String): Boolean
}
@@ -0,0 +1,5 @@
package io.legado.app.domain.gateway
interface BookCacheDownloadGateway {
suspend fun start(bookUrl: String, chapterIndices: List<Int>)
}
@@ -0,0 +1,5 @@
package io.legado.app.domain.gateway
interface BookSourceCallbackGateway {
suspend fun onDeleteFromShelf(bookUrl: String)
}
@@ -0,0 +1,5 @@
package io.legado.app.domain.gateway
interface DatabaseMaintenanceGateway {
fun shrink()
}
@@ -0,0 +1,5 @@
package io.legado.app.domain.gateway
interface LocalBookGateway {
suspend fun deleteBook(bookUrl: String, deleteOriginal: Boolean)
}
@@ -0,0 +1,10 @@
package io.legado.app.domain.gateway
import io.legado.app.domain.model.ReadingProgress
interface ReadingProgressGateway {
val isConfigured: Boolean
suspend fun getProgress(name: String, author: String): ReadingProgress?
suspend fun uploadProgress(progress: ReadingProgress): Long?
}
@@ -0,0 +1,13 @@
package io.legado.app.domain.gateway
import io.legado.app.domain.model.WebDavBackup
interface WebDavBackupGateway {
val isJianGuoYun: Boolean
suspend fun syncConfig()
suspend fun test(): Boolean
suspend fun getBackupNames(): List<String>
suspend fun getLatestBackup(): WebDavBackup?
suspend fun restore(name: String)
}
@@ -0,0 +1,6 @@
package io.legado.app.domain.model
data class BookGroupAssignment(
val bookUrl: String,
val group: Long
)
@@ -1,4 +1,4 @@
package io.legado.app.model package io.legado.app.domain.model
enum class BookShelfState { enum class BookShelfState {
IN_SHELF, IN_SHELF,
@@ -0,0 +1,9 @@
package io.legado.app.domain.model
data class CacheableBook(
val bookUrl: String,
val isLocal: Boolean,
val isAudio: Boolean,
val durChapterIndex: Int,
val lastChapterIndex: Int
)
@@ -0,0 +1,7 @@
package io.legado.app.domain.model
data class DeletableBook(
val bookUrl: String,
val origin: String,
val isLocal: Boolean
)
@@ -0,0 +1,10 @@
package io.legado.app.domain.model
data class ReadingProgress(
val name: String,
val author: String,
val durChapterIndex: Int,
val durChapterPos: Int,
val durChapterTime: Long,
val durChapterTitle: String?
)
@@ -0,0 +1,6 @@
package io.legado.app.domain.model
data class WebDavBackup(
val name: String,
val lastModify: Long
)
@@ -0,0 +1,15 @@
package io.legado.app.domain.repository
import io.legado.app.domain.model.BookGroupAssignment
import io.legado.app.domain.model.CacheableBook
import io.legado.app.domain.model.DeletableBook
interface BookDomainRepository {
suspend fun getCacheableBooks(bookUrls: Set<String>): List<CacheableBook>
suspend fun getDeletableBooks(bookUrls: Set<String>): List<DeletableBook>
suspend fun getBookGroupAssignments(bookUrls: Set<String>): List<BookGroupAssignment>
suspend fun updateBookGroups(assignments: List<BookGroupAssignment>)
suspend fun removeGroupFromBooks(groupId: Long)
suspend fun deleteBooks(bookUrls: Set<String>)
suspend fun deleteChaptersByBook(bookUrl: String)
}
@@ -0,0 +1,16 @@
package io.legado.app.domain.usecase
import io.legado.app.domain.gateway.AppStartupGateway
class AppStartupMaintenanceUseCase(
private val appStartupGateway: AppStartupGateway
) {
suspend fun deleteNotShelfBooks() {
appStartupGateway.deleteNotShelfBooks()
}
suspend fun ensureDefaultHttpTts() {
appStartupGateway.ensureDefaultHttpTts()
}
}
@@ -1,63 +1,43 @@
package io.legado.app.domain.usecase package io.legado.app.domain.usecase
import android.content.Context import io.legado.app.domain.gateway.BookCacheDownloadGateway
import io.legado.app.constant.BookType import io.legado.app.domain.model.CacheableBook
import io.legado.app.data.dao.BookDao import io.legado.app.domain.repository.BookDomainRepository
import io.legado.app.data.entities.Book
import io.legado.app.model.CacheBook
class BatchCacheDownloadUseCase( class BatchCacheDownloadUseCase(
private val bookDao: BookDao private val bookRepository: BookDomainRepository,
private val bookCacheDownloadGateway: BookCacheDownloadGateway
) { ) {
suspend fun execute( suspend fun execute(
context: Context,
bookUrls: Set<String>, bookUrls: Set<String>,
downloadAllChapters: Boolean, downloadAllChapters: Boolean,
skipAudioBooks: Boolean = false skipAudioBooks: Boolean = false
): Int { ): Int {
if (bookUrls.isEmpty()) return 0 if (bookUrls.isEmpty()) return 0
var count = 0 var count = 0
bookUrls.forEach { bookUrl -> bookRepository.getCacheableBooks(bookUrls).forEach { book ->
val book = bookDao.getBook(bookUrl) ?: return@forEach if (startIfNeeded(book, downloadAllChapters, skipAudioBooks)) {
if (startIfNeeded(context, book, downloadAllChapters, skipAudioBooks)) {
count++ count++
} }
} }
return count return count
} }
fun execute( private suspend fun startIfNeeded(
context: Context, book: CacheableBook,
books: List<Book>,
downloadAllChapters: Boolean,
skipAudioBooks: Boolean = false
): Int {
if (books.isEmpty()) return 0
var count = 0
books.forEach { book ->
if (startIfNeeded(context, book, downloadAllChapters, skipAudioBooks)) {
count++
}
}
return count
}
private fun startIfNeeded(
context: Context,
book: Book,
downloadAllChapters: Boolean, downloadAllChapters: Boolean,
skipAudioBooks: Boolean skipAudioBooks: Boolean
): Boolean { ): Boolean {
if (book.type and BookType.local > 0) return false if (book.isLocal) return false
if (skipAudioBooks && book.type and BookType.audio > 0) return false if (skipAudioBooks && book.isAudio) return false
val indices = if (downloadAllChapters) { val indices = if (downloadAllChapters) {
(0..book.lastChapterIndex).toList() (0..book.lastChapterIndex).toList()
} else { } else {
(book.durChapterIndex..book.lastChapterIndex).toList() (book.durChapterIndex..book.lastChapterIndex).toList()
} }
if (indices.isEmpty()) return false if (indices.isEmpty()) return false
CacheBook.start(context, book, indices) bookCacheDownloadGateway.start(book.bookUrl, indices)
return true return true
} }
} }
@@ -0,0 +1,15 @@
package io.legado.app.domain.usecase
import io.legado.app.domain.gateway.BookCacheDownloadGateway
class CacheBookChaptersUseCase(
private val bookCacheDownloadGateway: BookCacheDownloadGateway
) {
suspend fun execute(bookUrl: String, chapterIndices: Iterable<Int>): Int {
val indices = chapterIndices.distinct()
if (indices.isEmpty()) return 0
bookCacheDownloadGateway.start(bookUrl, indices)
return indices.size
}
}
@@ -1,22 +1,21 @@
package io.legado.app.domain.usecase package io.legado.app.domain.usecase
import io.legado.app.data.dao.BookDao import io.legado.app.domain.gateway.BookCacheCleanupGateway
import io.legado.app.data.entities.Book
import io.legado.app.help.book.BookHelp
class ClearBookCacheUseCase( class ClearBookCacheUseCase(
private val bookDao: BookDao private val bookCacheCleanupGateway: BookCacheCleanupGateway
) { ) {
fun execute(book: Book): String { fun executeAll() {
BookHelp.clearCache(book) bookCacheCleanupGateway.clearAll()
return book.bookUrl }
suspend fun execute(bookUrl: String): String? {
return if (bookCacheCleanupGateway.clear(bookUrl)) bookUrl else null
} }
suspend fun execute(bookUrls: Set<String>): List<String> { suspend fun execute(bookUrls: Set<String>): List<String> {
if (bookUrls.isEmpty()) return emptyList() if (bookUrls.isEmpty()) return emptyList()
return bookUrls.mapNotNull { bookUrl -> return bookUrls.mapNotNull { execute(it) }
bookDao.getBook(bookUrl)?.let { execute(it) }
}
} }
} }
@@ -1,33 +1,27 @@
package io.legado.app.domain.usecase package io.legado.app.domain.usecase
import io.legado.app.data.dao.BookChapterDao import io.legado.app.domain.gateway.BookSourceCallbackGateway
import io.legado.app.data.dao.BookDao import io.legado.app.domain.gateway.LocalBookGateway
import io.legado.app.data.dao.BookSourceDao import io.legado.app.domain.repository.BookDomainRepository
import io.legado.app.help.book.isLocal
import io.legado.app.model.SourceCallBack
import io.legado.app.model.localBook.LocalBook
class DeleteBooksUseCase( class DeleteBooksUseCase(
private val bookDao: BookDao, private val bookRepository: BookDomainRepository,
private val bookChapterDao: BookChapterDao, private val localBookGateway: LocalBookGateway,
private val bookSourceDao: BookSourceDao private val bookSourceCallbackGateway: BookSourceCallbackGateway
) { ) {
suspend fun execute(bookUrls: Set<String>, deleteOriginal: Boolean): List<String> { suspend fun execute(bookUrls: Set<String>, deleteOriginal: Boolean): List<String> {
if (bookUrls.isEmpty()) return emptyList() if (bookUrls.isEmpty()) return emptyList()
val books = bookUrls.mapNotNull { bookDao.getBook(it) } val books = bookRepository.getDeletableBooks(bookUrls)
books.forEach { book -> books.forEach { book ->
if (book.isLocal) { if (book.isLocal) {
LocalBook.deleteBook(book, deleteOriginal) localBookGateway.deleteBook(book.bookUrl, deleteOriginal)
} else { } else {
val source = bookSourceDao.getBookSource(book.origin) bookSourceCallbackGateway.onDeleteFromShelf(book.bookUrl)
SourceCallBack.callBackBook(SourceCallBack.DEL_BOOK_SHELF, source, book)
} }
bookChapterDao.delByBook(book.bookUrl) bookRepository.deleteChaptersByBook(book.bookUrl)
}
if (books.isNotEmpty()) {
bookDao.delete(*books.toTypedArray())
} }
bookRepository.deleteBooks(books.map { it.bookUrl }.toSet())
return books.map { it.bookUrl } return books.map { it.bookUrl }
} }
} }
@@ -0,0 +1,16 @@
package io.legado.app.domain.usecase
import io.legado.app.domain.gateway.ReadingProgressGateway
import io.legado.app.domain.model.ReadingProgress
class GetReadingProgressUseCase(
private val readingProgressGateway: ReadingProgressGateway
) {
val isConfigured: Boolean
get() = readingProgressGateway.isConfigured
suspend fun execute(name: String, author: String): ReadingProgress? {
return readingProgressGateway.getProgress(name, author)
}
}
@@ -0,0 +1,12 @@
package io.legado.app.domain.usecase
import io.legado.app.domain.repository.BookDomainRepository
class RemoveBookGroupAssignmentUseCase(
private val bookRepository: BookDomainRepository
) {
suspend fun execute(groupId: Long) {
bookRepository.removeGroupFromBooks(groupId)
}
}
@@ -0,0 +1,31 @@
package io.legado.app.domain.usecase
import io.legado.app.domain.model.BookShelfState
data class BookShelfKey(
val name: String,
val author: String,
val url: String?
)
class ResolveBookShelfStateUseCase {
fun execute(
name: String,
author: String,
url: String?,
shelf: Set<BookShelfKey>
): BookShelfState {
val exactMatch = shelf.any {
it.name == name && it.author == author && it.url == url
}
if (exactMatch) return BookShelfState.IN_SHELF
val sameNameAuthor = shelf.any {
it.name == name && it.author == author && it.url != url
}
if (sameNameAuthor) return BookShelfState.SAME_NAME_AUTHOR
return BookShelfState.NOT_IN_SHELF
}
}
@@ -0,0 +1,11 @@
package io.legado.app.domain.usecase
import io.legado.app.domain.gateway.DatabaseMaintenanceGateway
class ShrinkDatabaseUseCase(
private val databaseMaintenanceGateway: DatabaseMaintenanceGateway
) {
fun execute() {
databaseMaintenanceGateway.shrink()
}
}
@@ -1,9 +1,9 @@
package io.legado.app.domain.usecase package io.legado.app.domain.usecase
import io.legado.app.data.dao.BookDao import io.legado.app.domain.repository.BookDomainRepository
class UpdateBooksGroupUseCase( class UpdateBooksGroupUseCase(
private val bookDao: BookDao private val bookRepository: BookDomainRepository
) { ) {
suspend fun replaceGroup(bookUrls: Set<String>, groupId: Long) { suspend fun replaceGroup(bookUrls: Set<String>, groupId: Long) {
@@ -12,8 +12,7 @@ class UpdateBooksGroupUseCase(
suspend fun updateGroups(bookUrls: Set<String>, transform: (Long) -> Long) { suspend fun updateGroups(bookUrls: Set<String>, transform: (Long) -> Long) {
if (bookUrls.isEmpty()) return if (bookUrls.isEmpty()) return
val updateBooks = bookUrls.mapNotNull { bookUrl -> val updateGroups = bookRepository.getBookGroupAssignments(bookUrls).mapNotNull { book ->
bookDao.getBook(bookUrl)?.let { book ->
val targetGroup = transform(book.group) val targetGroup = transform(book.group)
if (targetGroup == book.group) { if (targetGroup == book.group) {
null null
@@ -21,9 +20,6 @@ class UpdateBooksGroupUseCase(
book.copy(group = targetGroup) book.copy(group = targetGroup)
} }
} }
} bookRepository.updateBookGroups(updateGroups)
if (updateBooks.isNotEmpty()) {
bookDao.update(*updateBooks.toTypedArray())
}
} }
} }
@@ -0,0 +1,13 @@
package io.legado.app.domain.usecase
import io.legado.app.domain.gateway.ReadingProgressGateway
import io.legado.app.domain.model.ReadingProgress
class UploadReadingProgressUseCase(
private val readingProgressGateway: ReadingProgressGateway
) {
suspend fun execute(progress: ReadingProgress): Long? {
return readingProgressGateway.uploadProgress(progress)
}
}
@@ -0,0 +1,36 @@
package io.legado.app.domain.usecase
import io.legado.app.domain.gateway.WebDavBackupGateway
import io.legado.app.domain.model.WebDavBackup
class WebDavBackupUseCase(
private val webDavBackupGateway: WebDavBackupGateway
) {
val isJianGuoYun: Boolean
get() = webDavBackupGateway.isJianGuoYun
suspend fun refreshConfig() {
webDavBackupGateway.syncConfig()
}
suspend fun test(): Boolean {
webDavBackupGateway.syncConfig()
return webDavBackupGateway.test()
}
suspend fun getBackupNames(): List<String> {
webDavBackupGateway.syncConfig()
return webDavBackupGateway.getBackupNames()
}
suspend fun getLatestBackup(): WebDavBackup? {
webDavBackupGateway.syncConfig()
return webDavBackupGateway.getLatestBackup()
}
suspend fun restore(name: String) {
webDavBackupGateway.syncConfig()
webDavBackupGateway.restore(name)
}
}
@@ -285,18 +285,23 @@ object AppWebDav {
} }
} }
suspend fun uploadBookProgress(bookProgress: BookProgress, onSuccess: (() -> Unit)? = null) { suspend fun uploadBookProgress(
bookProgress: BookProgress,
onSuccess: (() -> Unit)? = null
): Boolean {
try { try {
val authorization = authorization ?: return val authorization = authorization ?: return false
if (!AppConfig.syncBookProgress) return if (!AppConfig.syncBookProgress) return false
if (!NetworkUtils.isAvailable()) return if (!NetworkUtils.isAvailable()) return false
val json = GSON.toJson(bookProgress) val json = GSON.toJson(bookProgress)
val url = getProgressUrl(bookProgress.name, bookProgress.author) val url = getProgressUrl(bookProgress.name, bookProgress.author)
WebDav(url, authorization).upload(json.toByteArray(), "application/json") WebDav(url, authorization).upload(json.toByteArray(), "application/json")
onSuccess?.invoke() onSuccess?.invoke()
return true
} catch (e: Exception) { } catch (e: Exception) {
currentCoroutineContext().ensureActive() currentCoroutineContext().ensureActive()
AppLog.put("上传进度失败\n${e.localizedMessage}", e) AppLog.put("上传进度失败\n${e.localizedMessage}", e)
return false
} }
} }
@@ -312,7 +317,14 @@ object AppWebDav {
* 获取书籍进度 * 获取书籍进度
*/ */
suspend fun getBookProgress(book: Book): BookProgress? { suspend fun getBookProgress(book: Book): BookProgress? {
val url = getProgressUrl(book.name, book.author) return getBookProgress(book.name, book.author)
}
/**
* 获取书籍进度
*/
suspend fun getBookProgress(name: String, author: String): BookProgress? {
val url = getProgressUrl(name, author)
kotlin.runCatching { kotlin.runCatching {
val authorization = authorization ?: return null val authorization = authorization ?: return null
WebDav(url, authorization).download().let { byteArray -> WebDav(url, authorization).download().let { byteArray ->
@@ -2,6 +2,7 @@
package io.legado.app.help.book package io.legado.app.help.book
import io.legado.app.ui.config.otherConfig.OtherConfig
import android.net.Uri import android.net.Uri
import androidx.core.net.toUri import androidx.core.net.toUri
import com.script.buildScriptBindings import com.script.buildScriptBindings
@@ -168,7 +169,7 @@ fun Book.getLocalUri(): Uri {
return uri return uri
} }
//不同的设备书籍保存路径可能不一样, uri无效时尝试寻找当前保存路径下的文件 //不同的设备书籍保存路径可能不一样, uri无效时尝试寻找当前保存路径下的文件
val defaultBookDir = AppConfig.defaultBookTreeUri val defaultBookDir = OtherConfig.defaultBookTreeUri
val importBookDir = AppConfig.importBookPath val importBookDir = AppConfig.importBookPath
// 查找书籍保存目录 // 查找书籍保存目录
@@ -240,7 +241,7 @@ fun Book.getLocalUri(): Uri {
fun Book.getArchiveUri(): Uri? { fun Book.getArchiveUri(): Uri? {
val defaultBookDir = AppConfig.defaultBookTreeUri val defaultBookDir = OtherConfig.defaultBookTreeUri
return if (isArchive && !defaultBookDir.isNullOrBlank()) { return if (isArchive && !defaultBookDir.isNullOrBlank()) {
FileDoc.fromUri(defaultBookDir.toUri(), true) FileDoc.fromUri(defaultBookDir.toUri(), true)
.find(archiveName)?.uri .find(archiveName)?.uri
@@ -303,17 +303,6 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
} }
} }
// 书籍保存位置
var defaultBookTreeUri: String?
get() = appCtx.getPrefString(PreferKey.defaultBookTreeUri)
set(value) {
if (value.isNullOrEmpty()) {
appCtx.removePref(PreferKey.defaultBookTreeUri)
} else {
appCtx.putPrefString(PreferKey.defaultBookTreeUri, value)
}
}
val showDiscovery: Boolean val showDiscovery: Boolean
get() = appCtx.getPrefBoolean(PreferKey.showDiscovery, true) get() = appCtx.getPrefBoolean(PreferKey.showDiscovery, true)
@@ -1,5 +1,6 @@
package io.legado.app.model.localBook package io.legado.app.model.localBook
import io.legado.app.ui.config.otherConfig.OtherConfig
import android.net.Uri import android.net.Uri
import android.util.Base64 import android.util.Base64
import androidx.core.net.toUri import androidx.core.net.toUri
@@ -401,7 +402,7 @@ object LocalBook {
fileName: String, fileName: String,
source: BaseSource? = null, source: BaseSource? = null,
): Uri { ): Uri {
AppConfig.defaultBookTreeUri OtherConfig.defaultBookTreeUri
?: throw NoBooksDirException() ?: throw NoBooksDirException()
val inputStream = when { val inputStream = when {
str.isAbsUrl() -> AnalyzeUrl( str.isAbsUrl() -> AnalyzeUrl(
@@ -427,7 +428,7 @@ object LocalBook {
fileName: String fileName: String
): Uri { ): Uri {
inputStream.use { inputStream.use {
val defaultBookTreeUri = AppConfig.defaultBookTreeUri val defaultBookTreeUri = OtherConfig.defaultBookTreeUri
if (defaultBookTreeUri.isNullOrBlank()) throw NoBooksDirException() if (defaultBookTreeUri.isNullOrBlank()) throw NoBooksDirException()
val treeUri = defaultBookTreeUri.toUri() val treeUri = defaultBookTreeUri.toUri()
return if (treeUri.isContentScheme()) { return if (treeUri.isContentScheme()) {
@@ -481,7 +482,7 @@ object LocalBook {
val webDavUrl = localBook.getRemoteUrl() val webDavUrl = localBook.getRemoteUrl()
if (webDavUrl.isNullOrBlank()) throw NoStackTraceException("Book file is not webDav File") if (webDavUrl.isNullOrBlank()) throw NoStackTraceException("Book file is not webDav File")
try { try {
AppConfig.defaultBookTreeUri OtherConfig.defaultBookTreeUri
?: throw NoBooksDirException() ?: throw NoBooksDirException()
// 兼容旧版链接 // 兼容旧版链接
val webdav: WebDav = kotlin.runCatching { val webdav: WebDav = kotlin.runCatching {
@@ -1,5 +1,6 @@
package io.legado.app.model.remote package io.legado.app.model.remote
import io.legado.app.ui.config.otherConfig.OtherConfig
import android.net.Uri import android.net.Uri
import io.legado.app.constant.AppPattern.archiveFileRegex import io.legado.app.constant.AppPattern.archiveFileRegex
import io.legado.app.constant.AppPattern.bookFileRegex import io.legado.app.constant.AppPattern.bookFileRegex
@@ -7,7 +8,6 @@ import io.legado.app.constant.BookType
import io.legado.app.data.entities.Book import io.legado.app.data.entities.Book
import io.legado.app.exception.NoStackTraceException import io.legado.app.exception.NoStackTraceException
import io.legado.app.help.book.update import io.legado.app.help.book.update
import io.legado.app.help.config.AppConfig
import io.legado.app.lib.webdav.Authorization import io.legado.app.lib.webdav.Authorization
import io.legado.app.lib.webdav.WebDav import io.legado.app.lib.webdav.WebDav
import io.legado.app.lib.webdav.WebDavFile import io.legado.app.lib.webdav.WebDavFile
@@ -57,7 +57,7 @@ class RemoteBookWebDav(
} }
override suspend fun downloadRemoteBook(remoteBook: RemoteBook): Uri { override suspend fun downloadRemoteBook(remoteBook: RemoteBook): Uri {
AppConfig.defaultBookTreeUri OtherConfig.defaultBookTreeUri
?: throw NoStackTraceException("没有设置书籍保存位置!") ?: throw NoStackTraceException("没有设置书籍保存位置!")
if (!NetworkUtils.isAvailable()) throw NoStackTraceException("网络不可用") if (!NetworkUtils.isAvailable()) throw NoStackTraceException("网络不可用")
val webdav = WebDav(remoteBook.path, authorization) val webdav = WebDav(remoteBook.path, authorization)
@@ -1,5 +1,6 @@
package io.legado.app.ui.association package io.legado.app.ui.association
import io.legado.app.ui.config.otherConfig.OtherConfig
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import androidx.activity.viewModels import androidx.activity.viewModels
@@ -11,7 +12,6 @@ import io.legado.app.base.VMBaseActivity
import io.legado.app.constant.AppLog import io.legado.app.constant.AppLog
import io.legado.app.databinding.ActivityTranslucenceBinding import io.legado.app.databinding.ActivityTranslucenceBinding
import io.legado.app.exception.InvalidBooksDirException import io.legado.app.exception.InvalidBooksDirException
import io.legado.app.help.config.AppConfig
import io.legado.app.lib.dialogs.alert import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.permission.Permissions import io.legado.app.lib.permission.Permissions
import io.legado.app.lib.permission.PermissionsCompat import io.legado.app.lib.permission.PermissionsCompat
@@ -43,7 +43,7 @@ class FileAssociationActivity :
private val localBookTreeSelect = registerForActivityResult(HandleFileContract()) { private val localBookTreeSelect = registerForActivityResult(HandleFileContract()) {
intent.data?.let { uri -> intent.data?.let { uri ->
it.uri?.let { treeUri -> it.uri?.let { treeUri ->
AppConfig.defaultBookTreeUri = treeUri.toString() OtherConfig.defaultBookTreeUri = treeUri.toString()
importBook(treeUri, uri) importBook(treeUri, uri)
} ?: let { } ?: let {
val storageHelp = String(assets.open("storageHelp.md").readBytes()) val storageHelp = String(assets.open("storageHelp.md").readBytes())
@@ -134,7 +134,7 @@ class FileAssociationActivity :
private fun importBook(uri: Uri) { private fun importBook(uri: Uri) {
if (uri.isContentScheme()) { if (uri.isContentScheme()) {
val treeUriStr = AppConfig.defaultBookTreeUri val treeUriStr = OtherConfig.defaultBookTreeUri
if (treeUriStr.isNullOrEmpty()) { if (treeUriStr.isNullOrEmpty()) {
localBookTreeSelect.launch { localBookTreeSelect.launch {
title = getString(R.string.select_book_folder) title = getString(R.string.select_book_folder)
@@ -1,5 +1,6 @@
package io.legado.app.ui.book.cache package io.legado.app.ui.book.cache
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
@@ -50,7 +51,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
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
@@ -89,7 +89,6 @@ import io.legado.app.utils.isContentScheme
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.help.config.LocalConfig
import io.legado.app.ui.theme.adaptiveHorizontalPadding import io.legado.app.ui.theme.adaptiveHorizontalPadding
import org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.koinViewModel
@@ -158,7 +157,7 @@ private fun CacheScreen(
var isSearchMode by remember { mutableStateOf(false) } var isSearchMode by remember { mutableStateOf(false) }
var searchKey by remember { mutableStateOf("") } var searchKey by remember { mutableStateOf("") }
var selectedBookUrls by remember { mutableStateOf<Set<String>>(emptySet()) } var selectedBookUrls by remember { mutableStateOf<Set<String>>(emptySet()) }
var deleteOriginalBookFile by remember { mutableStateOf(LocalConfig.deleteBookOriginal) } var deleteOriginalBookFile by remember { mutableStateOf(state.deleteBookOriginal) }
val exportBookPathKey = remember { "exportBookPath" } val exportBookPathKey = remember { "exportBookPath" }
val exportTypes = remember { arrayListOf("txt", "epub") } val exportTypes = remember { arrayListOf("txt", "epub") }
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
@@ -409,7 +408,7 @@ private fun CacheScreen(
) { ) {
if (selectedBookUrls.isNotEmpty()) { if (selectedBookUrls.isNotEmpty()) {
pendingDeleteBookUrls = selectedBookUrls pendingDeleteBookUrls = selectedBookUrls
deleteOriginalBookFile = LocalConfig.deleteBookOriginal deleteOriginalBookFile = state.deleteBookOriginal
showDeleteBookConfirmDialog = true showDeleteBookConfirmDialog = true
} }
} }
@@ -638,7 +637,7 @@ private fun CacheScreen(
onClick = { toggleBookSelection(book) }, onClick = { toggleBookSelection(book) },
onLongClick = { toggleBookSelection(book) }, onLongClick = { toggleBookSelection(book) },
containerColor = if (isSelected) { containerColor = if (isSelected) {
LegadoTheme.colorScheme.secondaryContainer LegadoTheme.colorScheme.surfaceContainerHigh
} else { } else {
LegadoTheme.colorScheme.surfaceContainerLow LegadoTheme.colorScheme.surfaceContainerLow
} }
@@ -727,7 +726,7 @@ private fun CacheScreen(
text = "删除书籍", text = "删除书籍",
onClick = { onClick = {
pendingDeleteBookUrls = setOf(book.bookUrl) pendingDeleteBookUrls = setOf(book.bookUrl)
deleteOriginalBookFile = LocalConfig.deleteBookOriginal deleteOriginalBookFile = state.deleteBookOriginal
showDeleteBookConfirmDialog = true showDeleteBookConfirmDialog = true
dismiss() dismiss()
} }
@@ -806,7 +805,6 @@ private fun CacheScreen(
confirmText = stringResource(android.R.string.ok), confirmText = stringResource(android.R.string.ok),
onConfirm = { onConfirm = {
showDeleteBookConfirmDialog = false showDeleteBookConfirmDialog = false
LocalConfig.deleteBookOriginal = deleteOriginalBookFile
viewModel.dispatch( viewModel.dispatch(
CacheIntent.DeleteBooks( CacheIntent.DeleteBooks(
bookUrls = pendingDeleteBookUrls, bookUrls = pendingDeleteBookUrls,
@@ -10,6 +10,7 @@ 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.domain.usecase.BatchCacheDownloadUseCase import io.legado.app.domain.usecase.BatchCacheDownloadUseCase
import io.legado.app.domain.usecase.CacheBookChaptersUseCase
import io.legado.app.domain.usecase.ClearBookCacheUseCase 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
@@ -17,6 +18,7 @@ import io.legado.app.help.book.BookHelp
import io.legado.app.help.book.isAudio 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.model.CacheBook import io.legado.app.model.CacheBook
import io.legado.app.help.config.LocalConfig
import io.legado.app.service.ExportBookService import io.legado.app.service.ExportBookService
import io.legado.app.ui.config.cacheConfig.CacheConfig import io.legado.app.ui.config.cacheConfig.CacheConfig
import io.legado.app.utils.cnCompare import io.legado.app.utils.cnCompare
@@ -51,6 +53,7 @@ data class CacheUiState(
val books: List<Book> = emptyList(), val books: List<Book> = emptyList(),
val isDownloadRunning: Boolean = false, val isDownloadRunning: Boolean = false,
val cacheVersion: Long = 0, val cacheVersion: Long = 0,
val deleteBookOriginal: Boolean = LocalConfig.deleteBookOriginal,
val exportConfig: CacheExportConfig = CacheExportConfig() val exportConfig: CacheExportConfig = CacheExportConfig()
) )
@@ -93,6 +96,7 @@ class CacheViewModel(
private val bookChapterDao: BookChapterDao, private val bookChapterDao: BookChapterDao,
val cacheConfig: CacheConfig, val cacheConfig: CacheConfig,
private val batchCacheDownloadUseCase: BatchCacheDownloadUseCase, private val batchCacheDownloadUseCase: BatchCacheDownloadUseCase,
private val cacheBookChaptersUseCase: CacheBookChaptersUseCase,
private val clearBookCacheUseCase: ClearBookCacheUseCase, private val clearBookCacheUseCase: ClearBookCacheUseCase,
private val deleteBooksUseCase: DeleteBooksUseCase, private val deleteBooksUseCase: DeleteBooksUseCase,
private val updateBooksGroupUseCase: UpdateBooksGroupUseCase private val updateBooksGroupUseCase: UpdateBooksGroupUseCase
@@ -335,8 +339,7 @@ class CacheViewModel(
private fun startDownloadForVisibleBooks(books: List<Book>, downloadAllChapters: Boolean) { private fun startDownloadForVisibleBooks(books: List<Book>, downloadAllChapters: Boolean) {
execute { execute {
batchCacheDownloadUseCase.execute( batchCacheDownloadUseCase.execute(
context = context, bookUrls = books.map { it.bookUrl }.toSet(),
books = books,
downloadAllChapters = downloadAllChapters, downloadAllChapters = downloadAllChapters,
skipAudioBooks = true skipAudioBooks = true
) )
@@ -349,10 +352,14 @@ class CacheViewModel(
if (book.isLocal) return if (book.isLocal) return
if (isBookDownloading(book.bookUrl)) { if (isBookDownloading(book.bookUrl)) {
CacheBook.remove(context, book.bookUrl) CacheBook.remove(context, book.bookUrl)
} else {
CacheBook.start(context, book, (0..book.lastChapterIndex).toList())
}
syncDownloadRunning() syncDownloadRunning()
} else {
execute {
cacheBookChaptersUseCase.execute(book.bookUrl, 0..book.lastChapterIndex)
}.onFinally {
syncDownloadRunning()
}
}
} }
private fun moveBooksToGroup(bookUrls: Set<String>, groupId: Long) { private fun moveBooksToGroup(bookUrls: Set<String>, groupId: Long) {
@@ -368,8 +375,10 @@ class CacheViewModel(
private fun deleteBooks(bookUrls: Set<String>, deleteOriginal: Boolean) { private fun deleteBooks(bookUrls: Set<String>, deleteOriginal: Boolean) {
if (bookUrls.isEmpty()) return if (bookUrls.isEmpty()) return
execute { execute {
LocalConfig.deleteBookOriginal = deleteOriginal
deleteBooksUseCase.execute(bookUrls, deleteOriginal) deleteBooksUseCase.execute(bookUrls, deleteOriginal)
}.onSuccess { deletedBookUrls -> }.onSuccess { deletedBookUrls ->
_uiState.update { it.copy(deleteBookOriginal = deleteOriginal) }
deletedBookUrls.forEach { cacheChapters.remove(it) } deletedBookUrls.forEach { cacheChapters.remove(it) }
_effects.tryEmit(CacheEffect.ShowMessage("删除成功")) _effects.tryEmit(CacheEffect.ShowMessage("删除成功"))
}.onError { }.onError {
@@ -396,7 +405,6 @@ class CacheViewModel(
if (bookUrls.isEmpty()) return if (bookUrls.isEmpty()) return
execute { execute {
batchCacheDownloadUseCase.execute( batchCacheDownloadUseCase.execute(
context = context,
bookUrls = bookUrls, bookUrls = bookUrls,
downloadAllChapters = downloadAllChapters, downloadAllChapters = downloadAllChapters,
skipAudioBooks = true skipAudioBooks = true
@@ -414,10 +422,14 @@ class CacheViewModel(
} }
private fun clearCacheForBook(book: Book) { private fun clearCacheForBook(book: Book) {
val bookUrl = clearBookCacheUseCase.execute(book) execute {
clearBookCacheUseCase.execute(book.bookUrl)
}.onSuccess { bookUrl ->
bookUrl ?: return@onSuccess
cacheChapters[bookUrl] = hashSetOf() cacheChapters[bookUrl] = hashSetOf()
emitBookChanged(bookUrl) emitBookChanged(bookUrl)
} }
}
private fun emitBookChanged(bookUrl: String) { private fun emitBookChanged(bookUrl: String) {
_uiState.update { it.copy(cacheVersion = it.cacheVersion + 1) } _uiState.update { it.copy(cacheVersion = it.cacheVersion + 1) }
@@ -61,8 +61,8 @@ import androidx.compose.ui.unit.dp
import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import io.legado.app.data.entities.SearchBook import io.legado.app.data.entities.SearchBook
import io.legado.app.domain.usecase.ExploreKindUiUseCase import io.legado.app.ui.widget.components.explore.ExploreKindUiUseCase
import io.legado.app.model.BookShelfState import io.legado.app.domain.model.BookShelfState
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.theme.responsiveHazeEffect import io.legado.app.ui.theme.responsiveHazeEffect
@@ -84,7 +84,6 @@ 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 kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.koinViewModel
import org.koin.compose.koinInject import org.koin.compose.koinInject
@@ -444,12 +443,12 @@ fun ExploreShowScreen(
) { ) {
items( items(
items = books, items = books,
key = { it.bookUrl } key = { it.book.bookUrl }
) { book -> ) { item ->
ExploreBookGridItem( ExploreBookGridItem(
book = book, book = item.book,
shelfState = viewModel.getBookShelfStateFlow(book), shelfState = item.shelfState,
onClick = { onBookClick(book) }, onClick = { onBookClick(item.book) },
modifier = Modifier.animateItem() modifier = Modifier.animateItem()
) )
} }
@@ -476,12 +475,12 @@ fun ExploreShowScreen(
) { ) {
items( items(
items = books, items = books,
key = { it.bookUrl } key = { it.book.bookUrl }
) { book -> ) { item ->
ExploreBookItem( ExploreBookItem(
book = book, book = item.book,
shelfState = viewModel.getBookShelfStateFlow(book), shelfState = item.shelfState,
onClick = { onBookClick(book) }, onClick = { onBookClick(item.book) },
modifier = Modifier.animateItem() modifier = Modifier.animateItem()
) )
} }
@@ -505,7 +504,7 @@ fun ExploreShowScreen(
@Composable @Composable
fun ExploreBookItem( fun ExploreBookItem(
book: SearchBook, book: SearchBook,
shelfState: Flow<BookShelfState>, shelfState: BookShelfState,
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
@@ -521,7 +520,7 @@ fun ExploreBookItem(
fun ExploreBookGridItem( fun ExploreBookGridItem(
book: SearchBook, book: SearchBook,
onClick: () -> Unit, onClick: () -> Unit,
shelfState: Flow<BookShelfState>, shelfState: BookShelfState,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
SearchBookGridItem( SearchBookGridItem(
@@ -7,18 +7,16 @@ import io.legado.app.data.entities.BookSource
import io.legado.app.data.entities.SearchBook import io.legado.app.data.entities.SearchBook
import io.legado.app.data.entities.rule.ExploreKind import io.legado.app.data.entities.rule.ExploreKind
import io.legado.app.data.repository.ExploreRepository import io.legado.app.data.repository.ExploreRepository
import io.legado.app.domain.usecase.BookShelfKey
import io.legado.app.domain.usecase.ResolveBookShelfStateUseCase
import io.legado.app.help.config.AppConfig import io.legado.app.help.config.AppConfig
import io.legado.app.model.BookShelfState import io.legado.app.domain.model.BookShelfState
import io.legado.app.ui.book.search.BookKey
import io.legado.app.utils.exploreLayoutGrid import io.legado.app.utils.exploreLayoutGrid
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import splitties.init.appCtx import splitties.init.appCtx
@@ -46,8 +44,14 @@ sealed class UiState<out T> {
data object Empty : UiState<Nothing>() data object Empty : UiState<Nothing>()
} }
data class ExploreBookItemUi(
val book: SearchBook,
val shelfState: BookShelfState = BookShelfState.NOT_IN_SHELF,
)
class ExploreShowViewModel( class ExploreShowViewModel(
private val repository: ExploreRepository private val repository: ExploreRepository,
private val resolveBookShelfStateUseCase: ResolveBookShelfStateUseCase
) : ViewModel() { ) : ViewModel() {
private val _rawBooks = MutableStateFlow<List<SearchBook>>(emptyList()) private val _rawBooks = MutableStateFlow<List<SearchBook>>(emptyList())
@@ -60,7 +64,7 @@ class ExploreShowViewModel(
private var exploreUrl: String? = null private var exploreUrl: String? = null
private var page = 1 private var page = 1
private val _isEndStateFlow = MutableStateFlow(false) private val _isEndStateFlow = MutableStateFlow(false)
private val _bookshelf = MutableStateFlow<Set<BookKey>>(emptySet()) private val _bookshelf = MutableStateFlow<Set<BookShelfKey>>(emptySet())
private val _kinds = MutableStateFlow<List<ExploreKind>>(emptyList()) private val _kinds = MutableStateFlow<List<ExploreKind>>(emptyList())
val kinds = _kinds.asStateFlow() val kinds = _kinds.asStateFlow()
private val _selectedKindTitle = MutableStateFlow<String?>(null) private val _selectedKindTitle = MutableStateFlow<String?>(null)
@@ -85,6 +89,16 @@ class ExploreShowViewModel(
) { books, filter, bookshelf -> ) { books, filter, bookshelf ->
books.filter { item -> books.filter { item ->
isBookValid(item, filter, bookshelf) isBookValid(item, filter, bookshelf)
}.map { item ->
ExploreBookItemUi(
book = item,
shelfState = resolveBookShelfStateUseCase.execute(
name = item.name,
author = item.author,
url = item.bookUrl,
shelf = bookshelf
)
)
} }
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList()) }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
@@ -104,7 +118,7 @@ class ExploreShowViewModel(
init { init {
viewModelScope.launch { viewModelScope.launch {
repository.getBookshelfItems().collect { list -> repository.getBookshelfItems().collect { list ->
val keys = list.map { BookKey(it.name, it.author, it.bookUrl) }.toSet() val keys = list.map { BookShelfKey(it.name, it.author, it.bookUrl) }.toSet()
_bookshelf.value = keys _bookshelf.value = keys
} }
} }
@@ -199,11 +213,25 @@ class ExploreShowViewModel(
} }
fun getCurrentBookShelfState(item: SearchBook): BookShelfState { fun getCurrentBookShelfState(item: SearchBook): BookShelfState {
return getBookShelfState(item, _bookshelf.value) return resolveBookShelfStateUseCase.execute(
name = item.name,
author = item.author,
url = item.bookUrl,
shelf = _bookshelf.value
)
} }
private fun isBookValid(book: SearchBook, filter: BookFilterState, shelf: Set<BookKey>): Boolean { private fun isBookValid(
val state = getBookShelfState(book, shelf) book: SearchBook,
filter: BookFilterState,
shelf: Set<BookShelfKey>
): Boolean {
val state = resolveBookShelfStateUseCase.execute(
name = book.name,
author = book.author,
url = book.bookUrl,
shelf = shelf
)
return when (filter) { return when (filter) {
BookFilterState.SHOW_ALL -> true BookFilterState.SHOW_ALL -> true
BookFilterState.HIDE_IN_SHELF -> state != BookShelfState.IN_SHELF BookFilterState.HIDE_IN_SHELF -> state != BookShelfState.IN_SHELF
@@ -211,20 +239,4 @@ class ExploreShowViewModel(
BookFilterState.SHOW_NOT_IN_SHELF_ONLY -> state == BookShelfState.NOT_IN_SHELF BookFilterState.SHOW_NOT_IN_SHELF_ONLY -> state == BookShelfState.NOT_IN_SHELF
} }
} }
fun getBookShelfStateFlow(item: SearchBook): Flow<BookShelfState> {
return _bookshelf.map { shelf ->
getBookShelfState(item, shelf)
}.distinctUntilChanged()
}
private fun getBookShelfState(item: SearchBook, shelf: Set<BookKey>): BookShelfState {
val exactMatch = shelf.any { it.name == item.name && it.author == item.author && it.url == item.bookUrl }
if (exactMatch) return BookShelfState.IN_SHELF
val sameNameAuthor = shelf.any { it.name == item.name && it.author == item.author && it.url != item.bookUrl }
if (sameNameAuthor) return BookShelfState.SAME_NAME_AUTHOR
return BookShelfState.NOT_IN_SHELF
}
} }
@@ -2,13 +2,14 @@ package io.legado.app.ui.book.group
import android.app.Application import android.app.Application
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
import io.legado.app.data.appDb
import io.legado.app.data.entities.BookGroup import io.legado.app.data.entities.BookGroup
import io.legado.app.data.repository.BookGroupRepository import io.legado.app.data.repository.BookGroupRepository
import io.legado.app.domain.usecase.RemoveBookGroupAssignmentUseCase
class GroupViewModel( class GroupViewModel(
application: Application, application: Application,
private val bookGroupRepository: BookGroupRepository private val bookGroupRepository: BookGroupRepository,
private val removeBookGroupAssignmentUseCase: RemoveBookGroupAssignmentUseCase
) : BaseViewModel(application) { ) : BaseViewModel(application) {
fun upGroup(vararg bookGroup: BookGroup, finally: (() -> Unit)? = null) { fun upGroup(vararg bookGroup: BookGroup, finally: (() -> Unit)? = null) {
@@ -36,7 +37,7 @@ class GroupViewModel(
enableRefresh = enableRefresh, enableRefresh = enableRefresh,
order = bookGroupRepository.getMaxOrder().plus(1) order = bookGroupRepository.getMaxOrder().plus(1)
) )
bookGroupRepository.getByID(groupId) ?: appDb.bookDao.removeGroup(groupId) bookGroupRepository.getByID(groupId) ?: removeBookGroupAssignmentUseCase.execute(groupId)
bookGroupRepository.insert(bookGroup) bookGroupRepository.insert(bookGroup)
}.onFinally { }.onFinally {
finally() finally()
@@ -46,7 +47,7 @@ class GroupViewModel(
fun delGroup(bookGroup: BookGroup, finally: () -> Unit) { fun delGroup(bookGroup: BookGroup, finally: () -> Unit) {
execute { execute {
bookGroupRepository.delete(bookGroup) bookGroupRepository.delete(bookGroup)
appDb.bookDao.removeGroup(bookGroup.groupId) removeBookGroupAssignmentUseCase.execute(bookGroup.groupId)
}.onFinally { }.onFinally {
finally() finally()
} }
@@ -1,5 +1,6 @@
package io.legado.app.ui.book.import.local package io.legado.app.ui.book.import.local
import io.legado.app.ui.config.otherConfig.OtherConfig
import android.app.Application import android.app.Application
import android.net.Uri import android.net.Uri
import android.provider.DocumentsContract import android.provider.DocumentsContract
@@ -231,7 +232,7 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application)
fun hasRootDoc(): Boolean = _state.value.rootDoc != null fun hasRootDoc(): Boolean = _state.value.rootDoc != null
private fun initialize() { private fun initialize() {
val defaultPath = ImportBookConfig.defaultBookTreeUri?.takeIf { it.isUri() } val defaultPath = OtherConfig.defaultBookTreeUri?.takeIf { it.isUri() }
val effectiveDefaultPath = defaultPath ?: firstPersistedTreeUri()?.toString() val effectiveDefaultPath = defaultPath ?: firstPersistedTreeUri()?.toString()
if (effectiveDefaultPath.isNullOrBlank()) { if (effectiveDefaultPath.isNullOrBlank()) {
_effects.tryEmit( _effects.tryEmit(
@@ -239,8 +240,8 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application)
) )
return return
} }
if (ImportBookConfig.defaultBookTreeUri != effectiveDefaultPath) { if (OtherConfig.defaultBookTreeUri != effectiveDefaultPath) {
ImportBookConfig.defaultBookTreeUri = effectiveDefaultPath OtherConfig.defaultBookTreeUri = effectiveDefaultPath
} }
val importPath = ImportBookConfig.importBookPath val importPath = ImportBookConfig.importBookPath
if (importPath.isNullOrBlank() || !importPath.isUri()) { if (importPath.isNullOrBlank() || !importPath.isUri()) {
@@ -255,16 +256,16 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application)
val pickedUri = persistFolderPermission(uri) val pickedUri = persistFolderPermission(uri)
when (target) { when (target) {
ImportFolderPickTarget.DEFAULT_BOOK -> { ImportFolderPickTarget.DEFAULT_BOOK -> {
ImportBookConfig.defaultBookTreeUri = pickedUri.toString() OtherConfig.defaultBookTreeUri = pickedUri.toString()
if (ImportBookConfig.importBookPath.isNullOrBlank()) { if (ImportBookConfig.importBookPath.isNullOrBlank()) {
ImportBookConfig.importBookPath = ImportBookConfig.defaultBookTreeUri ImportBookConfig.importBookPath = OtherConfig.defaultBookTreeUri
} }
} }
ImportFolderPickTarget.IMPORT_FOLDER -> { ImportFolderPickTarget.IMPORT_FOLDER -> {
ImportBookConfig.importBookPath = pickedUri.toString() ImportBookConfig.importBookPath = pickedUri.toString()
if (ImportBookConfig.defaultBookTreeUri.isNullOrBlank()) { if (OtherConfig.defaultBookTreeUri.isNullOrBlank()) {
ImportBookConfig.defaultBookTreeUri = pickedUri.toString() OtherConfig.defaultBookTreeUri = pickedUri.toString()
} }
} }
} }
@@ -302,14 +303,14 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application)
ImportBookConfig.importBookPath ImportBookConfig.importBookPath
?.takeIf { it.isNotBlank() && it.isUri() } ?.takeIf { it.isNotBlank() && it.isUri() }
?.let(::add) ?.let(::add)
ImportBookConfig.defaultBookTreeUri OtherConfig.defaultBookTreeUri
?.takeIf { it.isNotBlank() && it.isUri() } ?.takeIf { it.isNotBlank() && it.isUri() }
?.let(::add) ?.let(::add)
} }
if (candidates.isEmpty()) { if (candidates.isEmpty()) {
firstPersistedTreeUri()?.toString()?.let { persistedPath -> firstPersistedTreeUri()?.toString()?.let { persistedPath ->
if (trySetRootDoc(persistedPath)) { if (trySetRootDoc(persistedPath)) {
ImportBookConfig.defaultBookTreeUri = persistedPath OtherConfig.defaultBookTreeUri = persistedPath
ImportBookConfig.importBookPath = persistedPath ImportBookConfig.importBookPath = persistedPath
return return
} }
@@ -341,7 +342,7 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application)
firstPersistedTreeUri()?.toString()?.let { persistedPath -> firstPersistedTreeUri()?.toString()?.let { persistedPath ->
if (trySetRootDoc(persistedPath)) { if (trySetRootDoc(persistedPath)) {
ImportBookConfig.defaultBookTreeUri = persistedPath OtherConfig.defaultBookTreeUri = persistedPath
ImportBookConfig.importBookPath = persistedPath ImportBookConfig.importBookPath = persistedPath
return return
} }
@@ -62,12 +62,10 @@ import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
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.AppConst import io.legado.app.constant.AppConst
import io.legado.app.data.entities.Server import io.legado.app.data.entities.Server
import io.legado.app.help.config.AppConfig
import io.legado.app.lib.dialogs.alert import io.legado.app.lib.dialogs.alert
import io.legado.app.lib.dialogs.selector import io.legado.app.lib.dialogs.selector
import io.legado.app.model.remote.RemoteBook import io.legado.app.model.remote.RemoteBook
@@ -91,7 +89,6 @@ 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.text.AppText import io.legado.app.ui.widget.components.text.AppText
import io.legado.app.utils.ConvertUtils import io.legado.app.utils.ConvertUtils
import io.legado.app.utils.isUri
import io.legado.app.utils.startActivityForBook import io.legado.app.utils.startActivityForBook
import io.legado.app.utils.toastOnUi import io.legado.app.utils.toastOnUi
import org.json.JSONObject import org.json.JSONObject
@@ -218,10 +215,7 @@ fun RemoteBookScreen(
when (effect) { when (effect) {
is RemoteBookEffect.OpenBook -> context.startActivityForBook(effect.book) is RemoteBookEffect.OpenBook -> context.startActivityForBook(effect.book)
is RemoteBookEffect.RequestBookFolderPicker -> { is RemoteBookEffect.RequestBookFolderPicker -> {
selectDocTree.launch( selectDocTree.launch(effect.initialUri)
effect.initialUri
?: AppConfig.defaultBookTreeUri?.takeIf { it.isUri() }?.toUri()
)
} }
is RemoteBookEffect.ShowArchiveEntries -> { is RemoteBookEffect.ShowArchiveEntries -> {
@@ -1,5 +1,6 @@
package io.legado.app.ui.book.import.remote package io.legado.app.ui.book.import.remote
import io.legado.app.ui.config.otherConfig.OtherConfig
import android.app.Application import android.app.Application
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.compose.runtime.Immutable import androidx.compose.runtime.Immutable
@@ -14,8 +15,6 @@ import io.legado.app.data.entities.Book
import io.legado.app.data.entities.Server import io.legado.app.data.entities.Server
import io.legado.app.data.repository.RemoteBookRepository import io.legado.app.data.repository.RemoteBookRepository
import io.legado.app.exception.NoStackTraceException import io.legado.app.exception.NoStackTraceException
import io.legado.app.help.AppWebDav
import io.legado.app.help.config.AppConfig
import io.legado.app.model.analyzeRule.CustomUrl import io.legado.app.model.analyzeRule.CustomUrl
import io.legado.app.model.localBook.LocalBook import io.legado.app.model.localBook.LocalBook
import io.legado.app.model.remote.RemoteBook import io.legado.app.model.remote.RemoteBook
@@ -29,6 +28,7 @@ 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.find import io.legado.app.utils.find
import io.legado.app.utils.isUri
import io.legado.app.utils.takePersistablePermissionSafely import io.legado.app.utils.takePersistablePermissionSafely
import io.legado.app.utils.toastOnUi import io.legado.app.utils.toastOnUi
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -162,13 +162,13 @@ class RemoteBookViewModel(
private fun onBookFolderPicked(uri: android.net.Uri?) { private fun onBookFolderPicked(uri: android.net.Uri?) {
uri ?: return uri ?: return
uri.takePersistablePermissionSafely(context) uri.takePersistablePermissionSafely(context)
AppConfig.defaultBookTreeUri = uri.toString() OtherConfig.defaultBookTreeUri = uri.toString()
} }
val uiState: StateFlow<RemoteBookUiState> = combine( val uiState: StateFlow<RemoteBookUiState> = combine(
_state, _state,
repository.flowLocalBooks(), repository.flowLocalBooks(),
appDb.serverDao.observeAll() repository.flowServers()
) { state, localBooks, servers -> ) { state, localBooks, servers ->
val localFileNames = localBooks.map { it.originName }.toSet() val localFileNames = localBooks.map { it.originName }.toSet()
@@ -224,7 +224,7 @@ class RemoteBookViewModel(
} }
onSuccess() onSuccess()
} else { } else {
val defaultWebDav = AppWebDav.defaultBookWebDav val defaultWebDav = repository.getDefaultBookWebDav()
?: throw NoStackTraceException("webDav没有配置") ?: throw NoStackTraceException("webDav没有配置")
_state.update { _state.update {
it.copy( it.copy(
@@ -282,7 +282,7 @@ class RemoteBookViewModel(
_state.update { it.copy(selectedIds = emptySet()) } _state.update { it.copy(selectedIds = emptySet()) }
Result.success(Unit) Result.success(Unit)
} catch (e: SecurityException) { } catch (e: SecurityException) {
_effects.emit(RemoteBookEffect.RequestBookFolderPicker()) _effects.emit(RemoteBookEffect.RequestBookFolderPicker(defaultBookTreeUri()))
Result.failure(e) Result.failure(e)
} catch (e: Exception) { } catch (e: Exception) {
AppLog.put("导入出错\n${e.localizedMessage}", e) AppLog.put("导入出错\n${e.localizedMessage}", e)
@@ -419,14 +419,14 @@ class RemoteBookViewModel(
return@launch return@launch
} }
val bookTreeUri = AppConfig.defaultBookTreeUri val bookTreeUri = defaultBookTreeUri()
if (bookTreeUri == null) { if (bookTreeUri == null) {
_effects.emit(RemoteBookEffect.RequestBookFolderPicker()) _effects.emit(RemoteBookEffect.RequestBookFolderPicker(defaultBookTreeUri()))
return@launch return@launch
} }
val downloadArchiveFileDoc = withContext(Dispatchers.IO) { val downloadArchiveFileDoc = withContext(Dispatchers.IO) {
FileDoc.fromUri(bookTreeUri.toUri(), true) FileDoc.fromUri(bookTreeUri, true)
.find(downloadFileName) .find(downloadFileName)
} }
if (downloadArchiveFileDoc == null) { if (downloadArchiveFileDoc == null) {
@@ -495,9 +495,15 @@ class RemoteBookViewModel(
} }
} }
private fun defaultBookTreeUri(): android.net.Uri? {
return OtherConfig.defaultBookTreeUri
?.takeIf { it.isUri() }
?.toUri()
}
fun saveServer(server: Server) { fun saveServer(server: Server) {
execute { execute {
appDb.serverDao.insert(server) repository.saveServer(server)
}.onSuccess { }.onSuccess {
if (ImportBookConfig.remoteServerId == server.id) { if (ImportBookConfig.remoteServerId == server.id) {
initData { loadRemoteBookList() } initData { loadRemoteBookList() }
@@ -507,7 +513,7 @@ class RemoteBookViewModel(
fun deleteServer(server: Server) { fun deleteServer(server: Server) {
execute { execute {
appDb.serverDao.delete(server) repository.deleteServer(server)
} }
} }
@@ -6,7 +6,6 @@ import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.viewModels
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseDialogFragment import io.legado.app.base.BaseDialogFragment
import io.legado.app.data.entities.Server import io.legado.app.data.entities.Server
@@ -18,6 +17,7 @@ import io.legado.app.utils.GSON
import io.legado.app.utils.setLayout import io.legado.app.utils.setLayout
import io.legado.app.utils.viewbindingdelegate.viewBinding import io.legado.app.utils.viewbindingdelegate.viewBinding
import org.json.JSONObject import org.json.JSONObject
import org.koin.androidx.viewmodel.ext.android.viewModel
class ServerConfigDialog() : BaseDialogFragment(R.layout.dialog_webdav_server, true), class ServerConfigDialog() : BaseDialogFragment(R.layout.dialog_webdav_server, true),
Toolbar.OnMenuItemClickListener { Toolbar.OnMenuItemClickListener {
@@ -29,7 +29,7 @@ class ServerConfigDialog() : BaseDialogFragment(R.layout.dialog_webdav_server, t
} }
private val binding by viewBinding(DialogWebdavServerBinding::bind) private val binding by viewBinding(DialogWebdavServerBinding::bind)
private val viewModel by viewModels<ServerConfigViewModel>() private val viewModel by viewModel<ServerConfigViewModel>()
private val webDavServerUi = listOf( private val webDavServerUi = listOf(
RowUi("url"), RowUi("url"),
@@ -2,11 +2,14 @@ package io.legado.app.ui.book.import.remote
import android.app.Application import android.app.Application
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
import io.legado.app.data.appDb
import io.legado.app.data.entities.Server import io.legado.app.data.entities.Server
import io.legado.app.data.repository.RemoteBookRepository
import io.legado.app.utils.toastOnUi import io.legado.app.utils.toastOnUi
class ServerConfigViewModel(application: Application): BaseViewModel(application) { class ServerConfigViewModel(
application: Application,
private val remoteBookRepository: RemoteBookRepository
) : BaseViewModel(application) {
var mServer: Server? = null var mServer: Server? = null
@@ -15,7 +18,7 @@ class ServerConfigViewModel(application: Application): BaseViewModel(application
if (mServer != null) return if (mServer != null) return
execute { execute {
mServer = if (id != null) { mServer = if (id != null) {
appDb.serverDao.get(id) remoteBookRepository.getServer(id)
} else { } else {
Server() Server()
} }
@@ -27,10 +30,10 @@ class ServerConfigViewModel(application: Application): BaseViewModel(application
fun save(server: Server, onSuccess: () -> Unit) { fun save(server: Server, onSuccess: () -> Unit) {
execute { execute {
mServer?.let { mServer?.let {
appDb.serverDao.delete(it) remoteBookRepository.deleteServer(it)
} }
mServer = server mServer = server
appDb.serverDao.insert(server) remoteBookRepository.saveServer(server)
}.onSuccess { }.onSuccess {
onSuccess.invoke() onSuccess.invoke()
}.onError { }.onError {
@@ -7,7 +7,6 @@ import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import io.legado.app.R import io.legado.app.R
@@ -16,7 +15,6 @@ import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.base.adapter.RecyclerAdapter import io.legado.app.base.adapter.RecyclerAdapter
import io.legado.app.constant.AppConst.DEFAULT_WEBDAV_ID import io.legado.app.constant.AppConst.DEFAULT_WEBDAV_ID
import io.legado.app.constant.AppLog import io.legado.app.constant.AppLog
import io.legado.app.data.appDb
import io.legado.app.data.entities.Server import io.legado.app.data.entities.Server
import io.legado.app.databinding.DialogRecyclerViewBinding import io.legado.app.databinding.DialogRecyclerViewBinding
import io.legado.app.databinding.ItemServerSelectBinding import io.legado.app.databinding.ItemServerSelectBinding
@@ -34,6 +32,7 @@ import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.androidx.viewmodel.ext.android.viewModel
/** /**
* 服务器配置 * 服务器配置
@@ -42,7 +41,7 @@ class ServersDialog : BaseDialogFragment(R.layout.dialog_recycler_view),
Toolbar.OnMenuItemClickListener { Toolbar.OnMenuItemClickListener {
val binding by viewBinding(DialogRecyclerViewBinding::bind) val binding by viewBinding(DialogRecyclerViewBinding::bind)
val viewModel by viewModels<ServersViewModel>() val viewModel by viewModel<ServersViewModel>()
private val callback get() = (activity as? Callback) private val callback get() = (activity as? Callback)
private val adapter by lazy { ServersAdapter(requireContext()) } private val adapter by lazy { ServersAdapter(requireContext()) }
@@ -86,7 +85,7 @@ class ServersDialog : BaseDialogFragment(R.layout.dialog_recycler_view),
private fun initData() { private fun initData() {
lifecycleScope.launch { lifecycleScope.launch {
appDb.serverDao.observeAll().catch { viewModel.flowServers().catch {
AppLog.put("服务器配置界面获取数据失败\n${it.localizedMessage}", it) AppLog.put("服务器配置界面获取数据失败\n${it.localizedMessage}", it)
}.flowOn(IO).collect { }.flowOn(IO).collect {
adapter.setItems(it) adapter.setItems(it)
@@ -2,15 +2,19 @@ package io.legado.app.ui.book.import.remote
import android.app.Application import android.app.Application
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
import io.legado.app.data.appDb
import io.legado.app.data.entities.Server import io.legado.app.data.entities.Server
import io.legado.app.data.repository.RemoteBookRepository
class ServersViewModel(application: Application): BaseViewModel(application) { class ServersViewModel(
application: Application,
private val remoteBookRepository: RemoteBookRepository
) : BaseViewModel(application) {
fun flowServers() = remoteBookRepository.flowServers()
fun delete(server: Server) { fun delete(server: Server) {
execute { execute {
appDb.serverDao.delete(server) remoteBookRepository.deleteServer(server)
} }
} }
@@ -1,5 +1,6 @@
package io.legado.app.ui.book.info package io.legado.app.ui.book.info
import io.legado.app.ui.config.otherConfig.OtherConfig
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
@@ -41,7 +42,7 @@ class BookInfoActivity : BaseComposeActivity(), VariableDialog.Callback {
} }
private val localBookTreeSelect = registerForActivityResult(HandleFileContract()) { private val localBookTreeSelect = registerForActivityResult(HandleFileContract()) {
it.uri?.let { treeUri -> it.uri?.let { treeUri ->
AppConfig.defaultBookTreeUri = treeUri.toString() OtherConfig.defaultBookTreeUri = treeUri.toString()
} }
} }
private val infoEditResult = registerForActivityResult( private val infoEditResult = registerForActivityResult(
@@ -19,6 +19,8 @@ data class BookInfoUiState(
val bookSource: BookSource? = null, val bookSource: BookSource? = null,
val isTocLoading: Boolean = true, val isTocLoading: Boolean = true,
val isBusy: Boolean = false, val isBusy: Boolean = false,
val deleteAlertEnabled: Boolean = true,
val deleteOriginal: Boolean = false,
val showAppLogSheet: Boolean = false, val showAppLogSheet: Boolean = false,
val sheet: BookInfoSheet = BookInfoSheet.None, val sheet: BookInfoSheet = BookInfoSheet.None,
val dialog: BookInfoDialog? = null, val dialog: BookInfoDialog? = null,
@@ -573,7 +573,7 @@ private fun BookInfoOverflowMenu(
RoundDropdownMenuItem( RoundDropdownMenuItem(
text = stringResource(R.string.delete_alert), text = stringResource(R.string.delete_alert),
onClick = { onMenuAction(BookInfoMenuAction.ToggleDeleteAlert) }, onClick = { onMenuAction(BookInfoMenuAction.ToggleDeleteAlert) },
isSelected = io.legado.app.help.config.LocalConfig.bookInfoDeleteAlert isSelected = state.deleteAlertEnabled
) )
RoundDropdownMenuItem( RoundDropdownMenuItem(
text = stringResource(R.string.clear_cache), text = stringResource(R.string.clear_cache),
@@ -894,7 +894,7 @@ private fun BookInfoDialogs(
onBack: () -> Unit, onBack: () -> Unit,
) { ) {
val dialog = state.dialog val dialog = state.dialog
var deleteOriginal by remember(dialog) { mutableStateOf(io.legado.app.help.config.LocalConfig.deleteBookOriginal) } 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.AddToShelfOnBack) { if (dialog is BookInfoDialog.AddToShelfOnBack) {
@@ -918,7 +918,6 @@ private fun BookInfoDialogs(
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 = {
io.legado.app.help.config.LocalConfig.deleteBookOriginal = deleteOriginal
onIntent(BookInfoIntent.ConfirmDelete(deleteOriginal)) onIntent(BookInfoIntent.ConfirmDelete(deleteOriginal))
}, },
dismissText = stringResource(android.R.string.cancel), dismissText = stringResource(android.R.string.cancel),
@@ -18,6 +18,7 @@ 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.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.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
import io.legado.app.help.book.BookHelp import io.legado.app.help.book.BookHelp
@@ -63,7 +64,8 @@ import kotlinx.coroutines.launch
class BookInfoViewModel( class BookInfoViewModel(
application: Application, application: Application,
private val remoteBookRepository: RemoteBookRepository, private val remoteBookRepository: RemoteBookRepository,
private val readRecordRepository: ReadRecordRepository private val readRecordRepository: ReadRecordRepository,
private val clearBookCacheUseCase: ClearBookCacheUseCase
) : BaseViewModel(application) { ) : BaseViewModel(application) {
private val _uiState = MutableStateFlow(BookInfoUiState()) private val _uiState = MutableStateFlow(BookInfoUiState())
@@ -400,7 +402,7 @@ class BookInfoViewModel(
fun clearCache() { fun clearCache() {
currentBook?.let { book -> currentBook?.let { book ->
execute { execute {
BookHelp.clearCache(book) clearBookCacheUseCase.execute(book.bookUrl)
if (ReadBook.book?.bookUrl == book.bookUrl) { if (ReadBook.book?.bookUrl == book.bookUrl) {
ReadBook.clearTextChapter() ReadBook.clearTextChapter()
} }
@@ -851,6 +853,8 @@ class BookInfoViewModel(
} }
private fun deleteBook(deleteOriginal: Boolean) { private fun deleteBook(deleteOriginal: Boolean) {
currentBook?.let { book -> currentBook?.let { book ->
LocalConfig.deleteBookOriginal = deleteOriginal
_uiState.update { it.copy(deleteOriginal = deleteOriginal) }
SourceCallBack.callBackBook(SourceCallBack.DEL_BOOK_SHELF, bookSource, book) SourceCallBack.callBackBook(SourceCallBack.DEL_BOOK_SHELF, bookSource, book)
delBook(deleteOriginal) { delBook(deleteOriginal) {
emitEffect(BookInfoEffect.Finish(resultCode = RESULT_OK)) emitEffect(BookInfoEffect.Finish(resultCode = RESULT_OK))
@@ -1168,6 +1172,8 @@ class BookInfoViewModel(
inBookshelf = inBookshelf, inBookshelf = inBookshelf,
bookSource = bookSource, bookSource = bookSource,
isTocLoading = isTocLoading, isTocLoading = isTocLoading,
deleteAlertEnabled = LocalConfig.bookInfoDeleteAlert,
deleteOriginal = LocalConfig.deleteBookOriginal,
) )
} }
} }
@@ -15,8 +15,9 @@ import io.legado.app.data.entities.Book
import io.legado.app.data.entities.Book.ReadConfig import io.legado.app.data.entities.Book.ReadConfig
import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookChapter
import io.legado.app.data.entities.BookProgress import io.legado.app.data.entities.BookProgress
import io.legado.app.domain.model.ReadingProgress
import io.legado.app.domain.usecase.GetReadingProgressUseCase
import io.legado.app.exception.NoStackTraceException import io.legado.app.exception.NoStackTraceException
import io.legado.app.help.AppWebDav
import io.legado.app.help.book.BookHelp import io.legado.app.help.book.BookHelp
import io.legado.app.help.book.isLocal import io.legado.app.help.book.isLocal
import io.legado.app.help.book.isLocalModified import io.legado.app.help.book.isLocalModified
@@ -47,7 +48,10 @@ import kotlinx.coroutines.withContext
import splitties.init.appCtx import splitties.init.appCtx
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
class ReadMangaViewModel(application: Application) : BaseViewModel(application) { class ReadMangaViewModel(
application: Application,
private val getReadingProgressUseCase: GetReadingProgressUseCase
) : BaseViewModel(application) {
private var changeSourceCoroutine: Coroutine<*>? = null private var changeSourceCoroutine: Coroutine<*>? = null
@@ -243,7 +247,7 @@ class ReadMangaViewModel(application: Application) : BaseViewModel(application)
) { ) {
if (!AppConfig.syncBookProgress) return if (!AppConfig.syncBookProgress) return
execute { execute {
AppWebDav.getBookProgress(book) getReadingProgressUseCase.execute(book.name, book.author)?.toBookProgress()
}.onError { }.onError {
AppLog.put("拉取阅读进度失败《${book.name}\n${it.localizedMessage}", it) AppLog.put("拉取阅读进度失败《${book.name}\n${it.localizedMessage}", it)
}.onSuccess { progress -> }.onSuccess { progress ->
@@ -260,6 +264,15 @@ class ReadMangaViewModel(application: Application) : BaseViewModel(application)
} }
} }
private fun ReadingProgress.toBookProgress() = BookProgress(
name = name,
author = author,
durChapterIndex = durChapterIndex,
durChapterPos = durChapterPos,
durChapterTime = durChapterTime,
durChapterTitle = durChapterTitle
)
/** /**
* 换源 * 换源
*/ */
@@ -39,7 +39,6 @@ import io.legado.app.data.entities.BookChapter
import io.legado.app.data.entities.BookProgress import io.legado.app.data.entities.BookProgress
import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.BookSource
import io.legado.app.exception.NoStackTraceException import io.legado.app.exception.NoStackTraceException
import io.legado.app.help.AppWebDav
import io.legado.app.help.IntentData import io.legado.app.help.IntentData
import io.legado.app.help.TTS import io.legado.app.help.TTS
import io.legado.app.help.book.BookHelp import io.legado.app.help.book.BookHelp
@@ -50,7 +49,6 @@ import io.legado.app.help.book.isLocal
import io.legado.app.help.book.isLocalTxt import io.legado.app.help.book.isLocalTxt
import io.legado.app.help.book.isMobi import io.legado.app.help.book.isMobi
import io.legado.app.help.book.removeType import io.legado.app.help.book.removeType
import io.legado.app.help.book.update
import io.legado.app.help.config.AppConfig import io.legado.app.help.config.AppConfig
import io.legado.app.help.config.ReadBookConfig import io.legado.app.help.config.ReadBookConfig
import io.legado.app.help.config.ReadTipConfig import io.legado.app.help.config.ReadTipConfig
@@ -525,10 +523,10 @@ class ReadBookActivity : BaseReadBookActivity(),
} }
lifecycleScope.launch { lifecycleScope.launch {
menu.findItem(R.id.menu_get_progress)?.isVisible = withContext(IO) { menu.findItem(R.id.menu_get_progress)?.isVisible = withContext(IO) {
AppWebDav.isOk viewModel.isReadingProgressSyncConfigured()
} }
menu.findItem(R.id.menu_cover_progress)?.isVisible = withContext(IO) { menu.findItem(R.id.menu_cover_progress)?.isVisible = withContext(IO) {
AppWebDav.isOk viewModel.isReadingProgressSyncConfigured()
} }
} }
} }
@@ -1841,9 +1839,8 @@ class ReadBookActivity : BaseReadBookActivity(),
backupJob = lifecycleScope.launch(IO) { backupJob = lifecycleScope.launch(IO) {
delay(300000) delay(300000)
ReadBook.book?.let { ReadBook.book?.let {
AppWebDav.uploadBookProgress(it) viewModel.uploadBookProgress(it)
ensureActive() ensureActive()
it.update()
Backup.autoBack(this@ReadBookActivity) Backup.autoBack(this@ReadBookActivity)
} }
} }
@@ -12,8 +12,10 @@ 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.BookProgress import io.legado.app.data.entities.BookProgress
import io.legado.app.domain.model.ReadingProgress
import io.legado.app.domain.usecase.GetReadingProgressUseCase
import io.legado.app.domain.usecase.UploadReadingProgressUseCase
import io.legado.app.exception.NoStackTraceException import io.legado.app.exception.NoStackTraceException
import io.legado.app.help.AppWebDav
import io.legado.app.help.book.BookHelp import io.legado.app.help.book.BookHelp
import io.legado.app.help.book.ContentProcessor import io.legado.app.help.book.ContentProcessor
import io.legado.app.help.book.isLocal import io.legado.app.help.book.isLocal
@@ -52,7 +54,11 @@ import kotlin.coroutines.coroutineContext
/** /**
* 阅读界面数据处理 * 阅读界面数据处理
*/ */
class ReadBookViewModel(application: Application) : BaseViewModel(application) { class ReadBookViewModel(
application: Application,
private val getReadingProgressUseCase: GetReadingProgressUseCase,
private val uploadReadingProgressUseCase: UploadReadingProgressUseCase
) : BaseViewModel(application) {
val permissionDenialLiveData = MutableLiveData<Int>() val permissionDenialLiveData = MutableLiveData<Int>()
var isInitFinish = false var isInitFinish = false
var searchContentQuery = "" var searchContentQuery = ""
@@ -253,7 +259,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
) { ) {
if (!AppConfig.syncBookProgress) return if (!AppConfig.syncBookProgress) return
execute { execute {
AppWebDav.getBookProgress(book) getReadingProgressUseCase.execute(book.name, book.author)?.toBookProgress()
}.onError { }.onError {
AppLog.put("拉取阅读进度失败《${book.name}\n${it.localizedMessage}", it) AppLog.put("拉取阅读进度失败《${book.name}\n${it.localizedMessage}", it)
}.onSuccess { progress -> }.onSuccess { progress ->
@@ -270,6 +276,35 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
} }
} }
fun isReadingProgressSyncConfigured(): Boolean {
return getReadingProgressUseCase.isConfigured
}
suspend fun uploadBookProgress(book: Book) {
uploadReadingProgressUseCase.execute(book.toReadingProgress())?.let { uploadTime ->
book.syncTime = uploadTime
appDb.bookDao.update(book)
}
}
private fun Book.toReadingProgress() = ReadingProgress(
name = name,
author = author,
durChapterIndex = durChapterIndex,
durChapterPos = durChapterPos,
durChapterTime = durChapterTime,
durChapterTitle = durChapterTitle
)
private fun ReadingProgress.toBookProgress() = BookProgress(
name = name,
author = author,
durChapterIndex = durChapterIndex,
durChapterPos = durChapterPos,
durChapterTime = durChapterTime,
durChapterTitle = durChapterTitle
)
/** /**
* 换源 * 换源
*/ */
@@ -1,7 +0,0 @@
package io.legado.app.ui.book.search
data class BookKey(
val name: String,
val author: String,
val url: String?
)
@@ -12,7 +12,7 @@ import io.legado.app.base.adapter.ItemViewHolder
import io.legado.app.data.entities.SearchBook import io.legado.app.data.entities.SearchBook
import io.legado.app.databinding.ItemSearchBinding import io.legado.app.databinding.ItemSearchBinding
import io.legado.app.help.config.AppConfig import io.legado.app.help.config.AppConfig
import io.legado.app.model.BookShelfState import io.legado.app.domain.model.BookShelfState
import io.legado.app.ui.widget.text.AccentBgTextView import io.legado.app.ui.widget.text.AccentBgTextView
import io.legado.app.utils.dpToPx import io.legado.app.utils.dpToPx
import io.legado.app.utils.gone import io.legado.app.utils.gone
@@ -3,12 +3,18 @@ package io.legado.app.ui.book.search
import io.legado.app.data.entities.BookSourcePart import io.legado.app.data.entities.BookSourcePart
import io.legado.app.data.entities.SearchBook import io.legado.app.data.entities.SearchBook
import io.legado.app.data.entities.SearchKeyword import io.legado.app.data.entities.SearchKeyword
import io.legado.app.domain.model.BookShelfState
import io.legado.app.ui.main.bookshelf.BookShelfItem import io.legado.app.ui.main.bookshelf.BookShelfItem
data class SearchResultItemUi(
val book: SearchBook,
val shelfState: BookShelfState = BookShelfState.NOT_IN_SHELF,
)
data class SearchUiState( data class SearchUiState(
val query: String = "", val query: String = "",
val committedQuery: String = "", val committedQuery: String = "",
val results: List<SearchBook> = emptyList(), val results: List<SearchResultItemUi> = emptyList(),
val history: List<SearchKeyword> = emptyList(), val history: List<SearchKeyword> = emptyList(),
val bookshelfHints: List<BookShelfItem> = emptyList(), val bookshelfHints: List<BookShelfItem> = emptyList(),
val enabledGroups: List<String> = emptyList(), val enabledGroups: List<String> = emptyList(),
@@ -337,16 +337,13 @@ fun SearchScreen(
) { ) {
itemsIndexed( itemsIndexed(
items = state.results, items = state.results,
key = { index, book -> "${book.origin}:${book.bookUrl}:$index" } key = { index, item -> "${item.book.origin}:${item.book.bookUrl}:$index" }
) { _, book -> ) { _, item ->
val shelfStateFlow = remember(book.bookUrl) {
viewModel.getBookShelfStateFlow(book)
}
SearchBookListItem( SearchBookListItem(
book = book, book = item.book,
shelfState = shelfStateFlow, shelfState = item.shelfState,
onClick = { onClick = {
viewModel.onIntent(SearchIntent.OpenSearchBook(book)) viewModel.onIntent(SearchIntent.OpenSearchBook(item.book))
} }
) )
} }
@@ -7,13 +7,13 @@ import io.legado.app.data.entities.BookSourcePart
import io.legado.app.data.entities.SearchBook import io.legado.app.data.entities.SearchBook
import io.legado.app.data.repository.SearchRepository import io.legado.app.data.repository.SearchRepository
import io.legado.app.data.repository.SearchSessionEvent import io.legado.app.data.repository.SearchSessionEvent
import io.legado.app.domain.usecase.BookShelfKey
import io.legado.app.domain.usecase.ResolveBookShelfStateUseCase
import io.legado.app.help.config.AppConfig import io.legado.app.help.config.AppConfig
import io.legado.app.model.BookShelfState
import io.legado.app.ui.main.bookshelf.BookShelfItem import io.legado.app.ui.main.bookshelf.BookShelfItem
import io.legado.app.utils.getPrefBoolean import io.legado.app.utils.getPrefBoolean
import io.legado.app.utils.putPrefBoolean import io.legado.app.utils.putPrefBoolean
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
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
@@ -29,6 +29,7 @@ import splitties.init.appCtx
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
class SearchViewModel( class SearchViewModel(
private val repository: SearchRepository, private val repository: SearchRepository,
private val resolveBookShelfStateUseCase: ResolveBookShelfStateUseCase,
) : ViewModel() { ) : ViewModel() {
private val _uiState = MutableStateFlow( private val _uiState = MutableStateFlow(
@@ -46,7 +47,7 @@ class SearchViewModel(
val effects = _effects.asSharedFlow() val effects = _effects.asSharedFlow()
private val queryFlow = MutableStateFlow("") private val queryFlow = MutableStateFlow("")
private val bookshelfKeys = MutableStateFlow<Set<BookKey>>(emptySet()) private val bookshelfKeys = MutableStateFlow<Set<BookShelfKey>>(emptySet())
private val searchScope = SearchScope(AppConfig.searchScope) private val searchScope = SearchScope(AppConfig.searchScope)
private val searchSession = repository.createSearchSession { searchScope } private val searchSession = repository.createSearchSession { searchScope }
@@ -142,12 +143,6 @@ class SearchViewModel(
} }
} }
fun getBookShelfStateFlow(item: SearchBook): Flow<BookShelfState> {
return bookshelfKeys
.map { keys -> resolveBookShelfState(item, keys) }
.distinctUntilChanged()
}
override fun onCleared() { override fun onCleared() {
super.onCleared() super.onCleared()
searchSession.close() searchSession.close()
@@ -194,6 +189,9 @@ class SearchViewModel(
.catch { emit(emptySet()) } .catch { emit(emptySet()) }
.collect { keys -> .collect { keys ->
bookshelfKeys.value = keys bookshelfKeys.value = keys
_uiState.update { state ->
state.copy(results = state.results.withShelfState(keys))
}
} }
} }
} }
@@ -233,9 +231,10 @@ class SearchViewModel(
} }
is SearchSessionEvent.Progress -> { is SearchSessionEvent.Progress -> {
val shelf = bookshelfKeys.value
_uiState.update { _uiState.update {
it.copy( it.copy(
results = event.books, results = event.books.toSearchResultItems(shelf),
processedSources = event.processedSources, processedSources = event.processedSources,
totalSources = event.totalSources, totalSources = event.totalSources,
) )
@@ -403,18 +402,35 @@ class SearchViewModel(
} }
} }
private fun resolveBookShelfState(item: SearchBook, shelf: Set<BookKey>): BookShelfState { private fun List<SearchBook>.toSearchResultItems(
val exactMatch = shelf.any { shelf: Set<BookShelfKey>
it.name == item.name && it.author == item.author && it.url == item.bookUrl ): List<SearchResultItemUi> {
return map { book ->
SearchResultItemUi(
book = book,
shelfState = resolveBookShelfStateUseCase.execute(
name = book.name,
author = book.author,
url = book.bookUrl,
shelf = shelf
)
)
} }
if (exactMatch) return BookShelfState.IN_SHELF
val sameNameAuthor = shelf.any {
it.name == item.name && it.author == item.author && it.url != item.bookUrl
} }
if (sameNameAuthor) return BookShelfState.SAME_NAME_AUTHOR
return BookShelfState.NOT_IN_SHELF private fun List<SearchResultItemUi>.withShelfState(
shelf: Set<BookShelfKey>
): List<SearchResultItemUi> {
return map { item ->
item.copy(
shelfState = resolveBookShelfStateUseCase.execute(
name = item.book.name,
author = item.book.author,
url = item.book.bookUrl,
shelf = shelf
)
)
}
} }
private fun emitEffect(effect: SearchEffect) { private fun emitEffect(effect: SearchEffect) {
@@ -13,6 +13,7 @@ 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.Bookmark import io.legado.app.data.entities.Bookmark
import io.legado.app.data.entities.ReplaceRule import io.legado.app.data.entities.ReplaceRule
import io.legado.app.domain.usecase.CacheBookChaptersUseCase
import io.legado.app.help.book.BookHelp import io.legado.app.help.book.BookHelp
import io.legado.app.help.book.ContentProcessor import io.legado.app.help.book.ContentProcessor
import io.legado.app.help.book.isLocal import io.legado.app.help.book.isLocal
@@ -113,7 +114,8 @@ data class FabAction(val icon: ImageVector, val label: String, val action: () ->
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
class TocViewModel( class TocViewModel(
application: Application, application: Application,
savedStateHandle: SavedStateHandle savedStateHandle: SavedStateHandle,
private val cacheBookChaptersUseCase: CacheBookChaptersUseCase
) : BaseRuleViewModel<TocItemUi, TocDomainItem, Int, TocActionState>( ) : BaseRuleViewModel<TocItemUi, TocDomainItem, Int, TocActionState>(
application, application,
initialState = TocActionState() initialState = TocActionState()
@@ -524,16 +526,22 @@ class TocViewModel(
val book = bookState.value ?: return val book = bookState.value ?: return
val indices = uiState.value.selectedIds.toList() val indices = uiState.value.selectedIds.toList()
if (indices.isEmpty()) return if (indices.isEmpty()) return
CacheBook.start(getApplication(), book, indices) execute {
getApplication<Application>().toastOnUi("开始下载 ${indices.size} 个章节") cacheBookChaptersUseCase.execute(book.bookUrl, indices)
}.onSuccess { count ->
getApplication<Application>().toastOnUi("开始下载 $count 个章节")
clearSelection() clearSelection()
} }
}
fun downloadChapter(index: Int) { fun downloadChapter(index: Int) {
val book = bookState.value ?: return val book = bookState.value ?: return
CacheBook.start(getApplication(), book, listOf(index)) execute {
cacheBookChaptersUseCase.execute(book.bookUrl, listOf(index))
}.onSuccess {
getApplication<Application>().toastOnUi("开始下载章节") getApplication<Application>().toastOnUi("开始下载章节")
} }
}
fun downloadAll() { fun downloadAll() {
val book = bookState.value ?: return val book = bookState.value ?: return
@@ -546,8 +554,11 @@ class TocViewModel(
return return
} }
CacheBook.start(getApplication(), book, targetIndices) execute {
getApplication<Application>().toastOnUi("开始下载剩余 ${targetIndices.size} 个章节") cacheBookChaptersUseCase.execute(book.bookUrl, targetIndices)
}.onSuccess { count ->
getApplication<Application>().toastOnUi("开始下载剩余 $count 个章节")
}
} }
private fun List<BookChapter>.groupAndReverseVolumes(): List<BookChapter> { private fun List<BookChapter>.groupAndReverseVolumes(): List<BookChapter> {
@@ -2,18 +2,20 @@ package io.legado.app.ui.config.backupConfig
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import io.legado.app.help.AppWebDav import io.legado.app.domain.usecase.WebDavBackupUseCase
import io.legado.app.help.storage.Backup import io.legado.app.help.storage.Backup
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import splitties.init.appCtx import splitties.init.appCtx
class BackupConfigViewModel : ViewModel() { class BackupConfigViewModel(
private val webDavBackupUseCase: WebDavBackupUseCase
) : ViewModel() {
private suspend fun syncWebDavConfig() { private suspend fun syncWebDavConfig() {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
AppWebDav.upConfig() webDavBackupUseCase.refreshConfig()
} }
} }
@@ -35,11 +37,9 @@ class BackupConfigViewModel : ViewModel() {
} }
suspend fun testWebDav(): Boolean { suspend fun testWebDav(): Boolean {
syncWebDavConfig()
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
try { try {
AppWebDav.testWebDav() webDavBackupUseCase.test()
true
} catch (e: Exception) { } catch (e: Exception) {
false false
} }
@@ -62,17 +62,15 @@ class BackupConfigViewModel : ViewModel() {
} }
suspend fun getBackupNames(): List<String> { suspend fun getBackupNames(): List<String> {
syncWebDavConfig()
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
AppWebDav.getBackupNames() webDavBackupUseCase.getBackupNames()
} }
} }
fun restoreWebDav(name: String, onSuccess: () -> Unit, onError: (String) -> Unit) { fun restoreWebDav(name: String, onSuccess: () -> Unit, onError: (String) -> Unit) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try { try {
AppWebDav.upConfig() webDavBackupUseCase.restore(name)
AppWebDav.restoreWebDav(name)
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
onSuccess() onSuccess()
} }
@@ -5,11 +5,6 @@ import io.legado.app.ui.config.prefDelegate
object ImportBookConfig { object ImportBookConfig {
var defaultBookTreeUri by prefDelegate(
PreferKey.defaultBookTreeUri,
null as String?
)
var importBookPath by prefDelegate( var importBookPath by prefDelegate(
PreferKey.importBookPath, PreferKey.importBookPath,
null as String? null as String?
@@ -48,7 +48,7 @@ object OtherConfig {
var defaultBookTreeUri by prefDelegate( var defaultBookTreeUri by prefDelegate(
PreferKey.defaultBookTreeUri, PreferKey.defaultBookTreeUri,
"" null as String?
) )
var antiAlias by prefDelegate( var antiAlias by prefDelegate(
@@ -9,9 +9,9 @@ import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.data.appDb import io.legado.app.domain.usecase.ClearBookCacheUseCase
import io.legado.app.domain.usecase.ShrinkDatabaseUseCase
import io.legado.app.help.DirectLinkUpload import io.legado.app.help.DirectLinkUpload
import io.legado.app.help.book.BookHelp
import io.legado.app.help.config.AppConfig 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.model.CheckSource import io.legado.app.model.CheckSource
@@ -25,7 +25,10 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import splitties.init.appCtx import splitties.init.appCtx
class OtherConfigViewModel : ViewModel() { class OtherConfigViewModel(
private val clearBookCacheUseCase: ClearBookCacheUseCase,
private val shrinkDatabaseUseCase: ShrinkDatabaseUseCase
) : ViewModel() {
private val packageManager = appCtx.packageManager private val packageManager = appCtx.packageManager
private val componentName = ComponentName( private val componentName = ComponentName(
@@ -52,7 +55,7 @@ class OtherConfigViewModel : ViewModel() {
fun clearCache(context: Context) { fun clearCache(context: Context) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
BookHelp.clearCache() clearBookCacheUseCase.executeAll()
FileUtils.delete(context.cacheDir.absolutePath) FileUtils.delete(context.cacheDir.absolutePath)
context.externalCacheDir?.deleteRecursively() context.externalCacheDir?.deleteRecursively()
context.cacheDir.deleteRecursively() context.cacheDir.deleteRecursively()
@@ -61,7 +64,7 @@ class OtherConfigViewModel : ViewModel() {
fun shrinkDatabase() { fun shrinkDatabase() {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
appDb.openHelper.writableDatabase.execSQL("VACUUM") shrinkDatabaseUseCase.execute()
} }
} }
@@ -90,7 +93,6 @@ class OtherConfigViewModel : ViewModel() {
fun updateLocalBookDir(path: String) { fun updateLocalBookDir(path: String) {
OtherConfig.defaultBookTreeUri = path OtherConfig.defaultBookTreeUri = path
AppConfig.defaultBookTreeUri = OtherConfig.defaultBookTreeUri
} }
var checkSourceTimeout by mutableStateOf((CheckSource.timeout / 1000)) var checkSourceTimeout by mutableStateOf((CheckSource.timeout / 1000))
@@ -3,7 +3,6 @@ package io.legado.app.ui.dict
import android.os.Bundle import android.os.Bundle
import android.text.method.LinkMovementMethod import android.text.method.LinkMovementMethod
import android.view.View import android.view.View
import androidx.fragment.app.viewModels
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseBottomSheetDialogFragment import io.legado.app.base.BaseBottomSheetDialogFragment
@@ -15,6 +14,7 @@ import io.legado.app.utils.setHtml
import io.legado.app.utils.toastOnUi import io.legado.app.utils.toastOnUi
import io.legado.app.utils.viewbindingdelegate.viewBinding import io.legado.app.utils.viewbindingdelegate.viewBinding
import io.legado.app.utils.visible import io.legado.app.utils.visible
import org.koin.androidx.viewmodel.ext.android.viewModel
/** /**
* 词典 * 词典
@@ -27,7 +27,7 @@ class DictDialog() : BaseBottomSheetDialogFragment(R.layout.dialog_dict) {
} }
} }
private val viewModel by viewModels<DictViewModel>() private val viewModel by viewModel<DictViewModel>()
private val binding by viewBinding(DialogDictBinding::bind) private val binding by viewBinding(DialogDictBinding::bind)
private var word: String? = null private var word: String? = null
private var glideImageGetter: GlideImageGetter? = null private var glideImageGetter: GlideImageGetter? = null
@@ -2,17 +2,20 @@ package io.legado.app.ui.dict
import android.app.Application import android.app.Application
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
import io.legado.app.data.appDb
import io.legado.app.data.entities.DictRule import io.legado.app.data.entities.DictRule
import io.legado.app.data.repository.DictRuleRepository
import io.legado.app.help.coroutine.Coroutine import io.legado.app.help.coroutine.Coroutine
class DictViewModel(application: Application) : BaseViewModel(application) { class DictViewModel(
application: Application,
private val dictRuleRepository: DictRuleRepository
) : BaseViewModel(application) {
private var dictJob: Coroutine<String>? = null private var dictJob: Coroutine<String>? = null
fun initData(onSuccess: (List<DictRule>) -> Unit) { fun initData(onSuccess: (List<DictRule>) -> Unit) {
execute { execute {
appDb.dictRuleDao.enabled dictRuleRepository.getEnabled()
}.onSuccess { }.onSuccess {
onSuccess.invoke(it) onSuccess.invoke(it)
} }
@@ -30,7 +30,6 @@ import io.legado.app.R
import io.legado.app.base.BaseComposeActivity import io.legado.app.base.BaseComposeActivity
import io.legado.app.constant.AppConst.appInfo import io.legado.app.constant.AppConst.appInfo
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.help.AppWebDav
import io.legado.app.help.book.BookHelp import io.legado.app.help.book.BookHelp
import io.legado.app.help.config.AppConfig import io.legado.app.help.config.AppConfig
import io.legado.app.help.config.LocalConfig import io.legado.app.help.config.LocalConfig
@@ -716,13 +715,13 @@ open class MainActivity : BaseComposeActivity() {
} }
lifecycleScope.launch { lifecycleScope.launch {
val lastBackupFile = val lastBackupFile =
withContext(IO) { AppWebDav.lastBackUp().getOrNull() } ?: return@launch withContext(IO) { viewModel.getLatestWebDavBackup() } ?: return@launch
if (lastBackupFile.lastModify - LocalConfig.lastBackup > DateUtils.MINUTE_IN_MILLIS) { if (lastBackupFile.lastModify - LocalConfig.lastBackup > DateUtils.MINUTE_IN_MILLIS) {
LocalConfig.lastBackup = lastBackupFile.lastModify LocalConfig.lastBackup = lastBackupFile.lastModify
alert(R.string.restore, R.string.webdav_after_local_restore_confirm) { alert(R.string.restore, R.string.webdav_after_local_restore_confirm) {
cancelButton() cancelButton()
okButton { okButton {
viewModel.restoreWebDav(lastBackupFile.displayName) viewModel.restoreWebDav(lastBackupFile.name)
} }
} }
} }
@@ -5,14 +5,17 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import io.legado.app.base.BaseViewModel import io.legado.app.base.BaseViewModel
import io.legado.app.constant.EventBus import io.legado.app.constant.EventBus
import io.legado.app.data.appDb import io.legado.app.domain.usecase.AppStartupMaintenanceUseCase
import io.legado.app.help.AppWebDav import io.legado.app.domain.usecase.WebDavBackupUseCase
import io.legado.app.help.DefaultData
import io.legado.app.ui.main.my.PrefClickEvent import io.legado.app.ui.main.my.PrefClickEvent
import io.legado.app.utils.eventBus.FlowEventBus import io.legado.app.utils.eventBus.FlowEventBus
import io.legado.app.utils.sendToClip import io.legado.app.utils.sendToClip
class MainViewModel(application: Application) : BaseViewModel(application) { class MainViewModel(
application: Application,
private val appStartupMaintenanceUseCase: AppStartupMaintenanceUseCase,
private val webDavBackupUseCase: WebDavBackupUseCase
) : BaseViewModel(application) {
init { init {
deleteNotShelfBook() deleteNotShelfBook()
@@ -24,23 +27,21 @@ class MainViewModel(application: Application) : BaseViewModel(application) {
fun postLoad() { fun postLoad() {
execute { execute {
if (appDb.httpTTSDao.count == 0) { appStartupMaintenanceUseCase.ensureDefaultHttpTts()
DefaultData.httpTTS.let {
appDb.httpTTSDao.insert(*it.toTypedArray())
}
}
} }
} }
fun restoreWebDav(name: String) { fun restoreWebDav(name: String) {
execute { execute {
AppWebDav.restoreWebDav(name) webDavBackupUseCase.restore(name)
} }
} }
suspend fun getLatestWebDavBackup() = webDavBackupUseCase.getLatestBackup()
private fun deleteNotShelfBook() { private fun deleteNotShelfBook() {
execute { execute {
appDb.bookDao.deleteNotShelfBook() appStartupMaintenanceUseCase.deleteNotShelfBooks()
} }
} }
@@ -70,7 +70,6 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseRuleEvent import io.legado.app.base.BaseRuleEvent
import io.legado.app.data.appDb
import io.legado.app.data.entities.BookGroup import io.legado.app.data.entities.BookGroup
import io.legado.app.ui.about.AppLogSheet import io.legado.app.ui.about.AppLogSheet
import io.legado.app.ui.book.info.GroupSelectSheet import io.legado.app.ui.book.info.GroupSelectSheet
@@ -101,7 +100,6 @@ import io.legado.app.utils.readText
import io.legado.app.utils.toastOnUi import io.legado.app.utils.toastOnUi
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.koinViewModel
@@ -226,14 +224,8 @@ fun BookshelfScreen(
} }
val isUsingStandaloneSearchGroup = uiState.isSearch && val isUsingStandaloneSearchGroup = uiState.isSearch &&
uiState.groups.none { it.groupId == currentGroupId } uiState.groups.none { it.groupId == currentGroupId }
val currentGroupBookCountFlow = remember(currentGroupId) { val currentGroupBookCount = uiState.currentGroupBookCount
appDb.bookDao.flowBookShelfByGroup(currentGroupId).map { it.size } val allGroupsBookCount = uiState.allBooksCount
}
val currentGroupBookCount by currentGroupBookCountFlow.collectAsState(initial = 0)
val allGroupsBookCountFlow = remember {
appDb.bookDao.flowBookShelfByGroup(BookGroup.IdAll).map { it.size }
}
val allGroupsBookCount by allGroupsBookCountFlow.collectAsState(initial = 0)
val bookGroupStyle = BookshelfConfig.bookGroupStyle val bookGroupStyle = BookshelfConfig.bookGroupStyle
// 控制是否处于“文件夹列表”根视图,还是“文件夹内部”书籍视图 // 控制是否处于“文件夹列表”根视图,还是“文件夹内部”书籍视图
@@ -13,6 +13,8 @@ data class BookshelfUiState(
val allGroups: List<BookGroup> = emptyList(), val allGroups: List<BookGroup> = emptyList(),
val groupPreviews: Map<Long, List<BookShelfItem>> = emptyMap(), val groupPreviews: Map<Long, List<BookShelfItem>> = emptyMap(),
val groupBookCounts: Map<Long, Int> = emptyMap(), val groupBookCounts: Map<Long, Int> = emptyMap(),
val currentGroupBookCount: Int = 0,
val allBooksCount: Int = 0,
val selectedGroupIndex: Int = 0, val selectedGroupIndex: Int = 0,
val selectedGroupId: Long = BookGroup.IdAll, val selectedGroupId: Long = BookGroup.IdAll,
val loadingText: String? = null, val loadingText: String? = null,
@@ -1,5 +1,6 @@
package io.legado.app.ui.main.bookshelf package io.legado.app.ui.main.bookshelf
import kotlinx.coroutines.flow.flow
import android.app.Application import android.app.Application
import android.net.Uri import android.net.Uri
import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.snapshotFlow
@@ -69,7 +70,6 @@ import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onCompletion import kotlinx.coroutines.flow.onCompletion
@@ -134,7 +134,8 @@ class BookshelfViewModel(
private data class GroupPreviewState( private data class GroupPreviewState(
val previews: Map<Long, List<BookShelfItem>>, val previews: Map<Long, List<BookShelfItem>>,
val counts: Map<Long, Int> val counts: Map<Long, Int>,
val allBookCount: Int
) )
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
@@ -206,9 +207,9 @@ class BookshelfViewModel(
} }
previews[group.groupId] = result previews[group.groupId] = result
} }
GroupPreviewState(previews, counts) GroupPreviewState(previews, counts, allBooks.size)
} else { } else {
GroupPreviewState(emptyMap(), emptyMap()) GroupPreviewState(emptyMap(), emptyMap(), allBooks.size)
} }
}.distinctUntilChanged().flowOn(Dispatchers.Default) }.distinctUntilChanged().flowOn(Dispatchers.Default)
@@ -257,6 +258,8 @@ class BookshelfViewModel(
allGroups = allGroups, allGroups = allGroups,
groupPreviews = previews.previews, groupPreviews = previews.previews,
groupBookCounts = previews.counts, groupBookCounts = previews.counts,
currentGroupBookCount = books.size,
allBooksCount = previews.allBookCount,
selectedGroupIndex = groups.indexOfFirst { it.groupId == internal.groupId } selectedGroupIndex = groups.indexOfFirst { it.groupId == internal.groupId }
.coerceAtLeast(0), .coerceAtLeast(0),
selectedGroupId = internal.groupId, selectedGroupId = internal.groupId,
@@ -393,7 +396,6 @@ class BookshelfViewModel(
if (bookUrls.isEmpty()) return if (bookUrls.isEmpty()) return
execute { execute {
batchCacheDownloadUseCase.execute( batchCacheDownloadUseCase.execute(
context = context,
bookUrls = bookUrls, bookUrls = bookUrls,
downloadAllChapters = downloadAllChapters, downloadAllChapters = downloadAllChapters,
skipAudioBooks = true skipAudioBooks = true
@@ -51,7 +51,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.legado.app.R import io.legado.app.R
import io.legado.app.data.entities.BookSourcePart import io.legado.app.data.entities.BookSourcePart
import io.legado.app.domain.usecase.ExploreKindUiUseCase import io.legado.app.ui.widget.components.explore.ExploreKindUiUseCase
import io.legado.app.ui.book.explore.ExploreShowActivity import io.legado.app.ui.book.explore.ExploreShowActivity
import io.legado.app.ui.book.search.SearchActivity import io.legado.app.ui.book.search.SearchActivity
import io.legado.app.ui.book.search.SearchScope import io.legado.app.ui.book.search.SearchScope
@@ -67,7 +67,6 @@ import com.script.rhino.runScriptWithContext
import io.legado.app.R import io.legado.app.R
import io.legado.app.constant.AppConst import io.legado.app.constant.AppConst
import io.legado.app.constant.AppLog import io.legado.app.constant.AppLog
import io.legado.app.help.config.AppConfig
import io.legado.app.help.http.CookieManager import io.legado.app.help.http.CookieManager
import io.legado.app.lib.dialogs.SelectItem import io.legado.app.lib.dialogs.SelectItem
import io.legado.app.lib.dialogs.selector import io.legado.app.lib.dialogs.selector
@@ -1,11 +1,11 @@
package io.legado.app.ui.welcome package io.legado.app.ui.welcome
import io.legado.app.ui.config.otherConfig.OtherConfig
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
import io.legado.app.R import io.legado.app.R
import io.legado.app.base.BaseFragment import io.legado.app.base.BaseFragment
import io.legado.app.databinding.FragmentBookFolderBinding import io.legado.app.databinding.FragmentBookFolderBinding
import io.legado.app.help.config.AppConfig
import io.legado.app.ui.file.HandleFileContract import io.legado.app.ui.file.HandleFileContract
import io.legado.app.utils.viewbindingdelegate.viewBinding import io.legado.app.utils.viewbindingdelegate.viewBinding
@@ -15,7 +15,7 @@ class BookFolderFragment : BaseFragment(R.layout.fragment_book_folder) {
private val selectBookFolder = registerForActivityResult(HandleFileContract()) { result -> private val selectBookFolder = registerForActivityResult(HandleFileContract()) { result ->
result.uri?.let { treeUri -> result.uri?.let { treeUri ->
AppConfig.defaultBookTreeUri = treeUri.toString() OtherConfig.defaultBookTreeUri = treeUri.toString()
updatePathText() updatePathText()
} }
} }
@@ -32,6 +32,6 @@ class BookFolderFragment : BaseFragment(R.layout.fragment_book_folder) {
private fun updatePathText() { private fun updatePathText() {
binding.tvFolderPath.text = binding.tvFolderPath.text =
AppConfig.defaultBookTreeUri ?: getString(R.string.welcome_book_folder_not_selected) OtherConfig.defaultBookTreeUri ?: getString(R.string.welcome_book_folder_not_selected)
} }
} }
@@ -16,9 +16,8 @@ import io.legado.app.base.BaseFragment
import io.legado.app.constant.AppLog import io.legado.app.constant.AppLog
import io.legado.app.constant.PreferKey import io.legado.app.constant.PreferKey
import io.legado.app.databinding.FragmentWebdavAuthBinding import io.legado.app.databinding.FragmentWebdavAuthBinding
import io.legado.app.domain.usecase.WebDavBackupUseCase
import io.legado.app.exception.NoStackTraceException import io.legado.app.exception.NoStackTraceException
import io.legado.app.help.AppWebDav
import io.legado.app.help.AppWebDav.testWebDav
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.help.storage.Restore import io.legado.app.help.storage.Restore
@@ -35,11 +34,13 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.ensureActive import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koin.android.ext.android.inject
import splitties.init.appCtx import splitties.init.appCtx
import kotlin.coroutines.coroutineContext import kotlin.coroutines.coroutineContext
class WebDavFragment : BaseFragment(R.layout.fragment_webdav_auth) { class WebDavFragment : BaseFragment(R.layout.fragment_webdav_auth) {
private val webDavBackupUseCase by inject<WebDavBackupUseCase>()
private var _binding: FragmentWebdavAuthBinding? = null private var _binding: FragmentWebdavAuthBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
private var restoreJob: Job? = null private var restoreJob: Job? = null
@@ -105,7 +106,7 @@ class WebDavFragment : BaseFragment(R.layout.fragment_webdav_auth) {
) )
saveWebDavConfig(config) saveWebDavConfig(config)
lifecycleScope.launch { lifecycleScope.launch {
testWebDav() webDavBackupUseCase.test()
} }
} }
@@ -120,7 +121,7 @@ class WebDavFragment : BaseFragment(R.layout.fragment_webdav_auth) {
binding.progressRestore.visible() binding.progressRestore.visible()
binding.btnRestore.text = "" binding.btnRestore.text = ""
saveWebDavConfig(config) saveWebDavConfig(config)
AppWebDav.upConfig() webDavBackupUseCase.refreshConfig()
restore() restore()
} finally { } finally {
binding.progressRestore.gone() binding.progressRestore.gone()
@@ -169,8 +170,8 @@ class WebDavFragment : BaseFragment(R.layout.fragment_webdav_auth) {
} }
private suspend fun showRestoreDialog(context: Context) { private suspend fun showRestoreDialog(context: Context) {
val names = withContext(IO) { AppWebDav.getBackupNames() } val names = withContext(IO) { webDavBackupUseCase.getBackupNames() }
if (AppWebDav.isJianGuoYun && names.size > 700) { if (webDavBackupUseCase.isJianGuoYun && names.size > 700) {
context.toastOnUi("由于坚果云限制列出文件数量,部分备份可能未显示,请及时清理旧备份") context.toastOnUi("由于坚果云限制列出文件数量,部分备份可能未显示,请及时清理旧备份")
} }
if (names.isNotEmpty()) { if (names.isNotEmpty()) {
@@ -194,7 +195,7 @@ class WebDavFragment : BaseFragment(R.layout.fragment_webdav_auth) {
waitDialog.setText("恢复中…") waitDialog.setText("恢复中…")
waitDialog.show() waitDialog.show()
val task = Coroutine.async { val task = Coroutine.async {
AppWebDav.restoreWebDav(name) webDavBackupUseCase.restore(name)
}.onError { }.onError {
AppLog.put("WebDav恢复出错\n${it.localizedMessage}", it) AppLog.put("WebDav恢复出错\n${it.localizedMessage}", it)
appCtx.toastOnUi("WebDav恢复出错\n${it.localizedMessage}") appCtx.toastOnUi("WebDav恢复出错\n${it.localizedMessage}")
@@ -32,7 +32,7 @@ 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.data.entities.SearchBook import io.legado.app.data.entities.SearchBook
import io.legado.app.model.BookShelfState import io.legado.app.domain.model.BookShelfState
import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.widget.components.cover.Cover import io.legado.app.ui.widget.components.cover.Cover
import io.legado.app.ui.widget.components.text.AppText import io.legado.app.ui.widget.components.text.AppText
@@ -46,8 +46,22 @@ fun SearchBookListItem(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val currentShelfState by shelfState.collectAsState(initial = BookShelfState.NOT_IN_SHELF) val currentShelfState by shelfState.collectAsState(initial = BookShelfState.NOT_IN_SHELF)
SearchBookListItem(
book = book,
shelfState = currentShelfState,
onClick = onClick,
modifier = modifier,
)
}
val badgeContent: (@Composable RowScope.() -> Unit)? = when (currentShelfState) { @Composable
fun SearchBookListItem(
book: SearchBook,
shelfState: BookShelfState,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val badgeContent: (@Composable RowScope.() -> Unit)? = when (shelfState) {
BookShelfState.IN_SHELF -> { BookShelfState.IN_SHELF -> {
{ {
androidx.compose.material3.Icon( androidx.compose.material3.Icon(
@@ -158,8 +172,22 @@ fun SearchBookGridItem(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val currentShelfState by shelfState.collectAsState(initial = BookShelfState.NOT_IN_SHELF) val currentShelfState by shelfState.collectAsState(initial = BookShelfState.NOT_IN_SHELF)
SearchBookGridItem(
book = book,
shelfState = currentShelfState,
onClick = onClick,
modifier = modifier,
)
}
val badgeText: String? = when (currentShelfState) { @Composable
fun SearchBookGridItem(
book: SearchBook,
shelfState: BookShelfState,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val badgeText: String? = when (shelfState) {
BookShelfState.IN_SHELF -> "已在书架" BookShelfState.IN_SHELF -> "已在书架"
BookShelfState.SAME_NAME_AUTHOR -> "同名书籍" BookShelfState.SAME_NAME_AUTHOR -> "同名书籍"
BookShelfState.NOT_IN_SHELF -> null BookShelfState.NOT_IN_SHELF -> null
@@ -37,7 +37,7 @@ import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import io.legado.app.App import io.legado.app.App
import io.legado.app.data.entities.rule.ExploreKind import io.legado.app.data.entities.rule.ExploreKind
import io.legado.app.domain.usecase.ExploreKindUiUseCase import io.legado.app.ui.widget.components.explore.ExploreKindUiUseCase
import io.legado.app.help.source.getExploreInfoMap import io.legado.app.help.source.getExploreInfoMap
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
@@ -1,4 +1,4 @@
package io.legado.app.domain.usecase package io.legado.app.ui.widget.components.explore
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import com.script.rhino.runScriptWithContext import com.script.rhino.runScriptWithContext