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:
+142
-35
@@ -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 1–6 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:
|
||||
|
||||
Reference in New Issue
Block a user