@signalk/app-dock 0.1.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/.github/workflows/ci.yml +11 -0
- package/.github/workflows/publish.yml +33 -0
- package/.prettierignore +6 -0
- package/.prettierrc +6 -0
- package/LICENSE +190 -0
- package/README.md +83 -0
- package/eslint.config.js +55 -0
- package/package.json +40 -0
- package/plugin/index.js +180 -0
- package/public/540.js +2 -0
- package/public/540.js.LICENSE.txt +9 -0
- package/public/805.js +1 -0
- package/public/app-icon.svg +29 -0
- package/public/config.html +469 -0
- package/public/dock.css +412 -0
- package/public/dock.js +456 -0
- package/public/icon-settings.svg +21 -0
- package/public/index.html +41 -0
- package/public/main.js +1 -0
- package/public/remoteEntry.js +1 -0
- package/src/configpanel/PluginConfigurationPanel.js +441 -0
- package/src/configpanel/index.js +1 -0
- package/test/plugin.test.js +394 -0
- package/webpack.config.js +39 -0
package/public/dock.js
ADDED
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
;(async function DockApp() {
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
const DEFAULTS = {
|
|
5
|
+
position: 'bottom',
|
|
6
|
+
triggerCorner: 'bottom-right',
|
|
7
|
+
iframeMode: 'keep-alive',
|
|
8
|
+
iconSize: 56,
|
|
9
|
+
magnification: true,
|
|
10
|
+
magnificationScale: 1.7,
|
|
11
|
+
apps: []
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// ─── Load config from plugin endpoints ───────────────────────────────────────
|
|
15
|
+
let cfg = { ...DEFAULTS }
|
|
16
|
+
try {
|
|
17
|
+
const [configRes, settingsRes] = await Promise.all([
|
|
18
|
+
fetch('/plugins/signalk-app-dock/config'),
|
|
19
|
+
fetch('/plugins/signalk-app-dock/settings')
|
|
20
|
+
])
|
|
21
|
+
if (configRes.ok) {
|
|
22
|
+
const data = await configRes.json()
|
|
23
|
+
const pluginCfg = data.configuration || data
|
|
24
|
+
cfg = { ...DEFAULTS, ...pluginCfg }
|
|
25
|
+
}
|
|
26
|
+
if (settingsRes.ok) {
|
|
27
|
+
const data = await settingsRes.json()
|
|
28
|
+
if (Array.isArray(data.apps) && data.apps.length > 0) {
|
|
29
|
+
cfg.apps = data.apps
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
} catch (e) {
|
|
33
|
+
console.warn('[Dock] Could not load config, using defaults.', e)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!Array.isArray(cfg.apps) || cfg.apps.length === 0) {
|
|
37
|
+
console.warn('[Dock] No apps configured — open Plugin Config to add webapps.')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ─── DOM refs ────────────────────────────────────────────────────────────────
|
|
41
|
+
const $iframeContainer = document.getElementById('iframe-container')
|
|
42
|
+
const $backdrop = document.getElementById('dock-backdrop')
|
|
43
|
+
const $dock = document.getElementById('dock')
|
|
44
|
+
const $dockInner = document.getElementById('dock-inner')
|
|
45
|
+
const $triggerCorner = document.getElementById('trigger-corner')
|
|
46
|
+
const $loadingOverlay = document.getElementById('loading-overlay')
|
|
47
|
+
const $loadingLabel = document.getElementById('loading-label')
|
|
48
|
+
const $idleHint = document.getElementById('idle-hint')
|
|
49
|
+
const $idleHintText = document.getElementById('idle-hint-text')
|
|
50
|
+
|
|
51
|
+
// ─── State ───────────────────────────────────────────────────────────────────
|
|
52
|
+
let dockVisible = false
|
|
53
|
+
const iframes = {}
|
|
54
|
+
|
|
55
|
+
// ─── Apply dock position class & alignment ───────────────────────────────────
|
|
56
|
+
const pos = cfg.position
|
|
57
|
+
$dock.classList.add(`pos-${pos}`)
|
|
58
|
+
$dockInner.classList.add(`align-${pos}`)
|
|
59
|
+
|
|
60
|
+
const isVertical = pos === 'left' || pos === 'right'
|
|
61
|
+
if (isVertical) $dockInner.classList.add('layout-vertical')
|
|
62
|
+
|
|
63
|
+
// ─── Magnification constants ─────────────────────────────────────────────────
|
|
64
|
+
const MAG_ENABLED = cfg.magnification !== false
|
|
65
|
+
const MAG_SCALE = Math.max(1, Math.min(cfg.magnificationScale || 1.7, 2.5))
|
|
66
|
+
const MAG_RADIUS = cfg.iconSize * 3.5
|
|
67
|
+
|
|
68
|
+
// ─── Welcome screen ──────────────────────────────────────────────────────────
|
|
69
|
+
function setIdleHint() {
|
|
70
|
+
const $version = document.getElementById('idle-version')
|
|
71
|
+
if ($version) {
|
|
72
|
+
fetch('/skServer/webapps')
|
|
73
|
+
.then((r) => r.json())
|
|
74
|
+
.then((apps) => {
|
|
75
|
+
const me = apps.find((a) => a.name === 'signalk-app-dock')
|
|
76
|
+
if (me) $version.textContent = 'v' + me.version
|
|
77
|
+
})
|
|
78
|
+
.catch(() => {})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!cfg.apps || cfg.apps.length === 0) {
|
|
82
|
+
$idleHintText.innerHTML =
|
|
83
|
+
'No apps configured yet.<br>' +
|
|
84
|
+
'Open <strong>Admin UI \u2192 Plugin Config \u2192 App Dock</strong><br>' +
|
|
85
|
+
'and click <strong>Discover Installed Webapps</strong> to get started.'
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const cornerNames = {
|
|
90
|
+
'bottom-right': 'bottom-right',
|
|
91
|
+
'bottom-left': 'bottom-left',
|
|
92
|
+
'top-right': 'top-right',
|
|
93
|
+
'top-left': 'top-left'
|
|
94
|
+
}
|
|
95
|
+
const corner = cornerNames[cfg.triggerCorner] || 'bottom-right'
|
|
96
|
+
|
|
97
|
+
$idleHintText.innerHTML =
|
|
98
|
+
'Double-tap the <strong>' +
|
|
99
|
+
corner +
|
|
100
|
+
' corner</strong> to open the dock' +
|
|
101
|
+
'<br><br><span style="font-size:12px;opacity:0.6">Configure in Admin UI \u2192 Plugin Config \u2192 App Dock</span>'
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function hideIdleHint() {
|
|
105
|
+
if ($idleHint) $idleHint.classList.add('hidden')
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ─── Tooltip direction based on dock edge ────────────────────────────────────
|
|
109
|
+
function labelClass() {
|
|
110
|
+
switch (pos) {
|
|
111
|
+
case 'top':
|
|
112
|
+
return 'dock-label-below'
|
|
113
|
+
case 'left':
|
|
114
|
+
return 'dock-label-right'
|
|
115
|
+
case 'right':
|
|
116
|
+
return 'dock-label-left'
|
|
117
|
+
default:
|
|
118
|
+
return ''
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ─── Build dock items ─────────────────────────────────────────────────────────
|
|
123
|
+
function buildDock() {
|
|
124
|
+
$dockInner.innerHTML = ''
|
|
125
|
+
const sz = cfg.iconSize
|
|
126
|
+
const radius = Math.round(sz * 0.25) + 'px'
|
|
127
|
+
const lblCls = labelClass()
|
|
128
|
+
|
|
129
|
+
cfg.apps.forEach((app, i) => {
|
|
130
|
+
const $item = document.createElement('div')
|
|
131
|
+
$item.className = 'dock-item'
|
|
132
|
+
$item.dataset.index = i
|
|
133
|
+
|
|
134
|
+
const $icon = document.createElement('div')
|
|
135
|
+
$icon.className = 'dock-icon'
|
|
136
|
+
$icon.style.width = sz + 'px'
|
|
137
|
+
$icon.style.height = sz + 'px'
|
|
138
|
+
$icon.style.borderRadius = radius
|
|
139
|
+
if (app.color) $icon.style.background = app.color
|
|
140
|
+
|
|
141
|
+
const iconVal = app.icon || ''
|
|
142
|
+
if (iconVal.startsWith('/') || iconVal.startsWith('http')) {
|
|
143
|
+
const $img = document.createElement('img')
|
|
144
|
+
$img.src = iconVal
|
|
145
|
+
$img.alt = app.label
|
|
146
|
+
$img.onerror = () => {
|
|
147
|
+
$icon.removeChild($img)
|
|
148
|
+
$icon.style.fontSize = Math.round(sz * 0.42) + 'px'
|
|
149
|
+
$icon.textContent = app.label.slice(0, 2).toUpperCase()
|
|
150
|
+
}
|
|
151
|
+
$icon.appendChild($img)
|
|
152
|
+
} else {
|
|
153
|
+
$icon.style.fontSize = Math.round(sz * 0.48) + 'px'
|
|
154
|
+
$icon.textContent = iconVal || app.label.slice(0, 2).toUpperCase()
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const $dot = document.createElement('div')
|
|
158
|
+
$dot.className = 'dock-dot'
|
|
159
|
+
|
|
160
|
+
const $label = document.createElement('div')
|
|
161
|
+
$label.className = 'dock-label' + (lblCls ? ' ' + lblCls : '')
|
|
162
|
+
$label.textContent = app.label
|
|
163
|
+
|
|
164
|
+
$item.appendChild($icon)
|
|
165
|
+
$item.appendChild($dot)
|
|
166
|
+
$item.appendChild($label)
|
|
167
|
+
|
|
168
|
+
$item.addEventListener('touchstart', () => $item.classList.add('pressing'), { passive: true })
|
|
169
|
+
$item.addEventListener('touchend', () => $item.classList.remove('pressing'), { passive: true })
|
|
170
|
+
$item.addEventListener('touchcancel', () => $item.classList.remove('pressing'), { passive: true })
|
|
171
|
+
|
|
172
|
+
$item.addEventListener('click', (e) => {
|
|
173
|
+
if (magScrubbed) {
|
|
174
|
+
e.preventDefault()
|
|
175
|
+
return
|
|
176
|
+
}
|
|
177
|
+
switchToApp(i)
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
$dockInner.appendChild($item)
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// ─── Magnification ───────────────────────────────────────────────────────────
|
|
185
|
+
let magRafPending = false
|
|
186
|
+
let magScrubbed = false
|
|
187
|
+
let magTouchStartAxis = 0
|
|
188
|
+
|
|
189
|
+
function applyMagnification(pointerPos) {
|
|
190
|
+
const items = $dockInner.querySelectorAll('.dock-item')
|
|
191
|
+
let closestIdx = -1
|
|
192
|
+
let closestDist = Infinity
|
|
193
|
+
|
|
194
|
+
items.forEach((item, i) => {
|
|
195
|
+
const icon = item.querySelector('.dock-icon')
|
|
196
|
+
const rect = item.getBoundingClientRect()
|
|
197
|
+
const center = isVertical ? rect.top + rect.height / 2 : rect.left + rect.width / 2
|
|
198
|
+
const p = isVertical ? pointerPos.y : pointerPos.x
|
|
199
|
+
|
|
200
|
+
const dist = Math.abs(p - center)
|
|
201
|
+
if (dist < closestDist) {
|
|
202
|
+
closestDist = dist
|
|
203
|
+
closestIdx = i
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const ratio = Math.min(dist / MAG_RADIUS, 1)
|
|
207
|
+
const scale = 1 + (MAG_SCALE - 1) * (1 - ratio * ratio)
|
|
208
|
+
const newSize = Math.round(cfg.iconSize * scale)
|
|
209
|
+
|
|
210
|
+
icon.classList.remove('settling')
|
|
211
|
+
icon.style.width = newSize + 'px'
|
|
212
|
+
icon.style.height = newSize + 'px'
|
|
213
|
+
icon.style.borderRadius = Math.round(newSize * 0.25) + 'px'
|
|
214
|
+
|
|
215
|
+
if (icon.querySelector('img')) {
|
|
216
|
+
// image icons scale via width/height, fontSize not needed
|
|
217
|
+
} else {
|
|
218
|
+
icon.style.fontSize = Math.round(newSize * 0.48) + 'px'
|
|
219
|
+
}
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
items.forEach((item, i) => {
|
|
223
|
+
item.classList.toggle('mag-closest', i === closestIdx)
|
|
224
|
+
})
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function resetMagnification() {
|
|
228
|
+
const items = $dockInner.querySelectorAll('.dock-item')
|
|
229
|
+
const sz = cfg.iconSize
|
|
230
|
+
|
|
231
|
+
items.forEach((item) => {
|
|
232
|
+
const icon = item.querySelector('.dock-icon')
|
|
233
|
+
icon.classList.add('settling')
|
|
234
|
+
icon.style.width = sz + 'px'
|
|
235
|
+
icon.style.height = sz + 'px'
|
|
236
|
+
icon.style.borderRadius = Math.round(sz * 0.25) + 'px'
|
|
237
|
+
|
|
238
|
+
if (!icon.querySelector('img')) {
|
|
239
|
+
icon.style.fontSize = Math.round(sz * 0.48) + 'px'
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
item.classList.remove('mag-closest')
|
|
243
|
+
})
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (MAG_ENABLED) {
|
|
247
|
+
$dockInner.addEventListener('mousemove', (e) => {
|
|
248
|
+
if (magRafPending) return
|
|
249
|
+
magRafPending = true
|
|
250
|
+
const pos = { x: e.clientX, y: e.clientY }
|
|
251
|
+
requestAnimationFrame(() => {
|
|
252
|
+
applyMagnification(pos)
|
|
253
|
+
magRafPending = false
|
|
254
|
+
})
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
$dockInner.addEventListener('mouseleave', () => {
|
|
258
|
+
magRafPending = false
|
|
259
|
+
resetMagnification()
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
$dockInner.addEventListener(
|
|
263
|
+
'touchstart',
|
|
264
|
+
(e) => {
|
|
265
|
+
const t = e.touches[0]
|
|
266
|
+
magScrubbed = false
|
|
267
|
+
magTouchStartAxis = isVertical ? t.clientY : t.clientX
|
|
268
|
+
},
|
|
269
|
+
{ passive: true }
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
$dockInner.addEventListener(
|
|
273
|
+
'touchmove',
|
|
274
|
+
(e) => {
|
|
275
|
+
const t = e.touches[0]
|
|
276
|
+
const current = isVertical ? t.clientY : t.clientX
|
|
277
|
+
if (Math.abs(current - magTouchStartAxis) > 10) {
|
|
278
|
+
magScrubbed = true
|
|
279
|
+
}
|
|
280
|
+
if (magRafPending) return
|
|
281
|
+
magRafPending = true
|
|
282
|
+
const pos = { x: t.clientX, y: t.clientY }
|
|
283
|
+
requestAnimationFrame(() => {
|
|
284
|
+
applyMagnification(pos)
|
|
285
|
+
magRafPending = false
|
|
286
|
+
})
|
|
287
|
+
},
|
|
288
|
+
{ passive: true }
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
$dockInner.addEventListener(
|
|
292
|
+
'touchend',
|
|
293
|
+
() => {
|
|
294
|
+
magRafPending = false
|
|
295
|
+
resetMagnification()
|
|
296
|
+
setTimeout(() => {
|
|
297
|
+
magScrubbed = false
|
|
298
|
+
}, 50)
|
|
299
|
+
},
|
|
300
|
+
{ passive: true }
|
|
301
|
+
)
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ─── Show / hide dock ────────────────────────────────────────────────────────
|
|
305
|
+
function showDock() {
|
|
306
|
+
if (dockVisible) return
|
|
307
|
+
dockVisible = true
|
|
308
|
+
$dock.classList.add('visible')
|
|
309
|
+
$backdrop.classList.add('visible')
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function hideDock() {
|
|
313
|
+
if (!dockVisible) return
|
|
314
|
+
dockVisible = false
|
|
315
|
+
$dock.classList.remove('visible')
|
|
316
|
+
$backdrop.classList.remove('visible')
|
|
317
|
+
resetMagnification()
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
$backdrop.addEventListener('click', hideDock)
|
|
321
|
+
|
|
322
|
+
// ─── Loading overlay helpers ──────────────────────────────────────────────────
|
|
323
|
+
function showLoading(label) {
|
|
324
|
+
$loadingLabel.textContent = `Loading ${label}\u2026`
|
|
325
|
+
$loadingOverlay.classList.add('visible')
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function hideLoading() {
|
|
329
|
+
$loadingOverlay.classList.remove('visible')
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// ─── Switch to app ────────────────────────────────────────────────────────────
|
|
333
|
+
function switchToApp(index) {
|
|
334
|
+
const app = cfg.apps[index]
|
|
335
|
+
if (!app) return
|
|
336
|
+
|
|
337
|
+
hideIdleHint()
|
|
338
|
+
|
|
339
|
+
document.querySelectorAll('.dock-item').forEach((el, i) => {
|
|
340
|
+
el.classList.toggle('active', i === index)
|
|
341
|
+
})
|
|
342
|
+
|
|
343
|
+
const alreadyLoaded = !!iframes[app.url]
|
|
344
|
+
|
|
345
|
+
if (cfg.iframeMode === 'destroy') {
|
|
346
|
+
$iframeContainer.innerHTML = ''
|
|
347
|
+
Object.keys(iframes).forEach((k) => delete iframes[k])
|
|
348
|
+
} else {
|
|
349
|
+
Object.values(iframes).forEach((f) => f.classList.remove('active'))
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (!iframes[app.url]) {
|
|
353
|
+
if (!alreadyLoaded) showLoading(app.label)
|
|
354
|
+
|
|
355
|
+
const $frame = document.createElement('iframe')
|
|
356
|
+
$frame.allow = 'fullscreen; geolocation'
|
|
357
|
+
$frame.sandbox = 'allow-same-origin allow-scripts allow-forms allow-popups allow-pointer-lock'
|
|
358
|
+
$frame.style.touchAction = 'auto'
|
|
359
|
+
|
|
360
|
+
$frame.addEventListener('load', hideLoading, { once: true })
|
|
361
|
+
|
|
362
|
+
$iframeContainer.appendChild($frame)
|
|
363
|
+
$frame.src = app.url
|
|
364
|
+
|
|
365
|
+
iframes[app.url] = $frame
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
iframes[app.url].classList.add('active')
|
|
369
|
+
hideDock()
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// ─── Corner trigger zone: position it ────────────────────────────────────────
|
|
373
|
+
function positionCornerZone() {
|
|
374
|
+
const [vert, horiz] = cfg.triggerCorner.split('-')
|
|
375
|
+
const sz = 120
|
|
376
|
+
|
|
377
|
+
$triggerCorner.style.width = sz + 'px'
|
|
378
|
+
$triggerCorner.style.height = sz + 'px'
|
|
379
|
+
|
|
380
|
+
$triggerCorner.style.top = ''
|
|
381
|
+
$triggerCorner.style.bottom = ''
|
|
382
|
+
$triggerCorner.style.left = ''
|
|
383
|
+
$triggerCorner.style.right = ''
|
|
384
|
+
|
|
385
|
+
$triggerCorner.style[vert] = '0'
|
|
386
|
+
$triggerCorner.style[horiz] = '0'
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// ─── Gesture: double-tap on corner (touch) + double-click (mouse) ─────────────
|
|
390
|
+
{
|
|
391
|
+
let lastTapTime = 0
|
|
392
|
+
const DOUBLE_TAP_MS = 400
|
|
393
|
+
|
|
394
|
+
$triggerCorner.addEventListener(
|
|
395
|
+
'touchend',
|
|
396
|
+
(e) => {
|
|
397
|
+
const now = Date.now()
|
|
398
|
+
if (now - lastTapTime < DOUBLE_TAP_MS) {
|
|
399
|
+
e.preventDefault()
|
|
400
|
+
lastTapTime = 0
|
|
401
|
+
showDock()
|
|
402
|
+
if (navigator.vibrate) navigator.vibrate(8)
|
|
403
|
+
} else {
|
|
404
|
+
lastTapTime = now
|
|
405
|
+
}
|
|
406
|
+
},
|
|
407
|
+
{ passive: false }
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
$triggerCorner.addEventListener('dblclick', (e) => {
|
|
411
|
+
e.preventDefault()
|
|
412
|
+
showDock()
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
$triggerCorner.addEventListener('contextmenu', (e) => e.preventDefault())
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// ─── Gesture: mouse hover at screen edge ─────────────────────────────────────
|
|
419
|
+
{
|
|
420
|
+
const EDGE_ZONE = 4
|
|
421
|
+
let edgeTimer = null
|
|
422
|
+
|
|
423
|
+
document.addEventListener('mousemove', (e) => {
|
|
424
|
+
if (dockVisible) return
|
|
425
|
+
const W = window.innerWidth
|
|
426
|
+
const H = window.innerHeight
|
|
427
|
+
let atEdge = false
|
|
428
|
+
|
|
429
|
+
if (pos === 'bottom' && e.clientY >= H - EDGE_ZONE) atEdge = true
|
|
430
|
+
if (pos === 'top' && e.clientY <= EDGE_ZONE) atEdge = true
|
|
431
|
+
if (pos === 'left' && e.clientX <= EDGE_ZONE) atEdge = true
|
|
432
|
+
if (pos === 'right' && e.clientX >= W - EDGE_ZONE) atEdge = true
|
|
433
|
+
|
|
434
|
+
if (atEdge && !edgeTimer) {
|
|
435
|
+
edgeTimer = setTimeout(() => {
|
|
436
|
+
showDock()
|
|
437
|
+
edgeTimer = null
|
|
438
|
+
}, 300)
|
|
439
|
+
} else if (!atEdge && edgeTimer) {
|
|
440
|
+
clearTimeout(edgeTimer)
|
|
441
|
+
edgeTimer = null
|
|
442
|
+
}
|
|
443
|
+
})
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// ─── Init ────────────────────────────────────────────────────────────────────
|
|
447
|
+
positionCornerZone()
|
|
448
|
+
buildDock()
|
|
449
|
+
|
|
450
|
+
const autostartIdx = cfg.apps.findIndex((a) => a.autostart)
|
|
451
|
+
if (autostartIdx >= 0) {
|
|
452
|
+
switchToApp(autostartIdx)
|
|
453
|
+
} else {
|
|
454
|
+
setIdleHint()
|
|
455
|
+
}
|
|
456
|
+
})()
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 72 72" width="72" height="72">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
|
|
4
|
+
<stop offset="0%" stop-color="#8e8e93"/>
|
|
5
|
+
<stop offset="100%" stop-color="#636366"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
</defs>
|
|
8
|
+
<rect width="72" height="72" rx="16" fill="url(#bg)"/>
|
|
9
|
+
<g transform="translate(36,36)">
|
|
10
|
+
<!-- Gear body -->
|
|
11
|
+
<path d="
|
|
12
|
+
M-2.8-16.5 a2.8 2.8 0 0 1 5.6 0 l.4 2.8 a13 13 0 0 1 3.6 1.5 l2.4-1.4 a2.8 2.8 0 0 1 3.8 1 l2.8 4.9 a2.8 2.8 0 0 1-1 3.8 l-2 1.4 a13 13 0 0 1 0 3.9 l2 1.4 a2.8 2.8 0 0 1 1 3.8 l-2.8 4.9 a2.8 2.8 0 0 1-3.8 1 l-2.4-1.4 a13 13 0 0 1-3.6 1.5 l-.4 2.8 a2.8 2.8 0 0 1-5.6 0 l-.4-2.8 a13 13 0 0 1-3.6-1.5 l-2.4 1.4 a2.8 2.8 0 0 1-3.8-1 l-2.8-4.9 a2.8 2.8 0 0 1 1-3.8 l2-1.4 a13 13 0 0 1 0-3.9 l-2-1.4 a2.8 2.8 0 0 1-1-3.8 l2.8-4.9 a2.8 2.8 0 0 1 3.8-1 l2.4 1.4 a13 13 0 0 1 3.6-1.5 Z
|
|
13
|
+
" fill="#e5e5ea" stroke="none"/>
|
|
14
|
+
<!-- Center hole -->
|
|
15
|
+
<circle r="5.5" fill="url(#bg)"/>
|
|
16
|
+
<!-- Inner ring highlight -->
|
|
17
|
+
<circle r="5.5" fill="none" stroke="#c7c7cc" stroke-width="1"/>
|
|
18
|
+
<!-- Outer subtle highlight -->
|
|
19
|
+
<circle r="9" fill="none" stroke="#d1d1d6" stroke-width="0.5" opacity="0.5"/>
|
|
20
|
+
</g>
|
|
21
|
+
</svg>
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
|
6
|
+
<meta name="mobile-web-app-capable" content="yes" />
|
|
7
|
+
<title>SK Dock</title>
|
|
8
|
+
<link rel="stylesheet" href="dock.css" />
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<!-- Full-screen iframe container: only the active iframe is visible -->
|
|
12
|
+
<div id="iframe-container"></div>
|
|
13
|
+
|
|
14
|
+
<!-- Welcome screen shown before any app is opened -->
|
|
15
|
+
<div id="idle-hint">
|
|
16
|
+
<img id="idle-logo" src="app-icon.svg" alt="App Dock" />
|
|
17
|
+
<div id="idle-title">App Dock</div>
|
|
18
|
+
<div id="idle-version"></div>
|
|
19
|
+
<div id="idle-hint-text"></div>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<!-- Dimming overlay shown when dock is open; tap to close -->
|
|
23
|
+
<div id="dock-backdrop"></div>
|
|
24
|
+
|
|
25
|
+
<!-- The dock itself - slides in from the configured edge -->
|
|
26
|
+
<nav id="dock" role="navigation" aria-label="App dock">
|
|
27
|
+
<div id="dock-inner"></div>
|
|
28
|
+
</nav>
|
|
29
|
+
|
|
30
|
+
<!-- Invisible corner hot-zone for long-press trigger -->
|
|
31
|
+
<div id="trigger-corner" aria-hidden="true"></div>
|
|
32
|
+
|
|
33
|
+
<!-- Loading indicator shown while an iframe is loading for the first time -->
|
|
34
|
+
<div id="loading-overlay" aria-hidden="true">
|
|
35
|
+
<div class="spinner"></div>
|
|
36
|
+
<span id="loading-label"></span>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<script src="dock.js"></script>
|
|
40
|
+
</body>
|
|
41
|
+
</html>
|
package/public/main.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(()=>{var e,r,t={316(){}},a={};function o(e){var r=a[e];if(void 0!==r)return r.exports;var n=a[e]={exports:{}};return t[e](n,n.exports,o),n.exports}o.m=t,o.c=a,o.f={},o.e=e=>Promise.all(Object.keys(o.f).reduce((r,t)=>(o.f[t](e,r),r),[])),o.u=e=>e+".js",o.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),o.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r),e={},r="signalk-app-dock:",o.l=(t,a,n,i)=>{if(e[t])e[t].push(a);else{var l,c;if(void 0!==n)for(var s=document.getElementsByTagName("script"),p=0;p<s.length;p++){var u=s[p];if(u.getAttribute("src")==t||u.getAttribute("data-webpack")==r+n){l=u;break}}l||(c=!0,(l=document.createElement("script")).charset="utf-8",o.nc&&l.setAttribute("nonce",o.nc),l.setAttribute("data-webpack",r+n),l.src=t),e[t]=[a];var d=(r,a)=>{l.onerror=l.onload=null,clearTimeout(f);var o=e[t];if(delete e[t],l.parentNode&&l.parentNode.removeChild(l),o&&o.forEach(e=>e(a)),r)return r(a)},f=setTimeout(d.bind(null,void 0,{type:"timeout",target:l}),12e4);l.onerror=d.bind(null,l.onerror),l.onload=d.bind(null,l.onload),c&&document.head.appendChild(l)}},(()=>{o.S={};var e={},r={};o.I=(t,a)=>{a||(a=[]);var n=r[t];if(n||(n=r[t]={}),!(a.indexOf(n)>=0)){if(a.push(n),e[t])return e[t];o.o(o.S,t)||(o.S[t]={});var i=o.S[t],l="signalk-app-dock",c=[];return"default"===t&&((e,r,t,a)=>{var n=i[e]=i[e]||{},c=n[r];(!c||!c.loaded&&(1!=!c.eager?a:l>c.from))&&(n[r]={get:()=>o.e(540).then(()=>()=>o(540)),from:l,eager:!1})})("react","19.2.4"),e[t]=c.length?Promise.all(c).then(()=>e[t]=1):1}}})(),(()=>{var e;o.g.importScripts&&(e=o.g.location+"");var r=o.g.document;if(!e&&r&&(r.currentScript&&"SCRIPT"===r.currentScript.tagName.toUpperCase()&&(e=r.currentScript.src),!e)){var t=r.getElementsByTagName("script");if(t.length)for(var a=t.length-1;a>-1&&(!e||!/^http(s?):/.test(e));)e=t[a--].src}if(!e)throw new Error("Automatic publicPath is not supported in this browser");e=e.replace(/^blob:/,"").replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),o.p=e})(),(()=>{var e={792:0};o.f.j=(r,t)=>{var a=o.o(e,r)?e[r]:void 0;if(0!==a)if(a)t.push(a[2]);else{var n=new Promise((t,o)=>a=e[r]=[t,o]);t.push(a[2]=n);var i=o.p+o.u(r),l=new Error;o.l(i,t=>{if(o.o(e,r)&&(0!==(a=e[r])&&(e[r]=void 0),a)){var n=t&&("load"===t.type?"missing":t.type),i=t&&t.target&&t.target.src;l.message="Loading chunk "+r+" failed.\n("+n+": "+i+")",l.name="ChunkLoadError",l.type=n,l.request=i,a[1](l)}},"chunk-"+r,r)}};var r=(r,t)=>{var a,n,[i,l,c]=t,s=0;if(i.some(r=>0!==e[r])){for(a in l)o.o(l,a)&&(o.m[a]=l[a]);c&&c(o)}for(r&&r(t);s<i.length;s++)n=i[s],o.o(e,n)&&e[n]&&e[n][0](),e[n]=0},t=self.webpackChunksignalk_app_dock=self.webpackChunksignalk_app_dock||[];t.forEach(r.bind(null,0)),t.push=r.bind(null,t.push.bind(t))})(),o(316)})();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var signalk_app_dock;(()=>{"use strict";var e,r,t,n,o,a,i,u,l,s,p,f,c,d,h,v,g,m,b,y={623(e,r,t){var n={"./PluginConfigurationPanel":()=>t.e(805).then(()=>()=>t(805))},o=(e,r)=>(t.R=r,r=t.o(n,e)?n[e]():Promise.resolve().then(()=>{throw new Error('Module "'+e+'" does not exist in container.')}),t.R=void 0,r),a=(e,r)=>{if(t.S){var n="default",o=t.S[n];if(o&&o!==e)throw new Error("Container initialization failed as it has already been initialized with a different share scope");return t.S[n]=e,t.I(n,r)}};t.d(r,{get:()=>o,init:()=>a})}},k={};function w(e){var r=k[e];if(void 0!==r)return r.exports;var t=k[e]={exports:{}};return y[e](t,t.exports,w),t.exports}w.m=y,w.c=k,w.d=(e,r)=>{for(var t in r)w.o(r,t)&&!w.o(e,t)&&Object.defineProperty(e,t,{enumerable:!0,get:r[t]})},w.f={},w.e=e=>Promise.all(Object.keys(w.f).reduce((r,t)=>(w.f[t](e,r),r),[])),w.u=e=>e+".js",w.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),w.o=(e,r)=>Object.prototype.hasOwnProperty.call(e,r),e={},r="signalk-app-dock:",w.l=(t,n,o,a)=>{if(e[t])e[t].push(n);else{var i,u;if(void 0!==o)for(var l=document.getElementsByTagName("script"),s=0;s<l.length;s++){var p=l[s];if(p.getAttribute("src")==t||p.getAttribute("data-webpack")==r+o){i=p;break}}i||(u=!0,(i=document.createElement("script")).charset="utf-8",w.nc&&i.setAttribute("nonce",w.nc),i.setAttribute("data-webpack",r+o),i.src=t),e[t]=[n];var f=(r,n)=>{i.onerror=i.onload=null,clearTimeout(c);var o=e[t];if(delete e[t],i.parentNode&&i.parentNode.removeChild(i),o&&o.forEach(e=>e(n)),r)return r(n)},c=setTimeout(f.bind(null,void 0,{type:"timeout",target:i}),12e4);i.onerror=f.bind(null,i.onerror),i.onload=f.bind(null,i.onload),u&&document.head.appendChild(i)}},w.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},(()=>{w.S={};var e={},r={};w.I=(t,n)=>{n||(n=[]);var o=r[t];if(o||(o=r[t]={}),!(n.indexOf(o)>=0)){if(n.push(o),e[t])return e[t];w.o(w.S,t)||(w.S[t]={});var a=w.S[t],i="signalk-app-dock",u=[];return"default"===t&&((e,r,t,n)=>{var o=a[e]=a[e]||{},u=o[r];(!u||!u.loaded&&(1!=!u.eager?n:i>u.from))&&(o[r]={get:()=>w.e(540).then(()=>()=>w(540)),from:i,eager:!1})})("react","19.2.4"),e[t]=u.length?Promise.all(u).then(()=>e[t]=1):1}}})(),(()=>{var e;w.g.importScripts&&(e=w.g.location+"");var r=w.g.document;if(!e&&r&&(r.currentScript&&"SCRIPT"===r.currentScript.tagName.toUpperCase()&&(e=r.currentScript.src),!e)){var t=r.getElementsByTagName("script");if(t.length)for(var n=t.length-1;n>-1&&(!e||!/^http(s?):/.test(e));)e=t[n--].src}if(!e)throw new Error("Automatic publicPath is not supported in this browser");e=e.replace(/^blob:/,"").replace(/#.*$/,"").replace(/\?.*$/,"").replace(/\/[^\/]+$/,"/"),w.p=e})(),t=e=>{var r=e=>e.split(".").map(e=>+e==e?+e:e),t=/^([^-+]+)?(?:-([^+]+))?(?:\+(.+))?$/.exec(e),n=t[1]?r(t[1]):[];return t[2]&&(n.length++,n.push.apply(n,r(t[2]))),t[3]&&(n.push([]),n.push.apply(n,r(t[3]))),n},n=(e,r)=>{e=t(e),r=t(r);for(var n=0;;){if(n>=e.length)return n<r.length&&"u"!=(typeof r[n])[0];var o=e[n],a=(typeof o)[0];if(n>=r.length)return"u"==a;var i=r[n],u=(typeof i)[0];if(a!=u)return"o"==a&&"n"==u||"s"==u||"u"==a;if("o"!=a&&"u"!=a&&o!=i)return o<i;n++}},o=e=>{var r=e[0],t="";if(1===e.length)return"*";if(r+.5){t+=0==r?">=":-1==r?"<":1==r?"^":2==r?"~":r>0?"=":"!=";for(var n=1,a=1;a<e.length;a++)n--,t+="u"==(typeof(u=e[a]))[0]?"-":(n>0?".":"")+(n=2,u);return t}var i=[];for(a=1;a<e.length;a++){var u=e[a];i.push(0===u?"not("+l()+")":1===u?"("+l()+" || "+l()+")":2===u?i.pop()+" "+i.pop():o(u))}return l();function l(){return i.pop().replace(/^\((.+)\)$/,"$1")}},a=(e,r)=>{if(0 in e){r=t(r);var n=e[0],o=n<0;o&&(n=-n-1);for(var i=0,u=1,l=!0;;u++,i++){var s,p,f=u<e.length?(typeof e[u])[0]:"";if(i>=r.length||"o"==(p=(typeof(s=r[i]))[0]))return!l||("u"==f?u>n&&!o:""==f!=o);if("u"==p){if(!l||"u"!=f)return!1}else if(l)if(f==p)if(u<=n){if(s!=e[u])return!1}else{if(o?s>e[u]:s<e[u])return!1;s!=e[u]&&(l=!1)}else if("s"!=f&&"n"!=f){if(o||u<=n)return!1;l=!1,u--}else{if(u<=n||p<f!=o)return!1;l=!1}else"s"!=f&&"n"!=f&&(l=!1,u--)}}var c=[],d=c.pop.bind(c);for(i=1;i<e.length;i++){var h=e[i];c.push(1==h?d()|d():2==h?d()&d():h?a(h,r):!d())}return!!d()},i=(e,r)=>e&&w.o(e,r),u=e=>(e.loaded=1,e.get()),l=e=>Object.keys(e).reduce((r,t)=>(e[t].eager&&(r[t]=e[t]),r),{}),s=(e,r,t)=>{var o=t?l(e[r]):e[r];return Object.keys(o).reduce((e,r)=>!e||!o[e].loaded&&n(e,r)?r:e,0)},p=(e,r,t,n)=>"Unsatisfied version "+t+" from "+(t&&e[r][t].from)+" of shared singleton module "+r+" (required "+o(n)+")",f=e=>{throw new Error(e)},c=e=>{"undefined"!=typeof console&&console.warn&&console.warn(e)},d=(e,r,t)=>t?t():((e,r)=>f("Shared module "+r+" doesn't exist in shared scope "+e))(e,r),h=(e=>function(r,t,n,o,a){var i=w.I(r);return i&&i.then&&!n?i.then(e.bind(e,r,w.S[r],t,!1,o,a)):e(r,w.S[r],t,n,o,a)})((e,r,t,n,o,l)=>{if(!i(r,t))return d(e,t,l);var f=s(r,t,n);return a(o,f)||c(p(r,t,f,o)),u(r[t][f])}),v={},g={231:()=>h("default","react",!1,[1,19],()=>w.e(540).then(()=>()=>w(540)))},m={805:[231]},b={},w.f.consumes=(e,r)=>{w.o(m,e)&&m[e].forEach(e=>{if(w.o(v,e))return r.push(v[e]);if(!b[e]){var t=r=>{v[e]=0,w.m[e]=t=>{delete w.c[e],t.exports=r()}};b[e]=!0;var n=r=>{delete v[e],w.m[e]=t=>{throw delete w.c[e],r}};try{var o=g[e]();o.then?r.push(v[e]=o.then(t).catch(n)):t(o)}catch(e){n(e)}}})},(()=>{var e={228:0};w.f.j=(r,t)=>{var n=w.o(e,r)?e[r]:void 0;if(0!==n)if(n)t.push(n[2]);else{var o=new Promise((t,o)=>n=e[r]=[t,o]);t.push(n[2]=o);var a=w.p+w.u(r),i=new Error;w.l(a,t=>{if(w.o(e,r)&&(0!==(n=e[r])&&(e[r]=void 0),n)){var o=t&&("load"===t.type?"missing":t.type),a=t&&t.target&&t.target.src;i.message="Loading chunk "+r+" failed.\n("+o+": "+a+")",i.name="ChunkLoadError",i.type=o,i.request=a,n[1](i)}},"chunk-"+r,r)}};var r=(r,t)=>{var n,o,[a,i,u]=t,l=0;if(a.some(r=>0!==e[r])){for(n in i)w.o(i,n)&&(w.m[n]=i[n]);u&&u(w)}for(r&&r(t);l<a.length;l++)o=a[l],w.o(e,o)&&e[o]&&e[o][0](),e[o]=0},t=self.webpackChunksignalk_app_dock=self.webpackChunksignalk_app_dock||[];t.forEach(r.bind(null,0)),t.push=r.bind(null,t.push.bind(t))})();var S=w(623);signalk_app_dock=S})();
|