mirror of
https://github.com/storytold/filmcraft.git
synced 2026-10-14 19:26:59 -04:00
Merge pull request #1471 from marcoarnulfo/fix/recent-projects
fix(home): list recent projects on Home and in File ▸ Open Recent
This commit is contained in:
commit
7bc739bfae
12 files changed
+141
-2
No files matched your search
@@ -248,6 +248,8 @@
|
||||
OMF… OMF…
|
||||
Offline File… Offline-Datei…
|
||||
Open Project… Projekt öffnen…
|
||||
Open Recent Zuletzt geöffnete Dateien
|
||||
Clear Recent Files Zuletzt geöffnete Dateien leeren
|
||||
OpenTimelineIO… OpenTimelineIO…
|
||||
Optical Flow Optischer Fluss
|
||||
Overwrite Überschreiben
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 45.
|
@@ -230,6 +230,9 @@
|
||||
Normalize Mix Track… Normalizar pista de mezcla…
|
||||
Offline File… Archivo sin conexión…
|
||||
Open Project… Abrir proyecto…
|
||||
Open Recent Archivos recientes
|
||||
Clear Recent Files Vaciar archivos recientes
|
||||
Recent Recientes
|
||||
Optical Flow Flujo óptico
|
||||
Overwrite Sobrescribir
|
||||
Paste Pegar
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 3 and column 95.
|
@@ -232,6 +232,9 @@
|
||||
Normalize Mix Track… Normaliser la piste de mixage…
|
||||
Offline File… Fichier hors ligne…
|
||||
Open Project… Ouvrir un projet…
|
||||
Open Recent Ouvrir un fichier récent
|
||||
Clear Recent Files Effacer la liste des fichiers récents
|
||||
Recent Récents
|
||||
Optical Flow Flux optique
|
||||
Overwrite Écraser
|
||||
Paste Coller
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 83.
|
@@ -237,6 +237,9 @@
|
||||
Normalize Mix Track… Normalisasi Trek Mix…
|
||||
Offline File… Berkas Offline…
|
||||
Open Project… Buka Proyek…
|
||||
Open Recent Terakhir Dibuka
|
||||
Clear Recent Files Hapus Daftar Berkas Terakhir
|
||||
Recent Terbaru
|
||||
Optical Flow Aliran Optik
|
||||
Overwrite Timpa
|
||||
Paste Tempel
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 14.
|
@@ -231,6 +231,9 @@
|
||||
Normalize Mix Track… ミックストラックをノーマライズ…
|
||||
Offline File… オフラインファイル…
|
||||
Open Project… プロジェクトを開く…
|
||||
Open Recent 最近使ったファイルを開く
|
||||
Clear Recent Files 最近使ったファイルを消去
|
||||
Recent 最近
|
||||
Optical Flow オプティカルフロー
|
||||
Overwrite 上書き
|
||||
Paste 貼り付け
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 11 and column 59.
|
@@ -234,6 +234,9 @@
|
||||
Normalize Mix Track… Normalizar faixa de mixagem…
|
||||
Offline File… Arquivo offline…
|
||||
Open Project… Abrir projeto…
|
||||
Open Recent Abrir recentes
|
||||
Clear Recent Files Limpar arquivos recentes
|
||||
Recent Recentes
|
||||
Optical Flow Fluxo óptico
|
||||
Overwrite Sobrescrever
|
||||
Paste Colar
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 4.
|
@@ -221,6 +221,8 @@
|
||||
Normalize Mix Track… Нормализовать мастер-дорожку…
|
||||
Offline File… Автономный файл…
|
||||
Open Project… Открыть проект…
|
||||
Open Recent Открыть недавние
|
||||
Clear Recent Files Очистить список недавних файлов
|
||||
Optical Flow Оптический поток
|
||||
Overwrite Перезаписать
|
||||
Paste Вставить
|
||||
|
||||
|
@@ -222,6 +222,8 @@
|
||||
Normalize Mix Track… Нормалізувати доріжку міксу…
|
||||
Offline File… Автономний файл…
|
||||
Open Project… Відкрити проєкт…
|
||||
Open Recent Нещодавні файли
|
||||
Clear Recent Files Очистити список нещодавніх файлів
|
||||
Optical Flow Оптичний потік
|
||||
Overwrite Перезаписати
|
||||
Paste Вставити
|
||||
|
||||
|
@@ -225,6 +225,9 @@
|
||||
Normalize Mix Track… 标准化混音轨道…
|
||||
Offline File… 脱机文件…
|
||||
Open Project… 打开项目…
|
||||
Open Recent 打开最近文件
|
||||
Clear Recent Files 清除最近文件
|
||||
Recent 最近使用
|
||||
Optical Flow 光流
|
||||
Overwrite 覆盖
|
||||
Paste 粘贴
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 3 and column 95.
|
@@ -550,6 +550,17 @@ pub fn invoke(app: &mut FilmcraftApp, ctx: &egui::Context, id: &str, mut params:
|
||||
crate::panels::settings::set_theme(app, ctx, k);
|
||||
return Ok(Value::Null);
|
||||
}
|
||||
// File ▸ Open Recent
|
||||
if let Some(i) = id.strip_prefix("file.openRecent.") {
|
||||
let path = i.parse::<usize>().ok().and_then(|i| app.session.prefs.general.recent_projects.get(i).cloned()).ok_or("no such recent project")?;
|
||||
return invoke(app, ctx, "file.open", json!({"path": path}));
|
||||
}
|
||||
if id == "file.clearRecent" {
|
||||
let mut next = app.session.prefs.clone();
|
||||
next.general.recent_projects.clear();
|
||||
app.session.set_prefs(next).map_err(|e| e.to_string())?;
|
||||
return Ok(Value::Null);
|
||||
}
|
||||
// File dialogs for commands that need a path.
|
||||
if (id == "file.import" && params.get("paths").is_none() && params.get("path").is_none())
|
||||
|| (id == "file.importImageSequence" && params.get("path").is_none())
|
||||
@@ -682,9 +693,31 @@ pub fn menu_items(app: &FilmcraftApp) -> Vec<MenuItem> {
|
||||
}
|
||||
}
|
||||
crate::panels::workspaces::menu(app, &mut v);
|
||||
open_recent(&mut v, &app.session, |n| app.ui.language.tr(n).to_string());
|
||||
v
|
||||
}
|
||||
|
||||
/// File ▸ Open Recent, after Open Project…: the recent projects (General ▸ `recentProjects`),
|
||||
/// newest first, then Clear Recent Files. Clear is always there, so the submenu exists (in the
|
||||
/// native menu too, built once at launch) before the first project is saved.
|
||||
fn open_recent(items: &mut Vec<MenuItem>, session: &filmcraft_engine::Session, tr: impl Fn(&str) -> String) {
|
||||
let Some(at) = items.iter().position(|it| it.id == "file.open") else { return };
|
||||
let path: Vec<String> = vec!["File".into(), "Open Recent".into()];
|
||||
let recent = &session.prefs.general.recent_projects;
|
||||
let mut sub: Vec<MenuItem> = recent
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, p)| MenuItem { id: format!("file.openRecent.{i}"), label: recent_name(p), path: path.clone(), shortcut: None, enabled: true, checked: None })
|
||||
.collect();
|
||||
sub.push(MenuItem { id: "file.clearRecent".into(), label: tr("Clear Recent Files"), path, shortcut: None, enabled: !recent.is_empty(), checked: None });
|
||||
items.splice(at + 1..at + 1, sub);
|
||||
}
|
||||
|
||||
/// A recent project as listed in File ▸ Open Recent and on Home: its file name.
|
||||
pub fn recent_name(path: &str) -> String {
|
||||
std::path::Path::new(path).file_name().map_or_else(|| path.to_string(), |n| n.to_string_lossy().into_owned())
|
||||
}
|
||||
|
||||
/// The menu bar as it ships, without a running app: English labels, default preferences, the
|
||||
/// built-in workspaces, nothing checked. Deterministic, so `cargo xtask parity` can measure it
|
||||
/// (`examples/menu_tree.rs` prints it as JSON).
|
||||
@@ -692,6 +725,7 @@ pub fn default_menu_items() -> Vec<MenuItem> {
|
||||
let session = filmcraft_engine::Session::default();
|
||||
let mut v = menu_items_for(&session);
|
||||
crate::panels::workspaces::default_menu(&mut v, &session);
|
||||
open_recent(&mut v, &session, str::to_string);
|
||||
v
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
//! Import mode (header "Import"): large browser cards for importing media and demo footage.
|
||||
//! Import mode (header "Import", also Home): large browser cards for importing media and demo footage,
|
||||
//! and the recent projects.
|
||||
|
||||
use egui::{Align2, Color32, Rect, Sense, pos2, vec2};
|
||||
use serde_json::json;
|
||||
@@ -43,8 +44,8 @@ pub fn show(app: &mut FilmcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
app.ui.mode = crate::state::Mode::Edit;
|
||||
}
|
||||
}
|
||||
let y = recent(app, ui, rect, 90.0 + 150.0 + 36.0);
|
||||
// Community and project links.
|
||||
let y = 90.0 + 150.0 + 36.0;
|
||||
// ArtCraft wordmark (first-party trademark, docs/brand/), then the section title.
|
||||
let logo = crate::brand::paint_wordmark(ui, rect.min + vec2(24.0, y), 15.0, app.ui.dark);
|
||||
let tx = logo.map_or(rect.min.x + 24.0, |r| r.max.x + 12.0);
|
||||
@@ -72,3 +73,45 @@ pub fn show(app: &mut FilmcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
x = r.max.x + 12.0;
|
||||
}
|
||||
}
|
||||
|
||||
/// Recent projects listed on Home, as in File ▸ Open Recent.
|
||||
const RECENT_ROWS: usize = 6;
|
||||
const RECENT_ROW_H: f32 = 28.0;
|
||||
|
||||
/// Home's Recent list at `y`: the recent projects (newest first, file name with its folder beside
|
||||
/// it), a click opens one. Returns where the next section starts.
|
||||
fn recent(app: &mut FilmcraftApp, ui: &mut egui::Ui, rect: Rect, y: f32) -> f32 {
|
||||
let paths: Vec<String> = app.session.prefs.general.recent_projects.iter().take(RECENT_ROWS).cloned().collect();
|
||||
if paths.is_empty() {
|
||||
return y;
|
||||
}
|
||||
let t = app.tokens;
|
||||
ui.painter().text(rect.min + vec2(24.0, y), Align2::LEFT_CENTER, tl!("Recent"), Tokens::semibold(15.0), t.text);
|
||||
let mut open = None;
|
||||
for (i, path) in paths.iter().enumerate() {
|
||||
let row =
|
||||
Rect::from_min_size(rect.min + vec2(16.0, y + 16.0 + i as f32 * RECENT_ROW_H), vec2(560.0_f32.min(rect.width() - 32.0).max(0.0), RECENT_ROW_H));
|
||||
let name = crate::menus::recent_name(path);
|
||||
let resp = ui.interact(row, egui::Id::new(("imp-recent", i)), Sense::click()).on_hover_text(path.as_str());
|
||||
app.auto.add(&format!("import.recent.{i}"), row, &name);
|
||||
if resp.hovered() {
|
||||
ui.painter().rect_filled(row, 4.0, t.hover);
|
||||
}
|
||||
let p = ui.painter().with_clip_rect(row);
|
||||
crate::icons::paint(&p, Rect::from_center_size(pos2(row.min.x + 16.0, row.center().y), vec2(14.0, 14.0)), crate::icons::Icon::Sequence, t.text_dim);
|
||||
let name_r = p.text(pos2(row.min.x + 32.0, row.center().y), Align2::LEFT_CENTER, &name, Tokens::ui(13.0), t.text);
|
||||
if let Some(folder) = std::path::Path::new(path).parent() {
|
||||
p.text(pos2(name_r.max.x + 12.0, row.center().y), Align2::LEFT_CENTER, folder.to_string_lossy(), Tokens::ui(11.5), t.text_dim);
|
||||
}
|
||||
if resp.clicked() {
|
||||
open = Some(path.clone());
|
||||
}
|
||||
}
|
||||
if let Some(path) = open {
|
||||
let ctx = ui.ctx().clone();
|
||||
if crate::menus::invoke(app, &ctx, "file.open", json!({"path": path})).is_ok() {
|
||||
app.ui.mode = crate::state::Mode::Edit;
|
||||
}
|
||||
}
|
||||
y + 16.0 + paths.len() as f32 * RECENT_ROW_H + 36.0
|
||||
}
|
||||
@@ -322,3 +322,41 @@ fn reveal_in_finder_opens_an_imported_clip() {
|
||||
assert_eq!(d.app().ui.extras.opened.last().map(String::as_str), Some(path.to_string_lossy().as_ref()));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
|
||||
/// A project saved for the first time is listed at the top of Home's Recent list and of File ▸
|
||||
/// Open Recent, both of which reopen it; Clear Recent Files empties the list (#1461).
|
||||
#[test]
|
||||
fn saved_projects_are_listed_in_home_and_open_recent() {
|
||||
let dir = std::env::temp_dir().join(format!("fc-ui-recent-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let a = dir.join("first cut.fcproj").to_string_lossy().to_string();
|
||||
let b = dir.join("second cut.fcproj").to_string_lossy().to_string();
|
||||
let mut d = Driver::new();
|
||||
d.exec("file.saveAs", json!({"path": a}));
|
||||
d.exec("file.newProject", json!({"name": "Second"}));
|
||||
d.exec("file.saveAs", json!({"path": b}));
|
||||
let menus = d.ok("ui.menu.list", json!({}));
|
||||
let recent: Vec<&Value> = menus.as_array().unwrap().iter().filter(|m| m["path"] == json!(["File", "Open Recent"])).collect();
|
||||
let labels: Vec<&str> = recent.iter().map(|m| m["label"].as_str().unwrap()).collect();
|
||||
assert_eq!(labels, ["second cut.fcproj", "first cut.fcproj", "Clear Recent Files"]);
|
||||
assert_eq!(recent[0]["id"], "file.openRecent.0");
|
||||
// File ▸ Open Recent reopens the older one, which moves to the top
|
||||
d.menu("file.openRecent.1");
|
||||
assert_eq!(d.app().session.path.as_deref(), Some(a.as_str()));
|
||||
assert_eq!(d.app().session.prefs.general.recent_projects, [a.clone(), b.clone()]);
|
||||
// Home lists them too, newest first, and a click opens one
|
||||
d.click("header.home");
|
||||
let rows = d.ok("ui.elements", json!({"prefix": "import.recent."}));
|
||||
let rows: Vec<&str> = rows.as_array().unwrap().iter().map(|e| e["label"].as_str().unwrap()).collect();
|
||||
assert_eq!(rows, ["first cut.fcproj", "second cut.fcproj"]);
|
||||
d.click("import.recent.1");
|
||||
assert_eq!(d.app().session.path.as_deref(), Some(b.as_str()));
|
||||
// Clear Recent Files empties both lists
|
||||
d.menu("file.clearRecent");
|
||||
assert!(d.app().session.prefs.general.recent_projects.is_empty());
|
||||
let menus = d.ok("ui.menu.list", json!({}));
|
||||
let clear = menus.as_array().unwrap().iter().find(|m| m["id"] == "file.clearRecent").cloned().expect("Clear Recent Files");
|
||||
assert_eq!(clear["enabled"], false);
|
||||
assert!(!menus.as_array().unwrap().iter().any(|m| m["id"].as_str().is_some_and(|id| id.starts_with("file.openRecent."))));
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
Reference in new issue
Block a user