Skip to content
Open
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 36 additions & 4 deletions common/app/components/Player.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,13 @@
TokenizedSubtitleModel,
VideoTabModel,
} from '@project/common';
import { ApplyStrategy, AsbplayerSettings, SettingsProvider, TokenState } from '@project/common/settings';
import {
ApplyStrategy,
AsbplayerSettings,
SettingsProvider,
TokenState,
VideoSubtitleSplitBehavior,
} from '@project/common/settings';
import { DictionaryProvider } from '@project/common/dictionary-db';
import { SubtitleCollection } from '@project/common/subtitle-collection';
import { renderRichTextOntoSubtitles, HoveredToken, SubtitleColoring } from '@project/common/subtitle-coloring';
Expand All @@ -40,6 +46,7 @@
import { MiningContext } from '../services/mining-context';
import { SeekTimestampCommand, WebSocketClient } from '../../web-socket-client';
import { ensureStoragePersisted } from '../../util';
import { resolveVideoSubtitleSplitLayout, useVideoAspectRatio } from './video-subtitle-split';

const minVideoPlayerWidth = 300;

Expand Down Expand Up @@ -254,7 +261,16 @@
);

const handleSubtitlePlayerResizeStart = useCallback(() => setSubtitlePlayerResizing(true), []);
const handleSubtitlePlayerResizeEnd = useCallback(() => setSubtitlePlayerResizing(false), []);
const handleSubtitlePlayerResizeEnd = useCallback(
(width: number) => {
setSubtitlePlayerResizing(false);

if (settings.videoSubtitleSplitBehavior === VideoSubtitleSplitBehavior.rememberSplitPosition) {
playbackPreferences.subtitlePlayerWidth = width;
}
},
[playbackPreferences, settings.videoSubtitleSplitBehavior]
);

const handleOnStartedShowingSubtitle = useCallback(() => {
if (
Expand Down Expand Up @@ -750,7 +766,7 @@
);
useEffect(
() => channel?.onPlay((forwardToMedia) => play(clock, mediaAdapter, forwardToMedia)),
[channel, mediaAdapter, clock]

Check warning on line 769 in common/app/components/Player.tsx

View workflow job for this annotation

GitHub Actions / build (22.x)

React Hook useEffect has a missing dependency: 'play'. Either include it or remove the dependency array
);
useEffect(
() => channel?.onPause((forwardToMedia) => pause(clock, mediaAdapter, forwardToMedia)),
Expand Down Expand Up @@ -900,7 +916,7 @@
break;
}
});
}, [miningContext, settings, wasPlayingWhenMiningStarted, clock, mediaAdapter]);

Check warning on line 919 in common/app/components/Player.tsx

View workflow job for this annotation

GitHub Actions / build (22.x)

React Hook useEffect has a missing dependency: 'play'. Either include it or remove the dependency array

useEffect(() => {
return miningContext.onEvent('started-mining', () => {
Expand Down Expand Up @@ -1002,7 +1018,7 @@
setLastJumpToTopTimestamp(Date.now());
}, [videoPopOut, channelId, videoFileUrl, videoFrameRef, videoChannelRef, origin]);

const handlePlay = useCallback(() => play(clock, mediaAdapter, true), [clock, mediaAdapter]);

Check warning on line 1021 in common/app/components/Player.tsx

View workflow job for this annotation

GitHub Actions / build (22.x)

React Hook useCallback has a missing dependency: 'play'. Either include it or remove the dependency array
const handlePause = useCallback(() => pause(clock, mediaAdapter, true), [clock, mediaAdapter]);
const handleSeek = useCallback(
async (progress: number) => {
Expand Down Expand Up @@ -1034,7 +1050,7 @@
play(clock, mediaAdapter, true);
}
},
[clock, seek, mediaAdapter]

Check warning on line 1053 in common/app/components/Player.tsx

View workflow job for this annotation

GitHub Actions / build (22.x)

React Hook useCallback has a missing dependency: 'play'. Either include it or remove the dependency array
);

const handleCopyFromSubtitlePlayer = useCallback(
Expand Down Expand Up @@ -1094,7 +1110,7 @@
play(clock, mediaAdapter, true);
}
},
[channel, clock, mediaAdapter, seek]

Check warning on line 1113 in common/app/components/Player.tsx

View workflow job for this annotation

GitHub Actions / build (22.x)

React Hook useCallback has a missing dependency: 'play'. Either include it or remove the dependency array
);

