feat: adiciona funcionalidade para reordenar faixas externas com botões ↑↓ na interface
This commit is contained in:
@@ -1,9 +1,9 @@
|
|||||||
# PRD — Simple Multimedia Track Audio Editor
|
# PRD — Simple Multimedia Track Audio Editor
|
||||||
|
|
||||||
**Versão:** 1.4
|
**Versão:** 1.5
|
||||||
**Data:** 01/03/2026
|
**Data:** 02/03/2026
|
||||||
**Status:** v1.0 funcional — Fases 1–9 concluídas; 33 testes passando; aplicação executável
|
**Status:** v1.0 funcional — Fases 1–9 concluídas; 72 testes passando; aplicação executável
|
||||||
**Revisão:** v1.4 — Adicionado RF-15 (Excluir faixa existente do output)
|
**Revisão:** v1.5 — Adicionado RF-16 (Reordenar faixas externas)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -167,6 +167,15 @@ As ferramentas existentes são complexas (ex: interface direta do FFmpeg via CLI
|
|||||||
- Ambos os pipelines (FFmpeg e mkvmerge) respeitam o flag: faixas excluídas são filtradas antes da geração dos argumentos de `-map` / `--audio-tracks` / `--subtitle-tracks`
|
- Ambos os pipelines (FFmpeg e mkvmerge) respeitam o flag: faixas excluídas são filtradas antes da geração dos argumentos de `-map` / `--audio-tracks` / `--subtitle-tracks`
|
||||||
- O estado `excluded` é persistido junto com a sessão (RF-14)
|
- O estado `excluded` é persistido junto com a sessão (RF-14)
|
||||||
|
|
||||||
|
### RF-16 — Reordenar faixas externas
|
||||||
|
|
||||||
|
- O usuário pode alterar a ordem das faixas externas adicionadas (áudio e legenda) usando os botões **↑** e **↓** na lista de faixas
|
||||||
|
- A ordem determina o índice de stream no container MKV final — relevante para players que selecionam faixas por posição (ex: VLC, Jellyfin)
|
||||||
|
- O botão ↑ fica desabilitado na primeira faixa da lista; o botão ↓, na última
|
||||||
|
- Implementado via método `move_track(id, delta: i8)` em `Project`, usando `Vec::swap` — ambos os pipelines (FFmpeg e mkvmerge) iteram `project.tracks` em ordem e não precisam de alteração
|
||||||
|
- Somente faixas **externas** (`project.tracks`) são reordenáveis; faixas existentes (`project.existing_tracks`, lidas via ffprobe) permanecem na ordem detectada
|
||||||
|
- A nova ordem é persistida automaticamente junto com a sessão (RF-14)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. Requisitos Não Funcionais
|
## 7. Requisitos Não Funcionais
|
||||||
@@ -422,6 +431,7 @@ src/
|
|||||||
- [x] É possível exportar uma faixa existente para arquivo separado (RF-13)
|
- [x] É possível exportar uma faixa existente para arquivo separado (RF-13)
|
||||||
- [x] É possível salvar e restaurar a sessão em disco (RF-14)
|
- [x] É possível salvar e restaurar a sessão em disco (RF-14)
|
||||||
- [x] É possível excluir faixas de áudio ou legenda existentes do arquivo de saída, com possibilidade de restauração antes da geração (RF-15)
|
- [x] É possível excluir faixas de áudio ou legenda existentes do arquivo de saída, com possibilidade de restauração antes da geração (RF-15)
|
||||||
|
- [x] É possível reordenar as faixas externas adicionadas com botões ↑↓; a ordem reflete o índice no container final (RF-16)
|
||||||
- [x] O modo lote permite configurar e processar múltiplos projetos sequencialmente (RF-09)
|
- [x] O modo lote permite configurar e processar múltiplos projetos sequencialmente (RF-09)
|
||||||
- [x] É possível definir fator de correção de drift por faixa (RF-12; requer mkvmerge)
|
- [x] É possível definir fator de correção de drift por faixa (RF-12; requer mkvmerge)
|
||||||
- [x] O arquivo MKV é gerado corretamente ao confirmar
|
- [x] O arquivo MKV é gerado corretamente ao confirmar
|
||||||
|
|||||||
@@ -0,0 +1,260 @@
|
|||||||
|
# Implementação — Reordenar Faixas Externas (botões ↑↓)
|
||||||
|
|
||||||
|
**Data:** 01/03/2026
|
||||||
|
**Status:** ✅ Implementado
|
||||||
|
**Prioridade:** Baixa — melhoria de usabilidade
|
||||||
|
**Arquivos afetados:**
|
||||||
|
|
||||||
|
- `src/domain/entities/project.rs`
|
||||||
|
- `src/ui/components/added_track_list.rs`
|
||||||
|
- `src/ui/app.rs`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Objetivo
|
||||||
|
|
||||||
|
Permitir que o usuário reordene as faixas externas adicionadas (áudio e legenda) usando botões ↑↓ na lista. A ordem do `Vec<Track>` em `project.tracks` determina diretamente o índice de stream no container MKV final — já que ambos os command builders (`FfmpegCommandBuilder` e `MkvmergeCommandBuilder`) iteram esse Vec posicionalmente a cada `build()`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Por que botões ↑↓ e não drag-and-drop
|
||||||
|
|
||||||
|
| Critério | Botões ↑↓ | Drag-and-drop (egui) |
|
||||||
|
| ------------------------ | --------------------------------------- | -------------------------------------------- |
|
||||||
|
| Esforço de implementação | ~1h | ~5–6h |
|
||||||
|
| Risco de regressão | Nulo — mudanças aditivas | Baixo, mas requer estado extra no componente |
|
||||||
|
| Clareza de UX | Explícita, sem ambiguidade | Mais fluido, porém menos óbvio em grids |
|
||||||
|
| Testabilidade | Método de domínio testável isoladamente | Lógica de UI difícil de testar |
|
||||||
|
|
||||||
|
**Conclusão:** botões ↑↓ entregam 100% do valor com ~15% do esforço.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Análise da arquitetura atual
|
||||||
|
|
||||||
|
A ordem das faixas externas já é a fonte de verdade do índice no output:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// FfmpegCommandBuilder::build() — sem cache de índice
|
||||||
|
let mut ext_idx = external_input_start;
|
||||||
|
for track in &project.tracks { // ← itera em ordem
|
||||||
|
args.push("-map".to_string());
|
||||||
|
// ...
|
||||||
|
ext_idx += 1;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// MkvmergeCommandBuilder::build() — idem
|
||||||
|
for track in &project.tracks { // ← itera em ordem
|
||||||
|
// cada arquivo externo vira um input separado
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`TrackId` é opaco (`TrackId(u32)`) — não representa posição, apenas identidade. Uma troca de posição no Vec não invalida nenhum `TrackId` existente.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Passo 1 — Método `move_track` em `Project`
|
||||||
|
|
||||||
|
**Arquivo:** `src/domain/entities/project.rs`
|
||||||
|
|
||||||
|
Adicionar logo após `remove_track`:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
/// Move uma faixa externa para cima (`delta = -1`) ou para baixo (`delta = 1`).
|
||||||
|
/// Retorna `true` se a faixa foi encontrada e o movimento era possível.
|
||||||
|
pub fn move_track(&mut self, id: TrackId, delta: i8) -> bool {
|
||||||
|
let Some(pos) = self.tracks.iter().position(|t| t.id() == id) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let new_pos = pos as i64 + delta as i64;
|
||||||
|
if new_pos < 0 || new_pos >= self.tracks.len() as i64 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
self.tracks.swap(pos, new_pos as usize);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Passo 2 — Novos eventos em `AddedTrackEvent`
|
||||||
|
|
||||||
|
**Arquivo:** `src/ui/components/added_track_list.rs`
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub enum AddedTrackEvent {
|
||||||
|
RemoveRequested(TrackId),
|
||||||
|
MoveUp(TrackId), // ← novo
|
||||||
|
MoveDown(TrackId), // ← novo
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Passo 3 — Botões ↑↓ na grid
|
||||||
|
|
||||||
|
**Arquivo:** `src/ui/components/added_track_list.rs`
|
||||||
|
|
||||||
|
Alterar a assinatura do método para receber o índice atual e o total, e adicionar os botões na última coluna.
|
||||||
|
|
||||||
|
A grid passa de 4 para 5 colunas (`num_columns(5)`). O cabeçalho ganha uma coluna `""` extra. Cada linha recebe os botões ↑ e ↓, desabilitados na primeira e última posição respectivamente:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
pub fn ui(&mut self, ui: &mut egui::Ui, tracks: &[Track]) -> Vec<AddedTrackEvent> {
|
||||||
|
let mut events = Vec::new();
|
||||||
|
let total = tracks.len();
|
||||||
|
|
||||||
|
ui.group(|ui| {
|
||||||
|
ui.heading("Faixas adicionadas");
|
||||||
|
|
||||||
|
if tracks.is_empty() {
|
||||||
|
ui.label("Nenhuma faixa adicionada.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
egui::Grid::new("added_tracks_grid")
|
||||||
|
.num_columns(5) // ← era 4
|
||||||
|
.max_col_width(200.0)
|
||||||
|
.striped(true)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
ui.strong("Tipo");
|
||||||
|
ui.strong("Arquivo");
|
||||||
|
ui.strong("Idioma");
|
||||||
|
ui.strong(""); // coluna de ordem
|
||||||
|
ui.strong(""); // coluna de remover
|
||||||
|
ui.end_row();
|
||||||
|
|
||||||
|
for (idx, track) in tracks.iter().enumerate() {
|
||||||
|
// ... células existentes (tipo, arquivo, idioma) ...
|
||||||
|
|
||||||
|
// ── Botões de ordenação ──
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.add_enabled_ui(idx > 0, |ui| {
|
||||||
|
if ui.small_button("↑").clicked() {
|
||||||
|
events.push(AddedTrackEvent::MoveUp(track.id()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ui.add_enabled_ui(idx + 1 < total, |ui| {
|
||||||
|
if ui.small_button("↓").clicked() {
|
||||||
|
events.push(AddedTrackEvent::MoveDown(track.id()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if ui.small_button("🗑 Remover").clicked() {
|
||||||
|
events.push(AddedTrackEvent::RemoveRequested(track.id()));
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.end_row();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
events
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Passo 4 — Tratar eventos em `app.rs`
|
||||||
|
|
||||||
|
**Arquivo:** `src/ui/app.rs`
|
||||||
|
|
||||||
|
O trecho que já trata `RemoveRequested` precisa ser expandido:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let track_events = self.added_track_list.ui(ui, &project.tracks);
|
||||||
|
for event in track_events {
|
||||||
|
use crate::ui::components::added_track_list::AddedTrackEvent;
|
||||||
|
match event {
|
||||||
|
AddedTrackEvent::RemoveRequested(id) => {
|
||||||
|
let _ = RemoveTrack::execute(project, id);
|
||||||
|
}
|
||||||
|
AddedTrackEvent::MoveUp(id) => {
|
||||||
|
project.move_track(id, -1);
|
||||||
|
}
|
||||||
|
AddedTrackEvent::MoveDown(id) => {
|
||||||
|
project.move_track(id, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Passo 5 — Testes unitários para `move_track`
|
||||||
|
|
||||||
|
**Arquivo:** `src/domain/entities/project.rs` — bloco `#[cfg(test)]` existente.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[test]
|
||||||
|
fn move_track_para_cima() {
|
||||||
|
let mut p = make_project("i.mkv", "o.mkv").unwrap();
|
||||||
|
let t1 = audio_track_with_drift(1, 1.0);
|
||||||
|
let t2 = audio_track_with_drift(2, 1.0);
|
||||||
|
let id1 = t1.id();
|
||||||
|
let id2 = t2.id();
|
||||||
|
p.tracks.push(t1);
|
||||||
|
p.tracks.push(t2);
|
||||||
|
|
||||||
|
assert!(p.move_track(id2, -1));
|
||||||
|
assert_eq!(p.tracks[0].id(), id2);
|
||||||
|
assert_eq!(p.tracks[1].id(), id1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn move_track_para_baixo() {
|
||||||
|
let mut p = make_project("i.mkv", "o.mkv").unwrap();
|
||||||
|
let t1 = audio_track_with_drift(1, 1.0);
|
||||||
|
let t2 = audio_track_with_drift(2, 1.0);
|
||||||
|
let id1 = t1.id();
|
||||||
|
let id2 = t2.id();
|
||||||
|
p.tracks.push(t1);
|
||||||
|
p.tracks.push(t2);
|
||||||
|
|
||||||
|
assert!(p.move_track(id1, 1));
|
||||||
|
assert_eq!(p.tracks[0].id(), id2);
|
||||||
|
assert_eq!(p.tracks[1].id(), id1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn move_track_limite_superior_ignorado() {
|
||||||
|
let mut p = make_project("i.mkv", "o.mkv").unwrap();
|
||||||
|
let t1 = audio_track_with_drift(1, 1.0);
|
||||||
|
let id1 = t1.id();
|
||||||
|
p.tracks.push(t1);
|
||||||
|
|
||||||
|
assert!(!p.move_track(id1, -1)); // já é o primeiro — não move
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn move_track_limite_inferior_ignorado() {
|
||||||
|
let mut p = make_project("i.mkv", "o.mkv").unwrap();
|
||||||
|
let t1 = audio_track_with_drift(1, 1.0);
|
||||||
|
let id1 = t1.id();
|
||||||
|
p.tracks.push(t1);
|
||||||
|
|
||||||
|
assert!(!p.move_track(id1, 1)); // já é o último — não move
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Escopo e limitações
|
||||||
|
|
||||||
|
- Somente faixas **externas** (`project.tracks`) são reordenáveis. Faixas existentes (`project.existing_tracks`, lidas via ffprobe) permanecem na ordem detectada — essa restrição deve ser comunicada visualmente (ex: tooltip ou nota abaixo da lista de faixas adicionadas).
|
||||||
|
- A ordenação é persistida automaticamente via RF-14 (sessão salva em JSON inclui o Vec na nova ordem).
|
||||||
|
- Sem impacto no modo Lote: cada `BatchItem` tem seu próprio `Project` independente; a mesma lógica se aplica.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Checklist de implementação
|
||||||
|
|
||||||
|
- [x] `Project::move_track()` adicionado e testado (4 testes novos)
|
||||||
|
- [x] `AddedTrackEvent` com variantes `MoveUp` e `MoveDown`
|
||||||
|
- [x] Grid de 5 colunas com botões ↑↓ desabilitados nas bordas
|
||||||
|
- [x] Handler em `app.rs` tratando os dois novos eventos
|
||||||
|
- [x] `cargo test` — 72 testes passando, 0 falhas
|
||||||
|
- [x] `cargo check` sem warnings (3 warnings pré-existentes não relacionados)
|
||||||
@@ -69,6 +69,20 @@ impl Project {
|
|||||||
self.tracks.len() < before
|
self.tracks.len() < before
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Move uma faixa externa para cima (`delta = -1`) ou para baixo (`delta = 1`).
|
||||||
|
/// Retorna `true` se a faixa foi encontrada e o movimento era possível.
|
||||||
|
pub fn move_track(&mut self, id: TrackId, delta: i8) -> bool {
|
||||||
|
let Some(pos) = self.tracks.iter().position(|t| t.id() == id) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let new_pos = pos as i64 + delta as i64;
|
||||||
|
if new_pos < 0 || new_pos >= self.tracks.len() as i64 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
self.tracks.swap(pos, new_pos as usize);
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
/// Retorna `true` se qualquer faixa (externa ou existente) possui `drift_scale != 1.0`,
|
/// Retorna `true` se qualquer faixa (externa ou existente) possui `drift_scale != 1.0`,
|
||||||
/// indicando que o projeto deve usar o pipeline mkvmerge em vez do FFmpeg.
|
/// indicando que o projeto deve usar o pipeline mkvmerge em vez do FFmpeg.
|
||||||
pub fn needs_mkvmerge(&self) -> bool {
|
pub fn needs_mkvmerge(&self) -> bool {
|
||||||
@@ -195,4 +209,54 @@ mod tests {
|
|||||||
assert!(!p.toggle_existing_track_excluded(id));
|
assert!(!p.toggle_existing_track_excluded(id));
|
||||||
assert!(!p.existing_tracks[0].excluded);
|
assert!(!p.existing_tracks[0].excluded);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn move_track_para_cima() {
|
||||||
|
let mut p = make_project("i.mkv", "o.mkv").unwrap();
|
||||||
|
let t1 = audio_track_with_drift(1, 1.0);
|
||||||
|
let t2 = audio_track_with_drift(2, 1.0);
|
||||||
|
let id1 = t1.id();
|
||||||
|
let id2 = t2.id();
|
||||||
|
p.tracks.push(t1);
|
||||||
|
p.tracks.push(t2);
|
||||||
|
|
||||||
|
assert!(p.move_track(id2, -1));
|
||||||
|
assert_eq!(p.tracks[0].id(), id2);
|
||||||
|
assert_eq!(p.tracks[1].id(), id1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn move_track_para_baixo() {
|
||||||
|
let mut p = make_project("i.mkv", "o.mkv").unwrap();
|
||||||
|
let t1 = audio_track_with_drift(1, 1.0);
|
||||||
|
let t2 = audio_track_with_drift(2, 1.0);
|
||||||
|
let id1 = t1.id();
|
||||||
|
let id2 = t2.id();
|
||||||
|
p.tracks.push(t1);
|
||||||
|
p.tracks.push(t2);
|
||||||
|
|
||||||
|
assert!(p.move_track(id1, 1));
|
||||||
|
assert_eq!(p.tracks[0].id(), id2);
|
||||||
|
assert_eq!(p.tracks[1].id(), id1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn move_track_limite_superior_ignorado() {
|
||||||
|
let mut p = make_project("i.mkv", "o.mkv").unwrap();
|
||||||
|
let t1 = audio_track_with_drift(1, 1.0);
|
||||||
|
let id1 = t1.id();
|
||||||
|
p.tracks.push(t1);
|
||||||
|
|
||||||
|
assert!(!p.move_track(id1, -1)); // já é o primeiro — não move
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn move_track_limite_inferior_ignorado() {
|
||||||
|
let mut p = make_project("i.mkv", "o.mkv").unwrap();
|
||||||
|
let t1 = audio_track_with_drift(1, 1.0);
|
||||||
|
let id1 = t1.id();
|
||||||
|
p.tracks.push(t1);
|
||||||
|
|
||||||
|
assert!(!p.move_track(id1, 1)); // já é o último — não move
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-2
@@ -805,13 +805,19 @@ impl eframe::App for App {
|
|||||||
ui.add_space(8.0);
|
ui.add_space(8.0);
|
||||||
|
|
||||||
// ── Faixas externas adicionadas ───────────────────────
|
// ── Faixas externas adicionadas ───────────────────────
|
||||||
let remove_events = self.added_track_list.ui(ui, &project.tracks);
|
let track_events = self.added_track_list.ui(ui, &project.tracks);
|
||||||
for event in remove_events {
|
for event in track_events {
|
||||||
use crate::ui::components::added_track_list::AddedTrackEvent;
|
use crate::ui::components::added_track_list::AddedTrackEvent;
|
||||||
match event {
|
match event {
|
||||||
AddedTrackEvent::RemoveRequested(id) => {
|
AddedTrackEvent::RemoveRequested(id) => {
|
||||||
let _ = RemoveTrack::execute(project, id);
|
let _ = RemoveTrack::execute(project, id);
|
||||||
}
|
}
|
||||||
|
AddedTrackEvent::MoveUp(id) => {
|
||||||
|
project.move_track(id, -1);
|
||||||
|
}
|
||||||
|
AddedTrackEvent::MoveDown(id) => {
|
||||||
|
project.move_track(id, 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ use eframe::egui;
|
|||||||
/// Evento emitido por interações com a lista de faixas externas adicionadas.
|
/// Evento emitido por interações com a lista de faixas externas adicionadas.
|
||||||
pub enum AddedTrackEvent {
|
pub enum AddedTrackEvent {
|
||||||
RemoveRequested(TrackId),
|
RemoveRequested(TrackId),
|
||||||
|
MoveUp(TrackId),
|
||||||
|
MoveDown(TrackId),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Lista as faixas externas adicionadas pelo usuário e permite removê-las.
|
/// Lista as faixas externas adicionadas pelo usuário e permite removê-las.
|
||||||
@@ -15,9 +17,10 @@ impl AddedTrackList {
|
|||||||
AddedTrackList
|
AddedTrackList
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Renderiza a lista. Retorna eventos de remoção.
|
/// Renderiza a lista. Retorna eventos de interação (remoção e reordenação).
|
||||||
pub fn ui(&mut self, ui: &mut egui::Ui, tracks: &[Track]) -> Vec<AddedTrackEvent> {
|
pub fn ui(&mut self, ui: &mut egui::Ui, tracks: &[Track]) -> Vec<AddedTrackEvent> {
|
||||||
let mut events = Vec::new();
|
let mut events = Vec::new();
|
||||||
|
let total = tracks.len();
|
||||||
|
|
||||||
ui.group(|ui| {
|
ui.group(|ui| {
|
||||||
ui.heading("Faixas adicionadas");
|
ui.heading("Faixas adicionadas");
|
||||||
@@ -28,17 +31,18 @@ impl AddedTrackList {
|
|||||||
}
|
}
|
||||||
|
|
||||||
egui::Grid::new("added_tracks_grid")
|
egui::Grid::new("added_tracks_grid")
|
||||||
.num_columns(4)
|
.num_columns(5)
|
||||||
.max_col_width(200.0)
|
.max_col_width(200.0)
|
||||||
.striped(true)
|
.striped(true)
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
ui.strong("Tipo");
|
ui.strong("Tipo");
|
||||||
ui.strong("Arquivo");
|
ui.strong("Arquivo");
|
||||||
ui.strong("Idioma");
|
ui.strong("Idioma");
|
||||||
|
ui.strong("Ordem");
|
||||||
ui.strong("");
|
ui.strong("");
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
|
|
||||||
for track in tracks {
|
for (idx, track) in tracks.iter().enumerate() {
|
||||||
let (kind_label, file_name, language) = match track {
|
let (kind_label, file_name, language) = match track {
|
||||||
Track::Audio(t) => (
|
Track::Audio(t) => (
|
||||||
"Áudio",
|
"Áudio",
|
||||||
@@ -67,6 +71,28 @@ impl AddedTrackList {
|
|||||||
.on_hover_text(track_full_path(track));
|
.on_hover_text(track_full_path(track));
|
||||||
ui.label(language);
|
ui.label(language);
|
||||||
|
|
||||||
|
// ── Botões de reordenação ──────────────────────────
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.add_enabled_ui(idx > 0, |ui| {
|
||||||
|
if ui
|
||||||
|
.small_button("↑")
|
||||||
|
.on_hover_text("Mover para cima")
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
events.push(AddedTrackEvent::MoveUp(track.id()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ui.add_enabled_ui(idx + 1 < total, |ui| {
|
||||||
|
if ui
|
||||||
|
.small_button("↓")
|
||||||
|
.on_hover_text("Mover para baixo")
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
events.push(AddedTrackEvent::MoveDown(track.id()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
if ui.small_button("🗑 Remover").clicked() {
|
if ui.small_button("🗑 Remover").clicked() {
|
||||||
events.push(AddedTrackEvent::RemoveRequested(track.id()));
|
events.push(AddedTrackEvent::RemoveRequested(track.id()));
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user