Explorar o código

[优化] 架构优化

HapeLee hai 6 meses
pai
achega
fce37cb1a6
Modificáronse 90 ficheiros con 1102 adicións e 349 borrados
  1. 63 0
      AGENTS.md
  2. 20 0
      app/src/main/java/io/legado/app/data/repository/AppStartupRepository.kt
  3. 20 0
      app/src/main/java/io/legado/app/data/repository/BookCacheCleanupRepository.kt
  4. 81 0
      app/src/main/java/io/legado/app/data/repository/BookDomainRepositoryImpl.kt
  5. 17 0
      app/src/main/java/io/legado/app/data/repository/BookSourceCallbackRepository.kt
  6. 16 0
      app/src/main/java/io/legado/app/data/repository/CacheBookDownloadRepository.kt
  7. 12 0
      app/src/main/java/io/legado/app/data/repository/DatabaseMaintenanceRepository.kt
  8. 4 0
      app/src/main/java/io/legado/app/data/repository/DictRuleRepository.kt
  9. 14 0
      app/src/main/java/io/legado/app/data/repository/LocalBookRepository.kt
  10. 21 0
      app/src/main/java/io/legado/app/data/repository/RemoteBookRepository.kt
  11. 4 4
      app/src/main/java/io/legado/app/data/repository/SearchRepository.kt
  12. 36 0
      app/src/main/java/io/legado/app/data/repository/WebDavBackupRepository.kt
  13. 40 0
      app/src/main/java/io/legado/app/data/repository/WebDavReadingProgressRepository.kt
  14. 53 1
      app/src/main/java/io/legado/app/di/appModule.kt
  15. 6 0
      app/src/main/java/io/legado/app/domain/gateway/AppStartupGateway.kt
  16. 6 0
      app/src/main/java/io/legado/app/domain/gateway/BookCacheCleanupGateway.kt
  17. 5 0
      app/src/main/java/io/legado/app/domain/gateway/BookCacheDownloadGateway.kt
  18. 5 0
      app/src/main/java/io/legado/app/domain/gateway/BookSourceCallbackGateway.kt
  19. 5 0
      app/src/main/java/io/legado/app/domain/gateway/DatabaseMaintenanceGateway.kt
  20. 5 0
      app/src/main/java/io/legado/app/domain/gateway/LocalBookGateway.kt
  21. 10 0
      app/src/main/java/io/legado/app/domain/gateway/ReadingProgressGateway.kt
  22. 13 0
      app/src/main/java/io/legado/app/domain/gateway/WebDavBackupGateway.kt
  23. 6 0
      app/src/main/java/io/legado/app/domain/model/BookGroupAssignment.kt
  24. 2 2
      app/src/main/java/io/legado/app/domain/model/BookShelfState.kt
  25. 9 0
      app/src/main/java/io/legado/app/domain/model/CacheableBook.kt
  26. 7 0
      app/src/main/java/io/legado/app/domain/model/DeletableBook.kt
  27. 10 0
      app/src/main/java/io/legado/app/domain/model/ReadingProgress.kt
  28. 6 0
      app/src/main/java/io/legado/app/domain/model/WebDavBackup.kt
  29. 15 0
      app/src/main/java/io/legado/app/domain/repository/BookDomainRepository.kt
  30. 16 0
      app/src/main/java/io/legado/app/domain/usecase/AppStartupMaintenanceUseCase.kt
  31. 12 32
      app/src/main/java/io/legado/app/domain/usecase/BatchCacheDownloadUseCase.kt
  32. 15 0
      app/src/main/java/io/legado/app/domain/usecase/CacheBookChaptersUseCase.kt
  33. 9 10
      app/src/main/java/io/legado/app/domain/usecase/ClearBookCacheUseCase.kt
  34. 11 17
      app/src/main/java/io/legado/app/domain/usecase/DeleteBooksUseCase.kt
  35. 16 0
      app/src/main/java/io/legado/app/domain/usecase/GetReadingProgressUseCase.kt
  36. 12 0
      app/src/main/java/io/legado/app/domain/usecase/RemoveBookGroupAssignmentUseCase.kt
  37. 31 0
      app/src/main/java/io/legado/app/domain/usecase/ResolveBookShelfStateUseCase.kt
  38. 11 0
      app/src/main/java/io/legado/app/domain/usecase/ShrinkDatabaseUseCase.kt
  39. 9 13
      app/src/main/java/io/legado/app/domain/usecase/UpdateBooksGroupUseCase.kt
  40. 13 0
      app/src/main/java/io/legado/app/domain/usecase/UploadReadingProgressUseCase.kt
  41. 36 0
      app/src/main/java/io/legado/app/domain/usecase/WebDavBackupUseCase.kt
  42. 18 6
      app/src/main/java/io/legado/app/help/AppWebDav.kt
  43. 3 2
      app/src/main/java/io/legado/app/help/book/BookExtensions.kt
  44. 0 11
      app/src/main/java/io/legado/app/help/config/AppConfig.kt
  45. 4 3
      app/src/main/java/io/legado/app/model/localBook/LocalBook.kt
  46. 2 2
      app/src/main/java/io/legado/app/model/remote/RemoteBookWebDav.kt
  47. 3 3
      app/src/main/java/io/legado/app/ui/association/FileAssociationActivity.kt
  48. 5 7
      app/src/main/java/io/legado/app/ui/book/cache/CacheScreen.kt
  49. 20 8
      app/src/main/java/io/legado/app/ui/book/cache/CacheViewModel.kt
  50. 14 15
      app/src/main/java/io/legado/app/ui/book/explore/ExploreShowScreen.kt
  51. 39 27
      app/src/main/java/io/legado/app/ui/book/explore/ExploreShowViewModel.kt
  52. 5 4
      app/src/main/java/io/legado/app/ui/book/group/GroupViewModel.kt
  53. 11 10
      app/src/main/java/io/legado/app/ui/book/import/local/ImportBookViewModel.kt
  54. 1 7
      app/src/main/java/io/legado/app/ui/book/import/remote/RemoteBookScreen.kt
  55. 17 11
      app/src/main/java/io/legado/app/ui/book/import/remote/RemoteBookViewModel.kt
  56. 3 3
      app/src/main/java/io/legado/app/ui/book/import/remote/ServerConfigDialog.kt
  57. 9 6
      app/src/main/java/io/legado/app/ui/book/import/remote/ServerConfigViewModel.kt
  58. 3 4
      app/src/main/java/io/legado/app/ui/book/import/remote/ServersDialog.kt
  59. 8 4
      app/src/main/java/io/legado/app/ui/book/import/remote/ServersViewModel.kt
  60. 2 1
      app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt
  61. 2 0
      app/src/main/java/io/legado/app/ui/book/info/BookInfoContract.kt
  62. 2 3
      app/src/main/java/io/legado/app/ui/book/info/BookInfoScreen.kt
  63. 8 2
      app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt
  64. 16 3
      app/src/main/java/io/legado/app/ui/book/manga/ReadMangaViewModel.kt
  65. 3 6
      app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt
  66. 38 3
      app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt
  67. 0 7
      app/src/main/java/io/legado/app/ui/book/search/BookKey.kt
  68. 1 1
      app/src/main/java/io/legado/app/ui/book/search/SearchAdapter.kt
  69. 7 1
      app/src/main/java/io/legado/app/ui/book/search/SearchContract.kt
  70. 5 8
      app/src/main/java/io/legado/app/ui/book/search/SearchScreen.kt
  71. 35 19
      app/src/main/java/io/legado/app/ui/book/search/SearchViewModel.kt
  72. 19 8
      app/src/main/java/io/legado/app/ui/book/toc/TocViewModel.kt
  73. 8 10
      app/src/main/java/io/legado/app/ui/config/backupConfig/BackupConfigViewModel.kt
  74. 0 5
      app/src/main/java/io/legado/app/ui/config/importBookConfig/ImportBookConfig.kt
  75. 1 1
      app/src/main/java/io/legado/app/ui/config/otherConfig/OtherConfig.kt
  76. 9 7
      app/src/main/java/io/legado/app/ui/config/otherConfig/OtherConfigViewModel.kt
  77. 2 2
      app/src/main/java/io/legado/app/ui/dict/DictDialog.kt
  78. 7 4
      app/src/main/java/io/legado/app/ui/dict/DictViewModel.kt
  79. 2 3
      app/src/main/java/io/legado/app/ui/main/MainActivity.kt
  80. 12 11
      app/src/main/java/io/legado/app/ui/main/MainViewModel.kt
  81. 2 10
      app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt
  82. 2 0
      app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfUiState.kt
  83. 7 5
      app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt
  84. 1 1
      app/src/main/java/io/legado/app/ui/main/explore/ExploreScreen.kt
  85. 0 1
      app/src/main/java/io/legado/app/ui/rss/read/RssReadRouteScreen.kt
  86. 3 3
      app/src/main/java/io/legado/app/ui/welcome/BookFolderFragment.kt
  87. 8 7
      app/src/main/java/io/legado/app/ui/welcome/WebDavFragment.kt
  88. 31 3
      app/src/main/java/io/legado/app/ui/widget/components/book/SearchBookItem.kt
  89. 1 1
      app/src/main/java/io/legado/app/ui/widget/components/explore/ExploreKindMultiTypeItem.kt
  90. 1 1
      app/src/main/java/io/legado/app/ui/widget/components/explore/ExploreKindUiUseCase.kt

+ 63 - 0
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.

+ 20 - 0
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())
+        }
+    }
+}

+ 20 - 0
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
+    }
+}

+ 81 - 0
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<String>): List<Book> {
+        if (bookUrls.isEmpty()) return emptyList()
+        return bookUrls.mapNotNull { bookDao.getBook(it) }
+    }
+
+    override suspend fun getCacheableBooks(bookUrls: Set<String>): List<CacheableBook> {
+        return getBooks(bookUrls).map { book ->
+            CacheableBook(
+                bookUrl = book.bookUrl,
+                isLocal = book.isLocal,
+                isAudio = book.isAudio,
+                durChapterIndex = book.durChapterIndex,
+                lastChapterIndex = book.lastChapterIndex
+            )
+        }
+    }
+
+    override suspend fun getDeletableBooks(bookUrls: Set<String>): List<DeletableBook> {
+        return getBooks(bookUrls).map { book ->
+            DeletableBook(
+                bookUrl = book.bookUrl,
+                origin = book.origin,
+                isLocal = book.isLocal
+            )
+        }
+    }
+
+    override suspend fun getBookGroupAssignments(bookUrls: Set<String>): List<BookGroupAssignment> {
+        return getBooks(bookUrls).map { book ->
+            BookGroupAssignment(
+                bookUrl = book.bookUrl,
+                group = book.group
+            )
+        }
+    }
+
+    override suspend fun updateBookGroups(assignments: List<BookGroupAssignment>) {
+        if (assignments.isEmpty()) return
+        val groups = assignments.associateBy { it.bookUrl }
+        val books = getBooks(groups.keys).mapNotNull { book ->
+            groups[book.bookUrl]?.let { assignment ->
+                if (book.group == assignment.group) null else book.copy(group = assignment.group)
+            }
+        }
+        if (books.isNotEmpty()) {
+            bookDao.update(*books.toTypedArray())
+        }
+    }
+
+    override suspend fun removeGroupFromBooks(groupId: Long) {
+        bookDao.removeGroup(groupId)
+    }
+
+    override suspend fun deleteBooks(bookUrls: Set<String>) {
+        val books = getBooks(bookUrls)
+        if (books.isNotEmpty()) {
+            bookDao.delete(*books.toTypedArray())
+        }
+    }
+
+    override suspend fun deleteChaptersByBook(bookUrl: String) {
+        bookChapterDao.delByBook(bookUrl)
+    }
+}

+ 17 - 0
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)
+    }
+}

+ 16 - 0
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<Int>) {
+        val book = bookDao.getBook(bookUrl) ?: return
+        CacheBook.start(appCtx, book, chapterIndices)
+    }
+}

+ 12 - 0
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")
+    }
+}

+ 4 - 0
app/src/main/java/io/legado/app/data/repository/DictRuleRepository.kt

@@ -22,6 +22,10 @@ class DictRuleRepository {
         return dao.all
         return dao.all
     }
     }
 
 
+    fun getEnabled(): List<DictRule> {
+        return dao.enabled
+    }
+
     suspend fun insert(vararg rule: DictRule) {
     suspend fun insert(vararg rule: DictRule) {
         withContext(Dispatchers.IO) {
         withContext(Dispatchers.IO) {
             dao.insert(*rule)
             dao.insert(*rule)

+ 14 - 0
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)
+    }
+}

+ 21 - 0
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.constant.BookType
 import io.legado.app.data.AppDatabase
 import io.legado.app.data.AppDatabase
 import io.legado.app.data.entities.Book
 import io.legado.app.data.entities.Book
+import io.legado.app.data.entities.Server
 import io.legado.app.exception.NoStackTraceException
 import io.legado.app.exception.NoStackTraceException
 import io.legado.app.help.AppWebDav
 import io.legado.app.help.AppWebDav
 import io.legado.app.help.book.getRemoteUrl
 import io.legado.app.help.book.getRemoteUrl
@@ -90,6 +91,10 @@ class RemoteBookRepository(
             }
             }
     }
     }
 
 
+    fun getDefaultBookWebDav(): RemoteBookWebDav? {
+        return AppWebDav.defaultBookWebDav
+    }
+
     suspend fun loadBooks(
     suspend fun loadBooks(
         webDav: RemoteBookWebDav,
         webDav: RemoteBookWebDav,
         path: String?
         path: String?
@@ -120,4 +125,20 @@ class RemoteBookRepository(
         return appDb.bookDao.flowLocal()
         return appDb.bookDao.flowLocal()
     }
     }
 
 
+    fun flowServers(): Flow<List<Server>> {
+        return appDb.serverDao.observeAll()
+    }
+
+    suspend fun getServer(id: Long): Server? {
+        return appDb.serverDao.get(id)
+    }
+
+    suspend fun saveServer(server: Server) {
+        appDb.serverDao.insert(server)
+    }
+
+    suspend fun deleteServer(server: Server) {
+        appDb.serverDao.delete(server)
+    }
+
 }
 }

+ 4 - 4
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.BookSourcePart
 import io.legado.app.data.entities.SearchBook
 import io.legado.app.data.entities.SearchBook
 import io.legado.app.data.entities.SearchKeyword
 import io.legado.app.data.entities.SearchKeyword
+import io.legado.app.domain.usecase.BookShelfKey
 import io.legado.app.help.book.isNotShelf
 import io.legado.app.help.book.isNotShelf
 import io.legado.app.model.webBook.SearchModel
 import io.legado.app.model.webBook.SearchModel
-import io.legado.app.ui.book.search.BookKey
 import io.legado.app.ui.book.search.SearchScope
 import io.legado.app.ui.book.search.SearchScope
 import io.legado.app.ui.main.bookshelf.BookShelfItem
 import io.legado.app.ui.main.bookshelf.BookShelfItem
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.CoroutineScope
@@ -23,7 +23,7 @@ import kotlinx.coroutines.withContext
 interface SearchRepository {
 interface SearchRepository {
     val enabledGroups: Flow<List<String>>
     val enabledGroups: Flow<List<String>>
     val enabledSources: Flow<List<BookSourcePart>>
     val enabledSources: Flow<List<BookSourcePart>>
-    val bookshelfKeys: Flow<Set<BookKey>>
+    val bookshelfKeys: Flow<Set<BookShelfKey>>
 
 
     fun searchBookshelf(query: String): Flow<List<BookShelfItem>>
     fun searchBookshelf(query: String): Flow<List<BookShelfItem>>
     fun searchHistory(query: String): Flow<List<SearchKeyword>>
     fun searchHistory(query: String): Flow<List<SearchKeyword>>
@@ -68,9 +68,9 @@ class SearchRepositoryImpl(
     override val enabledGroups: Flow<List<String>> = appDb.bookSourceDao.flowEnabledGroups()
     override val enabledGroups: Flow<List<String>> = appDb.bookSourceDao.flowEnabledGroups()
     override val enabledSources: Flow<List<BookSourcePart>> = appDb.bookSourceDao.flowEnabled()
     override val enabledSources: Flow<List<BookSourcePart>> = appDb.bookSourceDao.flowEnabled()
 
 
-    override val bookshelfKeys: Flow<Set<BookKey>> = appDb.bookDao.flowBookShelf().map { books ->
+    override val bookshelfKeys: Flow<Set<BookShelfKey>> = appDb.bookDao.flowBookShelf().map { books ->
         books.filterNot { it.isNotShelf }
         books.filterNot { it.isNotShelf }
-            .map { book -> BookKey(book.name, book.author, book.bookUrl) }
+            .map { book -> BookShelfKey(book.name, book.author, book.bookUrl) }
             .toSet()
             .toSet()
     }
     }
 
 

+ 36 - 0
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<String> {
+        return AppWebDav.getBackupNames()
+    }
+
+    override suspend fun getLatestBackup(): WebDavBackup? {
+        return AppWebDav.lastBackUp().getOrNull()?.let {
+            WebDavBackup(
+                name = it.displayName,
+                lastModify = it.lastModify
+            )
+        }
+    }
+
+    override suspend fun restore(name: String) {
+        AppWebDav.restoreWebDav(name)
+    }
+}

+ 40 - 0
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
+    }
+}

+ 53 - 1
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.ImageDecoderDecoder
 import coil.decode.SvgDecoder
 import coil.decode.SvgDecoder
 import io.legado.app.data.AppDatabase
 import io.legado.app.data.AppDatabase
+import io.legado.app.data.repository.AppStartupRepository
+import io.legado.app.data.repository.BookCacheCleanupRepository
+import io.legado.app.data.repository.BookDomainRepositoryImpl
 import io.legado.app.data.repository.BookGroupRepository
 import io.legado.app.data.repository.BookGroupRepository
 import io.legado.app.data.repository.BookRepository
 import io.legado.app.data.repository.BookRepository
+import io.legado.app.data.repository.BookSourceCallbackRepository
+import io.legado.app.data.repository.CacheBookDownloadRepository
+import io.legado.app.data.repository.DatabaseMaintenanceRepository
 import io.legado.app.data.repository.DirectLinkUploadRepository
 import io.legado.app.data.repository.DirectLinkUploadRepository
+import io.legado.app.data.repository.DictRuleRepository
 import io.legado.app.data.repository.ExploreRepository
 import io.legado.app.data.repository.ExploreRepository
 import io.legado.app.data.repository.ExploreRepositoryImpl
 import io.legado.app.data.repository.ExploreRepositoryImpl
