瀏覽代碼

[优化] 远程书籍同步逻辑

HapeLee 8 月之前
父節點
當前提交
f8b24b9588

+ 71 - 0
app/src/main/java/io/legado/app/data/repository/RemoteBookRepository.kt

@@ -1,19 +1,90 @@
 package io.legado.app.data.repository
 package io.legado.app.data.repository
 
 
+import io.legado.app.constant.AppConst
 import io.legado.app.constant.BookType
 import io.legado.app.constant.BookType
 import io.legado.app.data.AppDatabase
 import io.legado.app.data.AppDatabase
 import io.legado.app.data.entities.Book
 import io.legado.app.data.entities.Book
+import io.legado.app.exception.NoStackTraceException
+import io.legado.app.help.AppWebDav
+import io.legado.app.help.book.getRemoteUrl
+import io.legado.app.help.book.isLocal
+import io.legado.app.help.config.AppConfig
 import io.legado.app.lib.webdav.Authorization
 import io.legado.app.lib.webdav.Authorization
+import io.legado.app.model.analyzeRule.AnalyzeUrl
 import io.legado.app.model.analyzeRule.CustomUrl
 import io.legado.app.model.analyzeRule.CustomUrl
 import io.legado.app.model.localBook.LocalBook
 import io.legado.app.model.localBook.LocalBook
 import io.legado.app.model.remote.RemoteBook
 import io.legado.app.model.remote.RemoteBook
 import io.legado.app.model.remote.RemoteBookWebDav
 import io.legado.app.model.remote.RemoteBookWebDav
