This commit is contained in:
sansen
2026-07-29 10:46:19 +08:00
parent 2c4dd55f11
commit 83f5bb8b30
14 changed files with 593 additions and 167 deletions
+34 -3
View File
@@ -12,9 +12,9 @@
<div class="workspace-column">
<main class="workspace">
<EncodePage v-if="currentView === 'encode'" @taskAdded="onTaskAdded" />
<RemuxPage v-if="currentView === 'remux'" @taskAdded="onTaskAdded" />
<BurnPage v-if="currentView === 'burn'" @taskAdded="onTaskAdded" />
<EncodePage v-if="currentView === 'encode'" :gpuInfo="gpuInfo" @taskAdded="onTaskAdded" />
<RemuxPage v-if="currentView === 'remux'" :gpuInfo="gpuInfo" @taskAdded="onTaskAdded" />
<BurnPage v-if="currentView === 'burn'" :gpuInfo="gpuInfo" @taskAdded="onTaskAdded" />
<SettingsPage v-if="currentView === 'settings'" />
<LogPage v-show="currentView === 'logs'" ref="logPageRef" />
</main>
@@ -41,12 +41,15 @@ import type { Task, Progress } from './types'
const currentView = ref('encode')
const theme = ref<'dark'|'light'>('light')
const tasks = ref<Task[]>([])
const gpuInfo = ref<any[]>([])
const logPageRef = ref<InstanceType<typeof LogPage> | null>(null)
onMounted(() => {
document.addEventListener('contextmenu', e => e.preventDefault())
document.documentElement.setAttribute('data-theme', theme.value)
loadTasks()
loadGPUInfo()
onTaskUpdated((task: Task) => {
const found = tasks.value.find(t => t.id === task.id)
@@ -90,6 +93,34 @@ async function loadTasks() {
try { tasks.value = await api.getTasks() } catch { /* not connected */ }
}
async function loadGPUInfo() {
try {
const app = (window as any).go?.main?.App
if (app?.GetGPUInfo) {
gpuInfo.value = await app.GetGPUInfo() || []
} else {
const encs = await api.getHardwareEncoders()
gpuInfo.value = groupByVendor(encs)
}
} catch {
try {
const encs = await api.getHardwareEncoders()
gpuInfo.value = groupByVendor(encs)
} catch { gpuInfo.value = [] }
}
}
function groupByVendor(encs: any[]): any[] {
const groups: Record<string, any> = {}
for (const e of encs) {
if (!groups[e.type]) {
groups[e.type] = { name: e.type, vendor: e.type, encoders: [] }
}
groups[e.type].encoders.push({ name: e.name, codec: e.codec, label: e.label, available: e.available })
}
return [...Object.values(groups), { name: 'CPU', vendor: 'cpu', encoders: [{ name:'libx264',codec:'h264',label:'H.264',available:true },{ name:'libx265',codec:'hevc',label:'HEVC',available:true }]}]
}
function onTaskAdded() { loadTasks() }
async function handleCancel(id: string) {
+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1785291728333" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1480" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M861.257143 54.857143c27.428571 0 51.2 21.942857 53.028571 49.371428v446.171429h-69.485714V124.342857H142.628571v791.771429h316.342858V987.428571H126.171429c-27.428571 0-51.2-21.942857-53.028572-49.371428V107.885714C73.142857 78.628571 95.085714 56.685714 122.514286 54.857143H861.257143zM592.457143 499.2c3.657143 0 7.314286 1.828571 9.142857 3.657143L950.857143 725.942857c9.142857 5.485714 10.971429 16.457143 5.485714 25.6l-5.485714 5.485714-347.428572 221.257143c-9.142857 5.485714-20.114286 3.657143-25.6-5.485714-1.828571-3.657143-3.657143-5.485714-3.657142-9.142857V517.485714c0-10.971429 9.142857-18.285714 18.285714-18.285714z m54.857143 117.028571v245.028572L841.142857 738.742857l-193.828571-122.514286zM457.142857 548.571429c10.971429 0 18.285714 7.314286 18.285714 18.285714v36.571428c0 10.971429-7.314286 18.285714-18.285714 18.285715H274.285714c-10.971429 0-18.285714-7.314286-18.285714-18.285715v-36.571428c0-10.971429 7.314286-18.285714 18.285714-18.285714h182.857143z m292.571429-256c10.971429 0 18.285714 7.314286 18.285714 18.285714v36.571428c0 10.971429-7.314286 18.285714-18.285714 18.285715H274.285714c-10.971429 0-18.285714-7.314286-18.285714-18.285715v-36.571428c0-10.971429 7.314286-18.285714 18.285714-18.285714h475.428572z" fill="#1296db" p-id="1481"></path></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

+4 -6
View File
@@ -1,11 +1,7 @@
<template>
<header class="app-header" @dblclick="maximize">
<div class="header-left" style="--wails-draggable: drag">
<svg class="app-logo" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polygon points="23 7 16 12 23 17 23 7"/>
<rect x="1" y="5" width="15" height="14" rx="2" ry="2"/>
<line x1="1" y1="9" x2="15" y2="9"/>
</svg>
<img :src="logoSrc" class="app-logo" width="22" height="22" alt="" />
<span class="app-name">FFmpeg GUI</span>
</div>
@@ -51,6 +47,8 @@
</template>
<script setup lang="ts">
import logoSrc from '../assets/icon/app-logo.svg'
defineProps<{ currentView: string; theme: string }>()
defineEmits<{
navigate: [view: string]
@@ -94,7 +92,7 @@ function closeWindow() {
height: 100%;
}
.app-logo { color: var(--accent); flex-shrink: 0; }
.app-logo { flex-shrink: 0; }
.app-name {
font-size: 15px;
font-weight: 600;
+54 -8
View File
@@ -62,19 +62,33 @@
</div>
</div>
<!-- Encode Settings (simplified) -->
<!-- Hardware Acceleration -->
<div class="card">
<div class="card-header"><h3>编码设置</h3></div>
<div class="card-header"><h3>编码硬件与编码器</h3></div>
<div class="fields-row">
<div class="field">
<label class="field-label">硬件加速</label>
<select v-model="hwAccel">
<option v-for="o in hwAccelOptions" :key="o.value" :value="o.value">{{ o.label }}</option>
</select>
</div>
<div class="field">
<label class="field-label">视频编码器</label>
<select v-model="encodeSettings.videoCodec">
<option value="libx264">H.264 (libx264)</option>
<option value="libx265">H.265 / HEVC (libx265)</option>
<option value="h264_nvenc">H.264 NVENC</option>
<option value="hevc_nvenc">HEVC NVENC</option>
<optgroup v-for="grp in hwEncoders" :key="grp.label" :label="grp.label">
<option v-for="o in grp.options" :key="o.value" :value="o.value" :disabled="!o.avail">
{{ o.label }}{{ o.avail ? '' : ' (不可用)' }}
</option>
</optgroup>
</select>
</div>
</div>
</div>
<!-- Encode Settings -->
<div class="card">
<div class="card-header"><h3>编码参数</h3></div>
<div class="fields-row">
<div class="field">
<label class="field-label">质量 / CRF</label>
<input type="number" v-model.number="encodeSettings.crf" placeholder="23" min="0" max="51" />
@@ -113,8 +127,37 @@ import { ref, computed } from 'vue'
import { api } from '../api/wails'
import type { MediaInfo, StreamInfo, EncodeSettings, SubTrack } from '../types'
const props = defineProps<{ gpuInfo: any[] }>()
const emit = defineEmits<{ taskAdded: [] }>()
const hwAccel = ref('')
const hwEncoders = computed(() => {
const result: { label: string; options: { value: string; label: string; avail: boolean }[] }[] = []
for (const g of props.gpuInfo || []) {
if (!g.encoders?.length) continue
result.push({
label: g.vendor === 'cpu' ? '软件编码' : g.name,
options: g.encoders.map((e: any) => ({ value: e.name, label: e.label, avail: e.available }))
})
}
return result
})
const hwAccelOptions = computed(() => {
const opts: { value: string; label: string }[] = [{ value: '', label: 'CPU 软件编码' }]
for (const g of props.gpuInfo || []) {
if (g.vendor === 'cpu' || !g.encoders?.some((e: any) => e.available)) continue
switch (g.vendor) {
case 'nvidia': opts.push({ value: 'cuda', label: g.name + ' (CUDA)' }); break
case 'intel': opts.push({ value: 'qsv', label: g.name + ' (QSV)' }); break
case 'amd': opts.push({ value: 'd3d11va', label: g.name + ' (DXVA)' }); break
}
}
if (opts.length === 1) {
opts.push({ value: 'd3d11va', label: 'Direct3D 11' }, { value: 'dxva2', label: 'DirectX VA2' })
}
return opts
})
const inputFile = ref('')
const outputFile = ref('')
const mediaInfo = ref<MediaInfo | null>(null)
@@ -165,6 +208,7 @@ async function addTask() {
remux: {} as any, subtitle: { subtitles: [...subtitles.value] },
createdAt: new Date().toISOString(),
})
await api.setHWAccel(hwAccel.value)
await api.startTask(taskId)
emit('taskAdded')
resetForm()
@@ -184,8 +228,10 @@ function resetForm() {
}
function streamIcon(s: StreamInfo) {
if (s.codec_type === 'video') return '🎥'; if (s.codec_type === 'audio') return '🔊'
if (s.codec_type === 'subtitle') return '💬'; return '📄'
if (s.codec_type === 'video') return 'V'
if (s.codec_type === 'audio') return 'A'
if (s.codec_type === 'subtitle') return 'S'
return '?'
}
function streamLabel(s: StreamInfo) {
if (s.codec_type === 'video') return `视频: ${s.codec_name} ${s.width||''}x${s.height||''}`
+35 -24
View File
@@ -28,35 +28,16 @@
<div class="field">
<label class="field-label">硬件加速</label>
<select v-model="hwAccel">
<option value="">CPU 软件编码</option>
<option value="cuda">NVIDIA CUDA</option>
<option value="d3d11va">Direct3D 11 (DXVA)</option>
<option value="dxva2">DirectX VA2</option>
<option value="qsv">Intel Quick Sync</option>
<option v-for="o in hwAccelOptions" :key="o.value" :value="o.value">{{ o.label }}</option>
</select>
</div>
<div class="field">
<label class="field-label">视频编码器</label>
<select v-model="encodeSettings.videoCodec">
<optgroup label="软件编码">
<option value="libx264">H.264 / AVC (libx264)</option>
<option value="libx265">H.265 / HEVC (libx265)</option>
<option value="libsvtav1">AV1 (libsvtav1)</option>
</optgroup>
<optgroup label="NVIDIA NVENC">
<option value="h264_nvenc">H.264 NVENC</option>
<option value="hevc_nvenc">HEVC NVENC</option>
<option value="av1_nvenc">AV1 NVENC</option>
</optgroup>
<optgroup label="Intel QSV">
<option value="h264_qsv">H.264 QSV</option>
<option value="hevc_qsv">HEVC QSV</option>
<option value="av1_qsv">AV1 QSV</option>
</optgroup>
<optgroup label="AMD AMF">
<option value="h264_amf">H.264 AMF</option>
<option value="hevc_amf">HEVC AMF</option>
<option value="av1_amf">AV1 AMF</option>
<optgroup v-for="grp in hwEncoders" :key="grp.label" :label="grp.label">
<option v-for="o in grp.options" :key="o.value" :value="o.value" :disabled="!o.avail">
{{ o.label }}{{ o.avail ? '' : ' (不可用)' }}
</option>
</optgroup>
</select>
</div>
@@ -182,8 +163,38 @@ import { ref, computed } from 'vue'
import { api } from '../api/wails'
import type { MediaInfo, StreamInfo, EncodeSettings } from '../types'
const props = defineProps<{ gpuInfo: any[] }>()
const emit = defineEmits<{ taskAdded: [] }>()
// Filtered dropdown options based on detected hardware
const hwEncoders = computed(() => {
const result: { label: string; options: { value: string; label: string; avail: boolean }[] }[] = []
for (const g of props.gpuInfo || []) {
if (!g.encoders?.length) continue
result.push({
label: g.vendor === 'cpu' ? '软件编码' : g.name,
options: g.encoders.map((e: any) => ({ value: e.name, label: e.label, avail: e.available }))
})
}
return result
})
const hwAccelOptions = computed(() => {
const opts: { value: string; label: string }[] = [{ value: '', label: 'CPU 软件编码' }]
for (const g of props.gpuInfo || []) {
if (g.vendor === 'cpu' || !g.encoders?.some((e: any) => e.available)) continue
switch (g.vendor) {
case 'nvidia': opts.push({ value: 'cuda', label: g.name + ' (CUDA)' }); break
case 'intel': opts.push({ value: 'qsv', label: g.name + ' (QSV)' }); break
case 'amd': opts.push({ value: 'd3d11va', label: g.name + ' (DXVA)' }); break
}
}
if (opts.length === 1) {
opts.push({ value: 'd3d11va', label: 'Direct3D 11 (DXVA)' }, { value: 'dxva2', label: 'DirectX VA2' })
}
return opts
})
const inputFile = ref('')
const outputFile = ref('')
const mediaInfo = ref<MediaInfo | null>(null)
+17 -5
View File
@@ -35,11 +35,7 @@
<div class="field" style="max-width:300px">
<label class="field-label">硬件解码加速输入读取</label>
<select v-model="hwAccel">
<option value="">不使用硬件加速</option>
<option value="cuda">NVIDIA CUDA</option>
<option value="d3d11va">Direct3D 11 (DXVA)</option>
<option value="dxva2">DirectX VA2</option>
<option value="qsv">Intel Quick Sync</option>
<option v-for="o in hwAccelOpts" :key="o.value" :value="o.value">{{ o.label }}</option>
</select>
</div>
</div>
@@ -83,12 +79,28 @@ import { ref, reactive, computed } from 'vue'
import { api } from '../api/wails'
import type { MediaInfo, StreamInfo } from '../types'
const props = defineProps<{ gpuInfo: any[] }>()
const emit = defineEmits<{ taskAdded: [] }>()
const inputFile = ref('')
const outputFile = ref('')
const outputFormat = ref('mp4')
const hwAccel = ref('')
const hwAccelOpts = computed(() => {
const opts: { value: string; label: string }[] = [{ value: '', label: '不使用硬件加速' }]
for (const g of props.gpuInfo || []) {
if (g.vendor === 'cpu' || !g.encoders?.some((e: any) => e.available)) continue
switch (g.vendor) {
case 'nvidia': opts.push({ value: 'cuda', label: g.name + ' (CUDA)' }); break
case 'intel': opts.push({ value: 'qsv', label: g.name + ' (QSV)' }); break
case 'amd': opts.push({ value: 'd3d11va', label: g.name + ' (DXVA)' }); break
}
}
if (opts.length === 1) {
opts.push({ value: 'd3d11va', label: 'Direct3D 11' }, { value: 'dxva2', label: 'DirectX VA2' })
}
return opts
})
const mediaInfo = ref<MediaInfo | null>(null)
const selectedStreams = reactive<Record<number, boolean>>({})
+94 -118
View File
@@ -2,25 +2,21 @@
<div class="settings-page">
<h2>设置</h2>
<!-- Hardware Detection Card -->
<!-- GPU Detection Card -->
<div class="card">
<div class="card-header"><h3>硬件检测</h3></div>
<div v-if="loading" class="loading-hint">正在检测硬件编码器...</div>
<div v-else class="hw-cards">
<div v-for="gpu in gpuGroups" :key="gpu.type" class="gpu-card">
<div v-else class="gpu-cards">
<div v-for="gpu in gpus" :key="gpu.name" class="gpu-card">
<div class="gpu-header">
<span class="gpu-dot" :class="gpu.type"></span>
<span class="gpu-name">{{ gpu.label }}</span>
<span v-if="gpu.hasAvailable" class="gpu-badge avail">可用</span>
<span class="gpu-dot" :class="gpu.vendor"></span>
<span class="gpu-name">{{ gpu.name }}</span>
<span v-if="hasAvailable(gpu)" class="gpu-badge avail">可用</span>
<span v-else class="gpu-badge unavail">不可用</span>
</div>
<div class="gpu-encoders">
<div v-for="enc in gpu.encoders" :key="enc.name" class="encoder-row">
<span :class="['enc-check', { avail: enc.available }]">
{{ enc.available ? '✓' : '✗' }}
</span>
<span :class="['enc-check', { avail: enc.available }]">{{ enc.available ? '✓' : '✗' }}</span>
<span class="enc-name">{{ enc.label }}</span>
</div>
</div>
@@ -28,35 +24,28 @@
</div>
</div>
<!-- Hardware Priority Card -->
<!-- Preferred Hardware Card -->
<div class="card">
<div class="card-header"><h3>硬件优先级</h3></div>
<p class="field-hint" style="margin-bottom:12px">拖拽调整编码器优先级编解码时优先使用排在前面的硬件</p>
<div class="priority-list">
<div
v-for="(item, i) in priorities"
:key="item.key"
class="priority-item"
:class="{ disabled: !item.available }"
>
<span class="priority-rank">{{ i + 1 }}</span>
<span class="priority-dot" :class="item.key"></span>
<span class="priority-label">{{ item.label }}</span>
<span v-if="item.available" class="priority-check"></span>
</div>
<div class="card-header"><h3>首选硬件加速</h3></div>
<div class="field" style="max-width:360px">
<label class="field-label">默认使用的硬件加速器</label>
<select v-model="preferredAccel" @change="onPrefChange">
<option v-for="item in allOpts" :key="item.key" :value="item.key"
:disabled="!item.available">
{{ item.label }}{{ item.available ? '' : ' (不可用)' }}
</option>
</select>
</div>
</div>
<!-- Output Settings Card -->
<div class="card">
<div class="card-header"><h3>输出设置</h3></div>
<div class="fields-row">
<div class="field">
<label class="field-label">默认输出目录</label>
<div class="input-row">
<input :value="outputDir" readonly placeholder="选择默认保存目录..." />
<button class="btn-secondary" @click="selectOutputDir">浏览</button>
</div>
<div class="field">
<label class="field-label">默认输出目录</label>
<div class="input-row">
<input :value="outputDir" readonly placeholder="选择默认保存目录..." />
<button class="btn-secondary" @click="selectOutputDir">浏览</button>
</div>
</div>
<div class="field" style="margin-top:16px">
@@ -74,55 +63,81 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { api } from '../api/wails'
import type { HWEncoder } from '../types'
interface EncoderCap { name: string; codec: string; label: string; available: boolean }
interface GPUInfo { name: string; vendor: string; encoders: EncoderCap[] }
const loading = ref(true)
const encoders = ref<HWEncoder[]>([])
const gpus = ref<GPUInfo[]>([])
const outputDir = ref('')
const namingRule = ref('{name}_{codec}')
const preferredAccel = ref('')
interface SelectOpt { key: string; label: string; available: boolean }
const allOpts = computed<SelectOpt[]>(() => {
const opts: SelectOpt[] = []
for (const g of gpus.value) {
opts.push({ key: g.vendor, label: g.name, available: hasAvailable(g) })
}
return opts
})
onMounted(async () => {
try {
encoders.value = await api.getHardwareEncoders()
const app = (window as any).go?.main?.App
if (app?.GetGPUInfo) {
gpus.value = await app.GetGPUInfo() || []
} else {
const encoders = await api.getHardwareEncoders()
gpus.value = buildFromEncoders(encoders)
}
} catch {
encoders.value = [
{ name:'h264_nvenc',label:'H.264 NVENC',type:'nvidia',codec:'h264',available:true },
{ name:'hevc_nvenc',label:'HEVC NVENC',type:'nvidia',codec:'hevc',available:true },
{ name:'av1_nvenc', label:'AV1 NVENC', type:'nvidia',codec:'av1', available:true },
{ name:'h264_qsv', label:'H.264 QSV', type:'intel', codec:'h264',available:false },
{ name:'hevc_qsv', label:'HEVC QSV', type:'intel', codec:'hevc',available:false },
{ name:'h264_amf', label:'H.264 AMF', type:'amd', codec:'h264',available:false },
try {
const encoders = await api.getHardwareEncoders()
gpus.value = buildFromEncoders(encoders)
} catch { gpus.value = [] }
} finally {
loading.value = false
// Default to first available hardware GPU
const firstAvail = gpus.value.find(g => g.vendor !== 'cpu' && hasAvailable(g))
preferredAccel.value = firstAvail ? firstAvail.vendor : 'cpu'
syncAccel()
}
})
function onPrefChange() { syncAccel() }
function syncAccel() {
const g = gpus.value.find(g => g.vendor === preferredAccel.value)
if (g && g.vendor !== 'cpu' && hasAvailable(g)) {
api.setHWAccel(vendorToAccel(g.vendor))
} else {
api.setHWAccel('')
}
}
function vendorToAccel(v: string): string {
switch (v) { case 'nvidia': return 'cuda'; case 'intel': return 'qsv'; case 'amd': return 'd3d11va'; default: return '' }
}
function hasAvailable(g: GPUInfo) { return g.encoders.some(e => e.available) }
function buildFromEncoders(encoders: any[]): GPUInfo[] {
const groups: Record<string, any> = {}
for (const e of encoders) {
if (!groups[e.type]) {
groups[e.type] = { name: e.type, vendor: e.type, encoders: [] }
}
groups[e.type].encoders.push({ name: e.name, codec: e.codec, label: e.label, available: e.available })
}
return [...Object.values(groups), {
name: 'CPU', vendor: 'cpu', encoders: [
{ name: 'libx264', codec: 'h264', label: 'H.264 (libx264)', available: true },
{ name: 'libx265', codec: 'hevc', label: 'H.265/HEVC (libx265)', available: true },
]
} finally { loading.value = false }
})
const gpuGroups = computed(() => {
const map = new Map<string, { type: string; label: string; encoders: HWEncoder[]; hasAvailable: boolean }>()
const groups = [
{ type: 'nvidia', label: 'NVIDIA GPU' },
{ type: 'intel', label: 'Intel GPU' },
{ type: 'amd', label: 'AMD GPU' },
]
for (const g of groups) {
const encs = encoders.value.filter(e => e.type === g.type)
map.set(g.type, {
type: g.type,
label: g.label,
encoders: encs,
hasAvailable: encs.some(e => e.available),
})
}
return Array.from(map.values())
})
const priorities = computed(() => {
const items: { key: string; label: string; available: boolean }[] = []
for (const g of gpuGroups.value) {
items.push({ key: g.type, label: g.label, available: g.hasAvailable })
}
items.push({ key: 'cpu', label: 'CPU (软件编码)', available: true })
return items
})
}]
}
async function selectOutputDir() {
try {
@@ -133,66 +148,27 @@ async function selectOutputDir() {
</script>
<style scoped>
.settings-page {
display: flex;
flex-direction: column;
gap: 20px;
}
.settings-page { display: flex; flex-direction: column; gap: 20px; }
.settings-page > h2 { margin-bottom: 4px; }
.loading-hint { color: var(--text-dim); padding: 12px 0; }
/* GPU Cards */
.hw-cards { display: flex; flex-direction: column; gap: 12px; }
.gpu-card {
border: 1px solid var(--border-light);
border-radius: var(--radius);
padding: 16px;
background: var(--bg-surface);
}
.gpu-cards { display: flex; flex-direction: column; gap: 12px; }
.gpu-card { border: 1px solid var(--border-light); border-radius: var(--radius); padding: 16px; background: var(--bg-surface); }
.gpu-header { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; }
.gpu-dot { width: 10px; height: 10px; border-radius: 50%; }
.gpu-dot.nvidia { background: #76b900; }
.gpu-dot.intel { background: #00aaff; }
.gpu-dot.amd { background: #ed1c24; }
.gpu-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; }
.gpu-dot.nvidia { background: #76b900; } .gpu-dot.intel { background: #00aaff; }
.gpu-dot.amd { background: #ed1c24; } .gpu-dot.cpu,.gpu-dot.unknown { background: var(--text-dim); }
.gpu-name { font-size: 14px; font-weight: 600; flex: 1; }
.gpu-badge { font-size: 11px; padding: 2px 8px; border-radius: 10px; font-weight: 500; }
.gpu-badge.avail { background: #e6f4ea; color: var(--success); }
.gpu-badge.unavail { background: var(--bg-input); color: var(--text-dim); }
.gpu-encoders { display: flex; flex-direction: column; gap: 6px; }
.encoder-row { display: flex; align-items: center; gap: 10px; font-size: 13px; }
.enc-check { width: 18px; font-size: 12px; font-weight: 600; color: var(--text-dim); }
.enc-check.avail { color: var(--success); }
.enc-name { color: var(--text-secondary); }
/* Priority */
.priority-list { display: flex; flex-direction: column; gap: 6px; }
.priority-item {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 14px;
border-radius: var(--radius-sm);
background: var(--bg-surface);
border: 1px solid var(--border-light);
}
.priority-item.disabled { opacity: 0.5; }
.priority-rank {
font-size: 12px; font-weight: 700; color: var(--text-dim);
width: 22px; height: 22px; border-radius: 50%;
background: var(--bg-input); display: flex; align-items: center; justify-content: center;
}
.priority-dot { width: 8px; height: 8px; border-radius: 50%; }
.priority-dot.nvidia { background: #76b900; }
.priority-dot.intel { background: #00aaff; }
.priority-dot.amd { background: #ed1c24; }
.priority-dot.cpu { background: var(--text-dim); }
.priority-label { flex: 1; font-size: 13px; font-weight: 500; }
.priority-check { font-size: 14px; color: var(--success); font-weight: 600; }
/* Output */
.input-row { display: flex; gap: 8px; width: 100%; }
.input-row input { flex: 1; }