@design.estate/wcctools 5.1.0 → 6.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/.smartconfig.json +7 -3
- package/changelog.md +15 -0
- package/cli.js +3 -0
- package/dist_shell/bundle.js +2970 -0
- package/dist_shell/bundle.js.map +1 -0
- package/dist_shell/bundle.js.third-party-notices.json +1722 -0
- package/dist_shell/bundle.js.third-party-notices.txt +4195 -0
- package/dist_shell/index.html +21 -0
- package/dist_ts/00_commitinfo_data.d.ts +8 -0
- package/dist_ts/00_commitinfo_data.js +9 -0
- package/dist_ts/classes.devapi.d.ts +11 -0
- package/dist_ts/classes.devapi.js +21 -0
- package/dist_ts/classes.devserver.d.ts +49 -0
- package/dist_ts/classes.devserver.js +142 -0
- package/dist_ts/cli.d.ts +6 -0
- package/dist_ts/cli.js +45 -0
- package/dist_ts/index.d.ts +3 -0
- package/dist_ts/index.js +4 -0
- package/dist_ts/paths.d.ts +4 -0
- package/dist_ts/paths.js +6 -0
- package/dist_ts/plugins.d.ts +15 -0
- package/dist_ts/plugins.js +21 -0
- package/dist_ts/shellcontent.d.ts +9 -0
- package/dist_ts/shellcontent.js +42 -0
- package/dist_ts_interfaces/bridge.d.ts +88 -0
- package/dist_ts_interfaces/bridge.js +2 -0
- package/dist_ts_interfaces/catalog.d.ts +32 -0
- package/dist_ts_interfaces/catalog.js +6 -0
- package/dist_ts_interfaces/index.d.ts +3 -0
- package/dist_ts_interfaces/index.js +4 -0
- package/dist_ts_interfaces/plugins.d.ts +2 -0
- package/dist_ts_interfaces/plugins.js +3 -0
- package/dist_ts_interfaces/requests.d.ts +15 -0
- package/dist_ts_interfaces/requests.js +2 -0
- package/dist_ts_shared/index.d.ts +1 -0
- package/dist_ts_shared/index.js +2 -0
- package/dist_ts_shared/plugins.d.ts +2 -0
- package/dist_ts_shared/plugins.js +3 -0
- package/dist_ts_shared/previewroute.d.ts +18 -0
- package/dist_ts_shared/previewroute.js +47 -0
- package/{dist_ts_web → dist_ts_shell}/elements/wcc-contextmenu.js +1 -1
- package/dist_ts_shell/elements/wcc-preview-frame.d.ts +23 -0
- package/dist_ts_shell/elements/wcc-preview-frame.js +197 -0
- package/dist_ts_shell/elements/wcc-properties.d.ts +49 -0
- package/{dist_ts_web → dist_ts_shell}/elements/wcc-properties.js +149 -216
- package/{dist_ts_web → dist_ts_shell}/elements/wcc-record-button.js +1 -1
- package/{dist_ts_web → dist_ts_shell}/elements/wcc-recording-panel.d.ts +2 -2
- package/{dist_ts_web → dist_ts_shell}/elements/wcc-recording-panel.js +12 -16
- package/dist_ts_shell/elements/wcc-shell.d.ts +87 -0
- package/dist_ts_shell/elements/wcc-shell.js +524 -0
- package/{dist_ts_web → dist_ts_shell}/elements/wcc-sidebar.d.ts +12 -10
- package/{dist_ts_web → dist_ts_shell}/elements/wcc-sidebar.js +137 -183
- package/dist_ts_shell/index.d.ts +4 -0
- package/dist_ts_shell/index.js +5 -0
- package/dist_ts_shell/plugins.d.ts +4 -0
- package/dist_ts_shell/plugins.js +5 -0
- package/dist_ts_shell/previewconnection.d.ts +30 -0
- package/dist_ts_shell/previewconnection.js +86 -0
- package/dist_ts_shell/services/recorder.service.js +353 -0
- package/dist_ts_web/00_commitinfo_data.js +2 -2
- package/dist_ts_web/catalog.d.ts +45 -0
- package/dist_ts_web/catalog.js +153 -0
- package/dist_ts_web/elementproperties.d.ts +15 -0
- package/dist_ts_web/elementproperties.js +89 -0
- package/dist_ts_web/elements/wcc-preview.d.ts +19 -0
- package/dist_ts_web/elements/wcc-preview.js +164 -0
- package/dist_ts_web/elements/wcc-standalone-notice.d.ts +14 -0
- package/dist_ts_web/elements/wcc-standalone-notice.js +110 -0
- package/dist_ts_web/elements/wcctools.helpers.d.ts +2 -6
- package/dist_ts_web/elements/wcctools.helpers.js +1 -7
- package/dist_ts_web/index.d.ts +2 -5
- package/dist_ts_web/index.js +38 -17
- package/dist_ts_web/previewbridge.d.ts +39 -0
- package/dist_ts_web/previewbridge.js +153 -0
- package/dist_ts_web/renderedtree.d.ts +5 -0
- package/dist_ts_web/renderedtree.js +37 -0
- package/dist_ts_web/wcctools.interfaces.d.ts +3 -6
- package/dist_ts_web/wcctools.interfaces.js +1 -1
- package/dist_ts_web/wcctools.plugins.d.ts +3 -1
- package/dist_ts_web/wcctools.plugins.js +4 -2
- package/package.json +16 -8
- package/readme.md +60 -28
- package/ts/00_commitinfo_data.ts +8 -0
- package/ts/classes.devapi.ts +25 -0
- package/ts/classes.devserver.ts +161 -0
- package/ts/cli.ts +46 -0
- package/ts/index.ts +3 -0
- package/ts/paths.ts +7 -0
- package/ts/plugins.ts +29 -0
- package/ts/shellcontent.ts +42 -0
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/catalog.ts +194 -0
- package/ts_web/elementproperties.ts +96 -0
- package/ts_web/elements/wcc-preview.ts +111 -0
- package/ts_web/elements/wcc-standalone-notice.ts +65 -0
- package/ts_web/elements/wcctools.helpers.ts +2 -10
- package/ts_web/index.ts +38 -19
- package/ts_web/previewbridge.ts +189 -0
- package/ts_web/readme.md +26 -85
- package/ts_web/renderedtree.ts +43 -0
- package/ts_web/wcctools.interfaces.ts +4 -7
- package/ts_web/wcctools.plugins.ts +6 -1
- package/dist_bundle/bundle.js +0 -92419
- package/dist_bundle/bundle.js.map +0 -1
- package/dist_ts_web/elements/wcc-dashboard.d.ts +0 -119
- package/dist_ts_web/elements/wcc-dashboard.js +0 -700
- package/dist_ts_web/elements/wcc-frame.d.ts +0 -22
- package/dist_ts_web/elements/wcc-frame.js +0 -256
- package/dist_ts_web/elements/wcc-properties.d.ts +0 -43
- package/dist_ts_web/pages/index.d.ts +0 -1
- package/dist_ts_web/pages/index.js +0 -2
- package/dist_ts_web/services/recorder.service.js +0 -353
- package/ts_web/elements/wcc-contextmenu.ts +0 -291
- package/ts_web/elements/wcc-dashboard.ts +0 -672
- package/ts_web/elements/wcc-frame.ts +0 -189
- package/ts_web/elements/wcc-properties.ts +0 -1089
- package/ts_web/elements/wcc-record-button.ts +0 -108
- package/ts_web/elements/wcc-recording-panel.ts +0 -1010
- package/ts_web/elements/wcc-sidebar.ts +0 -1470
- package/ts_web/pages/index.ts +0 -1
- package/ts_web/services/recorder.service.ts +0 -451
- package/ts_web/types/dom-mediacapture-stub/index.d.ts +0 -12
- package/ts_web/types/dom-mediacapture-stub/package.json +0 -6
- package/ts_web/types/dom-webcodecs-stub/index.d.ts +0 -2
- package/ts_web/types/dom-webcodecs-stub/package.json +0 -6
- /package/{dist_ts_web → dist_ts_shell}/elements/wcc-contextmenu.d.ts +0 -0
- /package/{dist_ts_web → dist_ts_shell}/elements/wcc-record-button.d.ts +0 -0
- /package/{dist_ts_web → dist_ts_shell}/services/recorder.service.d.ts +0 -0
|
@@ -1,353 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* RecorderService - Handles all MediaRecorder, audio monitoring, and video export logic.
|
|
3
|
-
* Recording always uses MediaRecorder → WebM (the reliable browser path).
|
|
4
|
-
* MP4 output is produced by converting WebM → MP4 via mediabunny at export time.
|
|
5
|
-
*/
|
|
6
|
-
export class RecorderService {
|
|
7
|
-
// Recording state
|
|
8
|
-
mediaRecorder = null;
|
|
9
|
-
recordedChunks = [];
|
|
10
|
-
durationInterval = null;
|
|
11
|
-
_duration = 0;
|
|
12
|
-
_recordedBlob = null;
|
|
13
|
-
_isRecording = false;
|
|
14
|
-
_outputFormat = 'webm';
|
|
15
|
-
// Audio monitoring state
|
|
16
|
-
audioContext = null;
|
|
17
|
-
audioAnalyser = null;
|
|
18
|
-
audioMonitoringInterval = null;
|
|
19
|
-
monitoringStream = null;
|
|
20
|
-
// Current recording stream
|
|
21
|
-
currentStream = null;
|
|
22
|
-
// Event callbacks
|
|
23
|
-
events = {};
|
|
24
|
-
constructor(events) {
|
|
25
|
-
if (events) {
|
|
26
|
-
this.events = events;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
// Public getters
|
|
30
|
-
get isRecording() {
|
|
31
|
-
return this._isRecording;
|
|
32
|
-
}
|
|
33
|
-
get duration() {
|
|
34
|
-
return this._duration;
|
|
35
|
-
}
|
|
36
|
-
get recordedBlob() {
|
|
37
|
-
return this._recordedBlob;
|
|
38
|
-
}
|
|
39
|
-
get outputFormat() {
|
|
40
|
-
return this._outputFormat;
|
|
41
|
-
}
|
|
42
|
-
// Update event callbacks
|
|
43
|
-
setEvents(events) {
|
|
44
|
-
this.events = { ...this.events, ...events };
|
|
45
|
-
}
|
|
46
|
-
// ==================== Microphone Management ====================
|
|
47
|
-
async loadMicrophones(requestPermission = false) {
|
|
48
|
-
try {
|
|
49
|
-
if (requestPermission) {
|
|
50
|
-
// Request permission by getting a temporary stream
|
|
51
|
-
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
|
52
|
-
stream.getTracks().forEach(track => track.stop());
|
|
53
|
-
}
|
|
54
|
-
const devices = await navigator.mediaDevices.enumerateDevices();
|
|
55
|
-
return devices.filter(d => d.kind === 'audioinput');
|
|
56
|
-
}
|
|
57
|
-
catch (error) {
|
|
58
|
-
console.error('Error loading microphones:', error);
|
|
59
|
-
return [];
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
async startAudioMonitoring(deviceId) {
|
|
63
|
-
this.stopAudioMonitoring();
|
|
64
|
-
if (!deviceId)
|
|
65
|
-
return;
|
|
66
|
-
try {
|
|
67
|
-
const stream = await navigator.mediaDevices.getUserMedia({
|
|
68
|
-
audio: { deviceId: { exact: deviceId } }
|
|
69
|
-
});
|
|
70
|
-
this.monitoringStream = stream;
|
|
71
|
-
this.audioContext = new AudioContext();
|
|
72
|
-
const source = this.audioContext.createMediaStreamSource(stream);
|
|
73
|
-
this.audioAnalyser = this.audioContext.createAnalyser();
|
|
74
|
-
this.audioAnalyser.fftSize = 256;
|
|
75
|
-
source.connect(this.audioAnalyser);
|
|
76
|
-
const dataArray = new Uint8Array(this.audioAnalyser.frequencyBinCount);
|
|
77
|
-
this.audioMonitoringInterval = window.setInterval(() => {
|
|
78
|
-
if (this.audioAnalyser) {
|
|
79
|
-
this.audioAnalyser.getByteFrequencyData(dataArray);
|
|
80
|
-
const average = dataArray.reduce((a, b) => a + b) / dataArray.length;
|
|
81
|
-
const level = Math.min(100, (average / 128) * 100);
|
|
82
|
-
this.events.onAudioLevelUpdate?.(level);
|
|
83
|
-
}
|
|
84
|
-
}, 50);
|
|
85
|
-
}
|
|
86
|
-
catch (error) {
|
|
87
|
-
console.error('Error starting audio monitoring:', error);
|
|
88
|
-
this.events.onAudioLevelUpdate?.(0);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
stopAudioMonitoring() {
|
|
92
|
-
if (this.audioMonitoringInterval) {
|
|
93
|
-
clearInterval(this.audioMonitoringInterval);
|
|
94
|
-
this.audioMonitoringInterval = null;
|
|
95
|
-
}
|
|
96
|
-
if (this.audioContext) {
|
|
97
|
-
this.audioContext.close();
|
|
98
|
-
this.audioContext = null;
|
|
99
|
-
}
|
|
100
|
-
if (this.monitoringStream) {
|
|
101
|
-
this.monitoringStream.getTracks().forEach(track => track.stop());
|
|
102
|
-
this.monitoringStream = null;
|
|
103
|
-
}
|
|
104
|
-
this.audioAnalyser = null;
|
|
105
|
-
}
|
|
106
|
-
// ==================== Recording Control ====================
|
|
107
|
-
async startRecording(options) {
|
|
108
|
-
try {
|
|
109
|
-
this._outputFormat = options.outputFormat || 'webm';
|
|
110
|
-
// Stop audio monitoring before recording
|
|
111
|
-
this.stopAudioMonitoring();
|
|
112
|
-
// Get video stream based on mode
|
|
113
|
-
const displayMediaOptions = {
|
|
114
|
-
video: {
|
|
115
|
-
displaySurface: options.mode === 'viewport' ? 'browser' : 'monitor',
|
|
116
|
-
frameRate: { ideal: 60 },
|
|
117
|
-
},
|
|
118
|
-
audio: false
|
|
119
|
-
};
|
|
120
|
-
// Add preferCurrentTab hint for viewport mode
|
|
121
|
-
if (options.mode === 'viewport') {
|
|
122
|
-
displayMediaOptions.preferCurrentTab = true;
|
|
123
|
-
}
|
|
124
|
-
const videoStream = await navigator.mediaDevices.getDisplayMedia(displayMediaOptions);
|
|
125
|
-
// If viewport mode, try to crop to viewport element using Element Capture API
|
|
126
|
-
if (options.mode === 'viewport' && options.viewportElement) {
|
|
127
|
-
try {
|
|
128
|
-
if ('CropTarget' in window) {
|
|
129
|
-
const cropTarget = await window.CropTarget.fromElement(options.viewportElement);
|
|
130
|
-
const [videoTrack] = videoStream.getVideoTracks();
|
|
131
|
-
await videoTrack.cropTo(cropTarget);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
catch (e) {
|
|
135
|
-
console.warn('Element Capture not supported, recording full tab:', e);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
// Combine video with audio if enabled
|
|
139
|
-
let combinedStream = videoStream;
|
|
140
|
-
if (options.audioDeviceId) {
|
|
141
|
-
try {
|
|
142
|
-
const audioStream = await navigator.mediaDevices.getUserMedia({
|
|
143
|
-
audio: { deviceId: { exact: options.audioDeviceId } }
|
|
144
|
-
});
|
|
145
|
-
combinedStream = new MediaStream([
|
|
146
|
-
...videoStream.getVideoTracks(),
|
|
147
|
-
...audioStream.getAudioTracks()
|
|
148
|
-
]);
|
|
149
|
-
}
|
|
150
|
-
catch (audioError) {
|
|
151
|
-
console.warn('Could not add audio:', audioError);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
// Store stream for cleanup
|
|
155
|
-
this.currentStream = combinedStream;
|
|
156
|
-
// Handle stream ending (user clicks "Stop sharing")
|
|
157
|
-
videoStream.getVideoTracks()[0].onended = () => {
|
|
158
|
-
if (this._isRecording) {
|
|
159
|
-
this.stopRecording();
|
|
160
|
-
this.events.onStreamEnded?.();
|
|
161
|
-
}
|
|
162
|
-
};
|
|
163
|
-
// Always record as WebM — conversion to MP4 happens at export time
|
|
164
|
-
const mimeType = MediaRecorder.isTypeSupported('video/webm;codecs=vp9')
|
|
165
|
-
? 'video/webm;codecs=vp9'
|
|
166
|
-
: 'video/webm';
|
|
167
|
-
this.mediaRecorder = new MediaRecorder(combinedStream, {
|
|
168
|
-
mimeType,
|
|
169
|
-
videoBitsPerSecond: 8_000_000, // 8 Mbps for smooth, high-quality capture
|
|
170
|
-
});
|
|
171
|
-
this.recordedChunks = [];
|
|
172
|
-
this.mediaRecorder.ondataavailable = (e) => {
|
|
173
|
-
if (e.data.size > 0) {
|
|
174
|
-
this.recordedChunks.push(e.data);
|
|
175
|
-
}
|
|
176
|
-
};
|
|
177
|
-
this.mediaRecorder.onstop = () => this.handleRecordingComplete();
|
|
178
|
-
this.mediaRecorder.start(1000); // Capture in 1-second chunks
|
|
179
|
-
// Start duration timer
|
|
180
|
-
this._duration = 0;
|
|
181
|
-
this.durationInterval = window.setInterval(() => {
|
|
182
|
-
this._duration++;
|
|
183
|
-
this.events.onDurationUpdate?.(this._duration);
|
|
184
|
-
}, 1000);
|
|
185
|
-
this._isRecording = true;
|
|
186
|
-
}
|
|
187
|
-
catch (error) {
|
|
188
|
-
console.error('Error starting recording:', error);
|
|
189
|
-
this._isRecording = false;
|
|
190
|
-
this.events.onError?.(error);
|
|
191
|
-
throw error;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
stopRecording() {
|
|
195
|
-
if (this.mediaRecorder && this.mediaRecorder.state !== 'inactive') {
|
|
196
|
-
this.mediaRecorder.stop();
|
|
197
|
-
}
|
|
198
|
-
if (this.durationInterval) {
|
|
199
|
-
clearInterval(this.durationInterval);
|
|
200
|
-
this.durationInterval = null;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
async handleRecordingComplete() {
|
|
204
|
-
const blob = new Blob(this.recordedChunks, { type: 'video/webm' });
|
|
205
|
-
this._recordedBlob = blob;
|
|
206
|
-
// Stop all tracks
|
|
207
|
-
if (this.currentStream) {
|
|
208
|
-
this.currentStream.getTracks().forEach(track => track.stop());
|
|
209
|
-
this.currentStream = null;
|
|
210
|
-
}
|
|
211
|
-
this._isRecording = false;
|
|
212
|
-
this.events.onRecordingComplete?.(this._recordedBlob);
|
|
213
|
-
}
|
|
214
|
-
// ==================== Conversion & Export ====================
|
|
215
|
-
/**
|
|
216
|
-
* Converts a WebM blob to MP4 using mediabunny's Conversion API.
|
|
217
|
-
* Uses WebCodecs for hardware-accelerated H.264 encoding.
|
|
218
|
-
*/
|
|
219
|
-
async convertToMp4(webmBlob) {
|
|
220
|
-
const { Input, Output, Conversion, BlobSource, BufferTarget, Mp4OutputFormat, WEBM, QUALITY_HIGH, } = await import('mediabunny');
|
|
221
|
-
const input = new Input({
|
|
222
|
-
source: new BlobSource(webmBlob),
|
|
223
|
-
formats: [WEBM],
|
|
224
|
-
});
|
|
225
|
-
const target = new BufferTarget();
|
|
226
|
-
const output = new Output({
|
|
227
|
-
format: new Mp4OutputFormat({ fastStart: 'in-memory' }),
|
|
228
|
-
target,
|
|
229
|
-
});
|
|
230
|
-
const conversion = await Conversion.init({
|
|
231
|
-
input,
|
|
232
|
-
output,
|
|
233
|
-
// Force transcoding from VP9 → H.264 and Opus → AAC
|
|
234
|
-
video: {
|
|
235
|
-
codec: 'avc',
|
|
236
|
-
bitrate: QUALITY_HIGH,
|
|
237
|
-
fit: 'contain',
|
|
238
|
-
},
|
|
239
|
-
audio: {
|
|
240
|
-
codec: 'aac',
|
|
241
|
-
bitrate: QUALITY_HIGH,
|
|
242
|
-
},
|
|
243
|
-
});
|
|
244
|
-
await conversion.execute();
|
|
245
|
-
const buffer = target.buffer;
|
|
246
|
-
if (!buffer || buffer.byteLength === 0) {
|
|
247
|
-
throw new Error('MP4 conversion produced empty output');
|
|
248
|
-
}
|
|
249
|
-
return new Blob([buffer], { type: 'video/mp4' });
|
|
250
|
-
}
|
|
251
|
-
async exportTrimmedVideo(videoElement, trimStart, trimEnd) {
|
|
252
|
-
return new Promise((resolve, reject) => {
|
|
253
|
-
// Create a canvas for capturing frames
|
|
254
|
-
const canvas = document.createElement('canvas');
|
|
255
|
-
canvas.width = videoElement.videoWidth || 1280;
|
|
256
|
-
canvas.height = videoElement.videoHeight || 720;
|
|
257
|
-
const ctx = canvas.getContext('2d');
|
|
258
|
-
if (!ctx) {
|
|
259
|
-
reject(new Error('Could not get canvas context'));
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
// Create canvas stream for video
|
|
263
|
-
const canvasStream = canvas.captureStream(30);
|
|
264
|
-
// Try to capture audio from video element
|
|
265
|
-
let combinedStream;
|
|
266
|
-
try {
|
|
267
|
-
// Create audio context to capture video's audio
|
|
268
|
-
const audioCtx = new AudioContext();
|
|
269
|
-
const source = audioCtx.createMediaElementSource(videoElement);
|
|
270
|
-
const destination = audioCtx.createMediaStreamDestination();
|
|
271
|
-
source.connect(destination);
|
|
272
|
-
source.connect(audioCtx.destination); // Also play through speakers
|
|
273
|
-
// Combine video (from canvas) and audio (from video element)
|
|
274
|
-
combinedStream = new MediaStream([
|
|
275
|
-
...canvasStream.getVideoTracks(),
|
|
276
|
-
...destination.stream.getAudioTracks()
|
|
277
|
-
]);
|
|
278
|
-
// Store audioCtx for cleanup
|
|
279
|
-
const cleanup = () => {
|
|
280
|
-
audioCtx.close();
|
|
281
|
-
};
|
|
282
|
-
this.recordTrimmedStream(videoElement, canvas, ctx, combinedStream, trimStart, trimEnd, cleanup, resolve, reject);
|
|
283
|
-
}
|
|
284
|
-
catch (audioError) {
|
|
285
|
-
console.warn('Could not capture audio, recording video only:', audioError);
|
|
286
|
-
combinedStream = canvasStream;
|
|
287
|
-
this.recordTrimmedStream(videoElement, canvas, ctx, combinedStream, trimStart, trimEnd, () => { }, resolve, reject);
|
|
288
|
-
}
|
|
289
|
-
});
|
|
290
|
-
}
|
|
291
|
-
recordTrimmedStream(video, canvas, ctx, stream, trimStart, trimEnd, cleanup, resolve, reject) {
|
|
292
|
-
const mimeType = MediaRecorder.isTypeSupported('video/webm;codecs=vp9')
|
|
293
|
-
? 'video/webm;codecs=vp9'
|
|
294
|
-
: 'video/webm';
|
|
295
|
-
const recorder = new MediaRecorder(stream, { mimeType });
|
|
296
|
-
const chunks = [];
|
|
297
|
-
recorder.ondataavailable = (e) => {
|
|
298
|
-
if (e.data.size > 0) {
|
|
299
|
-
chunks.push(e.data);
|
|
300
|
-
}
|
|
301
|
-
};
|
|
302
|
-
recorder.onstop = () => {
|
|
303
|
-
cleanup();
|
|
304
|
-
resolve(new Blob(chunks, { type: 'video/webm' }));
|
|
305
|
-
};
|
|
306
|
-
recorder.onerror = (e) => {
|
|
307
|
-
cleanup();
|
|
308
|
-
reject(new Error('Recording error: ' + e));
|
|
309
|
-
};
|
|
310
|
-
// Seek to trim start
|
|
311
|
-
video.currentTime = trimStart;
|
|
312
|
-
video.onseeked = () => {
|
|
313
|
-
// Start recording
|
|
314
|
-
recorder.start(100);
|
|
315
|
-
// Start playing
|
|
316
|
-
video.play();
|
|
317
|
-
// Draw frames to canvas
|
|
318
|
-
const drawFrame = () => {
|
|
319
|
-
if (video.currentTime >= trimEnd || video.paused || video.ended) {
|
|
320
|
-
video.pause();
|
|
321
|
-
video.onseeked = null;
|
|
322
|
-
// Give a small delay before stopping to ensure last frame is captured
|
|
323
|
-
setTimeout(() => {
|
|
324
|
-
if (recorder.state === 'recording') {
|
|
325
|
-
recorder.stop();
|
|
326
|
-
}
|
|
327
|
-
}, 100);
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
331
|
-
requestAnimationFrame(drawFrame);
|
|
332
|
-
};
|
|
333
|
-
drawFrame();
|
|
334
|
-
};
|
|
335
|
-
}
|
|
336
|
-
// ==================== Cleanup ====================
|
|
337
|
-
reset() {
|
|
338
|
-
this._recordedBlob = null;
|
|
339
|
-
this.recordedChunks = [];
|
|
340
|
-
this._duration = 0;
|
|
341
|
-
this._isRecording = false;
|
|
342
|
-
}
|
|
343
|
-
dispose() {
|
|
344
|
-
this.stopRecording();
|
|
345
|
-
this.stopAudioMonitoring();
|
|
346
|
-
this.reset();
|
|
347
|
-
if (this.currentStream) {
|
|
348
|
-
this.currentStream.getTracks().forEach(track => track.stop());
|
|
349
|
-
this.currentStream = null;
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoicmVjb3JkZXIuc2VydmljZS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uL3RzX3dlYi9zZXJ2aWNlcy9yZWNvcmRlci5zZXJ2aWNlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOzs7O0dBSUc7QUFtQkgsTUFBTSxPQUFPLGVBQWU7SUFDMUIsa0JBQWtCO0lBQ1YsYUFBYSxHQUF5QixJQUFJLENBQUM7SUFDM0MsY0FBYyxHQUFXLEVBQUUsQ0FBQztJQUM1QixnQkFBZ0IsR0FBa0IsSUFBSSxDQUFDO0lBQ3ZDLFNBQVMsR0FBVyxDQUFDLENBQUM7SUFDdEIsYUFBYSxHQUFnQixJQUFJLENBQUM7SUFDbEMsWUFBWSxHQUFZLEtBQUssQ0FBQztJQUM5QixhQUFhLEdBQWtCLE1BQU0sQ0FBQztJQUU5Qyx5QkFBeUI7SUFDakIsWUFBWSxHQUF3QixJQUFJLENBQUM7SUFDekMsYUFBYSxHQUF3QixJQUFJLENBQUM7SUFDMUMsdUJBQXVCLEdBQWtCLElBQUksQ0FBQztJQUM5QyxnQkFBZ0IsR0FBdUIsSUFBSSxDQUFDO0lBRXBELDJCQUEyQjtJQUNuQixhQUFhLEdBQXVCLElBQUksQ0FBQztJQUVqRCxrQkFBa0I7SUFDVixNQUFNLEdBQW9CLEVBQUUsQ0FBQztJQUVyQyxZQUFZLE1BQXdCO1FBQ2xDLElBQUksTUFBTSxFQUFFLENBQUM7WUFDWCxJQUFJLENBQUMsTUFBTSxHQUFHLE1BQU0sQ0FBQztRQUN2QixDQUFDO0lBQ0gsQ0FBQztJQUVELGlCQUFpQjtJQUNqQixJQUFJLFdBQVc7UUFDYixPQUFPLElBQUksQ0FBQyxZQUFZLENBQUM7SUFDM0IsQ0FBQztJQUVELElBQUksUUFBUTtRQUNWLE9BQU8sSUFBSSxDQUFDLFNBQVMsQ0FBQztJQUN4QixDQUFDO0lBRUQsSUFBSSxZQUFZO1FBQ2QsT0FBTyxJQUFJLENBQUMsYUFBYSxDQUFDO0lBQzVCLENBQUM7SUFFRCxJQUFJLFlBQVk7UUFDZCxPQUFPLElBQUksQ0FBQyxhQUFhLENBQUM7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
|
|
@@ -1,291 +0,0 @@
|
|
|
1
|
-
import { DeesElement, property, html, customElement, type TemplateResult, state, css, cssManager } from '@design.estate/dees-element';
|
|
2
|
-
|
|
3
|
-
export interface IContextMenuItem {
|
|
4
|
-
name: string;
|
|
5
|
-
iconName?: string;
|
|
6
|
-
action: () => void | Promise<void>;
|
|
7
|
-
disabled?: boolean;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export interface IContextMenuOptions {
|
|
11
|
-
/** The element the menu belongs to: focus returns to it when the menu closes */
|
|
12
|
-
ownerElement?: HTMLElement;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
@customElement('wcc-contextmenu')
|
|
16
|
-
export class WccContextmenu extends DeesElement {
|
|
17
|
-
/**
|
|
18
|
-
* Show a context menu at the event's position. The menu takes focus (its first item),
|
|
19
|
-
* is operated with the arrow keys, Home/End, Enter/Space and Escape/Tab, and returns
|
|
20
|
-
* focus to `ownerElement` when it closes from the keyboard or after an action.
|
|
21
|
-
*/
|
|
22
|
-
public static async show(
|
|
23
|
-
event: MouseEvent,
|
|
24
|
-
menuItems: IContextMenuItem[],
|
|
25
|
-
options: IContextMenuOptions = {}
|
|
26
|
-
): Promise<void> {
|
|
27
|
-
event.preventDefault();
|
|
28
|
-
event.stopPropagation();
|
|
29
|
-
|
|
30
|
-
// Remove any existing context menu
|
|
31
|
-
const existing = document.querySelector('wcc-contextmenu');
|
|
32
|
-
if (existing) {
|
|
33
|
-
existing.remove();
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const menu = new WccContextmenu();
|
|
37
|
-
menu.menuItems = menuItems;
|
|
38
|
-
menu.x = event.clientX;
|
|
39
|
-
menu.y = event.clientY;
|
|
40
|
-
menu.ownerElement = options.ownerElement ?? null;
|
|
41
|
-
|
|
42
|
-
document.body.appendChild(menu);
|
|
43
|
-
|
|
44
|
-
// Wait for render then adjust position if needed
|
|
45
|
-
await menu.updateComplete;
|
|
46
|
-
menu.adjustPosition();
|
|
47
|
-
menu.focusItem(0);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** Focus returns here when the menu closes from the keyboard or after an action */
|
|
51
|
-
private ownerElement: HTMLElement | null = null;
|
|
52
|
-
private closing = false;
|
|
53
|
-
|
|
54
|
-
@property({ type: Array })
|
|
55
|
-
accessor menuItems: IContextMenuItem[] = [];
|
|
56
|
-
|
|
57
|
-
@property({ type: Number })
|
|
58
|
-
accessor x: number = 0;
|
|
59
|
-
|
|
60
|
-
@property({ type: Number })
|
|
61
|
-
accessor y: number = 0;
|
|
62
|
-
|
|
63
|
-
@state()
|
|
64
|
-
accessor visible: boolean = false;
|
|
65
|
-
|
|
66
|
-
private boundHandleOutsideClick = this.handleOutsideClick.bind(this);
|
|
67
|
-
private boundHandleKeydown = this.handleKeydown.bind(this);
|
|
68
|
-
|
|
69
|
-
public static styles = [
|
|
70
|
-
css`
|
|
71
|
-
:host {
|
|
72
|
-
position: fixed;
|
|
73
|
-
z-index: 10000;
|
|
74
|
-
opacity: 0;
|
|
75
|
-
transform: scale(0.95) translateY(-5px);
|
|
76
|
-
transition: opacity 0.15s ease, transform 0.15s ease;
|
|
77
|
-
pointer-events: none;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
:host(.visible) {
|
|
81
|
-
opacity: 1;
|
|
82
|
-
transform: scale(1) translateY(0);
|
|
83
|
-
pointer-events: auto;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
.menu {
|
|
87
|
-
min-width: 160px;
|
|
88
|
-
background: #0f0f0f;
|
|
89
|
-
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
90
|
-
border-radius: 6px;
|
|
91
|
-
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
|
|
92
|
-
padding: 4px 0;
|
|
93
|
-
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
|
94
|
-
font-size: 12px;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
.menu-item {
|
|
98
|
-
display: flex;
|
|
99
|
-
align-items: center;
|
|
100
|
-
gap: 8px;
|
|
101
|
-
padding: 8px 12px;
|
|
102
|
-
color: #ccc;
|
|
103
|
-
cursor: pointer;
|
|
104
|
-
transition: background 0.1s ease;
|
|
105
|
-
user-select: none;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
.menu {
|
|
109
|
-
outline: none;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
.menu-item:hover,
|
|
113
|
-
.menu-item:focus {
|
|
114
|
-
background: rgba(59, 130, 246, 0.15);
|
|
115
|
-
color: #fff;
|
|
116
|
-
outline: none;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
.menu-item:focus-visible {
|
|
120
|
-
box-shadow: inset 0 0 0 2px #3b82f6;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
.menu-item.disabled {
|
|
124
|
-
opacity: 0.4;
|
|
125
|
-
cursor: not-allowed;
|
|
126
|
-
pointer-events: none;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
.menu-item .icon {
|
|
130
|
-
font-family: 'Material Symbols Outlined';
|
|
131
|
-
font-size: 16px;
|
|
132
|
-
font-weight: normal;
|
|
133
|
-
font-style: normal;
|
|
134
|
-
line-height: 1;
|
|
135
|
-
letter-spacing: normal;
|
|
136
|
-
text-transform: none;
|
|
137
|
-
white-space: nowrap;
|
|
138
|
-
word-wrap: normal;
|
|
139
|
-
direction: ltr;
|
|
140
|
-
font-variation-settings: 'FILL' 0, 'wght' 300, 'GRAD' 0, 'opsz' 24;
|
|
141
|
-
opacity: 0.7;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
.menu-item:hover .icon {
|
|
145
|
-
opacity: 1;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
.menu-item .label {
|
|
149
|
-
flex: 1;
|
|
150
|
-
}
|
|
151
|
-
`
|
|
152
|
-
];
|
|
153
|
-
|
|
154
|
-
public render(): TemplateResult {
|
|
155
|
-
return html`
|
|
156
|
-
<div class="menu" role="menu" tabindex="-1" @keydown=${this.handleMenuKeydown}>
|
|
157
|
-
${this.menuItems.map(item => html`
|
|
158
|
-
<div
|
|
159
|
-
class="menu-item ${item.disabled ? 'disabled' : ''}"
|
|
160
|
-
role="menuitem"
|
|
161
|
-
tabindex="-1"
|
|
162
|
-
aria-disabled=${item.disabled ? 'true' : 'false'}
|
|
163
|
-
@click=${() => this.handleItemClick(item)}
|
|
164
|
-
>
|
|
165
|
-
${item.iconName ? html`<span class="icon" aria-hidden="true">${item.iconName}</span>` : null}
|
|
166
|
-
<span class="label">${item.name}</span>
|
|
167
|
-
</div>
|
|
168
|
-
`)}
|
|
169
|
-
</div>
|
|
170
|
-
`;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
async connectedCallback() {
|
|
174
|
-
await super.connectedCallback();
|
|
175
|
-
// Delay adding listeners to avoid immediate close
|
|
176
|
-
requestAnimationFrame(() => {
|
|
177
|
-
document.addEventListener('click', this.boundHandleOutsideClick);
|
|
178
|
-
document.addEventListener('contextmenu', this.boundHandleOutsideClick);
|
|
179
|
-
document.addEventListener('keydown', this.boundHandleKeydown);
|
|
180
|
-
this.classList.add('visible');
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
async disconnectedCallback() {
|
|
185
|
-
await super.disconnectedCallback();
|
|
186
|
-
document.removeEventListener('click', this.boundHandleOutsideClick);
|
|
187
|
-
document.removeEventListener('contextmenu', this.boundHandleOutsideClick);
|
|
188
|
-
document.removeEventListener('keydown', this.boundHandleKeydown);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
private adjustPosition() {
|
|
192
|
-
const rect = this.getBoundingClientRect();
|
|
193
|
-
const windowWidth = window.innerWidth;
|
|
194
|
-
const windowHeight = window.innerHeight;
|
|
195
|
-
|
|
196
|
-
let x = this.x;
|
|
197
|
-
let y = this.y;
|
|
198
|
-
|
|
199
|
-
// Adjust if menu goes off right edge
|
|
200
|
-
if (x + rect.width > windowWidth - 10) {
|
|
201
|
-
x = windowWidth - rect.width - 10;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
// Adjust if menu goes off bottom edge
|
|
205
|
-
if (y + rect.height > windowHeight - 10) {
|
|
206
|
-
y = windowHeight - rect.height - 10;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// Ensure not off left or top
|
|
210
|
-
if (x < 10) x = 10;
|
|
211
|
-
if (y < 10) y = 10;
|
|
212
|
-
|
|
213
|
-
this.style.left = `${x}px`;
|
|
214
|
-
this.style.top = `${y}px`;
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
private handleOutsideClick(e: Event) {
|
|
218
|
-
const path = e.composedPath();
|
|
219
|
-
if (!path.includes(this)) {
|
|
220
|
-
this.close();
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
private handleKeydown(e: KeyboardEvent) {
|
|
225
|
-
if (e.key === 'Escape') {
|
|
226
|
-
this.close(true);
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
private getEnabledItems(): HTMLElement[] {
|
|
231
|
-
return Array.from(this.shadowRoot?.querySelectorAll<HTMLElement>('.menu-item:not(.disabled)') ?? []);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/** Focus an enabled item by index (wrapping); the menu itself if there is none */
|
|
235
|
-
private focusItem(indexArg: number) {
|
|
236
|
-
const items = this.getEnabledItems();
|
|
237
|
-
if (items.length === 0) {
|
|
238
|
-
(this.shadowRoot?.querySelector('.menu') as HTMLElement | null)?.focus({ preventScroll: true });
|
|
239
|
-
return;
|
|
240
|
-
}
|
|
241
|
-
const index = ((indexArg % items.length) + items.length) % items.length;
|
|
242
|
-
items[index].focus({ preventScroll: true });
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
private handleMenuKeydown = (e: KeyboardEvent) => {
|
|
246
|
-
const items = this.getEnabledItems();
|
|
247
|
-
const current = items.indexOf(this.shadowRoot?.activeElement as HTMLElement);
|
|
248
|
-
switch (e.key) {
|
|
249
|
-
case 'ArrowDown':
|
|
250
|
-
this.focusItem(current + 1);
|
|
251
|
-
break;
|
|
252
|
-
case 'ArrowUp':
|
|
253
|
-
this.focusItem(current < 0 ? -1 : current - 1);
|
|
254
|
-
break;
|
|
255
|
-
case 'Home':
|
|
256
|
-
this.focusItem(0);
|
|
257
|
-
break;
|
|
258
|
-
case 'End':
|
|
259
|
-
this.focusItem(-1);
|
|
260
|
-
break;
|
|
261
|
-
case 'Enter':
|
|
262
|
-
case ' ':
|
|
263
|
-
if (current >= 0) items[current].click();
|
|
264
|
-
break;
|
|
265
|
-
case 'Escape':
|
|
266
|
-
case 'Tab':
|
|
267
|
-
this.close(true);
|
|
268
|
-
break;
|
|
269
|
-
default:
|
|
270
|
-
return;
|
|
271
|
-
}
|
|
272
|
-
e.preventDefault();
|
|
273
|
-
e.stopPropagation();
|
|
274
|
-
};
|
|
275
|
-
|
|
276
|
-
private async handleItemClick(item: IContextMenuItem) {
|
|
277
|
-
if (item.disabled) return;
|
|
278
|
-
await item.action();
|
|
279
|
-
this.close(true);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
private close(returnFocus = false) {
|
|
283
|
-
if (this.closing) return;
|
|
284
|
-
this.closing = true;
|
|
285
|
-
if (returnFocus && this.ownerElement?.isConnected) {
|
|
286
|
-
this.ownerElement.focus({ preventScroll: true });
|
|
287
|
-
}
|
|
288
|
-
this.classList.remove('visible');
|
|
289
|
-
setTimeout(() => this.remove(), 150);
|
|
290
|
-
}
|
|
291
|
-
}
|