473 lines
22 KiB
Vue
473 lines
22 KiB
Vue
<template>
|
||
<Teleport to="body">
|
||
<div
|
||
v-if="jobId"
|
||
class="fixed inset-0 z-50 flex items-start justify-center overflow-y-auto bg-black/70 p-4 sm:p-8"
|
||
@click.self="close"
|
||
>
|
||
<div class="glass w-full max-w-3xl rounded-3xl p-5 sm:p-6">
|
||
<div class="flex items-start justify-between gap-3">
|
||
<div class="min-w-0">
|
||
<p class="text-[11px] uppercase tracking-[0.18em] text-zinc-500">{{ kindLabel }}</p>
|
||
<h2 class="font-display text-2xl font-bold text-zinc-100">{{ draft.name || 'Queued job' }}</h2>
|
||
<p class="mt-1 text-sm text-zinc-400">{{ statusLine }}</p>
|
||
</div>
|
||
<button type="button" class="rounded-full border border-white/10 px-3 py-1 text-sm text-zinc-400 hover:text-white" @click="close">Close</button>
|
||
</div>
|
||
|
||
<p v-if="error" class="mt-4 rounded-2xl border border-red-400/30 bg-red-400/10 px-4 py-3 text-sm text-red-200">{{ error }}</p>
|
||
<p v-else-if="notice" class="mt-4 rounded-2xl border border-amber-300/30 bg-amber-400/10 px-4 py-3 text-sm text-amber-100">{{ notice }}</p>
|
||
<p v-else-if="loading" class="mt-4 text-sm text-zinc-400">Loading job…</p>
|
||
|
||
<div v-if="job" class="mt-5 space-y-4">
|
||
<div v-if="thumbUrl" class="overflow-hidden rounded-2xl border border-white/10 bg-black/40">
|
||
<img :src="thumbUrl" alt="" class="max-h-56 w-full object-contain">
|
||
</div>
|
||
<p v-if="job.lastError" class="rounded-2xl border border-red-400/30 bg-red-400/10 px-4 py-3 text-sm text-red-200">{{ job.lastError }}</p>
|
||
<p v-if="!editable && job.kind !== 'music'" class="text-xs text-zinc-500">This one is generating. You can copy it or save a template. Waiting jobs can be edited here.</p>
|
||
<p v-else-if="!editable && job.kind === 'music'" class="text-xs text-zinc-500">Music that’s already on the GPU can’t be rewritten. Copy it, save a template, or load it into Music.</p>
|
||
|
||
<label class="block text-sm">
|
||
<span class="mb-1 block font-medium text-zinc-300">Name</span>
|
||
<input
|
||
v-model="draft.name"
|
||
class="w-full rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm outline-none ring-amber-300/40 focus:ring-2 disabled:opacity-70"
|
||
:disabled="!editable"
|
||
maxlength="80"
|
||
>
|
||
</label>
|
||
|
||
<template v-if="job.kind === 'music'">
|
||
<label class="block text-sm">
|
||
<span class="mb-1 block font-medium text-zinc-300">Tags</span>
|
||
<textarea v-model="draft.tags" rows="3" class="max-h-32 w-full overflow-y-auto rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm outline-none ring-amber-300/40 [overflow-wrap:anywhere] focus:ring-2 disabled:opacity-70" :disabled="!editable" />
|
||
</label>
|
||
<label class="flex items-center gap-2 text-sm text-zinc-300">
|
||
<input v-model="draft.instrumental" type="checkbox" class="accent-amber-400" :disabled="!editable || isYueGp">
|
||
Instrumental
|
||
</label>
|
||
<label v-if="!draft.instrumental" class="block text-sm">
|
||
<span class="mb-1 block font-medium text-zinc-300">Lyrics</span>
|
||
<textarea v-model="draft.lyrics" rows="8" class="max-h-64 w-full overflow-y-auto rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm outline-none ring-amber-300/40 [overflow-wrap:anywhere] focus:ring-2 disabled:opacity-70" :disabled="!editable" />
|
||
</label>
|
||
</template>
|
||
|
||
<template v-else>
|
||
<label class="block text-sm">
|
||
<span class="mb-1 block font-medium text-zinc-300">Pre-text</span>
|
||
<textarea v-model="draft.promptPre" rows="2" class="max-h-28 w-full overflow-y-auto rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm outline-none ring-amber-300/40 [overflow-wrap:anywhere] focus:ring-2 disabled:opacity-70" :disabled="!editable" />
|
||
</label>
|
||
<label class="block text-sm">
|
||
<span class="mb-1 block font-medium text-zinc-300">{{ shots.length > 1 ? (job.kind === 'edit' ? 'Pass 1' : 'Shot 1') : 'Prompt' }}</span>
|
||
<textarea v-model="draft.prompt" rows="5" class="max-h-48 w-full overflow-y-auto rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm outline-none ring-amber-300/40 [overflow-wrap:anywhere] focus:ring-2 disabled:opacity-70" :disabled="!editable" />
|
||
</label>
|
||
<label class="block text-sm">
|
||
<span class="mb-1 block font-medium text-zinc-300">Post-text</span>
|
||
<textarea v-model="draft.promptPost" rows="2" class="max-h-28 w-full overflow-y-auto rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm outline-none ring-amber-300/40 [overflow-wrap:anywhere] focus:ring-2 disabled:opacity-70" :disabled="!editable" />
|
||
</label>
|
||
<label v-if="job.kind === 'edit'" class="block text-sm">
|
||
<span class="mb-1 block font-medium text-zinc-300">Negative</span>
|
||
<textarea v-model="draft.negative" rows="2" class="max-h-28 w-full overflow-y-auto rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm outline-none ring-amber-300/40 [overflow-wrap:anywhere] focus:ring-2 disabled:opacity-70" :disabled="!editable" />
|
||
</label>
|
||
</template>
|
||
|
||
<div v-if="extraShots.length" class="space-y-3">
|
||
<p class="text-xs text-zinc-500">{{ job.kind === 'edit' ? 'Later passes' : 'Later shots' }} queued with this job.</p>
|
||
<div v-for="(shot, index) in extraShots" :key="index" class="rounded-2xl border border-white/10 bg-zinc-950/50 px-3 py-3">
|
||
<p class="text-sm font-medium text-zinc-200">{{ job.kind === 'edit' ? `Pass ${index + 2}` : `Shot ${index + 2}` }}</p>
|
||
<textarea v-model="shot.prompt" rows="3" class="mt-2 max-h-36 w-full overflow-y-auto rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm outline-none ring-amber-300/40 [overflow-wrap:anywhere] focus:ring-2 disabled:opacity-70" :disabled="!editable" />
|
||
<ImageIterationSettings
|
||
v-if="job.kind === 'edit' && job.payload.v2Mode === 'iterate'"
|
||
:model-value="shot"
|
||
:base-steps="draft.steps"
|
||
:base-cfg="draft.cfg"
|
||
:lora-names="loraNames"
|
||
:engine="job.payload.engine === 'krea' ? 'krea' : 'flux'"
|
||
:allow-strength="job.payload.engine === 'krea' && Boolean(job.payload.stillId)"
|
||
:disabled="!editable"
|
||
@update:model-value="extraShots[index] = { ...$event, duration: shot.duration }"
|
||
/>
|
||
<label v-if="job.kind !== 'edit'" class="mt-2 block text-xs text-zinc-400">
|
||
Duration
|
||
<input v-model.number="shot.duration" type="number" min="0.5" max="120" step="0.1" class="mt-1 w-28 rounded-lg border border-white/10 bg-zinc-950 px-2 py-1 text-sm disabled:opacity-70" :disabled="!editable">
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="grid gap-3 sm:grid-cols-2">
|
||
<label v-if="job.kind !== 'edit' || draft.duration" class="block text-sm">
|
||
<span class="mb-1 block font-medium text-zinc-300">Duration</span>
|
||
<input v-model.number="draft.duration" type="number" :min="isYueGp ? 30 : 0.5" :max="isYueGp ? 150 : 180" :step="isYueGp ? 1 : 0.1" class="w-full rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm disabled:opacity-70" :disabled="!editable">
|
||
</label>
|
||
<label class="block text-sm">
|
||
<span class="mb-1 block font-medium text-zinc-300">Seed</span>
|
||
<input v-model="draft.seed" class="w-full rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm disabled:opacity-70" :disabled="!editable">
|
||
</label>
|
||
<label v-if="isYueGp" class="block text-sm">
|
||
<span class="mb-1 block font-medium text-zinc-300">YuEGP profile</span>
|
||
<select v-model.number="draft.yueProfile" :disabled="!editable" class="w-full rounded-xl border border-white/10 bg-zinc-950 px-3 py-2">
|
||
<option :value="1">1 · Full quality · 16GB</option>
|
||
<option :value="3">3 · Manual lower-memory fallback</option>
|
||
</select>
|
||
</label>
|
||
<label v-if="!isYueGp" class="block text-sm">
|
||
<span class="mb-1 block font-medium text-zinc-300">Steps</span>
|
||
<input v-model.number="draft.steps" type="number" min="1" max="100" class="w-full rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm disabled:opacity-70" :disabled="!editable">
|
||
</label>
|
||
<label v-if="!isYueGp" class="block text-sm">
|
||
<span class="mb-1 block font-medium text-zinc-300">CFG</span>
|
||
<input v-model.number="draft.cfg" type="number" min="0" max="20" step="0.1" class="w-full rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm disabled:opacity-70" :disabled="!editable">
|
||
</label>
|
||
<label v-if="job.kind === 'video'" class="block text-sm">
|
||
<span class="mb-1 block font-medium text-zinc-300">FPS</span>
|
||
<input v-model.number="draft.fps" type="number" class="w-full rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm disabled:opacity-70" :disabled="!editable">
|
||
</label>
|
||
<p v-if="metaLine" class="sm:col-span-2 text-xs text-zinc-500">{{ metaLine }}</p>
|
||
</div>
|
||
|
||
<label v-if="job.kind === 'video'" class="flex items-center gap-2 text-sm text-zinc-300">
|
||
<input v-model="draft.turbo" type="checkbox" class="accent-amber-400" :disabled="!editable">
|
||
Turbo
|
||
</label>
|
||
<label v-if="job.kind === 'video'" class="flex items-center gap-2 text-sm text-zinc-300">
|
||
<input v-model="draft.sound" type="checkbox" class="accent-amber-400" :disabled="!editable">
|
||
Sound
|
||
</label>
|
||
|
||
<div v-if="job.kind !== 'music'">
|
||
<LoraSelect
|
||
v-if="editable"
|
||
:model-value="draft.loraStack"
|
||
:names="loraNames"
|
||
:triggers="loraTriggers"
|
||
:aliases="loraAliases"
|
||
:engine="loraEngine"
|
||
label="LoRA"
|
||
compact
|
||
@update:model-value="draft.loraStack = $event"
|
||
/>
|
||
<p v-else-if="loraLine" class="text-xs text-zinc-500">{{ loraLine }}</p>
|
||
</div>
|
||
|
||
<div class="rounded-2xl border border-white/10 bg-zinc-950/40 p-3">
|
||
<label class="block text-sm">
|
||
<span class="mb-1 block font-medium text-zinc-300">Template name</span>
|
||
<input v-model="presetName" class="w-full rounded-xl border border-white/10 bg-zinc-950 px-3 py-2 text-sm outline-none ring-amber-300/40 focus:ring-2" maxlength="80" placeholder="Save these settings as a preset">
|
||
</label>
|
||
</div>
|
||
|
||
<div class="flex flex-wrap gap-2">
|
||
<button type="button" class="rounded-full border border-white/15 px-3 py-1.5 text-xs text-zinc-200 hover:border-amber-300/40" @click="copyJob">Copy</button>
|
||
<button type="button" class="rounded-full border border-white/15 px-3 py-1.5 text-xs text-zinc-200 hover:border-amber-300/40" :disabled="busy" @click="saveTemplate">Save as template</button>
|
||
<button v-if="editable" type="button" class="rounded-full bg-amber-400 px-3 py-1.5 text-xs font-semibold text-zinc-950 disabled:opacity-50" :disabled="busy" @click="saveChanges">Save changes</button>
|
||
<button type="button" class="rounded-full border border-sky-300/40 px-3 py-1.5 text-xs text-sky-100 hover:border-sky-200" @click="loadAway">{{ job.kind === 'music' ? 'Load into Music' : 'Load into Studio' }}</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Teleport>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import {
|
||
applyQueuedShots,
|
||
LOAD_MUSIC_JOB_KEY,
|
||
LOAD_QUEUED_JOB_KEY,
|
||
queuedJobCopyText,
|
||
queuedJobEditable,
|
||
queuedJobKindLabel,
|
||
queuedJobPresetBody,
|
||
queuedJobShots,
|
||
type QueuedInspectJob,
|
||
type QueuedInspectPayload,
|
||
type QueuedShotDraft
|
||
} from '~/utils/queuedJob'
|
||
import { restorePromptParts } from '~/utils/promptParts'
|
||
import { loraMatchesUse, loraStackLabel, persistLoraFields, readLoraStack, type LoraStackItem } from '~/utils/loras'
|
||
|
||
const props = defineProps<{
|
||
jobId?: string | null
|
||
}>()
|
||
|
||
const emit = defineEmits<{
|
||
close: []
|
||
saved: []
|
||
loaded: []
|
||
}>()
|
||
|
||
const job = ref<QueuedInspectJob | null>(null)
|
||
const loading = ref(false)
|
||
const busy = ref(false)
|
||
const error = ref('')
|
||
const notice = ref('')
|
||
const presetName = ref('')
|
||
const loraNames = ref<string[]>([])
|
||
const loraTriggers = ref<Record<string, string>>({})
|
||
const loraAliases = ref<Record<string, string>>({})
|
||
const draft = reactive({
|
||
name: '',
|
||
prompt: '',
|
||
promptPre: '',
|
||
promptPost: '',
|
||
negative: '',
|
||
tags: '',
|
||
lyrics: '',
|
||
instrumental: false,
|
||
yueProfile: 1 as 1 | 3,
|
||
duration: 5,
|
||
seed: '',
|
||
steps: 20,
|
||
cfg: 1,
|
||
fps: 24,
|
||
turbo: true,
|
||
sound: true,
|
||
loraStack: [] as LoraStackItem[]
|
||
})
|
||
const extraShots = ref<QueuedShotDraft[]>([])
|
||
|
||
const kindLabel = computed(() => job.value ? queuedJobKindLabel(job.value) : 'Job')
|
||
const isYueGp = computed(() => job.value?.kind === 'music' && (job.value.payload.musicEngine || job.value.payload.engine) === 'yue')
|
||
const editable = computed(() => job.value ? queuedJobEditable(job.value) : false)
|
||
const shots = computed(() => job.value ? queuedJobShots(currentPayload(), job.value.kind) : [])
|
||
const thumbUrl = computed(() => {
|
||
const stillId = job.value?.payload?.stillId
|
||
if (!stillId || job.value?.payload?.hideThumbnail || job.value?.payload?.folderLocked) return ''
|
||
return `/api/library/stills/${stillId}`
|
||
})
|
||
const loraLine = computed(() => loraStackLabel(draft.loraStack, loraAliases.value))
|
||
const loraEngine = computed(() => {
|
||
const engine = job.value?.payload?.engine
|
||
return engine === 'flux' || engine === 'krea' ? engine : ''
|
||
})
|
||
const metaLine = computed(() => {
|
||
const payload = job.value?.payload
|
||
if (!payload) return ''
|
||
if (isYueGp.value) return 'Standalone YuEGP · one section · compile off'
|
||
return [
|
||
payload.workflow,
|
||
payload.v2Mode,
|
||
payload.engine,
|
||
payload.aspect,
|
||
payload.samplerName,
|
||
payload.scheduler
|
||
].filter(Boolean).join(' · ')
|
||
})
|
||
const statusLine = computed(() => {
|
||
if (!job.value) return ''
|
||
if (job.value.status === 'waiting') return 'Waiting in the queue'
|
||
if (job.value.status === 'held') return 'Paused'
|
||
if (job.value.status === 'running') return 'Generating'
|
||
if (job.value.status === 'error') return 'Failed'
|
||
return job.value.status
|
||
})
|
||
|
||
function currentPayload(): QueuedInspectPayload {
|
||
const base = { ...(job.value?.payload || {}) }
|
||
base.name = draft.name
|
||
base.prompt = draft.prompt
|
||
base.promptMid = draft.prompt
|
||
base.promptPre = draft.promptPre
|
||
base.promptPost = draft.promptPost
|
||
base.negative = draft.negative
|
||
base.tags = draft.tags
|
||
base.lyrics = draft.lyrics
|
||
base.instrumental = draft.instrumental
|
||
if (isYueGp.value) base.yueProfile = draft.yueProfile
|
||
base.duration = draft.duration
|
||
base.steps = draft.steps
|
||
base.cfg = draft.cfg
|
||
base.fps = draft.fps
|
||
base.turbo = draft.turbo
|
||
base.sound = draft.sound
|
||
const seed = Number(draft.seed)
|
||
if (Number.isFinite(seed)) base.seed = seed
|
||
const fields = persistLoraFields(draft.loraStack)
|
||
base.loraName = fields.loraName
|
||
base.loraStack = fields.loraStack
|
||
if (!job.value) return base
|
||
return applyQueuedShots(job.value.kind, [
|
||
{ prompt: draft.prompt, duration: draft.duration, loraName: fields.loraName, loraStack: fields.loraStack },
|
||
...extraShots.value
|
||
], base)
|
||
}
|
||
|
||
function hydrate(next: QueuedInspectJob) {
|
||
job.value = next
|
||
const payload = next.payload || {}
|
||
draft.name = payload.name || next.name || ''
|
||
const parts = restorePromptParts(payload.promptMid || payload.prompt || next.prompt || '', {
|
||
pre: payload.promptPre,
|
||
prompt: payload.promptMid || payload.prompt || next.prompt || '',
|
||
post: payload.promptPost
|
||
})
|
||
draft.prompt = parts.prompt
|
||
draft.promptPre = parts.pre
|
||
draft.promptPost = parts.post
|
||
draft.negative = payload.negative || ''
|
||
draft.tags = payload.tags || (next.kind === 'music' ? (payload.prompt || next.prompt) : '') || ''
|
||
draft.lyrics = payload.lyrics || ''
|
||
draft.instrumental = payload.instrumental === true
|
||
draft.yueProfile = payload.yueProfile === 3 ? 3 : 1
|
||
draft.duration = Number(payload.duration) || 5
|
||
draft.seed = payload.seed != null ? String(payload.seed) : ''
|
||
draft.steps = Number(payload.steps) || 20
|
||
draft.cfg = Number(payload.cfg) || 1
|
||
draft.fps = Number(payload.fps) || 24
|
||
draft.turbo = payload.turbo !== false
|
||
draft.sound = payload.sound !== false
|
||
draft.loraStack = readLoraStack(payload)
|
||
extraShots.value = queuedJobShots(payload, next.kind).slice(1).map(item => ({ ...item }))
|
||
presetName.value = draft.name || kindLabel.value
|
||
}
|
||
|
||
async function loadJob(id: string) {
|
||
loading.value = true
|
||
error.value = ''
|
||
notice.value = ''
|
||
job.value = null
|
||
try {
|
||
const data = await $fetch<QueuedInspectJob>(`/api/studio-queue/${id}`)
|
||
hydrate(data)
|
||
const listed = await $fetch<{ video?: string[]; image?: string[] }>('/api/loras').catch(() => ({ video: [], image: [] }))
|
||
const library = await $fetch<{ loraTriggers?: Record<string, string>; loraAliases?: Record<string, string> }>('/api/library?light=1').catch(() => ({}))
|
||
if (library.loraTriggers) loraTriggers.value = library.loraTriggers
|
||
if (library.loraAliases) loraAliases.value = library.loraAliases
|
||
const video = listed.video || []
|
||
const image = listed.image || []
|
||
if (data.kind === 'edit') {
|
||
const engine = data.payload?.engine === 'krea' ? 'krea' : data.payload?.engine === 'flux' ? 'flux' : ''
|
||
loraNames.value = engine
|
||
? image.filter(name => loraMatchesUse(name, engine))
|
||
: image
|
||
} else {
|
||
loraNames.value = video.filter(name => loraMatchesUse(name, 'minimax') || loraMatchesUse(name, 'ltx'))
|
||
}
|
||
} catch (err: any) {
|
||
error.value = err?.data?.statusMessage || err?.statusMessage || err?.message || 'Could not load that job'
|
||
} finally {
|
||
loading.value = false
|
||
}
|
||
}
|
||
|
||
async function copyJob() {
|
||
if (!job.value) return
|
||
const text = queuedJobCopyText({ ...job.value, name: draft.name, prompt: draft.prompt, payload: currentPayload() })
|
||
try {
|
||
await navigator.clipboard.writeText(text)
|
||
notice.value = 'Copied to the clipboard.'
|
||
} catch {
|
||
error.value = 'Could not copy. Select the fields and copy them yourself.'
|
||
}
|
||
}
|
||
|
||
async function saveTemplate() {
|
||
if (!job.value) return
|
||
const name = presetName.value.trim()
|
||
if (!name) {
|
||
error.value = 'Name the template first.'
|
||
return
|
||
}
|
||
busy.value = true
|
||
error.value = ''
|
||
try {
|
||
await $fetch('/api/generation-presets', {
|
||
method: 'POST',
|
||
body: queuedJobPresetBody({ ...job.value, name: draft.name, payload: currentPayload() }, name)
|
||
})
|
||
notice.value = `Saved template “${name}”.`
|
||
} catch (err: any) {
|
||
error.value = err?.data?.statusMessage || err?.statusMessage || err?.message || 'Could not save that template'
|
||
} finally {
|
||
busy.value = false
|
||
}
|
||
}
|
||
|
||
async function saveChanges() {
|
||
if (!job.value || !editable.value) return
|
||
busy.value = true
|
||
error.value = ''
|
||
try {
|
||
const payload = currentPayload()
|
||
const saved = await $fetch<QueuedInspectJob>(`/api/studio-queue/${job.value.id}`, {
|
||
method: 'PATCH',
|
||
body: {
|
||
name: payload.name,
|
||
prompt: job.value.kind === 'music' ? payload.tags : payload.prompt,
|
||
promptMid: job.value.kind === 'music' ? undefined : payload.prompt,
|
||
tags: payload.tags,
|
||
lyrics: payload.lyrics,
|
||
instrumental: payload.instrumental,
|
||
...(isYueGp.value ? { yueProfile: payload.yueProfile } : {}),
|
||
promptPre: payload.promptPre,
|
||
promptPost: payload.promptPost,
|
||
negative: payload.negative,
|
||
duration: payload.duration,
|
||
seed: payload.seed,
|
||
steps: payload.steps,
|
||
cfg: payload.cfg,
|
||
fps: payload.fps,
|
||
turbo: payload.turbo,
|
||
sound: payload.sound,
|
||
loraStack: payload.loraStack,
|
||
extensions: payload.extensions,
|
||
passes: payload.passes
|
||
}
|
||
})
|
||
hydrate({ ...job.value, ...saved, payload: saved.payload || payload, source: 'studio' })
|
||
notice.value = 'Saved. The queue will use this the next time it runs.'
|
||
emit('saved')
|
||
} catch (err: any) {
|
||
error.value = err?.data?.statusMessage || err?.statusMessage || err?.message || 'Could not save those changes'
|
||
} finally {
|
||
busy.value = false
|
||
}
|
||
}
|
||
|
||
function loadAway() {
|
||
if (!job.value) return
|
||
try {
|
||
if (job.value.kind === 'music') {
|
||
sessionStorage.setItem(LOAD_MUSIC_JOB_KEY, JSON.stringify({
|
||
name: draft.name,
|
||
tags: draft.tags,
|
||
lyrics: draft.lyrics,
|
||
instrumental: draft.instrumental,
|
||
duration: draft.duration,
|
||
steps: draft.steps,
|
||
seed: draft.seed,
|
||
folderId: job.value.payload.folderId || ''
|
||
}))
|
||
if (window.location.pathname !== '/music') {
|
||
void navigateTo('/music')
|
||
}
|
||
} else {
|
||
sessionStorage.setItem(LOAD_QUEUED_JOB_KEY, job.value.id)
|
||
if (window.location.pathname !== '/') {
|
||
void navigateTo('/')
|
||
}
|
||
}
|
||
} catch { /* ignore */ }
|
||
emit('loaded')
|
||
close()
|
||
}
|
||
|
||
function close() {
|
||
emit('close')
|
||
}
|
||
|
||
function onKey(event: KeyboardEvent) {
|
||
if (event.key === 'Escape' && props.jobId) {
|
||
event.preventDefault()
|
||
close()
|
||
}
|
||
}
|
||
|
||
watch(() => props.jobId, (id) => {
|
||
if (id) void loadJob(id)
|
||
else job.value = null
|
||
}, { immediate: true })
|
||
|
||
onMounted(() => window.addEventListener('keydown', onKey))
|
||
onBeforeUnmount(() => window.removeEventListener('keydown', onKey))
|
||
</script>
|