@twick/studio 0.15.18 → 0.15.19

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.mjs CHANGED
@@ -2,7 +2,7 @@ var __defProp = Object.defineProperty;
2
2
  var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
3
3
  var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
4
4
  import { jsx, jsxs, Fragment } from "react/jsx-runtime";
5
- import { forwardRef, createElement, useState, useEffect, useRef, createContext, useContext, useCallback, useMemo } from "react";
5
+ import { forwardRef, createElement, useState, useEffect, useRef, useCallback, createContext, useContext, useMemo } from "react";
6
6
  import { useTimelineContext, TrackElement, Track, ImageElement, AudioElement, VideoElement, TextElement, RectElement, CircleElement, CAPTION_STYLE, CaptionElement, ElementTextEffect, ElementAnimation, PLAYER_STATE } from "@twick/timeline";
7
7
  import { AudioElement as AudioElement2, CAPTION_COLOR, CAPTION_FONT, CAPTION_STYLE as CAPTION_STYLE2, CAPTION_STYLE_OPTIONS, CaptionElement as CaptionElement2, CircleElement as CircleElement2, ElementAdder, ElementAnimation as ElementAnimation2, ElementCloner, ElementDeserializer, ElementFrameEffect, ElementRemover, ElementSerializer, ElementSplitter, ElementTextEffect as ElementTextEffect2, ElementUpdater, ElementValidator, INITIAL_TIMELINE_DATA, IconElement, ImageElement as ImageElement2, PROCESS_STATE, RectElement as RectElement2, TIMELINE_ACTION, TIMELINE_ELEMENT_TYPE, TextElement as TextElement2, TimelineEditor, TimelineProvider, Track as Track2, TrackElement as TrackElement2, VideoElement as VideoElement2, WORDS_PER_PHRASE, generateShortUuid, getCurrentElements, getTotalDuration, isElementId, isTrackId, useTimelineContext as useTimelineContext2 } from "@twick/timeline";
8
8
  import VideoEditor, { useEditorManager, BrowserMediaManager, TIMELINE_DROP_MEDIA_TYPE, AVAILABLE_TEXT_FONTS, TEXT_EFFECTS, ANIMATIONS } from "@twick/video-editor";
@@ -752,6 +752,182 @@ const useStudioManager = () => {
752
752
  updateElement
753
753
  };
754
754
  };
