From d6923fc605b3d1742c89f08ae5d27b16080a1da5 Mon Sep 17 00:00:00 2001 From: hymbz Date: Wed, 17 Jan 2024 23:38:56 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20:zap:=20=E4=BC=98=E5=8C=96=E5=8D=B7?= =?UTF-8?q?=E8=BD=B4=E6=A8=A1=E5=BC=8F=E6=80=A7=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .stylelintrc.js | 10 +- package.json | 4 +- pnpm-lock.yaml | 30 ++-- src/components/Fab/index.tsx | 6 +- src/components/Manga/actions/helper.ts | 30 +--- src/components/Manga/actions/hotkeys.ts | 26 ++- src/components/Manga/actions/image.ts | 79 ++------ src/components/Manga/actions/imageSize.ts | 163 +++++++++-------- src/components/Manga/actions/index.ts | 13 +- src/components/Manga/actions/memo/common.ts | 80 +++++++++ src/components/Manga/actions/memo/index.ts | 3 + src/components/Manga/actions/memo/observer.ts | 86 +++++++++ .../Manga/actions/memo/renderPage.ts | 67 +++++++ src/components/Manga/actions/operate.ts | 3 + src/components/Manga/actions/pointer.ts | 29 ++- src/components/Manga/actions/scrollbar.ts | 168 +++++++----------- src/components/Manga/actions/show.ts | 163 ++++++++--------- src/components/Manga/actions/switch.ts | 5 +- .../Manga/actions/translation/index.ts | 53 +++--- src/components/Manga/actions/turnPage.ts | 23 ++- src/components/Manga/actions/zoom.ts | 12 +- .../Manga/components/ComicImg.module.css | 86 ++++----- src/components/Manga/components/ComicImg.tsx | 35 ++-- .../Manga/components/ComicImgFlow.tsx | 134 ++++++++------ src/components/Manga/components/ComicPage.tsx | 15 +- src/components/Manga/components/CssVar.tsx | 4 +- .../Manga/components/Scrollbar.module.css | 35 ++-- src/components/Manga/components/Scrollbar.tsx | 85 ++++----- .../Manga/components/ScrollbarPage.tsx | 44 ----- .../Manga/components/ScrollbarPageStatus.tsx | 102 +++++++++++ src/components/Manga/components/Toolbar.tsx | 7 +- src/components/Manga/defaultButtonList.tsx | 4 +- src/components/Manga/defaultSettingList.tsx | 7 +- src/components/Manga/display.tsx | 10 +- src/components/Manga/helper.ts | 60 +++++++ src/components/Manga/hooks/useDrag.ts | 6 +- src/components/Manga/hooks/useHiddenMouse.ts | 4 +- src/components/Manga/hooks/useInit.ts | 60 +++---- src/components/Manga/index.module.css | 4 +- src/components/Manga/index.tsx | 1 + src/components/Manga/store/index.ts | 1 + src/components/Manga/store/other.ts | 20 +-- src/components/Manga/store/show.ts | 8 - src/components/useComponents/Manga.tsx | 1 + src/helper/imgMap.ts | 7 +- src/helper/index.ts | 24 ++- src/helper/useInit.tsx | 9 +- src/site/other.ts | 24 ++- 48 files changed, 1046 insertions(+), 804 deletions(-) create mode 100644 src/components/Manga/actions/memo/common.ts create mode 100644 src/components/Manga/actions/memo/index.ts create mode 100644 src/components/Manga/actions/memo/observer.ts create mode 100644 src/components/Manga/actions/memo/renderPage.ts delete mode 100644 src/components/Manga/components/ScrollbarPage.tsx create mode 100644 src/components/Manga/components/ScrollbarPageStatus.tsx diff --git a/.stylelintrc.js b/.stylelintrc.js index 2bdac821..53953822 100644 --- a/.stylelintrc.js +++ b/.stylelintrc.js @@ -1,11 +1,11 @@ module.exports = { ignoreFiles: ['**/node_modules/**/*.css', '**/dist/**/*.css'], - extends: ['stylelint-config-standard', 'stylelint-config-clean-order'], - plugins: [ - 'stylelint-prettier', - 'stylelint-order', - 'stylelint-high-performance-animation', + extends: [ + 'stylelint-config-standard', + 'stylelint-prettier/recommended', + 'stylelint-config-clean-order', ], + plugins: ['stylelint-order', 'stylelint-high-performance-animation'], rules: { // 允许 css 变量使用任意命名方式 'custom-property-pattern': null, diff --git a/package.json b/package.json index a3161ed0..31209fc7 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,9 @@ "dependencies": { "@material-design-icons/svg": "^0.14.13", "@placemarkio/flat-drop-files": "^1.0.2", + "@solid-primitives/scheduled": "^1.4.1", "browser-fs-access": "^0.35.0", + "fast-deep-equal": "^3.1.3", "fflate": "^0.8.1", "jsencrypt": "^3.3.2", "libarchive.js": "^1.3.0", @@ -34,7 +36,6 @@ "pwa-install-handler": "^2.5.0", "solid-element": "^1.8.0", "solid-js": "^1.8.7", - "throttle-debounce": "^5.0.0", "water.css": "^2.1.1" }, "devDependencies": { @@ -55,7 +56,6 @@ "@solidjs/router": "^0.10.5", "@types/libarchive.js": "^1.3.4", "@types/shelljs": "^0.8.15", - "@types/throttle-debounce": "^5.0.2", "@types/wicg-file-system-access": "^2023.10.4", "@types/wicg-web-app-launch": "^2023.1.3", "@typescript-eslint/eslint-plugin": "^6.16.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d0369ca6..be6361eb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,9 +11,15 @@ dependencies: '@placemarkio/flat-drop-files': specifier: ^1.0.2 version: 1.0.2 + '@solid-primitives/scheduled': + specifier: ^1.4.1 + version: 1.4.1(solid-js@1.8.7) browser-fs-access: specifier: ^0.35.0 version: 0.35.0 + fast-deep-equal: + specifier: ^3.1.3 + version: 3.1.3 fflate: specifier: ^0.8.1 version: 0.8.1 @@ -44,9 +50,6 @@ dependencies: solid-js: specifier: ^1.8.7 version: 1.8.7 - throttle-debounce: - specifier: ^5.0.0 - version: 5.0.0 water.css: specifier: ^2.1.1 version: 2.1.1 @@ -103,9 +106,6 @@ devDependencies: '@types/shelljs': specifier: ^0.8.15 version: 0.8.15 - '@types/throttle-debounce': - specifier: ^5.0.2 - version: 5.0.2 '@types/wicg-file-system-access': specifier: ^2023.10.4 version: 2023.10.4 @@ -2635,6 +2635,14 @@ packages: engines: {node: '>=18'} dev: true + /@solid-primitives/scheduled@1.4.1(solid-js@1.8.7): + resolution: {integrity: sha512-OLcNXwYpX7HUOEqNPcmR31dkyI1E2imkMDBRlqsGT0ZhJV1L2g0TEREpo4nm/kUhh8LVQzkfnxS+GONx9kh90A==} + peerDependencies: + solid-js: ^1.6.12 + dependencies: + solid-js: 1.8.7 + dev: false + /@solidjs/router@0.10.5(solid-js@1.8.7): resolution: {integrity: sha512-gxDeiyc97j8/UqzuuasZsQYA7jpmlwjkfpcay11Q2xfzoKR50eBM1AaxAPtf0MlBAdPsdPV3h/k8t5/XQCuebA==} peerDependencies: @@ -2802,10 +2810,6 @@ packages: resolution: {integrity: sha512-cSXSKOpTSr2HTdlGq8WskyZwNyxKhM7M/zJeLVdWjlUQmQ4d8TdtPrwz4JejglZdzIzSgU5loi5QUaEJF9JD8w==} dev: true - /@types/throttle-debounce@5.0.2: - resolution: {integrity: sha512-pDzSNulqooSKvSNcksnV72nk8p7gRqN8As71Sp28nov1IgmPKWbOEIwAWvBME5pPTtaXJAvG3O4oc76HlQ4kqQ==} - dev: true - /@types/trusted-types@2.0.6: resolution: {integrity: sha512-HYtNooPvUY9WAVRBr4u+4Qa9fYD1ze2IUlAD3HoA6oehn1taGwBx3Oa52U4mTslTS+GAExKpaFu39Y5xUEwfjg==} dev: true @@ -5290,7 +5294,6 @@ packages: /fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - dev: true /fast-diff@1.3.0: resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} @@ -9708,11 +9711,6 @@ packages: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} dev: true - /throttle-debounce@5.0.0: - resolution: {integrity: sha512-2iQTSgkkc1Zyk0MeVrt/3BvuOXYPl/R8Z0U2xxo9rjwNciaHDG3R+Lm6dh4EeUci49DanvBnuqI6jshoQQRGEg==} - engines: {node: '>=12.22'} - dev: false - /through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} dev: true diff --git a/src/components/Fab/index.tsx b/src/components/Fab/index.tsx index 721572ab..fc18c3d7 100644 --- a/src/components/Fab/index.tsx +++ b/src/components/Fab/index.tsx @@ -1,4 +1,5 @@ import type { Component, JSX } from 'solid-js'; +import { throttle } from 'helper'; import { For, onCleanup, @@ -8,7 +9,6 @@ import { mergeProps, Show, } from 'solid-js'; -import { throttle } from 'throttle-debounce'; import MdMenuBook from '@material-design-icons/svg/round/menu_book.svg'; import classes, { css as style } from './index.module.css'; @@ -51,7 +51,7 @@ export const Fab: Component = (_props) => { const [show, setShow] = createSignal(props.initialShow); // 绑定滚动事件 - const handleScroll = throttle(200, (e: Event) => { + const handleScroll = throttle((e: Event) => { // 跳过非用户操作的滚动 if (e.isTrusted === false) return; if (window.scrollY === lastY) return; @@ -62,7 +62,7 @@ export const Fab: Component = (_props) => { window.scrollY - lastY < 0, ); lastY = window.scrollY; - }); + }, 200); onMount(() => window.addEventListener('scroll', handleScroll)); onCleanup(() => window.removeEventListener('scroll', handleScroll)); diff --git a/src/components/Manga/actions/helper.ts b/src/components/Manga/actions/helper.ts index 4e828249..9b67fcbf 100644 --- a/src/components/Manga/actions/helper.ts +++ b/src/components/Manga/actions/helper.ts @@ -1,3 +1,4 @@ +import { scheduleIdle } from '@solid-primitives/scheduled'; import { difference, byPath } from 'helper'; import type { State } from '../store'; import { store, setState, refs } from '../store'; @@ -5,10 +6,10 @@ import type { Option } from '../store/option'; import { defaultOption } from '../store/option'; /** 触发 onOptionChange */ -export const triggerOnOptionChange = () => - setTimeout( - () => store.prop.OptionChange?.(difference(store.option, defaultOption)), - ); +export const triggerOnOptionChange = scheduleIdle( + () => store.prop.OptionChange?.(difference(store.option, defaultOption)), + 1000, +); /** 在 option 后手动触发 onOptionChange */ export const setOption = (fn: (option: Option, state: State) => void) => { @@ -35,22 +36,5 @@ export const resetUI = (state: State) => { state.show.touchArea = false; }; -/** 检查已加载图片中是否**连续**出现了多个指定类型的图片 */ -export const checkImgTypeCount = ( - state: State, - fn: (img: ComicImg) => boolean, - maxNum = 3, -) => { - let num = 0; - for (let i = 0; i < state.imgList.length; i++) { - const img = state.imgList[i]; - if (img.loadType !== 'loaded') continue; - if (!fn(img)) { - num = 0; - continue; - } - num += 1; - if (num >= maxNum) return true; - } - return false; -}; +export const scrollTo = (top: number, smooth = false) => + refs.mangaBox.scrollTo({ top, behavior: smooth ? 'smooth' : 'instant' }); diff --git a/src/components/Manga/actions/hotkeys.ts b/src/components/Manga/actions/hotkeys.ts index 19d6041b..bc98e86d 100644 --- a/src/components/Manga/actions/hotkeys.ts +++ b/src/components/Manga/actions/hotkeys.ts @@ -1,5 +1,5 @@ -import { createMemo, createRoot } from 'solid-js'; -import { isEqualArray } from 'helper'; +import { isEqual } from 'helper'; +import { createRootMemo } from '../helper'; import { _setState, store } from '../store'; export const defaultHotkeys: Readonly> = { @@ -24,26 +24,20 @@ export const setHotkeys = (...args: any[]) => { Object.fromEntries( Object.entries(store.hotkeys).filter( ([name, keys]) => - !defaultHotkeys[name] || !isEqualArray(keys, defaultHotkeys[name]), + !defaultHotkeys[name] || !isEqual(keys, defaultHotkeys[name]), ), ), ); }; -export const { hotkeysMap } = createRoot(() => { - const hotkeysMapMemo = createMemo(() => - Object.fromEntries( - Object.entries(store.hotkeys).flatMap(([name, key]) => - key.map((k) => [k, name]), - ), +/** 快捷键配置 */ +export const hotkeysMap = createRootMemo(() => + Object.fromEntries( + Object.entries(store.hotkeys).flatMap(([name, key]) => + key.map((k) => [k, name]), ), - ); - - return { - /** 快捷键配置 */ - hotkeysMap: hotkeysMapMemo, - }; -}); + ), +); /** 删除指定快捷键 */ export const delHotkeys = (code: string) => { diff --git a/src/components/Manga/actions/image.ts b/src/components/Manga/actions/image.ts index f0685a0e..d79916a4 100644 --- a/src/components/Manga/actions/image.ts +++ b/src/components/Manga/actions/image.ts @@ -1,47 +1,9 @@ -import { debounce } from 'throttle-debounce'; -import { createMemo, createRoot } from 'solid-js'; - -import { clamp } from 'helper'; -import { - autoCloseFill, - findFillIndex, - handleComicData, -} from '../handleComicData'; +import { clamp, isEqual, debounce } from 'helper'; +import { autoCloseFill, handleComicData } from '../handleComicData'; import type { State } from '../store'; -import { store, setState, refs } from '../store'; -import { contentHeight, updateDrag } from './scrollbar'; +import { store } from '../store'; import { setOption } from './helper'; - -export const { activeImgIndex, nowFillIndex, activePage, preloadNum } = - createRoot(() => { - const activePageMemo = createMemo( - () => store.pageList[store.activePageIndex] ?? [], - ); - - const activeImgIndexMemo = createMemo( - () => activePageMemo().find((i) => i !== -1) ?? 0, - ); - - const nowFillIndexMemo = createMemo(() => - findFillIndex(activeImgIndexMemo(), store.fillEffect), - ); - - const preloadNumMemo = createMemo(() => ({ - back: store.option.preloadPageNum, - front: Math.floor(store.option.preloadPageNum / 2), - })); - - return { - /** 当前显示的第一张图片的 index */ - activeImgIndex: activeImgIndexMemo, - /** 当前所处的图片流 */ - nowFillIndex: nowFillIndexMemo, - /** 当前显示页面 */ - activePage: activePageMemo, - /** 预加载页数 */ - preloadNum: preloadNumMemo, - }; - }); +import { activeImgIndex, preloadNum } from './memo'; type LoadImgDraft = { editNum: number; loadNum: number }; const loadImg = (state: State, index: number, draft: LoadImgDraft) => { @@ -85,25 +47,19 @@ export const zoomScrollModeImg = (zoomLevel: number, set = false) => { setOption((draftOption) => { const newVal = set ? zoomLevel - : // 放大到整数再运算,避免精度丢失导致的奇怪的值 - (store.option.scrollModeImgScale * 100 + zoomLevel * 100) / 100; - - draftOption.scrollModeImgScale = clamp(0.1, newVal, 3); - }); - // 在调整图片缩放后使当前滚动进度保持不变 - refs.mangaFlow.scrollTo({ - top: contentHeight() * store.scrollbar.dragTop, - behavior: 'instant', + : store.option.scrollModeImgScale + zoomLevel; + draftOption.scrollModeImgScale = clamp(0.1, +newVal.toFixed(2), 3); }); - setState(updateDrag); }; /** 根据当前页数更新所有图片的加载状态 */ -export const updateImgLoadType = debounce(100, (state: State) => { +export const updateImgLoadType = debounce((state: State) => { // 先将所有加载中的图片状态改为暂停 - state.imgList.forEach((img, i) => { - if (img.loadType === 'loading') state.imgList[i].loadType = 'wait'; - }); + let i = state.imgList.length; + while (i--) { + if (state.imgList[i].loadType === 'loading') + state.imgList[i].loadType = 'wait'; + } return ( // 优先加载当前显示页 @@ -131,10 +87,12 @@ export const updatePageData = (state: State) => { isMobile, } = state; + let newPageList: PageList = []; if (onePageMode || scrollMode || isMobile || imgList.length <= 1) - state.pageList = imgList.map((_, i) => [i]); - else state.pageList = handleComicData(imgList, fillEffect); - updateDrag(state); + newPageList = imgList.map((_, i) => [i]); + else newPageList = handleComicData(imgList, fillEffect); + if (!isEqual(state.pageList, newPageList)) state.pageList = newPageList; + updateImgLoadType(state); // 在图片排列改变后自动跳转回原先显示图片所在的页数 @@ -153,8 +111,9 @@ export const updatePageData = (state: State) => { * 3. updatePageData */ export const resetImgState = (state: State) => { - state.flag.autoScrollMode = true; + state.flag.autoScrollMode = false; state.flag.autoWide = false; + state.flag.autoLong = false; autoCloseFill.clear(); // 如果用户没有手动修改过首页填充,才将其恢复初始 if (typeof state.fillEffect['-1'] === 'boolean') diff --git a/src/components/Manga/actions/imageSize.ts b/src/components/Manga/actions/imageSize.ts index 8b85f4d9..345d16c7 100644 --- a/src/components/Manga/actions/imageSize.ts +++ b/src/components/Manga/actions/imageSize.ts @@ -1,16 +1,16 @@ -import { createEffect, createMemo, createRoot, on } from 'solid-js'; +import { createRoot } from 'solid-js'; import { getImgSize, plimit, singleThreaded } from 'helper'; import type { State } from '../store'; -import { refs, setState, store } from '../store'; -import { updateDrag } from './scrollbar'; +import { setState, store } from '../store'; import { isWideImg } from '../handleComicData'; import { resetImgState, updatePageData } from './image'; -import { checkImgTypeCount } from './helper'; +import { rootSize } from './memo'; +import { createEffectOn } from '../helper'; /** 根据比例更新图片类型。返回是否修改了图片类型 */ const updateImgType = (state: State, draftImg: ComicImg) => { const { width, height, type } = draftImg; - if (!width || !height || !state.memo.size.width || !state.memo.size.height) + if (!width || !height || !rootSize().width || !rootSize().height) return false; const imgRatio = width / height; @@ -23,6 +23,39 @@ const updateImgType = (state: State, draftImg: ComicImg) => { return type !== draftImg.type; }; +/** 检查指定图片周围包括自己在内,是否有足够数量的**连续**的符合条件的图片 */ +const checkImgTypeCount = ( + state: State, + index: number, + maxNum: number, + fn = (other: ComicImg, target: ComicImg) => other.type === target.type, +) => { + let num = 1; + + const targetImg = state.imgList[index]; + + let i = index; + while (i--) { + const img = state.imgList[i]; + if (img.loadType !== 'loaded') continue; + if (fn(img, targetImg)) { + num += 1; + if (num >= maxNum) return true; + } else break; + } + + for (i = index; i < state.imgList.length; i++) { + const img = state.imgList[i]; + if (img.loadType !== 'loaded') continue; + if (fn(img, targetImg)) { + num += 1; + if (num >= maxNum) return true; + } else break; + } + + return false; +}; + /** 更新图片尺寸 */ export const updateImgSize = (i: number, width: number, height: number) => { setState((state) => { @@ -33,10 +66,16 @@ export const updateImgSize = (i: number, width: number, height: number) => { let isEdited = updateImgType(state, img); switch (img.type) { + // 连续出现多张宽图后,自动将滚动条移至底部 + case 'long': { + if (!state.flag.autoLong && checkImgTypeCount(store, i, 5)) + state.flag.autoLong = true; + // fall through + } // 连续出现多张跨页图后,将剩余未加载图片类型设为跨页图 - case 'long': case 'wide': { - if (state.flag.autoWide || !checkImgTypeCount(state, isWideImg)) break; + if (state.flag.autoWide || !checkImgTypeCount(state, i, 3, isWideImg)) + break; state.imgList.forEach((comicImg, index) => { if (comicImg.loadType === 'wait' && comicImg.type === '') state.imgList[index].type = 'wide'; @@ -48,92 +87,64 @@ export const updateImgSize = (i: number, width: number, height: number) => { // 连续出现多张长图后,自动开启卷轴模式 case 'vertical': { - if ( - !state.flag.autoScrollMode || - !checkImgTypeCount(state, ({ type }) => type === 'vertical') - ) - break; + if (state.flag.autoScrollMode || !checkImgTypeCount(state, i, 3)) break; + state.imgList.forEach((comicImg, index) => { + if (comicImg.loadType === 'wait' && comicImg.type === '') + state.imgList[index].type = 'vertical'; + }); state.option.scrollMode = true; - state.flag.autoScrollMode = false; + state.flag.autoScrollMode = true; isEdited = true; break; } } - if (!isEdited) return updateDrag(state); + if (!isEdited) return; Reflect.deleteProperty(state.fillEffect, i); updatePageData(state); }); }; -export const { placeholderSize } = createRoot(() => { +createRoot(() => { // 预加载所有图片的尺寸 - createEffect( - on( - () => store.imgList, - singleThreaded((state) => - plimit( - store.imgList.map((img, i) => async () => { - if (state.continueRun) return; - if (img.loadType !== 'wait' || img.width || img.height || !img.src) - return; - - const size = await getImgSize(img.src, () => state.continueRun); - if (state.continueRun) return; - if (size) updateImgSize(i, ...size); - }), - undefined, - Math.max(store.option.preloadPageNum, 1), - ), + createEffectOn( + () => store.imgList, + singleThreaded((state) => + plimit( + store.imgList.map((img, i) => async () => { + if (state.continueRun) return; + if (img.loadType !== 'wait' || img.width || img.height || !img.src) + return; + + const size = await getImgSize(img.src, () => state.continueRun); + if (state.continueRun) return; + if (size) updateImgSize(i, ...size); + }), + undefined, + Math.max(store.option.preloadPageNum, 1), ), ), ); // 处理显示窗口的长宽变化 - createEffect( - on( - () => store.memo.size, - ({ width, height }) => - setState((state) => { - state.proportion.单页比例 = Math.min(width / 2 / height, 1); - state.proportion.横幅比例 = width / height; - state.proportion.条漫比例 = state.proportion.单页比例 / 2; - - let isEdited = false; - for (let i = 0; i < state.imgList.length; i++) { - if (!updateImgType(state, state.imgList[i])) continue; - isEdited = true; - Reflect.deleteProperty(state.fillEffect, i); - } - if (isEdited) resetImgState(state); - updatePageData(state); - }), - { defer: true }, - ), - ); + createEffectOn( + rootSize, + ({ width, height }) => + setState((state) => { + state.proportion.单页比例 = Math.min(width / 2 / height, 1); + state.proportion.横幅比例 = width / height; + state.proportion.条漫比例 = state.proportion.单页比例 / 2; - /** 获取图片列表中指定属性的中位数 */ - const getImgMedian = ( - sizeFn: (value: ComicImg) => number, - fallback: number, - ) => { - if (!store.option.scrollMode) return 0; - const list = store.imgList - .filter((img) => img.loadType === 'loaded' && img.width) - .map(sizeFn) - .sort(); - if (!list.length) return fallback; - return list[Math.floor(list.length / 2)]; - }; - - const placeholderSizeMemo = createMemo(() => ({ - width: getImgMedian((img) => img.width!, refs.root?.offsetWidth), - height: getImgMedian((img) => img.height!, refs.root?.offsetHeight), - })); - - return { - /** 图片占位尺寸 */ - placeholderSize: placeholderSizeMemo, - }; + let isEdited = false; + for (let i = 0; i < state.imgList.length; i++) { + if (!updateImgType(state, state.imgList[i])) continue; + isEdited = true; + Reflect.deleteProperty(state.fillEffect, i); + } + if (isEdited) resetImgState(state); + updatePageData(state); + }), + { defer: true }, + ); }); diff --git a/src/components/Manga/actions/index.ts b/src/components/Manga/actions/index.ts index f91a5d34..aac1d202 100644 --- a/src/components/Manga/actions/index.ts +++ b/src/components/Manga/actions/index.ts @@ -1,11 +1,12 @@ -export * from './image'; -export * from './operate'; -export * from './scrollbar'; export * from './helper'; export * from './hotkeys'; -export * from './pointer'; -export * from './zoom'; +export * from './image'; +export * from './memo'; export * from './imageSize'; -export * from './switch'; +export * from './operate'; +export * from './pointer'; +export * from './scrollbar'; export * from './show'; +export * from './switch'; export * from './turnPage'; +export * from './zoom'; diff --git a/src/components/Manga/actions/memo/common.ts b/src/components/Manga/actions/memo/common.ts new file mode 100644 index 00000000..d92f9eaf --- /dev/null +++ b/src/components/Manga/actions/memo/common.ts @@ -0,0 +1,80 @@ +import { createRootMemo, createThrottleMemo } from '../../helper'; +import { store, refs } from '../../store'; +import { findFillIndex } from '../../handleComicData'; + +/** 当前显示页面 */ +export const activePage = createRootMemo( + () => store.pageList[store.activePageIndex] ?? [], +); + +/** 当前显示的第一张图片的 index */ +export const activeImgIndex = createRootMemo( + () => activePage().find((i) => i !== -1) ?? 0, +); + +/** 当前所处的图片流 */ +export const nowFillIndex = createRootMemo(() => + findFillIndex(activeImgIndex(), store.fillEffect), +); + +/** 预加载页数 */ +export const preloadNum = createRootMemo(() => ({ + back: store.option.preloadPageNum, + front: Math.floor(store.option.preloadPageNum / 2), +})); + +/** 默认图片类型 */ +export const defaultImgType = createRootMemo(() => { + if (store.flag.autoWide) return 'wide'; + if (store.flag.autoScrollMode) return 'vertical'; + return ''; +}); + +/** 获取图片列表中指定属性的中位数 */ +const getImgMedian = (sizeFn: (value: ComicImg) => number) => { + if (!store.option.scrollMode) return 0; + const list = store.imgList + .filter((img) => img.loadType === 'loaded' && img.width) + .map(sizeFn) + .sort(); + if (!list.length) return null; + return list[Math.floor(list.length / 2)]; +}; + +/** 图片占位尺寸 */ +export const placeholderSize = createThrottleMemo( + () => ({ + width: getImgMedian((img) => img.width!) ?? refs.root?.offsetWidth, + height: getImgMedian((img) => img.height!) ?? refs.root?.offsetHeight, + }), + 500, +); + +/** 每张图片的高度 */ +export const imgHeightList = createRootMemo(() => + store.option.scrollMode + ? store.imgList.map( + (img) => + (img.height ?? placeholderSize().height) * + store.option.scrollModeImgScale, + ) + : [], +); + +/** 卷轴模式下每张图片的位置 */ +export const imgTopList = createRootMemo(() => { + if (!store.option.scrollMode) return []; + + const list = new Array(imgHeightList().length); + let top = 0; + for (let i = 0; i < imgHeightList().length; i++) { + list[i] = top; + top += imgHeightList()[i] + store.option.scrollModeSpacing * 7; + } + return list; +}); + +/** 漫画流的总高度 */ +export const contentHeight = createRootMemo( + () => (imgTopList().at(-1) ?? 0) + (imgHeightList().at(-1) ?? 0), +); diff --git a/src/components/Manga/actions/memo/index.ts b/src/components/Manga/actions/memo/index.ts new file mode 100644 index 00000000..94a0dc1d --- /dev/null +++ b/src/components/Manga/actions/memo/index.ts @@ -0,0 +1,3 @@ +export * from './observer'; +export * from './renderPage'; +export * from './common'; diff --git a/src/components/Manga/actions/memo/observer.ts b/src/components/Manga/actions/memo/observer.ts new file mode 100644 index 00000000..ef8c6b5b --- /dev/null +++ b/src/components/Manga/actions/memo/observer.ts @@ -0,0 +1,86 @@ +import { createRoot, createSignal, onCleanup } from 'solid-js'; +import { inRange, isEqual, throttle } from 'helper'; +import { createEffectOn } from '../../helper'; +import { store, _setState, setState } from '../../store'; + +/** 当前显示的图片 */ +export const showImgList = new Set(); + +const [_showPageList, setShowPageList] = createSignal([] as number[], { + equals: isEqual, +}); +/** 当前显示的页面 */ +export const showPageList = _showPageList; +const updateShowPageList = throttle(() => { + const newShowPageList = new Set(); + showImgList.forEach((img) => + newShowPageList.add(+img.parentElement!.getAttribute('data-index')!), + ); + setShowPageList([...newShowPageList].sort((a, b) => a - b)); +}); + +export const initIntersectionObserver = (root: HTMLElement) => { + const handleObserver: IntersectionObserverCallback = (entries) => { + if (!entries.length) return; + entries.forEach(({ isIntersecting, target }) => { + if (isIntersecting) showImgList.add(target as HTMLImageElement); + else showImgList.delete(target as HTMLImageElement); + }); + updateShowPageList(); + }; + + _setState( + 'observer', + new IntersectionObserver(handleObserver, { root, threshold: 0.01 }), + ); + onCleanup(() => { + setState((state) => { + state.observer?.disconnect(); + state.observer = null; + }); + }); +}; + +const [_rootSize, setRootSize] = createSignal( + { width: 0, height: 0 }, + // 宽高为零时不触发变更 + { equals: (_, { width, height }) => !width || !height }, +); +/** 容器尺寸 */ +export const rootSize = _rootSize; + +export const initResizeObserver = (dom: HTMLElement) => { + setRootSize({ width: dom.scrollWidth, height: dom.scrollHeight }); + // 在 rootDom 的大小改变时更新比例,并重新计算图片类型 + const resizeObserver = new ResizeObserver( + throttle(([{ contentRect }]) => + setRootSize({ width: contentRect.width, height: contentRect.height }), + ), + ); + resizeObserver.disconnect(); + resizeObserver.observe(dom); + onCleanup(() => resizeObserver.disconnect()); +}; + +const [_scrollTop, setScrollTop] = createSignal(0); +/** 滚动距离 */ +export const scrollTop = _scrollTop; +export const bindScrollTop = (dom: HTMLElement) => { + dom.addEventListener('scroll', () => setScrollTop(dom.scrollTop), { + passive: true, + }); +}; + +createRoot(() => { + // 卷轴模式下,将当前显示的第一页作为当前页 + createEffectOn(showPageList, ([firstPage]) => { + if (!store.gridMode && store.option.scrollMode) + _setState('activePageIndex', firstPage ?? 0); + }); + + // 窗口宽度小于800像素时,标记为移动端 + createEffectOn( + rootSize, + ({ width }) => inRange(1, width, 800) && _setState('isMobile', true), + ); +}); diff --git a/src/components/Manga/actions/memo/renderPage.ts b/src/components/Manga/actions/memo/renderPage.ts new file mode 100644 index 00000000..ccb5ade6 --- /dev/null +++ b/src/components/Manga/actions/memo/renderPage.ts @@ -0,0 +1,67 @@ +import { createRoot, createSignal } from 'solid-js'; +import { inRange } from 'helper'; +import type { State } from '../../store'; +import { setState, store } from '../../store'; +import { contentHeight, imgTopList } from './common'; +import { rootSize, scrollTop } from './observer'; +import { createEffectOn } from '../../helper'; + +const [renderRangeStart, setRenderRangeStart] = createSignal(0); +const [renderRangeEnd, setRenderRangeEnd] = createSignal(0); + +/** 渲染范围 */ +export const renderRange = { start: renderRangeStart, end: renderRangeEnd }; + +const findTopImg = (initIndex: number, top: number) => { + let i = initIndex || 1; + for (; i < imgTopList().length; i++) if (imgTopList()[i] > top) return i - 1; + return imgTopList().length - 1; +}; + +/** 计算渲染页面 */ +export const updateRenderRange = (state: State) => { + let startPage: number | undefined; + let endPage: number | undefined; + + if (state.option.scrollMode) { + if (contentHeight() === 0) { + startPage = 0; + endPage = 1; + } else { + const top = scrollTop() - rootSize().height * 4; + startPage = top < 0 ? 0 : findTopImg(0, top); + const bottom = scrollTop() + rootSize().height * 5; + endPage = + bottom > contentHeight() + ? imgTopList().length - 1 + : findTopImg(startPage, bottom); + } + } else { + startPage = Math.max(0, state.activePageIndex - 1); + endPage = Math.min(state.pageList.length, state.activePageIndex + 2); + } + + if (!startPage) startPage = 0; + if (!endPage) endPage = startPage + 1; + setRenderRangeStart(startPage); + setRenderRangeEnd(endPage); +}; + +createRoot(() => { + createEffectOn( + () => store.option.scrollModeImgScale, + () => setState(updateRenderRange), + ); + + const getImgBottom = (i: number) => + i === imgTopList().length - 1 ? contentHeight() : imgTopList()[i + 1]; + + let startImgBootom = 0; + let endImgTop = 0; + createEffectOn(scrollTop, (top) => { + if (inRange(startImgBootom, top, endImgTop)) return; + setState(updateRenderRange); + startImgBootom = getImgBottom(renderRangeStart()); + endImgTop = imgTopList()[renderRangeEnd()]; + }); +}); diff --git a/src/components/Manga/actions/operate.ts b/src/components/Manga/actions/operate.ts index 93ad96ea..b879f306 100644 --- a/src/components/Manga/actions/operate.ts +++ b/src/components/Manga/actions/operate.ts @@ -91,15 +91,18 @@ export const handleKeyDown = (e: KeyboardEvent) => { case 'End': case 'ArrowRight': case 'ArrowLeft': + e.stopPropagation(); return; case 'ArrowUp': case 'PageUp': + e.stopPropagation(); return store.gridMode || turnPage('prev'); case 'ArrowDown': case 'PageDown': case ' ': + e.stopPropagation(); return store.gridMode || turnPage('next'); } } diff --git a/src/components/Manga/actions/pointer.ts b/src/components/Manga/actions/pointer.ts index 374de4dd..a32cd3ca 100644 --- a/src/components/Manga/actions/pointer.ts +++ b/src/components/Manga/actions/pointer.ts @@ -1,13 +1,13 @@ -import { isEqual } from 'helper'; -import { debounce } from 'throttle-debounce'; +import { approx, debounce } from 'helper'; import type { Area } from '../components/TouchArea'; import { useDoubleClick } from '../hooks/useDoubleClick'; import type { UseDrag } from '../hooks/useDrag'; import { store, setState, refs } from '../store'; -import { resetUI } from './helper'; -import { updateRenderPage } from './show'; +import { resetUI, scrollTo } from './helper'; +import { resetPage } from './show'; import { turnPageFn, turnPageAnimation } from './turnPage'; import { zoom } from './zoom'; +import { imgTopList, rootSize } from './memo'; /** 根据坐标判断点击的元素 */ const findClickEle = (eleList: HTMLCollection, { x, y }: MouseEvent) => @@ -50,8 +50,7 @@ export const handleGridClick = (e: MouseEvent) => { state.activePageIndex = pageNum; state.gridMode = false; }); - if (store.option.scrollMode) - refs.mangaFlow.children[store.activePageIndex]?.scrollIntoView(); + if (store.option.scrollMode) scrollTo(imgTopList()[pageNum]); }; /** 双击放大 */ @@ -132,7 +131,7 @@ const handleDragEnd = (startTime?: number) => { state.isDragMode = false; }); }; -handleDragEnd.debounce = debounce(200, handleDragEnd); +handleDragEnd.debounce = debounce(handleDragEnd, 200); export const handleMangaFlowDrag: UseDrag = ({ type, @@ -152,15 +151,15 @@ export const handleMangaFlowDrag: UseDrag = ({ // 判断滑动方向 let slideDir: 'vertical' | 'horizontal' | undefined; - if (Math.abs(dx) > 5 && isEqual(dy, 0, 5)) slideDir = 'horizontal'; - if (Math.abs(dy) > 5 && isEqual(dx, 0, 5)) slideDir = 'vertical'; + if (Math.abs(dx) > 5 && approx(dy, 0, 5)) slideDir = 'horizontal'; + if (Math.abs(dy) > 5 && approx(dx, 0, 5)) slideDir = 'vertical'; if (!slideDir) return; setState((state) => { // 根据滑动方向自动切换排列模式 state.page.vertical = slideDir === 'vertical'; state.isDragMode = true; - updateRenderPage(state); + resetPage(state); }); return; } @@ -201,15 +200,15 @@ export const handleTrackpadWheel = (e: WheelEvent) => { } // 滚动过一页时 - if (dy <= -state.memo.size.height) { - if (turnPageFn(state, 'next')) dy += state.memo.size.height; - } else if (dy >= state.memo.size.height) { - if (turnPageFn(state, 'prev')) dy -= state.memo.size.height; + if (dy <= -rootSize().height) { + if (turnPageFn(state, 'next')) dy += rootSize().height; + } else if (dy >= rootSize().height) { + if (turnPageFn(state, 'prev')) dy -= rootSize().height; } state.page.vertical = true; state.isDragMode = true; - updateRenderPage(state); + resetPage(state); }); if (!animationId) animationId = requestAnimationFrame(handleDragAnima); diff --git a/src/components/Manga/actions/scrollbar.ts b/src/components/Manga/actions/scrollbar.ts index 4bd5298f..100d7f37 100644 --- a/src/components/Manga/actions/scrollbar.ts +++ b/src/components/Manga/actions/scrollbar.ts @@ -1,77 +1,45 @@ -import { t } from 'helper/i18n'; -import { createEffect, createMemo, createRoot, on } from 'solid-js'; +import { createRoot, createSignal } from 'solid-js'; +import { clamp } from 'helper'; import type { PointerState, UseDrag } from '../hooks/useDrag'; import type { State } from '../store'; import { store, refs, _setState } from '../store'; -import { checkImgTypeCount } from './helper'; - -/** 漫画流的总高度 */ -export const contentHeight = () => refs.mangaFlow.scrollHeight ?? 0; - -/** 能显示出漫画的高度 */ -export const windowHeight = () => refs.root.offsetHeight ?? 0; - -/** 滚动条长度 */ -export const scrollLength = () => - Math.max(refs.scrollbar?.clientWidth, refs.scrollbar?.clientHeight); +import { createEffectOn, createRootMemo } from '../helper'; +import { contentHeight, rootSize, scrollTop } from './memo'; +import { scrollTo } from './helper'; + +const [_scrollLength, setScrollLength] = createSignal(0); +/** 滚动条元素的长度 */ +export const scrollLength = _scrollLength; + +/** 滚动条滑块长度 */ +export const sliderHeight = createRootMemo(() => + store.option.scrollMode + ? rootSize().height / contentHeight() + : 1 / store.pageList.length, +); + +/** 滚动条滑块高度 */ +export const sliderTop = createRootMemo(() => + store.option.scrollMode + ? scrollTop() / contentHeight() + : (1 / store.pageList.length) * store.activePageIndex, +); + +/** 滚动条滑块的中心点高度 */ +export const sliderMidpoint = createRootMemo( + () => scrollLength() * (sliderTop() + sliderHeight() / 2), +); /** 滚动条位置 */ -export const scrollPosition = createRoot(() => { - const scrollPositionMemo = createMemo( - (): State['option']['scrollbar']['position'] => { - if (store.option.scrollbar.position === 'auto') { - if (store.isMobile) return 'top'; - return checkImgTypeCount(store, ({ type }) => type === 'long', 5) - ? 'bottom' - : 'right'; - } - return store.option.scrollbar.position; - }, - ); - return scrollPositionMemo; -}); - -/** 更新滚动条滑块的高度和所处高度 */ -export const updateDrag = (state: State) => { - if (!state.option.scrollMode) { - state.scrollbar.dragHeight = 0; - state.scrollbar.dragTop = 0; - return; - } - state.scrollbar.dragTop = refs.mangaFlow.scrollTop / contentHeight(); - state.scrollbar.dragHeight = - windowHeight() / (contentHeight() || windowHeight()); -}; - -/** 获取指定图片的提示文本 */ -export const getImgTip = (state: State, i: number) => { - if (i === -1) return t('other.fill_page'); - const img = state.imgList[i]; - - // 如果图片未加载完毕则在其 index 后增加显示当前加载状态 - if (img.loadType !== 'loaded') - return `${i + 1} (${t(`img_status.${img.loadType}`)})`; - - if ( - img.translationType && - img.translationType !== 'hide' && - img.translationMessage - ) - return `${i + 1}:${img.translationMessage}`; - - return `${i + 1}`; -}; - -/** 获取指定页面的提示文本 */ -export const getPageTip = (pageIndex: number): string => { - const page = store.pageList[pageIndex]; - if (!page) return 'null'; - const pageIndexText = page.map((index) => getImgTip(store, index)) as - | [string] - | [string, string]; - if (store.option.dir === 'rtl') pageIndexText.reverse(); - return pageIndexText.join(store.option.scrollMode ? '\n' : ' | '); -}; +export const scrollPosition = createRootMemo( + (): State['option']['scrollbar']['position'] => { + if (store.option.scrollbar.position === 'auto') { + if (store.isMobile) return 'top'; + return store.flag.autoLong ? 'bottom' : 'right'; + } + return store.option.scrollbar.position; + }, +); /** 判断点击位置在滚动条上的位置比率 */ const getClickTop = (x: number, y: number, e: HTMLElement): number => { @@ -88,7 +56,7 @@ const getClickTop = (x: number, y: number, e: HTMLElement): number => { }; /** 计算在滚动条上的拖动距离 */ -const getDragDist = ( +const getSliderDist = ( [x, y]: PointerState['xy'], [ix, iy]: PointerState['initial'], e: HTMLElement, @@ -105,9 +73,9 @@ const getDragDist = ( } }; -/** 开始拖拽时的 dragTop 值 */ +/** 开始拖拽时的 sliderTop 值 */ let startTop = 0; -export const handleScrollbarDrag: UseDrag = ({ type, xy, initial }, e) => { +export const handlescrollbarSlider: UseDrag = ({ type, xy, initial }, e) => { const [x, y] = xy; // 跳过拖拽结束事件(单击时会同时触发开始和结束,就用开始事件来完成单击的效果 @@ -119,29 +87,20 @@ export const handleScrollbarDrag: UseDrag = ({ type, xy, initial }, e) => { /** 点击位置在滚动条上的位置比率 */ const clickTop = getClickTop(x, y, e.target as HTMLElement); - let top = clickTop; if (store.option.scrollMode) { if (type === 'move') { - top = startTop + getDragDist(xy, initial, scrollbarDom); - // 处理超出范围的情况 - if (top < 0) top = 0; - else if (top > 1) top = 1; - refs.mangaFlow.scrollTo({ - top: top * contentHeight(), - behavior: 'instant', - }); + scrollTo( + clamp(0, startTop + getSliderDist(xy, initial, scrollbarDom), 1) * + contentHeight(), + ); } else { // 确保滚动条的中心会在点击位置 - top -= store.scrollbar.dragHeight / 2; - startTop = top; - refs.mangaFlow.scrollTo({ - top: top * contentHeight(), - behavior: 'smooth', - }); + startTop = clickTop - sliderHeight() / 2; + scrollTo(startTop * contentHeight(), true); } } else { - let newPageIndex = Math.floor(top * store.pageList.length); + let newPageIndex = Math.floor(clickTop * store.pageList.length); // 处理超出范围的情况 if (newPageIndex < 0) newPageIndex = 0; else if (newPageIndex >= store.pageList.length) @@ -152,20 +111,27 @@ export const handleScrollbarDrag: UseDrag = ({ type, xy, initial }, e) => { } }; -const updateScrollLength = () => - _setState( - 'memo', - 'scrollLength', - Math.max(refs.scrollbar?.clientWidth, refs.scrollbar?.clientHeight), - ); - createRoot(() => { // 更新 scrollLength - createEffect( - on([scrollPosition, () => store.memo.size], () => { - // 部分情况下,在窗口大小改变后滚动条大小不会立刻跟着修改,需要等待一帧渲染 - // 比如打开后台标签页后等一会再切换过去 - requestAnimationFrame(updateScrollLength); - }), + createEffectOn([scrollPosition, rootSize], () => { + if (!refs.scrollbar) return; + // 部分情况下,在窗口大小改变后滚动条大小不会立刻跟着修改,需要等待一帧渲染 + // 比如打开后台标签页后等一会再切换过去 + requestAnimationFrame(() => + setScrollLength( + Math.max(refs.scrollbar.clientWidth, refs.scrollbar.clientHeight), + ), + ); + }); + + // 在卷轴模式下缩放时保持滚动进度不变 + createEffectOn( + [contentHeight, scrollTop, () => store.option.scrollModeImgScale], + ([newHeight, , newScale], prev) => { + if (!prev) return; + const [oldHeight, oldScrollTop, oldScale] = prev; + if (newScale === oldScale) return; + scrollTo(oldScrollTop ? (oldScrollTop / oldHeight) * newHeight : 0); + }, ); }); diff --git a/src/components/Manga/actions/show.ts b/src/components/Manga/actions/show.ts index df00df97..1f4f7549 100644 --- a/src/components/Manga/actions/show.ts +++ b/src/components/Manga/actions/show.ts @@ -1,114 +1,107 @@ -import { createRoot, createEffect, on } from 'solid-js'; +import { createRoot } from 'solid-js'; +import { t } from 'helper/i18n'; +import { inRange } from 'helper'; import type { State } from '../store'; import { _setState, setState, store } from '../store'; -import { updateImgLoadType, activePage } from './image'; +import { updateImgLoadType } from './image'; import { resetUI } from './helper'; +import { activePage, renderRange, updateRenderRange } from './memo'; +import { createEffectOn } from '../helper'; -export const handleResize = (width: number, height: number) => { - if (!(width || height)) return; - setState((state) => { - state.memo.size = { width, height }; - state.isMobile = width < 800; - }); -}; - -/** 更新渲染页面相关变量 */ -export const updateRenderPage = (state: State, animation = false) => { - state.memo.renderPageList = state.pageList.slice( - Math.max(0, state.activePageIndex - 1), - Math.min(state.pageList.length, state.activePageIndex + 2), - ); - - const i = state.memo.renderPageList.indexOf( - state.pageList[state.activePageIndex], - ); +/** 将页面移回原位 */ +export const resetPage = (state: State, animation = false) => { + updateRenderRange(state); + if (state.option.scrollMode) { + state.page.anima = ''; + return; + } state.page.offset.x.pct = 0; state.page.offset.y.pct = 0; + let i = -1; + if (inRange(renderRange.start(), state.activePageIndex, renderRange.end())) + i = state.activePageIndex - renderRange.start(); if (store.page.vertical) state.page.offset.y.pct = i === -1 ? 0 : -i * 100; else state.page.offset.x.pct = i === -1 ? 0 : i * 100; state.page.anima = animation ? 'page' : ''; }; -const updateShowPageList = (state: State) => { - state.memo.showPageList = [ - ...new Set( - state.memo.showImgList.map( - (img) => +img.parentElement!.getAttribute('data-index')!, - ), - ), - ]; - state.memo.showPageList.sort(); +/** 获取指定图片的提示文本 */ +const getImgTip = (state: State, i: number) => { + if (i === -1) return t('other.fill_page'); + const img = state.imgList[i]; - if (state.option.scrollMode) - state.activePageIndex = state.memo.showPageList[0] ?? 0; -}; + // 如果图片未加载完毕则在其 index 后增加显示当前加载状态 + if (img.loadType !== 'loaded') + return `${i + 1} (${t(`img_status.${img.loadType}`)})`; -export const handleObserver: IntersectionObserverCallback = (entries) => { - setState((state) => { - entries.forEach(({ isIntersecting, target }) => { - if (isIntersecting) - state.memo.showImgList.push(target as HTMLImageElement); - else - state.memo.showImgList = state.memo.showImgList.filter( - (img) => img !== target, - ); - }); + if ( + img.translationType && + img.translationType !== 'hide' && + img.translationMessage + ) + return `${i + 1}:${img.translationMessage}`; - if (!store.gridMode) updateShowPageList(state); - }); + return `${i + 1}`; +}; + +/** 获取指定页面的提示文本 */ +export const getPageTip = (pageIndex: number): string => { + const page = store.pageList[pageIndex]; + if (!page) return 'null'; + const pageIndexText = page.map((index) => getImgTip(store, index)) as + | [string] + | [string, string]; + if (store.option.dir === 'rtl') pageIndexText.reverse(); + return pageIndexText.join(store.option.scrollMode ? '\n' : ' | '); }; createRoot(() => { // 页数发生变动时 - createEffect( - on( - () => store.activePageIndex, - () => { - setState((state) => { - updateImgLoadType(state); - if (state.show.endPage) state.show.endPage = undefined; - }); - }, - { defer: true }, - ), + createEffectOn( + () => store.activePageIndex, + () => { + setState((state) => { + updateImgLoadType(state); + if (state.show.endPage) state.show.endPage = undefined; + }); + }, + { defer: true }, ); - // 在关闭工具栏的同时关掉滚动条的强制显示 - createEffect( - on( - () => store.show.toolbar, - () => { - if (store.show.scrollbar && !store.show.toolbar) - _setState('show', 'scrollbar', false); - }, - { defer: true }, - ), + createEffectOn( + activePage, + (page) => { + if (!store.isDragMode) setState(resetPage); + // 如果当前显示页面有出错的图片,就重新加载一次 + page?.forEach((i) => { + if (store.imgList[i]?.loadType !== 'error') return; + _setState('imgList', i, 'loadType', 'wait'); + }); + }, + { defer: true }, ); - createEffect( - on( - activePage, - (page) => { - if (!store.option.scrollMode && !store.isDragMode) - setState(updateRenderPage); - // 如果当前显示页面有出错的图片,就重新加载一次 - page?.forEach((i) => { - if (store.imgList[i]?.loadType !== 'error') return; - _setState('imgList', i, 'loadType', 'wait'); - }); - }, - { defer: true }, - ), + // 在关闭工具栏的同时关掉滚动条的强制显示 + createEffectOn( + () => store.show.toolbar, + () => + store.show.scrollbar && + !store.show.toolbar && + _setState('show', 'scrollbar', false), + { defer: true }, ); // 在切换网格模式后关掉 滚动条和工具栏 的强制显示 - createEffect( - on( - () => store.gridMode, - () => setState(resetUI), - { defer: true }, - ), + createEffectOn( + () => store.gridMode, + () => setState(resetUI), + { defer: true }, + ); + + createEffectOn( + () => store.option.scrollModeImgScale, + () => setState(updateRenderRange), ); }); diff --git a/src/components/Manga/actions/switch.ts b/src/components/Manga/actions/switch.ts index 030c7192..cbf6d77d 100644 --- a/src/components/Manga/actions/switch.ts +++ b/src/components/Manga/actions/switch.ts @@ -1,8 +1,8 @@ import { refs, setState, store } from '../store'; -import { updateDrag } from './scrollbar'; import { zoom } from './zoom'; import { setOption } from './helper'; -import { nowFillIndex, updatePageData } from './image'; +import { updatePageData } from './image'; +import { nowFillIndex } from './memo'; /** 切换页面填充 */ export const switchFillEffect = () => { @@ -23,7 +23,6 @@ export const switchScrollMode = () => { draftOption.onePageMode = draftOption.scrollMode; updatePageData(state); }); - setState(updateDrag); // 切换到卷轴模式后自动定位到对应页 if (store.option.scrollMode) refs.mangaFlow.children[store.activePageIndex]?.scrollIntoView(); diff --git a/src/components/Manga/actions/translation/index.ts b/src/components/Manga/actions/translation/index.ts index da2d2368..dddceb51 100644 --- a/src/components/Manga/actions/translation/index.ts +++ b/src/components/Manga/actions/translation/index.ts @@ -1,10 +1,4 @@ -import { - createRoot, - createEffect, - on, - createSignal, - createMemo, -} from 'solid-js'; +import { createRoot, on, createSignal, createMemo } from 'solid-js'; import { lang, t } from 'helper/i18n'; import { singleThreaded } from 'helper'; import { store, setState, _setState } from '../../store'; @@ -12,6 +6,7 @@ import { createOptions, setMessage } from './helper'; import { getValidTranslators, selfhostedTranslation } from './selfhosted'; import { cotransTranslation, cotransTranslators } from './cotrans'; import { setOption } from '../helper'; +import { createEffectOn } from '../../helper'; // eslint-disable-next-line @typescript-eslint/consistent-type-imports declare const toast: typeof import('components/Toast/toast').toast | undefined; @@ -115,29 +110,27 @@ export const translatorOptions = createRoot(() => { ); // 在切换翻译服务器的同时切换可用翻译的选项列表 - createEffect( - on( - [ - () => store.option.translation.server, - () => store.option.translation.localUrl, - ], - async () => { - if (store.option.translation.server !== 'selfhosted') return; - - setSelfOptions((await getValidTranslators()) ?? []); - - // 如果切换服务器后原先选择的翻译服务失效了,就换成谷歌翻译 - if ( - !selfhostedOptions().some( - ([val]) => val === store.option.translation.options.translator, - ) - ) { - setOption((draftOption) => { - draftOption.translation.options.translator = 'google'; - }); - } - }, - ), + createEffectOn( + [ + () => store.option.translation.server, + () => store.option.translation.localUrl, + ], + async () => { + if (store.option.translation.server !== 'selfhosted') return; + + setSelfOptions((await getValidTranslators()) ?? []); + + // 如果切换服务器后原先选择的翻译服务失效了,就换成谷歌翻译 + if ( + !selfhostedOptions().some( + ([val]) => val === store.option.translation.options.translator, + ) + ) { + setOption((draftOption) => { + draftOption.translation.options.translator = 'google'; + }); + } + }, ); const options = createMemo( diff --git a/src/components/Manga/actions/turnPage.ts b/src/components/Manga/actions/turnPage.ts index 7ab16654..1786fc42 100644 --- a/src/components/Manga/actions/turnPage.ts +++ b/src/components/Manga/actions/turnPage.ts @@ -1,23 +1,22 @@ -import { debounce } from 'throttle-debounce'; - +import { debounce } from 'helper'; import type { State } from '../store'; import { store, setState, _setState } from '../store'; -import { updateRenderPage } from './show'; +import { contentHeight, rootSize, scrollTop } from './memo'; +import { resetPage } from './show'; /** 判断当前是否已经滚动到底部 */ const isBottom = (state: State) => state.option.scrollMode - ? store.scrollbar.dragHeight + store.scrollbar.dragTop >= 0.999 + ? scrollTop() + rootSize().height >= contentHeight() : state.activePageIndex === state.pageList.length - 1; /** 判断当前是否已经滚动到顶部 */ const isTop = (state: State) => - state.option.scrollMode - ? store.scrollbar.dragTop === 0 - : state.activePageIndex === 0; + state.option.scrollMode ? scrollTop() === 0 : state.activePageIndex === 0; -export const closeScrollLock = debounce(200, () => - _setState('flag', 'scrollLock', false), +export const closeScrollLock = debounce( + () => _setState('flag', 'scrollLock', false), + 200, ); /** 翻页。返回是否成功改变了当前页数 */ @@ -93,20 +92,20 @@ export const turnPageAnimation = (dir: 'next' | 'prev') => { if (!turnPageFn(state, dir)) { state.page.offset.x.px = 0; state.page.offset.y.px = 0; - updateRenderPage(state, true); + resetPage(state, true); state.isDragMode = false; return; } state.isDragMode = true; - updateRenderPage(state); + resetPage(state); if (store.page.vertical) state.page.offset.y.pct += dir === 'next' ? 100 : -100; else state.page.offset.x.pct += dir === 'next' ? -100 : 100; setTimeout(() => { setState((draftState) => { - updateRenderPage(draftState, true); + resetPage(draftState, true); draftState.page.offset.x.px = 0; draftState.page.offset.y.px = 0; draftState.isDragMode = false; diff --git a/src/components/Manga/actions/zoom.ts b/src/components/Manga/actions/zoom.ts index b0d7e1f1..b2f9fef0 100644 --- a/src/components/Manga/actions/zoom.ts +++ b/src/components/Manga/actions/zoom.ts @@ -1,10 +1,10 @@ import { createRoot, createMemo } from 'solid-js'; -import { clamp, isEqual } from 'helper'; -import { debounce } from 'throttle-debounce'; +import { clamp, approx } from 'helper'; import type { PointerState, UseDrag } from '../hooks/useDrag'; import type { State } from '../store'; -import { store, setState, refs, _setState } from '../store'; +import { store, setState, refs } from '../store'; import { resetUI } from './helper'; +import { closeScrollLock } from './turnPage'; export const touches = new Map(); @@ -25,10 +25,6 @@ const checkBound = (state: State) => { state.zoom.offset.y = clamp(bound.y(), state.zoom.offset.y, 0); }; -const closeScrollLock = debounce(200, () => - _setState('flag', 'scrollLock', false), -); - export const zoom = ( val: number, focal?: { x: number; y: number }, @@ -91,7 +87,7 @@ let lastTime: DOMHighResTimeStamp = 0; /** 逐帧计算惯性滑动 */ const handleSlideAnima = (timestamp: DOMHighResTimeStamp) => { // 当速率足够小时停止计算动画 - if (isEqual(velocity.x, 0, 1) && isEqual(velocity.y, 0, 1)) { + if (approx(velocity.x, 0, 1) && approx(velocity.y, 0, 1)) { animationId = null; return; } diff --git a/src/components/Manga/components/ComicImg.module.css b/src/components/Manga/components/ComicImg.module.css index 8740c79b..14ed4595 100644 --- a/src/components/Manga/components/ComicImg.module.css +++ b/src/components/Manga/components/ComicImg.module.css @@ -55,7 +55,7 @@ width: 100%; height: 100%; - background-color: var(--bg); + background-color: #eee; background-repeat: no-repeat; background-position: center; background-size: 30%; @@ -100,6 +100,21 @@ } } +.mangaBox { + width: 100%; + height: 100%; +} + +.root:not([data-grid-mode]) .mangaBox { + /* 隐藏滚动条但不影响滚动 */ + scrollbar-width: none; + + /* 隐藏滚动条但不影响滚动 */ + &::-webkit-scrollbar { + display: none; + } +} + .mangaFlow { touch-action: none; user-select: none; @@ -126,16 +141,6 @@ transition-duration: 0ms; - &:not([data-grid-mode]) { - /* 隐藏滚动条但不影响滚动 */ - scrollbar-width: none; - - /* 隐藏滚动条但不影响滚动 */ - &::-webkit-scrollbar { - display: none; - } - } - &[data-disable-zoom] .img { height: unset; max-height: 100%; @@ -164,7 +169,7 @@ } } -.mangaFlow[data-grid-mode] { +.root[data-grid-mode] .mangaFlow { transform: none; overflow: auto; @@ -205,47 +210,44 @@ } } -.root[data-scroll-mode] .mangaFlow { +.root[data-scroll-mode]:not([data-grid-mode]) .mangaBox { overflow: auto; - grid-auto-flow: row; - grid-auto-rows: auto; - /* 卷轴模式下的图片间距 */ - grid-row-gap: calc(var(--scroll-mode-spacing) * 0.1em); - grid-template-rows: auto; - - & .page { - transform: none; + & .mangaFlow { display: flex; - width: unset; + flex-direction: column; height: fit-content; - } - & .img { - display: unset; + & .page { + width: unset; + height: fit-content; - width: calc(var(--scroll-mode-img-scale) * min(100%, var(--width, 100%))); - max-width: unset; - height: auto; - max-height: unset; + &:not(:first-of-type) { + margin-top: calc(var(--scroll-mode-spacing) * 7px); + } + } - object-fit: contain; + & .img { + display: unset; - &[data-load-type="loading"] { - position: unset; - } + width: calc(var(--scroll-mode-img-scale) * min(100%, var(--width))); + max-width: unset; + height: auto; + max-height: unset; + + object-fit: contain; - &[data-load-type="error"] { - width: 30em; - height: 20em; + &[data-load-type="loading"] { + position: unset; + } } - } - &[data-grid-mode] .img { - width: fit-content; - max-width: 100%; - height: 100%; - max-height: 100%; + &[data-grid-mode] .img { + width: fit-content; + max-width: 100%; + height: 100%; + max-height: 100%; + } } } diff --git a/src/components/Manga/components/ComicImg.tsx b/src/components/Manga/components/ComicImg.tsx index 8d881561..f5e76e35 100644 --- a/src/components/Manga/components/ComicImg.tsx +++ b/src/components/Manga/components/ComicImg.tsx @@ -1,10 +1,15 @@ import type { Component, JSX } from 'solid-js'; -import { createMemo, onCleanup, onMount } from 'solid-js'; +import { createEffect, createMemo, onCleanup, onMount } from 'solid-js'; import { t } from 'helper/i18n'; import { log } from 'helper/logger'; import { setState, store } from '../store'; -import { placeholderSize, updateImgLoadType, updateImgSize } from '../actions'; +import { + placeholderSize, + showImgList, + updateImgLoadType, + updateImgSize, +} from '../actions'; import classes from '../index.module.css'; @@ -25,10 +30,6 @@ const handleImgLoaded = (i: number, e: HTMLImageElement) => { img.loadType = 'loaded'; updateImgLoadType(state); state.prop.Loading?.(state.imgList, img); - - // 火狐浏览器在图片进入视口前,即使已经加载完了也不会对图片进行解码 - // 所以需要手动调用 decode 提前解码,防止在翻页时闪烁 - e.decode(); }); }; @@ -54,17 +55,10 @@ const handleImgError = (i: number, e: HTMLImageElement) => { export const ComicImg: Component = (props) => { let ref: HTMLImageElement; - onMount(() => { - store.observer?.observe(ref); - - onCleanup(() => { - store.observer?.unobserve(ref); - setState((state) => { - state.memo.showImgList = state.memo.showImgList.filter( - (img) => img !== ref, - ); - }); - }); + onMount(() => store.observer?.observe(ref)); + onCleanup(() => { + store.observer?.unobserve(ref); + showImgList.delete(ref); }); const img = createMemo(() => store.imgList[props.index]); @@ -84,6 +78,13 @@ export const ComicImg: Component = (props) => { }; }); + createEffect(() => { + if (!src() || img().loadType !== 'loaded') return; + // 火狐浏览器在图片进入视口前,即使已经加载完了也不会对图片进行解码 + // 所以需要手动调用 decode 提前解码,防止在翻页时闪烁 + ref.decode(); + }); + return ( { }; onMount(() => { - useDrag({ ref: refs.mangaFlow, handleDrag, handleClick, touches }); - setState((state) => { - state.observer = new IntersectionObserver(handleObserver, { - root: refs.mangaFlow, - threshold: 0.01, - }); - }); - onCleanup(() => { - setState((state) => { - state.observer?.disconnect(); - state.observer = null; - }); - }); + useDrag({ ref: refs.mangaBox, handleDrag, handleClick, touches }); + bindScrollTop(refs.mangaBox); + initIntersectionObserver(refs.mangaBox); }); const handleTransitionEnd = () => { if (store.isDragMode) return; setState((state) => { - if (store.zoom.scale === 100) updateRenderPage(state, true); + if (store.zoom.scale === 100) resetPage(state, true); else state.page.anima = ''; }); }; - const pageXY = createMemo(() => { - const x = `calc(${store.page.offset.x.pct}% + ${store.page.offset.x.px}px)`; - return { - '--page-x': store.option.dir === 'rtl' ? x : `calc(${x} * -1)`, - '--page-y': `calc(${store.page.offset.y.pct}% + ${store.page.offset.y.px}px)`, - }; - }); + /** 卷轴模式下当前显示页之前未渲染页的总高度 */ + const scrollModeFill = createMemo( + () => imgTopList()[renderRange.start()] ?? 0, + ); - const zoom = createMemo(() => ({ - '--scale': store.zoom.scale / 100, - '--zoom-x': `${store.zoom.offset.x || 0}px`, - '--zoom-y': `${store.zoom.offset.y || 0}px`, - })); + /** 在当前页之前有图片被加载出来,导致内容高度发生变化后,重新滚动页面,确保当前显示位置不变 */ + createEffectOn([scrollModeFill, imgTopList], ([height, topList], prev) => { + if (!prev || !height) return; + const [prevHeight, prevTopList] = prev; + if (prevTopList === topList || prevHeight === height) return; + scrollTo(scrollTop() + height - prevHeight); + // 目前还是会有轻微偏移,但考虑到大部分情况下都是顺序阅读,本身出现概率就低,就不继续排查优化了 + }); - const touchAction = createMemo(() => { - if (store.gridMode) return 'auto'; - if (store.zoom.scale !== 100) { - if (store.option.scrollMode) { + const style = createMemoMap({ + '--scale': () => store.zoom.scale / 100, + '--zoom-x': () => `${store.zoom.offset.x || 0}px`, + '--zoom-y': () => `${store.zoom.offset.y || 0}px`, + '--page-x': () => { + if (store.option.scrollMode) return '0px'; + const x = `calc(${store.page.offset.x.pct}% + ${store.page.offset.x.px}px)`; + return store.option.dir === 'rtl' ? x : `calc(${x} * -1)`; + }, + '--page-y': () => + `calc(${store.page.offset.y.pct}% + ${store.page.offset.y.px}px)`, + 'touch-action': () => { + if (store.gridMode) return 'auto'; + if (store.zoom.scale !== 100) { + if (!store.option.scrollMode) return 'none'; if (store.zoom.offset.y === 0) return 'pan-up'; if (store.zoom.offset.y === bound.y()) return 'pan-down'; } - return 'none'; - } - if (store.option.scrollMode) return 'pan-y'; + if (store.option.scrollMode) return 'pan-y'; + }, + height: () => + !store.gridMode && store.option.scrollMode + ? `${contentHeight()}px` + : undefined, }); return (
setState(updateDrag)} - style={{ 'touch-action': touchAction(), ...zoom(), ...pageXY() }} - tabIndex={-1} + ref={bindRef('mangaBox')} + class={`${classes.mangaBox} ${classes.beautifyScrollbar}`} > - NULL}> - {(page, i) => } - +
+ + + + NULL}> + {(page, i) => } + +
); }; diff --git a/src/components/Manga/components/ComicPage.tsx b/src/components/Manga/components/ComicPage.tsx index 5bb23abb..68ab7146 100644 --- a/src/components/Manga/components/ComicPage.tsx +++ b/src/components/Manga/components/ComicPage.tsx @@ -1,7 +1,7 @@ -import { For, type Component, createMemo } from 'solid-js'; -import { boolDataVal } from '../../../helper'; +import { type Component, createMemo, Index } from 'solid-js'; +import { boolDataVal, inRange } from 'helper'; import { store } from '../store'; -import { getPageTip } from '../actions'; +import { getPageTip, renderRange } from '../actions'; import type { ComicImgProps } from './ComicImg'; import { ComicImg } from './ComicImg'; @@ -16,8 +16,7 @@ export const ComicPage: Component = (props) => { const show = createMemo( () => store.gridMode || - store.option.scrollMode || - store.memo.renderPageList.some((page) => page === props.page), + inRange(renderRange.start(), props.index, renderRange.end()), ); const fill = createMemo(() => { @@ -48,9 +47,9 @@ export const ComicPage: Component = (props) => { data-index={props.index} style={style()} > - NULL}> - {(imgIndex, i) => } - + NULL}> + {(imgIndex, i) => } + ); }; diff --git a/src/components/Manga/components/CssVar.tsx b/src/components/Manga/components/CssVar.tsx index 2f05d7b9..491ab8fd 100644 --- a/src/components/Manga/components/CssVar.tsx +++ b/src/components/Manga/components/CssVar.tsx @@ -10,7 +10,7 @@ const dark = ` --switch: #BDBDBD; --switch-bg: #6E6E6E; ---scrollbar-drag: #FFF6; +--scrollbar-slider: #FFF6; --page-bg: #303030; @@ -31,7 +31,7 @@ const light = ` --switch: #FAFAFA; --switch-bg: #9C9C9C; ---scrollbar-drag: #0006; +--scrollbar-slider: #0006; --page-bg: white; diff --git a/src/components/Manga/components/Scrollbar.module.css b/src/components/Manga/components/Scrollbar.module.css index 71b1a230..66122f15 100644 --- a/src/components/Manga/components/Scrollbar.module.css +++ b/src/components/Manga/components/Scrollbar.module.css @@ -1,7 +1,7 @@ .scrollbar { --arrow-y: clamp( 0.45em, - calc(var(--drag-midpoint)), + calc(var(--slider-midpoint)), calc(var(--scroll-length) - 0.45em) ); @@ -82,22 +82,19 @@ } } -/* 滚动条块 */ -.scrollbarDrag { - --top: calc(var(--top-ratio) * var(--scroll-length)); - --height: calc(var(--height-ratio) * var(--scroll-length)); - +/* 滚动条滑块 */ +.scrollbarSlider { position: absolute; z-index: 1; - transform: translateY(var(--top)); + transform: translateY(var(--slider-top)); justify-content: center; width: 100%; - height: var(--height); + height: var(--slider-height); opacity: 1; - background-color: var(--scrollbar-drag); + background-color: var(--scrollbar-slider); border-radius: 1em; transition: @@ -109,7 +106,7 @@ .scrollbarPoper { --poper-top: clamp( 0%, - calc(var(--drag-midpoint) - 50%), + calc(var(--slider-midpoint) - 50%), calc(var(--scroll-length) - 100%) ); @@ -118,6 +115,8 @@ transform: translateY(var(--poper-top)); width: fit-content; + min-width: 1em; + min-height: 1.5em; padding: 0.2em 0.5em; font-size: 0.8em; @@ -160,7 +159,7 @@ .scrollbar:hover, .scrollbar[data-force-show] { & .scrollbarPoper, - & .scrollbarDrag, + & .scrollbarSlider, &::before { opacity: 1; } @@ -168,11 +167,11 @@ /* 实现自动隐藏 */ .scrollbar[data-auto-hidden]:not([data-force-show]) { - & .scrollbarDrag { + & .scrollbarSlider { opacity: 0; } - &:hover .scrollbarDrag { + &:hover .scrollbarSlider { opacity: 1; } } @@ -240,9 +239,9 @@ } /* stylelint-disable-next-line no-descending-specificity */ - & .scrollbarDrag { - transform: translateX(calc(var(--top) * -1)); - width: var(--height); + & .scrollbarSlider { + transform: translateX(calc(var(--slider-top) * -1)); + width: var(--slider-height); height: 100%; } @@ -263,7 +262,7 @@ } /* stylelint-disable-next-line no-descending-specificity */ - & .scrollbarDrag { + & .scrollbarSlider { transform: translateX(var(--top)); } @@ -288,7 +287,7 @@ /* 卷轴模式下取消滚动条的位移动画 */ .root[data-scroll-mode] .scrollbar::before, -.root[data-scroll-mode] :is(.scrollbarDrag, .scrollbarPoper) { +.root[data-scroll-mode] :is(.scrollbarSlider, .scrollbarPoper) { transition: opacity 150ms; } diff --git a/src/components/Manga/components/Scrollbar.tsx b/src/components/Manga/components/Scrollbar.tsx index 71f20f54..eb82e1c6 100644 --- a/src/components/Manga/components/Scrollbar.tsx +++ b/src/components/Manga/components/Scrollbar.tsx @@ -1,54 +1,39 @@ -import type { Component } from 'solid-js'; -import { createSignal, createMemo, Show, For, onMount } from 'solid-js'; -import { debounce } from 'throttle-debounce'; - +import type { Component, JSX } from 'solid-js'; +import { createSignal, createMemo, Show, onMount } from 'solid-js'; +import { boolDataVal, debounce } from 'helper'; +import { createMemoMap, createThrottleMemo } from '../helper'; import { refs, store } from '../store'; import { useDrag } from '../hooks/useDrag'; import { bindRef, getPageTip, scrollPosition, - handleScrollbarDrag, + handlescrollbarSlider, + sliderMidpoint, + sliderHeight, + sliderTop, + showPageList, + scrollLength, } from '../actions'; -import { ScrollbarPage } from './ScrollbarPage'; +import { ScrollbarPageStatus } from './ScrollbarPageStatus'; import classes from '../index.module.css'; -import { boolDataVal } from '../../../helper'; /** 滚动条 */ export const Scrollbar: Component = () => { onMount(() => { useDrag({ ref: refs.scrollbar, - handleDrag: handleScrollbarDrag, + handleDrag: handlescrollbarSlider, easyMode: () => store.option.scrollMode && store.option.scrollbar.easyScroll, }); }); - /** 滚动条高度 */ - const height = createMemo(() => - store.option.scrollMode - ? store.scrollbar.dragHeight - : 1 / store.pageList.length, - ); - - /** 滚动条位置高度 */ - const top = createMemo(() => - store.option.scrollMode - ? store.scrollbar.dragTop - : (1 / store.pageList.length) * store.activePageIndex, - ); - - /** 滚动条滑块的中心点高度 */ - const dragMidpoint = createMemo( - () => store.memo.scrollLength * (top() + height() / 2), - ); - // 在被滚动时使自身可穿透,以便在卷轴模式下触发页面的滚动 const [penetrate, setPenetrate] = createSignal(false); - const resetPenetrate = debounce(100, () => setPenetrate(false)); + const resetPenetrate = debounce(() => setPenetrate(false)); const handleWheel = () => { setPenetrate(true); resetPenetrate(); @@ -57,30 +42,36 @@ export const Scrollbar: Component = () => { /** 是否强制显示滚动条 */ const showScrollbar = createMemo(() => store.show.scrollbar || !!penetrate()); - const showTip = createMemo(() => { - if (store.memo.showPageList.length === 0) return 'null'; - if (store.memo.showPageList.length === 1) - return getPageTip(store.memo.showPageList[0]); - - const tipList = store.memo.showPageList.map((i) => getPageTip(i)); + /** 滚动条提示文本 */ + const tipText = createThrottleMemo(() => { + switch (showPageList().length) { + case 0: + return ''; + case 1: + return getPageTip(showPageList()[0]); + } + const tipList = showPageList().map((i) => getPageTip(i)); if (store.option.scrollMode || store.page.vertical) return tipList.join('\n'); - if (store.option.dir === 'rtl') tipList.reverse(); return tipList.join(' '); }); + const style = createMemoMap({ + 'pointer-events': () => + penetrate() || store.isDragMode || store.gridMode ? 'none' : 'auto', + '--scroll-length': () => `${scrollLength()}px`, + '--slider-midpoint': () => `${sliderMidpoint()}px`, + '--slider-height': () => `${sliderHeight() * scrollLength()}px`, + '--slider-top': () => `${sliderTop() * scrollLength()}px`, + }); + return (
{ onWheel={handleWheel} >
-
+
- - {([a, b]) => } - +
); diff --git a/src/components/Manga/components/ScrollbarPage.tsx b/src/components/Manga/components/ScrollbarPage.tsx deleted file mode 100644 index a3e83700..00000000 --- a/src/components/Manga/components/ScrollbarPage.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import type { Component } from 'solid-js'; -import { createMemo } from 'solid-js'; - -import { store } from '../store'; -import { contentHeight, placeholderSize } from '../actions'; -import { boolDataVal } from '../../../helper'; - -import classes from '../index.module.css'; - -/** 显示对应图片加载情况的元素 */ -const ScrollbarImg: Component<{ index: number }> = (props) => { - const img = createMemo(() => store.imgList[props.index]); - - return ( -
- ); -}; - -/** 滚动条上用于显示对应页面下图片加载情况的元素 */ -export const ScrollbarPage: Component<{ a: number; b?: number }> = (props) => { - const flexBasis = createMemo(() => { - if (!store.option.scrollMode) return undefined; - return `${ - ((store.imgList[props.a]?.height || placeholderSize().height) / - contentHeight()) * - store.option.scrollModeImgScale - }%`; - }); - - return ( -
- - {props.b ? ( - - ) : null} -
- ); -}; diff --git a/src/components/Manga/components/ScrollbarPageStatus.tsx b/src/components/Manga/components/ScrollbarPageStatus.tsx new file mode 100644 index 00000000..003db03a --- /dev/null +++ b/src/components/Manga/components/ScrollbarPageStatus.tsx @@ -0,0 +1,102 @@ +import type { Component } from 'solid-js'; +import { For, createMemo } from 'solid-js'; + +import { boolDataVal } from 'helper'; +import { store } from '../store'; +import { contentHeight, imgHeightList } from '../actions'; + +import classes from '../index.module.css'; +import { createThrottleMemo } from '../helper'; + +interface ScrollbarPageItem { + /** 图片数量 */ + num: number; + /** 图片长度之和 */ + length: number; + loadType: ComicImg['loadType']; + isNull: boolean; + translationType: ComicImg['translationType']; +} + +const getScrollbarPage = (img: ComicImg, i: number): ScrollbarPageItem => ({ + num: 1, + length: imgHeightList()[i], + loadType: img.loadType, + isNull: !img.src, + translationType: img.translationType, +}); + +const ScrollbarPage: Component = (props) => { + const flexBasis = createMemo(() => + store.option.scrollMode + ? props.length / contentHeight() + : props.num / store.imgList.length, + ); + + return ( +
+ ); +}; + +/** 显示对应图片加载情况的元素 */ +export const ScrollbarPageStatus = () => { + // 将相同类型的页面合并显示 + const scrollbarPageList = createThrottleMemo(() => { + if (!store.pageList.length) return []; + + const list: ScrollbarPageItem[] = []; + let item: ScrollbarPageItem | undefined; + + const handleImg = (i: number) => { + const img = store.imgList[i]; + + if (!item) { + item = getScrollbarPage(img, i); + return; + } + + if ( + img.loadType === item.loadType && + !img.src === item.isNull && + img.translationType === item.translationType + ) { + item.num += 1; + item.length += imgHeightList()[i]; + } else { + list.push(item); + item = getScrollbarPage(img, i); + } + }; + + for (let i = 0; i < store.pageList.length; i++) { + const [a, b] = store.pageList[i]; + if (b === undefined) handleImg(a); + else if (a === -1) { + handleImg(b); + handleImg(b); + } else if (b === -1) { + handleImg(a); + handleImg(a); + } else { + handleImg(a); + handleImg(b); + } + } + + if (item) list.push(item); + + return list; + }, 100); + + return ( + + {(page) => } + + ); +}; diff --git a/src/components/Manga/components/Toolbar.tsx b/src/components/Manga/components/Toolbar.tsx index 35d30725..c0fdd4bb 100644 --- a/src/components/Manga/components/Toolbar.tsx +++ b/src/components/Manga/components/Toolbar.tsx @@ -24,10 +24,9 @@ export const Toolbar: Component = () => { >
- } - /> + + {(ButtonItem) => } +
); diff --git a/src/components/Manga/defaultButtonList.tsx b/src/components/Manga/defaultButtonList.tsx index f748446e..5ef446f6 100644 --- a/src/components/Manga/defaultButtonList.tsx +++ b/src/components/Manga/defaultButtonList.tsx @@ -64,7 +64,9 @@ export const defaultButtonList: ToolbarButtonList = [