Skip to content

Commit bd88071

Browse files
authored
Duck Player custom error copy update (#1706)
1 parent 5adf04d commit bd88071

37 files changed

+584
-285
lines changed

injected/src/features/duckplayer-native/custom-error/custom-error.css

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,8 +140,8 @@
140140
display: block;
141141
background-image: url("data:image/svg+xml,%3Csvg fill='none' viewBox='0 0 96 96' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='red' d='M47.5 70.802c1.945 0 3.484-1.588 3.841-3.5C53.076 58.022 61.218 51 71 51h4.96c2.225 0 4.04-1.774 4.04-4 0-.026-.007-9.022-1.338-14.004a8.02 8.02 0 0 0-5.659-5.658C68.014 26 48 26 48 26s-20.015 0-25.004 1.338a8.01 8.01 0 0 0-5.658 5.658C16 37.986 16 48.401 16 48.401s0 10.416 1.338 15.405a8.01 8.01 0 0 0 5.658 5.658c4.99 1.338 24.504 1.338 24.504 1.338'/%3E%3Cpath fill='%23fff' d='m41.594 58 16.627-9.598-16.627-9.599z'/%3E%3Cpath fill='%23EB102D' d='M87 71c0 8.837-7.163 16-16 16s-16-7.163-16-16 7.163-16 16-16 16 7.163 16 16'/%3E%3Cpath fill='%23fff' d='M73 77.8a2 2 0 1 1-4 0 2 2 0 0 1 4 0m-2.039-4.4c-.706 0-1.334-.49-1.412-1.12l-.942-8.75c-.079-.7.55-1.33 1.412-1.33h1.962c.785 0 1.492.63 1.413 1.33l-.942 8.75c-.157.63-.784 1.12-1.49 1.12Z'/%3E%3Cpath fill='%23CCC' d='M92.501 59c.298 0 .595.12.823.354.454.468.454 1.23 0 1.698l-2.333 2.4a1.145 1.145 0 0 1-1.65 0 1.227 1.227 0 0 1 0-1.698l2.333-2.4c.227-.234.524-.354.822-.354zm-1.166 10.798h3.499c.641 0 1.166.54 1.166 1.2s-.525 1.2-1.166 1.2h-3.499c-.641 0-1.166-.54-1.166-1.2s.525-1.2 1.166-1.2m-1.982 8.754c.227-.234.525-.354.822-.354h.006c.297 0 .595.12.822.354l2.332 2.4c.455.467.455 1.23 0 1.697a1.145 1.145 0 0 1-1.65 0l-2.332-2.4a1.227 1.227 0 0 1 0-1.697'/%3E%3C/svg%3E%0A");
142142
background-repeat: no-repeat;
143-
height: 48px;
144-
width: 48px;
143+
height: 96px;
144+
width: 96px;
145145
}
146146

147147
@media screen and (max-width: 320px) {
Lines changed: 5 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Logger } from '../duckplayer/util.js';
2+
import { checkForError, getErrorType } from './youtube-errors.js';
23

34
/**
45
* @import {DuckPlayerNativeSettings} from "@duckduckgo/privacy-configuration/schema/features/duckplayer-native.js"
@@ -14,14 +15,6 @@ import { Logger } from '../duckplayer/util.js';
1415
* @property {boolean} testMode
1516
*/
1617

