@editframe/elements 0.8.0-beta.2 → 0.8.0-beta.4

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 (44) hide show
  1. package/dist/elements/EFCaptions.d.ts +3 -2
  2. package/dist/elements/EFTemporal.d.ts +3 -0
  3. package/dist/elements/EFTimegroup.d.ts +1 -3
  4. package/dist/elements/durationConverter.d.ts +8 -0
  5. package/dist/elements/src/elements/EFCaptions.js +10 -7
  6. package/dist/elements/src/elements/EFMedia.js +6 -6
  7. package/dist/elements/src/elements/EFTemporal.js +51 -2
  8. package/dist/elements/src/elements/EFTimegroup.js +22 -39
  9. package/dist/elements/src/elements/EFWaveform.js +3 -3
  10. package/dist/elements/src/elements/parseTimeToMs.js +1 -0
  11. package/dist/elements/src/gui/ContextMixin.js +28 -18
  12. package/dist/elements/src/gui/EFFilmstrip.js +27 -25
  13. package/dist/elements/src/gui/EFToggleLoop.js +39 -0
  14. package/dist/elements/src/gui/EFTogglePlay.js +43 -0
  15. package/dist/elements/src/gui/EFWorkbench.js +4 -4
  16. package/dist/elements/src/gui/TWMixin.css.js +1 -1
  17. package/dist/elements/src/gui/efContext.js +7 -0
  18. package/dist/elements/src/gui/playingContext.js +2 -0
  19. package/dist/elements/src/index.js +4 -0
  20. package/dist/gui/ContextMixin.d.ts +8 -11
  21. package/dist/gui/EFFilmstrip.d.ts +7 -2
  22. package/dist/gui/EFPreview.d.ts +1 -15
  23. package/dist/gui/EFToggleLoop.d.ts +13 -0
  24. package/dist/gui/EFTogglePlay.d.ts +13 -0
  25. package/dist/gui/EFWorkbench.d.ts +1 -16
  26. package/dist/gui/efContext.d.ts +5 -0
  27. package/dist/gui/playingContext.d.ts +3 -0
  28. package/dist/index.d.ts +2 -0
  29. package/dist/style.css +7 -5
  30. package/package.json +2 -2
  31. package/src/elements/EFCaptions.ts +14 -8
  32. package/src/elements/EFMedia.ts +9 -6
  33. package/src/elements/EFTemporal.ts +60 -2
  34. package/src/elements/EFTimegroup.ts +21 -41
  35. package/src/elements/EFWaveform.ts +3 -3
  36. package/src/elements/durationConverter.ts +20 -0
  37. package/src/elements/parseTimeToMs.ts +1 -0
  38. package/src/gui/ContextMixin.ts +42 -30
  39. package/src/gui/EFFilmstrip.ts +27 -28
  40. package/src/gui/EFToggleLoop.ts +34 -0
  41. package/src/gui/EFTogglePlay.ts +38 -0
  42. package/src/gui/EFWorkbench.ts +4 -4
  43. package/src/gui/efContext.ts +6 -0
  44. package/src/gui/playingContext.ts +2 -0
