فهرست منبع

Merge branch 'gedoor:master' into rar7z

ag2s20150909 3 سال پیش
والد
کامیت
961e191a8a

+ 4 - 1
app/src/main/assets/updateLog.md

@@ -13,7 +13,10 @@
 * 正文出现缺字漏字、内容缺失、排版错乱等情况,有可能是净化规则或简繁转换出现问题。
 * 漫画源看书显示乱码,**阅读与其他软件的源并不通用**,请导入阅读的支持的漫画源!
 
-**2023/03/11**
+**2023/03/13**
+* 文件类书源支持zip 7z rar4解压
+
+**2023/03/12**
 
 * 远程书籍添加webDav多配置
 * 更新文件类书源详情页界面逻辑

+ 1 - 1
app/src/main/java/io/legado/app/base/adapter/RecyclerAdapter.kt

@@ -321,7 +321,7 @@ abstract class RecyclerAdapter<ITEM, VB : ViewBinding>(protected val context: Co
 
     fun getItemByLayoutPosition(position: Int) = items.getOrNull(getActualPosition(position))
 
-    fun getItems(): List<ITEM> = items
+    fun getItems(): List<ITEM> = items.toList()
 
     protected open fun getItemViewType(item: ITEM, position: Int) = 0
 

+ 2 - 0
app/src/main/java/io/legado/app/constant/AppPattern.kt

@@ -24,6 +24,8 @@ object AppPattern {
 
     //本地书籍支持类型
     val bookFileRegex = Regex(".*\\.(txt|epub|umd|pdf)", RegexOption.IGNORE_CASE)
+    //压缩文件支持类型
+    val archiveFileRegex = Regex(".*\\.(zip|rar|7z)", RegexOption.IGNORE_CASE)
 
     /**
      * 所有标点

+ 0 - 3
app/src/main/java/io/legado/app/data/dao/BookSourceDao.kt

@@ -54,9 +54,6 @@ interface BookSourceDao {
     @Query("select * from book_sources where enabledExplore = 1 and trim(exploreUrl) <> '' order by customOrder asc")
     fun flowExplore(): Flow<List<BookSource>>
 
-//    @Query("select * from book_sources where enabledReview = 1 order by customOrder asc")
-//    fun flowReview(): Flow<List<BookSource>>
-
     @Query("select * from book_sources where loginUrl is not null and loginUrl != ''")
     fun flowLogin(): Flow<List<BookSource>>
 

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

@@ -157,18 +157,10 @@ object LocalBook {
      */
     fun importFile(uri: Uri): Book {
         val bookUrl: String
-        val updateTime: Long
-        //这个变量不要修改,否则会导致读取不到缓存
-        val fileName = if (uri.isContentScheme()) {
-            bookUrl = uri.toString()
-            val doc = DocumentFile.fromSingleUri(appCtx, uri)!!
-            updateTime = doc.lastModified()
-            doc.name!!
-        } else {
-            bookUrl = uri.path!!
-            val file = File(bookUrl)
-            updateTime = file.lastModified()
-            file.name
+        //updateTime变量不要修改,否则会导致读取不到缓存
+        val (fileName, _, _, updateTime, _) = FileDoc.fromUri(uri, false).apply {
+           if (size == 0L) throw EmptyFileException("Unexpected empty File")
+            bookUrl = toString()
         }
         var book = appDb.bookDao.getBook(bookUrl)
         if (book == null) {
@@ -291,7 +283,6 @@ object LocalBook {
         fileName: String
     ): Uri {
         inputStream.use {
-            if (it.isEmpty()) throw EmptyFileException("Unexpected empty inputStream")
             val defaultBookTreeUri = AppConfig.defaultBookTreeUri
             if (defaultBookTreeUri.isNullOrBlank()) throw NoStackTraceException("没有设置书籍保存位置!")
             val treeUri = Uri.parse(defaultBookTreeUri)

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

@@ -507,6 +507,13 @@ class BookInfoActivity :
                 viewModel.importOrDownloadWebFile<Book>(webFile) {
                     onClick?.invoke(it)
                 }
+            } else if (webFile.isSupportDecompress) {
+                /* 解压筛选后再选择导入项 */
+                viewModel.importOrDownloadWebFile<Uri>(webFile) { uri ->
+                    viewModel.deCompress(uri) {
+                        showDecompressFileImportAlert(it)
+                    }
+                }
             } else {
                 alert(
                     title = getString(R.string.draw),
@@ -514,8 +521,8 @@ class BookInfoActivity :
                 ) {
                     neutralButton(R.string.open_fun) {
                         /* download only */
-                        viewModel.importOrDownloadWebFile<Uri>(webFile) { uri ->
-                            openFileUri(uri, "*/*")
+                        viewModel.importOrDownloadWebFile<Uri>(webFile) {
+                            openFileUri(it, "*/*")
                         }
                     }
                     noButton()
@@ -524,6 +531,22 @@ class BookInfoActivity :
         }
     }
 
+    private fun showDecompressFileImportAlert(
+        fileDocs: List<FileDoc>
+    ) {
+        if (fileDocs.isEmpty()) {
+            toastOnUi(R.string.unsupport_archivefile_entry)
+            return
+        }
+        val selectorNames = fileDocs.map { it.name }
+        selector(
+            R.string.import_select_book,
+            selectorNames
+        ) { _, _, index ->
+            viewModel.importBook(fileDocs[index])
+        }
+    }
+
     private fun readBook(book: Book) {
         if (!viewModel.inBookshelf) {
             viewModel.saveBook(book) {

+ 24 - 5
app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt

@@ -25,9 +25,7 @@ import io.legado.app.model.BookCover
 import io.legado.app.model.ReadBook
 import io.legado.app.model.localBook.LocalBook
 import io.legado.app.model.webBook.WebBook
-import io.legado.app.utils.isContentScheme
-import io.legado.app.utils.postEvent
-import io.legado.app.utils.toastOnUi
+import io.legado.app.utils.*
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers.IO
 
@@ -242,7 +240,8 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
             book.downloadUrls!!.map {
                 val mFileName = "${fileName}.${LocalBook.parseFileSuffix(it)}"
                 val isSupportedFile = AppPattern.bookFileRegex.matches(mFileName)
-                WebFile(it, mFileName, isSupportedFile)
+                val isSupportDecompress = AppPattern.archiveFileRegex.matches(mFileName)
+                WebFile(it, mFileName, isSupportedFile, isSupportDecompress)
             }
         }.onError {
             context.toastOnUi("LoadWebFileError\n${it.localizedMessage}")
@@ -251,6 +250,7 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
         }
     }
 
+    /* 导入或者下载在线文件 */
     fun <T> importOrDownloadWebFile(webFile: WebFile, success: ((T) -> Unit)?) {
        bookSource ?: return
        execute {
@@ -272,6 +272,22 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
        }
     }
 
+    fun deCompress(archiveFileUri: Uri, onSuccess: (List<FileDoc>) -> Unit) {
+        execute {
+            ArchiveUtils.deCompress(archiveFileUri).list {
+                AppPattern.bookFileRegex.matches(it.name)
+            } ?: emptyList()
+        }.onError {
+            context.toastOnUi("DeCompress Error:\n${it.localizedMessage}")
+        }.onSuccess {
+            onSuccess.invoke(it)
+        }
+    }
+
+    fun importBook(fileDoc: FileDoc) {
+        LocalBook.importFile(fileDoc.uri).let { changeToLocalBook(it) }
+    }
+
     fun changeTo(source: BookSource, book: Book, toc: List<BookChapter>) {
         changeSourceCoroutine?.cancel()
         changeSourceCoroutine = execute {
@@ -398,7 +414,10 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
     data class WebFile(
         val url: String,
         val name: String,
-        val isSupported: Boolean
+        // txt epub umd pdf等文件
+        val isSupported: Boolean,
+        // 压缩包形式的txt epub umd pdf文件
+        val isSupportDecompress: Boolean
     ) {
         override fun toString(): String {
             return name

+ 4 - 4
app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceActivity.kt

@@ -58,6 +58,7 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
     override val viewModel by viewModels<BookSourceViewModel>()
     private val importRecordKey = "bookSourceRecordKey"
     private val adapter by lazy { BookSourceAdapter(this, this) }
+    private val itemTouchCallback by lazy { ItemTouchCallback(adapter) }
     private val searchView: SearchView by lazy {
         binding.titleBar.findViewById(R.id.search_view)
     }
@@ -197,8 +198,6 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
         dragSelectTouchHelper.attachToRecyclerView(binding.recyclerView)
         dragSelectTouchHelper.activeSlideSelect()
         // Note: need judge selection first, so add ItemTouchHelper after it.
-        val itemTouchCallback = ItemTouchCallback(adapter)
-        itemTouchCallback.isCanDrag = true
         ItemTouchHelper(itemTouchCallback).attachToRecyclerView(binding.recyclerView)
     }
 
@@ -276,6 +275,7 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
                 AppLog.put("书源界面更新书源出错", it)
             }.conflate().collect { data ->
                 adapter.setItems(data, adapter.diffItemCallback)
+                itemTouchCallback.isCanDrag = sort == Sort.Default
                 delay(500)
             }
         }
@@ -596,8 +596,8 @@ class BookSourceActivity : VMBaseActivity<ActivityBookSourceBinding, BookSourceV
         }
     }
 
-    override fun upOrder() {
-        viewModel.upOrder()
+    override fun upOrder(items: List<BookSource>) {
+        viewModel.upOrder(items)
     }
 
     override fun toTop(bookSource: BookSource) {

+ 15 - 11
app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceAdapter.kt

@@ -263,15 +263,11 @@ class BookSourceAdapter(context: Context, val callBack: CallBack) :
         val srcItem = getItem(srcPosition)
         val targetItem = getItem(targetPosition)
         if (srcItem != null && targetItem != null) {
-            if (srcItem.customOrder == targetItem.customOrder) {
-                callBack.upOrder()
-            } else {
-                val srcOrder = srcItem.customOrder
-                srcItem.customOrder = targetItem.customOrder
-                targetItem.customOrder = srcOrder
-                movedItems.add(srcItem)
-                movedItems.add(targetItem)
-            }
+            val srcOrder = srcItem.customOrder
+            srcItem.customOrder = targetItem.customOrder
+            targetItem.customOrder = srcOrder
+            movedItems.add(srcItem)
+            movedItems.add(targetItem)
         }
         swapItem(srcPosition, targetPosition)
         return true
@@ -281,7 +277,15 @@ class BookSourceAdapter(context: Context, val callBack: CallBack) :
 
     override fun onClearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
         if (movedItems.isNotEmpty()) {
-            callBack.update(*movedItems.toTypedArray())
+            val sortNumberSet = hashSetOf<Int>()
+            movedItems.forEach {
+                sortNumberSet.add(it.customOrder)
+            }
+            if (movedItems.size > sortNumberSet.size) {
+                callBack.upOrder(getItems())
+            } else {
+                callBack.update(*movedItems.toTypedArray())
+            }
             movedItems.clear()
         }
     }
@@ -319,7 +323,7 @@ class BookSourceAdapter(context: Context, val callBack: CallBack) :
         fun toBottom(bookSource: BookSource)
         fun searchBook(bookSource: BookSource)
         fun debug(bookSource: BookSource)
-        fun upOrder()
+        fun upOrder(items: List<BookSource>)
         fun upCountView()
     }
 }

+ 6 - 5
app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceViewModel.kt

@@ -51,13 +51,14 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application)
         execute { appDb.bookSourceDao.update(*bookSource) }
     }
 
-    fun upOrder() {
+    fun upOrder(items: List<BookSource>) {
+        if (items.isEmpty()) return
         execute {
-            val sources = appDb.bookSourceDao.all
-            for ((index: Int, source: BookSource) in sources.withIndex()) {
-                source.customOrder = index + 1
+            val firstSortNumber = items[0].customOrder
+            items.forEachIndexed { index, bookSource ->
+                bookSource.customOrder = firstSortNumber + index
+                appDb.bookSourceDao.update(bookSource)
             }
-            appDb.bookSourceDao.update(*sources.toTypedArray())
         }
     }
 

+ 74 - 0
app/src/main/java/io/legado/app/utils/ArchiveUtils.kt

@@ -0,0 +1,74 @@
+package io.legado.app.utils
+
+import android.net.Uri
+import androidx.documentfile.provider.DocumentFile
+import java.io.File
+import java.util.regex.Pattern
+
+import splitties.init.appCtx
+
+/* 自动判断压缩文件后缀 然后再调用具体的实现 */
+object ArchiveUtils {
+
+    // 临时目录 下次启动自动删除
+    val TEMP_PATH: String by lazy {
+        appCtx.externalCache.getFile("ArchiveTemp").createFolderReplace().absolutePath
+    }
+
+    val ZIP_REGEX = Regex(".*\\.zip", RegexOption.IGNORE_CASE)
+    val RAR_REGEX = Regex(".*\\.rar", RegexOption.IGNORE_CASE)
+    val SERVEZ_REGEX = Regex(".*\\.7z", RegexOption.IGNORE_CASE)
+
+    fun deCompress(
+        archiveUri: Uri,
+        path: String = TEMP_PATH
+    ): FileDoc {
+        return deCompress(FileDoc.fromUri(archiveUri, false), path)
+    }
+
+    fun deCompress(
+        archivePath: String,
+        path: String = TEMP_PATH
+    ): FileDoc {
+        return deCompress(Uri.parse(archivePath), path)
+    }
+
+    fun deCompress(
+        archiveFile: File,
+        path: String = TEMP_PATH
+    ): FileDoc {
+        return deCompress(FileDoc.fromFile(archiveFile), path)
+    }
+
+    fun deCompress(
+        archiveDoc: DocumentFile,
+        path: String = TEMP_PATH
+    ): FileDoc {
+        return deCompress(FileDoc.fromDocumentFile(archiveDoc), path)
+    }
+
+    fun deCompress(
+        archiveFileDoc: FileDoc,
+        path: String = TEMP_PATH
+    ): FileDoc {
+        if (archiveFileDoc.isDir) throw IllegalArgumentException("Unexpected Folder input")
+        val name = archiveFileDoc.name
+        archiveFileDoc.uri.inputStream(appCtx).getOrThrow().use {
+            when {
+                ZIP_REGEX.matches(name) -> ZipUtils.unZipToPath(it, path) 
+                RAR_REGEX.matches(name) -> RarUtils.unRarToPath(it, path)
+                SERVEZ_REGEX.matches(name) -> SevenZipUtils.un7zToPath(it, path)
+                else -> throw IllegalArgumentException("Unexpected archive format")
+            }
+        }
+        return getCacheFolderFileDoc(name, path)
+    }
+
+    private fun getCacheFolderFileDoc(
+        archiveName: String,
+        workPath: String
+    ): FileDoc {
+        return FileDoc.fromUri(Uri.parse(workPath), true)
+            .createFolderIfNotExist(MD5Utils.md5Encode16(archiveName))
+    }
+}

+ 0 - 12
app/src/main/java/io/legado/app/utils/InputStreamExtensions.kt

@@ -23,15 +23,3 @@ fun InputStream?.contains(str: String): Boolean {
         return scanner.findWithinHorizon(str, 0) != null
     }
 }
-
-fun InputStream?.isEmpty(): Boolean {
-    this ?: return true
-    return if (markSupported()) {
-        mark(0)
-        val isEmpty = read(ByteArray(1)) == -1
-        reset()
-        isEmpty
-    } else {
-        available() == 0
-    }
-}

+ 1 - 0
app/src/main/res/values-es-rES/strings.xml

@@ -1080,4 +1080,5 @@
     <string name="keep_group">保留分组</string>
     <string name="server_config">服务器配置</string>
     <string name="sure_upload">Remote webDav url exists, Continue?</string>
+    <string name="unsupport_archivefile_entry">Cannot find supported files in archive</string>
 </resources>

+ 1 - 0
app/src/main/res/values-ja-rJP/strings.xml

@@ -1083,4 +1083,5 @@
     <string name="keep_group">保留分组</string>
     <string name="server_config">服务器配置</string>
     <string name="sure_upload">Remote webDav url exists, Continue?</string>
+    <string name="unsupport_archivefile_entry">Cannot find supported files in archive</string>
 </resources>

+ 1 - 0
app/src/main/res/values-pt-rBR/strings.xml

@@ -1083,4 +1083,5 @@
     <string name="keep_group">保留分组</string>
     <string name="server_config">服务器配置</string>
     <string name="sure_upload">Remote webDav url exists, Continue?</string>
+    <string name="unsupport_archivefile_entry">Cannot find supported files in archive</string>
 </resources>

+ 1 - 0
app/src/main/res/values-zh-rHK/strings.xml

@@ -1080,4 +1080,5 @@
     <string name="keep_group">保留分组</string>
     <string name="server_config">服务器配置</string>
     <string name="sure_upload">远程webDav链接已存在,是否继续</string>
+    <string name="unsupport_archivefile_entry">压缩文件内没有支持的文件</string>
 </resources>

+ 1 - 0
app/src/main/res/values-zh-rTW/strings.xml

@@ -1082,4 +1082,5 @@
     <string name="keep_group">保留分组</string>
     <string name="server_config">服务器配置</string>
     <string name="sure_upload">远程webDav链接已存在,是否继续</string>
+    <string name="unsupport_archivefile_entry">压缩文件内没有支持的文件</string>
 </resources>

+ 1 - 0
app/src/main/res/values-zh/strings.xml

@@ -1082,4 +1082,5 @@
     <string name="keep_group">保留分组</string>
     <string name="server_config">服务器配置</string>
     <string name="sure_upload">远程webDav链接已存在,是否继续</string>
+    <string name="unsupport_archivefile_entry">压缩文件内没有支持的文件</string>
 </resources>

+ 1 - 0
app/src/main/res/values/strings.xml

@@ -1083,4 +1083,5 @@
     <string name="keep_group">Keep group</string>
     <string name="server_config">服务器配置</string>
     <string name="sure_upload">Remote webDav url exists, Continue?</string>
+    <string name="unsupport_archivefile_entry">Cannot find supported files in archive</string>
 </resources>