@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
@@ -0,0 +1,1510 @@
1
+ <template>
2
+ <AppHubOriginLoadingScreen v-if="originBootstrapLoading" />
3
+
4
+ <AppHubOriginBlockScreen
5
+ v-else-if="!originSafety.safe"
6
+ :reason="originSafety.reason"
7
+ :parent-origin="originSafety.parentOrigin"
8
+ :expected-hub-origin="originSafety.expectedHubOrigin"
9
+ :expected-runtime-origin="originSafety.expectedRuntimeOrigin"
10
+ :labels="originBlockLabels"
11
+ />
12
+
13
+ <div
14
+ v-else
15
+ ref="desktopRoot"
16
+ class="apphub-desktop"
17
+ :class="{
18
+ 'apphub-desktop--drop-target': isMainScreen,
19
+ 'apphub-desktop--light': activeTheme === 'light',
20
+ }"
21
+ @click="onDesktopClick"
22
+ @dragenter.capture.prevent="onDesktopDragEnter"
23
+ @dragover.capture.prevent="onDesktopDragOver"
24
+ @dragleave="onDesktopDragLeave"
25
+ @drop.capture.prevent="onDesktopDrop"
26
+ >
27
+ <div class="apphub-desktop__wallpaper" :class="{ 'apphub-desktop__wallpaper--drop': dropInstall.state.dragActive }" />
28
+
29
+ <AppHubDesktopDropLayer
30
+ v-show="isMainScreen && dropInstall.state.dragActive"
31
+ :hint="labels.drop_hint"
32
+ />
33
+
34
+ <div
35
+ ref="iconsLayerRef"
36
+ class="apphub-desktop__icons-layer"
37
+ :class="{
38
+ 'apphub-desktop__icons-layer--drop-target': isMainScreen,
39
+ 'apphub-desktop__icons-layer--grid': desktopSettings.snapToGrid,
40
+ }"
41
+ >
42
+ <template v-for="item in desktopLayout" :key="item.id">
43
+ <AppHubDesktopIconGroup
44
+ v-if="item.type === 'group'"
45
+ :apps="item.apps"
46
+ :x="item.x"
47
+ :y="item.y"
48
+ :label="groupLabel(item.apps, item.x, item.y)"
49
+ :title="`${groupLabel(item.apps, item.x, item.y)} — ${labels.desktop_icon_hold_hint}`"
50
+ :dragging="isGroupDragging(item.apps)"
51
+ :holding="isGroupHolding(item.apps)"
52
+ :drop-highlight="isDropTargetCell(item.x, item.y)"
53
+ @pointer-down="onGroupPointerDown(item, $event)"
54
+ @click="onGroupClick(item)"
55
+ @context-menu="onGroupContextMenu(item, $event)"
56
+ />
57
+
58
+ <button
59
+ v-else
60
+ type="button"
61
+ class="apphub-desktop__icon apphub-desktop__icon--placed"
62
+ :class="{
63
+ 'apphub-desktop__icon--dragging': iconDrag.isDragging(item.app.id),
64
+ 'apphub-desktop__icon--holding': iconDrag.isHolding(item.app.id),
65
+ 'apphub-desktop__icon--drop-target': isDropTargetCell(item.x, item.y),
66
+ }"
67
+ :style="{ left: `${item.x}px`, top: `${item.y}px` }"
68
+ :title="`${item.app.name} — ${labels.desktop_icon_move_hint}`"
69
+ @mousedown.stop="onPlacedIconPointerDown(item.app, $event)"
70
+ @dblclick.stop="onOpenIcon(item.app)"
71
+ @contextmenu.prevent.stop="onIconContextMenu(item.app, $event)"
72
+ >
73
+ <span class="apphub-desktop__icon-img-wrap">
74
+ <span class="apphub-desktop__icon-img">{{ item.app.icon }}</span>
75
+ <span
76
+ v-if="item.app.status === 'draft'"
77
+ class="apphub-desktop__icon-flag"
78
+ :title="labels.desktop_icon_draft_hint"
79
+ >
80
+ {{ labels.app_store_status_draft }}
81
+ </span>
82
+ </span>
83
+ <span class="apphub-desktop__icon-label" :title="item.app.name">{{ item.app.name }}</span>
84
+ </button>
85
+ </template>
86
+
87
+ <AppHubDesktopIconFolder
88
+ v-if="dragFolderPreview"
89
+ :open="true"
90
+ preview
91
+ :x="dragFolderPreview.x"
92
+ :y="dragFolderPreview.y"
93
+ :apps="dragFolderPreview.apps"
94
+ :title="groupLabel(dragFolderPreview.apps, dragFolderPreview.x, dragFolderPreview.y)"
95
+ :count-label="formatLabel('group_folder_count', { count: dragFolderPreview.apps.length })"
96
+ :hint="labels.group_drop_hint"
97
+ :preview-new-ids="dragPreviewNewIds"
98
+ />
99
+
100
+ <AppHubDesktopIconFolder
101
+ :open="openFolder.open"
102
+ :x="openFolder.x"
103
+ :y="openFolder.y"
104
+ :apps="openFolder.apps"
105
+ :title="openFolder.title"
106
+ :count-label="openFolder.countLabel"
107
+ :hint="labels.desktop_icon_move_hint"
108
+ :is-dragging="iconDrag.isDragging"
109
+ :is-holding="iconDrag.isHolding"
110
+ @item-pointer-down="onFolderItemPointerDown"
111
+ @open-app="onFolderOpenApp"
112
+ @item-context-menu="onFolderItemContextMenu"
113
+ />
114
+
115
+ <AppHubDropInstallBadge
116
+ v-for="job in dropInstall.state.jobs"
117
+ :key="job.id"
118
+ :job="job"
119
+ :loading-label="labels.drop_installing"
120
+ :error-label="labels.drop_error"
121
+ :method-label="methodLabel(job)"
122
+ :done-publish-label="labels.drop_done_publish"
123
+ />
124
+
125
+ <AppHubDesktopIconContextMenu
126
+ :open="iconContextMenu.open"
127
+ :x="iconContextMenu.x"
128
+ :y="iconContextMenu.y"
129
+ :can-rename="contextMenuCanRename"
130
+ :show-pin="contextMenuShowPin"
131
+ :show-favorite="contextMenuShowFavorite"
132
+ :show-uninstall="contextMenuShowUninstall"
133
+ :open-label="contextMenuOpenLabel"
134
+ :pin-label="contextMenuPinLabel"
135
+ :favorite-label="contextMenuFavoriteLabel"
136
+ :uninstall-label="labels.icon_context_uninstall"
137
+ :rename-label="labels.icon_context_rename"
138
+ :properties-label="labels.icon_context_properties"
139
+ @open="onContextMenuOpen"
140
+ @pin="onContextMenuPin"
141
+ @favorite="onContextMenuFavorite"
142
+ @uninstall="onContextMenuUninstall"
143
+ @rename="onContextMenuRename"
144
+ @info="onContextMenuInfo"
145
+ />
146
+ </div>
147
+
148
+ <AppHubDesktopIconInfoDialog
149
+ :open="iconInfoDialog.open"
150
+ :title="iconInfoDialogTitle"
151
+ :app="iconInfoDialogApp"
152
+ :rows="iconInfoRows"
153
+ :close-label="labels.icon_info_close"
154
+ @close="iconInfoDialog.open = false; iconInfoDialog.group = null"
155
+ />
156
+
157
+ <AppHubDesktopIconRenameDialog
158
+ :open="iconRenameDialog.open"
159
+ :title="renameDialogTitle"
160
+ :name-label="renameDialogNameLabel"
161
+ :initial-name="renameDialogInitialName"
162
+ :save-label="labels.icon_rename_save"
163
+ :cancel-label="labels.icon_rename_cancel"
164
+ :error="iconRenameDialog.error"
165
+ @save="onIconRenameSave"
166
+ @cancel="iconRenameDialog.open = false; iconRenameDialog.group = null"
167
+ />
168
+
169
+ <AppHubDuplicateAppDialog
170
+ :open="duplicateDialog.open"
171
+ :title="labels.duplicate_app_title"
172
+ :message="duplicateMessage"
173
+ :hint="duplicateHint"
174
+ :replace-label="labels.duplicate_app_replace"
175
+ :keep-label="labels.duplicate_app_keep"
176
+ :cancel-label="labels.duplicate_app_cancel"
177
+ @replace="onDuplicateReplace"
178
+ @keep="onDuplicateKeep"
179
+ @cancel="onDuplicateCancel"
180
+ />
181
+
182
+ <div ref="workAreaRef" class="apphub-desktop__workarea">
183
+ <AppHubWindowFrame
184
+ v-for="win in visibleWindows"
185
+ :key="win.id"
186
+ :window="win"
187
+ :active="win.id === wm.state.activeId"
188
+ @session-change="persistSession"
189
+ />
190
+ </div>
191
+
192
+ <AppHubStartMenu
193
+ :open="shell.state.startOpen"
194
+ :favorite-apps="startMenuFavoriteApps"
195
+ :recent-apps="startMenuRecentApps"
196
+ :suggested-apps="startMenuSuggestedApps"
197
+ :catalog-apps="startMenuCatalogApps"
198
+ :visible-in-start-ids="visibleInStartIds"
199
+ :search-placeholder="labels.start_menu_search"
200
+ :favorites-label="labels.start_menu_favorites"
201
+ :recent-label="labels.start_menu_recent"
202
+ :search-results-label="labels.start_menu_search_results"
203
+ :suggested-label="labels.start_menu_suggested"
204
+ :empty-label="labels.start_menu_empty"
205
+ @close="shell.state.startOpen = false"
206
+ @open-app="onStartMenuOpenApp"
207
+ />
208
+
209
+ <footer class="apphub-desktop__taskbar" @click.stop>
210
+ <AppHubStartButton
211
+ :active="shell.state.startOpen"
212
+ :title="labels.desktop_start"
213
+ @toggle="onToggleStart"
214
+ />
215
+
216
+ <div class="apphub-desktop__tasks">
217
+ <button
218
+ v-for="win in taskbarWindows"
219
+ :key="win.id"
220
+ type="button"
221
+ class="apphub-desktop__task"
222
+ :class="{ active: win.id === wm.state.activeId, minimized: win.minimized }"
223
+ @click="onTaskClick(win)"
224
+ >
225
+ {{ win.icon }} {{ win.title }}
226
+ </button>
227
+ </div>
228
+
229
+ <AppHubTaskbarPins
230
+ :apps="taskbarPinnedApps"
231
+ :aria-label="labels.taskbar_pins"
232
+ @open-app="onOpenIcon"
233
+ />
234
+
235
+ <button
236
+ v-if="draftStoreApp"
237
+ type="button"
238
+ class="apphub-desktop__taskbar-draft-store"
239
+ :title="draftStoreApp.hint || draftStoreApp.name"
240
+ @click="onOpenIcon(draftStoreApp)"
241
+ >
242
+ <span class="apphub-desktop__taskbar-draft-store-icon" aria-hidden="true">{{ draftStoreApp.icon }}</span>
243
+ <span class="apphub-desktop__taskbar-draft-store-label">{{ draftStoreApp.name }}</span>
244
+ </button>
245
+
246
+ <span class="apphub-desktop__clock">{{ shell.state.clock }}</span>
247
+ </footer>
248
+
249
+ <AppHubDesktopDevOriginBar placement="corner" />
250
+
251
+ <AppHubDesktopNotifications />
252
+ </div>
253
+ </template>
254
+
255
+ <script setup>
256
+ import { computed, getCurrentInstance, inject, nextTick, onMounted, onUnmounted, provide, reactive, ref, watch } from 'vue'
257
+ import { getHostApiForApp, isBackendReadyForApp } from '../../../composables/useAppHubHostApi.js'
258
+ import { CATALOG_MODE_DRAFT, CATALOG_MODE_STORE } from '../../app-store/constants/catalogModes.js'
259
+ import { useAppStore } from '../../app-store/index.js'
260
+ import { resolveLang } from '../../../i18n/resolveLang.js'
261
+ import { isThemeLocked, resolveTheme } from '../../../i18n/resolveTheme.js'
262
+ import { t } from '../../../i18n/index.js'
263
+ import {
264
+ AppHubDesktopNotifications,
265
+ createDesktopNotificationsState,
266
+ DESKTOP_NOTIFICATIONS_KEY,
267
+ } from '../../notifications/index.js'
268
+ import { AppHubWindowFrame, useWindowManager } from '../../window-manager/index.js'
269
+ import AppHubDesktopDropLayer from './AppHubDesktopDropLayer.vue'
270
+ import AppHubStartButton from './AppHubStartButton.vue'
271
+ import AppHubStartMenu from './AppHubStartMenu.vue'
272
+ import AppHubTaskbarPins from './AppHubTaskbarPins.vue'
273
+ import AppHubDuplicateAppDialog from './AppHubDuplicateAppDialog.vue'
274
+ import AppHubDesktopIconContextMenu from './AppHubDesktopIconContextMenu.vue'
275
+ import AppHubDesktopIconInfoDialog from './AppHubDesktopIconInfoDialog.vue'
276
+ import AppHubDesktopIconRenameDialog from './AppHubDesktopIconRenameDialog.vue'
277
+ import AppHubDropInstallBadge from './AppHubDropInstallBadge.vue'
278
+ import AppHubDesktopIconGroup from './AppHubDesktopIconGroup.vue'
279
+ import AppHubDesktopIconFolder from './AppHubDesktopIconFolder.vue'
280
+ import AppHubOriginBlockScreen from './AppHubOriginBlockScreen.vue'
281
+ import AppHubOriginLoadingScreen from './AppHubOriginLoadingScreen.vue'
282
+ import AppHubDesktopDevOriginBar from './AppHubDesktopDevOriginBar.vue'
283
+ import { createDesktopDropInstall } from '../composables/useDesktopDropInstall.js'
284
+ import { evaluateOriginSafety } from '../../../utils/originSafety.js'
285
+ import { createDesktopShell } from '../composables/useDesktopShell.js'
286
+ import { useDesktopIconDrag } from '../composables/useDesktopIconDrag.js'
287
+ import { buildDesktopItems, getGroupDisplayName, migrateGroupDisplayName, setGroupDisplayName } from '../utils/desktopIconGroups.js'
288
+ import {
289
+ buildDesktopSession,
290
+ loadDesktopSession,
291
+ saveDesktopSession,
292
+ } from '../utils/desktopSession.js'
293
+ import { clampPointToLayer, nextIconGridSlot, snapPoint } from '../utils/desktopGrid.js'
294
+ import { DESKTOP_HUB_SETTINGS_KEY } from '../composables/useDesktopHubSettings.js'
295
+ import { applyDesktopSettings, loadDesktopSettings, saveDesktopSettings } from '../utils/desktopSettings.js'
296
+ import { loadHubKeyboardSettings, matchSnapShortcut, saveHubKeyboardSettings } from '../utils/hubKeyboardSettings.js'
297
+ import {
298
+ isAppPinned,
299
+ isAppVisibleInStart,
300
+ loadStartMenuPins,
301
+ pinApp,
302
+ saveStartMenuPins,
303
+ setPinVisible,
304
+ unpinApp,
305
+ } from '../utils/startMenuPins.js'
306
+ import {
307
+ favoriteApp,
308
+ isAppFavorite,
309
+ resolveStartMenuFavoriteApps,
310
+ resolveStartMenuRecentApps,
311
+ loadStartMenuFavorites,
312
+ saveStartMenuFavorites,
313
+ setFavoriteVisible,
314
+ unfavoriteApp,
315
+ } from '../utils/startMenuFavorites.js'
316
+ import {
317
+ loadRecentOpenLog,
318
+ recordRecentApp,
319
+ resolveRecentApps,
320
+ resolveSuggestedApps,
321
+ } from '../utils/recentApps.js'
322
+ import { nextDuplicateName } from '../utils/duplicateAppUtils.js'
323
+
324
+ const DESKTOP_HOST_KEY = 'apphubDesktopHost'
325
+
326
+ const props = defineProps({
327
+ language: { type: String, default: 'vi' },
328
+ /** Open installed (or catalog) app by slug on mount — e.g. /apphub?open=pilot-active */
329
+ initialOpenSlug: { type: String, default: '' },
330
+ openAppStoreOnMount: { type: Boolean, default: true },
331
+ /** 'dark' | 'light' | 'auto' — auto uses saved user preference */
332
+ theme: { type: String, default: 'auto' },
333
+ /** Show Light mode in Start menu. Default: hidden when theme prop locks appearance */
334
+ themeToggle: { type: Boolean, default: undefined },
335
+ })
336
+
337
+ const moduleOptions = inject('apphubOptions', {})
338
+ const rootApp = getCurrentInstance()?.appContext?.app
339
+
340
+ const lang = computed(() => resolveLang(moduleOptions?.language, props.language))
341
+
342
+ const originBootstrapLoading = computed(() => moduleOptions?.originBootstrapLoading === true)
343
+
344
+ const originSafety = computed(() => {
345
+ if (moduleOptions && Object.prototype.hasOwnProperty.call(moduleOptions, 'originBlocked')) {
346
+ return {
347
+ safe: !moduleOptions.originBlocked,
348
+ pending: moduleOptions.originCheckPending === true,
349
+ loading: moduleOptions.originBootstrapLoading === true,
350
+ reason: moduleOptions.originBlockReason ?? null,
351
+ parentOrigin: moduleOptions.originBlockParentOrigin ?? null,
352
+ expectedHubOrigin: moduleOptions.originBlockExpectedHubOrigin ?? null,
353
+ expectedRuntimeOrigin: moduleOptions.originBlockExpectedRuntimeOrigin ?? null,
354
+ }
355
+ }
356
+ return evaluateOriginSafety(moduleOptions)
357
+ })
358
+
359
+ const desktopReady = computed(() => !originBootstrapLoading.value && originSafety.value.safe)
360
+
361
+ const originBlockLabels = computed(() => ({
362
+ title: t('origin_block_title', lang.value),
363
+ same_origin_embed: t('origin_block_same_origin_embed', lang.value),
364
+ not_configured: t('origin_block_not_configured', lang.value),
365
+ wrong_origin: t('origin_block_wrong_origin', lang.value),
366
+ runtime_not_configured: t('origin_block_runtime_not_configured', lang.value),
367
+ runtime_same_origin: t('origin_block_runtime_same_origin', lang.value),
368
+ generic: t('origin_block_generic', lang.value),
369
+ hint: t('origin_block_hint', lang.value),
370
+ current_origin: t('origin_block_current_origin', lang.value),
371
+ expected_hub_origin: t('origin_block_expected_hub_origin', lang.value),
372
+ expected_runtime_origin: t('origin_block_expected_runtime_origin', lang.value),
373
+ parent_origin: t('origin_block_parent_origin', lang.value),
374
+ }))
375
+
376
+ const activeTheme = computed(() => {
377
+ const locked = resolveTheme(props.theme) ?? resolveTheme(moduleOptions?.theme)
378
+ if (locked) return locked
379
+ return desktopSettings.theme === 'light' ? 'light' : 'dark'
380
+ })
381
+
382
+ const showThemeToggle = computed(() => {
383
+ if (props.themeToggle === true) return true
384
+ if (props.themeToggle === false) return false
385
+ if (moduleOptions.themeToggle === true) return true
386
+ if (moduleOptions.themeToggle === false) return false
387
+ return !isThemeLocked(props.theme, moduleOptions?.theme)
388
+ })
389
+
390
+ const wm = useWindowManager()
391
+ const appStore = useAppStore()
392
+ const desktopRoot = ref(null)
393
+ const workAreaRef = ref(null)
394
+ const iconsLayerRef = ref(null)
395
+
396
+ provide(DESKTOP_HOST_KEY, workAreaRef)
397
+
398
+ const desktopSettings = reactive(loadDesktopSettings())
399
+ const keyboardSettings = reactive(loadHubKeyboardSettings())
400
+ const startMenuPins = reactive(loadStartMenuPins())
401
+ const startMenuFavorites = reactive(loadStartMenuFavorites())
402
+ const recentOpenLog = ref(loadRecentOpenLog())
403
+
404
+ const builtinPlacements = reactive({})
405
+
406
+ const duplicateDialog = reactive({
407
+ open: false,
408
+ app: null,
409
+ existing: null,
410
+ })
411
+ let duplicateResolve = null
412
+
413
+ const iconContextMenu = reactive({ open: false, x: 0, y: 0, app: null, group: null })
414
+ const iconInfoDialog = reactive({ open: false, app: null, group: null })
415
+ const iconRenameDialog = reactive({ open: false, app: null, group: null, error: '' })
416
+
417
+ function formatLabel(key, params = {}) {
418
+ return t(key, lang.value, params)
419
+ }
420
+
421
+ const labels = computed(() => ({
422
+ desktop_start: t('desktop_start', lang.value),
423
+ desktop_app_store: t('desktop_app_store', lang.value),
424
+ desktop_app_store_hint: t('desktop_app_store_hint', lang.value),
425
+ guide_app_name: t('guide_app_name', lang.value),
426
+ guide_app_hint: t('guide_app_hint', lang.value),
427
+ guide_app_title: t('guide_app_title', lang.value),
428
+ hub_settings_app_name: t('hub_settings_app_name', lang.value),
429
+ hub_settings_app_hint: t('hub_settings_app_hint', lang.value),
430
+ hub_settings_app_title: t('hub_settings_app_title', lang.value),
431
+ app_store_title: t('app_store_title', lang.value),
432
+ app_store_status_draft: t('app_store_status_draft', lang.value),
433
+ desktop_icon_draft_hint: t('desktop_icon_draft_hint', lang.value),
434
+ drop_hint: t('drop_hint', lang.value),
435
+ drop_installing: t('drop_installing', lang.value),
436
+ drop_error: t('drop_error', lang.value),
437
+ drop_done_publish: t('drop_done_publish', lang.value),
438
+ notif_error_title: t('notif_error_title', lang.value),
439
+ notif_publish_success: t('notif_publish_success', lang.value),
440
+ notif_publish_upgrade_success: t('notif_publish_upgrade_success', lang.value),
441
+ notif_install_cancelled: t('notif_install_cancelled', lang.value),
442
+ drop_method_publish: t('drop_method_publish', lang.value),
443
+ drop_method_appstore: t('drop_method_appstore', lang.value),
444
+ drop_method_local: t('drop_method_local', lang.value),
445
+ settings_snap_grid: t('settings_snap_grid', lang.value),
446
+ settings_light_mode: t('settings_light_mode', lang.value),
447
+ duplicate_app_title: t('duplicate_app_title', lang.value),
448
+ duplicate_app_replace: t('duplicate_app_replace', lang.value),
449
+ duplicate_app_keep: t('duplicate_app_keep', lang.value),
450
+ duplicate_app_cancel: t('duplicate_app_cancel', lang.value),
451
+ desktop_icon_move_hint: t('desktop_icon_move_hint', lang.value),
452
+ desktop_icon_hold_hint: t('desktop_icon_hold_hint', lang.value),
453
+ draft_store_app_name: t('draft_store_app_name', lang.value),
454
+ draft_store_app_hint: t('draft_store_app_hint', lang.value),
455
+ draft_store_title: t('draft_store_title', lang.value),
456
+ group_label: t('group_label', lang.value),
457
+ group_drop_hint: t('group_drop_hint', lang.value),
458
+ group_folder_title: t('group_folder_title', lang.value),
459
+ group_folder_count: t('group_folder_count', lang.value),
460
+ group_context_open: t('group_context_open', lang.value),
461
+ group_rename_title: t('group_rename_title', lang.value),
462
+ group_rename_label: t('group_rename_label', lang.value),
463
+ group_info_title: t('group_info_title', lang.value),
464
+ group_info_count: t('group_info_count', lang.value),
465
+ group_info_apps: t('group_info_apps', lang.value),
466
+ group_info_type_group: t('group_info_type_group', lang.value),
467
+ start_menu_search: t('start_menu_search', lang.value),
468
+ start_menu_recent: t('start_menu_recent', lang.value),
469
+ start_menu_search_results: t('start_menu_search_results', lang.value),
470
+ start_menu_suggested: t('start_menu_suggested', lang.value),
471
+ start_menu_empty: t('start_menu_empty', lang.value),
472
+ taskbar_pins: t('taskbar_pins', lang.value),
473
+ icon_context_open: t('icon_context_open', lang.value),
474
+ icon_context_rename: t('icon_context_rename', lang.value),
475
+ icon_context_properties: t('icon_context_properties', lang.value),
476
+ icon_context_pin: t('icon_context_pin', lang.value),
477
+ icon_context_unpin: t('icon_context_unpin', lang.value),
478
+ icon_context_favorite: t('icon_context_favorite', lang.value),
479
+ icon_context_unfavorite: t('icon_context_unfavorite', lang.value),
480
+ icon_context_uninstall: t('icon_context_uninstall', lang.value),
481
+ start_menu_favorites: t('start_menu_favorites', lang.value),
482
+ icon_info_title: t('icon_info_title', lang.value),
483
+ icon_info_name: t('icon_info_name', lang.value),
484
+ icon_info_slug: t('icon_info_slug', lang.value),
485
+ icon_info_version: t('icon_info_version', lang.value),
486
+ icon_info_created: t('icon_info_created', lang.value),
487
+ icon_info_source: t('icon_info_source', lang.value),
488
+ icon_info_source_appstore: t('icon_info_source_appstore', lang.value),
489
+ icon_info_source_local: t('icon_info_source_local', lang.value),
490
+ icon_info_description: t('icon_info_description', lang.value),
491
+ icon_info_position: t('icon_info_position', lang.value),
492
+ icon_info_type: t('icon_info_type', lang.value),
493
+ icon_info_type_builtin: t('icon_info_type_builtin', lang.value),
494
+ icon_info_date_unknown: t('icon_info_date_unknown', lang.value),
495
+ icon_info_close: t('icon_info_close', lang.value),
496
+ icon_rename_title: t('icon_rename_title', lang.value),
497
+ icon_rename_label: t('icon_rename_label', lang.value),
498
+ icon_rename_save: t('icon_rename_save', lang.value),
499
+ icon_rename_cancel: t('icon_rename_cancel', lang.value),
500
+ icon_rename_error_empty: t('icon_rename_error_empty', lang.value),
501
+ icon_rename_error_duplicate: t('icon_rename_error_duplicate', lang.value),
502
+ }))
503
+
504
+ const duplicateMessage = computed(() =>
505
+ duplicateDialog.app
506
+ ? formatLabel('duplicate_app_message', { name: duplicateDialog.app.name })
507
+ : '',
508
+ )
509
+
510
+ const duplicateHint = computed(() =>
511
+ duplicateDialog.app
512
+ ? formatLabel('duplicate_app_keep_as', {
513
+ name: nextDuplicateName(duplicateDialog.app.name, shell.state.userApps),
514
+ })
515
+ : t('duplicate_app_hint', lang.value),
516
+ )
517
+
518
+ const visibleWindows = computed(() =>
519
+ (wm.visibleWindows?.value ?? wm.visibleWindows ?? []).filter((w) => w?.id),
520
+ )
521
+ const taskbarWindows = computed(() =>
522
+ (wm.taskbarWindows?.value ?? wm.taskbarWindows ?? []).filter((w) => w?.id),
523
+ )
524
+
525
+ const contextMenuOpenLabel = computed(() =>
526
+ iconContextMenu.group ? labels.value.group_context_open : labels.value.icon_context_open,
527
+ )
528
+
529
+ const contextMenuCanRename = computed(() => {
530
+ if (iconContextMenu.group) return true
531
+ return !iconContextMenu.app?.builtin
532
+ })
533
+
534
+ const contextMenuShowPin = computed(() => !iconContextMenu.group && !!iconContextMenu.app?.id)
535
+
536
+ const contextMenuPinLabel = computed(() => {
537
+ const app = iconContextMenu.app
538
+ if (!app?.id) return labels.value.icon_context_pin
539
+ return isAppPinned(startMenuPins, app.id)
540
+ ? labels.value.icon_context_unpin
541
+ : labels.value.icon_context_pin
542
+ })
543
+
544
+ const contextMenuShowFavorite = computed(() => !iconContextMenu.group && !!iconContextMenu.app?.id)
545
+
546
+ const contextMenuShowUninstall = computed(() => {
547
+ const app = iconContextMenu.app
548
+ return !iconContextMenu.group && !!app?.id && !app.builtin && !app.module
549
+ })
550
+
551
+ const contextMenuFavoriteLabel = computed(() => {
552
+ const app = iconContextMenu.app
553
+ if (!app?.id) return labels.value.icon_context_favorite
554
+ return isAppFavorite(startMenuFavorites, app.id)
555
+ ? labels.value.icon_context_unfavorite
556
+ : labels.value.icon_context_favorite
557
+ })
558
+
559
+ const iconInfoDialogTitle = computed(() =>
560
+ iconInfoDialog.group ? labels.value.group_info_title : labels.value.icon_info_title,
561
+ )
562
+
563
+ const iconInfoDialogApp = computed(() => {
564
+ if (iconInfoDialog.group) {
565
+ return { icon: '📂', name: groupLabel(iconInfoDialog.group.apps, iconInfoDialog.group.x, iconInfoDialog.group.y) }
566
+ }
567
+ return iconInfoDialog.app
568
+ })
569
+
570
+ const renameDialogTitle = computed(() =>
571
+ iconRenameDialog.group ? labels.value.group_rename_title : labels.value.icon_rename_title,
572
+ )
573
+
574
+ const renameDialogNameLabel = computed(() =>
575
+ iconRenameDialog.group ? labels.value.group_rename_label : labels.value.icon_rename_label,
576
+ )
577
+
578
+ const renameDialogInitialName = computed(() => {
579
+ if (iconRenameDialog.group) return groupLabel(iconRenameDialog.group.apps, iconRenameDialog.group.x, iconRenameDialog.group.y)
580
+ return iconRenameDialog.app?.name ?? ''
581
+ })
582
+
583
+ const isMainScreen = computed(() => visibleWindows.value.length === 0)
584
+
585
+ const iconInfoRows = computed(() => {
586
+ const group = iconInfoDialog.group
587
+ if (group) {
588
+ const L = labels.value
589
+ const apps = group.apps ?? []
590
+ return [
591
+ { label: L.icon_info_name, value: groupLabel(apps, group.x, group.y) },
592
+ { label: L.icon_info_type, value: L.group_info_type_group },
593
+ { label: L.group_info_count, value: String(apps.length) },
594
+ { label: L.group_info_apps, value: apps.map((a) => a.name).join(', ') },
595
+ { label: L.icon_info_position, value: `${group.x}, ${group.y}` },
596
+ ]
597
+ }
598
+
599
+ const app = iconInfoDialog.app
600
+ if (!app) return []
601
+ const L = labels.value
602
+ const rows = [{ label: L.icon_info_name, value: app.name }]
603
+ if (app.builtin) {
604
+ rows.push({ label: L.icon_info_type, value: L.icon_info_type_builtin })
605
+ if (app.hint) rows.push({ label: L.icon_info_description, value: app.hint })
606
+ return rows
607
+ }
608
+ rows.push({ label: L.icon_info_slug, value: app.slug })
609
+ const pinnedVersion = app.installedVersion ?? app.version
610
+ if (pinnedVersion) rows.push({ label: L.icon_info_version, value: `v${pinnedVersion}` })
611
+ rows.push({ label: L.icon_info_created, value: formatAppCreatedAt(app.createdAt) })
612
+ rows.push({ label: L.icon_info_source, value: resolveAppInstallSource(app) })
613
+ if (app.hint) rows.push({ label: L.icon_info_description, value: app.hint })
614
+ if (app.desktopX != null && app.desktopY != null) {
615
+ rows.push({ label: L.icon_info_position, value: `${app.desktopX}, ${app.desktopY}` })
616
+ }
617
+ return rows
618
+ })
619
+
620
+ async function handleInstallUserApp(app, position, method = 'local') {
621
+ let result = shell.installUserApp(app, position, method)
622
+ while (result?.needsDuplicateChoice) {
623
+ const choice = await askDuplicateChoice(result.app, result.existing)
624
+ if (choice === 'cancel') return 'cancelled'
625
+ result = shell.installUserApp(result.app, result.position, result.method, choice)
626
+ }
627
+ assignDefaultIconPositions()
628
+ schedulePersist()
629
+ return result
630
+ }
631
+
632
+ function handleUninstallUserApp(appOrSlug) {
633
+ const slug = typeof appOrSlug === 'string' ? appOrSlug : appOrSlug?.slug
634
+ const app = (typeof appOrSlug === 'object' && appOrSlug?.id && !appOrSlug?.builtin ? appOrSlug : null)
635
+ ?? (slug ? shell.findUserAppBySlug(slug) : null)
636
+ if (!app || app.builtin) return false
637
+
638
+ if (slug) appStore.uninstallApp(slug)
639
+
640
+ const winId = `win-${app.id}`
641
+ if (wm.state.windows?.some((w) => w.id === winId)) {
642
+ wm.closeWindow(winId)
643
+ }
644
+ if (isAppPinned(startMenuPins, app.id)) unpinApp(startMenuPins, app.id)
645
+ if (isAppFavorite(startMenuFavorites, app.id)) unfavoriteApp(startMenuFavorites, app.id)
646
+
647
+ shell.removeUserApp(app.id)
648
+ assignDefaultIconPositions()
649
+ schedulePersist()
650
+ return true
651
+ }
652
+
653
+ function handleUpdateUserApp(app) {
654
+ if (!app?.slug || !app?.version) return false
655
+ const ok = shell.updateInstalledVersion(app.slug, app.version)
656
+ if (ok) schedulePersist()
657
+ return ok
658
+ }
659
+
660
+ const shell = createDesktopShell({
661
+ language: lang,
662
+ getLabels: () => labels.value,
663
+ handleInstall: handleInstallUserApp,
664
+ handleUninstall: handleUninstallUserApp,
665
+ onUpdateApp: handleUpdateUserApp,
666
+ onAppOpened: (appId) => touchRecentApp(appId),
667
+ })
668
+
669
+ const iconList = shell.iconList
670
+
671
+ const builtinIcons = computed(() => iconList.filter((a) => a?.builtin))
672
+ const startMenuCatalogApps = computed(() => iconList.filter((a) => a?.id))
673
+
674
+ const startMenuFavoriteApps = computed(() =>
675
+ resolveStartMenuFavoriteApps(startMenuCatalogApps.value, startMenuFavorites),
676
+ )
677
+
678
+ const startMenuRecentApps = computed(() =>
679
+ resolveStartMenuRecentApps(
680
+ startMenuCatalogApps.value,
681
+ startMenuFavorites,
682
+ recentOpenLog.value,
683
+ ),
684
+ )
685
+
686
+ const startMenuSuggestedApps = computed(() =>
687
+ resolveSuggestedApps(startMenuCatalogApps.value, recentOpenLog.value),
688
+ )
689
+
690
+ const taskbarPinnedApps = computed(() =>
691
+ startMenuCatalogApps.value.filter((a) => isAppVisibleInStart(startMenuPins, a.id)),
692
+ )
693
+
694
+ const draftStoreApp = computed(() => {
695
+ const apps = shell.taskbarBuiltinApps
696
+ const list = apps?.value ?? apps ?? []
697
+ return list[0] ?? null
698
+ })
699
+
700
+ const visibleInStartIds = computed(() =>
701
+ startMenuCatalogApps.value
702
+ .filter((a) => isAppVisibleInStart(startMenuPins, a.id))
703
+ .map((a) => a.id),
704
+ )
705
+
706
+ function getBuiltinPlacedApp(app) {
707
+ const pos = builtinPlacements[app.id]
708
+ if (!pos) return null
709
+ return {
710
+ ...app,
711
+ desktopX: pos.x,
712
+ desktopY: pos.y,
713
+ }
714
+ }
715
+
716
+ function getAllPlacedApps() {
717
+ const builtins = builtinIcons.value
718
+ .map((app) => getBuiltinPlacedApp(app))
719
+ .filter(Boolean)
720
+ const users = shell.state.userApps.filter((a) => a.desktopX != null && a.desktopY != null)
721
+ return [...builtins, ...users]
722
+ }
723
+
724
+ const desktopLayout = computed(() => buildDesktopItems(getAllPlacedApps()))
725
+
726
+ const dragFolderPreview = computed(() => {
727
+ const target = iconDrag.dropTarget.value
728
+ if (!target?.merging || !target.apps?.length) return null
729
+ return target
730
+ })
731
+
732
+ const dragPreviewNewIds = computed(() => {
733
+ if (!iconDrag.dropTarget.value?.merging || !iconDrag.drag.value?.moved) return []
734
+ return iconDrag.drag.value.ids
735
+ })
736
+
737
+ const openFolder = reactive({
738
+ open: false,
739
+ x: 0,
740
+ y: 0,
741
+ apps: [],
742
+ title: '',
743
+ countLabel: '',
744
+ })
745
+
746
+ let clockTimer = null
747
+ let resizeObserver = null
748
+ let persistTimer = null
749
+
750
+ function persistSession() {
751
+ saveDesktopSession(buildDesktopSession(shell, wm, appStore, desktopSettings))
752
+ saveDesktopSettings(desktopSettings)
753
+ }
754
+
755
+ function schedulePersist() {
756
+ if (persistTimer) clearTimeout(persistTimer)
757
+ persistTimer = setTimeout(persistSession, 150)
758
+ }
759
+
760
+ const iconDrag = useDesktopIconDrag({
761
+ getLayerEl: () => iconsLayerRef.value,
762
+ getSnapToGrid: () => desktopSettings.snapToGrid,
763
+ getDesktopApps: () => getAllPlacedApps(),
764
+ findApp: (id) => findAppForDrag(id),
765
+ onMoved: (details) => {
766
+ if (details?.fromCell && details?.toCell) {
767
+ migrateGroupDisplayName(
768
+ desktopSettings,
769
+ details.fromCell.x,
770
+ details.fromCell.y,
771
+ details.toCell.x,
772
+ details.toCell.y,
773
+ )
774
+ }
775
+ syncBuiltinPositionsToSettings()
776
+ closeOpenFolder()
777
+ schedulePersist()
778
+ },
779
+ })
780
+
781
+ function findAppForDrag(id) {
782
+ const user = shell.findUserApp(id)
783
+ if (user) return user
784
+
785
+ const meta = shell.findDesktopApp(id)
786
+ if (!meta?.builtin || !builtinPlacements[id]) return null
787
+
788
+ return {
789
+ ...meta,
790
+ get desktopX() {
791
+ return builtinPlacements[id].x
792
+ },
793
+ set desktopX(value) {
794
+ builtinPlacements[id].x = value
795
+ },
796
+ get desktopY() {
797
+ return builtinPlacements[id].y
798
+ },
799
+ set desktopY(value) {
800
+ builtinPlacements[id].y = value
801
+ },
802
+ }
803
+ }
804
+
805
+ function syncBuiltinPositionsToSettings() {
806
+ desktopSettings.builtinPositions = { ...builtinPlacements }
807
+ }
808
+
809
+ function groupLabel(apps, x, y) {
810
+ const n = apps?.length ?? 0
811
+ if (n <= 1) return apps?.[0]?.name ?? labels.value.group_label
812
+ return getGroupDisplayName(desktopSettings, x, y, labels.value, n)
813
+ }
814
+
815
+ function isDropTargetCell(x, y) {
816
+ const target = iconDrag.dropTarget.value
817
+ return target && target.x === x && target.y === y
818
+ }
819
+
820
+ function isGroupDragging(apps) {
821
+ return apps.some((a) => iconDrag.isDragging(a.id))
822
+ }
823
+
824
+ function isGroupHolding(apps) {
825
+ return apps.some((a) => iconDrag.isHolding(a.id))
826
+ }
827
+
828
+ function closeOpenFolder() {
829
+ openFolder.open = false
830
+ openFolder.apps = []
831
+ }
832
+
833
+ function openGroupFolder(item) {
834
+ openFolder.x = item.x
835
+ openFolder.y = item.y
836
+ openFolder.apps = [...item.apps]
837
+ openFolder.title = groupLabel(item.apps, item.x, item.y)
838
+ openFolder.countLabel = formatLabel('group_folder_count', { count: item.apps.length })
839
+ openFolder.open = true
840
+ }
841
+
842
+ function onPlacedIconPointerDown(app, event) {
843
+ if (event.button !== 0) return
844
+ event.preventDefault()
845
+ closeOpenFolder()
846
+ iconDrag.onPointerDown(app, event, { mode: 'single' })
847
+ }
848
+
849
+ function onGroupClick(item) {
850
+ if (iconDrag.lastWasDrag.value) {
851
+ iconDrag.lastWasDrag.value = false
852
+ return
853
+ }
854
+ openGroupFolder(item)
855
+ }
856
+
857
+ function onGroupPointerDown(item, event) {
858
+ if (event.button !== 0) return
859
+ event.preventDefault()
860
+ closeOpenFolder()
861
+ iconDrag.onPointerDown(item.apps, event, { mode: 'group' })
862
+ }
863
+
864
+ function onFolderItemPointerDown(app, event) {
865
+ if (event.button !== 0) return
866
+ event.preventDefault()
867
+ iconDrag.onPointerDown(app, event, {
868
+ mode: 'folder',
869
+ onTap: () => onOpenIcon(app),
870
+ })
871
+ }
872
+
873
+ function onFolderOpenApp(app) {
874
+ closeOpenFolder()
875
+ onOpenIcon(app)
876
+ }
877
+
878
+ function onFolderItemContextMenu(app, event) {
879
+ onIconContextMenu(app, event)
880
+ }
881
+
882
+ function onGroupContextMenu(item, event) {
883
+ event.preventDefault()
884
+ event.stopPropagation()
885
+ if (isGroupDragging(item.apps)) return
886
+
887
+ const layer = iconsLayerRef.value
888
+ if (!layer) return
889
+ const rect = layer.getBoundingClientRect()
890
+ const menuW = 220
891
+ const menuH = 132
892
+ let x = event.clientX - rect.left
893
+ let y = event.clientY - rect.top
894
+ x = Math.max(4, Math.min(x, rect.width - menuW - 4))
895
+ y = Math.max(4, Math.min(y, rect.height - menuH - 4))
896
+
897
+ iconContextMenu.app = null
898
+ iconContextMenu.group = item
899
+ iconContextMenu.x = x
900
+ iconContextMenu.y = y
901
+ iconContextMenu.open = true
902
+ }
903
+
904
+ function syncBuiltinPlacementsFromSettings() {
905
+ const saved = desktopSettings.builtinPositions
906
+ if (!saved || typeof saved !== 'object') return
907
+ for (const [id, pos] of Object.entries(saved)) {
908
+ if (pos && Number.isFinite(pos.x) && Number.isFinite(pos.y)) {
909
+ builtinPlacements[id] = { x: pos.x, y: pos.y }
910
+ }
911
+ }
912
+ }
913
+
914
+ function ensureBuiltinPositions() {
915
+ builtinIcons.value.forEach((app, index) => {
916
+ if (builtinPlacements[app.id]) return
917
+ const saved = desktopSettings.builtinPositions?.[app.id]
918
+ builtinPlacements[app.id] = saved
919
+ ? { ...saved }
920
+ : { x: 16, y: 16 + index * 96 }
921
+ })
922
+ }
923
+
924
+ function assignDefaultIconPositions() {
925
+ const layer = iconsLayerRef.value
926
+ if (!layer) return
927
+ const seen = new Set()
928
+ const occupied = []
929
+ for (const a of getAllPlacedApps()) {
930
+ const key = `${a.desktopX},${a.desktopY}`
931
+ if (seen.has(key)) continue
932
+ seen.add(key)
933
+ occupied.push({ x: a.desktopX, y: a.desktopY })
934
+ }
935
+ shell.state.userApps.forEach((app) => {
936
+ if (app.desktopX != null) return
937
+ const pos = nextIconGridSlot(occupied, layer.clientWidth, layer.clientHeight)
938
+ app.desktopX = pos.x
939
+ app.desktopY = pos.y
940
+ occupied.push(pos)
941
+ })
942
+ }
943
+
944
+ function askDuplicateChoice(app, existing) {
945
+ duplicateDialog.app = app
946
+ duplicateDialog.existing = existing
947
+ duplicateDialog.open = true
948
+ return new Promise((resolve) => {
949
+ duplicateResolve = resolve
950
+ })
951
+ }
952
+
953
+ function closeDuplicate(choice) {
954
+ duplicateDialog.open = false
955
+ duplicateResolve?.(choice)
956
+ duplicateResolve = null
957
+ duplicateDialog.app = null
958
+ duplicateDialog.existing = null
959
+ }
960
+
961
+ function onDuplicateReplace() {
962
+ closeDuplicate('replace')
963
+ }
964
+
965
+ function onDuplicateKeep() {
966
+ closeDuplicate('keep')
967
+ }
968
+
969
+ function onDuplicateCancel() {
970
+ closeDuplicate('cancel')
971
+ }
972
+
973
+ function onSnapGridChange(value) {
974
+ desktopSettings.snapToGrid = value
975
+ if (value) {
976
+ shell.state.userApps.forEach((app) => {
977
+ if (app.desktopX == null) return
978
+ const pos = snapPoint(app.desktopX, app.desktopY, true)
979
+ app.desktopX = pos.x
980
+ app.desktopY = pos.y
981
+ })
982
+ }
983
+ schedulePersist()
984
+ }
985
+
986
+ function onThemeChange(theme) {
987
+ desktopSettings.theme = theme === 'light' ? 'light' : 'dark'
988
+ schedulePersist()
989
+ }
990
+
991
+ function persistStartMenuPins() {
992
+ saveStartMenuPins(startMenuPins)
993
+ }
994
+
995
+ function persistStartMenuFavorites() {
996
+ saveStartMenuFavorites(startMenuFavorites)
997
+ }
998
+
999
+ function toggleAppPin(appId) {
1000
+ if (!appId) return
1001
+ if (isAppPinned(startMenuPins, appId)) {
1002
+ unpinApp(startMenuPins, appId)
1003
+ } else {
1004
+ pinApp(startMenuPins, appId)
1005
+ }
1006
+ persistStartMenuPins()
1007
+ }
1008
+
1009
+ function toggleAppFavorite(appId) {
1010
+ if (!appId) return
1011
+ if (isAppFavorite(startMenuFavorites, appId)) {
1012
+ unfavoriteApp(startMenuFavorites, appId)
1013
+ } else {
1014
+ favoriteApp(startMenuFavorites, appId)
1015
+ }
1016
+ persistStartMenuFavorites()
1017
+ }
1018
+
1019
+ function setAppPinVisible(appId, visible) {
1020
+ setPinVisible(startMenuPins, appId, visible)
1021
+ persistStartMenuPins()
1022
+ }
1023
+
1024
+ function setAppFavoriteVisible(appId, visible) {
1025
+ setFavoriteVisible(startMenuFavorites, appId, visible)
1026
+ persistStartMenuFavorites()
1027
+ }
1028
+
1029
+ const hubSettings = reactive({
1030
+ desktopSettings,
1031
+ keyboardSettings,
1032
+ startMenuPins,
1033
+ startMenuFavorites,
1034
+ recentOpenLog,
1035
+ desktopApps: startMenuCatalogApps,
1036
+ activeTheme,
1037
+ showThemeToggle,
1038
+ setSnapToGrid: onSnapGridChange,
1039
+ setTheme: onThemeChange,
1040
+ saveKeyboardSettings: () => saveHubKeyboardSettings(keyboardSettings),
1041
+ getDesktopApps: () => startMenuCatalogApps.value,
1042
+ getRecentApps: () => resolveRecentApps(startMenuCatalogApps.value, recentOpenLog.value),
1043
+ isAppPinned: (appId) => isAppPinned(startMenuPins, appId),
1044
+ isAppFavorite: (appId) => isAppFavorite(startMenuFavorites, appId),
1045
+ setPinVisible: setAppPinVisible,
1046
+ setFavoriteVisible: setAppFavoriteVisible,
1047
+ toggleAppPin,
1048
+ toggleAppFavorite,
1049
+ })
1050
+
1051
+ provide(DESKTOP_HUB_SETTINGS_KEY, hubSettings)
1052
+
1053
+ function resolveDropPosition(x, y) {
1054
+ const layer = iconsLayerRef.value
1055
+ if (!layer) return snapPoint(x, y, desktopSettings.snapToGrid)
1056
+ const clamped = clampPointToLayer(x, y, layer.clientWidth, layer.clientHeight)
1057
+ return snapPoint(clamped.x, clamped.y, desktopSettings.snapToGrid)
1058
+ }
1059
+
1060
+ const desktopNotifications = createDesktopNotificationsState()
1061
+ provide(DESKTOP_NOTIFICATIONS_KEY, desktopNotifications)
1062
+
1063
+ const dropInstall = createDesktopDropInstall({
1064
+ getAppStore: () => appStore,
1065
+ getHostApi: () => getHostApiForApp(rootApp),
1066
+ onNotify: (payload) => desktopNotifications.push(payload),
1067
+ getLabels: () => ({
1068
+ errorGeneric: labels.value.drop_error,
1069
+ errorTitle: labels.value.notif_error_title,
1070
+ publishSuccess: labels.value.notif_publish_success,
1071
+ publishUpgradeSuccess: labels.value.notif_publish_upgrade_success,
1072
+ installCancelled: labels.value.notif_install_cancelled,
1073
+ }),
1074
+ async onInstalled(app, { x, y, method }) {
1075
+ const position = resolveDropPosition(x, y)
1076
+ return handleInstallUserApp(app, position, method)
1077
+ },
1078
+ onPersist: schedulePersist,
1079
+ })
1080
+
1081
+ function measureWorkArea() {
1082
+ const work = workAreaRef.value
1083
+ if (!work) return
1084
+ wm.setWorkArea?.({ width: work.clientWidth, height: work.clientHeight })
1085
+ wm.relayoutWindows?.()
1086
+ }
1087
+
1088
+ function isTypingTarget(target) {
1089
+ if (!target || !(target instanceof Element)) return false
1090
+ return !!target.closest('input, textarea, select, [contenteditable="true"]')
1091
+ }
1092
+
1093
+ function onDocumentKeyDown(event) {
1094
+ const direction = matchSnapShortcut(event, keyboardSettings)
1095
+ if (!direction) return
1096
+ if (isTypingTarget(event.target)) return
1097
+ if (!wm.state.activeId) return
1098
+
1099
+ event.preventDefault()
1100
+ wm.snapActiveWindow?.(wm.state.activeId, direction)
1101
+ schedulePersist()
1102
+ }
1103
+
1104
+ function pointerInIconsLayer(event) {
1105
+ const layer = iconsLayerRef.value ?? desktopRoot.value
1106
+ if (!layer) return resolveDropPosition(16, 16)
1107
+ const rect = layer.getBoundingClientRect()
1108
+ const rawX = event.clientX - rect.left - 44
1109
+ const rawY = event.clientY - rect.top - 44
1110
+ return resolveDropPosition(rawX, rawY)
1111
+ }
1112
+
1113
+ function onDesktopDragEnter(event) {
1114
+ if (!isMainScreen.value) return
1115
+ dropInstall.onDragEnter(event, false)
1116
+ }
1117
+
1118
+ function onDesktopDragOver(event) {
1119
+ if (!isMainScreen.value) return
1120
+ dropInstall.onDragOver(event, false)
1121
+ }
1122
+
1123
+ function onDesktopDragLeave(event) {
1124
+ if (!isMainScreen.value) return
1125
+ dropInstall.onDragLeave(event, false)
1126
+ }
1127
+
1128
+ async function onDesktopDrop(event) {
1129
+ if (!isMainScreen.value) return
1130
+ await dropInstall.onDrop(event, false, pointerInIconsLayer(event))
1131
+ }
1132
+
1133
+ function onWindowDragEnd() {
1134
+ dropInstall.resetDrag()
1135
+ }
1136
+
1137
+ function methodLabel(job) {
1138
+ if (job.method === 'publish') return labels.value.drop_method_publish
1139
+ if (job.method === 'appstore') return labels.value.drop_method_appstore
1140
+ return labels.value.drop_method_local
1141
+ }
1142
+
1143
+ function touchRecentApp(appId) {
1144
+ if (!appId) return
1145
+ recentOpenLog.value = recordRecentApp(appId, recentOpenLog.value)
1146
+ }
1147
+
1148
+ function resolveAppIdFromWindow(win) {
1149
+ if (!win?.id || typeof win.id !== 'string') return null
1150
+ return win.id.startsWith('win-') ? win.id.slice(4) : null
1151
+ }
1152
+
1153
+ function onOpenIcon(app) {
1154
+ measureWorkArea()
1155
+ shell.openApp(app, wm)
1156
+ schedulePersist()
1157
+ }
1158
+
1159
+ const initialSlugOpened = ref(false)
1160
+
1161
+ async function ensureCatalogItemForSlug(slug) {
1162
+ let item = appStore.findCatalogItem(slug)
1163
+ if (item) return item
1164
+
1165
+ const api = getHostApiForApp(rootApp)
1166
+ if (!api?.apps || !isBackendReadyForApp(rootApp)) return null
1167
+
1168
+ const loadOpts = { backendReady: true, perPage: 48 }
1169
+ await appStore.loadCatalog(api, { ...loadOpts, mode: CATALOG_MODE_STORE })
1170
+ item = appStore.findCatalogItem(slug)
1171
+ if (item) return item
1172
+
1173
+ await appStore.loadCatalog(api, { ...loadOpts, mode: CATALOG_MODE_DRAFT })
1174
+ return appStore.findCatalogItem(slug)
1175
+ }
1176
+
1177
+ async function tryOpenAppBySlug(slug) {
1178
+ const normalized = String(slug ?? '').trim()
1179
+ if (!normalized) return false
1180
+
1181
+ let app = shell.findUserAppBySlug(normalized)
1182
+ if (!app) {
1183
+ const item = await ensureCatalogItemForSlug(normalized)
1184
+ if (item) {
1185
+ appStore.installApp(normalized)
1186
+ shell.onUserAppInstalled(item)
1187
+ app = shell.findUserAppBySlug(normalized)
1188
+ }
1189
+ }
1190
+
1191
+ if (app) {
1192
+ onOpenIcon(app)
1193
+ return true
1194
+ }
1195
+ return false
1196
+ }
1197
+
1198
+ async function tryInitialOpenSlug() {
1199
+ if (initialSlugOpened.value || !props.initialOpenSlug || !moduleOptions?.hasToken) return
1200
+ const ok = await tryOpenAppBySlug(props.initialOpenSlug)
1201
+ if (ok) initialSlugOpened.value = true
1202
+ }
1203
+
1204
+ watch(
1205
+ () => moduleOptions?.hasToken,
1206
+ (hasToken) => {
1207
+ if (hasToken) tryInitialOpenSlug()
1208
+ },
1209
+ )
1210
+
1211
+ function onDesktopClick(event) {
1212
+ if (event.target.closest('.apphub-icon-folder')) return
1213
+ shell.state.startOpen = false
1214
+ closeIconContextMenu()
1215
+ closeOpenFolder()
1216
+ }
1217
+
1218
+ function closeIconContextMenu() {
1219
+ iconContextMenu.open = false
1220
+ iconContextMenu.app = null
1221
+ iconContextMenu.group = null
1222
+ }
1223
+
1224
+ function onIconContextMenu(app, event) {
1225
+ event.preventDefault()
1226
+ event.stopPropagation()
1227
+ if (iconDrag.isDragging(app.id)) return
1228
+
1229
+ const layer = iconsLayerRef.value
1230
+ if (!layer) return
1231
+ const rect = layer.getBoundingClientRect()
1232
+ const menuW = 220
1233
+ const menuH = app.builtin ? 176 : 220
1234
+ let x = event.clientX - rect.left
1235
+ let y = event.clientY - rect.top
1236
+ x = Math.max(4, Math.min(x, rect.width - menuW - 4))
1237
+ y = Math.max(4, Math.min(y, rect.height - menuH - 4))
1238
+
1239
+ iconContextMenu.group = null
1240
+ iconContextMenu.app = app
1241
+ iconContextMenu.x = x
1242
+ iconContextMenu.y = y
1243
+ iconContextMenu.open = true
1244
+ }
1245
+
1246
+ function onContextMenuOpen() {
1247
+ const group = iconContextMenu.group
1248
+ const app = iconContextMenu.app
1249
+ closeIconContextMenu()
1250
+ if (group) {
1251
+ openGroupFolder(group)
1252
+ return
1253
+ }
1254
+ if (app) onOpenIcon(app)
1255
+ }
1256
+
1257
+ function onContextMenuPin() {
1258
+ const app = iconContextMenu.app
1259
+ closeIconContextMenu()
1260
+ if (!app?.id) return
1261
+ toggleAppPin(app.id)
1262
+ }
1263
+
1264
+ function onContextMenuUninstall() {
1265
+ const app = iconContextMenu.app
1266
+ closeIconContextMenu()
1267
+ if (!app) return
1268
+ handleUninstallUserApp(app)
1269
+ }
1270
+
1271
+ function onContextMenuFavorite() {
1272
+ const app = iconContextMenu.app
1273
+ closeIconContextMenu()
1274
+ if (!app?.id) return
1275
+ toggleAppFavorite(app.id)
1276
+ }
1277
+
1278
+ function onContextMenuRename() {
1279
+ const group = iconContextMenu.group
1280
+ const app = iconContextMenu.app
1281
+ closeIconContextMenu()
1282
+ if (group) {
1283
+ iconRenameDialog.group = group
1284
+ iconRenameDialog.app = null
1285
+ iconRenameDialog.error = ''
1286
+ iconRenameDialog.open = true
1287
+ return
1288
+ }
1289
+ if (!app || app.builtin) return
1290
+ iconRenameDialog.app = app
1291
+ iconRenameDialog.group = null
1292
+ iconRenameDialog.error = ''
1293
+ iconRenameDialog.open = true
1294
+ }
1295
+
1296
+ function onContextMenuInfo() {
1297
+ const group = iconContextMenu.group
1298
+ const app = iconContextMenu.app
1299
+ closeIconContextMenu()
1300
+ if (group) {
1301
+ iconInfoDialog.group = group
1302
+ iconInfoDialog.app = null
1303
+ iconInfoDialog.open = true
1304
+ return
1305
+ }
1306
+ if (!app) return
1307
+ iconInfoDialog.app = app
1308
+ iconInfoDialog.group = null
1309
+ iconInfoDialog.open = true
1310
+ }
1311
+
1312
+ function formatAppCreatedAt(iso) {
1313
+ if (!iso) return labels.value.icon_info_date_unknown
1314
+ try {
1315
+ const locale = lang.value === 'vi' ? 'vi-VN' : undefined
1316
+ return new Date(iso).toLocaleString(locale)
1317
+ } catch {
1318
+ return labels.value.icon_info_date_unknown
1319
+ }
1320
+ }
1321
+
1322
+ function resolveAppInstallSource(app) {
1323
+ const method = app.installMethod ?? (app.local ? 'local' : 'appstore')
1324
+ return method === 'local'
1325
+ ? labels.value.icon_info_source_local
1326
+ : labels.value.icon_info_source_appstore
1327
+ }
1328
+
1329
+ function syncAppWindowTitle(app) {
1330
+ const win = wm.state.windows.find((w) => w.id === `win-${app.id}`)
1331
+ if (win) win.title = app.name
1332
+ }
1333
+
1334
+ function onIconRenameSave(name) {
1335
+ const group = iconRenameDialog.group
1336
+ if (group) {
1337
+ const trimmed = String(name ?? '').trim()
1338
+ if (!trimmed) {
1339
+ iconRenameDialog.error = labels.value.icon_rename_error_empty
1340
+ return
1341
+ }
1342
+ setGroupDisplayName(
1343
+ desktopSettings,
1344
+ group.x,
1345
+ group.y,
1346
+ trimmed,
1347
+ labels.value,
1348
+ group.apps.length,
1349
+ )
1350
+ if (openFolder.open && openFolder.x === group.x && openFolder.y === group.y) {
1351
+ openFolder.title = groupLabel(group.apps, group.x, group.y)
1352
+ }
1353
+ iconRenameDialog.open = false
1354
+ iconRenameDialog.group = null
1355
+ iconRenameDialog.error = ''
1356
+ schedulePersist()
1357
+ return
1358
+ }
1359
+
1360
+ const app = iconRenameDialog.app
1361
+ if (!app) return
1362
+ const result = shell.renameUserApp(app.id, name)
1363
+ if (!result.ok) {
1364
+ iconRenameDialog.error =
1365
+ result.error === 'duplicate'
1366
+ ? labels.value.icon_rename_error_duplicate
1367
+ : labels.value.icon_rename_error_empty
1368
+ return
1369
+ }
1370
+ syncAppWindowTitle(result.app)
1371
+ iconRenameDialog.open = false
1372
+ iconRenameDialog.app = null
1373
+ iconRenameDialog.group = null
1374
+ iconRenameDialog.error = ''
1375
+ schedulePersist()
1376
+ }
1377
+
1378
+ function onDocumentPointerDown(event) {
1379
+ if (!iconContextMenu.open) return
1380
+ const root = desktopRoot.value
1381
+ if (root?.querySelector('.apphub-icon-menu')?.contains(event.target)) return
1382
+ closeIconContextMenu()
1383
+ }
1384
+
1385
+ function onToggleStart() {
1386
+ closeOpenFolder()
1387
+ shell.state.startOpen = !shell.state.startOpen
1388
+ }
1389
+
1390
+ function onStartMenuOpenApp(app) {
1391
+ shell.state.startOpen = false
1392
+ onOpenIcon(app)
1393
+ }
1394
+
1395
+ function onOpenAppStore() {
1396
+ shell.state.startOpen = false
1397
+ const app = iconList.find((a) => a.builtin && a.module === 'app-store')
1398
+ if (app) onOpenIcon(app)
1399
+ else {
1400
+ measureWorkArea()
1401
+ shell.openBuiltinAppStore(wm)
1402
+ schedulePersist()
1403
+ }
1404
+ }
1405
+
1406
+ function onTaskClick(win) {
1407
+ if (win.minimized) {
1408
+ touchRecentApp(resolveAppIdFromWindow(win))
1409
+ wm.focusWindow(win.id)
1410
+ } else if (wm.state.activeId === win.id) {
1411
+ wm.minimizeWindow(win.id)
1412
+ } else {
1413
+ touchRecentApp(resolveAppIdFromWindow(win))
1414
+ wm.focusWindow(win.id)
1415
+ }
1416
+ schedulePersist()
1417
+ }
1418
+
1419
+ function restoreInstalledApps(slugs) {
1420
+ if (!Array.isArray(slugs)) return
1421
+ slugs.forEach((slug) => appStore.installApp(slug))
1422
+ }
1423
+
1424
+ watch(() => wm.state.windows, () => schedulePersist(), { deep: true })
1425
+ watch(() => shell.state.userApps, () => schedulePersist(), { deep: true })
1426
+ watch(() => appStore.state.installedSlugs, () => schedulePersist(), { deep: true })
1427
+
1428
+ function onDocumentDragOver(event) {
1429
+ if (!isMainScreen.value) return
1430
+ dropInstall.onDragOver(event, false)
1431
+ }
1432
+
1433
+ onMounted(async () => {
1434
+ if (!desktopReady.value) return
1435
+ await initDesktopShell()
1436
+ })
1437
+
1438
+ watch(desktopReady, async (ready) => {
1439
+ if (ready) await initDesktopShell()
1440
+ })
1441
+
1442
+ async function initDesktopShell() {
1443
+ if (initDesktopShell.done) return
1444
+ initDesktopShell.done = true
1445
+
1446
+ shell.tickClock()
1447
+ clockTimer = setInterval(shell.tickClock, 30_000)
1448
+ window.addEventListener('beforeunload', persistSession)
1449
+ window.addEventListener('dragend', onWindowDragEnd)
1450
+ document.addEventListener('dragover', onDocumentDragOver)
1451
+ document.addEventListener('mousedown', onDocumentPointerDown)
1452
+ document.addEventListener('keydown', onDocumentKeyDown)
1453
+
1454
+ await nextTick()
1455
+ measureWorkArea()
1456
+
1457
+ if (typeof ResizeObserver !== 'undefined' && desktopRoot.value) {
1458
+ resizeObserver = new ResizeObserver(() => measureWorkArea())
1459
+ resizeObserver.observe(desktopRoot.value)
1460
+ } else {
1461
+ window.addEventListener('resize', measureWorkArea)
1462
+ }
1463
+
1464
+ const session = loadDesktopSession()
1465
+ if (session) {
1466
+ if (session.settings) applyDesktopSettings(desktopSettings, session.settings)
1467
+ syncBuiltinPlacementsFromSettings()
1468
+ restoreInstalledApps(session.installedSlugs)
1469
+ shell.restoreSession(session, wm)
1470
+ ensureBuiltinPositions()
1471
+ assignDefaultIconPositions()
1472
+ schedulePersist()
1473
+ await tryInitialOpenSlug()
1474
+ return
1475
+ }
1476
+
1477
+ ensureBuiltinPositions()
1478
+ assignDefaultIconPositions()
1479
+
1480
+ if (props.initialOpenSlug) {
1481
+ await tryInitialOpenSlug()
1482
+ return
1483
+ }
1484
+
1485
+ if (props.openAppStoreOnMount) {
1486
+ const app = iconList.find((a) => a.builtin && a.module === 'app-store')
1487
+ if (app) onOpenIcon(app)
1488
+ else {
1489
+ shell.openBuiltinAppStore(wm)
1490
+ schedulePersist()
1491
+ }
1492
+ }
1493
+ }
1494
+
1495
+ onUnmounted(() => {
1496
+ if (!initDesktopShell.done) return
1497
+
1498
+ if (clockTimer) clearInterval(clockTimer)
1499
+ if (persistTimer) clearTimeout(persistTimer)
1500
+ iconDrag.cleanup()
1501
+ resizeObserver?.disconnect()
1502
+ window.removeEventListener('resize', measureWorkArea)
1503
+ window.removeEventListener('beforeunload', persistSession)
1504
+ window.removeEventListener('dragend', onWindowDragEnd)
1505
+ document.removeEventListener('dragover', onDocumentDragOver)
1506
+ document.removeEventListener('mousedown', onDocumentPointerDown)
1507
+ document.removeEventListener('keydown', onDocumentKeyDown)
1508
+ persistSession()
1509
+ })
1510
+ </script>