Init Repo

This commit is contained in:
2026-02-28 03:22:04 +08:00
commit de59b57ee7
883 changed files with 156857 additions and 0 deletions
@@ -0,0 +1,372 @@
<template>
<div class="flex flex-col gap-4">
<!-- Main Toolbar Row -->
<div class="flex items-center justify-between gap-4">
<!-- Left Side - Filters -->
<div class="flex flex-wrap gap-2">
<!-- View Toggle -->
<div class="flex items-center border rounded-md h-8 p-0.5">
<Button
variant="ghost"
size="sm"
:class="{ 'bg-muted': viewMode === 'grid' }"
@click="$emit('update:view-mode', 'grid')"
class="h-7 px-2"
>
<LayoutGrid class="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
:class="{ 'bg-muted': viewMode === 'list' }"
@click="$emit('update:view-mode', 'list')"
class="h-7 px-2"
>
<List class="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="sm"
:class="{ 'bg-muted': viewMode === 'table' }"
@click="$emit('update:view-mode', 'table')"
class="h-7 px-2"
>
<Table2 class="h-4 w-4" />
</Button>
</div>
<!-- Episode Filter -->
<Popover v-if="episodes.length > 0">
<PopoverTrigger as-child>
<Button variant="outline" size="sm" class="h-8 border-dashed">
<Film class="mr-2 h-4 w-4" />
Episode
<Badge
v-if="episodeFilter !== null"
variant="secondary"
class="ml-2 rounded-sm px-1 font-normal"
>
1
</Badge>
</Button>
</PopoverTrigger>
<PopoverContent class="w-[200px] p-0" align="start">
<Command>
<CommandInput placeholder="Search episode..." />
<CommandList>
<CommandEmpty>No episode found.</CommandEmpty>
<CommandGroup>
<CommandItem
value="all"
@select="$emit('update:episode-filter', null)"
>
<div
:class="[
'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary',
episodeFilter === null
? 'bg-primary text-primary-foreground'
: 'opacity-50 [&_svg]:invisible'
]"
>
<Check class="h-4 w-4" />
</div>
<span>All Episodes</span>
</CommandItem>
<CommandItem
v-for="episode in episodes"
:key="episode.id"
:value="episode.id.toString()"
@select="$emit('update:episode-filter', episode.id)"
>
<div
:class="[
'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary',
episodeFilter === episode.id
? 'bg-primary text-primary-foreground'
: 'opacity-50 [&_svg]:invisible'
]"
>
<Check class="h-4 w-4" />
</div>
<span>{{ episode.name }}</span>
</CommandItem>
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<!-- Task Status Filter (only for table view) -->
<ShotTaskStatusFilter
v-if="viewMode === 'table'"
:all-task-types="allTaskTypes"
:project-id="projectId"
@filter-changed="$emit('task-status-filter-changed', $event)"
/>
<!-- Column Visibility Control (only for table view) -->
<Popover v-if="viewMode === 'table'">
<PopoverTrigger as-child>
<Button variant="outline" size="sm" class="h-8 border-dashed">
<Settings2 class="mr-2 h-4 w-4" />
View
<Badge
v-if="hiddenColumnsCount > 0"
variant="secondary"
class="ml-2 rounded-sm px-1 font-normal"
>
{{ hiddenColumnsCount }}
</Badge>
</Button>
</PopoverTrigger>
<PopoverContent class="w-[200px] p-0" align="end">
<Command>
<CommandInput placeholder="Search columns..." />
<CommandList>
<CommandEmpty>No columns found.</CommandEmpty>
<CommandGroup>
<CommandSeparator />
<template v-for="column in allColumns">
<CommandItem
v-if="column.type == 'default'"
:key="column.id"
:value="column.id"
@select="toggleColumn(column.id, !(columnVisibility[column.id] !== false))"
>
<div
:class="[
'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary',
columnVisibility[column.id] !== false
? 'bg-primary text-primary-foreground'
: 'opacity-50 [&_svg]:invisible'
]"
>
<Check class="h-4 w-4" />
</div>
<span>{{ column.label }}</span>
</CommandItem>
</template>
</CommandGroup>
<CommandGroup>
<div class="px-2 py-1.5 text-xs font-medium text-muted-foreground">
All Task Types
</div>
<CommandSeparator />
<template v-for="column in allColumns">
<CommandItem
v-if="column.type == 'task'"
:key="column.id"
:value="column.id"
@select="toggleColumn(column.id, !(columnVisibility[column.id] !== false))"
>
<div
:class="[
'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary',
columnVisibility[column.id] !== false
? 'bg-primary text-primary-foreground'
: 'opacity-50 [&_svg]:invisible'
]"
>
<Check class="h-4 w-4" />
</div>
<span>{{ column.label }}</span>
</CommandItem>
</template>
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
<!-- Task Columns Toggle Button (only for table view) -->
<Button
v-if="viewMode === 'table'"
variant="outline"
size="sm"
@click="toggleAllTaskColumns"
class="h-8"
>
<ListTodo v-if="!allTaskColumnsVisible" class="h-4 w-4 mr-2" />
<ListX v-else class="h-4 w-4 mr-2" />
{{ allTaskColumnsVisible ? 'Hide' : 'Show' }} Tasks
</Button>
<!-- Detail Panel Enable/Disable Toggle Button (only for table view) -->
<Button
v-if="viewMode === 'table'"
@click="$emit('toggle-detail-panel')"
:variant="isDetailPanelEnabled ? 'default' : 'outline'"
size="sm"
:class="[
'h-8 w-8 p-0',
isDetailPanelEnabled ? 'bg-primary text-primary-foreground hover:bg-primary/90' : ''
]"
:title="isDetailPanelEnabled ? 'Disable Auto Detail Panel' : 'Enable Auto Detail Panel'"
>
<PanelRightClose v-if="isDetailPanelEnabled" class="h-4 w-4" />
<PanelRightOpen v-else class="h-4 w-4" />
</Button>
<!-- Clear Filters -->
<Button
v-if="hasFilters"
variant="ghost"
size="sm"
class="h-8 px-2 lg:px-3"
@click="clearFilters"
>
Reset
<X class="ml-2 h-4 w-4" />
</Button>
</div>
<!-- Right Side - Search and Actions -->
<div class="flex items-center gap-2 flex-shrink-0">
<!-- Search -->
<div class="relative w-64">
<Search class="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
:model-value="search"
@update:model-value="debouncedSearch"
placeholder="Search shots..."
class="pl-9 h-8"
/>
</div>
<!-- Action Buttons -->
<Button @click="$emit('bulk-create')" variant="outline" size="sm" class="h-8 w-8 p-0">
<Layers class="h-4 w-4" />
</Button>
<Button @click="$emit('create-shot')" size="sm" class="h-8 w-8 p-0">
<Plus class="h-4 w-4" />
</Button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import {
LayoutGrid, List, Table2, Search, Film, Plus, Layers,
PanelRightClose, PanelRightOpen, Check, X, Settings2, ListTodo, ListX
} from 'lucide-vue-next'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Badge } from '@/components/ui/badge'
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover'
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
CommandSeparator,
} from '@/components/ui/command'
import ShotTaskStatusFilter from './ShotTaskStatusFilter.vue'
import type { VisibilityState } from '@tanstack/vue-table'
import type { Episode } from '@/services/episode'
import type { Shot } from '@/services/shot'
interface Props {
viewMode: 'grid' | 'list' | 'table'
episodeFilter: number | null
search: string
columnVisibility: VisibilityState
episodes: Episode[]
allTaskTypes: string[]
projectId: number
selectedShot: Shot | null
isDetailPanelEnabled: boolean
}
const props = defineProps<Props>()
const emit = defineEmits<{
'update:view-mode': [value: 'grid' | 'list' | 'table']
'update:episode-filter': [value: number | null]
'update:search': [value: string]
'update:column-visibility': [value: VisibilityState]
'task-status-filter-changed': [value: string]
'toggle-detail-panel': []
'toggle-task-columns': []
'bulk-create': []
'create-shot': []
}>()
// Column definitions - computed to be reactive to allTaskTypes changes
const allColumns = computed(() => [
{ id: 'thumbnail', label: 'Thumbnail', type: 'default' },
{ id: 'name', label: 'Shot Name', type: 'default' },
{ id: 'episode', label: 'Episode', type: 'default' },
{ id: 'frameRange', label: 'Frame Range', type: 'default' },
{ id: 'frames', label: 'Frames', type: 'default' },
{ id: 'status', label: 'Status', type: 'default' },
{ id: 'description', label: 'Description', type: 'default' },
...props.allTaskTypes.map(taskType => ({
id: taskType,
label: taskType.charAt(0).toUpperCase() + taskType.slice(1),
type: 'task'
})),
])
// Computed
const hasFilters = computed(() => {
return (
props.episodeFilter !== null ||
props.search !== ''
)
})
const hiddenColumnsCount = computed(() => {
return allColumns.value.filter(col => props.columnVisibility[col.id] === false).length
})
// Check if all task columns are visible
const allTaskColumnsVisible = computed(() => {
const taskColumns = allColumns.value.filter(col => col.type === 'task')
return taskColumns.length > 0 && taskColumns.every(col => props.columnVisibility[col.id] !== false)
})
// Debounced search
let searchTimeout: ReturnType<typeof setTimeout> | null = null
const debouncedSearch = (value: string | number) => {
const searchValue = typeof value === 'string' ? value : String(value)
if (searchTimeout) clearTimeout(searchTimeout)
searchTimeout = setTimeout(() => {
emit('update:search', searchValue)
}, 300)
}
// Methods
const toggleColumn = (columnId: string, value: any) => {
const newVisibility = { ...props.columnVisibility, [columnId]: value as boolean }
emit('update:column-visibility', newVisibility)
}
const toggleAllTaskColumns = () => {
const newVisibility = { ...props.columnVisibility }
const taskColumns = allColumns.value.filter(col => col.type === 'task')
// If all task columns are visible, hide them; otherwise show them
const shouldHide = allTaskColumnsVisible.value
taskColumns.forEach(col => {
newVisibility[col.id] = !shouldHide
})
emit('update:column-visibility', newVisibility)
}
const clearFilters = () => {
emit('update:episode-filter', null)
emit('update:search', '')
}
</script>