@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playpilot/tpi",
3
- "version": "8.11.0",
3
+ "version": "8.12.0",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite dev",
@@ -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
@@ -23,6 +23,7 @@ window.PlayPilotLinkInjections ||= {
23
23
  initial_link_injections_promise: null,
24
24
  time_at_initialize: 0,
25
25
  ads: [],
26
+ video_playtimes: {},
26
27
  require_consent: true,
27
28
  no_affiliate: false,
28
29
  consents: {
@@ -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(2));
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.5), 5vw, margin(2)));
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="flexible"
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, 80vw);
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: 80vh;
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(-2)};
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 (elementOffsetInParent < activeElement.clientWidth * 2) slider?.setIndex(index - 2)
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: #{calc(var(--image-height) / 9 * 16)};
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(6), explore-width(18), margin(11.5)))};
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) {
@@ -154,7 +154,7 @@
154
154
  }
155
155
 
156
156
  .header {
157
- padding: margin(10) 0 margin(1);
157
+ padding: min(margin(17), 70vw) 0 margin(1);
158
158
 
159
159
  @media (min-width: 390px) {
160
160
  display: grid;
@@ -43,8 +43,10 @@
43
43
  return '.' + classnames.join('.')
44
44
  }
45
45
 
46
- function elementHasDirectTextContent(element: Element): Node | undefined {
47
- return Array.from(element.childNodes).find(node => node.nodeName === '#text' && !!node.textContent?.trim())
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 () => {