@twick/studio 0.14.6 → 0.14.7
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/README.md +73 -6
- package/dist/components/panel/audio-panel.d.ts +1 -1
- package/dist/components/panel/image-panel.d.ts +1 -1
- package/dist/components/panel/video-panel.d.ts +1 -1
- package/dist/components/shared/index.d.ts +1 -0
- package/dist/components/shared/url-input.d.ts +6 -0
- package/dist/index.js +173 -62
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +173 -62
- package/dist/index.mjs.map +1 -1
- package/dist/studio.css +16 -0
- package/dist/types/media-panel.d.ts +11 -6
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -34,12 +34,32 @@ export default function App() {
|
|
|
34
34
|
initialData={INITIAL_TIMELINE_DATA}
|
|
35
35
|
contextId={"studio-demo"}
|
|
36
36
|
>
|
|
37
|
-
<TwickStudio
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
37
|
+
<TwickStudio
|
|
38
|
+
studioConfig={{
|
|
39
|
+
videoProps: {
|
|
40
|
+
width: 720,
|
|
41
|
+
height: 1280,
|
|
42
|
+
},
|
|
43
|
+
// Optional: Customize timeline tick marks
|
|
44
|
+
timelineTickConfigs: [
|
|
45
|
+
{ durationThreshold: 30, majorInterval: 5, minorTicks: 5 },
|
|
46
|
+
{ durationThreshold: 300, majorInterval: 30, minorTicks: 6 }
|
|
47
|
+
],
|
|
48
|
+
// Optional: Customize zoom behavior
|
|
49
|
+
timelineZoomConfig: {
|
|
50
|
+
min: 0.5, max: 2.0, step: 0.25, default: 1.0
|
|
51
|
+
},
|
|
52
|
+
// Optional: Customize element colors
|
|
53
|
+
elementColors: {
|
|
54
|
+
video: "#8B5FBF",
|
|
55
|
+
audio: "#3D8B8B",
|
|
56
|
+
image: "#D4956C",
|
|
57
|
+
text: "#A78EC8",
|
|
58
|
+
caption: "#9B8ACE",
|
|
59
|
+
fragment: "#1A1A1A"
|
|
60
|
+
}
|
|
61
|
+
}}
|
|
62
|
+
/>
|
|
43
63
|
</TimelineProvider>
|
|
44
64
|
</LivePlayerProvider>
|
|
45
65
|
);
|
|
@@ -59,6 +79,24 @@ The main studio component that provides a complete video editing interface.
|
|
|
59
79
|
width: 1920,
|
|
60
80
|
height: 1080
|
|
61
81
|
},
|
|
82
|
+
// Optional: Customize timeline tick marks
|
|
83
|
+
timelineTickConfigs: [
|
|
84
|
+
{ durationThreshold: 30, majorInterval: 5, minorTicks: 5 },
|
|
85
|
+
{ durationThreshold: 300, majorInterval: 30, minorTicks: 6 }
|
|
86
|
+
],
|
|
87
|
+
// Optional: Customize zoom behavior
|
|
88
|
+
timelineZoomConfig: {
|
|
89
|
+
min: 0.5, max: 2.0, step: 0.25, default: 1.0
|
|
90
|
+
},
|
|
91
|
+
// Optional: Customize element colors
|
|
92
|
+
elementColors: {
|
|
93
|
+
video: "#8B5FBF",
|
|
94
|
+
audio: "#3D8B8B",
|
|
95
|
+
image: "#D4956C",
|
|
96
|
+
text: "#A78EC8",
|
|
97
|
+
caption: "#9B8ACE",
|
|
98
|
+
fragment: "#1A1A1A"
|
|
99
|
+
},
|
|
62
100
|
saveProject: async (project, fileName) => {
|
|
63
101
|
// Custom save logic
|
|
64
102
|
return { status: true, message: "Project saved" };
|
|
@@ -85,10 +123,39 @@ interface StudioConfig {
|
|
|
85
123
|
width: number;
|
|
86
124
|
height: number;
|
|
87
125
|
};
|
|
126
|
+
// Timeline tick configuration
|
|
127
|
+
timelineTickConfigs?: TimelineTickConfig[];
|
|
128
|
+
// Zoom configuration
|
|
129
|
+
timelineZoomConfig?: TimelineZoomConfig;
|
|
130
|
+
// Element colors
|
|
131
|
+
elementColors?: ElementColors;
|
|
132
|
+
// Project management callbacks
|
|
88
133
|
saveProject?: (project: ProjectJSON, fileName: string) => Promise<Result>;
|
|
89
134
|
loadProject?: () => Promise<ProjectJSON>;
|
|
90
135
|
exportVideo?: (project: ProjectJSON, videoSettings: VideoSettings) => Promise<Result>;
|
|
91
136
|
}
|
|
137
|
+
|
|
138
|
+
interface TimelineTickConfig {
|
|
139
|
+
durationThreshold: number; // Applies when duration < threshold
|
|
140
|
+
majorInterval: number; // Major tick interval in seconds
|
|
141
|
+
minorTicks: number; // Number of minor ticks between majors
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
interface TimelineZoomConfig {
|
|
145
|
+
min: number; // Minimum zoom level
|
|
146
|
+
max: number; // Maximum zoom level
|
|
147
|
+
step: number; // Zoom step increment/decrement
|
|
148
|
+
default: number; // Default zoom level
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
interface ElementColors {
|
|
152
|
+
video: string;
|
|
153
|
+
audio: string;
|
|
154
|
+
image: string;
|
|
155
|
+
text: string;
|
|
156
|
+
caption: string;
|
|
157
|
+
fragment: string;
|
|
158
|
+
}
|
|
92
159
|
```
|
|
93
160
|
|
|
94
161
|
### Individual Panels
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { AudioPanelProps } from '../../types/media-panel';
|
|
2
2
|
|
|
3
|
-
export declare const AudioPanel: ({ items,
|
|
3
|
+
export declare const AudioPanel: ({ items, onItemSelect, onFileUpload, acceptFileTypes, onUrlAdd, }: AudioPanelProps) => import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { ImagePanelProps } from '../../types/media-panel';
|
|
2
2
|
|
|
3
|
-
export declare function ImagePanel({ items,
|
|
3
|
+
export declare function ImagePanel({ items, onItemSelect, onFileUpload, acceptFileTypes, onUrlAdd, }: ImagePanelProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
import { VideoPanelProps } from '../../types/media-panel';
|
|
2
2
|
|
|
3
|
-
export declare function VideoPanel({ items,
|
|
3
|
+
export declare function VideoPanel({ items, onItemSelect, onFileUpload, acceptFileTypes, onUrlAdd, }: VideoPanelProps): import("react/jsx-runtime").JSX.Element;
|
package/dist/index.js
CHANGED
|
@@ -797,6 +797,83 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
797
797
|
__publicField(_MediaManagerSingleton, "instance", null);
|
|
798
798
|
let MediaManagerSingleton = _MediaManagerSingleton;
|
|
799
799
|
const getMediaManager = () => MediaManagerSingleton.getInstance();
|
|
800
|
+
const EXTENSIONS = {
|
|
801
|
+
video: ["mp4", "webm", "ogg", "mov", "mkv", "m3u8"],
|
|
802
|
+
audio: ["mp3", "wav", "ogg", "m4a", "aac", "flac"],
|
|
803
|
+
image: ["jpg", "jpeg", "png", "gif", "webp", "svg"]
|
|
804
|
+
};
|
|
805
|
+
function isValidUrl(url) {
|
|
806
|
+
try {
|
|
807
|
+
new URL(url);
|
|
808
|
+
return true;
|
|
809
|
+
} catch {
|
|
810
|
+
return false;
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
function matchesType(url, type) {
|
|
814
|
+
const pathname = (() => {
|
|
815
|
+
try {
|
|
816
|
+
return new URL(url).pathname.toLowerCase();
|
|
817
|
+
} catch {
|
|
818
|
+
return url.toLowerCase();
|
|
819
|
+
}
|
|
820
|
+
})();
|
|
821
|
+
const ext = pathname.split(".").pop() || "";
|
|
822
|
+
return EXTENSIONS[type].includes(ext);
|
|
823
|
+
}
|
|
824
|
+
function UrlInput({
|
|
825
|
+
type,
|
|
826
|
+
onSubmit
|
|
827
|
+
}) {
|
|
828
|
+
const [url, setUrl] = react.useState("");
|
|
829
|
+
const [error, setError] = react.useState("");
|
|
830
|
+
const tryAdd = async () => {
|
|
831
|
+
const trimmed = url.trim();
|
|
832
|
+
if (!trimmed) return;
|
|
833
|
+
if (!isValidUrl(trimmed)) {
|
|
834
|
+
setError("Enter a valid URL");
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
if (!matchesType(trimmed, type)) {
|
|
838
|
+
setError(`URL must be a ${type} (${EXTENSIONS[type].join(", ")})`);
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
setError("");
|
|
842
|
+
onSubmit(trimmed);
|
|
843
|
+
setUrl("");
|
|
844
|
+
};
|
|
845
|
+
const onKeyDown = (e) => {
|
|
846
|
+
if (e.key === "Enter") {
|
|
847
|
+
e.preventDefault();
|
|
848
|
+
void tryAdd();
|
|
849
|
+
}
|
|
850
|
+
};
|
|
851
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
852
|
+
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-container", children: [
|
|
853
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
854
|
+
"input",
|
|
855
|
+
{
|
|
856
|
+
type: "url",
|
|
857
|
+
placeholder: `Paste ${type} URL...`,
|
|
858
|
+
value: url,
|
|
859
|
+
onChange: (e) => setUrl(e.target.value),
|
|
860
|
+
onKeyDown,
|
|
861
|
+
className: "input w-full"
|
|
862
|
+
}
|
|
863
|
+
),
|
|
864
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
865
|
+
"button",
|
|
866
|
+
{
|
|
867
|
+
className: "btn-ghost",
|
|
868
|
+
onClick: () => void tryAdd(),
|
|
869
|
+
"aria-label": `Add ${type} by URL`,
|
|
870
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(Plus, { size: 16 })
|
|
871
|
+
}
|
|
872
|
+
)
|
|
873
|
+
] }),
|
|
874
|
+
error ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-error", children: error }) : null
|
|
875
|
+
] });
|
|
876
|
+
}
|
|
800
877
|
const initialMediaState = {
|
|
801
878
|
items: [],
|
|
802
879
|
searchQuery: "",
|
|
@@ -964,24 +1041,6 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
964
1041
|
acceptFileTypes: config.acceptFileTypes
|
|
965
1042
|
};
|
|
966
1043
|
};
|
|
967
|
-
const SearchInput = ({
|
|
968
|
-
searchQuery,
|
|
969
|
-
setSearchQuery
|
|
970
|
-
}) => {
|
|
971
|
-
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "search-container", children: [
|
|
972
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
973
|
-
"input",
|
|
974
|
-
{
|
|
975
|
-
type: "text",
|
|
976
|
-
placeholder: "Search media...",
|
|
977
|
-
value: searchQuery,
|
|
978
|
-
onChange: (e) => setSearchQuery(e.target.value),
|
|
979
|
-
className: "input search-input w-full"
|
|
980
|
-
}
|
|
981
|
-
),
|
|
982
|
-
/* @__PURE__ */ jsxRuntime.jsx(Search, { className: "search-icon" })
|
|
983
|
-
] });
|
|
984
|
-
};
|
|
985
1044
|
const useAudioPreview = () => {
|
|
986
1045
|
const [playingAudio, setPlayingAudio] = react.useState(null);
|
|
987
1046
|
const audioRef = react.useRef(null);
|
|
@@ -1021,22 +1080,15 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
1021
1080
|
};
|
|
1022
1081
|
const AudioPanel = ({
|
|
1023
1082
|
items,
|
|
1024
|
-
searchQuery,
|
|
1025
|
-
onSearchChange,
|
|
1026
1083
|
onItemSelect,
|
|
1027
1084
|
onFileUpload,
|
|
1028
|
-
acceptFileTypes
|
|
1085
|
+
acceptFileTypes,
|
|
1086
|
+
onUrlAdd
|
|
1029
1087
|
}) => {
|
|
1030
1088
|
const { playingAudio, togglePlayPause } = useAudioPreview();
|
|
1031
1089
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "panel-container", children: [
|
|
1032
1090
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "panel-title", children: "Audio Library" }),
|
|
1033
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "
|
|
1034
|
-
SearchInput,
|
|
1035
|
-
{
|
|
1036
|
-
searchQuery,
|
|
1037
|
-
setSearchQuery: onSearchChange
|
|
1038
|
-
}
|
|
1039
|
-
) }),
|
|
1091
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "panel-section", children: /* @__PURE__ */ jsxRuntime.jsx(UrlInput, { type: "audio", onSubmit: onUrlAdd }) }),
|
|
1040
1092
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex panel-section", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1041
1093
|
FileInput,
|
|
1042
1094
|
{
|
|
@@ -1088,13 +1140,14 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
1088
1140
|
}) }),
|
|
1089
1141
|
items.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "empty-state", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "empty-state-content", children: [
|
|
1090
1142
|
/* @__PURE__ */ jsxRuntime.jsx(WandSparkles, { className: "empty-state-icon" }),
|
|
1091
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "empty-state-text", children: "No audio files found" })
|
|
1092
|
-
searchQuery && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "empty-state-subtext", children: "Try adjusting your search" })
|
|
1143
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "empty-state-text", children: "No audio files found" })
|
|
1093
1144
|
] }) })
|
|
1094
1145
|
] })
|
|
1095
1146
|
] });
|
|
1096
1147
|
};
|
|
1097
1148
|
const AudioPanelContainer = (props) => {
|
|
1149
|
+
const { addItem } = useMedia("audio");
|
|
1150
|
+
const mediaManager = getMediaManager();
|
|
1098
1151
|
const {
|
|
1099
1152
|
items,
|
|
1100
1153
|
searchQuery,
|
|
@@ -1112,6 +1165,24 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
1112
1165
|
},
|
|
1113
1166
|
props.videoResolution
|
|
1114
1167
|
);
|
|
1168
|
+
const onUrlAdd = async (url) => {
|
|
1169
|
+
const nameFromUrl = (() => {
|
|
1170
|
+
try {
|
|
1171
|
+
const u = new URL(url);
|
|
1172
|
+
const parts = u.pathname.split("/").filter(Boolean);
|
|
1173
|
+
return decodeURIComponent(parts[parts.length - 1] || url);
|
|
1174
|
+
} catch {
|
|
1175
|
+
return url;
|
|
1176
|
+
}
|
|
1177
|
+
})();
|
|
1178
|
+
const newItem = await mediaManager.addItem({
|
|
1179
|
+
name: nameFromUrl,
|
|
1180
|
+
url,
|
|
1181
|
+
type: "audio",
|
|
1182
|
+
metadata: { source: "url" }
|
|
1183
|
+
});
|
|
1184
|
+
addItem(newItem);
|
|
1185
|
+
};
|
|
1115
1186
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1116
1187
|
AudioPanel,
|
|
1117
1188
|
{
|
|
@@ -1121,27 +1192,21 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
1121
1192
|
onItemSelect: handleSelection,
|
|
1122
1193
|
onFileUpload: handleFileUpload,
|
|
1123
1194
|
isLoading,
|
|
1124
|
-
acceptFileTypes
|
|
1195
|
+
acceptFileTypes,
|
|
1196
|
+
onUrlAdd
|
|
1125
1197
|
}
|
|
1126
1198
|
);
|
|
1127
1199
|
};
|
|
1128
1200
|
function ImagePanel({
|
|
1129
1201
|
items,
|
|
1130
|
-
searchQuery,
|
|
1131
|
-
onSearchChange,
|
|
1132
1202
|
onItemSelect,
|
|
1133
1203
|
onFileUpload,
|
|
1134
|
-
acceptFileTypes
|
|
1204
|
+
acceptFileTypes,
|
|
1205
|
+
onUrlAdd
|
|
1135
1206
|
}) {
|
|
1136
1207
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "panel-container", children: [
|
|
1137
1208
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "panel-title", children: "Image Library" }),
|
|
1138
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "
|
|
1139
|
-
SearchInput,
|
|
1140
|
-
{
|
|
1141
|
-
searchQuery,
|
|
1142
|
-
setSearchQuery: onSearchChange
|
|
1143
|
-
}
|
|
1144
|
-
) }),
|
|
1209
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "panel-section", children: /* @__PURE__ */ jsxRuntime.jsx(UrlInput, { type: "image", onSubmit: onUrlAdd }) }),
|
|
1145
1210
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex panel-section", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1146
1211
|
FileInput,
|
|
1147
1212
|
{
|
|
@@ -1178,13 +1243,14 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
1178
1243
|
)) }),
|
|
1179
1244
|
items.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "empty-state", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "empty-state-content", children: [
|
|
1180
1245
|
/* @__PURE__ */ jsxRuntime.jsx(WandSparkles, { className: "empty-state-icon" }),
|
|
1181
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "empty-state-text", children: "No images found" })
|
|
1182
|
-
searchQuery && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "empty-state-subtext", children: "Try adjusting your search" })
|
|
1246
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "empty-state-text", children: "No images found" })
|
|
1183
1247
|
] }) })
|
|
1184
1248
|
] })
|
|
1185
1249
|
] });
|
|
1186
1250
|
}
|
|
1187
1251
|
function ImagePanelContainer(props) {
|
|
1252
|
+
const { addItem } = useMedia("image");
|
|
1253
|
+
const mediaManager = getMediaManager();
|
|
1188
1254
|
const {
|
|
1189
1255
|
items,
|
|
1190
1256
|
searchQuery,
|
|
@@ -1202,6 +1268,24 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
1202
1268
|
},
|
|
1203
1269
|
props.videoResolution
|
|
1204
1270
|
);
|
|
1271
|
+
const onUrlAdd = async (url) => {
|
|
1272
|
+
const nameFromUrl = (() => {
|
|
1273
|
+
try {
|
|
1274
|
+
const u = new URL(url);
|
|
1275
|
+
const parts = u.pathname.split("/").filter(Boolean);
|
|
1276
|
+
return decodeURIComponent(parts[parts.length - 1] || url);
|
|
1277
|
+
} catch {
|
|
1278
|
+
return url;
|
|
1279
|
+
}
|
|
1280
|
+
})();
|
|
1281
|
+
const newItem = await mediaManager.addItem({
|
|
1282
|
+
name: nameFromUrl,
|
|
1283
|
+
url,
|
|
1284
|
+
type: "image",
|
|
1285
|
+
metadata: { source: "url" }
|
|
1286
|
+
});
|
|
1287
|
+
addItem(newItem);
|
|
1288
|
+
};
|
|
1205
1289
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1206
1290
|
ImagePanel,
|
|
1207
1291
|
{
|
|
@@ -1211,7 +1295,8 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
1211
1295
|
onItemSelect: handleSelection,
|
|
1212
1296
|
onFileUpload: handleFileUpload,
|
|
1213
1297
|
isLoading,
|
|
1214
|
-
acceptFileTypes
|
|
1298
|
+
acceptFileTypes,
|
|
1299
|
+
onUrlAdd
|
|
1215
1300
|
}
|
|
1216
1301
|
);
|
|
1217
1302
|
}
|
|
@@ -1255,22 +1340,15 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
1255
1340
|
};
|
|
1256
1341
|
function VideoPanel({
|
|
1257
1342
|
items,
|
|
1258
|
-
searchQuery,
|
|
1259
|
-
onSearchChange,
|
|
1260
1343
|
onItemSelect,
|
|
1261
1344
|
onFileUpload,
|
|
1262
|
-
acceptFileTypes
|
|
1345
|
+
acceptFileTypes,
|
|
1346
|
+
onUrlAdd
|
|
1263
1347
|
}) {
|
|
1264
1348
|
const { playingVideo, togglePlayPause } = useVideoPreview();
|
|
1265
1349
|
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "panel-container", children: [
|
|
1266
1350
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "panel-title", children: "Video Library" }),
|
|
1267
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex panel-section", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1268
|
-
SearchInput,
|
|
1269
|
-
{
|
|
1270
|
-
searchQuery,
|
|
1271
|
-
setSearchQuery: onSearchChange
|
|
1272
|
-
}
|
|
1273
|
-
) }),
|
|
1351
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex panel-section", children: /* @__PURE__ */ jsxRuntime.jsx(UrlInput, { type: "video", onSubmit: onUrlAdd }) }),
|
|
1274
1352
|
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex panel-section", children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1275
1353
|
FileInput,
|
|
1276
1354
|
{
|
|
@@ -1304,7 +1382,6 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
1304
1382
|
}
|
|
1305
1383
|
}
|
|
1306
1384
|
),
|
|
1307
|
-
/* @__PURE__ */ jsxRuntime.jsx("div", { className: "media-duration", children: "0:13" }),
|
|
1308
1385
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: "media-actions", children: [
|
|
1309
1386
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1310
1387
|
"button",
|
|
@@ -1339,17 +1416,16 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
1339
1416
|
)) }),
|
|
1340
1417
|
items.length === 0 && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "empty-state", children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "empty-state-content", children: [
|
|
1341
1418
|
/* @__PURE__ */ jsxRuntime.jsx(WandSparkles, { className: "empty-state-icon" }),
|
|
1342
|
-
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "empty-state-text", children: "No videos found" })
|
|
1343
|
-
searchQuery && /* @__PURE__ */ jsxRuntime.jsx("p", { className: "empty-state-subtext", children: "Try adjusting your search" })
|
|
1419
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "empty-state-text", children: "No videos found" })
|
|
1344
1420
|
] }) })
|
|
1345
1421
|
] })
|
|
1346
1422
|
] });
|
|
1347
1423
|
}
|
|
1348
1424
|
function VideoPanelContainer(props) {
|
|
1425
|
+
const { addItem } = useMedia("video");
|
|
1426
|
+
const mediaManager = getMediaManager();
|
|
1349
1427
|
const {
|
|
1350
1428
|
items,
|
|
1351
|
-
searchQuery,
|
|
1352
|
-
setSearchQuery,
|
|
1353
1429
|
handleSelection,
|
|
1354
1430
|
handleFileUpload,
|
|
1355
1431
|
isLoading,
|
|
@@ -1363,16 +1439,33 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
1363
1439
|
},
|
|
1364
1440
|
props.videoResolution
|
|
1365
1441
|
);
|
|
1442
|
+
const onUrlAdd = async (url) => {
|
|
1443
|
+
const nameFromUrl = (() => {
|
|
1444
|
+
try {
|
|
1445
|
+
const u = new URL(url);
|
|
1446
|
+
const parts = u.pathname.split("/").filter(Boolean);
|
|
1447
|
+
return decodeURIComponent(parts[parts.length - 1] || url);
|
|
1448
|
+
} catch {
|
|
1449
|
+
return url;
|
|
1450
|
+
}
|
|
1451
|
+
})();
|
|
1452
|
+
const newItem = await mediaManager.addItem({
|
|
1453
|
+
name: nameFromUrl,
|
|
1454
|
+
url,
|
|
1455
|
+
type: "video",
|
|
1456
|
+
metadata: { source: "url" }
|
|
1457
|
+
});
|
|
1458
|
+
addItem(newItem);
|
|
1459
|
+
};
|
|
1366
1460
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1367
1461
|
VideoPanel,
|
|
1368
1462
|
{
|
|
1369
1463
|
items,
|
|
1370
|
-
searchQuery,
|
|
1371
|
-
onSearchChange: setSearchQuery,
|
|
1372
1464
|
onItemSelect: handleSelection,
|
|
1373
1465
|
onFileUpload: handleFileUpload,
|
|
1374
1466
|
isLoading,
|
|
1375
|
-
acceptFileTypes
|
|
1467
|
+
acceptFileTypes,
|
|
1468
|
+
onUrlAdd
|
|
1376
1469
|
}
|
|
1377
1470
|
);
|
|
1378
1471
|
}
|
|
@@ -1706,6 +1799,24 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
|
|
|
1706
1799
|
const textPanelProps = useTextPanel(props);
|
|
1707
1800
|
return /* @__PURE__ */ jsxRuntime.jsx(TextPanel, { ...textPanelProps });
|
|
1708
1801
|
}
|
|
1802
|
+
const SearchInput = ({
|
|
1803
|
+
searchQuery,
|
|
1804
|
+
setSearchQuery
|
|
1805
|
+
}) => {
|
|
1806
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "search-container", children: [
|
|
1807
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
1808
|
+
"input",
|
|
1809
|
+
{
|
|
1810
|
+
type: "text",
|
|
1811
|
+
placeholder: "Search media...",
|
|
1812
|
+
value: searchQuery,
|
|
1813
|
+
onChange: (e) => setSearchQuery(e.target.value),
|
|
1814
|
+
className: "input search-input w-full"
|
|
1815
|
+
}
|
|
1816
|
+
),
|
|
1817
|
+
/* @__PURE__ */ jsxRuntime.jsx(Search, { className: "search-icon" })
|
|
1818
|
+
] });
|
|
1819
|
+
};
|
|
1709
1820
|
function IconPanel({
|
|
1710
1821
|
icons,
|
|
1711
1822
|
loading,
|