embedded meetus
This commit is contained in:
@@ -1,14 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import SplitPane from '@framework/components/SplitPane.vue'
|
||||
import Panel from '@framework/components/Panel.vue'
|
||||
import VideoPlayer from '@framework/components/VideoPlayer.vue'
|
||||
import RunPicker from '@/components/RunPicker.vue'
|
||||
import TranscriptEditor from '@/components/TranscriptEditor.vue'
|
||||
import FrameSelector from '@/components/FrameSelector.vue'
|
||||
import ExportBar from '@/components/ExportBar.vue'
|
||||
import { useStore } from '@/store'
|
||||
|
||||
const { state } = useStore()
|
||||
import ReviewBody from '@/components/ReviewBody.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -20,34 +13,7 @@ const { state } = useStore()
|
||||
</header>
|
||||
|
||||
<div class="body">
|
||||
<SplitPane direction="horizontal" :initialSize="1.4" sizeMode="ratio" :min="0.4" :max="4">
|
||||
<template #first>
|
||||
<SplitPane direction="vertical" :initialSize="1.3" sizeMode="ratio" :min="0.3" :max="4">
|
||||
<template #first>
|
||||
<Panel title="Video" :status="state.playing ? 'live' : 'idle'">
|
||||
<VideoPlayer
|
||||
v-if="state.videoUrl"
|
||||
:src="state.videoUrl"
|
||||
v-model:currentTime="state.currentTime"
|
||||
v-model:playing="state.playing"
|
||||
@duration="state.duration = $event"
|
||||
/>
|
||||
<div v-else class="placeholder">
|
||||
{{ state.currentRunId
|
||||
? 'Source video not available at the path recorded in manifest.json.'
|
||||
: 'Select a meeting to begin.' }}
|
||||
</div>
|
||||
</Panel>
|
||||
</template>
|
||||
<template #second>
|
||||
<FrameSelector />
|
||||
</template>
|
||||
</SplitPane>
|
||||
</template>
|
||||
<template #second>
|
||||
<TranscriptEditor />
|
||||
</template>
|
||||
</SplitPane>
|
||||
<ReviewBody :allow-edit="true" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, nextTick, computed } from 'vue'
|
||||
import Panel from '@framework/components/Panel.vue'
|
||||
import { useStore } from '@/store'
|
||||
import { useStore } from '../store'
|
||||
|
||||
const { state, activeFrameIndex, selectedFrames, seek, setSelectAll } = useStore()
|
||||
|
||||
|
||||
56
ui/meetus-app/src/components/ReviewBody.vue
Normal file
56
ui/meetus-app/src/components/ReviewBody.vue
Normal file
@@ -0,0 +1,56 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* The meetus review — pure composition of video + frames + transcript over the
|
||||
* shared store. Uses only relative + @framework imports so it is portable: the
|
||||
* doocus app embeds this same component (pointed at its own meetus-shaped API).
|
||||
* Config is by prop; all logic lives in the store and child components.
|
||||
*/
|
||||
import SplitPane from '@framework/components/SplitPane.vue'
|
||||
import Panel from '@framework/components/Panel.vue'
|
||||
import VideoPlayer from '@framework/components/VideoPlayer.vue'
|
||||
import TranscriptEditor from './TranscriptEditor.vue'
|
||||
import FrameSelector from './FrameSelector.vue'
|
||||
import { useStore } from '../store'
|
||||
|
||||
withDefaults(defineProps<{ allowEdit?: boolean; embedded?: boolean }>(), {
|
||||
allowEdit: true, embedded: false,
|
||||
})
|
||||
const { state } = useStore()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="review-body">
|
||||
<SplitPane direction="horizontal" :initialSize="1.4" sizeMode="ratio" :min="0.4" :max="4">
|
||||
<template #first>
|
||||
<SplitPane direction="vertical" :initialSize="1.3" sizeMode="ratio" :min="0.3" :max="4">
|
||||
<template #first>
|
||||
<Panel title="Video" :status="state.playing ? 'live' : 'idle'">
|
||||
<VideoPlayer
|
||||
v-if="state.videoUrl"
|
||||
:src="state.videoUrl"
|
||||
v-model:currentTime="state.currentTime"
|
||||
v-model:playing="state.playing"
|
||||
@duration="state.duration = $event"
|
||||
/>
|
||||
<div v-else class="placeholder">
|
||||
{{ state.currentRunId
|
||||
? 'Source video not available at the path recorded in manifest.json.'
|
||||
: 'Select a meeting to begin.' }}
|
||||
</div>
|
||||
</Panel>
|
||||
</template>
|
||||
<template #second><FrameSelector /></template>
|
||||
</SplitPane>
|
||||
</template>
|
||||
<template #second><TranscriptEditor :allow-edit="allowEdit" /></template>
|
||||
</SplitPane>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.review-body { height: 100%; }
|
||||
.placeholder {
|
||||
display: flex; align-items: center; justify-content: center; height: 100%;
|
||||
padding: var(--space-6); text-align: center; color: var(--text-secondary);
|
||||
}
|
||||
</style>
|
||||
@@ -1,13 +1,17 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, nextTick, computed, onMounted } from 'vue'
|
||||
import Panel from '@framework/components/Panel.vue'
|
||||
import { useStore } from '@/store'
|
||||
import { useStore } from '../store'
|
||||
|
||||
const props = withDefaults(defineProps<{ allowEdit?: boolean }>(), { allowEdit: true })
|
||||
|
||||
const { state, activeSegmentIndex, seek } = useStore()
|
||||
|
||||
// View preferences (persisted).
|
||||
// View preferences (persisted). When edit is disallowed (e.g. embedded review),
|
||||
// the transcript is read-only regardless of the saved preference.
|
||||
type ViewMode = 'edit' | 'read'
|
||||
const viewMode = ref<ViewMode>((localStorage.getItem('meetus.viewMode') as ViewMode) || 'edit')
|
||||
const effectiveMode = computed<ViewMode>(() => (props.allowEdit ? viewMode.value : 'read'))
|
||||
const compact = ref(localStorage.getItem('meetus.compact') === '1')
|
||||
const showTimes = ref(localStorage.getItem('meetus.readTimes') !== '0')
|
||||
watch(viewMode, (v) => localStorage.setItem('meetus.viewMode', v))
|
||||
@@ -83,17 +87,17 @@ watch(activeSegmentIndex, async (i) => {
|
||||
<template>
|
||||
<Panel title="Transcript">
|
||||
<template #actions>
|
||||
<div class="seg-control">
|
||||
<button :class="{ on: viewMode === 'edit' }" @click="viewMode = 'edit'">Edit</button>
|
||||
<button :class="{ on: viewMode === 'read' }" @click="viewMode = 'read'">Read</button>
|
||||
<div v-if="allowEdit" class="seg-control">
|
||||
<button :class="{ on: effectiveMode === 'edit' }" @click="viewMode = 'edit'">Edit</button>
|
||||
<button :class="{ on: effectiveMode === 'read' }" @click="viewMode = 'read'">Read</button>
|
||||
</div>
|
||||
<button v-if="viewMode === 'edit'" :class="{ on: compact }" @click="compact = !compact">Compact</button>
|
||||
<button v-if="allowEdit && effectiveMode === 'edit'" :class="{ on: compact }" @click="compact = !compact">Compact</button>
|
||||
<button v-else :class="{ on: showTimes }" @click="showTimes = !showTimes">Times</button>
|
||||
<span class="count">{{ state.segments.length }} segments</span>
|
||||
</template>
|
||||
|
||||
<!-- EDIT: per-segment blocks (comfortable or compact density) -->
|
||||
<div v-if="viewMode === 'edit'" class="seg-list" :class="{ compact }">
|
||||
<div v-if="effectiveMode === 'edit'" class="seg-list" :class="{ compact }">
|
||||
<p v-if="!state.segments.length" class="empty">No transcript for this run.</p>
|
||||
<div
|
||||
v-for="(seg, i) in state.segments"
|
||||
@@ -139,7 +143,7 @@ watch(activeSegmentIndex, async (i) => {
|
||||
class="rseg"
|
||||
:class="{ active: it.i === activeSegmentIndex }"
|
||||
@click="seek(it.start)"
|
||||
>{{ it.text }} </span>
|
||||
>{{ it.text.trim() + ' ' }}</span>
|
||||
</template>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user