Files
simple-multimidia-track-aud…/DEVELOPMENT_PLAN.md
T
Felipe 121e919bbf feat: implement batch processing feature with tabbed interface
- Added `ActiveTab` enum to manage active tab state in `App`.
- Created `BatchItem` struct and integrated `batch_items` vector in `App`.
- Developed `BatchPanel` component for managing batch projects with inline forms.
- Implemented sequential processing in `App::start_batch_item()` with automatic advancement in `poll_background()`.
- Updated UI to display individual item states (` Aguardando | ⟳ Processando | ✓ Concluído | ⊸ Cancelado | ✗ Erro`).
- Blocked item addition/removal during processing.
- Enhanced session persistence to include batch projects in `SessionData`.
- Updated session save/load functions to handle new session structure.
2026-02-28 20:28:24 -03:00

17 KiB
Raw Blame History

Plano de Desenvolvimento — Simple Multimedia Track Audio Editor

Versão: 1.3
Data: 28/02/2026
Status: Fases 16 concluídas; Fase 7 (Persistência) e Fase 8 (Modo Lote) em planejamento
Referência: PRD v1.2


Estratégia Geral

O desenvolvimento segue a Clean Architecture de dentro para fora: as camadas mais internas (domínio) são implementadas e testadas primeiro, sem qualquer dependência de I/O, frameworks ou processos externos. A UI é sempre a última camada a ser construída.

Fase 1: Setup
  └── Fase 2: Domain (testável, sem I/O)
        └── Fase 3: Application (testável com mocks)
              └── Fase 4: Adapters (integração com FFmpeg)
                    └── Fase 5: Infrastructure (processo real)
                          └── Fase 6: UI (montagem final)

Cada fase deve estar compilando e com testes passando antes de avançar para a próxima.


Fase 1 — Setup do Projeto

Objetivo: Preparar o ambiente antes de escrever qualquer lógica de negócio.

Tarefas

  • Atualizar Cargo.toml com as dependências (rfd = "0.14" adicionado para diálogos nativos)
  • Criar a estrutura de pastas conforme especificado
  • Declarar os módulos em src/main.rs
  • Verificar que o projeto compila (cargo check)

Fase 2 — Domain (núcleo puro)

Objetivo: Modelar o problema de negócio sem nenhum acoplamento a frameworks, I/O ou processos externos.

Regra: nenhum use std::process, nenhum use eframe, nenhuma chamada de rede ou filesystem nessa camada.

Value Objects (src/domain/value_objects/)

Tipo Implementação
FilePath Newtype sobre PathBuf; derivar Clone, Debug, Serialize, Deserialize
TrackId Newtype opaco sobre u32; derivar Clone, Copy, Debug, PartialEq, Eq, Hash
SyncOffset Newtype sobre i64 (milissegundos, nunca f64); implementar método from_seconds_str e as_ms
TrackLanguage Newtype sobre String (ex: "por", "eng"); validar formato ISO 639-2

Entities (src/domain/entities/)

Tipo Campos principais
VideoFile path: FilePath
AudioTrack id: TrackId, path: FilePath, offset: SyncOffset, language: TrackLanguage
SubtitleTrack id: TrackId, path: FilePath, offset: SyncOffset, language: TrackLanguage
MediaTrackInfo id: TrackId, kind: TrackKind (enum: Video/Audio/Subtitle), codec: String, language: Option<TrackLanguage>
MkvOutput path: FilePath
Project Entidade raiz — ver abaixo

Estrutura de Project

pub struct Project {
    pub source: VideoFile,
    pub tracks: Vec<Track>,              // faixas externas adicionadas pelo usuário
    pub existing_tracks: Vec<MediaTrackInfo>, // faixas lidas do arquivo via ffprobe
    pub output: MkvOutput,
}

Testes obrigatórios

  • SyncOffset::from_seconds_str("1.2")SyncOffset(1200)
  • SyncOffset::from_seconds_str("-0.5")SyncOffset(-500)
  • Project não aceita output.path igual a source.path
  • TrackId é opaco (não expõe indexação interna)

