Skip to content

Commit 9df8056

Browse files
authored
Duck Player: Add support for new YouTube Player UI links (#1753)
1 parent 77f59d6 commit 9df8056

File tree

7 files changed

+101
-14
lines changed

7 files changed

+101
-14
lines changed

special-pages/pages/duckplayer/app/components/Components.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ export function Components() {
2222
platform: { name: 'macos' },
2323
customError: { state: 'enabled' },
2424
});
25-
let embed = EmbedSettings.fromHref('https://localhost?videoID=123');
25+
let embed = /** @type {EmbedSettings} */ (EmbedSettings.fromHref('https://localhost?videoID=123'));
2626
let url = embed?.toEmbedUrl();
2727
if (!url) throw new Error('unreachable');
2828
return (
@@ -76,7 +76,7 @@ export function Components() {
7676
</h2>
7777
<SettingsProvider settings={settings}>
7878
<PlayerContainer>
79-
<Player src={url} layout={'desktop'} />
79+
<Player src={url} layout={'desktop'} embed={embed} />
8080
<InfoBarContainer>
8181
<InfoBar embed={embed} />
8282
</InfoBarContainer>

special-pages/pages/duckplayer/app/components/DesktopApp.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ function DesktopLayout({ embed }) {
4040
<PlayerContainer>
4141
{embed === null && <PlayerError layout={'desktop'} kind={'invalid-id'} />}
4242
{embed !== null && showCustomError && <YouTubeError layout={'desktop'} embed={embed} />}
43-
{embed !== null && !showCustomError && <Player src={embed.toEmbedUrl()} layout={'desktop'} />}
43+
{embed !== null && !showCustomError && <Player src={embed.toEmbedUrl()} layout={'desktop'} embed={embed} />}
4444
<HideInFocusMode style={'slide'}>
4545
<InfoBarContainer>
4646
<InfoBar embed={embed} />

special-pages/pages/duckplayer/app/components/MobileApp.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ export function MobileApp({ embed }) {
2424
const settings = useSettings();
2525
const telemetry = useTelemetry();
2626
const showCustomError = useShowCustomError();
27-
2827
const features = createAppFeaturesFrom(settings);
28+
2929
return (
3030
<>
3131
{!showCustomError && features.focusMode()}
@@ -65,7 +65,7 @@ function MobileLayout({ embed }) {
6565
<div class={styles.embed}>
6666
{embed === null && <PlayerError layout={'mobile'} kind={'invalid-id'} />}
6767
{embed !== null && showCustomError && <YouTubeError layout={'mobile'} embed={embed} />}
68-
{embed !== null && !showCustomError && <Player src={embed.toEmbedUrl()} layout={'mobile'} />}
68+
{embed !== null && !showCustomError && <Player src={embed.toEmbedUrl()} layout={'mobile'} embed={embed} />}
6969
</div>
7070
<div class={cn(styles.logo, styles.hideInFocus)}>
7171
<MobileWordmark />

special-pages/pages/duckplayer/app/components/Player.jsx

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,25 @@ import { h } from 'preact';
22
import cn from 'classnames';
33
import styles from './Player.module.css';
44
import { useEffect, useRef } from 'preact/hooks';
5-
import { useSettings } from '../providers/SettingsProvider.jsx';
5+
import { useSettings, useOpenOnYoutubeHandler } from '../providers/SettingsProvider.jsx';
66
import { createIframeFeatures } from '../features/iframe.js';
77
import { Settings } from '../settings';
88
import { useTypedTranslation } from '../types.js';
99

10+
/**
11+
* @import {EmbedSettings} from '../embed-settings.js';
12+
*/
13+
1014
/**
1115
* Player component renders an embedded media player.
1216
*
1317
* @param {object} props
1418
* @param {string} props.src - The source URL of the media to be played.
1519
* @param {Settings['layout']} props.layout
20+
* @param {EmbedSettings} props.embed
1621
*/
17-
export function Player({ src, layout }) {
18-
const { ref, didLoad } = useIframeEffects(src);
22+
export function Player({ src, layout, embed }) {
23+
const { ref, didLoad } = useIframeEffects(src, embed);
1924
const wrapperClasses = cn({
2025
[styles.root]: true,
2126
[styles.player]: true,
@@ -80,20 +85,22 @@ export function PlayerError({ kind, layout }) {
8085
* When either event occurs, we proceed to apply our list of features.
8186
*
8287
* @param {string} src - the iframe `src` attribute
88+
* @param {EmbedSettings} embed
8389
* @return {{
8490
* ref: import("preact/hooks").MutableRef<HTMLIFrameElement|null>,
8591
* didLoad: () => void
8692
* }}
8793
*/
88-
function useIframeEffects(src) {
94+
function useIframeEffects(src, embed) {
8995
const ref = useRef(/** @type {HTMLIFrameElement|null} */ (null));
9096
const didLoad = useRef(/** @type {boolean} */ (false));
9197
const settings = useSettings();
98+
const openOnYoutube = useOpenOnYoutubeHandler();
9299

93100
useEffect(() => {
94101
if (!ref.current) return;
95102
const iframe = ref.current;
96-
const features = createIframeFeatures(settings);
103+
const features = createIframeFeatures(settings, embed);
97104

98105
/** @type {import("../features/iframe.js").IframeFeature[]} */
99106
const iframeFeatures = [
@@ -103,6 +110,7 @@ function useIframeEffects(src) {
103110
features.titleCapture(),
104111
features.mouseCapture(),
105112
features.errorDetection(),
113+
features.replaceWatchLinks(() => openOnYoutube(embed)),
106114
];
107115

108116
/**
@@ -131,7 +139,7 @@ function useIframeEffects(src) {
131139
}
132140
iframe.removeEventListener('load', loadHandler);
133141
};
134-
}, [src, settings]);
142+
}, [src, settings, embed]);
135143

136144
return { ref, didLoad: () => (didLoad.current = true) };
137145
}

special-pages/pages/duckplayer/app/features/iframe.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ import { ClickCapture } from './click-capture.js';
44
import { TitleCapture } from './title-capture.js';
55
import { MouseCapture } from './mouse-capture.js';
66
import { ErrorDetection } from './error-detection.js';
7+
import { ReplaceWatchLinks } from './replace-watch-links.js';
8+
9+
/**
10+
* @import {EmbedSettings} from '../embed-settings.js';
11+
*/
712

813
/**
914
* Represents an individual piece of functionality in the iframe.
@@ -36,9 +41,9 @@ export class IframeFeature {
3641
* global `Settings`
3742
*
3843
* @param {import("../settings").Settings} settings
39-
* @returns {Record<string, () => IframeFeature>}
44+
* @param {EmbedSettings} embed
4045
*/
41-
export function createIframeFeatures(settings) {
46+
export function createIframeFeatures(settings, embed) {
4247
return {
4348
/**
4449
* @return {IframeFeature}
@@ -81,5 +86,12 @@ export function createIframeFeatures(settings) {
8186
errorDetection: () => {
8287
return new ErrorDetection(settings.customError);
8388
},
89+
/**
90+
* @param {() => void} handler - what to invoke when a watch-link was clicked
91+
* @return {IframeFeature}
92+
*/
93+
replaceWatchLinks: (handler) => {
94+
return new ReplaceWatchLinks(embed.videoId.id, handler);
95+
},
8496
};
8597
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/**
2+
* @typedef {import("./iframe").IframeFeature} IframeFeature
3+
*/
4+
5+
import { VideoParams } from 'injected/src/features/duckplayer/util';
6+
7+
/**
8+
* @implements IframeFeature
9+
*/
10+
export class ReplaceWatchLinks {
11+
/**
12+
* @param {string} videoId
13+
* @param {() => void} handler - what to invoke when a watch-link was clicked
14+
*/
15+
constructor(videoId, handler) {
16+
this.videoId = videoId;
17+
this.handler = handler;
18+
}
19+
/**
20+
* @param {HTMLIFrameElement} iframe
21+
*/
22+
iframeDidLoad(iframe) {
23+
const doc = iframe.contentDocument;
24+
const win = iframe.contentWindow;
25+
26+
if (!doc) {
27+
console.log('could not access contentDocument');
28+
return () => {};
29+
}
30+
31+
if (win && doc) {
32+
doc.addEventListener(
33+
'click',
34+
(e) => {
35+
if (!(e.target instanceof /** @type {any} */ (win).Element)) return;
36+
37+
/** @type {HTMLLinkElement|null} */
38+
const closestLink = /** @type {Element} */ (e.target).closest('a[href]');
39+
if (closestLink && this.isWatchLink(closestLink.href)) {
40+
e.preventDefault();
41+
e.stopPropagation();
42+
this.handler();
43+
}
44+
},
45+
{
46+
capture: true,
47+
},
48+
);
49+
} else {
50+
console.warn('could not access iframe?.contentWindow && iframe?.contentDocument');
51+
}
52+
53+
return null;
54+
}
55+
56+
/**
57+
* @param {string} href
58+
* @return {boolean}
59+
*/
60+
isWatchLink(href) {
61+
const videoParams = VideoParams.forWatchPage(href);
62+
return videoParams?.id === this.videoId;
63+
}
64+
}

special-pages/pages/duckplayer/app/providers/SettingsProvider.jsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@ import { createContext } from 'preact';
33
import { Settings } from '../settings';
44
import { useContext } from 'preact/hooks';
55
import { useMessaging } from '../types.js';
6-
import { EmbedSettings } from '../embed-settings';
76

87
const SettingsContext = createContext(/** @type {{settings: Settings}} */ ({}));
98

9+
/**
10+
* @import {EmbedSettings} from '../embed-settings.js';
11+
*/
12+
1013
/**
1114
* @param {object} params
1215
* @param {Settings} params.settings

0 commit comments

Comments
 (0)