app: about dialog, modal close affordances, more preferences
CI / Build & test (Windows) (push) Failing after 16m35s
CI / Build & test (Linux) (push) Successful in 18m59s

- 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.
This commit is contained in:
2026-08-25 01:55:08 +08:00
parent ccfd8fec60
commit 186c41b894
12 changed files with 393 additions and 10 deletions
+9
View File
@@ -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"
+9
View File
@@ -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"
+9
View File
@@ -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"
+9
View File
@@ -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"
+9
View File
@@ -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": "フル解像度"
+9
View File
@@ -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"
+9
View File
@@ -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": "Полное разрешение"
+9
View File
@@ -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": "原始分辨率"
+133 -2
View File
@@ -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<E: AppEngine> {
modal: Entity<Modal>,
content: Entity<crate::dialogs::ProjectPropertiesContent<E>>,
},
/// The about dialog (Help > About Oak…; static content).
About { modal: Entity<Modal> },
}
/// A running export: the session the tick loop drains for progress.
@@ -207,7 +211,8 @@ impl<E: AppEngine> ModalState<E> {
| 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<E: AppEngine> OakApp<E> {
| 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<E: AppEngine> OakApp<E> {
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<E: AppEngine> OakApp<E> {
}
}
/// 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<Self>) {
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<E: AppEngine> OakApp<E> {
}
}
}
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).
+181 -5
View File
@@ -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<PreferencesEvent> 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<ComboBox>,
theme: Entity<ComboBox>,
cache_dir: Entity<PathField>,
cache_ahead: Entity<SpinBox>,
use_proxy: Entity<CheckBox>,
hw_decode: Entity<CheckBox>,
proxy_divider: Entity<ComboBox>,
@@ -103,6 +111,11 @@ pub struct PreferencesContent {
transition_length: Entity<SpinBox>,
audio_output: Entity<ComboBox>,
audio_input: Entity<ComboBox>,
storage_backend: Entity<ComboBox>,
storage_pg_url: Entity<PathField>,
/// 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<Self>) {
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<usize>, 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<Self>) -> 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::*;
+6 -2
View File
@@ -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
+1 -1
Submodule gpui updated: 79b3a8fbd4...7497035c12