Fase 3 — Application (casos de uso e ports)

Objetivo: Definir o que o sistema faz sem saber como. Ports são traits; use cases orquestram entidades.

Ports (src/application/ports/)

// MediaInfoPort: inspeciona faixas de um arquivo de mídia
pub trait MediaInfoPort {
    fn probe(&self, path: &FilePath) -> Result<Vec<MediaTrackInfo>>;
}

// MediaProcessorPort: executa o processamento final
pub trait MediaProcessorPort {
    fn execute(&self, args: Vec<String>) -> Result<()>;
}

// FileSystemPort: abstrai acesso ao sistema de arquivos
pub trait FileSystemPort {
    fn exists(&self, path: &FilePath) -> bool;
}

Use Cases (src/application/use_cases/)

Implementar nesta ordem (dependência crescente):

# Use Case Descrição
1 LoadMediaInfo Usa MediaInfoPort para popular Project::existing_tracks
2 AddAudioTrack Adiciona AudioTrack externo ao Project::tracks
3 AddSubtitle Adiciona SubtitleTrack externo ao Project::tracks
4 AdjustSync Altera SyncOffset de uma faixa existente pelo TrackId
5 EditExistingTrackSync Ajusta offset de faixa já presente no arquivo original
6 SetTrackLanguage Altera idioma de uma faixa pelo TrackId
7 GenerateOutput Constrói o comando final e delega ao MediaProcessorPort

Testes obrigatórios

  • Usar mocks dos ports (sem chamar nenhum processo externo)
  • GenerateOutput sempre produz comando com -c copy (verificado via mock)
  • AdjustSync com TrackId inexistente retorna erro
  • AddAudioTrack retorna erro se source e output conflitarem (coberto por Project::new)

Fase 4 — Adapters (implementações concretas)

Objetivo: Conectar o domínio ao FFmpeg e ao filesystem real.

FFmpeg (src/adapters/ffmpeg/)

FfmpegCommandBuilder

Responsabilidade única: converter Project em Vec<String> de argumentos para o FFmpeg.

Regras invariantes:

  • -c copy sempre presente (RNF-01 — nunca opcional, nunca configurável)
  • SyncOffset(i64 ms)-itsoffset 1.200 (conversão feita somente aqui)
  • TrackId-map 0:a:N (mapeamento de índice feito somente aqui)

Exemplo de saída esperada:

ffmpeg -i input.mkv -itsoffset 1.200 -i audio_pt.aac -map 0:v -map 0:a -map 1:a -c copy -metadata:s:a:1 language=por output.mkv
  • Implementar FfmpegCommandBuilder::build(project: &Project) -> Vec<String>
  • Implementar FfmpegCommandBuilder::build_export(source, track, output) -> Vec<String> (exportação de faixas individuais)
  • Testes: verificar presença de -c copy, ordem dos -map, formato de -itsoffset

FfprobeGateway

Implementa MediaInfoPort. Executa:

ffprobe -v quiet -print_format json -show_streams <path>

e mapeia a saída JSON para Vec<MediaTrackInfo>.

  • Implementar parsing de JSON via serde_json
  • Mapear codec_type para TrackKind
  • Mapear tags.language para Option<TrackLanguage>

FfmpegGateway

Implementa MediaProcessorPort. Executa o processo real do FFmpeg e captura stderr.

  • Capturar stderr para exibição de erros (RF-07)
  • Retornar erro com mensagem legível em caso de código de saída não-zero

Filesystem (src/adapters/filesystem/)

  • FilePickerAdapter: abstrai seleção de arquivo via diálogo nativo (crate rfd; inclui save_audio e save_subtitle por codec)

Fase 5 — Infrastructure

Objetivo: Execução real e não-bloqueante de processos externos.

