@playpilot/tpi 8.11.0 → 8.12.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 +8 -8
- package/dist/link-injections.js +2 -2
- package/dist/mount.js +8 -8
- package/package.json +1 -1
- package/src/lib/data/translations.ts +5 -0
- package/src/lib/types/script.d.ts +2 -0
- package/src/main.ts +1 -0
- package/src/routes/components/Explore/ExploreLayout.svelte +8 -3
- package/src/routes/components/Explore/Routes/ExploreHome.svelte +6 -4
- package/src/routes/components/Modals/RailModal.svelte +3 -3
- package/src/routes/components/Rails/TitlesRail.svelte +22 -4
- package/src/routes/components/Title.svelte +1 -1
- package/src/routes/components/TrackAnyClick.svelte +4 -2
- package/src/routes/components/YouTubeEmbed.svelte +17 -1
- package/src/tests/routes/components/YouTubeEmbed.test.js +12 -1
- package/src/tests/routes/components/YouTubeEmbedBackground.test.js +1 -1
- package/src/tests/routes/components/YouTubeEmbedOverlay.test.js +1 -1
package/package.json
CHANGED
|
@@ -166,6 +166,11 @@ export const translations = {
|
|
|
166
166
|
[Language.Swedish]: 'Streaming Guide',
|
|
167
167
|
[Language.Danish]: 'Streaming Guide',
|
|
168
168
|
},
|
|
169
|
+
'Streaming Guide Heading': {
|
|
170
|
+
[Language.English]: 'Discover and search all movies and tv-shows',
|
|
171
|
+
[Language.Swedish]: 'Upptäck och sök bland alla filmer och tv-serier',
|
|
172
|
+
[Language.Danish]: 'Opdag og søg i alle film og tv-serier',
|
|
173
|
+
},
|
|
169
174
|
'Streaming Guide Description': {
|
|
170
175
|
[Language.English]: 'Find where to watch movies online - the ultimate guide that helps you find the best movies and shows across streaming services.',
|
|
171
176
|
[Language.Swedish]: 'Sök bland alla filmer och serier för att ta reda på var du kan streama dem',
|
|
@@ -42,6 +42,8 @@ export type ScriptConfig = {
|
|
|
42
42
|
config?: ConfigResponse
|
|
43
43
|
// All ads as fetched from the ads endpoint. This is used as the primary store for ads, each individual ads gets it's data from here.
|
|
44
44
|
ads?: Campaign[]
|
|
45
|
+
// Estimate playtimes of video embeds listed by their video id.
|
|
46
|
+
video_playtimes?: Record<string, number>
|
|
45
47
|
// The region the user is in, either fetched from an external service or based on the default region in the config object
|
|
46
48
|
region?: string | null
|
|
47
49
|
// The time at which the script was initialized
|
package/src/main.ts
CHANGED
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
<div class="divider"></div>
|
|
46
46
|
|
|
47
47
|
<div class="heading" use:heading>
|
|
48
|
-
{t('Streaming Guide')}
|
|
48
|
+
{t('Streaming Guide Heading')}
|
|
49
49
|
</div>
|
|
50
50
|
|
|
51
51
|
{#if !useExploreRouter()}
|
|
@@ -99,8 +99,13 @@
|
|
|
99
99
|
display: flex;
|
|
100
100
|
flex-direction: column;
|
|
101
101
|
gap: margin(0.5);
|
|
102
|
-
margin: theme(explore-header-margin, 0 0 margin(
|
|
102
|
+
margin: theme(explore-header-margin, 0 0 margin(1));
|
|
103
103
|
width: 100%;
|
|
104
|
+
max-width: margin(12);
|
|
105
|
+
|
|
106
|
+
@include desktop {
|
|
107
|
+
max-width: margin(20);
|
|
108
|
+
}
|
|
104
109
|
}
|
|
105
110
|
|
|
106
111
|
.divider {
|
|
@@ -113,7 +118,7 @@
|
|
|
113
118
|
.heading {
|
|
114
119
|
margin: theme(explore-heading-margin, margin(0.25) 0);
|
|
115
120
|
color: theme(text-color);
|
|
116
|
-
font-size: theme(explore-heading-size, clamp(margin(1
|
|
121
|
+
font-size: theme(explore-heading-size, clamp(margin(1), 2.5vw, margin(1.5)));
|
|
117
122
|
font-weight: theme(explore-heading-font-weight, font-bold);
|
|
118
123
|
text-transform: theme(explore-heading-text-transform, normal);
|
|
119
124
|
line-height: theme(explore-heading-line-height, 1.5);
|
|
@@ -6,9 +6,11 @@
|
|
|
6
6
|
import { track } from '$lib/tracking'
|
|
7
7
|
import { TrackingEvent } from '$lib/enums/TrackingEvent'
|
|
8
8
|
import TitlesRail from '../../Rails/TitlesRail.svelte'
|
|
9
|
+
import { mobileBreakpoint } from '$lib/constants'
|
|
9
10
|
|
|
10
11
|
let expandedTitle: TitleData | null = $state(null)
|
|
11
12
|
let expandedRailKey: string | null = $state(null)
|
|
13
|
+
let windowWidth: number = $state(window.innerWidth)
|
|
12
14
|
|
|
13
15
|
const rails: { heading: string, params: Record<string, any>, properties: Record<string, any> }[] = [{
|
|
14
16
|
heading: 'List: Trending',
|
|
@@ -17,7 +19,7 @@
|
|
|
17
19
|
}, {
|
|
18
20
|
heading: 'List: New',
|
|
19
21
|
params: { from_playlist_sid: 'li42WR', include_playable_types: 'SVOD,FREE' },
|
|
20
|
-
properties: { aside: true },
|
|
22
|
+
properties: { aside: true, size: 'flexible' },
|
|
21
23
|
}, {
|
|
22
24
|
heading: 'List: Upcoming',
|
|
23
25
|
params: { from_playlist_sid: 'li42wf', region: null, no_region_filter: true },
|
|
@@ -29,7 +31,7 @@
|
|
|
29
31
|
}, {
|
|
30
32
|
heading: 'List: Demand',
|
|
31
33
|
params: { from_playlist_sid: 'licBS' },
|
|
32
|
-
properties: { aside: true },
|
|
34
|
+
properties: { aside: true, size: 'flexible' },
|
|
33
35
|
}]
|
|
34
36
|
|
|
35
37
|
async function getListTitles(params: Record<string, any> = {}): Promise<TitleData[]> {
|
|
@@ -43,7 +45,7 @@
|
|
|
43
45
|
}
|
|
44
46
|
</script>
|
|
45
47
|
|
|
46
|
-
<svelte:window {onscroll} />
|
|
48
|
+
<svelte:window {onscroll} bind:innerWidth={windowWidth} />
|
|
47
49
|
|
|
48
50
|
<div data-testid="explore-home"></div>
|
|
49
51
|
|
|
@@ -52,7 +54,7 @@
|
|
|
52
54
|
<TitlesRail
|
|
53
55
|
heading={t(heading)}
|
|
54
56
|
titles={getListTitles(params)}
|
|
55
|
-
size=
|
|
57
|
+
size={windowWidth >= mobileBreakpoint ? 'flexible' : 'huge'}
|
|
56
58
|
minimumLength={7}
|
|
57
59
|
{...properties}
|
|
58
60
|
bind:expandedTitle
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
</Modal>
|
|
79
79
|
|
|
80
80
|
<style lang="scss">
|
|
81
|
-
$size: min(600px,
|
|
81
|
+
$size: min(600px, 85vw);
|
|
82
82
|
|
|
83
83
|
.rail-modal {
|
|
84
84
|
--gap: #{margin(0.25)};
|
|
@@ -131,7 +131,7 @@
|
|
|
131
131
|
border-radius: theme(rail-modal-item-border-radius, border-radius-huge) theme(rail-modal-item-border-radius, border-radius-huge) 0 0;
|
|
132
132
|
background: theme(rail-modal-item-background, light);
|
|
133
133
|
box-shadow: none;
|
|
134
|
-
height:
|
|
134
|
+
height: 90vh;
|
|
135
135
|
overflow: auto;
|
|
136
136
|
overscroll-behavior: contain;
|
|
137
137
|
transition: box-shadow var(--transition-duration);
|
|
@@ -156,7 +156,7 @@
|
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
.arrow {
|
|
159
|
-
--offset: #{margin(-
|
|
159
|
+
--offset: #{margin(-1.25)};
|
|
160
160
|
--scale: 1;
|
|
161
161
|
cursor: pointer;
|
|
162
162
|
z-index: 2;
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
titles: Promise<TitleData[]> | TitleData[]
|
|
17
17
|
minimumLength?: number,
|
|
18
18
|
heading?: string,
|
|
19
|
-
size?: 'small' | 'large' | 'flexible'
|
|
19
|
+
size?: 'small' | 'large' | 'huge' | 'flexible'
|
|
20
20
|
aside?: boolean,
|
|
21
21
|
expandable?: boolean,
|
|
22
22
|
expandedTitle?: TitleData | null,
|
|
@@ -60,11 +60,16 @@
|
|
|
60
60
|
const activeElement = element?.querySelectorAll('.title')[index]!
|
|
61
61
|
const parentOffset = element?.getBoundingClientRect().right || 0
|
|
62
62
|
const elementOffsetInParent = parentOffset - activeElement.getBoundingClientRect().right
|
|
63
|
+
const isFullyVisible = elementOffsetInParent > activeElement.clientWidth * 2
|
|
63
64
|
|
|
64
65
|
recentlyExpanded = true
|
|
65
66
|
setTimeout(() => recentlyExpanded = false, 500)
|
|
66
67
|
|
|
67
|
-
if (
|
|
68
|
+
if (size === 'flexible' && !isFullyVisible) {
|
|
69
|
+
slider?.setIndex(index - 2)
|
|
70
|
+
} else if (size === 'huge' && !isFullyVisible) {
|
|
71
|
+
slider?.setIndex(index)
|
|
72
|
+
}
|
|
68
73
|
|
|
69
74
|
expandTitleIntoTrailer(title)
|
|
70
75
|
|
|
@@ -206,7 +211,7 @@
|
|
|
206
211
|
.titles {
|
|
207
212
|
--width: #{theme(rail-size-default, margin(6))};
|
|
208
213
|
--image-height: #{calc(var(--width) * 3 / 2)};
|
|
209
|
-
--expanded-width:
|
|
214
|
+
--expanded-width: var(--width);
|
|
210
215
|
--border-radius: #{theme(rail-border-radius, border-radius)};
|
|
211
216
|
|
|
212
217
|
&.with-aside {
|
|
@@ -217,8 +222,13 @@
|
|
|
217
222
|
--width: #{theme(rail-size-large, margin(7.5))};
|
|
218
223
|
}
|
|
219
224
|
|
|
225
|
+
&.huge {
|
|
226
|
+
--width: #{theme(rail-size-large, min(explore-width(65), margin(15)))};
|
|
227
|
+
}
|
|
228
|
+
|
|
220
229
|
&.flexible {
|
|
221
|
-
--width: #{theme(rail-size-flexible, clamp(margin(
|
|
230
|
+
--width: #{theme(rail-size-flexible, clamp(margin(8), explore-width(20), margin(11.5)))};
|
|
231
|
+
--expanded-width: #{calc(var(--image-height) / 9 * 16)};
|
|
222
232
|
}
|
|
223
233
|
}
|
|
224
234
|
|
|
@@ -286,6 +296,14 @@
|
|
|
286
296
|
:global(iframe) {
|
|
287
297
|
opacity: 0;
|
|
288
298
|
animation: fade-iframe 500ms 500ms forwards;
|
|
299
|
+
|
|
300
|
+
@include mobile {
|
|
301
|
+
position: absolute;
|
|
302
|
+
width: 225%;
|
|
303
|
+
top: 50%;
|
|
304
|
+
left: 50%;
|
|
305
|
+
transform: translateX(-50%) translateY(-50%);
|
|
306
|
+
}
|
|
289
307
|
}
|
|
290
308
|
|
|
291
309
|
:global(+ .poster) {
|
|
@@ -43,8 +43,10 @@
|
|
|
43
43
|
return '.' + classnames.join('.')
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
function elementHasDirectTextContent(element: Element): Node |
|
|
47
|
-
|
|
46
|
+
function elementHasDirectTextContent(element: Element | undefined): Node | null {
|
|
47
|
+
if (!element?.childNodes?.length) return null
|
|
48
|
+
|
|
49
|
+
return Array.from(element.childNodes).find(node => node.nodeName === '#text' && !!node.textContent?.trim()) || null
|
|
48
50
|
}
|
|
49
51
|
</script>
|
|
50
52
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { getVideoId } from '$lib/trailer'
|
|
3
|
+
import { onMount } from 'svelte'
|
|
3
4
|
import IconMute from './Icons/IconMute.svelte'
|
|
4
5
|
|
|
5
6
|
interface Props {
|
|
@@ -14,11 +15,26 @@
|
|
|
14
15
|
const { embeddable_url = '', controls = [], muted = false, loop = false, captions = false, showMuteControls = false }: Props = $props()
|
|
15
16
|
|
|
16
17
|
const videoId = $derived(getVideoId(embeddable_url))
|
|
18
|
+
const startTime = $derived((window?.PlayPilotLinkInjections?.video_playtimes?.[videoId || ''] || 1) - 1)
|
|
17
19
|
const color = window?.getComputedStyle(document.body).getPropertyValue('--playpilot-primary')?.replace('#', '') || 'fa548a'
|
|
18
20
|
|
|
19
21
|
let iframe: HTMLIFrameElement | null = $state(null)
|
|
20
22
|
let isMuted = $state(muted)
|
|
21
23
|
|
|
24
|
+
onMount(() => {
|
|
25
|
+
if (!videoId) return
|
|
26
|
+
|
|
27
|
+
if (!window.PlayPilotLinkInjections.video_playtimes) window.PlayPilotLinkInjections.video_playtimes = {}
|
|
28
|
+
|
|
29
|
+
let elapsedSeconds = startTime
|
|
30
|
+
const interval = setInterval(() => {
|
|
31
|
+
elapsedSeconds++
|
|
32
|
+
window.PlayPilotLinkInjections.video_playtimes![videoId] = elapsedSeconds
|
|
33
|
+
}, 1000)
|
|
34
|
+
|
|
35
|
+
return () => clearInterval(interval)
|
|
36
|
+
})
|
|
37
|
+
|
|
22
38
|
export function toggleMute(state = !isMuted): void {
|
|
23
39
|
if (isMuted != state) iframe?.contentWindow?.postMessage('mute', '*')
|
|
24
40
|
isMuted = state
|
|
@@ -30,7 +46,7 @@
|
|
|
30
46
|
bind:this={iframe}
|
|
31
47
|
width="600"
|
|
32
48
|
height="338"
|
|
33
|
-
src="https://video.playpilot.net/?video_id={videoId}&color={color}&muted={muted}&loop={loop}&captions={captions}&controls={controls.join(',')}&autoplay=true&playsinline=true"
|
|
49
|
+
src="https://video.playpilot.net/?video_id={videoId}&color={color}&muted={muted}&loop={loop}&captions={captions}&controls={controls.join(',')}&start_time={startTime}&autoplay=true&playsinline=true"
|
|
34
50
|
title="YouTube video player"
|
|
35
51
|
frameborder="0"
|
|
36
52
|
referrerpolicy="strict-origin-when-cross-origin"
|
|
@@ -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&captions=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=&start_time=0&autoplay=true&playsinline=true')
|
|
11
11
|
})
|
|
12
12
|
|
|
13
13
|
it('Should render embed iframe with given controls', () => {
|
|
@@ -40,4 +40,15 @@ describe('YouTubeEmbed.svelte', () => {
|
|
|
40
40
|
expect(container.querySelector('iframe')).not.toBeTruthy()
|
|
41
41
|
expect(getByText('Something went wrong')).toBeTruthy()
|
|
42
42
|
})
|
|
43
|
+
|
|
44
|
+
it('Should include start_time for given video id if present, minus 1 second', () => {
|
|
45
|
+
// @ts-ignore
|
|
46
|
+
window.PlayPilotLinkInjections = {
|
|
47
|
+
video_playtimes: { abc: 5 },
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const { container } = render(YouTubeEmbed, { embeddable_url: 'youtube.com/watch?v=abc', captions: true })
|
|
51
|
+
|
|
52
|
+
expect(/** @type {HTMLIFrameElement} */ (container.querySelector('iframe')).src).toContain('&start_time=4')
|
|
53
|
+
})
|
|
43
54
|
})
|
|
@@ -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&captions=false&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=&start_time=0&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&captions=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&start_time=0&autoplay=true&playsinline=true')
|
|
12
12
|
})
|
|
13
13
|
|
|
14
14
|
it('Should fire given onclose function on click of close button and backdrop', async () => {
|