@aippy/runtime 0.2.0-dev.5 → 0.2.1
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/audio/index.js +7 -7
- package/dist/core/index.js +29 -43
- package/dist/device/index.js +208 -270
- package/dist/errors-CDEBaBxB.js +26 -0
- package/dist/index/index.js +38 -38
- package/dist/pwa-DPd78fwK.js +342 -0
- package/dist/tweaks/index.js +3 -3
- package/dist/useAudioContext-DaLkaQ8P.js +223 -0
- package/dist/useTweaks-Bc26i-fJ.js +195 -0
- package/dist/utils/index.js +7 -7
- package/package.json +1 -1
- package/dist/errors-DAz5_jDJ.js +0 -25
- package/dist/pwa-BkviTQoN.js +0 -408
- package/dist/useAudioContext-D9Y4gIw9.js +0 -358
- package/dist/useTweaks-mK5PAWOs.js +0 -258
|
@@ -1,358 +0,0 @@
|
|
|
1
|
-
import { useState, useRef, useEffect } from "react";
|
|
2
|
-
function isIOSDevice() {
|
|
3
|
-
const userAgent = navigator.userAgent;
|
|
4
|
-
if (/iPad|iPhone|iPod/.test(userAgent)) {
|
|
5
|
-
return true;
|
|
6
|
-
}
|
|
7
|
-
if (navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1) {
|
|
8
|
-
return true;
|
|
9
|
-
}
|
|
10
|
-
return false;
|
|
11
|
-
}
|
|
12
|
-
function isMediaStreamAudioSupported() {
|
|
13
|
-
try {
|
|
14
|
-
if (!window.AudioContext) {
|
|
15
|
-
return false;
|
|
16
|
-
}
|
|
17
|
-
const tempContext = new AudioContext();
|
|
18
|
-
const hasMethod = typeof tempContext.createMediaStreamDestination === "function";
|
|
19
|
-
tempContext.close();
|
|
20
|
-
return hasMethod;
|
|
21
|
-
} catch {
|
|
22
|
-
return false;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
function createHiddenMediaElement(type = "video", debug = false) {
|
|
26
|
-
const element = document.createElement(type);
|
|
27
|
-
element.muted = false;
|
|
28
|
-
element.autoplay = true;
|
|
29
|
-
if (type === "video") {
|
|
30
|
-
element.playsInline = true;
|
|
31
|
-
}
|
|
32
|
-
if (debug) {
|
|
33
|
-
element.style.cssText = "position:fixed;bottom:10px;right:10px;width:200px;background:#ff0000;z-index:9999;";
|
|
34
|
-
} else {
|
|
35
|
-
element.style.cssText = "position:fixed;width:1px;height:1px;opacity:0;pointer-events:none;";
|
|
36
|
-
}
|
|
37
|
-
return element;
|
|
38
|
-
}
|
|
39
|
-
function createHiddenVideoElement(debug = false) {
|
|
40
|
-
return createHiddenMediaElement("video", debug);
|
|
41
|
-
}
|
|
42
|
-
class AudioSilenceDetector {
|
|
43
|
-
constructor(audioContext, mediaElement, options, debug = false) {
|
|
44
|
-
this.audioContext = audioContext;
|
|
45
|
-
this.mediaElement = mediaElement;
|
|
46
|
-
this.silenceThreshold = options.silenceThreshold;
|
|
47
|
-
this.silenceDuration = options.silenceDuration;
|
|
48
|
-
this.checkInterval = options.checkInterval;
|
|
49
|
-
this.debug = debug;
|
|
50
|
-
this.analyser = audioContext.createAnalyser();
|
|
51
|
-
this.analyser.fftSize = 512;
|
|
52
|
-
this.analyser.smoothingTimeConstant = 0.3;
|
|
53
|
-
this.dataArray = new Uint8Array(this.analyser.frequencyBinCount);
|
|
54
|
-
}
|
|
55
|
-
analyser;
|
|
56
|
-
dataArray;
|
|
57
|
-
rafId = null;
|
|
58
|
-
silenceStartTime = 0;
|
|
59
|
-
isPaused = false;
|
|
60
|
-
lastCheckTime = 0;
|
|
61
|
-
silenceThreshold;
|
|
62
|
-
silenceDuration;
|
|
63
|
-
checkInterval;
|
|
64
|
-
debug;
|
|
65
|
-
/**
|
|
66
|
-
* Connect the detector to the audio stream
|
|
67
|
-
*/
|
|
68
|
-
connect(source) {
|
|
69
|
-
source.connect(this.analyser);
|
|
70
|
-
}
|
|
71
|
-
/**
|
|
72
|
-
* Start monitoring audio levels
|
|
73
|
-
*/
|
|
74
|
-
start() {
|
|
75
|
-
if (this.rafId !== null) return;
|
|
76
|
-
this.lastCheckTime = performance.now();
|
|
77
|
-
this.check();
|
|
78
|
-
if (this.debug) {
|
|
79
|
-
console.log("[AudioSilenceDetector] Started monitoring");
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* Stop monitoring
|
|
84
|
-
*/
|
|
85
|
-
stop() {
|
|
86
|
-
if (this.rafId !== null) {
|
|
87
|
-
cancelAnimationFrame(this.rafId);
|
|
88
|
-
this.rafId = null;
|
|
89
|
-
}
|
|
90
|
-
if (this.debug) {
|
|
91
|
-
console.log("[AudioSilenceDetector] Stopped monitoring");
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
/**
|
|
95
|
-
* Cleanup resources
|
|
96
|
-
*/
|
|
97
|
-
dispose() {
|
|
98
|
-
this.stop();
|
|
99
|
-
this.analyser.disconnect();
|
|
100
|
-
}
|
|
101
|
-
/**
|
|
102
|
-
* Check audio levels and pause/resume as needed
|
|
103
|
-
*/
|
|
104
|
-
check = () => {
|
|
105
|
-
const now = performance.now();
|
|
106
|
-
const elapsed = now - this.lastCheckTime;
|
|
107
|
-
if (elapsed >= this.checkInterval) {
|
|
108
|
-
this.lastCheckTime = now;
|
|
109
|
-
const volume = this.getAudioLevel();
|
|
110
|
-
if (volume < this.silenceThreshold) {
|
|
111
|
-
if (this.silenceStartTime === 0) {
|
|
112
|
-
this.silenceStartTime = now;
|
|
113
|
-
} else {
|
|
114
|
-
const silenceDuration = now - this.silenceStartTime;
|
|
115
|
-
if (silenceDuration >= this.silenceDuration && !this.isPaused) {
|
|
116
|
-
this.pauseMedia();
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
} else {
|
|
120
|
-
this.silenceStartTime = 0;
|
|
121
|
-
if (this.isPaused) {
|
|
122
|
-
this.resumeMedia();
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
this.rafId = requestAnimationFrame(this.check);
|
|
127
|
-
};
|
|
128
|
-
/**
|
|
129
|
-
* Get current audio level (0-1)
|
|
130
|
-
*/
|
|
131
|
-
getAudioLevel() {
|
|
132
|
-
this.analyser.getByteTimeDomainData(this.dataArray);
|
|
133
|
-
let sum = 0;
|
|
134
|
-
for (let i = 0; i < this.dataArray.length; i++) {
|
|
135
|
-
const normalized = (this.dataArray[i] - 128) / 128;
|
|
136
|
-
sum += normalized * normalized;
|
|
137
|
-
}
|
|
138
|
-
return Math.sqrt(sum / this.dataArray.length);
|
|
139
|
-
}
|
|
140
|
-
/**
|
|
141
|
-
* Pause media element to stop audio output
|
|
142
|
-
*/
|
|
143
|
-
pauseMedia() {
|
|
144
|
-
try {
|
|
145
|
-
this.mediaElement.pause();
|
|
146
|
-
this.isPaused = true;
|
|
147
|
-
if (this.debug) {
|
|
148
|
-
console.log("[AudioSilenceDetector] Paused media element (silence detected)");
|
|
149
|
-
}
|
|
150
|
-
} catch (error) {
|
|
151
|
-
console.error("[AudioSilenceDetector] Failed to pause:", error);
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* Resume media element playback
|
|
156
|
-
*/
|
|
157
|
-
resumeMedia() {
|
|
158
|
-
try {
|
|
159
|
-
if (this.audioContext.state === "running") {
|
|
160
|
-
this.mediaElement.play().catch((error) => {
|
|
161
|
-
if (this.debug) {
|
|
162
|
-
console.warn("[AudioSilenceDetector] Failed to resume:", error);
|
|
163
|
-
}
|
|
164
|
-
});
|
|
165
|
-
}
|
|
166
|
-
this.isPaused = false;
|
|
167
|
-
if (this.debug) {
|
|
168
|
-
console.log("[AudioSilenceDetector] Resumed media element (audio detected)");
|
|
169
|
-
}
|
|
170
|
-
} catch (error) {
|
|
171
|
-
console.error("[AudioSilenceDetector] Failed to resume:", error);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
function patchAudioContext(audioContext, options = {}) {
|
|
176
|
-
const {
|
|
177
|
-
forceEnable = false,
|
|
178
|
-
autoCleanup = true,
|
|
179
|
-
debug = false,
|
|
180
|
-
mediaElementType = "video",
|
|
181
|
-
autoPause = {}
|
|
182
|
-
} = options;
|
|
183
|
-
const autoPauseOptions = {
|
|
184
|
-
enabled: autoPause.enabled ?? true,
|
|
185
|
-
silenceThreshold: autoPause.silenceThreshold ?? 1e-3,
|
|
186
|
-
silenceDuration: autoPause.silenceDuration ?? 50,
|
|
187
|
-
checkInterval: autoPause.checkInterval ?? 16
|
|
188
|
-
};
|
|
189
|
-
const needsPatch = forceEnable || isIOSDevice();
|
|
190
|
-
if (!needsPatch) {
|
|
191
|
-
return Object.assign(audioContext, {
|
|
192
|
-
unlock: async () => {
|
|
193
|
-
if (audioContext.state === "suspended") {
|
|
194
|
-
await audioContext.resume();
|
|
195
|
-
}
|
|
196
|
-
},
|
|
197
|
-
cleanup: () => {
|
|
198
|
-
},
|
|
199
|
-
isPatched: false,
|
|
200
|
-
originalDestination: audioContext.destination
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
if (!isMediaStreamAudioSupported()) {
|
|
204
|
-
console.warn(
|
|
205
|
-
"[AudioContext] MediaStreamAudioDestinationNode not supported, falling back to native"
|
|
206
|
-
);
|
|
207
|
-
return Object.assign(audioContext, {
|
|
208
|
-
unlock: async () => audioContext.resume(),
|
|
209
|
-
cleanup: () => {
|
|
210
|
-
},
|
|
211
|
-
isPatched: false,
|
|
212
|
-
originalDestination: audioContext.destination
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
const originalDestination = audioContext.destination;
|
|
216
|
-
const streamDestination = audioContext.createMediaStreamDestination();
|
|
217
|
-
const gainNode = audioContext.createGain();
|
|
218
|
-
gainNode.gain.value = 1;
|
|
219
|
-
gainNode.connect(streamDestination);
|
|
220
|
-
const mediaElement = createHiddenMediaElement(mediaElementType, debug);
|
|
221
|
-
mediaElement.srcObject = streamDestination.stream;
|
|
222
|
-
document.body.appendChild(mediaElement);
|
|
223
|
-
let silenceDetector = null;
|
|
224
|
-
if (autoPauseOptions.enabled) {
|
|
225
|
-
silenceDetector = new AudioSilenceDetector(
|
|
226
|
-
audioContext,
|
|
227
|
-
mediaElement,
|
|
228
|
-
autoPauseOptions,
|
|
229
|
-
debug
|
|
230
|
-
);
|
|
231
|
-
silenceDetector.connect(gainNode);
|
|
232
|
-
}
|
|
233
|
-
Object.defineProperty(audioContext, "destination", {
|
|
234
|
-
get: () => gainNode,
|
|
235
|
-
enumerable: true,
|
|
236
|
-
configurable: true
|
|
237
|
-
});
|
|
238
|
-
if (!("maxChannelCount" in gainNode)) {
|
|
239
|
-
Object.defineProperty(gainNode, "maxChannelCount", {
|
|
240
|
-
get: () => originalDestination.maxChannelCount,
|
|
241
|
-
enumerable: true
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
let isUnlocked = false;
|
|
245
|
-
const unlock = async () => {
|
|
246
|
-
if (isUnlocked) {
|
|
247
|
-
return;
|
|
248
|
-
}
|
|
249
|
-
try {
|
|
250
|
-
await mediaElement.play();
|
|
251
|
-
if (audioContext.state === "suspended") {
|
|
252
|
-
await audioContext.resume();
|
|
253
|
-
}
|
|
254
|
-
if (silenceDetector) {
|
|
255
|
-
silenceDetector.start();
|
|
256
|
-
}
|
|
257
|
-
isUnlocked = true;
|
|
258
|
-
if (debug) {
|
|
259
|
-
console.log("[AudioContext] iOS unlock successful");
|
|
260
|
-
}
|
|
261
|
-
} catch (error) {
|
|
262
|
-
if (error instanceof DOMException && error.name === "NotAllowedError") {
|
|
263
|
-
if (debug) {
|
|
264
|
-
console.log("[AudioContext] Unlock requires user interaction");
|
|
265
|
-
}
|
|
266
|
-
throw error;
|
|
267
|
-
}
|
|
268
|
-
console.error("[AudioContext] Unlock failed:", error);
|
|
269
|
-
throw error;
|
|
270
|
-
}
|
|
271
|
-
};
|
|
272
|
-
const cleanup = () => {
|
|
273
|
-
try {
|
|
274
|
-
if (silenceDetector) {
|
|
275
|
-
silenceDetector.dispose();
|
|
276
|
-
silenceDetector = null;
|
|
277
|
-
}
|
|
278
|
-
mediaElement.pause();
|
|
279
|
-
mediaElement.srcObject = null;
|
|
280
|
-
mediaElement.remove();
|
|
281
|
-
if (debug) {
|
|
282
|
-
console.log("[AudioContext] Cleanup completed");
|
|
283
|
-
}
|
|
284
|
-
} catch (error) {
|
|
285
|
-
console.error("[AudioContext] Cleanup error:", error);
|
|
286
|
-
}
|
|
287
|
-
};
|
|
288
|
-
if (autoCleanup) {
|
|
289
|
-
const originalClose = audioContext.close.bind(audioContext);
|
|
290
|
-
audioContext.close = async () => {
|
|
291
|
-
cleanup();
|
|
292
|
-
return originalClose();
|
|
293
|
-
};
|
|
294
|
-
}
|
|
295
|
-
return Object.assign(audioContext, {
|
|
296
|
-
unlock,
|
|
297
|
-
cleanup,
|
|
298
|
-
isPatched: true,
|
|
299
|
-
originalDestination
|
|
300
|
-
});
|
|
301
|
-
}
|
|
302
|
-
function useAudioContext(options = {}) {
|
|
303
|
-
const { autoUnlock = true, ...patchOptions } = options;
|
|
304
|
-
const [audioContext, setAudioContext] = useState(null);
|
|
305
|
-
const [isUnlocked, setIsUnlocked] = useState(false);
|
|
306
|
-
const unlockFnRef = useRef(null);
|
|
307
|
-
useEffect(() => {
|
|
308
|
-
const ctx = new AudioContext();
|
|
309
|
-
const patchedCtx = patchAudioContext(ctx, patchOptions);
|
|
310
|
-
setAudioContext(patchedCtx);
|
|
311
|
-
return () => {
|
|
312
|
-
patchedCtx.cleanup();
|
|
313
|
-
patchedCtx.close();
|
|
314
|
-
};
|
|
315
|
-
}, []);
|
|
316
|
-
useEffect(() => {
|
|
317
|
-
if (!audioContext) return;
|
|
318
|
-
unlockFnRef.current = async () => {
|
|
319
|
-
if (isUnlocked) return;
|
|
320
|
-
try {
|
|
321
|
-
await audioContext.unlock();
|
|
322
|
-
setIsUnlocked(true);
|
|
323
|
-
} catch (error) {
|
|
324
|
-
if (error instanceof DOMException && error.name === "NotAllowedError") {
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
327
|
-
console.warn("Failed to unlock audio:", error);
|
|
328
|
-
}
|
|
329
|
-
};
|
|
330
|
-
}, [audioContext, isUnlocked]);
|
|
331
|
-
useEffect(() => {
|
|
332
|
-
if (!autoUnlock || !audioContext) return;
|
|
333
|
-
const handleInteraction = async (event) => {
|
|
334
|
-
if (!event.isTrusted) return;
|
|
335
|
-
await unlockFnRef.current?.();
|
|
336
|
-
};
|
|
337
|
-
document.addEventListener("click", handleInteraction, { once: true, capture: true });
|
|
338
|
-
document.addEventListener("touchstart", handleInteraction, { once: true, capture: true });
|
|
339
|
-
return () => {
|
|
340
|
-
document.removeEventListener("click", handleInteraction, { capture: true });
|
|
341
|
-
document.removeEventListener("touchstart", handleInteraction, { capture: true });
|
|
342
|
-
};
|
|
343
|
-
}, [autoUnlock, audioContext]);
|
|
344
|
-
return {
|
|
345
|
-
audioContext,
|
|
346
|
-
isUnlocked,
|
|
347
|
-
unlock: unlockFnRef.current || (async () => {
|
|
348
|
-
})
|
|
349
|
-
};
|
|
350
|
-
}
|
|
351
|
-
export {
|
|
352
|
-
createHiddenVideoElement as a,
|
|
353
|
-
isMediaStreamAudioSupported as b,
|
|
354
|
-
createHiddenMediaElement as c,
|
|
355
|
-
isIOSDevice as i,
|
|
356
|
-
patchAudioContext as p,
|
|
357
|
-
useAudioContext as u
|
|
358
|
-
};
|
|
@@ -1,258 +0,0 @@
|
|
|
1
|
-
import { useReducer, useEffect } from "react";
|
|
2
|
-
class Cancellable {
|
|
3
|
-
constructor(cancelFn) {
|
|
4
|
-
this.cancelFn = cancelFn;
|
|
5
|
-
}
|
|
6
|
-
cancelled = false;
|
|
7
|
-
cancel() {
|
|
8
|
-
if (!this.cancelled) {
|
|
9
|
-
this.cancelled = true;
|
|
10
|
-
this.cancelFn?.();
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
get isCancelled() {
|
|
14
|
-
return this.cancelled;
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
class PassthroughSubject {
|
|
18
|
-
listeners = [];
|
|
19
|
-
send(data) {
|
|
20
|
-
this.listeners.forEach((listener) => listener(data));
|
|
21
|
-
}
|
|
22
|
-
subscribe(callback) {
|
|
23
|
-
this.listeners.push(callback);
|
|
24
|
-
return new Cancellable(() => {
|
|
25
|
-
const index = this.listeners.indexOf(callback);
|
|
26
|
-
if (index > -1) {
|
|
27
|
-
this.listeners.splice(index, 1);
|
|
28
|
-
}
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
class TweaksManager {
|
|
33
|
-
values = {};
|
|
34
|
-
subject = new PassthroughSubject();
|
|
35
|
-
observable;
|
|
36
|
-
constructor(observable, config) {
|
|
37
|
-
this.observable = observable;
|
|
38
|
-
for (const [key, item] of Object.entries(config)) {
|
|
39
|
-
this.values[key] = item.value;
|
|
40
|
-
}
|
|
41
|
-
this.observable.subscribe((data) => {
|
|
42
|
-
if (data && data.values) {
|
|
43
|
-
console.log("🔄 [Aippy Tweaks] Processing external update:", data.values);
|
|
44
|
-
const changedKeys = [];
|
|
45
|
-
for (const [key, newValue] of Object.entries(data.values)) {
|
|
46
|
-
let convertedValue = newValue;
|
|
47
|
-
if (this.values[key] !== void 0) {
|
|
48
|
-
const originalType = typeof this.values[key];
|
|
49
|
-
if (originalType === "number" && typeof newValue === "string") {
|
|
50
|
-
convertedValue = parseFloat(newValue);
|
|
51
|
-
} else if (originalType === "boolean" && typeof newValue !== "boolean") {
|
|
52
|
-
convertedValue = newValue === "true" || newValue === true;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
if (this.values[key] !== convertedValue) {
|
|
56
|
-
const oldValue = this.values[key];
|
|
57
|
-
this.values[key] = convertedValue;
|
|
58
|
-
this.subject.send(key);
|
|
59
|
-
changedKeys.push(key);
|
|
60
|
-
console.log(`📝 [Aippy Tweaks] Updated ${key}: ${oldValue} → ${convertedValue} (type: ${typeof convertedValue})`);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
if (changedKeys.length > 0) {
|
|
64
|
-
console.log(`✨ [Aippy Tweaks] Successfully updated ${changedKeys.length} tweak(s):`, changedKeys);
|
|
65
|
-
} else {
|
|
66
|
-
console.log("💡 [Aippy Tweaks] No values changed in this update");
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
getValue(key) {
|
|
72
|
-
const value = this.values[key];
|
|
73
|
-
if (value === void 0) {
|
|
74
|
-
throw new Error(`Tweak key not found in values: ${key}`);
|
|
75
|
-
}
|
|
76
|
-
return value;
|
|
77
|
-
}
|
|
78
|
-
// React Hook integration
|
|
79
|
-
useState(key) {
|
|
80
|
-
const [value, forceUpdate] = useReducer(() => this.getValue(key), this.getValue(key));
|
|
81
|
-
useEffect(() => {
|
|
82
|
-
const subscription = this.subject.subscribe((changedKey) => {
|
|
83
|
-
if (changedKey === key) {
|
|
84
|
-
forceUpdate();
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
return () => {
|
|
88
|
-
subscription.cancel();
|
|
89
|
-
};
|
|
90
|
-
}, [key]);
|
|
91
|
-
return value;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
class TweaksRuntime {
|
|
95
|
-
tweaksInstance = null;
|
|
96
|
-
tweaksDidWarn = false;
|
|
97
|
-
updateCallback;
|
|
98
|
-
// Store update callback function
|
|
99
|
-
originalConfig;
|
|
100
|
-
// Store original config for merging
|
|
101
|
-
tweaks(config) {
|
|
102
|
-
if (this.tweaksInstance) {
|
|
103
|
-
if (!this.tweaksDidWarn) {
|
|
104
|
-
this.tweaksDidWarn = true;
|
|
105
|
-
console.warn("⚠️ [Aippy Tweaks] tweaks() is expected to only be called once, returning previous value");
|
|
106
|
-
}
|
|
107
|
-
return this.tweaksInstance;
|
|
108
|
-
}
|
|
109
|
-
console.log("🚀 [Aippy Tweaks] Creating new tweaks runtime instance");
|
|
110
|
-
this.originalConfig = { ...config };
|
|
111
|
-
const subject = new PassthroughSubject();
|
|
112
|
-
const manager = new TweaksManager(subject, config);
|
|
113
|
-
const instance = {};
|
|
114
|
-
for (const key of Object.keys(config)) {
|
|
115
|
-
instance[key] = {
|
|
116
|
-
useState: () => manager.useState(key)
|
|
117
|
-
};
|
|
118
|
-
}
|
|
119
|
-
this.tweaksInstance = instance;
|
|
120
|
-
console.log("📋 [Aippy Tweaks] Created tweaks instance with keys:", Object.keys(config));
|
|
121
|
-
if (typeof window !== "undefined" && window.aippyTweaksRuntime) {
|
|
122
|
-
window.aippyTweaksRuntime.tweaksInstance = instance;
|
|
123
|
-
console.log("🌐 [Aippy Tweaks] Exposed tweaksInstance to window.aippyTweaksRuntime.tweaksInstance");
|
|
124
|
-
}
|
|
125
|
-
this.initializeExternalCommunication(config, (data) => {
|
|
126
|
-
subject.send(data);
|
|
127
|
-
});
|
|
128
|
-
return instance;
|
|
129
|
-
}
|
|
130
|
-
initializeExternalCommunication(config, callback) {
|
|
131
|
-
this.updateCallback = callback;
|
|
132
|
-
console.log("🎛️ [Aippy Tweaks] Initializing tweaks with config:", config);
|
|
133
|
-
console.log("🎛️ [Aippy Tweaks] Total tweaks count:", Object.keys(config).length);
|
|
134
|
-
const aippyListener = window.webkit?.messageHandlers?.aippyListener;
|
|
135
|
-
if (aippyListener) {
|
|
136
|
-
try {
|
|
137
|
-
const data = {
|
|
138
|
-
command: "tweaks.initialize",
|
|
139
|
-
parameters: JSON.stringify(config)
|
|
140
|
-
};
|
|
141
|
-
aippyListener.postMessage(data);
|
|
142
|
-
console.log("✅ [Aippy Tweaks] Successfully sent tweaks config to iOS app via aippyListener");
|
|
143
|
-
console.log("📤 [Aippy Tweaks] Sent data:", data);
|
|
144
|
-
} catch (error) {
|
|
145
|
-
console.warn("❌ [Aippy Tweaks] Failed to send tweaks config to iOS app:", error);
|
|
146
|
-
}
|
|
147
|
-
} else {
|
|
148
|
-
console.warn("⚠️ [Aippy Tweaks] webkit.messageHandlers.aippyListener not available");
|
|
149
|
-
console.log(
|
|
150
|
-
"🔍 [Aippy Tweaks] Available webkit handlers:",
|
|
151
|
-
window.webkit?.messageHandlers ? Object.keys(window.webkit.messageHandlers) : "none"
|
|
152
|
-
);
|
|
153
|
-
}
|
|
154
|
-
if (window.parent && window.parent !== window) {
|
|
155
|
-
try {
|
|
156
|
-
const message = {
|
|
157
|
-
type: "tweaks-initialize",
|
|
158
|
-
config: JSON.stringify(config)
|
|
159
|
-
};
|
|
160
|
-
window.parent.postMessage(message, "*");
|
|
161
|
-
console.log("📡 [Aippy Tweaks] Sent tweaks config to parent window:", message);
|
|
162
|
-
const runtimeMessage = {
|
|
163
|
-
type: "aippy-tweaks-ready",
|
|
164
|
-
aippyTweaksRuntime: {
|
|
165
|
-
tweaks: window.aippyTweaksRuntime?.tweaks,
|
|
166
|
-
tweaksInstance: window.aippyTweaksRuntime?.tweaksInstance
|
|
167
|
-
}
|
|
168
|
-
};
|
|
169
|
-
window.parent.postMessage(runtimeMessage, "*");
|
|
170
|
-
console.log("📡 [Aippy Tweaks] Sent aippyTweaksRuntime to parent window:", runtimeMessage);
|
|
171
|
-
} catch (error) {
|
|
172
|
-
console.warn("❌ [Aippy Tweaks] Failed to send to parent window:", error);
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
const messageHandler = (event) => {
|
|
176
|
-
if (event.data && event.data.type === "tweaks-update") {
|
|
177
|
-
console.log("📥 [Aippy Tweaks] Received tweaks update:", event.data);
|
|
178
|
-
callback(event.data);
|
|
179
|
-
}
|
|
180
|
-
};
|
|
181
|
-
window.addEventListener("message", messageHandler);
|
|
182
|
-
console.log("👂 [Aippy Tweaks] Listening for tweaks updates via window.postMessage");
|
|
183
|
-
}
|
|
184
|
-
// Process native data updates - Public method for external calls
|
|
185
|
-
processNativeData(data) {
|
|
186
|
-
if (!this.tweaksInstance) {
|
|
187
|
-
console.warn("⚠️ [Aippy Tweaks] processNativeData called but no tweaks instance exists");
|
|
188
|
-
return;
|
|
189
|
-
}
|
|
190
|
-
if (!this.updateCallback) {
|
|
191
|
-
console.warn("⚠️ [Aippy Tweaks] processNativeData called but no update callback available");
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
try {
|
|
195
|
-
if (!data || typeof data !== "object") {
|
|
196
|
-
console.warn("⚠️ [Aippy Tweaks] Invalid data type received from native:", typeof data);
|
|
197
|
-
return;
|
|
198
|
-
}
|
|
199
|
-
if (!this.originalConfig) {
|
|
200
|
-
console.warn("⚠️ [Aippy Tweaks] No original config available for merging");
|
|
201
|
-
return;
|
|
202
|
-
}
|
|
203
|
-
const values = {};
|
|
204
|
-
const mergedConfig = {};
|
|
205
|
-
let processedCount = 0;
|
|
206
|
-
console.log("📱 [DEBUG] Starting to process iOS data and merge with original config...");
|
|
207
|
-
for (const [key, iosConfigItem] of Object.entries(data)) {
|
|
208
|
-
console.log(`📱 [DEBUG] Processing key: ${key}`, iosConfigItem);
|
|
209
|
-
if (iosConfigItem && typeof iosConfigItem === "object" && "value" in iosConfigItem) {
|
|
210
|
-
const iosItem = iosConfigItem;
|
|
211
|
-
const originalItem = this.originalConfig[key];
|
|
212
|
-
if (!originalItem) {
|
|
213
|
-
console.warn(`⚠️ [Aippy Tweaks] Key "${key}" not found in original config, skipping`);
|
|
214
|
-
continue;
|
|
215
|
-
}
|
|
216
|
-
const iosSpecificFields = /* @__PURE__ */ new Set(["tweakKey", "valueBefore", "tweaksType", "editType"]);
|
|
217
|
-
const mergedItem = { ...originalItem };
|
|
218
|
-
for (const [fieldName, fieldValue] of Object.entries(iosItem)) {
|
|
219
|
-
if (iosSpecificFields.has(fieldName)) {
|
|
220
|
-
continue;
|
|
221
|
-
}
|
|
222
|
-
if (fieldValue !== void 0 && fieldValue !== null) {
|
|
223
|
-
mergedItem[fieldName] = fieldValue;
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
mergedConfig[key] = mergedItem;
|
|
227
|
-
values[key] = iosItem.value;
|
|
228
|
-
processedCount++;
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
if (processedCount === 0) {
|
|
232
|
-
console.warn("⚠️ [Aippy Tweaks] No valid values found in iOS data:", data);
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
const updateMessage = {
|
|
236
|
-
type: "tweaks-update",
|
|
237
|
-
values
|
|
238
|
-
};
|
|
239
|
-
this.updateCallback(updateMessage);
|
|
240
|
-
} catch (error) {
|
|
241
|
-
console.error("❌ [Aippy Tweaks] Error processing native data:", error);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
const globalTweaksRuntime = new TweaksRuntime();
|
|
246
|
-
const aippyTweaksRuntime = {
|
|
247
|
-
tweaks: (config) => globalTweaksRuntime.tweaks(config)
|
|
248
|
-
};
|
|
249
|
-
const aippyTweaks = aippyTweaksRuntime.tweaks;
|
|
250
|
-
if (typeof window !== "undefined") {
|
|
251
|
-
window.aippyTweaksRuntime = aippyTweaksRuntime;
|
|
252
|
-
window.processNativeData = globalTweaksRuntime.processNativeData.bind(globalTweaksRuntime);
|
|
253
|
-
console.log("🌐 [Aippy Tweaks] Exposed processNativeData to window.processNativeData");
|
|
254
|
-
}
|
|
255
|
-
export {
|
|
256
|
-
aippyTweaks as a,
|
|
257
|
-
aippyTweaksRuntime as b
|
|
258
|
-
};
|