This commit is contained in:
Mariano Gabriel
2026-07-05 22:59:27 -03:00
parent 8cfb4c2cbe
commit bb84558526
8 changed files with 245 additions and 29 deletions

View File

@@ -45,7 +45,8 @@ const VIDEO_MIME: Record<string, string> = {
}
export function meetusApi(opts: Options): Plugin {
const outputDir = path.resolve(opts.outputDir)
// Mutable so the UI can re-point the scan at another folder at runtime.
let outputDir = path.resolve(opts.outputDir)
const videoDir = opts.videoDir ? path.resolve(opts.videoDir) : null
/** Locate a run's source video. The manifest records an absolute path from
@@ -139,6 +140,22 @@ export function meetusApi(opts: Options): Plugin {
parts: string[],
logger: { warn: (m: string) => void },
): Promise<boolean> {
// GET/PUT the folder scanned (recursively) for meeting runs.
if (parts[0] === 'output') {
if (req.method === 'GET') { sendJson(res, 200, { dir: outputDir }); return true }
if (req.method === 'PUT') {
let body: { dir?: string }
try { body = JSON.parse((await readBody(req)) || '{}') } catch { sendJson(res, 400, { error: 'invalid JSON' }); return true }
const dir = (body.dir ?? '').trim()
try {
if (!dir || !fs.statSync(dir).isDirectory()) throw new Error('not a directory')
} catch { sendJson(res, 400, { error: `not a directory: ${dir}` }); return true }
outputDir = path.resolve(dir)
sendJson(res, 200, { dir: outputDir })
return true
}
}
if (parts[0] !== 'runs') return false
// GET /api/runs

View File

@@ -1,15 +1,23 @@
<script setup lang="ts">
import { onMounted } from 'vue'
import { onMounted, ref } from 'vue'
import { useStore } from '@/store'
const { state, loadRuns, openRun } = useStore()
const { state, loadRuns, openRun, setOutputDir } = useStore()
onMounted(loadRuns)
const showFolder = ref(false)
const folder = ref('')
function onChange(e: Event) {
const id = (e.target as HTMLSelectElement).value
if (id) openRun(id)
}
async function scanFolder() {
await setOutputDir(folder.value)
if (!state.error) showFolder.value = false
}
</script>
<template>
@@ -21,11 +29,21 @@ function onChange(e: Event) {
{{ r.name }} · {{ r.frameCount }} frames{{ r.hasVideo ? '' : ' · no video' }}
</option>
</select>
<button class="folder-btn" :title="`Scan folder — ${state.outputDir ?? ''}`" @click="showFolder = !showFolder">📁</button>
<span v-if="state.loading" class="hint">loading</span>
<span v-else-if="state.error" class="hint err">{{ state.error }}</span>
<span v-else-if="!state.runs.length" class="hint" :title="state.outputDir ?? ''">
no runs found in {{ state.outputDir ?? '(unknown)' }}
</span>
<div v-if="showFolder" class="folder-pop">
<div class="pop-title">Scan folder (recursive) for meetings</div>
<div class="pop-row">
<input v-model="folder" :placeholder="state.outputDir ?? '/path/to/meetings'" @keyup.enter="scanFolder" />
<button :disabled="state.loading || !folder.trim()" @click="scanFolder">Scan</button>
</div>
<div class="pop-cur">current: {{ state.outputDir ?? '(unset)' }}</div>
</div>
</div>
</template>
@@ -34,7 +52,18 @@ function onChange(e: Event) {
display: flex;
align-items: center;
gap: var(--space-2);
position: relative;
}
.folder-btn { padding: var(--space-1) var(--space-2); line-height: 1; }
.folder-pop {
position: absolute; top: 110%; left: 0; z-index: 20; min-width: 340px;
background: var(--surface-1); border: var(--panel-border); border-radius: var(--panel-radius);
padding: var(--space-2); box-shadow: 0 4px 16px rgba(0, 0, 0, 0.35);
}
.pop-title { font-size: 11px; color: var(--text-secondary); margin-bottom: var(--space-1); }
.pop-row { display: flex; gap: var(--space-1); }
.pop-row input { flex: 1; min-width: 0; padding: var(--space-1) var(--space-2); }
.pop-cur { font-size: var(--font-size-sm); color: var(--text-secondary); margin-top: var(--space-1); overflow: hidden; text-overflow: ellipsis; }
.lbl {
color: var(--text-secondary);
font-size: var(--font-size-sm);

Binary file not shown.