@kennofizet/apphub-frontend 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +84 -0
- package/package.json +31 -0
- package/src/api/coreApi.js +25 -0
- package/src/api/index.js +80 -0
- package/src/composables/createZoneContext.js +156 -0
- package/src/composables/useAppHubHostApi.js +24 -0
- package/src/composables/useAppHubZoneContext.js +11 -0
- package/src/composables/useDevOriginToggle.js +40 -0
- package/src/i18n/index.js +16 -0
- package/src/i18n/resolveLang.js +6 -0
- package/src/i18n/resolveTheme.js +30 -0
- package/src/i18n/translations/en.js +303 -0
- package/src/i18n/translations/vi.js +302 -0
- package/src/index.js +427 -0
- package/src/moduleStore.js +10 -0
- package/src/modules/app-store/components/AppHubAppStoreApp.vue +210 -0
- package/src/modules/app-store/components/AppHubAppStoreCard.vue +88 -0
- package/src/modules/app-store/components/AppHubAppStoreSettingsPanel.vue +266 -0
- package/src/modules/app-store/components/AppHubAppVersionHistory.vue +77 -0
- package/src/modules/app-store/components/AppHubDevReviewPanel.vue +206 -0
- package/src/modules/app-store/components/AppHubDraftStoreApp.vue +184 -0
- package/src/modules/app-store/components/AppHubDraftStoreCard.vue +116 -0
- package/src/modules/app-store/composables/useAppStore.js +206 -0
- package/src/modules/app-store/composables/useCatalogInfiniteScroll.js +47 -0
- package/src/modules/app-store/constants/catalogModes.js +2 -0
- package/src/modules/app-store/data/defaultCatalog.js +19 -0
- package/src/modules/app-store/index.js +9 -0
- package/src/modules/app-store/utils/normalizeCatalogApp.js +37 -0
- package/src/modules/desktop/components/AppHubDesktop.vue +1510 -0
- package/src/modules/desktop/components/AppHubDesktopDevOriginBar.vue +57 -0
- package/src/modules/desktop/components/AppHubDesktopDropLayer.vue +15 -0
- package/src/modules/desktop/components/AppHubDesktopDropTarget.vue +32 -0
- package/src/modules/desktop/components/AppHubDesktopIconContextMenu.vue +74 -0
- package/src/modules/desktop/components/AppHubDesktopIconFolder.vue +60 -0
- package/src/modules/desktop/components/AppHubDesktopIconGroup.vue +58 -0
- package/src/modules/desktop/components/AppHubDesktopIconInfoDialog.vue +33 -0
- package/src/modules/desktop/components/AppHubDesktopIconRenameDialog.vue +62 -0
- package/src/modules/desktop/components/AppHubDesktopSettings.vue +28 -0
- package/src/modules/desktop/components/AppHubDropInstallBadge.vue +65 -0
- package/src/modules/desktop/components/AppHubDuplicateAppDialog.vue +38 -0
- package/src/modules/desktop/components/AppHubGuideApp.vue +278 -0
- package/src/modules/desktop/components/AppHubOriginBlockScreen.vue +105 -0
- package/src/modules/desktop/components/AppHubOriginLoadingScreen.vue +23 -0
- package/src/modules/desktop/components/AppHubPlaceholderApp.vue +14 -0
- package/src/modules/desktop/components/AppHubSettingsApp.vue +319 -0
- package/src/modules/desktop/components/AppHubStartButton.vue +24 -0
- package/src/modules/desktop/components/AppHubStartMenu.vue +182 -0
- package/src/modules/desktop/components/AppHubTaskbarPins.vue +23 -0
- package/src/modules/desktop/components/settings/AppHubSettingsKeyboardPanel.vue +82 -0
- package/src/modules/desktop/components/settings/AppHubSettingsScreenPanel.vue +41 -0
- package/src/modules/desktop/components/settings/AppHubSettingsStartMenuPanel.vue +95 -0
- package/src/modules/desktop/composables/simulateInstallProgress.js +15 -0
- package/src/modules/desktop/composables/useDesktopDropInstall.js +272 -0
- package/src/modules/desktop/composables/useDesktopHubSettings.js +51 -0
- package/src/modules/desktop/composables/useDesktopIconDrag.js +207 -0
- package/src/modules/desktop/composables/useDesktopShell.js +335 -0
- package/src/modules/desktop/data/builtinApps.js +77 -0
- package/src/modules/desktop/index.js +12 -0
- package/src/modules/desktop/styles/desktop.css +3104 -0
- package/src/modules/desktop/styles/theme.css +616 -0
- package/src/modules/desktop/utils/desktopGrid.js +43 -0
- package/src/modules/desktop/utils/desktopIconGroups.js +103 -0
- package/src/modules/desktop/utils/desktopSession.js +40 -0
- package/src/modules/desktop/utils/desktopSettings.js +37 -0
- package/src/modules/desktop/utils/dropPackageParser.js +140 -0
- package/src/modules/desktop/utils/duplicateAppUtils.js +28 -0
- package/src/modules/desktop/utils/hubKeyboardSettings.js +63 -0
- package/src/modules/desktop/utils/recentApps.js +148 -0
- package/src/modules/desktop/utils/startMenuFavorites.js +100 -0
- package/src/modules/desktop/utils/startMenuPins.js +90 -0
- package/src/modules/notifications/components/AppHubDesktopNotifications.vue +54 -0
- package/src/modules/notifications/composables/createDesktopNotifications.js +86 -0
- package/src/modules/notifications/index.js +9 -0
- package/src/modules/notifications/styles/notifications.css +118 -0
- package/src/modules/notifications/utils/parseApiError.js +29 -0
- package/src/modules/runner/components/AppHubRunner.vue +292 -0
- package/src/modules/runner/index.js +1 -0
- package/src/modules/window-manager/components/AppHubWindowFrame.vue +224 -0
- package/src/modules/window-manager/composables/useWindowManager.js +652 -0
- package/src/modules/window-manager/index.js +7 -0
- package/src/modules/window-manager/utils/sessionLayout.js +28 -0
- package/src/modules/window-manager/utils/windowLayout.js +236 -0
- package/src/modules/window-manager/utils/windowSnap.js +146 -0
- package/src/utils/bootstrapCache.js +47 -0
- package/src/utils/devOriginSettings.js +22 -0
- package/src/utils/launchUrl.js +111 -0
- package/src/utils/originSafety.js +267 -0
- package/src/utils/safeStorage.js +191 -0
- package/src/utils/semver.js +30 -0
- package/src/utils/zoneContext.js +38 -0
|
@@ -0,0 +1,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>
|