Skip to content

Commit a414a90

Browse files
committed
fix: 🐛 调整滚动和翻页快捷键在卷轴模式下的行为,使之分别更接近方向键和 PageDown/PageUp
#241
1 parent 0b9d057 commit a414a90

5 files changed

Lines changed: 152 additions & 54 deletions

File tree

src/components/Manga/actions/hotkeys.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,12 @@ import { store } from '../store';
66
export const [defaultHotkeys, setDefaultHotkeys] = createSignal<
77
Record<string, string[]>
88
>({
9-
scroll_up: ['w', 'Shift + w', 'ArrowUp'],
10-
scroll_down: ['s', 'Shift + s', 'ArrowDown', ' '],
9+
scroll_up: ['w', 'ArrowUp'],
10+
scroll_down: ['s', 'ArrowDown', ' '],
1111
scroll_left: ['a', 'Shift + a', ',', 'ArrowLeft'],
1212
scroll_right: ['d', 'Shift + d', '.', 'ArrowRight'],
13-
page_up: ['PageUp'],
14-
page_down: [' ', 'PageDown'],
13+
page_up: ['PageUp', 'Shift + w'],
14+
page_down: [' ', 'PageDown', 'Shift + s'],
1515
jump_to_home: ['Home'],
1616
jump_to_end: ['End'],
1717
exit: ['Escape'],

src/components/Manga/actions/operate.ts

Lines changed: 63 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { hotkeysMap } from './hotkeys';
2323
import { zoom } from './zoom';
2424
import { closeScrollLock, turnPage } from './turnPage';
2525
import {
26+
constantScroll,
2627
scrollLength,
2728
scrollProgress,
2829
scrollTo,
@@ -47,12 +48,9 @@ export const handleMouseDown: EventHandler['on:mousedown'] = (e) => {
4748
};
4849

4950
/** 卷轴模式下的页面滚动 */
50-
const scrollModeScrollPage = (dir: 'next' | 'prev') => {
51+
const scrollModeScrollPage = (x: number) => {
5152
if (!store.show.endPage) {
52-
scrollTo(
53-
scrollTop() + store.rootSize.height * 0.8 * (dir === 'next' ? 1 : -1),
54-
true,
55-
);
53+
scrollTo(scrollTop() + x, true);
5654
_setState('scrollLock', true);
5755
}
5856
closeScrollLock();
@@ -64,6 +62,33 @@ const handleSwapPageTurnKey = (nextPage: boolean) => {
6462
return next ? 'next' : 'prev';
6563
};
6664

65+
/** 处理快捷键长按的情况 */
66+
export const handleHoldKey = new (class {
67+
holdKeys = new Map<string, () => void>();
68+
69+
linsten(code: string, holdFn: () => void, upFn: () => void) {
70+
if (this.holdKeys.has(code)) return;
71+
holdFn();
72+
this.holdKeys.set(code, upFn);
73+
}
74+
75+
onKeyUp = (e: KeyboardEvent) => {
76+
const code = getKeyboardCode(e);
77+
if (!this.holdKeys.has(code)) return;
78+
this.holdKeys.get(code)!();
79+
this.holdKeys.delete(code);
80+
};
81+
})();
82+
83+
/** 处理长按滚动 */
84+
const handleHoldScroll = (code: string, speed: number) => {
85+
handleHoldKey.linsten(
86+
code,
87+
() => constantScroll.start(speed),
88+
() => constantScroll.cancel(),
89+
);
90+
};
91+
6792
export const handleKeyDown = (e: KeyboardEvent) => {
6893
switch ((e.target as HTMLElement).tagName) {
6994
case 'INPUT':
@@ -128,29 +153,31 @@ export const handleKeyDown = (e: KeyboardEvent) => {
128153
e.preventDefault();
129154
} else return;
130155

156+
const hotkey = hotkeysMap()[code];
157+
131158
// 并排卷轴模式下的快捷键
132159
if (isAbreastMode()) {
133-
switch (hotkeysMap()[code]) {
160+
switch (hotkey) {
134161
case 'scroll_up':
135-
setAbreastScrollFill(
136-
abreastScrollFill() - store.rootSize.height * 0.02,
137-
);
162+
setAbreastScrollFill(abreastScrollFill() - 20);
138163
return;
139164
case 'scroll_down':
140-
setAbreastScrollFill(
141-
abreastScrollFill() + store.rootSize.height * 0.02,
142-
);
165+
setAbreastScrollFill(abreastScrollFill() + 20);
143166
return;
144167

145168
case 'scroll_left':
146-
return scrollTo(scrollProgress() + abreastColumnWidth());
169+
return scrollTo(
170+
scrollProgress() - (store.option.dir === 'rtl' ? 20 : -20),
171+
);
147172
case 'scroll_right':
148-
return scrollTo(scrollProgress() - abreastColumnWidth());
173+
return scrollTo(
174+
scrollProgress() + (store.option.dir === 'rtl' ? 20 : -20),
175+
);
149176

150177
case 'page_up':
151-
return scrollTo(scrollProgress() - store.rootSize.width * 0.8);
178+
return scrollTo(scrollProgress() - abreastColumnWidth());
152179
case 'page_down':
153-
return scrollTo(scrollProgress() + store.rootSize.width * 0.8);
180+
return scrollTo(scrollProgress() + abreastColumnWidth());
154181

155182
case 'jump_to_home':
156183
return scrollTo(0);
@@ -159,18 +186,31 @@ export const handleKeyDown = (e: KeyboardEvent) => {
159186
}
160187
}
161188

162-
switch (hotkeysMap()[code]) {
189+
// 普通卷轴模式下的快捷键
190+
if (isScrollMode()) {
191+
switch (hotkey) {
192+
case 'page_up':
193+
return scrollModeScrollPage(-store.rootSize.height * 0.8);
194+
case 'page_down':
195+
return scrollModeScrollPage(store.rootSize.height * 0.8);
196+
197+
case 'scroll_up':
198+
if (e.repeat) return handleHoldScroll(code, -1);
199+
return scrollModeScrollPage(-20);
200+
case 'scroll_down':
201+
if (e.repeat) return handleHoldScroll(code, 1);
202+
return scrollModeScrollPage(20);
203+
}
204+
}
205+
206+
switch (hotkey) {
163207
case 'page_up':
164-
case 'scroll_up': {
165-
if (isScrollMode()) scrollModeScrollPage('prev');
208+
case 'scroll_up':
166209
return turnPage('prev');
167-
}
168210

169211
case 'page_down':
170-
case 'scroll_down': {
171-
if (isScrollMode()) scrollModeScrollPage('next');
212+
case 'scroll_down':
172213
return turnPage('next');
173-
}
174214

175215
case 'scroll_left':
176216
return turnPage(handleSwapPageTurnKey(store.option.dir === 'rtl'));

src/components/Manga/actions/scroll.ts

Lines changed: 67 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { clamp, createRootMemo } from 'helper';
1+
import { AnimationFrame, clamp, createRootMemo } from 'helper';
22

33
import { _setState, refs, setState, store } from '../store';
44

@@ -51,47 +51,87 @@ export const isBottom = createRootMemo(
5151
/** 当前是否已经滚动到顶部 */
5252
export const isTop = createRootMemo(() => scrollPercentage() === 0);
5353

54-
// 动画时长
55-
const duration = 100;
56-
let animationId = 0;
57-
const cancelAnimation = () => {
58-
if (!animationId) return;
59-
cancelAnimationFrame(animationId);
60-
animationId = 0;
61-
};
6254
const _scrollTo = (x: number) =>
6355
refs.mangaBox.scrollTo({ top: x, behavior: 'instant' });
6456

65-
let startTime = 0;
66-
let startTop = 0;
67-
let distance = 0;
6857
/** 实现卷轴模式下的平滑滚动 */
69-
const scrollStep = (timestamp: DOMHighResTimeStamp) => {
70-
if (animationId) cancelAnimation();
71-
startTime ||= timestamp;
72-
const elapsed = timestamp - startTime;
73-
if (elapsed >= duration) return _scrollTo(startTop + distance);
74-
_scrollTo(startTop + distance * Math.min(elapsed / duration, 1));
75-
animationId = requestAnimationFrame(scrollStep);
76-
};
58+
const scrollStep = new (class extends AnimationFrame {
59+
/** 动画时长 */
60+
duration = 100;
61+
/** 要滚动的距离 */
62+
distance = 0;
63+
/** 滚动开始时间 */
64+
startTime = 0;
65+
/** 滚动开始位置 */
66+
startTop = 0;
67+
68+
frame = (timestamp: number) => {
69+
this.cancel();
70+
this.startTime ||= timestamp;
71+
/** 已滚动时间 */
72+
const elapsed = timestamp - this.startTime;
73+
if (elapsed >= this.duration)
74+
return _scrollTo(this.startTop + this.distance);
75+
_scrollTo(this.startTop + (elapsed / this.duration) * this.distance);
76+
this.call();
77+
};
78+
79+
start = (x: number) => {
80+
this.startTime = 0;
81+
this.startTop = scrollTop();
82+
this.distance = x - this.startTop;
83+
this.frame(0);
84+
};
85+
})();
86+
87+
/** 实现卷轴模式下的匀速滚动 */
88+
export const constantScroll = new (class extends AnimationFrame {
89+
speed = 0;
90+
lastTime = 0;
91+
92+
frame = (timestamp: DOMHighResTimeStamp) => {
93+
if (!this.animationId) return;
94+
95+
if (this.lastTime) {
96+
const scrollDelta = this.speed * (timestamp - this.lastTime);
97+
_scrollTo(scrollTop() + scrollDelta);
98+
}
99+
this.lastTime = timestamp;
100+
this.call();
101+
};
102+
103+
start = (speed: number) => {
104+
if (this.animationId && speed === this.speed) return;
105+
106+
this.cancel();
107+
this.speed = speed;
108+
this.lastTime = 0;
109+
this.call();
110+
};
111+
})();
112+
77113
/** 在卷轴模式下滚动到指定进度 */
78114
export const scrollTo = (x: number, smooth = false) => {
79115
if (!store.option.scrollMode.enabled) return;
116+
80117
if (store.option.scrollMode.abreastMode) {
81118
_scrollTo(0);
82119
const val = clamp(0, x, abreastScrollWidth());
83120
return _setState('page', 'offset', 'x', 'px', val);
84121
}
85-
if (animationId || !smooth) {
86-
cancelAnimation();
122+
123+
if (!smooth) {
124+
scrollStep.cancel();
87125
_scrollTo(x);
126+
return;
88127
}
89-
if (smooth) {
90-
startTime = 0;
91-
startTop = refs.mangaBox.scrollTop;
92-
distance = x - startTop;
93-
scrollStep(0);
128+
129+
if (scrollStep.animationId) {
130+
scrollStep.cancel();
131+
_scrollTo(x);
94132
}
133+
134+
scrollStep.start(x);
95135
};
96136

97137
/** 保存当前滚动进度,并在之后恢复 */

src/components/Manga/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { type Option } from './store/option';
2121
import {
2222
bindRef,
2323
focus,
24+
handleHoldKey,
2425
handleKeyDown,
2526
handleMouseDown,
2627
handleWheel,
@@ -79,6 +80,7 @@ export const Manga: Component<MangaProps> = (props) => {
7980
on:mousedown={handleMouseDown}
8081
on:wheel={handleWheel}
8182
oncapture:keydown={handleKeyDown}
83+
oncapture:keyup={handleHoldKey.onKeyUp}
8284
data-mobile={boolDataVal(store.isMobile)}
8385
data-scroll-mode={boolDataVal(store.option.scrollMode.enabled)}
8486
data-grid-mode={boolDataVal(store.gridMode)}

src/helper/other.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -590,3 +590,19 @@ export const waitUrlChange = async (isValidUrl: () => unknown) => {
590590
});
591591
});
592592
};
593+
594+
// TODO: 用这个重构相关实现
595+
export abstract class AnimationFrame {
596+
animationId = 0;
597+
abstract frame: (timestamp: DOMHighResTimeStamp) => unknown;
598+
599+
call = () => {
600+
this.animationId = requestAnimationFrame(this.frame);
601+
};
602+
603+
cancel = () => {
604+
if (!this.animationId) return;
605+
cancelAnimationFrame(this.animationId);
606+
this.animationId = 0;
607+
};
608+
}

0 commit comments

Comments
 (0)