- Fetch installed apps from /dock/apps on init instead of hardcoded list
- Refresh running status every 10s
- Map dock response to app format (add url: /apps/{id})
- Show running indicator dot on active apps in dock
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
153 lines
4.9 KiB
JavaScript
153 lines
4.9 KiB
JavaScript
document.addEventListener('alpine:init', () => {
|
|
Alpine.data('os', () => ({
|
|
|
|
// ── Canvas pan state ──────────────────────────────────────────────────
|
|
offset: { x: 0, y: 0 },
|
|
isPanning: false,
|
|
panStart: { x: 0, y: 0 },
|
|
|
|
startPan(e) {
|
|
if (e.target !== document.getElementById('canvas') &&
|
|
!e.target.closest('#canvas') ||
|
|
e.target.closest('.window')) return
|
|
this.isPanning = true
|
|
this.panStart = { x: e.clientX - this.offset.x, y: e.clientY - this.offset.y }
|
|
document.getElementById('canvas').classList.add('panning')
|
|
},
|
|
|
|
pan(e) {
|
|
if (!this.isPanning) return
|
|
this.offset = { x: e.clientX - this.panStart.x, y: e.clientY - this.panStart.y }
|
|
},
|
|
|
|
endPan() {
|
|
this.isPanning = false
|
|
document.getElementById('canvas').classList.remove('panning')
|
|
},
|
|
|
|
// ── Windows ───────────────────────────────────────────────────────────
|
|
windows: [],
|
|
zCounter: 100,
|
|
|
|
openApp(app) {
|
|
// Bring to front if already open
|
|
const existing = this.windows.find(w => w.appId === app.id)
|
|
if (existing) { this.focusWindow(existing); return }
|
|
|
|
this.windows.push({
|
|
id: Date.now(),
|
|
appId: app.id,
|
|
title: app.name,
|
|
url: app.url,
|
|
x: 120 + Math.random() * 200,
|
|
y: 80 + Math.random() * 100,
|
|
w: 900,
|
|
h: 600,
|
|
z: ++this.zCounter,
|
|
})
|
|
},
|
|
|
|
focusWindow(win) {
|
|
win.z = ++this.zCounter
|
|
},
|
|
|
|
closeWindow(win) {
|
|
this.windows = this.windows.filter(w => w.id !== win.id)
|
|
},
|
|
|
|
// ── Window drag ───────────────────────────────────────────────────────
|
|
dragging: null,
|
|
dragStart: { x: 0, y: 0 },
|
|
|
|
startDrag(e, win) {
|
|
this.focusWindow(win)
|
|
this.dragging = win
|
|
this.dragStart = { x: e.clientX - win.x, y: e.clientY - win.y }
|
|
|
|
const onMove = (e) => {
|
|
if (!this.dragging) return
|
|
this.dragging.x = e.clientX - this.dragStart.x
|
|
this.dragging.y = e.clientY - this.dragStart.y
|
|
}
|
|
const onUp = () => {
|
|
this.dragging = null
|
|
window.removeEventListener('mousemove', onMove)
|
|
window.removeEventListener('mouseup', onUp)
|
|
}
|
|
window.addEventListener('mousemove', onMove)
|
|
window.addEventListener('mouseup', onUp)
|
|
},
|
|
|
|
// ── Cursor input mode ─────────────────────────────────────────────────
|
|
inputMode: false,
|
|
cursorQuery: '',
|
|
|
|
handleKeydown(e) {
|
|
// ⌘+Space or Ctrl+Space toggles input mode
|
|
if ((e.metaKey || e.ctrlKey) && e.code === 'Space') {
|
|
e.preventDefault()
|
|
this.inputMode ? this.exitInputMode() : this.enterInputMode()
|
|
return
|
|
}
|
|
},
|
|
|
|
enterInputMode() {
|
|
this.inputMode = true
|
|
this.cursorQuery = ''
|
|
this.$nextTick(() => this.$refs.cursorInput?.focus())
|
|
},
|
|
|
|
exitInputMode() {
|
|
this.inputMode = false
|
|
this.cursorQuery = ''
|
|
},
|
|
|
|
submitQuery() {
|
|
const query = this.cursorQuery.trim().toLowerCase()
|
|
|
|
// Built-in commands
|
|
if (query === 'login') {
|
|
this.openApp({ id: 'login', name: 'Login', url: '/login', icon: '🔐' })
|
|
this.exitInputMode()
|
|
return
|
|
}
|
|
|
|
// Check if it matches an installed app name
|
|
const app = this.installedApps.find(a => a.name.toLowerCase() === query)
|
|
if (app) { this.openApp(app); this.exitInputMode(); return }
|
|
|
|
// Otherwise — send to LLM (placeholder)
|
|
console.log('LLM query:', query)
|
|
this.exitInputMode()
|
|
},
|
|
|
|
// ── Installed apps ────────────────────────────────────────────────────
|
|
installedApps: [],
|
|
|
|
async loadApps() {
|
|
try {
|
|
const res = await fetch('/dock/apps')
|
|
const data = await res.json()
|
|
this.installedApps = data.map(app => ({
|
|
...app,
|
|
url: `/apps/${app.id}`,
|
|
}))
|
|
} catch (e) {
|
|
console.error('dock unreachable:', e)
|
|
}
|
|
},
|
|
|
|
// ── Init ──────────────────────────────────────────────────────────────
|
|
async init() {
|
|
await this.loadApps()
|
|
|
|
// Refresh running status every 10s
|
|
setInterval(() => this.loadApps(), 10_000)
|
|
|
|
// Open bio full-screen on load
|
|
const bio = this.installedApps.find(a => a.id === 'bio')
|
|
if (bio) this.openApp(bio)
|
|
}
|
|
|
|
}))
|
|
})
|