@kennofizet/apphub-frontend 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/README.md +84 -0
- package/package.json +31 -0
- package/src/api/coreApi.js +25 -0
- package/src/api/index.js +80 -0
- package/src/composables/createZoneContext.js +156 -0
- package/src/composables/useAppHubHostApi.js +24 -0
- package/src/composables/useAppHubZoneContext.js +11 -0
- package/src/composables/useDevOriginToggle.js +40 -0
- package/src/i18n/index.js +16 -0
- package/src/i18n/resolveLang.js +6 -0
- package/src/i18n/resolveTheme.js +30 -0
- package/src/i18n/translations/en.js +303 -0
- package/src/i18n/translations/vi.js +302 -0
- package/src/index.js +427 -0
- package/src/moduleStore.js +10 -0
- package/src/modules/app-store/components/AppHubAppStoreApp.vue +210 -0
- package/src/modules/app-store/components/AppHubAppStoreCard.vue +88 -0
- package/src/modules/app-store/components/AppHubAppStoreSettingsPanel.vue +266 -0
- package/src/modules/app-store/components/AppHubAppVersionHistory.vue +77 -0
- package/src/modules/app-store/components/AppHubDevReviewPanel.vue +206 -0
- package/src/modules/app-store/components/AppHubDraftStoreApp.vue +184 -0
- package/src/modules/app-store/components/AppHubDraftStoreCard.vue +116 -0
- package/src/modules/app-store/composables/useAppStore.js +206 -0
- package/src/modules/app-store/composables/useCatalogInfiniteScroll.js +47 -0
- package/src/modules/app-store/constants/catalogModes.js +2 -0
- package/src/modules/app-store/data/defaultCatalog.js +19 -0
- package/src/modules/app-store/index.js +9 -0
- package/src/modules/app-store/utils/normalizeCatalogApp.js +37 -0
- package/src/modules/desktop/components/AppHubDesktop.vue +1510 -0
- package/src/modules/desktop/components/AppHubDesktopDevOriginBar.vue +57 -0
- package/src/modules/desktop/components/AppHubDesktopDropLayer.vue +15 -0
- package/src/modules/desktop/components/AppHubDesktopDropTarget.vue +32 -0
- package/src/modules/desktop/components/AppHubDesktopIconContextMenu.vue +74 -0
- package/src/modules/desktop/components/AppHubDesktopIconFolder.vue +60 -0
- package/src/modules/desktop/components/AppHubDesktopIconGroup.vue +58 -0
- package/src/modules/desktop/components/AppHubDesktopIconInfoDialog.vue +33 -0
- package/src/modules/desktop/components/AppHubDesktopIconRenameDialog.vue +62 -0
- package/src/modules/desktop/components/AppHubDesktopSettings.vue +28 -0
- package/src/modules/desktop/components/AppHubDropInstallBadge.vue +65 -0
- package/src/modules/desktop/components/AppHubDuplicateAppDialog.vue +38 -0
- package/src/modules/desktop/components/AppHubGuideApp.vue +278 -0
- package/src/modules/desktop/components/AppHubOriginBlockScreen.vue +105 -0
- package/src/modules/desktop/components/AppHubOriginLoadingScreen.vue +23 -0
- package/src/modules/desktop/components/AppHubPlaceholderApp.vue +14 -0
- package/src/modules/desktop/components/AppHubSettingsApp.vue +319 -0
- package/src/modules/desktop/components/AppHubStartButton.vue +24 -0
- package/src/modules/desktop/components/AppHubStartMenu.vue +182 -0
- package/src/modules/desktop/components/AppHubTaskbarPins.vue +23 -0
- package/src/modules/desktop/components/settings/AppHubSettingsKeyboardPanel.vue +82 -0
- package/src/modules/desktop/components/settings/AppHubSettingsScreenPanel.vue +41 -0
- package/src/modules/desktop/components/settings/AppHubSettingsStartMenuPanel.vue +95 -0
- package/src/modules/desktop/composables/simulateInstallProgress.js +15 -0
- package/src/modules/desktop/composables/useDesktopDropInstall.js +272 -0
- package/src/modules/desktop/composables/useDesktopHubSettings.js +51 -0
- package/src/modules/desktop/composables/useDesktopIconDrag.js +207 -0
- package/src/modules/desktop/composables/useDesktopShell.js +335 -0
- package/src/modules/desktop/data/builtinApps.js +77 -0
- package/src/modules/desktop/index.js +12 -0
- package/src/modules/desktop/styles/desktop.css +3104 -0
- package/src/modules/desktop/styles/theme.css +616 -0
- package/src/modules/desktop/utils/desktopGrid.js +43 -0
- package/src/modules/desktop/utils/desktopIconGroups.js +103 -0
- package/src/modules/desktop/utils/desktopSession.js +40 -0
- package/src/modules/desktop/utils/desktopSettings.js +37 -0
- package/src/modules/desktop/utils/dropPackageParser.js +140 -0
- package/src/modules/desktop/utils/duplicateAppUtils.js +28 -0
- package/src/modules/desktop/utils/hubKeyboardSettings.js +63 -0
- package/src/modules/desktop/utils/recentApps.js +148 -0
- package/src/modules/desktop/utils/startMenuFavorites.js +100 -0
- package/src/modules/desktop/utils/startMenuPins.js +90 -0
- package/src/modules/notifications/components/AppHubDesktopNotifications.vue +54 -0
- package/src/modules/notifications/composables/createDesktopNotifications.js +86 -0
- package/src/modules/notifications/index.js +9 -0
- package/src/modules/notifications/styles/notifications.css +118 -0
- package/src/modules/notifications/utils/parseApiError.js +29 -0
- package/src/modules/runner/components/AppHubRunner.vue +292 -0
- package/src/modules/runner/index.js +1 -0
- package/src/modules/window-manager/components/AppHubWindowFrame.vue +224 -0
- package/src/modules/window-manager/composables/useWindowManager.js +652 -0
- package/src/modules/window-manager/index.js +7 -0
- package/src/modules/window-manager/utils/sessionLayout.js +28 -0
- package/src/modules/window-manager/utils/windowLayout.js +236 -0
- package/src/modules/window-manager/utils/windowSnap.js +146 -0
- package/src/utils/bootstrapCache.js +47 -0
- package/src/utils/devOriginSettings.js +22 -0
- package/src/utils/launchUrl.js +111 -0
- package/src/utils/originSafety.js +267 -0
- package/src/utils/safeStorage.js +191 -0
- package/src/utils/semver.js +30 -0
- package/src/utils/zoneContext.js +38 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { safeParseJson, sanitizeWindowLayout } from '../../../utils/safeStorage.js'
|
|
2
|
+
|
|
3
|
+
export const TASKBAR_HEIGHT = 48
|
|
4
|
+
export const WINDOW_MIN_WIDTH = 280
|
|
5
|
+
export const WINDOW_MIN_HEIGHT = 200
|
|
6
|
+
const STORAGE_PREFIX = 'apphub-window-layout:'
|
|
7
|
+
|
|
8
|
+
/** Measured from `.apphub-desktop__workarea` — not the browser viewport. */
|
|
9
|
+
let desktopWorkArea = { width: 0, height: 0 }
|
|
10
|
+
|
|
11
|
+
export function setDesktopWorkArea(area) {
|
|
12
|
+
desktopWorkArea = {
|
|
13
|
+
width: Math.max(0, area?.width ?? 0),
|
|
14
|
+
height: Math.max(0, area?.height ?? 0),
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function getWorkArea() {
|
|
19
|
+
if (desktopWorkArea.width > 0 && desktopWorkArea.height > 0) {
|
|
20
|
+
return { ...desktopWorkArea }
|
|
21
|
+
}
|
|
22
|
+
return {
|
|
23
|
+
width: window.innerWidth,
|
|
24
|
+
height: Math.max(200, window.innerHeight - TASKBAR_HEIGHT),
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function fullscreenBounds() {
|
|
29
|
+
const area = getWorkArea()
|
|
30
|
+
return { x: 0, y: 0, width: area.width, height: area.height }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function centerWindow(width, height) {
|
|
34
|
+
const area = getWorkArea()
|
|
35
|
+
return {
|
|
36
|
+
x: Math.max(0, Math.round((area.width - width) / 2)),
|
|
37
|
+
y: Math.max(0, Math.round((area.height - height) / 2)),
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function clampWindowToWorkArea(win) {
|
|
42
|
+
const { width: areaW, height: areaH } = getWorkArea()
|
|
43
|
+
win.x = Math.min(Math.max(0, win.x), Math.max(0, areaW - win.width))
|
|
44
|
+
win.y = Math.min(Math.max(0, win.y), Math.max(0, areaH - win.height))
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function clampWindowDimensions(win) {
|
|
48
|
+
const area = getWorkArea()
|
|
49
|
+
const minW = Math.min(win.miniWidth ?? WINDOW_MIN_WIDTH, area.width)
|
|
50
|
+
const minH = Math.min(win.miniHeight ?? WINDOW_MIN_HEIGHT, area.height)
|
|
51
|
+
|
|
52
|
+
win.width = Math.max(minW, Math.min(area.width, win.width))
|
|
53
|
+
win.height = Math.max(minH, Math.min(area.height, win.height))
|
|
54
|
+
clampWindowToWorkArea(win)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** @param {'n'|'s'|'e'|'w'|'ne'|'nw'|'se'|'sw'} edge */
|
|
58
|
+
export function applyWindowResize(win, edge, dx, dy) {
|
|
59
|
+
const area = getWorkArea()
|
|
60
|
+
const minW = Math.min(win.miniWidth ?? WINDOW_MIN_WIDTH, area.width)
|
|
61
|
+
const minH = Math.min(win.miniHeight ?? WINDOW_MIN_HEIGHT, area.height)
|
|
62
|
+
|
|
63
|
+
let { x, y, width, height } = win
|
|
64
|
+
const right = x + width
|
|
65
|
+
const bottom = y + height
|
|
66
|
+
|
|
67
|
+
if (edge.includes('e')) width += dx
|
|
68
|
+
if (edge.includes('w')) {
|
|
69
|
+
x += dx
|
|
70
|
+
width -= dx
|
|
71
|
+
}
|
|
72
|
+
if (edge.includes('s')) height += dy
|
|
73
|
+
if (edge.includes('n')) {
|
|
74
|
+
y += dy
|
|
75
|
+
height -= dy
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
width = Math.max(minW, Math.min(area.width, width))
|
|
79
|
+
height = Math.max(minH, Math.min(area.height, height))
|
|
80
|
+
|
|
81
|
+
if (edge.includes('w')) x = right - width
|
|
82
|
+
if (edge.includes('n')) y = bottom - height
|
|
83
|
+
|
|
84
|
+
x = Math.max(0, Math.min(x, area.width - width))
|
|
85
|
+
y = Math.max(0, Math.min(y, area.height - height))
|
|
86
|
+
|
|
87
|
+
win.x = x
|
|
88
|
+
win.y = y
|
|
89
|
+
win.width = width
|
|
90
|
+
win.height = height
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function loadWindowLayout(layoutKey) {
|
|
94
|
+
if (!layoutKey || typeof layoutKey !== 'string' || layoutKey.length > 80) return null
|
|
95
|
+
try {
|
|
96
|
+
const raw = localStorage.getItem(STORAGE_PREFIX + layoutKey)
|
|
97
|
+
const parsed = safeParseJson(raw, 32 * 1024)
|
|
98
|
+
return parsed ? sanitizeWindowLayout(parsed) : null
|
|
99
|
+
} catch {
|
|
100
|
+
return null
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function saveWindowLayout(layoutKey, layout) {
|
|
105
|
+
if (!layoutKey) return
|
|
106
|
+
try {
|
|
107
|
+
localStorage.setItem(STORAGE_PREFIX + layoutKey, JSON.stringify(layout))
|
|
108
|
+
} catch {
|
|
109
|
+
/* ignore quota / private mode */
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Resolve initial bounds for a new window (fullscreen default, or restored mini position).
|
|
115
|
+
*/
|
|
116
|
+
export function resolveOpenLayout(definition) {
|
|
117
|
+
const miniWidth = definition.miniWidth ?? definition.width ?? 720
|
|
118
|
+
const miniHeight = definition.miniHeight ?? definition.height ?? 480
|
|
119
|
+
const layoutKey = definition.layoutKey ?? null
|
|
120
|
+
|
|
121
|
+
if (!layoutKey) {
|
|
122
|
+
const pos = definition.x != null && definition.y != null
|
|
123
|
+
? { x: definition.x, y: definition.y }
|
|
124
|
+
: {
|
|
125
|
+
x: 80 + (definition.offsetIndex ?? 0) * 28,
|
|
126
|
+
y: 60 + (definition.offsetIndex ?? 0) * 28,
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
display: 'mini',
|
|
130
|
+
layoutKey: null,
|
|
131
|
+
miniWidth,
|
|
132
|
+
miniHeight,
|
|
133
|
+
width: definition.width ?? miniWidth,
|
|
134
|
+
height: definition.height ?? miniHeight,
|
|
135
|
+
...pos,
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const saved = loadWindowLayout(layoutKey)
|
|
140
|
+
const defaultDisplay = definition.defaultDisplay ?? 'fullscreen'
|
|
141
|
+
|
|
142
|
+
if (saved?.display === 'mini') {
|
|
143
|
+
const mini = saved.mini ?? saved
|
|
144
|
+
const width = mini.width ?? miniWidth
|
|
145
|
+
const height = mini.height ?? miniHeight
|
|
146
|
+
const hasPosition = mini.x != null && mini.y != null
|
|
147
|
+
const pos = hasPosition ? { x: mini.x, y: mini.y } : centerWindow(width, height)
|
|
148
|
+
return {
|
|
149
|
+
display: 'mini',
|
|
150
|
+
layoutKey,
|
|
151
|
+
miniWidth,
|
|
152
|
+
miniHeight,
|
|
153
|
+
width,
|
|
154
|
+
height,
|
|
155
|
+
...pos,
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (saved?.display === 'fullscreen' || defaultDisplay === 'fullscreen') {
|
|
160
|
+
return {
|
|
161
|
+
display: 'fullscreen',
|
|
162
|
+
layoutKey,
|
|
163
|
+
miniWidth,
|
|
164
|
+
miniHeight,
|
|
165
|
+
...fullscreenBounds(),
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const pos = centerWindow(miniWidth, miniHeight)
|
|
170
|
+
return {
|
|
171
|
+
display: 'mini',
|
|
172
|
+
layoutKey,
|
|
173
|
+
miniWidth,
|
|
174
|
+
miniHeight,
|
|
175
|
+
width: miniWidth,
|
|
176
|
+
height: miniHeight,
|
|
177
|
+
...pos,
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function readMiniBounds(saved, fallback) {
|
|
182
|
+
const mini = saved?.mini ?? saved
|
|
183
|
+
return {
|
|
184
|
+
x: mini?.x ?? null,
|
|
185
|
+
y: mini?.y ?? null,
|
|
186
|
+
width: mini?.width ?? fallback.width,
|
|
187
|
+
height: mini?.height ?? fallback.height,
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function layoutSnapshot(win) {
|
|
192
|
+
const fallback = {
|
|
193
|
+
width: win.miniWidth ?? win.width,
|
|
194
|
+
height: win.miniHeight ?? win.height,
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (win.display === 'fullscreen') {
|
|
198
|
+
const existing = loadWindowLayout(win.layoutKey)
|
|
199
|
+
const mini = readMiniBounds(existing, fallback)
|
|
200
|
+
return {
|
|
201
|
+
display: 'fullscreen',
|
|
202
|
+
mini: {
|
|
203
|
+
x: mini.x,
|
|
204
|
+
y: mini.y,
|
|
205
|
+
width: mini.width,
|
|
206
|
+
height: mini.height,
|
|
207
|
+
},
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
display: 'mini',
|
|
213
|
+
mini: {
|
|
214
|
+
x: win.x,
|
|
215
|
+
y: win.y,
|
|
216
|
+
width: win.width,
|
|
217
|
+
height: win.height,
|
|
218
|
+
},
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function miniBoundsFromStorage(layoutKey, win) {
|
|
223
|
+
const saved = loadWindowLayout(layoutKey)
|
|
224
|
+
const fallback = {
|
|
225
|
+
width: win.miniWidth ?? 820,
|
|
226
|
+
height: win.miniHeight ?? 520,
|
|
227
|
+
}
|
|
228
|
+
const mini = readMiniBounds(saved, fallback)
|
|
229
|
+
const hasPosition = mini.x != null && mini.y != null
|
|
230
|
+
const pos = hasPosition ? { x: mini.x, y: mini.y } : centerWindow(mini.width, mini.height)
|
|
231
|
+
return {
|
|
232
|
+
width: mini.width,
|
|
233
|
+
height: mini.height,
|
|
234
|
+
...pos,
|
|
235
|
+
}
|
|
236
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { centerWindow, fullscreenBounds, getWorkArea } from './windowLayout.js'
|
|
2
|
+
|
|
3
|
+
/** @typedef {'left'|'right'|'up'|'down'} SnapDirection */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Windows-style snap transitions (Ctrl+Alt+Arrow in browser — OS owns Win+Arrow).
|
|
7
|
+
* @returns {{ action: 'snap'|'fullscreen'|'restore'|'minimize'|'none', bounds?: object, snap?: string|null, display?: string }}
|
|
8
|
+
*/
|
|
9
|
+
export function computeSnapAction(win, direction) {
|
|
10
|
+
const area = getWorkArea()
|
|
11
|
+
const halfW = Math.floor(area.width / 2)
|
|
12
|
+
const halfH = Math.floor(area.height / 2)
|
|
13
|
+
const snap = win.snap ?? (win.display === 'fullscreen' ? 'fullscreen' : null)
|
|
14
|
+
|
|
15
|
+
if (direction === 'left') {
|
|
16
|
+
return {
|
|
17
|
+
action: 'snap',
|
|
18
|
+
bounds: { x: 0, y: 0, width: halfW, height: area.height },
|
|
19
|
+
snap: 'left',
|
|
20
|
+
display: 'mini',
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (direction === 'right') {
|
|
25
|
+
return {
|
|
26
|
+
action: 'snap',
|
|
27
|
+
bounds: { x: halfW, y: 0, width: area.width - halfW, height: area.height },
|
|
28
|
+
snap: 'right',
|
|
29
|
+
display: 'mini',
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (direction === 'up') {
|
|
34
|
+
if (snap === 'left') {
|
|
35
|
+
return {
|
|
36
|
+
action: 'snap',
|
|
37
|
+
bounds: { x: 0, y: 0, width: halfW, height: halfH },
|
|
38
|
+
snap: 'top-left',
|
|
39
|
+
display: 'mini',
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (snap === 'right') {
|
|
43
|
+
return {
|
|
44
|
+
action: 'snap',
|
|
45
|
+
bounds: { x: halfW, y: 0, width: area.width - halfW, height: halfH },
|
|
46
|
+
snap: 'top-right',
|
|
47
|
+
display: 'mini',
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (snap === 'bottom-left') {
|
|
51
|
+
return {
|
|
52
|
+
action: 'snap',
|
|
53
|
+
bounds: { x: 0, y: 0, width: halfW, height: halfH },
|
|
54
|
+
snap: 'top-left',
|
|
55
|
+
display: 'mini',
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (snap === 'bottom-right') {
|
|
59
|
+
return {
|
|
60
|
+
action: 'snap',
|
|
61
|
+
bounds: { x: halfW, y: 0, width: area.width - halfW, height: halfH },
|
|
62
|
+
snap: 'top-right',
|
|
63
|
+
display: 'mini',
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (snap === 'fullscreen') return { action: 'none' }
|
|
67
|
+
return { action: 'fullscreen' }
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (direction === 'down') {
|
|
71
|
+
if (snap === 'fullscreen') return { action: 'restore' }
|
|
72
|
+
if (snap === 'left') {
|
|
73
|
+
return {
|
|
74
|
+
action: 'snap',
|
|
75
|
+
bounds: { x: 0, y: halfH, width: halfW, height: area.height - halfH },
|
|
76
|
+
snap: 'bottom-left',
|
|
77
|
+
display: 'mini',
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (snap === 'right') {
|
|
81
|
+
return {
|
|
82
|
+
action: 'snap',
|
|
83
|
+
bounds: { x: halfW, y: halfH, width: area.width - halfW, height: area.height - halfH },
|
|
84
|
+
snap: 'bottom-right',
|
|
85
|
+
display: 'mini',
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (snap === 'top-left' || snap === 'top-right' || snap === 'bottom-left' || snap === 'bottom-right') {
|
|
89
|
+
return { action: 'restore' }
|
|
90
|
+
}
|
|
91
|
+
return { action: 'minimize' }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return { action: 'none' }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function captureFloatingBounds(win) {
|
|
98
|
+
if (win.display === 'fullscreen') return
|
|
99
|
+
win.floatingBounds = {
|
|
100
|
+
x: win.x,
|
|
101
|
+
y: win.y,
|
|
102
|
+
width: win.width,
|
|
103
|
+
height: win.height,
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function defaultFloatingBounds(win) {
|
|
108
|
+
const width = win.miniWidth ?? 820
|
|
109
|
+
const height = win.miniHeight ?? 520
|
|
110
|
+
return { width, height, ...centerWindow(width, height) }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function resolveRestoreBounds(win) {
|
|
114
|
+
if (win.floatingBounds) {
|
|
115
|
+
return { ...win.floatingBounds }
|
|
116
|
+
}
|
|
117
|
+
if (win.layoutKey) {
|
|
118
|
+
return null
|
|
119
|
+
}
|
|
120
|
+
return defaultFloatingBounds(win)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function applyFullscreen(win) {
|
|
124
|
+
captureFloatingBounds(win)
|
|
125
|
+
Object.assign(win, fullscreenBounds())
|
|
126
|
+
win.display = 'fullscreen'
|
|
127
|
+
win.snap = 'fullscreen'
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Recompute snap bounds when work area size changes. */
|
|
131
|
+
export function getSnapBounds(snap) {
|
|
132
|
+
const area = getWorkArea()
|
|
133
|
+
const halfW = Math.floor(area.width / 2)
|
|
134
|
+
const halfH = Math.floor(area.height / 2)
|
|
135
|
+
|
|
136
|
+
const map = {
|
|
137
|
+
left: { x: 0, y: 0, width: halfW, height: area.height },
|
|
138
|
+
right: { x: halfW, y: 0, width: area.width - halfW, height: area.height },
|
|
139
|
+
'top-left': { x: 0, y: 0, width: halfW, height: halfH },
|
|
140
|
+
'top-right': { x: halfW, y: 0, width: area.width - halfW, height: halfH },
|
|
141
|
+
'bottom-left': { x: 0, y: halfH, width: halfW, height: area.height - halfH },
|
|
142
|
+
'bottom-right': { x: halfW, y: halfH, width: area.width - halfW, height: area.height - halfH },
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return map[snap] ?? null
|
|
146
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const KEY_PREFIX = 'apphub-bootstrap:'
|
|
2
|
+
|
|
3
|
+
function cacheKey(backendUrl) {
|
|
4
|
+
return KEY_PREFIX + String(backendUrl ?? '').replace(/\/$/, '')
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/** @returns {{ savedAt: number, isDevUser: boolean, origins: object } | null} */
|
|
8
|
+
export function loadBootstrapCache(backendUrl) {
|
|
9
|
+
if (typeof localStorage === 'undefined' || !backendUrl) return null
|
|
10
|
+
try {
|
|
11
|
+
const raw = localStorage.getItem(cacheKey(backendUrl))
|
|
12
|
+
if (!raw) return null
|
|
13
|
+
const parsed = JSON.parse(raw)
|
|
14
|
+
if (!parsed?.origins) return null
|
|
15
|
+
return parsed
|
|
16
|
+
} catch {
|
|
17
|
+
return null
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** @param {import('axios').AxiosResponse|{ data?: unknown }} bootstrapResponse */
|
|
22
|
+
export function saveBootstrapCache(backendUrl, bootstrapResponse) {
|
|
23
|
+
if (typeof localStorage === 'undefined' || !backendUrl) return
|
|
24
|
+
const data = bootstrapResponse?.data?.data ?? bootstrapResponse?.data ?? {}
|
|
25
|
+
if (!data.origins) return
|
|
26
|
+
try {
|
|
27
|
+
localStorage.setItem(cacheKey(backendUrl), JSON.stringify({
|
|
28
|
+
savedAt: Date.now(),
|
|
29
|
+
isDevUser: data.is_dev_user === true,
|
|
30
|
+
origins: data.origins,
|
|
31
|
+
}))
|
|
32
|
+
} catch {
|
|
33
|
+
/* ignore quota */
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Build axios-like bootstrap response from cache entry. */
|
|
38
|
+
export function bootstrapResponseFromCache(entry) {
|
|
39
|
+
return {
|
|
40
|
+
data: {
|
|
41
|
+
data: {
|
|
42
|
+
is_dev_user: entry.isDevUser === true,
|
|
43
|
+
origins: entry.origins,
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const STORAGE_KEY = 'apphub-dev-friendly-origins'
|
|
2
|
+
|
|
3
|
+
/** Default true — relaxed origin checks on localhost until dev turns off. */
|
|
4
|
+
export function loadDevFriendlyOriginsPreference() {
|
|
5
|
+
if (typeof localStorage === 'undefined') return true
|
|
6
|
+
try {
|
|
7
|
+
const raw = localStorage.getItem(STORAGE_KEY)
|
|
8
|
+
if (raw === null) return true
|
|
9
|
+
return raw !== '0' && raw !== 'false'
|
|
10
|
+
} catch {
|
|
11
|
+
return true
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function saveDevFriendlyOriginsPreference(enabled) {
|
|
16
|
+
if (typeof localStorage === 'undefined') return
|
|
17
|
+
try {
|
|
18
|
+
localStorage.setItem(STORAGE_KEY, enabled ? '1' : '0')
|
|
19
|
+
} catch {
|
|
20
|
+
/* ignore */
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { resolveRuntimeApiBase } from './originSafety.js'
|
|
2
|
+
|
|
3
|
+
const BLOCKED_PROTOCOLS = new Set(['javascript:', 'data:', 'blob:', 'file:'])
|
|
4
|
+
|
|
5
|
+
export const RUNTIME_HOSTED = 'hosted'
|
|
6
|
+
export const RUNTIME_IFRAME = 'iframe'
|
|
7
|
+
|
|
8
|
+
/** Hub-served bundle under {runtimeBase}/apps/{slug}/runtime/ */
|
|
9
|
+
export function isHubHostedRuntimeUrl(url, runtimeBaseOrOptions) {
|
|
10
|
+
const base = typeof runtimeBaseOrOptions === 'object'
|
|
11
|
+
? resolveRuntimeApiBase(runtimeBaseOrOptions)
|
|
12
|
+
: String(runtimeBaseOrOptions ?? '').replace(/\/$/, '')
|
|
13
|
+
|
|
14
|
+
if (!base || !url || typeof url !== 'string') return false
|
|
15
|
+
try {
|
|
16
|
+
const parsed = new URL(url)
|
|
17
|
+
const baseParsed = new URL(base)
|
|
18
|
+
if (parsed.origin !== baseParsed.origin) return false
|
|
19
|
+
return /\/apps\/[^/]+\/runtime\//.test(parsed.pathname)
|
|
20
|
+
} catch {
|
|
21
|
+
return false
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {string} url
|
|
27
|
+
* @param {string[]} [allowedOrigins]
|
|
28
|
+
* @param {{ backendUrl?: string, runtimePublicUrl?: string, runtimeType?: string }} [options]
|
|
29
|
+
*/
|
|
30
|
+
export function isAllowedLaunchUrl(url, allowedOrigins = [], options = {}) {
|
|
31
|
+
if (!url || typeof url !== 'string') return false
|
|
32
|
+
|
|
33
|
+
if (options.runtimeType === RUNTIME_HOSTED || isHubHostedRuntimeUrl(url, options)) {
|
|
34
|
+
return isHubHostedRuntimeUrl(url, options)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let parsed
|
|
38
|
+
try {
|
|
39
|
+
parsed = new URL(url)
|
|
40
|
+
} catch {
|
|
41
|
+
return false
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (BLOCKED_PROTOCOLS.has(parsed.protocol)) return false
|
|
45
|
+
|
|
46
|
+
if (parsed.protocol === 'http:' && (parsed.hostname === 'localhost' || parsed.hostname === '127.0.0.1')) {
|
|
47
|
+
return true
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (parsed.protocol !== 'https:') return false
|
|
51
|
+
|
|
52
|
+
if (allowedOrigins.length > 0) {
|
|
53
|
+
const origin = parsed.origin
|
|
54
|
+
return allowedOrigins.some((entry) => {
|
|
55
|
+
try {
|
|
56
|
+
return new URL(entry).origin === origin
|
|
57
|
+
} catch {
|
|
58
|
+
return false
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return true
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** @param {string} entryUrl */
|
|
67
|
+
export function entryUrlOrigin(entryUrl) {
|
|
68
|
+
if (!entryUrl || typeof entryUrl !== 'string') return ''
|
|
69
|
+
try {
|
|
70
|
+
return new URL(entryUrl).origin
|
|
71
|
+
} catch {
|
|
72
|
+
return ''
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Check catalog entry_url origin against host allowedRuntimeOrigins. */
|
|
77
|
+
export function isEntryUrlAllowed(entryUrl, allowedOrigins = []) {
|
|
78
|
+
if (!entryUrl || typeof entryUrl !== 'string') return false
|
|
79
|
+
return isAllowedLaunchUrl(entryUrl, allowedOrigins)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function resolveLaunchUrl(responseData) {
|
|
83
|
+
const data = responseData?.data ?? responseData
|
|
84
|
+
const base = data?.runtime_url ?? data?.entry_url ?? data?.launch?.url ?? ''
|
|
85
|
+
const token = data?.launch_token
|
|
86
|
+
if (!base) return ''
|
|
87
|
+
if (!token || typeof token !== 'string') return base
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const url = new URL(base)
|
|
91
|
+
url.searchParams.set('launch_token', token)
|
|
92
|
+
return url.toString()
|
|
93
|
+
} catch {
|
|
94
|
+
return base
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Hosted apps use opaque iframe origin (no allow-same-origin) so publisher JS cannot read Hub localStorage.
|
|
100
|
+
* @param {string} runtimeType
|
|
101
|
+
* @param {{ hostedSandboxSameOrigin?: boolean }} [options]
|
|
102
|
+
*/
|
|
103
|
+
export function iframeSandboxAttrs(runtimeType, options = {}) {
|
|
104
|
+
if (runtimeType === RUNTIME_HOSTED) {
|
|
105
|
+
if (options.hostedSandboxSameOrigin === true) {
|
|
106
|
+
return 'allow-scripts allow-forms allow-popups allow-same-origin'
|
|
107
|
+
}
|
|
108
|
+
return 'allow-scripts allow-forms allow-popups'
|
|
109
|
+
}
|
|
110
|
+
return 'allow-scripts allow-forms allow-popups'
|
|
111
|
+
}
|