755
+ const putFileWithProgress = (uploadUrl, file, onProgress) => {
756
+ return new Promise((resolve, reject) => {
757
+ const xhr = new XMLHttpRequest();
758
+ xhr.upload.addEventListener("progress", (e) => {
759
+ if (e.lengthComputable) {
760
+ onProgress(e.loaded / e.total * 100);
761
+ }
762
+ });
763
+ xhr.addEventListener("load", () => {
764
+ if (xhr.status >= 200 && xhr.status < 300) {
765
+ onProgress(100);
766
+ resolve();
767
+ } else {
768
+ reject(new Error(`Upload failed: ${xhr.status} ${xhr.statusText}`));
769
+ }
770
+ });
771
+ xhr.addEventListener("error", () => reject(new Error("Upload failed")));
772
+ xhr.addEventListener("abort", () => reject(new Error("Upload aborted")));
773
+ xhr.open("PUT", uploadUrl);
774
+ xhr.setRequestHeader("Content-Type", file.type || "application/octet-stream");
775
+ xhr.send(file);
776
+ });
777
+ };
778
+ const useCloudMediaUpload = (config) => {
779
+ const { uploadApiUrl, provider } = config;
780
+ const [isUploading, setIsUploading] = useState(false);
781
+ const [progress, setProgress] = useState(0);
782
+ const [error, setError] = useState(null);
783
+ const resetError = useCallback(() => {
784
+ setError(null);
785
+ }, []);
786
+ const uploadFile = useCallback(
787
+ async (file) => {
788
+ setIsUploading(true);
789
+ setProgress(0);
790
+ setError(null);
791
+ try {
792
+ if (provider === "s3") {
793
+ const presignRes = await fetch(uploadApiUrl, {
794
+ method: "POST",
795
+ headers: { "Content-Type": "application/json" },
796
+ body: JSON.stringify({
797
+ filename: file.name,
798
+ contentType: file.type || "application/octet-stream"
799
+ })
800
+ });
801
+ if (!presignRes.ok) {
802
+ const errBody = await presignRes.json().catch(() => ({}));
803
+ throw new Error(
804
+ errBody.error ?? `Failed to get upload URL: ${presignRes.statusText}`
805
+ );
806
+ }
807
+ const presignData = await presignRes.json();
808
+ const uploadUrl = presignData.uploadUrl;
809
+ await putFileWithProgress(uploadUrl, file, setProgress);
810
+ const publicUrl = uploadUrl.split("?")[0];
811
+ return { url: publicUrl };
812
+ }
813
+ if (provider === "gcs") {
814
+ setProgress(10);
815
+ const formData = new FormData();
816
+ formData.append("file", file);
817
+ const uploadRes = await fetch(uploadApiUrl, {
818
+ method: "POST",
819
+ body: formData
820
+ });
821
+ if (!uploadRes.ok) {
822
+ const errBody = await uploadRes.json().catch(() => ({}));
823
+ throw new Error(
824
+ errBody.error ?? `Upload failed: ${uploadRes.statusText}`
825
+ );
826
+ }
827
+ setProgress(100);
828
+ const data = await uploadRes.json();
829
+ if (!data.url) {
830
+ throw new Error("Upload response missing url");
831
+ }
832
+ return { url: data.url };
833
+ }
834
+ throw new Error(`Unknown provider: ${provider}`);
835
+ } catch (err) {
836
+ const message = err instanceof Error ? err.message : "Upload failed";
837
+ setError(message);
838
+ throw err;
839
+ } finally {
840
+ setIsUploading(false);
841
+ setProgress(0);
842
+ }
843
+ },
844
+ [uploadApiUrl, provider]
845
+ );
846
+ return {
847
+ uploadFile,
848
+ isUploading,
849
+ progress,
850
+ error,
851
+ resetError
852
+ };
853
+ };
854
+ const CloudMediaUpload = ({
855
+ onSuccess,
856
+ onError,
857
+ accept,
858
+ uploadApiUrl,
859
+ provider,
860
+ buttonText = "Upload to cloud",
861
+ className,
862
+ disabled = false,
863
+ id: providedId,
864
+ icon
865
+ }) => {
866
+ const id = providedId ?? `cloud-media-upload-${Math.random().toString(36).slice(2, 9)}`;
867
+ const inputRef = useRef(null);
868
+ const {
869
+ uploadFile,
870
+ isUploading,
871
+ progress,
872
+ error,
873
+ resetError
874
+ } = useCloudMediaUpload({ uploadApiUrl, provider });
875
+ const handleFileChange = async (e) => {
876
+ var _a;
877
+ const file = (_a = e.target.files) == null ? void 0 : _a[0];
878
+ if (!file) return;
879
+ try {
880
+ const { url } = await uploadFile(file);
881
+ onSuccess(url, file);
882
+ if (inputRef.current) {
883
+ inputRef.current.value = "";
884
+ }
885
+ } catch (err) {
886
+ const message = err instanceof Error ? err.message : "Upload failed";
887
+ onError == null ? void 0 : onError(message);
888
+ }
889
+ };
890
+ const handleLabelClick = () => {
891
+ if (disabled || isUploading) return;
892
+ resetError();
893
+ };
894
+ return /* @__PURE__ */ jsxs("div", { className: "file-input-container cloud-media-upload-container", children: [
895
+ /* @__PURE__ */ jsx(
896
+ "input",
897
+ {
898
+ ref: inputRef,
899
+ type: "file",
900
+ accept,
901
+ className: "file-input-hidden",
902
+ id,
903
+ onChange: handleFileChange,
904
+ disabled: disabled || isUploading,
905
+ "aria-label": buttonText
906
+ }
907
+ ),
908
+ /* @__PURE__ */ jsxs(
909
+ "label",
910
+ {
911
+ htmlFor: id,
912
+ className: className ?? "btn-primary file-input-label",
913
+ onClick: handleLabelClick,
914
+ style: { pointerEvents: disabled || isUploading ? "none" : void 0 },
915
+ children: [
916
+ icon ?? /* @__PURE__ */ jsx(Upload, { className: "icon-sm" }),
917
+ isUploading ? `${Math.round(progress)}%` : buttonText
918
+ ]
919
+ }
920
+ ),
921
+ isUploading && /* @__PURE__ */ jsx("div", { className: "cloud-media-upload-progress", role: "progressbar", "aria-valuenow": progress, "aria-valuemin": 0, "aria-valuemax": 100, children: /* @__PURE__ */ jsx(
922
+ "div",
923
+ {
924
+ className: "cloud-media-upload-progress-fill",
925
+ style: { width: `${progress}%` }
926
+ }
927
+ ) }),
928
+ error && /* @__PURE__ */ jsx("div", { className: "cloud-media-upload-error", role: "alert", children: error })
929
+ ] });
930
+ };
755
931
  const _MediaManagerSingleton = class _MediaManagerSingleton {
756
932
  constructor() {
757
933
  }
@@ -1309,19 +1485,42 @@ const AudioPanelContainer = (props) => {
1309
1485
  });