const handleOffsetChange = useCallback(
Expand Down Expand Up @@ -1176,7 +1192,7 @@
);

return () => unbind();
}, [keyBinder, clock, mediaAdapter, disableKeyEvents]);

Check warning on line 1195 in common/app/components/Player.tsx

View workflow job for this annotation

GitHub Actions / build (22.x)

React Hook useEffect has a missing dependency: 'play'. Either include it or remove the dependency array

useEffect(() => {
return keyBinder.bindAdjustPlaybackRate(
Expand Down Expand Up @@ -1279,10 +1295,24 @@
};
}, [webSocketClient, extension, seek, clock]);

const [windowWidth] = useWindowSize(true);
const [windowWidth, windowHeight] = useWindowSize(true);

const videoInWindow = Boolean(videoFileUrl && !videoPopOut);
const shouldLoadVideoAspectRatio =
videoInWindow && settings.videoSubtitleSplitBehavior !== VideoSubtitleSplitBehavior.rememberSplitPosition;
const videoAspectRatio = useVideoAspectRatio(videoFileUrl, shouldLoadVideoAspectRatio);
const loaded = videoFileUrl || subtitles;
const videoInWindow = Boolean(loaded && videoFileUrl && !videoPopOut);
const playerHeight = appBarHidden ? windowHeight : Math.max(0, windowHeight - appBarHeight);
const aspectFitVideoWidth = videoAspectRatio
? Math.max(minVideoPlayerWidth, Math.round(playerHeight * videoAspectRatio))
: undefined;
const autoSubtitlePlayerInitialWidth =
videoInWindow && aspectFitVideoWidth !== undefined ? Math.max(0, windowWidth - aspectFitVideoWidth) : undefined;
const subtitlePlayerInitialWidth = resolveVideoSubtitleSplitLayout({
behavior: settings.videoSubtitleSplitBehavior,
persistedWidth: playbackPreferences.subtitlePlayerWidth,
autoWidth: autoSubtitlePlayerInitialWidth,
});
const subtitlePlayerMaxResizeWidth = Math.max(0, windowWidth - minVideoPlayerWidth);
const notEnoughSpaceForSubtitlePlayer = subtitlePlayerMaxResizeWidth < minSubtitlePlayerWidth;
const actuallyHideSubtitlePlayer =
Expand Down Expand Up @@ -1318,6 +1348,7 @@
hidden={actuallyHideSubtitlePlayer}
style={{
flexGrow: videoInWindow ? 0 : 1,
flexShrink: 0,
width: 'auto',
}}
>
Expand Down Expand Up @@ -1385,6 +1416,7 @@
settings={settings}
keyBinder={keyBinder}
webSocketClient={webSocketClient}
initialWidth={subtitlePlayerInitialWidth}
/>
</Grid>
</Grid>
Expand Down
40 changes: 30 additions & 10 deletions common/app/components/SubtitlePlayer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import { MineSubtitleParams } from '../hooks/use-app-web-socket-client';
import { isMobile } from 'react-device-detect';
import ChromeExtension, { ExtensionMessage } from '../services/chrome-extension';
import { MineSubtitleCommand, WebSocketClient } from '../../web-socket-client';
import { clampSubtitlePlayerWidth } from './video-subtitle-split';
import './subtitles.css';

let lastKnownWidth: number | undefined;
Expand Down Expand Up @@ -362,7 +363,7 @@ interface SubtitlePlayerProps {
onMouseOver: (e: React.MouseEvent) => void;
onMouseOut: (e: React.MouseEvent) => void;
onResizeStart?: () => void;
onResizeEnd?: () => void;
onResizeEnd?: (width: number) => void;
autoPauseContext: AutoPauseContext;
subtitles?: DisplaySubtitleModel[];
subtitleCollection: SubtitleColoring | SubtitleCollection<DisplaySubtitleModel>;
Expand All @@ -383,6 +384,7 @@ interface SubtitlePlayerProps {
settings: AsbplayerSettings;
keyBinder: KeyBinder;
maxResizeWidth: number;
initialWidth?: number;
webSocketClient?: WebSocketClient;
}

Expand Down Expand Up @@ -418,6 +420,7 @@ export default function SubtitlePlayer({
settings,
keyBinder,
maxResizeWidth,
initialWidth,
webSocketClient,
}: SubtitlePlayerProps) {
const { t } = useTranslation();
Expand Down Expand Up @@ -1012,18 +1015,35 @@ export default function SubtitlePlayer({
onResizeEnd,
});

const appliedInitialWidthRef = useRef<number | undefined>(undefined);
useEffect(() => {
lastKnownWidth = width;
}, [width, maxResizeWidth]);
if (!resizable || initialWidth === undefined || maxResizeWidth < minSubtitlePlayerWidth) {
return;
}

const clampedInitialWidth = clampSubtitlePlayerWidth(initialWidth, minSubtitlePlayerWidth, maxResizeWidth);

if (appliedInitialWidthRef.current === clampedInitialWidth) {
return;
}

appliedInitialWidthRef.current = clampedInitialWidth;
setWidth(clampedInitialWidth);
lastKnownWidth = clampedInitialWidth;
}, [resizable, initialWidth, maxResizeWidth, setWidth]);

