@playpilot/tpi 8.6.0 → 8.7.0
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/editorial.mount.js +7 -7
- package/dist/link-injections.js +1 -1
- package/dist/mount.js +7 -7
- package/events.md +3 -0
- package/package.json +1 -1
- package/src/lib/enums/TrackingEvent.ts +1 -0
- package/src/routes/components/Icons/IconMute.svelte +15 -0
- package/src/routes/components/Modals/RailModal.svelte +11 -5
- package/src/routes/components/Modals/TitlesRailModal.svelte +3 -1
- package/src/routes/components/Rails/TitlesRail.svelte +3 -1
- package/src/routes/components/YouTubeEmbed.svelte +47 -2
- package/src/tests/routes/components/YouTubeEmbed.test.js +6 -1
- package/src/tests/routes/components/YouTubeEmbedBackground.test.js +1 -1
- package/src/tests/routes/components/YouTubeEmbedOverlay.test.js +1 -1
package/events.md
CHANGED
|
@@ -18,8 +18,10 @@ All events share a common payload:
|
|
|
18
18
|
Events related to titles share an additional set of data (referred to below as `Title`):
|
|
19
19
|
|
|
20
20
|
- `original_title`
|
|
21
|
+
- `title`
|
|
21
22
|
- `title_sid`
|
|
22
23
|
- `title_type`: "movie" or "series"
|
|
24
|
+
- `genres`: An array of all genres for the title
|
|
23
25
|
- `providers`: An array of provider names
|
|
24
26
|
|
|
25
27
|
Events may have additional data in their payload.
|
|
@@ -112,4 +114,5 @@ Event | Action | Info | Payload
|
|
|
112
114
|
`explore_search` | _Fires any time the user searches for something_ | | `query`
|
|
113
115
|
`venus_title_rail_modal_view` | _Fires any time a title is clicked in a rail, opening the rail modal_ | | `rail` (heading key of the rail)
|
|
114
116
|
`venus_title_rail_expand_click` | _Fires any time a title is expanded in a rail directly via a click_ | Does not fire when a title opens automatically on load or navigate | `Title`, `rail` (heading key of the rail)
|
|
117
|
+
`venus_title_rail_set_index` | _Fires when navigating in the titles rail modal, either via arrows, swipe, or clicking titles_ | | `Title`, `index` (index of the new position of the slider)
|
|
115
118
|
`venus_navigate` | _Fires when navigating on the explore page_ | Does not fire on initial load | `route` (the key of the given route)
|
package/package.json
CHANGED
|
@@ -73,6 +73,7 @@ export const TrackingEvent = {
|
|
|
73
73
|
ExploreFetchProvidersFailed: 'venus_fetch_providers_failed',
|
|
74
74
|
ExploreTitleRailModalView: 'venus_title_rail_modal_view',
|
|
75
75
|
ExploreTitleRailExpandClick: 'venus_title_rail_expand_click',
|
|
76
|
+
ExploreTitleRailSetIndex: 'venus_title_rail_set_index',
|
|
76
77
|
ExploreNavigate: 'venus_navigate',
|
|
77
78
|
} as const
|
|
78
79
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
muted?: boolean
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const { muted = false }: Props = $props()
|
|
7
|
+
</script>
|
|
8
|
+
|
|
9
|
+
<svg width="32px" height="32px" viewBox="0 -960 960 960">
|
|
10
|
+
{#if muted}
|
|
11
|
+
<path fill="currentColor" d="m616-320-56-56 104-104-104-104 56-56 104 104 104-104 56 56-104 104 104 104-56 56-104-104-104 104Zm-496-40v-240h160l200-200v640L280-360H120Zm280-246-86 86H200v80h114l86 86v-252ZM300-480Z"/>
|
|
12
|
+
{:else}
|
|
13
|
+
<path fill="currentColor" d="M560-131v-82q90-26 145-100t55-168q0-94-55-168T560-749v-82q124 28 202 125.5T840-481q0 127-78 224.5T560-131ZM120-360v-240h160l200-200v640L280-360H120Zm440 40v-322q47 22 73.5 66t26.5 96q0 51-26.5 94.5T560-320ZM400-606l-86 86H200v80h114l86 86v-252ZM300-480Z" />
|
|
14
|
+
{/if}
|
|
15
|
+
</svg>
|
|
@@ -11,10 +11,11 @@
|
|
|
11
11
|
interface Props {
|
|
12
12
|
items: Record<string, any>[]
|
|
13
13
|
initialIndex?: number
|
|
14
|
+
onchange?: (index: number) => void
|
|
14
15
|
each: Snippet<[item: any, currentIndex: number]>
|
|
15
16
|
}
|
|
16
17
|
|
|
17
|
-
const { items, initialIndex = 0, each }: Props = $props()
|
|
18
|
+
const { items, initialIndex = 0, onchange = () => null, each }: Props = $props()
|
|
18
19
|
|
|
19
20
|
const transitionDuration = 300
|
|
20
21
|
|
|
@@ -27,6 +28,11 @@
|
|
|
27
28
|
requestAnimationFrame(() => initialized = true)
|
|
28
29
|
})
|
|
29
30
|
})
|
|
31
|
+
|
|
32
|
+
function setSliderIndex(index: number): void {
|
|
33
|
+
onchange(index)
|
|
34
|
+
slider.setIndex(index)
|
|
35
|
+
}
|
|
30
36
|
</script>
|
|
31
37
|
|
|
32
38
|
<Modal blur>
|
|
@@ -37,7 +43,7 @@
|
|
|
37
43
|
{#each items as item, index}
|
|
38
44
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
39
45
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
40
|
-
<div class="item" class:active={index === currentIndex} onclick={() =>
|
|
46
|
+
<div class="item" class:active={index === currentIndex} onclick={() => setSliderIndex(index)}>
|
|
41
47
|
{#if Math.abs(index - currentIndex) === 1 || currentIndex === index}
|
|
42
48
|
<div class="content" transition:fade={{ duration: initialized ? transitionDuration : 0 }}>
|
|
43
49
|
{@render each(item, currentIndex)}
|
|
@@ -47,15 +53,15 @@
|
|
|
47
53
|
{/each}
|
|
48
54
|
{/snippet}
|
|
49
55
|
|
|
50
|
-
{#snippet controls({
|
|
56
|
+
{#snippet controls({ currentIndex, reachedEnd })}
|
|
51
57
|
{#if currentIndex > 0}
|
|
52
|
-
<button class="arrow left" onclick={() =>
|
|
58
|
+
<button class="arrow left" onclick={() => setSliderIndex(currentIndex - 1)} aria-label="Previous" aria-live="polite">
|
|
53
59
|
<IconArrow size={21} direction="left" title="Previous" />
|
|
54
60
|
</button>
|
|
55
61
|
{/if}
|
|
56
62
|
|
|
57
63
|
{#if !reachedEnd}
|
|
58
|
-
<button class="arrow right" onclick={() =>
|
|
64
|
+
<button class="arrow right" onclick={() => setSliderIndex(currentIndex + 1)} aria-label="Next" aria-live="polite">
|
|
59
65
|
<IconArrow size={21} title="Next" />
|
|
60
66
|
</button>
|
|
61
67
|
{/if}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { TrackingEvent } from '$lib/enums/TrackingEvent'
|
|
3
|
+
import { track } from '$lib/tracking'
|
|
2
4
|
import type { TitleData } from '$lib/types/title'
|
|
3
5
|
import Title from '../Title.svelte'
|
|
4
6
|
import RailModal from './RailModal.svelte'
|
|
@@ -11,7 +13,7 @@
|
|
|
11
13
|
const { titles, initialIndex = 0 }: Props = $props()
|
|
12
14
|
</script>
|
|
13
15
|
|
|
14
|
-
<RailModal items={titles} {initialIndex}>
|
|
16
|
+
<RailModal items={titles} {initialIndex} onchange={(index) => track(TrackingEvent.ExploreTitleRailSetIndex, titles[index], { index })}>
|
|
15
17
|
{#snippet each(title, currentIndex)}
|
|
16
18
|
<Title {title} useVideoBackground={title.sid === titles[currentIndex]?.sid} />
|
|
17
19
|
{/snippet}
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
|
|
45
45
|
let element: HTMLElement | null = $state(null)
|
|
46
46
|
let slider: ReturnType<typeof TinySlider> | null = $state(null)
|
|
47
|
+
let embed: ReturnType<typeof YouTubeEmbed> | null = $state(null)
|
|
47
48
|
let recentlyExpanded = false
|
|
48
49
|
|
|
49
50
|
onMount(() => {
|
|
@@ -80,6 +81,7 @@
|
|
|
80
81
|
openModal({ event, data: title, returnToTitle })
|
|
81
82
|
}
|
|
82
83
|
|
|
84
|
+
embed?.toggleMute(true)
|
|
83
85
|
onclick(title)
|
|
84
86
|
}
|
|
85
87
|
|
|
@@ -170,7 +172,7 @@
|
|
|
170
172
|
<a class="video-overlay" title="" {href} {onclick}></a>
|
|
171
173
|
|
|
172
174
|
{#if !!title.embeddable_url}
|
|
173
|
-
<YouTubeEmbed embeddable_url={title.embeddable_url!} muted loop />
|
|
175
|
+
<YouTubeEmbed bind:this={embed} embeddable_url={title.embeddable_url!} muted loop captions showMuteControls />
|
|
174
176
|
{:else}
|
|
175
177
|
<img class="video-fallback" src={title.medium_poster} alt="" />
|
|
176
178
|
{/if}
|
|
@@ -1,29 +1,47 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { getVideoId } from '$lib/trailer'
|
|
3
|
+
import IconMute from './Icons/IconMute.svelte'
|
|
3
4
|
|
|
4
5
|
interface Props {
|
|
5
6
|
embeddable_url: string
|
|
6
7
|
controls?: ('play' | 'mute' | 'fullscreen' | 'progress' | 'current-time')[]
|
|
7
8
|
muted?: boolean
|
|
8
9
|
loop?: boolean
|
|
10
|
+
captions?: boolean
|
|
11
|
+
showMuteControls?: boolean
|
|
9
12
|
}
|
|
10
13
|
|
|
11
|
-
const { embeddable_url = '', controls = [], muted = false, loop = false }: Props = $props()
|
|
14
|
+
const { embeddable_url = '', controls = [], muted = false, loop = false, captions = false, showMuteControls = false }: Props = $props()
|
|
12
15
|
|
|
13
16
|
const videoId = $derived(getVideoId(embeddable_url))
|
|
14
17
|
const color = window?.getComputedStyle(document.body).getPropertyValue('--playpilot-primary')?.replace('#', '') || 'fa548a'
|
|
18
|
+
|
|
19
|
+
let iframe: HTMLIFrameElement | null = $state(null)
|
|
20
|
+
let isMuted = $state(muted)
|
|
21
|
+
|
|
22
|
+
export function toggleMute(state = !isMuted): void {
|
|
23
|
+
if (isMuted != state) iframe?.contentWindow?.postMessage('mute', '*')
|
|
24
|
+
isMuted = state
|
|
25
|
+
}
|
|
15
26
|
</script>
|
|
16
27
|
|
|
17
28
|
{#if videoId}
|
|
18
29
|
<iframe
|
|
30
|
+
bind:this={iframe}
|
|
19
31
|
width="600"
|
|
20
32
|
height="338"
|
|
21
|
-
src="https://video.playpilot.net/?video_id={videoId}&color={color}&muted={muted}&loop={loop}&controls={controls.join(',')}&autoplay=true&playsinline=true"
|
|
33
|
+
src="https://video.playpilot.net/?video_id={videoId}&color={color}&muted={muted}&loop={loop}&captions={captions}&controls={controls.join(',')}&autoplay=true&playsinline=true"
|
|
22
34
|
title="YouTube video player"
|
|
23
35
|
frameborder="0"
|
|
24
36
|
referrerpolicy="strict-origin-when-cross-origin"
|
|
25
37
|
allowfullscreen>
|
|
26
38
|
</iframe>
|
|
39
|
+
|
|
40
|
+
{#if showMuteControls}
|
|
41
|
+
<button class="mute" onclick={() => toggleMute()}>
|
|
42
|
+
<IconMute muted={isMuted} />
|
|
43
|
+
</button>
|
|
44
|
+
{/if}
|
|
27
45
|
{:else}
|
|
28
46
|
Something went wrong
|
|
29
47
|
{/if}
|
|
@@ -33,4 +51,31 @@
|
|
|
33
51
|
width: 100%;
|
|
34
52
|
height: 100%;
|
|
35
53
|
}
|
|
54
|
+
|
|
55
|
+
.mute {
|
|
56
|
+
z-index: 10;
|
|
57
|
+
display: flex;
|
|
58
|
+
align-items: center;
|
|
59
|
+
justify-content: center;
|
|
60
|
+
position: absolute;
|
|
61
|
+
top: margin(0.5);
|
|
62
|
+
right: margin(0.5);
|
|
63
|
+
padding: margin(0.25);
|
|
64
|
+
border: 0;
|
|
65
|
+
border-radius: 50%;
|
|
66
|
+
background: transparent;
|
|
67
|
+
filter: drop-shadow(2px 2px 2px rgba(0, 0, 0, 0.5));
|
|
68
|
+
transition: transform 50ms;
|
|
69
|
+
cursor: pointer;
|
|
70
|
+
color: white;
|
|
71
|
+
|
|
72
|
+
&:hover {
|
|
73
|
+
outline: 1px solid white;
|
|
74
|
+
transform: scale(1.1);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
&:active {
|
|
78
|
+
transform: scale(0.95);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
36
81
|
</style>
|
|
@@ -7,7 +7,7 @@ describe('YouTubeEmbed.svelte', () => {
|
|
|
7
7
|
it('Should render embed iframe with given video url and default options', () => {
|
|
8
8
|
const { container } = render(YouTubeEmbed, { embeddable_url: 'youtube.com/watch?v=abc' })
|
|
9
9
|
|
|
10
|
-
expect(/** @type {HTMLIFrameElement} */ (container.querySelector('iframe')).src).toBe('https://video.playpilot.net/?video_id=abc&color=fa548a&muted=false&loop=false&controls=&autoplay=true&playsinline=true')
|
|
10
|
+
expect(/** @type {HTMLIFrameElement} */ (container.querySelector('iframe')).src).toBe('https://video.playpilot.net/?video_id=abc&color=fa548a&muted=false&loop=false&captions=false&controls=&autoplay=true&playsinline=true')
|
|
11
11
|
})
|
|
12
12
|
|
|
13
13
|
it('Should render embed iframe with given controls', () => {
|
|
@@ -28,6 +28,11 @@ describe('YouTubeEmbed.svelte', () => {
|
|
|
28
28
|
expect(/** @type {HTMLIFrameElement} */ (container.querySelector('iframe')).src).toContain('&loop=true')
|
|
29
29
|
})
|
|
30
30
|
|
|
31
|
+
it('Should render embed iframe with captions if given', () => {
|
|
32
|
+
const { container } = render(YouTubeEmbed, { embeddable_url: 'youtube.com/watch?v=abc', captions: true })
|
|
33
|
+
|
|
34
|
+
expect(/** @type {HTMLIFrameElement} */ (container.querySelector('iframe')).src).toContain('&captions=true')
|
|
35
|
+
})
|
|
31
36
|
|
|
32
37
|
it('Should render error message if embeddable_url is invalid', () => {
|
|
33
38
|
const { container, getByText } = render(YouTubeEmbed, { embeddable_url: '-' })
|
|
@@ -8,6 +8,6 @@ describe('YouTubeEmbedBackground.svelte', () => {
|
|
|
8
8
|
const { container } = render(YouTubeEmbedBackground, { embeddable_url: 'youtube.com/watch?v=abc' })
|
|
9
9
|
|
|
10
10
|
// @ts-ignore
|
|
11
|
-
expect(container.querySelector('iframe').src).toBe('https://video.playpilot.net/?video_id=abc&color=fa548a&muted=true&loop=true&controls=&autoplay=true&playsinline=true')
|
|
11
|
+
expect(container.querySelector('iframe').src).toBe('https://video.playpilot.net/?video_id=abc&color=fa548a&muted=true&loop=true&captions=false&controls=&autoplay=true&playsinline=true')
|
|
12
12
|
})
|
|
13
13
|
})
|
|
@@ -8,7 +8,7 @@ describe('YouTubeEmbedOverlay.svelte', () => {
|
|
|
8
8
|
const { container } = render(YouTubeEmbedOverlay, { embeddable_url: 'youtube.com/watch?v=abc', onclose: () => null })
|
|
9
9
|
|
|
10
10
|
// @ts-ignore
|
|
11
|
-
expect(container.querySelector('iframe').src).toBe('https://video.playpilot.net/?video_id=abc&color=fa548a&muted=false&loop=false&controls=current-time,fullscreen,mute,play,progress&autoplay=true&playsinline=true')
|
|
11
|
+
expect(container.querySelector('iframe').src).toBe('https://video.playpilot.net/?video_id=abc&color=fa548a&muted=false&loop=false&captions=false&controls=current-time,fullscreen,mute,play,progress&autoplay=true&playsinline=true')
|
|
12
12
|
})
|
|
13
13
|
|
|
14
14
|
it('Should fire given onclose function on click of close button and backdrop', async () => {
|