Módulo src/infrastructure/process/

  • Implementar execução via tokio::process::Command (assíncrono, RNF-05)
  • Capturar stderr em stream via run_ffmpeg_async com std::sync::mpsc::Sender<String> (progresso em tempo real — RF-07)
  • Cancelamento de execução via tokio::sync::oneshot + child.kill().await

Validação na inicialização

  • Verificar se ffmpeg está disponível no PATH (DT-06)
  • Verificar se ffprobe está disponível no PATH
  • Exibir mensagem de erro global (global_error) na UI se algum dos dois estiver ausente

Fase 6 — UI (camada mais externa)

Objetivo: Interface gráfica que conecta o usuário ao Project via use cases.

Todo estado da aplicação vive no Project. A UI apenas lê e dispara use cases.
Termos técnicos do FFmpeg nunca aparecem na interface (ver PRD seção 13).

Componentes (src/ui/components/), em ordem de construção

# Componente Descrição
1 VideoSelector Seletor de arquivo de vídeo; dispara LoadMediaInfo ao confirmar
2 ExistingTrackList Lista faixas detectadas (áudio/legenda); permite editar offset de cada uma
3 AddAudioTrackForm Formulário para adicionar faixa de áudio externa (arquivo, idioma, offset)
4 AddSubtitleForm Formulário para adicionar legenda externa (arquivo, idioma, offset)
5 SyncOffsetField Campo de offset em segundos (ex: -1.2s); converte para SyncOffset(ms) internamente
6 LanguageField Seletor/input de idioma (ex: por, eng)
7 OutputSelector Campo de caminho de saída + extensão .mkv forçada
8 ExecutionPanel Botão "Gerar MKV", exibição de progresso e erros legíveis

App (src/ui/app.rs)

  • Implementar eframe::App para App
  • App contém project: Option<Project> como única fonte de verdade do estado
  • Despachar eventos de UI para os use cases correspondentes
  • Integrar execução assíncrona (tokio + std::thread) com o loop de UI do eframe
  • Botão "Cancelar" via cancel_tx: Option<oneshot::Sender<()>>
  • ExecutionState: Idle | Running | Success | Cancelled | Error

Fase 7 — Persistência do Estado do Projeto

Objetivo: Salvar e carregar o estado do Project em disco, permitindo que o usuário retome uma sessão anterior sem precisar reconfigurar tudo.

Pré-requisito para a Fase 8: o modo lote se beneficia diretamente da persistência — um carrinho salvo pode ser retomado após fechar a aplicação.

Restrição: a serialização deve viver exclusivamente na camada de infraestrutura/UI. O domain/ não deve depender de serde diretamente, mas as entidades já derivam Serialize/Deserialize — nenhuma mudança no domínio é necessária.

Formato e local do arquivo

  • Formato: JSON via serde_json (já disponível no Cargo.toml)
  • Local: diretório de configuração do usuário (~/.config/simple-mkv-editor/session.json no Linux; AppData no Windows)
  • Um único arquivo de sessão por vez (sobrescreve ao salvar)

Quando salvar / carregar

Evento Ação
Usuário clica "Salvar sessão" Serializa Project para disco
Inicialização da aplicação Verifica se existe arquivo de sessão; oferece opção de restaurar
Usuário clica "Carregar sessão" Desserializa e substitui App::project

Salvamento automático fica fora do escopo desta fase para evitar escritas freqüentes em disco.

Mudanças necessárias

Arquivo O que muda
src/infrastructure/persistence/mod.rs Novo módulo: save_session(project) e load_session() -> Result<Project>
src/infrastructure/mod.rs Expor persistence
src/ui/app.rs Botões "Salvar sessão" / "Carregar sessão" no header; chamar o módulo de persistência

Tarefas

  • Criar src/infrastructure/persistence/mod.rs com save_session e load_session
  • Resolver caminho do arquivo via dirs crate (ou std::env)
  • Adicionar botões no header da UI
  • Tratar erros de desserialização (arquivo corrompido ou versão incompatível) com mensagem amigável

Fase 8 — Modo Lote via Abas

