@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,95 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="apphub-settings-panel">
|
|
3
|
+
<h3 class="apphub-settings-panel__title">{{ labels.page_title }}</h3>
|
|
4
|
+
<p class="apphub-settings-panel__hint">{{ labels.page_hint }}</p>
|
|
5
|
+
|
|
6
|
+
<p class="apphub-settings-panel__section-label">{{ labels.pins_section }}</p>
|
|
7
|
+
<p v-if="!pinnedRows.length" class="apphub-settings-panel__msg">{{ labels.pins_empty }}</p>
|
|
8
|
+
|
|
9
|
+
<ul v-else class="apphub-settings-panel__pin-list">
|
|
10
|
+
<li v-for="row in pinnedRows" :key="`pin-${row.id}`" class="apphub-settings-panel__pin-item">
|
|
11
|
+
<span class="apphub-settings-panel__pin-icon" aria-hidden="true">{{ row.icon }}</span>
|
|
12
|
+
<span class="apphub-settings-panel__pin-name">{{ row.name }}</span>
|
|
13
|
+
<label class="apphub-settings-panel__pin-toggle">
|
|
14
|
+
<input
|
|
15
|
+
type="checkbox"
|
|
16
|
+
:checked="row.visible"
|
|
17
|
+
@change="onPinVisibleChange(row.id, $event.target.checked)"
|
|
18
|
+
/>
|
|
19
|
+
<span>{{ labels.pins_show }}</span>
|
|
20
|
+
</label>
|
|
21
|
+
</li>
|
|
22
|
+
</ul>
|
|
23
|
+
|
|
24
|
+
<p class="apphub-settings-panel__section-label apphub-settings-panel__section-label--spaced">{{ labels.favorites_section }}</p>
|
|
25
|
+
<p v-if="!favoriteRows.length" class="apphub-settings-panel__msg">{{ labels.favorites_empty }}</p>
|
|
26
|
+
|
|
27
|
+
<ul v-else class="apphub-settings-panel__pin-list">
|
|
28
|
+
<li v-for="row in favoriteRows" :key="`fav-${row.id}`" class="apphub-settings-panel__pin-item">
|
|
29
|
+
<span class="apphub-settings-panel__pin-icon" aria-hidden="true">{{ row.icon }}</span>
|
|
30
|
+
<span class="apphub-settings-panel__pin-name">{{ row.name }}</span>
|
|
31
|
+
<label class="apphub-settings-panel__pin-toggle">
|
|
32
|
+
<input
|
|
33
|
+
type="checkbox"
|
|
34
|
+
:checked="row.visible"
|
|
35
|
+
@change="onFavoriteVisibleChange(row.id, $event.target.checked)"
|
|
36
|
+
/>
|
|
37
|
+
<span>{{ labels.favorites_show }}</span>
|
|
38
|
+
</label>
|
|
39
|
+
</li>
|
|
40
|
+
</ul>
|
|
41
|
+
|
|
42
|
+
<p class="apphub-settings-panel__note">{{ labels.page_note }}</p>
|
|
43
|
+
</div>
|
|
44
|
+
</template>
|
|
45
|
+
|
|
46
|
+
<script setup>
|
|
47
|
+
import { computed, inject } from 'vue'
|
|
48
|
+
import { t } from '../../../../i18n/index.js'
|
|
49
|
+
import { resolveLang } from '../../../../i18n/resolveLang.js'
|
|
50
|
+
import {
|
|
51
|
+
useDesktopHubSettings,
|
|
52
|
+
useHubFavoriteRows,
|
|
53
|
+
useHubPinnedRows,
|
|
54
|
+
} from '../../composables/useDesktopHubSettings.js'
|
|
55
|
+
|
|
56
|
+
const hub = useDesktopHubSettings()
|
|
57
|
+
const pinnedRows = useHubPinnedRows(hub)
|
|
58
|
+
const favoriteRows = useHubFavoriteRows(hub)
|
|
59
|
+
const lang = computed(() => resolveLang(inject('apphubOptions', {})?.language, 'vi'))
|
|
60
|
+
|
|
61
|
+
const labels = computed(() => ({
|
|
62
|
+
page_title: t('hub_settings_pin_favorite_title', lang.value),
|
|
63
|
+
page_hint: t('hub_settings_pin_favorite_hint', lang.value),
|
|
64
|
+
pins_section: t('hub_settings_pins_section', lang.value),
|
|
65
|
+
pins_empty: t('hub_settings_start_empty', lang.value),
|
|
66
|
+
pins_show: t('hub_settings_start_show', lang.value),
|
|
67
|
+
favorites_section: t('start_menu_favorites', lang.value),
|
|
68
|
+
favorites_empty: t('hub_settings_start_favorites_empty', lang.value),
|
|
69
|
+
favorites_show: t('hub_settings_start_favorites_show', lang.value),
|
|
70
|
+
page_note: t('hub_settings_pin_favorite_note', lang.value),
|
|
71
|
+
}))
|
|
72
|
+
|
|
73
|
+
function onPinVisibleChange(appId, visible) {
|
|
74
|
+
hub.setPinVisible?.(appId, visible)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function onFavoriteVisibleChange(appId, visible) {
|
|
78
|
+
hub.setFavoriteVisible?.(appId, visible)
|
|
79
|
+
}
|
|
80
|
+
</script>
|
|
81
|
+
|
|
82
|
+
<style scoped>
|
|
83
|
+
.apphub-settings-panel__section-label {
|
|
84
|
+
margin: 20px 0 10px;
|
|
85
|
+
font-size: 0.7rem;
|
|
86
|
+
font-weight: 600;
|
|
87
|
+
letter-spacing: 0.06em;
|
|
88
|
+
text-transform: uppercase;
|
|
89
|
+
color: var(--ah-text-muted, rgba(255, 255, 255, 0.38));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.apphub-settings-panel__section-label--spaced {
|
|
93
|
+
margin-top: 28px;
|
|
94
|
+
}
|
|
95
|
+
</style>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export function simulateInstallProgress(job, onTick) {
|
|
2
|
+
const steps = job.method === 'appstore' ? 28 : 22
|
|
3
|
+
const delay = job.method === 'appstore' ? 45 : 55
|
|
4
|
+
return new Promise((resolve) => {
|
|
5
|
+
let step = 0
|
|
6
|
+
const timer = setInterval(() => {
|
|
7
|
+
step += 1
|
|
8
|
+
onTick(Math.min(100, Math.round((step / steps) * 100)))
|
|
9
|
+
if (step >= steps) {
|
|
10
|
+
clearInterval(timer)
|
|
11
|
+
resolve()
|
|
12
|
+
}
|
|
13
|
+
}, delay)
|
|
14
|
+
})
|
|
15
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { reactive } from 'vue'
|
|
2
|
+
import { defaultAppStoreCatalog } from '../../app-store/data/defaultCatalog.js'
|
|
3
|
+
import { normalizeCatalogApp } from '../../app-store/utils/normalizeCatalogApp.js'
|
|
4
|
+
import { parseApiError } from '../../notifications/utils/parseApiError.js'
|
|
5
|
+
import { parseDropFiles } from '../utils/dropPackageParser.js'
|
|
6
|
+
import { simulateInstallProgress } from './simulateInstallProgress.js'
|
|
7
|
+
|
|
8
|
+
let jobSeq = 0
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Desktop drag-drop — local install, app store slug, or hosted publish (zip → draft API).
|
|
12
|
+
*/
|
|
13
|
+
export function createDesktopDropInstall(options = {}) {
|
|
14
|
+
const onInstalled = options.onInstalled ?? (() => {})
|
|
15
|
+
const onPersist = options.onPersist ?? (() => {})
|
|
16
|
+
const onNotify = options.onNotify ?? (() => {})
|
|
17
|
+
const getLabels = options.getLabels ?? (() => ({}))
|
|
18
|
+
const getAppStore = options.getAppStore ?? (() => null)
|
|
19
|
+
const getHostApi = options.getHostApi ?? (() => null)
|
|
20
|
+
|
|
21
|
+
const state = reactive({
|
|
22
|
+
dragActive: false,
|
|
23
|
+
dragDepth: 0,
|
|
24
|
+
jobs: [],
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
function label(key, fallback = '') {
|
|
28
|
+
const labels = getLabels()
|
|
29
|
+
return labels[key] ?? fallback
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function notify(payload) {
|
|
33
|
+
onNotify(payload)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function failJob(job, err) {
|
|
37
|
+
const message = parseApiError(err, label('errorGeneric', 'Something went wrong.'))
|
|
38
|
+
job.status = 'error'
|
|
39
|
+
job.errorMessage = message
|
|
40
|
+
notify({
|
|
41
|
+
type: 'error',
|
|
42
|
+
title: job.name || label('errorTitle', 'App Hub'),
|
|
43
|
+
message,
|
|
44
|
+
})
|
|
45
|
+
setTimeout(() => {
|
|
46
|
+
const idx = state.jobs.findIndex((j) => j.id === job.id)
|
|
47
|
+
if (idx !== -1) state.jobs.splice(idx, 1)
|
|
48
|
+
}, 2000)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function canAcceptDrop(hasOpenWindows) {
|
|
52
|
+
return !hasOpenWindows
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function onDragEnter(event, hasOpenWindows) {
|
|
56
|
+
if (!canAcceptDrop(hasOpenWindows) || !hasFiles(event)) return
|
|
57
|
+
event.preventDefault()
|
|
58
|
+
state.dragDepth += 1
|
|
59
|
+
state.dragActive = true
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function onDragLeave(event, hasOpenWindows) {
|
|
63
|
+
if (!canAcceptDrop(hasOpenWindows)) return
|
|
64
|
+
const root = event.currentTarget
|
|
65
|
+
if (event.relatedTarget && root?.contains?.(event.relatedTarget)) return
|
|
66
|
+
state.dragDepth = Math.max(0, state.dragDepth - 1)
|
|
67
|
+
if (state.dragDepth === 0) state.dragActive = false
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function resetDrag() {
|
|
71
|
+
state.dragDepth = 0
|
|
72
|
+
state.dragActive = false
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function onDragOver(event, hasOpenWindows) {
|
|
76
|
+
if (!canAcceptDrop(hasOpenWindows) || !hasFiles(event)) return
|
|
77
|
+
event.preventDefault()
|
|
78
|
+
if (event.dataTransfer) event.dataTransfer.dropEffect = 'copy'
|
|
79
|
+
state.dragActive = true
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function onDrop(event, hasOpenWindows, pointer) {
|
|
83
|
+
state.dragDepth = 0
|
|
84
|
+
state.dragActive = false
|
|
85
|
+
if (!canAcceptDrop(hasOpenWindows)) return
|
|
86
|
+
event.preventDefault()
|
|
87
|
+
|
|
88
|
+
const intent = await parseDropFiles(event.dataTransfer)
|
|
89
|
+
if (!intent) return
|
|
90
|
+
|
|
91
|
+
const job = {
|
|
92
|
+
id: `drop-${++jobSeq}`,
|
|
93
|
+
x: pointer.x,
|
|
94
|
+
y: pointer.y,
|
|
95
|
+
progress: 0,
|
|
96
|
+
status: 'installing',
|
|
97
|
+
icon: intent.icon,
|
|
98
|
+
name: intent.name,
|
|
99
|
+
method: intent.method,
|
|
100
|
+
intent,
|
|
101
|
+
}
|
|
102
|
+
state.jobs.push(job)
|
|
103
|
+
await runInstall(job)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function submitHostedPublish(job) {
|
|
107
|
+
const api = getHostApi()
|
|
108
|
+
if (!api?.registerApp) {
|
|
109
|
+
const err = new Error('no_api')
|
|
110
|
+
err.code = 'no_api'
|
|
111
|
+
throw err
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const body = new FormData()
|
|
115
|
+
body.append('bundle', job.intent.zipFile)
|
|
116
|
+
|
|
117
|
+
const res = await api.registerApp(body)
|
|
118
|
+
return res?.data?.data ?? null
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async function runInstall(job) {
|
|
122
|
+
const appStore = getAppStore()
|
|
123
|
+
try {
|
|
124
|
+
if (job.method === 'publish') {
|
|
125
|
+
await simulateInstallProgress(job, (value) => {
|
|
126
|
+
job.progress = value
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
const registered = await submitHostedPublish(job)
|
|
130
|
+
const catalogApp = normalizeCatalogApp(registered) ?? {
|
|
131
|
+
slug: registered?.slug ?? job.intent.slug,
|
|
132
|
+
name: registered?.name ?? job.intent.name,
|
|
133
|
+
icon: registered?.icon ?? job.intent.icon,
|
|
134
|
+
description: job.intent.description ?? '',
|
|
135
|
+
status: registered?.status ?? 'draft',
|
|
136
|
+
runtime_type: registered?.runtime_type ?? 'hosted',
|
|
137
|
+
entry_url: null,
|
|
138
|
+
healthcheck_url: null,
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
appStore?.installApp?.(catalogApp.slug)
|
|
142
|
+
|
|
143
|
+
const result = await onInstalled(
|
|
144
|
+
{
|
|
145
|
+
slug: catalogApp.slug,
|
|
146
|
+
name: catalogApp.name,
|
|
147
|
+
icon: catalogApp.icon,
|
|
148
|
+
description: catalogApp.description,
|
|
149
|
+
status: catalogApp.status,
|
|
150
|
+
runtime_type: catalogApp.runtime_type,
|
|
151
|
+
version: catalogApp.version,
|
|
152
|
+
entry_url: catalogApp.entry_url,
|
|
153
|
+
healthcheck_url: catalogApp.healthcheck_url,
|
|
154
|
+
},
|
|
155
|
+
{ x: job.x, y: job.y, method: 'publish' },
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
if (result === 'cancelled') {
|
|
159
|
+
const message = label('installCancelled', 'Install cancelled.')
|
|
160
|
+
job.status = 'error'
|
|
161
|
+
job.errorMessage = message
|
|
162
|
+
notify({
|
|
163
|
+
type: 'warning',
|
|
164
|
+
title: catalogApp.name,
|
|
165
|
+
message,
|
|
166
|
+
})
|
|
167
|
+
setTimeout(() => {
|
|
168
|
+
const idx = state.jobs.findIndex((j) => j.id === job.id)
|
|
169
|
+
if (idx !== -1) state.jobs.splice(idx, 1)
|
|
170
|
+
}, 800)
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
job.status = 'done'
|
|
175
|
+
job.progress = 100
|
|
176
|
+
job.name = catalogApp.name
|
|
177
|
+
job.icon = catalogApp.icon
|
|
178
|
+
job.publishSubmitted = true
|
|
179
|
+
|
|
180
|
+
notify({
|
|
181
|
+
type: 'success',
|
|
182
|
+
title: catalogApp.name,
|
|
183
|
+
message: result === 'updated'
|
|
184
|
+
? label('publishUpgradeSuccess', 'New version submitted. Update from App Store when you want to run it.')
|
|
185
|
+
: label('publishSuccess', 'Draft submitted and installed on your desktop.'),
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
setTimeout(() => {
|
|
189
|
+
const idx = state.jobs.findIndex((j) => j.id === job.id)
|
|
190
|
+
if (idx !== -1) state.jobs.splice(idx, 1)
|
|
191
|
+
onPersist()
|
|
192
|
+
}, 2200)
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
await simulateInstallProgress(job, (value) => {
|
|
197
|
+
job.progress = value
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
let app
|
|
201
|
+
if (job.method === 'appstore') {
|
|
202
|
+
const catalogItem = defaultAppStoreCatalog.find((a) => a.slug === job.intent.slug)
|
|
203
|
+
?? appStore?.findCatalogItem?.(job.intent.slug)
|
|
204
|
+
if (catalogItem) {
|
|
205
|
+
appStore?.installApp?.(catalogItem.slug)
|
|
206
|
+
app = catalogItem
|
|
207
|
+
} else {
|
|
208
|
+
app = {
|
|
209
|
+
slug: job.intent.slug,
|
|
210
|
+
name: job.intent.name,
|
|
211
|
+
icon: job.intent.icon,
|
|
212
|
+
description: job.intent.description ?? '',
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
} else {
|
|
216
|
+
app = {
|
|
217
|
+
slug: job.intent.slug,
|
|
218
|
+
name: job.intent.name,
|
|
219
|
+
icon: job.intent.icon,
|
|
220
|
+
description: job.intent.description ?? '',
|
|
221
|
+
local: true,
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
job.status = 'done'
|
|
226
|
+
job.progress = 100
|
|
227
|
+
job.name = app.name
|
|
228
|
+
job.icon = app.icon
|
|
229
|
+
|
|
230
|
+
const result = await onInstalled(app, { x: job.x, y: job.y, method: job.method })
|
|
231
|
+
if (result === 'cancelled') {
|
|
232
|
+
const message = label('installCancelled', 'Install cancelled.')
|
|
233
|
+
job.status = 'error'
|
|
234
|
+
job.errorMessage = message
|
|
235
|
+
notify({
|
|
236
|
+
type: 'warning',
|
|
237
|
+
title: app.name,
|
|
238
|
+
message,
|
|
239
|
+
})
|
|
240
|
+
setTimeout(() => {
|
|
241
|
+
const idx = state.jobs.findIndex((j) => j.id === job.id)
|
|
242
|
+
if (idx !== -1) state.jobs.splice(idx, 1)
|
|
243
|
+
}, 800)
|
|
244
|
+
return
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
setTimeout(() => {
|
|
248
|
+
const idx = state.jobs.findIndex((j) => j.id === job.id)
|
|
249
|
+
if (idx !== -1) state.jobs.splice(idx, 1)
|
|
250
|
+
onPersist()
|
|
251
|
+
}, 1200)
|
|
252
|
+
} catch (err) {
|
|
253
|
+
failJob(job, err)
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
state,
|
|
259
|
+
onDragEnter,
|
|
260
|
+
onDragLeave,
|
|
261
|
+
onDragOver,
|
|
262
|
+
onDrop,
|
|
263
|
+
canAcceptDrop,
|
|
264
|
+
resetDrag,
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function hasFiles(event) {
|
|
269
|
+
if (!event.dataTransfer) return false
|
|
270
|
+
const types = [...event.dataTransfer.types]
|
|
271
|
+
return types.includes('Files') || types.includes('application/x-moz-file')
|
|
272
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { computed, inject, toValue } from 'vue'
|
|
2
|
+
|
|
3
|
+
export const DESKTOP_HUB_SETTINGS_KEY = 'apphubDesktopHubSettings'
|
|
4
|
+
|
|
5
|
+
export function useDesktopHubSettings() {
|
|
6
|
+
const hub = inject(DESKTOP_HUB_SETTINGS_KEY, null)
|
|
7
|
+
if (!hub) {
|
|
8
|
+
throw new Error('useDesktopHubSettings() must be used inside AppHubDesktop')
|
|
9
|
+
}
|
|
10
|
+
return hub
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Catalog apps — reactive across install, rename, and session restore. */
|
|
14
|
+
export function useHubCatalogApps(hub = null) {
|
|
15
|
+
const ctx = hub ?? useDesktopHubSettings()
|
|
16
|
+
return computed(() => {
|
|
17
|
+
const apps = toValue(ctx.desktopApps)
|
|
18
|
+
if (Array.isArray(apps)) return apps
|
|
19
|
+
return ctx.getDesktopApps?.() ?? []
|
|
20
|
+
})
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function buildPinFavoriteRows(catalog, store, visibleKey = 'visible') {
|
|
24
|
+
const byId = new Map(catalog.map((app) => [app.id, app]))
|
|
25
|
+
return Object.keys(store)
|
|
26
|
+
.map((id) => {
|
|
27
|
+
const app = byId.get(id)
|
|
28
|
+
if (!app) return null
|
|
29
|
+
return {
|
|
30
|
+
id: app.id,
|
|
31
|
+
name: app.name,
|
|
32
|
+
icon: app.icon ?? '📦',
|
|
33
|
+
visible: store[id]?.[visibleKey] !== false,
|
|
34
|
+
}
|
|
35
|
+
})
|
|
36
|
+
.filter(Boolean)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Pinned apps for settings — tracks add/remove and visibility toggles live. */
|
|
40
|
+
export function useHubPinnedRows(hub = null) {
|
|
41
|
+
const ctx = hub ?? useDesktopHubSettings()
|
|
42
|
+
const catalog = useHubCatalogApps(ctx)
|
|
43
|
+
return computed(() => buildPinFavoriteRows(catalog.value, ctx.startMenuPins ?? {}))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Favorite apps for settings — tracks add/remove and visibility toggles live. */
|
|
47
|
+
export function useHubFavoriteRows(hub = null) {
|
|
48
|
+
const ctx = hub ?? useDesktopHubSettings()
|
|
49
|
+
const catalog = useHubCatalogApps(ctx)
|
|
50
|
+
return computed(() => buildPinFavoriteRows(catalog.value, ctx.startMenuFavorites ?? {}))
|
|
51
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { ref } from 'vue'
|
|
2
|
+
import { clampPointToLayer, snapPoint } from '../utils/desktopGrid.js'
|
|
3
|
+
import { findAppsAtCell, moveAppsToCell, previewGroupAtCell } from '../utils/desktopIconGroups.js'
|
|
4
|
+
|
|
5
|
+
const DRAG_THRESHOLD = 4
|
|
6
|
+
const HOLD_MS = 380
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Pointer drag for desktop app icons with iPhone-style grouping.
|
|
10
|
+
* Single icons drag immediately; group icons need a brief hold (tap opens folder).
|
|
11
|
+
*/
|
|
12
|
+
export function useDesktopIconDrag(options) {
|
|
13
|
+
const drag = ref(null)
|
|
14
|
+
const dropTarget = ref(null)
|
|
15
|
+
const lastWasDrag = ref(false)
|
|
16
|
+
|
|
17
|
+
let holdTimer = null
|
|
18
|
+
|
|
19
|
+
function getLayerRect() {
|
|
20
|
+
return options.getLayerEl()?.getBoundingClientRect() ?? null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function resolvePosition(x, y) {
|
|
24
|
+
const layer = options.getLayerEl()
|
|
25
|
+
if (!layer) return { x, y }
|
|
26
|
+
const clamped = clampPointToLayer(x, y, layer.clientWidth, layer.clientHeight)
|
|
27
|
+
return snapPoint(clamped.x, clamped.y, options.getSnapToGrid())
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getApps(ids) {
|
|
31
|
+
return ids.map((id) => options.findApp(id)).filter(Boolean)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function ensurePlaced(app, event) {
|
|
35
|
+
if (app.desktopX != null && app.desktopY != null) return
|
|
36
|
+
const layerRect = getLayerRect()
|
|
37
|
+
const el = event.currentTarget
|
|
38
|
+
if (!layerRect || !el) return
|
|
39
|
+
const rect = el.getBoundingClientRect()
|
|
40
|
+
const pos = resolvePosition(rect.left - layerRect.left, rect.top - layerRect.top)
|
|
41
|
+
app.desktopX = pos.x
|
|
42
|
+
app.desktopY = pos.y
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function clearHoldTimer() {
|
|
46
|
+
if (holdTimer) {
|
|
47
|
+
clearTimeout(holdTimer)
|
|
48
|
+
holdTimer = null
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function removeListeners() {
|
|
53
|
+
window.removeEventListener('mousemove', onPointerMove)
|
|
54
|
+
window.removeEventListener('mouseup', onPointerUp)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function updateDropTarget(pos, draggedApps) {
|
|
58
|
+
if (!drag.value?.moved) {
|
|
59
|
+
dropTarget.value = null
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const allApps = options.getDesktopApps?.() ?? []
|
|
64
|
+
const draggedIds = draggedApps.map((a) => a.id)
|
|
65
|
+
const atCell = findAppsAtCell(allApps, pos.x, pos.y, draggedIds)
|
|
66
|
+
|
|
67
|
+
if (atCell.length > 0) {
|
|
68
|
+
const preview = previewGroupAtCell(allApps, draggedApps, pos.x, pos.y)
|
|
69
|
+
dropTarget.value = { x: pos.x, y: pos.y, apps: preview, merging: true }
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (draggedApps.length >= 2) {
|
|
74
|
+
dropTarget.value = { x: pos.x, y: pos.y, apps: draggedApps }
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
dropTarget.value = null
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* @param {object|object[]} appOrApps - single app or all apps in a group
|
|
83
|
+
* @param {object} meta - { mode: 'single'|'group'|'folder', onTap?: fn }
|
|
84
|
+
*/
|
|
85
|
+
function onPointerDown(appOrApps, event, meta = {}) {
|
|
86
|
+
const apps = Array.isArray(appOrApps) ? appOrApps : [appOrApps]
|
|
87
|
+
const primary = apps[0]
|
|
88
|
+
if (!primary || event.button !== 0) return
|
|
89
|
+
|
|
90
|
+
for (const app of apps) ensurePlaced(app, event)
|
|
91
|
+
|
|
92
|
+
const ids = apps.map((a) => a.id)
|
|
93
|
+
const mode = meta.mode ?? 'single'
|
|
94
|
+
const requiresHold = mode === 'group'
|
|
95
|
+
|
|
96
|
+
drag.value = {
|
|
97
|
+
ids,
|
|
98
|
+
mode,
|
|
99
|
+
requiresHold,
|
|
100
|
+
startX: event.clientX,
|
|
101
|
+
startY: event.clientY,
|
|
102
|
+
anchorX: primary.desktopX ?? 0,
|
|
103
|
+
anchorY: primary.desktopY ?? 0,
|
|
104
|
+
moved: false,
|
|
105
|
+
ready: !requiresHold,
|
|
106
|
+
onTap: meta.onTap ?? null,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (requiresHold) {
|
|
110
|
+
holdTimer = setTimeout(() => {
|
|
111
|
+
if (drag.value) drag.value.ready = true
|
|
112
|
+
}, HOLD_MS)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
window.addEventListener('mousemove', onPointerMove)
|
|
116
|
+
window.addEventListener('mouseup', onPointerUp)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function onPointerMove(event) {
|
|
120
|
+
if (!drag.value?.ready) return
|
|
121
|
+
|
|
122
|
+
const dx = event.clientX - drag.value.startX
|
|
123
|
+
const dy = event.clientY - drag.value.startY
|
|
124
|
+
const dist = Math.abs(dx) + Math.abs(dy)
|
|
125
|
+
|
|
126
|
+
if (!drag.value.moved && dist < DRAG_THRESHOLD) return
|
|
127
|
+
|
|
128
|
+
drag.value.moved = true
|
|
129
|
+
const layerRect = getLayerRect()
|
|
130
|
+
if (!layerRect) return
|
|
131
|
+
|
|
132
|
+
const rawX = drag.value.anchorX + dx
|
|
133
|
+
const rawY = drag.value.anchorY + dy
|
|
134
|
+
const pos = resolvePosition(rawX, rawY)
|
|
135
|
+
const draggedApps = getApps(drag.value.ids)
|
|
136
|
+
|
|
137
|
+
for (const app of draggedApps) {
|
|
138
|
+
app.desktopX = pos.x
|
|
139
|
+
app.desktopY = pos.y
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
updateDropTarget(pos, draggedApps)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function finalizeDrop() {
|
|
146
|
+
if (!drag.value?.moved) return
|
|
147
|
+
|
|
148
|
+
const draggedApps = getApps(drag.value.ids)
|
|
149
|
+
if (!draggedApps.length) return
|
|
150
|
+
|
|
151
|
+
const primary = draggedApps[0]
|
|
152
|
+
const pos = resolvePosition(primary.desktopX ?? 0, primary.desktopY ?? 0)
|
|
153
|
+
moveAppsToCell(draggedApps, pos.x, pos.y)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function onPointerUp() {
|
|
157
|
+
clearHoldTimer()
|
|
158
|
+
removeListeners()
|
|
159
|
+
|
|
160
|
+
const session = drag.value
|
|
161
|
+
const wasDrag = session?.moved
|
|
162
|
+
lastWasDrag.value = !!wasDrag
|
|
163
|
+
|
|
164
|
+
if (wasDrag) {
|
|
165
|
+
finalizeDrop()
|
|
166
|
+
const draggedApps = getApps(session.ids)
|
|
167
|
+
const primary = draggedApps[0]
|
|
168
|
+
options.onMoved?.({
|
|
169
|
+
fromCell: { x: session.anchorX, y: session.anchorY },
|
|
170
|
+
toCell: primary
|
|
171
|
+
? { x: primary.desktopX, y: primary.desktopY }
|
|
172
|
+
: null,
|
|
173
|
+
})
|
|
174
|
+
} else if (session?.onTap) {
|
|
175
|
+
session.onTap()
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
drag.value = null
|
|
179
|
+
dropTarget.value = null
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function isDragging(appId) {
|
|
183
|
+
return drag.value?.ids?.includes(appId) && drag.value.moved
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function isHolding(appId) {
|
|
187
|
+
return drag.value?.ids?.includes(appId) && drag.value.requiresHold && drag.value.ready && !drag.value.moved
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function cleanup() {
|
|
191
|
+
clearHoldTimer()
|
|
192
|
+
removeListeners()
|
|
193
|
+
drag.value = null
|
|
194
|
+
dropTarget.value = null
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
drag,
|
|
199
|
+
dropTarget,
|
|
200
|
+
lastWasDrag,
|
|
201
|
+
onPointerDown,
|
|
202
|
+
isDragging,
|
|
203
|
+
isHolding,
|
|
204
|
+
cleanup,
|
|
205
|
+
resolvePosition,
|
|
206
|
+
}
|
|
207
|
+
}
|