From dd100f9e3424dd1b4dae874bec9b3b8f362381b5 Mon Sep 17 00:00:00 2001 From: Punnawit9285 Date: Sun, 30 Aug 2026 22:36:39 +0700 Subject: [PATCH 1/4] fix: require a deliberate drag to seek on touch devices video.js seeks as soon as a touch lands on the progress control, so a fingertip brushing the seek bar jumps the video, and that position is then saved as the play record. Wrap the SeekBar pointer handlers so a touch has to travel 8px before video.js is told the drag began: a touch that lands and lifts without moving no longer seeks or pauses. Mouse and keyboard interaction goes straight to the original handlers, so click-to-seek and the hotkey seek steps are unchanged. Closes #64 --- src/app/home/course/course.page.ts | 76 ++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/src/app/home/course/course.page.ts b/src/app/home/course/course.page.ts index f30584f..7564e21 100644 --- a/src/app/home/course/course.page.ts +++ b/src/app/home/course/course.page.ts @@ -41,6 +41,13 @@ import { AsyncPipe, DatePipe, DecimalPipe, NgClass } from '@angular/common'; import { ModalEvaluationComponent } from './modal-evaluation.component'; import { PomodoroTimerComponent } from '../../shared/pomodoro-timer.component'; +// The video.js SeekBar handlers wrapped by CoursePage.requireDragToSeekOnTouch() +interface SeekBarPointerHandlers { + handleMouseDown(event: Event): void; + handleMouseMove(event: Event, mouseDown?: boolean): void; + handleMouseUp(event: Event): void; +} + @Component({ selector: 'app-course', templateUrl: './course.page.html', @@ -194,6 +201,7 @@ export class CoursePage implements OnInit, AfterViewInit, OnDestroy { enableModifiersForNumbers: false, enableVolumeScroll: false, }); + this.requireDragToSeekOnTouch(); this.videoPlayer.on('ended', () => { this.updatePlayRecord(); if (!this.currentVideo.is_evaluated && !this.isEvaluated) { @@ -286,6 +294,74 @@ export class CoursePage implements OnInit, AfterViewInit, OnDestroy { this.stopPolling$.next(true); } + // video.js seeks as soon as a touch lands on the progress control, so a fingertip brushing + // the bar jumps the video and that position is saved as the play record (issue #64). + // Require the touch to move first. Mouse and keyboard handling is unchanged. + private requireDragToSeekOnTouch() { + const seekBar = this.videoPlayer.getChild('controlBar') + ?.getChild('progressControl') + ?.getChild('seekBar') as unknown as SeekBarPointerHandlers | undefined; + if (!seekBar) { + return; + } + + const dragThresholdPx = 8; + const originalMouseDown = seekBar.handleMouseDown.bind(seekBar); + const originalMouseMove = seekBar.handleMouseMove.bind(seekBar); + const originalMouseUp = seekBar.handleMouseUp.bind(seekBar); + const isTouch = (event: Event) => !!event && event.type.startsWith('touch'); + const touchPoint = (event: Event) => { + const touchEvent = event as TouchEvent; + return touchEvent.changedTouches?.[0] ?? touchEvent.touches?.[0] ?? null; + }; + + let touchOrigin: { x: number, y: number } | null = null; + let isDragging = false; + + seekBar.handleMouseDown = (event: Event) => { + if (!isTouch(event)) { + originalMouseDown(event); + return; + } + // Record the landing point only; the original handler pauses and seeks immediately. + // Do not stop propagation: ProgressControl attaches the document-level touchmove and + // touchend listeners that drive the handlers below. + const point = touchPoint(event); + touchOrigin = point ? { x: point.clientX, y: point.clientY } : null; + isDragging = false; + }; + + seekBar.handleMouseMove = (event: Event, mouseDown = false) => { + if (!isTouch(event) || isDragging) { + originalMouseMove(event, mouseDown); + return; + } + const origin = touchOrigin; + const point = touchPoint(event); + if (!origin || !point) { + return; + } + if (Math.abs(point.clientX - origin.x) < dragThresholdPx + && Math.abs(point.clientY - origin.y) < dragThresholdPx) { + return; // Within the slop of a stationary touch + } + // Hand over to video.js as if the drag started here. Set the flag first, + // since originalMouseDown calls back into handleMouseMove. + isDragging = true; + originalMouseDown(event); + }; + + seekBar.handleMouseUp = (event: Event) => { + const wasStrayTouch = isTouch(event) && !isDragging; + touchOrigin = null; + isDragging = false; + if (wasStrayTouch) { + return; // Nothing was started, so there is no scrub state to tear down + } + originalMouseUp(event); + }; + } + mergeVideoInfo(videos: CourseMembers, history: PlayHistory, evaluations: { [key: number]: EvaluationRecord }) { const progress = { viewed: 0, From 097a13b0ee88bbd95f75eec3718aafa6c2c49e26 Mon Sep 17 00:00:00 2001 From: Punnawit9285 Date: Sun, 30 Aug 2026 22:36:47 +0700 Subject: [PATCH 2/4] fix: make video player controls easier to hit on touch devices The stock control bar is 3em tall, making every control 30px high, short of the 44px a fingertip needs. Raise the bar to 4.4em on touch devices and restate the line-heights video.js uses to centre icons, the time display and the playback rate, which are all worked out against the 3em bar. Also hold the play/pause toggle in from the player's bottom-left corner, where it sits flush against the browser's own edge gesture area. Pointer devices keep the compact bar; video.js only adds .vjs-touch-enabled when touch is available. --- src/global.scss | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/global.scss b/src/global.scss index 17b3d0f..9e0a777 100644 --- a/src/global.scss +++ b/src/global.scss @@ -74,3 +74,29 @@ opacity: 1; transform: translate(-50%, 0); } + +/* Keep the play/pause toggle off the player's bottom-left corner, an awkward target on a tablet. */ +.video-js .vjs-control-bar .vjs-play-control { + margin-left: 1.5em; +} + +/* The stock 3em bar makes every control 30px tall, short of the 44px a fingertip needs. + video.js adds .vjs-touch-enabled only on touch devices, so pointer devices keep the compact bar. */ +.video-js.vjs-touch-enabled .vjs-control-bar { + height: 4.4em; +} + +/* video.js centres these with line-heights worked out against the 3em bar, so each has to be + restated for 4.4em. The progress control centres itself with flexbox and needs nothing. */ +.video-js.vjs-touch-enabled .vjs-control-bar .vjs-button > .vjs-icon-placeholder:before { + line-height: 2.4444; /* 1.8em glyph x 2.4444 */ +} + +.video-js.vjs-touch-enabled .vjs-control-bar .vjs-time-control, +.video-js.vjs-touch-enabled .vjs-control-bar .vjs-time-divider { + line-height: 4.4em; +} + +.video-js.vjs-touch-enabled .vjs-control-bar .vjs-playback-rate .vjs-playback-rate-value { + line-height: 2.9333; /* 1.5em digits x 2.9333 */ +} From 6c74f8bce82488b6da1ddf5ca5925b112f000373 Mon Sep 17 00:00:00 2001 From: Punnawit9285 Date: Sun, 30 Aug 2026 22:36:47 +0700 Subject: [PATCH 3/4] chore: ignore local tool settings directory --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 97f54ae..dcf7a7a 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,6 @@ npm-debug.log* # Sentry Config File .env.sentry-build-plugin .sentryclirc + +# Local editor and tool settings +.claude/ From f603b1fe5141a7c675273dd3c6f2137453cab148 Mon Sep 17 00:00:00 2001 From: "Siwat T." Date: Sat, 5 Sep 2026 21:43:49 +0700 Subject: [PATCH 4/4] fix: scope play/pause toggle spacing to touch devices only The margin-left rule for .vjs-play-control wasn't scoped to .vjs-touch-enabled like the rest of the touch-target fixes, so it was also shifting the button on desktop/pointer devices. video.js's .vjs-touch-enabled class is based on touch *capability* (navigator.maxTouchPoints), which stays true on touchscreen laptops even when the mouse is the input actually in use. Wrap the rules in (hover: none) and (pointer: coarse) so they apply only when touch is the primary input. --- src/global.scss | 45 +++++++++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/src/global.scss b/src/global.scss index 9e0a777..3c81298 100644 --- a/src/global.scss +++ b/src/global.scss @@ -75,28 +75,33 @@ transform: translate(-50%, 0); } -/* Keep the play/pause toggle off the player's bottom-left corner, an awkward target on a tablet. */ -.video-js .vjs-control-bar .vjs-play-control { - margin-left: 1.5em; -} +/* video.js adds .vjs-touch-enabled based on touch *capability* (navigator.maxTouchPoints), which + stays true on touchscreen laptops even when the mouse is the input in use. Gate on + (hover: none) and (pointer: coarse) too so these rules only kick in when touch is actually the + primary input, not just available. */ +@media (hover: none) and (pointer: coarse) { + /* Keep the play/pause toggle off the player's bottom-left corner, an awkward target on a tablet. */ + .video-js.vjs-touch-enabled .vjs-control-bar .vjs-play-control { + margin-left: 1.5em; + } -/* The stock 3em bar makes every control 30px tall, short of the 44px a fingertip needs. - video.js adds .vjs-touch-enabled only on touch devices, so pointer devices keep the compact bar. */ -.video-js.vjs-touch-enabled .vjs-control-bar { - height: 4.4em; -} + /* The stock 3em bar makes every control 30px tall, short of the 44px a fingertip needs. */ + .video-js.vjs-touch-enabled .vjs-control-bar { + height: 4.4em; + } -/* video.js centres these with line-heights worked out against the 3em bar, so each has to be - restated for 4.4em. The progress control centres itself with flexbox and needs nothing. */ -.video-js.vjs-touch-enabled .vjs-control-bar .vjs-button > .vjs-icon-placeholder:before { - line-height: 2.4444; /* 1.8em glyph x 2.4444 */ -} + /* video.js centres these with line-heights worked out against the 3em bar, so each has to be + restated for 4.4em. The progress control centres itself with flexbox and needs nothing. */ + .video-js.vjs-touch-enabled .vjs-control-bar .vjs-button > .vjs-icon-placeholder:before { + line-height: 2.4444; /* 1.8em glyph x 2.4444 */ + } -.video-js.vjs-touch-enabled .vjs-control-bar .vjs-time-control, -.video-js.vjs-touch-enabled .vjs-control-bar .vjs-time-divider { - line-height: 4.4em; -} + .video-js.vjs-touch-enabled .vjs-control-bar .vjs-time-control, + .video-js.vjs-touch-enabled .vjs-control-bar .vjs-time-divider { + line-height: 4.4em; + } -.video-js.vjs-touch-enabled .vjs-control-bar .vjs-playback-rate .vjs-playback-rate-value { - line-height: 2.9333; /* 1.5em digits x 2.9333 */ + .video-js.vjs-touch-enabled .vjs-control-bar .vjs-playback-rate .vjs-playback-rate-value { + line-height: 2.9333; /* 1.5em digits x 2.9333 */ + } }