// Scroll to selected subtitle when layout changes
useEffect(() => {
const listener = () => {
lastKnownWidth = undefined;
setWidth(calculateInitialWidth());
};
screen.orientation.addEventListener('change', listener);
return () => screen.orientation.removeEventListener('change', listener);
}, [setWidth]);
// Small delay to allow layout to settle
const timer = setTimeout(() => {
scrollToCurrentSubtitle();
}, 50);
return () => clearTimeout(timer);
}, [width, scrollToCurrentSubtitle]);

useEffect(() => {
lastKnownWidth = width;
}, [width, maxResizeWidth]);

const { dragging, draggingStartLocation, draggingCurrentLocation } = useDragging({ holdToDragMs: 750 });

Expand Down
15 changes: 15 additions & 0 deletions common/app/components/subtitles.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
html,
body {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this added to subtitles.css?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When theater mode was toggled back on, the iframe document was ending up a bit taller than the viewport, so the browser showed a scrollbar. This problem is also present on live app, This fixes it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Though it's mostly visible when using yarn run start, when using preview, it's rare

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kept only one overflow: hidden;, should be enough to fix this.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be addressed in a separate change then? It doesn't seem critical to fix, if it is only dev-facing. Also, I'm not completely convinced it's the right fix, if it's really the iframe that's taller then setting overflow to hidden would just hide the part of the iframe that's overflowing.

Copy link
Copy Markdown
Contributor Author

@khajiitvaper2017 khajiitvaper2017 Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, feel free to remove it, just didn't seem to be worthy of separate pull request. In my first post's video you can see scrollbars appearing for a split second when toggling theater mode, that's what I am addressing. On live version this problem also exists but layout changes faster than scrollbar can appear and it jerks a little instead.

2026-03-23.20-42-49.mp4

I tried to log it and for some reason, htmlScrollHeight is taller than viewport:
bodyClientHeight = 847
bodyScrollHeight = 847
htmlClientHeight = 847
htmlScrollHeight = 865
If you can find anything better than just slapping overflow: hidden;, that would be great.

height: 100%;
overflow: hidden;
}

body {
margin: 0;
}

#root {
height: 100%;
overflow: hidden;
}

