@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
package/README.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# App Hub Frontend
|
|
2
|
+
|
|
3
|
+
Vue 3 **Windows-style desktop** for App Hub. Modular apps run in draggable windows.
|
|
4
|
+
|
|
5
|
+
## Modules (independent)
|
|
6
|
+
|
|
7
|
+
| Module | Path | Role |
|
|
8
|
+
|--------|------|------|
|
|
9
|
+
| **desktop** | `src/modules/desktop` | Wallpaper, icons, taskbar, Start menu |
|
|
10
|
+
| **window-manager** | `src/modules/window-manager` | Open / close / focus / minimize windows |
|
|
11
|
+
| **app-store** | `src/modules/app-store` | Built-in **App Store** (default Hub app) |
|
|
12
|
+
|
|
13
|
+
Each module has its own `index.js` and can be imported alone:
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
import { AppHubDesktop } from '@kennofizet/apphub-frontend/modules/desktop'
|
|
17
|
+
import { useWindowManager } from '@kennofizet/apphub-frontend/modules/window-manager'
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Install (host app)
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install @kennofizet/apphub-frontend
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
import { createApp } from 'vue'
|
|
28
|
+
import { installAppHubModule, AppHubDesktop } from '@kennofizet/apphub-frontend'
|
|
29
|
+
|
|
30
|
+
const app = createApp(App)
|
|
31
|
+
const hostApi = installAppHubModule(app, {
|
|
32
|
+
coreUrl: 'https://your-api/api/knf',
|
|
33
|
+
backendUrl: 'https://your-api/api/knf/apphub',
|
|
34
|
+
token: sessionTokenFromHost,
|
|
35
|
+
hostAccessSecret: import.meta.env.VITE_APPHUB_HOST_ACCESS_SECRET,
|
|
36
|
+
language: 'vi',
|
|
37
|
+
theme: 'dark',
|
|
38
|
+
themeToggle: false,
|
|
39
|
+
allowedRuntimeOrigins: ['https://publisher-app.example.com'],
|
|
40
|
+
})
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**Local dev:** `backendUrl` + `token` is enough on `localhost` — the package auto-relaxes origin checks.
|
|
44
|
+
|
|
45
|
+
**Production:** Bootstrap auto-derives URLs from existing Laravel env (no extra App Hub vars):
|
|
46
|
+
|
|
47
|
+
- Hub host → browser `Origin` on `GET /bootstrap` (where the Vue app runs)
|
|
48
|
+
- Runtime API → `{APP_URL}/{KNF_CORE_API_PREFIX}/{APPHUB_API_PREFIX}` (Laravel backend)
|
|
49
|
+
|
|
50
|
+
Set `APP_URL` to your Laravel API host. Hub SPA can be on a different origin (e.g. Vite `:3000` in dev, `apphub.` subdomain in prod) — bootstrap learns it from the request. Optional overrides: `hubOrigin`, `runtimePublicUrl`. Embed the Hub URL in your product iframe — do not mount Hub inside the product Vue app.
|
|
51
|
+
|
|
52
|
+
`hostApi.grantBridgeScope`, `integrationDocsInternal` — host app only, not `inject()`.
|
|
53
|
+
|
|
54
|
+
Hub shell components use `useAppHubHostApi()` from the package. The host token and `hostAccessSecret` stay in private module credentials — not in `inject('apphubOptions')` and not via `provide('apphubApi')`.
|
|
55
|
+
|
|
56
|
+
### Who can read internal docs?
|
|
57
|
+
|
|
58
|
+
| Role | Internal docs (`integration-docs/internal`)? |
|
|
59
|
+
|------|---------------------------------------------|
|
|
60
|
+
| End user on Hub desktop | No — use Guide or public `integration-docs` |
|
|
61
|
+
| Publisher | No |
|
|
62
|
+
| packages-core **zone/server manager** (settings in other packages) | **No** — that is not host integrator |
|
|
63
|
+
| **Host team** embedding App Hub | Yes — `APPHUB_HOST_ACCESS_SECRET` + `hostAccessSecret` in `installAppHubModule` |
|
|
64
|
+
|
|
65
|
+
Do not pass `hostAccessSecret` to every logged-in user. Only your host app build/config (dev/ops), never user login API.
|
|
66
|
+
|
|
67
|
+
```vue
|
|
68
|
+
<template>
|
|
69
|
+
<AppHubDesktop language="vi" theme="light" :theme-toggle="false" />
|
|
70
|
+
</template>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Full-screen route example: `/apphub` → only `<AppHubDesktop />`.
|
|
74
|
+
|
|
75
|
+
## UX
|
|
76
|
+
|
|
77
|
+
- Desktop icons (double-click to open).
|
|
78
|
+
- **App Store** icon is always on the desktop (install user apps).
|
|
79
|
+
- On first load, App Store window opens automatically (`openAppStoreOnMount`, default `true`).
|
|
80
|
+
- Installed apps from the store appear as new desktop icons (demo placeholder window until backend launch).
|
|
81
|
+
|
|
82
|
+
## Verify
|
|
83
|
+
|
|
84
|
+
Use `____TEST/test` frontend (manual `npm install` there). Do not run `npm install` inside `apphub-packages` per project rules.
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kennofizet/apphub-frontend",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "App Hub Vue 3 UI — Windows-style desktop shell and modular apps (App Store default).",
|
|
5
|
+
"main": "./src/index.js",
|
|
6
|
+
"module": "./src/index.js",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./src/index.js",
|
|
11
|
+
"require": "./src/index.js",
|
|
12
|
+
"default": "./src/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./modules/desktop": "./src/modules/desktop/index.js",
|
|
15
|
+
"./modules/app-store": "./src/modules/app-store/index.js",
|
|
16
|
+
"./modules/window-manager": "./src/modules/window-manager/index.js"
|
|
17
|
+
},
|
|
18
|
+
"keywords": ["vue", "vue3", "apphub", "desktop", "frontend"],
|
|
19
|
+
"author": "KennoFizet",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"axios": "^1.1.2",
|
|
23
|
+
"vue": "^3.2.0"
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"vue": "^3.2.0"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=16.0.0"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import axios from 'axios'
|
|
2
|
+
|
|
3
|
+
const REQUEST_TIMEOUT_MS = 30_000
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* packages-core client (zones, auth) — same pattern as workpoint/rewardplay.
|
|
7
|
+
*/
|
|
8
|
+
export function createCoreApi(coreUrl, token) {
|
|
9
|
+
const baseURL = (coreUrl || '').replace(/\/$/, '')
|
|
10
|
+
|
|
11
|
+
const client = axios.create({
|
|
12
|
+
baseURL,
|
|
13
|
+
timeout: REQUEST_TIMEOUT_MS,
|
|
14
|
+
headers: {
|
|
15
|
+
Accept: 'application/json',
|
|
16
|
+
'Content-Type': 'application/json',
|
|
17
|
+
...(token ? { 'X-Knf-Token': token } : {}),
|
|
18
|
+
},
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
authCheck: () => client.get('/auth/check'),
|
|
23
|
+
getPlayerZones: () => client.get('/player/zones'),
|
|
24
|
+
}
|
|
25
|
+
}
|
package/src/api/index.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import axios from 'axios'
|
|
2
|
+
|
|
3
|
+
const REQUEST_TIMEOUT_MS = 30_000
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* HTTP client for apphub-backend.
|
|
7
|
+
*/
|
|
8
|
+
export function createAppHubApi(backendUrl, token, options = {}) {
|
|
9
|
+
const baseURL = (backendUrl || '').replace(/\/$/, '')
|
|
10
|
+
const hostAccessSecret = options.hostAccessSecret || ''
|
|
11
|
+
const getZoneHeaderId = options.getZoneHeaderId
|
|
12
|
+
|
|
13
|
+
const client = axios.create({
|
|
14
|
+
baseURL,
|
|
15
|
+
timeout: REQUEST_TIMEOUT_MS,
|
|
16
|
+
headers: {
|
|
17
|
+
Accept: 'application/json',
|
|
18
|
+
'Content-Type': 'application/json',
|
|
19
|
+
...(token ? { 'X-Knf-Token': token } : {}),
|
|
20
|
+
},
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
client.interceptors.request.use((config) => {
|
|
24
|
+
const zoneId = typeof getZoneHeaderId === 'function' ? getZoneHeaderId() : null
|
|
25
|
+
if (zoneId) {
|
|
26
|
+
config.headers = config.headers || {}
|
|
27
|
+
config.headers['X-Knf-Zone-Id'] = zoneId
|
|
28
|
+
}
|
|
29
|
+
return config
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
function bridgeHeaders(launchToken, appSlug) {
|
|
33
|
+
return {
|
|
34
|
+
'X-AppHub-Launch-Token': launchToken,
|
|
35
|
+
'X-AppHub-App-Slug': appSlug,
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function hostHeaders() {
|
|
40
|
+
return hostAccessSecret ? { 'X-AppHub-Host-Access': hostAccessSecret } : {}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
bootstrap: () => client.get('/bootstrap'),
|
|
45
|
+
integrationDocs: () => client.get('/integration-docs'),
|
|
46
|
+
integrationDocsInternal: () =>
|
|
47
|
+
client.get('/integration-docs/internal', { headers: hostHeaders() }),
|
|
48
|
+
apps: (params) => client.get('/apps', { params }),
|
|
49
|
+
launch: (slug, payload) => client.post(`/apps/${encodeURIComponent(slug)}/launch`, payload ?? {}),
|
|
50
|
+
ping: (slug) => client.post(`/apps/${encodeURIComponent(slug)}/ping`),
|
|
51
|
+
verifyLaunchToken: (launchToken, appSlug) =>
|
|
52
|
+
client.post('/verify-launch-token', {
|
|
53
|
+
launch_token: launchToken,
|
|
54
|
+
...(appSlug ? { app_slug: appSlug } : {}),
|
|
55
|
+
}),
|
|
56
|
+
usage: (slug, payload) =>
|
|
57
|
+
client.post(`/apps/${encodeURIComponent(slug)}/usage`, payload),
|
|
58
|
+
devApps: (params) => client.get('/dev/apps', { params }),
|
|
59
|
+
devInspectBundle: (slug) =>
|
|
60
|
+
client.get(`/dev/apps/${encodeURIComponent(slug)}/bundle-inspect`),
|
|
61
|
+
devDisableApp: (slug) =>
|
|
62
|
+
client.post(`/dev/apps/${encodeURIComponent(slug)}/disable`),
|
|
63
|
+
devSetAppStatus: (slug, status) =>
|
|
64
|
+
client.post(`/dev/apps/${encodeURIComponent(slug)}/status`, { status }),
|
|
65
|
+
registerApp: (formData) =>
|
|
66
|
+
client.post('/apps/register', formData, {
|
|
67
|
+
headers: { 'Content-Type': 'multipart/form-data' },
|
|
68
|
+
timeout: 120_000,
|
|
69
|
+
}),
|
|
70
|
+
appVersions: (slug) => client.get(`/apps/${encodeURIComponent(slug)}/versions`),
|
|
71
|
+
grantBridgeScope: (launchToken, scope) =>
|
|
72
|
+
client.post('/bridge/scopes', { launch_token: launchToken, scope }),
|
|
73
|
+
bridgeUser: (launchToken, appSlug) =>
|
|
74
|
+
client.get('/bridge/user', { headers: bridgeHeaders(launchToken, appSlug) }),
|
|
75
|
+
bridgeDesktopMessage: (launchToken, appSlug, payload) =>
|
|
76
|
+
client.post('/bridge/desktop/message', payload, {
|
|
77
|
+
headers: bridgeHeaders(launchToken, appSlug),
|
|
78
|
+
}),
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { computed, reactive } from 'vue'
|
|
2
|
+
import {
|
|
3
|
+
loadStoredZone,
|
|
4
|
+
parseZonesFromResponse,
|
|
5
|
+
parseZonesMeta,
|
|
6
|
+
saveStoredZone,
|
|
7
|
+
} from '../utils/zoneContext.js'
|
|
8
|
+
|
|
9
|
+
function parseUserFromBootstrap(resp) {
|
|
10
|
+
const data = resp?.data?.data ?? resp?.data?.datas ?? resp?.data ?? {}
|
|
11
|
+
const user = data.user
|
|
12
|
+
if (!user || user.id == null) return null
|
|
13
|
+
return {
|
|
14
|
+
id: user.id,
|
|
15
|
+
name: user.name ?? String(user.id),
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Hub session — user (apphub bootstrap) + zones (packages-core).
|
|
21
|
+
*/
|
|
22
|
+
export function createZoneContextState(getCoreApi, getHostApi, hooks = {}) {
|
|
23
|
+
const state = reactive({
|
|
24
|
+
user: { id: null, name: null },
|
|
25
|
+
zones: [],
|
|
26
|
+
selectedZoneId: null,
|
|
27
|
+
viewAllZones: false,
|
|
28
|
+
loading: false,
|
|
29
|
+
error: '',
|
|
30
|
+
timezone: null,
|
|
31
|
+
isManager: false,
|
|
32
|
+
authOk: false,
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const selectedZone = computed(() =>
|
|
36
|
+
state.zones.find((z) => z.id === state.selectedZoneId) ?? null,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
const activeZoneIds = computed(() => {
|
|
40
|
+
if (state.viewAllZones) {
|
|
41
|
+
return state.zones.map((z) => z.id).filter((id) => id != null)
|
|
42
|
+
}
|
|
43
|
+
return state.selectedZoneId != null ? [state.selectedZoneId] : []
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
function selectZone(zoneOrId) {
|
|
47
|
+
const id = typeof zoneOrId === 'object' ? zoneOrId?.id : zoneOrId
|
|
48
|
+
const zone = state.zones.find((z) => z.id === id)
|
|
49
|
+
if (!zone) return
|
|
50
|
+
state.selectedZoneId = zone.id
|
|
51
|
+
state.viewAllZones = false
|
|
52
|
+
saveStoredZone(zone)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function setViewAllZones(enabled) {
|
|
56
|
+
state.viewAllZones = !!enabled
|
|
57
|
+
if (!state.viewAllZones && state.selectedZoneId == null && state.zones.length) {
|
|
58
|
+
selectZone(state.zones[0])
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function resolveInitialZone(list) {
|
|
63
|
+
const stored = loadStoredZone()
|
|
64
|
+
if (stored?.id && list.some((z) => z.id === stored.id)) {
|
|
65
|
+
state.selectedZoneId = stored.id
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
if (list.length === 1) {
|
|
69
|
+
selectZone(list[0])
|
|
70
|
+
} else if (list.length > 1 && state.selectedZoneId == null) {
|
|
71
|
+
selectZone(list[0])
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function refreshUser() {
|
|
76
|
+
if (hooks.ensureBootstrapSession) {
|
|
77
|
+
await hooks.ensureBootstrapSession()
|
|
78
|
+
return
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const hostApi = getHostApi?.()
|
|
82
|
+
if (!hostApi?.bootstrap) return
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const res = await hostApi.bootstrap()
|
|
86
|
+
hooks.onBootstrap?.(res)
|
|
87
|
+
const user = parseUserFromBootstrap(res)
|
|
88
|
+
if (user) {
|
|
89
|
+
state.user.id = user.id
|
|
90
|
+
state.user.name = user.name
|
|
91
|
+
}
|
|
92
|
+
} catch {
|
|
93
|
+
/* keep previous user */
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function refreshZones() {
|
|
98
|
+
const api = getCoreApi?.()
|
|
99
|
+
if (!api?.getPlayerZones) {
|
|
100
|
+
state.error = 'no_core_api'
|
|
101
|
+
state.zones = []
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
state.error = ''
|
|
106
|
+
try {
|
|
107
|
+
if (api.authCheck) {
|
|
108
|
+
try {
|
|
109
|
+
const authRes = await api.authCheck()
|
|
110
|
+
state.authOk = authRes?.data?.success !== false
|
|
111
|
+
} catch {
|
|
112
|
+
state.authOk = false
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const resp = await api.getPlayerZones()
|
|
117
|
+
const list = parseZonesFromResponse(resp)
|
|
118
|
+
const meta = parseZonesMeta(resp)
|
|
119
|
+
state.zones = list
|
|
120
|
+
state.timezone = meta.timezone
|
|
121
|
+
state.isManager = meta.isManager
|
|
122
|
+
resolveInitialZone(list)
|
|
123
|
+
} catch {
|
|
124
|
+
state.error = 'load_failed'
|
|
125
|
+
state.zones = []
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function refresh(options = {}) {
|
|
130
|
+
state.loading = true
|
|
131
|
+
try {
|
|
132
|
+
if (options.skipBootstrap) {
|
|
133
|
+
await refreshZones()
|
|
134
|
+
} else {
|
|
135
|
+
await Promise.all([refreshUser(), refreshZones()])
|
|
136
|
+
}
|
|
137
|
+
} finally {
|
|
138
|
+
state.loading = false
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function getZoneHeaderId() {
|
|
143
|
+
if (state.viewAllZones || state.selectedZoneId == null) return null
|
|
144
|
+
return String(state.selectedZoneId)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
state,
|
|
149
|
+
selectedZone,
|
|
150
|
+
activeZoneIds,
|
|
151
|
+
selectZone,
|
|
152
|
+
setViewAllZones,
|
|
153
|
+
refresh,
|
|
154
|
+
getZoneHeaderId,
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { getCurrentInstance } from 'vue'
|
|
2
|
+
import { getAppHubStore } from '../moduleStore.js'
|
|
3
|
+
|
|
4
|
+
export function resolveRootApp(instance = getCurrentInstance()) {
|
|
5
|
+
if (!instance) return null
|
|
6
|
+
return instance.appContext?.app ?? instance.app ?? null
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function getHostApiForApp(app) {
|
|
10
|
+
return getAppHubStore(app)?.facade ?? null
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function isBackendReadyForApp(app) {
|
|
14
|
+
const store = getAppHubStore(app)
|
|
15
|
+
return !!(store?.credentials?.backendUrl && store?.credentials?.token)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Host-only API — use in Hub shell components. Not provided via inject
|
|
20
|
+
* so publisher app code cannot access grantBridgeScope or internal docs.
|
|
21
|
+
*/
|
|
22
|
+
export function useAppHubHostApi() {
|
|
23
|
+
return getHostApiForApp(resolveRootApp())
|
|
24
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { inject } from 'vue'
|
|
2
|
+
|
|
3
|
+
export const APPHUB_ZONE_CONTEXT_KEY = 'apphubZoneContext'
|
|
4
|
+
|
|
5
|
+
export function useAppHubZoneContext() {
|
|
6
|
+
const ctx = inject(APPHUB_ZONE_CONTEXT_KEY, null)
|
|
7
|
+
if (!ctx) {
|
|
8
|
+
throw new Error('useAppHubZoneContext() requires installAppHubModule()')
|
|
9
|
+
}
|
|
10
|
+
return ctx
|
|
11
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { computed, getCurrentInstance, inject } from 'vue'
|
|
2
|
+
import { getAppHubStore } from '../moduleStore.js'
|
|
3
|
+
import { saveDevFriendlyOriginsPreference } from '../utils/devOriginSettings.js'
|
|
4
|
+
import { isLocalDevHostPage } from '../utils/originSafety.js'
|
|
5
|
+
|
|
6
|
+
function reconcileAfterToggle(vueApp, enabled) {
|
|
7
|
+
import('../index.js').then(({ installAppHubModule }) => {
|
|
8
|
+
installAppHubModule(vueApp, { enforceDevFriendlyOrigins: enabled, isDevUser: true })
|
|
9
|
+
})
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Dev-only localhost toggle — requires bootstrap is_dev_user (APPHUB_DEV_USER_IDS). */
|
|
13
|
+
export function useDevOriginToggle() {
|
|
14
|
+
const moduleOptions = inject('apphubOptions', {})
|
|
15
|
+
const vueApp = getCurrentInstance()?.appContext?.app
|
|
16
|
+
|
|
17
|
+
const visible = computed(
|
|
18
|
+
() => moduleOptions?.isDevUser === true && isLocalDevHostPage(),
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
const devFriendlyOn = computed(
|
|
22
|
+
() => moduleOptions?.enforceDevFriendlyOrigins !== false,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
function toggle() {
|
|
26
|
+
if (!vueApp || moduleOptions?.isDevUser !== true) return
|
|
27
|
+
const next = !devFriendlyOn.value
|
|
28
|
+
saveDevFriendlyOriginsPreference(next)
|
|
29
|
+
const store = getAppHubStore(vueApp)
|
|
30
|
+
if (store) {
|
|
31
|
+
store.options.enforceDevFriendlyOrigins = next
|
|
32
|
+
if (!next) {
|
|
33
|
+
store.options.hubOrigin = ''
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
reconcileAfterToggle(vueApp, next)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return { visible, devFriendlyOn, toggle }
|
|
40
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import en from './translations/en.js'
|
|
2
|
+
import vi from './translations/vi.js'
|
|
3
|
+
import { resolveLang } from './resolveLang.js'
|
|
4
|
+
|
|
5
|
+
const catalogs = { en, vi }
|
|
6
|
+
|
|
7
|
+
export function t(key, lang = 'vi', params = {}) {
|
|
8
|
+
const code = resolveLang(lang)
|
|
9
|
+
let text = catalogs[code]?.[key] ?? catalogs.en?.[key] ?? String(key)
|
|
10
|
+
if (params && typeof params === 'object') {
|
|
11
|
+
Object.entries(params).forEach(([k, v]) => {
|
|
12
|
+
text = text.replace(new RegExp(`\\{${k}\\}`, 'g'), String(v ?? ''))
|
|
13
|
+
})
|
|
14
|
+
}
|
|
15
|
+
return text
|
|
16
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Normalize language from prop, inject, or ref. */
|
|
2
|
+
export function resolveLang(source, fallback = 'vi') {
|
|
3
|
+
if (typeof source === 'string' && source) return source
|
|
4
|
+
if (source && typeof source.value === 'string' && source.value) return source.value
|
|
5
|
+
return fallback
|
|
6
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const VALID = new Set(['dark', 'light'])
|
|
2
|
+
|
|
3
|
+
/** Normalize theme from prop, inject, ref, or saved setting. Returns null when unset/auto. */
|
|
4
|
+
export function resolveTheme(source, fallback = null) {
|
|
5
|
+
const read = (value) => {
|
|
6
|
+
if (typeof value !== 'string') return null
|
|
7
|
+
const code = value.trim().toLowerCase()
|
|
8
|
+
if (code === 'auto' || code === '') return null
|
|
9
|
+
return VALID.has(code) ? code : null
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const direct = read(typeof source === 'string' ? source : null)
|
|
13
|
+
if (direct) return direct
|
|
14
|
+
|
|
15
|
+
if (source && typeof source === 'object' && 'value' in source) {
|
|
16
|
+
const fromRef = read(source.value)
|
|
17
|
+
if (fromRef) return fromRef
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return read(fallback)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** True when host passes theme via prop or installAppHubModule (not auto). */
|
|
24
|
+
export function isThemeLocked(propTheme, injectTheme) {
|
|
25
|
+
return resolveTheme(propTheme) != null || resolveTheme(injectTheme) != null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function normalizeTheme(value, fallback = 'dark') {
|
|
29
|
+
return resolveTheme(value, fallback) ?? fallback
|
|
30
|
+
}
|