From 186c41b8945830322749224c1e6022a5151dc95a Mon Sep 17 00:00:00 2001 From: Mike Solar Date: Tue, 25 Aug 2026 01:55:08 +0800 Subject: [PATCH] app: about dialog, modal close affordances, more preferences MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Help > About Oak… opens a real dialog (was a placeholder print): name + version, the GPL-3.0 line, the Olive fork notice and the thanks to Enzo GD (Community Promoter). - Modal (gpui_widgets, submodule bump): a title-row close button on every dialog and opt-in backdrop dismissal (dismiss_on_mask) — Help > Search Actions and About use it, so they close on an outside click (previously a buttonless modal could only be closed with Escape). - Preferences general tab grows two live settings: the cache-ahead pre-render window (PlaybackPreRenderFrames, consumed by the preview scheduler every playback tick) and the library storage backend (Storage/Backend sqlite/pg + Storage/PgUrl connection string, read by oak-storage's write-through when a project binds; the pg URL field shows only for PostgreSQL and commits on dialog close). The C++ auto-recovery toggle is skipped on purpose: the Rust snapshot loop has no disable semantic, so the switch would be dead. i18n keys for all eight packs; new tests cover the about modal and the preferences writes. --- assets/i18n/de.yaml | 9 ++ assets/i18n/en-US.yaml | 9 ++ assets/i18n/es.yaml | 9 ++ assets/i18n/fr.yaml | 9 ++ assets/i18n/ja.yaml | 9 ++ assets/i18n/pt.yaml | 9 ++ assets/i18n/ru.yaml | 9 ++ assets/i18n/zh-CN.yaml | 9 ++ crates/oak-app/src/app.rs | 135 +++++++++++++++++++++- crates/oak-app/src/dialogs.rs | 186 ++++++++++++++++++++++++++++++- crates/oak-app/src/oakui/real.rs | 8 +- gpui | 2 +- 12 files changed, 393 insertions(+), 10 deletions(-) diff --git a/assets/i18n/de.yaml b/assets/i18n/de.yaml index f35652195..5ba00254b 100644 --- a/assets/i18n/de.yaml +++ b/assets/i18n/de.yaml @@ -247,6 +247,10 @@ "projprops.cache.default": "Standardort verwenden" "projprops.cache.alongside": "Zusammen mit dem Projekt speichern" "projprops.cache.custom": "Benutzerdefinierten Speicherort verwenden:" +"about.title": "Über Oak" +"about.description": "Oak Video Editor ist ein freier, quelloffener nichtlinearer Videoeditor. Diese Software ist unter der GNU GPL Version 3 lizenziert." +"about.thanks": "Besonderer Dank gilt Enzo GD, unserem Community Promoter, für seine großzügige Unterstützung bei der Verbreitung dieses Projekts in dessen Anfangszeit." +"about.fork_notice": "Dieses Projekt ist ein Fork des Olive Video Editors." "file.open.title": "Projekt öffnen" "file.import_footage.title": "Material importieren" "preferences.title": "Einstellungen" @@ -271,6 +275,11 @@ "preferences.theme.light": "Olive Light" "preferences.cache.dir": "Verzeichnis für den Platten-Cache" "preferences.cache.browse": "Durchsuchen…" +"preferences.cache.ahead": "Vorauspuffer (Frames)" +"preferences.section.storage": "Speicher" +"preferences.storage.backend": "Backend des Projektarchivs" +"preferences.storage.pg_url": "PostgreSQL-Verbindungszeichenfolge" +"preferences.storage.restart_hint": "Änderungen am Speicher-Backend gelten für Projekte, die anschließend geöffnet werden (ein Neustart wird empfohlen)." "preferences.proxy.enable": "Proxy-Medien verwenden" "preferences.proxy.resolution": "Proxy-Auflösung" "preferences.proxy.full": "Volle Auflösung" diff --git a/assets/i18n/en-US.yaml b/assets/i18n/en-US.yaml index ced3c9a96..4da35717b 100644 --- a/assets/i18n/en-US.yaml +++ b/assets/i18n/en-US.yaml @@ -247,6 +247,10 @@ "projprops.cache.default": "Use Default Location" "projprops.cache.alongside": "Store Alongside Project" "projprops.cache.custom": "Use Custom Location:" +"about.title": "About Oak" +"about.description": "Oak Video Editor is a free open source non-linear video editor. This software is licensed under the GNU GPL Version 3." +"about.thanks": "Special thanks to Enzo GD, our Community Promoter, for his generous support in spreading the word about this project in its early days." +"about.fork_notice": "This project is a fork of Olive Video Editor." "file.open.title": "Open Project" "file.import_footage.title": "Import Footage" "preferences.title": "Preferences" @@ -271,6 +275,11 @@ "preferences.theme.light": "Olive Light" "preferences.cache.dir": "Disk cache directory" "preferences.cache.browse": "Browse…" +"preferences.cache.ahead": "Cache ahead (frames)" +"preferences.section.storage": "Storage" +"preferences.storage.backend": "Library storage backend" +"preferences.storage.pg_url": "PostgreSQL connection string" +"preferences.storage.restart_hint": "Storage backend changes apply to projects opened afterwards (a restart is recommended)." "preferences.proxy.enable": "Use proxy media" "preferences.proxy.resolution": "Proxy resolution" "preferences.proxy.full": "Full resolution" diff --git a/assets/i18n/es.yaml b/assets/i18n/es.yaml index 33985ffb6..9e05614aa 100644 --- a/assets/i18n/es.yaml +++ b/assets/i18n/es.yaml @@ -247,6 +247,10 @@ "projprops.cache.default": "Usar ubicación predeterminada" "projprops.cache.alongside": "Almacenar junto al proyecto" "projprops.cache.custom": "Usar ubicación personalizada:" +"about.title": "Acerca de Oak" +"about.description": "Oak Video Editor es un editor de vídeo no lineal libre y de código abierto. Este software está licenciado bajo la GNU GPL versión 3." +"about.thanks": "Un agradecimiento especial a Enzo GD, nuestro Community Promoter, por su generoso apoyo al difundir este proyecto en sus primeros días." +"about.fork_notice": "Este proyecto es un fork de Olive Video Editor." "file.open.title": "Abrir proyecto" "file.import_footage.title": "Importar material" "preferences.title": "Preferencias" @@ -271,6 +275,11 @@ "preferences.theme.light": "Olive Light" "preferences.cache.dir": "Directorio de caché en disco" "preferences.cache.browse": "Examinar…" +"preferences.cache.ahead": "Búfer anticipado (fotogramas)" +"preferences.section.storage": "Almacenamiento" +"preferences.storage.backend": "Backend de almacenamiento de la biblioteca" +"preferences.storage.pg_url": "Cadena de conexión de PostgreSQL" +"preferences.storage.restart_hint": "Los cambios del backend de almacenamiento se aplican a los proyectos abiertos después del cambio (se recomienda reiniciar)." "preferences.proxy.enable": "Usar medios proxy" "preferences.proxy.resolution": "Resolución del proxy" "preferences.proxy.full": "Resolución completa" diff --git a/assets/i18n/fr.yaml b/assets/i18n/fr.yaml index ab98b0b6e..12bd60253 100644 --- a/assets/i18n/fr.yaml +++ b/assets/i18n/fr.yaml @@ -247,6 +247,10 @@ "projprops.cache.default": "Utiliser l'emplacement par défaut" "projprops.cache.alongside": "Stocker à côté du projet" "projprops.cache.custom": "Utiliser un emplacement personnalisé :" +"about.title": "À propos d'Oak" +"about.description": "Oak Video Editor est un éditeur vidéo non linéaire libre et open source. Ce logiciel est sous licence GNU GPL version 3." +"about.thanks": "Remerciements particuliers à Enzo GD, notre Community Promoter, pour son généreux soutien à la diffusion de ce projet à ses débuts." +"about.fork_notice": "Ce projet est un fork d'Olive Video Editor." "file.open.title": "Ouvrir le projet" "file.import_footage.title": "Importer des médias" "preferences.title": "Préférences" @@ -271,6 +275,11 @@ "preferences.theme.light": "Olive Light" "preferences.cache.dir": "Dossier du cache disque" "preferences.cache.browse": "Parcourir…" +"preferences.cache.ahead": "Préchargement (images)" +"preferences.section.storage": "Stockage" +"preferences.storage.backend": "Backend de stockage de la bibliothèque" +"preferences.storage.pg_url": "Chaîne de connexion PostgreSQL" +"preferences.storage.restart_hint": "Les changements de backend de stockage s'appliquent aux projets ouverts après la modification (un redémarrage est recommandé)." "preferences.proxy.enable": "Utiliser les médias proxy" "preferences.proxy.resolution": "Résolution du proxy" "preferences.proxy.full": "Pleine résolution" diff --git a/assets/i18n/ja.yaml b/assets/i18n/ja.yaml index 562dfe015..d000b2214 100644 --- a/assets/i18n/ja.yaml +++ b/assets/i18n/ja.yaml @@ -247,6 +247,10 @@ "projprops.cache.default": "デフォルトの場所を使用" "projprops.cache.alongside": "プロジェクトと同じ場所に保存" "projprops.cache.custom": "カスタムの場所を使用:" +"about.title": "Oak について" +"about.description": "Oak Video Editor はフリーでオープンソースのノンリニア動画エディタです。本ソフトウェアは GNU GPL バージョン 3 でライセンスされています。" +"about.thanks": "プロジェクト初期に広く紹介してくれたコミュニティプロモーター Enzo GD に心より感謝します。" +"about.fork_notice": "本プロジェクトは Olive Video Editor のフォークです。" "file.open.title": "プロジェクトを開く" "file.import_footage.title": "素材を読み込み" "preferences.title": "環境設定" @@ -271,6 +275,11 @@ "preferences.theme.light": "Olive Light" "preferences.cache.dir": "ディスクキャッシュの保存先" "preferences.cache.browse": "参照…" +"preferences.cache.ahead": "先読みキャッシュ(フレーム)" +"preferences.section.storage": "ストレージ" +"preferences.storage.backend": "ライブラリのストレージバックエンド" +"preferences.storage.pg_url": "PostgreSQL接続文字列" +"preferences.storage.restart_hint": "ストレージバックエンドの変更は、変更後に開いたプロジェクトに適用されます(再起動を推奨)。" "preferences.proxy.enable": "プロキシメディアを使用" "preferences.proxy.resolution": "プロキシ解像度" "preferences.proxy.full": "フル解像度" diff --git a/assets/i18n/pt.yaml b/assets/i18n/pt.yaml index 31d6eb51c..2bad73469 100644 --- a/assets/i18n/pt.yaml +++ b/assets/i18n/pt.yaml @@ -247,6 +247,10 @@ "projprops.cache.default": "Usar localização padrão" "projprops.cache.alongside": "Armazenar junto ao projeto" "projprops.cache.custom": "Usar localização personalizada:" +"about.title": "Sobre o Oak" +"about.description": "O Oak Video Editor é um editor de vídeo não linear livre e de código aberto. Este software é licenciado sob a GNU GPL versão 3." +"about.thanks": "Um agradecimento especial a Enzo GD, nosso Community Promoter, por seu generoso apoio na divulgação deste projeto em seus primórdios." +"about.fork_notice": "Este projeto é um fork do Olive Video Editor." "file.open.title": "Abrir projeto" "file.import_footage.title": "Importar mídia" "preferences.title": "Preferências" @@ -271,6 +275,11 @@ "preferences.theme.light": "Olive Light" "preferences.cache.dir": "Diretório do cache em disco" "preferences.cache.browse": "Procurar…" +"preferences.cache.ahead": "Pré-busca (quadros)" +"preferences.section.storage": "Armazenamento" +"preferences.storage.backend": "Backend de armazenamento da biblioteca" +"preferences.storage.pg_url": "String de conexão do PostgreSQL" +"preferences.storage.restart_hint": "As alterações do backend de armazenamento valem para projetos abertos depois da mudança (é recomendável reiniciar)." "preferences.proxy.enable": "Usar mídia proxy" "preferences.proxy.resolution": "Resolução do proxy" "preferences.proxy.full": "Resolução completa" diff --git a/assets/i18n/ru.yaml b/assets/i18n/ru.yaml index f3b6c8580..47756f4e9 100644 --- a/assets/i18n/ru.yaml +++ b/assets/i18n/ru.yaml @@ -247,6 +247,10 @@ "projprops.cache.default": "Использовать расположение по умолчанию" "projprops.cache.alongside": "Хранить рядом с проектом" "projprops.cache.custom": "Использовать пользовательское расположение:" +"about.title": "О программе Oak" +"about.description": "Oak Video Editor — свободный нелинейный видеоредактор с открытым исходным кодом. Программа распространяется по лицензии GNU GPL версии 3." +"about.thanks": "Особая благодарность Enzo GD, нашему Community Promoter, за щедрую поддержку в распространении этого проекта в первые дни." +"about.fork_notice": "Этот проект является форком Olive Video Editor." "file.open.title": "Открыть проект" "file.import_footage.title": "Импорт медиафайлов" "preferences.title": "Настройки" @@ -271,6 +275,11 @@ "preferences.theme.light": "Olive Light" "preferences.cache.dir": "Каталог дискового кэша" "preferences.cache.browse": "Обзор…" +"preferences.cache.ahead": "Упреждающий кэш (кадры)" +"preferences.section.storage": "Хранилище" +"preferences.storage.backend": "Бэкенд хранилища библиотеки" +"preferences.storage.pg_url": "Строка подключения PostgreSQL" +"preferences.storage.restart_hint": "Изменения бэкенда хранилища применяются к проектам, открытым после изменения (рекомендуется перезапуск)." "preferences.proxy.enable": "Использовать прокси-медиа" "preferences.proxy.resolution": "Разрешение прокси" "preferences.proxy.full": "Полное разрешение" diff --git a/assets/i18n/zh-CN.yaml b/assets/i18n/zh-CN.yaml index 5e780816b..084260a7e 100644 --- a/assets/i18n/zh-CN.yaml +++ b/assets/i18n/zh-CN.yaml @@ -247,6 +247,10 @@ "projprops.cache.default": "使用默认位置" "projprops.cache.alongside": "随工程文件存储" "projprops.cache.custom": "使用自定义位置:" +"about.title": "关于 Oak" +"about.description": "Oak 视频编辑器是一款自由开源的非线性视频编辑器。本软件以 GNU GPL 第三版许可证发布。" +"about.thanks": "特别感谢我们的社区推广者 Enzo GD,感谢他在项目早期为传播本项目所给予的慷慨支持。" +"about.fork_notice": "本项目是 Olive 视频编辑器的 fork。" "file.open.title": "打开项目" "file.import_footage.title": "导入素材" "preferences.title": "偏好设置" @@ -271,6 +275,11 @@ "preferences.theme.light": "Olive Light" "preferences.cache.dir": "磁盘缓存目录" "preferences.cache.browse": "浏览…" +"preferences.cache.ahead": "缓存提前量(帧)" +"preferences.section.storage": "存储" +"preferences.storage.backend": "素材库存储后端" +"preferences.storage.pg_url": "PostgreSQL 连接串" +"preferences.storage.restart_hint": "存储后端更改在之后打开的项目上生效(建议重启)。" "preferences.proxy.enable": "使用代理媒体" "preferences.proxy.resolution": "代理分辨率" "preferences.proxy.full": "原始分辨率" diff --git a/crates/oak-app/src/app.rs b/crates/oak-app/src/app.rs index 84f1990e4..c95f7f622 100644 --- a/crates/oak-app/src/app.rs +++ b/crates/oak-app/src/app.rs @@ -121,6 +121,8 @@ mod modal_ids { pub const PLUGIN_PROGRESS: usize = 10; /// The action search dialog (Help > Search Actions…, the `/` key). pub const ACTION_SEARCH: usize = 11; + /// The about dialog (Help > About Oak…). + pub const ABOUT: usize = 13; /// The project properties dialog (File > Project Properties…). pub const PROJECT_PROPERTIES: usize = 12; } @@ -187,6 +189,8 @@ enum ModalState { modal: Entity, content: Entity>, }, + /// The about dialog (Help > About Oak…; static content). + About { modal: Entity }, } /// A running export: the session the tick loop drains for progress. @@ -207,7 +211,8 @@ impl ModalState { | ModalState::ManagerDelete { modal, .. } | ModalState::Proxy { modal, .. } | ModalState::ActionSearch { modal, .. } - | ModalState::ProjectProperties { modal, .. } => Some(modal.clone()), + | ModalState::ProjectProperties { modal, .. } + | ModalState::About { modal } => Some(modal.clone()), } } } @@ -1080,6 +1085,7 @@ impl OakApp { | A::MulticamSwitchNoSplit9 => {} // --- Help -------------------------------------------------------- A::ActionSearch => self.open_action_search(cx), + A::About => self.open_about(cx), // --- everything else is a placeholder -------------------------- other => println!( "[action] {} not wired yet (placeholder)", @@ -1927,7 +1933,10 @@ impl OakApp { let modal = app.new(|cx| { Modal::new( modal_ids::ACTION_SEARCH, - ModalOptions::new(crate::i18n::tr("menu.help.action_search"), px(640.0)), + ModalOptions::new(crate::i18n::tr("menu.help.action_search"), px(640.0)) + // Launcher-style: a backdrop click dismisses (same as + // Escape and the title-row ✕). + .with_dismiss_on_mask(true), window, cx, ) @@ -1959,6 +1968,30 @@ impl OakApp { } } + /// Opens the about dialog (Help > About Oak…): the app name, version, + /// license line and fork notice. No engine interaction. + fn open_about(&mut self, cx: &mut Context) { + if !matches!(self.modal, ModalState::None) { + return; + } + self.spawn_modal(cx, |window, app| { + let content = app.new(|_cx| crate::dialogs::AboutContent::new()); + let modal = app.new(|cx| { + Modal::new( + modal_ids::ABOUT, + ModalOptions::new(crate::i18n::tr("about.title"), px(480.0)) + .with_button(DialogButton::primary(crate::i18n::tr("dialog.ok"))) + // Launcher-style dismissal is natural here too. + .with_dismiss_on_mask(true), + window, + cx, + ) + .with_content(content.clone()) + }); + ModalState::About { modal } + }); + } + /// Opens the proxy settings dialog (Tools > Proxy Settings; the C++ /// `ProxyDialog`): the global generation settings plus the footage /// proxy list with Generate / Delete buttons. @@ -2225,6 +2258,7 @@ impl OakApp { } } } + modal_ids::ABOUT => self.close_modal(cx), modal_ids::PROJECT_PROPERTIES => { if let ModalState::ProjectProperties { content, .. } = &self.modal { let content = content.clone(); @@ -3158,6 +3192,103 @@ mod tests { ); } + /// The General tab's new rows are live end to end: the cache-ahead spin + /// box writes `PlaybackPreRenderFrames` (the playback pre-render window + /// reads it every tick) and the storage backend combo writes + /// `Storage/Backend` — PostgreSQL reveals the `Storage/PgUrl` field the + /// write-through library consumes. + #[gpui::test] + async fn preferences_general_tab_writes_cache_ahead_and_storage(cx: &mut TestAppContext) { + let _guard = crate::actions::shortcuts_test_lock() + .lock() + .unwrap_or_else(|e| e.into_inner()); + let _lang = crate::i18n::lang_test_lock() + .lock() + .unwrap_or_else(|e| e.into_inner()); + crate::i18n::set_language_code("en-US"); + + // The config store is process-wide: pin the keys to a known state and + // restore them afterwards so no other test inherits these writes. + struct Restore(&'static str, String); + impl Drop for Restore { + fn drop(&mut self) { + crate::oakui::real::config_set_string(self.0, &self.1); + } + } + let _ahead = Restore( + "PlaybackPreRenderFrames", + crate::oakui::real::config_get_string("PlaybackPreRenderFrames"), + ); + let _backend = Restore( + "Storage/Backend", + crate::oakui::real::config_get_string("Storage/Backend"), + ); + let _pg_url = Restore( + "Storage/PgUrl", + crate::oakui::real::config_get_string("Storage/PgUrl"), + ); + crate::oakui::real::config_set_int("PlaybackPreRenderFrames", 120); + crate::oakui::real::config_set_string("Storage/Backend", "sqlite"); + + let (window, root) = mock_shell(cx); + cx.update(|app| root.update(app, |app, cx| app.on_menu(menu_ids::PREFERENCES, cx))); + cx.run_until_parked(); + cx.update_window(window.into(), |_root, window, cx| { + window.draw(cx).clear(); + }) + .expect("window is still open"); + cx.run_until_parked(); + // Clear the app keymap: the global up/down/home/end bindings + // (GoToPrevCut / GoToNextCut / GoToStart / GoToEnd) dispatch before + // the controls' element key handlers and would shadow the spin + // box's arrow keys and the combo's selection keys. + cx.update(|cx| cx.clear_key_bindings()); + + let mut cx = VisualTestContext::from_window(window.into(), cx).into_mut(); + // The connection-string row only exists for the PostgreSQL backend. + assert!( + cx.debug_bounds("preferences-storage-pg-url").is_none(), + "the PostgreSQL connection-string row is hidden for SQLite" + ); + + // Step the cache-ahead spin box once (8 frames): 120 → 128 lands in + // the config the playback pre-render window reads. + let ahead = cx + .debug_bounds("preferences-cache-ahead") + .expect("the cache-ahead row is rendered"); + cx.simulate_click(ahead.center(), gpui::Modifiers::none()); + cx.run_until_parked(); + cx.dispatch_keystroke(window.into(), gpui::Keystroke::parse("up").unwrap()); + cx.run_until_parked(); + assert_eq!( + crate::oakui::real::config_get_int("PlaybackPreRenderFrames", 120), + 128, + "the cache-ahead spin box writes its stepped value to the config" + ); + + // Re-select the storage backend: PostgreSQL (the combo starts on + // SQLite; with the popup closed the arrow keys drive the selection). + let backend = cx + .debug_bounds("preferences-storage-backend") + .expect("the storage backend row is rendered"); + cx.simulate_click(backend.center(), gpui::Modifiers::none()); + cx.run_until_parked(); + cx.dispatch_keystroke(window.into(), gpui::Keystroke::parse("escape").unwrap()); + cx.run_until_parked(); + cx.dispatch_keystroke(window.into(), gpui::Keystroke::parse("down").unwrap()); + cx.run_until_parked(); + assert_eq!( + crate::oakui::real::config_get_string("Storage/Backend"), + "pg", + "selecting PostgreSQL writes Storage/Backend to the config" + ); + // A fresh draw reveals the connection-string row. + cx.update(|window, cx| window.draw(cx).clear()); + cx.run_until_parked(); + cx.debug_bounds("preferences-storage-pg-url") + .expect("the PostgreSQL connection-string row appears after the switch"); + } + /// Opening File → Project Properties… must not crash: the content is /// built with the engine and the modal is layered onto the shell (the /// same deferred-build path as Preferences). diff --git a/crates/oak-app/src/dialogs.rs b/crates/oak-app/src/dialogs.rs index 52339a185..a4f520064 100644 --- a/crates/oak-app/src/dialogs.rs +++ b/crates/oak-app/src/dialogs.rs @@ -46,9 +46,11 @@ use crate::oakui::real::{ config_get_bool, config_get_int, config_get_string, config_set_bool, config_set_int, config_set_string, encoding_formats, proxy_dividers, renderer_backends, set_audio_input_device, set_audio_output_device, set_theme_dark, theme_is_dark, CONFIG_KEY_DEFAULT_TRANSITION_SEC, - CONFIG_KEY_DISK_CACHE_PATH, CONFIG_KEY_FFMPEG_PATH, CONFIG_KEY_PROXY_DIVIDER, - CONFIG_KEY_RENDERER_BACKEND, CONFIG_KEY_SNAPSHOT_INTERVAL_SEC, CONFIG_KEY_USE_PROXY, - DEFAULT_SNAPSHOT_INTERVAL_SEC, DEFAULT_TRANSITION_SEC, EXPORT_FORMAT_MP4, + CONFIG_KEY_DISK_CACHE_PATH, CONFIG_KEY_FFMPEG_PATH, CONFIG_KEY_PG_URL, + CONFIG_KEY_PREVIEW_WINDOW, CONFIG_KEY_PROXY_DIVIDER, CONFIG_KEY_RENDERER_BACKEND, + CONFIG_KEY_SNAPSHOT_INTERVAL_SEC, CONFIG_KEY_STORAGE_BACKEND, CONFIG_KEY_USE_PROXY, + DEFAULT_PREVIEW_WINDOW_FORWARD, DEFAULT_SNAPSHOT_INTERVAL_SEC, DEFAULT_TRANSITION_SEC, + EXPORT_FORMAT_MP4, }; // --------------------------------------------------------------------------- @@ -77,7 +79,12 @@ impl gpui::EventEmitter for PreferencesContent {} /// /// * **常规 General** — language, theme. /// * **渲染 Rendering** — the renderer backend. -/// * **缓存 Cache** — the disk cache directory (`DiskCachePath`). +/// * **缓存 Cache** — the disk cache directory (`DiskCachePath`) and the +/// playback pre-render window (`PlaybackPreRenderFrames`, the "cache +/// ahead" frames the preview scheduler fills during playback). +/// * **存储 Storage** — the write-through library backend +/// (`Storage/Backend`, SQLite / PostgreSQL) and the PostgreSQL +/// connection string (`Storage/PgUrl`). /// * **代理 Proxy** — use proxy media (`UseProxyMedia`), proxy resolution /// divider (`ProxyDivider`). /// * **项目 Project** — the snapshot interval (`Storage/SnapshotIntervalSec`, @@ -94,6 +101,7 @@ pub struct PreferencesContent { language: Entity, theme: Entity, cache_dir: Entity, + cache_ahead: Entity, use_proxy: Entity, hw_decode: Entity, proxy_divider: Entity, @@ -103,6 +111,11 @@ pub struct PreferencesContent { transition_length: Entity, audio_output: Entity, audio_input: Entity, + storage_backend: Entity, + storage_pg_url: Entity, + /// Whether the write-through library uses PostgreSQL (drives the + /// visibility of the connection-string field). + storage_is_pg: bool, /// The backend options, in display order. backends: Vec<&'static str>, /// The proxy divider options, in display order (1 = full resolution). @@ -211,6 +224,33 @@ impl PreferencesContent { let configured_cache = config_get_string(CONFIG_KEY_DISK_CACHE_PATH); cache_dir.update(cx, |field, cx| field.set_path(configured_cache, cx)); + // --- 缓存 Cache: the playback pre-render window (cache ahead) ------ + // The number of frames the preview scheduler fills ahead of the + // playhead during playback (`PlaybackPreRenderFrames`, consumed by + // `update_preview_window` every playback tick — no restart needed). + let cache_ahead = cx.new(|cx| { + let current = config_get_int( + CONFIG_KEY_PREVIEW_WINDOW, + DEFAULT_PREVIEW_WINDOW_FORWARD, + ) + .clamp(8, 1200); + SpinBox::new( + 10, + SliderModel::new(ValueKind::Integer, 8.0, 1200.0, 8.0, current as f64), + window, + cx, + ) + }); + cx.subscribe(&cache_ahead, |_this, _spin, event: &SpinBoxEvent, cx| { + let value = match event { + SpinBoxEvent::ValueChanged { value, .. } + | SpinBoxEvent::EditCommitted { value, .. } => value.to_f64() as i64, + }; + config_set_int(CONFIG_KEY_PREVIEW_WINDOW, value); + let _ = cx; + }) + .detach(); + // --- 代理 Proxy ----------------------------------------------------- let use_proxy = cx.new(|cx| { CheckBox::new( @@ -412,11 +452,50 @@ impl PreferencesContent { }) .detach(); + // --- 存储 Storage: the write-through library backend ---------------- + // `Storage/Backend` + `Storage/PgUrl` are read by oakstorage's + // write-through when a project binds (per-project library session), + // so a change applies to projects opened afterwards. + let storage_is_pg = config_get_string(CONFIG_KEY_STORAGE_BACKEND) == "pg"; + let storage_backend = cx.new(|cx| { + let options = vec![ + ComboBoxOption::new(0, "SQLite"), + ComboBoxOption::new(1, "PostgreSQL"), + ]; + ComboBox::new(11, options, window, cx) + }); + cx.subscribe(&storage_backend, |this, _combo, event: &ComboBoxEvent, cx| { + match *event { + ComboBoxEvent::Selected { value } => { + let backend = if value == 1 { "pg" } else { "sqlite" }; + this.storage_is_pg = backend == "pg"; + config_set_string(CONFIG_KEY_STORAGE_BACKEND, backend); + // Re-render so the connection-string row shows/hides with + // the selection. + cx.notify(); + } + } + }) + .detach(); + storage_backend.update(cx, |combo, cx| { + combo.set_selected(Some(if storage_is_pg { 1 } else { 0 }), cx) + }); + let storage_pg_url = cx.new(|cx| { + let editor = cx.new(|cx| EditableTextState::new(StringStorage::default(), cx)); + PathField { + editor, + enabled: true, + } + }); + let configured_pg_url = config_get_string(CONFIG_KEY_PG_URL); + storage_pg_url.update(cx, |field, cx| field.set_path(configured_pg_url, cx)); + Self { backend, language, theme, cache_dir, + cache_ahead, use_proxy, hw_decode, proxy_divider, @@ -426,6 +505,9 @@ impl PreferencesContent { transition_length, audio_output, audio_input, + storage_backend, + storage_pg_url, + storage_is_pg, backends, dividers, output_devices, @@ -440,6 +522,13 @@ impl PreferencesContent { config_set_string(crate::oakui::displaycolor::CONFIG_KEY_CUSTOM_ICC, &path); } + /// Commits the PostgreSQL connection-string field to the config (called + /// by the host when the dialog closes, like the cache directory). + pub fn commit_storage_pg_url(&self, cx: &App) { + let url = self.storage_pg_url.read(cx).path(cx).trim().to_string(); + config_set_string(CONFIG_KEY_PG_URL, &url); + } + /// Opens the platform file picker for a custom ICC profile. fn browse_display_icc(&mut self, cx: &mut Context) { let receiver = cx.prompt_for_paths(gpui::PathPromptOptions { @@ -650,6 +739,43 @@ impl Render for PreferencesContent { })), ), )) + // 缓存 Cache: cache ahead (playback pre-render window) + .child(form_row( + &colors, + i18n::tr("preferences.cache.ahead").into(), + div() + .debug_selector(|| "preferences-cache-ahead".into()) + .child(self.cache_ahead.clone()), + )) + // 存储 Storage + .child(section_header( + &colors, + i18n::tr("preferences.section.storage").into(), + )) + .child(form_row( + &colors, + i18n::tr("preferences.storage.backend").into(), + div() + .debug_selector(|| "preferences-storage-backend".into()) + .child(self.storage_backend.clone()), + )) + .child(if self.storage_is_pg { + form_row( + &colors, + i18n::tr("preferences.storage.pg_url").into(), + div() + .debug_selector(|| "preferences-storage-pg-url".into()) + .child(self.storage_pg_url.clone()), + ) + } else { + div() + }) + .child( + div() + .text_color(colors.disabled) + .text_xs() + .child(i18n::tr("preferences.storage.restart_hint")), + ) // 代理 Proxy .child(section_header( &colors, @@ -1637,11 +1763,13 @@ impl PreferencesDialogContent { } /// Commits the general tab's free-text fields (the cache directory, the - /// custom ICC path), for the host when the dialog closes. + /// custom ICC path, the PostgreSQL connection string), for the host when + /// the dialog closes. pub fn commit_cache_dir(&self, cx: &App) { let general = self.general.read(cx); general.commit_cache_dir(cx); general.commit_display_icc_path(cx); + general.commit_storage_pg_url(cx); } /// The keyboard tab's action-row count (tests). @@ -2558,6 +2686,54 @@ fn selection_step(visible: &[usize], current: Option, delta: i32) -> Opti Some(visible[next]) } +// --------------------------------------------------------------------------- +// About (帮助 → 关于 Oak) +// --------------------------------------------------------------------------- + +/// The About dialog's content: the app name + version, the GPL-3.0 license +/// line and the Olive fork notice (the C++ AboutDialog's text block; the +/// patrons scroller is not ported). +pub struct AboutContent; + +impl AboutContent { + /// Builds the static about text. + pub fn new() -> Self { + Self + } +} + +impl Render for AboutContent { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let colors = cx.default_colors().clone(); + div() + .flex() + .flex_col() + .gap_2() + .child( + div() + .text_color(colors.text) + .font_weight(gpui::FontWeight::BOLD) + .child(format!("Oak Video Editor {}", env!("CARGO_PKG_VERSION"))), + ) + .child( + div() + .text_color(colors.text) + .child(i18n::tr("about.description")), + ) + .child( + div() + .text_color(colors.text) + .child(i18n::tr("about.thanks")), + ) + .child( + div() + .text_color(colors.disabled) + .text_xs() + .child(i18n::tr("about.fork_notice")), + ) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/oak-app/src/oakui/real.rs b/crates/oak-app/src/oakui/real.rs index 5da50a1cb..467530683 100644 --- a/crates/oak-app/src/oakui/real.rs +++ b/crates/oak-app/src/oakui/real.rs @@ -253,9 +253,9 @@ enum FullResTarget { // their slots back to the workers. /// The default forward pre-render window (frames). -const DEFAULT_PREVIEW_WINDOW_FORWARD: i64 = 120; +pub const DEFAULT_PREVIEW_WINDOW_FORWARD: i64 = 120; /// Config key for the forward pre-render window size (frames). -const CONFIG_KEY_PREVIEW_WINDOW: &str = "PlaybackPreRenderFrames"; +pub const CONFIG_KEY_PREVIEW_WINDOW: &str = "PlaybackPreRenderFrames"; /// One monitor's playback pre-render state (UI-thread-owned; the completions /// delivered by the dispatcher's poll run on the UI thread too). @@ -5403,6 +5403,10 @@ pub fn audio_init_from_config() { /// The config key selecting the storage backend (see /// `crates/oakstorage/src/writethrough.rs`). pub const CONFIG_KEY_STORAGE_BACKEND: &str = "Storage/Backend"; +/// The config key holding the PostgreSQL connection string (`Storage/PgUrl`, +/// `user:pass@host:5432/dbname`; used when the backend is `"pg"` — see +/// `crates/oakstorage/src/writethrough.rs`). +pub const CONFIG_KEY_PG_URL: &str = "Storage/PgUrl"; /// Enables the SQLite write-through library unless the user configured the /// backend explicitly (any existing value — including "off" — wins over the diff --git a/gpui b/gpui index 79b3a8fbd..7497035c1 160000 --- a/gpui +++ b/gpui @@ -1 +1 @@ -Subproject commit 79b3a8fbd40823853a42773d2a4fa32f92c30050 +Subproject commit 7497035c12a20f1d8d0e22418e2f1c25d836e7ac