diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..9bef00426 --- /dev/null +++ b/AGENTS.md @@ -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. diff --git a/app/src/main/java/io/legado/app/data/repository/AppStartupRepository.kt b/app/src/main/java/io/legado/app/data/repository/AppStartupRepository.kt new file mode 100644 index 000000000..ca1e54878 --- /dev/null +++ b/app/src/main/java/io/legado/app/data/repository/AppStartupRepository.kt @@ -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()) + } + } +} diff --git a/app/src/main/java/io/legado/app/data/repository/BookCacheCleanupRepository.kt b/app/src/main/java/io/legado/app/data/repository/BookCacheCleanupRepository.kt new file mode 100644 index 000000000..8b6b388f3 --- /dev/null +++ b/app/src/main/java/io/legado/app/data/repository/BookCacheCleanupRepository.kt @@ -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 + } +} diff --git a/app/src/main/java/io/legado/app/data/repository/BookDomainRepositoryImpl.kt b/app/src/main/java/io/legado/app/data/repository/BookDomainRepositoryImpl.kt new file mode 100644 index 000000000..ee9988880 --- /dev/null +++ b/app/src/main/java/io/legado/app/data/repository/BookDomainRepositoryImpl.kt @@ -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): List { + if (bookUrls.isEmpty()) return emptyList() + return bookUrls.mapNotNull { bookDao.getBook(it) } + } + + override suspend fun getCacheableBooks(bookUrls: Set): List { + 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): List { + return getBooks(bookUrls).map { book -> + DeletableBook( + bookUrl = book.bookUrl, + origin = book.origin, + isLocal = book.isLocal + ) + } + } + + override suspend fun getBookGroupAssignments(bookUrls: Set): List { + return getBooks(bookUrls).map { book -> + BookGroupAssignment( + bookUrl = book.bookUrl, + group = book.group + ) + } + } + + override suspend fun updateBookGroups(assignments: List) { + 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) { + val books = getBooks(bookUrls) + if (books.isNotEmpty()) { + bookDao.delete(*books.toTypedArray()) + } + } + + override suspend fun deleteChaptersByBook(bookUrl: String) { + bookChapterDao.delByBook(bookUrl) + } +} diff --git a/app/src/main/java/io/legado/app/data/repository/BookSourceCallbackRepository.kt b/app/src/main/java/io/legado/app/data/repository/BookSourceCallbackRepository.kt new file mode 100644 index 000000000..d849ba26f --- /dev/null +++ b/app/src/main/java/io/legado/app/data/repository/BookSourceCallbackRepository.kt @@ -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) + } +} diff --git a/app/src/main/java/io/legado/app/data/repository/CacheBookDownloadRepository.kt b/app/src/main/java/io/legado/app/data/repository/CacheBookDownloadRepository.kt new file mode 100644 index 000000000..ca83ce2c0 --- /dev/null +++ b/app/src/main/java/io/legado/app/data/repository/CacheBookDownloadRepository.kt @@ -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) { + val book = bookDao.getBook(bookUrl) ?: return + CacheBook.start(appCtx, book, chapterIndices) + } +} diff --git a/app/src/main/java/io/legado/app/data/repository/DatabaseMaintenanceRepository.kt b/app/src/main/java/io/legado/app/data/repository/DatabaseMaintenanceRepository.kt new file mode 100644 index 000000000..c928af0e3 --- /dev/null +++ b/app/src/main/java/io/legado/app/data/repository/DatabaseMaintenanceRepository.kt @@ -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") + } +} diff --git a/app/src/main/java/io/legado/app/data/repository/DictRuleRepository.kt b/app/src/main/java/io/legado/app/data/repository/DictRuleRepository.kt index 66fa6ad0d..e7b07d9a6 100644 --- a/app/src/main/java/io/legado/app/data/repository/DictRuleRepository.kt +++ b/app/src/main/java/io/legado/app/data/repository/DictRuleRepository.kt @@ -22,6 +22,10 @@ class DictRuleRepository { return dao.all } + fun getEnabled(): List { + return dao.enabled + } + suspend fun insert(vararg rule: DictRule) { withContext(Dispatchers.IO) { dao.insert(*rule) diff --git a/app/src/main/java/io/legado/app/data/repository/LocalBookRepository.kt b/app/src/main/java/io/legado/app/data/repository/LocalBookRepository.kt new file mode 100644 index 000000000..3ad3fa67c --- /dev/null +++ b/app/src/main/java/io/legado/app/data/repository/LocalBookRepository.kt @@ -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) + } +} diff --git a/app/src/main/java/io/legado/app/data/repository/RemoteBookRepository.kt b/app/src/main/java/io/legado/app/data/repository/RemoteBookRepository.kt index 1fdb57b7e..046328928 100644 --- a/app/src/main/java/io/legado/app/data/repository/RemoteBookRepository.kt +++ b/app/src/main/java/io/legado/app/data/repository/RemoteBookRepository.kt @@ -4,6 +4,7 @@ import io.legado.app.constant.AppConst import io.legado.app.constant.BookType import io.legado.app.data.AppDatabase import io.legado.app.data.entities.Book +import io.legado.app.data.entities.Server import io.legado.app.exception.NoStackTraceException import io.legado.app.help.AppWebDav import io.legado.app.help.book.getRemoteUrl @@ -90,6 +91,10 @@ class RemoteBookRepository( } } + fun getDefaultBookWebDav(): RemoteBookWebDav? { + return AppWebDav.defaultBookWebDav + } + suspend fun loadBooks( webDav: RemoteBookWebDav, path: String? @@ -120,4 +125,20 @@ class RemoteBookRepository( return appDb.bookDao.flowLocal() } + fun flowServers(): Flow> { + 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) + } + } diff --git a/app/src/main/java/io/legado/app/data/repository/SearchRepository.kt b/app/src/main/java/io/legado/app/data/repository/SearchRepository.kt index a84b56771..a8ce16ec9 100644 --- a/app/src/main/java/io/legado/app/data/repository/SearchRepository.kt +++ b/app/src/main/java/io/legado/app/data/repository/SearchRepository.kt @@ -4,9 +4,9 @@ import io.legado.app.data.AppDatabase import io.legado.app.data.entities.BookSourcePart import io.legado.app.data.entities.SearchBook 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.model.webBook.SearchModel -import io.legado.app.ui.book.search.BookKey import io.legado.app.ui.book.search.SearchScope import io.legado.app.ui.main.bookshelf.BookShelfItem import kotlinx.coroutines.CoroutineScope @@ -23,7 +23,7 @@ import kotlinx.coroutines.withContext interface SearchRepository { val enabledGroups: Flow> val enabledSources: Flow> - val bookshelfKeys: Flow> + val bookshelfKeys: Flow> fun searchBookshelf(query: String): Flow> fun searchHistory(query: String): Flow> @@ -68,9 +68,9 @@ class SearchRepositoryImpl( override val enabledGroups: Flow> = appDb.bookSourceDao.flowEnabledGroups() override val enabledSources: Flow> = appDb.bookSourceDao.flowEnabled() - override val bookshelfKeys: Flow> = appDb.bookDao.flowBookShelf().map { books -> + override val bookshelfKeys: Flow> = appDb.bookDao.flowBookShelf().map { books -> books.filterNot { it.isNotShelf } - .map { book -> BookKey(book.name, book.author, book.bookUrl) } + .map { book -> BookShelfKey(book.name, book.author, book.bookUrl) } .toSet() } diff --git a/app/src/main/java/io/legado/app/data/repository/WebDavBackupRepository.kt b/app/src/main/java/io/legado/app/data/repository/WebDavBackupRepository.kt new file mode 100644 index 000000000..568a5a59f --- /dev/null +++ b/app/src/main/java/io/legado/app/data/repository/WebDavBackupRepository.kt @@ -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 { + 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) + } +} diff --git a/app/src/main/java/io/legado/app/data/repository/WebDavReadingProgressRepository.kt b/app/src/main/java/io/legado/app/data/repository/WebDavReadingProgressRepository.kt new file mode 100644 index 000000000..fb5a2c411 --- /dev/null +++ b/app/src/main/java/io/legado/app/data/repository/WebDavReadingProgressRepository.kt @@ -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 + } +} diff --git a/app/src/main/java/io/legado/app/di/appModule.kt b/app/src/main/java/io/legado/app/di/appModule.kt index 5f7b02a39..1c9fa12f0 100644 --- a/app/src/main/java/io/legado/app/di/appModule.kt +++ b/app/src/main/java/io/legado/app/di/appModule.kt @@ -6,22 +6,49 @@ import coil.decode.GifDecoder import coil.decode.ImageDecoderDecoder import coil.decode.SvgDecoder 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.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.DictRuleRepository import io.legado.app.data.repository.ExploreRepository 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.RemoteBookRepository import io.legado.app.data.repository.SearchRepository import io.legado.app.data.repository.SearchRepositoryImpl import io.legado.app.data.repository.SearchContentRepository 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.CacheBookChaptersUseCase import io.legado.app.domain.usecase.ClearBookCacheUseCase 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.UploadReadingProgressUseCase +import io.legado.app.domain.usecase.WebDavBackupUseCase import io.legado.app.help.coil.CoverFetcher import io.legado.app.help.coil.CoverInterceptor 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.import.local.ImportBookViewModel 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.readRecord.ReadRecordViewModel 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.readConfig.ReadConfigViewModel 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.main.MainViewModel import io.legado.app.ui.main.bookshelf.BookshelfViewModel @@ -76,16 +106,34 @@ val appModule = module { singleOf(::ReadRecordRepository) singleOf(::BookRepository) singleOf(::BookGroupRepository) + singleOf(::DictRuleRepository) singleOf(::SearchContentRepository) singleOf(::RemoteBookRepository) singleOf(::ExploreKindUiUseCase) + singleOf(::AppStartupMaintenanceUseCase) singleOf(::BatchCacheDownloadUseCase) + singleOf(::CacheBookChaptersUseCase) singleOf(::ClearBookCacheUseCase) singleOf(::DeleteBooksUseCase) + singleOf(::GetReadingProgressUseCase) + singleOf(::RemoveBookGroupAssignmentUseCase) singleOf(::UpdateBooksGroupUseCase) + singleOf(::UploadReadingProgressUseCase) + singleOf(::ResolveBookShelfStateUseCase) + singleOf(::ShrinkDatabaseUseCase) + singleOf(::WebDavBackupUseCase) singleOf(::CacheConfig) single { DirectLinkUploadRepository() } + single { AppStartupRepository(get()) } + single { CacheBookDownloadRepository(get()) } + single { BookCacheCleanupRepository(get()) } + single { BookSourceCallbackRepository(get(), get()) } + single { LocalBookRepository(get()) } + single { DatabaseMaintenanceRepository(get()) } + single { WebDavBackupRepository() } + single { WebDavReadingProgressRepository() } + single { BookDomainRepositoryImpl(get(), get()) } single { ExploreRepositoryImpl(get()) } single { SearchRepositoryImpl(get()) } @@ -106,6 +154,7 @@ val appModule = module { } viewModelOf(::DictRuleViewModel) + viewModelOf(::DictViewModel) viewModelOf(::RssSourceViewModel) viewModelOf(::RssSortViewModel) viewModelOf(::RssArticlesViewModel) @@ -127,6 +176,8 @@ val appModule = module { viewModelOf(::TocViewModel) viewModelOf(::ImportBookViewModel) viewModelOf(::RemoteBookViewModel) + viewModelOf(::ServerConfigViewModel) + viewModelOf(::ServersViewModel) viewModelOf(::BookInfoViewModel) viewModelOf(::ChangeCoverViewModel) viewModelOf(::ChangeBookSourceComposeViewModel) @@ -142,6 +193,7 @@ val appModule = module { bookChapterDao = get(), cacheConfig = get(), batchCacheDownloadUseCase = get(), + cacheBookChaptersUseCase = get(), clearBookCacheUseCase = get(), deleteBooksUseCase = get(), updateBooksGroupUseCase = get() diff --git a/app/src/main/java/io/legado/app/domain/gateway/AppStartupGateway.kt b/app/src/main/java/io/legado/app/domain/gateway/AppStartupGateway.kt new file mode 100644 index 000000000..206699512 --- /dev/null +++ b/app/src/main/java/io/legado/app/domain/gateway/AppStartupGateway.kt @@ -0,0 +1,6 @@ +package io.legado.app.domain.gateway + +interface AppStartupGateway { + suspend fun deleteNotShelfBooks() + suspend fun ensureDefaultHttpTts() +} diff --git a/app/src/main/java/io/legado/app/domain/gateway/BookCacheCleanupGateway.kt b/app/src/main/java/io/legado/app/domain/gateway/BookCacheCleanupGateway.kt new file mode 100644 index 000000000..94f5ec624 --- /dev/null +++ b/app/src/main/java/io/legado/app/domain/gateway/BookCacheCleanupGateway.kt @@ -0,0 +1,6 @@ +package io.legado.app.domain.gateway + +interface BookCacheCleanupGateway { + fun clearAll() + suspend fun clear(bookUrl: String): Boolean +} diff --git a/app/src/main/java/io/legado/app/domain/gateway/BookCacheDownloadGateway.kt b/app/src/main/java/io/legado/app/domain/gateway/BookCacheDownloadGateway.kt new file mode 100644 index 000000000..86d813917 --- /dev/null +++ b/app/src/main/java/io/legado/app/domain/gateway/BookCacheDownloadGateway.kt @@ -0,0 +1,5 @@ +package io.legado.app.domain.gateway + +interface BookCacheDownloadGateway { + suspend fun start(bookUrl: String, chapterIndices: List) +} diff --git a/app/src/main/java/io/legado/app/domain/gateway/BookSourceCallbackGateway.kt b/app/src/main/java/io/legado/app/domain/gateway/BookSourceCallbackGateway.kt new file mode 100644 index 000000000..c69a5e745 --- /dev/null +++ b/app/src/main/java/io/legado/app/domain/gateway/BookSourceCallbackGateway.kt @@ -0,0 +1,5 @@ +package io.legado.app.domain.gateway + +interface BookSourceCallbackGateway { + suspend fun onDeleteFromShelf(bookUrl: String) +} diff --git a/app/src/main/java/io/legado/app/domain/gateway/DatabaseMaintenanceGateway.kt b/app/src/main/java/io/legado/app/domain/gateway/DatabaseMaintenanceGateway.kt new file mode 100644 index 000000000..e16f03465 --- /dev/null +++ b/app/src/main/java/io/legado/app/domain/gateway/DatabaseMaintenanceGateway.kt @@ -0,0 +1,5 @@ +package io.legado.app.domain.gateway + +interface DatabaseMaintenanceGateway { + fun shrink() +} diff --git a/app/src/main/java/io/legado/app/domain/gateway/LocalBookGateway.kt b/app/src/main/java/io/legado/app/domain/gateway/LocalBookGateway.kt new file mode 100644 index 000000000..37d3ba213 --- /dev/null +++ b/app/src/main/java/io/legado/app/domain/gateway/LocalBookGateway.kt @@ -0,0 +1,5 @@ +package io.legado.app.domain.gateway + +interface LocalBookGateway { + suspend fun deleteBook(bookUrl: String, deleteOriginal: Boolean) +} diff --git a/app/src/main/java/io/legado/app/domain/gateway/ReadingProgressGateway.kt b/app/src/main/java/io/legado/app/domain/gateway/ReadingProgressGateway.kt new file mode 100644 index 000000000..ef2698f9f --- /dev/null +++ b/app/src/main/java/io/legado/app/domain/gateway/ReadingProgressGateway.kt @@ -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? +} diff --git a/app/src/main/java/io/legado/app/domain/gateway/WebDavBackupGateway.kt b/app/src/main/java/io/legado/app/domain/gateway/WebDavBackupGateway.kt new file mode 100644 index 000000000..1ecca2c41 --- /dev/null +++ b/app/src/main/java/io/legado/app/domain/gateway/WebDavBackupGateway.kt @@ -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 + suspend fun getLatestBackup(): WebDavBackup? + suspend fun restore(name: String) +} diff --git a/app/src/main/java/io/legado/app/domain/model/BookGroupAssignment.kt b/app/src/main/java/io/legado/app/domain/model/BookGroupAssignment.kt new file mode 100644 index 000000000..3fcf0f44e --- /dev/null +++ b/app/src/main/java/io/legado/app/domain/model/BookGroupAssignment.kt @@ -0,0 +1,6 @@ +package io.legado.app.domain.model + +data class BookGroupAssignment( + val bookUrl: String, + val group: Long +) diff --git a/app/src/main/java/io/legado/app/model/BookShelfState.kt b/app/src/main/java/io/legado/app/domain/model/BookShelfState.kt similarity index 68% rename from app/src/main/java/io/legado/app/model/BookShelfState.kt rename to app/src/main/java/io/legado/app/domain/model/BookShelfState.kt index e7ab2a7ac..38698b266 100644 --- a/app/src/main/java/io/legado/app/model/BookShelfState.kt +++ b/app/src/main/java/io/legado/app/domain/model/BookShelfState.kt @@ -1,7 +1,7 @@ -package io.legado.app.model +package io.legado.app.domain.model enum class BookShelfState { IN_SHELF, SAME_NAME_AUTHOR, NOT_IN_SHELF -} \ No newline at end of file +} diff --git a/app/src/main/java/io/legado/app/domain/model/CacheableBook.kt b/app/src/main/java/io/legado/app/domain/model/CacheableBook.kt new file mode 100644 index 000000000..2d7b1b236 --- /dev/null +++ b/app/src/main/java/io/legado/app/domain/model/CacheableBook.kt @@ -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 +) diff --git a/app/src/main/java/io/legado/app/domain/model/DeletableBook.kt b/app/src/main/java/io/legado/app/domain/model/DeletableBook.kt new file mode 100644 index 000000000..f6dc37b13 --- /dev/null +++ b/app/src/main/java/io/legado/app/domain/model/DeletableBook.kt @@ -0,0 +1,7 @@ +package io.legado.app.domain.model + +data class DeletableBook( + val bookUrl: String, + val origin: String, + val isLocal: Boolean +) diff --git a/app/src/main/java/io/legado/app/domain/model/ReadingProgress.kt b/app/src/main/java/io/legado/app/domain/model/ReadingProgress.kt new file mode 100644 index 000000000..a236af950 --- /dev/null +++ b/app/src/main/java/io/legado/app/domain/model/ReadingProgress.kt @@ -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? +) diff --git a/app/src/main/java/io/legado/app/domain/model/WebDavBackup.kt b/app/src/main/java/io/legado/app/domain/model/WebDavBackup.kt new file mode 100644 index 000000000..6ebf4c0c4 --- /dev/null +++ b/app/src/main/java/io/legado/app/domain/model/WebDavBackup.kt @@ -0,0 +1,6 @@ +package io.legado.app.domain.model + +data class WebDavBackup( + val name: String, + val lastModify: Long +) diff --git a/app/src/main/java/io/legado/app/domain/repository/BookDomainRepository.kt b/app/src/main/java/io/legado/app/domain/repository/BookDomainRepository.kt new file mode 100644 index 000000000..5c5ba5a4c --- /dev/null +++ b/app/src/main/java/io/legado/app/domain/repository/BookDomainRepository.kt @@ -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): List + suspend fun getDeletableBooks(bookUrls: Set): List + suspend fun getBookGroupAssignments(bookUrls: Set): List + suspend fun updateBookGroups(assignments: List) + suspend fun removeGroupFromBooks(groupId: Long) + suspend fun deleteBooks(bookUrls: Set) + suspend fun deleteChaptersByBook(bookUrl: String) +} diff --git a/app/src/main/java/io/legado/app/domain/usecase/AppStartupMaintenanceUseCase.kt b/app/src/main/java/io/legado/app/domain/usecase/AppStartupMaintenanceUseCase.kt new file mode 100644 index 000000000..fb809d85a --- /dev/null +++ b/app/src/main/java/io/legado/app/domain/usecase/AppStartupMaintenanceUseCase.kt @@ -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() + } +} diff --git a/app/src/main/java/io/legado/app/domain/usecase/BatchCacheDownloadUseCase.kt b/app/src/main/java/io/legado/app/domain/usecase/BatchCacheDownloadUseCase.kt index 7a2cb5e2e..7bdbd5930 100644 --- a/app/src/main/java/io/legado/app/domain/usecase/BatchCacheDownloadUseCase.kt +++ b/app/src/main/java/io/legado/app/domain/usecase/BatchCacheDownloadUseCase.kt @@ -1,63 +1,43 @@ package io.legado.app.domain.usecase -import android.content.Context -import io.legado.app.constant.BookType -import io.legado.app.data.dao.BookDao -import io.legado.app.data.entities.Book -import io.legado.app.model.CacheBook +import io.legado.app.domain.gateway.BookCacheDownloadGateway +import io.legado.app.domain.model.CacheableBook +import io.legado.app.domain.repository.BookDomainRepository class BatchCacheDownloadUseCase( - private val bookDao: BookDao + private val bookRepository: BookDomainRepository, + private val bookCacheDownloadGateway: BookCacheDownloadGateway ) { suspend fun execute( - context: Context, bookUrls: Set, downloadAllChapters: Boolean, skipAudioBooks: Boolean = false ): Int { if (bookUrls.isEmpty()) return 0 var count = 0 - bookUrls.forEach { bookUrl -> - val book = bookDao.getBook(bookUrl) ?: return@forEach - if (startIfNeeded(context, book, downloadAllChapters, skipAudioBooks)) { + bookRepository.getCacheableBooks(bookUrls).forEach { book -> + if (startIfNeeded(book, downloadAllChapters, skipAudioBooks)) { count++ } } return count } - fun execute( - context: Context, - books: List, - 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, + private suspend fun startIfNeeded( + book: CacheableBook, downloadAllChapters: Boolean, skipAudioBooks: Boolean ): Boolean { - if (book.type and BookType.local > 0) return false - if (skipAudioBooks && book.type and BookType.audio > 0) return false + if (book.isLocal) return false + if (skipAudioBooks && book.isAudio) return false val indices = if (downloadAllChapters) { (0..book.lastChapterIndex).toList() } else { (book.durChapterIndex..book.lastChapterIndex).toList() } if (indices.isEmpty()) return false - CacheBook.start(context, book, indices) + bookCacheDownloadGateway.start(book.bookUrl, indices) return true } } diff --git a/app/src/main/java/io/legado/app/domain/usecase/CacheBookChaptersUseCase.kt b/app/src/main/java/io/legado/app/domain/usecase/CacheBookChaptersUseCase.kt new file mode 100644 index 000000000..672accb4b --- /dev/null +++ b/app/src/main/java/io/legado/app/domain/usecase/CacheBookChaptersUseCase.kt @@ -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 { + val indices = chapterIndices.distinct() + if (indices.isEmpty()) return 0 + bookCacheDownloadGateway.start(bookUrl, indices) + return indices.size + } +} diff --git a/app/src/main/java/io/legado/app/domain/usecase/ClearBookCacheUseCase.kt b/app/src/main/java/io/legado/app/domain/usecase/ClearBookCacheUseCase.kt index 7882a24d5..a9d1eed5f 100644 --- a/app/src/main/java/io/legado/app/domain/usecase/ClearBookCacheUseCase.kt +++ b/app/src/main/java/io/legado/app/domain/usecase/ClearBookCacheUseCase.kt @@ -1,22 +1,21 @@ package io.legado.app.domain.usecase -import io.legado.app.data.dao.BookDao -import io.legado.app.data.entities.Book -import io.legado.app.help.book.BookHelp +import io.legado.app.domain.gateway.BookCacheCleanupGateway class ClearBookCacheUseCase( - private val bookDao: BookDao + private val bookCacheCleanupGateway: BookCacheCleanupGateway ) { - fun execute(book: Book): String { - BookHelp.clearCache(book) - return book.bookUrl + fun executeAll() { + bookCacheCleanupGateway.clearAll() + } + + suspend fun execute(bookUrl: String): String? { + return if (bookCacheCleanupGateway.clear(bookUrl)) bookUrl else null } suspend fun execute(bookUrls: Set): List { if (bookUrls.isEmpty()) return emptyList() - return bookUrls.mapNotNull { bookUrl -> - bookDao.getBook(bookUrl)?.let { execute(it) } - } + return bookUrls.mapNotNull { execute(it) } } } diff --git a/app/src/main/java/io/legado/app/domain/usecase/DeleteBooksUseCase.kt b/app/src/main/java/io/legado/app/domain/usecase/DeleteBooksUseCase.kt index ffe9d8cec..269b8ec22 100644 --- a/app/src/main/java/io/legado/app/domain/usecase/DeleteBooksUseCase.kt +++ b/app/src/main/java/io/legado/app/domain/usecase/DeleteBooksUseCase.kt @@ -1,33 +1,27 @@ package io.legado.app.domain.usecase -import io.legado.app.data.dao.BookChapterDao -import io.legado.app.data.dao.BookDao -import io.legado.app.data.dao.BookSourceDao -import io.legado.app.help.book.isLocal -import io.legado.app.model.SourceCallBack -import io.legado.app.model.localBook.LocalBook +import io.legado.app.domain.gateway.BookSourceCallbackGateway +import io.legado.app.domain.gateway.LocalBookGateway +import io.legado.app.domain.repository.BookDomainRepository class DeleteBooksUseCase( - private val bookDao: BookDao, - private val bookChapterDao: BookChapterDao, - private val bookSourceDao: BookSourceDao + private val bookRepository: BookDomainRepository, + private val localBookGateway: LocalBookGateway, + private val bookSourceCallbackGateway: BookSourceCallbackGateway ) { suspend fun execute(bookUrls: Set, deleteOriginal: Boolean): List { if (bookUrls.isEmpty()) return emptyList() - val books = bookUrls.mapNotNull { bookDao.getBook(it) } + val books = bookRepository.getDeletableBooks(bookUrls) books.forEach { book -> if (book.isLocal) { - LocalBook.deleteBook(book, deleteOriginal) + localBookGateway.deleteBook(book.bookUrl, deleteOriginal) } else { - val source = bookSourceDao.getBookSource(book.origin) - SourceCallBack.callBackBook(SourceCallBack.DEL_BOOK_SHELF, source, book) + bookSourceCallbackGateway.onDeleteFromShelf(book.bookUrl) } - bookChapterDao.delByBook(book.bookUrl) - } - if (books.isNotEmpty()) { - bookDao.delete(*books.toTypedArray()) + bookRepository.deleteChaptersByBook(book.bookUrl) } + bookRepository.deleteBooks(books.map { it.bookUrl }.toSet()) return books.map { it.bookUrl } } } diff --git a/app/src/main/java/io/legado/app/domain/usecase/GetReadingProgressUseCase.kt b/app/src/main/java/io/legado/app/domain/usecase/GetReadingProgressUseCase.kt new file mode 100644 index 000000000..f94259a6b --- /dev/null +++ b/app/src/main/java/io/legado/app/domain/usecase/GetReadingProgressUseCase.kt @@ -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) + } +} diff --git a/app/src/main/java/io/legado/app/domain/usecase/RemoveBookGroupAssignmentUseCase.kt b/app/src/main/java/io/legado/app/domain/usecase/RemoveBookGroupAssignmentUseCase.kt new file mode 100644 index 000000000..39225995f --- /dev/null +++ b/app/src/main/java/io/legado/app/domain/usecase/RemoveBookGroupAssignmentUseCase.kt @@ -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) + } +} diff --git a/app/src/main/java/io/legado/app/domain/usecase/ResolveBookShelfStateUseCase.kt b/app/src/main/java/io/legado/app/domain/usecase/ResolveBookShelfStateUseCase.kt new file mode 100644 index 000000000..08456fb06 --- /dev/null +++ b/app/src/main/java/io/legado/app/domain/usecase/ResolveBookShelfStateUseCase.kt @@ -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 + ): 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 + } +} diff --git a/app/src/main/java/io/legado/app/domain/usecase/ShrinkDatabaseUseCase.kt b/app/src/main/java/io/legado/app/domain/usecase/ShrinkDatabaseUseCase.kt new file mode 100644 index 000000000..05d13428f --- /dev/null +++ b/app/src/main/java/io/legado/app/domain/usecase/ShrinkDatabaseUseCase.kt @@ -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() + } +} diff --git a/app/src/main/java/io/legado/app/domain/usecase/UpdateBooksGroupUseCase.kt b/app/src/main/java/io/legado/app/domain/usecase/UpdateBooksGroupUseCase.kt index 67a81f735..3373fc9e4 100644 --- a/app/src/main/java/io/legado/app/domain/usecase/UpdateBooksGroupUseCase.kt +++ b/app/src/main/java/io/legado/app/domain/usecase/UpdateBooksGroupUseCase.kt @@ -1,9 +1,9 @@ package io.legado.app.domain.usecase -import io.legado.app.data.dao.BookDao +import io.legado.app.domain.repository.BookDomainRepository class UpdateBooksGroupUseCase( - private val bookDao: BookDao + private val bookRepository: BookDomainRepository ) { suspend fun replaceGroup(bookUrls: Set, groupId: Long) { @@ -12,18 +12,14 @@ class UpdateBooksGroupUseCase( suspend fun updateGroups(bookUrls: Set, transform: (Long) -> Long) { if (bookUrls.isEmpty()) return - val updateBooks = bookUrls.mapNotNull { bookUrl -> - bookDao.getBook(bookUrl)?.let { book -> - val targetGroup = transform(book.group) - if (targetGroup == book.group) { - null - } else { - book.copy(group = targetGroup) - } + val updateGroups = bookRepository.getBookGroupAssignments(bookUrls).mapNotNull { book -> + val targetGroup = transform(book.group) + if (targetGroup == book.group) { + null + } else { + book.copy(group = targetGroup) } } - if (updateBooks.isNotEmpty()) { - bookDao.update(*updateBooks.toTypedArray()) - } + bookRepository.updateBookGroups(updateGroups) } } diff --git a/app/src/main/java/io/legado/app/domain/usecase/UploadReadingProgressUseCase.kt b/app/src/main/java/io/legado/app/domain/usecase/UploadReadingProgressUseCase.kt new file mode 100644 index 000000000..c757a0f48 --- /dev/null +++ b/app/src/main/java/io/legado/app/domain/usecase/UploadReadingProgressUseCase.kt @@ -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) + } +} diff --git a/app/src/main/java/io/legado/app/domain/usecase/WebDavBackupUseCase.kt b/app/src/main/java/io/legado/app/domain/usecase/WebDavBackupUseCase.kt new file mode 100644 index 000000000..9681c94d3 --- /dev/null +++ b/app/src/main/java/io/legado/app/domain/usecase/WebDavBackupUseCase.kt @@ -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 { + webDavBackupGateway.syncConfig() + return webDavBackupGateway.getBackupNames() + } + + suspend fun getLatestBackup(): WebDavBackup? { + webDavBackupGateway.syncConfig() + return webDavBackupGateway.getLatestBackup() + } + + suspend fun restore(name: String) { + webDavBackupGateway.syncConfig() + webDavBackupGateway.restore(name) + } +} diff --git a/app/src/main/java/io/legado/app/help/AppWebDav.kt b/app/src/main/java/io/legado/app/help/AppWebDav.kt index 2aa968c7e..61b1f10b4 100644 --- a/app/src/main/java/io/legado/app/help/AppWebDav.kt +++ b/app/src/main/java/io/legado/app/help/AppWebDav.kt @@ -285,18 +285,23 @@ object AppWebDav { } } - suspend fun uploadBookProgress(bookProgress: BookProgress, onSuccess: (() -> Unit)? = null) { + suspend fun uploadBookProgress( + bookProgress: BookProgress, + onSuccess: (() -> Unit)? = null + ): Boolean { try { - val authorization = authorization ?: return - if (!AppConfig.syncBookProgress) return - if (!NetworkUtils.isAvailable()) return + val authorization = authorization ?: return false + if (!AppConfig.syncBookProgress) return false + if (!NetworkUtils.isAvailable()) return false val json = GSON.toJson(bookProgress) val url = getProgressUrl(bookProgress.name, bookProgress.author) WebDav(url, authorization).upload(json.toByteArray(), "application/json") onSuccess?.invoke() + return true } catch (e: Exception) { currentCoroutineContext().ensureActive() AppLog.put("上传进度失败\n${e.localizedMessage}", e) + return false } } @@ -312,7 +317,14 @@ object AppWebDav { * 获取书籍进度 */ 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 { val authorization = authorization ?: return null WebDav(url, authorization).download().let { byteArray -> @@ -364,4 +376,4 @@ object AppWebDav { } } -} \ No newline at end of file +} diff --git a/app/src/main/java/io/legado/app/help/book/BookExtensions.kt b/app/src/main/java/io/legado/app/help/book/BookExtensions.kt index cfad5af0d..c95b6778f 100644 --- a/app/src/main/java/io/legado/app/help/book/BookExtensions.kt +++ b/app/src/main/java/io/legado/app/help/book/BookExtensions.kt @@ -2,6 +2,7 @@ package io.legado.app.help.book +import io.legado.app.ui.config.otherConfig.OtherConfig import android.net.Uri import androidx.core.net.toUri import com.script.buildScriptBindings @@ -168,7 +169,7 @@ fun Book.getLocalUri(): Uri { return uri } //不同的设备书籍保存路径可能不一样, uri无效时尝试寻找当前保存路径下的文件 - val defaultBookDir = AppConfig.defaultBookTreeUri + val defaultBookDir = OtherConfig.defaultBookTreeUri val importBookDir = AppConfig.importBookPath // 查找书籍保存目录 @@ -240,7 +241,7 @@ fun Book.getLocalUri(): Uri { fun Book.getArchiveUri(): Uri? { - val defaultBookDir = AppConfig.defaultBookTreeUri + val defaultBookDir = OtherConfig.defaultBookTreeUri return if (isArchive && !defaultBookDir.isNullOrBlank()) { FileDoc.fromUri(defaultBookDir.toUri(), true) .find(archiveName)?.uri diff --git a/app/src/main/java/io/legado/app/help/config/AppConfig.kt b/app/src/main/java/io/legado/app/help/config/AppConfig.kt index 6fff1055b..f5a78fdb9 100644 --- a/app/src/main/java/io/legado/app/help/config/AppConfig.kt +++ b/app/src/main/java/io/legado/app/help/config/AppConfig.kt @@ -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 get() = appCtx.getPrefBoolean(PreferKey.showDiscovery, true) diff --git a/app/src/main/java/io/legado/app/model/localBook/LocalBook.kt b/app/src/main/java/io/legado/app/model/localBook/LocalBook.kt index 12149714a..2ca3965f1 100644 --- a/app/src/main/java/io/legado/app/model/localBook/LocalBook.kt +++ b/app/src/main/java/io/legado/app/model/localBook/LocalBook.kt @@ -1,5 +1,6 @@ package io.legado.app.model.localBook +import io.legado.app.ui.config.otherConfig.OtherConfig import android.net.Uri import android.util.Base64 import androidx.core.net.toUri @@ -401,7 +402,7 @@ object LocalBook { fileName: String, source: BaseSource? = null, ): Uri { - AppConfig.defaultBookTreeUri + OtherConfig.defaultBookTreeUri ?: throw NoBooksDirException() val inputStream = when { str.isAbsUrl() -> AnalyzeUrl( @@ -427,7 +428,7 @@ object LocalBook { fileName: String ): Uri { inputStream.use { - val defaultBookTreeUri = AppConfig.defaultBookTreeUri + val defaultBookTreeUri = OtherConfig.defaultBookTreeUri if (defaultBookTreeUri.isNullOrBlank()) throw NoBooksDirException() val treeUri = defaultBookTreeUri.toUri() return if (treeUri.isContentScheme()) { @@ -481,7 +482,7 @@ object LocalBook { val webDavUrl = localBook.getRemoteUrl() if (webDavUrl.isNullOrBlank()) throw NoStackTraceException("Book file is not webDav File") try { - AppConfig.defaultBookTreeUri + OtherConfig.defaultBookTreeUri ?: throw NoBooksDirException() // 兼容旧版链接 val webdav: WebDav = kotlin.runCatching { diff --git a/app/src/main/java/io/legado/app/model/remote/RemoteBookWebDav.kt b/app/src/main/java/io/legado/app/model/remote/RemoteBookWebDav.kt index 31f8fa48c..ad0c169da 100644 --- a/app/src/main/java/io/legado/app/model/remote/RemoteBookWebDav.kt +++ b/app/src/main/java/io/legado/app/model/remote/RemoteBookWebDav.kt @@ -1,5 +1,6 @@ package io.legado.app.model.remote +import io.legado.app.ui.config.otherConfig.OtherConfig import android.net.Uri import io.legado.app.constant.AppPattern.archiveFileRegex 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.exception.NoStackTraceException 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.WebDav import io.legado.app.lib.webdav.WebDavFile @@ -57,7 +57,7 @@ class RemoteBookWebDav( } override suspend fun downloadRemoteBook(remoteBook: RemoteBook): Uri { - AppConfig.defaultBookTreeUri + OtherConfig.defaultBookTreeUri ?: throw NoStackTraceException("没有设置书籍保存位置!") if (!NetworkUtils.isAvailable()) throw NoStackTraceException("网络不可用") val webdav = WebDav(remoteBook.path, authorization) diff --git a/app/src/main/java/io/legado/app/ui/association/FileAssociationActivity.kt b/app/src/main/java/io/legado/app/ui/association/FileAssociationActivity.kt index a4877fcfc..d688c079c 100644 --- a/app/src/main/java/io/legado/app/ui/association/FileAssociationActivity.kt +++ b/app/src/main/java/io/legado/app/ui/association/FileAssociationActivity.kt @@ -1,5 +1,6 @@ package io.legado.app.ui.association +import io.legado.app.ui.config.otherConfig.OtherConfig import android.net.Uri import android.os.Bundle import androidx.activity.viewModels @@ -11,7 +12,6 @@ import io.legado.app.base.VMBaseActivity import io.legado.app.constant.AppLog import io.legado.app.databinding.ActivityTranslucenceBinding 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.permission.Permissions import io.legado.app.lib.permission.PermissionsCompat @@ -43,7 +43,7 @@ class FileAssociationActivity : private val localBookTreeSelect = registerForActivityResult(HandleFileContract()) { intent.data?.let { uri -> it.uri?.let { treeUri -> - AppConfig.defaultBookTreeUri = treeUri.toString() + OtherConfig.defaultBookTreeUri = treeUri.toString() importBook(treeUri, uri) } ?: let { val storageHelp = String(assets.open("storageHelp.md").readBytes()) @@ -134,7 +134,7 @@ class FileAssociationActivity : private fun importBook(uri: Uri) { if (uri.isContentScheme()) { - val treeUriStr = AppConfig.defaultBookTreeUri + val treeUriStr = OtherConfig.defaultBookTreeUri if (treeUriStr.isNullOrEmpty()) { localBookTreeSelect.launch { title = getString(R.string.select_book_folder) diff --git a/app/src/main/java/io/legado/app/ui/book/cache/CacheScreen.kt b/app/src/main/java/io/legado/app/ui/book/cache/CacheScreen.kt index 55c6b4981..69324beee 100644 --- a/app/src/main/java/io/legado/app/ui/book/cache/CacheScreen.kt +++ b/app/src/main/java/io/legado/app/ui/book/cache/CacheScreen.kt @@ -1,5 +1,6 @@ package io.legado.app.ui.book.cache +import androidx.compose.ui.focus.focusRequester import androidx.activity.compose.BackHandler import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts @@ -50,7 +51,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.Alignment import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource 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.toastOnUi import io.legado.app.utils.verificationField -import io.legado.app.help.config.LocalConfig import io.legado.app.ui.theme.adaptiveHorizontalPadding import org.koin.androidx.compose.koinViewModel @@ -158,7 +157,7 @@ private fun CacheScreen( var isSearchMode by remember { mutableStateOf(false) } var searchKey by remember { mutableStateOf("") } var selectedBookUrls by remember { mutableStateOf>(emptySet()) } - var deleteOriginalBookFile by remember { mutableStateOf(LocalConfig.deleteBookOriginal) } + var deleteOriginalBookFile by remember { mutableStateOf(state.deleteBookOriginal) } val exportBookPathKey = remember { "exportBookPath" } val exportTypes = remember { arrayListOf("txt", "epub") } val focusRequester = remember { FocusRequester() } @@ -409,7 +408,7 @@ private fun CacheScreen( ) { if (selectedBookUrls.isNotEmpty()) { pendingDeleteBookUrls = selectedBookUrls - deleteOriginalBookFile = LocalConfig.deleteBookOriginal + deleteOriginalBookFile = state.deleteBookOriginal showDeleteBookConfirmDialog = true } } @@ -638,7 +637,7 @@ private fun CacheScreen( onClick = { toggleBookSelection(book) }, onLongClick = { toggleBookSelection(book) }, containerColor = if (isSelected) { - LegadoTheme.colorScheme.secondaryContainer + LegadoTheme.colorScheme.surfaceContainerHigh } else { LegadoTheme.colorScheme.surfaceContainerLow } @@ -727,7 +726,7 @@ private fun CacheScreen( text = "删除书籍", onClick = { pendingDeleteBookUrls = setOf(book.bookUrl) - deleteOriginalBookFile = LocalConfig.deleteBookOriginal + deleteOriginalBookFile = state.deleteBookOriginal showDeleteBookConfirmDialog = true dismiss() } @@ -806,7 +805,6 @@ private fun CacheScreen( confirmText = stringResource(android.R.string.ok), onConfirm = { showDeleteBookConfirmDialog = false - LocalConfig.deleteBookOriginal = deleteOriginalBookFile viewModel.dispatch( CacheIntent.DeleteBooks( bookUrls = pendingDeleteBookUrls, diff --git a/app/src/main/java/io/legado/app/ui/book/cache/CacheViewModel.kt b/app/src/main/java/io/legado/app/ui/book/cache/CacheViewModel.kt index 394f4c301..a58a6d24e 100644 --- a/app/src/main/java/io/legado/app/ui/book/cache/CacheViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/cache/CacheViewModel.kt @@ -10,6 +10,7 @@ import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookGroup 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.DeleteBooksUseCase 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.isLocal import io.legado.app.model.CacheBook +import io.legado.app.help.config.LocalConfig import io.legado.app.service.ExportBookService import io.legado.app.ui.config.cacheConfig.CacheConfig import io.legado.app.utils.cnCompare @@ -51,6 +53,7 @@ data class CacheUiState( val books: List = emptyList(), val isDownloadRunning: Boolean = false, val cacheVersion: Long = 0, + val deleteBookOriginal: Boolean = LocalConfig.deleteBookOriginal, val exportConfig: CacheExportConfig = CacheExportConfig() ) @@ -93,6 +96,7 @@ class CacheViewModel( private val bookChapterDao: BookChapterDao, val cacheConfig: CacheConfig, private val batchCacheDownloadUseCase: BatchCacheDownloadUseCase, + private val cacheBookChaptersUseCase: CacheBookChaptersUseCase, private val clearBookCacheUseCase: ClearBookCacheUseCase, private val deleteBooksUseCase: DeleteBooksUseCase, private val updateBooksGroupUseCase: UpdateBooksGroupUseCase @@ -335,8 +339,7 @@ class CacheViewModel( private fun startDownloadForVisibleBooks(books: List, downloadAllChapters: Boolean) { execute { batchCacheDownloadUseCase.execute( - context = context, - books = books, + bookUrls = books.map { it.bookUrl }.toSet(), downloadAllChapters = downloadAllChapters, skipAudioBooks = true ) @@ -349,10 +352,14 @@ class CacheViewModel( if (book.isLocal) return if (isBookDownloading(book.bookUrl)) { CacheBook.remove(context, book.bookUrl) + syncDownloadRunning() } else { - CacheBook.start(context, book, (0..book.lastChapterIndex).toList()) + execute { + cacheBookChaptersUseCase.execute(book.bookUrl, 0..book.lastChapterIndex) + }.onFinally { + syncDownloadRunning() + } } - syncDownloadRunning() } private fun moveBooksToGroup(bookUrls: Set, groupId: Long) { @@ -368,8 +375,10 @@ class CacheViewModel( private fun deleteBooks(bookUrls: Set, deleteOriginal: Boolean) { if (bookUrls.isEmpty()) return execute { + LocalConfig.deleteBookOriginal = deleteOriginal deleteBooksUseCase.execute(bookUrls, deleteOriginal) }.onSuccess { deletedBookUrls -> + _uiState.update { it.copy(deleteBookOriginal = deleteOriginal) } deletedBookUrls.forEach { cacheChapters.remove(it) } _effects.tryEmit(CacheEffect.ShowMessage("删除成功")) }.onError { @@ -396,7 +405,6 @@ class CacheViewModel( if (bookUrls.isEmpty()) return execute { batchCacheDownloadUseCase.execute( - context = context, bookUrls = bookUrls, downloadAllChapters = downloadAllChapters, skipAudioBooks = true @@ -414,9 +422,13 @@ class CacheViewModel( } private fun clearCacheForBook(book: Book) { - val bookUrl = clearBookCacheUseCase.execute(book) - cacheChapters[bookUrl] = hashSetOf() - emitBookChanged(bookUrl) + execute { + clearBookCacheUseCase.execute(book.bookUrl) + }.onSuccess { bookUrl -> + bookUrl ?: return@onSuccess + cacheChapters[bookUrl] = hashSetOf() + emitBookChanged(bookUrl) + } } private fun emitBookChanged(bookUrl: String) { diff --git a/app/src/main/java/io/legado/app/ui/book/explore/ExploreShowScreen.kt b/app/src/main/java/io/legado/app/ui/book/explore/ExploreShowScreen.kt index 79643842b..6052e3c94 100644 --- a/app/src/main/java/io/legado/app/ui/book/explore/ExploreShowScreen.kt +++ b/app/src/main/java/io/legado/app/ui/book/explore/ExploreShowScreen.kt @@ -61,8 +61,8 @@ import androidx.compose.ui.unit.dp import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import io.legado.app.data.entities.SearchBook -import io.legado.app.domain.usecase.ExploreKindUiUseCase -import io.legado.app.model.BookShelfState +import io.legado.app.ui.widget.components.explore.ExploreKindUiUseCase +import io.legado.app.domain.model.BookShelfState import io.legado.app.ui.theme.LegadoTheme import io.legado.app.ui.theme.ThemeResolver 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.GlassTopAppBarDefaults import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.Flow import org.koin.androidx.compose.koinViewModel import org.koin.compose.koinInject @@ -444,12 +443,12 @@ fun ExploreShowScreen( ) { items( items = books, - key = { it.bookUrl } - ) { book -> + key = { it.book.bookUrl } + ) { item -> ExploreBookGridItem( - book = book, - shelfState = viewModel.getBookShelfStateFlow(book), - onClick = { onBookClick(book) }, + book = item.book, + shelfState = item.shelfState, + onClick = { onBookClick(item.book) }, modifier = Modifier.animateItem() ) } @@ -476,12 +475,12 @@ fun ExploreShowScreen( ) { items( items = books, - key = { it.bookUrl } - ) { book -> + key = { it.book.bookUrl } + ) { item -> ExploreBookItem( - book = book, - shelfState = viewModel.getBookShelfStateFlow(book), - onClick = { onBookClick(book) }, + book = item.book, + shelfState = item.shelfState, + onClick = { onBookClick(item.book) }, modifier = Modifier.animateItem() ) } @@ -505,7 +504,7 @@ fun ExploreShowScreen( @Composable fun ExploreBookItem( book: SearchBook, - shelfState: Flow, + shelfState: BookShelfState, onClick: () -> Unit, modifier: Modifier = Modifier ) { @@ -521,7 +520,7 @@ fun ExploreBookItem( fun ExploreBookGridItem( book: SearchBook, onClick: () -> Unit, - shelfState: Flow, + shelfState: BookShelfState, modifier: Modifier = Modifier ) { SearchBookGridItem( diff --git a/app/src/main/java/io/legado/app/ui/book/explore/ExploreShowViewModel.kt b/app/src/main/java/io/legado/app/ui/book/explore/ExploreShowViewModel.kt index ab4368beb..3b099f6d7 100644 --- a/app/src/main/java/io/legado/app/ui/book/explore/ExploreShowViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/explore/ExploreShowViewModel.kt @@ -7,18 +7,16 @@ import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.SearchBook import io.legado.app.data.entities.rule.ExploreKind 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.model.BookShelfState -import io.legado.app.ui.book.search.BookKey +import io.legado.app.domain.model.BookShelfState import io.legado.app.utils.exploreLayoutGrid -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import splitties.init.appCtx @@ -46,8 +44,14 @@ sealed class UiState { data object Empty : UiState() } +data class ExploreBookItemUi( + val book: SearchBook, + val shelfState: BookShelfState = BookShelfState.NOT_IN_SHELF, +) + class ExploreShowViewModel( - private val repository: ExploreRepository + private val repository: ExploreRepository, + private val resolveBookShelfStateUseCase: ResolveBookShelfStateUseCase ) : ViewModel() { private val _rawBooks = MutableStateFlow>(emptyList()) @@ -60,7 +64,7 @@ class ExploreShowViewModel( private var exploreUrl: String? = null private var page = 1 private val _isEndStateFlow = MutableStateFlow(false) - private val _bookshelf = MutableStateFlow>(emptySet()) + private val _bookshelf = MutableStateFlow>(emptySet()) private val _kinds = MutableStateFlow>(emptyList()) val kinds = _kinds.asStateFlow() private val _selectedKindTitle = MutableStateFlow(null) @@ -85,6 +89,16 @@ class ExploreShowViewModel( ) { books, filter, bookshelf -> books.filter { item -> 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()) @@ -104,7 +118,7 @@ class ExploreShowViewModel( init { viewModelScope.launch { 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 } } @@ -199,11 +213,25 @@ class ExploreShowViewModel( } 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): Boolean { - val state = getBookShelfState(book, shelf) + private fun isBookValid( + book: SearchBook, + filter: BookFilterState, + shelf: Set + ): Boolean { + val state = resolveBookShelfStateUseCase.execute( + name = book.name, + author = book.author, + url = book.bookUrl, + shelf = shelf + ) return when (filter) { BookFilterState.SHOW_ALL -> true 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 } } - - fun getBookShelfStateFlow(item: SearchBook): Flow { - return _bookshelf.map { shelf -> - getBookShelfState(item, shelf) - }.distinctUntilChanged() - } - - private fun getBookShelfState(item: SearchBook, shelf: Set): 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 - } } diff --git a/app/src/main/java/io/legado/app/ui/book/group/GroupViewModel.kt b/app/src/main/java/io/legado/app/ui/book/group/GroupViewModel.kt index 4a279992f..831f29cfa 100644 --- a/app/src/main/java/io/legado/app/ui/book/group/GroupViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/group/GroupViewModel.kt @@ -2,13 +2,14 @@ package io.legado.app.ui.book.group import android.app.Application import io.legado.app.base.BaseViewModel -import io.legado.app.data.appDb import io.legado.app.data.entities.BookGroup import io.legado.app.data.repository.BookGroupRepository +import io.legado.app.domain.usecase.RemoveBookGroupAssignmentUseCase class GroupViewModel( application: Application, - private val bookGroupRepository: BookGroupRepository + private val bookGroupRepository: BookGroupRepository, + private val removeBookGroupAssignmentUseCase: RemoveBookGroupAssignmentUseCase ) : BaseViewModel(application) { fun upGroup(vararg bookGroup: BookGroup, finally: (() -> Unit)? = null) { @@ -36,7 +37,7 @@ class GroupViewModel( enableRefresh = enableRefresh, order = bookGroupRepository.getMaxOrder().plus(1) ) - bookGroupRepository.getByID(groupId) ?: appDb.bookDao.removeGroup(groupId) + bookGroupRepository.getByID(groupId) ?: removeBookGroupAssignmentUseCase.execute(groupId) bookGroupRepository.insert(bookGroup) }.onFinally { finally() @@ -46,7 +47,7 @@ class GroupViewModel( fun delGroup(bookGroup: BookGroup, finally: () -> Unit) { execute { bookGroupRepository.delete(bookGroup) - appDb.bookDao.removeGroup(bookGroup.groupId) + removeBookGroupAssignmentUseCase.execute(bookGroup.groupId) }.onFinally { finally() } diff --git a/app/src/main/java/io/legado/app/ui/book/import/local/ImportBookViewModel.kt b/app/src/main/java/io/legado/app/ui/book/import/local/ImportBookViewModel.kt index 6b11ba8be..839293d8e 100644 --- a/app/src/main/java/io/legado/app/ui/book/import/local/ImportBookViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/import/local/ImportBookViewModel.kt @@ -1,5 +1,6 @@ package io.legado.app.ui.book.import.local +import io.legado.app.ui.config.otherConfig.OtherConfig import android.app.Application import android.net.Uri import android.provider.DocumentsContract @@ -231,7 +232,7 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application) fun hasRootDoc(): Boolean = _state.value.rootDoc != null private fun initialize() { - val defaultPath = ImportBookConfig.defaultBookTreeUri?.takeIf { it.isUri() } + val defaultPath = OtherConfig.defaultBookTreeUri?.takeIf { it.isUri() } val effectiveDefaultPath = defaultPath ?: firstPersistedTreeUri()?.toString() if (effectiveDefaultPath.isNullOrBlank()) { _effects.tryEmit( @@ -239,8 +240,8 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application) ) return } - if (ImportBookConfig.defaultBookTreeUri != effectiveDefaultPath) { - ImportBookConfig.defaultBookTreeUri = effectiveDefaultPath + if (OtherConfig.defaultBookTreeUri != effectiveDefaultPath) { + OtherConfig.defaultBookTreeUri = effectiveDefaultPath } val importPath = ImportBookConfig.importBookPath if (importPath.isNullOrBlank() || !importPath.isUri()) { @@ -255,16 +256,16 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application) val pickedUri = persistFolderPermission(uri) when (target) { ImportFolderPickTarget.DEFAULT_BOOK -> { - ImportBookConfig.defaultBookTreeUri = pickedUri.toString() + OtherConfig.defaultBookTreeUri = pickedUri.toString() if (ImportBookConfig.importBookPath.isNullOrBlank()) { - ImportBookConfig.importBookPath = ImportBookConfig.defaultBookTreeUri + ImportBookConfig.importBookPath = OtherConfig.defaultBookTreeUri } } ImportFolderPickTarget.IMPORT_FOLDER -> { ImportBookConfig.importBookPath = pickedUri.toString() - if (ImportBookConfig.defaultBookTreeUri.isNullOrBlank()) { - ImportBookConfig.defaultBookTreeUri = pickedUri.toString() + if (OtherConfig.defaultBookTreeUri.isNullOrBlank()) { + OtherConfig.defaultBookTreeUri = pickedUri.toString() } } } @@ -302,14 +303,14 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application) ImportBookConfig.importBookPath ?.takeIf { it.isNotBlank() && it.isUri() } ?.let(::add) - ImportBookConfig.defaultBookTreeUri + OtherConfig.defaultBookTreeUri ?.takeIf { it.isNotBlank() && it.isUri() } ?.let(::add) } if (candidates.isEmpty()) { firstPersistedTreeUri()?.toString()?.let { persistedPath -> if (trySetRootDoc(persistedPath)) { - ImportBookConfig.defaultBookTreeUri = persistedPath + OtherConfig.defaultBookTreeUri = persistedPath ImportBookConfig.importBookPath = persistedPath return } @@ -341,7 +342,7 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application) firstPersistedTreeUri()?.toString()?.let { persistedPath -> if (trySetRootDoc(persistedPath)) { - ImportBookConfig.defaultBookTreeUri = persistedPath + OtherConfig.defaultBookTreeUri = persistedPath ImportBookConfig.importBookPath = persistedPath return } diff --git a/app/src/main/java/io/legado/app/ui/book/import/remote/RemoteBookScreen.kt b/app/src/main/java/io/legado/app/ui/book/import/remote/RemoteBookScreen.kt index 51db6d1b3..955f48572 100644 --- a/app/src/main/java/io/legado/app/ui/book/import/remote/RemoteBookScreen.kt +++ b/app/src/main/java/io/legado/app/ui/book/import/remote/RemoteBookScreen.kt @@ -62,12 +62,10 @@ import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.core.net.toUri import androidx.lifecycle.compose.collectAsStateWithLifecycle import io.legado.app.R import io.legado.app.constant.AppConst 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.selector 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.text.AppText import io.legado.app.utils.ConvertUtils -import io.legado.app.utils.isUri import io.legado.app.utils.startActivityForBook import io.legado.app.utils.toastOnUi import org.json.JSONObject @@ -218,10 +215,7 @@ fun RemoteBookScreen( when (effect) { is RemoteBookEffect.OpenBook -> context.startActivityForBook(effect.book) is RemoteBookEffect.RequestBookFolderPicker -> { - selectDocTree.launch( - effect.initialUri - ?: AppConfig.defaultBookTreeUri?.takeIf { it.isUri() }?.toUri() - ) + selectDocTree.launch(effect.initialUri) } is RemoteBookEffect.ShowArchiveEntries -> { diff --git a/app/src/main/java/io/legado/app/ui/book/import/remote/RemoteBookViewModel.kt b/app/src/main/java/io/legado/app/ui/book/import/remote/RemoteBookViewModel.kt index e4a23597f..778035087 100644 --- a/app/src/main/java/io/legado/app/ui/book/import/remote/RemoteBookViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/import/remote/RemoteBookViewModel.kt @@ -1,5 +1,6 @@ package io.legado.app.ui.book.import.remote +import io.legado.app.ui.config.otherConfig.OtherConfig import android.app.Application import androidx.core.net.toUri 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.repository.RemoteBookRepository 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.localBook.LocalBook 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.FileDoc import io.legado.app.utils.find +import io.legado.app.utils.isUri import io.legado.app.utils.takePersistablePermissionSafely import io.legado.app.utils.toastOnUi import kotlinx.coroutines.Dispatchers @@ -162,13 +162,13 @@ class RemoteBookViewModel( private fun onBookFolderPicked(uri: android.net.Uri?) { uri ?: return uri.takePersistablePermissionSafely(context) - AppConfig.defaultBookTreeUri = uri.toString() + OtherConfig.defaultBookTreeUri = uri.toString() } val uiState: StateFlow = combine( _state, repository.flowLocalBooks(), - appDb.serverDao.observeAll() + repository.flowServers() ) { state, localBooks, servers -> val localFileNames = localBooks.map { it.originName }.toSet() @@ -224,7 +224,7 @@ class RemoteBookViewModel( } onSuccess() } else { - val defaultWebDav = AppWebDav.defaultBookWebDav + val defaultWebDav = repository.getDefaultBookWebDav() ?: throw NoStackTraceException("webDav没有配置") _state.update { it.copy( @@ -282,7 +282,7 @@ class RemoteBookViewModel( _state.update { it.copy(selectedIds = emptySet()) } Result.success(Unit) } catch (e: SecurityException) { - _effects.emit(RemoteBookEffect.RequestBookFolderPicker()) + _effects.emit(RemoteBookEffect.RequestBookFolderPicker(defaultBookTreeUri())) Result.failure(e) } catch (e: Exception) { AppLog.put("导入出错\n${e.localizedMessage}", e) @@ -419,14 +419,14 @@ class RemoteBookViewModel( return@launch } - val bookTreeUri = AppConfig.defaultBookTreeUri + val bookTreeUri = defaultBookTreeUri() if (bookTreeUri == null) { - _effects.emit(RemoteBookEffect.RequestBookFolderPicker()) + _effects.emit(RemoteBookEffect.RequestBookFolderPicker(defaultBookTreeUri())) return@launch } val downloadArchiveFileDoc = withContext(Dispatchers.IO) { - FileDoc.fromUri(bookTreeUri.toUri(), true) + FileDoc.fromUri(bookTreeUri, true) .find(downloadFileName) } 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) { execute { - appDb.serverDao.insert(server) + repository.saveServer(server) }.onSuccess { if (ImportBookConfig.remoteServerId == server.id) { initData { loadRemoteBookList() } @@ -507,7 +513,7 @@ class RemoteBookViewModel( fun deleteServer(server: Server) { execute { - appDb.serverDao.delete(server) + repository.deleteServer(server) } } diff --git a/app/src/main/java/io/legado/app/ui/book/import/remote/ServerConfigDialog.kt b/app/src/main/java/io/legado/app/ui/book/import/remote/ServerConfigDialog.kt index 767dbf9b8..effc18dba 100644 --- a/app/src/main/java/io/legado/app/ui/book/import/remote/ServerConfigDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/import/remote/ServerConfigDialog.kt @@ -6,7 +6,6 @@ import android.view.MenuItem import android.view.View import android.view.ViewGroup import androidx.appcompat.widget.Toolbar -import androidx.fragment.app.viewModels import io.legado.app.R import io.legado.app.base.BaseDialogFragment 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.viewbindingdelegate.viewBinding import org.json.JSONObject +import org.koin.androidx.viewmodel.ext.android.viewModel class ServerConfigDialog() : BaseDialogFragment(R.layout.dialog_webdav_server, true), Toolbar.OnMenuItemClickListener { @@ -29,7 +29,7 @@ class ServerConfigDialog() : BaseDialogFragment(R.layout.dialog_webdav_server, t } private val binding by viewBinding(DialogWebdavServerBinding::bind) - private val viewModel by viewModels() + private val viewModel by viewModel() private val webDavServerUi = listOf( RowUi("url"), @@ -128,4 +128,4 @@ class ServerConfigDialog() : BaseDialogFragment(R.layout.dialog_webdav_server, t return data } -} \ No newline at end of file +} diff --git a/app/src/main/java/io/legado/app/ui/book/import/remote/ServerConfigViewModel.kt b/app/src/main/java/io/legado/app/ui/book/import/remote/ServerConfigViewModel.kt index 875304232..d2bd4e7c6 100644 --- a/app/src/main/java/io/legado/app/ui/book/import/remote/ServerConfigViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/import/remote/ServerConfigViewModel.kt @@ -2,11 +2,14 @@ package io.legado.app.ui.book.import.remote import android.app.Application import io.legado.app.base.BaseViewModel -import io.legado.app.data.appDb import io.legado.app.data.entities.Server +import io.legado.app.data.repository.RemoteBookRepository 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 @@ -15,7 +18,7 @@ class ServerConfigViewModel(application: Application): BaseViewModel(application if (mServer != null) return execute { mServer = if (id != null) { - appDb.serverDao.get(id) + remoteBookRepository.getServer(id) } else { Server() } @@ -27,10 +30,10 @@ class ServerConfigViewModel(application: Application): BaseViewModel(application fun save(server: Server, onSuccess: () -> Unit) { execute { mServer?.let { - appDb.serverDao.delete(it) + remoteBookRepository.deleteServer(it) } mServer = server - appDb.serverDao.insert(server) + remoteBookRepository.saveServer(server) }.onSuccess { onSuccess.invoke() }.onError { @@ -38,4 +41,4 @@ class ServerConfigViewModel(application: Application): BaseViewModel(application } } -} \ No newline at end of file +} diff --git a/app/src/main/java/io/legado/app/ui/book/import/remote/ServersDialog.kt b/app/src/main/java/io/legado/app/ui/book/import/remote/ServersDialog.kt index 5aa88ef3d..75ff81a85 100644 --- a/app/src/main/java/io/legado/app/ui/book/import/remote/ServersDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/import/remote/ServersDialog.kt @@ -7,7 +7,6 @@ import android.view.MenuItem import android.view.View import android.view.ViewGroup import androidx.appcompat.widget.Toolbar -import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager 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.constant.AppConst.DEFAULT_WEBDAV_ID import io.legado.app.constant.AppLog -import io.legado.app.data.appDb import io.legado.app.data.entities.Server import io.legado.app.databinding.DialogRecyclerViewBinding import io.legado.app.databinding.ItemServerSelectBinding @@ -34,6 +32,7 @@ import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.flowOn 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 { val binding by viewBinding(DialogRecyclerViewBinding::bind) - val viewModel by viewModels() + val viewModel by viewModel() private val callback get() = (activity as? Callback) private val adapter by lazy { ServersAdapter(requireContext()) } @@ -86,7 +85,7 @@ class ServersDialog : BaseDialogFragment(R.layout.dialog_recycler_view), private fun initData() { lifecycleScope.launch { - appDb.serverDao.observeAll().catch { + viewModel.flowServers().catch { AppLog.put("服务器配置界面获取数据失败\n${it.localizedMessage}", it) }.flowOn(IO).collect { adapter.setItems(it) diff --git a/app/src/main/java/io/legado/app/ui/book/import/remote/ServersViewModel.kt b/app/src/main/java/io/legado/app/ui/book/import/remote/ServersViewModel.kt index 0508e87cb..b2602af6c 100644 --- a/app/src/main/java/io/legado/app/ui/book/import/remote/ServersViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/import/remote/ServersViewModel.kt @@ -2,16 +2,20 @@ package io.legado.app.ui.book.import.remote import android.app.Application import io.legado.app.base.BaseViewModel -import io.legado.app.data.appDb 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) { execute { - appDb.serverDao.delete(server) + remoteBookRepository.deleteServer(server) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt b/app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt index 674b88e8f..cb0810e6f 100644 --- a/app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt @@ -1,5 +1,6 @@ package io.legado.app.ui.book.info +import io.legado.app.ui.config.otherConfig.OtherConfig import android.content.Intent import android.os.Bundle import androidx.activity.compose.setContent @@ -41,7 +42,7 @@ class BookInfoActivity : BaseComposeActivity(), VariableDialog.Callback { } private val localBookTreeSelect = registerForActivityResult(HandleFileContract()) { it.uri?.let { treeUri -> - AppConfig.defaultBookTreeUri = treeUri.toString() + OtherConfig.defaultBookTreeUri = treeUri.toString() } } private val infoEditResult = registerForActivityResult( diff --git a/app/src/main/java/io/legado/app/ui/book/info/BookInfoContract.kt b/app/src/main/java/io/legado/app/ui/book/info/BookInfoContract.kt index 66d4bedfc..5dd5c95df 100644 --- a/app/src/main/java/io/legado/app/ui/book/info/BookInfoContract.kt +++ b/app/src/main/java/io/legado/app/ui/book/info/BookInfoContract.kt @@ -19,6 +19,8 @@ data class BookInfoUiState( val bookSource: BookSource? = null, val isTocLoading: Boolean = true, val isBusy: Boolean = false, + val deleteAlertEnabled: Boolean = true, + val deleteOriginal: Boolean = false, val showAppLogSheet: Boolean = false, val sheet: BookInfoSheet = BookInfoSheet.None, val dialog: BookInfoDialog? = null, diff --git a/app/src/main/java/io/legado/app/ui/book/info/BookInfoScreen.kt b/app/src/main/java/io/legado/app/ui/book/info/BookInfoScreen.kt index 1bfdaa04d..db9f1beda 100644 --- a/app/src/main/java/io/legado/app/ui/book/info/BookInfoScreen.kt +++ b/app/src/main/java/io/legado/app/ui/book/info/BookInfoScreen.kt @@ -573,7 +573,7 @@ private fun BookInfoOverflowMenu( RoundDropdownMenuItem( text = stringResource(R.string.delete_alert), onClick = { onMenuAction(BookInfoMenuAction.ToggleDeleteAlert) }, - isSelected = io.legado.app.help.config.LocalConfig.bookInfoDeleteAlert + isSelected = state.deleteAlertEnabled ) RoundDropdownMenuItem( text = stringResource(R.string.clear_cache), @@ -894,7 +894,7 @@ private fun BookInfoDialogs( onBack: () -> Unit, ) { 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()) } if (dialog is BookInfoDialog.AddToShelfOnBack) { @@ -918,7 +918,6 @@ private fun BookInfoDialogs( text = stringResource(R.string.sure_del), confirmText = stringResource(android.R.string.ok), onConfirm = { - io.legado.app.help.config.LocalConfig.deleteBookOriginal = deleteOriginal onIntent(BookInfoIntent.ConfirmDelete(deleteOriginal)) }, dismissText = stringResource(android.R.string.cancel), diff --git a/app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt b/app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt index 63750707f..6210d7182 100644 --- a/app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt @@ -18,6 +18,7 @@ import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.readRecord.ReadRecordTimelineDay import io.legado.app.data.repository.ReadRecordRepository 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.NoStackTraceException import io.legado.app.help.book.BookHelp @@ -63,7 +64,8 @@ import kotlinx.coroutines.launch class BookInfoViewModel( application: Application, private val remoteBookRepository: RemoteBookRepository, - private val readRecordRepository: ReadRecordRepository + private val readRecordRepository: ReadRecordRepository, + private val clearBookCacheUseCase: ClearBookCacheUseCase ) : BaseViewModel(application) { private val _uiState = MutableStateFlow(BookInfoUiState()) @@ -400,7 +402,7 @@ class BookInfoViewModel( fun clearCache() { currentBook?.let { book -> execute { - BookHelp.clearCache(book) + clearBookCacheUseCase.execute(book.bookUrl) if (ReadBook.book?.bookUrl == book.bookUrl) { ReadBook.clearTextChapter() } @@ -851,6 +853,8 @@ class BookInfoViewModel( } private fun deleteBook(deleteOriginal: Boolean) { currentBook?.let { book -> + LocalConfig.deleteBookOriginal = deleteOriginal + _uiState.update { it.copy(deleteOriginal = deleteOriginal) } SourceCallBack.callBackBook(SourceCallBack.DEL_BOOK_SHELF, bookSource, book) delBook(deleteOriginal) { emitEffect(BookInfoEffect.Finish(resultCode = RESULT_OK)) @@ -1168,6 +1172,8 @@ class BookInfoViewModel( inBookshelf = inBookshelf, bookSource = bookSource, isTocLoading = isTocLoading, + deleteAlertEnabled = LocalConfig.bookInfoDeleteAlert, + deleteOriginal = LocalConfig.deleteBookOriginal, ) } } diff --git a/app/src/main/java/io/legado/app/ui/book/manga/ReadMangaViewModel.kt b/app/src/main/java/io/legado/app/ui/book/manga/ReadMangaViewModel.kt index d35acce89..4bbea6e31 100644 --- a/app/src/main/java/io/legado/app/ui/book/manga/ReadMangaViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/manga/ReadMangaViewModel.kt @@ -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.BookChapter 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.help.AppWebDav import io.legado.app.help.book.BookHelp import io.legado.app.help.book.isLocal import io.legado.app.help.book.isLocalModified @@ -47,7 +48,10 @@ import kotlinx.coroutines.withContext import splitties.init.appCtx 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 @@ -243,7 +247,7 @@ class ReadMangaViewModel(application: Application) : BaseViewModel(application) ) { if (!AppConfig.syncBookProgress) return execute { - AppWebDav.getBookProgress(book) + getReadingProgressUseCase.execute(book.name, book.author)?.toBookProgress() }.onError { AppLog.put("拉取阅读进度失败《${book.name}》\n${it.localizedMessage}", it) }.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 + ) + /** * 换源 */ diff --git a/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt b/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt index 301f69065..799423122 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt @@ -39,7 +39,6 @@ import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.BookProgress import io.legado.app.data.entities.BookSource import io.legado.app.exception.NoStackTraceException -import io.legado.app.help.AppWebDav import io.legado.app.help.IntentData import io.legado.app.help.TTS 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.isMobi 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.ReadBookConfig import io.legado.app.help.config.ReadTipConfig @@ -525,10 +523,10 @@ class ReadBookActivity : BaseReadBookActivity(), } lifecycleScope.launch { menu.findItem(R.id.menu_get_progress)?.isVisible = withContext(IO) { - AppWebDav.isOk + viewModel.isReadingProgressSyncConfigured() } 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) { delay(300000) ReadBook.book?.let { - AppWebDav.uploadBookProgress(it) + viewModel.uploadBookProgress(it) ensureActive() - it.update() Backup.autoBack(this@ReadBookActivity) } } diff --git a/app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt b/app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt index a3e5911d5..6336f5c62 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt @@ -12,8 +12,10 @@ import io.legado.app.data.appDb import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookChapter 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.help.AppWebDav import io.legado.app.help.book.BookHelp import io.legado.app.help.book.ContentProcessor 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() var isInitFinish = false var searchContentQuery = "" @@ -253,7 +259,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) { ) { if (!AppConfig.syncBookProgress) return execute { - AppWebDav.getBookProgress(book) + getReadingProgressUseCase.execute(book.name, book.author)?.toBookProgress() }.onError { AppLog.put("拉取阅读进度失败《${book.name}》\n${it.localizedMessage}", it) }.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 + ) + /** * 换源 */ diff --git a/app/src/main/java/io/legado/app/ui/book/search/BookKey.kt b/app/src/main/java/io/legado/app/ui/book/search/BookKey.kt deleted file mode 100644 index bb271c54b..000000000 --- a/app/src/main/java/io/legado/app/ui/book/search/BookKey.kt +++ /dev/null @@ -1,7 +0,0 @@ -package io.legado.app.ui.book.search - -data class BookKey( - val name: String, - val author: String, - val url: String? -) diff --git a/app/src/main/java/io/legado/app/ui/book/search/SearchAdapter.kt b/app/src/main/java/io/legado/app/ui/book/search/SearchAdapter.kt index 6c0050642..f1360c436 100644 --- a/app/src/main/java/io/legado/app/ui/book/search/SearchAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/book/search/SearchAdapter.kt @@ -12,7 +12,7 @@ import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.data.entities.SearchBook import io.legado.app.databinding.ItemSearchBinding 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.utils.dpToPx import io.legado.app.utils.gone diff --git a/app/src/main/java/io/legado/app/ui/book/search/SearchContract.kt b/app/src/main/java/io/legado/app/ui/book/search/SearchContract.kt index 08ef305df..febe4a1a8 100644 --- a/app/src/main/java/io/legado/app/ui/book/search/SearchContract.kt +++ b/app/src/main/java/io/legado/app/ui/book/search/SearchContract.kt @@ -3,12 +3,18 @@ package io.legado.app.ui.book.search import io.legado.app.data.entities.BookSourcePart import io.legado.app.data.entities.SearchBook import io.legado.app.data.entities.SearchKeyword +import io.legado.app.domain.model.BookShelfState import io.legado.app.ui.main.bookshelf.BookShelfItem +data class SearchResultItemUi( + val book: SearchBook, + val shelfState: BookShelfState = BookShelfState.NOT_IN_SHELF, +) + data class SearchUiState( val query: String = "", val committedQuery: String = "", - val results: List = emptyList(), + val results: List = emptyList(), val history: List = emptyList(), val bookshelfHints: List = emptyList(), val enabledGroups: List = emptyList(), diff --git a/app/src/main/java/io/legado/app/ui/book/search/SearchScreen.kt b/app/src/main/java/io/legado/app/ui/book/search/SearchScreen.kt index a8e3928b7..5cba2283e 100644 --- a/app/src/main/java/io/legado/app/ui/book/search/SearchScreen.kt +++ b/app/src/main/java/io/legado/app/ui/book/search/SearchScreen.kt @@ -337,16 +337,13 @@ fun SearchScreen( ) { itemsIndexed( items = state.results, - key = { index, book -> "${book.origin}:${book.bookUrl}:$index" } - ) { _, book -> - val shelfStateFlow = remember(book.bookUrl) { - viewModel.getBookShelfStateFlow(book) - } + key = { index, item -> "${item.book.origin}:${item.book.bookUrl}:$index" } + ) { _, item -> SearchBookListItem( - book = book, - shelfState = shelfStateFlow, + book = item.book, + shelfState = item.shelfState, onClick = { - viewModel.onIntent(SearchIntent.OpenSearchBook(book)) + viewModel.onIntent(SearchIntent.OpenSearchBook(item.book)) } ) } diff --git a/app/src/main/java/io/legado/app/ui/book/search/SearchViewModel.kt b/app/src/main/java/io/legado/app/ui/book/search/SearchViewModel.kt index 85470ced0..a11a3524c 100644 --- a/app/src/main/java/io/legado/app/ui/book/search/SearchViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/search/SearchViewModel.kt @@ -7,13 +7,13 @@ import io.legado.app.data.entities.BookSourcePart import io.legado.app.data.entities.SearchBook import io.legado.app.data.repository.SearchRepository 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.model.BookShelfState import io.legado.app.ui.main.bookshelf.BookShelfItem import io.legado.app.utils.getPrefBoolean import io.legado.app.utils.putPrefBoolean import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asSharedFlow @@ -29,6 +29,7 @@ import splitties.init.appCtx @OptIn(ExperimentalCoroutinesApi::class) class SearchViewModel( private val repository: SearchRepository, + private val resolveBookShelfStateUseCase: ResolveBookShelfStateUseCase, ) : ViewModel() { private val _uiState = MutableStateFlow( @@ -46,7 +47,7 @@ class SearchViewModel( val effects = _effects.asSharedFlow() private val queryFlow = MutableStateFlow("") - private val bookshelfKeys = MutableStateFlow>(emptySet()) + private val bookshelfKeys = MutableStateFlow>(emptySet()) private val searchScope = SearchScope(AppConfig.searchScope) private val searchSession = repository.createSearchSession { searchScope } @@ -142,12 +143,6 @@ class SearchViewModel( } } - fun getBookShelfStateFlow(item: SearchBook): Flow { - return bookshelfKeys - .map { keys -> resolveBookShelfState(item, keys) } - .distinctUntilChanged() - } - override fun onCleared() { super.onCleared() searchSession.close() @@ -194,6 +189,9 @@ class SearchViewModel( .catch { emit(emptySet()) } .collect { keys -> bookshelfKeys.value = keys + _uiState.update { state -> + state.copy(results = state.results.withShelfState(keys)) + } } } } @@ -233,9 +231,10 @@ class SearchViewModel( } is SearchSessionEvent.Progress -> { + val shelf = bookshelfKeys.value _uiState.update { it.copy( - results = event.books, + results = event.books.toSearchResultItems(shelf), processedSources = event.processedSources, totalSources = event.totalSources, ) @@ -403,18 +402,35 @@ class SearchViewModel( } } - private fun resolveBookShelfState(item: SearchBook, shelf: Set): BookShelfState { - val exactMatch = shelf.any { - it.name == item.name && it.author == item.author && it.url == item.bookUrl + private fun List.toSearchResultItems( + shelf: Set + ): List { + 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 + private fun List.withShelfState( + shelf: Set + ): List { + return map { item -> + item.copy( + shelfState = resolveBookShelfStateUseCase.execute( + name = item.book.name, + author = item.book.author, + url = item.book.bookUrl, + shelf = shelf + ) + ) } - if (sameNameAuthor) return BookShelfState.SAME_NAME_AUTHOR - - return BookShelfState.NOT_IN_SHELF } private fun emitEffect(effect: SearchEffect) { diff --git a/app/src/main/java/io/legado/app/ui/book/toc/TocViewModel.kt b/app/src/main/java/io/legado/app/ui/book/toc/TocViewModel.kt index afc22db53..cee70dd93 100644 --- a/app/src/main/java/io/legado/app/ui/book/toc/TocViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/toc/TocViewModel.kt @@ -13,6 +13,7 @@ import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.Bookmark 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.ContentProcessor 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) class TocViewModel( application: Application, - savedStateHandle: SavedStateHandle + savedStateHandle: SavedStateHandle, + private val cacheBookChaptersUseCase: CacheBookChaptersUseCase ) : BaseRuleViewModel( application, initialState = TocActionState() @@ -524,15 +526,21 @@ class TocViewModel( val book = bookState.value ?: return val indices = uiState.value.selectedIds.toList() if (indices.isEmpty()) return - CacheBook.start(getApplication(), book, indices) - getApplication().toastOnUi("开始下载 ${indices.size} 个章节") - clearSelection() + execute { + cacheBookChaptersUseCase.execute(book.bookUrl, indices) + }.onSuccess { count -> + getApplication().toastOnUi("开始下载 $count 个章节") + clearSelection() + } } fun downloadChapter(index: Int) { val book = bookState.value ?: return - CacheBook.start(getApplication(), book, listOf(index)) - getApplication().toastOnUi("开始下载章节") + execute { + cacheBookChaptersUseCase.execute(book.bookUrl, listOf(index)) + }.onSuccess { + getApplication().toastOnUi("开始下载章节") + } } fun downloadAll() { @@ -546,8 +554,11 @@ class TocViewModel( return } - CacheBook.start(getApplication(), book, targetIndices) - getApplication().toastOnUi("开始下载剩余 ${targetIndices.size} 个章节") + execute { + cacheBookChaptersUseCase.execute(book.bookUrl, targetIndices) + }.onSuccess { count -> + getApplication().toastOnUi("开始下载剩余 $count 个章节") + } } private fun List.groupAndReverseVolumes(): List { diff --git a/app/src/main/java/io/legado/app/ui/config/backupConfig/BackupConfigViewModel.kt b/app/src/main/java/io/legado/app/ui/config/backupConfig/BackupConfigViewModel.kt index 58aeebec9..4cd0d09f2 100644 --- a/app/src/main/java/io/legado/app/ui/config/backupConfig/BackupConfigViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/config/backupConfig/BackupConfigViewModel.kt @@ -2,18 +2,20 @@ package io.legado.app.ui.config.backupConfig import androidx.lifecycle.ViewModel 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 kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import splitties.init.appCtx -class BackupConfigViewModel : ViewModel() { +class BackupConfigViewModel( + private val webDavBackupUseCase: WebDavBackupUseCase +) : ViewModel() { private suspend fun syncWebDavConfig() { withContext(Dispatchers.IO) { - AppWebDav.upConfig() + webDavBackupUseCase.refreshConfig() } } @@ -35,11 +37,9 @@ class BackupConfigViewModel : ViewModel() { } suspend fun testWebDav(): Boolean { - syncWebDavConfig() return withContext(Dispatchers.IO) { try { - AppWebDav.testWebDav() - true + webDavBackupUseCase.test() } catch (e: Exception) { false } @@ -62,17 +62,15 @@ class BackupConfigViewModel : ViewModel() { } suspend fun getBackupNames(): List { - syncWebDavConfig() return withContext(Dispatchers.IO) { - AppWebDav.getBackupNames() + webDavBackupUseCase.getBackupNames() } } fun restoreWebDav(name: String, onSuccess: () -> Unit, onError: (String) -> Unit) { viewModelScope.launch(Dispatchers.IO) { try { - AppWebDav.upConfig() - AppWebDav.restoreWebDav(name) + webDavBackupUseCase.restore(name) withContext(Dispatchers.Main) { onSuccess() } diff --git a/app/src/main/java/io/legado/app/ui/config/importBookConfig/ImportBookConfig.kt b/app/src/main/java/io/legado/app/ui/config/importBookConfig/ImportBookConfig.kt index 500e42f01..54914cda0 100644 --- a/app/src/main/java/io/legado/app/ui/config/importBookConfig/ImportBookConfig.kt +++ b/app/src/main/java/io/legado/app/ui/config/importBookConfig/ImportBookConfig.kt @@ -5,11 +5,6 @@ import io.legado.app.ui.config.prefDelegate object ImportBookConfig { - var defaultBookTreeUri by prefDelegate( - PreferKey.defaultBookTreeUri, - null as String? - ) - var importBookPath by prefDelegate( PreferKey.importBookPath, null as String? diff --git a/app/src/main/java/io/legado/app/ui/config/otherConfig/OtherConfig.kt b/app/src/main/java/io/legado/app/ui/config/otherConfig/OtherConfig.kt index 9043c59d2..dc643b0fb 100644 --- a/app/src/main/java/io/legado/app/ui/config/otherConfig/OtherConfig.kt +++ b/app/src/main/java/io/legado/app/ui/config/otherConfig/OtherConfig.kt @@ -48,7 +48,7 @@ object OtherConfig { var defaultBookTreeUri by prefDelegate( PreferKey.defaultBookTreeUri, - "" + null as String? ) var antiAlias by prefDelegate( diff --git a/app/src/main/java/io/legado/app/ui/config/otherConfig/OtherConfigViewModel.kt b/app/src/main/java/io/legado/app/ui/config/otherConfig/OtherConfigViewModel.kt index adf1290c3..69d9f0643 100644 --- a/app/src/main/java/io/legado/app/ui/config/otherConfig/OtherConfigViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/config/otherConfig/OtherConfigViewModel.kt @@ -9,9 +9,9 @@ import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope 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.book.BookHelp import io.legado.app.help.config.AppConfig import io.legado.app.help.config.LocalConfig import io.legado.app.model.CheckSource @@ -25,7 +25,10 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch 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 componentName = ComponentName( @@ -52,7 +55,7 @@ class OtherConfigViewModel : ViewModel() { fun clearCache(context: Context) { viewModelScope.launch(Dispatchers.IO) { - BookHelp.clearCache() + clearBookCacheUseCase.executeAll() FileUtils.delete(context.cacheDir.absolutePath) context.externalCacheDir?.deleteRecursively() context.cacheDir.deleteRecursively() @@ -61,7 +64,7 @@ class OtherConfigViewModel : ViewModel() { fun shrinkDatabase() { viewModelScope.launch(Dispatchers.IO) { - appDb.openHelper.writableDatabase.execSQL("VACUUM") + shrinkDatabaseUseCase.execute() } } @@ -90,7 +93,6 @@ class OtherConfigViewModel : ViewModel() { fun updateLocalBookDir(path: String) { OtherConfig.defaultBookTreeUri = path - AppConfig.defaultBookTreeUri = OtherConfig.defaultBookTreeUri } var checkSourceTimeout by mutableStateOf((CheckSource.timeout / 1000)) @@ -154,4 +156,4 @@ class OtherConfigViewModel : ViewModel() { } -} \ No newline at end of file +} diff --git a/app/src/main/java/io/legado/app/ui/dict/DictDialog.kt b/app/src/main/java/io/legado/app/ui/dict/DictDialog.kt index e8ecfaf2f..6c37137b1 100644 --- a/app/src/main/java/io/legado/app/ui/dict/DictDialog.kt +++ b/app/src/main/java/io/legado/app/ui/dict/DictDialog.kt @@ -3,7 +3,6 @@ package io.legado.app.ui.dict import android.os.Bundle import android.text.method.LinkMovementMethod import android.view.View -import androidx.fragment.app.viewModels import com.google.android.material.tabs.TabLayout import io.legado.app.R 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.viewbindingdelegate.viewBinding 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() + private val viewModel by viewModel() private val binding by viewBinding(DialogDictBinding::bind) private var word: String? = null private var glideImageGetter: GlideImageGetter? = null diff --git a/app/src/main/java/io/legado/app/ui/dict/DictViewModel.kt b/app/src/main/java/io/legado/app/ui/dict/DictViewModel.kt index f8c56d3aa..cf4101ce1 100644 --- a/app/src/main/java/io/legado/app/ui/dict/DictViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/dict/DictViewModel.kt @@ -2,17 +2,20 @@ package io.legado.app.ui.dict import android.app.Application import io.legado.app.base.BaseViewModel -import io.legado.app.data.appDb import io.legado.app.data.entities.DictRule +import io.legado.app.data.repository.DictRuleRepository 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? = null fun initData(onSuccess: (List) -> Unit) { execute { - appDb.dictRuleDao.enabled + dictRuleRepository.getEnabled() }.onSuccess { onSuccess.invoke(it) } @@ -34,4 +37,4 @@ class DictViewModel(application: Application) : BaseViewModel(application) { } -} \ No newline at end of file +} diff --git a/app/src/main/java/io/legado/app/ui/main/MainActivity.kt b/app/src/main/java/io/legado/app/ui/main/MainActivity.kt index b28f75885..0949edda8 100644 --- a/app/src/main/java/io/legado/app/ui/main/MainActivity.kt +++ b/app/src/main/java/io/legado/app/ui/main/MainActivity.kt @@ -30,7 +30,6 @@ import io.legado.app.R import io.legado.app.base.BaseComposeActivity import io.legado.app.constant.AppConst.appInfo import io.legado.app.constant.PreferKey -import io.legado.app.help.AppWebDav import io.legado.app.help.book.BookHelp import io.legado.app.help.config.AppConfig import io.legado.app.help.config.LocalConfig @@ -716,13 +715,13 @@ open class MainActivity : BaseComposeActivity() { } lifecycleScope.launch { val lastBackupFile = - withContext(IO) { AppWebDav.lastBackUp().getOrNull() } ?: return@launch + withContext(IO) { viewModel.getLatestWebDavBackup() } ?: return@launch if (lastBackupFile.lastModify - LocalConfig.lastBackup > DateUtils.MINUTE_IN_MILLIS) { LocalConfig.lastBackup = lastBackupFile.lastModify alert(R.string.restore, R.string.webdav_after_local_restore_confirm) { cancelButton() okButton { - viewModel.restoreWebDav(lastBackupFile.displayName) + viewModel.restoreWebDav(lastBackupFile.name) } } } diff --git a/app/src/main/java/io/legado/app/ui/main/MainViewModel.kt b/app/src/main/java/io/legado/app/ui/main/MainViewModel.kt index 68532378e..87430cd5b 100644 --- a/app/src/main/java/io/legado/app/ui/main/MainViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/main/MainViewModel.kt @@ -5,14 +5,17 @@ import android.content.Context import android.content.Intent import io.legado.app.base.BaseViewModel import io.legado.app.constant.EventBus -import io.legado.app.data.appDb -import io.legado.app.help.AppWebDav -import io.legado.app.help.DefaultData +import io.legado.app.domain.usecase.AppStartupMaintenanceUseCase +import io.legado.app.domain.usecase.WebDavBackupUseCase import io.legado.app.ui.main.my.PrefClickEvent import io.legado.app.utils.eventBus.FlowEventBus 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 { deleteNotShelfBook() @@ -24,23 +27,21 @@ class MainViewModel(application: Application) : BaseViewModel(application) { fun postLoad() { execute { - if (appDb.httpTTSDao.count == 0) { - DefaultData.httpTTS.let { - appDb.httpTTSDao.insert(*it.toTypedArray()) - } - } + appStartupMaintenanceUseCase.ensureDefaultHttpTts() } } fun restoreWebDav(name: String) { execute { - AppWebDav.restoreWebDav(name) + webDavBackupUseCase.restore(name) } } + suspend fun getLatestWebDavBackup() = webDavBackupUseCase.getLatestBackup() + private fun deleteNotShelfBook() { execute { - appDb.bookDao.deleteNotShelfBook() + appStartupMaintenanceUseCase.deleteNotShelfBooks() } } diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt index eccae4106..8d624f92d 100644 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt @@ -70,7 +70,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import io.legado.app.R import io.legado.app.base.BaseRuleEvent -import io.legado.app.data.appDb import io.legado.app.data.entities.BookGroup import io.legado.app.ui.about.AppLogSheet 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 kotlinx.coroutines.delay import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import org.koin.androidx.compose.koinViewModel @@ -226,14 +224,8 @@ fun BookshelfScreen( } val isUsingStandaloneSearchGroup = uiState.isSearch && uiState.groups.none { it.groupId == currentGroupId } - val currentGroupBookCountFlow = remember(currentGroupId) { - appDb.bookDao.flowBookShelfByGroup(currentGroupId).map { it.size } - } - 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 currentGroupBookCount = uiState.currentGroupBookCount + val allGroupsBookCount = uiState.allBooksCount val bookGroupStyle = BookshelfConfig.bookGroupStyle // 控制是否处于“文件夹列表”根视图,还是“文件夹内部”书籍视图 diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfUiState.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfUiState.kt index 363c6191b..79d26ee27 100644 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfUiState.kt +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfUiState.kt @@ -13,6 +13,8 @@ data class BookshelfUiState( val allGroups: List = emptyList(), val groupPreviews: Map> = emptyMap(), val groupBookCounts: Map = emptyMap(), + val currentGroupBookCount: Int = 0, + val allBooksCount: Int = 0, val selectedGroupIndex: Int = 0, val selectedGroupId: Long = BookGroup.IdAll, val loadingText: String? = null, diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt index 4fdbc0b37..e45b9184e 100644 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt @@ -1,5 +1,6 @@ package io.legado.app.ui.main.bookshelf +import kotlinx.coroutines.flow.flow import android.app.Application import android.net.Uri import androidx.compose.runtime.snapshotFlow @@ -69,7 +70,6 @@ import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onCompletion @@ -134,7 +134,8 @@ class BookshelfViewModel( private data class GroupPreviewState( val previews: Map>, - val counts: Map + val counts: Map, + val allBookCount: Int ) @OptIn(ExperimentalCoroutinesApi::class) @@ -206,9 +207,9 @@ class BookshelfViewModel( } previews[group.groupId] = result } - GroupPreviewState(previews, counts) + GroupPreviewState(previews, counts, allBooks.size) } else { - GroupPreviewState(emptyMap(), emptyMap()) + GroupPreviewState(emptyMap(), emptyMap(), allBooks.size) } }.distinctUntilChanged().flowOn(Dispatchers.Default) @@ -257,6 +258,8 @@ class BookshelfViewModel( allGroups = allGroups, groupPreviews = previews.previews, groupBookCounts = previews.counts, + currentGroupBookCount = books.size, + allBooksCount = previews.allBookCount, selectedGroupIndex = groups.indexOfFirst { it.groupId == internal.groupId } .coerceAtLeast(0), selectedGroupId = internal.groupId, @@ -393,7 +396,6 @@ class BookshelfViewModel( if (bookUrls.isEmpty()) return execute { batchCacheDownloadUseCase.execute( - context = context, bookUrls = bookUrls, downloadAllChapters = downloadAllChapters, skipAudioBooks = true diff --git a/app/src/main/java/io/legado/app/ui/main/explore/ExploreScreen.kt b/app/src/main/java/io/legado/app/ui/main/explore/ExploreScreen.kt index 528155752..f4c6d092e 100644 --- a/app/src/main/java/io/legado/app/ui/main/explore/ExploreScreen.kt +++ b/app/src/main/java/io/legado/app/ui/main/explore/ExploreScreen.kt @@ -51,7 +51,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import io.legado.app.R 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.search.SearchActivity import io.legado.app.ui.book.search.SearchScope diff --git a/app/src/main/java/io/legado/app/ui/rss/read/RssReadRouteScreen.kt b/app/src/main/java/io/legado/app/ui/rss/read/RssReadRouteScreen.kt index 292386f11..da4f1f667 100644 --- a/app/src/main/java/io/legado/app/ui/rss/read/RssReadRouteScreen.kt +++ b/app/src/main/java/io/legado/app/ui/rss/read/RssReadRouteScreen.kt @@ -67,7 +67,6 @@ import com.script.rhino.runScriptWithContext import io.legado.app.R import io.legado.app.constant.AppConst import io.legado.app.constant.AppLog -import io.legado.app.help.config.AppConfig import io.legado.app.help.http.CookieManager import io.legado.app.lib.dialogs.SelectItem import io.legado.app.lib.dialogs.selector diff --git a/app/src/main/java/io/legado/app/ui/welcome/BookFolderFragment.kt b/app/src/main/java/io/legado/app/ui/welcome/BookFolderFragment.kt index 60b2032a7..ab702a264 100644 --- a/app/src/main/java/io/legado/app/ui/welcome/BookFolderFragment.kt +++ b/app/src/main/java/io/legado/app/ui/welcome/BookFolderFragment.kt @@ -1,11 +1,11 @@ package io.legado.app.ui.welcome +import io.legado.app.ui.config.otherConfig.OtherConfig import android.os.Bundle import android.view.View import io.legado.app.R import io.legado.app.base.BaseFragment import io.legado.app.databinding.FragmentBookFolderBinding -import io.legado.app.help.config.AppConfig import io.legado.app.ui.file.HandleFileContract 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 -> result.uri?.let { treeUri -> - AppConfig.defaultBookTreeUri = treeUri.toString() + OtherConfig.defaultBookTreeUri = treeUri.toString() updatePathText() } } @@ -32,6 +32,6 @@ class BookFolderFragment : BaseFragment(R.layout.fragment_book_folder) { private fun updatePathText() { binding.tvFolderPath.text = - AppConfig.defaultBookTreeUri ?: getString(R.string.welcome_book_folder_not_selected) + OtherConfig.defaultBookTreeUri ?: getString(R.string.welcome_book_folder_not_selected) } } diff --git a/app/src/main/java/io/legado/app/ui/welcome/WebDavFragment.kt b/app/src/main/java/io/legado/app/ui/welcome/WebDavFragment.kt index 9f641eadf..7308038af 100644 --- a/app/src/main/java/io/legado/app/ui/welcome/WebDavFragment.kt +++ b/app/src/main/java/io/legado/app/ui/welcome/WebDavFragment.kt @@ -16,9 +16,8 @@ import io.legado.app.base.BaseFragment import io.legado.app.constant.AppLog import io.legado.app.constant.PreferKey import io.legado.app.databinding.FragmentWebdavAuthBinding +import io.legado.app.domain.usecase.WebDavBackupUseCase 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.coroutine.Coroutine import io.legado.app.help.storage.Restore @@ -35,11 +34,13 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.ensureActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.koin.android.ext.android.inject import splitties.init.appCtx import kotlin.coroutines.coroutineContext class WebDavFragment : BaseFragment(R.layout.fragment_webdav_auth) { + private val webDavBackupUseCase by inject() private var _binding: FragmentWebdavAuthBinding? = null private val binding get() = _binding!! private var restoreJob: Job? = null @@ -105,7 +106,7 @@ class WebDavFragment : BaseFragment(R.layout.fragment_webdav_auth) { ) saveWebDavConfig(config) lifecycleScope.launch { - testWebDav() + webDavBackupUseCase.test() } } @@ -120,7 +121,7 @@ class WebDavFragment : BaseFragment(R.layout.fragment_webdav_auth) { binding.progressRestore.visible() binding.btnRestore.text = "" saveWebDavConfig(config) - AppWebDav.upConfig() + webDavBackupUseCase.refreshConfig() restore() } finally { binding.progressRestore.gone() @@ -169,8 +170,8 @@ class WebDavFragment : BaseFragment(R.layout.fragment_webdav_auth) { } private suspend fun showRestoreDialog(context: Context) { - val names = withContext(IO) { AppWebDav.getBackupNames() } - if (AppWebDav.isJianGuoYun && names.size > 700) { + val names = withContext(IO) { webDavBackupUseCase.getBackupNames() } + if (webDavBackupUseCase.isJianGuoYun && names.size > 700) { context.toastOnUi("由于坚果云限制列出文件数量,部分备份可能未显示,请及时清理旧备份") } if (names.isNotEmpty()) { @@ -194,7 +195,7 @@ class WebDavFragment : BaseFragment(R.layout.fragment_webdav_auth) { waitDialog.setText("恢复中…") waitDialog.show() val task = Coroutine.async { - AppWebDav.restoreWebDav(name) + webDavBackupUseCase.restore(name) }.onError { AppLog.put("WebDav恢复出错\n${it.localizedMessage}", it) appCtx.toastOnUi("WebDav恢复出错\n${it.localizedMessage}") diff --git a/app/src/main/java/io/legado/app/ui/widget/components/book/SearchBookItem.kt b/app/src/main/java/io/legado/app/ui/widget/components/book/SearchBookItem.kt index aee32bf59..01d96e5e5 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/book/SearchBookItem.kt +++ b/app/src/main/java/io/legado/app/ui/widget/components/book/SearchBookItem.kt @@ -32,7 +32,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp 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.widget.components.cover.Cover import io.legado.app.ui.widget.components.text.AppText @@ -46,8 +46,22 @@ fun SearchBookListItem( modifier: Modifier = Modifier, ) { 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 -> { { androidx.compose.material3.Icon( @@ -158,8 +172,22 @@ fun SearchBookGridItem( modifier: Modifier = Modifier, ) { 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.SAME_NAME_AUTHOR -> "同名书籍" BookShelfState.NOT_IN_SHELF -> null diff --git a/app/src/main/java/io/legado/app/ui/widget/components/explore/ExploreKindMultiTypeItem.kt b/app/src/main/java/io/legado/app/ui/widget/components/explore/ExploreKindMultiTypeItem.kt index d53a4b6fc..71092572e 100644 --- a/app/src/main/java/io/legado/app/ui/widget/components/explore/ExploreKindMultiTypeItem.kt +++ b/app/src/main/java/io/legado/app/ui/widget/components/explore/ExploreKindMultiTypeItem.kt @@ -37,7 +37,7 @@ import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import io.legado.app.App 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.ui.widget.components.menuItem.RoundDropdownMenu import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem diff --git a/app/src/main/java/io/legado/app/domain/usecase/ExploreKindUiUseCase.kt b/app/src/main/java/io/legado/app/ui/widget/components/explore/ExploreKindUiUseCase.kt similarity index 98% rename from app/src/main/java/io/legado/app/domain/usecase/ExploreKindUiUseCase.kt rename to app/src/main/java/io/legado/app/ui/widget/components/explore/ExploreKindUiUseCase.kt index 80058761a..11b9f588b 100644 --- a/app/src/main/java/io/legado/app/domain/usecase/ExploreKindUiUseCase.kt +++ b/app/src/main/java/io/legado/app/ui/widget/components/explore/ExploreKindUiUseCase.kt @@ -1,4 +1,4 @@ -package io.legado.app.domain.usecase +package io.legado.app.ui.widget.components.explore import androidx.appcompat.app.AppCompatActivity import com.script.rhino.runScriptWithContext