[优化] 架构优化
This commit is contained in:
@@ -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
|
||||
)
|
||||
+2
-2
@@ -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
|
||||
|
||||
+1
-1
@@ -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
-1
@@ -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
|
||||
Reference in New Issue
Block a user