1310
1486
  addItem(newItem);
1311
1487
  };
1312
- return /* @__PURE__ */ jsx(
1313
- AudioPanel,
1314
- {
1315
- items,
1316
- searchQuery,
1317
- onSearchChange: setSearchQuery,
1318
- onItemSelect: handleSelection,
1319
- onFileUpload: handleFileUpload,
1320
- isLoading,
1321
- acceptFileTypes,
1322
- onUrlAdd
1323
- }
1324
- );
1488
+ const onCloudUploadSuccess = async (url, file) => {
1489
+ var _a;
1490
+ const newItem = await mediaManager.addItem({
1491
+ name: file.name,
1492
+ url,
1493
+ type: "audio",
1494
+ metadata: { source: ((_a = props.uploadConfig) == null ? void 0 : _a.provider) ?? "s3" }
1495
+ });
1496
+ addItem(newItem);
1497
+ };
1498
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1499
+ props.uploadConfig && /* @__PURE__ */ jsx("div", { className: "flex panel-section", children: /* @__PURE__ */ jsx(
1500
+ CloudMediaUpload,
1501
+ {
1502
+ uploadApiUrl: props.uploadConfig.uploadApiUrl,
1503
+ provider: props.uploadConfig.provider,
1504
+ accept: "audio/*",
1505
+ onSuccess: onCloudUploadSuccess,
1506
+ buttonText: "Upload audio",
1507
+ className: "btn-ghost w-full"
1508
+ }
1509
+ ) }),
1510
+ /* @__PURE__ */ jsx(
1511
+ AudioPanel,
1512
+ {
1513
+ items,
1514
+ searchQuery,
1515
+ onSearchChange: setSearchQuery,
1516
+ onItemSelect: handleSelection,
1517
+ onFileUpload: handleFileUpload,
1518
+ isLoading,
1519
+ acceptFileTypes,
1520
+ onUrlAdd
1521
+ }
1522
+ )
1523
+ ] });
1325
1524
  };
1326
1525
  function ImagePanel({
1327
1526
  items,
@@ -1407,19 +1606,42 @@ function ImagePanelContainer(props) {
1407
1606
  });
1408
1607
  addItem(newItem);
1409
1608
  };
1410
- return /* @__PURE__ */ jsx(
1411
- ImagePanel,
1412
- {
1413
- items,
1414
- searchQuery,
1415
- onSearchChange: setSearchQuery,
1416
- onItemSelect: handleSelection,
1417
- onFileUpload: handleFileUpload,
1418
- isLoading,
1419
- acceptFileTypes,
1420
- onUrlAdd
1421
- }
1422
- );
1609
+ const onCloudUploadSuccess = async (url, file) => {
1610
+ var _a;
1611
+ const newItem = await mediaManager.addItem({
1612
+ name: file.name,
1613
+ url,
1614
+ type: "image",
1615
+ metadata: { source: ((_a = props.uploadConfig) == null ? void 0 : _a.provider) ?? "s3" }
1616
+ });
1617
+ addItem(newItem);
1618
+ };
1619
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1620
+ props.uploadConfig && /* @__PURE__ */ jsx("div", { className: "flex panel-section", children: /* @__PURE__ */ jsx(
1621
+ CloudMediaUpload,
1622
+ {
1623
+ uploadApiUrl: props.uploadConfig.uploadApiUrl,
1624
+ provider: props.uploadConfig.provider,
1625
+ accept: "image/*",
1626
+ onSuccess: onCloudUploadSuccess,
1627
+ buttonText: "Upload image",
1628
+ className: "btn-ghost w-full"
1629
+ }
1630
+ ) }),
1631
+ /* @__PURE__ */ jsx(
1632
+ ImagePanel,
1633
+ {
1634
+ items,
1635
+ searchQuery,
1636
+ onSearchChange: setSearchQuery,
1637
+ onItemSelect: handleSelection,
1638
+ onFileUpload: handleFileUpload,
1639
+ isLoading,
1640
+ acceptFileTypes,
1641
+ onUrlAdd
1642
+ }
1643
+ )
1644
+ ] });
1423
1645
  }
1424
1646
  const useVideoPreview = () => {
1425
1647
  const [playingVideo, setPlayingVideo] = useState(null);
@@ -1573,17 +1795,40 @@ function VideoPanelContainer(props) {
1573
1795
  });
1574
1796
  addItem(newItem);