Objetivo: Permitir que o usuário configure e processe múltiplos projetos sequencialmente, um por vez, sem sobrecarga de I/O.

Modelo mental — carrinho: o usuário adiciona itens livremente, na ordem que quiser, e só inicia o processamento quando clicar "Processar Tudo". Itens podem ser removidos do carrinho a qualquer momento antes de processar.

Restrição: Nenhuma camada abaixo da UI (domain, application, adapters, infrastructure) precisa ser alterada — Project já é a unidade de trabalho reutilizável.

Seleção de arquivos

Igual ao modo Projeto Único — diálogos nativos via FilePickerAdapter / rfd. Nenhum path é digitado manualmente pelo usuário. O formulário de cada item do lote expõe os mesmos botões "Escolher..." já existentes.

Formulário — Opção A (inline na aba Lote)

O formulário de configuração de um novo item expande inline na própria aba Lote, abaixo da lista. Não altera nem reutiliza a aba Projeto Único. Ao confirmar, o item é adicionado ao carrinho e o formulário é limpo.

[ + Adicionar item ]           ← clique expande o formulário abaixo
┌─────────────────────────────────────────────────┐
│ Vídeo:   [ep03.mkv        ] [Escolher...]       │
│ Saída:   [ep03_pt.mkv     ] [Escolher...]       │
│ Faixas:  [+ Áudio] [+ Legenda]                  │
│                        [Cancelar] [Adicionar ✓] │
└─────────────────────────────────────────────────┘

Mudanças na UI

Arquivo O que muda
src/ui/app.rs Adicionar enum ActiveTab { Single, Batch } e active_tab: ActiveTab; adicionar batch_items: Vec<BatchItem>
src/ui/components/batch_panel.rs Novo componente: carrinho de itens, formulário inline de adição, botão "Processar Tudo"
src/ui/components/mod.rs Expor batch_panel

Estrutura de BatchItem

struct BatchItem {
    project: Project,
    state: ExecutionState, // reutiliza o enum já existente
}

Fluxo de execução do lote

  1. Usuário adiciona itens ao carrinho (zero ou mais), configura cada um com diálogo nativo
  2. Itens podem ser removidos do carrinho enquanto nenhum processamento estiver em curso
  3. Ao clicar "Processar Tudo", o lote é bloqueado (sem mais adições/remoções)
  4. Para cada item em ordem: state → Runningrun_ffmpeg_async → aguarda BackgroundMsg::Done | Errorstate → Success | Error → próximo item
  5. "Cancelar" interrompe o item atual via cancel_tx; os demais permanecem no carrinho com estado Idle

Tarefas

  • Criar enum ActiveTab e barra de abas no update() de App
  • Criar BatchItem e batch_items: Vec<BatchItem> em App
  • Criar componente BatchPanel com carrinho e formulário inline (Opção A)
  • Implementar loop sequencial de execução em App::start_batch_item() + avanço automático em poll_background()
  • Exibir estado individual por item (Aguardando | Processando | Concluído | Erro)
  • Bloquear adição/remoção de itens durante processamento

Critérios de Conclusão (v1.0)

Alinhados com o PRD seção 12:

  • É possível selecionar um arquivo de vídeo base
  • Ao selecionar o vídeo, as faixas existentes são listadas automaticamente (via ffprobe)
  • É possível adicionar uma ou mais faixas de áudio externas
  • É possível adicionar uma ou mais faixas de legenda
  • É possível definir offset de sincronização por faixa ao adicionar uma nova faixa
  • É possível editar o offset de sincronização de uma faixa já existente no arquivo
  • É possível atribuir idioma a cada faixa
  • O arquivo MKV é gerado corretamente ao confirmar
  • Erros do FFmpeg são exibidos de forma legível
  • A interface não trava durante o processamento
  • Nenhum reencoding ocorre (verificável via ffprobe no arquivo de saída)
  • O flag -c copy está sempre presente no comando gerado (verificável via testes unitários)