@daydreamui/media 0.0.0-stage → 0.3.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/dist/index.js ADDED
@@ -0,0 +1,678 @@
1
+ import { createComponent, delegateEvents, effect, insert, memo, readShallow, ref, setAttribute, style, template } from "@solidjs/web";
2
+ import { Show, createSignal, onCleanup, onSettled, untrack } from "solid-js";
3
+ var IMAGE_KIND = "daydream.image";
4
+ var VIDEO_KIND = "daydream.video";
5
+ var DEFAULT_FOCAL_POINT = {
6
+ x: .5,
7
+ y: .5
8
+ };
9
+ var DEFAULT_IMAGE_FIT = "cover";
10
+ function newMediaPayload(kind, src, label) {
11
+ return {
12
+ src,
13
+ ...label === void 0 ? {} : { label },
14
+ ...kind === "daydream.image" ? { fit: DEFAULT_IMAGE_FIT } : {}
15
+ };
16
+ }
17
+ function applyDefaultImageFit(item) {
18
+ if (item.kind === "daydream.image" && item.payload.fit === void 0) item.payload.fit = DEFAULT_IMAGE_FIT;
19
+ }
20
+ var ASPECT_RATIO_TOLERANCE = .005;
21
+ function resizeBreaksAspect(context) {
22
+ if (context.proportional) return false;
23
+ const startRatio = context.startSize.width / context.startSize.height;
24
+ const ratio = context.size.width / context.size.height;
25
+ return Number.isFinite(startRatio) && Number.isFinite(ratio) && Math.abs(ratio / startRatio - 1) > ASPECT_RATIO_TOLERANCE;
26
+ }
27
+ function imageFit(payload) {
28
+ return payload.fit === "cover" ? "cover" : "contain";
29
+ }
30
+ function imageFocalPoint(payload) {
31
+ return payload.focalPoint ?? DEFAULT_FOCAL_POINT;
32
+ }
33
+ var DOCUMENT_FILE = /^assets\/(?:[A-Za-z0-9_-]{1,80}\/){0,7}[A-Za-z0-9_-]{1,80}\.[A-Za-z0-9]{1,8}$/;
34
+ function mediaSrcProblem(src) {
35
+ if (DOCUMENT_FILE.test(src)) return null;
36
+ if (src.startsWith("https://")) try {
37
+ if (new URL(src).protocol === "https:") return null;
38
+ } catch {}
39
+ return "src must be an https URL or a file of the project's own, assets/<file>";
40
+ }
41
+ function mediaPayloadProblem(raw, where, noun) {
42
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw)) return `${where} is not ${noun === "image" ? "an" : "a"} ${noun} payload`;
43
+ const payload = raw;
44
+ if (typeof payload["src"] !== "string" || payload["src"] === "") return `${where}.src must be a non-empty string`;
45
+ const problem = mediaSrcProblem(payload["src"]);
46
+ if (problem !== null) return `${where}.${problem}`;
47
+ if (payload["label"] !== void 0 && (typeof payload["label"] !== "string" || payload["label"].trim() === "")) return `${where}.label must be a non-empty string (or omitted)`;
48
+ if (noun === "video" && (payload["fit"] !== void 0 || payload["focalPoint"] !== void 0)) return `${where}.fit and ${where}.focalPoint are only valid for images`;
49
+ if (payload["fit"] !== void 0 && payload["fit"] !== "contain" && payload["fit"] !== "cover") return `${where}.fit must be "contain", "cover", or omitted`;
50
+ if (payload["focalPoint"] !== void 0) {
51
+ const focalPoint = payload["focalPoint"];
52
+ if (focalPoint === null || typeof focalPoint !== "object" || Array.isArray(focalPoint)) return `${where}.focalPoint must be an object or omitted`;
53
+ for (const axis of ["x", "y"]) {
54
+ const value = focalPoint[axis];
55
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0 || value > 1) return `${where}.focalPoint.${axis} must be a finite number from 0 to 1`;
56
+ }
57
+ }
58
+ return null;
59
+ }
60
+ function mediaName(item) {
61
+ return item.payload.label ?? (item.kind === "daydream.image" ? "Image" : "Video");
62
+ }
63
+ var extensions = {
64
+ png: "image/png",
65
+ jpg: "image/jpeg",
66
+ jpeg: "image/jpeg",
67
+ gif: "image/gif",
68
+ webp: "image/webp",
69
+ avif: "image/avif",
70
+ svg: "image/svg+xml",
71
+ mp4: "video/mp4",
72
+ m4v: "video/mp4",
73
+ webm: "video/webm",
74
+ mov: "video/quicktime",
75
+ ogv: "video/ogg"
76
+ };
77
+ function mediaFile(file) {
78
+ const type = file.type === "" || file.type === "application/octet-stream" ? extensions[file.name.split(".").pop()?.toLowerCase() ?? ""] : file.type;
79
+ if (type === void 0 || !Object.values(extensions).includes(type)) return null;
80
+ return type === file.type ? file : new File([file], file.name, {
81
+ type,
82
+ lastModified: file.lastModified
83
+ });
84
+ }
85
+ function labelFromFileName(name) {
86
+ const dot = name.lastIndexOf(".");
87
+ return (dot === -1 ? name : name.slice(0, dot)).trim() || void 0;
88
+ }
89
+ function mediaKindOfUrl(src) {
90
+ try {
91
+ const type = extensions[new URL(src, "https://local.invalid").pathname.split(".").pop()?.toLowerCase() ?? ""];
92
+ return type?.startsWith("image/") ? IMAGE_KIND : type?.startsWith("video/") ? VIDEO_KIND : null;
93
+ } catch {
94
+ return null;
95
+ }
96
+ }
97
+ var _tmpl$ = template(`<div class=dd-media-item><!><!>`);
98
+ var _tmpl$2 = template(`<img alt>`);
99
+ var _tmpl$3 = template(`<img alt aria-hidden=true style=position:absolute;visibility:hidden;pointer-events:none;width:100%;height:auto>`);
100
+ var _tmpl$4 = template(`<img alt aria-hidden=true style=position:absolute;visibility:hidden;pointer-events:none;width:auto;height:100%>`);
101
+ var _tmpl$5 = template(`<video controls preload=metadata>`);
102
+ var _tmpl$6 = template(`<span class=dd-media-fit-label role=status>`);
103
+ function mediaStyle(frame, item) {
104
+ if (frame === void 0) return { display: "block" };
105
+ if (frame.height === void 0) return {
106
+ display: "block",
107
+ width: "100%",
108
+ height: "auto"
109
+ };
110
+ const fit = item?.kind === "daydream.image" ? imageFit(item.payload) : "contain";
111
+ const focalPoint = item?.kind === "daydream.image" ? imageFocalPoint(item.payload) : void 0;
112
+ return {
113
+ display: "block",
114
+ width: "100%",
115
+ height: "100%",
116
+ "object-fit": fit,
117
+ ...focalPoint === void 0 ? {} : { "object-position": `${focalPoint.x * 100}% ${focalPoint.y * 100}%` }
118
+ };
119
+ }
120
+ function MediaItem(props) {
121
+ const id = untrack(() => props.item.id);
122
+ const isImage = untrack(() => props.item.kind === IMAGE_KIND);
123
+ let binding;
124
+ let cropBinding;
125
+ let cropTransaction;
126
+ let cropStart = DEFAULT_FOCAL_POINT;
127
+ let cropOverflow = {
128
+ x: 0,
129
+ y: 0
130
+ };
131
+ let disposing = false;
132
+ let boxNode;
133
+ let widthProbeNode;
134
+ let heightProbeNode;
135
+ let geometryObserver;
136
+ let geometryRaf;
137
+ let fitLabelTimer;
138
+ const [fitLabel, setFitLabel] = createSignal(null);
139
+ const [cropping, setCropping] = createSignal(false);
140
+ const [overflow, setOverflow] = createSignal({
141
+ x: 0,
142
+ y: 0
143
+ });
144
+ onSettled(() => () => untrack(() => {
145
+ disposing = true;
146
+ const transaction = cropTransaction;
147
+ cropTransaction = void 0;
148
+ binding?.dispose();
149
+ cropBinding?.dispose();
150
+ geometryObserver?.disconnect();
151
+ if (geometryRaf !== void 0) cancelAnimationFrame(geometryRaf);
152
+ if (transaction !== void 0) queueMicrotask(() => transaction.cancel());
153
+ if (fitLabelTimer !== void 0) clearTimeout(fitLabelTimer);
154
+ }));
155
+ const src = () => mediaSrcProblem(props.item.payload.src) === null ? props.dd.assetUrl(props.item.payload.src) : void 0;
156
+ const style$1 = () => ({
157
+ left: `${props.item.position.x}px`,
158
+ top: `${props.item.position.y}px`,
159
+ ...props.item.frame === void 0 ? {} : { width: `${props.item.frame.width}px` },
160
+ ...props.item.frame?.height === void 0 ? {} : { height: `${props.item.frame.height}px` },
161
+ ...isImage && props.item.frame?.height !== void 0 && imageFit(props.item.payload) === "cover" ? { overflow: "hidden" } : {}
162
+ });
163
+ const failed = () => props.onError?.(`Could not display ${mediaName(props.item)}. This browser may not support the file's format.`);
164
+ const renderedOverflow = () => {
165
+ if (boxNode === void 0 || widthProbeNode === void 0 || heightProbeNode === void 0) return {
166
+ x: 0,
167
+ y: 0
168
+ };
169
+ const frameRect = boxNode.getBoundingClientRect();
170
+ const widthRect = widthProbeNode.getBoundingClientRect();
171
+ const heightRect = heightProbeNode.getBoundingClientRect();
172
+ const coverRect = widthRect.height >= frameRect.height ? widthRect : heightRect;
173
+ return {
174
+ x: Math.max(0, coverRect.width - frameRect.width),
175
+ y: Math.max(0, coverRect.height - frameRect.height)
176
+ };
177
+ };
178
+ const updateOverflow = () => {
179
+ geometryRaf = void 0;
180
+ setOverflow(renderedOverflow());
181
+ };
182
+ const scheduleOverflowUpdate = () => {
183
+ if (geometryRaf === void 0) geometryRaf = requestAnimationFrame(updateOverflow);
184
+ };
185
+ const observeGeometry = () => {
186
+ if (geometryObserver !== void 0 || boxNode === void 0) return;
187
+ geometryObserver = new ResizeObserver(scheduleOverflowUpdate);
188
+ geometryObserver.observe(boxNode);
189
+ scheduleOverflowUpdate();
190
+ };
191
+ const cropPossible = () => {
192
+ if (imageFit(props.item.payload) !== "cover") return false;
193
+ const current = overflow();
194
+ return current.x > .01 || current.y > .01;
195
+ };
196
+ const applyCrop = (delta) => {
197
+ const clamp = (value) => Math.max(0, Math.min(1, value));
198
+ const focalPoint = {
199
+ x: cropOverflow.x > .01 ? clamp(cropStart.x - delta.x / cropOverflow.x) : cropStart.x,
200
+ y: cropOverflow.y > .01 ? clamp(cropStart.y - delta.y / cropOverflow.y) : cropStart.y
201
+ };
202
+ cropTransaction?.update(id, (item) => {
203
+ if (item.kind === "daydream.image") item.payload.focalPoint = focalPoint;
204
+ });
205
+ };
206
+ const bindCrop = (node) => {
207
+ cropBinding?.dispose();
208
+ cropBinding = props.dd.canvas.bindDrag(node, {
209
+ shouldStart: (event) => event.altKey && cropPossible(),
210
+ movementAxes: () => ({
211
+ x: cropOverflow.x > .01,
212
+ y: cropOverflow.y > .01
213
+ }),
214
+ onStart: () => {
215
+ if (!props.dd.itemSelection().includes(id)) props.dd.select(id);
216
+ cropStart = { ...imageFocalPoint(props.item.payload) };
217
+ cropOverflow = renderedOverflow();
218
+ node.dataset["mediaCropping"] = "";
219
+ setCropping(true);
220
+ cropTransaction = props.dd.beginItemTransaction({ onInterrupted: () => {
221
+ cropTransaction = void 0;
222
+ delete node.dataset["mediaCropping"];
223
+ setCropping(false);
224
+ } });
225
+ },
226
+ onMove: (context) => {
227
+ if (context.moved) applyCrop(context.deltaScreen);
228
+ },
229
+ onEnd: (context) => {
230
+ if (disposing) return;
231
+ delete node.dataset["mediaCropping"];
232
+ setCropping(false);
233
+ if (context.canceled) cropTransaction?.cancel();
234
+ else if (context.moved) {
235
+ applyCrop(context.deltaScreen);
236
+ cropTransaction?.commit();
237
+ } else cropTransaction?.cancel();
238
+ cropTransaction = void 0;
239
+ }
240
+ });
241
+ };
242
+ const toggleFit = (event) => {
243
+ event.stopPropagation();
244
+ if (!isImage || event.button !== 0 || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey || props.dd.canvas.panMode() || props.item.frame?.height === void 0) return;
245
+ const fit = imageFit(props.item.payload) === "cover" ? "contain" : "cover";
246
+ props.dd.updateItem(id, (item) => {
247
+ if (item.kind === "daydream.image") item.payload.fit = fit;
248
+ });
249
+ props.dd.select(id);
250
+ setFitLabel(fit === "cover" ? "Fill" : "Fit");
251
+ if (fitLabelTimer !== void 0) clearTimeout(fitLabelTimer);
252
+ fitLabelTimer = setTimeout(() => setFitLabel(null), 900);
253
+ };
254
+ var _el$ = _tmpl$();
255
+ var _el$2 = _el$.firstChild;
256
+ var _el$3 = _el$2.nextSibling;
257
+ ref(() => {
258
+ return (node) => {
259
+ boxNode = node;
260
+ observeGeometry();
261
+ binding = props.dd.canvas.bindItemNode(id, node, {
262
+ draggable: isImage,
263
+ canMove: (event) => !isImage || !event.altKey
264
+ });
265
+ };
266
+ }, _el$);
267
+ _el$._$$dblclick = toggleFit;
268
+ insert(_el$, (() => {
269
+ var _c$ = memo(() => {
270
+ return props.item.kind === IMAGE_KIND;
271
+ });
272
+ return () => {
273
+ return _c$() ? [
274
+ (() => {
275
+ var _el$4 = _tmpl$2();
276
+ _el$4.addEventListener("error", failed);
277
+ _el$4.addEventListener("load", scheduleOverflowUpdate);
278
+ ref(() => {
279
+ return (node) => {
280
+ observeGeometry();
281
+ bindCrop(node);
282
+ };
283
+ }, _el$4);
284
+ effect(() => {
285
+ return {
286
+ e: src(),
287
+ t: {
288
+ ...mediaStyle(props.item.frame, props.item),
289
+ ...cropping() ? { cursor: "grabbing" } : props.dd.canvas.optionMode() && cropPossible() ? { cursor: "grab" } : {}
290
+ }
291
+ };
292
+ }, ({ e, t }, _p$) => {
293
+ e !== _p$?.e && setAttribute(_el$4, "src", e);
294
+ style(_el$4, t, _p$?.t);
295
+ });
296
+ return _el$4;
297
+ })(),
298
+ (() => {
299
+ var _el$5 = _tmpl$3();
300
+ _el$5.addEventListener("load", scheduleOverflowUpdate);
301
+ ref(() => {
302
+ return (node) => widthProbeNode = node;
303
+ }, _el$5);
304
+ effect(() => src(), (_v$) => {
305
+ setAttribute(_el$5, "src", _v$);
306
+ });
307
+ return _el$5;
308
+ })(),
309
+ (() => {
310
+ var _el$6 = _tmpl$4();
311
+ _el$6.addEventListener("load", scheduleOverflowUpdate);
312
+ ref(() => {
313
+ return (node) => heightProbeNode = node;
314
+ }, _el$6);
315
+ effect(() => src(), (_v$) => {
316
+ setAttribute(_el$6, "src", _v$);
317
+ });
318
+ return _el$6;
319
+ })()
320
+ ] : (() => {
321
+ var _el$7 = _tmpl$5();
322
+ _el$7.addEventListener("error", failed);
323
+ effect(() => {
324
+ return {
325
+ e: src(),
326
+ t: mediaName(props.item),
327
+ a: readShallow(mediaStyle(props.item.frame, props.item))
328
+ };
329
+ }, ({ e, t, a }, _p$) => {
330
+ e !== _p$?.e && setAttribute(_el$7, "src", e);
331
+ t !== _p$?.t && setAttribute(_el$7, "aria-label", t);
332
+ style(_el$7, a, _p$?.a);
333
+ });
334
+ return _el$7;
335
+ })();
336
+ };
337
+ })(), _el$2);
338
+ insert(_el$, createComponent(Show, {
339
+ get when() {
340
+ return fitLabel();
341
+ },
342
+ children: (label) => (() => {
343
+ var _el$8 = _tmpl$6();
344
+ insert(_el$8, label);
345
+ return _el$8;
346
+ })()
347
+ }), _el$3);
348
+ effect(() => {
349
+ return {
350
+ e: readShallow(style$1()),
351
+ t: props.item.kind
352
+ };
353
+ }, ({ e, t }, _p$) => {
354
+ style(_el$, e, _p$?.e);
355
+ t !== _p$?.t && setAttribute(_el$, "data-item-kind", t);
356
+ });
357
+ return _el$;
358
+ }
359
+ delegateEvents(["dblclick"]);
360
+ async function measureIncomingMediaItem(item, assetUrl, signal) {
361
+ const src = assetUrl(item.payload.src);
362
+ if (typeof document === "undefined" || document.body === null) return null;
363
+ if (signal?.aborted) return null;
364
+ const box = document.createElement("div");
365
+ Object.assign(box.style, {
366
+ position: "fixed",
367
+ left: "-100000px",
368
+ top: "-100000px",
369
+ visibility: "hidden",
370
+ pointerEvents: "none",
371
+ ...item.frame === void 0 ? {} : { width: `${item.frame.width}px` },
372
+ ...item.frame?.height === void 0 ? {} : { height: `${item.frame.height}px` }
373
+ });
374
+ const media = item.kind === "daydream.image" ? document.createElement("img") : document.createElement("video");
375
+ if (media instanceof HTMLVideoElement) {
376
+ media.controls = true;
377
+ media.preload = "metadata";
378
+ }
379
+ for (const [property, value] of Object.entries(mediaStyle(item.frame, item))) media.style.setProperty(property, value);
380
+ box.append(media);
381
+ try {
382
+ if (!await new Promise((resolve) => {
383
+ const readyEvent = item.kind === "daydream.image" ? "load" : "loadedmetadata";
384
+ const settle = (ready) => {
385
+ clearTimeout(timer);
386
+ media.removeEventListener(readyEvent, onReady);
387
+ media.removeEventListener("error", onError);
388
+ signal?.removeEventListener("abort", onError);
389
+ resolve(ready);
390
+ };
391
+ const onReady = () => settle(true);
392
+ const onError = () => settle(false);
393
+ const timer = setTimeout(() => settle(false), 1e3);
394
+ media.addEventListener(readyEvent, onReady);
395
+ media.addEventListener("error", onError);
396
+ signal?.addEventListener("abort", onError, { once: true });
397
+ media.src = src;
398
+ document.body.append(box);
399
+ })) return null;
400
+ const { width, height } = box.getBoundingClientRect();
401
+ return width > 0 && height > 0 ? {
402
+ width,
403
+ height
404
+ } : null;
405
+ } finally {
406
+ box.remove();
407
+ media.removeAttribute("src");
408
+ if (media instanceof HTMLVideoElement) media.load();
409
+ }
410
+ }
411
+ var MAX_DROPPED_IMAGE_WIDTH = 1200;
412
+ var CONCURRENCY = 4;
413
+ var MEASUREMENT_BUDGET_MS = 1e3;
414
+ async function intrinsicImageWidth(file) {
415
+ try {
416
+ const bitmap = await createImageBitmap(file);
417
+ const width = bitmap.width;
418
+ bitmap.close();
419
+ return width;
420
+ } catch {
421
+ return;
422
+ }
423
+ }
424
+ function createMediaImporter(dd, status, deps = {}) {
425
+ let active = true;
426
+ const batches = new Set();
427
+ const measureSource = deps.measureSource ?? ((item, signal) => measureIncomingMediaItem(item, (src) => dd.assetUrl(src), signal));
428
+ const cancel = () => {
429
+ const aborted = batches.size > 0;
430
+ for (const batch of batches) batch.abort();
431
+ batches.clear();
432
+ return aborted;
433
+ };
434
+ const run = async (jobs, at) => {
435
+ if (!active) return;
436
+ if (jobs.length === 0) {
437
+ status("Choose a supported image or video file.");
438
+ return;
439
+ }
440
+ const load = dd.loadVersion();
441
+ const controller = new AbortController();
442
+ const measurement = new AbortController();
443
+ const stopMeasurement = () => measurement.abort();
444
+ controller.signal.addEventListener("abort", stopMeasurement, { once: true });
445
+ const timer = setTimeout(stopMeasurement, MEASUREMENT_BUDGET_MS);
446
+ const current = () => active && !controller.signal.aborted && dd.loadVersion() === load;
447
+ batches.add(controller);
448
+ const results = new Array(jobs.length);
449
+ const failures = [];
450
+ let cursor = 0;
451
+ let completed = 0;
452
+ const progress = () => status(`Importing media… ${completed}/${jobs.length}`);
453
+ progress();
454
+ const worker = async () => {
455
+ while (cursor < jobs.length && current()) {
456
+ const index = cursor++;
457
+ const job = jobs[index];
458
+ try {
459
+ results[index] = await job.prepare(controller.signal, measurement.signal);
460
+ } catch (error) {
461
+ failures.push(`${job.name}: ${error instanceof Error ? error.message : "could not import file"}`);
462
+ }
463
+ if (!current()) return;
464
+ completed++;
465
+ progress();
466
+ }
467
+ };
468
+ try {
469
+ await Promise.all(Array.from({ length: Math.min(CONCURRENCY, jobs.length) }, worker));
470
+ if (!current()) return;
471
+ const items = results.filter((result) => result !== void 0).map(({ kind, payload, width }, index) => ({
472
+ id: dd.core.generateId(),
473
+ kind,
474
+ payload,
475
+ position: {
476
+ x: at.x + index * 24,
477
+ y: at.y + index * 24
478
+ },
479
+ ...kind === "daydream.image" && width !== void 0 && width > 1200 ? { frame: { width: MAX_DROPPED_IMAGE_WIDTH } } : {}
480
+ }));
481
+ if (items.length > 0) dd.mutateItems((draft) => {
482
+ draft.push(...items);
483
+ });
484
+ status(failures.length > 0 ? failures.join("; ") : `Added ${items.length} ${items.length === 1 ? "file" : "files"}.`);
485
+ } catch (error) {
486
+ if (current()) status(error instanceof Error ? error.message : "Could not add media.");
487
+ } finally {
488
+ clearTimeout(timer);
489
+ controller.signal.removeEventListener("abort", stopMeasurement);
490
+ measurement.abort();
491
+ batches.delete(controller);
492
+ }
493
+ };
494
+ return {
495
+ cancel() {
496
+ if (cancel() && active) status("Import canceled.");
497
+ },
498
+ dispose() {
499
+ active = false;
500
+ cancel();
501
+ },
502
+ add(files, at) {
503
+ return run(files.map(mediaFile).filter((file) => file !== null).map((file) => ({
504
+ name: file.name,
505
+ async prepare(signal) {
506
+ const { pageSrc: src } = await dd.vendorFile(file);
507
+ signal.throwIfAborted();
508
+ const image = file.type.startsWith("image/");
509
+ const width = image ? await (deps.imageWidth ?? intrinsicImageWidth)(file) : void 0;
510
+ const label = labelFromFileName(file.name);
511
+ return {
512
+ kind: image ? IMAGE_KIND : VIDEO_KIND,
513
+ payload: newMediaPayload(image ? IMAGE_KIND : VIDEO_KIND, src, label),
514
+ width
515
+ };
516
+ }
517
+ })), at);
518
+ },
519
+ addSources(sources, at) {
520
+ return run(sources.filter(({ src }) => mediaSrcProblem(src) === null).map(({ kind, src }) => ({
521
+ name: src,
522
+ async prepare(_signal, measurementSignal) {
523
+ const item = {
524
+ id: "probe",
525
+ kind,
526
+ payload: newMediaPayload(kind, src),
527
+ position: {
528
+ x: 0,
529
+ y: 0
530
+ }
531
+ };
532
+ const size = measurementSignal.aborted ? null : await measureSource(item, measurementSignal);
533
+ return {
534
+ kind,
535
+ payload: newMediaPayload(kind, src),
536
+ width: size?.width
537
+ };
538
+ }
539
+ })), at);
540
+ }
541
+ };
542
+ }
543
+ function mediaSources(dd, transfer) {
544
+ const sources = [];
545
+ const add = (kind, src) => {
546
+ if (src === null || mediaSrcProblem(src) !== null) return;
547
+ if (!sources.some((source) => source.src === src)) sources.push({
548
+ kind,
549
+ src
550
+ });
551
+ };
552
+ const uriList = transfer.getData("text/uri-list");
553
+ if (uriList !== "") {
554
+ for (const line of uriList.split(/\r?\n/)) {
555
+ const src = line.trim();
556
+ if (src.startsWith("#")) continue;
557
+ const kind = mediaKindOfUrl(src);
558
+ if (kind !== null) add(kind, src);
559
+ }
560
+ if (sources.length > 0) return sources;
561
+ }
562
+ const html = transfer.getData("text/html");
563
+ if (html !== "") {
564
+ const template = document.createElement("template");
565
+ template.innerHTML = html;
566
+ const elements = [...template.content.querySelectorAll("img,video")];
567
+ const prose = template.content.cloneNode(true);
568
+ for (const element of prose.querySelectorAll("img,video,picture,script,style,template")) element.remove();
569
+ if (prose.textContent?.trim()) return [];
570
+ for (const element of elements) add(element.localName === "img" ? IMAGE_KIND : VIDEO_KIND, element.getAttribute("src") ?? element.querySelector("source[src]")?.getAttribute("src") ?? null);
571
+ if (elements.length > 0) return sources;
572
+ }
573
+ const lines = transfer.getData("text/plain").trim().split(/\r?\n/).map((line) => line.trim());
574
+ for (const src of lines) {
575
+ const kind = mediaKindOfUrl(src);
576
+ if (kind === null || mediaSrcProblem(src) !== null) return [];
577
+ add(kind, src);
578
+ }
579
+ return sources;
580
+ }
581
+ function registerTransfers(dd, importer) {
582
+ const receive = (event, transfer, at) => {
583
+ if (transfer === null) return;
584
+ const files = Array.from(transfer.files);
585
+ if (files.some((file) => mediaFile(file) !== null)) {
586
+ event.preventDefault();
587
+ importer.add(files, at);
588
+ return;
589
+ }
590
+ const sources = mediaSources(dd, transfer);
591
+ if (sources.length === 0) return;
592
+ event.preventDefault();
593
+ importer.addSources(sources, at);
594
+ };
595
+ dd.canvas.onPaste((event) => receive(event, event.clipboardData, dd.canvas.center()), { priority: 10 });
596
+ dd.canvas.onDragOver((event) => {
597
+ if (!event.dataTransfer?.types.some((type) => [
598
+ "Files",
599
+ "text/html",
600
+ "text/uri-list"
601
+ ].includes(type))) return;
602
+ event.preventDefault();
603
+ event.dataTransfer.dropEffect = "copy";
604
+ });
605
+ dd.canvas.onDrop((event) => receive(event, event.dataTransfer, dd.canvas.screenToWorld(event.clientX, event.clientY)));
606
+ }
607
+ var mediaStyles = `
608
+ .dd-media-item { position: absolute; }
609
+ .dd-media-item img[data-media-cropping] { cursor: grabbing; }
610
+ .dd-media-fit-label {
611
+ position: absolute;
612
+ left: 50%;
613
+ top: 50%;
614
+ z-index: 1;
615
+ transform: translate(-50%, -50%);
616
+ padding: 4px 8px;
617
+ border-radius: 4px;
618
+ background: color-mix(in srgb, var(--dream-app-raised, #141416) 88%, transparent);
619
+ color: var(--dream-app-bright, #f4f4f5);
620
+ font-weight: 500;
621
+ font-size: 12px;
622
+ line-height: 1.4;
623
+ pointer-events: none;
624
+ }
625
+ `;
626
+ function activate(dd) {
627
+ const report = (message) => {
628
+ console.info(`[${dd.plugin.id}] ${message}`);
629
+ };
630
+ const importer = createMediaImporter(dd, report);
631
+ let load = untrack(() => dd.loadVersion());
632
+ dd.on("document", () => {
633
+ const nextLoad = untrack(() => dd.loadVersion());
634
+ if (nextLoad === load) return;
635
+ load = nextLoad;
636
+ importer.cancel();
637
+ });
638
+ const style = document.createElement("style");
639
+ style.dataset["daydreamMediaStyles"] = "";
640
+ style.textContent = mediaStyles;
641
+ document.head.append(style);
642
+ onCleanup(() => {
643
+ importer.dispose();
644
+ style.remove();
645
+ });
646
+ for (const kind of [IMAGE_KIND, VIDEO_KIND]) dd.registerItemKind({
647
+ kind,
648
+ payloadProblem: (raw, where) => mediaPayloadProblem(raw, where, kind === "daydream.image" ? "image" : "video"),
649
+ render: (props) => createComponent(MediaItem, {
650
+ dd,
651
+ get item() {
652
+ return props.item;
653
+ },
654
+ onError: report
655
+ }),
656
+ name: (item) => mediaName(item),
657
+ rename: (item, name) => {
658
+ item.payload.label = name;
659
+ },
660
+ describe: (item) => {
661
+ const { src, label } = item.payload;
662
+ const source = label === void 0 ? src : `${label} — ${src}`;
663
+ return kind === "daydream.image" ? `${source} — ${imageFit(item.payload) === "cover" ? "Fill" : "Fit"}` : source;
664
+ },
665
+ resize: {
666
+ reset: true,
667
+ ...kind === "daydream.image" ? {
668
+ freeform: true,
669
+ handles: "all",
670
+ onResize: (item, context) => {
671
+ if (resizeBreaksAspect(context)) applyDefaultImageFit(item);
672
+ }
673
+ } : {}
674
+ }
675
+ });
676
+ registerTransfers(dd, importer);
677
+ }
678
+ export { activate as default };
package/icon.svg ADDED
@@ -0,0 +1,5 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke-width="1.8" stroke-linecap="square" stroke-linejoin="miter">
2
+ <path d="M3 4h18v16H3V4Z" stroke="#839999"/>
3
+ <path d="m4 17 5-5 5 5h6" stroke="#E7E5DE"/>
4
+ <circle cx="16" cy="9" r="2.2" fill="#F66A2C"/>
5
+ </svg>
package/manifest.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "id": "daydream.media",
3
+ "name": "Media",
4
+ "description": "Images and videos as canvas items, beside your pages.",
5
+ "version": "0.3.0",
6
+ "api": "3.1",
7
+ "contributes": {
8
+ "itemKinds": [
9
+ "daydream.image",
10
+ "daydream.video"
11
+ ],
12
+ "instructions": "Images and videos on the canvas are provided together by Media. Media contributes no panel, command or shortcut. They are daydream.image and daydream.video items with payload {src, label?, fit?, focalPoint?}; src is an https URL, kept as written and loaded from its source, or a file of the project's own, assets/<file>, from the project's root; label is an optional non-empty canvas name. Image fit is contain or cover and defaults to cover for new images; focalPoint is normalized {x,y} from 0 to 1 and defaults to center. Omit frame for intrinsic size; width alone scales proportionally, while width and height use the image fit mode. Change an existing one with update_item and take it out with remove_item. Double-click a two-axis image to toggle Fill/Fit; Option-click-drag repositions cropped Fill content. Images otherwise drag by their body; videos always contain, play with native controls, and move by the title bar. Disabling Media preserves existing items as placeholders. Pasting or dropping a browser image/video, or a direct https media URL, adds an item: only its safe source URL is stored, never the foreign HTML, and it depends on that source staying available. Pasting or dropping an image or video FILE copies it into the project's assets/, named by its bytes (assets/gen-<hash>.<ext>), and adds an item whose src names that copy, project-relative."
13
+ },
14
+ "built": "dist/index.js"
15
+ }
package/package.json CHANGED
@@ -1,6 +1,17 @@
1
1
  {
2
2
  "name": "@daydreamui/media",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.3.0",
4
+ "type": "module",
5
+ "description": "Images and videos as canvas items, beside your pages.",
6
+ "keywords": [
7
+ "daydream-plugin"
8
+ ],
9
+ "publishConfig": {
10
+ "access": "public"
11
+ },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/martinbavio/daydream-plugins.git",
15
+ "directory": "plugins/daydream.media"
16
+ }
17
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.