1575
1797
  };
1576
- return /* @__PURE__ */ jsx(
1577
- VideoPanel,
1578
- {
1579
- items,
1580
- onItemSelect: handleSelection,
1581
- onFileUpload: handleFileUpload,
1582
- isLoading,
1583
- acceptFileTypes,
1584
- onUrlAdd
1585
- }
1586
- );
1798
+ const onCloudUploadSuccess = async (url, file) => {
1799
+ var _a;
1800
+ const newItem = await mediaManager.addItem({
1801
+ name: file.name,
1802
+ url,
1803
+ type: "video",
1804
+ metadata: { source: ((_a = props.uploadConfig) == null ? void 0 : _a.provider) ?? "s3" }
1805
+ });
1806
+ addItem(newItem);
1807
+ };
1808
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1809
+ props.uploadConfig && /* @__PURE__ */ jsx("div", { className: "flex panel-section", children: /* @__PURE__ */ jsx(
1810
+ CloudMediaUpload,
1811
+ {
1812
+ uploadApiUrl: props.uploadConfig.uploadApiUrl,
1813
+ provider: props.uploadConfig.provider,
1814
+ accept: "video/*",
1815
+ onSuccess: onCloudUploadSuccess,
1816
+ buttonText: "Upload video",
1817
+ className: "btn-ghost w-full"
1818
+ }
1819
+ ) }),
1820
+ /* @__PURE__ */ jsx(
1821
+ VideoPanel,
1822
+ {
1823
+ items,
1824
+ onItemSelect: handleSelection,
1825
+ onFileUpload: handleFileUpload,
1826
+ isLoading,
1827
+ acceptFileTypes,
1828
+ onUrlAdd
1829
+ }
1830
+ )
1831
+ ] });
1587
1832
  }
1588
1833
  function TextPanel({
1589
1834
  textContent,
@@ -2678,7 +2923,8 @@ const ElementPanelContainer = ({
2678
2923
  videoResolution,
2679
2924
  selectedElement,
2680
2925
  addElement,
2681
- updateElement
2926
+ updateElement,
2927
+ uploadConfig
2682
2928
  }) => {
2683
2929
  const addNewElement = async (element) => {
2684
2930
  await addElement(element);
@@ -2692,7 +2938,8 @@ const ElementPanelContainer = ({
2692
2938
  videoResolution,
2693
2939
  selectedElement,
2694
2940
  addElement: addNewElement,
2695
- updateElement
2941
+ updateElement,
2942
+ uploadConfig
2696
2943
  }
2697
2944
  );
2698
2945
  case "audio":
@@ -2702,7 +2949,8 @@ const ElementPanelContainer = ({
2702
2949
  videoResolution,
2703
2950
  selectedElement,
2704
2951
  addElement: addNewElement,
2705
- updateElement
2952
+ updateElement,
2953
+ uploadConfig
2706
2954
  }
2707
2955
  );
2708
2956
  case "video":
@@ -2712,7 +2960,8 @@ const ElementPanelContainer = ({
2712
2960
  videoResolution,
2713
2961
  selectedElement,
2714
2962
  addElement: addNewElement,
2715
- updateElement
2963
+ updateElement,
2964
+ uploadConfig
2716
2965
  }
2717
2966
  );
2718
2967
  case "text":
@@ -3909,7 +4158,8 @@ function TwickStudio({ studioConfig }) {
3909
4158
  setSelectedTool,
3910
4159
  selectedElement,
3911
4160
  addElement,
3912
- updateElement
4161
+ updateElement,
4162
+ uploadConfig: twickStudiConfig.uploadConfig
3913
4163
  }
3914
4164
  ) }),
3915
4165
  /* @__PURE__ */ jsx("main", { className: "main-container", children: /* @__PURE__ */ jsx("div", { className: "canvas-wrapper", children: /* @__PURE__ */ jsx(
@@ -3994,6 +4244,7 @@ export {
3994
4244
  CaptionsPanel,
3995
4245
  CircleElement2 as CircleElement,
3996
4246
  CirclePanel,
4247
+ CloudMediaUpload,
3997
4248
  ElementAdder,
3998
4249
  ElementAnimation2 as ElementAnimation,
3999
4250
  ElementCloner,
@@ -4044,6 +4295,7 @@ export {
4044
4295
  isElementId,
4045
4296
  isTrackId,
4046
4297
  setElementColors,
4298
+ useCloudMediaUpload,
4047
4299
  useEditorManager2 as useEditorManager,
4048
4300
  useGenerateCaptions,
4049
4301
  useLivePlayerContext2 as useLivePlayerContext,