[优化] 架构优化

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