feat: atualiza versão do projeto para 1.3 e revisa o plano de desenvolvimento e PRD com novas funcionalidades e requisitos

This commit is contained in:
2026-02-28 18:19:03 -03:00
parent 16dcb0b9d4
commit 18739c3ce7
3 changed files with 266 additions and 136 deletions
+114 -7
View File
@@ -1,9 +1,9 @@
# Plano de Desenvolvimento — Simple Multimedia Track Audio Editor # Plano de Desenvolvimento — Simple Multimedia Track Audio Editor
**Versão:** 1.1 **Versão:** 1.3
**Data:** 28/02/2026 **Data:** 28/02/2026
**Status:** Concluído — todas as fases implementadas, 33 testes passando **Status:** Fases 16 concluídas; Fase 7 (Persistência) e Fase 8 (Modo Lote) em planejamento
**Referência:** PRD v1.1 **Referência:** PRD v1.2
--- ---
@@ -46,7 +46,7 @@ Cada fase deve estar **compilando e com testes passando** antes de avançar para
### Value Objects (`src/domain/value_objects/`) ### Value Objects (`src/domain/value_objects/`)
| Tipo | Implementação | | Tipo | Implementação |
|---|---| | --------------- | ----------------------------------------------------------------------------------------------------- |
| `FilePath` | Newtype sobre `PathBuf`; derivar `Clone`, `Debug`, `Serialize`, `Deserialize` | | `FilePath` | Newtype sobre `PathBuf`; derivar `Clone`, `Debug`, `Serialize`, `Deserialize` |
| `TrackId` | Newtype opaco sobre `u32`; derivar `Clone`, `Copy`, `Debug`, `PartialEq`, `Eq`, `Hash` | | `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` | | `SyncOffset` | Newtype sobre `i64` (milissegundos, **nunca `f64`**); implementar método `from_seconds_str` e `as_ms` |
@@ -55,7 +55,7 @@ Cada fase deve estar **compilando e com testes passando** antes de avançar para
### Entities (`src/domain/entities/`) ### Entities (`src/domain/entities/`)
| Tipo | Campos principais | | Tipo | Campos principais |
|---|---| | ---------------- | ----------------------------------------------------------------------------------------------------------------- |
| `VideoFile` | `path: FilePath` | | `VideoFile` | `path: FilePath` |
| `AudioTrack` | `id: TrackId`, `path: FilePath`, `offset: SyncOffset`, `language: TrackLanguage` | | `AudioTrack` | `id: TrackId`, `path: FilePath`, `offset: SyncOffset`, `language: TrackLanguage` |
| `SubtitleTrack` | `id: TrackId`, `path: FilePath`, `offset: SyncOffset`, `language: TrackLanguage` | | `SubtitleTrack` | `id: TrackId`, `path: FilePath`, `offset: SyncOffset`, `language: TrackLanguage` |
@@ -111,7 +111,7 @@ pub trait FileSystemPort {
Implementar nesta ordem (dependência crescente): Implementar nesta ordem (dependência crescente):
| # | Use Case | Descrição | | # | Use Case | Descrição |
|---|---|---| | --- | ----------------------- | ----------------------------------------------------------- |
| 1 | `LoadMediaInfo` | Usa `MediaInfoPort` para popular `Project::existing_tracks` | | 1 | `LoadMediaInfo` | Usa `MediaInfoPort` para popular `Project::existing_tracks` |
| 2 | `AddAudioTrack` | Adiciona `AudioTrack` externo ao `Project::tracks` | | 2 | `AddAudioTrack` | Adiciona `AudioTrack` externo ao `Project::tracks` |
| 3 | `AddSubtitle` | Adiciona `SubtitleTrack` externo ao `Project::tracks` | | 3 | `AddSubtitle` | Adiciona `SubtitleTrack` externo ao `Project::tracks` |
@@ -140,11 +140,13 @@ Implementar nesta ordem (dependência crescente):
Responsabilidade única: converter `Project` em `Vec<String>` de argumentos para o FFmpeg. Responsabilidade única: converter `Project` em `Vec<String>` de argumentos para o FFmpeg.
Regras invariantes: Regras invariantes:
- `-c copy` **sempre presente** (RNF-01 — nunca opcional, nunca configurável) - `-c copy` **sempre presente** (RNF-01 — nunca opcional, nunca configurável)
- `SyncOffset(i64 ms)``-itsoffset 1.200` (conversão feita **somente aqui**) - `SyncOffset(i64 ms)``-itsoffset 1.200` (conversão feita **somente aqui**)
- `TrackId``-map 0:a:N` (mapeamento de índice feito **somente aqui**) - `TrackId``-map 0:a:N` (mapeamento de índice feito **somente aqui**)
Exemplo de saída esperada: 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 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
``` ```
@@ -156,9 +158,11 @@ ffmpeg -i input.mkv -itsoffset 1.200 -i audio_pt.aac -map 0:v -map 0:a -map 1:a
#### `FfprobeGateway` #### `FfprobeGateway`
Implementa `MediaInfoPort`. Executa: Implementa `MediaInfoPort`. Executa:
``` ```
ffprobe -v quiet -print_format json -show_streams <path> ffprobe -v quiet -print_format json -show_streams <path>
``` ```
e mapeia a saída JSON para `Vec<MediaTrackInfo>`. e mapeia a saída JSON para `Vec<MediaTrackInfo>`.
- [x] Implementar parsing de JSON via `serde_json` - [x] Implementar parsing de JSON via `serde_json`
@@ -206,7 +210,7 @@ Implementa `MediaProcessorPort`. Executa o processo real do FFmpeg e captura std
### Componentes (`src/ui/components/`), em ordem de construção ### Componentes (`src/ui/components/`), em ordem de construção
| # | Componente | Descrição | | # | Componente | Descrição |
|---|---|---| | --- | ------------------- | -------------------------------------------------------------------------------------- |
| 1 | `VideoSelector` | Seletor de arquivo de vídeo; dispara `LoadMediaInfo` ao confirmar | | 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 | | 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) | | 3 | `AddAudioTrackForm` | Formulário para adicionar faixa de áudio externa (arquivo, idioma, offset) |
@@ -227,6 +231,109 @@ Implementa `MediaProcessorPort`. Executa o processo real do FFmpeg e captura std
--- ---
## 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`
```rust
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 → Running``run_ffmpeg_async` → aguarda `BackgroundMsg::Done | Error``state → 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::process_batch()`
- [ ] 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) ## Critérios de Conclusão (v1.0)
Alinhados com o PRD seção 12: Alinhados com o PRD seção 12:
+27 -16
View File
@@ -1,9 +1,9 @@
# PRD — Simple Multimedia Track Audio Editor # PRD — Simple Multimedia Track Audio Editor
**Versão:** 1.1 **Versão:** 1.2
**Data:** 28/02/2026 **Data:** 28/02/2026
**Status:** Em desenvolvimento **Status:** Em desenvolvimento
**Revisão:** Incorporados feedbacks de arquitetura — modelo de sessão, ffprobe, tipos fortes e regra de no-reencode **Revisão:** v1.2 — Adicionado RF-09 (Modo Lote); batch promovido de backlog para requisito planejado
--- ---
@@ -105,6 +105,17 @@ As ferramentas existentes são complexas (ex: interface direta do FFmpeg via CLI
- A faixa original é remapeada com o novo timestamp via `ffmpeg -itsoffset` combinado com `-map` - A faixa original é remapeada com o novo timestamp via `ffmpeg -itsoffset` combinado com `-map`
- Útil para corrigir atrasos em faixas de dublagem ou legenda já incorporadas ao arquivo - Útil para corrigir atrasos em faixas de dublagem ou legenda já incorporadas ao arquivo
### RF-09 — Processamento em lote
- A interface oferece duas abas: **Projeto Único** (fluxo atual) e **Lote**
- A aba Lote funciona como um **carrinho**: o usuário adiciona, revisa e remove itens livremente antes de processar
- Cada item é um projeto completo e independente (vídeo, faixas e saída próprios), configurado via **formulário inline** na própria aba Lote — sem navegação para outra tela
- A seleção de arquivos usa **diálogos nativos** (igual ao modo Projeto Único) — nenhum path é digitado manualmente
- O processamento é **sequencial** — um projeto por vez, sem paralelismo, para evitar sobrecarga de I/O
- Durante o processamento, o carrinho é bloqueado: não é possível adicionar nem remover itens
- Cada item exibe seu estado individual: `Aguardando | Processando | Concluído | Erro`
- É possível cancelar o item em execução; os demais permanecem no carrinho com estado `Aguardando`
--- ---
## 7. Requisitos Não Funcionais ## 7. Requisitos Não Funcionais
@@ -159,9 +170,9 @@ Arquivo de saída (.mkv)
### Stack ### Stack
| Camada | Tecnologia | Motivo | | Camada | Tecnologia | Motivo |
|--------------|-------------------------------|---------------------------------------------| | ------------ | ------------------------------ | ------------------------------------ |
| Interface | `egui` / `eframe` | Nativo, simples, multiplataforma | | Interface | `egui` / `eframe` | Nativo, simples, multiplataforma |
| Backend | Rust (`std::process::Command`)| Estável, sem dependências externas | | Backend | Rust (`std::process::Command`) | Estável, sem dependências externas |
| Erros | `anyhow` | Propagação de erros simplificada | | Erros | `anyhow` | Propagação de erros simplificada |
| Configuração | `serde` | Serialização de perfis e histórico | | Configuração | `serde` | Serialização de perfis e histórico |
| Async | `tokio` (opcional) | Execução não bloqueante da interface | | Async | `tokio` (opcional) | Execução não bloqueante da interface |
@@ -209,7 +220,7 @@ As dependências sempre apontam de fora para dentro. Camadas internas não conhe
Contém as entidades e objetos de valor do negócio. Não depende de nada externo. Contém as entidades e objetos de valor do negócio. Não depende de nada externo.
| Tipo | Exemplos | | Tipo | Exemplos |
|----------------|-----------------------------------------------------------------------------------| | ------------- | ------------------------------------------------------------------------------------ |
| Entities | `Project`, `VideoFile`, `AudioTrack`, `SubtitleTrack`, `MkvOutput`, `MediaTrackInfo` | | Entities | `Project`, `VideoFile`, `AudioTrack`, `SubtitleTrack`, `MkvOutput`, `MediaTrackInfo` |
| Value Objects | `TrackId`, `SyncOffset`, `TrackLanguage`, `FilePath` | | Value Objects | `TrackId`, `SyncOffset`, `TrackLanguage`, `FilePath` |
@@ -233,7 +244,7 @@ Contém as regras de negócio da aplicação. Orquestra as entidades do domínio
Define traits (ports) que as camadas externas devem implementar. Define traits (ports) que as camadas externas devem implementar.
| Tipo | Exemplos | | Tipo | Exemplos |
|------------|-------------------------------------------------------------------------------------------------------------------| | --------- | ---------------------------------------------------------------------------------------------------------------------------- |
| Use Cases | `LoadMediaInfo`, `AddAudioTrack`, `AddSubtitle`, `AdjustSync`, `EditExistingTrackSync`, `SetTrackLanguage`, `GenerateOutput` | | Use Cases | `LoadMediaInfo`, `AddAudioTrack`, `AddSubtitle`, `AdjustSync`, `EditExistingTrackSync`, `SetTrackLanguage`, `GenerateOutput` |
| Ports | `MediaProcessorPort`, `MediaInfoPort`, `FileSystemPort` | | Ports | `MediaProcessorPort`, `MediaInfoPort`, `FileSystemPort` |
@@ -254,7 +265,7 @@ Define traits (ports) que as camadas externas devem implementar.
Implementam os ports definidos na camada de Application. Traduzem dados entre o domínio e o mundo externo. Implementam os ports definidos na camada de Application. Traduzem dados entre o domínio e o mundo externo.
| Tipo | Exemplos | | Tipo | Exemplos |
|--------------------|---------------------------------------------------------------------------| | ------------ | --------------------------------------------------------- |
| Gateway | `FfmpegCommandBuilder`, `FfmpegGateway`, `FfprobeGateway` | | Gateway | `FfmpegCommandBuilder`, `FfmpegGateway`, `FfprobeGateway` |
| Presenter | `ErrorPresenter` (formata stderr do FFmpeg) | | Presenter | `ErrorPresenter` (formata stderr do FFmpeg) |
| File Adapter | `FilePickerAdapter` | | File Adapter | `FilePickerAdapter` |
@@ -268,9 +279,9 @@ Implementam os ports definidos na camada de Application. Traduzem dados entre o
Camada mais externa. Contém o framework de UI e a execução real de processos. Camada mais externa. Contém o framework de UI e a execução real de processos.
| Tipo | Exemplos | | Tipo | Exemplos |
|--------------|-------------------------------------------------------| | ---------- | ------------------------------------------------------ |
| UI | `App` (eframe), componentes egui | | UI | `App` (eframe), componentes egui |
| Process | Execução de `std::process::Command` / `tokio::process`| | Process | Execução de `std::process::Command` / `tokio::process` |
| Filesystem | Leitura e escrita de arquivos | | Filesystem | Leitura e escrita de arquivos |
--- ---
@@ -301,7 +312,7 @@ src/
## 10. Débitos Técnicos ## 10. Débitos Técnicos
| ID | Descrição | Impacto | Mitigação | | ID | Descrição | Impacto | Mitigação |
|-----|--------------------------------------|------------------|--------------------------------------| | ----- | ----------------------------------- | ----------------------------------- | ------------------------------------------------------------------------------ |
| DT-01 | FFmpeg não embutido | Usuário precisa instalar | Distribuir junto com a aplicação | | DT-01 | FFmpeg não embutido | Usuário precisa instalar | Distribuir junto com a aplicação |
| DT-02 | Dependência de processo externo | Menor controle interno | Encapsular via módulo de serviço | | DT-02 | Dependência de processo externo | Menor controle interno | Encapsular via módulo de serviço |
| DT-03 | Parsing de erros do FFmpeg (stderr) | Necessário tratamento manual | Parsear saída e exibir mensagem amigável | | DT-03 | Parsing de erros do FFmpeg (stderr) | Necessário tratamento manual | Parsear saída e exibir mensagem amigável |
@@ -318,7 +329,6 @@ src/
- Seleção de faixa padrão no container MKV - Seleção de faixa padrão no container MKV
- Remoção de faixas existentes do arquivo original - Remoção de faixas existentes do arquivo original
- Suporte a perfis de configuração salvos - Suporte a perfis de configuração salvos
- Modo de processamento em lote (batch)
--- ---
@@ -345,17 +355,17 @@ src/
O FFmpeg é uma ferramenta de poder absurdo — e opacidade equivalente. O usuário não entende `track index`, `stream mapping` ou `itsoffset`. O usuário entende: O FFmpeg é uma ferramenta de poder absurdo — e opacidade equivalente. O usuário não entende `track index`, `stream mapping` ou `itsoffset`. O usuário entende:
- *"Áudio em português está 1.2 segundos atrasado"* - _"Áudio em português está 1.2 segundos atrasado"_
- *"Quero adicionar a legenda em inglês"* - _"Quero adicionar a legenda em inglês"_
- *"Gerar o arquivo final"* - _"Gerar o arquivo final"_
Toda decisão de UX deve partir dessa perspectiva. Termos técnicos do FFmpeg nunca devem aparecer na interface. Toda decisão de UX deve partir dessa perspectiva. Termos técnicos do FFmpeg nunca devem aparecer na interface.
| FFmpeg (interno) | Interface (usuário) | | FFmpeg (interno) | Interface (usuário) |
|-----------------------|--------------------------------------| | -------------------- | ------------------------------ |
| `-itsoffset -1200ms` | "Adiantar 1.2s" | | `-itsoffset -1200ms` | "Adiantar 1.2s" |
| `stream 0:a:1` | "Faixa de áudio 2 — Português" | | `stream 0:a:1` | "Faixa de áudio 2 — Português" |
| `-c copy` | *(invisível — nunca exposto)* | | `-c copy` | _(invisível — nunca exposto)_ |
| `ffprobe output` | Lista de faixas detectadas | | `ffprobe output` | Lista de faixas detectadas |
--- ---
@@ -373,6 +383,7 @@ O MKV (Matroska) é estruturalmente um banco de dados multimídia. Suas operaç
O arquivo de saída não é uma criação nova — é uma nova **visão** do container original com faixas adicionadas e metadados ajustados, sem tocar nos bytes de mídia. O arquivo de saída não é uma criação nova — é uma nova **visão** do container original com faixas adicionadas e metadados ajustados, sem tocar nos bytes de mídia.
Esse modelo mental simplifica a implementação: Esse modelo mental simplifica a implementação:
- Sem buffers de vídeo - Sem buffers de vídeo
- Sem pipelines de transcoding - Sem pipelines de transcoding
- Apenas mapeamento de streams e metadados - Apenas mapeamento de streams e metadados
+20 -8
View File
@@ -2,7 +2,7 @@
**Data:** 28/02/2026 **Data:** 28/02/2026
**Status:** Fases 16 concluídas + exportação de faixa + exibição/remoção de faixas adicionadas + layout responsivo + progresso em tempo real + cancelamento de geração — compilando, 33 testes passando, zero warnings do projeto, aplicação executável **Status:** Fases 16 concluídas + exportação de faixa + exibição/remoção de faixas adicionadas + layout responsivo + progresso em tempo real + cancelamento de geração — compilando, 33 testes passando, zero warnings do projeto, aplicação executável
**Referência:** DEVELOPMENT_PLAN.md v1.1 **Referência:** DEVELOPMENT_PLAN.md v1.3
--- ---
@@ -14,6 +14,7 @@ Todas as 6 fases do plano de desenvolvimento foram implementadas. O projeto comp
Todos os 14 warnings de `unused`/`dead_code` foram resolvidos: assignment morto em `command_builder.rs` removido, lifetime explícito em `FilePath::to_string_lossy` corrigido, e APIs arquiteturais não chamadas pela UI suprimidas com `#[allow(dead_code)]`. Todos os 14 warnings de `unused`/`dead_code` foram resolvidos: assignment morto em `command_builder.rs` removido, lifetime explícito em `FilePath::to_string_lossy` corrigido, e APIs arquiteturais não chamadas pela UI suprimidas com `#[allow(dead_code)]`.
Após a conclusão das fases, foram implementadas funcionalidades adicionais: Após a conclusão das fases, foram implementadas funcionalidades adicionais:
- Exportação de faixa de áudio ou legenda diretamente da lista de faixas existentes - Exportação de faixa de áudio ou legenda diretamente da lista de faixas existentes
- Exibição das faixas externas adicionadas com opção de remoção individual - Exibição das faixas externas adicionadas com opção de remoção individual
- Layout responsivo com painéis fixos (cabeçalho, rodapé, barra lateral) e área central com scroll - Layout responsivo com painéis fixos (cabeçalho, rodapé, barra lateral) e área central com scroll
@@ -25,7 +26,7 @@ Após a conclusão das fases, foram implementadas funcionalidades adicionais:
## Status por Fase ## Status por Fase
| Fase | Descrição | Status | | Fase | Descrição | Status |
|------|----------------------------------|-------------| | ---- | ------------------------------- | ------------ |
| 1 | Setup do projeto | ✅ Concluída | | 1 | Setup do projeto | ✅ Concluída |
| 2 | Domain (núcleo puro) | ✅ Concluída | | 2 | Domain (núcleo puro) | ✅ Concluída |
| 3 | Application (use cases + ports) | ✅ Concluída | | 3 | Application (use cases + ports) | ✅ Concluída |
@@ -110,8 +111,9 @@ src/
## Testes Unitários (33/33 passando) ## Testes Unitários (33/33 passando)
### Domain — Value Objects ### Domain — Value Objects
| Teste | Resultado | | Teste | Resultado |
|-------|-----------| | ------------------------------------------------------------------------ | --------- |
| `sync_offset::from_seconds_str_positivo``"1.2"``SyncOffset(1200)` | ✅ | | `sync_offset::from_seconds_str_positivo``"1.2"``SyncOffset(1200)` | ✅ |
| `sync_offset::from_seconds_str_negativo``"-0.5"``SyncOffset(-500)` | ✅ | | `sync_offset::from_seconds_str_negativo``"-0.5"``SyncOffset(-500)` | ✅ |
| `sync_offset::from_seconds_str_com_sufixo_s` | ✅ | | `sync_offset::from_seconds_str_com_sufixo_s` | ✅ |
@@ -123,15 +125,17 @@ src/
| `track_language::idioma_invalido_longo``"port"` | ✅ | | `track_language::idioma_invalido_longo``"port"` | ✅ |
### Domain — Entities ### Domain — Entities
| Teste | Resultado | | Teste | Resultado |
|-------|-----------| | ------------------------------------------------------------------------- | --------- |
| `project::projeto_valido` | ✅ | | `project::projeto_valido` | ✅ |
| `project::projeto_invalido_mesmo_caminho` — output == source retorna erro | ✅ | | `project::projeto_invalido_mesmo_caminho` — output == source retorna erro | ✅ |
| `project::track_id_opaco` — TrackId não expõe indexação interna | ✅ | | `project::track_id_opaco` — TrackId não expõe indexação interna | ✅ |
### Application — Use Cases (todos com mocks, sem I/O real) ### Application — Use Cases (todos com mocks, sem I/O real)
| Teste | Resultado | | Teste | Resultado |
|-------|-----------| | ------------------------------------------------------------------ | --------- |
| `load_media_info::carrega_faixas_no_projeto` | ✅ | | `load_media_info::carrega_faixas_no_projeto` | ✅ |
| `add_audio_track::adiciona_audio_no_projeto` | ✅ | | `add_audio_track::adiciona_audio_no_projeto` | ✅ |
| `add_subtitle::adiciona_legenda_no_projeto` | ✅ | | `add_subtitle::adiciona_legenda_no_projeto` | ✅ |
@@ -143,8 +147,9 @@ src/
| `generate_output::sempre_inclui_c_copy` | ✅ | | `generate_output::sempre_inclui_c_copy` | ✅ |
### Adapters — FfmpegCommandBuilder ### Adapters — FfmpegCommandBuilder
| Teste | Resultado | | Teste | Resultado |
|-------|-----------| | -------------------------------------------------------- | --------- |
| `sempre_contem_c_copy` — invariante RNF-01 | ✅ | | `sempre_contem_c_copy` — invariante RNF-01 | ✅ |
| `output_e_o_ultimo_argumento` | ✅ | | `output_e_o_ultimo_argumento` | ✅ |
| `itsoffset_formato_correto``1200ms``"1.200"` | ✅ | | `itsoffset_formato_correto``1200ms``"1.200"` | ✅ |
@@ -152,8 +157,9 @@ src/
| `metadata_idioma_audio``-metadata:s:a:0 language=por` | ✅ | | `metadata_idioma_audio``-metadata:s:a:0 language=por` | ✅ |
### Application — ExportTrack ### Application — ExportTrack
| Teste | Resultado | | Teste | Resultado |
|-------|-----------| | ----------------------------------------------------------------------------------------- | --------- |
| `export_track::exporta_audio_com_c_a_copy``-c:a copy` presente, sem `-c copy` genérico | ✅ | | `export_track::exporta_audio_com_c_a_copy``-c:a copy` presente, sem `-c copy` genérico | ✅ |
| `export_track::exporta_audio_mapeia_stream_correto``0:stream_index` correto | ✅ | | `export_track::exporta_audio_mapeia_stream_correto``0:stream_index` correto | ✅ |
| `export_track::exporta_legenda_sem_c_copy` — nenhuma forma de `-c` para legendas | ✅ | | `export_track::exporta_legenda_sem_c_copy` — nenhuma forma de `-c` para legendas | ✅ |
@@ -161,8 +167,9 @@ src/
| `export_track::rejeita_faixa_de_video` — retorna erro para `TrackKind::Video` | ✅ | | `export_track::rejeita_faixa_de_video` — retorna erro para `TrackKind::Video` | ✅ |
### Application — RemoveTrack ### Application — RemoveTrack
| Teste | Resultado | | Teste | Resultado |
|-------|-----------| | ---------------------------------------------------------------------- | --------- |
| `remove_track::remove_faixa_existente` — faixa removida do Vec | ✅ | | `remove_track::remove_faixa_existente` — faixa removida do Vec | ✅ |
| `remove_track::erro_se_id_inexistente` — retorna erro se id não existe | ✅ | | `remove_track::erro_se_id_inexistente` — retorna erro se id não existe | ✅ |
@@ -195,12 +202,16 @@ rfd = "0.14"
## O Que Falta (Backlog de Refinamento) ## O Que Falta (Backlog de Refinamento)
### Funcional ### Funcional
- [x] Exportar faixa de áudio ou legenda existente para arquivo separado - [x] Exportar faixa de áudio ou legenda existente para arquivo separado
- [x] Exibição de faixas externas adicionadas com opção de remover - [x] Exibição de faixas externas adicionadas com opção de remover
- [x] Progresso em tempo real via `run_ffmpeg_async``run_ffmpeg_async` usa `std::sync::mpsc::Sender<String>`; `start_generation` cria tokio Runtime + thread encaminhadora; cada linha de stderr do FFmpeg aparece no log antes do término - [x] Progresso em tempo real via `run_ffmpeg_async``run_ffmpeg_async` usa `std::sync::mpsc::Sender<String>`; `start_generation` cria tokio Runtime + thread encaminhadora; cada linha de stderr do FFmpeg aparece no log antes do término
- [x] Cancelamento de geração — botão "Cancelar" visível durante `Running`; sinal via `tokio::sync::oneshot`; `child.kill().await` no `tokio::select!`; estado `ExecutionState::Cancelled` exibido em amarelo - [x] Cancelamento de geração — botão "Cancelar" visível durante `Running`; sinal via `tokio::sync::oneshot`; `child.kill().await` no `tokio::select!`; estado `ExecutionState::Cancelled` exibido em amarelo
- [ ] **Fase 7 — Persistência do Estado** — salvar/carregar `Project` em JSON (`~/.config/...`); botões "Salvar sessão" / "Carregar sessão" no header; novo módulo `infrastructure/persistence` — ver DEVELOPMENT_PLAN.md Fase 7
- [ ] **Fase 8 — Modo Lote via Abas** — navegação por abas (`Projeto Único` / `Lote`); `Vec<BatchItem>` em `App`; processamento sequencial (um por vez); novo componente `BatchPanel` — ver DEVELOPMENT_PLAN.md Fase 8
### Qualidade ### Qualidade
- [x] Corrigir 14 warnings de `unused`/`dead_code` identificados por `cargo build`: - [x] Corrigir 14 warnings de `unused`/`dead_code` identificados por `cargo build`:
- `next_input_idx += 1` removido do loop externo em `command_builder.rs` (assignment nunca lido) - `next_input_idx += 1` removido do loop externo em `command_builder.rs` (assignment nunca lido)
- Lifetime explícito `Cow<'_, str>` adicionado em `FilePath::to_string_lossy` - Lifetime explícito `Cow<'_, str>` adicionado em `FilePath::to_string_lossy`
@@ -210,6 +221,7 @@ rfd = "0.14"
- [ ] Testes de snapshot para o `FfmpegCommandBuilder` com projetos mais complexos - [ ] Testes de snapshot para o `FfmpegCommandBuilder` com projetos mais complexos
### UI ### UI
- [x] Layout responsivo mais refinado - [x] Layout responsivo mais refinado
- [ ] Exibição do comando FFmpeg gerado em modo debug (ex: painel colapsável na `ExecutionPanel`) - [ ] Exibição do comando FFmpeg gerado em modo debug (ex: painel colapsável na `ExecutionPanel`)
- [ ] Persistência do estado do projeto em disco (`serde` já disponível) - [ ] Persistência do estado do projeto em disco (`serde` já disponível)