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
+142 -35
View File
@@ -1,9 +1,9 @@
# Plano de Desenvolvimento — Simple Multimedia Track Audio Editor
**Versão:** 1.1
**Versão:** 1.3
**Data:** 28/02/2026
**Status:** Concluído — todas as fases implementadas, 33 testes passando
**Referência:** PRD v1.1
**Status:** Fases 16 concluídas; Fase 7 (Persistência) e Fase 8 (Modo Lote) em planejamento
**Referência:** PRD v1.2
---
@@ -45,23 +45,23 @@ Cada fase deve estar **compilando e com testes passando** antes de avançar para
### 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 |
| 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` |
| 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 |
| `MkvOutput` | `path: FilePath` |
| `Project` | Entidade raiz — ver abaixo |
#### Estrutura de `Project`
@@ -110,15 +110,15 @@ pub trait FileSystemPort {
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` |
| # | 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
@@ -140,11 +140,13 @@ Implementar nesta ordem (dependência crescente):
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
```
@@ -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`
Implementa `MediaInfoPort`. Executa:
```
ffprobe -v quiet -print_format json -show_streams <path>
```
e mapeia a saída JSON para `Vec<MediaTrackInfo>`.
- [x] Implementar parsing de JSON via `serde_json`
@@ -205,16 +209,16 @@ Implementa `MediaProcessorPort`. Executa o processo real do FFmpeg e captura std
### 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 |
| # | 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`)
@@ -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)
Alinhados com o PRD seção 12: