fix: another round of firxes for M3

This commit is contained in:
2026-04-05 19:04:49 +02:00
parent 95cacdcd0e
commit 17c4c79fd6
12 changed files with 321 additions and 80 deletions

View File

@@ -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.

View File

@@ -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 {

View File

@@ -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)

View File

@@ -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()

View File

@@ -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

View File

@@ -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))

View File

@@ -187,6 +187,7 @@ pub fn view<'a>(
]
.spacing(8)
.width(Length::Fill)
.height(Length::Fill)
.into();
// Validation error

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",