17-
/** @type {Record<string,YouTubeError>} */
18-
export const YOUTUBE_ERRORS = {
19-
ageRestricted: 'age-restricted',
20-
signInRequired: 'sign-in-required',
21-
noEmbed: 'no-embed',
22-
unknown: 'unknown',
23-
};
24-
2518
/**
2619
* Detects YouTube errors based on DOM queries
2720
*/
@@ -59,8 +52,8 @@ export class ErrorDetection {
5952
const documentBody = document?.body;
6053
if (documentBody) {
6154
// Check if iframe already contains error
62-
if (this.checkForError(documentBody)) {
63-
const error = this.getErrorType();
55+
if (checkForError(this.selectors.youtubeError, documentBody)) {
56+
const error = getErrorType(window, this.selectors.signInRequiredError, this.logger);
6457
this.handleError(error);
6558
return;
6659
}
@@ -102,87 +95,13 @@ export class ErrorDetection {
10295
for (const mutation of mutationsList) {
10396
if (mutation.type === 'childList') {
10497
mutation.addedNodes.forEach((node) => {
105-
if (this.checkForError(node)) {
98+
if (checkForError(this.selectors.youtubeError, node)) {
10699
this.logger.log('A node with an error has been added to the document:', node);
107-
const error = this.getErrorType();
100+
const error = getErrorType(window, this.selectors.signInRequiredError, this.logger);
108101
this.handleError(error);
109102
}
110103
});
111104
}
112105
}
113106
}
114-
115-
/**
116-
* Attempts to detect the type of error in the YouTube embed iframe
117-
* @returns {YouTubeError}
118-
*/
119-
getErrorType() {
120-
const currentWindow = /** @type {Window & typeof globalThis & { ytcfg: object }} */ (window);
121-
let playerResponse;
122-
123-
if (!currentWindow.ytcfg) {
124-
this.logger.warn('ytcfg missing!');
125-
} else {
126-
this.logger.log('Got ytcfg', currentWindow.ytcfg);
127-
}
128-
129-
try {
130-
const playerResponseJSON = currentWindow.ytcfg?.get('PLAYER_VARS')?.embedded_player_response;
131-
this.logger.log('Player response', playerResponseJSON);
132-
133-
playerResponse = JSON.parse(playerResponseJSON);
134-
} catch (e) {
135-
this.logger.log('Could not parse player response', e);
136-
}
137-
138-
if (typeof playerResponse === 'object') {
139-
const {
140-
previewPlayabilityStatus: { desktopLegacyAgeGateReason, status },
141-
} = playerResponse;
142-
143-
// 1. Check for UNPLAYABLE status
144-
if (status === 'UNPLAYABLE') {
145-
// 1.1. Check for presence of desktopLegacyAgeGateReason
146-
if (desktopLegacyAgeGateReason === 1) {
147-
this.logger.log('AGE RESTRICTED ERROR');
148-
return YOUTUBE_ERRORS.ageRestricted;
149-
}
150-
151-
// 1.2. Fall back to embed not allowed error
152-
this.logger.log('NO EMBED ERROR');
153-
return YOUTUBE_ERRORS.noEmbed;
154-
}
155-
}
156-
157-
// 2. Check for sign-in support link
158-
try {
159-
if (document.querySelector(this.selectors.signInRequiredError)) {
160-
this.logger.log('SIGN-IN ERROR');
161-
return YOUTUBE_ERRORS.signInRequired;
162-
}
163-
} catch (e) {
164-
this.logger.log('Sign-in required query failed', e);
165-
}
166-
167-
// 3. Fall back to unknown error
168-
this.logger.log('UNKNOWN ERROR');
169-
return YOUTUBE_ERRORS.unknown;
170-
}
171-
172-
/**
173-
* Analyses a node and its children to determine if it contains an error state
174-
*
175-
* @param {Node} [node]
176-
*/
177-
checkForError(node) {
178-
if (node?.nodeType === Node.ELEMENT_NODE) {
179-
const { youtubeError } = this.selectors;
180-
const element = /** @type {HTMLElement} */ (node);
181-
// Check if element has the error class or contains any children with that class
182-
const isError = element.matches(youtubeError) || !!element.querySelector(youtubeError);
183-
return isError;
184-
}
185-
186-
return false;
187-
}
188107
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/**
2+
* @import {DuckPlayerNativeSettings} from "@duckduckgo/privacy-configuration/schema/features/duckplayer-native.js"
3+
* @import {Logger} from "../duckplayer/util.js"
4+
* @typedef {"age-restricted" | "sign-in-required" | "no-embed" | "unknown"} YouTubeError
5+
*/
6+
7+
export const YOUTUBE_ERROR_EVENT = 'ddg-duckplayer-youtube-error';
8+
9+
/** @type {Record<string,YouTubeError>} */
10+
export const YOUTUBE_ERRORS = {
11+
ageRestricted: 'age-restricted',
12+
signInRequired: 'sign-in-required',
13+
noEmbed: 'no-embed',
14+
unknown: 'unknown',
15+
};
16+
17+
/** @type {YouTubeError[]} */
18+
export const YOUTUBE_ERROR_IDS = Object.values(YOUTUBE_ERRORS);
19+
20+
/**
21+
* Analyses a node and its children to determine if it contains an error state
22+
*
23+
* @param {string} errorSelector
24+
* @param {Node} [node]
25+
*/
26+
export function checkForError(errorSelector, node) {
27+
if (node?.nodeType === Node.ELEMENT_NODE) {
28+
const element = /** @type {HTMLElement} */ (node);
29+
// Check if element has the error class or contains any children with that class
30+
const isError = element.matches(errorSelector) || !!element.querySelector(errorSelector);
31+
return isError;
32+
}
33+
34+
return false;
35+
}
36+
37+
/**
38+
* Attempts to detect the type of error in the YouTube embed iframe
39+
* @param {Window|null} windowObject
40+
* @param {string} [signInRequiredSelector]
41+
* @param {Logger} [logger]
42+
* @returns {YouTubeError}
43+
*/
44+
export function getErrorType(windowObject, signInRequiredSelector, logger) {
45+
const currentWindow = /** @type {Window & typeof globalThis & { ytcfg: object }} */ (windowObject);
46+
const currentDocument = currentWindow.document;
47+
48+
if (!currentWindow || !currentDocument) {
49+
logger?.warn('Window or document missing!');
50+
return YOUTUBE_ERRORS.unknown;
51+
}
52+
53+
let playerResponse;
54+
55+
if (!currentWindow.ytcfg) {
56+
logger?.warn('ytcfg missing!');
57+
} else {
58+
logger?.log('Got ytcfg', currentWindow.ytcfg);
59+
}
60+
61+
try {
62+
const playerResponseJSON = currentWindow.ytcfg?.get('PLAYER_VARS')?.embedded_player_response;
63+
logger?.log('Player response', playerResponseJSON);
64+
65+
playerResponse = JSON.parse(playerResponseJSON);
66+
} catch (e) {
67+
logger?.log('Could not parse player response', e);
68+
}
69+
70+
if (typeof playerResponse === 'object') {
71+
const {
72+
previewPlayabilityStatus: { desktopLegacyAgeGateReason, status },
73+
} = playerResponse;
74+
75+
// 1. Check for UNPLAYABLE status
76+
if (status === 'UNPLAYABLE') {
77+
// 1.1. Check for presence of desktopLegacyAgeGateReason
78+
if (desktopLegacyAgeGateReason === 1) {
79+
logger?.log('AGE RESTRICTED ERROR');
80+
return YOUTUBE_ERRORS.ageRestricted;
81+
}
82+
83+
// 1.2. Fall back to embed not allowed error
84+
logger?.log('NO EMBED ERROR');
85+
return YOUTUBE_ERRORS.noEmbed;
86+
}
87+
}
88+
89+
// 2. Check for sign-in support link
90+
try {
91+
if (signInRequiredSelector && !!currentDocument.querySelector(signInRequiredSelector)) {
92+
logger?.log('SIGN-IN ERROR');
93+
return YOUTUBE_ERRORS.signInRequired;
94+
}
95+
} catch (e) {
96+
logger?.log('Sign-in required query failed', e);
97+
}
98+
99+
// 3. Fall back to unknown error
100+
logger?.log('UNKNOWN ERROR');
101+
return YOUTUBE_ERRORS.unknown;
102+
}

injected/src/messages/duckplayer-native/youtubeError.json

Whitespace-only changes.

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

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,26 @@ import styles from './Button.module.css';
77
* @param {object} props
88
* @param {import("preact").ComponentChild} props.children
99
* @param {"mobile" | "desktop"} [props.formfactor]
10+
* @param {"standard" | "accent"} [props.variant]
1011
* @param {boolean} [props.icon]
1112
* @param {boolean} [props.fill]
1213
* @param {boolean} [props.highlight]
1314
* @param {import("preact").ComponentProps<"button">} [props.buttonProps]
1415
*/
15-
export function Button({ children, formfactor = 'mobile', icon = false, fill = false, highlight = false, buttonProps = {} }) {
16+
export function Button({
17+
children,
18+
formfactor = 'mobile',
19+
variant = 'standard',
20+
icon = false,
21+
fill = false,
22+
highlight = false,
23+
buttonProps = {},
24+
}) {
1625
const classes = cn({
1726
[styles.button]: true,
1827
[styles.desktop]: formfactor === 'desktop',
1928
[styles.highlight]: highlight === true,
29+
[styles.accent]: variant === 'accent',
2030
[styles.fill]: fill === true,
2131
[styles.iconOnly]: icon === true,
2232
});
@@ -58,3 +68,18 @@ export function Icon({ src }) {
5868
</span>
5969
);
6070
}
71+
72+
export function OpenInIcon() {
73+
return (
74+
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" className={styles.svgIcon}>
75+
<path
76+
fill="currentColor"
77+
d="M7.361 1.013a.626.626 0 0 1 0 1.224l-.126.013H5A2.75 2.75 0 0 0 2.25 5v6A2.75 2.75 0 0 0 5 13.75h6A2.75 2.75 0 0 0 13.75 11V8.765a.625.625 0 0 1 1.25 0V11a4 4 0 0 1-4 4H5a4 4 0 0 1-4-4V5a4 4 0 0 1 4-4h2.235l.126.013Z"
78+
/>
79+
<path
80+
fill="currentColor"
81+
d="M12.875 1C14.049 1 15 1.951 15 3.125v2.25a.625.625 0 1 1-1.25 0v-2.24L9.067 7.817a.626.626 0 0 1-.884-.884l4.682-4.683h-2.24a.625.625 0 1 1 0-1.25h2.25Z"
82+
/>
83+
</svg>
84+
);
85+
}

special-pages/pages/duckplayer/app/components/Button.module.css

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,33 @@
11
.button {
2+
--button-background: rgba(255, 255, 255, 0.18);
3+
--button-color: rgba(255, 255, 255, 1);
4+
--button-background-hover: rgba(255, 255, 255, 0.2);
5+
6+
align-items: center;
27
border: none;
38
outline: none;
49
display: flex;
10+
gap: 8px;
511
height: 44px;
612
line-height: 44px;
713
font-size: 15px;
814
font-weight: bold;
915
padding: 0 20px;
1016
flex-shrink: 0;
1117
box-shadow: none;
12-
background: rgba(255, 255, 255, 0.18);
18+
background: var(--button-background);
1319
border-radius: var(--inner-radius);
14-
color: rgba(255, 255, 255, 1);
20+
color: var(--button-color);
1521
text-decoration: none;
1622

1723
[data-layout="mobile"] & {
18-
background-color: rgba(255, 255, 255, 0.12);
24+
--button-background: rgba(255, 255, 255, 0.12);
1925
}
2026
}
2127

22-
2328
.button:hover, .button:focus-visible {
29+
background: var(--button-background-hover);
2430
cursor: pointer;
25-
background: rgba(255, 255, 255, 0.2);
2631
}
2732

2833
.fill {
@@ -59,7 +64,6 @@
5964
transform: translateY(-50%) translateX(-50%);
6065
}
6166

62-
.icon {}
6367
.icon img {
6468
display: block;
6569
width: 100%;
@@ -80,3 +84,17 @@
8084
transition-delay: 2s;
8185
transform: scale(1.1);
8286
}
87+
88+
/* Accent variant */
89+
90+
.button.accent {
91+
--button-background: #3969ef;
92+
--button-color: #fff;
93+
--button-background-hover: #2b55ca;
94+
}
95+
96+
.svgIcon {
97+
width: 16px;
98+
height: 16px;
99+
margin-left: -8px;
100+
}

0 commit comments

Comments
 (0)