diff --git a/app/build.gradle b/app/build.gradle index 626d5c1e2..2965ba11c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -96,7 +96,6 @@ android { } flavorDimensions.add("mode") - productFlavors { app { dimension "mode" @@ -108,7 +107,6 @@ android { manifestPlaceholders.put("APP_CHANNEL_VALUE", "google") } } - compileOptions { // Flag to enable support for the new language APIs coreLibraryDesugaringEnabled true @@ -117,11 +115,6 @@ android { targetCompatibility JavaVersion.VERSION_11 } - packagingOptions { - // fix found with path 'META-INF/INDEX.LIST' from inputs - exclude 'META-INF/INDEX.LIST' - } - sourceSets { // Adds exported schema location as test app assets. androidTest.assets.srcDirs += files("$projectDir/schemas".toString()) diff --git a/app/src/main/java/io/legado/app/data/entities/Server.kt b/app/src/main/java/io/legado/app/data/entities/Server.kt index 2faa6b2c9..4c47e135e 100644 --- a/app/src/main/java/io/legado/app/data/entities/Server.kt +++ b/app/src/main/java/io/legado/app/data/entities/Server.kt @@ -49,9 +49,9 @@ data class Server( @Parcelize data class WebDavConfig( - var url: String, - var username: String, - var password: String + var url: String? = null, + var username: String? = null, + var password: String? = null, ) : Parcelable } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/book/BookExtensions.kt b/app/src/main/java/io/legado/app/help/book/BookExtensions.kt index 94041e500..65cdef262 100644 --- a/app/src/main/java/io/legado/app/help/book/BookExtensions.kt +++ b/app/src/main/java/io/legado/app/help/book/BookExtensions.kt @@ -18,39 +18,49 @@ import java.util.concurrent.ConcurrentHashMap val Book.isAudio: Boolean - get() = isType(BookType.audio) + get() { + return type and BookType.audio > 0 + } val Book.isImage: Boolean - get() = isType(BookType.image) + get() { + return type and BookType.image > 0 + } val Book.isLocal: Boolean get() { if (type == 0) { return origin == BookType.localTag || origin.startsWith(BookType.webDavTag) } - return isType(BookType.local) + return type and BookType.local > 0 } val Book.isLocalTxt: Boolean - get() = isLocal && originName.endsWith(".txt", true) + get() { + return isLocal && originName.endsWith(".txt", true) + } val Book.isEpub: Boolean - get() = isLocal && originName.endsWith(".epub", true) + get() { + return isLocal && originName.endsWith(".epub", true) + } val Book.isUmd: Boolean - get() = isLocal && originName.endsWith(".umd", true) - + get() { + return isLocal && originName.endsWith(".umd", true) + } val Book.isPdf: Boolean - get() = isLocal && originName.endsWith(".pdf", true) + get() { + return isLocal && originName.endsWith(".pdf", true) + } val Book.isOnLineTxt: Boolean - get() = !isLocal && isType(BookType.text) - -val Book.isWebFile: Boolean - get() = isType(BookType.webFile) + get() { + return !isLocal && type and BookType.text > 0 + } val Book.isUpError: Boolean - get() = isType(BookType.updateError) + get() = type and BookType.updateError > 0 fun Book.contains(word: String?): Boolean { if (word.isNullOrEmpty()) { @@ -162,8 +172,6 @@ fun Book.clearType() { type = 0 } -fun Book.isType(@BookType.Type bookType: Int): Boolean = type and bookType > 0 - fun Book.upType() { if (type < 8) { type = when (type) { diff --git a/app/src/main/java/io/legado/app/lib/webdav/Authorization.kt b/app/src/main/java/io/legado/app/lib/webdav/Authorization.kt index b89126ba3..1b44d5f78 100644 --- a/app/src/main/java/io/legado/app/lib/webdav/Authorization.kt +++ b/app/src/main/java/io/legado/app/lib/webdav/Authorization.kt @@ -24,7 +24,7 @@ data class Authorization( constructor(serverID: Long?): this("","") { serverID ?: throw NoStackTraceException("Unexpected server ID") - appDb.serverDao.get(serverID)?.getWebDavConfig()?.run { + appDb.serverDao.get(serverID)?.getWebDavConfig().run { data = Credentials.basic(username, password, charset) } ?: throw WebDavException("Unexpected WebDav Authorization") } diff --git a/app/src/main/java/io/legado/app/model/webBook/BookInfo.kt b/app/src/main/java/io/legado/app/model/webBook/BookInfo.kt index 58b52f259..1052d6a18 100644 --- a/app/src/main/java/io/legado/app/model/webBook/BookInfo.kt +++ b/app/src/main/java/io/legado/app/model/webBook/BookInfo.kt @@ -7,7 +7,6 @@ import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookSource import io.legado.app.exception.NoStackTraceException import io.legado.app.help.book.BookHelp -import io.legado.app.help.book.isWebFile import io.legado.app.model.Debug import io.legado.app.model.analyzeRule.AnalyzeRule import io.legado.app.utils.DebugLog @@ -138,7 +137,7 @@ object BookInfo { Debug.log(bookSource.bookSourceUrl, "└${e.localizedMessage}") DebugLog.e("获取封面出错", e) } - if (book.isWebFile) { + if (book.type and BookType.webFile == 0) { coroutineContext.ensureActive() Debug.log(bookSource.bookSourceUrl, "┌获取目录链接") book.tocUrl = analyzeRule.getString(infoRule.tocUrl, isUrl = true) diff --git a/app/src/main/java/io/legado/app/ui/association/ImportOnLineBookFileDialog.kt b/app/src/main/java/io/legado/app/ui/association/ImportOnLineBookFileDialog.kt new file mode 100644 index 000000000..3b3562f64 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/association/ImportOnLineBookFileDialog.kt @@ -0,0 +1,131 @@ +package io.legado.app.ui.association + +import android.content.Context +import android.os.Bundle +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.viewModels +import androidx.recyclerview.widget.LinearLayoutManager +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.databinding.DialogRecyclerViewBinding +import io.legado.app.databinding.ItemBookFileImportBinding +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.theme.primaryColor +import io.legado.app.ui.widget.dialog.WaitDialog +import io.legado.app.utils.openFileUri +import io.legado.app.utils.setLayout +import io.legado.app.utils.viewbindingdelegate.viewBinding +import io.legado.app.utils.visible + + +/** + * 导入在线书籍文件弹出窗口 + */ +class ImportOnLineBookFileDialog : BaseDialogFragment(R.layout.dialog_recycler_view) { + + + private val binding by viewBinding(DialogRecyclerViewBinding::bind) + private val viewModel by viewModels() + private val adapter by lazy { BookFileAdapter(requireContext()) } + + override fun onStart() { + super.onStart() + setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + val bookUrl = arguments?.getString("bookUrl") + viewModel.initData(bookUrl) + binding.toolBar.setBackgroundColor(primaryColor) + binding.toolBar.setTitle(R.string.download_and_import_file) + binding.rotateLoading.visible() + binding.recyclerView.layoutManager = LinearLayoutManager(requireContext()) + binding.recyclerView.adapter = adapter + viewModel.errorLiveData.observe(this) { + binding.rotateLoading.gone() + binding.tvMsg.apply { + text = it + visible() + } + } + viewModel.successLiveData.observe(this) { + binding.rotateLoading.gone() + if (it > 0) { + adapter.setItems(viewModel.allBookFiles) + } + } + viewModel.savedFileUriData.observe(this) { + requireContext().openFileUri(it, "*/*") + } + } + + private fun importFileAndUpdate(url: String, fileName: String) { + val waitDialog = WaitDialog(requireContext()) + waitDialog.show() + viewModel.importOnLineBookFile(url, fileName) { + waitDialog.dismiss() + dismissAllowingStateLoss() + } + } + + private fun downloadFile(url: String, fileName: String) { + val waitDialog = WaitDialog(requireContext()) + waitDialog.show() + viewModel.downloadUrl(url, fileName) { + waitDialog.dismiss() + dismissAllowingStateLoss() + } +} + + inner class BookFileAdapter(context: Context) : + RecyclerAdapter +, ItemBookFileImportBinding>(context) { + + override fun getViewBinding(parent: ViewGroup): ItemBookFileImportBinding { + return ItemBookFileImportBinding.inflate(inflater, parent, false) + } + + override fun convert( + holder: ItemViewHolder, + binding: ItemBookFileImportBinding, + item: Triple, + payloads: MutableList + ) { + binding.apply { + cbFileName.text = item.second + } + } + + override fun registerListener( + holder: ItemViewHolder, + binding: ItemBookFileImportBinding + ) { + binding.apply { + cbFileName.setOnClickListener { + val selectFile = viewModel.allBookFiles[holder.layoutPosition] + if (selectFile.third) { + importFileAndUpdate(selectFile.first, selectFile.second) + } else { + alert( + title = getString(R.string.draw), + message = getString(R.string.file_not_supported, selectFile.second) + ) { + yesButton { + importFileAndUpdate(selectFile.first, selectFile.second) + } + neutralButton(R.string.open_fun) { + downloadFile(selectFile.first, selectFile.second) + } + noButton() + } + } + } + } + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/association/ImportOnLineBookFileViewModel.kt b/app/src/main/java/io/legado/app/ui/association/ImportOnLineBookFileViewModel.kt new file mode 100644 index 000000000..667c9042c --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/association/ImportOnLineBookFileViewModel.kt @@ -0,0 +1,78 @@ +package io.legado.app.ui.association + +import android.app.Application +import android.net.Uri +import androidx.lifecycle.MutableLiveData +import io.legado.app.base.BaseViewModel +import io.legado.app.constant.AppPattern +import io.legado.app.constant.EventBus +import io.legado.app.data.appDb +import io.legado.app.data.entities.BookSource +import io.legado.app.exception.NoStackTraceException +import io.legado.app.model.analyzeRule.AnalyzeRule +import io.legado.app.model.analyzeRule.AnalyzeUrl +import io.legado.app.model.localBook.LocalBook +import io.legado.app.utils.postEvent +import io.legado.app.utils.toastOnUi + +class ImportOnLineBookFileViewModel(app: Application) : BaseViewModel(app) { + + val allBookFiles = arrayListOf>() + val errorLiveData = MutableLiveData() + val successLiveData = MutableLiveData() + val savedFileUriData = MutableLiveData() + var bookSource: BookSource? = null + + fun initData(bookUrl: String?) { + execute { + bookUrl ?: throw NoStackTraceException("书籍详情页链接为空") + val book = appDb.searchBookDao.getSearchBook(bookUrl)?.toBook() + ?: throw NoStackTraceException("book is null") + bookSource = appDb.bookSourceDao.getBookSource(book.origin) + ?: throw NoStackTraceException("bookSource is null") + val ruleDownloadUrls = bookSource?.getBookInfoRule()?.downloadUrls + val content = AnalyzeUrl(bookUrl, source = bookSource).getStrResponse().body + val analyzeRule = AnalyzeRule(book, bookSource) + analyzeRule.setContent(content).setBaseUrl(bookUrl) + val fileName = "${book.name} 作者:${book.author}" + analyzeRule.getStringList(ruleDownloadUrls, isUrl = true)?.let { + it.forEach { url -> + val mFileName = "${fileName}.${LocalBook.parseFileSuffix(url)}" + val isSupportedFile = AppPattern.bookFileRegex.matches(mFileName) + allBookFiles.add(Triple(url, mFileName, isSupportedFile)) + } + } ?: throw NoStackTraceException("下载链接规则解析为空") + }.onSuccess { + successLiveData.postValue(allBookFiles.size) + }.onError { + errorLiveData.postValue(it.localizedMessage ?: "") + context.toastOnUi("获取书籍下载链接失败\n${it.localizedMessage}") + } + + } + + fun downloadUrl(url: String, fileName: String, success: () -> Unit) { + execute { + LocalBook.saveBookFile(url, fileName, bookSource).let { + savedFileUriData.postValue(it) + } + }.onSuccess { + success.invoke() + }.onError { + context.toastOnUi("下载书籍文件失败\n${it.localizedMessage}") + } + } + + fun importOnLineBookFile(url: String, fileName: String, success: () -> Unit) { + execute { + LocalBook.importFileOnLine(url, fileName, bookSource).let { + postEvent(EventBus.FILE_SOURCE_DOWNLOAD_DONE, it.bookUrl) + } + }.onSuccess { + success.invoke() + }.onError { + context.toastOnUi("下载书籍文件失败\n${it.localizedMessage}") + } + } + +} diff --git a/app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt b/app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt index c1067e463..fd8b4e97a 100644 --- a/app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt @@ -21,7 +21,10 @@ import io.legado.app.databinding.ActivityBookInfoBinding import io.legado.app.databinding.DialogEditTextBinding import io.legado.app.exception.NoStackTraceException import io.legado.app.help.AppWebDav -import io.legado.app.help.book.* +import io.legado.app.help.book.isAudio +import io.legado.app.help.book.isLocal +import io.legado.app.help.book.isLocalTxt +import io.legado.app.help.book.getRemoteUrl import io.legado.app.help.config.AppConfig import io.legado.app.lib.dialogs.alert import io.legado.app.lib.theme.backgroundColor @@ -30,6 +33,7 @@ import io.legado.app.lib.theme.getPrimaryTextColor import io.legado.app.model.BookCover import io.legado.app.model.remote.RemoteBookWebDav import io.legado.app.ui.about.AppLogDialog +import io.legado.app.ui.association.ImportOnLineBookFileDialog import io.legado.app.ui.book.audio.AudioPlayActivity import io.legado.app.ui.book.changecover.ChangeCoverDialog import io.legado.app.ui.book.changesource.ChangeBookSourceDialog @@ -227,6 +231,7 @@ class BookInfoActivity : waitDialog.setText("上传中.....") waitDialog.show() try { + bookWebDav ?.upload(book) ?: throw NoStackTraceException("未配置webDav") @@ -248,7 +253,6 @@ class BookInfoActivity : tvOrigin.text = getString(R.string.origin_show, book.originName) tvLasted.text = getString(R.string.lasted_show, book.latestChapterTitle) tvIntro.text = book.getDisplayIntro() - tvToc.visible(!book.isWebFile) upTvBookshelf() val kinds = book.getKindList() if (kinds.isEmpty()) { @@ -274,10 +278,11 @@ class BookInfoActivity : binding.tvToc.text = getString(R.string.toc_s, getString(R.string.loading)) } chapterList.isNullOrEmpty() -> { - binding.tvToc.text = getString( - R.string.toc_s, - getString(R.string.error_load_toc) - ) + binding.tvToc.text = + if (viewModel.isImportBookOnLine) getString(R.string.click_read_button_load) else getString( + R.string.toc_s, + getString(R.string.error_load_toc) + ) } else -> { viewModel.bookData.value?.let { @@ -328,9 +333,9 @@ class BookInfoActivity : } tvRead.setOnClickListener { viewModel.bookData.value?.let { book -> - if (book.isWebFile) { - showWebFileDownloadAlert { - readBook(book) + if (viewModel.isImportBookOnLine) { + showDialogFragment { + putString("bookUrl", book.bookUrl) } } else { readBook(book) @@ -338,21 +343,11 @@ class BookInfoActivity : } ?: toastOnUi("Book is null") } tvShelf.setOnClickListener { - viewModel.bookData.value?.let { book -> - if (viewModel.inBookshelf) { - deleteBook() - } else { - if (book.isWebFile) { - showWebFileDownloadAlert { - viewModel.addToBookshelf { - upTvBookshelf() - } - } - } else { - viewModel.addToBookshelf { - upTvBookshelf() - } - } + if (viewModel.inBookshelf) { + deleteBook() + } else { + viewModel.addToBookshelf { + upTvBookshelf() } } } @@ -498,34 +493,6 @@ class BookInfoActivity : } } - private fun showWebFileDownloadAlert(onClick: (() -> Unit)?) { - viewModel.webFileData ?: return - alert(titleResource = R.string.download_and_import_file) { - val webFileData = viewModel.webFileData - webFileData?.let { - items(it) { _, webFile, _ -> - if (webFile.isSupported) { - viewModel.importOrDownloadWebFile(webFile) { - onClick?.invoke() - } - } else { - alert( - title = getString(R.string.draw), - message = getString(R.string.file_not_supported, webFile.name) - ) { - neutralButton(R.string.open_fun) { - viewModel.importOrDownloadWebFile(webFile) { uri -> - openFileUri(uri!!, "*/*") - } - } - noButton() - } - } - } - } - } - } - private fun readBook(book: Book) { if (!viewModel.inBookshelf) { viewModel.saveBook(book) { diff --git a/app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt b/app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt index a71da7e73..1ef3978bb 100644 --- a/app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt @@ -1,6 +1,5 @@ package io.legado.app.ui.book.info -import android.net.Uri import android.app.Application import android.content.Intent import androidx.lifecycle.MutableLiveData @@ -8,7 +7,6 @@ import androidx.lifecycle.viewModelScope import io.legado.app.R import io.legado.app.base.BaseViewModel import io.legado.app.constant.AppLog -import io.legado.app.constant.AppPattern import io.legado.app.constant.BookSourceType import io.legado.app.constant.BookType import io.legado.app.constant.EventBus @@ -21,8 +19,6 @@ import io.legado.app.help.AppWebDav import io.legado.app.help.book.* import io.legado.app.help.coroutine.Coroutine import io.legado.app.lib.webdav.ObjectNotFoundException -import io.legado.app.model.analyzeRule.AnalyzeRule -import io.legado.app.model.analyzeRule.AnalyzeUrl import io.legado.app.model.BookCover import io.legado.app.model.ReadBook import io.legado.app.model.localBook.LocalBook @@ -36,10 +32,11 @@ import kotlinx.coroutines.Dispatchers.IO class BookInfoViewModel(application: Application) : BaseViewModel(application) { val bookData = MutableLiveData() val chapterListData = MutableLiveData>() - val webFileData: MutableList? = null var inBookshelf = false var bookSource: BookSource? = null private var changeSourceCoroutine: Coroutine<*>? = null + val isImportBookOnLine: Boolean + get() = bookSource?.bookSourceType == BookSourceType.file fun initData(intent: Intent) { execute { @@ -85,6 +82,8 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) { appDb.bookSourceDao.getBookSource(book.origin) if (book.tocUrl.isEmpty() && !book.isLocal) { loadBookInfo(book) + } else if (isImportBookOnLine) { + chapterListData.postValue(emptyList()) } else { val chapterList = appDb.bookChapterDao.getChapterList(book.bookUrl) if (chapterList.isNotEmpty()) { @@ -157,14 +156,13 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) { inBookshelf = true } bookData.postValue(book) + if (isImportBookOnLine) { + appDb.searchBookDao.update(book.toSearchBook()) + } if (inBookshelf) { appDb.bookDao.update(book) } - if (it.isWebFile) { - loadWebFile(book, bookSource, scope) - } else { - loadChapter(it, scope) - } + loadChapter(it, scope) }.onError { AppLog.put("获取数据信息失败\n${it.localizedMessage}", it) context.toastOnUi(R.string.error_get_book_info) @@ -189,7 +187,7 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) { appDb.bookChapterDao.insert(*it.toTypedArray()) chapterListData.postValue(it) } - } else if (book.isWebFile) { + } else if (isImportBookOnLine) { chapterListData.postValue(emptyList()) } else { bookSource?.let { bookSource -> @@ -226,7 +224,6 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) { } } - fun loadGroup(groupId: Long, success: ((groupNames: String?) -> Unit)) { execute { appDb.bookGroupDao.getGroupNames(groupId).joinToString(",") @@ -235,54 +232,6 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) { } } - private fun loadWebFile( - book: Book, - bookSource: BookSource, - scope: CoroutineScope = viewModelScope - ) { - execute(scope) { - webFileData?.clear() - val fileName = "${book.name} 作者:${book.author}" - if (book.downloadUrls.isNullOrEmpty()) { - val ruleDownloadUrls = bookSource.getBookInfoRule().downloadUrls - val content = AnalyzeUrl(book.bookUrl, source = bookSource).getStrResponse().body - val analyzeRule = AnalyzeRule(book, bookSource) - analyzeRule.setContent(content).setBaseUrl(book.bookUrl) - analyzeRule.getStringList(ruleDownloadUrls, isUrl = true)?.let { - parseDownloadUrls(it, fileName) - } ?: throw NoStackTraceException("Unexpected ruleDownloadUrls") - } else { - parseDownloadUrls(book.downloadUrls, fileName) - } - }.onError { - context.toastOnUi("LoadWebFileError\n${it.localizedMessage}") - } - } - - private fun parseDownloadUrls(downloadUrls: List?, fileName: String) { - val urls = downloadUrls - urls?.forEach { url -> - val mFileName = "${fileName}.${LocalBook.parseFileSuffix(url)}" - val isSupportedFile = AppPattern.bookFileRegex.matches(mFileName) - webFileData?.add(WebFile(url, mFileName, isSupportedFile)) - } - } - - fun importOrDownloadWebFile(webFile: WebFile, success: ((Uri?) -> Unit)?) { - bookSource ?: return - execute { - if (webFile.isSupported) { - LocalBook.importFileOnLine(webFile.url, webFile.name, bookSource) - } else { - LocalBook.saveBookFile(webFile.url, webFile.name, bookSource) - } - }.onSuccess { - success?.invoke(it as? Uri) - }.onError { - context.toastOnUi("ImportWebFileError\n${it.localizedMessage}") - } - } - fun changeTo(source: BookSource, book: Book, toc: List) { changeSourceCoroutine?.cancel() changeSourceCoroutine = execute { @@ -407,14 +356,4 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) { } } - data class WebFile( - val url: String, - val name: String, - val isSupported: Boolean - ) { - override fun toString(): String { - return name - } - } - }