+import io.legado.app.data.repository.LocalBookRepository
 import io.legado.app.data.repository.ReadRecordRepository
 import io.legado.app.data.repository.ReadRecordRepository
 import io.legado.app.data.repository.RemoteBookRepository
 import io.legado.app.data.repository.RemoteBookRepository
 import io.legado.app.data.repository.SearchRepository
 import io.legado.app.data.repository.SearchRepository
 import io.legado.app.data.repository.SearchRepositoryImpl
 import io.legado.app.data.repository.SearchRepositoryImpl
 import io.legado.app.data.repository.SearchContentRepository
 import io.legado.app.data.repository.SearchContentRepository
 import io.legado.app.data.repository.UploadRepository
 import io.legado.app.data.repository.UploadRepository
+import io.legado.app.data.repository.WebDavBackupRepository
+import io.legado.app.data.repository.WebDavReadingProgressRepository
+import io.legado.app.domain.gateway.BookCacheCleanupGateway
+import io.legado.app.domain.gateway.AppStartupGateway
+import io.legado.app.domain.gateway.BookCacheDownloadGateway
+import io.legado.app.domain.gateway.BookSourceCallbackGateway
+import io.legado.app.domain.gateway.DatabaseMaintenanceGateway
+import io.legado.app.domain.gateway.LocalBookGateway
+import io.legado.app.domain.gateway.ReadingProgressGateway
+import io.legado.app.domain.gateway.WebDavBackupGateway
+import io.legado.app.domain.repository.BookDomainRepository
+import io.legado.app.domain.usecase.AppStartupMaintenanceUseCase
 import io.legado.app.domain.usecase.BatchCacheDownloadUseCase
 import io.legado.app.domain.usecase.BatchCacheDownloadUseCase
+import io.legado.app.domain.usecase.CacheBookChaptersUseCase
 import io.legado.app.domain.usecase.ClearBookCacheUseCase
 import io.legado.app.domain.usecase.ClearBookCacheUseCase
 import io.legado.app.domain.usecase.DeleteBooksUseCase
 import io.legado.app.domain.usecase.DeleteBooksUseCase
-import io.legado.app.domain.usecase.ExploreKindUiUseCase
+import io.legado.app.domain.usecase.GetReadingProgressUseCase
+import io.legado.app.domain.usecase.RemoveBookGroupAssignmentUseCase
+import io.legado.app.ui.widget.components.explore.ExploreKindUiUseCase
+import io.legado.app.domain.usecase.ResolveBookShelfStateUseCase
+import io.legado.app.domain.usecase.ShrinkDatabaseUseCase
 import io.legado.app.domain.usecase.UpdateBooksGroupUseCase
 import io.legado.app.domain.usecase.UpdateBooksGroupUseCase
+import io.legado.app.domain.usecase.UploadReadingProgressUseCase
+import io.legado.app.domain.usecase.WebDavBackupUseCase
 import io.legado.app.help.coil.CoverFetcher
 import io.legado.app.help.coil.CoverFetcher
 import io.legado.app.help.coil.CoverInterceptor
 import io.legado.app.help.coil.CoverInterceptor
 import io.legado.app.help.http.okHttpClient
 import io.legado.app.help.http.okHttpClient
@@ -35,6 +62,8 @@ import io.legado.app.ui.book.explore.ExploreShowViewModel
 import io.legado.app.ui.book.group.GroupViewModel
 import io.legado.app.ui.book.group.GroupViewModel
 import io.legado.app.ui.book.import.local.ImportBookViewModel
 import io.legado.app.ui.book.import.local.ImportBookViewModel
 import io.legado.app.ui.book.import.remote.RemoteBookViewModel
 import io.legado.app.ui.book.import.remote.RemoteBookViewModel
+import io.legado.app.ui.book.import.remote.ServerConfigViewModel
+import io.legado.app.ui.book.import.remote.ServersViewModel
 import io.legado.app.ui.book.info.BookInfoViewModel
 import io.legado.app.ui.book.info.BookInfoViewModel
 import io.legado.app.ui.book.readRecord.ReadRecordViewModel
 import io.legado.app.ui.book.readRecord.ReadRecordViewModel
 import io.legado.app.ui.book.search.SearchViewModel
 import io.legado.app.ui.book.search.SearchViewModel
@@ -47,6 +76,7 @@ import io.legado.app.ui.config.coverConfig.CoverConfigViewModel
 import io.legado.app.ui.config.otherConfig.OtherConfigViewModel
 import io.legado.app.ui.config.otherConfig.OtherConfigViewModel
 import io.legado.app.ui.config.readConfig.ReadConfigViewModel
 import io.legado.app.ui.config.readConfig.ReadConfigViewModel
 import io.legado.app.ui.config.themeConfig.ThemeConfigViewModel
 import io.legado.app.ui.config.themeConfig.ThemeConfigViewModel
+import io.legado.app.ui.dict.DictViewModel
 import io.legado.app.ui.dict.rule.DictRuleViewModel
 import io.legado.app.ui.dict.rule.DictRuleViewModel
 import io.legado.app.ui.main.MainViewModel
 import io.legado.app.ui.main.MainViewModel
 import io.legado.app.ui.main.bookshelf.BookshelfViewModel
 import io.legado.app.ui.main.bookshelf.BookshelfViewModel
@@ -76,16 +106,34 @@ val appModule = module {
     singleOf(::ReadRecordRepository)
     singleOf(::ReadRecordRepository)
     singleOf(::BookRepository)
     singleOf(::BookRepository)
     singleOf(::BookGroupRepository)
     singleOf(::BookGroupRepository)
+    singleOf(::DictRuleRepository)
     singleOf(::SearchContentRepository)
     singleOf(::SearchContentRepository)
     singleOf(::RemoteBookRepository)
     singleOf(::RemoteBookRepository)
     singleOf(::ExploreKindUiUseCase)
     singleOf(::ExploreKindUiUseCase)
+    singleOf(::AppStartupMaintenanceUseCase)
     singleOf(::BatchCacheDownloadUseCase)
     singleOf(::BatchCacheDownloadUseCase)
+    singleOf(::CacheBookChaptersUseCase)
     singleOf(::ClearBookCacheUseCase)
     singleOf(::ClearBookCacheUseCase)
     singleOf(::DeleteBooksUseCase)
     singleOf(::DeleteBooksUseCase)
+    singleOf(::GetReadingProgressUseCase)
+    singleOf(::RemoveBookGroupAssignmentUseCase)
     singleOf(::UpdateBooksGroupUseCase)
     singleOf(::UpdateBooksGroupUseCase)
+    singleOf(::UploadReadingProgressUseCase)
+    singleOf(::ResolveBookShelfStateUseCase)
+    singleOf(::ShrinkDatabaseUseCase)
+    singleOf(::WebDavBackupUseCase)
     singleOf(::CacheConfig)
     singleOf(::CacheConfig)
 
 
     single<UploadRepository> { DirectLinkUploadRepository() }
     single<UploadRepository> { DirectLinkUploadRepository() }
+    single<AppStartupGateway> { AppStartupRepository(get()) }
+    single<BookCacheDownloadGateway> { CacheBookDownloadRepository(get()) }
+    single<BookCacheCleanupGateway> { BookCacheCleanupRepository(get()) }
+    single<BookSourceCallbackGateway> { BookSourceCallbackRepository(get(), get()) }
+    single<LocalBookGateway> { LocalBookRepository(get()) }
+    single<DatabaseMaintenanceGateway> { DatabaseMaintenanceRepository(get()) }
+    single<WebDavBackupGateway> { WebDavBackupRepository() }
+    single<ReadingProgressGateway> { WebDavReadingProgressRepository() }
+    single<BookDomainRepository> { BookDomainRepositoryImpl(get(), get()) }
     single<ExploreRepository> { ExploreRepositoryImpl(get()) }
     single<ExploreRepository> { ExploreRepositoryImpl(get()) }
     single<SearchRepository> { SearchRepositoryImpl(get()) }
     single<SearchRepository> { SearchRepositoryImpl(get()) }
 
 
@@ -106,6 +154,7 @@ val appModule = module {
     }
     }
 
 
     viewModelOf(::DictRuleViewModel)
     viewModelOf(::DictRuleViewModel)
+    viewModelOf(::DictViewModel)
     viewModelOf(::RssSourceViewModel)
     viewModelOf(::RssSourceViewModel)
     viewModelOf(::RssSortViewModel)
     viewModelOf(::RssSortViewModel)
     viewModelOf(::RssArticlesViewModel)
     viewModelOf(::RssArticlesViewModel)
@@ -127,6 +176,8 @@ val appModule = module {
     viewModelOf(::TocViewModel)
     viewModelOf(::TocViewModel)
     viewModelOf(::ImportBookViewModel)
     viewModelOf(::ImportBookViewModel)
     viewModelOf(::RemoteBookViewModel)
     viewModelOf(::RemoteBookViewModel)
+    viewModelOf(::ServerConfigViewModel)
+    viewModelOf(::ServersViewModel)
     viewModelOf(::BookInfoViewModel)
     viewModelOf(::BookInfoViewModel)
     viewModelOf(::ChangeCoverViewModel)
     viewModelOf(::ChangeCoverViewModel)
     viewModelOf(::ChangeBookSourceComposeViewModel)
     viewModelOf(::ChangeBookSourceComposeViewModel)
@@ -142,6 +193,7 @@ val appModule = module {
             bookChapterDao = get(),
             bookChapterDao = get(),
             cacheConfig = get(),
             cacheConfig = get(),
             batchCacheDownloadUseCase = get(),
             batchCacheDownloadUseCase = get(),
+            cacheBookChaptersUseCase = get(),
             clearBookCacheUseCase = get(),
             clearBookCacheUseCase = get(),
             deleteBooksUseCase = get(),
             deleteBooksUseCase = get(),
             updateBooksGroupUseCase = get()
             updateBooksGroupUseCase = get()

+ 6 - 0
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()
+}

+ 6 - 0
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
+}

+ 5 - 0
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<Int>)
+}

+ 5 - 0
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)
+}

+ 5 - 0
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()
+}

+ 5 - 0
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)
+}

+ 10 - 0
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?
+}

+ 13 - 0
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<String>
+    suspend fun getLatestBackup(): WebDavBackup?
+    suspend fun restore(name: String)
+}

+ 6 - 0
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
+)

+ 2 - 2
app/src/main/java/io/legado/app/model/BookShelfState.kt → 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 {
 enum class BookShelfState {
     IN_SHELF,
     IN_SHELF,
     SAME_NAME_AUTHOR,
     SAME_NAME_AUTHOR,
     NOT_IN_SHELF
     NOT_IN_SHELF
-}
+}

+ 9 - 0
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
+)

+ 7 - 0
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
+)

+ 10 - 0
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?
+)

+ 6 - 0
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
+)

+ 15 - 0
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<String>): List<CacheableBook>
+    suspend fun getDeletableBooks(bookUrls: Set<String>): List<DeletableBook>
+    suspend fun getBookGroupAssignments(bookUrls: Set<String>): List<BookGroupAssignment>
+    suspend fun updateBookGroups(assignments: List<BookGroupAssignment>)
+    suspend fun removeGroupFromBooks(groupId: Long)
+    suspend fun deleteBooks(bookUrls: Set<String>)
+    suspend fun deleteChaptersByBook(bookUrl: String)
+}

+ 16 - 0
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()
+    }
+}

+ 12 - 32
app/src/main/java/io/legado/app/domain/usecase/BatchCacheDownloadUseCase.kt

@@ -1,63 +1,43 @@
 package io.legado.app.domain.usecase
 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(
 class BatchCacheDownloadUseCase(
-    private val bookDao: BookDao
+    private val bookRepository: BookDomainRepository,
+    private val bookCacheDownloadGateway: BookCacheDownloadGateway
 ) {
 ) {
 
 
     suspend fun execute(
     suspend fun execute(
-        context: Context,
         bookUrls: Set<String>,
         bookUrls: Set<String>,
         downloadAllChapters: Boolean,
         downloadAllChapters: Boolean,
         skipAudioBooks: Boolean = false
         skipAudioBooks: Boolean = false
     ): Int {
     ): Int {
         if (bookUrls.isEmpty()) return 0
         if (bookUrls.isEmpty()) return 0
         var count = 0
         var count = 0
-        bookUrls.forEach { bookUrl ->
-            val book = bookDao.getBook(bookUrl) ?: return@forEach
-            if (startIfNeeded(context, book, downloadAllChapters, skipAudioBooks)) {
+        bookRepository.getCacheableBooks(bookUrls).forEach { book ->
+            if (startIfNeeded(book, downloadAllChapters, skipAudioBooks)) {
                 count++
                 count++
             }
             }
         }
         }
         return count
         return count
     }
     }
 
 
-    fun execute(
-        context: Context,
-        books: List<Book>,
-        downloadAllChapters: Boolean,
-        skipAudioBooks: Boolean = false
-    ): Int {
-        if (books.isEmpty()) return 0
-        var count = 0
-        books.forEach { book ->
-            if (startIfNeeded(context, book, downloadAllChapters, skipAudioBooks)) {
-                count++
-            }
-        }
-        return count
-    }
-
-    private fun startIfNeeded(
-        context: Context,
-        book: Book,
+    private suspend fun startIfNeeded(
+        book: CacheableBook,
         downloadAllChapters: Boolean,
         downloadAllChapters: Boolean,
         skipAudioBooks: Boolean
         skipAudioBooks: Boolean
     ): 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) {
         val indices = if (downloadAllChapters) {
             (0..book.lastChapterIndex).toList()
             (0..book.lastChapterIndex).toList()
         } else {
         } else {
             (book.durChapterIndex..book.lastChapterIndex).toList()
             (book.durChapterIndex..book.lastChapterIndex).toList()
         }
         }
         if (indices.isEmpty()) return false
         if (indices.isEmpty()) return false
-        CacheBook.start(context, book, indices)
+        bookCacheDownloadGateway.start(book.bookUrl, indices)
         return true
         return true
     }
     }
 }
 }

+ 15 - 0
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>): Int {
+        val indices = chapterIndices.distinct()
+        if (indices.isEmpty()) return 0
+        bookCacheDownloadGateway.start(bookUrl, indices)
+        return indices.size
+    }
+}

+ 9 - 10
app/src/main/java/io/legado/app/domain/usecase/ClearBookCacheUseCase.kt

@@ -1,22 +1,21 @@
 package io.legado.app.domain.usecase
 package io.legado.app.domain.usecase
 
 
-import io.legado.app.data.dao.BookDao
-import io.legado.app.data.entities.Book
-import io.legado.app.help.book.BookHelp
+import io.legado.app.domain.gateway.BookCacheCleanupGateway
 
 
 class ClearBookCacheUseCase(
 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<String>): List<String> {
     suspend fun execute(bookUrls: Set<String>): List<String> {
         if (bookUrls.isEmpty()) return emptyList()
         if (bookUrls.isEmpty()) return emptyList()
-        return bookUrls.mapNotNull { bookUrl ->
-            bookDao.getBook(bookUrl)?.let { execute(it) }
-        }
+        return bookUrls.mapNotNull { execute(it) }
     }
     }
 }
 }

+ 11 - 17
app/src/main/java/io/legado/app/domain/usecase/DeleteBooksUseCase.kt

@@ -1,33 +1,27 @@
 package io.legado.app.domain.usecase
 package io.legado.app.domain.usecase
 
 
-import io.legado.app.data.dao.BookChapterDao
-import io.legado.app.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(
 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<String>, deleteOriginal: Boolean): List<String> {
     suspend fun execute(bookUrls: Set<String>, deleteOriginal: Boolean): List<String> {
         if (bookUrls.isEmpty()) return emptyList()
         if (bookUrls.isEmpty()) return emptyList()
-        val books = bookUrls.mapNotNull { bookDao.getBook(it) }
+        val books = bookRepository.getDeletableBooks(bookUrls)
         books.forEach { book ->
         books.forEach { book ->
             if (book.isLocal) {
             if (book.isLocal) {
-                LocalBook.deleteBook(book, deleteOriginal)
+                localBookGateway.deleteBook(book.bookUrl, deleteOriginal)
             } else {
             } else {
-                val source = bookSourceDao.getBookSource(book.origin)
-                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 }
         return books.map { it.bookUrl }
     }
     }
 }
 }

+ 16 - 0
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)
+    }
+}

+ 12 - 0
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)
+    }
+}

+ 31 - 0
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<BookShelfKey>
+    ): BookShelfState {
+        val exactMatch = shelf.any {
+            it.name == name && it.author == author && it.url == url
+        }
+        if (exactMatch) return BookShelfState.IN_SHELF
+
+        val sameNameAuthor = shelf.any {
+            it.name == name && it.author == author && it.url != url
+        }
+        if (sameNameAuthor) return BookShelfState.SAME_NAME_AUTHOR
+
+        return BookShelfState.NOT_IN_SHELF
+    }
+}

+ 11 - 0
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()
+    }
+}

+ 9 - 13
app/src/main/java/io/legado/app/domain/usecase/UpdateBooksGroupUseCase.kt

@@ -1,9 +1,9 @@
 package io.legado.app.domain.usecase
 package io.legado.app.domain.usecase
 
 
-import io.legado.app.data.dao.BookDao
+import io.legado.app.domain.repository.BookDomainRepository
 
 
 class UpdateBooksGroupUseCase(
 class UpdateBooksGroupUseCase(
-    private val bookDao: BookDao
+    private val bookRepository: BookDomainRepository
 ) {
 ) {
 
 
     suspend fun replaceGroup(bookUrls: Set<String>, groupId: Long) {
     suspend fun replaceGroup(bookUrls: Set<String>, groupId: Long) {
@@ -12,18 +12,14 @@ class UpdateBooksGroupUseCase(
 
 
     suspend fun updateGroups(bookUrls: Set<String>, transform: (Long) -> Long) {
     suspend fun updateGroups(bookUrls: Set<String>, transform: (Long) -> Long) {
         if (bookUrls.isEmpty()) return
         if (bookUrls.isEmpty()) return
-        val updateBooks = bookUrls.mapNotNull { bookUrl ->
-            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)
     }
     }
 }
 }

+ 13 - 0
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)
+    }
+}

+ 36 - 0
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<String> {
+        webDavBackupGateway.syncConfig()
+        return webDavBackupGateway.getBackupNames()
+    }
+
+    suspend fun getLatestBackup(): WebDavBackup? {
+        webDavBackupGateway.syncConfig()
+        return webDavBackupGateway.getLatestBackup()
+    }
+
+    suspend fun restore(name: String) {
+        webDavBackupGateway.syncConfig()
+        webDavBackupGateway.restore(name)
+    }
+}

