@playpilot/tpi 5.32.0-beta.3 → 5.32.0-beta.youtube.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/link-injections.js +11 -11
- package/package.json +1 -1
- package/src/lib/data/translations.ts +5 -5
- package/src/lib/enums/SplitTest.ts +0 -5
- package/src/lib/fakeData.ts +1 -0
- package/src/lib/injection.ts +4 -35
- package/src/lib/scss/global.scss +0 -39
- package/src/lib/text.ts +2 -1
- package/src/lib/trailer.ts +22 -0
- package/src/lib/types/title.d.ts +1 -0
- package/src/routes/+page.svelte +2 -17
- package/src/routes/components/Button.svelte +61 -0
- package/src/routes/components/Debugger.svelte +0 -8
- package/src/routes/components/Icons/IconClose.svelte +9 -1
- package/src/routes/components/Icons/IconPlay.svelte +3 -0
- package/src/routes/components/Playlinks/PlaylinkIcon.svelte +4 -1
- package/src/routes/components/RoundButton.svelte +4 -5
- package/src/routes/components/Share.svelte +5 -23
- package/src/routes/components/Title.svelte +22 -22
- package/src/routes/components/Trailer.svelte +18 -0
- package/src/routes/components/YouTubeEmbedOverlay.svelte +96 -0
- package/src/tests/lib/injections.test.js +11 -0
- package/src/tests/lib/text.test.js +10 -0
- package/src/tests/lib/trailer.test.js +56 -0
- package/src/tests/routes/+page.test.js +0 -2
- package/src/tests/routes/components/Button.test.js +28 -0
- package/src/tests/routes/components/Share.test.js +12 -12
- package/src/tests/routes/components/Title.test.js +13 -0
- package/src/tests/routes/components/Trailer.test.js +20 -0
- package/src/tests/routes/components/YouTubeEmbedOverlay.test.js +31 -0
- package/src/routes/components/HighlightedInjection.svelte +0 -230
- package/src/tests/routes/components/HighlightedInjection.test.js +0 -98
package/package.json
CHANGED
|
@@ -61,11 +61,6 @@ export const translations = {
|
|
|
61
61
|
[Language.Swedish]: 'minuter',
|
|
62
62
|
[Language.Danish]: 'minutter',
|
|
63
63
|
},
|
|
64
|
-
'Minutes Short': {
|
|
65
|
-
[Language.English]: 'min.',
|
|
66
|
-
[Language.Swedish]: 'min.',
|
|
67
|
-
[Language.Danish]: 'min.',
|
|
68
|
-
},
|
|
69
64
|
'Type: movie': {
|
|
70
65
|
[Language.English]: 'Movie',
|
|
71
66
|
[Language.Swedish]: 'Film',
|
|
@@ -131,6 +126,11 @@ export const translations = {
|
|
|
131
126
|
[Language.Swedish]: 'Liknande filmer och serier',
|
|
132
127
|
[Language.Danish]: 'Lignende film og serier',
|
|
133
128
|
},
|
|
129
|
+
'Watch Trailer': {
|
|
130
|
+
[Language.English]: 'Watch trailer',
|
|
131
|
+
[Language.Swedish]: 'Se trailer',
|
|
132
|
+
[Language.Danish]: 'Se trailer',
|
|
133
|
+
},
|
|
134
134
|
|
|
135
135
|
// Genres
|
|
136
136
|
'All': {
|
|
@@ -9,9 +9,4 @@ export const SplitTest = {
|
|
|
9
9
|
numberOfVariants: 2,
|
|
10
10
|
variantNames: ['Image', 'Label'] as string[],
|
|
11
11
|
},
|
|
12
|
-
InTextEngagement: {
|
|
13
|
-
key: 'in-text-engagement',
|
|
14
|
-
numberOfVariants: 4,
|
|
15
|
-
variantNames: ['Default', '(i) Icon', 'Providers', 'Highlight'] as string[]
|
|
16
|
-
}
|
|
17
12
|
} as const
|
package/src/lib/fakeData.ts
CHANGED
|
@@ -38,6 +38,7 @@ export const title: TitleData = {
|
|
|
38
38
|
standing_poster: 'https://img.playpilot.tech/6239ee86a58f11efb0b50a58a9feac02/src/img?optimizer=image&class=2by3x18',
|
|
39
39
|
title: 'Dune: Prophecy',
|
|
40
40
|
original_title: 'Dune: Prophecy',
|
|
41
|
+
embeddable_url: null,
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
export const linkInjections: LinkInjection[] = [{
|
package/src/lib/injection.ts
CHANGED
|
@@ -13,9 +13,6 @@ import { destroyAllModals, openModal } from './modal'
|
|
|
13
13
|
import { track } from './tracking'
|
|
14
14
|
import { TrackingEvent } from './enums/TrackingEvent'
|
|
15
15
|
import InTextDisclaimer from '../routes/components/InTextDisclaimer.svelte'
|
|
16
|
-
import { getSplitTestVariantName, trackSplitTestAction } from './splitTest'
|
|
17
|
-
import { SplitTest } from './enums/SplitTest'
|
|
18
|
-
import { mergePlaylinks } from './playlink'
|
|
19
16
|
|
|
20
17
|
const keyDataAttribute = 'data-playpilot-injection-key'
|
|
21
18
|
const keySelector = `[${keyDataAttribute}]`
|
|
@@ -200,6 +197,10 @@ export function injectLinksInDocument(elements: HTMLElement[], injections: LinkI
|
|
|
200
197
|
|
|
201
198
|
const startIndex = match.index + leadingSpaces
|
|
202
199
|
const endIndex = match.index + match.match.length - trailingSpaces
|
|
200
|
+
|
|
201
|
+
// Stop if the content already contains a link, which we identify soley by if the contain string contains a href attribute
|
|
202
|
+
if (element.innerHTML.slice(startIndex, endIndex).includes('href=')) continue
|
|
203
|
+
|
|
203
204
|
element.innerHTML = replaceBetween(element.innerHTML, injectionElement.outerHTML, startIndex, endIndex)
|
|
204
205
|
|
|
205
206
|
replacementIndex = match.index
|
|
@@ -310,33 +311,6 @@ function createLinkInjectionElement(injection: LinkInjection): { injectionElemen
|
|
|
310
311
|
linkElement.target = '_blank'
|
|
311
312
|
linkElement.rel = 'noopener nofollow noreferrer'
|
|
312
313
|
|
|
313
|
-
// Part of a split test, insert an absolute positioned (i) icon in the top right of links.
|
|
314
|
-
if (getSplitTestVariantName(SplitTest.InTextEngagement) === '(i) Icon') {
|
|
315
|
-
const iconElement = document.createElement('span')
|
|
316
|
-
iconElement.classList.add('playpilot-injection-info-icon')
|
|
317
|
-
iconElement.dataset.playpilotElement = 'true'
|
|
318
|
-
|
|
319
|
-
linkElement.insertAdjacentElement('beforeend', iconElement)
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
// Part of a split test, insert a list of max 3 provider icons after the link, these are inline and cause page shift.
|
|
323
|
-
if (getSplitTestVariantName(SplitTest.InTextEngagement) === 'Providers') {
|
|
324
|
-
const size = parseFloat(getComputedStyle(document.documentElement).fontSize) * 0.875 // 0.875 rem, 14 pixels if 1rem is 16 pixels
|
|
325
|
-
|
|
326
|
-
for (const provider of mergePlaylinks(injection.title_details!.providers).slice(0, 3)) {
|
|
327
|
-
const imageElement = document.createElement('img')
|
|
328
|
-
imageElement.loading = 'lazy'
|
|
329
|
-
imageElement.src = provider.logo_url
|
|
330
|
-
imageElement.alt = '' // Intentionally ignore the image or TTS would make a mess of the sentence
|
|
331
|
-
imageElement.width = size
|
|
332
|
-
imageElement.height = size
|
|
333
|
-
imageElement.dataset.playpilotElement = 'true'
|
|
334
|
-
imageElement.classList.add('playpilot-injection-provider-icon')
|
|
335
|
-
|
|
336
|
-
linkElement.insertAdjacentElement('beforeend', imageElement)
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
314
|
injectionElement.insertAdjacentElement('beforeend', linkElement)
|
|
341
315
|
|
|
342
316
|
return { injectionElement, linkElement }
|
|
@@ -452,8 +426,6 @@ function addLinkInjectionEventListeners(injections: LinkInjection[]): void {
|
|
|
452
426
|
|
|
453
427
|
event.preventDefault()
|
|
454
428
|
|
|
455
|
-
trackSplitTestAction(SplitTest.InTextEngagement, 'click')
|
|
456
|
-
|
|
457
429
|
playFallbackViewTransition(() => {
|
|
458
430
|
destroyLinkPopover(false)
|
|
459
431
|
openModal({ event, injection, data: injection.title_details })
|
|
@@ -634,9 +606,6 @@ export function clearLinkInjection(key: string): void {
|
|
|
634
606
|
const element: HTMLAnchorElement | null = document.querySelector(`[${keyDataAttribute}="${key}"]`)
|
|
635
607
|
if (!element) return
|
|
636
608
|
|
|
637
|
-
const playpilotElements = element.querySelectorAll('[data-playpilot-element]')
|
|
638
|
-
playpilotElements.forEach(element => element.remove())
|
|
639
|
-
|
|
640
609
|
const linkContent = element.querySelector('a')?.innerHTML
|
|
641
610
|
element.outerHTML = linkContent || ''
|
|
642
611
|
}
|
package/src/lib/scss/global.scss
CHANGED
|
@@ -11,45 +11,6 @@
|
|
|
11
11
|
position: relative;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
.playpilot-injection-info-icon {
|
|
15
|
-
position: relative;
|
|
16
|
-
display: inline-block;
|
|
17
|
-
height: 1em;
|
|
18
|
-
|
|
19
|
-
&::before {
|
|
20
|
-
content: "i";
|
|
21
|
-
display: inline-flex;
|
|
22
|
-
justify-content: center;
|
|
23
|
-
align-items: center;
|
|
24
|
-
position: absolute;
|
|
25
|
-
top: -0.25em;
|
|
26
|
-
right: -1em;
|
|
27
|
-
width: 0.75em;
|
|
28
|
-
height: 0.75em;
|
|
29
|
-
border-radius: 50%;
|
|
30
|
-
box-shadow: 0 0 0 1px currentColor;
|
|
31
|
-
line-height: 1;
|
|
32
|
-
font-size: 0.65em;
|
|
33
|
-
text-align: center;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
h1 &, h2 &, h3 &, h4 &, h5 &, h6 & {
|
|
37
|
-
display: none
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
.playpilot-injection-provider-icon {
|
|
42
|
-
display: inline-block;
|
|
43
|
-
margin-left: margin(0.25);
|
|
44
|
-
border-radius: 2px;
|
|
45
|
-
box-shadow: 0 0 1px rgba(0, 0, 0, 0.25);
|
|
46
|
-
overflow: hidden;
|
|
47
|
-
|
|
48
|
-
h1 &, h2 &, h3 &, h4 &, h5 &, h6 & {
|
|
49
|
-
display: none
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
14
|
.playpilot-injection-highlight {
|
|
54
15
|
outline: margin(0.25) solid var(--playpilot-primary) !important;
|
|
55
16
|
outline-offset: margin(0.5) !important;
|
package/src/lib/text.ts
CHANGED
|
@@ -100,7 +100,8 @@ export function findSurroundingPhrases(parentNode: Node, startIndex: number, end
|
|
|
100
100
|
if (!sentenceNode || !parentNode.textContent) return { before: '', after: '' }
|
|
101
101
|
|
|
102
102
|
// Include the index of where the text is contained, as parentNode might start later within the sentenceNode
|
|
103
|
-
|
|
103
|
+
// Use 0 if no index was found. This happens when the sentenceNode and parentNode don't match
|
|
104
|
+
const parentNodeStartIndex = Math.max(sentenceNode.innerText.indexOf(parentNode.textContent), 0)
|
|
104
105
|
|
|
105
106
|
const stringBefore = reverseString(sentenceNode.innerText.slice(0, startIndex + parentNodeStartIndex))
|
|
106
107
|
const stringAfter = sentenceNode.innerText.slice(endIndex + parentNodeStartIndex)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { mount, unmount } from "svelte"
|
|
2
|
+
import { getPlayPilotWrapperElement } from "./injection"
|
|
3
|
+
import type { TitleData } from "./types/title"
|
|
4
|
+
import YouTubeEmbedOverlay from "../routes/components/YouTubeEmbedOverlay.svelte"
|
|
5
|
+
|
|
6
|
+
let currentTrailerComponent: object | null = {}
|
|
7
|
+
|
|
8
|
+
export function openTrailerOverlay(title: TitleData) {
|
|
9
|
+
const target = getPlayPilotWrapperElement()
|
|
10
|
+
// !! Temporarily falls back to a placeholder is while embeddable_url is not yet present
|
|
11
|
+
const props = { onclose: closeTrailerOverlay, embeddable_url: title.embeddable_url || 'https://www.youtube.com/watch?v=xGTq0blCPVQ' }
|
|
12
|
+
|
|
13
|
+
currentTrailerComponent = mount(YouTubeEmbedOverlay, { target, props })
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function closeTrailerOverlay(): void {
|
|
17
|
+
if (!currentTrailerComponent) return
|
|
18
|
+
|
|
19
|
+
unmount(currentTrailerComponent, { outro: true })
|
|
20
|
+
|
|
21
|
+
currentTrailerComponent = null
|
|
22
|
+
}
|
package/src/lib/types/title.d.ts
CHANGED
package/src/routes/+page.svelte
CHANGED
|
@@ -9,8 +9,6 @@
|
|
|
9
9
|
import { fetchAds } from '$lib/api/ads'
|
|
10
10
|
import { fetchConfig } from '$lib/api/config'
|
|
11
11
|
import { authorize, getAuthToken, isEditorialModeEnabled, removeAuthCookie, setEditorialParamInUrl } from '$lib/api/auth'
|
|
12
|
-
import { getSplitTestVariantName, trackSplitTestView } from '$lib/splitTest'
|
|
13
|
-
import { SplitTest } from '$lib/enums/SplitTest'
|
|
14
12
|
import type { LinkInjectionResponse, LinkInjection, LinkInjectionTypes } from '$lib/types/injection'
|
|
15
13
|
import Editor from './components/Editorial/Editor.svelte'
|
|
16
14
|
import EditorTrigger from './components/Editorial/EditorTrigger.svelte'
|
|
@@ -18,7 +16,6 @@
|
|
|
18
16
|
import TrackingPixels from './components/TrackingPixels.svelte'
|
|
19
17
|
import Consent from './components/Consent.svelte'
|
|
20
18
|
import Debugger from './components/Debugger.svelte'
|
|
21
|
-
import HighlightedInjection from './components/HighlightedInjection.svelte'
|
|
22
19
|
|
|
23
20
|
let parentElement: HTMLElement | null = $state(null)
|
|
24
21
|
let elements: HTMLElement[] = $state([])
|
|
@@ -30,7 +27,6 @@
|
|
|
30
27
|
let loading = $state(true)
|
|
31
28
|
let isUrlExcluded = $state(true)
|
|
32
29
|
let linkInjections: LinkInjection[] = $state([])
|
|
33
|
-
let randomRenderId = $state(Math.random())
|
|
34
30
|
|
|
35
31
|
// @ts-ignore It's ok if the response is empty
|
|
36
32
|
const { ai_injections: aiInjections = [], manual_injections: manualInjections = [] } = $derived(response || {})
|
|
@@ -58,11 +54,7 @@
|
|
|
58
54
|
fireQueuedTrackingEvents()
|
|
59
55
|
track(TrackingEvent.ArticlePageView)
|
|
60
56
|
|
|
61
|
-
if (
|
|
62
|
-
|
|
63
|
-
window.PlayPilotLinkInjections.ads = await fetchAds()
|
|
64
|
-
|
|
65
|
-
trackSplitTestView(SplitTest.InTextEngagement)
|
|
57
|
+
if (aiInjections.length || manualInjections.length) window.PlayPilotLinkInjections.ads = await fetchAds()
|
|
66
58
|
}
|
|
67
59
|
|
|
68
60
|
async function initialize(): Promise<void> {
|
|
@@ -124,7 +116,6 @@
|
|
|
124
116
|
|
|
125
117
|
function rerender(): void {
|
|
126
118
|
inject(separateLinkInjectionTypes(linkInjections))
|
|
127
|
-
randomRenderId = Math.random()
|
|
128
119
|
}
|
|
129
120
|
|
|
130
121
|
function reinitializeEditor(): void {
|
|
@@ -234,13 +225,7 @@
|
|
|
234
225
|
</svelte:boundary>
|
|
235
226
|
{/if}
|
|
236
227
|
|
|
237
|
-
|
|
238
|
-
{#if linkInjections.length && getSplitTestVariantName(SplitTest.InTextEngagement) === 'Highlight'}
|
|
239
|
-
<HighlightedInjection {linkInjections} />
|
|
240
|
-
{/if}
|
|
241
|
-
{/key}
|
|
242
|
-
|
|
243
|
-
<Debugger onrerender={rerender} />
|
|
228
|
+
<Debugger />
|
|
244
229
|
</div>
|
|
245
230
|
|
|
246
231
|
{#if response?.pixels?.length}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte'
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
variant?: 'filled' | 'border'
|
|
6
|
+
// eslint-disable-next-line no-unused-vars
|
|
7
|
+
onclick?: (event: MouseEvent) => void
|
|
8
|
+
children?: Snippet
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { variant = 'filled', onclick, children }: Props = $props()
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<button class="button {variant}" {onclick}>
|
|
15
|
+
{@render children?.()}
|
|
16
|
+
</button>
|
|
17
|
+
|
|
18
|
+
<style lang="scss">
|
|
19
|
+
.button {
|
|
20
|
+
appearance: none;
|
|
21
|
+
display: flex;
|
|
22
|
+
height: 100%;
|
|
23
|
+
align-items: center;
|
|
24
|
+
gap: margin(0.25);
|
|
25
|
+
border: 0;
|
|
26
|
+
padding: 0.25em 0.5em;
|
|
27
|
+
border-radius: theme(button-border-radius, border-radius);
|
|
28
|
+
background: theme(button-background, content);
|
|
29
|
+
color: theme(button-text-color, text-color-alt);
|
|
30
|
+
font-size: inherit;
|
|
31
|
+
font-family: inherit;
|
|
32
|
+
font-weight: theme(button-font-weight, normal);
|
|
33
|
+
cursor: pointer;
|
|
34
|
+
|
|
35
|
+
:global(svg) {
|
|
36
|
+
width: 1.5em;
|
|
37
|
+
height: 1.5em;
|
|
38
|
+
opacity: 0.75;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.filled {
|
|
43
|
+
&:hover,
|
|
44
|
+
&:active {
|
|
45
|
+
background: theme(button-hover-background, content-light);
|
|
46
|
+
color: theme(button-hover-text-color, text-color);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.border {
|
|
51
|
+
background: transparent;
|
|
52
|
+
box-shadow: inset 0 0 0 1px theme(button-border-color, content-light);
|
|
53
|
+
|
|
54
|
+
&:hover,
|
|
55
|
+
&:active {
|
|
56
|
+
background: theme(button-border-color, content);
|
|
57
|
+
color: theme(button-hover-text-color, text-color);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
</style>
|
|
61
|
+
|
|
@@ -3,12 +3,6 @@
|
|
|
3
3
|
import { getFullUrlPath } from '$lib/url'
|
|
4
4
|
import { onDestroy } from 'svelte'
|
|
5
5
|
|
|
6
|
-
interface Props {
|
|
7
|
-
onrerender: () => void
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
const { onrerender }: Props = $props()
|
|
11
|
-
|
|
12
6
|
const secrets = ['tpidebug', 'debugtpi']
|
|
13
7
|
const lastInputs: string[] = []
|
|
14
8
|
const isUsingBetaScript = !!document.querySelector('script[src*="scripts.playpilot.com/link-injection@next"]')
|
|
@@ -157,8 +151,6 @@
|
|
|
157
151
|
|
|
158
152
|
<hr />
|
|
159
153
|
|
|
160
|
-
<button onclick={onrerender}>Re-inject</button>
|
|
161
|
-
|
|
162
154
|
{#if isUsingBetaScript}
|
|
163
155
|
<small>You are using the beta version of TPI</small>
|
|
164
156
|
{:else}
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
-
<
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
size?: number
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const { size = 16 }: Props = $props()
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<svg width={size} height={size} viewBox="0 0 16 16" fill="none">
|
|
2
10
|
<path d="M14 2L2 14M2 2L14 14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
3
11
|
</svg>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg width="24px" height="24px" viewBox="0 -960 960 960">
|
|
2
|
+
<path fill="currentColor" d="m380-300 280-180-280-180v360ZM480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z"/>
|
|
3
|
+
</svg>
|
|
@@ -14,13 +14,15 @@
|
|
|
14
14
|
const { name, url, logo_url } = $derived(playlink)
|
|
15
15
|
</script>
|
|
16
16
|
|
|
17
|
-
<a href={url} target="_blank" class="playlink" data-playlink={name} rel="sponsored" {onclick}>
|
|
17
|
+
<a href={url} target="_blank" class="playlink" data-playlink={name} rel="sponsored" {onclick} style:--size="{size}px">
|
|
18
18
|
<img src={removeImageUrlPrefix(logo_url)} alt={name} height={size} width={size} />
|
|
19
19
|
</a>
|
|
20
20
|
|
|
21
21
|
<style lang="scss">
|
|
22
22
|
.playlink {
|
|
23
23
|
display: inline-block;
|
|
24
|
+
width: var(--size);
|
|
25
|
+
height: var(--size);
|
|
24
26
|
background: theme(playlink-background, light);
|
|
25
27
|
border-radius: theme(playlink-border-radius, border-radius);
|
|
26
28
|
overflow: hidden;
|
|
@@ -36,6 +38,7 @@
|
|
|
36
38
|
img {
|
|
37
39
|
display: block;
|
|
38
40
|
margin: 0;
|
|
41
|
+
width: auto;
|
|
39
42
|
}
|
|
40
43
|
}
|
|
41
44
|
</style>
|
|
@@ -2,11 +2,10 @@
|
|
|
2
2
|
import type { Snippet } from 'svelte'
|
|
3
3
|
|
|
4
4
|
interface Props {
|
|
5
|
-
size?: string
|
|
6
|
-
children?: Snippet
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
[key: string]: any
|
|
5
|
+
size?: string;
|
|
6
|
+
children?: Snippet;
|
|
7
|
+
onclick?: () => void;
|
|
8
|
+
[key: string]: any;
|
|
10
9
|
}
|
|
11
10
|
|
|
12
11
|
const { children, size = '32px', onclick = () => null, ...rest }: Props = $props()
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import IconShare from './Icons/IconShare.svelte'
|
|
10
10
|
import IconLink from './Icons/IconLink.svelte'
|
|
11
11
|
import IconEmail from './Icons/IconEmail.svelte'
|
|
12
|
+
import Button from './Button.svelte'
|
|
12
13
|
import { onMount } from 'svelte'
|
|
13
14
|
|
|
14
15
|
interface Props {
|
|
@@ -55,9 +56,9 @@
|
|
|
55
56
|
<svelte:window onclick={() => showContextMenu = false} />
|
|
56
57
|
|
|
57
58
|
<div class="share">
|
|
58
|
-
<
|
|
59
|
-
<IconShare />
|
|
60
|
-
</
|
|
59
|
+
<Button onclick={toggle}>
|
|
60
|
+
<IconShare /> Share
|
|
61
|
+
</Button>
|
|
61
62
|
|
|
62
63
|
{#if showContextMenu}
|
|
63
64
|
<div class="context-menu" transition:scale={{ duration: 50, start: 0.85 }}>
|
|
@@ -79,30 +80,11 @@
|
|
|
79
80
|
position: relative;
|
|
80
81
|
}
|
|
81
82
|
|
|
82
|
-
.button {
|
|
83
|
-
display: flex;
|
|
84
|
-
align-items: center;
|
|
85
|
-
justify-content: center;
|
|
86
|
-
cursor: pointer;
|
|
87
|
-
appearance: none;
|
|
88
|
-
border: 0;
|
|
89
|
-
border-radius: margin(3);
|
|
90
|
-
aspect-ratio: 1 / 1;
|
|
91
|
-
background: transparent;
|
|
92
|
-
color: theme(detail-text-color-alt, text-color-alt);
|
|
93
|
-
|
|
94
|
-
&:hover {
|
|
95
|
-
color: theme(detail-text-color, text-color);
|
|
96
|
-
background: theme(share-button-hover-background, content);
|
|
97
|
-
box-shadow: 0 0 0 2px theme(share-button-hover-background, content);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
83
|
.context-menu {
|
|
102
84
|
z-index: 10;
|
|
103
85
|
position: absolute;
|
|
104
86
|
bottom: calc(100% + margin(0.5));
|
|
105
|
-
|
|
87
|
+
left: 0;
|
|
106
88
|
max-width: margin(15);
|
|
107
89
|
border-radius: $border-radius;
|
|
108
90
|
background: theme(detail-background, lighter);
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import SimilarRail from './Rails/SimilarRail.svelte'
|
|
8
8
|
import TitlePoster from './TitlePoster.svelte'
|
|
9
9
|
import Share from './Share.svelte'
|
|
10
|
+
import Trailer from './Trailer.svelte'
|
|
10
11
|
import { t } from '$lib/localization'
|
|
11
12
|
import type { TitleData } from '$lib/types/title'
|
|
12
13
|
import { heading } from '$lib/actions/heading'
|
|
@@ -36,26 +37,28 @@
|
|
|
36
37
|
|
|
37
38
|
<div class="heading" use:heading={2} class:truncate={small} id="title">{title.title}</div>
|
|
38
39
|
|
|
39
|
-
<div class="
|
|
40
|
-
<div class="
|
|
41
|
-
<
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
</div>
|
|
40
|
+
<div class="info">
|
|
41
|
+
<div class="imdb">
|
|
42
|
+
<IconIMDb />
|
|
43
|
+
{title.imdb_score}
|
|
44
|
+
</div>
|
|
45
45
|
|
|
46
|
-
|
|
46
|
+
<Genres genres={title.genres} />
|
|
47
47
|
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
<div>{title.year}</div>
|
|
49
|
+
<div class="capitalize">{t(`Type: ${title.type}`)}</div>
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
{#if !small && title.length}
|
|
52
|
+
<div>{title.length} {t('Minutes')}</div>
|
|
53
|
+
{/if}
|
|
54
|
+
</div>
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
<div class="actions">
|
|
57
|
+
<!-- !! Button is temporarily always visible while embeddable_url is not yet available -->
|
|
58
|
+
{#if true || title.embeddable_url}
|
|
59
|
+
<Trailer title={title} />
|
|
57
60
|
<Share title={title.title} url={titleUrl(title)} />
|
|
58
|
-
|
|
61
|
+
{/if}
|
|
59
62
|
</div>
|
|
60
63
|
</div>
|
|
61
64
|
|
|
@@ -146,11 +149,6 @@
|
|
|
146
149
|
}
|
|
147
150
|
}
|
|
148
151
|
|
|
149
|
-
.row {
|
|
150
|
-
display: flex;
|
|
151
|
-
align-items: flex-start;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
152
|
.info {
|
|
155
153
|
display: flex;
|
|
156
154
|
flex-wrap: wrap;
|
|
@@ -182,8 +180,10 @@
|
|
|
182
180
|
}
|
|
183
181
|
}
|
|
184
182
|
|
|
185
|
-
.
|
|
186
|
-
|
|
183
|
+
.actions {
|
|
184
|
+
display: flex;
|
|
185
|
+
gap: margin(0.5);
|
|
186
|
+
margin-top: margin(0.5);
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
.background {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { t } from '$lib/localization'
|
|
3
|
+
import { openTrailerOverlay } from '$lib/trailer'
|
|
4
|
+
import type { TitleData } from '$lib/types/title'
|
|
5
|
+
import Button from './Button.svelte'
|
|
6
|
+
import IconPlay from './Icons/IconPlay.svelte'
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
title: TitleData
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const { title }: Props = $props()
|
|
13
|
+
</script>
|
|
14
|
+
|
|
15
|
+
<Button onclick={() => openTrailerOverlay(title)}>
|
|
16
|
+
<IconPlay />
|
|
17
|
+
{t('Watch Trailer')}
|
|
18
|
+
</Button>
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { fade } from 'svelte/transition'
|
|
3
|
+
import IconClose from './Icons/IconClose.svelte'
|
|
4
|
+
|
|
5
|
+
interface Props {
|
|
6
|
+
embeddable_url: string,
|
|
7
|
+
onclose: () => void
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { embeddable_url, onclose }: Props = $props()
|
|
11
|
+
|
|
12
|
+
const videoId = $derived(getVideoId(embeddable_url))
|
|
13
|
+
|
|
14
|
+
// Gets the YouTube ID from a url, can be a large number of differnet formats
|
|
15
|
+
// https://stackoverflow.com/a/54200105/1665157
|
|
16
|
+
function getVideoId(url: string): string | null {
|
|
17
|
+
const regExp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*/
|
|
18
|
+
const match = url.match(regExp)
|
|
19
|
+
|
|
20
|
+
return match?.[7] || null
|
|
21
|
+
}
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<div class="overlay" transition:fade={{ duration: 100 }}>
|
|
25
|
+
{#if videoId}
|
|
26
|
+
<iframe width="600" height="338" src="https://www.youtube.com/embed/{videoId}?autoplay=true" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
|
|
27
|
+
{:else}
|
|
28
|
+
Something went wrong
|
|
29
|
+
{/if}
|
|
30
|
+
|
|
31
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
32
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
33
|
+
<div class="backdrop" onclick={onclose} data-testid="backdrop"></div>
|
|
34
|
+
|
|
35
|
+
<button class="close" onclick={onclose} aria-label="Close">
|
|
36
|
+
<IconClose size={24} />
|
|
37
|
+
</button>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<style lang="scss">
|
|
41
|
+
iframe {
|
|
42
|
+
z-index: 1;
|
|
43
|
+
position: relative;
|
|
44
|
+
display: block;
|
|
45
|
+
width: 95vmin;
|
|
46
|
+
height: auto;
|
|
47
|
+
aspect-ratio: 16/9;
|
|
48
|
+
box-shadow: 0 0 margin(4) rgba(255, 255, 255, 0.15);
|
|
49
|
+
background: black;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.overlay {
|
|
53
|
+
z-index: 2147483647; // As high as she goes
|
|
54
|
+
box-sizing: border-box;
|
|
55
|
+
display: flex;
|
|
56
|
+
align-items: center;
|
|
57
|
+
justify-content: center;
|
|
58
|
+
position: fixed;
|
|
59
|
+
top: 0;
|
|
60
|
+
right: 0;
|
|
61
|
+
bottom: 0;
|
|
62
|
+
left: 0;
|
|
63
|
+
background: theme(detail-backdrop, rgba(0, 0, 0, 0.95));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.backdrop {
|
|
67
|
+
z-index: 0;
|
|
68
|
+
position: fixed;
|
|
69
|
+
top: 0;
|
|
70
|
+
right: 0;
|
|
71
|
+
bottom: 0;
|
|
72
|
+
left: 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.close {
|
|
76
|
+
appearance: none;
|
|
77
|
+
z-index: 1;
|
|
78
|
+
position: fixed;
|
|
79
|
+
top: margin(2);
|
|
80
|
+
right: margin(2);
|
|
81
|
+
padding: 0;
|
|
82
|
+
margin: 0;
|
|
83
|
+
border: 0;
|
|
84
|
+
background: transparent;
|
|
85
|
+
color: white;
|
|
86
|
+
cursor: pointer;
|
|
87
|
+
|
|
88
|
+
&:hover {
|
|
89
|
+
transform: scale(1.1);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
&:active {
|
|
93
|
+
transform: scale(1);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
</style>
|