diff --git a/crates/bds-core/src/engine/post.rs b/crates/bds-core/src/engine/post.rs index d4a2eaf..53d0bb0 100644 --- a/crates/bds-core/src/engine/post.rs +++ b/crates/bds-core/src/engine/post.rs @@ -580,6 +580,63 @@ pub fn rebuild_posts_from_filesystem_with_progress( Ok(report) } +/// Rebuild the inter-post link graph for all published posts in a project. +/// Reads each published post's content from the filesystem, parses links, +/// and recreates post_links records. +pub fn rebuild_all_links( + conn: &Connection, + data_dir: &Path, + project_id: &str, +) -> EngineResult { + let posts = qp::list_posts_by_project(conn, project_id)?; + let now = now_unix_ms(); + let mut link_count = 0; + + for post in &posts { + // Delete existing links from this post + ql::delete_links_by_source(conn, &post.id)?; + + // Get post content: from DB or filesystem + let content = if let Some(ref c) = post.content { + c.clone() + } else if !post.file_path.is_empty() { + let abs_path = data_dir.join(&post.file_path); + if abs_path.exists() { + if let Ok(raw) = fs::read_to_string(&abs_path) { + if let Ok((_fm, body)) = read_post_file(&raw) { + body + } else { + continue; + } + } else { + continue; + } + } else { + continue; + } + } else { + continue; + }; + + let parsed = parse_post_links(&content); + for (target_slug, link_text) in &parsed { + if let Ok(target) = qp::get_post_by_project_and_slug(conn, project_id, target_slug) { + let link = PostLink { + id: Uuid::new_v4().to_string(), + source_post_id: post.id.clone(), + target_post_id: target.id.clone(), + link_text: Some(link_text.clone()), + created_at: now, + }; + let _ = ql::insert_post_link(conn, &link); + link_count += 1; + } + } + } + + Ok(link_count) +} + // --- Internal helpers --- /// Parse inter-post links from markdown content. diff --git a/crates/bds-ui/src/app.rs b/crates/bds-ui/src/app.rs index d43848a..50de056 100644 --- a/crates/bds-ui/src/app.rs +++ b/crates/bds-ui/src/app.rs @@ -22,7 +22,7 @@ use crate::state::tabs::{self, Tab, TabType}; use crate::state::toast::{Toast, ToastLevel}; use crate::views::{ modal, workspace, - post_editor::{PostEditorState, PostEditorMsg}, + post_editor::{PostEditorState, PostEditorMsg, ResolvedPostLink}, media_editor::{MediaEditorState, MediaEditorMsg}, template_editor::{TemplateEditorState, TemplateEditorMsg}, script_editor::{ScriptEditorState, ScriptEditorMsg}, @@ -1139,7 +1139,8 @@ impl BdsApp { db.conn(), &post.id, ).ok()) .unwrap_or_default(); - let state = PostEditorState::from_post(&post, &self.blog_languages, &translations); + let (outlinks, backlinks) = self.load_post_links(&post.id); + let state = PostEditorState::from_post(&post, &self.blog_languages, &translations, outlinks, backlinks); self.post_editors.insert(post.id.clone(), state); } Err(e) => self.notify(ToastLevel::Error, &e), @@ -1933,7 +1934,9 @@ impl BdsApp { let mut state = SettingsViewState::default(); if let Some(ref project) = self.active_project { state.project_name = project.name.clone(); - state.project_description = project.description.clone().unwrap_or_default(); + state.project_description = iced::widget::text_editor::Content::with_text( + &project.description.clone().unwrap_or_default(), + ); state.data_path = project.data_path.clone().unwrap_or_default(); } if let Some(ref data_dir) = self.data_dir { @@ -1963,7 +1966,9 @@ impl BdsApp { } } SettingsMsg::ProjectNameChanged(s) => { state.project_name = s; } - SettingsMsg::ProjectDescriptionChanged(s) => { state.project_description = s; } + SettingsMsg::ProjectDescriptionAction(action) => { + state.project_description.perform(action); + } SettingsMsg::DataPathChanged(s) => { state.data_path = s; } SettingsMsg::BrowseDataPath => { return crate::platform::dialog::pick_folder(t(self.ui_locale, "dialog.selectFolder")); @@ -1996,14 +2001,39 @@ impl BdsApp { state.offline_mode = b; return Task::done(Message::SetOfflineMode(b)); } - SettingsMsg::SystemPromptChanged(s) => { state.system_prompt = s; } + SettingsMsg::SystemPromptAction(action) => { + state.system_prompt.perform(action); + } SettingsMsg::SaveSystemPrompt => { /* System prompt save will be wired */ } - SettingsMsg::ResetSystemPrompt => { state.system_prompt.clear(); } + SettingsMsg::ResetSystemPrompt => { + state.system_prompt = iced::widget::text_editor::Content::new(); + } SettingsMsg::RebuildPosts => { return Task::done(Message::RebuildDatabase); } SettingsMsg::RebuildMedia => { return Task::done(Message::RebuildDatabase); } SettingsMsg::RebuildScripts => { return Task::done(Message::RebuildDatabase); } SettingsMsg::RebuildTemplates => { return Task::done(Message::RebuildDatabase); } - SettingsMsg::RebuildLinks => { return Task::done(Message::ReindexText); } + SettingsMsg::RebuildLinks => { + if let (Some(db), Some(data_dir), Some(project)) = + (&self.db, &self.data_dir, &self.active_project) + { + match bds_core::engine::post::rebuild_all_links( + db.conn(), data_dir, &project.id, + ) { + Ok(count) => { + self.notify( + ToastLevel::Info, + &format!("Rebuilt {} links", count), + ); + } + Err(e) => { + self.notify( + ToastLevel::Error, + &format!("Failed to rebuild links: {e}"), + ); + } + } + } + } SettingsMsg::RegenerateThumbnails => { self.notify(ToastLevel::Info, &t(self.ui_locale, "settings.regeneratingThumbnails")); } @@ -2016,6 +2046,38 @@ impl BdsApp { Task::none() } + /// Load outlinks and backlinks for a post, resolving target post titles. + fn load_post_links(&self, post_id: &str) -> (Vec, Vec) { + let Some(ref db) = self.db else { + return (Vec::new(), Vec::new()); + }; + let outlinks = bds_core::db::queries::post_link::list_links_by_source(db.conn(), post_id) + .unwrap_or_default() + .into_iter() + .filter_map(|link| { + bds_core::db::queries::post::get_post_by_id(db.conn(), &link.target_post_id) + .ok() + .map(|p| ResolvedPostLink { + post_id: p.id, + title: p.title, + }) + }) + .collect(); + let backlinks = bds_core::db::queries::post_link::list_links_by_target(db.conn(), post_id) + .unwrap_or_default() + .into_iter() + .filter_map(|link| { + bds_core::db::queries::post::get_post_by_id(db.conn(), &link.source_post_id) + .ok() + .map(|p| ResolvedPostLink { + post_id: p.id, + title: p.title, + }) + }) + .collect(); + (outlinks, backlinks) + } + /// Load editor state when a tab is opened for an entity. fn load_editor_for_tab(&mut self, tab: &Tab) { let Some(ref db) = self.db else { return }; @@ -2045,9 +2107,10 @@ impl BdsApp { let translations = bds_core::db::queries::post_translation::list_post_translations_by_post( db.conn(), &post.id, ).unwrap_or_default(); + let (outlinks, backlinks) = self.load_post_links(&post.id); self.post_editors.insert( post.id.clone(), - PostEditorState::from_post(&post, &self.blog_languages, &translations), + PostEditorState::from_post(&post, &self.blog_languages, &translations, outlinks, backlinks), ); } Err(e) => { @@ -2072,13 +2135,17 @@ impl BdsApp { if !self.template_editors.contains_key(&tab.id) { match bds_core::db::queries::template::get_template_by_id(db.conn(), &tab.id) { Ok(mut template) => { - // Published templates: read content from file + // Published templates: read content from file, strip frontmatter if template.content.is_none() { if let Some(ref data_dir) = self.data_dir { let rel = bds_core::util::paths::template_file_path(&template.slug); let path = data_dir.join(&rel); - if let Ok(body) = std::fs::read_to_string(&path) { - template.content = Some(body); + if let Ok(raw) = std::fs::read_to_string(&path) { + if let Ok((_fm, body)) = + bds_core::util::frontmatter::read_template_file(&raw) + { + template.content = Some(body); + } } } } @@ -2094,13 +2161,16 @@ impl BdsApp { if !self.script_editors.contains_key(&tab.id) { match bds_core::db::queries::script::get_script_by_id(db.conn(), &tab.id) { Ok(mut script) => { - // Published scripts: read content from file + // Published scripts: read content from file using actual file_path if script.content.is_none() { if let Some(ref data_dir) = self.data_dir { - let rel = bds_core::util::paths::script_file_path(&script.slug); - let path = data_dir.join(&rel); - if let Ok(body) = std::fs::read_to_string(&path) { - script.content = Some(body); + let path = data_dir.join(&script.file_path); + if let Ok(raw) = std::fs::read_to_string(&path) { + if let Ok((_fm, body)) = + bds_core::util::frontmatter::read_script_file(&raw) + { + script.content = Some(body); + } } } } @@ -2114,9 +2184,16 @@ impl BdsApp { } TabType::Tags => { if self.tags_view_state.is_none() { + let project_id = self.active_project.as_ref().map(|p| p.id.as_str()).unwrap_or(""); + // Sync tags from posts first to ensure tags table is populated + if let Some(ref data_dir) = self.data_dir { + let _ = bds_core::engine::tag::sync_tags_from_posts( + db.conn(), data_dir, project_id, + ); + } let tags = bds_core::db::queries::tag::list_tags_by_project( db.conn(), - self.active_project.as_ref().map(|p| p.id.as_str()).unwrap_or(""), + project_id, ).unwrap_or_default(); self.tags_view_state = Some(TagsViewState::new(tags)); } @@ -2126,7 +2203,9 @@ impl BdsApp { let mut state = SettingsViewState::default(); if let Some(ref project) = self.active_project { state.project_name = project.name.clone(); - state.project_description = project.description.clone().unwrap_or_default(); + state.project_description = iced::widget::text_editor::Content::with_text( + &project.description.clone().unwrap_or_default(), + ); state.data_path = project.data_path.clone().unwrap_or_default(); } if let Some(ref data_dir) = self.data_dir { diff --git a/crates/bds-ui/src/platform/menu.rs b/crates/bds-ui/src/platform/menu.rs index e7e6b64..65ed639 100644 --- a/crates/bds-ui/src/platform/menu.rs +++ b/crates/bds-ui/src/platform/menu.rs @@ -249,28 +249,28 @@ pub fn build_menu_bar(locale: UiLocale) -> (Menu, MenuRegistry) { let _ = view_menu.append(&PredefinedMenuItem::separator()); let _ = view_menu.append(&PredefinedMenuItem::fullscreen(None)); - // -- Window -- - let window_menu = Submenu::new(translate(locale, "menu.group.window"), true); - let _ = window_menu.append(&item(&mut reg, MenuAction::PublishSelected, locale, + // -- Blog -- + let blog_menu = Submenu::new(translate(locale, "menu.group.blog"), true); + let _ = blog_menu.append(&item(&mut reg, MenuAction::PublishSelected, locale, Some(Accelerator::new(Some(CMD_OR_CTRL | Modifiers::SHIFT), Code::KeyP)))); - let _ = window_menu.append(&item(&mut reg, MenuAction::PreviewPost, locale, + let _ = blog_menu.append(&item(&mut reg, MenuAction::PreviewPost, locale, Some(Accelerator::new(Some(CMD_OR_CTRL | Modifiers::SHIFT), Code::KeyV)))); - let _ = window_menu.append(&PredefinedMenuItem::separator()); - let _ = window_menu.append(&item(&mut reg, MenuAction::EditMenu, locale, None)); - let _ = window_menu.append(&PredefinedMenuItem::separator()); - let _ = window_menu.append(&item(&mut reg, MenuAction::RebuildDatabase, locale, None)); - let _ = window_menu.append(&item(&mut reg, MenuAction::ReindexText, locale, None)); - let _ = window_menu.append(&item(&mut reg, MenuAction::MetadataDiff, locale, None)); - let _ = window_menu.append(&PredefinedMenuItem::separator()); - let _ = window_menu.append(&item(&mut reg, MenuAction::RegenerateCalendar, locale, None)); - let _ = window_menu.append(&item(&mut reg, MenuAction::ValidateTranslations, locale, None)); - let _ = window_menu.append(&item(&mut reg, MenuAction::FillMissingTranslations, locale, None)); - let _ = window_menu.append(&PredefinedMenuItem::separator()); - let _ = window_menu.append(&item(&mut reg, MenuAction::GenerateSitemap, locale, + let _ = blog_menu.append(&PredefinedMenuItem::separator()); + let _ = blog_menu.append(&item(&mut reg, MenuAction::EditMenu, locale, None)); + let _ = blog_menu.append(&PredefinedMenuItem::separator()); + let _ = blog_menu.append(&item(&mut reg, MenuAction::RebuildDatabase, locale, None)); + let _ = blog_menu.append(&item(&mut reg, MenuAction::ReindexText, locale, None)); + let _ = blog_menu.append(&item(&mut reg, MenuAction::MetadataDiff, locale, None)); + let _ = blog_menu.append(&PredefinedMenuItem::separator()); + let _ = blog_menu.append(&item(&mut reg, MenuAction::RegenerateCalendar, locale, None)); + let _ = blog_menu.append(&item(&mut reg, MenuAction::ValidateTranslations, locale, None)); + let _ = blog_menu.append(&item(&mut reg, MenuAction::FillMissingTranslations, locale, None)); + let _ = blog_menu.append(&PredefinedMenuItem::separator()); + let _ = blog_menu.append(&item(&mut reg, MenuAction::GenerateSitemap, locale, Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyR)))); - let _ = window_menu.append(&item(&mut reg, MenuAction::ValidateSite, locale, + let _ = blog_menu.append(&item(&mut reg, MenuAction::ValidateSite, locale, Some(Accelerator::new(Some(CMD_OR_CTRL | Modifiers::SHIFT), Code::KeyL)))); - let _ = window_menu.append(&item(&mut reg, MenuAction::UploadSite, locale, + let _ = blog_menu.append(&item(&mut reg, MenuAction::UploadSite, locale, Some(Accelerator::new(Some(CMD_OR_CTRL | Modifiers::SHIFT), Code::KeyU)))); // -- Help -- @@ -286,7 +286,7 @@ pub fn build_menu_bar(locale: UiLocale) -> (Menu, MenuRegistry) { let _ = menu.append(&file_menu); let _ = menu.append(&edit_menu); let _ = menu.append(&view_menu); - let _ = menu.append(&window_menu); + let _ = menu.append(&blog_menu); let _ = menu.append(&help_menu); (menu, reg) diff --git a/crates/bds-ui/src/views/post_editor.rs b/crates/bds-ui/src/views/post_editor.rs index 67aa6fd..02eebbb 100644 --- a/crates/bds-ui/src/views/post_editor.rs +++ b/crates/bds-ui/src/views/post_editor.rs @@ -1,7 +1,7 @@ use std::cell::RefCell; use std::collections::HashMap; -use iced::widget::{button, column, container, row, scrollable, text, text_input, Space}; +use iced::widget::{button, column, container, row, scrollable, text, text_input, Column, Space}; use iced::widget::text::{Shaping, Wrapping}; use iced::{Color, Element, Length, Theme}; @@ -32,6 +32,13 @@ pub struct TranslationDraft { pub is_dirty: bool, } +/// Resolved post link for display in metadata. +#[derive(Debug, Clone)] +pub struct ResolvedPostLink { + pub post_id: String, + pub title: String, +} + /// State for an open post editor. pub struct PostEditorState { pub post_id: String, @@ -65,6 +72,10 @@ pub struct PostEditorState { pub saved_canonical: Option, /// Translation drafts keyed by language code. pub translation_drafts: HashMap, + /// Outgoing links from this post to other posts. + pub outlinks: Vec, + /// Incoming links from other posts to this post. + pub backlinks: Vec, } impl std::fmt::Debug for PostEditorState { @@ -105,6 +116,8 @@ impl Clone for PostEditorState { blog_languages: self.blog_languages.clone(), saved_canonical: self.saved_canonical.clone(), translation_drafts: self.translation_drafts.clone(), + outlinks: self.outlinks.clone(), + backlinks: self.backlinks.clone(), } } } @@ -114,6 +127,8 @@ impl PostEditorState { post: &Post, blog_languages: &[String], translations: &[PostTranslation], + outlinks: Vec, + backlinks: Vec, ) -> Self { let title = post.title.clone(); let content = post.content.clone().unwrap_or_default(); @@ -155,6 +170,8 @@ impl PostEditorState { blog_languages: blog_languages.to_vec(), saved_canonical: None, translation_drafts, + outlinks, + backlinks, title, } } @@ -218,13 +235,14 @@ impl PostEditorState { } /// Build the translation flags list for the view. + /// Flags are driven by the post's actual translations, not blog-level languages. pub fn translation_flags(&self) -> Vec { - if self.blog_languages.len() <= 1 { + if self.translation_drafts.is_empty() { return Vec::new(); } let mut flags = Vec::new(); - // Canonical language first + // Canonical language first (always shown when translations exist) let canon = &self.canonical_language; let canon_locale = i18n::normalize_language(canon); flags.push(TranslationFlag { @@ -234,23 +252,20 @@ impl PostEditorState { is_active: self.active_language == *canon, }); - // Each other blog language - for lang in &self.blog_languages { - if lang == canon { - continue; - } + // Each existing translation for this post + let mut langs: Vec<&String> = self.translation_drafts.keys().collect(); + langs.sort(); + for lang in langs { let locale = i18n::normalize_language(lang); - let status = self.translation_drafts.get(lang) - .map(|d| match d.status { - PostStatus::Published => "published", - _ => "draft", - }) - .unwrap_or("missing"); + let status = match self.translation_drafts[lang].status { + PostStatus::Published => "published", + _ => "draft", + }; flags.push(TranslationFlag { language: lang.clone(), flag_emoji: locale.flag_emoji().to_string(), status: status.to_string(), - is_active: self.active_language == *lang, + is_active: self.active_language == **lang, }); } @@ -427,7 +442,44 @@ pub fn view<'a>( |cat| Message::PostEditor(PostEditorMsg::RemoveCategory(cat)), ); - column![meta_row1, meta_row2, tags_section, categories_section] + // Post links sections + let outlinks_section: Element<'a, Message> = if state.outlinks.is_empty() { + Space::new(0, 0).into() + } else { + let mut items: Vec> = vec![ + text(t(locale, "editor.outlinks")).size(12).color(inputs::LABEL_COLOR).shaping(Shaping::Advanced).into(), + ]; + for link in &state.outlinks { + items.push( + text(format!("\u{2192} {}", link.title)) + .size(12) + .shaping(Shaping::Advanced) + .color(Color::from_rgb(0.55, 0.70, 0.90)) + .into() + ); + } + Column::with_children(items).spacing(2).into() + }; + + let backlinks_section: Element<'a, Message> = if state.backlinks.is_empty() { + Space::new(0, 0).into() + } else { + let mut items: Vec> = vec![ + text(t(locale, "editor.backlinks")).size(12).color(inputs::LABEL_COLOR).shaping(Shaping::Advanced).into(), + ]; + for link in &state.backlinks { + items.push( + text(format!("\u{2190} {}", link.title)) + .size(12) + .shaping(Shaping::Advanced) + .color(Color::from_rgb(0.55, 0.70, 0.90)) + .into() + ); + } + Column::with_children(items).spacing(2).into() + }; + + column![meta_row1, meta_row2, tags_section, categories_section, outlinks_section, backlinks_section] .spacing(8) .width(Length::Fill) .into() diff --git a/crates/bds-ui/src/views/script_editor.rs b/crates/bds-ui/src/views/script_editor.rs index d3227c3..ccb8dbb 100644 --- a/crates/bds-ui/src/views/script_editor.rs +++ b/crates/bds-ui/src/views/script_editor.rs @@ -16,6 +16,7 @@ pub struct ScriptEditorState { pub script_id: String, pub title: String, pub slug: String, + pub file_path: String, pub kind: ScriptKind, pub entrypoint: String, pub enabled: bool, @@ -45,6 +46,7 @@ impl Clone for ScriptEditorState { script_id: self.script_id.clone(), title: self.title.clone(), slug: self.slug.clone(), + file_path: self.file_path.clone(), kind: self.kind.clone(), entrypoint: self.entrypoint.clone(), enabled: self.enabled, @@ -69,6 +71,7 @@ impl ScriptEditorState { script_id: script.id.clone(), title: script.title.clone(), slug: script.slug.clone(), + file_path: script.file_path.clone(), kind: script.kind.clone(), entrypoint: script.entrypoint.clone(), enabled: script.enabled, @@ -192,14 +195,15 @@ pub fn view<'a>( .spacing(16) .width(Length::Fill); - // Content editor (CodeEditor with Lua syntax highlighting) + // Content editor (CodeEditor with syntax highlighting based on file extension) + let syntax_ext = if state.file_path.ends_with(".py") { "py" } else { "lua" }; let content_section: Element<'a, Message> = column![ inputs::section_header(&t(locale, "editor.content")), Element::from( CodeEditor::new( &state.editor_buffer, highlighter(), - "lua", + syntax_ext, ) .on_change(|msg| match msg { EditorMessage::ContentChanged(s) => Message::ScriptEditor(ScriptEditorMsg::ContentChanged(s)), @@ -209,6 +213,7 @@ pub fn view<'a>( ] .spacing(8) .width(Length::Fill) + .height(Length::Fill) .into(); // Validation error diff --git a/crates/bds-ui/src/views/settings_view.rs b/crates/bds-ui/src/views/settings_view.rs index 97cc533..6aa43e8 100644 --- a/crates/bds-ui/src/views/settings_view.rs +++ b/crates/bds-ui/src/views/settings_view.rs @@ -1,4 +1,5 @@ -use iced::widget::{button, column, container, row, scrollable, text, text_input}; +use iced::widget::{button, column, container, row, scrollable, text, text_editor, text_input}; +use iced::widget::text::Shaping; use iced::{Alignment, Color, Element, Length, Theme}; use bds_core::i18n::UiLocale; @@ -49,13 +50,12 @@ impl SettingsSection { } /// State for the settings view. -#[derive(Debug, Clone)] pub struct SettingsViewState { pub search_query: String, pub collapsed: Vec, // Project pub project_name: String, - pub project_description: String, + pub project_description: text_editor::Content, pub data_path: String, pub public_url: String, pub default_author: String, @@ -72,7 +72,40 @@ pub struct SettingsViewState { pub ssh_remote_path: String, // AI pub offline_mode: bool, - pub system_prompt: String, + pub system_prompt: text_editor::Content, +} + +impl std::fmt::Debug for SettingsViewState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SettingsViewState") + .field("project_name", &self.project_name) + .finish_non_exhaustive() + } +} + +impl Clone for SettingsViewState { + fn clone(&self) -> Self { + Self { + search_query: self.search_query.clone(), + collapsed: self.collapsed.clone(), + project_name: self.project_name.clone(), + project_description: text_editor::Content::with_text(&self.project_description.text()), + data_path: self.data_path.clone(), + public_url: self.public_url.clone(), + default_author: self.default_author.clone(), + max_posts_per_page: self.max_posts_per_page.clone(), + default_mode: self.default_mode.clone(), + diff_view_style: self.diff_view_style.clone(), + wrap_long_lines: self.wrap_long_lines, + hide_unchanged_regions: self.hide_unchanged_regions, + ssh_mode: self.ssh_mode.clone(), + ssh_host: self.ssh_host.clone(), + ssh_username: self.ssh_username.clone(), + ssh_remote_path: self.ssh_remote_path.clone(), + offline_mode: self.offline_mode, + system_prompt: text_editor::Content::with_text(&self.system_prompt.text()), + } + } } impl Default for SettingsViewState { @@ -81,7 +114,7 @@ impl Default for SettingsViewState { search_query: String::new(), collapsed: Vec::new(), project_name: String::new(), - project_description: String::new(), + project_description: text_editor::Content::new(), data_path: String::new(), public_url: String::new(), default_author: String::new(), @@ -95,7 +128,7 @@ impl Default for SettingsViewState { ssh_username: String::new(), ssh_remote_path: String::new(), offline_mode: false, - system_prompt: String::new(), + system_prompt: text_editor::Content::new(), } } } @@ -107,7 +140,7 @@ pub enum SettingsMsg { ToggleSection(SettingsSection), // Project ProjectNameChanged(String), - ProjectDescriptionChanged(String), + ProjectDescriptionAction(text_editor::Action), DataPathChanged(String), BrowseDataPath, ResetDataPath, @@ -130,7 +163,7 @@ pub enum SettingsMsg { ClearPublishing, // AI OfflineModeChanged(bool), - SystemPromptChanged(String), + SystemPromptAction(text_editor::Action), SaveSystemPrompt, ResetSystemPrompt, // Data maintenance @@ -228,12 +261,14 @@ fn section_project<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Elemen &state.project_name, |s| Message::Settings(SettingsMsg::ProjectNameChanged(s)), ); - let desc = inputs::labeled_input( - &t(locale, "settings.projectDescription"), - "", - &state.project_description, - |s| Message::Settings(SettingsMsg::ProjectDescriptionChanged(s)), - ); + let desc = column![ + text(t(locale, "settings.projectDescription")).size(12).color(inputs::LABEL_COLOR).shaping(Shaping::Advanced), + text_editor(&state.project_description) + .on_action(|a| Message::Settings(SettingsMsg::ProjectDescriptionAction(a))) + .height(Length::Fixed(80.0)) + .size(14), + ] + .spacing(4); let data_path = row![ inputs::labeled_input( &t(locale, "settings.dataPath"), @@ -316,12 +351,14 @@ fn section_ai<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Element<'a, state.offline_mode, |b| Message::Settings(SettingsMsg::OfflineModeChanged(b)), ); - let prompt = inputs::labeled_input( - &t(locale, "settings.systemPrompt"), - "", - &state.system_prompt, - |s| Message::Settings(SettingsMsg::SystemPromptChanged(s)), - ); + let prompt = column![ + text(t(locale, "settings.systemPrompt")).size(12).color(inputs::LABEL_COLOR).shaping(Shaping::Advanced), + text_editor(&state.system_prompt) + .on_action(|a| Message::Settings(SettingsMsg::SystemPromptAction(a))) + .height(Length::Fixed(200.0)) + .size(14), + ] + .spacing(4); let btns = row![ button(text(t(locale, "common.save")).size(13)) .on_press(Message::Settings(SettingsMsg::SaveSystemPrompt)) diff --git a/crates/bds-ui/src/views/template_editor.rs b/crates/bds-ui/src/views/template_editor.rs index 28646a4..a44ab7a 100644 --- a/crates/bds-ui/src/views/template_editor.rs +++ b/crates/bds-ui/src/views/template_editor.rs @@ -187,6 +187,7 @@ pub fn view<'a>( ] .spacing(8) .width(Length::Fill) + .height(Length::Fill) .into(); // Validation error diff --git a/locales/ui/de.json b/locales/ui/de.json index ef7d07d..d22fd91 100644 --- a/locales/ui/de.json +++ b/locales/ui/de.json @@ -2,7 +2,7 @@ "menu.group.file": "Datei", "menu.group.edit": "Bearbeiten", "menu.group.view": "Ansicht", - "menu.group.window": "Fenster", + "menu.group.blog": "Blog", "menu.group.help": "Hilfe", "menu.item.newPost": "Neuer Beitrag", "menu.item.importMedia": "Medien importieren...", @@ -198,6 +198,8 @@ "editor.tagsPlaceholder": "Tag hinzufügen...", "editor.categories": "Kategorien", "editor.categoriesPlaceholder": "Kategorie hinzufügen...", + "editor.outlinks": "Ausgehende Links", + "editor.backlinks": "Eingehende Links", "editor.untitled": "Ohne Titel", "editor.createdAt": "Erstellt", "editor.updatedAt": "Aktualisiert", diff --git a/locales/ui/en.json b/locales/ui/en.json index b9f81d0..a3814f9 100644 --- a/locales/ui/en.json +++ b/locales/ui/en.json @@ -2,7 +2,7 @@ "menu.group.file": "File", "menu.group.edit": "Edit", "menu.group.view": "View", - "menu.group.window": "Window", + "menu.group.blog": "Blog", "menu.group.help": "Help", "menu.item.newPost": "New Post", "menu.item.importMedia": "Import Media...", @@ -198,6 +198,8 @@ "editor.tagsPlaceholder": "Add tag...", "editor.categories": "Categories", "editor.categoriesPlaceholder": "Add category...", + "editor.outlinks": "Outgoing Links", + "editor.backlinks": "Incoming Links", "editor.untitled": "Untitled", "editor.createdAt": "Created", "editor.updatedAt": "Updated", diff --git a/locales/ui/es.json b/locales/ui/es.json index ee9e3d7..5516883 100644 --- a/locales/ui/es.json +++ b/locales/ui/es.json @@ -2,7 +2,7 @@ "menu.group.file": "Archivo", "menu.group.edit": "Editar", "menu.group.view": "Ver", - "menu.group.window": "Ventana", + "menu.group.blog": "Blog", "menu.group.help": "Ayuda", "menu.item.newPost": "Nueva entrada", "menu.item.importMedia": "Importar medios...", @@ -198,6 +198,8 @@ "editor.tagsPlaceholder": "Añadir etiqueta...", "editor.categories": "Categorías", "editor.categoriesPlaceholder": "Añadir categoría...", + "editor.outlinks": "Enlaces salientes", + "editor.backlinks": "Enlaces entrantes", "editor.untitled": "Sin título", "editor.createdAt": "Creado", "editor.updatedAt": "Actualizado", diff --git a/locales/ui/fr.json b/locales/ui/fr.json index 4c7e980..fc92651 100644 --- a/locales/ui/fr.json +++ b/locales/ui/fr.json @@ -2,7 +2,7 @@ "menu.group.file": "Fichier", "menu.group.edit": "Édition", "menu.group.view": "Affichage", - "menu.group.window": "Fenêtre", + "menu.group.blog": "Blog", "menu.group.help": "Aide", "menu.item.newPost": "Nouvel article", "menu.item.importMedia": "Importer des médias...", @@ -198,6 +198,8 @@ "editor.tagsPlaceholder": "Ajouter un tag...", "editor.categories": "Catégories", "editor.categoriesPlaceholder": "Ajouter une catégorie...", + "editor.outlinks": "Liens sortants", + "editor.backlinks": "Liens entrants", "editor.untitled": "Sans titre", "editor.createdAt": "Créé", "editor.updatedAt": "Mis à jour", diff --git a/locales/ui/it.json b/locales/ui/it.json index ed8eaa6..a000d9c 100644 --- a/locales/ui/it.json +++ b/locales/ui/it.json @@ -2,7 +2,7 @@ "menu.group.file": "Archivio", "menu.group.edit": "Modifica", "menu.group.view": "Vista", - "menu.group.window": "Finestra", + "menu.group.blog": "Blog", "menu.group.help": "Aiuto", "menu.item.newPost": "Nuovo post", "menu.item.importMedia": "Importa media...", @@ -198,6 +198,8 @@ "editor.tagsPlaceholder": "Aggiungi tag...", "editor.categories": "Categorie", "editor.categoriesPlaceholder": "Aggiungi categoria...", + "editor.outlinks": "Link in uscita", + "editor.backlinks": "Link in entrata", "editor.untitled": "Senza titolo", "editor.createdAt": "Creato", "editor.updatedAt": "Aggiornato",