@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.
Files changed (90) hide show
  1. package/README.md +84 -0
  2. package/package.json +31 -0
  3. package/src/api/coreApi.js +25 -0
  4. package/src/api/index.js +80 -0
  5. package/src/composables/createZoneContext.js +156 -0
  6. package/src/composables/useAppHubHostApi.js +24 -0
  7. package/src/composables/useAppHubZoneContext.js +11 -0
  8. package/src/composables/useDevOriginToggle.js +40 -0
  9. package/src/i18n/index.js +16 -0
  10. package/src/i18n/resolveLang.js +6 -0
  11. package/src/i18n/resolveTheme.js +30 -0
  12. package/src/i18n/translations/en.js +303 -0
  13. package/src/i18n/translations/vi.js +302 -0
  14. package/src/index.js +427 -0
  15. package/src/moduleStore.js +10 -0
  16. package/src/modules/app-store/components/AppHubAppStoreApp.vue +210 -0
  17. package/src/modules/app-store/components/AppHubAppStoreCard.vue +88 -0
  18. package/src/modules/app-store/components/AppHubAppStoreSettingsPanel.vue +266 -0
  19. package/src/modules/app-store/components/AppHubAppVersionHistory.vue +77 -0
  20. package/src/modules/app-store/components/AppHubDevReviewPanel.vue +206 -0
  21. package/src/modules/app-store/components/AppHubDraftStoreApp.vue +184 -0
  22. package/src/modules/app-store/components/AppHubDraftStoreCard.vue +116 -0
  23. package/src/modules/app-store/composables/useAppStore.js +206 -0
  24. package/src/modules/app-store/composables/useCatalogInfiniteScroll.js +47 -0
  25. package/src/modules/app-store/constants/catalogModes.js +2 -0
  26. package/src/modules/app-store/data/defaultCatalog.js +19 -0
  27. package/src/modules/app-store/index.js +9 -0
  28. package/src/modules/app-store/utils/normalizeCatalogApp.js +37 -0
  29. package/src/modules/desktop/components/AppHubDesktop.vue +1510 -0
  30. package/src/modules/desktop/components/AppHubDesktopDevOriginBar.vue +57 -0
  31. package/src/modules/desktop/components/AppHubDesktopDropLayer.vue +15 -0
  32. package/src/modules/desktop/components/AppHubDesktopDropTarget.vue +32 -0
  33. package/src/modules/desktop/components/AppHubDesktopIconContextMenu.vue +74 -0
  34. package/src/modules/desktop/components/AppHubDesktopIconFolder.vue +60 -0
  35. package/src/modules/desktop/components/AppHubDesktopIconGroup.vue +58 -0
  36. package/src/modules/desktop/components/AppHubDesktopIconInfoDialog.vue +33 -0
  37. package/src/modules/desktop/components/AppHubDesktopIconRenameDialog.vue +62 -0
  38. package/src/modules/desktop/components/AppHubDesktopSettings.vue +28 -0
  39. package/src/modules/desktop/components/AppHubDropInstallBadge.vue +65 -0
  40. package/src/modules/desktop/components/AppHubDuplicateAppDialog.vue +38 -0
  41. package/src/modules/desktop/components/AppHubGuideApp.vue +278 -0
  42. package/src/modules/desktop/components/AppHubOriginBlockScreen.vue +105 -0
  43. package/src/modules/desktop/components/AppHubOriginLoadingScreen.vue +23 -0
  44. package/src/modules/desktop/components/AppHubPlaceholderApp.vue +14 -0
  45. package/src/modules/desktop/components/AppHubSettingsApp.vue +319 -0
  46. package/src/modules/desktop/components/AppHubStartButton.vue +24 -0
  47. package/src/modules/desktop/components/AppHubStartMenu.vue +182 -0
  48. package/src/modules/desktop/components/AppHubTaskbarPins.vue +23 -0
  49. package/src/modules/desktop/components/settings/AppHubSettingsKeyboardPanel.vue +82 -0
  50. package/src/modules/desktop/components/settings/AppHubSettingsScreenPanel.vue +41 -0
  51. package/src/modules/desktop/components/settings/AppHubSettingsStartMenuPanel.vue +95 -0
  52. package/src/modules/desktop/composables/simulateInstallProgress.js +15 -0
  53. package/src/modules/desktop/composables/useDesktopDropInstall.js +272 -0
  54. package/src/modules/desktop/composables/useDesktopHubSettings.js +51 -0
  55. package/src/modules/desktop/composables/useDesktopIconDrag.js +207 -0
  56. package/src/modules/desktop/composables/useDesktopShell.js +335 -0
  57. package/src/modules/desktop/data/builtinApps.js +77 -0
  58. package/src/modules/desktop/index.js +12 -0
  59. package/src/modules/desktop/styles/desktop.css +3104 -0
  60. package/src/modules/desktop/styles/theme.css +616 -0
  61. package/src/modules/desktop/utils/desktopGrid.js +43 -0
  62. package/src/modules/desktop/utils/desktopIconGroups.js +103 -0
  63. package/src/modules/desktop/utils/desktopSession.js +40 -0
  64. package/src/modules/desktop/utils/desktopSettings.js +37 -0
  65. package/src/modules/desktop/utils/dropPackageParser.js +140 -0
  66. package/src/modules/desktop/utils/duplicateAppUtils.js +28 -0
  67. package/src/modules/desktop/utils/hubKeyboardSettings.js +63 -0
  68. package/src/modules/desktop/utils/recentApps.js +148 -0
  69. package/src/modules/desktop/utils/startMenuFavorites.js +100 -0
  70. package/src/modules/desktop/utils/startMenuPins.js +90 -0
  71. package/src/modules/notifications/components/AppHubDesktopNotifications.vue +54 -0
  72. package/src/modules/notifications/composables/createDesktopNotifications.js +86 -0
  73. package/src/modules/notifications/index.js +9 -0
  74. package/src/modules/notifications/styles/notifications.css +118 -0
  75. package/src/modules/notifications/utils/parseApiError.js +29 -0
  76. package/src/modules/runner/components/AppHubRunner.vue +292 -0
  77. package/src/modules/runner/index.js +1 -0
  78. package/src/modules/window-manager/components/AppHubWindowFrame.vue +224 -0
  79. package/src/modules/window-manager/composables/useWindowManager.js +652 -0
  80. package/src/modules/window-manager/index.js +7 -0
  81. package/src/modules/window-manager/utils/sessionLayout.js +28 -0
  82. package/src/modules/window-manager/utils/windowLayout.js +236 -0
  83. package/src/modules/window-manager/utils/windowSnap.js +146 -0
  84. package/src/utils/bootstrapCache.js +47 -0
  85. package/src/utils/devOriginSettings.js +22 -0
  86. package/src/utils/launchUrl.js +111 -0
  87. package/src/utils/originSafety.js +267 -0
  88. package/src/utils/safeStorage.js +191 -0
  89. package/src/utils/semver.js +30 -0
  90. package/src/utils/zoneContext.js +38 -0
