@playpilot/tpi 3.1.0 → 3.2.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.
- package/dist/link-injections.js +8 -7
- package/package.json +1 -1
- package/src/lib/actions/heading.ts +11 -0
- package/src/lib/api.ts +1 -3
- package/src/lib/auth.ts +13 -1
- package/src/lib/constants.ts +2 -0
- package/src/lib/enums/TrackingEvent.ts +1 -0
- package/src/lib/linkInjection.ts +43 -32
- package/src/lib/scss/_mixins.scss +8 -0
- package/src/lib/scss/global.scss +6 -6
- package/src/lib/scss/variables.scss +2 -0
- package/src/lib/stores/organization.ts +4 -0
- package/src/lib/tracking.ts +14 -1
- package/src/lib/types/injection.d.ts +5 -0
- package/src/routes/+layout.svelte +6 -2
- package/src/routes/+page.svelte +31 -13
- package/src/routes/components/Description.svelte +2 -3
- package/src/routes/components/Editorial/AIIndicator.svelte +85 -91
- package/src/routes/components/Editorial/Alert.svelte +12 -2
- package/src/routes/components/Editorial/Editor.svelte +82 -61
- package/src/routes/components/Editorial/EditorItem.svelte +32 -7
- package/src/routes/components/Editorial/ManualInjection.svelte +10 -9
- package/src/routes/components/Editorial/PlaylinkTypeSelect.svelte +14 -0
- package/src/routes/components/Editorial/ResizeHandle.svelte +1 -1
- package/src/routes/components/Editorial/Search/TitleSearchItem.svelte +7 -5
- package/src/routes/components/Icons/IconWarning.svelte +5 -0
- package/src/routes/components/Modal.svelte +2 -0
- package/src/routes/components/Playlinks.svelte +12 -11
- package/src/routes/components/Popover.svelte +2 -0
- package/src/routes/components/Title.svelte +3 -2
- package/src/routes/components/TitlePopover.svelte +1 -1
- package/src/tests/lib/auth.test.js +31 -1
- package/src/tests/lib/linkInjection.test.js +87 -48
- package/src/tests/lib/tracking.test.js +61 -1
- package/src/tests/routes/+page.test.js +94 -4
- package/src/tests/routes/components/Editorial/AiIndicator.test.js +28 -42
- package/src/tests/routes/components/Editorial/Alert.test.js +10 -3
- package/src/tests/routes/components/Editorial/Editor.test.js +15 -0
- package/src/tests/routes/components/Editorial/EditorItem.test.js +32 -7
- package/src/tests/routes/components/Editorial/PlaylinkTypeSelect.test.js +13 -1
- package/src/tests/routes/components/Title.test.js +2 -2
- package/svelte.config.js +1 -0
|
@@ -1,75 +1,62 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import type { LinkInjection } from '$lib/types/injection'
|
|
3
2
|
import IconAi from '../Icons/IconAi.svelte'
|
|
4
3
|
|
|
5
4
|
interface Props {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
aiRunning?: boolean
|
|
6
|
+
automationEnabled?: boolean,
|
|
7
|
+
message?: string,
|
|
8
|
+
percentage?: number,
|
|
10
9
|
}
|
|
11
10
|
|
|
12
|
-
const {
|
|
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))
|
|
17
|
-
|
|
18
|
-
let running = $state(true)
|
|
19
|
-
let injectionsToBeInserted: LinkInjection[] = $state([])
|
|
20
|
-
let dismissed = $state(false)
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* This is called from the Editor component when AI links are ready.
|
|
24
|
-
* From here we determine what to show the user. Either we show them an option
|
|
25
|
-
* to inject new links, or we tell them no new injections were found.
|
|
26
|
-
*/
|
|
27
|
-
export function notifyUserOfNewState(injections: LinkInjection[]) {
|
|
28
|
-
running = false
|
|
29
|
-
injectionsToBeInserted = injections
|
|
30
|
-
}
|
|
11
|
+
const { aiRunning = false, automationEnabled = false, message = '', percentage = 0 }: Props = $props()
|
|
31
12
|
</script>
|
|
32
13
|
|
|
33
|
-
{
|
|
34
|
-
<div class="
|
|
35
|
-
<div class="
|
|
36
|
-
<
|
|
37
|
-
<IconAi />
|
|
38
|
-
</div>
|
|
39
|
-
|
|
40
|
-
<div>
|
|
41
|
-
{#if running}
|
|
42
|
-
AI links are currently processing. This can take several minutes.<br>
|
|
43
|
-
You can add manual links while this is ongoing.
|
|
44
|
-
{:else if injectionsToBeInserted?.length}
|
|
45
|
-
AI links are ready.
|
|
46
|
-
<strong>{injectionsToBeInserted.length} New {injectionsToBeInserted.length > 1 ? 'links were' : 'link was'} found.</strong>
|
|
47
|
-
New links will be used next time you refresh the page, or you can insert them now.
|
|
48
|
-
|
|
49
|
-
<button class="button" onclick={() => { onadd(injectionsToBeInserted); dismissed = true }}>
|
|
50
|
-
Add AI links
|
|
51
|
-
</button>
|
|
52
|
-
{:else}
|
|
53
|
-
AI links finished running, but no new links were found.
|
|
54
|
-
|
|
55
|
-
<button class="button" onclick={() => dismissed = true}>Dismiss</button>
|
|
56
|
-
{/if}
|
|
57
|
-
</div>
|
|
58
|
-
|
|
59
|
-
{#if running}
|
|
60
|
-
<div class="loading-bar" data-testid="loading-bar" style:animation-duration="{fakeLoadTimes}ms"></div>
|
|
61
|
-
{/if}
|
|
14
|
+
<div class="ai-indicator" class:running={aiRunning}>
|
|
15
|
+
<div class="content">
|
|
16
|
+
<div class="icon">
|
|
17
|
+
<IconAi />
|
|
62
18
|
</div>
|
|
63
19
|
|
|
64
|
-
<div
|
|
65
|
-
{#if
|
|
66
|
-
<
|
|
20
|
+
<div>
|
|
21
|
+
{#if !automationEnabled}
|
|
22
|
+
<strong>AI processing is disabled.</strong> Enable AI from the <a href="https://partner.playpilot.net">Partner Portal</a>
|
|
23
|
+
{:else if aiRunning}
|
|
24
|
+
AI links are currently processing. This can take several minutes. We'll automatically insert all found injections once ready.
|
|
25
|
+
|
|
26
|
+
<div class="message">
|
|
27
|
+
{message}
|
|
28
|
+
|
|
29
|
+
<span class="ellipses">
|
|
30
|
+
{#each { length: 3 }}
|
|
31
|
+
<span>.</span>
|
|
32
|
+
{/each}
|
|
33
|
+
</span>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<div class="loading-bar">
|
|
37
|
+
<div class="loading-bar-progress">
|
|
38
|
+
<div class="loading-bar-fill" data-testid="loading-bar" style:width="{Math.max(percentage, 3)}%"></div>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<div class="loading-bar-label">{percentage}%</div>
|
|
42
|
+
</div>
|
|
67
43
|
{/if}
|
|
68
44
|
</div>
|
|
45
|
+
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div class="border">
|
|
49
|
+
{#if aiRunning}
|
|
50
|
+
<div class="animator" data-test-id="animator"></div>
|
|
51
|
+
{/if}
|
|
69
52
|
</div>
|
|
70
|
-
|
|
53
|
+
</div>
|
|
71
54
|
|
|
72
55
|
<style lang="scss">
|
|
56
|
+
a {
|
|
57
|
+
color: currentColor;
|
|
58
|
+
}
|
|
59
|
+
|
|
73
60
|
.ai-indicator {
|
|
74
61
|
position: relative;
|
|
75
62
|
margin: 0 margin(0.5);
|
|
@@ -90,6 +77,12 @@
|
|
|
90
77
|
overflow: hidden;
|
|
91
78
|
}
|
|
92
79
|
|
|
80
|
+
.message {
|
|
81
|
+
display: flex;
|
|
82
|
+
margin-top: margin(0.25);
|
|
83
|
+
font-style: italic;
|
|
84
|
+
}
|
|
85
|
+
|
|
93
86
|
.icon {
|
|
94
87
|
color: var(--playpilot-green);
|
|
95
88
|
}
|
|
@@ -129,49 +122,50 @@
|
|
|
129
122
|
filter: blur(5rem);
|
|
130
123
|
}
|
|
131
124
|
|
|
132
|
-
.
|
|
133
|
-
display:
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
border: 1px solid currentColor;
|
|
138
|
-
border-radius: 0.25rem;
|
|
139
|
-
background: transparent;
|
|
140
|
-
color: var(--playpilot-green);
|
|
141
|
-
font-family: var(--playpilot-font-family);
|
|
142
|
-
cursor: pointer;
|
|
125
|
+
.loading-bar {
|
|
126
|
+
display: grid;
|
|
127
|
+
grid-template-columns: auto 3em;
|
|
128
|
+
align-items: center;
|
|
129
|
+
}
|
|
143
130
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
131
|
+
.loading-bar-progress {
|
|
132
|
+
height: 0.5em;
|
|
133
|
+
border-radius: 0.25rem;
|
|
134
|
+
background: var(--playpilot-dark);
|
|
147
135
|
}
|
|
148
136
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
137
|
+
.loading-bar-fill {
|
|
138
|
+
height: 100%;
|
|
139
|
+
border-radius: 0.25rem;
|
|
140
|
+
background: var(--playpilot-green);
|
|
141
|
+
transition: width 200ms;
|
|
142
|
+
}
|
|
153
143
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
144
|
+
.loading-bar-label {
|
|
145
|
+
text-align: right;
|
|
146
|
+
}
|
|
157
147
|
|
|
158
|
-
|
|
159
|
-
|
|
148
|
+
@keyframes fade {
|
|
149
|
+
0%,
|
|
150
|
+
100% {
|
|
151
|
+
opacity: 0;
|
|
160
152
|
}
|
|
161
153
|
|
|
162
|
-
|
|
163
|
-
|
|
154
|
+
25%,
|
|
155
|
+
75% {
|
|
156
|
+
opacity: 1;
|
|
164
157
|
}
|
|
165
158
|
}
|
|
166
159
|
|
|
167
|
-
.
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
160
|
+
.ellipses {
|
|
161
|
+
span {
|
|
162
|
+
animation: fade 2000ms infinite;
|
|
163
|
+
|
|
164
|
+
@for $i from 1 through 3 {
|
|
165
|
+
&:nth-child(#{$i}) {
|
|
166
|
+
animation-delay: 200ms * $i;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
176
170
|
}
|
|
177
171
|
</style>
|
|
@@ -2,13 +2,14 @@
|
|
|
2
2
|
import type { Snippet } from 'svelte'
|
|
3
3
|
|
|
4
4
|
interface Props {
|
|
5
|
+
type?: 'error' | 'warning'
|
|
5
6
|
children: Snippet
|
|
6
7
|
}
|
|
7
8
|
|
|
8
|
-
const { children }: Props = $props()
|
|
9
|
+
const { type = 'error', children }: Props = $props()
|
|
9
10
|
</script>
|
|
10
11
|
|
|
11
|
-
<div class="alert">
|
|
12
|
+
<div class="alert {type}">
|
|
12
13
|
{@render children()}
|
|
13
14
|
</div>
|
|
14
15
|
|
|
@@ -19,5 +20,14 @@
|
|
|
19
20
|
border: 1px solid var(--playpilot-error);
|
|
20
21
|
background: var(--playpilot-error-dark);
|
|
21
22
|
font-size: margin(0.75);
|
|
23
|
+
|
|
24
|
+
&.warning {
|
|
25
|
+
border-color: var(--playpilot-warning);
|
|
26
|
+
background: var(--playpilot-warning-dark);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
:global(a) {
|
|
30
|
+
color: currentColor;
|
|
31
|
+
}
|
|
22
32
|
}
|
|
23
33
|
</style>
|
|
@@ -9,20 +9,32 @@
|
|
|
9
9
|
import { saveLinkInjections } from '$lib/api'
|
|
10
10
|
import { untrack } from 'svelte'
|
|
11
11
|
import AIIndicator from './AIIndicator.svelte'
|
|
12
|
-
import { isEquivalentInjection, isValidInjection } from '$lib/linkInjection'
|
|
13
12
|
import type { Position } from '$lib/types/position'
|
|
14
13
|
import type { LinkInjection } from '$lib/types/injection'
|
|
15
14
|
import { track } from '$lib/tracking'
|
|
16
15
|
import { TrackingEvent } from '$lib/enums/TrackingEvent'
|
|
16
|
+
import { heading } from '$lib/actions/heading'
|
|
17
17
|
|
|
18
18
|
interface Props {
|
|
19
19
|
linkInjections: LinkInjection[],
|
|
20
20
|
htmlString?: string,
|
|
21
21
|
loading?: boolean,
|
|
22
|
-
|
|
22
|
+
injectionsEnabled?: boolean,
|
|
23
|
+
aiStatus: {
|
|
24
|
+
automationEnabled?: boolean,
|
|
25
|
+
aiRunning?: boolean,
|
|
26
|
+
message?: string,
|
|
27
|
+
percentage?: number
|
|
28
|
+
}
|
|
23
29
|
}
|
|
24
30
|
|
|
25
|
-
let {
|
|
31
|
+
let {
|
|
32
|
+
linkInjections = $bindable(),
|
|
33
|
+
htmlString = '',
|
|
34
|
+
loading = false,
|
|
35
|
+
injectionsEnabled = false,
|
|
36
|
+
aiStatus = {},
|
|
37
|
+
}: Props = $props()
|
|
26
38
|
|
|
27
39
|
const editorPositionKey = 'editor-position'
|
|
28
40
|
const editorHeightKey = 'editor-height'
|
|
@@ -35,19 +47,19 @@
|
|
|
35
47
|
let hasError = $state(false)
|
|
36
48
|
let scrollDistance = $state(0)
|
|
37
49
|
let initialStateString = $state('')
|
|
38
|
-
let aIIndicator = $state()
|
|
39
50
|
|
|
40
51
|
const linkInjectionsString = $derived(JSON.stringify(linkInjections))
|
|
41
|
-
const hasChanged = $derived(initialStateString !== linkInjectionsString)
|
|
52
|
+
const hasChanged = $derived(initialStateString && initialStateString !== linkInjectionsString)
|
|
42
53
|
// Filter out injections without title_details, injections that are removed, duplicate, or are AI injections that failed to inject
|
|
43
54
|
const filteredInjections = $derived(linkInjections.filter((i) => i.title_details && !i.removed && !i.duplicate && (i.manual || (!i.manual && !i.failed))))
|
|
44
55
|
const sortedInjections = $derived(sortInjections(filteredInjections))
|
|
56
|
+
const { automationEnabled = false, aiRunning = false } = $derived(aiStatus)
|
|
45
57
|
|
|
46
58
|
$effect(() => {
|
|
47
59
|
if (loading) return
|
|
48
60
|
|
|
49
61
|
untrack(() => {
|
|
50
|
-
initialStateString = linkInjectionsString
|
|
62
|
+
requestAnimationFrame(() => initialStateString = linkInjectionsString)
|
|
51
63
|
trackInjectionsCount()
|
|
52
64
|
})
|
|
53
65
|
})
|
|
@@ -120,41 +132,27 @@
|
|
|
120
132
|
|
|
121
133
|
track(TrackingEvent.TotalInjectionsCount, null, payload)
|
|
122
134
|
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* This is called from outside when new AI links are ready.
|
|
126
|
-
* We only pass on links that do not already exist.
|
|
127
|
-
*/
|
|
128
|
-
export function requestNewAIInjections(injections: LinkInjection[]): void {
|
|
129
|
-
const newInjections = injections.filter(injection => {
|
|
130
|
-
if (!isValidInjection(injection)) return
|
|
131
|
-
return !linkInjections.some((i) => isEquivalentInjection(injection, i))
|
|
132
|
-
})
|
|
133
|
-
|
|
134
|
-
// @ts-ignore
|
|
135
|
-
aIIndicator.notifyUserOfNewState(newInjections)
|
|
136
|
-
}
|
|
137
135
|
</script>
|
|
138
136
|
|
|
139
137
|
<section class="editor playpilot-styled-scrollbar" class:panel-open={manualInjectionActive} class:loading bind:this={editorElement} {onscroll}>
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
<
|
|
144
|
-
|
|
145
|
-
</div>
|
|
146
|
-
{/if}
|
|
138
|
+
{#if editorElement && !loading}
|
|
139
|
+
<div class="handles">
|
|
140
|
+
<div class="handle">
|
|
141
|
+
<ResizeHandle element={editorElement} {height} onchange={(height) => saveLocalStorage(editorHeightKey, JSON.stringify(height))} />
|
|
142
|
+
</div>
|
|
147
143
|
|
|
148
144
|
<div class="handle">
|
|
149
145
|
<DragHandle element={editorElement} {position} limit={{ x: 16, y: 16 }} onchange={(position) => saveLocalStorage(editorPositionKey, JSON.stringify(position))} />
|
|
150
146
|
</div>
|
|
151
|
-
|
|
147
|
+
</div>
|
|
148
|
+
{/if}
|
|
152
149
|
|
|
153
|
-
|
|
150
|
+
<header class="header">
|
|
151
|
+
<div class="heading" use:heading>Playlinks</div>
|
|
154
152
|
|
|
155
153
|
{#if loading}
|
|
156
154
|
<div class="loading">Loading...</div>
|
|
157
|
-
{:else}
|
|
155
|
+
{:else if !aiRunning}
|
|
158
156
|
<div class="bubble" aria-label="{filteredInjections.length} found playlinks">
|
|
159
157
|
{filteredInjections.length}
|
|
160
158
|
</div>
|
|
@@ -165,32 +163,43 @@
|
|
|
165
163
|
{/if}
|
|
166
164
|
</header>
|
|
167
165
|
|
|
168
|
-
{#if !loading && aiRunning}
|
|
169
|
-
<AIIndicator {htmlString} bind:this={aIIndicator} onadd={(newInjections) => newInjections.forEach(i => linkInjections.push(i))} />
|
|
170
|
-
{/if}
|
|
171
|
-
|
|
172
166
|
{#if !loading}
|
|
167
|
+
{#if !injectionsEnabled}
|
|
168
|
+
<div class="alert">
|
|
169
|
+
<Alert type="warning">
|
|
170
|
+
<strong>Playlinks are currently not published.</strong> Visitors to this page will not see any of the injected links.
|
|
171
|
+
Publish playlinks from the <a href="https://partner.playpilot.net">Partner Portal</a>
|
|
172
|
+
</Alert>
|
|
173
|
+
</div>
|
|
174
|
+
{/if}
|
|
175
|
+
|
|
176
|
+
{#if aiRunning || !automationEnabled}
|
|
177
|
+
<AIIndicator {...aiStatus} />
|
|
178
|
+
{/if}
|
|
179
|
+
|
|
173
180
|
{#if hasError}
|
|
174
181
|
<div class="error" transition:slide|global={{ duration: 150 }}>
|
|
175
182
|
<Alert>Something went wrong, check your links below.</Alert>
|
|
176
183
|
</div>
|
|
177
184
|
{/if}
|
|
178
185
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
186
|
+
{#if !aiRunning}
|
|
187
|
+
<div class="items">
|
|
188
|
+
{#each sortedInjections as linkInjection (linkInjection.key)}
|
|
189
|
+
<!-- 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 -->
|
|
190
|
+
{@const index = linkInjections.findIndex((i) => i.key === linkInjection.key)}
|
|
183
191
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
192
|
+
<EditorItem bind:linkInjection={linkInjections[index]} onremove={() => removeInjection(linkInjection.key)} onhighlight={scrollToItem} />
|
|
193
|
+
{:else}
|
|
194
|
+
<div class="empty">No links available. Add links manually by clicking the + button and select text to add a link to.</div>
|
|
195
|
+
{/each}
|
|
196
|
+
</div>
|
|
189
197
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
198
|
+
{#if hasChanged && linkInjections.length}
|
|
199
|
+
<button class="save" disabled={saving} onclick={save} in:fade={{ duration: 100 }}>
|
|
200
|
+
{saving ? 'Saving...' : 'Save links'}
|
|
201
|
+
</button>
|
|
202
|
+
{/if}
|
|
194
203
|
{/if}
|
|
195
204
|
{/if}
|
|
196
205
|
|
|
@@ -208,14 +217,6 @@
|
|
|
208
217
|
</section>
|
|
209
218
|
|
|
210
219
|
<style lang="scss">
|
|
211
|
-
h1 {
|
|
212
|
-
margin: 0 margin(0.75) 0 0;
|
|
213
|
-
color: var(--playpilot-text-color);
|
|
214
|
-
font-size: margin(1.25);
|
|
215
|
-
font-weight: normal;
|
|
216
|
-
line-height: normal;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
220
|
.editor {
|
|
220
221
|
z-index: 2147483646; // 1 less than as high as she goes;
|
|
221
222
|
display: flex;
|
|
@@ -225,7 +226,7 @@
|
|
|
225
226
|
right: margin(1);
|
|
226
227
|
width: 100%;
|
|
227
228
|
max-width: margin(22);
|
|
228
|
-
height: min(
|
|
229
|
+
height: min(70vh, margin(40));
|
|
229
230
|
min-height: 10rem;
|
|
230
231
|
margin: 0;
|
|
231
232
|
padding: margin(1);
|
|
@@ -238,6 +239,8 @@
|
|
|
238
239
|
overflow-y: auto;
|
|
239
240
|
overflow-x: hidden;
|
|
240
241
|
line-height: normal;
|
|
242
|
+
|
|
243
|
+
@include reset-svg();
|
|
241
244
|
}
|
|
242
245
|
|
|
243
246
|
.panel-open {
|
|
@@ -254,6 +257,13 @@
|
|
|
254
257
|
font-size: margin(0.85);
|
|
255
258
|
}
|
|
256
259
|
|
|
260
|
+
.handles {
|
|
261
|
+
z-index: 20;
|
|
262
|
+
position: sticky;
|
|
263
|
+
top: margin(-1);
|
|
264
|
+
margin: margin(-1) margin(-1) 0;
|
|
265
|
+
}
|
|
266
|
+
|
|
257
267
|
.handle {
|
|
258
268
|
opacity: 0;
|
|
259
269
|
transition: opacity 150ms;
|
|
@@ -264,21 +274,28 @@
|
|
|
264
274
|
}
|
|
265
275
|
|
|
266
276
|
.header {
|
|
277
|
+
@extend .handles;
|
|
267
278
|
z-index: 5;
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
margin: margin(-1) margin(-1) 0;
|
|
279
|
+
display: flex;
|
|
280
|
+
align-items: center;
|
|
271
281
|
padding: margin(1) margin(1) margin(1) margin(1.5);
|
|
282
|
+
margin: 0 margin(-1) 0;
|
|
272
283
|
border: 0;
|
|
273
284
|
background: var(--playpilot-dark);
|
|
274
|
-
display: flex;
|
|
275
|
-
align-items: center;
|
|
276
285
|
|
|
277
286
|
.loading & {
|
|
278
287
|
margin: margin(-1);
|
|
279
288
|
}
|
|
280
289
|
}
|
|
281
290
|
|
|
291
|
+
.heading {
|
|
292
|
+
margin: 0 margin(0.75) 0 0;
|
|
293
|
+
color: var(--playpilot-text-color);
|
|
294
|
+
font-size: margin(1.25);
|
|
295
|
+
font-weight: normal;
|
|
296
|
+
line-height: normal;
|
|
297
|
+
}
|
|
298
|
+
|
|
282
299
|
.bubble {
|
|
283
300
|
appearance: none;
|
|
284
301
|
width: margin(1.5);
|
|
@@ -340,6 +357,10 @@
|
|
|
340
357
|
margin-top: margin(0.5);
|
|
341
358
|
}
|
|
342
359
|
|
|
360
|
+
.alert {
|
|
361
|
+
margin: 0 margin(0.5) margin(0.5);
|
|
362
|
+
}
|
|
363
|
+
|
|
343
364
|
.panel {
|
|
344
365
|
z-index: 10;
|
|
345
366
|
position: absolute;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { slide } from 'svelte/transition'
|
|
2
|
+
import { fade, slide } from 'svelte/transition'
|
|
3
3
|
import IconChevron from '../Icons/IconChevron.svelte'
|
|
4
|
+
import IconWarning from '../Icons/IconWarning.svelte'
|
|
4
5
|
import Switch from './Switch.svelte'
|
|
5
6
|
import TextInput from './TextInput.svelte'
|
|
6
7
|
import PlaylinkTypeSelect from './PlaylinkTypeSelect.svelte'
|
|
@@ -11,7 +12,9 @@
|
|
|
11
12
|
import { TrackingEvent } from '$lib/enums/TrackingEvent'
|
|
12
13
|
import type { LinkInjection } from '$lib/types/injection'
|
|
13
14
|
import type { TitleData } from '$lib/types/title'
|
|
14
|
-
import { truncateAroundPhrase } from '$lib/text'
|
|
15
|
+
import { cleanPhrase, truncateAroundPhrase } from '$lib/text'
|
|
16
|
+
import { getLinkInjectionElements, getLinkInjectionsParentElement, isValidPlaylinkType } from '$lib/linkInjection'
|
|
17
|
+
import { imagePlaceholderDataUrl } from '$lib/constants'
|
|
15
18
|
|
|
16
19
|
interface Props {
|
|
17
20
|
linkInjection: LinkInjection,
|
|
@@ -22,7 +25,7 @@
|
|
|
22
25
|
|
|
23
26
|
const { linkInjection = $bindable(), onremove = () => null, onhighlight = () => null }: Props = $props()
|
|
24
27
|
|
|
25
|
-
const { key, sentence, title_details, failed, inactive } = $derived(linkInjection || {})
|
|
28
|
+
const { key, sentence, title_details, failed, failed_message, inactive } = $derived(linkInjection || {})
|
|
26
29
|
|
|
27
30
|
// @ts-ignore Definitely not null
|
|
28
31
|
const title: TitleData = $derived(title_details)
|
|
@@ -42,8 +45,9 @@
|
|
|
42
45
|
*/
|
|
43
46
|
function toggleOnPageResultHighlight(state: boolean = true): void {
|
|
44
47
|
const matchingElements = getMatchingElements()
|
|
48
|
+
|
|
45
49
|
matchingElements.forEach(element => {
|
|
46
|
-
element.classList.toggle('injection-highlight', state)
|
|
50
|
+
element.classList.toggle('playpilot-injection-highlight', state)
|
|
47
51
|
})
|
|
48
52
|
}
|
|
49
53
|
|
|
@@ -76,7 +80,14 @@
|
|
|
76
80
|
}
|
|
77
81
|
|
|
78
82
|
function getMatchingElements(): Element[] {
|
|
79
|
-
|
|
83
|
+
const injectedElements = Array.from(document.querySelectorAll(`[data-playpilot-injection-key="${key}"]`))
|
|
84
|
+
if (injectedElements.length) return injectedElements
|
|
85
|
+
|
|
86
|
+
// No matching injection was found, so we try and get the element the sentence might be in.
|
|
87
|
+
// Could not be entirely accurate. Could also result in nothing depending on why the injection failed.
|
|
88
|
+
return getLinkInjectionElements((getLinkInjectionsParentElement())).filter((element) => {
|
|
89
|
+
return cleanPhrase(element.innerText).includes(cleanPhrase(linkInjection.sentence))
|
|
90
|
+
}) || []
|
|
80
91
|
}
|
|
81
92
|
</script>
|
|
82
93
|
|
|
@@ -94,7 +105,7 @@
|
|
|
94
105
|
bind:this={element}
|
|
95
106
|
out:slide|global={{ duration: 200 }}>
|
|
96
107
|
<div class="header">
|
|
97
|
-
<img class="poster" src={title.standing_poster} alt="" width="32" height="48" />
|
|
108
|
+
<img class="poster" src={title.standing_poster} alt="" width="32" height="48" onerror={({ target }) => (target as HTMLImageElement).src = imagePlaceholderDataUrl} />
|
|
98
109
|
|
|
99
110
|
<div class="info">
|
|
100
111
|
<div class="title">{title.title}</div>
|
|
@@ -120,13 +131,19 @@
|
|
|
120
131
|
|
|
121
132
|
<div class="content">
|
|
122
133
|
{#if failed}
|
|
123
|
-
<Alert>
|
|
134
|
+
<Alert>{failed_message}</Alert>
|
|
124
135
|
{:else}
|
|
125
136
|
<div class="actions">
|
|
126
137
|
<button class="expand" onclick={() => expanded = !expanded} aria-label="Expand" aria-expanded={expanded}>
|
|
127
138
|
<IconChevron {expanded} />
|
|
128
139
|
</button>
|
|
129
140
|
|
|
141
|
+
{#if !isValidPlaylinkType(linkInjection)}
|
|
142
|
+
<div class="warning" transition:fade={{ duration: 100 }} aria-label="Invalid playlink settings">
|
|
143
|
+
<IconWarning />
|
|
144
|
+
</div>
|
|
145
|
+
{/if}
|
|
146
|
+
|
|
130
147
|
<Switch label={inactive ? 'Inactive' : 'Visible'} active={!linkInjection.inactive} onclick={(active) => { linkInjection.inactive = !active; linkInjection.manual = true }}>
|
|
131
148
|
{inactive ? 'Inactive' : 'Visible'}
|
|
132
149
|
</Switch>
|
|
@@ -293,5 +310,13 @@
|
|
|
293
310
|
.offset {
|
|
294
311
|
margin-top: margin(0.75);
|
|
295
312
|
}
|
|
313
|
+
|
|
314
|
+
.warning {
|
|
315
|
+
margin: 0 auto 0 margin(0.5);
|
|
316
|
+
|
|
317
|
+
:global(svg) {
|
|
318
|
+
display: block;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
296
321
|
</style>
|
|
297
322
|
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { getLinkInjectionsParentElement } from '$lib/linkInjection'
|
|
12
12
|
import type { LinkInjection } from '$lib/types/injection'
|
|
13
13
|
import type { TitleData } from '$lib/types/title'
|
|
14
|
+
import { heading } from '$lib/actions/heading'
|
|
14
15
|
|
|
15
16
|
interface Props {
|
|
16
17
|
htmlString: string
|
|
@@ -159,7 +160,7 @@
|
|
|
159
160
|
<section class="layout">
|
|
160
161
|
<div class="header">
|
|
161
162
|
<RoundButton onclick={onclose} size="24px"><IconBack /></RoundButton>
|
|
162
|
-
<
|
|
163
|
+
<div class="heading" use:heading={2}>Add Playlink manually</div>
|
|
163
164
|
</div>
|
|
164
165
|
|
|
165
166
|
<p>Highlight the text section in your post that you want to turn into a Playlink.</p>
|
|
@@ -186,14 +187,6 @@
|
|
|
186
187
|
</section>
|
|
187
188
|
|
|
188
189
|
<style lang="scss">
|
|
189
|
-
h2 {
|
|
190
|
-
margin: 0;
|
|
191
|
-
color: var(--playpilot-text-color);
|
|
192
|
-
font-size: margin(1);
|
|
193
|
-
line-height: normal;
|
|
194
|
-
font-weight: normal;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
190
|
p,
|
|
198
191
|
label {
|
|
199
192
|
font-size: margin(0.75);
|
|
@@ -220,6 +213,14 @@
|
|
|
220
213
|
margin-bottom: margin(1);
|
|
221
214
|
}
|
|
222
215
|
|
|
216
|
+
.heading {
|
|
217
|
+
margin: 0;
|
|
218
|
+
color: var(--playpilot-text-color);
|
|
219
|
+
font-size: margin(1);
|
|
220
|
+
line-height: normal;
|
|
221
|
+
font-weight: normal;
|
|
222
|
+
}
|
|
223
|
+
|
|
223
224
|
.error {
|
|
224
225
|
margin-top: margin(0.5);
|
|
225
226
|
}
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
import IconAlign from '../Icons/IconAlign.svelte'
|
|
4
4
|
import Switch from './Switch.svelte'
|
|
5
5
|
import type { LinkInjection } from '$lib/types/injection'
|
|
6
|
+
import { isValidPlaylinkType } from '$lib/linkInjection'
|
|
7
|
+
import Alert from './Alert.svelte'
|
|
6
8
|
|
|
7
9
|
interface Props {
|
|
8
10
|
linkInjection: LinkInjection
|
|
@@ -33,6 +35,14 @@
|
|
|
33
35
|
</Switch>
|
|
34
36
|
</div>
|
|
35
37
|
|
|
38
|
+
{#if !isValidPlaylinkType(linkInjection)}
|
|
39
|
+
<div class="alert" transition:slide={{ duration: 200 }}>
|
|
40
|
+
<Alert type="warning">
|
|
41
|
+
At least one layout option must be selected for the playlink to be visible.
|
|
42
|
+
</Alert>
|
|
43
|
+
</div>
|
|
44
|
+
{/if}
|
|
45
|
+
|
|
36
46
|
{#if linkInjection.after_article}
|
|
37
47
|
<div transition:slide={{ duration: 100 }}>
|
|
38
48
|
<div class="label">Bottom playlinks style</div>
|
|
@@ -133,4 +143,8 @@
|
|
|
133
143
|
font-size: margin(0.675);
|
|
134
144
|
color: var(--playpilot-text-color-alt);
|
|
135
145
|
}
|
|
146
|
+
|
|
147
|
+
.alert {
|
|
148
|
+
margin-top: margin(0.5);
|
|
149
|
+
}
|
|
136
150
|
</style>
|