@opendaw/studio-core 0.0.99 → 0.0.101
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/EffectFactories.d.ts.map +1 -1
- package/dist/EffectFactories.js +39 -24
- package/dist/Engine.d.ts +2 -0
- package/dist/Engine.d.ts.map +1 -1
- package/dist/EngineFacade.d.ts +2 -0
- package/dist/EngineFacade.d.ts.map +1 -1
- package/dist/EngineFacade.js +3 -0
- package/dist/EngineWorklet.d.ts +2 -0
- package/dist/EngineWorklet.d.ts.map +1 -1
- package/dist/EngineWorklet.js +10 -2
- package/dist/HRClockWorker.d.ts +7 -0
- package/dist/HRClockWorker.d.ts.map +1 -0
- package/dist/HRClockWorker.js +54 -0
- package/dist/OfflineEngineRenderer.d.ts.map +1 -1
- package/dist/OfflineEngineRenderer.js +5 -2
- package/dist/RecordingWorklet.d.ts +2 -1
- package/dist/RecordingWorklet.d.ts.map +1 -1
- package/dist/RecordingWorklet.js +9 -1
- package/dist/Storage.d.ts.map +1 -1
- package/dist/Storage.js +1 -0
- package/dist/StudioPreferences.d.ts +8 -0
- package/dist/StudioPreferences.d.ts.map +1 -1
- package/dist/StudioSettings.d.ts +13 -0
- package/dist/StudioSettings.d.ts.map +1 -1
- package/dist/StudioSettings.js +15 -0
- package/dist/capture/CaptureAudio.d.ts.map +1 -1
- package/dist/capture/CaptureAudio.js +8 -11
- package/dist/capture/RecordAudio.d.ts.map +1 -1
- package/dist/capture/RecordAudio.js +1 -0
- package/dist/processors.js +24 -24
- package/dist/processors.js.map +4 -4
- package/dist/project/Project.d.ts +6 -1
- package/dist/project/Project.d.ts.map +1 -1
- package/dist/project/Project.js +34 -2
- package/dist/project/ProjectApi.d.ts.map +1 -1
- package/dist/project/ProjectApi.js +14 -3
- package/dist/project/audio/AudioContentFactory.d.ts +5 -0
- package/dist/project/audio/AudioContentFactory.d.ts.map +1 -1
- package/dist/project/audio/AudioContentFactory.js +13 -0
- package/dist/ui/{generic → clipboard}/ClipboardManager.d.ts +3 -3
- package/dist/ui/clipboard/ClipboardManager.d.ts.map +1 -0
- package/dist/ui/{generic → clipboard}/ClipboardManager.js +70 -14
- package/dist/ui/clipboard/ClipboardUtils.d.ts +12 -0
- package/dist/ui/clipboard/ClipboardUtils.d.ts.map +1 -0
- package/dist/ui/clipboard/ClipboardUtils.js +94 -0
- package/dist/ui/{generic → clipboard}/ContextMenu.d.ts +1 -1
- package/dist/ui/clipboard/ContextMenu.d.ts.map +1 -0
- package/dist/ui/{generic → clipboard}/ContextMenu.js +1 -1
- package/dist/ui/clipboard/types/AudioUnitsClipboardHandler.d.ts +18 -0
- package/dist/ui/clipboard/types/AudioUnitsClipboardHandler.d.ts.map +1 -0
- package/dist/ui/clipboard/types/AudioUnitsClipboardHandler.js +215 -0
- package/dist/ui/clipboard/types/DevicesClipboardHandler.d.ts +18 -0
- package/dist/ui/clipboard/types/DevicesClipboardHandler.d.ts.map +1 -0
- package/dist/ui/clipboard/types/DevicesClipboardHandler.js +188 -0
- package/dist/ui/clipboard/types/NotesClipboardHandler.d.ts +21 -0
- package/dist/ui/clipboard/types/NotesClipboardHandler.d.ts.map +1 -0
- package/dist/ui/clipboard/types/NotesClipboardHandler.js +72 -0
- package/dist/ui/clipboard/types/RegionsClipboardHandler.d.ts +24 -0
- package/dist/ui/clipboard/types/RegionsClipboardHandler.d.ts.map +1 -0
- package/dist/ui/clipboard/types/RegionsClipboardHandler.js +137 -0
- package/dist/ui/clipboard/types/ValuesClipboardHandler.d.ts +22 -0
- package/dist/ui/clipboard/types/ValuesClipboardHandler.d.ts.map +1 -0
- package/dist/ui/clipboard/types/ValuesClipboardHandler.js +135 -0
- package/dist/ui/index.d.ts +14 -3
- package/dist/ui/index.d.ts.map +1 -1
- package/dist/ui/index.js +14 -3
- package/dist/ui/menu/MenuItems.d.ts.map +1 -0
- package/dist/ui/timeline/RegionClipResolver.d.ts +5 -1
- package/dist/ui/timeline/RegionClipResolver.d.ts.map +1 -1
- package/dist/ui/timeline/RegionClipResolver.js +5 -1
- package/dist/ui/timeline/RegionKeepExistingResolver.d.ts +34 -0
- package/dist/ui/timeline/RegionKeepExistingResolver.d.ts.map +1 -0
- package/dist/ui/timeline/RegionKeepExistingResolver.js +171 -0
- package/dist/ui/timeline/RegionOverlapResolver.d.ts +33 -0
- package/dist/ui/timeline/RegionOverlapResolver.d.ts.map +1 -0
- package/dist/ui/timeline/RegionOverlapResolver.js +79 -0
- package/dist/ui/timeline/RegionPushExistingResolver.d.ts +31 -0
- package/dist/ui/timeline/RegionPushExistingResolver.d.ts.map +1 -0
- package/dist/ui/timeline/RegionPushExistingResolver.js +159 -0
- package/dist/ui/timeline/TimelineFocus.d.ts +14 -0
- package/dist/ui/timeline/TimelineFocus.d.ts.map +1 -0
- package/dist/ui/timeline/TimelineFocus.js +45 -0
- package/dist/ui/timeline/TrackResolver.d.ts +11 -0
- package/dist/ui/timeline/TrackResolver.d.ts.map +1 -0
- package/dist/ui/timeline/TrackResolver.js +5 -0
- package/package.json +62 -61
- package/dist/ui/generic/ClipboardManager.d.ts.map +0 -1
- package/dist/ui/generic/ContextMenu.d.ts.map +0 -1
- package/dist/ui/generic/MenuItems.d.ts.map +0 -1
- /package/dist/ui/{generic → menu}/MenuItems.d.ts +0 -0
- /package/dist/ui/{generic → menu}/MenuItems.js +0 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { ByteArrayInput, ByteArrayOutput, Option } from "@opendaw/lib-std";
|
|
2
|
+
import { IndexedBox } from "@opendaw/lib-box";
|
|
3
|
+
import { AudioUnitType, Pointers } from "@opendaw/studio-enums";
|
|
4
|
+
import { AudioBusBox, AudioUnitBox, AuxSendBox, CaptureAudioBox, CaptureMidiBox, MIDIControllerBox, RootBox } from "@opendaw/studio-boxes";
|
|
5
|
+
import { AudioUnitOrdering } from "@opendaw/studio-adapters";
|
|
6
|
+
import { ClipboardUtils } from "../ClipboardUtils";
|
|
7
|
+
export var AudioUnitsClipboard;
|
|
8
|
+
(function (AudioUnitsClipboard) {
|
|
9
|
+
const encodeMetadata = (metadata) => {
|
|
10
|
+
const output = ByteArrayOutput.create();
|
|
11
|
+
output.writeString(metadata.type);
|
|
12
|
+
return output.toArrayBuffer();
|
|
13
|
+
};
|
|
14
|
+
const decodeMetadata = (buffer) => {
|
|
15
|
+
const input = new ByteArrayInput(buffer);
|
|
16
|
+
return { type: input.readString() };
|
|
17
|
+
};
|
|
18
|
+
AudioUnitsClipboard.createHandler = ({ getEnabled, editing, boxGraph, rootBoxAdapter, audioUnitEditing, getEditedAudioUnit }) => {
|
|
19
|
+
const copyAudioUnit = () => {
|
|
20
|
+
const optAudioUnit = getEditedAudioUnit();
|
|
21
|
+
if (optAudioUnit.isEmpty()) {
|
|
22
|
+
return Option.None;
|
|
23
|
+
}
|
|
24
|
+
const audioUnitAdapter = optAudioUnit.unwrap();
|
|
25
|
+
const audioUnitBox = audioUnitAdapter.box;
|
|
26
|
+
const isOutput = audioUnitAdapter.type === AudioUnitType.Output;
|
|
27
|
+
const dependencies = Array.from(audioUnitBox.graph.dependenciesOf(audioUnitBox, {
|
|
28
|
+
alwaysFollowMandatory: true,
|
|
29
|
+
stopAtResources: true,
|
|
30
|
+
excludeBox: (box) => {
|
|
31
|
+
if (box.ephemeral) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
if (box.name === RootBox.ClassName) {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
if (box.name === AudioBusBox.ClassName) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
if (box.name === AuxSendBox.ClassName) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
if (box.name === MIDIControllerBox.ClassName) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
if (isOutput && box.name === CaptureAudioBox.ClassName) {
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
if (isOutput && box.name === CaptureMidiBox.ClassName) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}).boxes);
|
|
55
|
+
const metadata = { type: audioUnitAdapter.type };
|
|
56
|
+
const allBoxes = [audioUnitBox, ...dependencies];
|
|
57
|
+
const data = ClipboardUtils.serializeBoxes(allBoxes, encodeMetadata(metadata));
|
|
58
|
+
return Option.wrap({ type: "audio-units", data });
|
|
59
|
+
};
|
|
60
|
+
return {
|
|
61
|
+
canCopy: () => getEnabled() && getEditedAudioUnit().nonEmpty(),
|
|
62
|
+
canCut: () => {
|
|
63
|
+
if (!getEnabled()) {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
const optAudioUnit = getEditedAudioUnit();
|
|
67
|
+
if (optAudioUnit.isEmpty()) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
return !optAudioUnit.unwrap().isOutput;
|
|
71
|
+
},
|
|
72
|
+
canPaste: (entry) => getEnabled() && entry.type === "audio-units",
|
|
73
|
+
copy: copyAudioUnit,
|
|
74
|
+
cut: () => {
|
|
75
|
+
const optAudioUnit = getEditedAudioUnit();
|
|
76
|
+
if (optAudioUnit.isEmpty()) {
|
|
77
|
+
return Option.None;
|
|
78
|
+
}
|
|
79
|
+
const audioUnit = optAudioUnit.unwrap();
|
|
80
|
+
if (audioUnit.isOutput) {
|
|
81
|
+
return Option.None;
|
|
82
|
+
}
|
|
83
|
+
const result = copyAudioUnit();
|
|
84
|
+
result.ifSome(() => {
|
|
85
|
+
editing.modify(() => {
|
|
86
|
+
audioUnit.box.delete();
|
|
87
|
+
rootBoxAdapter.audioUnits.adapters()
|
|
88
|
+
.forEach((adapter, index) => adapter.indexField.setValue(index));
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
return result;
|
|
92
|
+
},
|
|
93
|
+
paste: (entry) => {
|
|
94
|
+
if (entry.type !== "audio-units" || !getEnabled()) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const metadata = decodeMetadata(ClipboardUtils.extractMetadata(entry.data));
|
|
98
|
+
const isOutputPaste = metadata.type === AudioUnitType.Output;
|
|
99
|
+
if (isOutputPaste) {
|
|
100
|
+
// Split into two transactions to ensure deletion notifications fire
|
|
101
|
+
// before new boxes are created (avoids "already has input" conflict)
|
|
102
|
+
editing.modify(() => clearOutputContent(rootBoxAdapter));
|
|
103
|
+
editing.modify(() => pasteOutputContent(entry.data, boxGraph, rootBoxAdapter));
|
|
104
|
+
}
|
|
105
|
+
else {
|
|
106
|
+
editing.modify(() => {
|
|
107
|
+
const pastedBox = pasteNewAudioUnit(entry.data, boxGraph, rootBoxAdapter, getEditedAudioUnit());
|
|
108
|
+
if (pastedBox) {
|
|
109
|
+
audioUnitEditing.edit(pastedBox.editing);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
};
|
|
116
|
+
const clearOutputContent = (rootBoxAdapter) => {
|
|
117
|
+
const outputAdapter = rootBoxAdapter.audioUnits.adapters().find(adapter => adapter.isOutput);
|
|
118
|
+
if (!outputAdapter) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
outputAdapter.tracks.collection.adapters().forEach(track => track.box.delete());
|
|
122
|
+
const inputAdapter = outputAdapter.input.adapter();
|
|
123
|
+
if (inputAdapter.nonEmpty() && inputAdapter.unwrap().type === "instrument") {
|
|
124
|
+
inputAdapter.unwrap().box.delete();
|
|
125
|
+
}
|
|
126
|
+
outputAdapter.midiEffects.adapters().forEach(effect => effect.box.delete());
|
|
127
|
+
outputAdapter.audioEffects.adapters().forEach(effect => effect.box.delete());
|
|
128
|
+
};
|
|
129
|
+
const pasteOutputContent = (data, boxGraph, rootBoxAdapter) => {
|
|
130
|
+
const outputAdapter = rootBoxAdapter.audioUnits.adapters().find(adapter => adapter.isOutput);
|
|
131
|
+
if (!outputAdapter) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const outputBox = outputAdapter.box;
|
|
135
|
+
const primaryBusAddress = rootBoxAdapter.audioBusses.adapters().at(0)?.address;
|
|
136
|
+
if (!primaryBusAddress) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
ClipboardUtils.deserializeBoxes(data, boxGraph, {
|
|
140
|
+
mapPointer: (pointer, address) => {
|
|
141
|
+
if (pointer.pointerType === Pointers.TrackCollection) {
|
|
142
|
+
return Option.wrap(outputBox.tracks.address);
|
|
143
|
+
}
|
|
144
|
+
if (pointer.pointerType === Pointers.InstrumentHost) {
|
|
145
|
+
return Option.wrap(outputBox.input.address);
|
|
146
|
+
}
|
|
147
|
+
if (pointer.pointerType === Pointers.MIDIEffectHost) {
|
|
148
|
+
return Option.wrap(outputBox.midiEffects.address);
|
|
149
|
+
}
|
|
150
|
+
if (pointer.pointerType === Pointers.AudioEffectHost) {
|
|
151
|
+
return Option.wrap(outputBox.audioEffects.address);
|
|
152
|
+
}
|
|
153
|
+
if (pointer.pointerType === Pointers.AudioOutput) {
|
|
154
|
+
return address.map(addr => addr.moveTo(primaryBusAddress.uuid));
|
|
155
|
+
}
|
|
156
|
+
return Option.None;
|
|
157
|
+
},
|
|
158
|
+
excludeBox: box => box.name === AudioUnitBox.ClassName || box.name === AudioBusBox.ClassName || box.name === RootBox.ClassName
|
|
159
|
+
});
|
|
160
|
+
};
|
|
161
|
+
const pasteNewAudioUnit = (data, boxGraph, rootBoxAdapter, currentAudioUnit) => {
|
|
162
|
+
const rootBox = rootBoxAdapter.box;
|
|
163
|
+
const primaryBusAddress = rootBoxAdapter.audioBusses.adapters().at(0)?.address;
|
|
164
|
+
if (!primaryBusAddress) {
|
|
165
|
+
return undefined;
|
|
166
|
+
}
|
|
167
|
+
const boxes = ClipboardUtils.deserializeBoxes(data, boxGraph, {
|
|
168
|
+
mapPointer: (pointer, address) => {
|
|
169
|
+
if (pointer.pointerType === Pointers.AudioUnits) {
|
|
170
|
+
return Option.wrap(rootBox.audioUnits.address);
|
|
171
|
+
}
|
|
172
|
+
if (pointer.pointerType === Pointers.AudioOutput) {
|
|
173
|
+
return address.map(addr => addr.moveTo(primaryBusAddress.uuid));
|
|
174
|
+
}
|
|
175
|
+
return Option.None;
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
const pastedAudioUnit = boxes.find(box => box.name === AudioUnitBox.ClassName);
|
|
179
|
+
if (!pastedAudioUnit) {
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
const insertAfterIndex = currentAudioUnit
|
|
183
|
+
.map(adapter => adapter.indexField.getValue())
|
|
184
|
+
.unwrapOrElse(() => -1);
|
|
185
|
+
reorderAudioUnitsAfterPaste(pastedAudioUnit, insertAfterIndex, rootBoxAdapter);
|
|
186
|
+
return pastedAudioUnit;
|
|
187
|
+
};
|
|
188
|
+
const reorderAudioUnitsAfterPaste = (pastedAudioUnit, insertAfterIndex, rootBoxAdapter) => {
|
|
189
|
+
const rootBox = rootBoxAdapter.box;
|
|
190
|
+
const allAudioUnits = IndexedBox.collectIndexedBoxes(rootBox.audioUnits, AudioUnitBox);
|
|
191
|
+
allAudioUnits.toSorted((a, b) => {
|
|
192
|
+
const orderA = AudioUnitOrdering[a.type.getValue()];
|
|
193
|
+
const orderB = AudioUnitOrdering[b.type.getValue()];
|
|
194
|
+
const orderDiff = orderA - orderB;
|
|
195
|
+
if (orderDiff !== 0) {
|
|
196
|
+
return orderDiff;
|
|
197
|
+
}
|
|
198
|
+
const aIsPasted = a === pastedAudioUnit;
|
|
199
|
+
const bIsPasted = b === pastedAudioUnit;
|
|
200
|
+
if (aIsPasted && !bIsPasted) {
|
|
201
|
+
if (insertAfterIndex === -1) {
|
|
202
|
+
return -1;
|
|
203
|
+
}
|
|
204
|
+
return b.index.getValue() <= insertAfterIndex ? 1 : -1;
|
|
205
|
+
}
|
|
206
|
+
if (bIsPasted && !aIsPasted) {
|
|
207
|
+
if (insertAfterIndex === -1) {
|
|
208
|
+
return 1;
|
|
209
|
+
}
|
|
210
|
+
return a.index.getValue() <= insertAfterIndex ? -1 : 1;
|
|
211
|
+
}
|
|
212
|
+
return a.index.getValue() - b.index.getValue();
|
|
213
|
+
}).forEach((box, index) => box.index.setValue(index));
|
|
214
|
+
};
|
|
215
|
+
})(AudioUnitsClipboard || (AudioUnitsClipboard = {}));
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Option, Provider } from "@opendaw/lib-std";
|
|
2
|
+
import { BoxEditing, BoxGraph } from "@opendaw/lib-box";
|
|
3
|
+
import { BoxAdapters, DeviceBoxAdapter, DeviceHost, FilteredSelection } from "@opendaw/studio-adapters";
|
|
4
|
+
import { ClipboardEntry, ClipboardHandler } from "../ClipboardManager";
|
|
5
|
+
type ClipboardDevices = ClipboardEntry<"devices">;
|
|
6
|
+
export declare namespace DevicesClipboard {
|
|
7
|
+
type Context = {
|
|
8
|
+
readonly getEnabled: Provider<boolean>;
|
|
9
|
+
readonly editing: BoxEditing;
|
|
10
|
+
readonly selection: FilteredSelection<DeviceBoxAdapter>;
|
|
11
|
+
readonly boxGraph: BoxGraph;
|
|
12
|
+
readonly boxAdapters: BoxAdapters;
|
|
13
|
+
readonly getHost: Provider<Option<DeviceHost>>;
|
|
14
|
+
};
|
|
15
|
+
const createHandler: ({ getEnabled, editing, selection, boxGraph, boxAdapters, getHost }: Context) => ClipboardHandler<ClipboardDevices>;
|
|
16
|
+
}
|
|
17
|
+
export {};
|
|
18
|
+
//# sourceMappingURL=DevicesClipboardHandler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DevicesClipboardHandler.d.ts","sourceRoot":"","sources":["../../../../src/ui/clipboard/types/DevicesClipboardHandler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkD,MAAM,EAAY,QAAQ,EAAkB,MAAM,kBAAkB,CAAA;AAC7H,OAAO,EAAM,UAAU,EAAE,QAAQ,EAAC,MAAM,kBAAkB,CAAA;AAE1D,OAAO,EAEH,WAAW,EACX,gBAAgB,EAEhB,UAAU,EAGV,iBAAiB,EAGpB,MAAM,0BAA0B,CAAA;AACjC,OAAO,EAAC,cAAc,EAAE,gBAAgB,EAAC,MAAM,qBAAqB,CAAA;AAGpE,KAAK,gBAAgB,GAAG,cAAc,CAAC,SAAS,CAAC,CAAA;AAajD,yBAAiB,gBAAgB,CAAC;IAC9B,KAAY,OAAO,GAAG;QAClB,QAAQ,CAAC,UAAU,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAA;QACtC,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAA;QAC5B,QAAQ,CAAC,SAAS,EAAE,iBAAiB,CAAC,gBAAgB,CAAC,CAAA;QACvD,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAA;QAC3B,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAA;QACjC,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAA;KACjD,CAAA;IAyBM,MAAM,aAAa,GAAI,oEAOG,OAAO,KAAG,gBAAgB,CAAC,gBAAgB,CA0J3E,CAAA;CACJ"}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { ByteArrayInput, ByteArrayOutput, isDefined, Option, RuntimeNotifier } from "@opendaw/lib-std";
|
|
2
|
+
import { Pointers } from "@opendaw/studio-enums";
|
|
3
|
+
import { DeviceBoxUtils, Devices } from "@opendaw/studio-adapters";
|
|
4
|
+
import { ClipboardUtils } from "../ClipboardUtils";
|
|
5
|
+
export var DevicesClipboard;
|
|
6
|
+
(function (DevicesClipboard) {
|
|
7
|
+
const encodeMetadata = (metadata) => {
|
|
8
|
+
const output = ByteArrayOutput.create();
|
|
9
|
+
output.writeBoolean(metadata.hasInstrument);
|
|
10
|
+
output.writeString(metadata.instrumentContent);
|
|
11
|
+
output.writeInt(metadata.midiEffectCount);
|
|
12
|
+
output.writeInt(metadata.midiEffectMaxIndex);
|
|
13
|
+
output.writeInt(metadata.audioEffectCount);
|
|
14
|
+
output.writeInt(metadata.audioEffectMaxIndex);
|
|
15
|
+
return output.toArrayBuffer();
|
|
16
|
+
};
|
|
17
|
+
const decodeMetadata = (buffer) => {
|
|
18
|
+
const input = new ByteArrayInput(buffer);
|
|
19
|
+
return {
|
|
20
|
+
hasInstrument: input.readBoolean(),
|
|
21
|
+
instrumentContent: input.readString(),
|
|
22
|
+
midiEffectCount: input.readInt(),
|
|
23
|
+
midiEffectMaxIndex: input.readInt(),
|
|
24
|
+
audioEffectCount: input.readInt(),
|
|
25
|
+
audioEffectMaxIndex: input.readInt()
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
DevicesClipboard.createHandler = ({ getEnabled, editing, selection, boxGraph, boxAdapters, getHost }) => {
|
|
29
|
+
const isCopyable = (adapter) => adapter.box.tags.copyable !== false;
|
|
30
|
+
const copyableSelected = () => selection.selected().filter(isCopyable);
|
|
31
|
+
const copyDevices = () => {
|
|
32
|
+
const selected = copyableSelected();
|
|
33
|
+
if (selected.length === 0) {
|
|
34
|
+
return Option.None;
|
|
35
|
+
}
|
|
36
|
+
let instrument = null;
|
|
37
|
+
const midiEffects = [];
|
|
38
|
+
const audioEffects = [];
|
|
39
|
+
for (const adapter of selected) {
|
|
40
|
+
if (adapter.type === "instrument") {
|
|
41
|
+
instrument = adapter;
|
|
42
|
+
}
|
|
43
|
+
else if (adapter.type === "midi-effect") {
|
|
44
|
+
midiEffects.push(adapter);
|
|
45
|
+
}
|
|
46
|
+
else if (adapter.type === "audio-effect") {
|
|
47
|
+
audioEffects.push(adapter);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (instrument === null && midiEffects.length === 0 && audioEffects.length === 0) {
|
|
51
|
+
return Option.None;
|
|
52
|
+
}
|
|
53
|
+
midiEffects.sort((a, b) => a.indexField.getValue() - b.indexField.getValue());
|
|
54
|
+
audioEffects.sort((a, b) => a.indexField.getValue() - b.indexField.getValue());
|
|
55
|
+
const midiEffectMaxIndex = midiEffects.length > 0
|
|
56
|
+
? midiEffects[midiEffects.length - 1].indexField.getValue()
|
|
57
|
+
: 0;
|
|
58
|
+
const audioEffectMaxIndex = audioEffects.length > 0
|
|
59
|
+
? audioEffects[audioEffects.length - 1].indexField.getValue()
|
|
60
|
+
: 0;
|
|
61
|
+
const deviceBoxes = [
|
|
62
|
+
...(instrument !== null ? [instrument.box] : []),
|
|
63
|
+
...midiEffects.map(adapter => adapter.box),
|
|
64
|
+
...audioEffects.map(adapter => adapter.box)
|
|
65
|
+
];
|
|
66
|
+
const dependencies = deviceBoxes.flatMap(box => Array.from(boxGraph.dependenciesOf(box, {
|
|
67
|
+
alwaysFollowMandatory: true,
|
|
68
|
+
excludeBox: (dep) => dep.ephemeral || DeviceBoxUtils.isDeviceBox(dep)
|
|
69
|
+
}).boxes).filter(dep => dep.resource === "external"));
|
|
70
|
+
const allBoxes = [...deviceBoxes, ...dependencies];
|
|
71
|
+
const instrumentContent = instrument !== null
|
|
72
|
+
? instrument.box.tags.content ?? ""
|
|
73
|
+
: "";
|
|
74
|
+
const metadata = {
|
|
75
|
+
hasInstrument: instrument !== null,
|
|
76
|
+
instrumentContent,
|
|
77
|
+
midiEffectCount: midiEffects.length,
|
|
78
|
+
midiEffectMaxIndex,
|
|
79
|
+
audioEffectCount: audioEffects.length,
|
|
80
|
+
audioEffectMaxIndex
|
|
81
|
+
};
|
|
82
|
+
const data = ClipboardUtils.serializeBoxes(allBoxes, encodeMetadata(metadata));
|
|
83
|
+
return Option.wrap({ type: "devices", data });
|
|
84
|
+
};
|
|
85
|
+
return {
|
|
86
|
+
canCopy: () => getEnabled() && copyableSelected().length > 0,
|
|
87
|
+
canCut: () => getEnabled() && copyableSelected().length > 0,
|
|
88
|
+
canPaste: (entry) => getEnabled() && entry.type === "devices",
|
|
89
|
+
copy: copyDevices,
|
|
90
|
+
cut: () => {
|
|
91
|
+
const result = copyDevices();
|
|
92
|
+
result.ifSome(() => {
|
|
93
|
+
const optHost = getHost();
|
|
94
|
+
if (optHost.isEmpty()) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
const host = optHost.unwrap();
|
|
98
|
+
const selected = new Set(selection.selected().filter(adapter => adapter.type !== "instrument"));
|
|
99
|
+
const remainingMidi = host.midiEffects.adapters().filter(adapter => !selected.has(adapter));
|
|
100
|
+
const remainingAudio = host.audioEffects.adapters().filter(adapter => !selected.has(adapter));
|
|
101
|
+
editing.modify(() => {
|
|
102
|
+
selected.forEach(adapter => adapter.box.delete());
|
|
103
|
+
remainingMidi.forEach((adapter, index) => adapter.indexField.setValue(index));
|
|
104
|
+
remainingAudio.forEach((adapter, index) => adapter.indexField.setValue(index));
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
return result;
|
|
108
|
+
},
|
|
109
|
+
paste: (entry) => {
|
|
110
|
+
if (entry.type !== "devices" || !getEnabled()) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
const optHost = getHost();
|
|
114
|
+
if (optHost.isEmpty()) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const host = optHost.unwrap();
|
|
118
|
+
const metadata = decodeMetadata(ClipboardUtils.extractMetadata(entry.data));
|
|
119
|
+
const selected = selection.selected();
|
|
120
|
+
const selectedInstrument = selected.find(adapter => adapter.type === "instrument");
|
|
121
|
+
const selectedMidiEffects = selected.filter(adapter => adapter.type === "midi-effect");
|
|
122
|
+
const selectedAudioEffects = selected.filter(adapter => adapter.type === "audio-effect");
|
|
123
|
+
let replaceInstrument = metadata.hasInstrument && isDefined(selectedInstrument)
|
|
124
|
+
&& selectedInstrument.box.tags.copyable !== false;
|
|
125
|
+
if (replaceInstrument && isDefined(selectedInstrument)) {
|
|
126
|
+
const selectedContent = selectedInstrument.box.tags.content;
|
|
127
|
+
if (isDefined(selectedContent) && metadata.instrumentContent !== ""
|
|
128
|
+
&& selectedContent !== metadata.instrumentContent) {
|
|
129
|
+
RuntimeNotifier.info({
|
|
130
|
+
headline: "Incompatible Instrument",
|
|
131
|
+
message: `Cannot replace a '${selectedContent}' instrument with a '${metadata.instrumentContent}' instrument.`
|
|
132
|
+
}).finally();
|
|
133
|
+
replaceInstrument = false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const midiInsertIndex = selectedMidiEffects.length > 0
|
|
137
|
+
? selectedMidiEffects.reduce((max, adapter) => Math.max(max, adapter.indexField.getValue()), -1) + 1
|
|
138
|
+
: 0;
|
|
139
|
+
const audioInsertIndex = selectedAudioEffects.length > 0
|
|
140
|
+
? selectedAudioEffects.reduce((max, adapter) => Math.max(max, adapter.indexField.getValue()), -1) + 1
|
|
141
|
+
: 0;
|
|
142
|
+
editing.modify(() => {
|
|
143
|
+
selection.deselectAll();
|
|
144
|
+
if (replaceInstrument && isDefined(selectedInstrument)) {
|
|
145
|
+
selectedInstrument.box.delete();
|
|
146
|
+
}
|
|
147
|
+
for (const adapter of host.midiEffects.adapters()) {
|
|
148
|
+
const currentIndex = adapter.indexField.getValue();
|
|
149
|
+
if (currentIndex >= midiInsertIndex) {
|
|
150
|
+
adapter.indexField.setValue(currentIndex + metadata.midiEffectCount);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
for (const adapter of host.audioEffects.adapters()) {
|
|
154
|
+
const currentIndex = adapter.indexField.getValue();
|
|
155
|
+
if (currentIndex >= audioInsertIndex) {
|
|
156
|
+
adapter.indexField.setValue(currentIndex + metadata.audioEffectCount);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const boxes = ClipboardUtils.deserializeBoxes(entry.data, boxGraph, {
|
|
160
|
+
mapPointer: pointer => {
|
|
161
|
+
if (pointer.pointerType === Pointers.InstrumentHost && replaceInstrument) {
|
|
162
|
+
return Option.wrap(host.inputField.address);
|
|
163
|
+
}
|
|
164
|
+
if (pointer.pointerType === Pointers.MIDIEffectHost) {
|
|
165
|
+
return Option.wrap(host.midiEffectsField.address);
|
|
166
|
+
}
|
|
167
|
+
if (pointer.pointerType === Pointers.AudioEffectHost) {
|
|
168
|
+
return Option.wrap(host.audioEffectsField.address);
|
|
169
|
+
}
|
|
170
|
+
return Option.None;
|
|
171
|
+
},
|
|
172
|
+
excludeBox: box => DeviceBoxUtils.isInstrumentDeviceBox(box) && !replaceInstrument
|
|
173
|
+
});
|
|
174
|
+
const deviceBoxes = boxes.filter(box => DeviceBoxUtils.isDeviceBox(box));
|
|
175
|
+
const newMidiEffects = deviceBoxes
|
|
176
|
+
.filter((box) => DeviceBoxUtils.isEffectDeviceBox(box) && box.tags.deviceType === "midi-effect")
|
|
177
|
+
.sort((a, b) => a.index.getValue() - b.index.getValue());
|
|
178
|
+
const newAudioEffects = deviceBoxes
|
|
179
|
+
.filter((box) => DeviceBoxUtils.isEffectDeviceBox(box) && box.tags.deviceType === "audio-effect")
|
|
180
|
+
.sort((a, b) => a.index.getValue() - b.index.getValue());
|
|
181
|
+
newMidiEffects.forEach((box, idx) => box.index.setValue(midiInsertIndex + idx));
|
|
182
|
+
newAudioEffects.forEach((box, idx) => box.index.setValue(audioInsertIndex + idx));
|
|
183
|
+
selection.select(...deviceBoxes.map(box => boxAdapters.adapterFor(box, Devices.isAny)));
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
};
|
|
188
|
+
})(DevicesClipboard || (DevicesClipboard = {}));
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { Procedure, Provider, Selection } from "@opendaw/lib-std";
|
|
2
|
+
import { Address, BoxEditing, BoxGraph } from "@opendaw/lib-box";
|
|
3
|
+
import { ppqn } from "@opendaw/lib-dsp";
|
|
4
|
+
import { BoxAdapters, NoteEventBoxAdapter } from "@opendaw/studio-adapters";
|
|
5
|
+
import { ClipboardEntry, ClipboardHandler } from "../ClipboardManager";
|
|
6
|
+
type ClipboardNotes = ClipboardEntry<"notes">;
|
|
7
|
+
export declare namespace NotesClipboard {
|
|
8
|
+
type Context = {
|
|
9
|
+
readonly getEnabled: Provider<boolean>;
|
|
10
|
+
readonly getPosition: Provider<ppqn>;
|
|
11
|
+
readonly setPosition: Procedure<ppqn>;
|
|
12
|
+
readonly editing: BoxEditing;
|
|
13
|
+
readonly selection: Selection<NoteEventBoxAdapter>;
|
|
14
|
+
readonly targetAddress: Address;
|
|
15
|
+
readonly boxGraph: BoxGraph;
|
|
16
|
+
readonly boxAdapters: BoxAdapters;
|
|
17
|
+
};
|
|
18
|
+
const createHandler: ({ getEnabled, getPosition, setPosition, editing, selection, targetAddress, boxGraph, boxAdapters }: Context) => ClipboardHandler<ClipboardNotes>;
|
|
19
|
+
}
|
|
20
|
+
export {};
|
|
21
|
+
//# sourceMappingURL=NotesClipboardHandler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NotesClipboardHandler.d.ts","sourceRoot":"","sources":["../../../../src/ui/clipboard/types/NotesClipboardHandler.ts"],"names":[],"mappings":"AAAA,OAAO,EAA0C,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAC,MAAM,kBAAkB,CAAA;AACxG,OAAO,EAAC,OAAO,EAAO,UAAU,EAAE,QAAQ,EAAC,MAAM,kBAAkB,CAAA;AACnE,OAAO,EAAC,IAAI,EAAC,MAAM,kBAAkB,CAAA;AAGrC,OAAO,EAAC,WAAW,EAAE,mBAAmB,EAAC,MAAM,0BAA0B,CAAA;AACzE,OAAO,EAAC,cAAc,EAAE,gBAAgB,EAAC,MAAM,qBAAqB,CAAA;AAGpE,KAAK,cAAc,GAAG,cAAc,CAAC,OAAO,CAAC,CAAA;AAE7C,yBAAiB,cAAc,CAAC;IAC5B,KAAY,OAAO,GAAG;QAClB,QAAQ,CAAC,UAAU,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAA;QACtC,QAAQ,CAAC,WAAW,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAA;QACpC,QAAQ,CAAC,WAAW,EAAE,SAAS,CAAC,IAAI,CAAC,CAAA;QACrC,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAA;QAC5B,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC,mBAAmB,CAAC,CAAA;QAClD,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAA;QAC/B,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAA;QAC3B,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAA;KACpC,CAAA;IAcM,MAAM,aAAa,GAAI,oGASG,OAAO,KAAG,gBAAgB,CAAC,cAAc,CAsDzE,CAAA;CACJ"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { ByteArrayInput, ByteArrayOutput, Option } from "@opendaw/lib-std";
|
|
2
|
+
import { Pointers } from "@opendaw/studio-enums";
|
|
3
|
+
import { NoteEventBox } from "@opendaw/studio-boxes";
|
|
4
|
+
import { NoteEventBoxAdapter } from "@opendaw/studio-adapters";
|
|
5
|
+
import { ClipboardUtils } from "../ClipboardUtils";
|
|
6
|
+
export var NotesClipboard;
|
|
7
|
+
(function (NotesClipboard) {
|
|
8
|
+
const encodeMetadata = (min, max) => {
|
|
9
|
+
const output = ByteArrayOutput.create();
|
|
10
|
+
output.writeFloat(min);
|
|
11
|
+
output.writeFloat(max);
|
|
12
|
+
return output.toArrayBuffer();
|
|
13
|
+
};
|
|
14
|
+
const decodeMetadata = (buffer) => {
|
|
15
|
+
const input = new ByteArrayInput(buffer);
|
|
16
|
+
return { min: input.readFloat(), max: input.readFloat() };
|
|
17
|
+
};
|
|
18
|
+
NotesClipboard.createHandler = ({ getEnabled, getPosition, setPosition, editing, selection, targetAddress, boxGraph, boxAdapters }) => {
|
|
19
|
+
const copyNotes = () => {
|
|
20
|
+
const selected = selection.selected();
|
|
21
|
+
if (selected.length === 0) {
|
|
22
|
+
return Option.None;
|
|
23
|
+
}
|
|
24
|
+
const min = selected.reduce((acc, { position }) => Math.min(acc, position), Number.POSITIVE_INFINITY);
|
|
25
|
+
const max = selected.reduce((acc, { complete }) => Math.max(acc, complete), Number.NEGATIVE_INFINITY);
|
|
26
|
+
const eventBoxes = selected.map(adapter => adapter.box);
|
|
27
|
+
const dependencies = eventBoxes.flatMap(box => Array.from(boxGraph.dependenciesOf(box, {
|
|
28
|
+
alwaysFollowMandatory: true,
|
|
29
|
+
excludeBox: (dep) => dep.ephemeral
|
|
30
|
+
}).boxes));
|
|
31
|
+
const allBoxes = [...eventBoxes, ...dependencies];
|
|
32
|
+
const data = ClipboardUtils.serializeBoxes(allBoxes, encodeMetadata(min, max));
|
|
33
|
+
setPosition(max);
|
|
34
|
+
return Option.wrap({ type: "notes", data });
|
|
35
|
+
};
|
|
36
|
+
return {
|
|
37
|
+
canCopy: () => getEnabled() && selection.nonEmpty(),
|
|
38
|
+
canCut: () => getEnabled() && selection.nonEmpty(),
|
|
39
|
+
canPaste: (entry) => getEnabled() && entry.type === "notes",
|
|
40
|
+
copy: copyNotes,
|
|
41
|
+
cut: () => {
|
|
42
|
+
const result = copyNotes();
|
|
43
|
+
result.ifSome(() => editing.modify(() => selection.selected().forEach(adapter => adapter.box.delete())));
|
|
44
|
+
return result;
|
|
45
|
+
},
|
|
46
|
+
paste: (entry) => {
|
|
47
|
+
if (entry.type !== "notes" || !getEnabled()) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const position = getPosition();
|
|
51
|
+
const { min, max } = decodeMetadata(ClipboardUtils.extractMetadata(entry.data));
|
|
52
|
+
const positionOffset = Math.max(0, position) - min;
|
|
53
|
+
editing.modify(() => {
|
|
54
|
+
selection.deselectAll();
|
|
55
|
+
const boxes = ClipboardUtils.deserializeBoxes(entry.data, boxGraph, {
|
|
56
|
+
mapPointer: pointer => pointer.pointerType === Pointers.NoteEvents
|
|
57
|
+
? Option.wrap(targetAddress)
|
|
58
|
+
: Option.None,
|
|
59
|
+
modifyBox: box => {
|
|
60
|
+
if (box instanceof NoteEventBox) {
|
|
61
|
+
box.position.setValue(box.position.getValue() + positionOffset);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
const noteEventBoxes = boxes.filter((box) => box instanceof NoteEventBox);
|
|
66
|
+
selection.select(...noteEventBoxes.map(box => boxAdapters.adapterFor(box, NoteEventBoxAdapter)));
|
|
67
|
+
setPosition(Math.max(0, position) + (max - min));
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
};
|
|
72
|
+
})(NotesClipboard || (NotesClipboard = {}));
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { Option, Procedure, Provider, Selection } from "@opendaw/lib-std";
|
|
2
|
+
import { BoxEditing, BoxGraph } from "@opendaw/lib-box";
|
|
3
|
+
import { ppqn } from "@opendaw/lib-dsp";
|
|
4
|
+
import { AnyRegionBoxAdapter, BoxAdapters, TrackBoxAdapter } from "@opendaw/studio-adapters";
|
|
5
|
+
import { ClipboardEntry, ClipboardHandler } from "../ClipboardManager";
|
|
6
|
+
import { RegionOverlapResolver } from "../../timeline/RegionOverlapResolver";
|
|
7
|
+
type ClipboardRegions = ClipboardEntry<"regions">;
|
|
8
|
+
export declare namespace RegionsClipboard {
|
|
9
|
+
type Context = {
|
|
10
|
+
readonly getEnabled: Provider<boolean>;
|
|
11
|
+
readonly getPosition: Provider<ppqn>;
|
|
12
|
+
readonly setPosition: Procedure<ppqn>;
|
|
13
|
+
readonly editing: BoxEditing;
|
|
14
|
+
readonly selection: Selection<AnyRegionBoxAdapter>;
|
|
15
|
+
readonly boxGraph: BoxGraph;
|
|
16
|
+
readonly boxAdapters: BoxAdapters;
|
|
17
|
+
readonly getTracks: Provider<ReadonlyArray<TrackBoxAdapter>>;
|
|
18
|
+
readonly getFocusedTrack: Provider<Option<TrackBoxAdapter>>;
|
|
19
|
+
readonly overlapResolver: RegionOverlapResolver;
|
|
20
|
+
};
|
|
21
|
+
const createHandler: ({ getEnabled, getPosition, setPosition, editing, selection, boxGraph, boxAdapters, getTracks, getFocusedTrack, overlapResolver }: Context) => ClipboardHandler<ClipboardRegions>;
|
|
22
|
+
}
|
|
23
|
+
export {};
|
|
24
|
+
//# sourceMappingURL=RegionsClipboardHandler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RegionsClipboardHandler.d.ts","sourceRoot":"","sources":["../../../../src/ui/clipboard/types/RegionsClipboardHandler.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkC,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,SAAS,EAAO,MAAM,kBAAkB,CAAA;AAC9G,OAAO,EAAM,UAAU,EAAE,QAAQ,EAAC,MAAM,kBAAkB,CAAA;AAC1D,OAAO,EAAC,IAAI,EAAC,MAAM,kBAAkB,CAAA;AAErC,OAAO,EACH,mBAAmB,EACnB,WAAW,EAEX,eAAe,EAGlB,MAAM,0BAA0B,CAAA;AACjC,OAAO,EAAC,cAAc,EAAE,gBAAgB,EAAC,MAAM,qBAAqB,CAAA;AAEpE,OAAO,EAAC,qBAAqB,EAAC,MAAM,sCAAsC,CAAA;AAE1E,KAAK,gBAAgB,GAAG,cAAc,CAAC,SAAS,CAAC,CAAA;AAcjD,yBAAiB,gBAAgB,CAAC;IAC9B,KAAY,OAAO,GAAG;QAClB,QAAQ,CAAC,UAAU,EAAE,QAAQ,CAAC,OAAO,CAAC,CAAA;QACtC,QAAQ,CAAC,WAAW,EAAE,QAAQ,CAAC,IAAI,CAAC,CAAA;QACpC,QAAQ,CAAC,WAAW,EAAE,SAAS,CAAC,IAAI,CAAC,CAAA;QACrC,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAA;QAC5B,QAAQ,CAAC,SAAS,EAAE,SAAS,CAAC,mBAAmB,CAAC,CAAA;QAClD,QAAQ,CAAC,QAAQ,EAAE,QAAQ,CAAA;QAC3B,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAA;QACjC,QAAQ,CAAC,SAAS,EAAE,QAAQ,CAAC,aAAa,CAAC,eAAe,CAAC,CAAC,CAAA;QAC5D,QAAQ,CAAC,eAAe,EAAE,QAAQ,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC,CAAA;QAC3D,QAAQ,CAAC,eAAe,EAAE,qBAAqB,CAAA;KAClD,CAAA;IA+BM,MAAM,aAAa,GAAI,kIAWG,OAAO,KAAG,gBAAgB,CAAC,gBAAgB,CAgG3E,CAAA;CACJ"}
|