Persist status-bar airplane mode.
This commit is contained in:
@@ -22,7 +22,7 @@ The project is under active development. Core blogging workflows are broadly ava
|
|||||||
- `bds-cli server` hosting the shared application engines over a loopback-by-default, public-key-only SSH service, with restrictive private key material, live authorization updates, terminal-session transport, CLI-change synchronization, ordered domain/task events, and native desktop remote-project selection.
|
- `bds-cli server` hosting the shared application engines over a loopback-by-default, public-key-only SSH service, with restrictive private key material, live authorization updates, terminal-session transport, CLI-change synchronization, ordered domain/task events, and native desktop remote-project selection.
|
||||||
- bDS2-compatible Markdown/Liquid rendering with native macros, descriptive category archive titles, canonical multilingual and flat page routes, recursive menus, calendar archives, feeds, a root hreflang sitemap, Pagefind, and incremental site generation through cancellable section task groups.
|
- bDS2-compatible Markdown/Liquid rendering with native macros, descriptive category archive titles, canonical multilingual and flat page routes, recursive menus, calendar archives, feeds, a root hreflang sitemap, Pagefind, and incremental site generation through cancellable section task groups.
|
||||||
- Navigable generated-route preview in the app or system browser, with draft database overlays and published filesystem content.
|
- Navigable generated-route preview in the app or system browser, with draft database overlays and published filesystem content.
|
||||||
- Optional one-shot AI translation, description, analysis, taxonomy, and language-detection operations run in background tasks with editor-level waiting indicators, using provider-portable JSON-only requests through independent online and local OpenAI-compatible profiles. Each profile has secure credentials, persistently discovered chat/title/image model selections, explicit tool/vision overrides, chat testing, and status-bar airplane-mode routing.
|
- Optional one-shot AI translation, description, analysis, taxonomy, and language-detection operations run in background tasks with editor-level waiting indicators, using provider-portable JSON-only requests through independent online and local OpenAI-compatible profiles. Each profile has secure credentials, persistently discovered chat/title/image model selections, explicit tool/vision overrides, chat testing, and restart-persistent status-bar airplane-mode routing.
|
||||||
- Persistent conversational AI with safe Markdown, streamed and cancellable responses, model/session/token tracking, bounded project-aware blog tools, and localized conversation management in the Chat workspace. Allowlisted render tools add persistent native cards, charts, forms, lists, metrics, mind maps, tables, and tabs without executing assistant-provided HTML or JavaScript.
|
- Persistent conversational AI with safe Markdown, streamed and cancellable responses, model/session/token tracking, bounded project-aware blog tools, and localized conversation management in the Chat workspace. Allowlisted render tools add persistent native cards, charts, forms, lists, metrics, mind maps, tables, and tabs without executing assistant-provided HTML or JavaScript.
|
||||||
- SSH-agent-based SCP or rsync publishing.
|
- SSH-agent-based SCP or rsync publishing.
|
||||||
- Integrated Git workflow with repository initialization, Git LFS image tracking, status and diffs, branch/file history, commits, remotes, cancellable fetch/pull/push, and post-pull filesystem reconciliation; network actions respect airplane mode.
|
- Integrated Git workflow with repository initialization, Git LFS image tracking, status and diffs, branch/file history, commits, remotes, cancellable fetch/pull/push, and post-pull filesystem reconciliation; network actions respect airplane mode.
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use crate::engine::{EngineError, EngineResult, domain_events};
|
|||||||
use crate::util::now_unix_ms;
|
use crate::util::now_unix_ms;
|
||||||
|
|
||||||
pub const UI_LANGUAGE_KEY: &str = "ui.language";
|
pub const UI_LANGUAGE_KEY: &str = "ui.language";
|
||||||
|
pub const AIRPLANE_MODE_KEY: &str = "ai.airplane_mode_enabled";
|
||||||
pub const ONLINE_API_KEY: &str = "ai.endpoint.online.api_key";
|
pub const ONLINE_API_KEY: &str = "ai.endpoint.online.api_key";
|
||||||
pub const AIRPLANE_API_KEY: &str = "ai.endpoint.airplane.api_key";
|
pub const AIRPLANE_API_KEY: &str = "ai.endpoint.airplane.api_key";
|
||||||
const ONLINE_API_KEY_CONFIGURED: &str = "ai.endpoint.online.api_key_configured";
|
const ONLINE_API_KEY_CONFIGURED: &str = "ai.endpoint.online.api_key_configured";
|
||||||
@@ -112,6 +113,18 @@ pub fn ui_language(conn: &Connection) -> EngineResult<Option<String>> {
|
|||||||
get(conn, UI_LANGUAGE_KEY)
|
get(conn, UI_LANGUAGE_KEY)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn airplane_mode(conn: &Connection) -> EngineResult<bool> {
|
||||||
|
Ok(get(conn, AIRPLANE_MODE_KEY)?.as_deref() != Some("false"))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_airplane_mode(conn: &Connection, enabled: bool) -> EngineResult<()> {
|
||||||
|
set(
|
||||||
|
conn,
|
||||||
|
AIRPLANE_MODE_KEY,
|
||||||
|
if enabled { "true" } else { "false" },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -127,4 +140,26 @@ mod tests {
|
|||||||
let removed_prefix = ["style", "."].concat();
|
let removed_prefix = ["style", "."].concat();
|
||||||
assert!(!values.keys().any(|key| key.starts_with(&removed_prefix)));
|
assert!(!values.keys().any(|key| key.starts_with(&removed_prefix)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn airplane_mode_defaults_to_safe_mode_and_persists_changes() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let path = dir.path().join("bds.db");
|
||||||
|
let db = Database::open(&path).unwrap();
|
||||||
|
db.migrate().unwrap();
|
||||||
|
|
||||||
|
assert!(airplane_mode(db.conn()).unwrap());
|
||||||
|
|
||||||
|
set_airplane_mode(db.conn(), false).unwrap();
|
||||||
|
drop(db);
|
||||||
|
|
||||||
|
let db = Database::open(&path).unwrap();
|
||||||
|
assert!(!airplane_mode(db.conn()).unwrap());
|
||||||
|
|
||||||
|
set_airplane_mode(db.conn(), true).unwrap();
|
||||||
|
drop(db);
|
||||||
|
|
||||||
|
let db = Database::open(&path).unwrap();
|
||||||
|
assert!(airplane_mode(db.conn()).unwrap());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1061,6 +1061,9 @@ impl BdsApp {
|
|||||||
.and_then(|db| engine::settings::ui_language(db.conn()).ok().flatten())
|
.and_then(|db| engine::settings::ui_language(db.conn()).ok().flatten())
|
||||||
.map(|language| normalize_language(&language))
|
.map(|language| normalize_language(&language))
|
||||||
.unwrap_or(os_locale);
|
.unwrap_or(os_locale);
|
||||||
|
let offline_mode = db
|
||||||
|
.as_ref()
|
||||||
|
.is_none_or(|db| engine::settings::airplane_mode(db.conn()).unwrap_or(true));
|
||||||
let (menu_bar, registry) = menu::build_menu_bar(locale);
|
let (menu_bar, registry) = menu::build_menu_bar(locale);
|
||||||
let search_index_rebuild_required = db
|
let search_index_rebuild_required = db
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -1186,7 +1189,7 @@ impl BdsApp {
|
|||||||
ui_locale: locale,
|
ui_locale: locale,
|
||||||
content_language: "en".to_string(),
|
content_language: "en".to_string(),
|
||||||
blog_languages: Vec::new(),
|
blog_languages: Vec::new(),
|
||||||
offline_mode: false,
|
offline_mode,
|
||||||
locale_dropdown_open: false,
|
locale_dropdown_open: false,
|
||||||
project_dropdown_open: false,
|
project_dropdown_open: false,
|
||||||
theme_badge: String::from("pico"),
|
theme_badge: String::from("pico"),
|
||||||
@@ -1234,6 +1237,7 @@ impl BdsApp {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
fn new_for_tests(db: Database, project: Project, data_dir: PathBuf) -> Self {
|
fn new_for_tests(db: Database, project: Project, data_dir: PathBuf) -> Self {
|
||||||
let chat_conversations = engine::chat::list_conversations(db.conn()).unwrap_or_default();
|
let chat_conversations = engine::chat::list_conversations(db.conn()).unwrap_or_default();
|
||||||
|
let offline_mode = engine::settings::airplane_mode(db.conn()).unwrap_or(true);
|
||||||
Self {
|
Self {
|
||||||
db: Some(db),
|
db: Some(db),
|
||||||
db_path: data_dir.join("bds.db"),
|
db_path: data_dir.join("bds.db"),
|
||||||
@@ -1287,7 +1291,7 @@ impl BdsApp {
|
|||||||
ui_locale: UiLocale::En,
|
ui_locale: UiLocale::En,
|
||||||
content_language: "en".to_string(),
|
content_language: "en".to_string(),
|
||||||
blog_languages: Vec::new(),
|
blog_languages: Vec::new(),
|
||||||
offline_mode: false,
|
offline_mode,
|
||||||
locale_dropdown_open: false,
|
locale_dropdown_open: false,
|
||||||
project_dropdown_open: false,
|
project_dropdown_open: false,
|
||||||
theme_badge: String::from("pico"),
|
theme_badge: String::from("pico"),
|
||||||
@@ -2645,6 +2649,21 @@ impl BdsApp {
|
|||||||
|
|
||||||
// ── Settings ──
|
// ── Settings ──
|
||||||
Message::SetOfflineMode(mode) => {
|
Message::SetOfflineMode(mode) => {
|
||||||
|
if let Some(db) = &self.db
|
||||||
|
&& let Err(error) = engine::settings::set_airplane_mode(db.conn(), mode)
|
||||||
|
{
|
||||||
|
let operation = t(self.ui_locale, "settings.offlineMode");
|
||||||
|
let error = error.to_string();
|
||||||
|
self.notify(
|
||||||
|
ToastLevel::Error,
|
||||||
|
&tw(
|
||||||
|
self.ui_locale,
|
||||||
|
"common.operationFailed",
|
||||||
|
&[("operation", &operation), ("error", &error)],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
self.offline_mode = mode;
|
self.offline_mode = mode;
|
||||||
let models = self.chat_model_options();
|
let models = self.chat_model_options();
|
||||||
let selected = self.db.as_ref().and_then(|db| {
|
let selected = self.db.as_ref().and_then(|db| {
|
||||||
@@ -10111,6 +10130,33 @@ mod tests {
|
|||||||
BdsApp::new_for_tests(db, project, tmp.path().to_path_buf())
|
BdsApp::new_for_tests(db, project, tmp.path().to_path_buf())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn airplane_mode_is_restored_and_status_bar_changes_are_persisted() {
|
||||||
|
let (db, project, tmp) = setup();
|
||||||
|
bds_core::engine::settings::set(
|
||||||
|
db.conn(),
|
||||||
|
bds_core::engine::settings::AIRPLANE_MODE_KEY,
|
||||||
|
"true",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let mut app = make_app(db, project, &tmp);
|
||||||
|
assert!(app.offline_mode);
|
||||||
|
|
||||||
|
let _ = app.update(Message::SetOfflineMode(false));
|
||||||
|
|
||||||
|
assert!(!app.offline_mode);
|
||||||
|
assert_eq!(
|
||||||
|
bds_core::engine::settings::get(
|
||||||
|
app.db.as_ref().unwrap().conn(),
|
||||||
|
bds_core::engine::settings::AIRPLANE_MODE_KEY
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.as_deref(),
|
||||||
|
Some("false")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn native_paste_menu_action_is_queued_for_the_focused_widget() {
|
fn native_paste_menu_action_is_queued_for_the_focused_widget() {
|
||||||
let (db, project, tmp) = setup();
|
let (db, project, tmp) = setup();
|
||||||
|
|||||||
Reference in New Issue
Block a user