mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-14 19:27:05 -04:00
Merge pull request #1210 from arnav-hiwarkar/feat/copy-presets-folder-path
This commit is contained in:
commit
acb77a8c85
6 files changed
+150
-1
No files matched your search
@@ -37,6 +37,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
fe!("app.keyboardShortcuts", "Keyboard Shortcuts", ["Edit"], Some("Cmd+Alt+'"), "{}", always),
|
||||
fe!("app.templates", "Templates", [], None, "{kind: renderSettings|outputModule}", always),
|
||||
fe!("app.find", "Find", ["File"], Some("Cmd+F"), "{query?}", always),
|
||||
fe!("effects.copyPresetsFolderPath", "Copy Presets Folder Path", [], None, "{}", always),
|
||||
// Preview.
|
||||
fe!(
|
||||
"playback.toggle",
|
||||
|
||||
@@ -227,6 +227,7 @@ const JAPANESE: &[(&str, &str)] = &[
|
||||
("Edit in the Timeline", "タイムラインで編集"),
|
||||
("EffectCraft didn't quit normally last time.", "EffectCraftは前回正常に終了しませんでした。"),
|
||||
("Effects & Presets options", "エフェクト&プリセットのオプション"),
|
||||
("Copy Presets Folder Path", "プリセットフォルダーのパスをコピー"),
|
||||
("Effects: {}", "エフェクト:{}"),
|
||||
("Empty folder", "空のフォルダー"),
|
||||
("Enable Depth of Field", "被写界深度を有効化"),
|
||||
@@ -770,6 +771,7 @@ const SIMPLIFIED_CHINESE: &[(&str, &str)] = &[
|
||||
("Edit in the Timeline", "在时间轴中编辑"),
|
||||
("EffectCraft didn't quit normally last time.", "EffectCraft 上次未正常退出。"),
|
||||
("Effects & Presets options", "效果和预设选项"),
|
||||
("Copy Presets Folder Path", "复制预设文件夹路径"),
|
||||
("Effects: {}", "效果:{}"),
|
||||
("Empty folder", "空文件夹"),
|
||||
("Enable Depth of Field", "启用景深"),
|
||||
@@ -1501,6 +1503,7 @@ const TRADITIONAL_CHINESE: &[(&str, &str)] = &[
|
||||
("Edit in the Timeline", "在時間軸中編輯"),
|
||||
("EffectCraft didn't quit normally last time.", "EffectCraft 上次未正常結束。"),
|
||||
("Effects & Presets options", "效果和預設集選項"),
|
||||
("Copy Presets Folder Path", "複製預設集資料夾路徑"),
|
||||
("Effects: {}", "效果:{}"),
|
||||
("Empty folder", "空資料夾"),
|
||||
("Enable Depth of Field", "啟用景深"),
|
||||
@@ -2235,6 +2238,7 @@ const UKRAINIAN: &[(&str, &str)] = &[
|
||||
("Edit in the Timeline", "Редагувати на шкалі часу"),
|
||||
("EffectCraft didn't quit normally last time.", "Минулого разу EffectCraft завершив роботу некоректно."),
|
||||
("Effects & Presets options", "Параметри ефектів і наборів"),
|
||||
("Copy Presets Folder Path", "Копіювати шлях до папки наборів"),
|
||||
("Effects: {}", "Ефекти: {}"),
|
||||
("Empty folder", "Порожня тека"),
|
||||
("Enable Depth of Field", "Увімкнути глибину різкості"),
|
||||
@@ -2786,6 +2790,7 @@ const PORTUGUESE: &[(&str, &str)] = &[
|
||||
("Edit in the Timeline", "Editar na linha do tempo"),
|
||||
("EffectCraft didn't quit normally last time.", "O EffectCraft não foi encerrado normalmente da última vez."),
|
||||
("Effects & Presets options", "Opções de Efeitos e predefinições"),
|
||||
("Copy Presets Folder Path", "Copiar caminho da pasta de predefinições"),
|
||||
("Effects: {}", "Efeitos: {}"),
|
||||
("Empty folder", "Pasta vazia"),
|
||||
("Enable Depth of Field", "Ativar profundidade de campo"),
|
||||
@@ -3235,6 +3240,7 @@ const CZECH: &[(&str, &str)] = &[
|
||||
("Edit in the Timeline", "Upravit na časové ose"),
|
||||
("EffectCraft didn't quit normally last time.", "EffectCraft nebyl při posledním použití ukončen běžným způsobem."),
|
||||
("Effects & Presets options", "Možnosti efektů a přednastavení"),
|
||||
("Copy Presets Folder Path", "Kopírovat cestu ke složce přednastavení"),
|
||||
("Effects: {}", "Efekty: {}"),
|
||||
("Empty folder", "Prázdná složka"),
|
||||
("Enable Depth of Field", "Povolit hloubku ostrosti"),
|
||||
|
||||
@@ -291,6 +291,9 @@ fn only_comp(p: &Value) -> Option<effectcraft_engine::project::ItemId> {
|
||||
|
||||
/// Execute a UI or engine command by id.
|
||||
pub fn invoke(app: &mut EffectcraftApp, ctx: &egui::Context, id: &str, params: Value) -> Result<Value, String> {
|
||||
if id == "effects.copyPresetsFolderPath" {
|
||||
return frontend(app, ctx, id, params);
|
||||
}
|
||||
// Layer ▸ New ▸ Text (menu, shortcut, context menus) puts an insertion point in the centre of
|
||||
// the comp, as After Effects does, so typing goes into Source Text rather than running
|
||||
// shortcuts (#866). `engine.execute` keeps the headless default.
|
||||
@@ -877,6 +880,16 @@ pub fn frontend(app: &mut EffectcraftApp, ctx: &egui::Context, id: &str, p: Valu
|
||||
let now = ctx.input(|i| i.time);
|
||||
let v = &mut app.ui.viewer;
|
||||
Ok(match id {
|
||||
"effects.copyPresetsFolderPath" => {
|
||||
if cfg!(target_arch = "wasm32") {
|
||||
return Err("The web app has no local presets folder".into());
|
||||
}
|
||||
let path = crate::panels::effects_presets::presets_dir().filter(|path| !path.as_os_str().is_empty()).ok_or("No presets folder is configured")?;
|
||||
let path = std::path::absolute(path).map_err(|e| format!("Cannot resolve the presets folder path: {e}"))?;
|
||||
let path = path.to_str().ok_or("The presets folder path is not valid UTF-8")?;
|
||||
ctx.copy_text(path.to_string());
|
||||
json!({"path": path})
|
||||
}
|
||||
"app.about" => {
|
||||
app.dialog = Some(crate::Dialog::About);
|
||||
Value::Null
|
||||
|
||||
@@ -300,9 +300,34 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
"-".into(),
|
||||
" Refresh List".into(),
|
||||
" Clear Recently Used".into(),
|
||||
"-".into(),
|
||||
format!(" {}", tr("Copy Presets Folder Path")),
|
||||
];
|
||||
let mut force_scan = false;
|
||||
if let Some(i) = widgets::popup_menu(ui, mpop, mr.left_bottom() - vec2(220.0, 0.0), &menu, None) {
|
||||
let separators = menu.iter().filter(|label| *label == "-").count();
|
||||
let choice = if widgets::popup_is_open(ui, mpop) {
|
||||
widgets::popup_list(ui, mpop, mr.left_bottom() - vec2(220.0, 0.0), Rect::NOTHING, menu.len().saturating_sub(separators), separators, |ui| {
|
||||
ui.set_min_width(160.0);
|
||||
let mut chosen = None;
|
||||
for (i, label) in menu.iter().enumerate() {
|
||||
if label == "-" {
|
||||
ui.separator();
|
||||
} else {
|
||||
let resp = ui.selectable_label(false, label);
|
||||
if i == 12 {
|
||||
app.auto.add("effects.copyPresetsFolderPath", resp.rect, "Copy Presets Folder Path");
|
||||
}
|
||||
if resp.clicked() {
|
||||
chosen = Some(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
chosen
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(i) = choice {
|
||||
let v = &mut app.ui.effects_view;
|
||||
match i {
|
||||
0 => v.depth = "all".into(),
|
||||
@@ -313,6 +338,11 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
7 => v.alphabetical = true,
|
||||
9 => force_scan = true,
|
||||
10 => app.ui.effects_recent.clear(),
|
||||
12 => {
|
||||
if let Err(e) = crate::menus::invoke(app, &ctx, "effects.copyPresetsFolderPath", json!({})) {
|
||||
app.ui.status = e;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
//! Effects & Presets: copy the configured folder path through the contents menu.
|
||||
|
||||
use effectcraft_engine::Session;
|
||||
use effectcraft_ui_egui::{ControlRequest, EffectcraftApp, control, dock::PanelKind};
|
||||
use egui::{Event, Modifiers, OutputCommand, PointerButton, pos2};
|
||||
use egui_kittest::Harness;
|
||||
use serde_json::json;
|
||||
|
||||
fn expected_folder() -> String {
|
||||
let path = std::env::var("EC_PRESETS_DIR").unwrap_or_else(|_| {
|
||||
let home = std::env::var("HOME").or_else(|_| std::env::var("USERPROFILE")).unwrap();
|
||||
std::path::Path::new(&home).join("Documents/EffectCraft/Presets").to_str().unwrap().to_string()
|
||||
});
|
||||
if path.is_empty() { path } else { std::path::absolute(path).unwrap().to_str().unwrap().to_string() }
|
||||
}
|
||||
|
||||
fn click(h: &mut Harness<'_, EffectcraftApp>, id: &str) -> Vec<String> {
|
||||
let e = h.state().auto.find(id).unwrap_or_else(|| panic!("missing {id}"));
|
||||
let pos = pos2(e.rect[0] + e.rect[2] / 2.0, e.rect[1] + e.rect[3] / 2.0);
|
||||
let mut copied = Vec::new();
|
||||
for pressed in [true, false] {
|
||||
h.input_mut().events.push(Event::PointerMoved(pos));
|
||||
h.input_mut().events.push(Event::PointerButton { pos, button: PointerButton::Primary, pressed, modifiers: Modifiers::NONE });
|
||||
h.step();
|
||||
for command in &h.output().platform_output.commands {
|
||||
if let OutputCommand::CopyText(text) = command {
|
||||
copied.push(text.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
copied
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn presets_menu_copies_the_configured_folder_without_editing_the_project() {
|
||||
let expected = expected_folder();
|
||||
let mut h = Harness::builder().with_size(egui::vec2(1600.0, 1000.0)).build_eframe(|_| {
|
||||
let mut app = EffectcraftApp::new(Session::default());
|
||||
app.ui.start_screen = false;
|
||||
app.show_panel(PanelKind::EffectsPresets);
|
||||
app
|
||||
});
|
||||
h.run_steps(3);
|
||||
let before = serde_json::to_value(&h.state().session.project).unwrap();
|
||||
let revision = h.state().session.revision;
|
||||
assert!(h.state().auto.find("effects.copyPresetsFolderPath").is_none());
|
||||
click(&mut h, "effects.menu");
|
||||
h.run_steps(3);
|
||||
if let Ok(path) = std::env::var("PRESETS_FOLDER_SNAPSHOT") {
|
||||
h.render().unwrap().save(path).unwrap();
|
||||
}
|
||||
assert_eq!(click(&mut h, "effects.copyPresetsFolderPath"), [expected]);
|
||||
h.run_steps(2);
|
||||
assert!(h.state().auto.find("effects.copyPresetsFolderPath").is_none());
|
||||
assert_eq!(serde_json::to_value(&h.state().session.project).unwrap(), before);
|
||||
assert_eq!(h.state().session.revision, revision);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn presets_folder_control_command_returns_the_path_or_a_configuration_error() {
|
||||
let mut app = EffectcraftApp::new(Session::default());
|
||||
let ctx = egui::Context::default();
|
||||
let expected = expected_folder();
|
||||
let mut reply = None;
|
||||
let mut output = ctx.run_ui(Default::default(), |ui| {
|
||||
let (request, _rx) = ControlRequest::new("ui.menu.invoke", json!({"id": "effects.copyPresetsFolderPath"}));
|
||||
reply = match control::handle(&mut app, ui.ctx(), &request) {
|
||||
control::Outcome::Done(value) => Some(value),
|
||||
_ => panic!("expected an immediate reply"),
|
||||
};
|
||||
});
|
||||
output.textures_delta.clear();
|
||||
let copied: Vec<_> = output
|
||||
.platform_output
|
||||
.commands
|
||||
.iter()
|
||||
.filter_map(|command| match command {
|
||||
OutputCommand::CopyText(text) => Some(text.as_str()),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
if expected.is_empty() {
|
||||
assert_eq!(reply, Some(json!({"ok": false, "error": "No presets folder is configured"})));
|
||||
assert!(copied.is_empty());
|
||||
assert_eq!(app.ui.status, "No presets folder is configured");
|
||||
} else {
|
||||
assert_eq!(reply, Some(json!({"ok": true, "result": {"path": expected}})));
|
||||
assert_eq!(copied, [expected.as_str()]);
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,15 @@ user can do, an agent can do too. There are three ways in:
|
||||
| **CLI** (`effectcraft-cli exec/get/set/...`) | one-shot scripting and CI; JSON output with `--json` | no |
|
||||
| **Control channel** (`effectcraft --control 9877`) | driving and seeing the live UI; see [control-protocol.md](control-protocol.md) | yes |
|
||||
|
||||
## Find the presets folder
|
||||
|
||||
The native Effects & Presets panel's options menu includes **Copy Presets Folder Path**. It copies
|
||||
the folder the panel scans: `EC_PRESETS_DIR` when set, otherwise `~/Documents/EffectCraft/Presets`
|
||||
using `HOME` or `USERPROFILE`. Relative paths are resolved to an absolute path. The folder may
|
||||
not exist yet; this action does not create it.
|
||||
Over the live UI control channel, `ui.menu.invoke {"id":"effects.copyPresetsFolderPath"}` copies
|
||||
the same path and returns `{path}`. The web app reports that it has no local presets folder.
|
||||
|
||||
## MCP setup
|
||||
|
||||
Build once with `cargo build --release -p effectcraft-cli`, then register the server. For Claude
|
||||
|
||||
Reference in new issue
Block a user