Adiciona features de ordenação, drag-and-drop e melhorias de UX

---

- Adicionado campo order em Stream e migração automática em readStreams() para ordenação persistida no streams.json;
- Implementado drag-and-drop de cards com @dnd-kit/core + @dnd-kit/sortable, com faixa de drag dedicada no topo de cada card;
- Adicionado endpoint PUT /api/streams/reorder para persistir a nova ordem no servidor;
- Atualizada playlist M3U para respeitar a ordem dos cards e incluir tvg-chno com número de canal;
- Corrigida geração de thumbnail para capturar via ffmpeg -f x11grab direto do Xvfb, usando arquivo temporário thumb.tmp.jpg;
- Adicionada política gerenciada do Chromium no Dockerfile para suprimir diálogo de salvar senha;
- Adicionadas flags --password-store=basic e --disable-features=PasswordManagerRedesign no template do Chromium;
- Substituído confirm() nativo por modal de confirmação customizado no delete de stream;
- Adicionado tamanho mini e redefinidos os tamanhos de card; padrão alterado para md (300px);
- Adicionado logo do projeto no header e ícone GripVertical na faixa de drag;
- Erros de validação do formulário agora exibidos em vermelho negrito;

---
This commit is contained in:
2026-04-25 03:24:20 -03:00
parent 1f8385e450
commit f6879781c1
20 changed files with 348 additions and 97 deletions
+4 -3
View File
@@ -8,10 +8,11 @@ export async function GET(req: Request) {
const streams = readStreams()
const lines = ["#EXTM3U"]
for (const s of streams) {
lines.push(`#EXTINF:-1 tvg-id="${s.id}" tvg-name="${s.name}" group-title="DecapStream",${s.name} [${s.id}] ${s.resolution} ${s.fps}fps`)
streams.forEach((s, i) => {
const chno = i + 1
lines.push(`#EXTINF:-1 tvg-id="${s.id}" tvg-name="${s.name}" tvg-chno="${chno}" group-title="DecapStream",${chno}. ${s.name} [${s.id}] ${s.resolution} ${s.fps}fps`)
lines.push(`http://${host}:${port}/live/${s.id}`)
}
})
return new Response(lines.join("\n"), {
headers: {
+20
View File
@@ -0,0 +1,20 @@
import { NextResponse } from "next/server"
import { readStreams, writeStreams } from "@/lib/db"
export async function PUT(req: Request) {
const { ids } = (await req.json()) as { ids: string[] }
if (!Array.isArray(ids))
return NextResponse.json({ error: "ids must be an array" }, { status: 400 })
const streams = readStreams()
const streamMap = new Map(streams.map((s) => [s.id, s]))
ids.forEach((id, i) => {
const s = streamMap.get(id)
if (s) s.order = i
})
writeStreams(streams.sort((a, b) => a.order - b.order))
return NextResponse.json({ ok: true })
}
+3
View File
@@ -23,6 +23,8 @@ export async function POST(req: Request) {
const ports = allocatePorts()
const now = new Date().toISOString()
const existing = readStreams()
const nextOrder = existing.length > 0 ? Math.max(...existing.map((s) => s.order)) + 1 : 0
const stream = {
...STREAM_DEFAULTS,
@@ -30,6 +32,7 @@ export async function POST(req: Request) {
scale: normalizeScale(body.scale ?? STREAM_DEFAULTS.scale), // #13
...ports,
desiredState: "running" as const, // #19
order: nextOrder,
createdAt: now,
updatedAt: now,
}