Add CUTOS capability and core API examples
This commit is contained in:
@@ -0,0 +1,340 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, reactive, ref } from 'vue'
|
||||
import { CoreAPI } from '@cutos/core'
|
||||
import CutosTopbar from '@/components/CutosTopbar.vue'
|
||||
import { loadConfig } from '@/utils/config'
|
||||
|
||||
type ApiKey = 'platform' | 'runtimeConfig' | 'box' | 'device'
|
||||
type LogLevel = 'info' | 'warning' | 'error' | 'debug'
|
||||
|
||||
interface RuntimeResult {
|
||||
key: ApiKey
|
||||
method: string
|
||||
label: string
|
||||
value: unknown
|
||||
error: string
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
interface NotificationMessage {
|
||||
event?: string
|
||||
msg?: unknown
|
||||
}
|
||||
|
||||
interface ActivityItem {
|
||||
id: number
|
||||
time: string
|
||||
kind: string
|
||||
message: string
|
||||
}
|
||||
|
||||
const title = ref('CUTOS Core API')
|
||||
const lwaName = ref('')
|
||||
const lwaVersion = ref('')
|
||||
const coreVersion = ref('')
|
||||
const host = ref('localhost')
|
||||
const connected = ref(false)
|
||||
const initializing = ref(true)
|
||||
const initError = ref('')
|
||||
const volume = ref(50)
|
||||
const volumeResult = ref('')
|
||||
const proxyResult = ref<unknown>(null)
|
||||
const proxyLoading = ref(false)
|
||||
const proxyCompleted = ref(false)
|
||||
const pingResult = ref<unknown>(null)
|
||||
const pingLoading = ref(false)
|
||||
const activities = ref<ActivityItem[]>([])
|
||||
|
||||
const results = reactive<RuntimeResult[]>([
|
||||
{ key: 'platform', method: 'CoreAPI.getPlatform()', label: 'Platform', value: null, error: '', loading: true },
|
||||
{ key: 'runtimeConfig', method: 'CoreAPI.getConfig()', label: 'Runtime config', value: null, error: '', loading: true },
|
||||
{ key: 'box', method: 'CoreAPI.getBoxInfo()', label: 'Box', value: null, error: '', loading: true },
|
||||
{ key: 'device', method: 'CoreAPI.getDeviceInfo()', label: 'Device', value: null, error: '', loading: true }
|
||||
])
|
||||
|
||||
const statusLabel = computed(() => {
|
||||
if (initializing.value) return 'Connecting'
|
||||
if (connected.value) return 'Connected'
|
||||
return 'Disconnected'
|
||||
})
|
||||
|
||||
function format(value: unknown) {
|
||||
if (value === null || value === undefined) return 'No data'
|
||||
if (typeof value === 'string') return value
|
||||
return JSON.stringify(value, null, 2)
|
||||
}
|
||||
|
||||
function errorMessage(error: unknown) {
|
||||
return error instanceof Error ? error.message : String(error)
|
||||
}
|
||||
|
||||
function addActivity(kind: string, message: string) {
|
||||
activities.value.unshift({
|
||||
id: Date.now() + Math.random(),
|
||||
time: new Date().toLocaleTimeString(),
|
||||
kind,
|
||||
message
|
||||
})
|
||||
activities.value = activities.value.slice(0, 8)
|
||||
}
|
||||
|
||||
async function loadResult(key: ApiKey, operation: () => Promise<unknown>) {
|
||||
const item = results.find((result) => result.key === key)
|
||||
if (!item) return
|
||||
item.loading = true
|
||||
item.error = ''
|
||||
try {
|
||||
item.value = await operation()
|
||||
} catch (error) {
|
||||
item.error = errorMessage(error)
|
||||
} finally {
|
||||
item.loading = false
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshRuntime() {
|
||||
await Promise.all([
|
||||
loadResult('platform', () => CoreAPI.getPlatform()),
|
||||
loadResult('runtimeConfig', () => CoreAPI.getConfig()),
|
||||
loadResult('box', () => CoreAPI.getBoxInfo()),
|
||||
loadResult('device', () => CoreAPI.getDeviceInfo())
|
||||
])
|
||||
addActivity('runtime', 'Runtime information refreshed')
|
||||
}
|
||||
|
||||
async function readVolume() {
|
||||
try {
|
||||
const current = await CoreAPI.getVolume<number>()
|
||||
volume.value = Number(current)
|
||||
} catch (error) {
|
||||
volumeResult.value = errorMessage(error)
|
||||
}
|
||||
}
|
||||
|
||||
async function applyVolume() {
|
||||
volumeResult.value = ''
|
||||
try {
|
||||
const result = await CoreAPI.setVolume(volume.value)
|
||||
volumeResult.value = format(result)
|
||||
addActivity('volume', `Volume set to ${volume.value}`)
|
||||
} catch (error) {
|
||||
volumeResult.value = errorMessage(error)
|
||||
}
|
||||
}
|
||||
|
||||
function writeLog(level: LogLevel) {
|
||||
const logger = CoreAPI.getLogger()
|
||||
const content = `Core API demo ${level} message at ${new Date().toISOString()}`
|
||||
try {
|
||||
logger[level]('demo-core-api', content)
|
||||
addActivity(`log.${level}`, content)
|
||||
} catch (error) {
|
||||
addActivity(`log.${level}.error`, errorMessage(error))
|
||||
}
|
||||
}
|
||||
|
||||
async function configureProxy() {
|
||||
proxyLoading.value = true
|
||||
proxyCompleted.value = false
|
||||
proxyResult.value = null
|
||||
try {
|
||||
proxyResult.value = await CoreAPI.setHttpProxy('api', 'https://www.cut-os.com')
|
||||
addActivity('proxy', 'HTTP proxy configured')
|
||||
} catch (error) {
|
||||
proxyResult.value = { error: errorMessage(error) }
|
||||
} finally {
|
||||
proxyLoading.value = false
|
||||
proxyCompleted.value = true
|
||||
}
|
||||
}
|
||||
|
||||
async function pingProxy() {
|
||||
pingLoading.value = true
|
||||
pingResult.value = null
|
||||
try {
|
||||
const response = await fetch('http://localhost:3000/proxy/api/rest/sv/system/runtime/ping')
|
||||
const contentType = response.headers.get('content-type') || ''
|
||||
const data = contentType.includes('application/json')
|
||||
? await response.json()
|
||||
: await response.text()
|
||||
|
||||
pingResult.value = {
|
||||
ok: response.ok,
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
data
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status} ${response.statusText}`.trim())
|
||||
}
|
||||
addActivity('proxy.ping', 'HTTP proxy ping succeeded')
|
||||
} catch (error) {
|
||||
const message = errorMessage(error)
|
||||
pingResult.value = { error: message, response: pingResult.value }
|
||||
addActivity('proxy.ping.error', message)
|
||||
} finally {
|
||||
pingLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const notification = CoreAPI.getNotification()
|
||||
|
||||
onMounted(async () => {
|
||||
const config = await loadConfig()
|
||||
title.value = config.params.title
|
||||
lwaName.value = config.name
|
||||
lwaVersion.value = config.version
|
||||
host.value = import.meta.env.VITE_CUTOS_BROKER_URL || config.params.host
|
||||
|
||||
try {
|
||||
await CoreAPI.init(host.value)
|
||||
connected.value = CoreAPI.connected()
|
||||
coreVersion.value = CoreAPI.getVersion()
|
||||
addActivity('core', `Core initialized at ${host.value}`)
|
||||
|
||||
notification.register((data) => {
|
||||
const message = data as NotificationMessage
|
||||
if (message.event === 'networkConnection') {
|
||||
connected.value = Boolean(message.msg)
|
||||
}
|
||||
addActivity(`notification.${message.event || 'event'}`, format(message.msg))
|
||||
})
|
||||
|
||||
await Promise.all([refreshRuntime(), readVolume(), configureProxy()])
|
||||
} catch (error) {
|
||||
connected.value = false
|
||||
initError.value = errorMessage(error)
|
||||
} finally {
|
||||
initializing.value = false
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => notification.unregister())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="app-shell">
|
||||
<CutosTopbar
|
||||
:title="title"
|
||||
subtitle="Core API · CUTOS 4.0"
|
||||
:core-version="coreVersion"
|
||||
:lwa-name="lwaName"
|
||||
:lwa-version="lwaVersion"
|
||||
:connected="connected"
|
||||
:status-label="statusLabel"
|
||||
/>
|
||||
|
||||
<main>
|
||||
<section v-if="initError" class="error-banner">
|
||||
<strong>Core initialization failed</strong>
|
||||
<span>{{ initError }}</span>
|
||||
</section>
|
||||
|
||||
<section class="runtime-section">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<span class="eyebrow">{{ host }}</span>
|
||||
<h2>Runtime snapshot</h2>
|
||||
</div>
|
||||
<button type="button" class="button secondary" :disabled="!connected" @click="refreshRuntime">
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="result-grid">
|
||||
<article v-for="item in results" :key="item.key" class="result-card">
|
||||
<div class="result-header">
|
||||
<h3>{{ item.label }}</h3>
|
||||
<code>{{ item.method }}</code>
|
||||
</div>
|
||||
<pre v-if="item.loading" class="pending">Loading…</pre>
|
||||
<pre v-else-if="item.error" class="result-error">{{ item.error }}</pre>
|
||||
<pre v-else>{{ format(item.value) }}</pre>
|
||||
</article>
|
||||
|
||||
<article class="result-card activity-card">
|
||||
<div class="result-header activity-header">
|
||||
<div>
|
||||
<h3>Activity</h3>
|
||||
<code>CoreAPI.getNotification()</code>
|
||||
</div>
|
||||
<button v-if="activities.length" type="button" class="text-button" @click="activities = []">Clear</button>
|
||||
</div>
|
||||
<div v-if="activities.length" class="activity-list">
|
||||
<div v-for="item in activities" :key="item.id" class="activity-row">
|
||||
<time>{{ item.time }}</time>
|
||||
<code>{{ item.kind }}</code>
|
||||
<span>{{ item.message }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<p v-else class="empty-state">No activity</p>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="controls-section">
|
||||
<div class="control-panel volume-panel">
|
||||
<div class="panel-heading card-style-heading">
|
||||
<div>
|
||||
<h2>Audio</h2>
|
||||
<span class="eyebrow">CoreAPI.getVolume() / setVolume()</span>
|
||||
</div>
|
||||
<strong>{{ volume }}</strong>
|
||||
</div>
|
||||
<input v-model.number="volume" type="range" min="0" max="100" step="1" :disabled="!connected" />
|
||||
<div class="panel-actions">
|
||||
<button type="button" class="button primary" :disabled="!connected" @click="applyVolume">Apply volume</button>
|
||||
<span class="inline-result">{{ volumeResult }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-panel logger-panel">
|
||||
<div class="panel-heading card-style-heading">
|
||||
<div>
|
||||
<h2>Logger</h2>
|
||||
<span class="eyebrow">CoreAPI.getLogger()</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="button-row">
|
||||
<button type="button" class="button secondary" :disabled="!connected" @click="writeLog('info')">Info</button>
|
||||
<button type="button" class="button secondary" :disabled="!connected" @click="writeLog('warning')">Warning</button>
|
||||
<button type="button" class="button danger" :disabled="!connected" @click="writeLog('error')">Error</button>
|
||||
<button type="button" class="button secondary" :disabled="!connected" @click="writeLog('debug')">Debug</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="control-panel proxy-panel">
|
||||
<div class="panel-heading card-style-heading">
|
||||
<div>
|
||||
<h2>HTTP proxy</h2>
|
||||
<span class="eyebrow">CoreAPI.setHttpProxy()</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="proxy-columns">
|
||||
<div class="proxy-column">
|
||||
<div class="proxy-target"><code>api → https://www.cut-os.com</code></div>
|
||||
<pre v-if="proxyLoading" class="compact-result pending">Configuring…</pre>
|
||||
<pre v-else-if="proxyCompleted" class="compact-result">{{ format(proxyResult) }}</pre>
|
||||
</div>
|
||||
<div class="proxy-column ping-column">
|
||||
<div class="ping-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="button secondary"
|
||||
:disabled="!connected || proxyLoading || pingLoading"
|
||||
@click="pingProxy"
|
||||
>
|
||||
{{ pingLoading ? 'Pinging…' : 'Ping' }}
|
||||
</button>
|
||||
<p class="ping-hint">Verify access through <code>/proxy/api/</code></p>
|
||||
</div>
|
||||
<pre v-if="pingResult" class="compact-result">{{ format(pingResult) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 7.6 KiB |
@@ -0,0 +1,75 @@
|
||||
<script setup lang="ts">
|
||||
import cutosLogo from '@/assets/cutos.png'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
title: string
|
||||
subtitle?: string
|
||||
coreVersion?: string
|
||||
lwaName?: string
|
||||
lwaVersion?: string
|
||||
connected: boolean
|
||||
statusLabel?: string
|
||||
}>(), {
|
||||
subtitle: 'CUTOS 4.0 LWA',
|
||||
coreVersion: '',
|
||||
lwaName: '',
|
||||
lwaVersion: '',
|
||||
statusLabel: ''
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<header class="topbar">
|
||||
<div class="brand">
|
||||
<img :src="cutosLogo" alt="CUTOS" />
|
||||
<div>
|
||||
<h1>{{ title }}</h1>
|
||||
<p>{{ subtitle }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status-cluster">
|
||||
<span class="version-tag">Core {{ coreVersion || '—' }}</span>
|
||||
<span class="version-tag">LWA {{ lwaName }} {{ lwaVersion || '—' }}</span>
|
||||
<span class="connection" :class="{ online: connected }">
|
||||
<i></i>{{ statusLabel || (connected ? 'Connected' : 'Disconnected') }}
|
||||
</span>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.topbar {
|
||||
min-height: 82px;
|
||||
padding: 14px clamp(18px, 4vw, 52px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 24px;
|
||||
color: #f8fafb;
|
||||
background: #20262c;
|
||||
border-bottom: 4px solid #16a394;
|
||||
}
|
||||
.brand { display: flex; align-items: center; gap: 15px; min-width: 0; }
|
||||
.brand img { width: 42px; height: 42px; object-fit: contain; }
|
||||
.brand h1 { margin: 0; font-size: 20px; line-height: 1.2; font-weight: 700; }
|
||||
.brand p { margin: 4px 0 0; font-size: 12px; color: #aeb8bf; }
|
||||
.status-cluster { display: flex; align-items: center; justify-content: flex-end; gap: 9px; flex-wrap: wrap; }
|
||||
.version-tag, .connection {
|
||||
height: 30px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0 10px;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
border: 1px solid #4b555d;
|
||||
border-radius: 4px;
|
||||
background: #2b3339;
|
||||
}
|
||||
.connection i { width: 8px; height: 8px; margin-right: 7px; border-radius: 50%; background: #e06b65; }
|
||||
.connection.online i { background: #42c6a8; box-shadow: 0 0 0 3px rgba(66, 198, 168, 0.14); }
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.topbar { align-items: flex-start; flex-direction: column; gap: 12px; }
|
||||
.status-cluster { justify-content: flex-start; }
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,5 @@
|
||||
import { createApp } from 'vue'
|
||||
import './style.css'
|
||||
import App from './App.vue'
|
||||
|
||||
createApp(App).mount('#app')
|
||||
@@ -0,0 +1,100 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
font-family: Inter, "Segoe UI", Arial, sans-serif;
|
||||
color: #20262c;
|
||||
background: #eef1f3;
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
body { margin: 0; min-width: 320px; min-height: 100vh; }
|
||||
button, input { font: inherit; }
|
||||
button { cursor: pointer; }
|
||||
button:disabled { cursor: not-allowed; opacity: 0.48; }
|
||||
|
||||
.app-shell { min-height: 100vh; background: #eef1f3; }
|
||||
|
||||
main { width: 100%; margin: 0 auto; padding: 28px clamp(18px, 2vw, 36px) 32px; }
|
||||
section + section { margin-top: 34px; }
|
||||
.error-banner { display: flex; gap: 12px; padding: 12px 16px; color: #7e2420; background: #fff0ef; border-left: 4px solid #d54d47; }
|
||||
.section-heading, .panel-heading { display: flex; align-items: flex-end; justify-content: space-between; gap: 16px; }
|
||||
.section-heading { margin-bottom: 15px; }
|
||||
.section-heading h2, .panel-heading h2 { margin: 3px 0 0; font-size: 18px; line-height: 1.2; }
|
||||
.card-style-heading h2 { margin: 0 0 6px; }
|
||||
.eyebrow { display: block; color: #64717a; font-family: "Cascadia Code", Consolas, monospace; font-size: 11px; }
|
||||
|
||||
.result-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; }
|
||||
.result-card { min-width: 0; min-height: 224px; background: #fff; border: 1px solid #d8dee2; border-radius: 6px; overflow: hidden; }
|
||||
.result-header { min-height: 66px; padding: 13px 15px; border-bottom: 1px solid #e4e8eb; background: #f8fafb; }
|
||||
.result-header h3 { margin: 0 0 6px; font-size: 14px; }
|
||||
.result-header code { display: block; color: #68757d; font-size: 11px; overflow-wrap: anywhere; }
|
||||
pre { margin: 0; padding: 15px; font: 12px/1.55 "Cascadia Code", Consolas, monospace; white-space: pre-wrap; overflow-wrap: anywhere; }
|
||||
.result-card pre { max-height: 220px; overflow: auto; }
|
||||
.pending { color: #75828a; }
|
||||
.result-error { color: #b43b35; }
|
||||
.activity-header { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
|
||||
.activity-card { grid-column: span 2; }
|
||||
.activity-header h3 { margin-bottom: 6px; }
|
||||
|
||||
.controls-section { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); border-top: 1px solid #cbd3d8; border-bottom: 1px solid #cbd3d8; }
|
||||
.control-panel { min-width: 0; padding: 22px; background: #f7f9fa; }
|
||||
.control-panel + .control-panel { border-left: 1px solid #cbd3d8; }
|
||||
.panel-heading strong { font-size: 28px; color: #138b80; }
|
||||
.volume-panel input[type="range"] { width: 100%; margin: 28px 0 20px; accent-color: #138b80; }
|
||||
.panel-actions { min-height: 34px; display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
|
||||
.inline-result { max-width: 100%; color: #64717a; font: 11px/1.4 "Cascadia Code", Consolas, monospace; overflow-wrap: anywhere; }
|
||||
.button-row { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; margin-top: 25px; }
|
||||
.button { min-height: 34px; padding: 7px 13px; border: 1px solid transparent; border-radius: 4px; font-weight: 650; font-size: 12px; }
|
||||
.button.primary { color: #fff; background: #138b80; border-color: #138b80; }
|
||||
.button.secondary { color: #2d373d; background: #fff; border-color: #bfc8cd; }
|
||||
.button.danger { color: #a12d28; background: #fff; border-color: #dba7a4; }
|
||||
.proxy-panel { grid-column: span 2; }
|
||||
.proxy-columns { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; margin-top: 18px; }
|
||||
.proxy-column { min-width: 0; }
|
||||
.ping-column { min-width: 0; }
|
||||
.ping-actions { min-height: 44px; display: flex; align-items: center; gap: 10px; }
|
||||
.proxy-target { padding: 9px 10px; background: #fff; border: 1px solid #d8dee2; overflow-wrap: anywhere; }
|
||||
.proxy-target code { font-size: 11px; }
|
||||
.compact-result { width: 100%; margin-top: 10px; padding: 9px 10px; background: #fff; border: 1px solid #d8dee2; max-height: 82px; overflow: auto; }
|
||||
.ping-column .compact-result { margin-top: 10px; }
|
||||
.ping-hint { margin: 0; color: #64717a; font-size: 11px; line-height: 1.5; }
|
||||
.ping-hint code { overflow-wrap: anywhere; }
|
||||
|
||||
.text-button { padding: 4px 0; border: 0; color: #138b80; background: transparent; font-size: 12px; }
|
||||
.activity-list { max-height: 157px; overflow: auto; }
|
||||
.activity-row { display: grid; grid-template-columns: 82px 240px minmax(0, 1fr); gap: 12px; padding: 10px 13px; font-size: 12px; border-bottom: 1px solid #edf0f2; }
|
||||
.activity-row:last-child { border-bottom: 0; }
|
||||
.activity-row time { color: #77838b; font-variant-numeric: tabular-nums; }
|
||||
.activity-row code { color: #087b71; overflow-wrap: anywhere; }
|
||||
.activity-row span { min-width: 0; overflow-wrap: anywhere; }
|
||||
.empty-state { margin: 0; padding: 24px 15px; color: #77838b; font-size: 13px; }
|
||||
|
||||
@media (min-width: 1400px) and (min-height: 800px) {
|
||||
main { min-height: calc(100vh - 82px); display: flex; flex-direction: column; }
|
||||
.runtime-section { flex: 1; display: flex; flex-direction: column; }
|
||||
.result-grid { flex: 1; grid-template-rows: repeat(2, minmax(0, 1fr)); }
|
||||
.result-card { min-height: 0; }
|
||||
.controls-section { min-height: 220px; }
|
||||
.control-panel { display: flex; flex-direction: column; justify-content: center; }
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.result-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.activity-card { grid-column: auto; }
|
||||
.controls-section { grid-template-columns: 1fr; }
|
||||
.proxy-panel { grid-column: auto; }
|
||||
.control-panel + .control-panel { border-left: 0; border-top: 1px solid #cbd3d8; }
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.result-grid { grid-template-columns: 1fr; }
|
||||
.result-card { min-height: 190px; }
|
||||
.activity-row { grid-template-columns: 70px minmax(0, 1fr); }
|
||||
.activity-row span { grid-column: 1 / -1; }
|
||||
.proxy-columns { grid-template-columns: 1fr; }
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
export interface LwaConfig {
|
||||
name: string
|
||||
version: string
|
||||
description?: string
|
||||
params: {
|
||||
title: string
|
||||
host: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
drvDependencies?: Record<string, string>
|
||||
}
|
||||
|
||||
const defaultConfig: LwaConfig = {
|
||||
name: 'cutos-lwa',
|
||||
version: '0.0.0',
|
||||
params: {
|
||||
title: 'CUTOS LWA',
|
||||
host: 'localhost'
|
||||
}
|
||||
}
|
||||
|
||||
export let config: LwaConfig = { ...defaultConfig, params: { ...defaultConfig.params } }
|
||||
|
||||
function parse(str?: string) {
|
||||
if (!str || !str.length) return {}
|
||||
|
||||
return str.split('&').reduce<Record<string, string>>((acc, row) => {
|
||||
const [key, value] = row.split('=').map(decodeURIComponent)
|
||||
acc[key] = value
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
|
||||
function mergeQueryParams(config: LwaConfig) {
|
||||
if (typeof window === 'undefined') {
|
||||
return config
|
||||
}
|
||||
|
||||
const query = location.href.split('?')[1]
|
||||
const { params } = parse(query) as { params?: string }
|
||||
|
||||
try {
|
||||
if (params) config.params = { ...config.params, ...JSON.parse(params) }
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
export async function loadConfig(): Promise<LwaConfig> {
|
||||
try {
|
||||
const configUrl =
|
||||
typeof window === 'undefined' ? 'config.json' : new URL('config.json', window.location.href).toString()
|
||||
const response = await fetch(configUrl, { cache: 'no-cache' })
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load config.json: ${response.status}`)
|
||||
}
|
||||
config = mergeQueryParams((await response.json()) as LwaConfig)
|
||||
return config
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
config = mergeQueryParams({ ...defaultConfig, params: { ...defaultConfig.params } })
|
||||
return config
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_CUTOS_BROKER_URL?: string
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
readonly env: ImportMetaEnv
|
||||
}
|
||||
Reference in New Issue
Block a user