Horis 3 tahun lalu
induk
melakukan
6932f767c9

+ 1 - 1
app/src/main/java/io/legado/app/help/book/BookExtensions.kt

@@ -232,7 +232,7 @@ fun Book.isSameNameAuthor(other: Any?): Boolean {
 fun Book.getExportFileName(suffix: String): String {
     val jsStr = AppConfig.bookExportFileName
     if (jsStr.isNullOrBlank()) {
-        return "${name} 作者:${getRealAuthor()}.$suffix"
+        return "$name 作者:${getRealAuthor()}.$suffix"
     }
     val bindings = SimpleBindings()
     bindings["name"] = name

+ 9 - 0
app/src/main/java/io/legado/app/help/glide/OkHttpStreamFetcher.kt

@@ -35,7 +35,15 @@ class OkHttpStreamFetcher(private val url: GlideUrl, private val options: Option
     @Volatile
     private var call: Call? = null
 
+    companion object {
+        val failUrl = hashSetOf<String>()
+    }
+
     override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
+        if (failUrl.contains(url.toStringUrl())) {
+            callback.onLoadFailed(NoStackTraceException("跳过加载失败的图片"))
+            return
+        }
         val loadOnlyWifi = options.get(OkHttpModelLoader.loadOnlyWifiOption) ?: false
         if (loadOnlyWifi && !appCtx.isWifiConnect) {
             callback.onLoadFailed(NoStackTraceException("只在wifi加载图片"))
@@ -96,6 +104,7 @@ class OkHttpStreamFetcher(private val url: GlideUrl, private val options: Option
                 callback?.onDataReady(stream)
             }
         } else {
+            failUrl.add(url.toStringUrl())
             callback?.onLoadFailed(HttpException(response.message, response.code))
         }
     }

+ 5 - 7
app/src/main/java/io/legado/app/lib/webdav/Authorization.kt

@@ -22,13 +22,11 @@ data class Authorization(
         return "$username:$password"
     }
 
-    constructor(serverID: Long?): this("","") {
-        serverID ?: throw WebDavException("Unexpected server ID")
-        appDb.serverDao.get(serverID)?.getWebDavConfig()?.run {
-            data = Credentials.basic(username, password, charset)
-        } ?: throw WebDavException("Unexpected WebDav Authorization")
-    }
+    constructor(serverID: Long) : this(
+        appDb.serverDao.get(serverID)?.getWebDavConfig()
+            ?: throw WebDavException("Unexpected WebDav Authorization")
+    )
 
-    constructor(webDavConfig: WebDavConfig): this(webDavConfig.username, webDavConfig.password)
+    constructor(webDavConfig: WebDavConfig) : this(webDavConfig.username, webDavConfig.password)
 
 }

+ 11 - 1
app/src/main/java/io/legado/app/lib/webdav/WebDav.kt

@@ -39,7 +39,7 @@ open class WebDav(
     companion object {
 
         fun fromPath(path: String): WebDav {
-            val id = AnalyzeUrl(path).serverID
+            val id = AnalyzeUrl(path).serverID ?: throw WebDavException("没有serverID")
             val authorization = Authorization(id)
             return WebDav(path, authorization)
         }
@@ -386,6 +386,16 @@ open class WebDav(
     private fun checkResult(response: Response) {
         if (!response.isSuccessful) {
             val body = response.body?.string()
+            if (response.code == 401) {
+                val headers = response.headers("WWW-Authenticate")
+                val supportBasicAuth = headers.any {
+                    it.startsWith("Basic", ignoreCase = true)
+                }
+                if (!supportBasicAuth) {
+                    AppLog.put("服务器不支持BasicAuth认证")
+                }
+            }
+
             if (response.message.isNotBlank() || body.isNullOrBlank()) {
                 throw WebDavException("${url}\n${response.code}:${response.message}")
             }

+ 3 - 3
app/src/main/java/io/legado/app/ui/book/import/remote/ServerConfigViewModel.kt

@@ -14,10 +14,10 @@ class ServerConfigViewModel(application: Application): BaseViewModel(application
         //mServer不为空可能是旋转屏幕界面重新创建,不用更新数据
         if (mServer != null) return
         execute {
-            if (id != null) {
-                mServer = appDb.serverDao.get(id)
+            mServer = if (id != null) {
+                appDb.serverDao.get(id)
             } else {
-                mServer = Server()
+                Server()
             }
         }.onSuccess {
             onSuccess.invoke()

+ 5 - 1
app/src/main/java/io/legado/app/web/HttpServer.kt

@@ -98,6 +98,10 @@ class HttpServer(port: Int) : NanoHTTPD(port) {
                 )
             } else {
                 try {
+                    val data = returnData.data
+                    if (data is List<*> && data.size > 3000) {
+                        throw OutOfMemoryError()
+                    }
                     newFixedLengthResponse(GSON.toJson(returnData))
                 } catch (e: OutOfMemoryError) {
                     val path = FileUtils.getPath(
@@ -106,7 +110,7 @@ class HttpServer(port: Int) : NanoHTTPD(port) {
                         "bookSources.json"
                     )
                     val file = FileUtils.createFileIfNotExist(path)
-                    BufferedWriter(FileWriter(file)).use {
+                    BufferedWriter(FileWriter(file), 128 * 1024).use {
                         GSON.toJson(returnData, it)
                     }
                     val fis = FileInputStream(file)

+ 0 - 1
modules/web/.eslintrc-auto-import.json

@@ -4,7 +4,6 @@
     "ComponentPublicInstance": true,
     "ComputedRef": true,
     "EffectScope": true,
-    "ElLoading": true,
     "ElMessage": true,
     "InjectionKey": true,
     "PropType": true,

+ 4 - 4
modules/web/jsconfig.json

@@ -1,6 +1,6 @@
 {
   "compilerOptions": {
-    "types": ["element-plus/global", "@element-plus/icons-vue","@vueuse/shared", "vite/client"],
+    "types": ["@element-plus/icons-vue", "@vueuse/shared", "vite/client"],
     "target": "ESNext",
     "useDefineForClassFields": true,
     "module": "ESNext",
@@ -10,9 +10,9 @@
     "skipLibCheck": true,
     "noEmit": true,
     "paths": {
-      "@/*":["./src/*"],
-      "@api":["./src/api"],
-      "@utils/*":["./src/utils/*"]
+      "@/*": ["./src/*"],
+      "@api": ["./src/api"],
+      "@utils/*": ["./src/utils/*"]
     }
   },
   //"exclude": ["node_modules", "dist"],

+ 3 - 2
modules/web/package.json

@@ -21,7 +21,8 @@
     "hotkeys-js": "^3.10.2",
     "pinia": "^2.0.34",
     "vue": "^3.2.47",
-    "vue-router": "^4.1.6"
+    "vue-router": "^4.1.6",
+    "vue3-virtual-scroll-list": "^0.2.1"
   },
   "devDependencies": {
     "@vitejs/plugin-vue": "^4.1.0",
@@ -36,4 +37,4 @@
     "unplugin-vue-components": "^0.24.1",
     "vite": "^4.2.1"
   }
-}
+}

+ 1 - 1
modules/web/src/api/axios.js

@@ -4,7 +4,7 @@ const SECOND = 1000;
 
 const ajax = axios.create({
   baseURL: import.meta.env.VITE_API || location.origin,
-  timeout: 5 * SECOND,
+  timeout: 120 * SECOND,
 });
 
 export default ajax;

+ 0 - 4
modules/web/src/assets/main.css

@@ -1,10 +1,6 @@
 @import './kbd.css';
 @import './code.css';
 
-::-webkit-scrollbar {
-    width: 0;
-    height: 0;
-}
 body {
     padding: 0;
     margin: 0;

+ 0 - 1
modules/web/src/auto-imports.d.ts

@@ -5,7 +5,6 @@
 export {}
 declare global {
   const EffectScope: typeof import('vue')['EffectScope']
-  const ElLoading: typeof import('element-plus/es')['ElLoading']
   const ElMessage: typeof import('element-plus/es')['ElMessage']
   const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
   const computed: typeof import('vue')['computed']

+ 1 - 1
modules/web/src/components.d.ts

@@ -17,7 +17,6 @@ declare module '@vue/runtime-core' {
     ElDialog: typeof import('element-plus/es')['ElDialog']
     ElForm: typeof import('element-plus/es')['ElForm']
     ElFormItem: typeof import('element-plus/es')['ElFormItem']
-    ElIcon: typeof import('element-plus/es')['ElIcon']
     ElInput: typeof import('element-plus/es')['ElInput']
     ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
     ElLink: typeof import('element-plus/es')['ElLink']
@@ -36,6 +35,7 @@ declare module '@vue/runtime-core' {
     RouterView: typeof import('vue-router')['RouterView']
     SourceDebug: typeof import('./components/SourceDebug.vue')['default']
     SourceHelp: typeof import('./components/SourceHelp.vue')['default']
+    SourceItem: typeof import('./components/SourceItem.vue')['default']
     SourceJson: typeof import('./components/SourceJson.vue')['default']
     SourceList: typeof import('./components/SourceList.vue')['default']
     SourceTabForm: typeof import('./components/SourceTabForm.vue')['default']

+ 2 - 2
modules/web/src/components/BookItems.vue

@@ -4,7 +4,7 @@
       <div
         class="book"
         v-for="book in props.books"
-        :key="book.noteUrl"
+        :key="book.bookUrl"
         @click="handleClick(book)"
       >
         <div class="cover-img">
@@ -24,7 +24,7 @@
             </div>
             <div class="tags" v-show="props.isSearch">
               <el-tag
-                v-for="tag in book.kind.split(',').slice(0, 2)"
+                v-for="tag in book.kind?.split(',').slice(0, 2)"
                 :key="tag"
               >
                 {{ tag }}

+ 5 - 1
modules/web/src/components/SourceDebug.vue

@@ -52,4 +52,8 @@ const isBookSource = computed(() => {
 });
 </script>
 
-<style lang="scss" scoped></style>
+<style lang="scss" scoped>
+:deep(#debug-text) {
+  height: calc(100vh - 45px - 36px - 5px);
+}
+</style>

+ 21 - 0
modules/web/src/components/SourceItem.vue

@@ -0,0 +1,21 @@
+<template>
+  <el-checkbox
+    size="large"
+    border
+    :label="source"
+    :class="{ error: errorPushSources.includes(source) }"
+    @change="handleSourceClick(source)"
+    :key="source.bookSourceUrl"
+  >
+    {{ source.bookSourceName || source.sourceName }}
+  </el-checkbox>
+</template>
+
+<script setup>
+const { source } = defineProps(['source'])
+const store = useSourceStore()
+const { errorPushSources } = storeToRefs(store)
+const handleSourceClick = source => {
+  store.changeCurrentSource(source)
+}
+</script>

+ 6 - 2
modules/web/src/components/SourceJson.vue

@@ -1,5 +1,6 @@
 <template>
   <el-input
+    id="source-json"
     v-model="sourceString"
     type="textarea"
     placeholder="这里输出序列化的JSON数据,可直接导入'阅读'APP"
@@ -33,8 +34,11 @@ watchEffect(async () => {
   }
 });
 </script>
-<style>
-.el-input {
+<style scoped>
+:deep(.el-input) {
   width: 100%;
 }
+:deep(#source-json) {
+  height: calc(100vh - 50px);
+}
 </style>

+ 16 - 25
modules/web/src/components/SourceList.vue

@@ -8,7 +8,7 @@
   <div class="tool">
     <el-button @click="importSourceFile" :icon="Folder"> 打开 </el-button>
     <el-button
-      :disabled="sourceSelect.length === 0"
+      :disabled="sourcesFiltered.length === 0"
       @click="outExport"
       :icon="Download"
     >
@@ -29,35 +29,28 @@
     >
   </div>
   <el-checkbox-group id="source-list" v-model="sourceSelect">
-    <el-checkbox
-      v-for="source in sourcesFiltered"
-      size="large"
-      border
-      :label="source"
-      :class="{ error: errorPushSources.includes(source) }"
-      @click="handleSourceClick(source)"
-      :key="source.bookSourceName"
-    >
-      {{ source.bookSourceName || source.sourceName }}
-    </el-checkbox>
+    <virtual-list
+      style="height: 100%; overflow-y: auto; overflow-x: hidden;"
+      :data-key="'bookSourceUrl'"
+      :data-sources="sourcesFiltered"
+      :data-component="SourceItem"
+      :estimate-size="45"
+    />
   </el-checkbox-group>
 </template>
 
 <script setup>
 import { Folder, Delete, Download, Search } from "@element-plus/icons-vue";
 import { isSourceContains } from "../utils/souce";
-
+import VirtualList from 'vue3-virtual-scroll-list';
+import SourceItem from "./SourceItem.vue";
 const store = useSourceStore();
 const sourceSelect = ref([]);
 const searchKey = ref("");
-const { sources, errorPushSources } = storeToRefs(store);
-
+const { sources } = storeToRefs(store);
 const isBookSource = computed(() => {
   return /bookSource/.test(window.location.href);
 });
-const handleSourceClick = (source) => {
-  store.changeCurrentSource(source);
-};
 const deleteSelectSources = () => {
   store.deleteSources(sourceSelect.value);
   sourceSelect.value = [];
@@ -104,7 +97,7 @@ const importSourceFile = () => {
 };
 const outExport = () => {
   const exportFile = document.createElement("a");
-  let sources = store.sources,
+  let sources = sourceSelect.value.length === 0 ? sourcesFiltered.value : sourceSelect.value,
     sourceType = isBookSource.value ? "BookSource" : "RssSource";
 
   exportFile.download = `${sourceType}_${Date()
@@ -122,15 +115,13 @@ const outExport = () => {
 <style lang="scss" scoped>
 .tool {
   display: flex;
-  padding: 4px 0;
-  justify-content: space-between;
+  margin: 4px 0;
+  justify-content: center;
 }
 
 #source-list {
-  padding-top: 6px;
-  height: calc(100vh - 112px - 20px);
-  overflow-y: auto;
-  overflow-x: hidden;
+  margin-top: 6px;
+  height: calc(100vh - 112px - 7px);
 
   :deep(.el-checkbox) {
     margin-bottom: 4px;

+ 6 - 1
modules/web/src/components/SourceTabForm.vue

@@ -69,7 +69,12 @@ const { currentSource } = storeToRefs(store);
 
 <style lang="scss" scoped>
 :deep(.el-tab-pane) {
-  height: calc(100vh - 40px);
+  height: calc(100vh - 55px);
+  padding-top: 15px;
+  padding-right: 5px;
   overflow-y: auto;
 }
+:deep(.el-tabs__header) {
+  margin: 0;
+}
 </style>

+ 5 - 1
modules/web/src/components/SourceTabTools.vue

@@ -29,4 +29,8 @@ const tabData = ref([
 ]);
 </script>
 
-<style lang="scss" scoped></style>
+<style lang="scss" scoped>
+:deep(.el-tabs__header) {
+  margin-bottom: 5px;
+}
+</style>

+ 6 - 1
modules/web/src/components/ToolBar.vue

@@ -72,6 +72,11 @@ import { isInvaildSource } from "../utils/souce";
 
 const store = useSourceStore();
 const pull = () => {
+  const loadingMsg = ElMessage({
+    message: '加载中……',
+    showClose: true,
+    duration: 0
+  })
   API.getSources().then(({ data }) => {
     if (data.isSuccess) {
       store.changeTabName("editList");
@@ -86,7 +91,7 @@ const pull = () => {
         type: "error",
       });
     }
-  });
+  }).finally(() => loadingMsg.close());
 };
 
 const push = () => {

+ 5 - 5
modules/web/src/utils/bookSourceEditConfig.js

@@ -552,11 +552,11 @@ id: "deleteUrl",
         id: "enabledExplore",
         type: "Boolean",
       },
-      {
-        title: "启用段评",
-        id: "enabledReview",
-        type: "Boolean",
-      },
+      // {
+      //   title: "启用段评",
+      //   id: "enabledReview",
+      //   type: "Boolean",
+      // },
       {
         title: "Cookie",
         id: "enabledCookieJar",

+ 1 - 0
modules/web/src/views/SourceEditor.vue

@@ -30,6 +30,7 @@ if (/bookSource/i.test(location.href)) {
     margin-left: 20px;
   }
   .right {
+    flex: 1;
     width: 360px;
     margin-right: 20px;
   }