Open Blogmarks through the standard tab flow
This commit is contained in:
@@ -26,7 +26,7 @@ The project is under active development. Core blogging workflows are broadly ava
|
|||||||
- 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.
|
||||||
- Site, media, and translation validation plus `ruds://new-post` Blogmark capture and Lua transforms; bDS2 keeps its separate `bds2://` bookmarklet protocol.
|
- Site, media, and translation validation plus `ruds://new-post` Blogmark capture and Lua transforms; captures open directly in the post editor and defer automatic translation until an explicit save or publish. bDS2 keeps its separate `bds2://` bookmarklet protocol.
|
||||||
|
|
||||||
RuDS uses no JavaScript application runtime and loads no CSS or JavaScript from CDNs. The preview is served by the Rust application and displayed by the operating-system webview.
|
RuDS uses no JavaScript application runtime and loads no CSS or JavaScript from CDNs. The preview is served by the Rust application and displayed by the operating-system webview.
|
||||||
|
|
||||||
|
|||||||
@@ -1627,26 +1627,7 @@ impl BdsApp {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Tabs ──
|
// ── Tabs ──
|
||||||
Message::OpenTab(tab) => {
|
Message::OpenTab(tab) => self.open_tab(tab),
|
||||||
self.flush_active_post_editor();
|
|
||||||
let idx = tabs::open_tab(&mut self.tabs, tab);
|
|
||||||
let mut semantic_post_id = None;
|
|
||||||
if let Some(t) = self.tabs.get(idx) {
|
|
||||||
self.active_tab = Some(t.id.clone());
|
|
||||||
let tab_clone = t.clone();
|
|
||||||
if tab_clone.tab_type == TabType::Post {
|
|
||||||
semantic_post_id = Some(tab_clone.id.clone());
|
|
||||||
}
|
|
||||||
self.load_editor_for_tab(&tab_clone);
|
|
||||||
}
|
|
||||||
self.enforce_panel_tab_fallback();
|
|
||||||
self.sync_menu_state();
|
|
||||||
let mut tasks = vec![self.sync_embedded_previews()];
|
|
||||||
if let Some(post_id) = semantic_post_id {
|
|
||||||
tasks.push(Task::done(Message::LoadSemanticTagSuggestions(post_id)));
|
|
||||||
}
|
|
||||||
Task::batch(tasks)
|
|
||||||
}
|
|
||||||
Message::CloseTab(id) => {
|
Message::CloseTab(id) => {
|
||||||
if self.active_tab.as_deref() == Some(id.as_str()) {
|
if self.active_tab.as_deref() == Some(id.as_str()) {
|
||||||
self.flush_active_post_editor();
|
self.flush_active_post_editor();
|
||||||
@@ -2611,13 +2592,9 @@ impl BdsApp {
|
|||||||
is_transient: false,
|
is_transient: false,
|
||||||
is_dirty: false,
|
is_dirty: false,
|
||||||
};
|
};
|
||||||
let index = tabs::open_tab(&mut self.tabs, tab);
|
let open_editor = self.open_tab(tab);
|
||||||
self.active_tab = self.tabs.get(index).map(|tab| tab.id.clone());
|
|
||||||
if let Some(tab) = self.tabs.get(index).cloned() {
|
|
||||||
self.load_editor_for_tab(&tab);
|
|
||||||
}
|
|
||||||
self.notify(ToastLevel::Success, &t(self.ui_locale, "blogmark.imported"));
|
self.notify(ToastLevel::Success, &t(self.ui_locale, "blogmark.imported"));
|
||||||
self.refresh_counts()
|
Task::batch([open_editor, self.refresh_counts()])
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
if self.task_manager.status(task_id) != Some(TaskStatus::Cancelled) {
|
if self.task_manager.status(task_id) != Some(TaskStatus::Cancelled) {
|
||||||
@@ -5522,6 +5499,26 @@ impl BdsApp {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn open_tab(&mut self, tab: Tab) -> Task<Message> {
|
||||||
|
self.flush_active_post_editor();
|
||||||
|
let index = tabs::open_tab(&mut self.tabs, tab);
|
||||||
|
let mut semantic_post_id = None;
|
||||||
|
if let Some(tab) = self.tabs.get(index).cloned() {
|
||||||
|
self.active_tab = Some(tab.id.clone());
|
||||||
|
if tab.tab_type == TabType::Post {
|
||||||
|
semantic_post_id = Some(tab.id.clone());
|
||||||
|
}
|
||||||
|
self.load_editor_for_tab(&tab);
|
||||||
|
}
|
||||||
|
self.enforce_panel_tab_fallback();
|
||||||
|
self.sync_menu_state();
|
||||||
|
let mut tasks = vec![self.sync_embedded_previews()];
|
||||||
|
if let Some(post_id) = semantic_post_id {
|
||||||
|
tasks.push(Task::done(Message::LoadSemanticTagSuggestions(post_id)));
|
||||||
|
}
|
||||||
|
Task::batch(tasks)
|
||||||
|
}
|
||||||
|
|
||||||
fn find_next_in_active_editor(&mut self, query: &str) -> bool {
|
fn find_next_in_active_editor(&mut self, query: &str) -> bool {
|
||||||
let Some(id) = self.active_tab.clone() else {
|
let Some(id) = self.active_tab.clone() else {
|
||||||
return false;
|
return false;
|
||||||
@@ -9596,6 +9593,16 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn imported_blogmark_activates_posts_and_opens_its_editor() {
|
fn imported_blogmark_activates_posts_and_opens_its_editor() {
|
||||||
let (db, project, temp) = setup();
|
let (db, project, temp) = setup();
|
||||||
|
ai::save_endpoint(
|
||||||
|
db.conn(),
|
||||||
|
&ai::AiEndpointConfig {
|
||||||
|
kind: ai::AiEndpointKind::Airplane,
|
||||||
|
url: "http://127.0.0.1:9".to_string(),
|
||||||
|
model: "test".to_string(),
|
||||||
|
api_key: None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
let created = post::create_post(
|
let created = post::create_post(
|
||||||
db.conn(),
|
db.conn(),
|
||||||
temp.path(),
|
temp.path(),
|
||||||
@@ -9610,6 +9617,10 @@ mod tests {
|
|||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let mut app = BdsApp::new_for_tests(db, project, temp.path().to_path_buf());
|
let mut app = BdsApp::new_for_tests(db, project, temp.path().to_path_buf());
|
||||||
|
let mut settings = SettingsViewState::default();
|
||||||
|
settings.default_mode = "preview".to_string();
|
||||||
|
app.settings_state = Some(settings);
|
||||||
|
app.offline_mode = true;
|
||||||
app.sidebar_view = SidebarView::Settings;
|
app.sidebar_view = SidebarView::Settings;
|
||||||
app.sidebar_visible = false;
|
app.sidebar_visible = false;
|
||||||
let task_id = app.task_manager.submit("Importing blogmark");
|
let task_id = app.task_manager.submit("Importing blogmark");
|
||||||
@@ -9627,6 +9638,13 @@ mod tests {
|
|||||||
assert!(app.sidebar_visible);
|
assert!(app.sidebar_visible);
|
||||||
assert_eq!(app.active_tab.as_deref(), Some(created.id.as_str()));
|
assert_eq!(app.active_tab.as_deref(), Some(created.id.as_str()));
|
||||||
assert!(app.post_editors.contains_key(&created.id));
|
assert!(app.post_editors.contains_key(&created.id));
|
||||||
|
assert!(app.embedded_preview.is_some());
|
||||||
|
assert!(
|
||||||
|
app.task_manager
|
||||||
|
.snapshots()
|
||||||
|
.iter()
|
||||||
|
.all(|task| task.group_name.as_deref() != Some("AI"))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -11078,6 +11096,16 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn task_tick_autosaves_dirty_post_editor() {
|
fn task_tick_autosaves_dirty_post_editor() {
|
||||||
let (db, project, tmp) = setup();
|
let (db, project, tmp) = setup();
|
||||||
|
ai::save_endpoint(
|
||||||
|
db.conn(),
|
||||||
|
&ai::AiEndpointConfig {
|
||||||
|
kind: ai::AiEndpointKind::Airplane,
|
||||||
|
url: "http://127.0.0.1:9".to_string(),
|
||||||
|
model: "test".to_string(),
|
||||||
|
api_key: None,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
let created = post::create_post(
|
let created = post::create_post(
|
||||||
db.conn(),
|
db.conn(),
|
||||||
tmp.path(),
|
tmp.path(),
|
||||||
@@ -11108,6 +11136,7 @@ mod tests {
|
|||||||
editor.last_edit_at_ms = bds_core::util::now_unix_ms() - POST_AUTO_SAVE_DELAY_MS - 100;
|
editor.last_edit_at_ms = bds_core::util::now_unix_ms() - POST_AUTO_SAVE_DELAY_MS - 100;
|
||||||
|
|
||||||
let mut app = make_app(db, project, &tmp);
|
let mut app = make_app(db, project, &tmp);
|
||||||
|
app.offline_mode = true;
|
||||||
app.post_editors.insert(created.id.clone(), editor);
|
app.post_editors.insert(created.id.clone(), editor);
|
||||||
app.tabs.push(crate::state::tabs::Tab {
|
app.tabs.push(crate::state::tabs::Tab {
|
||||||
id: created.id.clone(),
|
id: created.id.clone(),
|
||||||
@@ -11127,6 +11156,12 @@ mod tests {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(saved.title, "Autosaved");
|
assert_eq!(saved.title, "Autosaved");
|
||||||
assert!(!app.post_editors.get(&created.id).unwrap().is_dirty);
|
assert!(!app.post_editors.get(&created.id).unwrap().is_dirty);
|
||||||
|
assert!(
|
||||||
|
app.task_manager
|
||||||
|
.snapshots()
|
||||||
|
.iter()
|
||||||
|
.all(|task| task.group_name.as_deref() != Some("AI"))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user