+ 18 - 6
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 {
         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 json = GSON.toJson(bookProgress)
             val url = getProgressUrl(bookProgress.name, bookProgress.author)
             val url = getProgressUrl(bookProgress.name, bookProgress.author)
             WebDav(url, authorization).upload(json.toByteArray(), "application/json")
             WebDav(url, authorization).upload(json.toByteArray(), "application/json")
             onSuccess?.invoke()
             onSuccess?.invoke()
+            return true
         } catch (e: Exception) {
         } catch (e: Exception) {
             currentCoroutineContext().ensureActive()
             currentCoroutineContext().ensureActive()
             AppLog.put("上传进度失败\n${e.localizedMessage}", e)
             AppLog.put("上传进度失败\n${e.localizedMessage}", e)
+            return false
         }
         }
     }
     }
 
 
@@ -312,7 +317,14 @@ object AppWebDav {
      * 获取书籍进度
      * 获取书籍进度
      */
      */
     suspend fun getBookProgress(book: Book): BookProgress? {
     suspend fun getBookProgress(book: Book): BookProgress? {
-        val url = getProgressUrl(book.name, book.author)
+        return getBookProgress(book.name, book.author)
+    }
+
+    /**
+     * 获取书籍进度
+     */
+    suspend fun getBookProgress(name: String, author: String): BookProgress? {
+        val url = getProgressUrl(name, author)
         kotlin.runCatching {
         kotlin.runCatching {
             val authorization = authorization ?: return null
             val authorization = authorization ?: return null
             WebDav(url, authorization).download().let { byteArray ->
             WebDav(url, authorization).download().let { byteArray ->
@@ -364,4 +376,4 @@ object AppWebDav {
         }
         }
     }
     }
 
 
-}
+}

+ 3 - 2
app/src/main/java/io/legado/app/help/book/BookExtensions.kt

@@ -2,6 +2,7 @@
 
 
 package io.legado.app.help.book
 package io.legado.app.help.book
 
 
+import io.legado.app.ui.config.otherConfig.OtherConfig
 import android.net.Uri
 import android.net.Uri
 import androidx.core.net.toUri
 import androidx.core.net.toUri
 import com.script.buildScriptBindings
 import com.script.buildScriptBindings
@@ -168,7 +169,7 @@ fun Book.getLocalUri(): Uri {
         return uri
         return uri
     }
     }
     //不同的设备书籍保存路径可能不一样, uri无效时尝试寻找当前保存路径下的文件
     //不同的设备书籍保存路径可能不一样, uri无效时尝试寻找当前保存路径下的文件
-    val defaultBookDir = AppConfig.defaultBookTreeUri
+    val defaultBookDir = OtherConfig.defaultBookTreeUri
     val importBookDir = AppConfig.importBookPath
     val importBookDir = AppConfig.importBookPath
 
 
     // 查找书籍保存目录
     // 查找书籍保存目录
@@ -240,7 +241,7 @@ fun Book.getLocalUri(): Uri {
 
 
 
 
 fun Book.getArchiveUri(): Uri? {
 fun Book.getArchiveUri(): Uri? {
-    val defaultBookDir = AppConfig.defaultBookTreeUri
+    val defaultBookDir = OtherConfig.defaultBookTreeUri
     return if (isArchive && !defaultBookDir.isNullOrBlank()) {
     return if (isArchive && !defaultBookDir.isNullOrBlank()) {
         FileDoc.fromUri(defaultBookDir.toUri(), true)
         FileDoc.fromUri(defaultBookDir.toUri(), true)
             .find(archiveName)?.uri
             .find(archiveName)?.uri

+ 0 - 11
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
     val showDiscovery: Boolean
         get() = appCtx.getPrefBoolean(PreferKey.showDiscovery, true)
         get() = appCtx.getPrefBoolean(PreferKey.showDiscovery, true)
 
 

+ 4 - 3
app/src/main/java/io/legado/app/model/localBook/LocalBook.kt

@@ -1,5 +1,6 @@
 package io.legado.app.model.localBook
 package io.legado.app.model.localBook
 
 
+import io.legado.app.ui.config.otherConfig.OtherConfig
 import android.net.Uri
 import android.net.Uri
 import android.util.Base64
 import android.util.Base64
 import androidx.core.net.toUri
 import androidx.core.net.toUri
@@ -401,7 +402,7 @@ object LocalBook {
         fileName: String,
         fileName: String,
         source: BaseSource? = null,
         source: BaseSource? = null,
     ): Uri {
     ): Uri {
-        AppConfig.defaultBookTreeUri
+        OtherConfig.defaultBookTreeUri
             ?: throw NoBooksDirException()
             ?: throw NoBooksDirException()
         val inputStream = when {
         val inputStream = when {
             str.isAbsUrl() -> AnalyzeUrl(
             str.isAbsUrl() -> AnalyzeUrl(
@@ -427,7 +428,7 @@ object LocalBook {
         fileName: String
         fileName: String
     ): Uri {
     ): Uri {
         inputStream.use {
         inputStream.use {
-            val defaultBookTreeUri = AppConfig.defaultBookTreeUri
+            val defaultBookTreeUri = OtherConfig.defaultBookTreeUri
             if (defaultBookTreeUri.isNullOrBlank()) throw NoBooksDirException()
             if (defaultBookTreeUri.isNullOrBlank()) throw NoBooksDirException()
             val treeUri = defaultBookTreeUri.toUri()
             val treeUri = defaultBookTreeUri.toUri()
             return if (treeUri.isContentScheme()) {
             return if (treeUri.isContentScheme()) {
@@ -481,7 +482,7 @@ object LocalBook {
         val webDavUrl = localBook.getRemoteUrl()
         val webDavUrl = localBook.getRemoteUrl()
         if (webDavUrl.isNullOrBlank()) throw NoStackTraceException("Book file is not webDav File")
         if (webDavUrl.isNullOrBlank()) throw NoStackTraceException("Book file is not webDav File")
         try {
         try {
-            AppConfig.defaultBookTreeUri
+            OtherConfig.defaultBookTreeUri
                 ?: throw NoBooksDirException()
                 ?: throw NoBooksDirException()
             // 兼容旧版链接
             // 兼容旧版链接
             val webdav: WebDav = kotlin.runCatching {
             val webdav: WebDav = kotlin.runCatching {

+ 2 - 2
app/src/main/java/io/legado/app/model/remote/RemoteBookWebDav.kt

@@ -1,5 +1,6 @@
 package io.legado.app.model.remote
 package io.legado.app.model.remote
 
 
+import io.legado.app.ui.config.otherConfig.OtherConfig
 import android.net.Uri
 import android.net.Uri
 import io.legado.app.constant.AppPattern.archiveFileRegex
 import io.legado.app.constant.AppPattern.archiveFileRegex
 import io.legado.app.constant.AppPattern.bookFileRegex
 import io.legado.app.constant.AppPattern.bookFileRegex
@@ -7,7 +8,6 @@ import io.legado.app.constant.BookType
 import io.legado.app.data.entities.Book
 import io.legado.app.data.entities.Book
 import io.legado.app.exception.NoStackTraceException
 import io.legado.app.exception.NoStackTraceException
 import io.legado.app.help.book.update
 import io.legado.app.help.book.update
-import io.legado.app.help.config.AppConfig
 import io.legado.app.lib.webdav.Authorization
 import io.legado.app.lib.webdav.Authorization
 import io.legado.app.lib.webdav.WebDav
 import io.legado.app.lib.webdav.WebDav
 import io.legado.app.lib.webdav.WebDavFile
 import io.legado.app.lib.webdav.WebDavFile
@@ -57,7 +57,7 @@ class RemoteBookWebDav(
     }
     }
 
 
     override suspend fun downloadRemoteBook(remoteBook: RemoteBook): Uri {
     override suspend fun downloadRemoteBook(remoteBook: RemoteBook): Uri {
-        AppConfig.defaultBookTreeUri
+        OtherConfig.defaultBookTreeUri
             ?: throw NoStackTraceException("没有设置书籍保存位置!")
             ?: throw NoStackTraceException("没有设置书籍保存位置!")
         if (!NetworkUtils.isAvailable()) throw NoStackTraceException("网络不可用")
         if (!NetworkUtils.isAvailable()) throw NoStackTraceException("网络不可用")
         val webdav = WebDav(remoteBook.path, authorization)
         val webdav = WebDav(remoteBook.path, authorization)

+ 3 - 3
app/src/main/java/io/legado/app/ui/association/FileAssociationActivity.kt

@@ -1,5 +1,6 @@
 package io.legado.app.ui.association
 package io.legado.app.ui.association
 
 
+import io.legado.app.ui.config.otherConfig.OtherConfig
 import android.net.Uri
 import android.net.Uri
 import android.os.Bundle
 import android.os.Bundle
 import androidx.activity.viewModels
 import androidx.activity.viewModels
@@ -11,7 +12,6 @@ import io.legado.app.base.VMBaseActivity
 import io.legado.app.constant.AppLog
 import io.legado.app.constant.AppLog
 import io.legado.app.databinding.ActivityTranslucenceBinding
 import io.legado.app.databinding.ActivityTranslucenceBinding
 import io.legado.app.exception.InvalidBooksDirException
 import io.legado.app.exception.InvalidBooksDirException
-import io.legado.app.help.config.AppConfig
 import io.legado.app.lib.dialogs.alert
 import io.legado.app.lib.dialogs.alert
 import io.legado.app.lib.permission.Permissions
 import io.legado.app.lib.permission.Permissions
 import io.legado.app.lib.permission.PermissionsCompat
 import io.legado.app.lib.permission.PermissionsCompat
@@ -43,7 +43,7 @@ class FileAssociationActivity :
     private val localBookTreeSelect = registerForActivityResult(HandleFileContract()) {
     private val localBookTreeSelect = registerForActivityResult(HandleFileContract()) {
         intent.data?.let { uri ->
         intent.data?.let { uri ->
             it.uri?.let { treeUri ->
             it.uri?.let { treeUri ->
-                AppConfig.defaultBookTreeUri = treeUri.toString()
+                OtherConfig.defaultBookTreeUri = treeUri.toString()
                 importBook(treeUri, uri)
                 importBook(treeUri, uri)
             } ?: let {
             } ?: let {
                 val storageHelp = String(assets.open("storageHelp.md").readBytes())
                 val storageHelp = String(assets.open("storageHelp.md").readBytes())
@@ -134,7 +134,7 @@ class FileAssociationActivity :
 
 
     private fun importBook(uri: Uri) {
     private fun importBook(uri: Uri) {
         if (uri.isContentScheme()) {
         if (uri.isContentScheme()) {
-            val treeUriStr = AppConfig.defaultBookTreeUri
+            val treeUriStr = OtherConfig.defaultBookTreeUri
             if (treeUriStr.isNullOrEmpty()) {
             if (treeUriStr.isNullOrEmpty()) {
                 localBookTreeSelect.launch {
                 localBookTreeSelect.launch {
                     title = getString(R.string.select_book_folder)
                     title = getString(R.string.select_book_folder)

+ 5 - 7
app/src/main/java/io/legado/app/ui/book/cache/CacheScreen.kt

@@ -1,5 +1,6 @@
 package io.legado.app.ui.book.cache
 package io.legado.app.ui.book.cache
 
 
+import androidx.compose.ui.focus.focusRequester
 import androidx.activity.compose.BackHandler
 import androidx.activity.compose.BackHandler
 import androidx.activity.compose.rememberLauncherForActivityResult
 import androidx.activity.compose.rememberLauncherForActivityResult
 import androidx.activity.result.contract.ActivityResultContracts
 import androidx.activity.result.contract.ActivityResultContracts
@@ -50,7 +51,6 @@ import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.focus.FocusRequester
 import androidx.compose.ui.focus.FocusRequester
-import androidx.compose.ui.focus.focusRequester
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.dp
@@ -89,7 +89,6 @@ import io.legado.app.utils.isContentScheme
 import io.legado.app.utils.startService
 import io.legado.app.utils.startService
 import io.legado.app.utils.toastOnUi
 import io.legado.app.utils.toastOnUi
 import io.legado.app.utils.verificationField
 import io.legado.app.utils.verificationField
-import io.legado.app.help.config.LocalConfig
 import io.legado.app.ui.theme.adaptiveHorizontalPadding
 import io.legado.app.ui.theme.adaptiveHorizontalPadding
 import org.koin.androidx.compose.koinViewModel
 import org.koin.androidx.compose.koinViewModel
 
 
@@ -158,7 +157,7 @@ private fun CacheScreen(
     var isSearchMode by remember { mutableStateOf(false) }
     var isSearchMode by remember { mutableStateOf(false) }
     var searchKey by remember { mutableStateOf("") }
     var searchKey by remember { mutableStateOf("") }
     var selectedBookUrls by remember { mutableStateOf<Set<String>>(emptySet()) }
     var selectedBookUrls by remember { mutableStateOf<Set<String>>(emptySet()) }
-    var deleteOriginalBookFile by remember { mutableStateOf(LocalConfig.deleteBookOriginal) }
+    var deleteOriginalBookFile by remember { mutableStateOf(state.deleteBookOriginal) }
     val exportBookPathKey = remember { "exportBookPath" }
     val exportBookPathKey = remember { "exportBookPath" }
     val exportTypes = remember { arrayListOf("txt", "epub") }
     val exportTypes = remember { arrayListOf("txt", "epub") }
     val focusRequester = remember { FocusRequester() }
     val focusRequester = remember { FocusRequester() }
@@ -409,7 +408,7 @@ private fun CacheScreen(
         ) {
         ) {
             if (selectedBookUrls.isNotEmpty()) {
             if (selectedBookUrls.isNotEmpty()) {
                 pendingDeleteBookUrls = selectedBookUrls
                 pendingDeleteBookUrls = selectedBookUrls
-                deleteOriginalBookFile = LocalConfig.deleteBookOriginal
+            deleteOriginalBookFile = state.deleteBookOriginal
                 showDeleteBookConfirmDialog = true
                 showDeleteBookConfirmDialog = true
             }
             }
         }
         }
@@ -638,7 +637,7 @@ private fun CacheScreen(
                     onClick = { toggleBookSelection(book) },
                     onClick = { toggleBookSelection(book) },
                     onLongClick = { toggleBookSelection(book) },
                     onLongClick = { toggleBookSelection(book) },
                     containerColor = if (isSelected) {
                     containerColor = if (isSelected) {
-                        LegadoTheme.colorScheme.secondaryContainer
+                        LegadoTheme.colorScheme.surfaceContainerHigh
                     } else {
                     } else {
                         LegadoTheme.colorScheme.surfaceContainerLow
                         LegadoTheme.colorScheme.surfaceContainerLow
                     }
                     }
@@ -727,7 +726,7 @@ private fun CacheScreen(
                                         text = "删除书籍",
                                         text = "删除书籍",
                                         onClick = {
                                         onClick = {
                                             pendingDeleteBookUrls = setOf(book.bookUrl)
                                             pendingDeleteBookUrls = setOf(book.bookUrl)
-                                            deleteOriginalBookFile = LocalConfig.deleteBookOriginal
+                                            deleteOriginalBookFile = state.deleteBookOriginal
                                             showDeleteBookConfirmDialog = true
                                             showDeleteBookConfirmDialog = true
                                             dismiss()
                                             dismiss()
                                         }
                                         }
@@ -806,7 +805,6 @@ private fun CacheScreen(
         confirmText = stringResource(android.R.string.ok),
         confirmText = stringResource(android.R.string.ok),
         onConfirm = {
         onConfirm = {
             showDeleteBookConfirmDialog = false
             showDeleteBookConfirmDialog = false
-            LocalConfig.deleteBookOriginal = deleteOriginalBookFile
             viewModel.dispatch(
             viewModel.dispatch(
                 CacheIntent.DeleteBooks(
                 CacheIntent.DeleteBooks(
                     bookUrls = pendingDeleteBookUrls,
                     bookUrls = pendingDeleteBookUrls,

+ 20 - 8
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.BookChapter
 import io.legado.app.data.entities.BookGroup
 import io.legado.app.data.entities.BookGroup
 import io.legado.app.domain.usecase.BatchCacheDownloadUseCase
 import io.legado.app.domain.usecase.BatchCacheDownloadUseCase
+import io.legado.app.domain.usecase.CacheBookChaptersUseCase
 import io.legado.app.domain.usecase.ClearBookCacheUseCase
 import io.legado.app.domain.usecase.ClearBookCacheUseCase
 import io.legado.app.domain.usecase.DeleteBooksUseCase
 import io.legado.app.domain.usecase.DeleteBooksUseCase
 import io.legado.app.domain.usecase.UpdateBooksGroupUseCase
 import io.legado.app.domain.usecase.UpdateBooksGroupUseCase
@@ -17,6 +18,7 @@ import io.legado.app.help.book.BookHelp
 import io.legado.app.help.book.isAudio
 import io.legado.app.help.book.isAudio
 import io.legado.app.help.book.isLocal
 import io.legado.app.help.book.isLocal
 import io.legado.app.model.CacheBook
 import io.legado.app.model.CacheBook
+import io.legado.app.help.config.LocalConfig
 import io.legado.app.service.ExportBookService
 import io.legado.app.service.ExportBookService
 import io.legado.app.ui.config.cacheConfig.CacheConfig
 import io.legado.app.ui.config.cacheConfig.CacheConfig
 import io.legado.app.utils.cnCompare
 import io.legado.app.utils.cnCompare
@@ -51,6 +53,7 @@ data class CacheUiState(
     val books: List<Book> = emptyList(),
     val books: List<Book> = emptyList(),
     val isDownloadRunning: Boolean = false,
     val isDownloadRunning: Boolean = false,
     val cacheVersion: Long = 0,
     val cacheVersion: Long = 0,
+    val deleteBookOriginal: Boolean = LocalConfig.deleteBookOriginal,
     val exportConfig: CacheExportConfig = CacheExportConfig()
     val exportConfig: CacheExportConfig = CacheExportConfig()
 )
 )
 
 
@@ -93,6 +96,7 @@ class CacheViewModel(
     private val bookChapterDao: BookChapterDao,
     private val bookChapterDao: BookChapterDao,
     val cacheConfig: CacheConfig,
     val cacheConfig: CacheConfig,
     private val batchCacheDownloadUseCase: BatchCacheDownloadUseCase,
     private val batchCacheDownloadUseCase: BatchCacheDownloadUseCase,
+    private val cacheBookChaptersUseCase: CacheBookChaptersUseCase,
     private val clearBookCacheUseCase: ClearBookCacheUseCase,
     private val clearBookCacheUseCase: ClearBookCacheUseCase,
     private val deleteBooksUseCase: DeleteBooksUseCase,
     private val deleteBooksUseCase: DeleteBooksUseCase,
     private val updateBooksGroupUseCase: UpdateBooksGroupUseCase
     private val updateBooksGroupUseCase: UpdateBooksGroupUseCase
@@ -335,8 +339,7 @@ class CacheViewModel(
     private fun startDownloadForVisibleBooks(books: List<Book>, downloadAllChapters: Boolean) {
     private fun startDownloadForVisibleBooks(books: List<Book>, downloadAllChapters: Boolean) {
         execute {
         execute {
             batchCacheDownloadUseCase.execute(
             batchCacheDownloadUseCase.execute(
-                context = context,
-                books = books,
+                bookUrls = books.map { it.bookUrl }.toSet(),
                 downloadAllChapters = downloadAllChapters,
                 downloadAllChapters = downloadAllChapters,
                 skipAudioBooks = true
                 skipAudioBooks = true
             )
             )
@@ -349,10 +352,14 @@ class CacheViewModel(
         if (book.isLocal) return
         if (book.isLocal) return
         if (isBookDownloading(book.bookUrl)) {
         if (isBookDownloading(book.bookUrl)) {
             CacheBook.remove(context, book.bookUrl)
             CacheBook.remove(context, book.bookUrl)
+            syncDownloadRunning()
         } else {
         } 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<String>, groupId: Long) {
     private fun moveBooksToGroup(bookUrls: Set<String>, groupId: Long) {
@@ -368,8 +375,10 @@ class CacheViewModel(
     private fun deleteBooks(bookUrls: Set<String>, deleteOriginal: Boolean) {
     private fun deleteBooks(bookUrls: Set<String>, deleteOriginal: Boolean) {
         if (bookUrls.isEmpty()) return
         if (bookUrls.isEmpty()) return
         execute {
         execute {
+            LocalConfig.deleteBookOriginal = deleteOriginal
             deleteBooksUseCase.execute(bookUrls, deleteOriginal)
             deleteBooksUseCase.execute(bookUrls, deleteOriginal)
         }.onSuccess { deletedBookUrls ->
         }.onSuccess { deletedBookUrls ->
+            _uiState.update { it.copy(deleteBookOriginal = deleteOriginal) }
             deletedBookUrls.forEach { cacheChapters.remove(it) }
             deletedBookUrls.forEach { cacheChapters.remove(it) }
             _effects.tryEmit(CacheEffect.ShowMessage("删除成功"))
             _effects.tryEmit(CacheEffect.ShowMessage("删除成功"))
         }.onError {
         }.onError {
@@ -396,7 +405,6 @@ class CacheViewModel(
         if (bookUrls.isEmpty()) return
         if (bookUrls.isEmpty()) return
         execute {
         execute {
             batchCacheDownloadUseCase.execute(
             batchCacheDownloadUseCase.execute(
-                context = context,
                 bookUrls = bookUrls,
                 bookUrls = bookUrls,
                 downloadAllChapters = downloadAllChapters,
                 downloadAllChapters = downloadAllChapters,
                 skipAudioBooks = true
                 skipAudioBooks = true
@@ -414,9 +422,13 @@ class CacheViewModel(
     }
     }
 
 
     private fun clearCacheForBook(book: Book) {
     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) {
     private fun emitBookChanged(bookUrl: String) {

+ 14 - 15
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.HazeState
 import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
 import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
 import io.legado.app.data.entities.SearchBook
 import io.legado.app.data.entities.SearchBook
-import io.legado.app.domain.usecase.ExploreKindUiUseCase
-import io.legado.app.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.LegadoTheme
 import io.legado.app.ui.theme.ThemeResolver
 import io.legado.app.ui.theme.ThemeResolver
 import io.legado.app.ui.theme.responsiveHazeEffect
 import io.legado.app.ui.theme.responsiveHazeEffect
@@ -84,7 +84,6 @@ import io.legado.app.ui.widget.components.text.AppText
 import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
 import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
 import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
 import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.Flow
 import org.koin.androidx.compose.koinViewModel
 import org.koin.androidx.compose.koinViewModel
 import org.koin.compose.koinInject
 import org.koin.compose.koinInject
 
 
@@ -444,12 +443,12 @@ fun ExploreShowScreen(
                     ) {
                     ) {
                         items(
                         items(
                             items = books,
                             items = books,
-                            key = { it.bookUrl }
-                        ) { book ->
+                            key = { it.book.bookUrl }
+                        ) { item ->
                             ExploreBookGridItem(
                             ExploreBookGridItem(
-                                book = book,
-                                shelfState = viewModel.getBookShelfStateFlow(book),
-                                onClick = { onBookClick(book) },
+                                book = item.book,
+                                shelfState = item.shelfState,
+                                onClick = { onBookClick(item.book) },
                                 modifier = Modifier.animateItem()
                                 modifier = Modifier.animateItem()
                             )
                             )
                         }
                         }
@@ -476,12 +475,12 @@ fun ExploreShowScreen(
                     ) {
                     ) {
                         items(
                         items(
                             items = books,
                             items = books,
-                            key = { it.bookUrl }
-                        ) { book ->
+                            key = { it.book.bookUrl }
+                        ) { item ->
                             ExploreBookItem(
                             ExploreBookItem(
-                                book = book,
-                                shelfState = viewModel.getBookShelfStateFlow(book),
-                                onClick = { onBookClick(book) },
+                                book = item.book,
+                                shelfState = item.shelfState,
+                                onClick = { onBookClick(item.book) },
                                 modifier = Modifier.animateItem()
                                 modifier = Modifier.animateItem()
                             )
                             )
                         }
                         }
@@ -505,7 +504,7 @@ fun ExploreShowScreen(
 @Composable
 @Composable
 fun ExploreBookItem(
 fun ExploreBookItem(
     book: SearchBook,
     book: SearchBook,
-    shelfState: Flow<BookShelfState>,
+    shelfState: BookShelfState,
     onClick: () -> Unit,
     onClick: () -> Unit,
     modifier: Modifier = Modifier
     modifier: Modifier = Modifier
 ) {
 ) {
@@ -521,7 +520,7 @@ fun ExploreBookItem(
 fun ExploreBookGridItem(
 fun ExploreBookGridItem(
     book: SearchBook,
     book: SearchBook,
     onClick: () -> Unit,
     onClick: () -> Unit,
-    shelfState: Flow<BookShelfState>,
+    shelfState: BookShelfState,
     modifier: Modifier = Modifier
     modifier: Modifier = Modifier
 ) {
 ) {
     SearchBookGridItem(
     SearchBookGridItem(

+ 39 - 27
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.SearchBook
 import io.legado.app.data.entities.rule.ExploreKind
 import io.legado.app.data.entities.rule.ExploreKind
 import io.legado.app.data.repository.ExploreRepository
 import io.legado.app.data.repository.ExploreRepository
+import io.legado.app.domain.usecase.BookShelfKey
+import io.legado.app.domain.usecase.ResolveBookShelfStateUseCase
 import io.legado.app.help.config.AppConfig
 import io.legado.app.help.config.AppConfig
-import io.legado.app.model.BookShelfState
-import io.legado.app.ui.book.search.BookKey
+import io.legado.app.domain.model.BookShelfState
 import io.legado.app.utils.exploreLayoutGrid
 import io.legado.app.utils.exploreLayoutGrid
-import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.SharingStarted
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.StateFlow
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.asStateFlow
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.launch
 import splitties.init.appCtx
 import splitties.init.appCtx
@@ -46,8 +44,14 @@ sealed class UiState<out T> {
     data object Empty : UiState<Nothing>()
     data object Empty : UiState<Nothing>()
 }
 }
 
 
+data class ExploreBookItemUi(
+    val book: SearchBook,
+    val shelfState: BookShelfState = BookShelfState.NOT_IN_SHELF,
+)
+
 class ExploreShowViewModel(
 class ExploreShowViewModel(
-    private val repository: ExploreRepository
+    private val repository: ExploreRepository,
+    private val resolveBookShelfStateUseCase: ResolveBookShelfStateUseCase
 ) : ViewModel() {
 ) : ViewModel() {
 
 
     private val _rawBooks = MutableStateFlow<List<SearchBook>>(emptyList())
     private val _rawBooks = MutableStateFlow<List<SearchBook>>(emptyList())
@@ -60,7 +64,7 @@ class ExploreShowViewModel(
     private var exploreUrl: String? = null
     private var exploreUrl: String? = null
     private var page = 1
     private var page = 1
     private val _isEndStateFlow = MutableStateFlow(false)
     private val _isEndStateFlow = MutableStateFlow(false)
-    private val _bookshelf = MutableStateFlow<Set<BookKey>>(emptySet())
+    private val _bookshelf = MutableStateFlow<Set<BookShelfKey>>(emptySet())
     private val _kinds = MutableStateFlow<List<ExploreKind>>(emptyList())
     private val _kinds = MutableStateFlow<List<ExploreKind>>(emptyList())
     val kinds = _kinds.asStateFlow()
     val kinds = _kinds.asStateFlow()
     private val _selectedKindTitle = MutableStateFlow<String?>(null)
     private val _selectedKindTitle = MutableStateFlow<String?>(null)
@@ -85,6 +89,16 @@ class ExploreShowViewModel(
     ) { books, filter, bookshelf ->
     ) { books, filter, bookshelf ->
         books.filter { item ->
         books.filter { item ->
             isBookValid(item, filter, bookshelf)
             isBookValid(item, filter, bookshelf)
+        }.map { item ->
+            ExploreBookItemUi(
+                book = item,
+                shelfState = resolveBookShelfStateUseCase.execute(
+                    name = item.name,
+                    author = item.author,
+                    url = item.bookUrl,
+                    shelf = bookshelf
+                )
+            )
         }
         }
     }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
     }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
 
 
@@ -104,7 +118,7 @@ class ExploreShowViewModel(
     init {
     init {
         viewModelScope.launch {
         viewModelScope.launch {
             repository.getBookshelfItems().collect { list ->
             repository.getBookshelfItems().collect { list ->
-                val keys = list.map { BookKey(it.name, it.author, it.bookUrl) }.toSet()
+                val keys = list.map { BookShelfKey(it.name, it.author, it.bookUrl) }.toSet()
                 _bookshelf.value = keys
                 _bookshelf.value = keys
             }
             }
         }
         }
@@ -199,11 +213,25 @@ class ExploreShowViewModel(
     }
     }
 
 
     fun getCurrentBookShelfState(item: SearchBook): BookShelfState {
     fun getCurrentBookShelfState(item: SearchBook): BookShelfState {
-        return getBookShelfState(item, _bookshelf.value)
+        return resolveBookShelfStateUseCase.execute(
+            name = item.name,
+            author = item.author,
+            url = item.bookUrl,
+            shelf = _bookshelf.value
+        )
     }
     }
 
 
-    private fun isBookValid(book: SearchBook, filter: BookFilterState, shelf: Set<BookKey>): Boolean {
-        val state = getBookShelfState(book, shelf)
+    private fun isBookValid(
+        book: SearchBook,
+        filter: BookFilterState,
+        shelf: Set<BookShelfKey>
+    ): Boolean {
+        val state = resolveBookShelfStateUseCase.execute(
+            name = book.name,
+            author = book.author,
+            url = book.bookUrl,
+            shelf = shelf
+        )
         return when (filter) {
         return when (filter) {
             BookFilterState.SHOW_ALL -> true
             BookFilterState.SHOW_ALL -> true
             BookFilterState.HIDE_IN_SHELF -> state != BookShelfState.IN_SHELF
             BookFilterState.HIDE_IN_SHELF -> state != BookShelfState.IN_SHELF
@@ -211,20 +239,4 @@ class ExploreShowViewModel(
             BookFilterState.SHOW_NOT_IN_SHELF_ONLY -> state == BookShelfState.NOT_IN_SHELF
             BookFilterState.SHOW_NOT_IN_SHELF_ONLY -> state == BookShelfState.NOT_IN_SHELF
         }
         }
     }
     }
-
-    fun getBookShelfStateFlow(item: SearchBook): Flow<BookShelfState> {
-        return _bookshelf.map { shelf ->
-            getBookShelfState(item, shelf)
-        }.distinctUntilChanged()
-    }
-
-    private fun getBookShelfState(item: SearchBook, shelf: Set<BookKey>): BookShelfState {
-        val exactMatch = shelf.any { it.name == item.name && it.author == item.author && it.url == item.bookUrl }
-        if (exactMatch) return BookShelfState.IN_SHELF
-
-        val sameNameAuthor = shelf.any { it.name == item.name && it.author == item.author && it.url != item.bookUrl }
-        if (sameNameAuthor) return BookShelfState.SAME_NAME_AUTHOR
-
-        return BookShelfState.NOT_IN_SHELF
-    }
 }
 }

+ 5 - 4
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 android.app.Application
 import io.legado.app.base.BaseViewModel
 import io.legado.app.base.BaseViewModel
-import io.legado.app.data.appDb
 import io.legado.app.data.entities.BookGroup
 import io.legado.app.data.entities.BookGroup
 import io.legado.app.data.repository.BookGroupRepository
 import io.legado.app.data.repository.BookGroupRepository
+import io.legado.app.domain.usecase.RemoveBookGroupAssignmentUseCase
 
 
 class GroupViewModel(
 class GroupViewModel(
     application: Application,
     application: Application,
-    private val bookGroupRepository: BookGroupRepository
+    private val bookGroupRepository: BookGroupRepository,
+    private val removeBookGroupAssignmentUseCase: RemoveBookGroupAssignmentUseCase
 ) : BaseViewModel(application) {
 ) : BaseViewModel(application) {
 
 
     fun upGroup(vararg bookGroup: BookGroup, finally: (() -> Unit)? = null) {
     fun upGroup(vararg bookGroup: BookGroup, finally: (() -> Unit)? = null) {
@@ -36,7 +37,7 @@ class GroupViewModel(
                 enableRefresh = enableRefresh,
                 enableRefresh = enableRefresh,
                 order = bookGroupRepository.getMaxOrder().plus(1)
                 order = bookGroupRepository.getMaxOrder().plus(1)
             )
             )
-            bookGroupRepository.getByID(groupId) ?: appDb.bookDao.removeGroup(groupId)
+            bookGroupRepository.getByID(groupId) ?: removeBookGroupAssignmentUseCase.execute(groupId)
             bookGroupRepository.insert(bookGroup)
             bookGroupRepository.insert(bookGroup)
         }.onFinally {
         }.onFinally {
             finally()
             finally()
@@ -46,7 +47,7 @@ class GroupViewModel(
     fun delGroup(bookGroup: BookGroup, finally: () -> Unit) {
     fun delGroup(bookGroup: BookGroup, finally: () -> Unit) {
         execute {
         execute {
             bookGroupRepository.delete(bookGroup)
             bookGroupRepository.delete(bookGroup)
-            appDb.bookDao.removeGroup(bookGroup.groupId)
+            removeBookGroupAssignmentUseCase.execute(bookGroup.groupId)
         }.onFinally {
         }.onFinally {
             finally()
             finally()
         }
         }

+ 11 - 10
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
 package io.legado.app.ui.book.import.local
 
 
+import io.legado.app.ui.config.otherConfig.OtherConfig
 import android.app.Application
 import android.app.Application
 import android.net.Uri
 import android.net.Uri
 import android.provider.DocumentsContract
 import android.provider.DocumentsContract
@@ -231,7 +232,7 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application)
     fun hasRootDoc(): Boolean = _state.value.rootDoc != null
     fun hasRootDoc(): Boolean = _state.value.rootDoc != null
 
 
     private fun initialize() {
     private fun initialize() {
-        val defaultPath = ImportBookConfig.defaultBookTreeUri?.takeIf { it.isUri() }
+        val defaultPath = OtherConfig.defaultBookTreeUri?.takeIf { it.isUri() }
         val effectiveDefaultPath = defaultPath ?: firstPersistedTreeUri()?.toString()
         val effectiveDefaultPath = defaultPath ?: firstPersistedTreeUri()?.toString()
         if (effectiveDefaultPath.isNullOrBlank()) {
         if (effectiveDefaultPath.isNullOrBlank()) {
             _effects.tryEmit(
             _effects.tryEmit(
@@ -239,8 +240,8 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application)
             )
             )
             return
             return
         }
         }
-        if (ImportBookConfig.defaultBookTreeUri != effectiveDefaultPath) {
-            ImportBookConfig.defaultBookTreeUri = effectiveDefaultPath
+        if (OtherConfig.defaultBookTreeUri != effectiveDefaultPath) {
+            OtherConfig.defaultBookTreeUri = effectiveDefaultPath
         }
         }
         val importPath = ImportBookConfig.importBookPath
         val importPath = ImportBookConfig.importBookPath
         if (importPath.isNullOrBlank() || !importPath.isUri()) {
         if (importPath.isNullOrBlank() || !importPath.isUri()) {
@@ -255,16 +256,16 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application)
         val pickedUri = persistFolderPermission(uri)
         val pickedUri = persistFolderPermission(uri)
         when (target) {
         when (target) {
             ImportFolderPickTarget.DEFAULT_BOOK -> {
             ImportFolderPickTarget.DEFAULT_BOOK -> {
-                ImportBookConfig.defaultBookTreeUri = pickedUri.toString()
+                OtherConfig.defaultBookTreeUri = pickedUri.toString()
                 if (ImportBookConfig.importBookPath.isNullOrBlank()) {
                 if (ImportBookConfig.importBookPath.isNullOrBlank()) {
-                    ImportBookConfig.importBookPath = ImportBookConfig.defaultBookTreeUri
+                    ImportBookConfig.importBookPath = OtherConfig.defaultBookTreeUri
                 }
                 }
             }
             }
 
 
             ImportFolderPickTarget.IMPORT_FOLDER -> {
             ImportFolderPickTarget.IMPORT_FOLDER -> {
                 ImportBookConfig.importBookPath = pickedUri.toString()
                 ImportBookConfig.importBookPath = pickedUri.toString()
-                if (ImportBookConfig.defaultBookTreeUri.isNullOrBlank()) {
-                    ImportBookConfig.defaultBookTreeUri = pickedUri.toString()
+                if (OtherConfig.defaultBookTreeUri.isNullOrBlank()) {
+                    OtherConfig.defaultBookTreeUri = pickedUri.toString()
                 }
                 }
             }
             }
         }
         }
@@ -302,14 +303,14 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application)
             ImportBookConfig.importBookPath
             ImportBookConfig.importBookPath
                 ?.takeIf { it.isNotBlank() && it.isUri() }
                 ?.takeIf { it.isNotBlank() && it.isUri() }
                 ?.let(::add)
                 ?.let(::add)
-            ImportBookConfig.defaultBookTreeUri
+            OtherConfig.defaultBookTreeUri
                 ?.takeIf { it.isNotBlank() && it.isUri() }
                 ?.takeIf { it.isNotBlank() && it.isUri() }
                 ?.let(::add)
                 ?.let(::add)
         }
         }
         if (candidates.isEmpty()) {
         if (candidates.isEmpty()) {
             firstPersistedTreeUri()?.toString()?.let { persistedPath ->
             firstPersistedTreeUri()?.toString()?.let { persistedPath ->
                 if (trySetRootDoc(persistedPath)) {
                 if (trySetRootDoc(persistedPath)) {
-                    ImportBookConfig.defaultBookTreeUri = persistedPath
+                    OtherConfig.defaultBookTreeUri = persistedPath
                     ImportBookConfig.importBookPath = persistedPath
                     ImportBookConfig.importBookPath = persistedPath
                     return
                     return
                 }
                 }
@@ -341,7 +342,7 @@ class ImportBookViewModel(application: Application) : BaseViewModel(application)
 
 
         firstPersistedTreeUri()?.toString()?.let { persistedPath ->
         firstPersistedTreeUri()?.toString()?.let { persistedPath ->
             if (trySetRootDoc(persistedPath)) {
             if (trySetRootDoc(persistedPath)) {
-                ImportBookConfig.defaultBookTreeUri = persistedPath
+                OtherConfig.defaultBookTreeUri = persistedPath
                 ImportBookConfig.importBookPath = persistedPath
                 ImportBookConfig.importBookPath = persistedPath
                 return
                 return
             }
             }

+ 1 - 7
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.input.VisualTransformation
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.text.style.TextOverflow
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.dp
-import androidx.core.net.toUri
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import androidx.lifecycle.compose.collectAsStateWithLifecycle
 import io.legado.app.R
 import io.legado.app.R
 import io.legado.app.constant.AppConst
 import io.legado.app.constant.AppConst
 import io.legado.app.data.entities.Server
 import io.legado.app.data.entities.Server
-import io.legado.app.help.config.AppConfig
 import io.legado.app.lib.dialogs.alert
 import io.legado.app.lib.dialogs.alert
 import io.legado.app.lib.dialogs.selector
 import io.legado.app.lib.dialogs.selector
 import io.legado.app.model.remote.RemoteBook
 import io.legado.app.model.remote.RemoteBook
@@ -91,7 +89,6 @@ import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
 import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
 import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
 import io.legado.app.ui.widget.components.text.AppText
 import io.legado.app.ui.widget.components.text.AppText
 import io.legado.app.utils.ConvertUtils
 import io.legado.app.utils.ConvertUtils
-import io.legado.app.utils.isUri
 import io.legado.app.utils.startActivityForBook
 import io.legado.app.utils.startActivityForBook
 import io.legado.app.utils.toastOnUi
 import io.legado.app.utils.toastOnUi
 import org.json.JSONObject
 import org.json.JSONObject
@@ -218,10 +215,7 @@ fun RemoteBookScreen(
             when (effect) {
             when (effect) {
                 is RemoteBookEffect.OpenBook -> context.startActivityForBook(effect.book)
                 is RemoteBookEffect.OpenBook -> context.startActivityForBook(effect.book)
                 is RemoteBookEffect.RequestBookFolderPicker -> {
                 is RemoteBookEffect.RequestBookFolderPicker -> {
-                    selectDocTree.launch(
-                        effect.initialUri
-                            ?: AppConfig.defaultBookTreeUri?.takeIf { it.isUri() }?.toUri()
-                    )
+                    selectDocTree.launch(effect.initialUri)
                 }
                 }
 
 
                 is RemoteBookEffect.ShowArchiveEntries -> {
                 is RemoteBookEffect.ShowArchiveEntries -> {

+ 17 - 11
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
 package io.legado.app.ui.book.import.remote
 
 
+import io.legado.app.ui.config.otherConfig.OtherConfig
 import android.app.Application
 import android.app.Application
 import androidx.core.net.toUri
 import androidx.core.net.toUri
 import androidx.compose.runtime.Immutable
 import androidx.compose.runtime.Immutable
@@ -14,8 +15,6 @@ import io.legado.app.data.entities.Book
 import io.legado.app.data.entities.Server
 import io.legado.app.data.entities.Server
 import io.legado.app.data.repository.RemoteBookRepository
 import io.legado.app.data.repository.RemoteBookRepository
 import io.legado.app.exception.NoStackTraceException
 import io.legado.app.exception.NoStackTraceException
-import io.legado.app.help.AppWebDav
-import io.legado.app.help.config.AppConfig
 import io.legado.app.model.analyzeRule.CustomUrl
 import io.legado.app.model.analyzeRule.CustomUrl
 import io.legado.app.model.localBook.LocalBook
 import io.legado.app.model.localBook.LocalBook
 import io.legado.app.model.remote.RemoteBook
 import io.legado.app.model.remote.RemoteBook
@@ -29,6 +28,7 @@ import io.legado.app.utils.ArchiveUtils
 import io.legado.app.utils.ConvertUtils
 import io.legado.app.utils.ConvertUtils
 import io.legado.app.utils.FileDoc
 import io.legado.app.utils.FileDoc
 import io.legado.app.utils.find
 import io.legado.app.utils.find
+import io.legado.app.utils.isUri
 import io.legado.app.utils.takePersistablePermissionSafely
 import io.legado.app.utils.takePersistablePermissionSafely
 import io.legado.app.utils.toastOnUi
 import io.legado.app.utils.toastOnUi
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.Dispatchers
@@ -162,13 +162,13 @@ class RemoteBookViewModel(
     private fun onBookFolderPicked(uri: android.net.Uri?) {
     private fun onBookFolderPicked(uri: android.net.Uri?) {
         uri ?: return
         uri ?: return
         uri.takePersistablePermissionSafely(context)
         uri.takePersistablePermissionSafely(context)
-        AppConfig.defaultBookTreeUri = uri.toString()
+        OtherConfig.defaultBookTreeUri = uri.toString()
     }
     }
 
 
     val uiState: StateFlow<RemoteBookUiState> = combine(
     val uiState: StateFlow<RemoteBookUiState> = combine(
         _state,
         _state,
         repository.flowLocalBooks(),
         repository.flowLocalBooks(),
-        appDb.serverDao.observeAll()
+        repository.flowServers()
     ) { state, localBooks, servers ->
     ) { state, localBooks, servers ->
         val localFileNames = localBooks.map { it.originName }.toSet()
         val localFileNames = localBooks.map { it.originName }.toSet()
 
 
@@ -224,7 +224,7 @@ class RemoteBookViewModel(
                     }
                     }
                     onSuccess()
                     onSuccess()
                 } else {
                 } else {
-                    val defaultWebDav = AppWebDav.defaultBookWebDav
+                    val defaultWebDav = repository.getDefaultBookWebDav()
                         ?: throw NoStackTraceException("webDav没有配置")
                         ?: throw NoStackTraceException("webDav没有配置")
                     _state.update {
                     _state.update {
                         it.copy(
                         it.copy(
@@ -282,7 +282,7 @@ class RemoteBookViewModel(
             _state.update { it.copy(selectedIds = emptySet()) }
             _state.update { it.copy(selectedIds = emptySet()) }
             Result.success(Unit)
             Result.success(Unit)
         } catch (e: SecurityException) {
         } catch (e: SecurityException) {
-            _effects.emit(RemoteBookEffect.RequestBookFolderPicker())
+            _effects.emit(RemoteBookEffect.RequestBookFolderPicker(defaultBookTreeUri()))
             Result.failure(e)
             Result.failure(e)
         } catch (e: Exception) {
         } catch (e: Exception) {
             AppLog.put("导入出错\n${e.localizedMessage}", e)
             AppLog.put("导入出错\n${e.localizedMessage}", e)
@@ -419,14 +419,14 @@ class RemoteBookViewModel(
                 return@launch
                 return@launch
             }
             }
 
 
-            val bookTreeUri = AppConfig.defaultBookTreeUri
+            val bookTreeUri = defaultBookTreeUri()
             if (bookTreeUri == null) {
             if (bookTreeUri == null) {
-                _effects.emit(RemoteBookEffect.RequestBookFolderPicker())
+                _effects.emit(RemoteBookEffect.RequestBookFolderPicker(defaultBookTreeUri()))
                 return@launch
                 return@launch
             }
             }
 
 
             val downloadArchiveFileDoc = withContext(Dispatchers.IO) {
             val downloadArchiveFileDoc = withContext(Dispatchers.IO) {
-                FileDoc.fromUri(bookTreeUri.toUri(), true)
+                FileDoc.fromUri(bookTreeUri, true)
                     .find(downloadFileName)
                     .find(downloadFileName)
             }
             }
             if (downloadArchiveFileDoc == null) {
             if (downloadArchiveFileDoc == null) {
@@ -495,9 +495,15 @@ class RemoteBookViewModel(
         }
         }
     }
     }
 
 
+    private fun defaultBookTreeUri(): android.net.Uri? {
+        return OtherConfig.defaultBookTreeUri
+            ?.takeIf { it.isUri() }
+            ?.toUri()
+    }
+
     fun saveServer(server: Server) {
     fun saveServer(server: Server) {
         execute {
         execute {
-            appDb.serverDao.insert(server)
+            repository.saveServer(server)
         }.onSuccess {
         }.onSuccess {
             if (ImportBookConfig.remoteServerId == server.id) {
             if (ImportBookConfig.remoteServerId == server.id) {
                 initData { loadRemoteBookList() }
                 initData { loadRemoteBookList() }
@@ -507,7 +513,7 @@ class RemoteBookViewModel(
 
 
     fun deleteServer(server: Server) {
     fun deleteServer(server: Server) {
         execute {
         execute {
-            appDb.serverDao.delete(server)
+            repository.deleteServer(server)
         }
         }
     }
     }
 
 

+ 3 - 3
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.View
 import android.view.ViewGroup
 import android.view.ViewGroup
 import androidx.appcompat.widget.Toolbar
 import androidx.appcompat.widget.Toolbar
-import androidx.fragment.app.viewModels
 import io.legado.app.R
 import io.legado.app.R
 import io.legado.app.base.BaseDialogFragment
 import io.legado.app.base.BaseDialogFragment
 import io.legado.app.data.entities.Server
 import io.legado.app.data.entities.Server
@@ -18,6 +17,7 @@ import io.legado.app.utils.GSON
 import io.legado.app.utils.setLayout
 import io.legado.app.utils.setLayout
 import io.legado.app.utils.viewbindingdelegate.viewBinding
 import io.legado.app.utils.viewbindingdelegate.viewBinding
 import org.json.JSONObject
 import org.json.JSONObject
+import org.koin.androidx.viewmodel.ext.android.viewModel
 
 
 class ServerConfigDialog() : BaseDialogFragment(R.layout.dialog_webdav_server, true),
 class ServerConfigDialog() : BaseDialogFragment(R.layout.dialog_webdav_server, true),
     Toolbar.OnMenuItemClickListener {
     Toolbar.OnMenuItemClickListener {
@@ -29,7 +29,7 @@ class ServerConfigDialog() : BaseDialogFragment(R.layout.dialog_webdav_server, t
     }
     }
 
 
     private val binding by viewBinding(DialogWebdavServerBinding::bind)
     private val binding by viewBinding(DialogWebdavServerBinding::bind)
-    private val viewModel by viewModels<ServerConfigViewModel>()
+    private val viewModel by viewModel<ServerConfigViewModel>()
 
 
     private val webDavServerUi = listOf(
     private val webDavServerUi = listOf(
         RowUi("url"),
         RowUi("url"),
@@ -128,4 +128,4 @@ class ServerConfigDialog() : BaseDialogFragment(R.layout.dialog_webdav_server, t
         return data
         return data
     }
     }
 
 
-}
+}

+ 9 - 6
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 android.app.Application
 import io.legado.app.base.BaseViewModel
 import io.legado.app.base.BaseViewModel
-import io.legado.app.data.appDb
 import io.legado.app.data.entities.Server
 import io.legado.app.data.entities.Server
+import io.legado.app.data.repository.RemoteBookRepository
 import io.legado.app.utils.toastOnUi
 import io.legado.app.utils.toastOnUi
 
 
-class ServerConfigViewModel(application: Application): BaseViewModel(application) {
+class ServerConfigViewModel(
+    application: Application,
+    private val remoteBookRepository: RemoteBookRepository
+) : BaseViewModel(application) {
 
 
     var mServer: Server? = null
     var mServer: Server? = null
 
 
@@ -15,7 +18,7 @@ class ServerConfigViewModel(application: Application): BaseViewModel(application
         if (mServer != null) return
         if (mServer != null) return
         execute {
         execute {
             mServer = if (id != null) {
             mServer = if (id != null) {
-                appDb.serverDao.get(id)
+                remoteBookRepository.getServer(id)
             } else {
             } else {
                 Server()
                 Server()
             }
             }
@@ -27,10 +30,10 @@ class ServerConfigViewModel(application: Application): BaseViewModel(application
     fun save(server: Server, onSuccess: () -> Unit) {
     fun save(server: Server, onSuccess: () -> Unit) {
         execute {
         execute {
             mServer?.let {
             mServer?.let {
-                appDb.serverDao.delete(it)
+                remoteBookRepository.deleteServer(it)
             }
             }
             mServer = server
             mServer = server
-            appDb.serverDao.insert(server)
+            remoteBookRepository.saveServer(server)
         }.onSuccess {
         }.onSuccess {
             onSuccess.invoke()
             onSuccess.invoke()
         }.onError {
         }.onError {
@@ -38,4 +41,4 @@ class ServerConfigViewModel(application: Application): BaseViewModel(application
         }
         }
     }
     }
 
 
-}
+}

+ 3 - 4
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.View
 import android.view.ViewGroup
 import android.view.ViewGroup
 import androidx.appcompat.widget.Toolbar
 import androidx.appcompat.widget.Toolbar
-import androidx.fragment.app.viewModels
 import androidx.lifecycle.lifecycleScope
 import androidx.lifecycle.lifecycleScope
 import androidx.recyclerview.widget.LinearLayoutManager
 import androidx.recyclerview.widget.LinearLayoutManager
 import io.legado.app.R
 import io.legado.app.R
@@ -16,7 +15,6 @@ import io.legado.app.base.adapter.ItemViewHolder
 import io.legado.app.base.adapter.RecyclerAdapter
 import io.legado.app.base.adapter.RecyclerAdapter
 import io.legado.app.constant.AppConst.DEFAULT_WEBDAV_ID
 import io.legado.app.constant.AppConst.DEFAULT_WEBDAV_ID
 import io.legado.app.constant.AppLog
 import io.legado.app.constant.AppLog
-import io.legado.app.data.appDb
 import io.legado.app.data.entities.Server
 import io.legado.app.data.entities.Server
 import io.legado.app.databinding.DialogRecyclerViewBinding
 import io.legado.app.databinding.DialogRecyclerViewBinding
 import io.legado.app.databinding.ItemServerSelectBinding
 import io.legado.app.databinding.ItemServerSelectBinding
@@ -34,6 +32,7 @@ import kotlinx.coroutines.Dispatchers.IO
 import kotlinx.coroutines.flow.catch
 import kotlinx.coroutines.flow.catch
 import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.launch
+import org.koin.androidx.viewmodel.ext.android.viewModel
 
 
 /**
 /**
  * 服务器配置
  * 服务器配置
@@ -42,7 +41,7 @@ class ServersDialog : BaseDialogFragment(R.layout.dialog_recycler_view),
     Toolbar.OnMenuItemClickListener {
     Toolbar.OnMenuItemClickListener {
 
 
     val binding by viewBinding(DialogRecyclerViewBinding::bind)
     val binding by viewBinding(DialogRecyclerViewBinding::bind)
-    val viewModel by viewModels<ServersViewModel>()
+    val viewModel by viewModel<ServersViewModel>()
 
 
     private val callback get() = (activity as? Callback)
     private val callback get() = (activity as? Callback)
     private val adapter by lazy { ServersAdapter(requireContext()) }
     private val adapter by lazy { ServersAdapter(requireContext()) }
@@ -86,7 +85,7 @@ class ServersDialog : BaseDialogFragment(R.layout.dialog_recycler_view),
 
 
     private fun initData() {
     private fun initData() {
         lifecycleScope.launch {
         lifecycleScope.launch {
-            appDb.serverDao.observeAll().catch {
+            viewModel.flowServers().catch {
                 AppLog.put("服务器配置界面获取数据失败\n${it.localizedMessage}", it)
                 AppLog.put("服务器配置界面获取数据失败\n${it.localizedMessage}", it)
             }.flowOn(IO).collect {
             }.flowOn(IO).collect {
                 adapter.setItems(it)
                 adapter.setItems(it)

+ 8 - 4
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 android.app.Application
 import io.legado.app.base.BaseViewModel
 import io.legado.app.base.BaseViewModel
-import io.legado.app.data.appDb
 import io.legado.app.data.entities.Server
 import io.legado.app.data.entities.Server
+import io.legado.app.data.repository.RemoteBookRepository
 
 
-class ServersViewModel(application: Application): BaseViewModel(application) {
+class ServersViewModel(
+    application: Application,
+    private val remoteBookRepository: RemoteBookRepository
+) : BaseViewModel(application) {
 
 
+    fun flowServers() = remoteBookRepository.flowServers()
 
 
     fun delete(server: Server) {
     fun delete(server: Server) {
         execute {
         execute {
-            appDb.serverDao.delete(server)
+            remoteBookRepository.deleteServer(server)
         }
         }
     }
     }
 
 
-}
+}

+ 2 - 1
app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt

@@ -1,5 +1,6 @@
 package io.legado.app.ui.book.info
 package io.legado.app.ui.book.info
 
 
+import io.legado.app.ui.config.otherConfig.OtherConfig
 import android.content.Intent
 import android.content.Intent
 import android.os.Bundle
 import android.os.Bundle
 import androidx.activity.compose.setContent
 import androidx.activity.compose.setContent
@@ -41,7 +42,7 @@ class BookInfoActivity : BaseComposeActivity(), VariableDialog.Callback {
     }
     }
     private val localBookTreeSelect = registerForActivityResult(HandleFileContract()) {
     private val localBookTreeSelect = registerForActivityResult(HandleFileContract()) {
         it.uri?.let { treeUri ->
         it.uri?.let { treeUri ->
-            AppConfig.defaultBookTreeUri = treeUri.toString()
+            OtherConfig.defaultBookTreeUri = treeUri.toString()
         }
         }
     }
     }
     private val infoEditResult = registerForActivityResult(
     private val infoEditResult = registerForActivityResult(

+ 2 - 0
app/src/main/java/io/legado/app/ui/book/info/BookInfoContract.kt

@@ -19,6 +19,8 @@ data class BookInfoUiState(
     val bookSource: BookSource? = null,
     val bookSource: BookSource? = null,
     val isTocLoading: Boolean = true,
     val isTocLoading: Boolean = true,
     val isBusy: Boolean = false,
     val isBusy: Boolean = false,
+    val deleteAlertEnabled: Boolean = true,
+    val deleteOriginal: Boolean = false,
     val showAppLogSheet: Boolean = false,
     val showAppLogSheet: Boolean = false,
     val sheet: BookInfoSheet = BookInfoSheet.None,
     val sheet: BookInfoSheet = BookInfoSheet.None,
     val dialog: BookInfoDialog? = null,
     val dialog: BookInfoDialog? = null,

+ 2 - 3
app/src/main/java/io/legado/app/ui/book/info/BookInfoScreen.kt

@@ -573,7 +573,7 @@ private fun BookInfoOverflowMenu(
         RoundDropdownMenuItem(
         RoundDropdownMenuItem(
             text = stringResource(R.string.delete_alert),
             text = stringResource(R.string.delete_alert),
             onClick = { onMenuAction(BookInfoMenuAction.ToggleDeleteAlert) },
             onClick = { onMenuAction(BookInfoMenuAction.ToggleDeleteAlert) },
-            isSelected = io.legado.app.help.config.LocalConfig.bookInfoDeleteAlert
+            isSelected = state.deleteAlertEnabled
         )
         )
         RoundDropdownMenuItem(
         RoundDropdownMenuItem(
             text = stringResource(R.string.clear_cache),
             text = stringResource(R.string.clear_cache),
@@ -894,7 +894,7 @@ private fun BookInfoDialogs(
     onBack: () -> Unit,
     onBack: () -> Unit,
 ) {
 ) {
     val dialog = state.dialog
     val dialog = state.dialog
-    var deleteOriginal by remember(dialog) { mutableStateOf(io.legado.app.help.config.LocalConfig.deleteBookOriginal) }
+    var deleteOriginal by remember(dialog, state.deleteOriginal) { mutableStateOf(state.deleteOriginal) }
     var remarkText by remember(dialog) { mutableStateOf((dialog as? BookInfoDialog.EditRemark)?.remark.orEmpty()) }
     var remarkText by remember(dialog) { mutableStateOf((dialog as? BookInfoDialog.EditRemark)?.remark.orEmpty()) }
 
 
     if (dialog is BookInfoDialog.AddToShelfOnBack) {
     if (dialog is BookInfoDialog.AddToShelfOnBack) {
@@ -918,7 +918,6 @@ private fun BookInfoDialogs(
             text = stringResource(R.string.sure_del),
             text = stringResource(R.string.sure_del),
             confirmText = stringResource(android.R.string.ok),
             confirmText = stringResource(android.R.string.ok),
             onConfirm = {
             onConfirm = {
-                io.legado.app.help.config.LocalConfig.deleteBookOriginal = deleteOriginal
                 onIntent(BookInfoIntent.ConfirmDelete(deleteOriginal))
                 onIntent(BookInfoIntent.ConfirmDelete(deleteOriginal))
             },
             },
             dismissText = stringResource(android.R.string.cancel),
             dismissText = stringResource(android.R.string.cancel),

+ 8 - 2
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.entities.readRecord.ReadRecordTimelineDay
 import io.legado.app.data.repository.ReadRecordRepository
 import io.legado.app.data.repository.ReadRecordRepository
 import io.legado.app.data.repository.RemoteBookRepository
 import io.legado.app.data.repository.RemoteBookRepository
+import io.legado.app.domain.usecase.ClearBookCacheUseCase
 import io.legado.app.exception.NoBooksDirException
 import io.legado.app.exception.NoBooksDirException
 import io.legado.app.exception.NoStackTraceException
 import io.legado.app.exception.NoStackTraceException
 import io.legado.app.help.book.BookHelp
 import io.legado.app.help.book.BookHelp
@@ -63,7 +64,8 @@ import kotlinx.coroutines.launch
 class BookInfoViewModel(
 class BookInfoViewModel(
     application: Application,
     application: Application,
     private val remoteBookRepository: RemoteBookRepository,
     private val remoteBookRepository: RemoteBookRepository,
-    private val readRecordRepository: ReadRecordRepository
+    private val readRecordRepository: ReadRecordRepository,
+    private val clearBookCacheUseCase: ClearBookCacheUseCase
 ) : BaseViewModel(application) {
 ) : BaseViewModel(application) {
 
 
     private val _uiState = MutableStateFlow(BookInfoUiState())
     private val _uiState = MutableStateFlow(BookInfoUiState())
@@ -400,7 +402,7 @@ class BookInfoViewModel(
     fun clearCache() {
     fun clearCache() {
         currentBook?.let { book ->
         currentBook?.let { book ->
             execute {
             execute {
-                BookHelp.clearCache(book)
+                clearBookCacheUseCase.execute(book.bookUrl)
                 if (ReadBook.book?.bookUrl == book.bookUrl) {
                 if (ReadBook.book?.bookUrl == book.bookUrl) {
                     ReadBook.clearTextChapter()
                     ReadBook.clearTextChapter()
                 }
                 }
@@ -851,6 +853,8 @@ class BookInfoViewModel(
     }
     }
     private fun deleteBook(deleteOriginal: Boolean) {
     private fun deleteBook(deleteOriginal: Boolean) {
         currentBook?.let { book ->
         currentBook?.let { book ->
+            LocalConfig.deleteBookOriginal = deleteOriginal
+            _uiState.update { it.copy(deleteOriginal = deleteOriginal) }
             SourceCallBack.callBackBook(SourceCallBack.DEL_BOOK_SHELF, bookSource, book)
             SourceCallBack.callBackBook(SourceCallBack.DEL_BOOK_SHELF, bookSource, book)
             delBook(deleteOriginal) {
             delBook(deleteOriginal) {
                 emitEffect(BookInfoEffect.Finish(resultCode = RESULT_OK))
                 emitEffect(BookInfoEffect.Finish(resultCode = RESULT_OK))
@@ -1168,6 +1172,8 @@ class BookInfoViewModel(
                 inBookshelf = inBookshelf,
                 inBookshelf = inBookshelf,
                 bookSource = bookSource,
                 bookSource = bookSource,
                 isTocLoading = isTocLoading,
                 isTocLoading = isTocLoading,
+                deleteAlertEnabled = LocalConfig.bookInfoDeleteAlert,
+                deleteOriginal = LocalConfig.deleteBookOriginal,
             )
             )
         }
         }
     }
     }

+ 16 - 3
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.Book.ReadConfig
 import io.legado.app.data.entities.BookChapter
 import io.legado.app.data.entities.BookChapter
 import io.legado.app.data.entities.BookProgress
 import io.legado.app.data.entities.BookProgress
+import io.legado.app.domain.model.ReadingProgress
+import io.legado.app.domain.usecase.GetReadingProgressUseCase
 import io.legado.app.exception.NoStackTraceException
 import io.legado.app.exception.NoStackTraceException
-import io.legado.app.help.AppWebDav
 import io.legado.app.help.book.BookHelp
 import io.legado.app.help.book.BookHelp
 import io.legado.app.help.book.isLocal
 import io.legado.app.help.book.isLocal
 import io.legado.app.help.book.isLocalModified
 import io.legado.app.help.book.isLocalModified
@@ -47,7 +48,10 @@ import kotlinx.coroutines.withContext
 import splitties.init.appCtx
 import splitties.init.appCtx
 import java.io.ByteArrayOutputStream
 import java.io.ByteArrayOutputStream
 
 
-class ReadMangaViewModel(application: Application) : BaseViewModel(application) {
+class ReadMangaViewModel(
+    application: Application,
+    private val getReadingProgressUseCase: GetReadingProgressUseCase
+) : BaseViewModel(application) {
 
 
     private var changeSourceCoroutine: Coroutine<*>? = null
     private var changeSourceCoroutine: Coroutine<*>? = null
 
 
@@ -243,7 +247,7 @@ class ReadMangaViewModel(application: Application) : BaseViewModel(application)
     ) {
     ) {
         if (!AppConfig.syncBookProgress) return
         if (!AppConfig.syncBookProgress) return
         execute {
         execute {
-            AppWebDav.getBookProgress(book)
+            getReadingProgressUseCase.execute(book.name, book.author)?.toBookProgress()
         }.onError {
         }.onError {
             AppLog.put("拉取阅读进度失败《${book.name}》\n${it.localizedMessage}", it)
             AppLog.put("拉取阅读进度失败《${book.name}》\n${it.localizedMessage}", it)
         }.onSuccess { progress ->
         }.onSuccess { progress ->
@@ -260,6 +264,15 @@ class ReadMangaViewModel(application: Application) : BaseViewModel(application)
         }
         }
     }
     }
 
 
+    private fun ReadingProgress.toBookProgress() = BookProgress(
+        name = name,
+        author = author,
+        durChapterIndex = durChapterIndex,
+        durChapterPos = durChapterPos,
+        durChapterTime = durChapterTime,
+        durChapterTitle = durChapterTitle
+    )
+
     /**
     /**
      * 换源
      * 换源
      */
      */

+ 3 - 6
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.BookProgress
 import io.legado.app.data.entities.BookSource
 import io.legado.app.data.entities.BookSource
 import io.legado.app.exception.NoStackTraceException
 import io.legado.app.exception.NoStackTraceException
-import io.legado.app.help.AppWebDav
 import io.legado.app.help.IntentData
 import io.legado.app.help.IntentData
 import io.legado.app.help.TTS
 import io.legado.app.help.TTS
 import io.legado.app.help.book.BookHelp
 import io.legado.app.help.book.BookHelp
@@ -50,7 +49,6 @@ import io.legado.app.help.book.isLocal
 import io.legado.app.help.book.isLocalTxt
 import io.legado.app.help.book.isLocalTxt
 import io.legado.app.help.book.isMobi
 import io.legado.app.help.book.isMobi
 import io.legado.app.help.book.removeType
 import io.legado.app.help.book.removeType
-import io.legado.app.help.book.update
 import io.legado.app.help.config.AppConfig
 import io.legado.app.help.config.AppConfig
 import io.legado.app.help.config.ReadBookConfig
 import io.legado.app.help.config.ReadBookConfig
 import io.legado.app.help.config.ReadTipConfig
 import io.legado.app.help.config.ReadTipConfig
@@ -525,10 +523,10 @@ class ReadBookActivity : BaseReadBookActivity(),
         }
         }
         lifecycleScope.launch {
         lifecycleScope.launch {
             menu.findItem(R.id.menu_get_progress)?.isVisible = withContext(IO) {
             menu.findItem(R.id.menu_get_progress)?.isVisible = withContext(IO) {
-                AppWebDav.isOk
+                viewModel.isReadingProgressSyncConfigured()
             }
             }
             menu.findItem(R.id.menu_cover_progress)?.isVisible = withContext(IO) {
             menu.findItem(R.id.menu_cover_progress)?.isVisible = withContext(IO) {
-                AppWebDav.isOk
+                viewModel.isReadingProgressSyncConfigured()
             }
             }
         }
         }
     }
     }
@@ -1841,9 +1839,8 @@ class ReadBookActivity : BaseReadBookActivity(),
         backupJob = lifecycleScope.launch(IO) {
         backupJob = lifecycleScope.launch(IO) {
             delay(300000)
             delay(300000)
             ReadBook.book?.let {
             ReadBook.book?.let {
-                AppWebDav.uploadBookProgress(it)
+                viewModel.uploadBookProgress(it)
                 ensureActive()
                 ensureActive()
-                it.update()
                 Backup.autoBack(this@ReadBookActivity)
                 Backup.autoBack(this@ReadBookActivity)
             }
             }
         }
         }

+ 38 - 3
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.Book
 import io.legado.app.data.entities.BookChapter
 import io.legado.app.data.entities.BookChapter
 import io.legado.app.data.entities.BookProgress
 import io.legado.app.data.entities.BookProgress
+import io.legado.app.domain.model.ReadingProgress
+import io.legado.app.domain.usecase.GetReadingProgressUseCase
+import io.legado.app.domain.usecase.UploadReadingProgressUseCase
 import io.legado.app.exception.NoStackTraceException
 import io.legado.app.exception.NoStackTraceException
-import io.legado.app.help.AppWebDav
 import io.legado.app.help.book.BookHelp
 import io.legado.app.help.book.BookHelp
 import io.legado.app.help.book.ContentProcessor
 import io.legado.app.help.book.ContentProcessor
 import io.legado.app.help.book.isLocal
 import io.legado.app.help.book.isLocal
@@ -52,7 +54,11 @@ import kotlin.coroutines.coroutineContext
 /**
 /**
  * 阅读界面数据处理
  * 阅读界面数据处理
  */
  */
-class ReadBookViewModel(application: Application) : BaseViewModel(application) {
+class ReadBookViewModel(
+    application: Application,
+    private val getReadingProgressUseCase: GetReadingProgressUseCase,
+    private val uploadReadingProgressUseCase: UploadReadingProgressUseCase
+) : BaseViewModel(application) {
     val permissionDenialLiveData = MutableLiveData<Int>()
     val permissionDenialLiveData = MutableLiveData<Int>()
     var isInitFinish = false
     var isInitFinish = false
     var searchContentQuery = ""
     var searchContentQuery = ""
@@ -253,7 +259,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
     ) {
     ) {
         if (!AppConfig.syncBookProgress) return
         if (!AppConfig.syncBookProgress) return
         execute {
         execute {
-            AppWebDav.getBookProgress(book)
+            getReadingProgressUseCase.execute(book.name, book.author)?.toBookProgress()
         }.onError {
         }.onError {
             AppLog.put("拉取阅读进度失败《${book.name}》\n${it.localizedMessage}", it)
             AppLog.put("拉取阅读进度失败《${book.name}》\n${it.localizedMessage}", it)
         }.onSuccess { progress ->
         }.onSuccess { progress ->
@@ -270,6 +276,35 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
         }
         }
     }
     }
 
 
+    fun isReadingProgressSyncConfigured(): Boolean {
+        return getReadingProgressUseCase.isConfigured
+    }
+
+    suspend fun uploadBookProgress(book: Book) {
+        uploadReadingProgressUseCase.execute(book.toReadingProgress())?.let { uploadTime ->
+            book.syncTime = uploadTime
+            appDb.bookDao.update(book)
+        }
+    }
+
+    private fun Book.toReadingProgress() = ReadingProgress(
+        name = name,
+        author = author,
+        durChapterIndex = durChapterIndex,
+        durChapterPos = durChapterPos,
+        durChapterTime = durChapterTime,
+        durChapterTitle = durChapterTitle
+    )
+
+    private fun ReadingProgress.toBookProgress() = BookProgress(
+        name = name,
+        author = author,
+        durChapterIndex = durChapterIndex,
+        durChapterPos = durChapterPos,
+        durChapterTime = durChapterTime,
+        durChapterTitle = durChapterTitle
+    )
+
     /**
     /**
      * 换源
      * 换源
      */
      */

+ 0 - 7
app/src/main/java/io/legado/app/ui/book/search/BookKey.kt

@@ -1,7 +0,0 @@
-package io.legado.app.ui.book.search
-
-data class BookKey(
-    val name: String,
-    val author: String,
-    val url: String?
-)

+ 1 - 1
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.data.entities.SearchBook
 import io.legado.app.databinding.ItemSearchBinding
 import io.legado.app.databinding.ItemSearchBinding
 import io.legado.app.help.config.AppConfig
 import io.legado.app.help.config.AppConfig
-import io.legado.app.model.BookShelfState
+import io.legado.app.domain.model.BookShelfState
 import io.legado.app.ui.widget.text.AccentBgTextView
 import io.legado.app.ui.widget.text.AccentBgTextView
 import io.legado.app.utils.dpToPx
 import io.legado.app.utils.dpToPx
 import io.legado.app.utils.gone
 import io.legado.app.utils.gone

+ 7 - 1
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.BookSourcePart
 import io.legado.app.data.entities.SearchBook
 import io.legado.app.data.entities.SearchBook
 import io.legado.app.data.entities.SearchKeyword
 import io.legado.app.data.entities.SearchKeyword
+import io.legado.app.domain.model.BookShelfState
 import io.legado.app.ui.main.bookshelf.BookShelfItem
 import io.legado.app.ui.main.bookshelf.BookShelfItem
 
 
+data class SearchResultItemUi(
+    val book: SearchBook,
+    val shelfState: BookShelfState = BookShelfState.NOT_IN_SHELF,
+)
+
 data class SearchUiState(
 data class SearchUiState(
     val query: String = "",
     val query: String = "",
     val committedQuery: String = "",
     val committedQuery: String = "",
-    val results: List<SearchBook> = emptyList(),
+    val results: List<SearchResultItemUi> = emptyList(),
     val history: List<SearchKeyword> = emptyList(),
     val history: List<SearchKeyword> = emptyList(),
     val bookshelfHints: List<BookShelfItem> = emptyList(),
     val bookshelfHints: List<BookShelfItem> = emptyList(),
     val enabledGroups: List<String> = emptyList(),
     val enabledGroups: List<String> = emptyList(),

+ 5 - 8
app/src/main/java/io/legado/app/ui/book/search/SearchScreen.kt

@@ -337,16 +337,13 @@ fun SearchScreen(
                             ) {
                             ) {
                                 itemsIndexed(
                                 itemsIndexed(
                                     items = state.results,
                                     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(
                                     SearchBookListItem(
-                                        book = book,
-                                        shelfState = shelfStateFlow,
+                                        book = item.book,
+                                        shelfState = item.shelfState,
                                         onClick = {
                                         onClick = {
-                                            viewModel.onIntent(SearchIntent.OpenSearchBook(book))
+                                            viewModel.onIntent(SearchIntent.OpenSearchBook(item.book))
                                         }
                                         }
                                     )
                                     )
                                 }
                                 }

+ 35 - 19
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.entities.SearchBook
 import io.legado.app.data.repository.SearchRepository
 import io.legado.app.data.repository.SearchRepository
 import io.legado.app.data.repository.SearchSessionEvent
 import io.legado.app.data.repository.SearchSessionEvent
+import io.legado.app.domain.usecase.BookShelfKey
+import io.legado.app.domain.usecase.ResolveBookShelfStateUseCase
 import io.legado.app.help.config.AppConfig
 import io.legado.app.help.config.AppConfig
-import io.legado.app.model.BookShelfState
 import io.legado.app.ui.main.bookshelf.BookShelfItem
 import io.legado.app.ui.main.bookshelf.BookShelfItem
 import io.legado.app.utils.getPrefBoolean
 import io.legado.app.utils.getPrefBoolean
 import io.legado.app.utils.putPrefBoolean
 import io.legado.app.utils.putPrefBoolean
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableSharedFlow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.asSharedFlow
 import kotlinx.coroutines.flow.asSharedFlow
@@ -29,6 +29,7 @@ import splitties.init.appCtx
 @OptIn(ExperimentalCoroutinesApi::class)
 @OptIn(ExperimentalCoroutinesApi::class)
 class SearchViewModel(
 class SearchViewModel(
     private val repository: SearchRepository,
     private val repository: SearchRepository,
+    private val resolveBookShelfStateUseCase: ResolveBookShelfStateUseCase,
 ) : ViewModel() {
 ) : ViewModel() {
 
 
     private val _uiState = MutableStateFlow(
     private val _uiState = MutableStateFlow(
@@ -46,7 +47,7 @@ class SearchViewModel(
     val effects = _effects.asSharedFlow()
     val effects = _effects.asSharedFlow()
 
 
     private val queryFlow = MutableStateFlow("")
     private val queryFlow = MutableStateFlow("")
-    private val bookshelfKeys = MutableStateFlow<Set<BookKey>>(emptySet())
+    private val bookshelfKeys = MutableStateFlow<Set<BookShelfKey>>(emptySet())
     private val searchScope = SearchScope(AppConfig.searchScope)
     private val searchScope = SearchScope(AppConfig.searchScope)
     private val searchSession = repository.createSearchSession { searchScope }
     private val searchSession = repository.createSearchSession { searchScope }
 
 
@@ -142,12 +143,6 @@ class SearchViewModel(
         }
         }
     }
     }
 
 
-    fun getBookShelfStateFlow(item: SearchBook): Flow<BookShelfState> {
-        return bookshelfKeys
-            .map { keys -> resolveBookShelfState(item, keys) }
-            .distinctUntilChanged()
-    }
-
     override fun onCleared() {
     override fun onCleared() {
         super.onCleared()
         super.onCleared()
         searchSession.close()
         searchSession.close()
@@ -194,6 +189,9 @@ class SearchViewModel(
                 .catch { emit(emptySet()) }
                 .catch { emit(emptySet()) }
                 .collect { keys ->
                 .collect { keys ->
                     bookshelfKeys.value = keys
                     bookshelfKeys.value = keys
+                    _uiState.update { state ->
+                        state.copy(results = state.results.withShelfState(keys))
+                    }
                 }
                 }
         }
         }
     }
     }
@@ -233,9 +231,10 @@ class SearchViewModel(
                     }
                     }
 
 
                     is SearchSessionEvent.Progress -> {
                     is SearchSessionEvent.Progress -> {
+                        val shelf = bookshelfKeys.value
                         _uiState.update {
                         _uiState.update {
                             it.copy(
                             it.copy(
-                                results = event.books,
+                                results = event.books.toSearchResultItems(shelf),
                                 processedSources = event.processedSources,
                                 processedSources = event.processedSources,
                                 totalSources = event.totalSources,
                                 totalSources = event.totalSources,
                             )
                             )
@@ -403,18 +402,35 @@ class SearchViewModel(
         }
         }
     }
     }
 
 
-    private fun resolveBookShelfState(item: SearchBook, shelf: Set<BookKey>): BookShelfState {
-        val exactMatch = shelf.any {
-            it.name == item.name && it.author == item.author && it.url == item.bookUrl
+    private fun List<SearchBook>.toSearchResultItems(
+        shelf: Set<BookShelfKey>
+    ): List<SearchResultItemUi> {
+        return map { book ->
+            SearchResultItemUi(
+                book = book,
+                shelfState = resolveBookShelfStateUseCase.execute(
+                    name = book.name,
+                    author = book.author,
+                    url = book.bookUrl,
+                    shelf = shelf
+                )
+            )
         }
         }
-        if (exactMatch) return BookShelfState.IN_SHELF
+    }
 
 
-        val sameNameAuthor = shelf.any {
-            it.name == item.name && it.author == item.author && it.url != item.bookUrl
+    private fun List<SearchResultItemUi>.withShelfState(
+        shelf: Set<BookShelfKey>
+    ): List<SearchResultItemUi> {
+        return map { item ->
+            item.copy(
+                shelfState = resolveBookShelfStateUseCase.execute(
+                    name = item.book.name,
+                    author = item.book.author,
+                    url = item.book.bookUrl,
+                    shelf = shelf
+                )
+            )
         }
         }
-        if (sameNameAuthor) return BookShelfState.SAME_NAME_AUTHOR
-
-        return BookShelfState.NOT_IN_SHELF
     }
     }
 
 
     private fun emitEffect(effect: SearchEffect) {
     private fun emitEffect(effect: SearchEffect) {

+ 19 - 8
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.BookChapter
 import io.legado.app.data.entities.Bookmark
 import io.legado.app.data.entities.Bookmark
 import io.legado.app.data.entities.ReplaceRule
 import io.legado.app.data.entities.ReplaceRule
+import io.legado.app.domain.usecase.CacheBookChaptersUseCase
 import io.legado.app.help.book.BookHelp
 import io.legado.app.help.book.BookHelp
 import io.legado.app.help.book.ContentProcessor
 import io.legado.app.help.book.ContentProcessor
 import io.legado.app.help.book.isLocal
 import io.legado.app.help.book.isLocal
@@ -113,7 +114,8 @@ data class FabAction(val icon: ImageVector, val label: String, val action: () ->
 @OptIn(ExperimentalCoroutinesApi::class)
 @OptIn(ExperimentalCoroutinesApi::class)
 class TocViewModel(
 class TocViewModel(
     application: Application,
     application: Application,
-    savedStateHandle: SavedStateHandle
+    savedStateHandle: SavedStateHandle,
+    private val cacheBookChaptersUseCase: CacheBookChaptersUseCase
 ) : BaseRuleViewModel<TocItemUi, TocDomainItem, Int, TocActionState>(
 ) : BaseRuleViewModel<TocItemUi, TocDomainItem, Int, TocActionState>(
     application,
     application,
     initialState = TocActionState()
     initialState = TocActionState()
@@ -524,15 +526,21 @@ class TocViewModel(
         val book = bookState.value ?: return
         val book = bookState.value ?: return
         val indices = uiState.value.selectedIds.toList()
         val indices = uiState.value.selectedIds.toList()
         if (indices.isEmpty()) return
         if (indices.isEmpty()) return
-        CacheBook.start(getApplication(), book, indices)
-        getApplication<Application>().toastOnUi("开始下载 ${indices.size} 个章节")
-        clearSelection()
+        execute {
+            cacheBookChaptersUseCase.execute(book.bookUrl, indices)
+        }.onSuccess { count ->
+            getApplication<Application>().toastOnUi("开始下载 $count 个章节")
+            clearSelection()
+        }
     }
     }
 
 
     fun downloadChapter(index: Int) {
     fun downloadChapter(index: Int) {
         val book = bookState.value ?: return
         val book = bookState.value ?: return
-        CacheBook.start(getApplication(), book, listOf(index))
-        getApplication<Application>().toastOnUi("开始下载章节")
+        execute {
+            cacheBookChaptersUseCase.execute(book.bookUrl, listOf(index))
+        }.onSuccess {
+            getApplication<Application>().toastOnUi("开始下载章节")
+        }
     }
     }
 
 
     fun downloadAll() {
     fun downloadAll() {
@@ -546,8 +554,11 @@ class TocViewModel(
             return
             return
         }
         }
 
 
-        CacheBook.start(getApplication(), book, targetIndices)
-        getApplication<Application>().toastOnUi("开始下载剩余 ${targetIndices.size} 个章节")
+        execute {
+            cacheBookChaptersUseCase.execute(book.bookUrl, targetIndices)
+        }.onSuccess { count ->
+            getApplication<Application>().toastOnUi("开始下载剩余 $count 个章节")
+        }
     }
     }
 
 
     private fun List<BookChapter>.groupAndReverseVolumes(): List<BookChapter> {
     private fun List<BookChapter>.groupAndReverseVolumes(): List<BookChapter> {

+ 8 - 10
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.ViewModel
 import androidx.lifecycle.viewModelScope
 import androidx.lifecycle.viewModelScope
-import io.legado.app.help.AppWebDav
+import io.legado.app.domain.usecase.WebDavBackupUseCase
 import io.legado.app.help.storage.Backup
 import io.legado.app.help.storage.Backup
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 import kotlinx.coroutines.withContext
 import splitties.init.appCtx
 import splitties.init.appCtx
 
 
-class BackupConfigViewModel : ViewModel() {
+class BackupConfigViewModel(
+    private val webDavBackupUseCase: WebDavBackupUseCase
+) : ViewModel() {
 
 
     private suspend fun syncWebDavConfig() {
     private suspend fun syncWebDavConfig() {
         withContext(Dispatchers.IO) {
         withContext(Dispatchers.IO) {
-            AppWebDav.upConfig()
+            webDavBackupUseCase.refreshConfig()
         }
         }
     }
     }
 
 
@@ -35,11 +37,9 @@ class BackupConfigViewModel : ViewModel() {
     }
     }
 
 
     suspend fun testWebDav(): Boolean {
     suspend fun testWebDav(): Boolean {
-        syncWebDavConfig()
         return withContext(Dispatchers.IO) {
         return withContext(Dispatchers.IO) {
             try {
             try {
-                AppWebDav.testWebDav()
-                true
+                webDavBackupUseCase.test()
             } catch (e: Exception) {
             } catch (e: Exception) {
                 false
                 false
             }
             }
@@ -62,17 +62,15 @@ class BackupConfigViewModel : ViewModel() {
     }
     }
 
 
     suspend fun getBackupNames(): List<String> {
     suspend fun getBackupNames(): List<String> {
-        syncWebDavConfig()
         return withContext(Dispatchers.IO) {
         return withContext(Dispatchers.IO) {
-            AppWebDav.getBackupNames()
+            webDavBackupUseCase.getBackupNames()
         }
         }
     }
     }
 
 
     fun restoreWebDav(name: String, onSuccess: () -> Unit, onError: (String) -> Unit) {
     fun restoreWebDav(name: String, onSuccess: () -> Unit, onError: (String) -> Unit) {
         viewModelScope.launch(Dispatchers.IO) {
         viewModelScope.launch(Dispatchers.IO) {
             try {
             try {
-                AppWebDav.upConfig()
-                AppWebDav.restoreWebDav(name)
+                webDavBackupUseCase.restore(name)
                 withContext(Dispatchers.Main) {
                 withContext(Dispatchers.Main) {
                     onSuccess()
                     onSuccess()
                 }
                 }

+ 0 - 5
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 {
 object ImportBookConfig {
 
 
-    var defaultBookTreeUri by prefDelegate(
-        PreferKey.defaultBookTreeUri,
-        null as String?
-    )
-
     var importBookPath by prefDelegate(
     var importBookPath by prefDelegate(
         PreferKey.importBookPath,
         PreferKey.importBookPath,
         null as String?
         null as String?

+ 1 - 1
app/src/main/java/io/legado/app/ui/config/otherConfig/OtherConfig.kt

@@ -48,7 +48,7 @@ object OtherConfig {
 
 
     var defaultBookTreeUri by prefDelegate(
     var defaultBookTreeUri by prefDelegate(
         PreferKey.defaultBookTreeUri,
         PreferKey.defaultBookTreeUri,
-        ""
+        null as String?
     )
     )
 
 
     var antiAlias by prefDelegate(
     var antiAlias by prefDelegate(

+ 9 - 7
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.ViewModel
 import androidx.lifecycle.viewModelScope
 import androidx.lifecycle.viewModelScope
 import io.legado.app.constant.PreferKey
 import io.legado.app.constant.PreferKey
-import io.legado.app.data.appDb
+import io.legado.app.domain.usecase.ClearBookCacheUseCase
+import io.legado.app.domain.usecase.ShrinkDatabaseUseCase
 import io.legado.app.help.DirectLinkUpload
 import io.legado.app.help.DirectLinkUpload
-import io.legado.app.help.book.BookHelp
 import io.legado.app.help.config.AppConfig
 import io.legado.app.help.config.AppConfig
 import io.legado.app.help.config.LocalConfig
 import io.legado.app.help.config.LocalConfig
 import io.legado.app.model.CheckSource
 import io.legado.app.model.CheckSource
@@ -25,7 +25,10 @@ import kotlinx.coroutines.delay
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.launch
 import splitties.init.appCtx
 import splitties.init.appCtx
 
 
-class OtherConfigViewModel : ViewModel() {
+class OtherConfigViewModel(
+    private val clearBookCacheUseCase: ClearBookCacheUseCase,
+    private val shrinkDatabaseUseCase: ShrinkDatabaseUseCase
+) : ViewModel() {
 
 
     private val packageManager = appCtx.packageManager
     private val packageManager = appCtx.packageManager
     private val componentName = ComponentName(
     private val componentName = ComponentName(
@@ -52,7 +55,7 @@ class OtherConfigViewModel : ViewModel() {
 
 
     fun clearCache(context: Context) {
     fun clearCache(context: Context) {
         viewModelScope.launch(Dispatchers.IO) {
         viewModelScope.launch(Dispatchers.IO) {
-            BookHelp.clearCache()
+            clearBookCacheUseCase.executeAll()
             FileUtils.delete(context.cacheDir.absolutePath)
             FileUtils.delete(context.cacheDir.absolutePath)
             context.externalCacheDir?.deleteRecursively()
             context.externalCacheDir?.deleteRecursively()
             context.cacheDir.deleteRecursively()
             context.cacheDir.deleteRecursively()
@@ -61,7 +64,7 @@ class OtherConfigViewModel : ViewModel() {
 
 
     fun shrinkDatabase() {
     fun shrinkDatabase() {
         viewModelScope.launch(Dispatchers.IO) {
         viewModelScope.launch(Dispatchers.IO) {
-            appDb.openHelper.writableDatabase.execSQL("VACUUM")
+            shrinkDatabaseUseCase.execute()
         }
         }
     }
     }
 
 
@@ -90,7 +93,6 @@ class OtherConfigViewModel : ViewModel() {
 
 
     fun updateLocalBookDir(path: String) {
     fun updateLocalBookDir(path: String) {
         OtherConfig.defaultBookTreeUri = path
         OtherConfig.defaultBookTreeUri = path
-        AppConfig.defaultBookTreeUri = OtherConfig.defaultBookTreeUri
     }
     }
 
 
     var checkSourceTimeout by mutableStateOf((CheckSource.timeout / 1000))
     var checkSourceTimeout by mutableStateOf((CheckSource.timeout / 1000))
@@ -154,4 +156,4 @@ class OtherConfigViewModel : ViewModel() {
     }
     }
 
 
 
 
-}
+}

+ 2 - 2
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.os.Bundle
 import android.text.method.LinkMovementMethod
 import android.text.method.LinkMovementMethod
 import android.view.View
 import android.view.View
-import androidx.fragment.app.viewModels
 import com.google.android.material.tabs.TabLayout
 import com.google.android.material.tabs.TabLayout
 import io.legado.app.R
 import io.legado.app.R
 import io.legado.app.base.BaseBottomSheetDialogFragment
 import io.legado.app.base.BaseBottomSheetDialogFragment
@@ -15,6 +14,7 @@ import io.legado.app.utils.setHtml
 import io.legado.app.utils.toastOnUi
 import io.legado.app.utils.toastOnUi
 import io.legado.app.utils.viewbindingdelegate.viewBinding
 import io.legado.app.utils.viewbindingdelegate.viewBinding
 import io.legado.app.utils.visible
 import io.legado.app.utils.visible
+import org.koin.androidx.viewmodel.ext.android.viewModel
 
 
 /**
 /**
  * 词典
  * 词典
@@ -27,7 +27,7 @@ class DictDialog() : BaseBottomSheetDialogFragment(R.layout.dialog_dict) {
         }
         }
     }
     }
 
 
-    private val viewModel by viewModels<DictViewModel>()
+    private val viewModel by viewModel<DictViewModel>()
     private val binding by viewBinding(DialogDictBinding::bind)
     private val binding by viewBinding(DialogDictBinding::bind)
     private var word: String? = null
     private var word: String? = null
     private var glideImageGetter: GlideImageGetter? = null
     private var glideImageGetter: GlideImageGetter? = null

+ 7 - 4
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 android.app.Application
 import io.legado.app.base.BaseViewModel
 import io.legado.app.base.BaseViewModel
-import io.legado.app.data.appDb
 import io.legado.app.data.entities.DictRule
 import io.legado.app.data.entities.DictRule
+import io.legado.app.data.repository.DictRuleRepository
 import io.legado.app.help.coroutine.Coroutine
 import io.legado.app.help.coroutine.Coroutine
 
 
-class DictViewModel(application: Application) : BaseViewModel(application) {
+class DictViewModel(
+    application: Application,
+    private val dictRuleRepository: DictRuleRepository
+) : BaseViewModel(application) {
 
 
     private var dictJob: Coroutine<String>? = null
     private var dictJob: Coroutine<String>? = null
 
 
     fun initData(onSuccess: (List<DictRule>) -> Unit) {
     fun initData(onSuccess: (List<DictRule>) -> Unit) {
         execute {
         execute {
-            appDb.dictRuleDao.enabled
+            dictRuleRepository.getEnabled()
         }.onSuccess {
         }.onSuccess {
             onSuccess.invoke(it)
             onSuccess.invoke(it)
         }
         }
@@ -34,4 +37,4 @@ class DictViewModel(application: Application) : BaseViewModel(application) {
     }
     }
 
 
 
 
-}
+}

+ 2 - 3
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.base.BaseComposeActivity
 import io.legado.app.constant.AppConst.appInfo
 import io.legado.app.constant.AppConst.appInfo
 import io.legado.app.constant.PreferKey
 import io.legado.app.constant.PreferKey
-import io.legado.app.help.AppWebDav
 import io.legado.app.help.book.BookHelp
 import io.legado.app.help.book.BookHelp
 import io.legado.app.help.config.AppConfig
 import io.legado.app.help.config.AppConfig
 import io.legado.app.help.config.LocalConfig
 import io.legado.app.help.config.LocalConfig
@@ -716,13 +715,13 @@ open class MainActivity : BaseComposeActivity() {
         }
         }
         lifecycleScope.launch {
         lifecycleScope.launch {
             val lastBackupFile =
             val lastBackupFile =
-                withContext(IO) { AppWebDav.lastBackUp().getOrNull() } ?: return@launch
+                withContext(IO) { viewModel.getLatestWebDavBackup() } ?: return@launch
             if (lastBackupFile.lastModify - LocalConfig.lastBackup > DateUtils.MINUTE_IN_MILLIS) {
             if (lastBackupFile.lastModify - LocalConfig.lastBackup > DateUtils.MINUTE_IN_MILLIS) {
                 LocalConfig.lastBackup = lastBackupFile.lastModify
                 LocalConfig.lastBackup = lastBackupFile.lastModify
                 alert(R.string.restore, R.string.webdav_after_local_restore_confirm) {
                 alert(R.string.restore, R.string.webdav_after_local_restore_confirm) {
                     cancelButton()
                     cancelButton()
                     okButton {
                     okButton {
-                        viewModel.restoreWebDav(lastBackupFile.displayName)
+                        viewModel.restoreWebDav(lastBackupFile.name)
                     }
                     }
                 }
                 }
             }
             }

+ 12 - 11
app/src/main/java/io/legado/app/ui/main/MainViewModel.kt

@@ -5,14 +5,17 @@ import android.content.Context
 import android.content.Intent
 import android.content.Intent
 import io.legado.app.base.BaseViewModel
 import io.legado.app.base.BaseViewModel
 import io.legado.app.constant.EventBus
 import io.legado.app.constant.EventBus
-import io.legado.app.data.appDb
-import io.legado.app.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.ui.main.my.PrefClickEvent
 import io.legado.app.utils.eventBus.FlowEventBus
 import io.legado.app.utils.eventBus.FlowEventBus
 import io.legado.app.utils.sendToClip
 import io.legado.app.utils.sendToClip
 
 
-class MainViewModel(application: Application) : BaseViewModel(application) {
+class MainViewModel(
+    application: Application,
+    private val appStartupMaintenanceUseCase: AppStartupMaintenanceUseCase,
+    private val webDavBackupUseCase: WebDavBackupUseCase
+) : BaseViewModel(application) {
 
 
     init {
     init {
         deleteNotShelfBook()
         deleteNotShelfBook()
@@ -24,23 +27,21 @@ class MainViewModel(application: Application) : BaseViewModel(application) {
 
 
     fun postLoad() {
     fun postLoad() {
         execute {
         execute {
-            if (appDb.httpTTSDao.count == 0) {
-                DefaultData.httpTTS.let {
-                    appDb.httpTTSDao.insert(*it.toTypedArray())
-                }
-            }
+            appStartupMaintenanceUseCase.ensureDefaultHttpTts()
         }
         }
     }
     }
 
 
     fun restoreWebDav(name: String) {
     fun restoreWebDav(name: String) {
         execute {
         execute {
-            AppWebDav.restoreWebDav(name)
+            webDavBackupUseCase.restore(name)
         }
         }
     }
     }
 
 
+    suspend fun getLatestWebDavBackup() = webDavBackupUseCase.getLatestBackup()
+
     private fun deleteNotShelfBook() {
     private fun deleteNotShelfBook() {
         execute {
         execute {
-            appDb.bookDao.deleteNotShelfBook()
+            appStartupMaintenanceUseCase.deleteNotShelfBooks()
         }
         }
     }
     }
 
 

+ 2 - 10
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 androidx.compose.ui.window.Dialog
 import io.legado.app.R
 import io.legado.app.R
 import io.legado.app.base.BaseRuleEvent
 import io.legado.app.base.BaseRuleEvent
-import io.legado.app.data.appDb
 import io.legado.app.data.entities.BookGroup
 import io.legado.app.data.entities.BookGroup
 import io.legado.app.ui.about.AppLogSheet
 import io.legado.app.ui.about.AppLogSheet
 import io.legado.app.ui.book.info.GroupSelectSheet
 import io.legado.app.ui.book.info.GroupSelectSheet
@@ -101,7 +100,6 @@ import io.legado.app.utils.readText
 import io.legado.app.utils.toastOnUi
 import io.legado.app.utils.toastOnUi
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.launch
 import org.koin.androidx.compose.koinViewModel
 import org.koin.androidx.compose.koinViewModel
 
 
@@ -226,14 +224,8 @@ fun BookshelfScreen(
     }
     }
     val isUsingStandaloneSearchGroup = uiState.isSearch &&
     val isUsingStandaloneSearchGroup = uiState.isSearch &&
             uiState.groups.none { it.groupId == currentGroupId }
             uiState.groups.none { it.groupId == currentGroupId }
-    val currentGroupBookCountFlow = remember(currentGroupId) {
-        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
     val bookGroupStyle = BookshelfConfig.bookGroupStyle
     // 控制是否处于“文件夹列表”根视图,还是“文件夹内部”书籍视图
     // 控制是否处于“文件夹列表”根视图,还是“文件夹内部”书籍视图

+ 2 - 0
app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfUiState.kt

@@ -13,6 +13,8 @@ data class BookshelfUiState(
     val allGroups: List<BookGroup> = emptyList(),
     val allGroups: List<BookGroup> = emptyList(),
     val groupPreviews: Map<Long, List<BookShelfItem>> = emptyMap(),
     val groupPreviews: Map<Long, List<BookShelfItem>> = emptyMap(),
     val groupBookCounts: Map<Long, Int> = emptyMap(),
     val groupBookCounts: Map<Long, Int> = emptyMap(),
+    val currentGroupBookCount: Int = 0,
+    val allBooksCount: Int = 0,
     val selectedGroupIndex: Int = 0,
     val selectedGroupIndex: Int = 0,
     val selectedGroupId: Long = BookGroup.IdAll,
     val selectedGroupId: Long = BookGroup.IdAll,
     val loadingText: String? = null,
     val loadingText: String? = null,

+ 7 - 5
app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt

@@ -1,5 +1,6 @@
 package io.legado.app.ui.main.bookshelf
 package io.legado.app.ui.main.bookshelf
 
 
+import kotlinx.coroutines.flow.flow
 import android.app.Application
 import android.app.Application
 import android.net.Uri
 import android.net.Uri
 import androidx.compose.runtime.snapshotFlow
 import androidx.compose.runtime.snapshotFlow
@@ -69,7 +70,6 @@ import kotlinx.coroutines.flow.collect
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.combine
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.flatMapLatest
 import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.flow
 import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.flowOn
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onCompletion
 import kotlinx.coroutines.flow.onCompletion
@@ -134,7 +134,8 @@ class BookshelfViewModel(
 
 
     private data class GroupPreviewState(
     private data class GroupPreviewState(
         val previews: Map<Long, List<BookShelfItem>>,
         val previews: Map<Long, List<BookShelfItem>>,
-        val counts: Map<Long, Int>
+        val counts: Map<Long, Int>,
+        val allBookCount: Int
     )
     )
 
 
     @OptIn(ExperimentalCoroutinesApi::class)
     @OptIn(ExperimentalCoroutinesApi::class)
@@ -206,9 +207,9 @@ class BookshelfViewModel(
                     }
                     }
                     previews[group.groupId] = result
                     previews[group.groupId] = result
                 }
                 }
-                GroupPreviewState(previews, counts)
+                GroupPreviewState(previews, counts, allBooks.size)
             } else {
             } else {
-                GroupPreviewState(emptyMap(), emptyMap())
+                GroupPreviewState(emptyMap(), emptyMap(), allBooks.size)
             }
             }
         }.distinctUntilChanged().flowOn(Dispatchers.Default)
         }.distinctUntilChanged().flowOn(Dispatchers.Default)
 
 
@@ -257,6 +258,8 @@ class BookshelfViewModel(
             allGroups = allGroups,
             allGroups = allGroups,
             groupPreviews = previews.previews,
             groupPreviews = previews.previews,
             groupBookCounts = previews.counts,
             groupBookCounts = previews.counts,
+            currentGroupBookCount = books.size,
+            allBooksCount = previews.allBookCount,
             selectedGroupIndex = groups.indexOfFirst { it.groupId == internal.groupId }
             selectedGroupIndex = groups.indexOfFirst { it.groupId == internal.groupId }
                 .coerceAtLeast(0),
                 .coerceAtLeast(0),
             selectedGroupId = internal.groupId,
             selectedGroupId = internal.groupId,
@@ -393,7 +396,6 @@ class BookshelfViewModel(
         if (bookUrls.isEmpty()) return
         if (bookUrls.isEmpty()) return
         execute {
         execute {
             batchCacheDownloadUseCase.execute(
             batchCacheDownloadUseCase.execute(
-                context = context,
                 bookUrls = bookUrls,
                 bookUrls = bookUrls,
                 downloadAllChapters = downloadAllChapters,
                 downloadAllChapters = downloadAllChapters,
                 skipAudioBooks = true
                 skipAudioBooks = true

+ 1 - 1
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 androidx.compose.ui.unit.dp
 import io.legado.app.R
 import io.legado.app.R
 import io.legado.app.data.entities.BookSourcePart
 import io.legado.app.data.entities.BookSourcePart
-import io.legado.app.domain.usecase.ExploreKindUiUseCase
+import io.legado.app.ui.widget.components.explore.ExploreKindUiUseCase
 import io.legado.app.ui.book.explore.ExploreShowActivity
 import io.legado.app.ui.book.explore.ExploreShowActivity
 import io.legado.app.ui.book.search.SearchActivity
 import io.legado.app.ui.book.search.SearchActivity
 import io.legado.app.ui.book.search.SearchScope
 import io.legado.app.ui.book.search.SearchScope

+ 0 - 1
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.R
 import io.legado.app.constant.AppConst
 import io.legado.app.constant.AppConst
 import io.legado.app.constant.AppLog
 import io.legado.app.constant.AppLog
-import io.legado.app.help.config.AppConfig
 import io.legado.app.help.http.CookieManager
 import io.legado.app.help.http.CookieManager
 import io.legado.app.lib.dialogs.SelectItem
 import io.legado.app.lib.dialogs.SelectItem
 import io.legado.app.lib.dialogs.selector
 import io.legado.app.lib.dialogs.selector

+ 3 - 3
app/src/main/java/io/legado/app/ui/welcome/BookFolderFragment.kt

@@ -1,11 +1,11 @@
 package io.legado.app.ui.welcome
 package io.legado.app.ui.welcome
 
 
+import io.legado.app.ui.config.otherConfig.OtherConfig
 import android.os.Bundle
 import android.os.Bundle
 import android.view.View
 import android.view.View
 import io.legado.app.R
 import io.legado.app.R
 import io.legado.app.base.BaseFragment
 import io.legado.app.base.BaseFragment
 import io.legado.app.databinding.FragmentBookFolderBinding
 import io.legado.app.databinding.FragmentBookFolderBinding
-import io.legado.app.help.config.AppConfig
 import io.legado.app.ui.file.HandleFileContract
 import io.legado.app.ui.file.HandleFileContract
 import io.legado.app.utils.viewbindingdelegate.viewBinding
 import io.legado.app.utils.viewbindingdelegate.viewBinding
 
 
@@ -15,7 +15,7 @@ class BookFolderFragment : BaseFragment(R.layout.fragment_book_folder) {
 
 
     private val selectBookFolder = registerForActivityResult(HandleFileContract()) { result ->
     private val selectBookFolder = registerForActivityResult(HandleFileContract()) { result ->
         result.uri?.let { treeUri ->
         result.uri?.let { treeUri ->
-            AppConfig.defaultBookTreeUri = treeUri.toString()
+            OtherConfig.defaultBookTreeUri = treeUri.toString()
             updatePathText()
             updatePathText()
         }
         }
     }
     }
@@ -32,6 +32,6 @@ class BookFolderFragment : BaseFragment(R.layout.fragment_book_folder) {
 
 
     private fun updatePathText() {
     private fun updatePathText() {
         binding.tvFolderPath.text =
         binding.tvFolderPath.text =
-            AppConfig.defaultBookTreeUri ?: getString(R.string.welcome_book_folder_not_selected)
+            OtherConfig.defaultBookTreeUri ?: getString(R.string.welcome_book_folder_not_selected)
     }
     }
 }
 }

+ 8 - 7
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.AppLog
 import io.legado.app.constant.PreferKey
 import io.legado.app.constant.PreferKey
 import io.legado.app.databinding.FragmentWebdavAuthBinding
 import io.legado.app.databinding.FragmentWebdavAuthBinding
+import io.legado.app.domain.usecase.WebDavBackupUseCase
 import io.legado.app.exception.NoStackTraceException
 import io.legado.app.exception.NoStackTraceException
-import io.legado.app.help.AppWebDav
-import io.legado.app.help.AppWebDav.testWebDav
 import io.legado.app.help.config.LocalConfig
 import io.legado.app.help.config.LocalConfig
 import io.legado.app.help.coroutine.Coroutine
 import io.legado.app.help.coroutine.Coroutine
 import io.legado.app.help.storage.Restore
 import io.legado.app.help.storage.Restore
@@ -35,11 +34,13 @@ import kotlinx.coroutines.Job
 import kotlinx.coroutines.ensureActive
 import kotlinx.coroutines.ensureActive
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 import kotlinx.coroutines.withContext
+import org.koin.android.ext.android.inject
 import splitties.init.appCtx
 import splitties.init.appCtx
 import kotlin.coroutines.coroutineContext
 import kotlin.coroutines.coroutineContext
 
 
 class WebDavFragment : BaseFragment(R.layout.fragment_webdav_auth) {
 class WebDavFragment : BaseFragment(R.layout.fragment_webdav_auth) {
 
 
+    private val webDavBackupUseCase by inject<WebDavBackupUseCase>()
     private var _binding: FragmentWebdavAuthBinding? = null
     private var _binding: FragmentWebdavAuthBinding? = null
     private val binding get() = _binding!!
     private val binding get() = _binding!!
     private var restoreJob: Job? = null
     private var restoreJob: Job? = null
@@ -105,7 +106,7 @@ class WebDavFragment : BaseFragment(R.layout.fragment_webdav_auth) {
             )
             )
             saveWebDavConfig(config)
             saveWebDavConfig(config)
             lifecycleScope.launch {
             lifecycleScope.launch {
-                testWebDav()
+                webDavBackupUseCase.test()
             }
             }
         }
         }
 
 
@@ -120,7 +121,7 @@ class WebDavFragment : BaseFragment(R.layout.fragment_webdav_auth) {
                     binding.progressRestore.visible()
                     binding.progressRestore.visible()
                     binding.btnRestore.text = ""
                     binding.btnRestore.text = ""
                     saveWebDavConfig(config)
                     saveWebDavConfig(config)
-                    AppWebDav.upConfig()
+                    webDavBackupUseCase.refreshConfig()
                     restore()
                     restore()
                 } finally {
                 } finally {
                     binding.progressRestore.gone()
                     binding.progressRestore.gone()
@@ -169,8 +170,8 @@ class WebDavFragment : BaseFragment(R.layout.fragment_webdav_auth) {
     }
     }
 
 
     private suspend fun showRestoreDialog(context: Context) {
     private suspend fun showRestoreDialog(context: Context) {
-        val names = withContext(IO) { AppWebDav.getBackupNames() }
-        if (AppWebDav.isJianGuoYun && names.size > 700) {
+        val names = withContext(IO) { webDavBackupUseCase.getBackupNames() }
+        if (webDavBackupUseCase.isJianGuoYun && names.size > 700) {
             context.toastOnUi("由于坚果云限制列出文件数量,部分备份可能未显示,请及时清理旧备份")
             context.toastOnUi("由于坚果云限制列出文件数量,部分备份可能未显示,请及时清理旧备份")
         }
         }
         if (names.isNotEmpty()) {
         if (names.isNotEmpty()) {
@@ -194,7 +195,7 @@ class WebDavFragment : BaseFragment(R.layout.fragment_webdav_auth) {
         waitDialog.setText("恢复中…")
         waitDialog.setText("恢复中…")
         waitDialog.show()
         waitDialog.show()
         val task = Coroutine.async {
         val task = Coroutine.async {
-            AppWebDav.restoreWebDav(name)
+            webDavBackupUseCase.restore(name)
         }.onError {
         }.onError {
             AppLog.put("WebDav恢复出错\n${it.localizedMessage}", it)
             AppLog.put("WebDav恢复出错\n${it.localizedMessage}", it)
             appCtx.toastOnUi("WebDav恢复出错\n${it.localizedMessage}")
             appCtx.toastOnUi("WebDav恢复出错\n${it.localizedMessage}")

+ 31 - 3
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.dp
 import androidx.compose.ui.unit.sp
 import androidx.compose.ui.unit.sp
 import io.legado.app.data.entities.SearchBook
 import io.legado.app.data.entities.SearchBook
-import io.legado.app.model.BookShelfState
+import io.legado.app.domain.model.BookShelfState
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.theme.LegadoTheme
 import io.legado.app.ui.widget.components.cover.Cover
 import io.legado.app.ui.widget.components.cover.Cover
 import io.legado.app.ui.widget.components.text.AppText
 import io.legado.app.ui.widget.components.text.AppText
@@ -46,8 +46,22 @@ fun SearchBookListItem(
     modifier: Modifier = Modifier,
     modifier: Modifier = Modifier,
 ) {
 ) {
     val currentShelfState by shelfState.collectAsState(initial = BookShelfState.NOT_IN_SHELF)
     val currentShelfState by shelfState.collectAsState(initial = BookShelfState.NOT_IN_SHELF)
+    SearchBookListItem(
+        book = book,
+        shelfState = currentShelfState,
+        onClick = onClick,
+        modifier = modifier,
+    )
+}
 
 
-    val badgeContent: (@Composable RowScope.() -> Unit)? = when (currentShelfState) {
+@Composable
+fun SearchBookListItem(
+    book: SearchBook,
+    shelfState: BookShelfState,
+    onClick: () -> Unit,
+    modifier: Modifier = Modifier,
+) {
+    val badgeContent: (@Composable RowScope.() -> Unit)? = when (shelfState) {
         BookShelfState.IN_SHELF -> {
         BookShelfState.IN_SHELF -> {
             {
             {
                 androidx.compose.material3.Icon(
                 androidx.compose.material3.Icon(
@@ -158,8 +172,22 @@ fun SearchBookGridItem(
     modifier: Modifier = Modifier,
     modifier: Modifier = Modifier,
 ) {
 ) {
     val currentShelfState by shelfState.collectAsState(initial = BookShelfState.NOT_IN_SHELF)
     val currentShelfState by shelfState.collectAsState(initial = BookShelfState.NOT_IN_SHELF)
+    SearchBookGridItem(
+        book = book,
+        shelfState = currentShelfState,
+        onClick = onClick,
+        modifier = modifier,
+    )
+}
 
 
-    val badgeText: String? = when (currentShelfState) {
+@Composable
+fun SearchBookGridItem(
+    book: SearchBook,
+    shelfState: BookShelfState,
+    onClick: () -> Unit,
+    modifier: Modifier = Modifier,
+) {
+    val badgeText: String? = when (shelfState) {
         BookShelfState.IN_SHELF -> "已在书架"
         BookShelfState.IN_SHELF -> "已在书架"
         BookShelfState.SAME_NAME_AUTHOR -> "同名书籍"
         BookShelfState.SAME_NAME_AUTHOR -> "同名书籍"
         BookShelfState.NOT_IN_SHELF -> null
         BookShelfState.NOT_IN_SHELF -> null

+ 1 - 1
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 androidx.compose.ui.unit.dp
 import io.legado.app.App
 import io.legado.app.App
 import io.legado.app.data.entities.rule.ExploreKind
 import io.legado.app.data.entities.rule.ExploreKind
-import io.legado.app.domain.usecase.ExploreKindUiUseCase
+import io.legado.app.ui.widget.components.explore.ExploreKindUiUseCase
 import io.legado.app.help.source.getExploreInfoMap
 import io.legado.app.help.source.getExploreInfoMap
 import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
 import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
 import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
 import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem

+ 1 - 1
app/src/main/java/io/legado/app/domain/usecase/ExploreKindUiUseCase.kt → 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 androidx.appcompat.app.AppCompatActivity
 import com.script.rhino.runScriptWithContext
 import com.script.rhino.runScriptWithContext