@playpilot/tpi 1.0.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/.github/workflows/tests.yml +22 -0
- package/.prettierignore +4 -0
- package/.prettierrc +16 -0
- package/README.md +38 -0
- package/dist/link-injections.js +7 -0
- package/eslint.config.js +33 -0
- package/index.html +11 -0
- package/jsconfig.json +19 -0
- package/package.json +35 -0
- package/src/app.d.ts +13 -0
- package/src/app.html +12 -0
- package/src/demo.spec.js +7 -0
- package/src/lib/api.js +160 -0
- package/src/lib/array.js +15 -0
- package/src/lib/auth.js +84 -0
- package/src/lib/constants.js +2 -0
- package/src/lib/enums/TrackingEvent.js +15 -0
- package/src/lib/fakeData.js +140 -0
- package/src/lib/genres.json +420 -0
- package/src/lib/global.css +37 -0
- package/src/lib/hash.js +15 -0
- package/src/lib/html.js +21 -0
- package/src/lib/index.js +1 -0
- package/src/lib/linkInjection.js +275 -0
- package/src/lib/search.js +24 -0
- package/src/lib/text.js +61 -0
- package/src/lib/tracking.js +32 -0
- package/src/lib/variables.css +16 -0
- package/src/main.js +45 -0
- package/src/routes/+layout.svelte +54 -0
- package/src/routes/+page.svelte +96 -0
- package/src/routes/components/AfterArticlePlaylinks.svelte +90 -0
- package/src/routes/components/ContextMenu.svelte +67 -0
- package/src/routes/components/Description.svelte +47 -0
- package/src/routes/components/Editorial/Alert.svelte +18 -0
- package/src/routes/components/Editorial/DragHandle.svelte +134 -0
- package/src/routes/components/Editorial/Editor.svelte +277 -0
- package/src/routes/components/Editorial/EditorItem.svelte +260 -0
- package/src/routes/components/Editorial/ManualInjection.svelte +192 -0
- package/src/routes/components/Editorial/PlaylinkTypeSelect.svelte +132 -0
- package/src/routes/components/Editorial/Search/TitleSearch.svelte +176 -0
- package/src/routes/components/Editorial/Switch.svelte +76 -0
- package/src/routes/components/Editorial/TextInput.svelte +29 -0
- package/src/routes/components/Genres.svelte +41 -0
- package/src/routes/components/Icons/IconAlign.svelte +12 -0
- package/src/routes/components/Icons/IconBack.svelte +3 -0
- package/src/routes/components/Icons/IconBookmark.svelte +3 -0
- package/src/routes/components/Icons/IconChevron.svelte +18 -0
- package/src/routes/components/Icons/IconClose.svelte +3 -0
- package/src/routes/components/Icons/IconContinue.svelte +3 -0
- package/src/routes/components/Icons/IconDots.svelte +5 -0
- package/src/routes/components/Icons/IconEnlarge.svelte +12 -0
- package/src/routes/components/Icons/IconIMDb.svelte +3 -0
- package/src/routes/components/Icons/IconNewTab.svelte +3 -0
- package/src/routes/components/Modal.svelte +106 -0
- package/src/routes/components/Participants.svelte +44 -0
- package/src/routes/components/Playlinks.svelte +155 -0
- package/src/routes/components/Popover.svelte +95 -0
- package/src/routes/components/RoundButton.svelte +38 -0
- package/src/routes/components/SkeletonText.svelte +33 -0
- package/src/routes/components/Title.svelte +180 -0
- package/src/routes/components/TitleModal.svelte +24 -0
- package/src/routes/components/TitlePopover.svelte +17 -0
- package/src/tests/helpers.js +18 -0
- package/src/tests/lib/api.test.js +162 -0
- package/src/tests/lib/array.test.js +14 -0
- package/src/tests/lib/auth.test.js +115 -0
- package/src/tests/lib/hash.test.js +28 -0
- package/src/tests/lib/html.test.js +16 -0
- package/src/tests/lib/linkInjection.test.js +754 -0
- package/src/tests/lib/search.test.js +42 -0
- package/src/tests/lib/text.test.js +94 -0
- package/src/tests/lib/tracking.test.js +71 -0
- package/src/tests/routes/+page.test.js +109 -0
- package/src/tests/routes/components/AfterArticlePlaylinks.test.js +115 -0
- package/src/tests/routes/components/ContextMenu.test.js +37 -0
- package/src/tests/routes/components/Description.test.js +58 -0
- package/src/tests/routes/components/Editorial/Alert.test.js +17 -0
- package/src/tests/routes/components/Editorial/DragHandle.test.js +55 -0
- package/src/tests/routes/components/Editorial/Editor.test.js +64 -0
- package/src/tests/routes/components/Editorial/EditorItem.test.js +142 -0
- package/src/tests/routes/components/Editorial/ManualInjection.test.js +114 -0
- package/src/tests/routes/components/Editorial/PlaylinkTypeSelect.test.js +63 -0
- package/src/tests/routes/components/Editorial/Search/TitleSearch.test.js +58 -0
- package/src/tests/routes/components/Editorial/Switch.test.js +60 -0
- package/src/tests/routes/components/Editorial/TextInput.test.js +30 -0
- package/src/tests/routes/components/Genres.test.js +37 -0
- package/src/tests/routes/components/Modal.test.js +84 -0
- package/src/tests/routes/components/Participants.test.js +33 -0
- package/src/tests/routes/components/Playlinks.test.js +101 -0
- package/src/tests/routes/components/Popover.test.js +66 -0
- package/src/tests/routes/components/RoundButton.test.js +35 -0
- package/src/tests/routes/components/SkeletonText.test.js +12 -0
- package/src/tests/routes/components/Title.test.js +82 -0
- package/src/tests/routes/components/TitleModal.test.js +33 -0
- package/src/tests/routes/components/TitlePopover.test.js +23 -0
- package/src/tests/setup.js +53 -0
- package/src/typedefs.js +72 -0
- package/static/favicon.png +0 -0
- package/svelte.config.js +13 -0
- package/vite.config.js +61 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { slide } from 'svelte/transition'
|
|
3
|
+
import IconChevron from '../Icons/IconChevron.svelte'
|
|
4
|
+
import IconIMDb from '../Icons/IconIMDb.svelte'
|
|
5
|
+
import Switch from './Switch.svelte'
|
|
6
|
+
import TextInput from './TextInput.svelte'
|
|
7
|
+
import PlaylinkTypeSelect from './PlaylinkTypeSelect.svelte'
|
|
8
|
+
import Alert from './Alert.svelte'
|
|
9
|
+
import ContextMenu from '../ContextMenu.svelte'
|
|
10
|
+
|
|
11
|
+
/** @type {{ linkInjection: LinkInjection, onremove?: () => void, onhighlight?: (element: HTMLElement) => void }} */
|
|
12
|
+
const { linkInjection = $bindable(), onremove = () => null, onhighlight = () => null } = $props()
|
|
13
|
+
|
|
14
|
+
const { key, title_details, failed } = $derived(linkInjection || {})
|
|
15
|
+
|
|
16
|
+
/** @type {TitleData} */
|
|
17
|
+
// @ts-ignore Definitely not null
|
|
18
|
+
const title = /** @type {TitleData} */ $derived(title_details)
|
|
19
|
+
|
|
20
|
+
let expanded = $state(false)
|
|
21
|
+
let highlighted = $state(false)
|
|
22
|
+
/** @type {HTMLElement | null} */
|
|
23
|
+
let element = $state(null)
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Highlight links beloning to this item in the article itself.
|
|
27
|
+
* @param {boolean} state
|
|
28
|
+
*/
|
|
29
|
+
function toggleOnPageResultHighlight(state = true) {
|
|
30
|
+
const matchingElements = getMatchingElements()
|
|
31
|
+
matchingElements.forEach(element => {
|
|
32
|
+
element.classList.toggle('injection-highlight', state)
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Highlight this editor when hovering links in the article itself.
|
|
38
|
+
* @param {MouseEvent} event
|
|
39
|
+
*/
|
|
40
|
+
function setInEditorHighlight(event) {
|
|
41
|
+
let target = /** @type {HTMLElement | null} */ (event.target)
|
|
42
|
+
let injectionKey = target?.dataset.playpilotInjectionKey
|
|
43
|
+
|
|
44
|
+
if (target && !injectionKey) {
|
|
45
|
+
target = /** @type {HTMLElement | null} */ (target.closest(`[data-playpilot-injection-key="${key}"]`))
|
|
46
|
+
injectionKey = target?.dataset.playpilotInjectionKey
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
highlighted = injectionKey === key
|
|
50
|
+
|
|
51
|
+
if (element && highlighted) onhighlight(element)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** @param {MouseEvent} event */
|
|
55
|
+
function scrollLinkIntoView(event) {
|
|
56
|
+
requestAnimationFrame(() => toggleOnPageResultHighlight()) // Reset highlight in case the playlink type changed
|
|
57
|
+
|
|
58
|
+
const target = /** @type {HTMLElement} */ (event.target)
|
|
59
|
+
if (['BUTTON', 'INPUT'].includes(target.nodeName)) return
|
|
60
|
+
if (target.closest('button') || target.closest('input')) return
|
|
61
|
+
|
|
62
|
+
const matchingElement = getMatchingElements()[0]
|
|
63
|
+
if (matchingElement) matchingElement.scrollIntoView({ behavior: 'smooth' })
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** @returns {Element[]} */
|
|
67
|
+
function getMatchingElements() {
|
|
68
|
+
return Array.from(document.querySelectorAll(`[data-playpilot-injection-key="${key}"]`))
|
|
69
|
+
}
|
|
70
|
+
</script>
|
|
71
|
+
|
|
72
|
+
<svelte:window on:mouseover={setInEditorHighlight} />
|
|
73
|
+
|
|
74
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
75
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
76
|
+
<div
|
|
77
|
+
class="item"
|
|
78
|
+
class:highlighted
|
|
79
|
+
onmouseenter={() => toggleOnPageResultHighlight(true)}
|
|
80
|
+
onmouseleave={() => toggleOnPageResultHighlight(false)}
|
|
81
|
+
onclick={scrollLinkIntoView}
|
|
82
|
+
bind:this={element}
|
|
83
|
+
out:slide|global={{ duration: 200 }}>
|
|
84
|
+
<div class="header">
|
|
85
|
+
<img class="poster" src={title.standing_poster} alt="" />
|
|
86
|
+
|
|
87
|
+
<div class="info">
|
|
88
|
+
<div class="title">{title.title}</div>
|
|
89
|
+
|
|
90
|
+
<div class="meta">
|
|
91
|
+
<span><IconIMDb /> 7.1</span>
|
|
92
|
+
<span>{title.year}</span>
|
|
93
|
+
<span>{title.type}</span>
|
|
94
|
+
|
|
95
|
+
{#if title.length}
|
|
96
|
+
<span data-testid="length">{title.length}m</span>
|
|
97
|
+
{/if}
|
|
98
|
+
</div>
|
|
99
|
+
</div>
|
|
100
|
+
|
|
101
|
+
<div class="context-menu">
|
|
102
|
+
<ContextMenu ariaLabel="More options">
|
|
103
|
+
<button class="context-menu-action" onclick={onremove}>Remove</button>
|
|
104
|
+
</ContextMenu>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
|
|
108
|
+
<div class="content">
|
|
109
|
+
{#if failed}
|
|
110
|
+
<Alert>A match was found, but the link could not be injected.</Alert>
|
|
111
|
+
{:else}
|
|
112
|
+
<div class="actions">
|
|
113
|
+
<button class="expand" onclick={() => expanded = !expanded} aria-label="Expand" aria-expanded={expanded}>
|
|
114
|
+
<IconChevron {expanded} />
|
|
115
|
+
</button>
|
|
116
|
+
|
|
117
|
+
<Switch label="Visible" active={!linkInjection.inactive} onclick={(active) => linkInjection.inactive = !active}>
|
|
118
|
+
Visible
|
|
119
|
+
</Switch>
|
|
120
|
+
</div>
|
|
121
|
+
{/if}
|
|
122
|
+
|
|
123
|
+
{#if expanded}
|
|
124
|
+
<div class="expanded" transition:slide={{ duration: 100 }}>
|
|
125
|
+
<div class="label">Link URL</div>
|
|
126
|
+
<TextInput bind:value={linkInjection.playpilot_url} label="Playlink URL" />
|
|
127
|
+
|
|
128
|
+
<div class="label offset">Layout options</div>
|
|
129
|
+
<div class="type-select">
|
|
130
|
+
<PlaylinkTypeSelect {linkInjection} />
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
{/if}
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
<style>
|
|
138
|
+
.item {
|
|
139
|
+
padding: 1rem 0.5rem 1rem 0.5rem;
|
|
140
|
+
border-bottom: 1px solid var(--playpilot-lighter);
|
|
141
|
+
transition: outline-offset 100ms;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.item:hover,
|
|
145
|
+
.item.highlighted {
|
|
146
|
+
border-radius: 0.5rem;
|
|
147
|
+
border-color: transparent;
|
|
148
|
+
outline: 2px solid var(--playpilot-content);
|
|
149
|
+
outline-offset: 1px;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.item.highlighted {
|
|
153
|
+
outline: 2px solid var(--playpilot-primary);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.header {
|
|
157
|
+
display: flex;
|
|
158
|
+
gap: 1rem;
|
|
159
|
+
width: 100%;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.poster {
|
|
163
|
+
display: block;
|
|
164
|
+
width: 2rem;
|
|
165
|
+
height: 3rem;
|
|
166
|
+
border-radius: 0.25rem;
|
|
167
|
+
background: var(--playpilot-content);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.title {
|
|
171
|
+
font-size: 0.875rem;
|
|
172
|
+
word-break: break-word;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.meta {
|
|
176
|
+
display: flex;
|
|
177
|
+
gap: 0.5rem;
|
|
178
|
+
font-size: 0.75rem;
|
|
179
|
+
color: var(--playpilot-text-color-alt);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.meta span {
|
|
183
|
+
display: flex;
|
|
184
|
+
align-items: center;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.meta :global(svg) {
|
|
188
|
+
display: block;
|
|
189
|
+
margin: -0.125rem 0.125rem 0 0;
|
|
190
|
+
height: 1em;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.content {
|
|
194
|
+
padding-top: 1rem;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.context-menu {
|
|
198
|
+
margin: 0 -0.25rem 0 auto;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.context-menu-action {
|
|
202
|
+
appearance: none;
|
|
203
|
+
background: transparent;
|
|
204
|
+
border: 0;
|
|
205
|
+
padding: 1rem;
|
|
206
|
+
font-family: inherit;
|
|
207
|
+
color: var(--playpilot-text-color-alt);
|
|
208
|
+
cursor: pointer;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.context-menu-action:hover {
|
|
212
|
+
color: var(--playpilot-text-color);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.actions {
|
|
216
|
+
display: flex;
|
|
217
|
+
align-items: center;
|
|
218
|
+
justify-content: space-between;
|
|
219
|
+
width: 100%;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.expand {
|
|
223
|
+
appearance: none;
|
|
224
|
+
display: flex;
|
|
225
|
+
align-items: center;
|
|
226
|
+
justify-content: center;
|
|
227
|
+
width: 1.5rem;
|
|
228
|
+
height: 1.5rem;
|
|
229
|
+
border: 0;
|
|
230
|
+
border-radius: 50%;
|
|
231
|
+
background: var(--playpilot-content);
|
|
232
|
+
color: var(--playpilot-text-color);
|
|
233
|
+
box-shadow: var(--playpilot-shadow);
|
|
234
|
+
cursor: pointer;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.expand:hover {
|
|
238
|
+
filter: brightness(1.2);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.expanded {
|
|
242
|
+
padding-top: 1rem;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.type-select {
|
|
246
|
+
margin-top: 0.5rem;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.label {
|
|
250
|
+
margin-bottom: 0.25rem;
|
|
251
|
+
opacity: 0.75;
|
|
252
|
+
font-size: 0.675rem;
|
|
253
|
+
color: var(--playpilot-text-color-alt);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.offset {
|
|
257
|
+
margin-top: 0.75rem;
|
|
258
|
+
}
|
|
259
|
+
</style>
|
|
260
|
+
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { onMount } from 'svelte'
|
|
3
|
+
import IconBack from '../Icons/IconBack.svelte'
|
|
4
|
+
import RoundButton from '../RoundButton.svelte'
|
|
5
|
+
import Alert from './Alert.svelte'
|
|
6
|
+
import TextInput from './TextInput.svelte'
|
|
7
|
+
import TitleSearch from './Search/TitleSearch.svelte'
|
|
8
|
+
import { playPilotBaseUrl } from '$lib/constants'
|
|
9
|
+
import { generateInjectionKey } from '$lib/api'
|
|
10
|
+
import { decodeHtmlEntities } from '$lib/html'
|
|
11
|
+
|
|
12
|
+
/** @type {{ htmlString: string, onsave: (linkInjection: LinkInjection) => void, onclose?: () => void }} */
|
|
13
|
+
let { htmlString = '', onsave, onclose = () => null } = $props()
|
|
14
|
+
|
|
15
|
+
let currentSelection = $state('')
|
|
16
|
+
let selectionSentence = $state('')
|
|
17
|
+
/** @type {TitleData | null} */
|
|
18
|
+
let selectedTitle = $state(null)
|
|
19
|
+
let error = $state('')
|
|
20
|
+
let query = $state('')
|
|
21
|
+
|
|
22
|
+
onMount(updateSelection)
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Find the user selected content on the page, if any.
|
|
26
|
+
* Results in a visual error if the selected content was not within the given HTML.
|
|
27
|
+
* @returns {void}
|
|
28
|
+
*/
|
|
29
|
+
function updateSelection() {
|
|
30
|
+
const selection = window.getSelection()
|
|
31
|
+
if (!selection) return
|
|
32
|
+
|
|
33
|
+
const selectionText = selection.toString().trim()
|
|
34
|
+
if (!selectionText) return // No content was selected
|
|
35
|
+
|
|
36
|
+
error = ''
|
|
37
|
+
currentSelection = selectionText
|
|
38
|
+
query = currentSelection
|
|
39
|
+
selectionSentence = findSentenceForSelection(selection, selectionText)
|
|
40
|
+
|
|
41
|
+
const nodeContent = selection.getRangeAt(0).commonAncestorContainer.textContent
|
|
42
|
+
const documentTextContent = decodeHtmlEntities(htmlString)
|
|
43
|
+
|
|
44
|
+
if (!nodeContent || !documentTextContent.includes(nodeContent)) { // Selected content is not within the ALI selector
|
|
45
|
+
error = 'Selection was not inside of given content'
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Find the sentence that the given selected phrase is in. This is limited by the node that the text is in.
|
|
51
|
+
* @param {Selection} selection
|
|
52
|
+
* @param {string} selectionText
|
|
53
|
+
* @returns {string}
|
|
54
|
+
*/
|
|
55
|
+
function findSentenceForSelection(selection, selectionText) {
|
|
56
|
+
const range = selection.getRangeAt(0)
|
|
57
|
+
|
|
58
|
+
// Get the node the text is in. If the content of the node is very short we use the parent node instead.
|
|
59
|
+
// This is meant for content that is inside of other elements such as <p>Some <strong>word</strong> in a sentence</p>
|
|
60
|
+
// If we selected "word", we'd still want the full sentence, rather than just the "word".
|
|
61
|
+
let node = range.startContainer
|
|
62
|
+
if ((node.textContent || '').length <= selectionText.length * 2 && range.startContainer.parentNode) {
|
|
63
|
+
node = range.startContainer.parentNode
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (!node) return ''
|
|
67
|
+
|
|
68
|
+
const fullText = node.textContent || ''
|
|
69
|
+
const startOffset = range.startOffset
|
|
70
|
+
const endOffset = range.endOffset
|
|
71
|
+
|
|
72
|
+
const before = fullText.slice(0, startOffset).lastIndexOf('.') // Character at start of the sentence
|
|
73
|
+
const after = fullText.slice(endOffset).search(/[.!?]/) // Character at end of the sentence
|
|
74
|
+
|
|
75
|
+
const sentenceStart = before === -1 ? 0 : before + 1
|
|
76
|
+
const sentenceEnd = after === -1 ? fullText.length : endOffset + after + 1
|
|
77
|
+
|
|
78
|
+
return fullText.slice(sentenceStart, sentenceEnd).trim()
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @returns {void}
|
|
83
|
+
*/
|
|
84
|
+
function save() {
|
|
85
|
+
if (!currentSelection) return
|
|
86
|
+
if (!selectedTitle) return
|
|
87
|
+
|
|
88
|
+
const typePath = selectedTitle.type === 'movie' ? 'movie' : 'show'
|
|
89
|
+
const url = playPilotBaseUrl + `/${typePath}/${selectedTitle.slug}`
|
|
90
|
+
|
|
91
|
+
/** @type {LinkInjection} */
|
|
92
|
+
const linkInjection = {
|
|
93
|
+
sid: selectedTitle.sid,
|
|
94
|
+
title: currentSelection,
|
|
95
|
+
sentence: selectionSentence,
|
|
96
|
+
playpilot_url: url,
|
|
97
|
+
key: generateInjectionKey(selectedTitle.sid),
|
|
98
|
+
title_details: selectedTitle,
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
onsave(linkInjection)
|
|
102
|
+
onclose()
|
|
103
|
+
}
|
|
104
|
+
</script>
|
|
105
|
+
|
|
106
|
+
<svelte:window onmouseup={updateSelection} />
|
|
107
|
+
|
|
108
|
+
<section class="layout">
|
|
109
|
+
<div class="header">
|
|
110
|
+
<RoundButton onclick={onclose} size="1.5rem"><IconBack /></RoundButton>
|
|
111
|
+
<h2>Add Playlink manually</h2>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
<p>Highlight the text section in your post that you want to turn into a Playlink.</p>
|
|
115
|
+
|
|
116
|
+
<label for="text">Selected text</label>
|
|
117
|
+
<TextInput value={currentSelection} label="Select text on the page" name="selected-text" readonly />
|
|
118
|
+
|
|
119
|
+
{#if error}
|
|
120
|
+
<div class="error">
|
|
121
|
+
<Alert>{error}</Alert>
|
|
122
|
+
</div>
|
|
123
|
+
{/if}
|
|
124
|
+
|
|
125
|
+
<label for="text">Paired title</label>
|
|
126
|
+
<TitleSearch onselect={(title) => selectedTitle = title} bind:query />
|
|
127
|
+
|
|
128
|
+
<button class="save" onclick={save} disabled={!currentSelection || !selectedTitle}>
|
|
129
|
+
Add playlink
|
|
130
|
+
</button>
|
|
131
|
+
</section>
|
|
132
|
+
|
|
133
|
+
<style>
|
|
134
|
+
h2 {
|
|
135
|
+
margin: 0;
|
|
136
|
+
font-size: 1rem;
|
|
137
|
+
font-weight: normal
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
p,
|
|
141
|
+
label {
|
|
142
|
+
font-size: 0.75rem;
|
|
143
|
+
max-width: 15rem;
|
|
144
|
+
color: var(--playpilot-text-color-alt);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
label {
|
|
148
|
+
display: block;
|
|
149
|
+
margin-top: 1rem;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.layout {
|
|
153
|
+
height: 100%;
|
|
154
|
+
display: flex;
|
|
155
|
+
flex-direction: column;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.header {
|
|
159
|
+
display: flex;
|
|
160
|
+
align-items: center;
|
|
161
|
+
gap: 0.5rem;
|
|
162
|
+
margin-bottom: 1rem;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.error {
|
|
166
|
+
margin-top: 0.5rem;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
.save {
|
|
170
|
+
cursor: pointer;
|
|
171
|
+
appearance: none;
|
|
172
|
+
width: 100%;
|
|
173
|
+
margin-top: auto;
|
|
174
|
+
padding: 0.5rem;
|
|
175
|
+
border: 0;
|
|
176
|
+
border-radius: 2rem;
|
|
177
|
+
background: var(--playpilot-content);
|
|
178
|
+
transition: opacity 100ms;
|
|
179
|
+
font-family: inherit;
|
|
180
|
+
color: var(--playpilot-text-color-alt);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.save:not([disabled]):hover {
|
|
184
|
+
background: var(--playpilot-content-light);
|
|
185
|
+
color: var(--playpilot-text-color);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.save[disabled] {
|
|
189
|
+
cursor: default;
|
|
190
|
+
opacity: 0.5;
|
|
191
|
+
}
|
|
192
|
+
</style>
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
<script>
|
|
2
|
+
import { slide } from 'svelte/transition'
|
|
3
|
+
import IconAlign from '../Icons/IconAlign.svelte'
|
|
4
|
+
import Switch from './Switch.svelte'
|
|
5
|
+
|
|
6
|
+
/** @type {{ linkInjection: LinkInjection }} */
|
|
7
|
+
let { linkInjection } = $props()
|
|
8
|
+
|
|
9
|
+
let isAfterArticleButton = $derived(linkInjection.after_article_style === 'modal_button')
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<div class="switches">
|
|
13
|
+
<Switch
|
|
14
|
+
fullwidth
|
|
15
|
+
active={linkInjection.in_text ?? true}
|
|
16
|
+
onclick={(active) => linkInjection.in_text = active}
|
|
17
|
+
label="In-text Playlink">
|
|
18
|
+
<IconAlign align="center" />
|
|
19
|
+
In-text Playlink
|
|
20
|
+
</Switch>
|
|
21
|
+
|
|
22
|
+
<Switch
|
|
23
|
+
fullwidth
|
|
24
|
+
active={linkInjection.after_article ?? false}
|
|
25
|
+
onclick={(active) => linkInjection.after_article = active}
|
|
26
|
+
label="Bottom Playlink">
|
|
27
|
+
<IconAlign align="bottom" />
|
|
28
|
+
Bottom Playlink
|
|
29
|
+
</Switch>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
{#if linkInjection.after_article}
|
|
33
|
+
<div transition:slide={{ duration: 100 }}>
|
|
34
|
+
<div class="label">Bottom playlinks style</div>
|
|
35
|
+
|
|
36
|
+
<div
|
|
37
|
+
class="group"
|
|
38
|
+
role="listbox"
|
|
39
|
+
tabindex="0"
|
|
40
|
+
aria-label="Playlink type"
|
|
41
|
+
aria-activedescendant="playlinks-{isAfterArticleButton ? 'after-article' : 'in-text'}">
|
|
42
|
+
<button
|
|
43
|
+
class:active={!isAfterArticleButton}
|
|
44
|
+
role="option"
|
|
45
|
+
id="playlinks-in-text"
|
|
46
|
+
aria-selected={!isAfterArticleButton || null}
|
|
47
|
+
onclick={() => linkInjection.after_article_style = 'playlinks'}>
|
|
48
|
+
Playlinks sentence
|
|
49
|
+
</button>
|
|
50
|
+
|
|
51
|
+
<button
|
|
52
|
+
class:active={isAfterArticleButton}
|
|
53
|
+
role="option"
|
|
54
|
+
id="playlinks-after-article"
|
|
55
|
+
aria-selected={isAfterArticleButton || null}
|
|
56
|
+
onclick={() => linkInjection.after_article_style = 'modal_button'}>
|
|
57
|
+
Modal button
|
|
58
|
+
</button>
|
|
59
|
+
|
|
60
|
+
<div class="active-marker" class:right={isAfterArticleButton}></div>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
{/if}
|
|
64
|
+
|
|
65
|
+
<style>
|
|
66
|
+
.switches {
|
|
67
|
+
display: flex;
|
|
68
|
+
flex-direction: column;
|
|
69
|
+
gap: 0.75rem;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
button {
|
|
73
|
+
z-index: 1;
|
|
74
|
+
appearance: none;
|
|
75
|
+
position: relative;
|
|
76
|
+
display: flex;
|
|
77
|
+
align-items: center;
|
|
78
|
+
justify-content: center;
|
|
79
|
+
gap: 0.5rem;
|
|
80
|
+
border: 0;
|
|
81
|
+
border-radius: 2rem;
|
|
82
|
+
padding: 0.5rem;
|
|
83
|
+
background: transparent;
|
|
84
|
+
font-family: var(--playpilot-font-family);
|
|
85
|
+
font-size: 0.65rem;
|
|
86
|
+
color: var(--playpilot-text-color-alt);
|
|
87
|
+
cursor: pointer;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
button:hover,
|
|
91
|
+
button.active {
|
|
92
|
+
color: var(--playpilot-text-color);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.group {
|
|
96
|
+
position: relative;
|
|
97
|
+
display: grid;
|
|
98
|
+
grid-template-columns: 1fr 1fr;
|
|
99
|
+
gap: 0.5rem;
|
|
100
|
+
width: 100%;
|
|
101
|
+
padding: 0.25rem;
|
|
102
|
+
border: 0;
|
|
103
|
+
border-radius: 2rem;
|
|
104
|
+
background: var(--playpilot-light);
|
|
105
|
+
color: var(--playpilot-text-color-alt);
|
|
106
|
+
font-size: 0.75rem;
|
|
107
|
+
font-family: var(--playpilot-font-family);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.active-marker {
|
|
111
|
+
z-index: 0;
|
|
112
|
+
position: absolute;
|
|
113
|
+
width: calc(50% - 0.5rem);
|
|
114
|
+
height: calc(100% - 0.5rem);
|
|
115
|
+
left: 0.25rem;
|
|
116
|
+
top: 0.25rem;
|
|
117
|
+
border-radius: 2rem;
|
|
118
|
+
background: var(--playpilot-content);
|
|
119
|
+
transition: left 100ms;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.right {
|
|
123
|
+
left: calc(50% + 0.25rem);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.label {
|
|
127
|
+
margin: 0.675rem 0 0.25rem;
|
|
128
|
+
opacity: 0.75;
|
|
129
|
+
font-size: 0.675rem;
|
|
130
|
+
color: var(--playpilot-text-color-alt);
|
|
131
|
+
}
|
|
132
|
+
</style>
|