@playpilot/tpi 3.0.1 → 3.1.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.
Files changed (30) hide show
  1. package/dist/link-injections.js +7 -7
  2. package/package.json +1 -1
  3. package/src/lib/api.ts +21 -1
  4. package/src/lib/enums/TrackingEvent.ts +2 -0
  5. package/src/lib/linkInjection.ts +8 -8
  6. package/src/lib/text.ts +24 -0
  7. package/src/lib/types/config.d.ts +3 -0
  8. package/src/routes/+page.svelte +18 -3
  9. package/src/routes/components/Editorial/AIIndicator.svelte +43 -2
  10. package/src/routes/components/Editorial/DragHandle.svelte +3 -3
  11. package/src/routes/components/Editorial/Editor.svelte +58 -28
  12. package/src/routes/components/Editorial/EditorItem.svelte +52 -24
  13. package/src/routes/components/Editorial/ManualInjection.svelte +69 -16
  14. package/src/routes/components/Editorial/ResizeHandle.svelte +111 -0
  15. package/src/routes/components/Editorial/Search/TitleSearch.svelte +17 -90
  16. package/src/routes/components/Editorial/Search/TitleSearchItem.svelte +107 -0
  17. package/src/routes/components/Title.svelte +28 -4
  18. package/src/tests/helpers.js +2 -1
  19. package/src/tests/lib/api.test.js +30 -1
  20. package/src/tests/lib/linkInjection.test.js +39 -11
  21. package/src/tests/lib/text.test.js +23 -1
  22. package/src/tests/routes/+page.test.js +52 -5
  23. package/src/tests/routes/components/Editorial/AiIndicator.test.js +5 -2
  24. package/src/tests/routes/components/Editorial/DragHandle.test.js +5 -5
  25. package/src/tests/routes/components/Editorial/Editor.test.js +69 -0
  26. package/src/tests/routes/components/Editorial/EditorItem.test.js +34 -10
  27. package/src/tests/routes/components/Editorial/ManualInjection.test.js +5 -1
  28. package/src/tests/routes/components/Editorial/ResizeHandle.test.js +45 -0
  29. package/src/tests/routes/components/Editorial/Search/TitleSearch.test.js +6 -4
  30. package/src/tests/routes/components/Editorial/Search/TitleSearchItem.test.js +44 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playpilot/tpi",
3
- "version": "3.0.1",
3
+ "version": "3.1.0-beta.2",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite dev",
package/src/lib/api.ts CHANGED
@@ -3,6 +3,7 @@ import { apiBaseUrl } from './constants'
3
3
  import { stringToHash } from './hash'
4
4
  import { getLanguage } from './localization'
5
5
  import { getPageMetaData } from './meta'
6
+ import type { ConfigResponse } from './types/config'
6
7
  import type { LinkInjectionResponse, LinkInjection } from './types/injection'
7
8
  import { getFullUrlPath } from './url'
8
9
 
@@ -123,6 +124,25 @@ export async function saveLinkInjections(linkInjections: LinkInjection[], html:
123
124
  return linkInjections
124
125
  }
125
126
 
