@mywallpaper/addon-sdk 2.6.1 → 2.8.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/README.md +1 -1
- package/dist/index.d.mts +147 -2
- package/dist/index.d.ts +147 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/manifest.d.mts +147 -2
- package/dist/manifest.d.ts +147 -2
- package/dist/manifest.js.map +1 -1
- package/dist/manifest.mjs.map +1 -1
- package/package.json +2 -1
- package/src/runtime/addon-client.js +627 -0
- package/src/runtime/network-sandbox.js +153 -0
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
/* eslint-disable no-var, curly, object-shorthand */
|
|
2
|
+
/**
|
|
3
|
+
* MyWallpaper Addon SDK v2.0
|
|
4
|
+
*
|
|
5
|
+
* API disponible pour les addons:
|
|
6
|
+
* - window.MyWallpaper.config → Configuration actuelle
|
|
7
|
+
* - window.MyWallpaper.layerId → ID du layer
|
|
8
|
+
* - window.MyWallpaper.onSettingsChange(fn) → Callback hot-reload
|
|
9
|
+
* - window.MyWallpaper.onEvent(type, fn) → Événements système
|
|
10
|
+
* - window.MyWallpaper.ready(options) → Signal prêt
|
|
11
|
+
* - window.MyWallpaper.renderComplete() → Signal rendu visuel terminé
|
|
12
|
+
*
|
|
13
|
+
* Note: Uses 'var' intentionally for maximum iframe/browser compatibility
|
|
14
|
+
*/
|
|
15
|
+
; (function () {
|
|
16
|
+
'use strict'
|
|
17
|
+
|
|
18
|
+
var layerId = window.__MYWALLPAPER_LAYER_ID__ || ''
|
|
19
|
+
var config = window.__MYWALLPAPER_INITIAL_CONFIG__ || {}
|
|
20
|
+
var isReady = false
|
|
21
|
+
var isRenderComplete = false
|
|
22
|
+
|
|
23
|
+
var settingsCallbacks = []
|
|
24
|
+
var eventCallbacks = new Map()
|
|
25
|
+
var lifecycleCallbacks = { mount: [], unmount: [], pause: [], resume: [] }
|
|
26
|
+
var pendingStorage = new Map()
|
|
27
|
+
var pendingPermissions = new Map()
|
|
28
|
+
var pendingNetwork = new Map()
|
|
29
|
+
var pendingOAuth = new Map()
|
|
30
|
+
var pendingOAuthScopes = new Map()
|
|
31
|
+
var pendingFileAccess = new Map()
|
|
32
|
+
var grantedBlobUrls = new Map() // settingKey -> blobUrl
|
|
33
|
+
|
|
34
|
+
// Audio state (synced from host)
|
|
35
|
+
var audioState = {
|
|
36
|
+
status: 'none',
|
|
37
|
+
currentTime: 0,
|
|
38
|
+
duration: 0,
|
|
39
|
+
volume: 1,
|
|
40
|
+
loop: false,
|
|
41
|
+
playbackRate: 1
|
|
42
|
+
}
|
|
43
|
+
var audioCallbacks = new Set()
|
|
44
|
+
|
|
45
|
+
// Get the original fetch saved by network-sandbox.js (before it was blocked)
|
|
46
|
+
var originalFetch = window.__MYWALLPAPER_ORIGINAL_FETCH__ || null
|
|
47
|
+
|
|
48
|
+
// Message handler
|
|
49
|
+
function onMessage(e) {
|
|
50
|
+
if (e.source !== window.parent) return
|
|
51
|
+
var d = e.data
|
|
52
|
+
if (!d || d.source !== 'MyWallpaperHost') return
|
|
53
|
+
|
|
54
|
+
switch (d.type) {
|
|
55
|
+
case 'SETTINGS_UPDATE':
|
|
56
|
+
var p = d.payload || d
|
|
57
|
+
config = p.settings || p
|
|
58
|
+
var keys = p.changedKeys || Object.keys(config)
|
|
59
|
+
settingsCallbacks.forEach(function (cb) {
|
|
60
|
+
try { cb(config, keys) } catch (err) { console.error('[MyWallpaper]', err) }
|
|
61
|
+
})
|
|
62
|
+
break
|
|
63
|
+
|
|
64
|
+
case 'SYSTEM_EVENT':
|
|
65
|
+
var cbs = eventCallbacks.get(d.payload.event)
|
|
66
|
+
if (cbs) cbs.forEach(function (cb) {
|
|
67
|
+
try { cb(d.payload.data) } catch (err) { console.error('[MyWallpaper]', err) }
|
|
68
|
+
})
|
|
69
|
+
break
|
|
70
|
+
|
|
71
|
+
case 'LIFECYCLE':
|
|
72
|
+
var lcbs = lifecycleCallbacks[d.payload.event]
|
|
73
|
+
if (lcbs) lcbs.forEach(function (cb) {
|
|
74
|
+
try { cb() } catch (err) { console.error('[MyWallpaper]', err) }
|
|
75
|
+
})
|
|
76
|
+
break
|
|
77
|
+
|
|
78
|
+
case 'STORAGE_RESPONSE':
|
|
79
|
+
var op = pendingStorage.get(d.payload.requestId)
|
|
80
|
+
if (op) {
|
|
81
|
+
pendingStorage.delete(d.payload.requestId)
|
|
82
|
+
d.payload.success ? op.resolve(d.payload.value) : op.reject(new Error(d.payload.error))
|
|
83
|
+
}
|
|
84
|
+
break
|
|
85
|
+
|
|
86
|
+
case 'PERMISSION_GRANT':
|
|
87
|
+
var permOp = pendingPermissions.get(d.payload.requestId)
|
|
88
|
+
if (permOp) {
|
|
89
|
+
pendingPermissions.delete(d.payload.requestId)
|
|
90
|
+
permOp(d.payload.granted === true)
|
|
91
|
+
}
|
|
92
|
+
break
|
|
93
|
+
|
|
94
|
+
case 'NETWORK_RESPONSE':
|
|
95
|
+
var netOp = pendingNetwork.get(d.payload.requestId)
|
|
96
|
+
if (netOp) {
|
|
97
|
+
pendingNetwork.delete(d.payload.requestId)
|
|
98
|
+
if (d.payload.success) {
|
|
99
|
+
netOp.resolve({
|
|
100
|
+
ok: d.payload.status >= 200 && d.payload.status < 300,
|
|
101
|
+
status: d.payload.status || 200,
|
|
102
|
+
statusText: d.payload.statusText || 'OK',
|
|
103
|
+
headers: d.payload.headers || {},
|
|
104
|
+
data: d.payload.data
|
|
105
|
+
})
|
|
106
|
+
} else {
|
|
107
|
+
netOp.reject(new Error(d.payload.error || 'Network request failed'))
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
break
|
|
111
|
+
|
|
112
|
+
case 'OAUTH_RESPONSE':
|
|
113
|
+
var oauthOp = pendingOAuth.get(d.payload.requestId)
|
|
114
|
+
if (oauthOp) {
|
|
115
|
+
pendingOAuth.delete(d.payload.requestId)
|
|
116
|
+
if (d.payload.success) {
|
|
117
|
+
// Check if it's a scopes error
|
|
118
|
+
if (d.payload.error === 'insufficient_scopes') {
|
|
119
|
+
oauthOp.resolve({
|
|
120
|
+
error: 'insufficient_scopes',
|
|
121
|
+
message: d.payload.message,
|
|
122
|
+
provider: d.payload.provider,
|
|
123
|
+
missingScopes: d.payload.missing_scopes || d.payload.missingScopes || [],
|
|
124
|
+
requiredScopes: d.payload.required_scopes || d.payload.requiredScopes || []
|
|
125
|
+
})
|
|
126
|
+
} else {
|
|
127
|
+
oauthOp.resolve({
|
|
128
|
+
ok: d.payload.status >= 200 && d.payload.status < 300,
|
|
129
|
+
status: d.payload.status || 200,
|
|
130
|
+
headers: d.payload.headers || {},
|
|
131
|
+
data: d.payload.data
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
} else {
|
|
135
|
+
// Check if it's a scopes error from failed request
|
|
136
|
+
if (d.payload.error === 'insufficient_scopes' ||
|
|
137
|
+
(d.payload.data && d.payload.data.error === 'insufficient_scopes')) {
|
|
138
|
+
var scopesData = d.payload.data || d.payload
|
|
139
|
+
oauthOp.resolve({
|
|
140
|
+
error: 'insufficient_scopes',
|
|
141
|
+
message: scopesData.message || 'Insufficient OAuth scopes',
|
|
142
|
+
provider: scopesData.provider,
|
|
143
|
+
missingScopes: scopesData.missing_scopes || scopesData.missingScopes || [],
|
|
144
|
+
requiredScopes: scopesData.required_scopes || scopesData.requiredScopes || []
|
|
145
|
+
})
|
|
146
|
+
} else {
|
|
147
|
+
oauthOp.reject(new Error(d.payload.error || 'OAuth request failed'))
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
break
|
|
152
|
+
|
|
153
|
+
case 'OAUTH_SCOPES_GRANT':
|
|
154
|
+
var scopeResolver = pendingOAuthScopes.get(d.payload.requestId)
|
|
155
|
+
if (scopeResolver) {
|
|
156
|
+
pendingOAuthScopes.delete(d.payload.requestId)
|
|
157
|
+
scopeResolver(d.payload.granted === true)
|
|
158
|
+
}
|
|
159
|
+
break
|
|
160
|
+
|
|
161
|
+
case 'FILE_ACCESS_RESPONSE':
|
|
162
|
+
// Handle file access response from host
|
|
163
|
+
// Host sends us a Blob object (cloned via structured clone)
|
|
164
|
+
// We create our own blob URL from it
|
|
165
|
+
var fileOp = pendingFileAccess.get(d.requestId)
|
|
166
|
+
if (fileOp) {
|
|
167
|
+
pendingFileAccess.delete(d.requestId)
|
|
168
|
+
if (d.granted && d.blob instanceof Blob) {
|
|
169
|
+
// Create our own blob URL from the received Blob
|
|
170
|
+
var blobUrl = URL.createObjectURL(d.blob)
|
|
171
|
+
grantedBlobUrls.set(d.settingKey, blobUrl)
|
|
172
|
+
fileOp.resolve({ granted: true, blobUrl: blobUrl })
|
|
173
|
+
} else {
|
|
174
|
+
fileOp.resolve({ granted: false, error: d.error || 'Access denied' })
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
break
|
|
178
|
+
|
|
179
|
+
case 'AUDIO_STATE':
|
|
180
|
+
// Sync audio state from host
|
|
181
|
+
audioState = d.payload || d
|
|
182
|
+
audioCallbacks.forEach(function (cb) {
|
|
183
|
+
try { cb(audioState) } catch (err) { console.error('[MyWallpaper] Audio callback error:', err) }
|
|
184
|
+
})
|
|
185
|
+
break
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function send(type, payload) {
|
|
190
|
+
window.parent.postMessage({ type: type, layerId: layerId, timestamp: Date.now(), payload: payload }, '*')
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function storageOp(op, key, value) {
|
|
194
|
+
return new Promise(function (resolve, reject) {
|
|
195
|
+
var id = `${Date.now()}-${Math.random().toString(36).slice(2)}`
|
|
196
|
+
pendingStorage.set(id, { resolve: resolve, reject: reject })
|
|
197
|
+
setTimeout(function () {
|
|
198
|
+
if (pendingStorage.has(id)) {
|
|
199
|
+
pendingStorage.delete(id)
|
|
200
|
+
reject(new Error('Storage timeout'))
|
|
201
|
+
}
|
|
202
|
+
}, 5000)
|
|
203
|
+
send('STORAGE_OPERATION', { operation: op, key: key, value: value, requestId: id })
|
|
204
|
+
})
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Network fetch via secure host proxy
|
|
209
|
+
* This is the ONLY way addons can make network requests
|
|
210
|
+
* Domain must be declared in manifest.json permissions.network.domains
|
|
211
|
+
*/
|
|
212
|
+
function networkFetch(url, options) {
|
|
213
|
+
return new Promise(function (resolve, reject) {
|
|
214
|
+
var id = `${Date.now()}-${Math.random().toString(36).slice(2)}`
|
|
215
|
+
pendingNetwork.set(id, { resolve: resolve, reject: reject })
|
|
216
|
+
// 30s timeout for network requests
|
|
217
|
+
setTimeout(function () {
|
|
218
|
+
if (pendingNetwork.has(id)) {
|
|
219
|
+
pendingNetwork.delete(id)
|
|
220
|
+
reject(new Error('Network request timeout (30s)'))
|
|
221
|
+
}
|
|
222
|
+
}, 30000)
|
|
223
|
+
options = options || {}
|
|
224
|
+
send('NETWORK_REQUEST', {
|
|
225
|
+
url: url,
|
|
226
|
+
method: options.method || 'GET',
|
|
227
|
+
headers: options.headers,
|
|
228
|
+
body: options.body,
|
|
229
|
+
requestId: id
|
|
230
|
+
})
|
|
231
|
+
})
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* OAuth API request via secure host proxy
|
|
236
|
+
* Makes authenticated API calls to OAuth providers (GitHub, Google, etc.)
|
|
237
|
+
* Tokens are stored securely on the server - never exposed to addons
|
|
238
|
+
*
|
|
239
|
+
* @param {string} provider - OAuth provider ('github', 'google', 'discord', 'spotify', 'twitch')
|
|
240
|
+
* @param {string} endpoint - API endpoint path (e.g., '/user', '/user/repos')
|
|
241
|
+
* @param {object} options - Request options (method, body, headers, requiredScopes)
|
|
242
|
+
*/
|
|
243
|
+
function oauthRequest(provider, endpoint, options) {
|
|
244
|
+
return new Promise(function (resolve, reject) {
|
|
245
|
+
var id = `${Date.now()}-${Math.random().toString(36).slice(2)}`
|
|
246
|
+
pendingOAuth.set(id, { resolve: resolve, reject: reject })
|
|
247
|
+
// 30s timeout for OAuth requests
|
|
248
|
+
setTimeout(function () {
|
|
249
|
+
if (pendingOAuth.has(id)) {
|
|
250
|
+
pendingOAuth.delete(id)
|
|
251
|
+
reject(new Error('OAuth request timeout (30s)'))
|
|
252
|
+
}
|
|
253
|
+
}, 30000)
|
|
254
|
+
options = options || {}
|
|
255
|
+
send('OAUTH_REQUEST', {
|
|
256
|
+
provider: provider,
|
|
257
|
+
endpoint: endpoint,
|
|
258
|
+
method: options.method || 'GET',
|
|
259
|
+
body: options.body,
|
|
260
|
+
headers: options.headers,
|
|
261
|
+
requiredScopes: options.requiredScopes || [],
|
|
262
|
+
requestId: id
|
|
263
|
+
})
|
|
264
|
+
})
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Get the scopes granted for a connected OAuth provider
|
|
269
|
+
* Returns an empty array if not connected
|
|
270
|
+
*
|
|
271
|
+
* @param {string} provider - OAuth provider
|
|
272
|
+
* @returns {Promise<string[]>} Array of granted scopes
|
|
273
|
+
*/
|
|
274
|
+
function oauthGetScopes(provider) {
|
|
275
|
+
return new Promise(function (resolve, reject) {
|
|
276
|
+
var id = `${Date.now()}-${Math.random().toString(36).slice(2)}`
|
|
277
|
+
var handler = function (e) {
|
|
278
|
+
if (e.source !== window.parent) return
|
|
279
|
+
var d = e.data
|
|
280
|
+
if (!d || d.source !== 'MyWallpaperHost') return
|
|
281
|
+
if (d.type === 'OAUTH_SCOPES_RESPONSE' && d.payload.requestId === id) {
|
|
282
|
+
window.removeEventListener('message', handler)
|
|
283
|
+
if (d.payload.success) {
|
|
284
|
+
resolve(d.payload.scopes || [])
|
|
285
|
+
} else {
|
|
286
|
+
reject(new Error(d.payload.error || 'Failed to get scopes'))
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
window.addEventListener('message', handler)
|
|
291
|
+
// 10s timeout
|
|
292
|
+
setTimeout(function () {
|
|
293
|
+
window.removeEventListener('message', handler)
|
|
294
|
+
resolve([]) // Return empty array on timeout (not connected)
|
|
295
|
+
}, 10000)
|
|
296
|
+
send('OAUTH_GET_SCOPES', { provider: provider, requestId: id })
|
|
297
|
+
})
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Request re-authorization with additional scopes
|
|
302
|
+
* Opens the OAuth flow with new scopes while preserving existing ones
|
|
303
|
+
*
|
|
304
|
+
* @param {string} provider - OAuth provider
|
|
305
|
+
* @param {string[]} scopes - Additional scopes to request
|
|
306
|
+
* @param {string} reason - Reason shown to user
|
|
307
|
+
* @returns {Promise<boolean>} True if re-auth was successful
|
|
308
|
+
*/
|
|
309
|
+
function oauthRequestScopes(provider, scopes, reason) {
|
|
310
|
+
return new Promise(function (resolve) {
|
|
311
|
+
var requestId = `${Date.now()}-${Math.random().toString(36).slice(2)}`
|
|
312
|
+
pendingOAuthScopes.set(requestId, resolve)
|
|
313
|
+
// 60s timeout - user needs time to complete OAuth flow
|
|
314
|
+
setTimeout(function () {
|
|
315
|
+
if (pendingOAuthScopes.has(requestId)) {
|
|
316
|
+
pendingOAuthScopes.delete(requestId)
|
|
317
|
+
resolve(false)
|
|
318
|
+
}
|
|
319
|
+
}, 60000)
|
|
320
|
+
send('OAUTH_REQUEST_SCOPES', { provider: provider, scopes: scopes, reason: reason, requestId: requestId })
|
|
321
|
+
})
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
window.addEventListener('message', onMessage)
|
|
325
|
+
|
|
326
|
+
window.MyWallpaper = {
|
|
327
|
+
get config() { return config },
|
|
328
|
+
layerId: layerId,
|
|
329
|
+
version: '2.5',
|
|
330
|
+
|
|
331
|
+
onSettingsChange: function (fn) { if (typeof fn === 'function') settingsCallbacks.push(fn) },
|
|
332
|
+
|
|
333
|
+
onEvent: function (event, fn) {
|
|
334
|
+
if (typeof fn !== 'function') return
|
|
335
|
+
if (!eventCallbacks.has(event)) eventCallbacks.set(event, new Set())
|
|
336
|
+
eventCallbacks.get(event).add(fn)
|
|
337
|
+
},
|
|
338
|
+
|
|
339
|
+
offEvent: function (event, fn) {
|
|
340
|
+
var cbs = eventCallbacks.get(event)
|
|
341
|
+
if (cbs) cbs.delete(fn)
|
|
342
|
+
},
|
|
343
|
+
|
|
344
|
+
onMount: function (fn) { if (typeof fn === 'function') lifecycleCallbacks.mount.push(fn) },
|
|
345
|
+
onUnmount: function (fn) { if (typeof fn === 'function') lifecycleCallbacks.unmount.push(fn) },
|
|
346
|
+
onPause: function (fn) { if (typeof fn === 'function') lifecycleCallbacks.pause.push(fn) },
|
|
347
|
+
onResume: function (fn) { if (typeof fn === 'function') lifecycleCallbacks.resume.push(fn) },
|
|
348
|
+
|
|
349
|
+
ready: function (opts) {
|
|
350
|
+
if (isReady) return
|
|
351
|
+
isReady = true
|
|
352
|
+
opts = opts || {}
|
|
353
|
+
send('ADDON_READY', {
|
|
354
|
+
version: '2.5',
|
|
355
|
+
capabilities: opts.capabilities || [],
|
|
356
|
+
subscribedEvents: opts.subscribedEvents || []
|
|
357
|
+
})
|
|
358
|
+
},
|
|
359
|
+
|
|
360
|
+
requestPermission: function (perm, reason) {
|
|
361
|
+
return new Promise(function (resolve) {
|
|
362
|
+
var requestId = `${Date.now()}-${Math.random().toString(36).slice(2)}`
|
|
363
|
+
pendingPermissions.set(requestId, resolve)
|
|
364
|
+
// 30s timeout - user needs time to see and respond to permission modal
|
|
365
|
+
setTimeout(function () {
|
|
366
|
+
if (pendingPermissions.has(requestId)) {
|
|
367
|
+
pendingPermissions.delete(requestId)
|
|
368
|
+
resolve(false) // Timeout = denied
|
|
369
|
+
}
|
|
370
|
+
}, 30000)
|
|
371
|
+
send('REQUEST_PERMISSION', { permission: perm, reason: reason, requestId: requestId })
|
|
372
|
+
})
|
|
373
|
+
},
|
|
374
|
+
|
|
375
|
+
renderComplete: function () {
|
|
376
|
+
if (isRenderComplete) return
|
|
377
|
+
isRenderComplete = true
|
|
378
|
+
var renderTimeMs = Math.round(performance.now())
|
|
379
|
+
send('RENDER_COMPLETE', { renderTimeMs: renderTimeMs })
|
|
380
|
+
},
|
|
381
|
+
|
|
382
|
+
storage: {
|
|
383
|
+
get: function (k) { return storageOp('get', k) },
|
|
384
|
+
set: function (k, v) { return storageOp('set', k, v) },
|
|
385
|
+
delete: function (k) { return storageOp('delete', k) },
|
|
386
|
+
clear: function () { return storageOp('clear') },
|
|
387
|
+
keys: function () { return storageOp('keys') },
|
|
388
|
+
size: function () { return storageOp('size') }
|
|
389
|
+
},
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Network API - Secure proxy for HTTP requests
|
|
393
|
+
* All requests go through the host which validates domain whitelist
|
|
394
|
+
*
|
|
395
|
+
* @example
|
|
396
|
+
* // manifest.json: "permissions": { "network": { "domains": ["api.weather.com"] } }
|
|
397
|
+
* const response = await MyWallpaper.network.fetch('https://api.weather.com/current')
|
|
398
|
+
* if (response.ok) console.log(response.data)
|
|
399
|
+
*/
|
|
400
|
+
network: {
|
|
401
|
+
fetch: networkFetch
|
|
402
|
+
},
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* OAuth API - Secure authenticated API calls to OAuth providers
|
|
406
|
+
* Tokens are stored securely on the server - never exposed to addons
|
|
407
|
+
*
|
|
408
|
+
* Supported providers: github, google, discord, spotify, twitch
|
|
409
|
+
*
|
|
410
|
+
* @example
|
|
411
|
+
* // First request permission
|
|
412
|
+
* const granted = await MyWallpaper.requestPermission('oauth:github', 'To display your GitHub profile')
|
|
413
|
+
* if (granted) {
|
|
414
|
+
* // Make authenticated API request
|
|
415
|
+
* const response = await MyWallpaper.oauth.request('github', '/user')
|
|
416
|
+
* if (response.ok) console.log('GitHub user:', response.data)
|
|
417
|
+
*
|
|
418
|
+
* // List repositories (with required scopes)
|
|
419
|
+
* const repos = await MyWallpaper.oauth.request('github', '/user/repos', {
|
|
420
|
+
* requiredScopes: ['repo'] // Will return scope error if missing
|
|
421
|
+
* })
|
|
422
|
+
* if (repos.error === 'insufficient_scopes') {
|
|
423
|
+
* // Request additional scopes
|
|
424
|
+
* const granted = await MyWallpaper.oauth.requestScopes('github', repos.missingScopes, 'Access private repos')
|
|
425
|
+
* if (granted) {
|
|
426
|
+
* // Retry request
|
|
427
|
+
* const repos = await MyWallpaper.oauth.request('github', '/user/repos', { requiredScopes: ['repo'] })
|
|
428
|
+
* }
|
|
429
|
+
* }
|
|
430
|
+
* }
|
|
431
|
+
*/
|
|
432
|
+
oauth: {
|
|
433
|
+
request: oauthRequest,
|
|
434
|
+
getScopes: oauthGetScopes,
|
|
435
|
+
requestScopes: oauthRequestScopes,
|
|
436
|
+
isConnected: function (provider) {
|
|
437
|
+
// Check connection by getting scopes - if scopes array is returned, provider is connected
|
|
438
|
+
return oauthGetScopes(provider)
|
|
439
|
+
.then(function (scopes) { return scopes.length > 0 })
|
|
440
|
+
.catch(function () { return false })
|
|
441
|
+
}
|
|
442
|
+
},
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* File Access API - Request access to user-uploaded files
|
|
446
|
+
* Files are not sent automatically - the addon must request access
|
|
447
|
+
* A modal is shown to the user EVERY TIME (not persisted)
|
|
448
|
+
*
|
|
449
|
+
* @example
|
|
450
|
+
* const videoRef = MyWallpaper.config.backgroundVideo
|
|
451
|
+
* if (MyWallpaper.files.isFileReference(videoRef)) {
|
|
452
|
+
* const result = await MyWallpaper.files.request('backgroundVideo')
|
|
453
|
+
* if (result.granted) {
|
|
454
|
+
* video.src = result.blobUrl
|
|
455
|
+
* }
|
|
456
|
+
* }
|
|
457
|
+
*/
|
|
458
|
+
files: {
|
|
459
|
+
/**
|
|
460
|
+
* Check if a value is a file reference placeholder
|
|
461
|
+
* @param {unknown} value - Any value from config
|
|
462
|
+
* @returns {boolean} true if this is a FileReference
|
|
463
|
+
*/
|
|
464
|
+
isFileReference: function (value) {
|
|
465
|
+
return value !== null &&
|
|
466
|
+
typeof value === 'object' &&
|
|
467
|
+
value.__type === 'file-reference' &&
|
|
468
|
+
typeof value.settingKey === 'string'
|
|
469
|
+
},
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Request access to a file from the user
|
|
473
|
+
* Shows a modal asking for permission (every time)
|
|
474
|
+
* @param {string} settingKey - The setting key containing the file reference
|
|
475
|
+
* @returns {Promise<{granted: boolean, blobUrl?: string, error?: string}>}
|
|
476
|
+
*/
|
|
477
|
+
request: function (settingKey) {
|
|
478
|
+
return new Promise(function (resolve) {
|
|
479
|
+
var requestId = Date.now() + '-' + Math.random().toString(36).slice(2)
|
|
480
|
+
pendingFileAccess.set(requestId, { resolve: resolve })
|
|
481
|
+
// 60s timeout - user needs time to respond to modal
|
|
482
|
+
setTimeout(function () {
|
|
483
|
+
if (pendingFileAccess.has(requestId)) {
|
|
484
|
+
pendingFileAccess.delete(requestId)
|
|
485
|
+
resolve({ granted: false, error: 'Request timeout' })
|
|
486
|
+
}
|
|
487
|
+
}, 60000)
|
|
488
|
+
send('FILE_ACCESS_REQUEST', { settingKey: settingKey, requestId: requestId })
|
|
489
|
+
})
|
|
490
|
+
},
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Revoke access to a previously granted file
|
|
494
|
+
* The blob URL will be invalidated
|
|
495
|
+
* @param {string} settingKey - The setting key to revoke access for
|
|
496
|
+
*/
|
|
497
|
+
revoke: function (settingKey) {
|
|
498
|
+
var blobUrl = grantedBlobUrls.get(settingKey)
|
|
499
|
+
if (blobUrl) {
|
|
500
|
+
URL.revokeObjectURL(blobUrl)
|
|
501
|
+
grantedBlobUrls.delete(settingKey)
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
},
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Audio API - Parent-managed audio playback
|
|
508
|
+
* Audio is played by the host (parent window) to bypass iframe sandbox restrictions
|
|
509
|
+
* The addon controls playback via commands, host syncs state back
|
|
510
|
+
*
|
|
511
|
+
* @example
|
|
512
|
+
* // Play audio from a URL
|
|
513
|
+
* MyWallpaper.audio.play('https://example.com/sound.mp3', { volume: 0.8 })
|
|
514
|
+
*
|
|
515
|
+
* // Play audio from a file setting
|
|
516
|
+
* MyWallpaper.audio.play('audioFile') // settingKey
|
|
517
|
+
*
|
|
518
|
+
* // Listen to state changes
|
|
519
|
+
* MyWallpaper.audio.onStateChange(function(state) {
|
|
520
|
+
* console.log('Audio status:', state.status, 'Time:', state.currentTime)
|
|
521
|
+
* })
|
|
522
|
+
*/
|
|
523
|
+
audio: {
|
|
524
|
+
/**
|
|
525
|
+
* Play audio from a source
|
|
526
|
+
* @param {string|object} source - URL, settingKey, or FileReference
|
|
527
|
+
* @param {object} options - { volume, loop, playbackRate, startTime }
|
|
528
|
+
*/
|
|
529
|
+
play: function (source, options) {
|
|
530
|
+
var payload = { command: 'play', options: options || {} }
|
|
531
|
+
|
|
532
|
+
if (typeof source === 'string') {
|
|
533
|
+
if (source.startsWith('http') || source.startsWith('data:') || source.startsWith('blob:')) {
|
|
534
|
+
payload.url = source
|
|
535
|
+
} else {
|
|
536
|
+
// Assume it's a settingKey
|
|
537
|
+
payload.settingKey = source
|
|
538
|
+
}
|
|
539
|
+
} else if (source && source.__type === 'file-reference') {
|
|
540
|
+
payload.fileRef = source
|
|
541
|
+
} else if (source && source.settingKey) {
|
|
542
|
+
payload.settingKey = source.settingKey
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
send('AUDIO_COMMAND', payload)
|
|
546
|
+
},
|
|
547
|
+
|
|
548
|
+
/** Pause audio playback */
|
|
549
|
+
pause: function () {
|
|
550
|
+
send('AUDIO_COMMAND', { command: 'pause' })
|
|
551
|
+
},
|
|
552
|
+
|
|
553
|
+
/** Resume audio playback */
|
|
554
|
+
resume: function () {
|
|
555
|
+
send('AUDIO_COMMAND', { command: 'resume' })
|
|
556
|
+
},
|
|
557
|
+
|
|
558
|
+
/** Stop and reset audio */
|
|
559
|
+
stop: function () {
|
|
560
|
+
send('AUDIO_COMMAND', { command: 'stop' })
|
|
561
|
+
},
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Seek to position
|
|
565
|
+
* @param {number} seconds - Position in seconds
|
|
566
|
+
*/
|
|
567
|
+
seek: function (seconds) {
|
|
568
|
+
send('AUDIO_COMMAND', { command: 'seek', value: seconds })
|
|
569
|
+
},
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Set volume
|
|
573
|
+
* @param {number} volume - Volume from 0.0 to 1.0
|
|
574
|
+
*/
|
|
575
|
+
setVolume: function (volume) {
|
|
576
|
+
send('AUDIO_COMMAND', { command: 'setVolume', value: Math.max(0, Math.min(1, volume)) })
|
|
577
|
+
},
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Enable or disable loop
|
|
581
|
+
* @param {boolean} loop - Whether to loop
|
|
582
|
+
*/
|
|
583
|
+
setLoop: function (loop) {
|
|
584
|
+
send('AUDIO_COMMAND', { command: 'setLoop', value: !!loop })
|
|
585
|
+
},
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Set playback rate
|
|
589
|
+
* @param {number} rate - Playback rate (0.25 to 4.0)
|
|
590
|
+
*/
|
|
591
|
+
setPlaybackRate: function (rate) {
|
|
592
|
+
send('AUDIO_COMMAND', { command: 'setPlaybackRate', value: rate })
|
|
593
|
+
},
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Get current audio state
|
|
597
|
+
* @returns {object} { status, currentTime, duration, volume, loop, playbackRate, error? }
|
|
598
|
+
*/
|
|
599
|
+
getState: function () {
|
|
600
|
+
return {
|
|
601
|
+
status: audioState.status,
|
|
602
|
+
currentTime: audioState.currentTime,
|
|
603
|
+
duration: audioState.duration,
|
|
604
|
+
volume: audioState.volume,
|
|
605
|
+
loop: audioState.loop,
|
|
606
|
+
playbackRate: audioState.playbackRate,
|
|
607
|
+
error: audioState.error
|
|
608
|
+
}
|
|
609
|
+
},
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Listen to audio state changes
|
|
613
|
+
* @param {function} callback - Called with AudioState on each change
|
|
614
|
+
* @returns {function} Unsubscribe function
|
|
615
|
+
*/
|
|
616
|
+
onStateChange: function (callback) {
|
|
617
|
+
if (typeof callback !== 'function') return function () { }
|
|
618
|
+
audioCallbacks.add(callback)
|
|
619
|
+
// Call immediately with current state
|
|
620
|
+
callback(audioState)
|
|
621
|
+
return function () { audioCallbacks.delete(callback) }
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
console.warn('[MyWallpaper] SDK v2.5 ready:', layerId)
|
|
627
|
+
})()
|