- Register .fig and .pen files with desktop bundles - Open associated files on startup and via single-instance handoff - Reuse the existing Tauri file open flow without broad filesystem scopes
214 lines
6.8 KiB
Vue
214 lines
6.8 KiB
Vue
<script setup lang="ts">
|
|
import { onMounted, onUnmounted, provide, ref } from 'vue'
|
|
import { useEventListener, useUrlSearchParams } from '@vueuse/core'
|
|
import { useRoute } from 'vue-router'
|
|
import { useHead } from '@unhead/vue'
|
|
import { SplitterGroup, SplitterPanel, SplitterResizeHandle } from 'reka-ui'
|
|
|
|
import { useViewportKind } from '@open-pencil/vue'
|
|
import { useKeyboard } from '@/app/shell/keyboard/use'
|
|
import { loadEditorLayout, saveEditorLayout } from '@/app/shell/layout-storage'
|
|
import { openFileFromPath, useMenu } from '@/app/shell/menu/use'
|
|
import { useCollab, COLLAB_KEY } from '@/app/collab/use'
|
|
import { connectAutomation } from '@/app/automation/bridge/server'
|
|
import { spawnMCPIfNeeded } from '@/app/automation/mcp/spawn'
|
|
import { isTauri } from '@/app/tauri/env'
|
|
import { createDemoShapes } from '@/app/demo/document'
|
|
import { useEditorStore } from '@/app/editor/active-store'
|
|
import { createTab, activeTab, getActiveStore, tabCount } from '@/app/tabs'
|
|
|
|
import CollabPanel from '@/components/CollabPanel/CollabPanel.vue'
|
|
import EditorCanvas from '@/components/EditorCanvas.vue'
|
|
import LayersPanel from '@/components/LayersPanel.vue'
|
|
import MobileDrawer from '@/components/MobileDrawer.vue'
|
|
import MobileHud from '@/components/MobileHud/MobileHud.vue'
|
|
import PropertiesPanel from '@/components/PropertiesPanel.vue'
|
|
import SafariBanner from '@/components/SafariBanner.vue'
|
|
import TabBar from '@/components/TabBar.vue'
|
|
import Toolbar from '@/components/Toolbar/Toolbar.vue'
|
|
|
|
const route = useRoute()
|
|
const params = useUrlSearchParams('history')
|
|
const showChrome = !('no-chrome' in params)
|
|
|
|
const createdInitialTab = tabCount() === 0
|
|
const firstTab = createdInitialTab ? createTab() : (activeTab.value ?? createTab())
|
|
const store = useEditorStore()
|
|
const { isMobile } = useViewportKind()
|
|
|
|
if (createdInitialTab && route.meta.demo && !('test' in params)) {
|
|
createDemoShapes(firstTab.store)
|
|
}
|
|
|
|
useHead({ title: route.meta.demo ? 'Demo' : undefined })
|
|
useKeyboard()
|
|
useMenu()
|
|
|
|
const collab = useCollab(getActiveStore)
|
|
provide(COLLAB_KEY, collab)
|
|
|
|
useEventListener(
|
|
document,
|
|
'wheel',
|
|
(e: WheelEvent) => {
|
|
if (e.ctrlKey || e.metaKey) e.preventDefault()
|
|
},
|
|
{ passive: false }
|
|
)
|
|
|
|
const automationCleanup = ref<(() => void) | null>(null)
|
|
const mcpCleanup = ref<(() => void) | null>(null)
|
|
const fileAssociationCleanup = ref<(() => void) | null>(null)
|
|
const initialEditorLayout = loadEditorLayout()
|
|
|
|
type PendingOpenFile = {
|
|
path: string
|
|
}
|
|
|
|
async function openPendingAssociatedFiles() {
|
|
const { invoke } = await import('@tauri-apps/api/core')
|
|
const files = await invoke<PendingOpenFile[]>('take_pending_open')
|
|
for (const file of files) {
|
|
await openFileFromPath(file.path)
|
|
}
|
|
}
|
|
|
|
async function bindAssociatedFileOpen() {
|
|
if (!isTauri()) return
|
|
const { listen } = await import('@tauri-apps/api/event')
|
|
fileAssociationCleanup.value = await listen('open-associated-files', () => {
|
|
void openPendingAssociatedFiles().catch((e) => console.error('[Open With]', e))
|
|
})
|
|
await openPendingAssociatedFiles()
|
|
}
|
|
|
|
onMounted(async () => {
|
|
try {
|
|
const mcp = await spawnMCPIfNeeded()
|
|
mcpCleanup.value = mcp?.disconnect ?? null
|
|
const tauri = isTauri()
|
|
if (import.meta.env.DEV || tauri) {
|
|
automationCleanup.value = connectAutomation(getActiveStore, mcp?.authToken ?? null).disconnect
|
|
}
|
|
} catch (e) {
|
|
console.warn('[MCP]', e)
|
|
}
|
|
|
|
try {
|
|
await bindAssociatedFileOpen()
|
|
} catch (e) {
|
|
console.error('[Open With]', e)
|
|
}
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
mcpCleanup.value?.()
|
|
automationCleanup.value?.()
|
|
fileAssociationCleanup.value?.()
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div data-test-id="editor-root" class="flex h-screen w-screen flex-col">
|
|
<SafariBanner />
|
|
<TabBar />
|
|
|
|
<!-- Desktop layout -->
|
|
<SplitterGroup
|
|
v-if="!isMobile && showChrome && store.state.showUI"
|
|
:key="activeTab?.id"
|
|
direction="horizontal"
|
|
class="flex-1 overflow-hidden"
|
|
@layout="saveEditorLayout"
|
|
>
|
|
<SplitterPanel
|
|
id="layers"
|
|
:default-size="initialEditorLayout[0]"
|
|
:min-size="10"
|
|
:max-size="30"
|
|
class="flex"
|
|
>
|
|
<LayersPanel />
|
|
</SplitterPanel>
|
|
<SplitterResizeHandle
|
|
data-test-id="left-splitter-handle"
|
|
class="group relative z-10 -mx-1 w-2 cursor-col-resize"
|
|
>
|
|
<div class="pointer-events-none absolute inset-y-0 left-1/2 w-px -translate-x-1/2" />
|
|
</SplitterResizeHandle>
|
|
<SplitterPanel id="canvas" :default-size="initialEditorLayout[1]" :min-size="30" class="flex">
|
|
<div class="relative flex min-w-0 flex-1">
|
|
<EditorCanvas />
|
|
<Toolbar />
|
|
</div>
|
|
</SplitterPanel>
|
|
<SplitterResizeHandle class="group relative z-10 -mx-1 w-2 cursor-col-resize">
|
|
<div class="pointer-events-none absolute inset-y-0 left-1/2 w-px -translate-x-1/2" />
|
|
</SplitterResizeHandle>
|
|
<SplitterPanel
|
|
id="properties"
|
|
:default-size="initialEditorLayout[2]"
|
|
:min-size="10"
|
|
:max-size="30"
|
|
class="flex flex-col"
|
|
>
|
|
<div
|
|
class="flex shrink-0 items-center justify-between border-b border-border px-1.5 py-1.5"
|
|
>
|
|
<CollabPanel />
|
|
</div>
|
|
<PropertiesPanel />
|
|
</SplitterPanel>
|
|
</SplitterGroup>
|
|
|
|
<!-- Mobile layout -->
|
|
<div
|
|
v-else-if="isMobile && showChrome && store.state.showUI"
|
|
:key="'mobile-' + activeTab?.id"
|
|
class="flex flex-1 overflow-hidden"
|
|
>
|
|
<div class="relative flex min-w-0 flex-1">
|
|
<EditorCanvas />
|
|
<MobileHud />
|
|
<Toolbar />
|
|
</div>
|
|
<MobileDrawer />
|
|
</div>
|
|
|
|
<!-- Collapsed UI (showUI=false) -->
|
|
<div
|
|
v-else-if="showChrome"
|
|
:key="'collapsed-' + activeTab?.id"
|
|
class="flex flex-1 overflow-hidden"
|
|
>
|
|
<div class="relative flex min-w-0 flex-1">
|
|
<EditorCanvas />
|
|
<div
|
|
v-if="!isMobile"
|
|
class="absolute top-7 left-7 z-10 flex items-center gap-2 rounded-lg border border-border bg-panel px-2 py-1 shadow-sm"
|
|
>
|
|
<img src="/favicon-32.png" class="size-4" alt="OpenPencil" />
|
|
<span data-test-id="editor-document-name" class="text-xs text-surface">{{
|
|
store.state.documentName
|
|
}}</span>
|
|
<button
|
|
data-test-id="editor-show-ui"
|
|
class="ml-1 flex size-6 cursor-pointer items-center justify-center rounded text-muted transition-colors hover:bg-hover hover:text-surface"
|
|
title="Show UI (⌘\)"
|
|
@click="store.state.showUI = true"
|
|
>
|
|
<icon-lucide-sidebar class="size-3.5" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Bare canvas (no chrome, e.g. ?no-chrome) -->
|
|
<div v-else :key="'bare-' + activeTab?.id" class="flex flex-1 overflow-hidden">
|
|
<div class="relative flex min-w-0 flex-1">
|
|
<EditorCanvas />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|