127
+ export async function fetchConfig(): Promise<ConfigResponse | null> {
128
+ const headers = new Headers({ 'Content-Type': 'application/json' })
129
+ const apiToken = getApiToken()
130
+
131
+ if (!apiToken) throw new Error('No token was provided')
132
+
133
+ const response = await fetch(apiBaseUrl + `/domains/config?api-token=${apiToken}`, {
134
+ headers,
135
+ })
136
+
137
+ if (!response.ok) throw response
138
+
139
+ try {
140
+ return await response.json() || null
141
+ } catch {
142
+ return null
143
+ }
144
+ }
145
+
126
146
  /**
127
147
  * Insert random keys into link injections. These are used to identify the links on the page.
128
148
  * We can't just use SIDs, as a page might include multiple links of the same title
@@ -143,6 +163,6 @@ export function generateInjectionKey(sid: string): string {
143
163
  return sid + '-' + (Math.random() + 1).toString(36).substring(7)
144
164
  }
145
165
 
146
- export function getApiToken() {
166
+ export function getApiToken(): string | undefined {
147
167
  return window.PlayPilotLinkInjections?.token
148
168
  }
@@ -14,4 +14,6 @@ export const TrackingEvent = Object.freeze({
14
14
  AfterArticleModalButtonClick: 'ali_after_article_modal_button_click',
15
15
 
16
16
  InjectionFailed: 'ali_injection_failed',
17
+ TotalInjectionsCount: 'ali_injection_count',
18
+ FetchingConfigFailed: 'ali_fetch_config_failed',
17
19
  })
@@ -3,7 +3,6 @@ import TitlePopover from '../routes/components/TitlePopover.svelte'
3
3
  import AfterArticlePlaylinks from '../routes/components/AfterArticlePlaylinks.svelte'
4
4
  import { cleanPhrase, findTextNodeContaining, isNodeInLink, replaceStartingFrom } from './text'
5
5
  import { getLargestValueInArray } from './array'
6
- import { decodeHtmlEntities } from './html'
7
6
  import type { LinkInjection, LinkInjectionTypes, LinkInjectionRanges } from './types/injection'
8
7
 
9
8
  const keyDataAttribute = 'data-playpilot-injection-key'
@@ -343,24 +342,25 @@ export function isValidInjection(injection: LinkInjection): boolean {
343
342
  * Filter links for in-text injections, removing after article, inactive, removed, duplicate, and items without title_details
344
343
  */
345
344
  export function filterInvalidInTextInjections(injections: LinkInjection[]): LinkInjection[] {
346
- return filterRemovedInjections(injections).filter(i => i.in_text !== false && isValidInjection(i))
345
+ return filterRemovedAndInactiveInjections(injections).filter(i => i.in_text !== false && isValidInjection(i))
347
346
  }
348
347
 
349
348
  /**
350
349
  * Filter links for after article injections, removing in-text only, inactive, removed, duplicate, and items without title_details
351
350
  */
352
351
  export function filterInvalidAfterArticleInjections(injections: LinkInjection[]): LinkInjection[] {
353
- return filterRemovedInjections(injections).filter(i => i.after_article === true && isValidInjection(i))
352
+ return filterRemovedAndInactiveInjections(injections).filter(i => i.after_article === true && isValidInjection(i))
354
353
  }
355
354
 
356
355
  /**
357
- * Filter injections that were marked as removed or have an equivalent removed manual injections, soley based on the same sentence and title.
356
+ * Filter injections that were marked as removed or inactive or have an equivalent removed or inactive manual injections, soley based on the same sentence and title.
358
357
  */
