fix: another round of firxes for M3
This commit is contained in:
@@ -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<usize> {
|
||||
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.
|
||||
|
||||
@@ -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<ResolvedPostLink>, Vec<ResolvedPostLink>) {
|
||||
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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<TranslationDraft>,
|
||||
/// Translation drafts keyed by language code.
|
||||
pub translation_drafts: HashMap<String, TranslationDraft>,
|
||||
/// Outgoing links from this post to other posts.
|
||||
pub outlinks: Vec<ResolvedPostLink>,
|
||||
/// Incoming links from other posts to this post.
|
||||
pub backlinks: Vec<ResolvedPostLink>,
|
||||
}
|
||||
|
||||
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<ResolvedPostLink>,
|
||||
backlinks: Vec<ResolvedPostLink>,
|
||||
) -> 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<TranslationFlag> {
|
||||
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<Element<'a, Message>> = 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<Element<'a, Message>> = 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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<SettingsSection>,
|
||||
// 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))
|
||||
|
||||
@@ -187,6 +187,7 @@ pub fn view<'a>(
|
||||
]
|
||||
.spacing(8)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
.into();
|
||||
|
||||
// Validation error
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user