@@ -13,6 +13,7 @@ import {
13
13
  import { TimegroupController } from "./TimegroupController.ts";
14
14
  import { EF_INTERACTIVE } from "../EF_INTERACTIVE.ts";
15
15
  import { deepGetMediaElements } from "./EFMedia.ts";
16
+ import { durationConverter } from "./durationConverter.ts";
16
17
 
17
18
  const log = debug("ef:elements:EFTimegroup");
18
19
 
@@ -37,7 +38,7 @@ export class EFTimegroup extends EFTemporal(LitElement) {
37
38
  display: block;
38
39
  width: 100%;
39
40
  height: 100%;
40
- position: relative;
41
+ position: absolute;
41
42
  top: 0;
42
43
  }
43
44
  `;
@@ -53,6 +54,13 @@ export class EFTimegroup extends EFTemporal(LitElement) {
53
54
  })
54
55
  mode: "fixed" | "sequence" | "contain" = "sequence";
55
56
 
57
+ @property({
58
+ type: Number,
59
+ converter: durationConverter,
60
+ attribute: "overlap",
61
+ })
62
+ overlapMs = 0;
63
+
56
64
  @property({ type: Number })
57
65
  set currentTime(time: number) {
58
66
  this.#currentTime = Math.max(0, Math.min(time, this.durationMs / 1000));
@@ -76,25 +84,6 @@ export class EFTimegroup extends EFTemporal(LitElement) {
76
84
  this.currentTime = ms / 1000;
77
85
  }
78
86
 
79
- @property({
80
- attribute: "crossover",
81
- converter: {
82
- fromAttribute: (value: string): number => {
83
- if (value.endsWith("ms")) {
84
- return Number.parseFloat(value);
85
- }
86
- if (value.endsWith("s")) {
87
- return Number.parseFloat(value) * 1000;
88
- }
89
- throw new Error(
90
- "`crossover` MUST be in milliseconds or seconds (10s, 10000ms)",
91
- );
92
- },
93
- toAttribute: (value: number) => `${value}ms`,
94
- },
95
- })
96
- crossoverMs = 0;
97
-
98
87
  render() {
99
88
  return html`<slot></slot> `;
100
89
  }
@@ -134,32 +123,18 @@ export class EFTimegroup extends EFTemporal(LitElement) {
134
123
  return `ef-timegroup-${this.id}`;
135
124
  }
136
125
 
137
- get crossoverStartMs() {
138
- const parentTimeGroup = this.parentTimegroup;
139
- if (!parentTimeGroup || !this.previousElementSibling) {
140
- return 0;
141
- }
142
-
143
- return parentTimeGroup.crossoverMs;
144
- }
145
- get crossoverEndMs() {
146
- const parentTimeGroup = this.parentTimegroup;
147
- if (!parentTimeGroup || !this.nextElementSibling) {
148
- return 0;
149
- }
150
-
151
- return parentTimeGroup.crossoverMs;
152
- }
153
-
154
126
  get durationMs() {
155
127
  switch (this.mode) {
156
128
  case "fixed":
157
129
  return super.durationMs;
158
130
  case "sequence": {
159
131
  let duration = 0;
160
- for (const node of this.childTemporals) {
132
+ this.childTemporals.forEach((node, index) => {
133
+ if (index > 0) {
134
+ duration -= this.overlapMs;
135
+ }
161
136
  duration += node.durationMs;
162
- }
137
+ });
163
138
  return duration;
164
139
  }
165
140
  case "contain": {
@@ -207,9 +182,14 @@ export class EFTimegroup extends EFTemporal(LitElement) {
207
182
  this.style.display = "";
208
183
 
209
184
  const animations = this.getAnimations({ subtree: true });
185
+ this.style.setProperty("--ef-duration", `${this.durationMs}ms`);
186
+ this.style.setProperty(
187
+ "--ef-transition--duration",
188
+ `${this.parentTimegroup?.overlapMs ?? 0}ms`,
189
+ );
210
190
  this.style.setProperty(
211
- "--ef-duration",
212
- `${this.durationMs + this.crossoverEndMs + this.crossoverStartMs}ms`,
191
+ "--ef-transition-out-start",
192
+ `${this.durationMs - (this.parentTimegroup?.overlapMs ?? 0)}ms`,
213
193
  );
214
194
 
215
195
  for (const animation of animations) {
@@ -351,7 +351,7 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
351
351
  if (!this.targetElement.audioBufferTask.value) {
352
352
  return;
353
353
  }
354
- if (this.targetElement.ownCurrentTimeMs > 0) {
354
+ if (this.targetElement.trimAdjustedOwnCurrentTimeMs > 0) {
355
355
  const audioContext = new OfflineAudioContext(2, 48000 / 25, 48000);
356
356
  const audioBufferSource = audioContext.createBufferSource();
357
357
  audioBufferSource.buffer =
@@ -364,7 +364,7 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
364
364
  0,
365
365
  Math.max(
366
366
  0,
367
- (this.targetElement.ownCurrentTimeMs -
367
+ (this.targetElement.trimAdjustedOwnCurrentTimeMs -
368
368
  this.targetElement.audioBufferTask.value.startOffsetMs) /
369
369
  1000,
370
370
  ),
@@ -412,6 +412,6 @@ export class EFWaveform extends EFTemporal(TWMixin(LitElement)) {
412
412
  if (target instanceof EFAudio || target instanceof EFVideo) {
413
413
  return target;
414
414
  }
415
- throw new Error("Invalid target, must be an EFAudio element");
415
+ throw new Error("Invalid target, must be an EFAudio or EFVideo element");
416
416
  }
417
417
  }
@@ -4,3 +4,23 @@ export const durationConverter = {
4
4
  fromAttribute: (value: string): number => parseTimeToMs(value),
5
5
  toAttribute: (value: number) => `${value}s`,
6
6
  };
7
+
8
+ const positiveDurationConverter = (error: string) => {
9
+ return {
10
+ fromAttribute: (value: string): number => {
11
+ if (value.startsWith("-")) {
12
+ throw new Error(error);
13
+ }
14
+ return parseTimeToMs(value);
15
+ },
16
+ toAttribute: (value: number) => `${value}s`,
17
+ };
18
+ };
19
+
20
+ export const trimDurationConverter = positiveDurationConverter(
21
+ "Trimstart & trimend must be a positive value in milliseconds or seconds (1s, 1000ms)",
22
+ );
23
+
24
+ export const imageDurationConverter = positiveDurationConverter(
25
+ "Image duration must be a positive value in milliseconds or seconds (1s, 1000ms)",
26
+ );
@@ -1,4 +1,5 @@
1
1
  export const parseTimeToMs = (time: string) => {
2
+ console.log("parseTimeToMs", time);
2
3
  if (time.endsWith("ms")) {
3
4
  return Number.parseFloat(time);
4
5
  }
@@ -5,25 +5,23 @@ import { property, state } from "lit/decorators.js";
5
5
  import { focusContext, type FocusContext } from "./focusContext.ts";
6
6
  import { focusedElementContext } from "./focusedElementContext.ts";
7
7
  import { fetchContext } from "./fetchContext.ts";
8
- import { apiHostContext } from "./apiHostContext.ts";
9
8
  import { createRef } from "lit/directives/ref.js";
10
- import { playingContext } from "./playingContext.ts";
9
+ import { loopContext, playingContext } from "./playingContext.ts";
11
10
  import type { EFTimegroup } from "../elements/EFTimegroup.ts";
11
+ import { efContext } from "./efContext.ts";
12
12
 
13
- declare class ContextMixinInterface {
14
- focusContext: FocusContext;
15
- focusedElement?: HTMLElement;
16
- fetch: typeof fetch;
13
+ export declare class ContextMixinInterface {
17
14
  signingURL?: string;
18
- apiToken?: string;
19
- apiHost: string;
20
- stageScale: number;
21
15
  rendering: boolean;
22
- stageRef: ReturnType<typeof createRef<HTMLDivElement>>;
23
- canvasRef: ReturnType<typeof createRef<HTMLElement>>;
24
16
  playing: boolean;
25
- targetTimegroup?: EFTimegroup;
17
+ loop: boolean;
26
18
  currentTimeMs: number;
19
+ focusedElement?: HTMLElement;
20
+ stageRef: ReturnType<typeof createRef<HTMLDivElement>>;
21
+ canvasRef: ReturnType<typeof createRef<HTMLElement>>;
22
+ targetTimegroup: EFTimegroup | null;
23
+ play(): void;
24
+ pause(): void;
27
25
  }
28
26
 
29
27
  type Constructor<T = {}> = new (...args: any[]) => T;
@@ -36,6 +34,9 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
36
34
  @state()
37
35
  focusedElement?: HTMLElement;
38
36
 
37
+ @provide({ context: efContext })
38
+ efContext = this;
39
+
39
40
  @provide({ context: fetchContext })
40
41
  fetch = async (url: string, init: RequestInit = {}) => {
41
42
  init.headers ||= {};
@@ -43,13 +44,6 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
43
44
  "Content-Type": "application/json",
44
45
  });
45
46
 
46
- const bearerToken = this.apiToken;
47
- if (bearerToken) {
48
- Object.assign(init.headers, {
49
- Authorization: `Bearer ${bearerToken}`,
50
- });
51
- }
52
-
53
47
  if (this.signingURL) {
54
48
  if (!this.#URLTokens[url]) {
55
49
  this.#URLTokens[url] = fetch(this.signingURL, {
@@ -77,25 +71,23 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
77
71
 
78
72
  #URLTokens: Record<string, Promise<string>> = {};
79
73
 
74
+ /**
75
+ * A URL that will be used to generated signed tokens for accessing media files from the
76
+ * editframe API. This is used to authenticate media requests per-user.
77
+ */
80
78
  @property({ type: String })
81
79
  signingURL?: string;
82
80
 
83
- @property({ type: String })
84
- apiToken?: string;
85
-
86
- @provide({ context: apiHostContext })
87
- @property({ type: String })
88
- apiHost = "";
89
-
90
81
  @provide({ context: playingContext })
91
82
  @property({ type: Boolean, reflect: true })
92
83
  playing = false;
93
84
 
85
+ @provide({ context: loopContext })
94
86
  @property({ type: Boolean, reflect: true })
95
87
  loop = false;
96
88
 
97
89
  @state()
98
- stageScale = 1;
90
+ private stageScale = 1;
99
91
 
100
92
  @property({ type: Boolean })
101
93
  rendering = false;
@@ -110,9 +102,11 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
110
102
  if (this.isConnected && !this.rendering) {
111
103
  const canvasElement = this.canvasRef.value;
112
104
  const stageElement = this.stageRef.value;
113
- if (stageElement && canvasElement) {
105
+ const canvasChild = canvasElement?.assignedElements()[0];
106
+ if (stageElement && canvasElement && canvasChild) {
114
107
  // Determine the appropriate scale factor to make the canvas fit into
115
- // it's parent element.
108
+ canvasElement.style.width = `${canvasChild.clientWidth}px`;
109
+ canvasElement.style.height = `${canvasChild.clientHeight}px`;
116
110
  const stageWidth = stageElement.clientWidth;
117
111
  const stageHeight = stageElement.clientHeight;
118
112
  const canvasWidth = canvasElement.clientWidth;
@@ -123,12 +117,14 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
123
117
  const scale = stageHeight / canvasHeight;
124
118
  if (this.stageScale !== scale) {
125
119
  canvasElement.style.transform = `scale(${scale})`;
120
+ canvasElement.style.transformOrigin = "top";
126
121
  }
127
122
  this.stageScale = scale;
128
123
  } else {
129
124
  const scale = stageWidth / canvasWidth;
130
125
  if (this.stageScale !== scale) {
131
126
  canvasElement.style.transform = `scale(${scale})`;
127
+ canvasElement.style.transformOrigin = "top";
132
128
  }
133
129
  this.stageScale = scale;
134
130
  }
@@ -167,6 +163,14 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
167
163
  return this.querySelector("ef-timegroup");
168
164
  }
169
165
 
166
+ play() {
167
+ this.playing = true;
168
+ }
169
+
170
+ pause() {
171
+ this.playing = false;
172
+ }
173
+
170
174
  #playbackAudioContext: AudioContext | null = null;
171
175
  #playbackAnimationFrameRequest: number | null = null;
172
176
  #AUDIO_PLAYBACK_SLICE_MS = 1000;
@@ -239,7 +243,15 @@ export function ContextMixin<T extends Constructor<LitElement>>(superClass: T) {
239
243
  source.onended = () => {
240
244
  bufferCount--;
241
245
  if (endMs >= toMs) {
242
- this.playing = false;
246
+ this.pause();
247
+ if (this.loop) {
248
+ this.updateComplete.then(() => {
249
+ this.currentTimeMs = 0;
250
+ this.updateComplete.then(() => {
251
+ this.play();
252
+ });
253
+ });
254
+ }
243
255
  } else {
244
256
  fillBuffer();
245
257
  }
@@ -29,7 +29,7 @@ import { TWMixin } from "./TWMixin.ts";
29
29
  import { msToTimeCode } from "../msToTimeCode.ts";
30
30
  import { focusedElementContext } from "./focusedElementContext.ts";
31
31
  import { type FocusContext, focusContext } from "./focusContext.ts";
32
- import { playingContext } from "./playingContext.ts";
32
+ import { playingContext, loopContext } from "./playingContext.ts";
33
33
  import type { EFWorkbench } from "./EFWorkbench.ts";
34
34
  import type { EFPreview } from "./EFPreview.ts";
35
35
 
@@ -86,18 +86,25 @@ class FilmstripItem extends TWMixin(LitElement) {
86
86
  @property({ type: Number })
87
87
  pixelsPerMs = 0.04;
88
88
 
89
- get styles() {
89
+ get gutterStyles() {
90
90
  return {
91
91
  position: "relative",
92
- left: `${this.pixelsPerMs * this.element.startTimeWithinParentMs}px`,
92
+ left: `${this.pixelsPerMs * (this.element.startTimeWithinParentMs - this.element.trimStartMs)}px`,
93
+ width: `${this.pixelsPerMs * (this.element.durationMs + this.element.trimStartMs + this.element.trimEndMs)}px`,
94
+ };
95
+ }
96
+
97
+ get trimPortionStyles() {
98
+ return {
93
99
  width: `${this.pixelsPerMs * this.element.durationMs}px`,
100
+ left: `${this.pixelsPerMs * this.element.trimStartMs}px`,
94
101
  };
95
102
  }
96
103
 
97
104
  render() {
98
- return html` <div class="" style=${styleMap(this.styles)}>
105
+ return html`<div style=${styleMap(this.gutterStyles)}>
99
106
  <div
100
- class="border-outset relative mb-[1px] block h-[1.1rem] text-nowrap border border-slate-500 bg-blue-200 hover:bg-blue-400 text-sm data-[focused]:bg-slate-400"
107
+ class="bg-slate-300"
101
108
  ?data-focused=${this.isFocused}
102
109
  @mouseenter=${() => {
103
110
  if (this.focusContext) {
@@ -110,7 +117,13 @@ class FilmstripItem extends TWMixin(LitElement) {
110
117
  }
111
118
  }}
112
119
  >
113
- ${this.animations()}
120
+ <div
121
+ ?data-focused=${this.isFocused}
122
+ class="border-outset relative mb-[1px] block h-[1.1rem] text-nowrap border border-slate-500 bg-blue-200 text-sm data-[focused]:bg-slate-400"
123
+ style=${styleMap(this.trimPortionStyles)}
124
+ >
125
+ ${this.animations()}
126
+ </div>
114
127
  </div>
115
128
  ${this.renderChildren()}
116
129
  </div>`;
@@ -500,6 +513,10 @@ export class EFFilmstrip extends TWMixin(LitElement) {
500
513
  @state()
501
514
  playing?: boolean;
502
515
 
516
+ @consume({ context: loopContext, subscribe: true })
517
+ @state()
518
+ loop?: boolean;
519
+
503
520
  timegroupController?: TimegroupController;
504
521
 
505
522
  @state()
@@ -532,12 +549,13 @@ export class EFFilmstrip extends TWMixin(LitElement) {
532
549
  #handleKeyPress = (event: KeyboardEvent) => {
533
550
  // On spacebar, toggle playback
534
551
  if (event.key === " ") {
552
+ const [target] = event.composedPath();
535
553
  // CSS selector to match all interactive elements
536
554
  const interactiveSelector =
537
555
  "input, textarea, button, select, a, [contenteditable]";
538
556
 
539
557
  // Check if the event target or its ancestor matches an interactive element
540
- const closestInteractive = (event.target as HTMLElement | null)?.closest(
558
+ const closestInteractive = (target as HTMLElement | null)?.closest(
541
559
  interactiveSelector,
542
560
  );
543
561
  if (closestInteractive) {
@@ -676,27 +694,8 @@ export class EFFilmstrip extends TWMixin(LitElement) {
676
694
  />
677
695
  <code>${msToTimeCode(this.currentTimeMs, true)} </code> /
678
696
  <code>${msToTimeCode(target?.durationMs ?? 0, true)}</code>
679
- ${
680
- this.playing
681
- ? html`<button
682
- @click=${() => {
683
- if (this.#contextElement) {
684
- this.#contextElement.playing = false;
685
- }
686
- }}
687
- >
688
- ⏸️
689
- </button>`
690
- : html`<button
691
- @click=${() => {
692
- if (this.#contextElement) {
693
- this.#contextElement.playing = true;
694
- }
695
- }}
696
- >
697
- ▶️
698
- </button>`
699
- }
697
+ <ef-toggle-play><button>${this.playing ? "⏸️" : "▶️"}</button></ef-toggle-play>
698
+ <ef-toggle-loop><button>${this.loop ? "🔁" : html`<span class="opacity-50">🔁</span>`}</button></ef-toggle-loop>
700
699
  </div>
701
700
  <div
702
701
  class="z-10 pl-1 pr-1 pt-2 shadow shadow-slate-600 overflow-auto"
@@ -0,0 +1,34 @@
1
+ import { css, html, LitElement } from "lit";
2
+ import { customElement } from "lit/decorators.js";
3
+ import { consume } from "@lit/context";
4
+
5
+ import { efContext } from "./efContext.ts";
6
+ import type { ContextMixinInterface } from "./ContextMixin.ts";
7
+
8
+ @customElement("ef-toggle-loop")
9
+ export class EFToggleLoop extends LitElement {
10
+ static styles = [
11
+ css`
12
+ :host {}
13
+ `,
14
+ ];
15
+
16
+ @consume({ context: efContext })
17
+ context?: ContextMixinInterface | null;
18
+
19
+ render() {
20
+ return html`
21
+ <slot @click=${() => {
22
+ if (this.context) {
23
+ this.context.loop = !this.context.loop;
24
+ }
25
+ }}></slot>
26
+ `;
27
+ }
28
+ }
29
+
30
+ declare global {
31
+ interface HTMLElementTagNameMap {
32
+ "ef-toggle-loop": EFToggleLoop;
33
+ }
34
+ }
@@ -0,0 +1,38 @@
1
+ import { css, html, LitElement } from "lit";
2
+ import { customElement } from "lit/decorators.js";
3
+ import { consume } from "@lit/context";
4
+
5
+ import { efContext } from "./efContext.ts";
6
+ import type { ContextMixinInterface } from "./ContextMixin.ts";
7
+
8
+ @customElement("ef-toggle-play")
9
+ export class EFTogglePlay extends LitElement {
10
+ static styles = [
11
+ css`
12
+ :host {}
13
+ `,
14
+ ];
15
+
16
+ @consume({ context: efContext })
17
+ context?: ContextMixinInterface | null;
18
+
19
+ render() {
20
+ return html`
21
+ <slot @click=${() => {
22
+ if (this.context) {
23
+ if (this.context.playing) {
24
+ this.context.pause();
25
+ } else {
26
+ this.context.play();
27
+ }
28
+ }
29
+ }}></slot>
30
+ `;
31
+ }
32
+ }
33
+
34
+ declare global {
35
+ interface HTMLElementTagNameMap {
36
+ "ef-toggle-play": EFTogglePlay;
37
+ }
38
+ }
@@ -78,21 +78,21 @@ export class EFWorkbench extends ContextMixin(TWMixin(LitElement)) {
78
78
  >
79
79
  <div
80
80
  ${ref(this.stageRef)}
81
- class="relative grid h-full w-full place-content-center place-items-center overflow-hidden"
81
+ class="relative grid h-full w-full justify-center overflow-hidden"
82
82
  @wheel=${this.handleStageWheel}
83
83
  >
84
84
  <slot
85
85
  ${ref(this.canvasRef)}
86
- class="inline-block"
87
86
  name="canvas"
87
+ class="inline-block"
88
88
  ></slot>
89
89
  <div
90
- class="border border-blue-500 bg-blue-200 bg-opacity-20"
90
+ class="border border-blue-500 bg-blue-200 bg-opacity-20 absolute"
91
91
  ${ref(this.focusOverlay)}
92
92
  ></div>
93
93
  </div>
94
94
 
95
- <slot class="overflow" name="timeline"></slot>
95
+ <slot class="overflow inline-block" name="timeline"></slot>
96
96
  </div>
97
97
  `;
98
98
  }
@@ -0,0 +1,6 @@
1
+ import { createContext } from "@lit/context";
2
+ import type { ContextMixinInterface } from "./ContextMixin.ts";
3
+
4
+ export const efContext = createContext<ContextMixinInterface | null>(
5
+ Symbol("efContext"),
6
+ );
@@ -1,3 +1,5 @@
1
1
  import { createContext } from "@lit/context";
2
2
 
3
3
  export const playingContext = createContext<boolean>(Symbol("playingContext"));
4
+
5
+ export const loopContext = createContext<boolean>(Symbol("loopContext"));