359
- export function filterRemovedInjections(injections: LinkInjection[]): LinkInjection[] {
358
+ export function filterRemovedAndInactiveInjections(injections: LinkInjection[]): LinkInjection[] {
360
359
  return injections.filter(injection => {
361
- if (injection.removed) return false
362
- if (injection.manual && !injection.removed) return true
363
- return !injections.some(i => i.manual && i.removed && isEquivalentInjection(i, injection))
360
+ if (injection.removed || injection.inactive) return false
361
+ if (injection.manual && (!injection.removed && !injection.inactive)) return true
362
+
363
+ return !injections.some(i => i.manual && (i.removed || i.inactive) && isEquivalentInjection(i, injection))
364
364
  })
365
365
  }
366
366
 
package/src/lib/text.ts CHANGED
@@ -55,3 +55,27 @@ export function replaceStartingFrom(text: string, search: string, replacement: s
55
55
  export function cleanPhrase(phrase: string): string {
56
56
  return decodeHtmlEntities(phrase).toLowerCase().replace(/\s+/g, '')
57
57
  }
58
+
59
+ /**
60
+ * Truncate a string, but leave a given phrase within view, truncating at both the start and end if necessary.
61
+ */
62
+ export function truncateAroundPhrase(sentence: string, phrase: string, maxLength: number): string {
63
+ const index = sentence.indexOf(phrase)
64
+ const phraseStart = index
65
+ const padding = Math.floor((maxLength - phrase.length) / 2)
66
+
67
+ let start = Math.max(0, phraseStart - padding)
68
+ let end = start + maxLength
69
+
70
+ if (end > sentence.length) {
71
+ end = sentence.length
72
+ start = Math.max(0, end - maxLength)
73
+ }
74
+
75
+ let result = sentence.slice(start, end).trim()
76
+
77
+ if (start > 0) result = '…' + result
78
+ if (end < sentence.length) result += '…'
79
+
80
+ return result
81
+ }
@@ -0,0 +1,3 @@
1
+ export type ConfigResponse = {
2
+ exclude_urls_pattern?: string
3
+ }
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { onMount } from 'svelte'
3
- import { pollLinkInjections } from '$lib/api'
3
+ import { fetchConfig, pollLinkInjections } from '$lib/api'
4
4
  import { clearLinkInjections, getLinkInjectionElements, getLinkInjectionsParentElement, injectLinksInDocument, separateLinkInjectionTypes } from '$lib/linkInjection'
5
5
  import { track } from '$lib/tracking'
6
6
  import { getFullUrlPath } from '$lib/url'
@@ -41,10 +41,25 @@
41
41
  async function initialize(): Promise<void> {
42
42
  if (isEditorialMode) authorized = await authorize()
43
43
 
44
+ const url = getFullUrlPath()
45
+
46
+ try {
47
+ const config = await fetchConfig()
48
+
49
+ // URL was marked as being excluded, we stop injections here unless we're in editorial mode.
50
+ if (!isEditorialMode && config?.exclude_urls_pattern && url.match(config.exclude_urls_pattern)) return
51
+ } catch(error) {
52
+ // We also return if the config did not get fetched properly, as we can't determine what should and should
53
+ // get injected without it.
54
+ track(TrackingEvent.FetchingConfigFailed)
55
+ console.error('TPI Config failed to fetch', error)
56
+ return
57
+ }
58
+
44
59
  // Only trying once when not in editorial mode to prevent late injections (as well as a ton of requests)
45
60
  // by users who are not in the editorial view.
46
61
  // [TODO] TEMP: Only try once for editorial as well
47
- response = await pollLinkInjections(getFullUrlPath(), htmlString, { maxTries: 1 })
62
+ response = await pollLinkInjections(url, htmlString, { maxTries: 1 })
48
63
 
49
64
  inject({ aiInjections, manualInjections })
50
65
 
@@ -56,7 +71,7 @@
56
71
  if (!response?.ai_running) return
57
72
  if (!isEditorialMode) return
58
73
 
59
- const continuedResponse = await pollLinkInjections(getFullUrlPath(), htmlString, { requireCompletedResult: true })
74
+ const continuedResponse = await pollLinkInjections(url, htmlString, { requireCompletedResult: true })
60
75
 
61
76
  // @ts-ignore
62
77
  editor.requestNewAIInjections(continuedResponse?.ai_injections || [])
@@ -5,9 +5,15 @@
5
5
  interface Props {
6
6
  // eslint-disable-next-line no-unused-vars
7
7
  onadd: (injections: LinkInjection[]) => void
8
+ /** Used to guesstimate the load times. */
9
+ htmlString?: string
8
10
  }
9
11
 
10
- let { onadd }: Props = $props()
12
+ const { onadd, htmlString = '' }: Props = $props()
13
+
14
+ // Guesstimate AI load times based on the given text length.
15
+ // The value will always be between 1 and 10 minutes.
16
+ const fakeLoadTimes = $derived(Math.min(Math.max(htmlString.length * 30, 60000), 600000))
11
17
 
12
18
  let running = $state(true)
13
19
  let injectionsToBeInserted: LinkInjection[] = $state([])
@@ -33,7 +39,8 @@
33
39
 
34
40
  <div>
35
41
  {#if running}
36
- AI links are currently processing. You can add manual links while this is ongoing.
42
+ AI links are currently processing. This can take several minutes.<br>
43
+ You can add manual links while this is ongoing.
37
44
  {:else if injectionsToBeInserted?.length}
38
45
  AI links are ready.
39
46
  <strong>{injectionsToBeInserted.length} New {injectionsToBeInserted.length > 1 ? 'links were' : 'link was'} found.</strong>
@@ -48,6 +55,10 @@
48
55
  <button class="button" onclick={() => dismissed = true}>Dismiss</button>
49
56
  {/if}
50
57
  </div>
58
+
59
+ {#if running}
60
+ <div class="loading-bar" data-testid="loading-bar" style:animation-duration="{fakeLoadTimes}ms"></div>
61
+ {/if}
51
62
  </div>
52
63
 
53
64
  <div class="border">
@@ -76,6 +87,7 @@
76
87
  line-height: 1.5;
77
88
  z-index: 1;
78
89
  color: var(--playpilot-text-color-alt);
90
+ overflow: hidden;
79
91
  }
80
92
 
81
93
  .icon {
@@ -133,4 +145,33 @@
133
145
  color: white;
134
146
  }
135
147
  }
148
+
149
+ @keyframes fake-load {
150
+ 0% {
151
+ width: 0%;
152
+ }
153
+
154
+ 2% {
155
+ width: 20%;
156
+ }
157
+
158
+ 70% {
159
+ width: 85%;
160
+ }
161
+
162
+ 100% {
163
+ width: 90%;
164
+ }
165
+ }
166
+
167
+ .loading-bar {
168
+ position: absolute;
169
+ bottom: 0;
170
+ left: 0;
171
+ height: 0.15em;
172
+ border-radius: 0 0.25rem 0.25rem 0;
173
+ background: linear-gradient(to right, var(--playpilot-light), var(--playpilot-green));
174
+ animation: fake-load forwards;
175
+ animation-duration: 20000ms;
176
+ }
136
177
  </style>
@@ -88,7 +88,7 @@
88
88
  z-index: 10;
89
89
  appearance: none;
90
90
  position: absolute;
91
- top: 0;
91
+ top: margin(0.5);
92
92
  left: 30%;
93
93
  width: 40%;
94
94
  height: margin(1);
@@ -114,9 +114,9 @@
114
114
  display: block;
115
115
  content: "";
116
116
  position: absolute;
117
- top: 40%;
117
+ top: 20%;
118
118
  right: 0;
119
- bottom: 40%;
119
+ bottom: 60%;
120
120
  left: 0;
121
121
  border-radius: margin(1);
122
122
  background: var(--playpilot-text-color);
@@ -1,8 +1,8 @@
1
1
  <script lang="ts">
2
2
  import { fade, fly, slide } from 'svelte/transition'
3
- import { flip } from 'svelte/animate'
4
3
  import EditorItem from './EditorItem.svelte'
5
4
  import DragHandle from './DragHandle.svelte'
5
+ import ResizeHandle from './ResizeHandle.svelte'
6
6
  import Alert from './Alert.svelte'
7
7
  import ManualInjection from './ManualInjection.svelte'
8
8
  import RoundButton from '../RoundButton.svelte'
@@ -12,6 +12,8 @@
12
12
  import { isEquivalentInjection, isValidInjection } from '$lib/linkInjection'
13
13
  import type { Position } from '$lib/types/position'
14
14
  import type { LinkInjection } from '$lib/types/injection'
15
+ import { track } from '$lib/tracking'
16
+ import { TrackingEvent } from '$lib/enums/TrackingEvent'
15
17
 
16
18
  interface Props {
17
19
  linkInjections: LinkInjection[],
@@ -23,9 +25,11 @@
23
25
  let { linkInjections = $bindable(), htmlString = '', loading = false, aiRunning = false }: Props = $props()
24
26
 
25
27
  const editorPositionKey = 'editor-position'
28
+ const editorHeightKey = 'editor-height'
26
29
 
27
30
  let editorElement: HTMLElement | null = $state(null)
28
31
  let position: Position = $state(JSON.parse(localStorage.getItem(editorPositionKey) || '{ "x": 0, "y": 0 }'))
32
+ let height: number = $state(parseInt(localStorage.getItem(editorHeightKey) || '0'))
29
33
  let manualInjectionActive = $state(false)
30
34
  let saving = $state(false)
31
35
  let hasError = $state(false)
@@ -35,12 +39,26 @@
35
39
 
36
40
  const linkInjectionsString = $derived(JSON.stringify(linkInjections))
37
41
  const hasChanged = $derived(initialStateString !== linkInjectionsString)
38
- const filteredInjections = $derived(linkInjections.filter((i) => i.title_details && !i.removed && !i.duplicate))
42
+ // Filter out injections without title_details, injections that are removed, duplicate, or are AI injections that failed to inject
43
+ const filteredInjections = $derived(linkInjections.filter((i) => i.title_details && !i.removed && !i.duplicate && (i.manual || (!i.manual && !i.failed))))
44
+ const sortedInjections = $derived(sortInjections(filteredInjections))
39
45
 
40
46
  $effect(() => {
41
- if (!loading) untrack(() => initialStateString = linkInjectionsString)
47
+ if (loading) return
48
+
49
+ untrack(() => {
50
+ initialStateString = linkInjectionsString
51
+ trackInjectionsCount()
52
+ })
42
53
  })
43
54
 
55
+ function sortInjections(injections: LinkInjection[]): LinkInjection[] {
56
+ return injections.sort((a, b) => {
57
+ if (a.failed !== b.failed) return a.failed ? 1 : -1
58
+ return a.title_details!.title.localeCompare(b.title_details!.title)
59
+ })
60
+ }
61
+
44
62
  /** Save the given injections in their current state and rewrite object to returned value */
45
63
  async function save(): Promise<void> {
46
64
  try {
@@ -56,8 +74,8 @@
56
74
  }
57
75
  }
58
76
 
59
- function savePosition(position: Position): void {
60
- localStorage.setItem(editorPositionKey, JSON.stringify(position))
77
+ function saveLocalStorage(key: string, value: string): void {
78
+ localStorage.setItem(key, value)
61
79
  }
62
80
 
63
81
  function removeInjection(key: string): void {
@@ -69,9 +87,6 @@
69
87
  linkInjections = [...linkInjections]
70
88
  }
71
89
 
72
- /**
73
- * @param {HTMLElement} itemElement
74
- */
75
90
  function scrollToItem(itemElement: HTMLElement): void {
76
91
  if (!editorElement) return
77
92
 
@@ -90,18 +105,25 @@
90
105
  }
91
106
  }
92
107
 
93
- /**
94
- * @param {Event} event
95
- */
96
108
  function onscroll(event: Event): void {
97
109
  const target = event.target as HTMLElement
98
110
  scrollDistance = target.scrollTop
99
111
  }
100
112
 
113
+ function trackInjectionsCount() {
114
+ const payload = {
115
+ total: linkInjections.length.toString(),
116
+ failed_automatic: linkInjections.filter(i => i.failed && !i.manual).length.toString(),
117
+ failed_manual: linkInjections.filter(i => i.failed && i.manual).length.toString(),
118
+ final_injected: filteredInjections.length.toString(),
119
+ }
120
+
121
+ track(TrackingEvent.TotalInjectionsCount, null, payload)
122
+ }
123
+
101
124
  /**
102
125
  * This is called from outside when new AI links are ready.
103
126
  * We only pass on links that do not already exist.
104
- * @param {LinkInjection[]} injections
105
127
  */
106
128
  export function requestNewAIInjections(injections: LinkInjection[]): void {
107
129
  const newInjections = injections.filter(injection => {
@@ -115,20 +137,26 @@
115
137
  </script>
116
138
 
117
139
  <section class="editor playpilot-styled-scrollbar" class:panel-open={manualInjectionActive} class:loading bind:this={editorElement} {onscroll}>
118
- {#if editorElement}
119
- <div class="drag-handle">
120
- <DragHandle element={editorElement} {position} limit={{ x: 16, y: 16 }} onchange={savePosition} />
121
- </div>
122
- {/if}
123
-
124
140
  <header class="header">
141
+ {#if editorElement}
142
+ {#if !loading}
143
+ <div class="handle">
144
+ <ResizeHandle element={editorElement} {height} onchange={(height) => saveLocalStorage(editorHeightKey, JSON.stringify(height))} />
145
+ </div>
146
+ {/if}
147
+
148
+ <div class="handle">
149
+ <DragHandle element={editorElement} {position} limit={{ x: 16, y: 16 }} onchange={(position) => saveLocalStorage(editorPositionKey, JSON.stringify(position))} />
150
+ </div>
151
+ {/if}
152
+
125
153
  <h1>Playlinks</h1>
126
154
 
127
155
  {#if loading}
128
156
  <div class="loading">Loading...</div>
129
157
  {:else}
130
- <div class="bubble" aria-label="{linkInjections.length} found playlinks">
131
- {linkInjections.length}
158
+ <div class="bubble" aria-label="{filteredInjections.length} found playlinks">
159
+ {filteredInjections.length}
132
160
  </div>
133
161
 
134
162
  <RoundButton onclick={() => manualInjectionActive = true} size="24px" aria-label="Add manual injection">
@@ -138,7 +166,7 @@
138
166
  </header>
139
167
 
140
168
  {#if !loading && aiRunning}
141
- <AIIndicator bind:this={aIIndicator} onadd={(newInjections) => newInjections.forEach(i => linkInjections.push(i))} />
169
+ <AIIndicator {htmlString} bind:this={aIIndicator} onadd={(newInjections) => newInjections.forEach(i => linkInjections.push(i))} />
142
170
  {/if}
143
171
 
144
172
  {#if !loading}
@@ -149,13 +177,11 @@
149
177
  {/if}
150
178
 
151
179
  <div class="items">
152
- {#each filteredInjections as linkInjection (linkInjection.key)}
180
+ {#each sortedInjections as linkInjection (linkInjection.key)}
153
181
  <!-- We want to bind to the original object, not the derived object, so we get the index of the injection in the original object by it's key -->
154
182
  {@const index = linkInjections.findIndex((i) => i.key === linkInjection.key)}
155
183
 
156
- <div animate:flip={{ duration: 300 }}>
157
- <EditorItem bind:linkInjection={linkInjections[index]} onremove={() => removeInjection(linkInjection.key)} onhighlight={scrollToItem} />
158
- </div>
184
+ <EditorItem bind:linkInjection={linkInjections[index]} onremove={() => removeInjection(linkInjection.key)} onhighlight={scrollToItem} />
159
185
  {:else}
160
186
  <div class="empty">No links available. Add links manually by clicking the + button and select text to add a link to.</div>
161
187
  {/each}
@@ -170,7 +196,7 @@
170
196
 
171
197
  {#if manualInjectionActive}
172
198
  <div
173
- class="panel"
199
+ class="panel playpilot-styled-scrollbar"
174
200
  style:top="{scrollDistance}px"
175
201
  transition:fly={{ x: Math.min(window.innerWidth, 320), duration: 200, opacity: 1 }}>
176
202
  <ManualInjection
@@ -198,8 +224,9 @@
198
224
  bottom: margin(1);
199
225
  right: margin(1);
200
226
  width: 100%;
201
- max-width: margin(20);
227
+ max-width: margin(22);
202
228
  height: min(50vh, margin(50));
229
+ min-height: 10rem;
203
230
  padding: margin(1);
204
231
  border-radius: margin(1.5);
205
232
  background: var(--playpilot-dark);
@@ -218,6 +245,7 @@
218
245
 
219
246
  .loading {
220
247
  height: auto;
248
+ min-height: 0;
221
249
  border-radius: margin(2);
222
250
  margin-left: auto;
223
251
  padding-right: margin(0.5);
@@ -225,7 +253,7 @@
225
253
  font-size: margin(0.85);
226
254
  }
227
255
 
228
- .drag-handle {
256
+ .handle {
229
257
  opacity: 0;
230
258
  transition: opacity 150ms;
231
259
 
@@ -240,6 +268,7 @@
240
268
  top: margin(-1);
241
269
  margin: margin(-1) margin(-1) 0;
242
270
  padding: margin(1) margin(1) margin(1) margin(1.5);
271
+ border: 0;
243
272
  background: var(--playpilot-dark);
244
273
  display: flex;
245
274
  align-items: center;
@@ -291,6 +320,7 @@
291
320
  transition: opacity 100ms;
292
321
  font-family: inherit;
293
322
  color: var(--playpilot-text-color-alt);
323
+ font-size: 0.85rem;
294
324
  cursor: pointer;
295
325
 
296
326
  &:hover {
@@ -1,7 +1,6 @@
1
1
  <script lang="ts">
2
2
  import { slide } from 'svelte/transition'
3
3
  import IconChevron from '../Icons/IconChevron.svelte'
4
- import IconIMDb from '../Icons/IconIMDb.svelte'
5
4
  import Switch from './Switch.svelte'
6
5
  import TextInput from './TextInput.svelte'
7
6
  import PlaylinkTypeSelect from './PlaylinkTypeSelect.svelte'
@@ -12,6 +11,7 @@
12
11
  import { TrackingEvent } from '$lib/enums/TrackingEvent'
13
12
  import type { LinkInjection } from '$lib/types/injection'
14
13
  import type { TitleData } from '$lib/types/title'
14
+ import { truncateAroundPhrase } from '$lib/text'
15
15
 
16
16
  interface Props {
17
17
  linkInjection: LinkInjection,
@@ -22,17 +22,19 @@
22
22
 
23
23
  const { linkInjection = $bindable(), onremove = () => null, onhighlight = () => null }: Props = $props()
24
24
 
25
- const { key, sentence, title_details, failed } = $derived(linkInjection || {})
25
+ const { key, sentence, title_details, failed, inactive } = $derived(linkInjection || {})
26
26
 
27
27
  // @ts-ignore Definitely not null
28
28
  const title: TitleData = $derived(title_details)
29
+ const truncatedSentence = $derived(truncateAroundPhrase(linkInjection.sentence, linkInjection.title, 60))
29
30
 
30
31
  let expanded = $state(false)
32
+ let expandedSentence = $state(false)
31
33
  let highlighted = $state(false)
32
34
  let element: HTMLElement | null = $state(null)
33
35
 
34
36
  onMount(() => {
35
- if (failed) track(TrackingEvent.InjectionFailed, title, { phrase: linkInjection.title, sentence })
37
+ if (failed) track(TrackingEvent.InjectionFailed, title, { phrase: linkInjection.title, sentence})
36
38
  })
37
39
 
38
40
  /**
@@ -85,24 +87,26 @@
85
87
  <div
86
88
  class="item"
87
89
  class:highlighted
90
+ class:inactive
88
91
  onmouseenter={() => toggleOnPageResultHighlight(true)}
89
92
  onmouseleave={() => toggleOnPageResultHighlight(false)}
90
93
  onclick={scrollLinkIntoView}
91
94
  bind:this={element}
92
95
  out:slide|global={{ duration: 200 }}>
93
96
  <div class="header">
94
- <img class="poster" src={title.standing_poster} alt="" />
97
+ <img class="poster" src={title.standing_poster} alt="" width="32" height="48" />
95
98
 
96
99
  <div class="info">
97
100
  <div class="title">{title.title}</div>
98
101
 
99
- <div class="meta">
100
- <span><IconIMDb /> 7.1</span>
101
- <span>{title.year}</span>
102
- <span>{title.type}</span>
102
+ <div class="sentence">
103
+ <!-- eslint-disable-next-line svelte/no-at-html-tags -->
104
+ {@html (expandedSentence ? sentence : truncatedSentence).replace(linkInjection.title, `<em>${linkInjection.title}</em>`)}
103
105
 
104
- {#if title.length}
105
- <span data-testid="length">{title.length}m</span>
106
+ {#if truncatedSentence.length < sentence.length}
107
+ <button class="expand-sentence" onclick={() => expandedSentence = !expandedSentence}>
108
+ {expandedSentence ? 'Less' : 'More'}
109
+ </button>
106
110
  {/if}
107
111
  </div>
108
112
  </div>
@@ -123,8 +127,8 @@
123
127
  <IconChevron {expanded} />
124
128
  </button>
125
129
 
126
- <Switch label="Visible" active={!linkInjection.inactive} onclick={(active) => { linkInjection.inactive = !active; linkInjection.manual = true }}>
127
- Visible
130
+ <Switch label={inactive ? 'Inactive' : 'Visible'} active={!linkInjection.inactive} onclick={(active) => { linkInjection.inactive = !active; linkInjection.manual = true }}>
131
+ {inactive ? 'Inactive' : 'Visible'}
128
132
  </Switch>
129
133
  </div>
130
134
  {/if}
@@ -174,6 +178,10 @@
174
178
  height: margin(3);
175
179
  border-radius: margin(0.25);
176
180
  background: var(--playpilot-content);
181
+
182
+ .inactive & {
183
+ opacity: 0.35;
184
+ }
177
185
  }
178
186
 
179
187
  .title {
@@ -181,26 +189,46 @@
181
189
  word-break: break-word;
182
190
  }
183
191
 
184
- .meta {
185
- display: flex;
186
- gap: margin(0.5);
187
- font-size: margin(0.75);
192
+ .sentence {
193
+ font-size: 0.7em;
194
+ line-height: 150%;
195
+ margin-right: margin(-0.5);
188
196
  color: var(--playpilot-text-color-alt);
197
+ opacity: 0.75;
189
198
 
190
- span {
191
- display: flex;
192
- align-items: center;
199
+ :global(em) {
200
+ color: var(--playpilot-text-color);
201
+ font-weight: bold;
193
202
  }
203
+ }
204
+
205
+ .expand-sentence {
206
+ cursor: pointer;
207
+ appearance: none;
208
+ border: 0;
209
+ padding: 0;
210
+ margin: 0;
211
+ background: transparent;
212
+ font-family: inherit;
213
+ color: inherit;
214
+ font-size: inherit;
215
+ line-height: inherit;
216
+ font-style: italic;
217
+ text-decoration: underline;
194
218
 
195
- :global(svg) {
196
- display: block;
197
- margin: margin(-0.125) margin(0.125) 0 0;
198
- height: 1em;
219
+ &:hover {
220
+ color: var(--playpilot-text-color);
199
221
  }
200
222
  }
201
223
 
202
224
  .content {
203
- padding-top: margin(1);
225
+ padding-top: margin(0.5);
226
+ }
227
+
228
+ .info {
229
+ .inactive & {
230
+ opacity: 0.35;
231
+ }
204
232
  }
205
233
 
206
234
  .context-menu {