.asbplayer-offscreen {
position: fixed !important;
left: 100% !important;
Expand Down
36 changes: 36 additions & 0 deletions common/app/components/video-subtitle-split.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { VideoSubtitleSplitBehavior } from '@project/common/settings';
import { clampSubtitlePlayerWidth, resolveVideoSubtitleSplitLayout } from './video-subtitle-split';

it('uses the saved split width in remember mode', () => {
expect(
resolveVideoSubtitleSplitLayout({
behavior: VideoSubtitleSplitBehavior.rememberSplitPosition,
persistedWidth: 420,
autoWidth: 300,
})
).toBe(420);
});

it('leaves the width unchanged in remember mode when nothing has been saved', () => {
expect(
resolveVideoSubtitleSplitLayout({
behavior: VideoSubtitleSplitBehavior.rememberSplitPosition,
autoWidth: 300,
})
).toBeUndefined();
});

it('ignores saved width in auto-maximize mode', () => {
expect(
resolveVideoSubtitleSplitLayout({
behavior: VideoSubtitleSplitBehavior.autoMaximizeVideo,
persistedWidth: 420,
autoWidth: 300,
})
).toBe(300);
});

it('clamps split width to the current bounds', () => {
expect(clampSubtitlePlayerWidth(150, 200, 500)).toBe(200);
expect(clampSubtitlePlayerWidth(600, 200, 500)).toBe(500);
});
51 changes: 51 additions & 0 deletions common/app/components/video-subtitle-split.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { useEffect, useState } from 'react';
import { VideoSubtitleSplitBehavior } from '../../settings';

interface ResolveVideoSubtitleSplitLayoutArgs {
behavior: VideoSubtitleSplitBehavior;
persistedWidth?: number;
autoWidth?: number;
}

export function clampSubtitlePlayerWidth(width: number, minWidth: number, maxWidth: number) {
return Math.min(maxWidth, Math.max(minWidth, width));
}

export function resolveVideoSubtitleSplitLayout({
behavior,
persistedWidth,
autoWidth,
}: ResolveVideoSubtitleSplitLayoutArgs) {
if (behavior === VideoSubtitleSplitBehavior.rememberSplitPosition) {
return persistedWidth;
}

return autoWidth;
}

export function useVideoAspectRatio(videoFileUrl: string | undefined, enabled: boolean) {
const [videoAspectRatio, setVideoAspectRatio] = useState<number>();

useEffect(() => {
setVideoAspectRatio(undefined);

if (!enabled || !videoFileUrl) {
return;
}

const video = document.createElement('video');
video.preload = 'metadata';
video.onloadedmetadata = () => {
setVideoAspectRatio(video.videoWidth / video.videoHeight);
};
video.src = videoFileUrl;

return () => {
video.onloadedmetadata = null;
video.removeAttribute('src');
video.load();
};
}, [videoFileUrl, enabled]);

return videoAspectRatio;
}
17 changes: 13 additions & 4 deletions common/app/hooks/use-resize.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,37 @@
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';

interface Props {
initialWidth: () => number;
minWidth: number;
maxWidth: number;
onResizeStart?: () => void;
onResizeEnd?: () => void;
onResizeEnd?: (width: number) => void;
}

// https://stackoverflow.com/questions/49469834/recommended-way-to-have-drawer-resizable
export const useResize = ({ initialWidth, minWidth, maxWidth, onResizeStart, onResizeEnd }: Props) => {
const [isResizing, setIsResizing] = useState(false);
const [width, setWidth] = useState(initialWidth);
const [lastMouseDownClientX, setLastMouseDownClientX] = useState<number>(0);
const widthRef = useRef(width);

useEffect(() => {
widthRef.current = width;
}, [width]);

const enableResize = useCallback(() => {
setIsResizing(true);
onResizeStart?.();
}, [setIsResizing, onResizeStart]);

const disableResize = useCallback(() => {
if (!isResizing) {
return;
}

setIsResizing(false);
onResizeEnd?.();
}, [setIsResizing, onResizeEnd]);
onResizeEnd?.(widthRef.current);
}, [isResizing, setIsResizing, onResizeEnd]);

const recordLastMouseDownPosition = useCallback((e: MouseEvent) => {
setLastMouseDownClientX(e.clientX);
Expand Down
15 changes: 15 additions & 0 deletions common/app/services/playback-preferences.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const theaterModeKey = 'theaterMode';
const offsetKey = 'offset';
const displaySubtitlesKey = 'displaySubtitles';
const hideSubtitleListKey = 'hideSubtitleList';
const subtitlePlayerWidthKey = 'subtitlePlayerWidth';
const defaultVolume = 100;

interface PlaybackPrefSettings {
Expand Down Expand Up @@ -96,4 +97,18 @@ export default class PlaybackPreferences {
set displaySubtitles(displaySubtitles: boolean) {
this._storage.set(displaySubtitlesKey, String(displaySubtitles));
}

get subtitlePlayerWidth(): number | undefined {
const value = this._storage.get(subtitlePlayerWidthKey);

if (value === null) {
return undefined;
}

return Number(value);
}

set subtitlePlayerWidth(width: number) {
this._storage.set(subtitlePlayerWidthKey, String(width));
}
}
2 changes: 2 additions & 0 deletions common/app/services/video-channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,7 @@ export default class VideoChannel {
miscSettings(settings: MiscSettings) {
const {
themeType,
videoSubtitleSplitBehavior,
copyToClipboardOnMine,
autoPausePreference,
seekDuration,
Expand All @@ -658,6 +659,7 @@ export default class VideoChannel {
command: 'miscSettings',
value: {
themeType,
videoSubtitleSplitBehavior,
copyToClipboardOnMine,
autoPausePreference,
seekDuration,
Expand Down
Loading
Loading