@@ -0,0 +1,543 @@
< template >
< div class = "m-shell" data -mobile = " 1 " >
< header class = "m-header" >
< span class = "m-brand" > { { ctx . instanceName } } < / span >
< label class = "m-crumb" >
< select v-model = "ctx.form.folderId" aria-label="Folder" >
< option v-for = "f in ctx.library.folders" :key="f.id" :value="f.id" > {{ f.name }} < / option >
< / select >
< / label >
< span class = "m-grow" / >
< span class = "m-gpu" :data-tone = "ctx.gpu.tone" :title = "ctx.gpu.detail" > ● { { ctx . gpuChipLabel } } < / span >
< button type = "button" class = "m-chip" @click ="toggleSheet('queue')" > Queue {{ ctx.queueCount }} < / button >
< details class = "m-more" >
< summary aria -label = " More " > ⋯ < / summary >
< div class = "m-more-menu" >
< NuxtLink to = "/library" @click ="closeMenus" > Library < / NuxtLink >
< NuxtLink to = "/queue" @click ="closeMenus" > Queue < / NuxtLink >
< NuxtLink to = "/settings" @click ="closeMenus" > Settings < / NuxtLink >
< / div >
< / details >
< / header >
< div class = "m-chips" role = "tablist" aria -label = " Tasks " >
< button
v-for = "task in ctx.tasks"
:key = "task"
type = "button"
class = "m-chip capitalize"
: class = "{ on: ctx.form.mode===task }"
:aria-pressed = "ctx.form.mode===task"
@click ="ctx.chooseTask(task)"
> { { task } } < / button >
< / div >
< div v-if = "ctx.form.mode!=='music'" class="m-chips" role="group" aria-label="Model" >
< button
v-for = "opt in ctx.engineOptions"
:key = "opt.value"
type = "button"
class = "m-chip"
: class = "{ on: ctx.form.engine===opt.value }"
:disabled = "opt.disabled"
: title = "opt.hint || undefined"
:aria-pressed = "ctx.form.engine===opt.value"
@click ="!opt.disabled && (ctx.form.engine=opt.value)"
> { { opt . label } } < / button >
< / div >
< div v-else class = "m-chips" > < span class = "m-chip on" > YuE2 < / span > < / div >
< div class = "m-stage" >
< button
v-if = "ctx.selected && ctx.xaigen"
type = "button"
class = "m-eye"
: aria -label = " ctx.isHidden ( ctx.selected ) ? ' Show preview ' : ' Hide preview ' "
@click.stop ="ctx.toggleReveal(ctx.selected.id)"
>
< img : src = "ctx.isHidden(ctx.selected) ? '/icons/display/eye-off.svg' : '/icons/display/eye.svg'" alt = "" width = "16" height = "16" >
< / button >
< template v-if = "ctx.selected && ctx.isHidden(ctx.selected)" >
< p class = "m-muted" > { { ctx . selected . kind === 'track' ? 'Track loaded · preview hidden' : 'Preview hidden' } } < / p >
< / template >
< template v-else-if = "ctx.selected?.kind==='track'" >
< div class = "m-audio" >
< AudioPlayer :src = "`/api/library/tracks/${ctx.selected.id}/audio`" : download -name = " ` $ { ctx.selected.name | | ' track ' } . $ { ctx.selected.ext | | ' wav ' } ` " : label = "ctx.selected.name || 'Track'" / >
< / div >
< / template >
< video
v-else-if = "ctx.selected?.kind==='clip'"
:key = "ctx.selected.id"
:src = "`/api/library/clips/${ctx.selected.id}/video`"
controls
playsinline
/ >
< img v-else-if = "ctx.selected" :src="ctx.stillUrl(ctx.selected.id)" alt="Selected still" >
< p v-else class = "m-muted" > Select a still or clip < / p >
< / div >
< div class = "m-strip" aria -label = " Family outputs " >
< div
v-for = "o in ctx.family"
:key = "o.id"
class = "m-tile"
: class = "{ on: ctx.selected?.id===o.id }"
>
< button type = "button" class = "m-tile-pick" @click ="ctx.selectOutput(o)" >
< span v-if = "ctx.isHidden(o)" class="m-tile-hid" > Hidden < / span >
< span v-else-if = "o.kind==='track'" class="m-tile-hid" > Audio < / span >
< img v-else : src = "o.kind==='clip' ? `/api/library/clips/${o.id}/thumb` : ctx.stillUrl(o.id)" alt = "" >
< / button >
< button
v-if = "ctx.xaigen"
type = "button"
class = "m-tile-eye"
: aria -label = " ctx.isHidden ( o ) ? ' Show tile ' : ' Hide tile ' "
@click.stop ="ctx.toggleReveal(o.id)"
>
< img : src = "ctx.isHidden(o) ? '/icons/display/eye-off.svg' : '/icons/display/eye.svg'" alt = "" width = "12" height = "12" >
< / button >
< button
v-if = "ctx.selected?.id===o.id"
type = "button"
class = "m-tile-trash"
aria -label = " Delete selected "
@click.stop ="ctx.deleteOutput"
> 🗑 < / button >
< / div >
< / div >
< div class = "m-dock" >
< button type = "button" : class = "{ on: sheet==='slots' }" @click ="toggleSheet('slots')" > Slots < / button >
< button type = "button" : class = "{ on: sheet==='prompt' }" @click ="toggleSheet('prompt')" > Prompt < / button >
< button type = "button" : class = "{ on: sheet==='look' }" @click ="toggleSheet('look')" > Look < / button >
< button type = "button" : class = "{ on: sheet==='gen' }" @click ="toggleSheet('gen')" > Gen < / button >
< button type = "button" class = "m-cta" :disabled = "ctx.submitting" @click ="ctx.queue" > {{ ctx.submitting ? ' … ' : ' Generate ' }} < / button >
< / div >
< ! - - Sheets - - >
< div v-if = "sheet" class="m-sheet-backdrop" @click="sheet=''" / >
< aside v-if = "sheet==='slots'" class="m-sheet" role="dialog" aria-label="Slots" >
< button type = "button" class = "m-handle" aria -label = " Close " @click ="sheet=''" / >
< div class = "m-sheet-head" > < strong > Slots < / strong > < button type = "button" @click ="sheet=''" > Done < / button > < / div >
< div class = "m-sheet-body" >
< template v-if = "ctx.form.mode==='music'" >
< p class = "m-muted" > Music uses tags + lyrics in Prompt / Gen — no hero slot . < / p >
< / template >
< template v-else >
< Studio2AssetSlot size = "hero" label = "Hero still" :asset = "ctx.stills.find((a:any)=>a.id===ctx.form.identityStillId)" :hidden = "ctx.slotHidden(ctx.form.identityStillId)" upload :disabled = "ctx.uploading" @pick ="ctx.setPicker('identityStillId')" @upload ="ctx.upload($event,'hero')" / >
< Studio2AssetSlot : label = "ctx.form.mode==='extend'?'Previous clip':'Start still'" :asset = "ctx.sourceSlot" :hidden = "ctx.slotHidden(ctx.form.mode==='extend'?ctx.form.startClipId:ctx.form.imageAId)" upload :disabled = "ctx.uploading" @pick ="ctx.setPicker(ctx.form.mode==='extend'?'startClipId':'imageAId')" @upload ="ctx.upload($event,'source')" / >
< Studio2AssetSlot v-if = "ctx.form.mode==='compose'" label="Second still" :asset="ctx.stills.find((a:any)=>a.id===ctx.form.imageBId)" :hidden="ctx.slotHidden(ctx.form.imageBId)" @pick="ctx.setPicker('imageBId')" / >
< Studio2AssetSlot v-if = "ctx.form.mode==='refine'" label="Mask" :asset="ctx.stills.find((a:any)=>a.id===ctx.form.maskId)" :hidden="ctx.slotHidden(ctx.form.maskId)" @pick="ctx.setPicker('maskId')" / >
< Studio2AssetSlot v-if = "ctx.video" label="End still" :asset="ctx.stills.find((a:any)=>a.id===ctx.form.endStillId)" :hidden="ctx.slotHidden(ctx.form.endStillId)" @pick="ctx.setPicker('endStillId')" / >
< label class = "m-check" > < input v-model = "ctx.form.lockFace" type="checkbox" > Lock face < / label >
< label class = "m-check" > < input v-model = "ctx.form.lockOutfit" type="checkbox" > Lock outfit < / label >
< p class = "m-hint" > hero encode + lock text — not a MiniMax face / clothes split on stills < / p >
< p v-if = "ctx.uploading || ctx.uploadMessage" class="m-amber" > {{ ctx.uploading ? ' Uploading … ' : ctx.uploadMessage }} < / p >
< / template >
< / div >
< / aside >
< aside v-if = "sheet==='prompt'" class="m-sheet" role="dialog" aria-label="Prompt" >
< button type = "button" class = "m-handle" aria -label = " Close " @click ="sheet=''" / >
< div class = "m-sheet-head" > < strong > Prompt < / strong > < button type = "button" @click ="sheet=''" > Done < / button > < / div >
< div class = "m-sheet-body" >
< template v-if = "ctx.form.mode==='music'" >
< label class = "m-label" > Genre / style tags < textarea v-model = "ctx.music.tags" rows="3" class="m-area" / > < / label >
< label class = "m-label" > Lyrics < textarea v-model = "ctx.music.lyrics" rows="8" class="m-area" / > < / label >
< / template >
< template v-else >
< label class = "m-label" > Prompt < textarea v-model = "ctx.form.promptSections.action" rows="5" class="m-area" placeholder="Describe the image or the shot." / > < / label >
< div >
< span class = "m-label" > Negative prompt < / span >
< div class = "m-neg" >
< span v-for = "chip in ctx.negativeChips" :key="chip" class="m-neg-chip" >
{{ chip }}
< button type = "button" : aria -label = " ' Remove ' + chip " @click ="ctx.removeNegative(chip)" > × < / button >
< / span >
< / div >
< / div >
< Studio2StyleChips v-if = "!ctx.video" v-model="ctx.form.imageStyles" / >
< / template >
< p v-if = "ctx.error" class="m-err" > {{ ctx.error }} < / p >
< / div >
< / aside >
< aside v-if = "sheet==='look'" class="m-sheet" role="dialog" aria-label="Look" >
< button type = "button" class = "m-handle" aria -label = " Close " @click ="sheet=''" / >
< div class = "m-sheet-head" > < strong > Look < / strong > < button type = "button" @click ="sheet=''" > Done < / button > < / div >
< div class = "m-sheet-body" >
< template v-if = "ctx.form.mode==='music'" >
< label class = "m-label" > Seed < input v-model = "ctx.music.seed" class="m-input" placeholder="random" > < / label >
< label class = "m-label" > Track name < input v-model = "ctx.music.name" class="m-input" placeholder="Optional" > < / label >
< / template >
< template v-else >
< div >
< span class = "m-label" > LoRA < / span >
< LoraSelect v-model = "ctx.form.settings.loraStack" compact :names="ctx.pickerLoras" :use="ctx.currentLoraUse" :clip-strength="!ctx.video" filename-subtitle :aliases="ctx.loraAliases" :triggers="ctx.loraTriggers" missing-label="None" @insert-trigger="ctx.form.promptSections.action += ' '+$event" / >
< / div >
< label class = "m-label" > Seed
< div class = "m-seed" >
< input
: value = "ctx.form.settings.seed ?? ''"
type = "number"
min = "0"
max = "2147483647"
class = "m-input"
@input ="ctx.form.settings.seed=Number(($event.target as HTMLInputElement).value);ctx.form.settings.seedMode='locked'"
>
< button type = "button" class = "m-dice" aria -label = " Random seed " @click ="ctx.randomizeSeed" > 🎲 < / button >
< / div >
< / label >
< label class = "m-label" > Aspect
< select v-model = "ctx.form.settings.aspect" class="m-input" >
< option v-for = "opt in ctx.aspectOptions" :key="opt.id" :value="opt.id" > {{ opt.label }} < / option >
< / select >
< / label >
< label class = "m-label" > Steps < input v -model .number = " ctx.form.settings.steps " type = "number" class = "m-input" > < / label >
< label class = "m-label" > CFG < input v -model .number = " ctx.form.settings.cfg " type = "number" class = "m-input" > < / label >
< / template >
< / div >
< / aside >
< aside v-if = "sheet==='gen'" class="m-sheet" role="dialog" aria-label="Gen extras" >
< button type = "button" class = "m-handle" aria -label = " Close " @click ="sheet=''" / >
< div class = "m-sheet-head" > < strong > Gen < / strong > < button type = "button" @click ="sheet=''" > Done < / button > < / div >
< div class = "m-sheet-body" >
< template v-if = "ctx.form.mode==='music'" >
< label class = "m-label" > Target length ( sec ) < input v -model .number = " ctx.music.duration " type = "number" min = "15" max = "150" class = "m-input" > < / label >
< p class = "m-hint" > Caps semantic tokens ( ~ 25 / s ) . Actual length follows the model end token . < / p >
< / template >
< template v-else-if = "ctx.video" >
< label class = "m-label" > Duration ( sec ) < input v -model .number = " ctx.form.settings.duration " type = "number" class = "m-input" > < / label >
< label class = "m-label" > FPS < input v -model .number = " ctx.form.settings.fps " type = "number" class = "m-input" > < / label >
< label class = "m-check" > < input v-model = "ctx.form.settings.turbo" type="checkbox" > Turbo < / label >
< Studio2PromptControls v-model = "ctx.form.promptSections" / >
< details class = "m-details" >
< summary > Queue more shots < / summary >
< textarea :value = "ctx.batchText" rows = "4" class = "m-area" placeholder = "First additional prompt Next" @input ="ctx.setBatchText(($event.target as HTMLTextAreaElement).value)" / >
< / details >
< / template >
< template v-else >
< p class = "m-muted" > Image extras live in Look ( steps / CFG / seed / aspect ) . < / p >
< details v-if = "ctx.form.mode==='edit'" class="m-details" >
< summary > Queue extra edit passes < / summary >
< textarea :value = "ctx.batchText" rows = "4" class = "m-area" @input ="ctx.setBatchText(($event.target as HTMLTextAreaElement).value)" / >
< / details >
< / template >
< / div >
< / aside >
< aside v-if = "sheet==='queue'" class="m-sheet" role="dialog" aria-label="Queue" >
< button type = "button" class = "m-handle" aria -label = " Close " @click ="sheet=''" / >
< div class = "m-sheet-head" > < strong > Queue < / strong > < button type = "button" @click ="sheet=''" > Done < / button > < / div >
< div class = "m-sheet-body" >
< div
v-for = "card in ctx.queueCards"
:key = "card.id"
class = "m-job"
: class = "{ on: ctx.focusedJobId===card.id }"
@click ="ctx.focusQueueCard(card)"
>
< div class = "m-job-id" > { { card . code } } < / div >
< div class = "m-job-title" > { { card . title } } < / div >
< div class = "m-muted" > { { card . meta } } < / div >
< div class = "m-job-status" >
< span > ● { { card . statusLabel } } < / span >
< button v-if = "card.cancelable" type="button" @click.stop="ctx.cancel(card.job)" > Cancel < / button >
< / div >
< / div >
< p v-if = "!ctx.queueCards.length" class="m-muted" > No active jobs < / p >
< button v-if = "ctx.canClearFinished" type="button" class="m-link" @click="ctx.clearFinished" > Clear finished < / button >
< / div >
< / aside >
< / div >
< / template >
< script setup lang = "ts" >
export type Studio2MobileCtx = Record < string , any >
const props = defineProps < { ctx : Studio2MobileCtx } > ( )
const sheet = ref ( '' )
function toggleSheet ( id : string ) {
sheet . value = sheet . value === id ? '' : id
}
function closeMenus ( ) {
sheet . value = ''
}
watch ( ( ) => props . ctx . form . mode , ( ) => { sheet . value = '' } )
< / script >
< style scoped >
. m - shell {
-- bg : # 0b0 b0d ;
-- panel : # 141416 ;
-- line : # 2 a2a2e ;
-- amber : # f5a524 ;
-- amber - on : # 1 a1204 ;
-- text : # f3f1ea ;
-- muted : # 9 a9588 ;
position : relative ;
display : flex ;
height : 100 dvh ;
max - height : 100 dvh ;
flex - direction : column ;
overflow : hidden ;
background : var ( -- bg ) ;
color : var ( -- text ) ;
font : 14 px ui - sans - serif , system - ui , sans - serif ;
}
. m - header {
display : flex ;
height : 48 px ;
flex - shrink : 0 ;
align - items : center ;
gap : 8 px ;
border - bottom : 1 px solid var ( -- line ) ;
padding : 0 12 px ;
}
. m - brand { font - weight : 700 ; }
. m - crumb select {
max - width : 7 rem ;
border : 0 ;
background : transparent ;
color : var ( -- muted ) ;
font - size : 12 px ;
}
. m - grow { flex : 1 ; }
. m - gpu {
border : 1 px solid # 2 d4a2a ;
border - radius : 999 px ;
padding : 3 px 8 px ;
color : # b8e0a8 ;
font - size : 11 px ;
}
. m - gpu [ data - tone = 'busy' ] { border - color : color - mix ( in srgb , var ( -- amber ) 40 % , transparent ) ; color : var ( -- amber ) ; }
. m - chips {
display : flex ;
flex - shrink : 0 ;
gap : 6 px ;
overflow - x : auto ;
padding : 8 px 12 px ;
}
. m - chip {
flex : 0 0 auto ;
border : 1 px solid var ( -- line ) ;
border - radius : 8 px ;
background : # 1 a1a1d ;
padding : 6 px 10 px ;
color : var ( -- muted ) ;
font : inherit ;
}
. m - chip . on , . m - dock button . on {
border - color : var ( -- amber ) ;
background : var ( -- amber ) ;
color : var ( -- amber - on ) ;
font - weight : 700 ;
}
. m - chip : disabled { opacity : 0.45 ; }
. m - more { position : relative ; }
. m - more summary { list - style : none ; cursor : pointer ; padding : 4 px 8 px ; color : var ( -- muted ) ; }
. m - more - menu {
position : absolute ;
right : 0 ;
z - index : 20 ;
display : flex ;
min - width : 8 rem ;
flex - direction : column ;
gap : 4 px ;
border : 1 px solid var ( -- line ) ;
border - radius : 8 px ;
background : var ( -- panel ) ;
padding : 8 px ;
}
. m - more - menu a { color : var ( -- text ) ; text - decoration : none ; padding : 6 px 8 px ; }
. m - stage {
position : relative ;
display : flex ;
flex : 1 ;
min - height : 0 ;
min - width : 0 ;
align - items : center ;
justify - content : center ;
overflow : hidden ;
background : # 080808 ;
}
. m - stage > img ,
. m - stage > video {
width : 100 % ;
height : 100 % ;
max - width : 100 % ;
max - height : 100 % ;
object - fit : contain ;
}
. m - audio { width : 100 % ; max - width : 100 % ; padding : 12 px ; }
. m - eye {
position : absolute ;
top : 8 px ;
right : 8 px ;
z - index : 5 ;
display : grid ;
width : 28 px ;
height : 28 px ;
place - items : center ;
border : 1 px solid var ( -- line ) ;
border - radius : 6 px ;
background : # 000 a ;
}
. m - strip {
display : flex ;
height : 64 px ;
flex - shrink : 0 ;
gap : 6 px ;
overflow - x : auto ;
border - top : 1 px solid var ( -- line ) ;
padding : 8 px 12 px ;
}
. m - tile {
position : relative ;
width : 72 px ;
height : 48 px ;
flex : 0 0 auto ;
overflow : hidden ;
border : 2 px solid transparent ;
border - radius : 6 px ;
background : # 222 ;
}
. m - tile . on { border - color : var ( -- amber ) ; }
. m - tile - pick { display : block ; width : 100 % ; height : 100 % ; border : 0 ; padding : 0 ; background : transparent ; }
. m - tile img { width : 100 % ; height : 100 % ; object - fit : cover ; }
. m - tile - hid { display : grid ; height : 100 % ; place - items : center ; color : var ( -- muted ) ; font - size : 10 px ; }
. m - tile - eye , . m - tile - trash {
position : absolute ;
top : 2 px ;
right : 2 px ;
display : grid ;
width : 16 px ;
height : 16 px ;
place - items : center ;
border : 0 ;
border - radius : 3 px ;
background : # 000 a ;
font - size : 10 px ;
}
. m - tile - trash { right : 20 px ; }
. m - dock {
display : grid ;
grid - template - columns : repeat ( 4 , 1 fr ) 120 px ;
flex - shrink : 0 ;
gap : 6 px ;
border - top : 1 px solid var ( -- line ) ;
background : var ( -- panel ) ;
padding : 8 px 8 px calc ( 8 px + env ( safe - area - inset - bottom ) ) ;
}
. m - dock button {
height : 48 px ;
border : 1 px solid var ( -- line ) ;
border - radius : 8 px ;
background : # 1 a1a1d ;
color : var ( -- text ) ;
font : inherit ;
}
. m - cta {
background : var ( -- amber ) ! important ;
color : var ( -- amber - on ) ! important ;
font - weight : 700 ! important ;
}
. m - cta : disabled { opacity : 0.5 ; }
. m - sheet - backdrop {
position : absolute ;
inset : 0 ;
z - index : 30 ;
background : # 0008 ;
}
. m - sheet {
position : absolute ;
right : 0 ;
bottom : 0 ;
left : 0 ;
z - index : 31 ;
display : flex ;
max - height : min ( 85 vh , 100 % ) ;
flex - direction : column ;
border - top : 1 px solid var ( -- line ) ;
border - radius : 16 px 16 px 0 0 ;
background : var ( -- panel ) ;
padding : 10 px 14 px 20 px ;
}
. m - handle {
display : block ;
width : 40 px ;
height : 4 px ;
margin : 4 px auto 12 px ;
border : 0 ;
border - radius : 4 px ;
background : # 444 ;
}
. m - sheet - head {
display : flex ;
flex - shrink : 0 ;
align - items : center ;
justify - content : space - between ;
margin - bottom : 8 px ;
}
. m - sheet - head button { border : 0 ; background : transparent ; color : var ( -- amber ) ; }
. m - sheet - body {
flex : 1 ;
min - height : 0 ;
overflow : auto ;
display : flex ;
flex - direction : column ;
gap : 10 px ;
}
. m - label { display : block ; font - size : 11 px ; color : var ( -- muted ) ; }
. m - area , . m - input {
width : 100 % ;
margin - top : 4 px ;
border : 1 px solid var ( -- line ) ;
border - radius : 8 px ;
background : # 1 a1a1d ;
padding : 8 px ;
color : var ( -- text ) ;
font : inherit ;
}
. m - check { display : flex ; align - items : center ; gap : 8 px ; font - size : 13 px ; color : var ( -- text ) ; }
. m - hint , . m - muted { color : var ( -- muted ) ; font - size : 12 px ; }
. m - amber { color : var ( -- amber ) ; font - size : 12 px ; }
. m - err { color : # fca5a5 ; font - size : 12 px ; }
. m - neg { display : flex ; flex - wrap : wrap ; gap : 6 px ; margin - top : 4 px ; }
. m - neg - chip {
display : inline - flex ;
align - items : center ;
gap : 4 px ;
border : 1 px solid # 5 a4a28 ;
border - radius : 999 px ;
background : # 2 a2418 ;
padding : 4 px 10 px ;
color : # e8c37a ;
font - size : 12 px ;
}
. m - neg - chip button { border : 0 ; background : transparent ; color : inherit ; }
. m - seed { display : flex ; gap : 8 px ; margin - top : 4 px ; }
. m - seed . m - input { margin - top : 0 ; flex : 1 ; }
. m - dice {
width : 40 px ;
height : 40 px ;
border : 1 px solid var ( -- line ) ;
border - radius : 8 px ;
background : # 1 a1a1d ;
}
. m - details { border : 1 px solid var ( -- line ) ; border - radius : 8 px ; padding : 8 px ; }
. m - job {
border : 1 px solid var ( -- line ) ;
border - radius : 8 px ;
padding : 10 px ;
cursor : pointer ;
}
. m - job . on { border - color : var ( -- amber ) ; border - width : 2 px ; }
. m - job - id { color : var ( -- muted ) ; font - size : 12 px ; }
. m - job - title { font - weight : 600 ; }
. m - job - status { display : flex ; justify - content : space - between ; margin - top : 6 px ; font - size : 12 px ; }
. m - link { border : 0 ; background : transparent ; color : var ( -- amber ) ; text - align : left ; }
< / style >