package/src/index.js ADDED
@@ -0,0 +1,427 @@
1
+ import './modules/desktop/styles/desktop.css'
2
+ import './modules/desktop/styles/theme.css'
3
+ import { reactive } from 'vue'
4
+ import { createAppHubApi } from './api/index.js'
5
+ import { createCoreApi } from './api/coreApi.js'
6
+ import { createZoneContextState } from './composables/createZoneContext.js'
7
+ import { APPHUB_ZONE_CONTEXT_KEY } from './composables/useAppHubZoneContext.js'
8
+ import { createAppStoreState, provideAppStore } from './modules/app-store/index.js'
9
+ import { AppHubDesktop } from './modules/desktop/index.js'
10
+ import { AppHubRunner } from './modules/runner/index.js'
11
+ import { getAppHubStore, registerAppHubStore } from './moduleStore.js'
12
+ import {
13
+ createWindowManagerState,
14
+ provideWindowManager,
15
+ } from './modules/window-manager/index.js'
16
+ import {
17
+ evaluateOriginSafety,
18
+ parseDevUserFromBootstrap,
19
+ parseOriginsFromBootstrap,
20
+ } from './utils/originSafety.js'
21
+ import { loadDevFriendlyOriginsPreference } from './utils/devOriginSettings.js'
22
+ import {
23
+ bootstrapResponseFromCache,
24
+ loadBootstrapCache,
25
+ saveBootstrapCache,
26
+ } from './utils/bootstrapCache.js'
27
+
28
+ const bootstrapInflight = new WeakMap()
29
+
30
+ function buildPublicOptions(options = {}) {
31
+ const origins = Array.isArray(options.allowedRuntimeOrigins)
32
+ ? options.allowedRuntimeOrigins.filter((o) => typeof o === 'string')
33
+ : []
34
+
35
+ return {
36
+ language: options.language || 'vi',
37
+ theme: options.theme ?? 'auto',
38
+ themeToggle: options.themeToggle,
39
+ openAppStoreOnMount: options.openAppStoreOnMount !== false,
40
+ allowedRuntimeOrigins: origins,
41
+ coreUrl: options.coreUrl || '',
42
+ backendUrl: (options.backendUrl || '').replace(/\/$/, ''),
43
+ hasToken: !!(options.token),
44
+ allowSameOriginEmbed: options.allowSameOriginEmbed === true,
45
+ allowUnsafeOrigin: options.allowUnsafeOrigin === true,
46
+ hubOrigin: typeof options.hubOrigin === 'string' ? options.hubOrigin.trim() : '',
47
+ runtimePublicUrl: typeof options.runtimePublicUrl === 'string' ? options.runtimePublicUrl.trim() : '',
48
+ enforceDedicatedHubOrigin: options.enforceDedicatedHubOrigin !== false,
49
+ enforceIsolatedHostedRuntime: options.enforceIsolatedHostedRuntime !== false,
50
+ allowSameOriginHostedRuntime: options.allowSameOriginHostedRuntime === true,
51
+ hostedSandboxSameOrigin: options.hostedSandboxSameOrigin === true,
52
+ enforceDevFriendlyOrigins: typeof options.enforceDevFriendlyOrigins === 'boolean'
53
+ ? options.enforceDevFriendlyOrigins
54
+ : true,
55
+ isDevUser: options.isDevUser === true,
56
+ serverHubPublicUrl: typeof options.serverHubPublicUrl === 'string' ? options.serverHubPublicUrl.trim() : '',
57
+ serverFrontendOrigin: typeof options.serverFrontendOrigin === 'string' ? options.serverFrontendOrigin.trim() : '',
58
+ serverRuntimePublicUrl: typeof options.serverRuntimePublicUrl === 'string' ? options.serverRuntimePublicUrl.trim() : '',
59
+ serverOriginsAuto: options.serverOriginsAuto === true,
60
+ serverOriginsResolved: options.serverOriginsResolved === true,
61
+ originBootstrapLoading: options.originBootstrapLoading === true,
62
+ originBlocked: false,
63
+ originCheckPending: false,
64
+ originBlockReason: null,
65
+ originBlockParentOrigin: null,
66
+ originBlockExpectedHubOrigin: null,
67
+ originBlockExpectedRuntimeOrigin: null,
68
+ }
69
+ }
70
+
71
+ /** @param {Record<string, unknown>} target */
72
+ function applyOriginSafety(target, sourceOptions = {}) {
73
+ const check = evaluateOriginSafety({ ...target, ...sourceOptions })
74
+ target.originBootstrapLoading = check.loading === true
75
+ target.originCheckPending = check.pending === true
76
+ target.originBlocked = !check.safe && check.loading !== true
77
+ target.originBlockReason = check.reason
78
+ target.originBlockParentOrigin = check.parentOrigin
79
+ target.originBlockExpectedHubOrigin = check.expectedHubOrigin
80
+ target.originBlockExpectedRuntimeOrigin = check.expectedRuntimeOrigin
81
+ return check
82
+ }
83
+
84
+ function parseUserFromBootstrap(resp) {
85
+ const data = resp?.data?.data ?? resp?.data ?? {}
86
+ const user = data.user
87
+ if (!user || user.id == null) return null
88
+ return {
89
+ id: user.id,
90
+ name: user.name ?? String(user.id),
91
+ }
92
+ }
93
+
94
+ function applyBootstrapOrigins(store, bootstrapResponse, { fromCache = false } = {}) {
95
+ const {
96
+ hubPublicUrl,
97
+ frontendOrigin,
98
+ runtimePublicUrl,
99
+ originsAuto,
100
+ } = parseOriginsFromBootstrap(bootstrapResponse)
101
+
102
+ store.options.isDevUser = parseDevUserFromBootstrap(bootstrapResponse)
103
+ store.options.serverOriginsAuto = originsAuto
104
+
105
+ if (!store.options.isDevUser) {
106
+ store.options.enforceDevFriendlyOrigins = true
107
+ } else {
108
+ store.options.enforceDevFriendlyOrigins = loadDevFriendlyOriginsPreference()
109
+ }
110
+
111
+ if (hubPublicUrl) {
112
+ store.options.serverHubPublicUrl = hubPublicUrl
113
+ }
114
+ if (frontendOrigin) {
115
+ store.options.serverFrontendOrigin = frontendOrigin
116
+ }
117
+ if (runtimePublicUrl) {
118
+ store.options.serverRuntimePublicUrl = runtimePublicUrl
119
+ if (!store.options.runtimePublicUrl) store.options.runtimePublicUrl = runtimePublicUrl
120
+ }
121
+
122
+ store.options.serverOriginsResolved = true
123
+ store.options.originBootstrapLoading = false
124
+
125
+ if (!fromCache) {
126
+ saveBootstrapCache(store.credentials.backendUrl, bootstrapResponse)
127
+ }
128
+
129
+ const user = parseUserFromBootstrap(bootstrapResponse)
130
+ if (user && store.zoneContext?.state) {
131
+ store.zoneContext.state.user.id = user.id
132
+ store.zoneContext.state.user.name = user.name
133
+ }
134
+
135
+ reconcileOriginSafety(store)
136
+ }
137
+
138
+ function applyCachedBootstrapIfAny(store) {
139
+ const cached = loadBootstrapCache(store.credentials.backendUrl)
140
+ if (!cached) return false
141
+ applyBootstrapOrigins(store, bootstrapResponseFromCache(cached), { fromCache: true })
142
+ return true
143
+ }
144
+
145
+ async function fetchBootstrapSession(store) {
146
+ let promise = bootstrapInflight.get(store)
147
+ if (promise) return promise
148
+
149
+ promise = (async () => {
150
+ syncApi(store)
151
+ if (!store.facade?.bootstrap) return
152
+
153
+ const res = await store.facade.bootstrap()
154
+ if (res) applyBootstrapOrigins(store, res)
155
+ })().catch(() => {
156
+ store.options.originBootstrapLoading = false
157
+ if (!store.options.serverOriginsResolved) {
158
+ reconcileOriginSafety(store)
159
+ }
160
+ }).finally(() => {
161
+ bootstrapInflight.delete(store)
162
+ })
163
+
164
+ bootstrapInflight.set(store, promise)
165
+ return promise
166
+ }
167
+
168
+ function startBootstrapSession(store) {
169
+ const { backendUrl, token } = store.credentials
170
+ if (!backendUrl || !token) {
171
+ store.options.originBootstrapLoading = false
172
+ reconcileOriginSafety(store)
173
+ return Promise.resolve()
174
+ }
175
+
176
+ syncApi(store)
177
+ applyOriginSafety(store.options)
178
+
179
+ const hadCache = applyCachedBootstrapIfAny(store)
180
+ if (!hadCache && !store.options.originBlocked) {
181
+ store.options.originBootstrapLoading = true
182
+ store.options.originBlocked = false
183
+ applyOriginSafety(store.options)
184
+ disableModuleServices(store)
185
+ }
186
+
187
+ return fetchBootstrapSession(store)
188
+ }
189
+
190
+ function reconcileOriginSafety(store) {
191
+ const wasBlocked = store.options.originBlocked === true
192
+ const wasLoading = store.options.originBootstrapLoading === true
193
+ applyOriginSafety(store.options)
194
+
195
+ if (store.options.originBootstrapLoading) {
196
+ disableModuleServices(store)
197
+ return
198
+ }
199
+
200
+ if (store.options.originBlocked && !wasBlocked) {
201
+ disableModuleServices(store)
202
+ } else if (!store.options.originBlocked && (wasBlocked || wasLoading)) {
203
+ enableModuleApi(store)
204
+ }
205
+ }
206
+
207
+ function disableModuleServices(store) {
208
+ store.facade.setImpl(null)
209
+ store.coreApi = null
210
+ }
211
+
212
+ /** Window manager + app store — required even when origin is blocked (AppHubDesktop setup). */
213
+ function ensureModuleInfrastructure(vueApp, store) {
214
+ ensureZoneContext(vueApp, store)
215
+ ensureModuleState(vueApp, store)
216
+ }
217
+
218
+ function enableModuleApi(store) {
219
+ syncApi(store)
220
+ syncZoneContext(store)
221
+ }
222
+
223
+ function enableModuleServices(vueApp, store) {
224
+ ensureModuleInfrastructure(vueApp, store)
225
+ enableModuleApi(store)
226
+ }
227
+
228
+ function buildCredentials(options = {}) {
229
+ return {
230
+ coreUrl: options.coreUrl || '',
231
+ backendUrl: options.backendUrl || '',
232
+ token: options.token || '',
233
+ hostAccessSecret: options.hostAccessSecret || '',
234
+ }
235
+ }
236
+
237
+ function createApiFacade() {
238
+ let impl = null
239
+ return {
240
+ setImpl(next) {
241
+ impl = next
242
+ },
243
+ bootstrap: (...args) => impl?.bootstrap?.(...args),
244
+ apps: (...args) => impl?.apps?.(...args),
245
+ launch: (...args) => impl?.launch?.(...args),
246
+ ping: (...args) => impl?.ping?.(...args),
247
+ verifyLaunchToken: (...args) => impl?.verifyLaunchToken?.(...args),
248
+ usage: (...args) => impl?.usage?.(...args),
249
+ devApps: (...args) => impl?.devApps?.(...args),
250
+ devInspectBundle: (...args) => impl?.devInspectBundle?.(...args),
251
+ devDisableApp: (...args) => impl?.devDisableApp?.(...args),
252
+ devSetAppStatus: (...args) => impl?.devSetAppStatus?.(...args),
253
+ registerApp: (...args) => impl?.registerApp?.(...args),
254
+ appVersions: (...args) => impl?.appVersions?.(...args),
255
+ integrationDocs: (...args) => impl?.integrationDocs?.(...args),
256
+ integrationDocsInternal: (...args) => impl?.integrationDocsInternal?.(...args),
257
+ grantBridgeScope: (...args) => impl?.grantBridgeScope?.(...args),
258
+ bridgeUser: (...args) => impl?.bridgeUser?.(...args),
259
+ bridgeDesktopMessage: (...args) => impl?.bridgeDesktopMessage?.(...args),
260
+ }
261
+ }
262
+
263
+ function applyModuleOptions(store, options = {}) {
264
+ const nextPublic = buildPublicOptions({ ...options, token: options.token ?? store.credentials.token })
265
+ Object.assign(store.options, {
266
+ language: nextPublic.language,
267
+ theme: nextPublic.theme,
268
+ themeToggle: nextPublic.themeToggle,
269
+ openAppStoreOnMount: nextPublic.openAppStoreOnMount,
270
+ allowedRuntimeOrigins: nextPublic.allowedRuntimeOrigins,
271
+ coreUrl: nextPublic.coreUrl,
272
+ backendUrl: nextPublic.backendUrl,
273
+ hasToken: nextPublic.hasToken,
274
+ allowSameOriginEmbed: nextPublic.allowSameOriginEmbed,
275
+ allowUnsafeOrigin: nextPublic.allowUnsafeOrigin,
276
+ hubOrigin: nextPublic.hubOrigin,
277
+ runtimePublicUrl: nextPublic.runtimePublicUrl,
278
+ enforceDedicatedHubOrigin: nextPublic.enforceDedicatedHubOrigin,
279
+ enforceIsolatedHostedRuntime: nextPublic.enforceIsolatedHostedRuntime,
280
+ allowSameOriginHostedRuntime: nextPublic.allowSameOriginHostedRuntime,
281
+ hostedSandboxSameOrigin: nextPublic.hostedSandboxSameOrigin,
282
+ enforceDevFriendlyOrigins: nextPublic.enforceDevFriendlyOrigins,
283
+ })
284
+ applyOriginSafety(store.options, options)
285
+ if (store.credentials.backendUrl && store.credentials.token) {
286
+ void startBootstrapSession(store)
287
+ } else {
288
+ reconcileOriginSafety(store)
289
+ }
290
+ Object.assign(store.credentials, buildCredentials(options))
291
+ }
292
+
293
+ function syncApi(store) {
294
+ const { credentials, facade, zoneContext } = store
295
+ const api = credentials.backendUrl && credentials.token
296
+ ? createAppHubApi(credentials.backendUrl, credentials.token, {
297
+ hostAccessSecret: credentials.hostAccessSecret,
298
+ getZoneHeaderId: () => zoneContext?.getZoneHeaderId?.() ?? null,
299
+ })
300
+ : null
301
+ facade.setImpl(api)
302
+ }
303
+
304
+ function syncCoreApi(store) {
305
+ const { credentials } = store
306
+ store.coreApi = credentials.coreUrl && credentials.token
307
+ ? createCoreApi(credentials.coreUrl, credentials.token)
308
+ : null
309
+ }
310
+
311
+ function ensureZoneContext(app, store) {
312
+ if (store.zoneContext) return
313
+ store.zoneContext = createZoneContextState(
314
+ () => store.coreApi,
315
+ () => store.facade,
316
+ {
317
+ ensureBootstrapSession: () => fetchBootstrapSession(store),
318
+ },
319
+ )
320
+ app.provide(APPHUB_ZONE_CONTEXT_KEY, store.zoneContext)
321
+ }
322
+
323
+ function syncZoneContext(store) {
324
+ syncCoreApi(store)
325
+ if (store.zoneContext) {
326
+ store.zoneContext.refresh({ skipBootstrap: true })
327
+ }
328
+ }
329
+
330
+ function ensureModuleState(app, store) {
331
+ if (store.windowManager && store.appStore) {
332
+ return
333
+ }
334
+
335
+ store.windowManager = createWindowManagerState()
336
+ store.appStore = createAppStoreState()
337
+ provideWindowManager(app, store.windowManager)
338
+ provideAppStore(app, store.appStore)
339
+ }
340
+
341
+ /**
342
+ * Install App Hub — Windows desktop shell + modular apps (App Store default).
343
+ * Returns host API facade — keep in host app code, not publisher apps.
344
+ */
345
+ export function installAppHubModule(vueApp, options = {}) {
346
+ let store = getAppHubStore(vueApp)
347
+
348
+ if (store) {
349
+ applyModuleOptions(store, options)
350
+ ensureModuleInfrastructure(vueApp, store)
351
+ if (store.options.originBootstrapLoading || store.options.originBlocked) {
352
+ disableModuleServices(store)
353
+ } else {
354
+ enableModuleApi(store)
355
+ }
356
+ return store.facade
357
+ }
358
+
359
+ const facade = createApiFacade()
360
+ const moduleOptions = reactive(buildPublicOptions(options))
361
+ applyOriginSafety(moduleOptions, options)
362
+ const credentials = buildCredentials(options)
363
+
364
+ store = {
365
+ app: vueApp,
366
+ facade,
367
+ options: moduleOptions,
368
+ credentials,
369
+ coreApi: null,
370
+ zoneContext: null,
371
+ windowManager: null,
372
+ appStore: null,
373
+ }
374
+ registerAppHubStore(vueApp, store)
375
+
376
+ vueApp.provide('apphubOptions', moduleOptions)
377
+ vueApp.component('AppHubDesktop', AppHubDesktop)
378
+ vueApp.component('AppHubRunner', AppHubRunner)
379
+
380
+ ensureModuleInfrastructure(vueApp, store)
381
+ void startBootstrapSession(store)
382
+
383
+ return facade
384
+ }
385
+
386
+ /** Whether installAppHubModule has been called for this Vue app instance. */
387
+ export function isAppHubModuleInstalled(vueApp) {
388
+ return vueApp != null && getAppHubStore(vueApp) != null
389
+ }
390
+
391
+ export { useAppHubHostApi } from './composables/useAppHubHostApi.js'
392
+ export { useAppHubZoneContext } from './composables/useAppHubZoneContext.js'
393
+ export { createAppHubApi } from './api/index.js'
394
+ export { createCoreApi } from './api/coreApi.js'
395
+ export { AppHubDesktop } from './modules/desktop/index.js'
396
+ export {
397
+ createDesktopNotificationsState,
398
+ useDesktopNotifications,
399
+ AppHubDesktopNotifications,
400
+ parseApiError,
401
+ } from './modules/notifications/index.js'
402
+ export { AppHubRunner } from './modules/runner/index.js'
403
+ export { resolveLang } from './i18n/resolveLang.js'
404
+ export { resolveTheme, normalizeTheme, isThemeLocked } from './i18n/resolveTheme.js'
405
+ export { t } from './i18n/index.js'
406
+ export {
407
+ evaluateOriginSafety,
408
+ isEmbeddedFrame,
409
+ isLocalDevOrigin,
410
+ parseOriginsFromBootstrap,
411
+ resolveEffectiveOrigins,
412
+ ORIGIN_UNSAFE_SAME_ORIGIN_EMBED,
413
+ ORIGIN_UNSAFE_NOT_CONFIGURED,
414
+ ORIGIN_UNSAFE_WRONG_ORIGIN,
415
+ ORIGIN_UNSAFE_RUNTIME_NOT_CONFIGURED,
416
+ ORIGIN_UNSAFE_RUNTIME_SAME_ORIGIN,
417
+ resolveRuntimeApiBase,
418
+ } from './utils/originSafety.js'
419
+
420
+ /** True when installAppHubModule blocked due to unsafe same-origin embed. */
421
+ export function isAppHubOriginBlocked(vueApp) {
422
+ const store = getAppHubStore(vueApp)
423
+ return store?.options?.originBlocked === true
424
+ }
425
+ export * from './modules/app-store/index.js'
426
+ export * from './modules/window-manager/index.js'
427
+ export * from './modules/runner/index.js'
@@ -0,0 +1,10 @@
1
+ /** Private per-Vue-app store — not exposed via provide/inject. */
2
+ const installedApps = new WeakMap()
3
+
4
+ export function registerAppHubStore(app, store) {
5
+ installedApps.set(app, store)
6
+ }
7
+
8
+ export function getAppHubStore(app) {
9
+ return app != null ? installedApps.get(app) ?? null : null
10
+ }
@@ -0,0 +1,210 @@
1
+ <template>
2
+ <div class="apphub-store">
3
+ <header class="apphub-store__header">
4
+ <h2 class="apphub-store__title">
5
+ {{ settingsOpen ? labels.settings_title : labels.app_store_title }}
6
+ </h2>
7
+ <div class="apphub-store__toolbar">
8
+ <button
9
+ type="button"
10
+ class="apphub-store__settings-btn"
11
+ :class="{ 'apphub-store__settings-btn--active': settingsOpen }"
12
+ :title="settingsOpen ? labels.settings_close : labels.settings"
13
+ @click="settingsOpen = !settingsOpen"
14
+ >
15
+ <svg v-if="settingsOpen" class="apphub-store__settings-icon" viewBox="0 0 24 24" aria-hidden="true">
16
+ <path
17
+ fill="currentColor"
18
+ d="M20 11H7.83l5.59-5.59L12 4l-8 8 8 8 1.41-1.41L7.83 13H20v-2z"
19
+ />
20
+ </svg>
21
+ <svg v-else class="apphub-store__settings-icon" viewBox="0 0 24 24" aria-hidden="true">
22
+ <path
23
+ fill="currentColor"
24
+ d="M19.14 12.94c.04-.31.06-.63.06-.94 0-.31-.02-.63-.06-.94l2.03-1.58a.49.49 0 0 0 .12-.61l-1.92-3.32a.49.49 0 0 0-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54a.484.484 0 0 0-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.04.31-.06.63-.06.94s.02.63.06.94l-2.03 1.58a.49.49 0 0 0-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"
25
+ />
26
+ </svg>
27
+ </button>
28
+ </div>
29
+ </header>
30
+
31
+ <div class="apphub-store__content">
32
+ <AppHubAppStoreSettingsPanel
33
+ v-if="settingsOpen"
34
+ :root-app="rootApp"
35
+ @refreshed="reloadCatalog"
36
+ />
37
+ <div v-else ref="scrollRoot" class="apphub-store__panel apphub-store__panel--scroll">
38
+ <input
39
+ v-model="catalog.search"
40
+ type="search"
41
+ class="apphub-store__search"
42
+ :placeholder="labels.app_store_search"
43
+ />
44
+
45
+ <p v-if="catalog.loading && !catalog.items.length" class="apphub-store__msg">
46
+ {{ labels.app_store_loading }}
47
+ </p>
48
+ <p v-else-if="catalog.error === 'permission_denied'" class="apphub-store__msg apphub-store__msg--error">
49
+ {{ labels.app_store_permission_denied }}
50
+ </p>
51
+ <p v-else-if="catalog.error === 'load_failed'" class="apphub-store__msg apphub-store__msg--error">
52
+ {{ labels.app_store_load_error }}
53
+ </p>
54
+ <p v-else-if="catalog.error === 'no_api'" class="apphub-store__msg apphub-store__msg--warn">
55
+ {{ labels.app_store_no_api }}
56
+ </p>
57
+
58
+ <div
59
+ v-else-if="!catalog.loading && !appStore.filteredStoreApps.length"
60
+ class="apphub-store__empty"
61
+ >
62
+ <p>{{ labels.app_store_empty }}</p>
63
+ <p
64
+ v-if="catalog.loaded && zone?.state?.selectedZoneId && !zone?.state?.viewAllZones"
65
+ class="apphub-store__empty-hint"
66
+ >
67
+ {{ labels.app_store_empty_zone_hint }}
68
+ </p>
69
+ </div>
70
+
71
+ <template v-else-if="appStore.filteredStoreApps.length">
72
+ <ul class="apphub-store__grid">
73
+ <li
74
+ v-for="app in appStore.filteredStoreApps"
75
+ :key="app.slug"
76
+ class="apphub-store__card"
77
+ :class="{ 'apphub-store__card--offline': app.status === 'disabled' }"
78
+ >
79
+ <AppHubAppStoreCard
80
+ :app="app"
81
+ :labels="labels"
82
+ :installed="appStore.isInstalled(app.slug)"
83
+ :installed-version="installedVersionFor(app.slug)"
84
+ :can-install="appStore.canInstall(app)"
85
+ @install="onInstall"
86
+ @update="onUpdate"
87
+ @uninstall="onUninstall"
88
+ />
89
+ </li>
90
+ </ul>
91
+ <p v-if="catalog.loadingMore" class="apphub-catalog-footer">{{ labels.app_store_loading_more }}</p>
92
+ <div ref="scrollSentinel" class="apphub-catalog-sentinel" aria-hidden="true" />
93
+ </template>
94
+ </div>
95
+ </div>
96
+ </div>
97
+ </template>
98
+
99
+ <script setup>
100
+ import { computed, getCurrentInstance, inject, onMounted, ref, watch } from 'vue'
101
+ import {
102
+ getHostApiForApp,
103
+ isBackendReadyForApp,
104
+ resolveRootApp,
105
+ } from '../../../composables/useAppHubHostApi.js'
106
+ import { useAppHubZoneContext } from '../../../composables/useAppHubZoneContext.js'
107
+ import { t } from '../../../i18n/index.js'
108
+ import { resolveLang } from '../../../i18n/resolveLang.js'
109
+ import { CATALOG_MODE_STORE } from '../constants/catalogModes.js'
110
+ import { useCatalogInfiniteScroll } from '../composables/useCatalogInfiniteScroll.js'
111
+ import { useAppStore } from '../composables/useAppStore.js'
112
+ import AppHubAppStoreCard from './AppHubAppStoreCard.vue'
113
+ import AppHubAppStoreSettingsPanel from './AppHubAppStoreSettingsPanel.vue'
114
+
115
+ const props = defineProps({
116
+ getInstalledVersion: { type: Function, default: null },
117
+ onInstalled: { type: Function, default: null },
118
+ onUpdateApp: { type: Function, default: null },
119
+ onUninstalled: { type: Function, default: null },
120
+ })
121
+
122
+ const settingsOpen = ref(false)
123
+ const appStore = useAppStore()
124
+ const catalog = appStore.catalogs.store
125
+ const rootApp = resolveRootApp(getCurrentInstance())
126
+ const zone = useAppHubZoneContext()
127
+ const moduleOptions = inject('apphubOptions', {})
128
+ const lang = computed(() => resolveLang(moduleOptions?.language, 'vi'))
129
+
130
+ const labels = computed(() => ({
131
+ app_store_title: t('app_store_title', lang.value),
132
+ app_store_search: t('app_store_search', lang.value),
133
+ app_store_install: t('app_store_install', lang.value),
134
+ app_store_empty: t('app_store_empty', lang.value),
135
+ app_store_empty_zone_hint: t('app_store_empty_zone_hint', lang.value),
136
+ app_store_loading: t('app_store_loading', lang.value),
137
+ app_store_loading_more: t('app_store_loading_more', lang.value),
138
+ app_store_load_error: t('app_store_load_error', lang.value),
139
+ app_store_permission_denied: t('app_store_permission_denied', lang.value),
140
+ app_store_no_api: t('app_store_no_api', lang.value),
141
+ app_store_unavailable: t('app_store_unavailable', lang.value),
142
+ app_store_status_draft: t('app_store_status_draft', lang.value),
143
+ app_store_status_offline: t('app_store_status_offline', lang.value),
144
+ app_store_installed: t('app_store_installed', lang.value),
145
+ app_store_uninstall: t('app_store_uninstall', lang.value),
146
+ app_store_update: t('app_store_update', lang.value),
147
+ app_store_installed_version: t('app_store_installed_version', lang.value),
148
+ settings: t('app_store_settings_btn', lang.value),
149
+ settings_title: t('app_store_settings_title', lang.value),
150
+ settings_close: t('app_store_settings_close', lang.value),
151
+ }))
152
+
153
+ function hostApiOptions() {
154
+ return {
155
+ backendReady: isBackendReadyForApp(rootApp),
156
+ mode: CATALOG_MODE_STORE,
157
+ }
158
+ }
159
+
160
+ async function reloadCatalog() {
161
+ if (!rootApp) return
162
+ await appStore.loadCatalog(getHostApiForApp(rootApp), hostApiOptions())
163
+ }
164
+
165
+ async function loadMore() {
166
+ if (!rootApp) return
167
+ await appStore.loadMoreCatalog(getHostApiForApp(rootApp), CATALOG_MODE_STORE, hostApiOptions())
168
+ }
169
+
170
+ const { rootRef: scrollRoot, sentinelRef: scrollSentinel } = useCatalogInfiniteScroll({
171
+ canLoadMore: () => catalog.hasMore && !catalog.loading && !catalog.loadingMore,
172
+ onLoadMore: loadMore,
173
+ })
174
+
175
+ function installedVersionFor(slug) {
176
+ return props.getInstalledVersion?.(slug) ?? null
177
+ }
178
+
179
+ async function onInstall(app) {
180
+ if (!appStore.installApp(app.slug)) return
181
+ await props.onInstalled?.(app)
182
+ }
183
+
184
+ async function onUpdate(app) {
185
+ await props.onUpdateApp?.(app)
186
+ }
187
+
188
+ async function onUninstall(app) {
189
+ if (!appStore.uninstallApp(app.slug)) return
190
+ await props.onUninstalled?.(app)
191
+ }
192
+
193
+ onMounted(() => {
194
+ if (!catalog.loaded) reloadCatalog()
195
+ })
196
+
197
+ watch(
198
+ () => moduleOptions?.hasToken,
199
+ (hasToken) => {
200
+ if (hasToken && !catalog.loaded) reloadCatalog()
201
+ },
202
+ )
203
+
204
+ watch(
205
+ () => [zone?.state?.selectedZoneId, zone?.state?.viewAllZones],
206
+ () => {
207
+ if (moduleOptions?.hasToken) reloadCatalog()
208
+ },
209
+ )
210
+ </script>