+import io.legado.app.utils.isContentScheme
 import kotlinx.coroutines.flow.Flow
 import kotlinx.coroutines.flow.Flow
 
 
 class RemoteBookRepository(
 class RemoteBookRepository(
     private val appDb: AppDatabase
     private val appDb: AppDatabase
 ) {
 ) {
 
 
+    suspend fun getWebDav(book: Book): RemoteBookWebDav? {
+        val remoteUrl = book.getRemoteUrl() ?: return null
+        val analyzeUrl = AnalyzeUrl(remoteUrl)
+
+        // 优先从书籍 origin 中解析 serverID
+        val serverId = analyzeUrl.serverID
+        if (serverId != null && serverId != AppConst.DEFAULT_WEBDAV_ID) {
+            appDb.serverDao.get(serverId)?.getWebDavConfig()?.let {
+                return RemoteBookWebDav(it.url, Authorization(it), serverId)
+            }
+        }
+
+        // 如果没有指定 serverID,尝试使用目前选择的 WebDAV 服务器
+        val currentServerId = AppConfig.remoteServerId
+        if (currentServerId != AppConst.DEFAULT_WEBDAV_ID) {
+            appDb.serverDao.get(currentServerId)?.getWebDavConfig()?.let {
+                return RemoteBookWebDav(it.url, Authorization(it), currentServerId)
+            }
+        }
+
+        // 最后回退到默认的 WebDAV 配置
+        return AppWebDav.defaultBookWebDav
+    }
+
+    suspend fun refreshLocalBook(book: Book): Boolean {
+        if (!book.isLocal) return false
+        val remoteUrl = book.getRemoteUrl() ?: return false
+        val webDav = getWebDav(book) ?: throw NoStackTraceException("webDav没有配置")
+        val remoteBook = webDav.getRemoteBook(remoteUrl)
+        if (remoteBook == null) {
+            book.origin = BookType.localTag
+            return true
+        }
+        if (remoteBook.lastModify > book.lastCheckTime) {
+            val uri = webDav.downloadRemoteBook(remoteBook)
+            book.bookUrl = if (uri.isContentScheme()) uri.toString() else uri.path!!
+            book.lastCheckTime = remoteBook.lastModify
+            return true
+        }
+        return false
+    }
+
+    suspend fun syncBookFromRemote(book: Book): Book {
+        val remoteUrl = book.getRemoteUrl() ?: throw NoStackTraceException("不是远程书籍")
+        val webDav = getWebDav(book) ?: throw NoStackTraceException("webDav没有配置")
+        val remoteBook =
+            webDav.getRemoteBook(remoteUrl) ?: throw NoStackTraceException("远程文件不存在")
+        val downloadBookUri = webDav.downloadRemoteBook(remoteBook)
+        val importedBooks = LocalBook.importFiles(downloadBookUri)
+        val newBook = importedBooks.firstOrNull() ?: throw NoStackTraceException("导入失败")
+        newBook.durChapterIndex = book.durChapterIndex
+        newBook.durChapterPos = book.durChapterPos
+        newBook.order = book.order
+        newBook.group = book.group
+        return newBook
+    }
+
+    suspend fun uploadBook(book: Book) {
+        val webDav = getWebDav(book) ?: throw NoStackTraceException("未配置webDav")
+        webDav.upload(book)
+        book.lastCheckTime = System.currentTimeMillis()
+    }
+
     suspend fun createWebDav(serverId: Long): RemoteBookWebDav? {
     suspend fun createWebDav(serverId: Long): RemoteBookWebDav? {
         return appDb.serverDao.get(serverId)
         return appDb.serverDao.get(serverId)
             ?.getWebDavConfig()
             ?.getWebDavConfig()

+ 2 - 0
app/src/main/java/io/legado/app/di/appModule.kt

@@ -12,6 +12,7 @@ import io.legado.app.data.repository.UploadRepository
 import io.legado.app.ui.book.bookmark.AllBookmarkViewModel
 import io.legado.app.ui.book.bookmark.AllBookmarkViewModel
 import io.legado.app.ui.book.explore.ExploreShowViewModel
 import io.legado.app.ui.book.explore.ExploreShowViewModel
 import io.legado.app.ui.book.import.remote.RemoteBookViewModel
 import io.legado.app.ui.book.import.remote.RemoteBookViewModel
+import io.legado.app.ui.book.info.BookInfoViewModel
 import io.legado.app.ui.book.readRecord.ReadRecordViewModel
 import io.legado.app.ui.book.readRecord.ReadRecordViewModel
 import io.legado.app.ui.book.searchContent.SearchContentViewModel
 import io.legado.app.ui.book.searchContent.SearchContentViewModel
 import io.legado.app.ui.book.toc.TocViewModel
 import io.legado.app.ui.book.toc.TocViewModel
@@ -51,6 +52,7 @@ val appModule = module {
     viewModelOf(::OtherConfigViewModel)
     viewModelOf(::OtherConfigViewModel)
     viewModelOf(::TocViewModel)
     viewModelOf(::TocViewModel)
     viewModelOf(::RemoteBookViewModel)
     viewModelOf(::RemoteBookViewModel)
+    viewModelOf(::BookInfoViewModel)
 
 
     viewModel { (route: ReplaceEditRoute) ->
     viewModel { (route: ReplaceEditRoute) ->
         ReplaceEditViewModel(
         ReplaceEditViewModel(

+ 6 - 48
app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt

@@ -22,7 +22,6 @@ import android.view.View
 import android.widget.CheckBox
 import android.widget.CheckBox
 import android.widget.LinearLayout
 import android.widget.LinearLayout
 import androidx.activity.addCallback
 import androidx.activity.addCallback
-import androidx.activity.viewModels
 import androidx.core.app.ActivityCompat
 import androidx.core.app.ActivityCompat
 import androidx.core.app.ActivityOptionsCompat
 import androidx.core.app.ActivityOptionsCompat
 import androidx.core.content.ContextCompat
 import androidx.core.content.ContextCompat
@@ -44,10 +43,7 @@ import io.legado.app.data.entities.BookChapter
 import io.legado.app.data.entities.BookSource
 import io.legado.app.data.entities.BookSource
 import io.legado.app.databinding.ActivityBookInfoBinding
 import io.legado.app.databinding.ActivityBookInfoBinding
 import io.legado.app.databinding.DialogEditTextBinding
 import io.legado.app.databinding.DialogEditTextBinding
-import io.legado.app.exception.NoStackTraceException
-import io.legado.app.help.AppWebDav
 import io.legado.app.help.book.addType
 import io.legado.app.help.book.addType
-import io.legado.app.help.book.getRemoteUrl
 import io.legado.app.help.book.isAudio
 import io.legado.app.help.book.isAudio
 import io.legado.app.help.book.isImage
 import io.legado.app.help.book.isImage
 import io.legado.app.help.book.isLocal
 import io.legado.app.help.book.isLocal
@@ -60,7 +56,6 @@ import io.legado.app.lib.dialogs.alert
 import io.legado.app.lib.dialogs.selector
 import io.legado.app.lib.dialogs.selector
 import io.legado.app.model.BookCover
 import io.legado.app.model.BookCover
 import io.legado.app.model.SourceCallBack
 import io.legado.app.model.SourceCallBack
-import io.legado.app.model.remote.RemoteBookWebDav
 import io.legado.app.ui.about.AppLogDialog
 import io.legado.app.ui.about.AppLogDialog
 import io.legado.app.ui.book.audio.AudioPlayActivity
 import io.legado.app.ui.book.audio.AudioPlayActivity
 import io.legado.app.ui.book.changecover.ChangeCoverDialog
 import io.legado.app.ui.book.changecover.ChangeCoverDialog
@@ -101,6 +96,7 @@ import kotlinx.coroutines.Dispatchers.IO
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
 import kotlinx.coroutines.withContext
+import org.koin.androidx.viewmodel.ext.android.viewModel
 
 
 class BookInfoActivity :
 class BookInfoActivity :
     VMBaseActivity<ActivityBookInfoBinding, BookInfoViewModel>(),
     VMBaseActivity<ActivityBookInfoBinding, BookInfoViewModel>(),
@@ -170,7 +166,7 @@ class BookInfoActivity :
     private val book get() = viewModel.getBook(false)
     private val book get() = viewModel.getBook(false)
 
 
     override val binding by viewBinding(ActivityBookInfoBinding::inflate)
     override val binding by viewBinding(ActivityBookInfoBinding::inflate)
-    override val viewModel by viewModels<BookInfoViewModel>()
+    override val viewModel: BookInfoViewModel by viewModel()
 
 
     @SuppressLint("PrivateResource")
     @SuppressLint("PrivateResource")
     override fun onCreate(savedInstanceState: Bundle?) {
     override fun onCreate(savedInstanceState: Bundle?) {
@@ -333,14 +329,9 @@ class BookInfoActivity :
             R.id.menu_delete_alert -> LocalConfig.bookInfoDeleteAlert = !item.isChecked
             R.id.menu_delete_alert -> LocalConfig.bookInfoDeleteAlert = !item.isChecked
             R.id.menu_upload -> {
             R.id.menu_upload -> {
                 viewModel.getBook()?.let { book ->
                 viewModel.getBook()?.let { book ->
-                    book.getRemoteUrl()?.let {
-                        alert(R.string.draw, R.string.sure_upload) {
-                            okButton {
-                                upLoadBook(book)
-                            }
-                            cancelButton()
-                        }
-                    } ?: upLoadBook(book)
+                    viewModel.uploadBook(book) {
+                        toastOnUi("上传成功")
+                    }
                 }
                 }
             }
             }
         }
         }
@@ -374,17 +365,6 @@ class BookInfoActivity :
         }
         }
     }
     }
 
 
-//    override fun dispatchTouchEvent(ev: MotionEvent): Boolean {
-//        if (ev.action == MotionEvent.ACTION_DOWN) {
-//            currentFocus?.let {
-//                if (it === binding.tvIntro && binding.tvIntro.hasSelection()) {
-//                    it.clearFocus()
-//                }
-//            }
-//        }
-//        return super.dispatchTouchEvent(ev)
-//    }
-
     private fun setupBackCallback() {
     private fun setupBackCallback() {
         onBackPressedDispatcher.addCallback(this) {
         onBackPressedDispatcher.addCallback(this) {
             if (!viewModel.inBookshelf && AppConfig.showAddToShelfAlert) {
             if (!viewModel.inBookshelf && AppConfig.showAddToShelfAlert) {
@@ -413,28 +393,6 @@ class BookInfoActivity :
         }
         }
     }
     }
 
 
-    private fun upLoadBook(
-        book: Book,
-        bookWebDav: RemoteBookWebDav? = AppWebDav.defaultBookWebDav,
-    ) {
-        lifecycleScope.launch {
-            waitDialog.setText("上传中.....")
-            waitDialog.show()
-            try {
-                bookWebDav
-                    ?.upload(book)
-                    ?: throw NoStackTraceException("未配置webDav")
-                //更新书籍最后更新时间,使之比远程书籍的时间新
-                book.lastCheckTime = System.currentTimeMillis()
-                viewModel.saveBook(book)
-            } catch (e: Exception) {
-                toastOnUi(e.localizedMessage)
-            } finally {
-                waitDialog.dismiss()
-            }
-        }
-    }
-
     private fun showBook(book: Book) = binding.run {
     private fun showBook(book: Book) = binding.run {
         showCover(book)
         showCover(book)
         addColorScheme(binding.ivCover.drawable)
         addColorScheme(binding.ivCover.drawable)
@@ -1172,4 +1130,4 @@ class BookInfoActivity :
             toastOnUi("已添加到书架")
             toastOnUi("已添加到书架")
         }
         }
     }
     }
-}
+}

+ 22 - 33
app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt

@@ -15,12 +15,11 @@ import io.legado.app.data.appDb
 import io.legado.app.data.entities.Book
 import io.legado.app.data.entities.Book
 import io.legado.app.data.entities.BookChapter
 import io.legado.app.data.entities.BookChapter
 import io.legado.app.data.entities.BookSource
 import io.legado.app.data.entities.BookSource
+import io.legado.app.data.repository.RemoteBookRepository
 import io.legado.app.exception.NoBooksDirException
 import io.legado.app.exception.NoBooksDirException
 import io.legado.app.exception.NoStackTraceException
 import io.legado.app.exception.NoStackTraceException
-import io.legado.app.help.AppWebDav
 import io.legado.app.help.book.BookHelp
 import io.legado.app.help.book.BookHelp
 import io.legado.app.help.book.getExportFileName
 import io.legado.app.help.book.getExportFileName
-import io.legado.app.help.book.getRemoteUrl
 import io.legado.app.help.book.isLocal
 import io.legado.app.help.book.isLocal
 import io.legado.app.help.book.isNotShelf
 import io.legado.app.help.book.isNotShelf
 import io.legado.app.help.book.isSameNameAuthor
 import io.legado.app.help.book.isSameNameAuthor
@@ -38,13 +37,15 @@ import io.legado.app.model.localBook.LocalBook
 import io.legado.app.model.webBook.WebBook
 import io.legado.app.model.webBook.WebBook
 import io.legado.app.utils.ArchiveUtils
 import io.legado.app.utils.ArchiveUtils
 import io.legado.app.utils.UrlUtil
 import io.legado.app.utils.UrlUtil
-import io.legado.app.utils.isContentScheme
 import io.legado.app.utils.postEvent
 import io.legado.app.utils.postEvent
 import io.legado.app.utils.toastOnUi
 import io.legado.app.utils.toastOnUi
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers.IO
 import kotlinx.coroutines.Dispatchers.IO
 
 
-class BookInfoViewModel(application: Application) : BaseViewModel(application) {
+class BookInfoViewModel(
+    application: Application,
+    private val remoteBookRepository: RemoteBookRepository
+) : BaseViewModel(application) {
     val bookData = MutableLiveData<Book>()
     val bookData = MutableLiveData<Book>()
     val chapterListData = MutableLiveData<List<BookChapter>>()
     val chapterListData = MutableLiveData<List<BookChapter>>()
     val webFiles = mutableListOf<WebFile>()
     val webFiles = mutableListOf<WebFile>()
@@ -122,18 +123,7 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
         executeLazy(executeContext = IO) {
         executeLazy(executeContext = IO) {
             if (book.isLocal) {
             if (book.isLocal) {
                 book.tocUrl = ""
                 book.tocUrl = ""
-                book.getRemoteUrl()?.let {
-                    val bookWebDav = AppWebDav.defaultBookWebDav
-                        ?: throw NoStackTraceException("webDav没有配置")
-                    val remoteBook = bookWebDav.getRemoteBook(it)
-                    if (remoteBook == null) {
-                        book.origin = BookType.localTag
-                    } else if (remoteBook.lastModify > book.lastCheckTime) {
-                        val uri = bookWebDav.downloadRemoteBook(remoteBook)
-                        book.bookUrl = if (uri.isContentScheme()) uri.toString() else uri.path!!
-                        book.lastCheckTime = remoteBook.lastModify
-                    }
-                }
+                remoteBookRepository.refreshLocalBook(book)
             } else {
             } else {
                 val bs = bookSource ?: return@executeLazy
                 val bs = bookSource ?: return@executeLazy
                 if (book.originName != bs.bookSourceName) {
                 if (book.originName != bs.bookSourceName) {
@@ -159,25 +149,9 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
         val book = bookData.value ?: return
         val book = bookData.value ?: return
         if (!book.isLocal) return
         if (!book.isLocal) return
 
 
-        val remoteUrl = book.getRemoteUrl() ?: return
-
         execute {
         execute {
             waitDialogData.postValue(true)
             waitDialogData.postValue(true)
-            val bookWebDav = AppWebDav.defaultBookWebDav
-                ?: throw NoStackTraceException("webDav没有配置")
-
-            val remoteBook = bookWebDav.getRemoteBook(remoteUrl)
-                ?: throw NoStackTraceException("远程文件不存在")
-
-            val downloadBookUri = bookWebDav.downloadRemoteBook(remoteBook)
-
-            val importedBooks = LocalBook.importFiles(downloadBookUri)
-            val newBook = importedBooks.firstOrNull() ?: throw NoStackTraceException("导入失败")
-
-            newBook.durChapterIndex = book.durChapterIndex
-            newBook.durChapterPos = book.durChapterPos
-            newBook.order = book.order
-            newBook.group = book.group
+            val newBook = remoteBookRepository.syncBookFromRemote(book)
 
 
             appDb.bookDao.delete(book)
             appDb.bookDao.delete(book)
             appDb.bookDao.insert(newBook)
             appDb.bookDao.insert(newBook)
@@ -188,6 +162,21 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
             context.toastOnUi("同步完成")
             context.toastOnUi("同步完成")
         }.onFinally {
         }.onFinally {
             waitDialogData.postValue(false)
             waitDialogData.postValue(false)
+        }.onError {
+            context.toastOnUi(it.localizedMessage)
+        }
+    }
+
+    fun uploadBook(book: Book, success: () -> Unit) {
+        execute {
+            waitDialogData.postValue(true)
+            remoteBookRepository.uploadBook(book)
+            saveBook(book)
+            success.invoke()
+        }.onFinally {
+            waitDialogData.postValue(false)
+        }.onError {
+            context.toastOnUi(it.localizedMessage)
         }
         }
     }
     }