@playpilot/tpi 8.34.5 → 8.35.0-beta.2

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.
@@ -54,6 +54,7 @@ export const participants: ParticipantData[] = [
54
54
  image_uuid: null,
55
55
  gender: 'Male',
56
56
  character: 'Will Rodman (archive footage) (uncredited)',
57
+ description: 'Some description',
57
58
  },
58
59
  {
59
60
  sid: 'pr8bZm',
@@ -65,6 +66,7 @@ export const participants: ParticipantData[] = [
65
66
  image_uuid: null,
66
67
  gender: 'Male',
67
68
  character: 'Old Man',
69
+ description: 'Some description',
68
70
  },
69
71
  {
70
72
  sid: 'pr45Dp',
@@ -76,6 +78,7 @@ export const participants: ParticipantData[] = [
76
78
  image_uuid: null,
77
79
  gender: 'Male',
78
80
  character: 'Self (archive footage) (uncredited)',
81
+ description: 'Some description',
79
82
  },
80
83
  {
81
84
  sid: 'pr6DnN',
@@ -87,6 +90,7 @@ export const participants: ParticipantData[] = [
87
90
  image_uuid: null,
88
91
  gender: 'Male',
89
92
  character: 'Dreyfus',
93
+ description: 'Some description',
90
94
  },
91
95
  {
92
96
  sid: 'pr7GK8',
@@ -98,6 +102,7 @@ export const participants: ParticipantData[] = [
98
102
  image_uuid: null,
99
103
  gender: 'Male',
100
104
  character: 'Cannon-Gunner',
105
+ description: 'Some description',
101
106
  },
102
107
  {
103
108
  sid: 'pr88KG',
@@ -109,6 +114,7 @@ export const participants: ParticipantData[] = [
109
114
  image_uuid: null,
110
115
  gender: 'Female',
111
116
  character: 'Cornelia',
117
+ description: 'Some description',
112
118
  },
113
119
  ]
114
120
 
@@ -1,4 +1,7 @@
1
+ import type { Sorting } from '$lib/enums/Sorting'
2
+
1
3
  export type ExploreFilterType = 'array' | 'range' | 'string'
2
- export type ExploreFilterItem = { type: ExploreFilterType, value: unknown }
4
+ export type ExploreFilterItem = { type: ExploreFilterType; value: unknown }
3
5
  export type ExploreFilter = Record<string, ExploreFilterItem>
4
- export type ExploreFilterItemArrayValue = { label: string, value: string }
6
+ export type ExploreFilterItemArrayValue = { label: string; value: string }
7
+ export type ExploreTitleSorting = { label: string; value: (typeof Sorting)[keyof typeof Sorting] }
@@ -1,6 +1,7 @@
1
1
  export type ParticipantData = {
2
2
  sid: string
3
3
  name: string
4
+ description: string
4
5
  birth_date: string
5
6
  death_date: string | null
6
7
  jobs: Job[]
@@ -16,7 +17,6 @@ export type ParticipantData = {
16
17
  }
17
18
  gender: string
18
19
  character?: string | null
19
- bio?: string | null
20
20
  }
21
21
 
22
22
  export type Job = 'actor' | 'writer' | 'director'
@@ -1,148 +1,154 @@
1
- <script lang="ts">
2
- import type { ExploreFilter } from '$lib/types/filter'
3
- import genres from '$lib/data/genres.json'
4
- import { fetchProviders } from '$lib/api/providers'
5
- import { t } from '$lib/localization'
6
- import FilterItem from './FilterItem.svelte'
7
- import FilterSorting from './FilterSorting.svelte'
8
- import Button from '../../Button.svelte'
9
- import IconArrow from '../../Icons/IconArrow.svelte'
10
- import IconFilter from '../../Icons/IconFilter.svelte'
11
- import { fly, scale } from 'svelte/transition'
12
- import ActiveFilterItems from './ActiveFilterItems.svelte'
13
- import { isFilterItemActive } from '$lib/filter'
14
- import { cdnBaseUrl } from '$lib/constants'
15
-
16
- interface Props {
17
- filter: ExploreFilter
18
- limit?: boolean
19
- showSorting?: boolean
20
- searchQuery?: string
21
- onchange?: () => void
22
- onempty?: () => void
23
- }
24
-
25
- const {
26
- filter,
27
- limit = true,
28
- showSorting = true,
29
- searchQuery = '',
30
- onchange = () => null,
31
- onempty = () => null,
32
- }: Props = $props()
33
-
34
- const shownItemsLimit = 3
35
- const items = [{
36
- label: t('Services'),
37
- param: 'providers',
38
- fetchData: async () => (await fetchProviders()).map(playlink => ({ label: playlink.name, value: playlink.slug })),
39
- }, {
40
- label: t('Genres'),
41
- param: 'genres',
42
- data: genres.filter(genre => parseInt(genre.slug) < 900).map(genre => ({ label: t(genre.name), value: genre.slug })),
43
- }, {
44
- label: t('IMDb'),
45
- param: 'imdb_score',
46
- range: [0, 10] as [number, number],
47
- }, {
48
- label: t('Year'),
49
- param: 'year',
50
- range: [1900, new Date().getFullYear()] as [number, number],
51
- }, {
52
- label: t('Runtime'),
53
- param: 'length',
54
- range: [0, 180] as [number, number],
55
- valueAppend: t('Minutes'),
56
- }, {
57
- label: t('Countries'),
58
- param: 'country_codes',
59
- fetchData: async () => import.meta.env.DEV ?
60
- (await import('$lib/data/countries.json')).default :
61
- (await fetch(`${cdnBaseUrl}/src/lib/data/countries.json`)).json(),
62
- }, {
63
- label: t('Category'),
64
- param: 'category',
65
- data: [{ label: t('Movies'), value: 'movie' }, { label: t('Shows'), value: 'series' }, { label: t('Kids'), value: 'kids' }, { label: t('Documentary'), value: 'documentary' }],
66
- }]
67
-
68
- let limited = $state(limit)
69
- let wasPreviouslyActive = false
70
-
71
- $effect(() => {
72
- const hasAnyActiveFilter = Object.keys(filter).some((param) => {
73
- const { range } = items.find((i: any) => param === i.param) || {}
74
-
75
- return isFilterItemActive(filter[param], range)
76
- })
77
-
78
- if (hasAnyActiveFilter || searchQuery) {
79
- wasPreviouslyActive = true
80
- } else if (wasPreviouslyActive) {
81
- wasPreviouslyActive = false
82
- onempty()
83
- }
84
- })
85
- </script>
86
-
87
- <div class="filter" role="navigation" class:limit>
88
- <div class="items">
89
- {#each limited ? items.slice(0, shownItemsLimit) : items as item}
90
- <div transition:scale={{ start: 0.75, duration: 100 }}>
91
- <FilterItem {filter} {onchange} {...item} />
92
- </div>
93
- {/each}
94
- </div>
95
-
96
- <div class="actions">
97
- {#if limit}
98
- <Button variant="link" onclick={() => limited = !limited}>
99
- <IconFilter />
100
- {t('All Filters')}
101
- <IconArrow direction={limited ? 'down' : 'up'} />
102
- </Button>
103
- {/if}
104
-
105
- {#if showSorting}
106
- <div class="sorting" transition:fly={{ x: 5, duration: 100 }}>
107
- <FilterSorting {filter} {onchange} />
108
- </div>
109
- {/if}
110
- </div>
111
- </div>
112
-
113
- <ActiveFilterItems {filter} {items} />
114
-
115
- <style lang="scss">
116
- .filter {
117
- position: relative;
118
- }
119
-
120
- .items {
121
- display: flex;
122
- flex-wrap: wrap;
123
- gap: margin(0.5);
124
- padding-right: margin(7);
125
-
126
- .limit & {
127
- padding-right: 0;
128
- }
129
- }
130
-
131
- .actions {
132
- position: absolute;
133
- top: margin(0.25);
134
- right: 0;
135
-
136
- .limit & {
137
- position: initial;
138
- top: auto;
139
- display: flex;
140
- justify-content: space-between;
141
- margin-top: margin(0.5);
142
- }
143
- }
144
-
145
- .sorting {
146
- margin-top: margin(0.1);
147
- }
148
- </style>
1
+ <script lang="ts">
2
+ import type { ExploreFilter, ExploreTitleSorting } from '$lib/types/filter'
3
+ import genres from '$lib/data/genres.json'
4
+ import { fetchProviders } from '$lib/api/providers'
5
+ import { t } from '$lib/localization'
6
+ import FilterItem from './FilterItem.svelte'
7
+ import FilterSorting from './FilterSorting.svelte'
8
+ import Button from '../../Button.svelte'
9
+ import IconArrow from '../../Icons/IconArrow.svelte'
10
+ import IconFilter from '../../Icons/IconFilter.svelte'
11
+ import { fly, scale } from 'svelte/transition'
12
+ import ActiveFilterItems from './ActiveFilterItems.svelte'
13
+ import { isFilterItemActive } from '$lib/filter'
14
+ import { cdnBaseUrl } from '$lib/constants'
15
+
16
+ interface Props {
17
+ filter: ExploreFilter
18
+ limit?: boolean
19
+ hideActive?: boolean
20
+ showSorting?: boolean
21
+ sortings?: null | ExploreTitleSorting[]
22
+ searchQuery?: string
23
+ onchange?: () => void
24
+ onempty?: () => void
25
+ }
26
+
27
+ const {
28
+ filter,
29
+ limit = true,
30
+ hideActive = false,
31
+ showSorting = true,
32
+ sortings = null,
33
+ searchQuery = '',
34
+ onchange = () => null,
35
+ onempty = () => null,
36
+ }: Props = $props()
37
+
38
+ const shownItemsLimit = 3
39
+ const items = [{
40
+ label: t('Services'),
41
+ param: 'providers',
42
+ fetchData: async () => (await fetchProviders()).map(playlink => ({ label: playlink.name, value: playlink.slug })),
43
+ }, {
44
+ label: t('Genres'),
45
+ param: 'genres',
46
+ data: genres.filter(genre => parseInt(genre.slug) < 900).map(genre => ({ label: t(genre.name), value: genre.slug })),
47
+ }, {
48
+ label: t('IMDb'),
49
+ param: 'imdb_score',
50
+ range: [0, 10] as [number, number],
51
+ }, {
52
+ label: t('Year'),
53
+ param: 'year',
54
+ range: [1900, new Date().getFullYear()] as [number, number],
55
+ }, {
56
+ label: t('Runtime'),
57
+ param: 'length',
58
+ range: [0, 180] as [number, number],
59
+ valueAppend: t('Minutes'),
60
+ }, {
61
+ label: t('Countries'),
62
+ param: 'country_codes',
63
+ fetchData: async () => import.meta.env.DEV ?
64
+ (await import('$lib/data/countries.json')).default :
65
+ (await fetch(`${cdnBaseUrl}/src/lib/data/countries.json`)).json(),
66
+ }, {
67
+ label: t('Category'),
68
+ param: 'category',
69
+ data: [{ label: t('Movies'), value: 'movie' }, { label: t('Shows'), value: 'series' }, { label: t('Kids'), value: 'kids' }, { label: t('Documentary'), value: 'documentary' }],
70
+ }]
71
+
72
+ let limited = $derived(limit)
73
+ let wasPreviouslyActive = false
74
+
75
+ $effect(() => {
76
+ const hasAnyActiveFilter = Object.keys(filter).some((param) => {
77
+ const { range } = items.find((i: any) => param === i.param) || {}
78
+
79
+ return isFilterItemActive(filter[param], range)
80
+ })
81
+
82
+ if (hasAnyActiveFilter || searchQuery) {
83
+ wasPreviouslyActive = true
84
+ } else if (wasPreviouslyActive) {
85
+ wasPreviouslyActive = false
86
+ onempty()
87
+ }
88
+ })
89
+ </script>
90
+
91
+ <div class="filter" role="navigation" class:limit>
92
+ <div class="items">
93
+ {#each limited ? items.slice(0, shownItemsLimit) : items as item}
94
+ <div transition:scale={{ start: 0.75, duration: 100 }}>
95
+ <FilterItem {filter} {onchange} {...item} />
96
+ </div>
97
+ {/each}
98
+ </div>
99
+
100
+ <div class="actions">
101
+ {#if limit}
102
+ <Button variant="link" onclick={() => limited = !limited}>
103
+ <IconFilter />
104
+ {t('All Filters')}
105
+ <IconArrow direction={limited ? 'down' : 'up'} />
106
+ </Button>
107
+ {/if}
108
+
109
+ {#if showSorting}
110
+ <div class="sorting" transition:fly={{ x: 5, duration: 100 }}>
111
+ <FilterSorting {filter} {onchange} {sortings} />
112
+ </div>
113
+ {/if}
114
+ </div>
115
+ </div>
116
+
117
+ {#if !hideActive}
118
+ <ActiveFilterItems {filter} {items} />
119
+ {/if}
120
+
121
+ <style lang="scss">
122
+ .filter {
123
+ position: relative;
124
+ }
125
+
126
+ .items {
127
+ display: flex;
128
+ flex-wrap: wrap;
129
+ gap: margin(0.5);
130
+ padding-right: margin(7);
131
+
132
+ .limit & {
133
+ padding-right: 0;
134
+ }
135
+ }
136
+
137
+ .actions {
138
+ position: absolute;
139
+ top: margin(0.25);
140
+ right: 0;
141
+
142
+ .limit & {
143
+ position: initial;
144
+ top: auto;
145
+ display: flex;
146
+ justify-content: space-between;
147
+ margin-top: margin(0.5);
148
+ }
149
+ }
150
+
151
+ .sorting {
152
+ margin-top: margin(0.1);
153
+ }
154
+ </style>
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
2
  import Button from '../../Button.svelte'
3
3
  import IconArrow from '../../Icons/IconArrow.svelte'
4
- import type { ExploreFilter } from '$lib/types/filter'
4
+ import type { ExploreFilter, ExploreTitleSorting } from '$lib/types/filter'
5
5
  import { t } from '$lib/localization'
6
6
  import Dropdown from './Dropdown.svelte'
7
7
  import { Sorting } from '$lib/enums/Sorting'
@@ -9,17 +9,18 @@
9
9
  interface Props {
10
10
  filter: ExploreFilter
11
11
  onchange?: () => void
12
+ sortings?: null | ExploreTitleSorting[]
12
13
  }
13
14
 
14
- const { filter, onchange = () => null }: Props = $props()
15
+ const { filter, onchange = () => null, sortings = null }: Props = $props()
15
16
 
16
17
  const param = 'ordering'
17
18
 
18
- const options = [
19
+ const options = $derived(sortings || [
19
20
  { label: 'Popularity', value: Sorting.Popular },
20
21
  { label: 'New', value: Sorting.New },
21
22
  { label: 'Top Rated', value: Sorting.Best },
22
- ]
23
+ ])
23
24
 
24
25
  const label = $derived(options.find(({ value }) => value === filter[param]?.value)?.label || 'Sort By')
25
26
 
@@ -20,9 +20,12 @@
20
20
  interface Props {
21
21
  searchQuery?: string,
22
22
  filter?: ExploreFilter
23
+ forceGrid?: boolean
24
+ hidePlaylinks?: boolean
25
+ onTitleClick?: null | ((event: MouseEvent, titles: TitleData[], index: number) => void)
23
26
  }
24
27
 
25
- const { searchQuery = '', filter = {} }: Props = $props()
28
+ const { searchQuery = '', filter = {}, forceGrid = false, hidePlaylinks = false, onTitleClick = null }: Props = $props()
26
29
 
27
30
  // svelte-ignore non_reactive_update
28
31
  let page = 1
@@ -33,12 +36,13 @@
33
36
  let promise = $state(getTitlesForFilter())
34
37
  let width = $state(0)
35
38
 
36
- const grid = $derived(width > 500)
39
+ const grid = $derived(forceGrid || width > 500)
37
40
  const gridColumns = $derived.by(() => {
38
41
  if (width > 1400) return 8
39
42
  if (width > 900) return 6
40
- else if (width > 600) return 4
41
- return 3
43
+ if (width > 600) return 4
44
+ if (width > 400) return 3
45
+ return 2
42
46
  })
43
47
  const TitleComponent = $derived(grid ? GridTitle : ListTitle)
44
48
  const SkeletonComponent = $derived(grid ? GridTitleSkeleton : ListTitleSkeleton)
@@ -51,9 +55,7 @@
51
55
  if (filter) setFilter()
52
56
  })
53
57
 
54
- onDestroy(() => {
55
- emptyFilter()
56
- })
58
+ onDestroy(emptyFilter)
57
59
 
58
60
  async function getTitlesForFilter(): Promise<APIPaginatedResult<TitleData>> {
59
61
  latestRequestId += 1
@@ -130,7 +132,12 @@
130
132
  track(TrackingEvent.ExploreShowMore, null, { page })
131
133
  }
132
134
 
133
- function openTitle(event: MouseEvent, title: TitleData): void {
135
+ function openTitle(event: MouseEvent, title: TitleData, index: number): void {
136
+ if (onTitleClick) {
137
+ onTitleClick(event, titles, index)
138
+ return
139
+ }
140
+
134
141
  openModal({ event, data: title })
135
142
  track(TrackingEvent.ExploreTitleClick, title)
136
143
  }
@@ -145,9 +152,9 @@
145
152
  </script>
146
153
 
147
154
  <div class="titles" class:grid role="main" style:--grid-columns={gridColumns} bind:clientWidth={width} data-testid="explore-results">
148
- {#each titles as title}
155
+ {#each titles as title, index}
149
156
  {#key title.sid}
150
- <TitleComponent {title} onclick={(event: MouseEvent) => openTitle(event, title)} />
157
+ <TitleComponent {title} {hidePlaylinks} onclick={(event: MouseEvent) => openTitle(event, title, index)} />
151
158
  {/key}
152
159
  {/each}
153
160
 
@@ -28,7 +28,7 @@
28
28
 
29
29
  {#snippet back(offset = false)}
30
30
  <div class="back" class:offset>
31
- <Button variant="link" onclick={() => { navigate('home') }}>
31
+ <Button variant="link" onclick={() => navigate('home')}>
32
32
  <IconArrow direction="left" />
33
33
  {t('Home')}
34
34
  </Button>
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import type { TitleData } from '$lib/types/title'
3
+ import { getContext } from 'svelte'
3
4
  import IconIMDb from './Icons/IconIMDb.svelte'
4
5
  import PlaylinksCompact from './Playlinks/PlaylinksCompact.svelte'
5
6
  import TitlePoster from './TitlePoster.svelte'
@@ -7,9 +8,10 @@
7
8
  interface Props {
8
9
  title: TitleData
9
10
  onclick?: (event: MouseEvent) => void
11
+ hidePlaylinks?: boolean
10
12
  }
11
13
 
12
- const { title, onclick = () => null }: Props = $props()
14
+ const { title, onclick = () => null, hidePlaylinks = false }: Props = $props()
13
15
  </script>
14
16
 
15
17
  <button class="title" {onclick} data-testid="title">
@@ -33,9 +35,11 @@
33
35
  </div>
34
36
 
35
37
  <div class="content">
36
- <div class="heading">{title.title}</div>
38
+ <div class="heading" class:margin={!hidePlaylinks}>{title.title}</div>
37
39
 
38
- <PlaylinksCompact playlinks={title.providers} size={20} />
40
+ {#if !hidePlaylinks}
41
+ <PlaylinksCompact playlinks={title.providers} size={20} />
42
+ {/if}
39
43
  </div>
40
44
  </button>
41
45
 
@@ -81,7 +85,6 @@
81
85
  }
82
86
 
83
87
  .heading {
84
- margin-bottom: margin(0.5);
85
88
  overflow: hidden;
86
89
  text-overflow: ellipsis;
87
90
  white-space: nowrap;
@@ -96,6 +99,10 @@
96
99
  color: theme(grid-item-title-hover-text-color, text-color);
97
100
  text-decoration: theme(grid-item-title-hover-text-decoration, none);
98
101
  }
102
+
103
+ &.margin {
104
+ margin-bottom: margin(0.5);
105
+ }
99
106
  }
100
107
 
101
108
  .meta {
@@ -215,6 +215,10 @@
215
215
  &.background-blur {
216
216
  backdrop-filter: blur(1.5rem);
217
217
  }
218
+
219
+ :global(*) {
220
+ box-sizing: border-box;
221
+ }
218
222
  }
219
223
 
220
224
  .dialog,
@@ -223,7 +227,7 @@
223
227
  max-width: 600px;
224
228
 
225
229
  .wide & {
226
- max-width: 900px;
230
+ max-width: 1200px;
227
231
  }
228
232
  }
229
233
 
@@ -14,8 +14,6 @@
14
14
 
15
15
  const { participant, initialScrollPosition = 0, ...restProps }: Props = $props()
16
16
 
17
- let windowWidth = $state(0)
18
-
19
17
  track(TrackingEvent.ParticipantModalView, null, { participant: participant.name })
20
18
 
21
19
  onMount(() => {
@@ -25,8 +23,6 @@
25
23
  })
26
24
  </script>
27
25
 
28
- <svelte:window bind:innerWidth={windowWidth} />
29
-
30
- <Modal {initialScrollPosition} {...restProps} closeButtonStyle="flat" tall>
26
+ <Modal {initialScrollPosition} {...restProps} closeButtonStyle="flat" tall wide blur>
31
27
  <Participant {participant} />
32
28
  </Modal>