diff --git a/README.md b/README.md index 87f2e4b2..846f0893 100755 --- a/README.md +++ b/README.md @@ -47,7 +47,15 @@ Suna, kibo, [@genericdave](https://github.com/genericdave), Daniel, -Cristian +Cristian, +Joey Potter, +[@InteractiveNinja](https://github.com/InteractiveNinja), +[@agloo](https://github.com/agloo), +[@Venous771](https://github.com/Venous771), +[@Viterkim](https://github.com/Viterkim), +Julian, +DanglingSabSuu + and to those who have donated privately. @@ -63,7 +71,8 @@ Thank you to all those who have contributed to asbplayer: [@bpwhelan](https://github.com/bpwhelan), [@pooky-programs](https://github.com/pooky-programs), [@m-edlund](https://github.com/m-edlund), -[@nekorushi](https://github.com/nekorushi) +[@nekorushi](https://github.com/nekorushi), +[@Viterkim](https://github.com/Viterkim) Thank you to all those who have translated asbplayer: @@ -75,7 +84,7 @@ Thank you to all those who have translated asbplayer: **Leo Gonzalez** (Spanish), **Yuri (ganqqwerty)** (Russian) -If you are a non-English native, and would like to help translate asbplayer, join the [Crowdin project](https://crowdin.com/project/asbplayer). +If you are a non-English native, and would like to help translate asbplayer, join the [Crowdin project](https://crowdin.com/project/asbplayer). If your language isn't there, feel free to create an issue to add it on the [issues page](https://github.com/killergerbah/asbplayer/issues). ## Getting Started @@ -86,7 +95,7 @@ First, see if you can get started by following one of the [community guides](#co Otherwise, the following steps for setting up automated Anki flashcards should work for any language: -1. Install and set up a dictionary tool for your target language that allows you to do instant lookups. Popular ones are [Yomitan](https://chromewebstore.google.com/detail/yomitan/likgccmbimhjbgkjambclfkhldnlhbnn) for Japanese and [VocabSieve](https://github.com/FreeLanguageTools/vocabsieve) for European languages. +1. Install and set up a dictionary tool for your target language that allows you to do instant lookups. Popular ones are [Yomitan](https://chromewebstore.google.com/detail/yomitan/likgccmbimhjbgkjambclfkhldnlhbnn) (see [supported languages](https://yomitan.wiki/other/supported-languages/)) and [VocabSieve](https://github.com/FreeLanguageTools/vocabsieve) (tuned for European languages. Works with Asian languages too but doesn't automatically detect word boundaries). 2. Install [Anki](https://apps.ankiweb.net/), and create a deck and note type. More details on [Refold's guide](https://refold.la/roadmap/stage-1/a/anki-setup). 3. Install the [AnkiConnect](https://ankiweb.net/shared/info/2055492159) plugin for Anki. 4. [Configure](https://killergerbah.github.io/asbplayer/?view=settings) asbplayer to create cards via AnkiConnect using your deck and note type. @@ -158,7 +167,7 @@ Use Ctrl + Shift + F to see auto-detected subtitle tracks for streami - Netflix - Youtube -- Disney Plus (known issue: subtitles sometimes off by ~5 seconds) +- Disney Plus (known issues: flakey video detection, subtitles sometimes off by ~5 seconds) - Hulu - TVer - Bandai Channel @@ -179,8 +188,14 @@ You can replace filtered content similarly by entering a string into the "Subtit Useful examples of regular expressions: -- `([((]([^()()]|(([((][^()()]+[))])))+[))])` : Remove names enclosed by parenthesis to indicate speakers (i.e. "**(山田)** 元気ですか?") -- `\[.*\]` : Remove indications enclosed by brackets that sound or music that is playing (i.e. "**\[PLAYFUL MUSIC]**") +- `([((]([^()()]|(([((][^()()]+[))])))+[))])` : Remove names enclosed by parenthesis to indicate speakers (e.g. "**(山田)** 元気ですか?") +- `(.*)\n+(?!-)(.*)` : Some subtitles are split in several lines and this regex forces them into a single line. For this filter to work, you must also put `$1 $2` in the "Subtitle regex filter text replacement" field. + - **NB**: When using this regex pattern in combination with other patterns (using the `|` operator, see below), place this pattern at the end. This ensures that all other regex transformations are applied first, and then the results are finally combined into a single line. +- `-?\[.*\]` : Remove indications enclosed by square brackets that sound or music that is playing (e.g. "**\[PLAYFUL MUSIC]**" or "**\-[GASPS]**") + - `^[-\(\)\.\sA-ZAÂÃÀÇÉÊÍÓÔÕÚÑ]+$` : As an alternative to the above, filter out descriptions written in capital letters, but without the square brackets (e.g. "**PLAYFUL MUSIC**"). If your language has additional letters with diacritics, you feel free to add them to this list. +- `[♪♬#~〜]+` : Any combination of symbols on their own that represent playing music (e.g. `♪♬♪`) + +Regular expressions can be combined with the character `|` (no spaces needed inbetween). E.g., if you want to use the 2 last regexes from this list, you can use `-?\[.*\]|[♪♬#~〜]+`. You can combine as many regexes as you wish this way. Learn how to write and test custom regular expressions at [Regex Learn - Playground](https://regexlearn.com/playground). @@ -248,6 +263,8 @@ asbplayer can be setup to support one-click mining workflows by integrating with 5. Configure Yomitan to use the same note type you have configured for asbplayer. 6. Using Yomitan's `+` button on asbplayer subtitles will now trigger the flashcard creator with word and definition fields pre-populated by Yomitan. +The proxy is very lightweight, so it's fine to leave it running in the background. On Windows, [RBTray](https://github.com/benbuck/rbtray) can be used to minimise it to the taskbar. + See the proxy's [example configuration file](https://github.com/killergerbah/asbplayer/blob/main/scripts/anki-connect-proxy/.env.example) for how to further configure it. #### WebSocket interface diff --git a/client/public/extension.json b/client/public/extension.json index 084230a7..19f1b607 100644 --- a/client/public/extension.json +++ b/client/public/extension.json @@ -1,48 +1,48 @@ { "latest": { - "version": "1.2.0", - "url": "https://github.com/killergerbah/asbplayer/releases/tag/v1.2.0" + "version": "1.3.2", + "url": "https://github.com/killergerbah/asbplayer/releases/tag/v1.3.2" }, "languages": [ { "code": "en", "url": "/locales/en.json", - "version": 5 + "version": 6 }, { "code": "es", "url": "/locales/es.json", - "version": 4 + "version": 5 }, { "code": "ja", "url": "/locales/ja.json", - "version": 7 + "version": 9 }, { "code": "de", "url": "/locales/de.json", - "version": 5 + "version": 6 }, { "code": "pl", "url": "/locales/pl.json", - "version": 5 + "version": 6 }, { "code": "pt_BR", "url": "/locales/pt_BR.json", - "version": 4 + "version": 5 }, { "code": "zh_CN", "url": "/locales/zh_CN.json", - "version": 4 + "version": 5 }, { "code": "ru", "url": "/locales/ru.json", - "version": 3 + "version": 4 } ] } diff --git a/client/public/firefox-extension-updates.json b/client/public/firefox-extension-updates.json index a6196479..84d5d4a5 100644 --- a/client/public/firefox-extension-updates.json +++ b/client/public/firefox-extension-updates.json @@ -5,6 +5,14 @@ { "version": "1.3.1", "update_link": "https://github.com/killergerbah/asbplayer/releases/download/v1.3.1/asbplayer-extension-1.3.1-firefox.xpi" + }, + { + "version": "1.3.2", + "update_link": "https://github.com/killergerbah/asbplayer/releases/download/v1.3.2/asbplayer-extension-1.3.2-firefox.xpi" + }, + { + "version": "1.4.2", + "update_link": "https://github.com/killergerbah/asbplayer/releases/download/v1.4.2/asbplayer-extension-1.4.2-firefox.xpi" } ] } diff --git a/common/anki/anki.ts b/common/anki/anki.ts index 88c2d085..beab9f84 100755 --- a/common/anki/anki.ts +++ b/common/anki/anki.ts @@ -1,10 +1,16 @@ import { AudioClip } from '@project/common/audio-clip'; import { CardModel, Image } from '@project/common'; import { HttpFetcher, Fetcher } from '@project/common'; -import { AnkiSettings } from '@project/common/settings'; +import { AnkiSettings, AnkiSettingsFieldKey } from '@project/common/settings'; import sanitize from 'sanitize-filename'; import { extractText, sourceString } from '@project/common/util'; +declare global { + interface String { + toWellFormed?: () => string; + } +} + const ankiQuerySpecialCharacters = ['"', '*', '_', '\\', ':']; const alphaNumericCharacters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; @@ -325,17 +331,10 @@ export class Anki { if (infoResponse.result.length > 0 && infoResponse.result[0].noteId === lastNoteId) { const info = infoResponse.result[0]; - if ( - this.settingsProvider.sentenceField && - info.fields && - typeof info.fields[this.settingsProvider.sentenceField]?.value === 'string' && - typeof params.note.fields[this.settingsProvider.sentenceField] === 'string' - ) { - params.note.fields[this.settingsProvider.sentenceField] = inheritHtmlMarkup( - params.note.fields[this.settingsProvider.sentenceField], - info.fields[this.settingsProvider.sentenceField].value - ); - } + this._inheritHtmlMarkupFromField('sentenceField', info, params); + this._inheritHtmlMarkupFromField('track1Field', info, params); + this._inheritHtmlMarkupFromField('track2Field', info, params); + this._inheritHtmlMarkupFromField('track3Field', info, params); await this._executeAction('updateNoteFields', params, ankiConnectUrl); @@ -384,6 +383,10 @@ export class Anki { } private _sanitizeFileName(name: string) { + if (typeof name.toWellFormed === 'function') { + name = name.toWellFormed(); + } + return sanitize(name, { replacement: '_' }); } @@ -395,6 +398,22 @@ export class Anki { ); } + private _inheritHtmlMarkupFromField(fieldKey: AnkiSettingsFieldKey, info: any, params: any) { + const fieldName = this.settingsProvider[fieldKey]; + + if ( + fieldName && + info.fields && + typeof info.fields[fieldName]?.value === 'string' && + typeof params.note.fields[fieldName] === 'string' + ) { + params.note.fields[fieldName] = inheritHtmlMarkup( + params.note.fields[fieldName], + info.fields[fieldName].value + ); + } + } + private async _executeAction(action: string, params: any, ankiConnectUrl?: string) { const body: any = { action: action, diff --git a/common/app/components/App.tsx b/common/app/components/App.tsx index 71cb7fb0..409ea615 100755 --- a/common/app/components/App.tsx +++ b/common/app/components/App.tsx @@ -23,7 +23,7 @@ import { createTheme } from '@project/common/theme'; import { AsbplayerSettings, Profile } from '@project/common/settings'; import { humanReadableTime, download, extractText } from '@project/common/util'; import { AudioClip } from '@project/common/audio-clip'; -import { AnkiExportMode, ExportParams } from '@project/common/anki'; +import { ExportParams } from '@project/common/anki'; import { SubtitleReader } from '@project/common/subtitle-reader'; import { v4 as uuidv4 } from 'uuid'; import clsx from 'clsx'; @@ -53,7 +53,7 @@ import { useAnki } from '../hooks/use-anki'; import { usePlaybackPreferences } from '../hooks/use-playback-preferences'; import { MiningContext } from '../services/mining-context'; -const latestExtensionVersion = '1.2.0'; +const latestExtensionVersion = '1.4.2'; const extensionUrl = 'https://chromewebstore.google.com/detail/asbplayer-language-learni/hkledmpjpaehamkiehglnbelcpdflcab'; const mp3WorkerFactory = () => new Worker(new URL('../../audio-clip/mp3-encoder-worker.ts', import.meta.url)); @@ -228,9 +228,13 @@ function App({ origin, logoUrl, settings, extension, fetcher, onSettingsChanged, const drawerRatio = videoFrameRef.current ? 0.2 : 0.3; const minDrawerSize = videoFrameRef.current ? 150 : 300; const drawerWidth = Math.max(minDrawerSize, width * drawerRatio); - const { copyHistoryItems, refreshCopyHistory, deleteCopyHistoryItem, saveCopyHistoryItem } = useCopyHistory( - settings.miningHistoryStorageLimit - ); + const { + copyHistoryItems, + refreshCopyHistory, + deleteCopyHistoryItem, + saveCopyHistoryItem, + deleteAllCopyHistoryItems, + } = useCopyHistory(settings.miningHistoryStorageLimit); const copyHistoryItemsRef = useRef([]); copyHistoryItemsRef.current = copyHistoryItems; const [copyHistoryOpen, setCopyHistoryOpen] = useState(false); @@ -518,13 +522,6 @@ function App({ origin, logoUrl, settings, extension, fetcher, onSettingsChanged, setDisableKeyEvents(ankiDialogOpen); }, [ankiDialogOpen]); - const handleDeleteCopyHistoryItem = useCallback( - (item: CopyHistoryItem) => { - deleteCopyHistoryItem(item); - }, - [deleteCopyHistoryItem] - ); - const handleUnloadVideo = useCallback( (videoFileUrl: string) => { if (videoFileUrl !== sources.videoFileUrl) { @@ -1179,7 +1176,8 @@ function App({ origin, logoUrl, settings, extension, fetcher, onSettingsChanged, open={effectiveCopyHistoryOpen} drawerWidth={drawerWidth} onClose={handleCloseCopyHistory} - onDelete={handleDeleteCopyHistoryItem} + onDelete={deleteCopyHistoryItem} + onDeleteAll={deleteAllCopyHistoryItems} onClipAudio={handleClipAudio} onDownloadImage={handleDownloadImage} onDownloadSectionAsSrt={handleDownloadCopyHistorySectionAsSrt} diff --git a/common/app/components/CopyHistory.tsx b/common/app/components/CopyHistory.tsx index 6a111fab..e07ea61c 100755 --- a/common/app/components/CopyHistory.tsx +++ b/common/app/components/CopyHistory.tsx @@ -15,6 +15,7 @@ interface CopyHistoryProps { forceShowDownloadOptions?: boolean; onClose: () => void; onDelete: (item: CopyHistoryItem) => void; + onDeleteAll: () => void; onAnki: (item: CopyHistoryItem) => void; onSelect?: (item: CopyHistoryItem) => void; onClipAudio: (item: CopyHistoryItem) => void; diff --git a/common/app/components/CopyHistoryList.tsx b/common/app/components/CopyHistoryList.tsx index 68b07d41..ee9bb14b 100644 --- a/common/app/components/CopyHistoryList.tsx +++ b/common/app/components/CopyHistoryList.tsx @@ -2,6 +2,7 @@ import React, { useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { makeStyles } from '@material-ui/core/styles'; import { timeDurationDisplay } from '../services/util'; +import Button from '@material-ui/core/Button'; import ListItemSecondaryAction from '@material-ui/core/ListItemSecondaryAction'; import Grid from '@material-ui/core/Grid'; import IconButton from '@material-ui/core/IconButton'; @@ -10,6 +11,7 @@ import ListItem from '@material-ui/core/ListItem'; import ListItemIcon from '@material-ui/core/ListItemIcon'; import ListItemText from '@material-ui/core/ListItemText'; import Popover from '@material-ui/core/Popover'; +import DeleteIcon from '@material-ui/icons/Delete'; import MoreVertIcon from '@material-ui/icons/MoreVert'; import NoteAddIcon from '@material-ui/icons/NoteAdd'; import SaveAltIcon from '@material-ui/icons/SaveAlt'; @@ -25,6 +27,7 @@ interface CopyHistoryListProps { items: CopyHistoryItem[]; onClose: () => void; onDelete: (item: CopyHistoryItem) => void; + onDeleteAll: () => void; onAnki: (item: CopyHistoryItem) => void; onSelect?: (item: CopyHistoryItem) => void; onClipAudio: (item: CopyHistoryItem) => void; @@ -34,11 +37,18 @@ interface CopyHistoryListProps { const useStyles = makeStyles((theme) => ({ listContainer: { - position: 'relative', + display: 'flex', height: '100%', + flexDirection: 'column', overflowY: 'auto', overflowX: 'hidden', }, + list: { + flexGrow: 1, + }, + clearButton: { + margin: theme.spacing(2), + }, listItem: { '&:hover': { backgroundColor: theme.palette.action.hover, @@ -217,6 +227,7 @@ export default function CopyHistoryList({ onClipAudio, onDownloadImage, onDelete, + onDeleteAll, onDownloadSectionAsSrt, onAnki, }: CopyHistoryListProps) { @@ -336,7 +347,16 @@ export default function CopyHistoryList({ content = (
- {elements} + {elements} +
); } else { diff --git a/common/app/components/SettingsDialog.tsx b/common/app/components/SettingsDialog.tsx index f4d591ee..80510213 100755 --- a/common/app/components/SettingsDialog.tsx +++ b/common/app/components/SettingsDialog.tsx @@ -81,6 +81,8 @@ export default function SettingsDialog({ extensionSupportsSidePanel={extension.supportsSidePanel} extensionSupportsOrderableAnkiFields={extension.supportsOrderableAnkiFields} extensionSupportsTrackSpecificSettings={extension.supportsTrackSpecificSettings} + extensionSupportsSubtitlesWidthSetting={extension.supportsSubtitlesWidthSetting} + extensionSupportsPauseOnHover={extension.supportsPauseOnHover} insideApp chromeKeyBinds={extension.extensionCommands} onOpenChromeExtensionShortcuts={extension.openShortcuts} diff --git a/common/app/components/VideoPlayer.tsx b/common/app/components/VideoPlayer.tsx index 1207c9a9..cd53d2ae 100755 --- a/common/app/components/VideoPlayer.tsx +++ b/common/app/components/VideoPlayer.tsx @@ -22,6 +22,7 @@ import { SubtitleAlignment, changeForTextSubtitleSetting, textSubtitleSettingsForTrack, + PauseOnHoverMode, } from '@project/common/settings'; import { surroundingSubtitles, mockSurroundingSubtitles, seekWithNudge } from '@project/common/util'; import { SubtitleCollection } from '@project/common/subtitle-collection'; @@ -68,8 +69,8 @@ const useStyles = makeStyles({ paddingLeft: 20, paddingRight: 20, textAlign: 'center', - whiteSpace: 'pre-wrap', - lineHeight: 'normal', + whiteSpace: 'normal', + lineHeight: 'inherit', }, }); @@ -152,9 +153,8 @@ const showingSubtitleHtml = ( (imageBasedSubtitleScaleFactor * (videoRef.current?.width ?? window.screen.availWidth)) / subtitle.textImage.screen.width; const width = imageScale * subtitle.textImage.image.width; - return ` -
+
subtitle${subtitle.text}`; + const lines = subtitle.text.split('\n'); + const wrappedText = lines.map((line) => `

${line}

`).join(''); + return `${wrappedText}`; }; interface ShowingSubtitleProps { @@ -174,6 +176,7 @@ interface ShowingSubtitleProps { subtitleStyles: any; imageBasedSubtitleScaleFactor: number; className?: string; + onMouseOver: React.MouseEventHandler; } const ShowingSubtitle = ({ @@ -182,6 +185,7 @@ const ShowingSubtitle = ({ subtitleStyles, imageBasedSubtitleScaleFactor, className, + onMouseOver, }: ShowingSubtitleProps) => { let content; @@ -194,26 +198,38 @@ const ShowingSubtitle = ({ /> ); } else { - content = {subtitle.text}; + const lines = subtitle.text.split('\n'); + content = lines.map((line, index) => ( +

+ {line} +

+ )); } - return
{content}
; + return ( +
+ {content} +
+ ); }; interface CachedShowingSubtitleProps { index: number; domCache: OffscreenDomCache; className?: string; + onMouseOver: React.MouseEventHandler; } const CachedShowingSubtitle = React.memo(function CachedShowingSubtitle({ index, domCache, className, + onMouseOver, }: CachedShowingSubtitleProps) { return (
{ if (!ref) { return; @@ -1208,38 +1224,6 @@ export default function VideoPlayer({ [onSettingsChanged] ); - useEffect(() => { - const onWheel = (event: WheelEvent) => { - if (!displaySubtitles || !showSubtitlesRef.current?.length) { - return; - } - - if (Math.abs(event.deltaY) < 10) { - return; - } - - let shouldIncreaseOffset: boolean; - - switch (subtitleAlignment) { - case 'bottom': - shouldIncreaseOffset = event.deltaY > 0; - break; - case 'top': - shouldIncreaseOffset = event.deltaY < 0; - break; - } - - setSubtitlePositionOffset((offset) => { - const newOffset = shouldIncreaseOffset ? --offset : ++offset; - onSettingsChanged({ subtitlePositionOffset: newOffset }); - return newOffset; - }); - }; - - window.addEventListener('wheel', onWheel); - return () => window.removeEventListener('wheel', onWheel); - }, [subtitleAlignment, displaySubtitles, playbackPreferences, onSettingsChanged]); - const handleClick = useCallback(() => { if (playing()) { playerChannel.pause(); @@ -1297,6 +1281,23 @@ export default function VideoPlayer({ ms: 500, }); + const isPausedDueToHoverRef = useRef(); + + const handleSubtitleMouseOver = useCallback(() => { + if (miscSettings.pauseOnHoverMode !== PauseOnHoverMode.disabled && videoRef.current?.paused === false) { + playerChannel.pause(); + isPausedDueToHoverRef.current = true; + } + }, [miscSettings.pauseOnHoverMode, playerChannel]); + + const handleVideoMouseOver = useCallback(() => { + if (miscSettings.pauseOnHoverMode === PauseOnHoverMode.inAndOut && isPausedDueToHoverRef.current) { + playerChannel.play(); + } + + isPausedDueToHoverRef.current = false; + }, [miscSettings.pauseOnHoverMode, playerChannel]); + // If the video player is taking up the entire screen, then the subtitle player isn't showing // This code assumes some behavior in Player, namely that the subtitle player is automatically hidden // (and therefore the VideoPlayer takes up all the space) when there isn't enough room for the subtitle player @@ -1323,20 +1324,31 @@ export default function VideoPlayer({ className={showCursor ? classes.video : `${classes.cursorHidden} ${classes.video}`} ref={videoRefCallback} src={videoFile} + onMouseOver={handleVideoMouseOver} /> {displaySubtitles && (
{showSubtitles.map((subtitle, index) => { if (miscSettings.preCacheSubtitleDom) { const domCache = getSubtitleDomCache(); - return ; + return ( + + ); } return ( @@ -1347,6 +1359,7 @@ export default function VideoPlayer({ className={trackStyles[subtitle.track].classes} videoRef={videoRef} imageBasedSubtitleScaleFactor={subtitleSettings.imageBasedSubtitleScaleFactor} + onMouseOver={handleSubtitleMouseOver} /> ); })} diff --git a/common/app/components/video-player.css b/common/app/components/video-player.css index 9124b98b..7e834621 100644 --- a/common/app/components/video-player.css +++ b/common/app/components/video-player.css @@ -11,3 +11,8 @@ .asbplayer-subtitles-blurred:hover { filter: none; } + +.subtitle-line { + margin: 0; + padding: 0; +} diff --git a/common/app/hooks/use-copy-history.ts b/common/app/hooks/use-copy-history.ts index 1901461d..d6e5016d 100644 --- a/common/app/hooks/use-copy-history.ts +++ b/common/app/hooks/use-copy-history.ts @@ -56,5 +56,16 @@ export const useCopyHistory = (miningHistoryStorageLimit: number) => { [copyHistoryRepository] ); - return { copyHistoryItems, refreshCopyHistory, deleteCopyHistoryItem, saveCopyHistoryItem }; + const deleteAllCopyHistoryItems = useCallback(async () => { + setCopyHistoryItems([]); + await copyHistoryRepository.clear(); + }, [copyHistoryRepository]); + + return { + copyHistoryItems, + refreshCopyHistory, + deleteCopyHistoryItem, + deleteAllCopyHistoryItems, + saveCopyHistoryItem, + }; }; diff --git a/common/app/services/chrome-extension.ts b/common/app/services/chrome-extension.ts index 0bdc6c76..e68067f3 100755 --- a/common/app/services/chrome-extension.ts +++ b/common/app/services/chrome-extension.ts @@ -112,6 +112,15 @@ export default class ChromeExtension { window.addEventListener('message', this.windowEventListener); } + + get supportsPauseOnHover() { + return this.installed && gte(this.version, '1.4.0'); + } + + get supportsSubtitlesWidthSetting() { + return this.installed && gte(this.version, '1.4.0'); + } + get supportsOrderableAnkiFields() { return this.installed && gte(this.version, '1.3.0'); } diff --git a/common/app/services/video-channel.ts b/common/app/services/video-channel.ts index 5b66f3a2..358316da 100755 --- a/common/app/services/video-channel.ts +++ b/common/app/services/video-channel.ts @@ -412,6 +412,7 @@ export default class VideoChannel { subtitleAlignment, subtitleTracksV2, subtitlePositionOffset, + subtitlesWidth, } = settings; const message: SubtitleSettingsToVideoMessage = { command: 'subtitleSettings', @@ -432,6 +433,7 @@ export default class VideoChannel { subtitleAlignment, subtitleTracksV2, subtitlePositionOffset, + subtitlesWidth, }, }; this.protocol.postMessage(message); @@ -548,6 +550,7 @@ export default class VideoChannel { language, lastSubtitleOffset, tabName, + pauseOnHoverMode, } = settings; const message: MiscSettingsToVideoMessage = { command: 'miscSettings', @@ -570,6 +573,7 @@ export default class VideoChannel { language, lastSubtitleOffset, tabName, + pauseOnHoverMode, }, }; this.protocol.postMessage(message); diff --git a/common/components/AnkiDialog.tsx b/common/components/AnkiDialog.tsx index 1a8b46d9..7280c2d3 100755 --- a/common/components/AnkiDialog.tsx +++ b/common/components/AnkiDialog.tsx @@ -140,19 +140,6 @@ const ValueLabelComponent = ({ children, open, value }: ValueLabelComponentProps ); }; -interface TextImageSetProps { - selectedSubtitles: SubtitleModel[]; - width: number; -} - -const useTextImageSetStyles = makeStyles((theme) => ({ - root: { - marginBottom: theme.spacing(1), - padding: theme.spacing(1), - backgroundColor: theme.palette.action.disabledBackground, - }, -})); - export interface AnkiDialogState { text: string; subtitle: SubtitleModel; diff --git a/common/components/AudioField.tsx b/common/components/AudioField.tsx index 77f548e7..3eec2222 100644 --- a/common/components/AudioField.tsx +++ b/common/components/AudioField.tsx @@ -57,7 +57,14 @@ export default function AudioField({ audioClip, onPlayAudio, onRerecord }: Props audioActionElement = ( - + { + e.stopPropagation(); + onRerecord(); + }} + edge="end" + > diff --git a/common/components/SettingsForm.tsx b/common/components/SettingsForm.tsx index 6fdffb62..f18ee1d9 100644 --- a/common/components/SettingsForm.tsx +++ b/common/components/SettingsForm.tsx @@ -4,8 +4,10 @@ import { makeStyles, useTheme } from '@material-ui/core/styles'; import AddIcon from '@material-ui/icons/Add'; import LockIcon from '@material-ui/icons/Lock'; import Box from '@material-ui/core/Box'; +import ClearIcon from '@material-ui/icons/Clear'; import EditIcon from '@material-ui/icons/Edit'; import InfoIcon from '@material-ui/icons/Info'; +import UndoIcon from '@material-ui/icons/Undo'; import FormControl from '@material-ui/core/FormControl'; import FormGroup from '@material-ui/core/FormGroup'; import FormLabel from '@material-ui/core/FormLabel'; @@ -33,6 +35,7 @@ import { AsbplayerSettings, CustomAnkiFieldSettings, KeyBindName, + PauseOnHoverMode, SubtitleListPreference, TextSubtitleSettings, changeForTextSubtitleSetting, @@ -40,7 +43,7 @@ import { textSubtitleSettingsAreDirty, textSubtitleSettingsForTrack, } from '@project/common/settings'; -import { computeStyles, download, isNumeric } from '@project/common/util'; +import { download, isNumeric } from '@project/common/util'; import { CustomStyle, validateSettings } from '@project/common/settings'; import { useOutsideClickListener } from '@project/common/hooks'; import TagsTextField from './TagsTextField'; @@ -66,7 +69,6 @@ import { WebSocketClient } from '../web-socket-client/web-socket-client'; import { isFirefox } from '@project/common/browser-detection'; import SubtitleAppearanceTrackSelector from './SubtitleAppearanceTrackSelector'; import SubtitlePreview from './SubtitlePreview'; -import UndoIcon from '@material-ui/icons/Undo'; interface StylesProps { smallScreen: boolean; @@ -250,9 +252,9 @@ function SelectableSetting({ onChange={onSelectionChange} > {selections && - selections.map((s) => ( + ['', ...selections].map((s) => ( - {s} + {s === '' ? ' ' : s} ))} @@ -635,6 +637,8 @@ interface Props { extensionSupportsSidePanel: boolean; extensionSupportsOrderableAnkiFields: boolean; extensionSupportsTrackSpecificSettings: boolean; + extensionSupportsSubtitlesWidthSetting: boolean; + extensionSupportsPauseOnHover: boolean; insideApp?: boolean; settings: AsbplayerSettings; scrollToId?: string; @@ -661,6 +665,8 @@ export default function SettingsForm({ extensionSupportsSidePanel, extensionSupportsOrderableAnkiFields, extensionSupportsTrackSpecificSettings, + extensionSupportsSubtitlesWidthSetting, + extensionSupportsPauseOnHover, insideApp, scrollToId, chromeKeyBinds, @@ -786,6 +792,7 @@ export default function SettingsForm({ subtitlePreview, subtitlePositionOffset, subtitleAlignment, + subtitlesWidth, audioPaddingStart, audioPaddingEnd, maxImageWidth, @@ -828,6 +835,7 @@ export default function SettingsForm({ streamingEnableOverlay, webSocketClientEnabled, webSocketServerUrl, + pauseOnHoverMode, } = settings; const [selectedSubtitleAppearanceTrack, setSelectedSubtitleAppearanceTrack] = useState(); @@ -944,7 +952,7 @@ export default function SettingsForm({ return; } - setFieldNames(['', ...(await anki.modelFieldNames(noteType, ankiConnectUrl))]); + setFieldNames(await anki.modelFieldNames(noteType, ankiConnectUrl)); setAnkiConnectUrlError(undefined); } catch (e) { if (canceled) { @@ -1836,6 +1844,25 @@ export default function SettingsForm({
)} + + {subtitleBlur !== undefined && ( + + { + handleSubtitleTextSettingChanged('subtitleBlur', e.target.checked); + }} + /> + } + label={t('settings.subtitleBlur')} + labelPlacement="start" + className={classes.switchLabel} + /> + + )} + {subtitleCustomStyles !== undefined && ( <> {subtitleCustomStyles.map((customStyle, index) => { @@ -1873,48 +1900,12 @@ export default function SettingsForm({ )} - {subtitleBlur !== undefined && ( - - { - handleSubtitleTextSettingChanged('subtitleBlur', e.target.checked); - }} - /> - } - label={t('settings.subtitleBlur')} - labelPlacement="start" - className={classes.switchLabel} - /> - - )} {selectedSubtitleAppearanceTrack === undefined && ( <>
- - handleSettingChanged( - 'imageBasedSubtitleScaleFactor', - Number(event.target.value) - ) - } - /> -
-
- {t('settings.subtitleAlignment')} + + {t('settings.subtitleAlignment')} +
+ {(!extensionInstalled || extensionSupportsSubtitlesWidthSetting) && ( +
+ { + const numberValue = Number(e.target.value); + + if ( + !Number.isNaN(numberValue) && + numberValue >= 0 && + numberValue <= 100 + ) { + handleSettingChanged('subtitlesWidth', numberValue); + } + }} + InputProps={{ + endAdornment: ( + <> + {subtitlesWidth === -1 && ( + + + handleSettingChanged('subtitlesWidth', 100) + } + > + + + + )} + {subtitlesWidth !== -1 && ( + <> + % + + + handleSettingChanged('subtitlesWidth', -1) + } + > + + + + + )} + + ), + }} + /> +
+ )} +
+ + handleSettingChanged( + 'imageBasedSubtitleScaleFactor', + Number(event.target.value) + ) + } + /> +
)} @@ -2353,6 +2419,54 @@ export default function SettingsForm({ /> + {(!extensionInstalled || extensionSupportsPauseOnHover) && ( + + + {t('settings.pauseOnHoverMode')} + + + + event.target.checked && + handleSettingChanged('pauseOnHoverMode', PauseOnHoverMode.disabled) + } + /> + } + label={t('pauseOnHoverMode.disabled')} + /> + + event.target.checked && + handleSettingChanged('pauseOnHoverMode', PauseOnHoverMode.inAndOut) + } + /> + } + label={t('pauseOnHoverMode.inAndOut')} + /> + + event.target.checked && + handleSettingChanged('pauseOnHoverMode', PauseOnHoverMode.inNotOut) + } + /> + } + label={t('pauseOnHoverMode.inNotOut')} + /> + + + )} +
subtitle
); diff --git a/common/copy-history/copy-history-repository.ts b/common/copy-history/copy-history-repository.ts index d88a29ba..c7e0392d 100644 --- a/common/copy-history/copy-history-repository.ts +++ b/common/copy-history/copy-history-repository.ts @@ -72,7 +72,7 @@ export class CopyHistoryRepository { } async clear() { - await this._db.delete(); + await this._db.copyHistoryItems.clear(); } async fetch(count: number): Promise { diff --git a/common/locales/de.json b/common/locales/de.json index 30cc3134..1750a769 100644 --- a/common/locales/de.json +++ b/common/locales/de.json @@ -34,6 +34,11 @@ "play": "Always play", "pause": "Always pause" }, + "pauseOnHoverMode": { + "disabled": "Disabled", + "inNotOut": "Enabled", + "inAndOut": "Enabled with auto-resume" + }, "ankiDialog": { "applySelection": "Auswahl anwenden", "audio": "Audio", @@ -141,7 +146,8 @@ "blank": "Leer", "downloadMinedSubsAsSrt": "Extrahierte Untertitel als SRT herunterladen", "exportToAnki": "Nach Anki exportieren", - "miningHistoryEmpty": "Keine Untertitel extrahiert" + "miningHistoryEmpty": "Keine Untertitel extrahiert", + "deleteAll": "Clear" }, "error": { "bothAudioAndVideNotAllowed": "Cannot load both an audio and video file simultaneously", @@ -323,6 +329,7 @@ "subtitleRegexFilterTextReplacement": "Untertitelfilter Ersetzung", "subtitleSize": "Untertitelgröße", "subtitleThickness": "Subtitle Font Thickness", + "subtitlesWidth": "Subtitles Width", "surroundingSubtitlesCountRadius": "Surrounding Subtitles Count Radius", "surroundingSubtitlesTimeRadius": "Surrounding Subtitles Time Radius", "tags": "Schlagwörter", @@ -338,7 +345,8 @@ "webSocketClientEnabled": "Enable WebSocket client", "webSocketServerUrl": "WebSocket Server URL", "firefoxExtensionShortcutHelp": "Edit this shortcut from the Plugin manager at about:addons.", - "postMinePlayback": "Post-mining playback state" + "postMinePlayback": "Post-mining playback state", + "pauseOnHoverMode": "Auto-pause when mousing over subtitles" }, "subtitlePlayer": { "multiSubtitleSelectHelp": "Click, hold, and drag to mine multiple subtitles" diff --git a/common/locales/en.json b/common/locales/en.json index 42343471..e5aaaac8 100644 --- a/common/locales/en.json +++ b/common/locales/en.json @@ -34,6 +34,11 @@ "play": "Always play", "pause": "Always pause" }, + "pauseOnHoverMode": { + "disabled": "Disabled", + "inNotOut": "Enabled", + "inAndOut": "Enabled with auto-resume" + }, "ankiDialog": { "applySelection": "Apply Selection", "audio": "Audio", @@ -141,7 +146,8 @@ "blank": "Blank", "downloadMinedSubsAsSrt": "Download Mined Subtitles as SRT", "exportToAnki": "Export to Anki", - "miningHistoryEmpty": "Mining history is empty." + "miningHistoryEmpty": "Mining history is empty.", + "deleteAll": "Clear" }, "error": { "bothAudioAndVideNotAllowed": "Cannot load both an audio and video file simultaneously", @@ -323,6 +329,7 @@ "subtitleRegexFilterTextReplacement": "Subtitle regex filter text replacement", "subtitleSize": "Subtitle Size", "subtitleThickness": "Subtitle Font Thickness", + "subtitlesWidth": "Subtitles Width", "surroundingSubtitlesCountRadius": "Surrounding Subtitles Count Radius", "surroundingSubtitlesTimeRadius": "Surrounding Subtitles Time Radius", "tags": "Tags", @@ -338,7 +345,8 @@ "webSocketClientEnabled": "Enable WebSocket client", "webSocketServerUrl": "WebSocket Server URL", "firefoxExtensionShortcutHelp": "Edit this shortcut from the Plugin manager at about:addons.", - "postMinePlayback": "Post-mining playback state" + "postMinePlayback": "Post-mining playback state", + "pauseOnHoverMode": "Auto-pause when mousing over subtitles" }, "subtitlePlayer": { "multiSubtitleSelectHelp": "Click, hold, and drag to mine multiple subtitles" diff --git a/common/locales/es.json b/common/locales/es.json index f658fc88..2878440d 100644 --- a/common/locales/es.json +++ b/common/locales/es.json @@ -34,6 +34,11 @@ "play": "Siempre reproducir", "pause": "Siempre pausar" }, + "pauseOnHoverMode": { + "disabled": "Disabled", + "inNotOut": "Enabled", + "inAndOut": "Enabled with auto-resume" + }, "ankiDialog": { "applySelection": "Aplicar Selección", "audio": "Audio", @@ -141,7 +146,8 @@ "blank": "En Blanco", "downloadMinedSubsAsSrt": "Descargar Subtítulos minados como SRT", "exportToAnki": "Exportar a Anki", - "miningHistoryEmpty": "El historial de minado está vacío." + "miningHistoryEmpty": "El historial de minado está vacío.", + "deleteAll": "Clear" }, "error": { "bothAudioAndVideNotAllowed": "No pueden cargarse archivos de audio y video simultáneamente", @@ -323,6 +329,7 @@ "subtitleRegexFilterTextReplacement": "Texto de reemplazo para el filtro regex", "subtitleSize": "Tamaño de los subtítulos", "subtitleThickness": "Grosor de la Fuente de los Subtítulos", + "subtitlesWidth": "Subtitles Width", "surroundingSubtitlesCountRadius": "Radio de Cantidad de Subtítulos Circundantes", "surroundingSubtitlesTimeRadius": "Radio Temporal para Subtítulos Circundantes", "tags": "Etiquetas", @@ -338,7 +345,8 @@ "webSocketClientEnabled": "Habilitar cliente WebSocket", "webSocketServerUrl": "URL del servidor WebSocket", "firefoxExtensionShortcutHelp": "Edita este acceso directo desde el gestor de plugins en about:addons.", - "postMinePlayback": "Estado de reproducción post-minado" + "postMinePlayback": "Estado de reproducción post-minado", + "pauseOnHoverMode": "Auto-pause when mousing over subtitles" }, "subtitlePlayer": { "multiSubtitleSelectHelp": "Haz click, mantén y arrastra para minar múltiples subtítulos" diff --git a/common/locales/ja.json b/common/locales/ja.json index 32a670d1..a5a59aad 100644 --- a/common/locales/ja.json +++ b/common/locales/ja.json @@ -34,6 +34,11 @@ "play": "常に再生する", "pause": "常に停止する" }, + "pauseOnHoverMode": { + "disabled": "無効", + "inNotOut": "有効", + "inAndOut": "有効。マウスカーソルを外すと自動再生。" + }, "ankiDialog": { "applySelection": "選択範囲を適用する", "audio": "音声", @@ -88,7 +93,7 @@ "extensionToggleRecording": "字幕ファイルがロードされていても手動で録音を開始/終了する。", "extensionUpdateLastCard": "最後に作成した Anki カードに asbplayer でキャプチャしたメディアを添付して更新する。動画が字幕ファイルなしで同期されている場合、録音を開始/停止する。", "increaseOffset": "字幕の表示タイミング +100ms", - "increasePlaybackRate": "再生速度を下げる", + "increasePlaybackRate": "再生速度を上げる", "resetOffset": "字幕の表示タイミングをリセット", "seekBackward": "10 秒巻き戻し", "seekBackwardOrForward": "10 秒巻き戻し/早送り", @@ -141,7 +146,8 @@ "blank": "空白", "downloadMinedSubsAsSrt": "マイニングした字幕を SRT 形式でダウンロードする", "exportToAnki": "Anki にエクスポートする", - "miningHistoryEmpty": "マイニング履歴はありませïん" + "miningHistoryEmpty": "マイニング履歴はありませïん", + "deleteAll": "Clear" }, "error": { "bothAudioAndVideNotAllowed": "音声と動画ファイルを同時にロードできません", @@ -244,9 +250,9 @@ "settings": { "activeProfile": "設定プロファイル", "defaultProfile": "デフォルト", - "newProfile": "新しい設定プロフィールを作成", - "profileName": "設定プロフィール名", - "enterProfileName": "設定プロフィール名を入力", + "newProfile": "新しい設定プロファイルを作成", + "profileName": "設定プロファイル名", + "enterProfileName": "設定プロファイル名を入力", "profileLimitReached": "設定プロファイルの上限数に達しました", "addCustomCss": "カスタムCSSを追加", "addCustomField": "カスタムフィールドを追加", @@ -323,6 +329,7 @@ "subtitleRegexFilterTextReplacement": "字幕の正規表現フィルタの置き換えテキスト", "subtitleSize": "字幕のサイズ", "subtitleThickness": "字幕のフォントの太さ", + "subtitlesWidth": "字幕の幅", "surroundingSubtitlesCountRadius": "前後に表示される字幕の最大数", "surroundingSubtitlesTimeRadius": "前後の字幕が表示される範囲(時間)", "tags": "タグ", @@ -338,7 +345,8 @@ "webSocketClientEnabled": "WebSocketクライアントを有効", "webSocketServerUrl": "WebSocketサーバーのURL", "firefoxExtensionShortcutHelp": "ショートカットの編集はプラグイン管理 (about:addons)で行えます。", - "postMinePlayback": "マイニング後のプレイバック状態" + "postMinePlayback": "マイニング後のプレイバック状態", + "pauseOnHoverMode": "マウスカーソルを字幕に重ねると自動一時停止" }, "subtitlePlayer": { "multiSubtitleSelectHelp": "複数の字幕をマイニングするには、クリックし、押し続け、ドラッグしてください" diff --git a/common/locales/pl.json b/common/locales/pl.json index 065a23a4..d00b6cad 100644 --- a/common/locales/pl.json +++ b/common/locales/pl.json @@ -34,6 +34,11 @@ "play": "Zawsze odtwarzaj", "pause": "Zawsze pauzuj" }, + "pauseOnHoverMode": { + "disabled": "Disabled", + "inNotOut": "Enabled", + "inAndOut": "Enabled with auto-resume" + }, "ankiDialog": { "applySelection": "Zastosuj wybór", "audio": "Audio", @@ -141,7 +146,8 @@ "blank": "Puste", "downloadMinedSubsAsSrt": "Pobierz wykopane napisy jako plik SRT", "exportToAnki": "Wyeksportuj do Anki", - "miningHistoryEmpty": "Historia kopania jest pusta." + "miningHistoryEmpty": "Historia kopania jest pusta.", + "deleteAll": "Clear" }, "error": { "bothAudioAndVideNotAllowed": "Nie można załadować plików audio i wideo naraz", @@ -323,6 +329,7 @@ "subtitleRegexFilterTextReplacement": "Zamiana tekstu z filtru Regex napisów", "subtitleSize": "Wielkość napisów", "subtitleThickness": "Grubość czcionki napisów", + "subtitlesWidth": "Subtitles Width", "surroundingSubtitlesCountRadius": "Limit liczby napisów otaczających", "surroundingSubtitlesTimeRadius": "Limit czasu napisów otaczających", "tags": "Tagi", @@ -338,7 +345,8 @@ "webSocketClientEnabled": "Włącz klienta WebSocket", "webSocketServerUrl": "Adres URL serwera WebSocket", "firefoxExtensionShortcutHelp": "Edytuj ten skrót z poziomu menedżera wtyczek na about:addons.", - "postMinePlayback": "Stan odtwarzania po kopaniu" + "postMinePlayback": "Stan odtwarzania po kopaniu", + "pauseOnHoverMode": "Auto-pause when mousing over subtitles" }, "subtitlePlayer": { "multiSubtitleSelectHelp": "Kliknij, przytrzymaj i przeciągnij, aby wykopać więcej linii napisów" diff --git a/common/locales/pt_BR.json b/common/locales/pt_BR.json index a093cea7..27ee0b94 100644 --- a/common/locales/pt_BR.json +++ b/common/locales/pt_BR.json @@ -34,6 +34,11 @@ "play": "Sempre reproduzir", "pause": "Sempre pausado" }, + "pauseOnHoverMode": { + "disabled": "Disabled", + "inNotOut": "Enabled", + "inAndOut": "Enabled with auto-resume" + }, "ankiDialog": { "applySelection": "Aplicar seleção", "audio": "Áudio", @@ -141,7 +146,8 @@ "blank": "Em branco", "downloadMinedSubsAsSrt": "Baixar legendas mineradas como SRT", "exportToAnki": "Exportar para o Anki", - "miningHistoryEmpty": "O histórico de mineração está vazio." + "miningHistoryEmpty": "O histórico de mineração está vazio.", + "deleteAll": "Clear" }, "error": { "bothAudioAndVideNotAllowed": "Não é possível carregar simultaneamente um arquivo de áudio e um arquivo de vídeo", @@ -323,6 +329,7 @@ "subtitleRegexFilterTextReplacement": "Substituição de texto do filtro regex da legenda", "subtitleSize": "Tamanho da legenda", "subtitleThickness": "Expessura da fonte da legenda", + "subtitlesWidth": "Subtitles Width", "surroundingSubtitlesCountRadius": "Raio de contagem de legendas ao redor", "surroundingSubtitlesTimeRadius": "Raio de tempo das legendas ao redor", "tags": "Tags", @@ -338,7 +345,8 @@ "webSocketClientEnabled": "Enable WebSocket client", "webSocketServerUrl": "WebSocket Server URL", "firefoxExtensionShortcutHelp": "Edit this shortcut from the Plugin manager at about:addons.", - "postMinePlayback": "Post-mining playback state" + "postMinePlayback": "Post-mining playback state", + "pauseOnHoverMode": "Auto-pause when mousing over subtitles" }, "subtitlePlayer": { "multiSubtitleSelectHelp": "Clique, segure e arraste para minerar múltiplas legendas" diff --git a/common/locales/ru.json b/common/locales/ru.json index e0c60519..eff5029d 100644 --- a/common/locales/ru.json +++ b/common/locales/ru.json @@ -34,6 +34,11 @@ "play": "Always play", "pause": "Always pause" }, + "pauseOnHoverMode": { + "disabled": "Disabled", + "inNotOut": "Enabled", + "inAndOut": "Enabled with auto-resume" + }, "ankiDialog": { "applySelection": "Применить выбор", "audio": "Аудио", @@ -141,7 +146,8 @@ "blank": "Пусто", "downloadMinedSubsAsSrt": "Скачать смайненные субтитры в формате SRT", "exportToAnki": "Экспортировать в Анки", - "miningHistoryEmpty": "История майнинга пуста." + "miningHistoryEmpty": "История майнинга пуста.", + "deleteAll": "Clear" }, "error": { "bothAudioAndVideNotAllowed": "Невозможно одновременно загрузить аудиофайл и видеофайл", @@ -323,6 +329,7 @@ "subtitleRegexFilterTextReplacement": "Замена текста для фильтра регулярных выражений для субтитров", "subtitleSize": "Размер субтитров", "subtitleThickness": "Толщина шрифта субтитров", + "subtitlesWidth": "Subtitles Width", "surroundingSubtitlesCountRadius": "Радиус количества окружающих субтитров", "surroundingSubtitlesTimeRadius": "Временной радиус окружающих субтитров", "tags": "Теги", @@ -338,7 +345,8 @@ "webSocketClientEnabled": "Enable WebSocket client", "webSocketServerUrl": "WebSocket Server URL", "firefoxExtensionShortcutHelp": "Edit this shortcut from the Plugin manager at about:addons.", - "postMinePlayback": "Post-mining playback state" + "postMinePlayback": "Post-mining playback state", + "pauseOnHoverMode": "Auto-pause when mousing over subtitles" }, "subtitlePlayer": { "multiSubtitleSelectHelp": "Нажмите, удерживайте и перетащите, чтобы смайнить несколько субтитров" diff --git a/common/locales/zh_CN.json b/common/locales/zh_CN.json index d8f87db1..46b7a1b2 100644 --- a/common/locales/zh_CN.json +++ b/common/locales/zh_CN.json @@ -34,6 +34,11 @@ "play": "Always play", "pause": "Always pause" }, + "pauseOnHoverMode": { + "disabled": "Disabled", + "inNotOut": "Enabled", + "inAndOut": "Enabled with auto-resume" + }, "ankiDialog": { "applySelection": "应用选中时间范围", "audio": "音频", @@ -141,7 +146,8 @@ "blank": "空白的", "downloadMinedSubsAsSrt": "下载已挖掘字幕作为SRT文件", "exportToAnki": "导出到Anki", - "miningHistoryEmpty": "挖掘历史记录为空。" + "miningHistoryEmpty": "挖掘历史记录为空。", + "deleteAll": "Clear" }, "error": { "bothAudioAndVideNotAllowed": "无法同时加载音频和视频文件", @@ -323,6 +329,7 @@ "subtitleRegexFilterTextReplacement": "字幕正则筛选器文本替换", "subtitleSize": "字幕大小", "subtitleThickness": "字幕字体厚度", + "subtitlesWidth": "Subtitles Width", "surroundingSubtitlesCountRadius": "环绕字幕计数半径", "surroundingSubtitlesTimeRadius": "环绕字幕时间半径", "tags": "标签", @@ -338,7 +345,8 @@ "webSocketClientEnabled": "Enable WebSocket client", "webSocketServerUrl": "WebSocket Server URL", "firefoxExtensionShortcutHelp": "Edit this shortcut from the Plugin manager at about:addons.", - "postMinePlayback": "Post-mining playback state" + "postMinePlayback": "Post-mining playback state", + "pauseOnHoverMode": "Auto-pause when mousing over subtitles" }, "subtitlePlayer": { "multiSubtitleSelectHelp": "Click, hold, and drag to mine multiple subtitles" diff --git a/common/settings/settings-import-export.test.ts b/common/settings/settings-import-export.test.ts index 09b7e31e..79f243ee 100644 --- a/common/settings/settings-import-export.test.ts +++ b/common/settings/settings-import-export.test.ts @@ -1,3 +1,4 @@ +import { PauseOnHoverMode } from './settings'; import { validateSettings } from './settings-import-export'; import { defaultSettings } from './settings-provider'; @@ -132,5 +133,6 @@ it('validates exported settings', () => { streamingCondensedPlaybackMinimumSkipIntervalMs: 1000, streamingScreenshotDelay: 1000, streamingSubtitleListPreference: 'app', + pauseOnHoverMode: PauseOnHoverMode.disabled, }); }); diff --git a/common/settings/settings-import-export.ts b/common/settings/settings-import-export.ts index 05d89b8d..aa80aa2b 100644 --- a/common/settings/settings-import-export.ts +++ b/common/settings/settings-import-export.ts @@ -332,6 +332,9 @@ const settingsSchema = { }, }, }, + subtitlesWidth: { + type: 'number', + }, streamingAppUrl: { type: 'string', }, @@ -383,6 +386,9 @@ const settingsSchema = { webSocketServerUrl: { type: 'string', }, + pauseOnHoverMode: { + type: 'number', + }, _schema: { type: 'number', }, diff --git a/common/settings/settings-provider.test.ts b/common/settings/settings-provider.test.ts index 25d5c120..9350b7d0 100644 --- a/common/settings/settings-provider.test.ts +++ b/common/settings/settings-provider.test.ts @@ -170,6 +170,7 @@ const subtitleSettings = { imageBasedSubtitleScaleFactor: 1, subtitlePositionOffset: 70, subtitleAlignment: 'top' as SubtitleAlignment, + subtitlesWidth: 100, subtitleTracksV2: [ { subtitleSize: 36, diff --git a/common/settings/settings-provider.ts b/common/settings/settings-provider.ts index 47cdbcec..3c839c1e 100755 --- a/common/settings/settings-provider.ts +++ b/common/settings/settings-provider.ts @@ -62,6 +62,7 @@ export const defaultSettings: AsbplayerSettings = { subtitlePositionOffset: 75, subtitleAlignment: 'bottom', subtitleTracksV2: [], + subtitlesWidth: -1, audioPaddingStart: 0, audioPaddingEnd: 500, maxImageWidth: 0, @@ -138,6 +139,7 @@ export const defaultSettings: AsbplayerSettings = { streamingEnableOverlay: true, webSocketClientEnabled: false, webSocketServerUrl: 'ws://127.0.0.1:8766/ws', + pauseOnHoverMode: 0, }; export interface AnkiFieldUiModel { diff --git a/common/settings/settings.ts b/common/settings/settings.ts index 0a2a1933..c644848a 100755 --- a/common/settings/settings.ts +++ b/common/settings/settings.ts @@ -1,5 +1,11 @@ import { AutoPausePreference, PostMineAction, PostMinePlayback } from '../src/model'; +export enum PauseOnHoverMode { + disabled = 0, + inAndOut = 1, + inNotOut = 2, +} + export interface MiscSettings { readonly themeType: 'dark' | 'light'; readonly copyToClipboardOnMine: boolean; @@ -19,8 +25,21 @@ export interface MiscSettings { readonly postMiningPlaybackState: PostMinePlayback; readonly lastSubtitleOffset: number; readonly tabName: string; + readonly pauseOnHoverMode: PauseOnHoverMode; } +export type AnkiSettingsFieldKey = + | 'sentenceField' + | 'definitionField' + | 'audioField' + | 'imageField' + | 'wordField' + | 'sourceField' + | 'urlField' + | 'track1Field' + | 'track2Field' + | 'track3Field'; + export interface AnkiSettings { readonly ankiConnectUrl: string; readonly deck: string; @@ -133,6 +152,7 @@ const subtitleSettingsKeysObject: { [key in keyof SubtitleSettings]: boolean } = subtitlePositionOffset: true, subtitleAlignment: true, subtitleTracksV2: true, + subtitlesWidth: true, }; export const subtitleSettingsKeys: (keyof SubtitleSettings)[] = Object.keys( @@ -172,6 +192,9 @@ export interface SubtitleSettings extends TextSubtitleSettings { // We don't configure track 0 here to avoid having to migrate old settings into this new data structure. // Track 0 continues to be configured from the top-level settings object. readonly subtitleTracksV2: TextSubtitleSettings[]; + + // Percentage of containing video width; -1 means 'auto' + readonly subtitlesWidth: number; } export interface KeyBind { diff --git a/common/src/model.ts b/common/src/model.ts index 05c7e74c..bf5cc9bb 100755 --- a/common/src/model.ts +++ b/common/src/model.ts @@ -133,7 +133,7 @@ export interface AnkiUiSavedState { dialogRequestedTimestamp: number; } -export interface VideoDataSubtitleTrack { +export interface VideoDataSubtitleTrackDef { label: string; language: string; url: string; @@ -141,12 +141,12 @@ export interface VideoDataSubtitleTrack { extension: string; } -export interface ConfirmedVideoDataSubtitleTrack { +export interface VideoDataSubtitleTrack extends VideoDataSubtitleTrackDef { + id: string; +} + +export interface ConfirmedVideoDataSubtitleTrack extends VideoDataSubtitleTrack { name: string; - language: string; - subtitleUrl: string; - m3U8BaseUrl?: string; - extension: string; } export interface VideoData { diff --git a/common/subtitle-collection/subtitle-collection.ts b/common/subtitle-collection/subtitle-collection.ts index cbc9ef41..d05ad374 100644 --- a/common/subtitle-collection/subtitle-collection.ts +++ b/common/subtitle-collection/subtitle-collection.ts @@ -37,17 +37,19 @@ export class SubtitleCollection { } for (const s of subtitles) { - this.tree.insert([s.start, s.end], s); + if (s.start < s.end) { + this.tree.insert([s.start, s.end - 1], s); + } if (last !== undefined && last.end < s.start) { - this.gapsTree.insert([last.end + 1, s.start - 1], last); + this.gapsTree.insert([last.end, s.start - 1], last); } last = s; } } else { for (const s of subtitles) { - this.tree.insert([s.start, s.end], s); + this.tree.insert([s.start, s.end - 1], s); } } } @@ -80,7 +82,7 @@ export class SubtitleCollection { } } else if (this.options.showingCheckRadiusMs !== undefined) { for (const s of showing) { - if (willStopShowing === undefined && s.end < timestamp + this.options.showingCheckRadiusMs) { + if (willStopShowing === undefined && s.end <= timestamp + this.options.showingCheckRadiusMs) { willStopShowing = s; } diff --git a/extension/src/background.ts b/extension/src/background.ts index 1238adff..7811d986 100755 --- a/extension/src/background.ts +++ b/extension/src/background.ts @@ -85,6 +85,7 @@ const installListener = async (details: chrome.runtime.InstalledDetails) => { streamingTakeScreenshot: false, // Kiwi Browser does not support captureVisibleTab subtitleSize: 18, subtitlePositionOffset: 25, + subtitlesWidth: 100, }); } diff --git a/extension/src/controllers/anki-ui-controller.ts b/extension/src/controllers/anki-ui-controller.ts index bb4900c9..ec1b330c 100755 --- a/extension/src/controllers/anki-ui-controller.ts +++ b/extension/src/controllers/anki-ui-controller.ts @@ -80,7 +80,7 @@ export default class AnkiUiController { this._prepareShow(context); const client = await this._client(context); - const url = context.url; + const url = context.url(subtitle.start, subtitle.end); const themeType = await context.settings.getSingle('themeType'); const state: AnkiUiInitialState = { diff --git a/extension/src/controllers/mobile-video-overlay-controller.ts b/extension/src/controllers/mobile-video-overlay-controller.ts index 1d28f456..d83d271c 100644 --- a/extension/src/controllers/mobile-video-overlay-controller.ts +++ b/extension/src/controllers/mobile-video-overlay-controller.ts @@ -6,12 +6,14 @@ import { VideoToExtensionCommand, } from '@project/common'; import Binding from '../services/binding'; -import { CachingElementOverlay, ElementOverlay, OffsetAnchor } from '../services/element-overlay'; +import { CachingElementOverlay, OffsetAnchor } from '../services/element-overlay'; import { adjacentSubtitle } from '@project/common/key-binder'; +const smallScreenVideoHeighThreshold = 300; + export class MobileVideoOverlayController { private readonly _context: Binding; - private _overlay: ElementOverlay; + private _overlay: CachingElementOverlay; private _pauseListener?: () => void; private _playListener?: () => void; private _seekedListener?: () => void; @@ -24,6 +26,7 @@ export class MobileVideoOverlayController { sendResponse: (response?: any) => void ) => void; private _bound = false; + private _smallScreen = false; constructor(context: Binding, offsetAnchor: OffsetAnchor) { this._context = context; @@ -43,6 +46,8 @@ export class MobileVideoOverlayController { fullscreenContentClassName: 'asbplayer-mobile-video-overlay', offsetAnchor, contentPositionOffset: 8, + contentWidthPercentage: -1, + onMouseOver: () => {}, }); } @@ -186,15 +191,25 @@ export class MobileVideoOverlayController { private _doShow() { const anchor = this._overlay.offsetAnchor === OffsetAnchor.bottom ? 'bottom' : 'top'; + const smallScreen = this._context.video.getBoundingClientRect().height < smallScreenVideoHeighThreshold; + const height = smallScreen ? 64 : 108; + const tooltips = !smallScreen; + + if (smallScreen !== this._smallScreen) { + this._overlay.uncacheHtml(); + this._smallScreen = smallScreen; + } + this._overlay.setHtml([ { key: 'ui', html: () => - `