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)
|
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 ---
|
// --- Internal helpers ---
|
||||||
|
|
||||||
/// Parse inter-post links from markdown content.
|
/// 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::state::toast::{Toast, ToastLevel};
|
||||||
use crate::views::{
|
use crate::views::{
|
||||||
modal, workspace,
|
modal, workspace,
|
||||||
post_editor::{PostEditorState, PostEditorMsg},
|
post_editor::{PostEditorState, PostEditorMsg, ResolvedPostLink},
|
||||||
media_editor::{MediaEditorState, MediaEditorMsg},
|
media_editor::{MediaEditorState, MediaEditorMsg},
|
||||||
template_editor::{TemplateEditorState, TemplateEditorMsg},
|
template_editor::{TemplateEditorState, TemplateEditorMsg},
|
||||||
script_editor::{ScriptEditorState, ScriptEditorMsg},
|
script_editor::{ScriptEditorState, ScriptEditorMsg},
|
||||||
@@ -1139,7 +1139,8 @@ impl BdsApp {
|
|||||||
db.conn(), &post.id,
|
db.conn(), &post.id,
|
||||||
).ok())
|
).ok())
|
||||||
.unwrap_or_default();
|
.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);
|
self.post_editors.insert(post.id.clone(), state);
|
||||||
}
|
}
|
||||||
Err(e) => self.notify(ToastLevel::Error, &e),
|
Err(e) => self.notify(ToastLevel::Error, &e),
|
||||||
@@ -1933,7 +1934,9 @@ impl BdsApp {
|
|||||||
let mut state = SettingsViewState::default();
|
let mut state = SettingsViewState::default();
|
||||||
if let Some(ref project) = self.active_project {
|
if let Some(ref project) = self.active_project {
|
||||||
state.project_name = project.name.clone();
|
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();
|
state.data_path = project.data_path.clone().unwrap_or_default();
|
||||||
}
|
}
|
||||||
if let Some(ref data_dir) = self.data_dir {
|
if let Some(ref data_dir) = self.data_dir {
|
||||||
@@ -1963,7 +1966,9 @@ impl BdsApp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
SettingsMsg::ProjectNameChanged(s) => { state.project_name = s; }
|
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::DataPathChanged(s) => { state.data_path = s; }
|
||||||
SettingsMsg::BrowseDataPath => {
|
SettingsMsg::BrowseDataPath => {
|
||||||
return crate::platform::dialog::pick_folder(t(self.ui_locale, "dialog.selectFolder"));
|
return crate::platform::dialog::pick_folder(t(self.ui_locale, "dialog.selectFolder"));
|
||||||
@@ -1996,14 +2001,39 @@ impl BdsApp {
|
|||||||
state.offline_mode = b;
|
state.offline_mode = b;
|
||||||
return Task::done(Message::SetOfflineMode(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::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::RebuildPosts => { return Task::done(Message::RebuildDatabase); }
|
||||||
SettingsMsg::RebuildMedia => { return Task::done(Message::RebuildDatabase); }
|
SettingsMsg::RebuildMedia => { return Task::done(Message::RebuildDatabase); }
|
||||||
SettingsMsg::RebuildScripts => { return Task::done(Message::RebuildDatabase); }
|
SettingsMsg::RebuildScripts => { return Task::done(Message::RebuildDatabase); }
|
||||||
SettingsMsg::RebuildTemplates => { 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 => {
|
SettingsMsg::RegenerateThumbnails => {
|
||||||
self.notify(ToastLevel::Info, &t(self.ui_locale, "settings.regeneratingThumbnails"));
|
self.notify(ToastLevel::Info, &t(self.ui_locale, "settings.regeneratingThumbnails"));
|
||||||
}
|
}
|
||||||
@@ -2016,6 +2046,38 @@ impl BdsApp {
|
|||||||
Task::none()
|
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.
|
/// Load editor state when a tab is opened for an entity.
|
||||||
fn load_editor_for_tab(&mut self, tab: &Tab) {
|
fn load_editor_for_tab(&mut self, tab: &Tab) {
|
||||||
let Some(ref db) = self.db else { return };
|
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(
|
let translations = bds_core::db::queries::post_translation::list_post_translations_by_post(
|
||||||
db.conn(), &post.id,
|
db.conn(), &post.id,
|
||||||
).unwrap_or_default();
|
).unwrap_or_default();
|
||||||
|
let (outlinks, backlinks) = self.load_post_links(&post.id);
|
||||||
self.post_editors.insert(
|
self.post_editors.insert(
|
||||||
post.id.clone(),
|
post.id.clone(),
|
||||||
PostEditorState::from_post(&post, &self.blog_languages, &translations),
|
PostEditorState::from_post(&post, &self.blog_languages, &translations, outlinks, backlinks),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -2072,13 +2135,17 @@ impl BdsApp {
|
|||||||
if !self.template_editors.contains_key(&tab.id) {
|
if !self.template_editors.contains_key(&tab.id) {
|
||||||
match bds_core::db::queries::template::get_template_by_id(db.conn(), &tab.id) {
|
match bds_core::db::queries::template::get_template_by_id(db.conn(), &tab.id) {
|
||||||
Ok(mut template) => {
|
Ok(mut template) => {
|
||||||
// Published templates: read content from file
|
// Published templates: read content from file, strip frontmatter
|
||||||
if template.content.is_none() {
|
if template.content.is_none() {
|
||||||
if let Some(ref data_dir) = self.data_dir {
|
if let Some(ref data_dir) = self.data_dir {
|
||||||
let rel = bds_core::util::paths::template_file_path(&template.slug);
|
let rel = bds_core::util::paths::template_file_path(&template.slug);
|
||||||
let path = data_dir.join(&rel);
|
let path = data_dir.join(&rel);
|
||||||
if let Ok(body) = std::fs::read_to_string(&path) {
|
if let Ok(raw) = std::fs::read_to_string(&path) {
|
||||||
template.content = Some(body);
|
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) {
|
if !self.script_editors.contains_key(&tab.id) {
|
||||||
match bds_core::db::queries::script::get_script_by_id(db.conn(), &tab.id) {
|
match bds_core::db::queries::script::get_script_by_id(db.conn(), &tab.id) {
|
||||||
Ok(mut script) => {
|
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 script.content.is_none() {
|
||||||
if let Some(ref data_dir) = self.data_dir {
|
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(&script.file_path);
|
||||||
let path = data_dir.join(&rel);
|
if let Ok(raw) = std::fs::read_to_string(&path) {
|
||||||
if let Ok(body) = std::fs::read_to_string(&path) {
|
if let Ok((_fm, body)) =
|
||||||
script.content = Some(body);
|
bds_core::util::frontmatter::read_script_file(&raw)
|
||||||
|
{
|
||||||
|
script.content = Some(body);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2114,9 +2184,16 @@ impl BdsApp {
|
|||||||
}
|
}
|
||||||
TabType::Tags => {
|
TabType::Tags => {
|
||||||
if self.tags_view_state.is_none() {
|
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(
|
let tags = bds_core::db::queries::tag::list_tags_by_project(
|
||||||
db.conn(),
|
db.conn(),
|
||||||
self.active_project.as_ref().map(|p| p.id.as_str()).unwrap_or(""),
|
project_id,
|
||||||
).unwrap_or_default();
|
).unwrap_or_default();
|
||||||
self.tags_view_state = Some(TagsViewState::new(tags));
|
self.tags_view_state = Some(TagsViewState::new(tags));
|
||||||
}
|
}
|
||||||
@@ -2126,7 +2203,9 @@ impl BdsApp {
|
|||||||
let mut state = SettingsViewState::default();
|
let mut state = SettingsViewState::default();
|
||||||
if let Some(ref project) = self.active_project {
|
if let Some(ref project) = self.active_project {
|
||||||
state.project_name = project.name.clone();
|
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();
|
state.data_path = project.data_path.clone().unwrap_or_default();
|
||||||
}
|
}
|
||||||
if let Some(ref data_dir) = self.data_dir {
|
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::separator());
|
||||||
let _ = view_menu.append(&PredefinedMenuItem::fullscreen(None));
|
let _ = view_menu.append(&PredefinedMenuItem::fullscreen(None));
|
||||||
|
|
||||||
// -- Window --
|
// -- Blog --
|
||||||
let window_menu = Submenu::new(translate(locale, "menu.group.window"), true);
|
let blog_menu = Submenu::new(translate(locale, "menu.group.blog"), true);
|
||||||
let _ = window_menu.append(&item(&mut reg, MenuAction::PublishSelected, locale,
|
let _ = blog_menu.append(&item(&mut reg, MenuAction::PublishSelected, locale,
|
||||||
Some(Accelerator::new(Some(CMD_OR_CTRL | Modifiers::SHIFT), Code::KeyP))));
|
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))));
|
Some(Accelerator::new(Some(CMD_OR_CTRL | Modifiers::SHIFT), Code::KeyV))));
|
||||||
let _ = window_menu.append(&PredefinedMenuItem::separator());
|
let _ = blog_menu.append(&PredefinedMenuItem::separator());
|
||||||
let _ = window_menu.append(&item(&mut reg, MenuAction::EditMenu, locale, None));
|
let _ = blog_menu.append(&item(&mut reg, MenuAction::EditMenu, locale, None));
|
||||||
let _ = window_menu.append(&PredefinedMenuItem::separator());
|
let _ = blog_menu.append(&PredefinedMenuItem::separator());
|
||||||
let _ = window_menu.append(&item(&mut reg, MenuAction::RebuildDatabase, locale, None));
|
let _ = blog_menu.append(&item(&mut reg, MenuAction::RebuildDatabase, locale, None));
|
||||||
let _ = window_menu.append(&item(&mut reg, MenuAction::ReindexText, locale, None));
|
let _ = blog_menu.append(&item(&mut reg, MenuAction::ReindexText, locale, None));
|
||||||
let _ = window_menu.append(&item(&mut reg, MenuAction::MetadataDiff, locale, None));
|
let _ = blog_menu.append(&item(&mut reg, MenuAction::MetadataDiff, locale, None));
|
||||||
let _ = window_menu.append(&PredefinedMenuItem::separator());
|
let _ = blog_menu.append(&PredefinedMenuItem::separator());
|
||||||
let _ = window_menu.append(&item(&mut reg, MenuAction::RegenerateCalendar, locale, None));
|
let _ = blog_menu.append(&item(&mut reg, MenuAction::RegenerateCalendar, locale, None));
|
||||||
let _ = window_menu.append(&item(&mut reg, MenuAction::ValidateTranslations, locale, None));
|
let _ = blog_menu.append(&item(&mut reg, MenuAction::ValidateTranslations, locale, None));
|
||||||
let _ = window_menu.append(&item(&mut reg, MenuAction::FillMissingTranslations, locale, None));
|
let _ = blog_menu.append(&item(&mut reg, MenuAction::FillMissingTranslations, locale, None));
|
||||||
let _ = window_menu.append(&PredefinedMenuItem::separator());
|
let _ = blog_menu.append(&PredefinedMenuItem::separator());
|
||||||
let _ = window_menu.append(&item(&mut reg, MenuAction::GenerateSitemap, locale,
|
let _ = blog_menu.append(&item(&mut reg, MenuAction::GenerateSitemap, locale,
|
||||||
Some(Accelerator::new(Some(CMD_OR_CTRL), Code::KeyR))));
|
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))));
|
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))));
|
Some(Accelerator::new(Some(CMD_OR_CTRL | Modifiers::SHIFT), Code::KeyU))));
|
||||||
|
|
||||||
// -- Help --
|
// -- Help --
|
||||||
@@ -286,7 +286,7 @@ pub fn build_menu_bar(locale: UiLocale) -> (Menu, MenuRegistry) {
|
|||||||
let _ = menu.append(&file_menu);
|
let _ = menu.append(&file_menu);
|
||||||
let _ = menu.append(&edit_menu);
|
let _ = menu.append(&edit_menu);
|
||||||
let _ = menu.append(&view_menu);
|
let _ = menu.append(&view_menu);
|
||||||
let _ = menu.append(&window_menu);
|
let _ = menu.append(&blog_menu);
|
||||||
let _ = menu.append(&help_menu);
|
let _ = menu.append(&help_menu);
|
||||||
|
|
||||||
(menu, reg)
|
(menu, reg)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::collections::HashMap;
|
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::widget::text::{Shaping, Wrapping};
|
||||||
use iced::{Color, Element, Length, Theme};
|
use iced::{Color, Element, Length, Theme};
|
||||||
|
|
||||||
@@ -32,6 +32,13 @@ pub struct TranslationDraft {
|
|||||||
pub is_dirty: bool,
|
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.
|
/// State for an open post editor.
|
||||||
pub struct PostEditorState {
|
pub struct PostEditorState {
|
||||||
pub post_id: String,
|
pub post_id: String,
|
||||||
@@ -65,6 +72,10 @@ pub struct PostEditorState {
|
|||||||
pub saved_canonical: Option<TranslationDraft>,
|
pub saved_canonical: Option<TranslationDraft>,
|
||||||
/// Translation drafts keyed by language code.
|
/// Translation drafts keyed by language code.
|
||||||
pub translation_drafts: HashMap<String, TranslationDraft>,
|
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 {
|
impl std::fmt::Debug for PostEditorState {
|
||||||
@@ -105,6 +116,8 @@ impl Clone for PostEditorState {
|
|||||||
blog_languages: self.blog_languages.clone(),
|
blog_languages: self.blog_languages.clone(),
|
||||||
saved_canonical: self.saved_canonical.clone(),
|
saved_canonical: self.saved_canonical.clone(),
|
||||||
translation_drafts: self.translation_drafts.clone(),
|
translation_drafts: self.translation_drafts.clone(),
|
||||||
|
outlinks: self.outlinks.clone(),
|
||||||
|
backlinks: self.backlinks.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -114,6 +127,8 @@ impl PostEditorState {
|
|||||||
post: &Post,
|
post: &Post,
|
||||||
blog_languages: &[String],
|
blog_languages: &[String],
|
||||||
translations: &[PostTranslation],
|
translations: &[PostTranslation],
|
||||||
|
outlinks: Vec<ResolvedPostLink>,
|
||||||
|
backlinks: Vec<ResolvedPostLink>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let title = post.title.clone();
|
let title = post.title.clone();
|
||||||
let content = post.content.clone().unwrap_or_default();
|
let content = post.content.clone().unwrap_or_default();
|
||||||
@@ -155,6 +170,8 @@ impl PostEditorState {
|
|||||||
blog_languages: blog_languages.to_vec(),
|
blog_languages: blog_languages.to_vec(),
|
||||||
saved_canonical: None,
|
saved_canonical: None,
|
||||||
translation_drafts,
|
translation_drafts,
|
||||||
|
outlinks,
|
||||||
|
backlinks,
|
||||||
title,
|
title,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -218,13 +235,14 @@ impl PostEditorState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Build the translation flags list for the view.
|
/// 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> {
|
pub fn translation_flags(&self) -> Vec<TranslationFlag> {
|
||||||
if self.blog_languages.len() <= 1 {
|
if self.translation_drafts.is_empty() {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
let mut flags = 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 = &self.canonical_language;
|
||||||
let canon_locale = i18n::normalize_language(canon);
|
let canon_locale = i18n::normalize_language(canon);
|
||||||
flags.push(TranslationFlag {
|
flags.push(TranslationFlag {
|
||||||
@@ -234,23 +252,20 @@ impl PostEditorState {
|
|||||||
is_active: self.active_language == *canon,
|
is_active: self.active_language == *canon,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Each other blog language
|
// Each existing translation for this post
|
||||||
for lang in &self.blog_languages {
|
let mut langs: Vec<&String> = self.translation_drafts.keys().collect();
|
||||||
if lang == canon {
|
langs.sort();
|
||||||
continue;
|
for lang in langs {
|
||||||
}
|
|
||||||
let locale = i18n::normalize_language(lang);
|
let locale = i18n::normalize_language(lang);
|
||||||
let status = self.translation_drafts.get(lang)
|
let status = match self.translation_drafts[lang].status {
|
||||||
.map(|d| match d.status {
|
PostStatus::Published => "published",
|
||||||
PostStatus::Published => "published",
|
_ => "draft",
|
||||||
_ => "draft",
|
};
|
||||||
})
|
|
||||||
.unwrap_or("missing");
|
|
||||||
flags.push(TranslationFlag {
|
flags.push(TranslationFlag {
|
||||||
language: lang.clone(),
|
language: lang.clone(),
|
||||||
flag_emoji: locale.flag_emoji().to_string(),
|
flag_emoji: locale.flag_emoji().to_string(),
|
||||||
status: status.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)),
|
|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)
|
.spacing(8)
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
.into()
|
.into()
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ pub struct ScriptEditorState {
|
|||||||
pub script_id: String,
|
pub script_id: String,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
pub slug: String,
|
pub slug: String,
|
||||||
|
pub file_path: String,
|
||||||
pub kind: ScriptKind,
|
pub kind: ScriptKind,
|
||||||
pub entrypoint: String,
|
pub entrypoint: String,
|
||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
@@ -45,6 +46,7 @@ impl Clone for ScriptEditorState {
|
|||||||
script_id: self.script_id.clone(),
|
script_id: self.script_id.clone(),
|
||||||
title: self.title.clone(),
|
title: self.title.clone(),
|
||||||
slug: self.slug.clone(),
|
slug: self.slug.clone(),
|
||||||
|
file_path: self.file_path.clone(),
|
||||||
kind: self.kind.clone(),
|
kind: self.kind.clone(),
|
||||||
entrypoint: self.entrypoint.clone(),
|
entrypoint: self.entrypoint.clone(),
|
||||||
enabled: self.enabled,
|
enabled: self.enabled,
|
||||||
@@ -69,6 +71,7 @@ impl ScriptEditorState {
|
|||||||
script_id: script.id.clone(),
|
script_id: script.id.clone(),
|
||||||
title: script.title.clone(),
|
title: script.title.clone(),
|
||||||
slug: script.slug.clone(),
|
slug: script.slug.clone(),
|
||||||
|
file_path: script.file_path.clone(),
|
||||||
kind: script.kind.clone(),
|
kind: script.kind.clone(),
|
||||||
entrypoint: script.entrypoint.clone(),
|
entrypoint: script.entrypoint.clone(),
|
||||||
enabled: script.enabled,
|
enabled: script.enabled,
|
||||||
@@ -192,14 +195,15 @@ pub fn view<'a>(
|
|||||||
.spacing(16)
|
.spacing(16)
|
||||||
.width(Length::Fill);
|
.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![
|
let content_section: Element<'a, Message> = column![
|
||||||
inputs::section_header(&t(locale, "editor.content")),
|
inputs::section_header(&t(locale, "editor.content")),
|
||||||
Element::from(
|
Element::from(
|
||||||
CodeEditor::new(
|
CodeEditor::new(
|
||||||
&state.editor_buffer,
|
&state.editor_buffer,
|
||||||
highlighter(),
|
highlighter(),
|
||||||
"lua",
|
syntax_ext,
|
||||||
)
|
)
|
||||||
.on_change(|msg| match msg {
|
.on_change(|msg| match msg {
|
||||||
EditorMessage::ContentChanged(s) => Message::ScriptEditor(ScriptEditorMsg::ContentChanged(s)),
|
EditorMessage::ContentChanged(s) => Message::ScriptEditor(ScriptEditorMsg::ContentChanged(s)),
|
||||||
@@ -209,6 +213,7 @@ pub fn view<'a>(
|
|||||||
]
|
]
|
||||||
.spacing(8)
|
.spacing(8)
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fill)
|
||||||
.into();
|
.into();
|
||||||
|
|
||||||
// Validation error
|
// 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 iced::{Alignment, Color, Element, Length, Theme};
|
||||||
|
|
||||||
use bds_core::i18n::UiLocale;
|
use bds_core::i18n::UiLocale;
|
||||||
@@ -49,13 +50,12 @@ impl SettingsSection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// State for the settings view.
|
/// State for the settings view.
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct SettingsViewState {
|
pub struct SettingsViewState {
|
||||||
pub search_query: String,
|
pub search_query: String,
|
||||||
pub collapsed: Vec<SettingsSection>,
|
pub collapsed: Vec<SettingsSection>,
|
||||||
// Project
|
// Project
|
||||||
pub project_name: String,
|
pub project_name: String,
|
||||||
pub project_description: String,
|
pub project_description: text_editor::Content,
|
||||||
pub data_path: String,
|
pub data_path: String,
|
||||||
pub public_url: String,
|
pub public_url: String,
|
||||||
pub default_author: String,
|
pub default_author: String,
|
||||||
@@ -72,7 +72,40 @@ pub struct SettingsViewState {
|
|||||||
pub ssh_remote_path: String,
|
pub ssh_remote_path: String,
|
||||||
// AI
|
// AI
|
||||||
pub offline_mode: bool,
|
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 {
|
impl Default for SettingsViewState {
|
||||||
@@ -81,7 +114,7 @@ impl Default for SettingsViewState {
|
|||||||
search_query: String::new(),
|
search_query: String::new(),
|
||||||
collapsed: Vec::new(),
|
collapsed: Vec::new(),
|
||||||
project_name: String::new(),
|
project_name: String::new(),
|
||||||
project_description: String::new(),
|
project_description: text_editor::Content::new(),
|
||||||
data_path: String::new(),
|
data_path: String::new(),
|
||||||
public_url: String::new(),
|
public_url: String::new(),
|
||||||
default_author: String::new(),
|
default_author: String::new(),
|
||||||
@@ -95,7 +128,7 @@ impl Default for SettingsViewState {
|
|||||||
ssh_username: String::new(),
|
ssh_username: String::new(),
|
||||||
ssh_remote_path: String::new(),
|
ssh_remote_path: String::new(),
|
||||||
offline_mode: false,
|
offline_mode: false,
|
||||||
system_prompt: String::new(),
|
system_prompt: text_editor::Content::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -107,7 +140,7 @@ pub enum SettingsMsg {
|
|||||||
ToggleSection(SettingsSection),
|
ToggleSection(SettingsSection),
|
||||||
// Project
|
// Project
|
||||||
ProjectNameChanged(String),
|
ProjectNameChanged(String),
|
||||||
ProjectDescriptionChanged(String),
|
ProjectDescriptionAction(text_editor::Action),
|
||||||
DataPathChanged(String),
|
DataPathChanged(String),
|
||||||
BrowseDataPath,
|
BrowseDataPath,
|
||||||
ResetDataPath,
|
ResetDataPath,
|
||||||
@@ -130,7 +163,7 @@ pub enum SettingsMsg {
|
|||||||
ClearPublishing,
|
ClearPublishing,
|
||||||
// AI
|
// AI
|
||||||
OfflineModeChanged(bool),
|
OfflineModeChanged(bool),
|
||||||
SystemPromptChanged(String),
|
SystemPromptAction(text_editor::Action),
|
||||||
SaveSystemPrompt,
|
SaveSystemPrompt,
|
||||||
ResetSystemPrompt,
|
ResetSystemPrompt,
|
||||||
// Data maintenance
|
// Data maintenance
|
||||||
@@ -228,12 +261,14 @@ fn section_project<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Elemen
|
|||||||
&state.project_name,
|
&state.project_name,
|
||||||
|s| Message::Settings(SettingsMsg::ProjectNameChanged(s)),
|
|s| Message::Settings(SettingsMsg::ProjectNameChanged(s)),
|
||||||
);
|
);
|
||||||
let desc = inputs::labeled_input(
|
let desc = column![
|
||||||
&t(locale, "settings.projectDescription"),
|
text(t(locale, "settings.projectDescription")).size(12).color(inputs::LABEL_COLOR).shaping(Shaping::Advanced),
|
||||||
"",
|
text_editor(&state.project_description)
|
||||||
&state.project_description,
|
.on_action(|a| Message::Settings(SettingsMsg::ProjectDescriptionAction(a)))
|
||||||
|s| Message::Settings(SettingsMsg::ProjectDescriptionChanged(s)),
|
.height(Length::Fixed(80.0))
|
||||||
);
|
.size(14),
|
||||||
|
]
|
||||||
|
.spacing(4);
|
||||||
let data_path = row![
|
let data_path = row![
|
||||||
inputs::labeled_input(
|
inputs::labeled_input(
|
||||||
&t(locale, "settings.dataPath"),
|
&t(locale, "settings.dataPath"),
|
||||||
@@ -316,12 +351,14 @@ fn section_ai<'a>(state: &'a SettingsViewState, locale: UiLocale) -> Element<'a,
|
|||||||
state.offline_mode,
|
state.offline_mode,
|
||||||
|b| Message::Settings(SettingsMsg::OfflineModeChanged(b)),
|
|b| Message::Settings(SettingsMsg::OfflineModeChanged(b)),
|
||||||
);
|
);
|
||||||
let prompt = inputs::labeled_input(
|
let prompt = column![
|
||||||
&t(locale, "settings.systemPrompt"),
|
text(t(locale, "settings.systemPrompt")).size(12).color(inputs::LABEL_COLOR).shaping(Shaping::Advanced),
|
||||||
"",
|
text_editor(&state.system_prompt)
|
||||||
&state.system_prompt,
|
.on_action(|a| Message::Settings(SettingsMsg::SystemPromptAction(a)))
|
||||||
|s| Message::Settings(SettingsMsg::SystemPromptChanged(s)),
|
.height(Length::Fixed(200.0))
|
||||||
);
|
.size(14),
|
||||||
|
]
|
||||||
|
.spacing(4);
|
||||||
let btns = row![
|
let btns = row![
|
||||||
button(text(t(locale, "common.save")).size(13))
|
button(text(t(locale, "common.save")).size(13))
|
||||||
.on_press(Message::Settings(SettingsMsg::SaveSystemPrompt))
|
.on_press(Message::Settings(SettingsMsg::SaveSystemPrompt))
|
||||||
|
|||||||
@@ -187,6 +187,7 @@ pub fn view<'a>(
|
|||||||
]
|
]
|
||||||
.spacing(8)
|
.spacing(8)
|
||||||
.width(Length::Fill)
|
.width(Length::Fill)
|
||||||
|
.height(Length::Fill)
|
||||||
.into();
|
.into();
|
||||||
|
|
||||||
// Validation error
|
// Validation error
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"menu.group.file": "Datei",
|
"menu.group.file": "Datei",
|
||||||
"menu.group.edit": "Bearbeiten",
|
"menu.group.edit": "Bearbeiten",
|
||||||
"menu.group.view": "Ansicht",
|
"menu.group.view": "Ansicht",
|
||||||
"menu.group.window": "Fenster",
|
"menu.group.blog": "Blog",
|
||||||
"menu.group.help": "Hilfe",
|
"menu.group.help": "Hilfe",
|
||||||
"menu.item.newPost": "Neuer Beitrag",
|
"menu.item.newPost": "Neuer Beitrag",
|
||||||
"menu.item.importMedia": "Medien importieren...",
|
"menu.item.importMedia": "Medien importieren...",
|
||||||
@@ -198,6 +198,8 @@
|
|||||||
"editor.tagsPlaceholder": "Tag hinzufügen...",
|
"editor.tagsPlaceholder": "Tag hinzufügen...",
|
||||||
"editor.categories": "Kategorien",
|
"editor.categories": "Kategorien",
|
||||||
"editor.categoriesPlaceholder": "Kategorie hinzufügen...",
|
"editor.categoriesPlaceholder": "Kategorie hinzufügen...",
|
||||||
|
"editor.outlinks": "Ausgehende Links",
|
||||||
|
"editor.backlinks": "Eingehende Links",
|
||||||
"editor.untitled": "Ohne Titel",
|
"editor.untitled": "Ohne Titel",
|
||||||
"editor.createdAt": "Erstellt",
|
"editor.createdAt": "Erstellt",
|
||||||
"editor.updatedAt": "Aktualisiert",
|
"editor.updatedAt": "Aktualisiert",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"menu.group.file": "File",
|
"menu.group.file": "File",
|
||||||
"menu.group.edit": "Edit",
|
"menu.group.edit": "Edit",
|
||||||
"menu.group.view": "View",
|
"menu.group.view": "View",
|
||||||
"menu.group.window": "Window",
|
"menu.group.blog": "Blog",
|
||||||
"menu.group.help": "Help",
|
"menu.group.help": "Help",
|
||||||
"menu.item.newPost": "New Post",
|
"menu.item.newPost": "New Post",
|
||||||
"menu.item.importMedia": "Import Media...",
|
"menu.item.importMedia": "Import Media...",
|
||||||
@@ -198,6 +198,8 @@
|
|||||||
"editor.tagsPlaceholder": "Add tag...",
|
"editor.tagsPlaceholder": "Add tag...",
|
||||||
"editor.categories": "Categories",
|
"editor.categories": "Categories",
|
||||||
"editor.categoriesPlaceholder": "Add category...",
|
"editor.categoriesPlaceholder": "Add category...",
|
||||||
|
"editor.outlinks": "Outgoing Links",
|
||||||
|
"editor.backlinks": "Incoming Links",
|
||||||
"editor.untitled": "Untitled",
|
"editor.untitled": "Untitled",
|
||||||
"editor.createdAt": "Created",
|
"editor.createdAt": "Created",
|
||||||
"editor.updatedAt": "Updated",
|
"editor.updatedAt": "Updated",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"menu.group.file": "Archivo",
|
"menu.group.file": "Archivo",
|
||||||
"menu.group.edit": "Editar",
|
"menu.group.edit": "Editar",
|
||||||
"menu.group.view": "Ver",
|
"menu.group.view": "Ver",
|
||||||
"menu.group.window": "Ventana",
|
"menu.group.blog": "Blog",
|
||||||
"menu.group.help": "Ayuda",
|
"menu.group.help": "Ayuda",
|
||||||
"menu.item.newPost": "Nueva entrada",
|
"menu.item.newPost": "Nueva entrada",
|
||||||
"menu.item.importMedia": "Importar medios...",
|
"menu.item.importMedia": "Importar medios...",
|
||||||
@@ -198,6 +198,8 @@
|
|||||||
"editor.tagsPlaceholder": "Añadir etiqueta...",
|
"editor.tagsPlaceholder": "Añadir etiqueta...",
|
||||||
"editor.categories": "Categorías",
|
"editor.categories": "Categorías",
|
||||||
"editor.categoriesPlaceholder": "Añadir categoría...",
|
"editor.categoriesPlaceholder": "Añadir categoría...",
|
||||||
|
"editor.outlinks": "Enlaces salientes",
|
||||||
|
"editor.backlinks": "Enlaces entrantes",
|
||||||
"editor.untitled": "Sin título",
|
"editor.untitled": "Sin título",
|
||||||
"editor.createdAt": "Creado",
|
"editor.createdAt": "Creado",
|
||||||
"editor.updatedAt": "Actualizado",
|
"editor.updatedAt": "Actualizado",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"menu.group.file": "Fichier",
|
"menu.group.file": "Fichier",
|
||||||
"menu.group.edit": "Édition",
|
"menu.group.edit": "Édition",
|
||||||
"menu.group.view": "Affichage",
|
"menu.group.view": "Affichage",
|
||||||
"menu.group.window": "Fenêtre",
|
"menu.group.blog": "Blog",
|
||||||
"menu.group.help": "Aide",
|
"menu.group.help": "Aide",
|
||||||
"menu.item.newPost": "Nouvel article",
|
"menu.item.newPost": "Nouvel article",
|
||||||
"menu.item.importMedia": "Importer des médias...",
|
"menu.item.importMedia": "Importer des médias...",
|
||||||
@@ -198,6 +198,8 @@
|
|||||||
"editor.tagsPlaceholder": "Ajouter un tag...",
|
"editor.tagsPlaceholder": "Ajouter un tag...",
|
||||||
"editor.categories": "Catégories",
|
"editor.categories": "Catégories",
|
||||||
"editor.categoriesPlaceholder": "Ajouter une catégorie...",
|
"editor.categoriesPlaceholder": "Ajouter une catégorie...",
|
||||||
|
"editor.outlinks": "Liens sortants",
|
||||||
|
"editor.backlinks": "Liens entrants",
|
||||||
"editor.untitled": "Sans titre",
|
"editor.untitled": "Sans titre",
|
||||||
"editor.createdAt": "Créé",
|
"editor.createdAt": "Créé",
|
||||||
"editor.updatedAt": "Mis à jour",
|
"editor.updatedAt": "Mis à jour",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"menu.group.file": "Archivio",
|
"menu.group.file": "Archivio",
|
||||||
"menu.group.edit": "Modifica",
|
"menu.group.edit": "Modifica",
|
||||||
"menu.group.view": "Vista",
|
"menu.group.view": "Vista",
|
||||||
"menu.group.window": "Finestra",
|
"menu.group.blog": "Blog",
|
||||||
"menu.group.help": "Aiuto",
|
"menu.group.help": "Aiuto",
|
||||||
"menu.item.newPost": "Nuovo post",
|
"menu.item.newPost": "Nuovo post",
|
||||||
"menu.item.importMedia": "Importa media...",
|
"menu.item.importMedia": "Importa media...",
|
||||||
@@ -198,6 +198,8 @@
|
|||||||
"editor.tagsPlaceholder": "Aggiungi tag...",
|
"editor.tagsPlaceholder": "Aggiungi tag...",
|
||||||
"editor.categories": "Categorie",
|
"editor.categories": "Categorie",
|
||||||
"editor.categoriesPlaceholder": "Aggiungi categoria...",
|
"editor.categoriesPlaceholder": "Aggiungi categoria...",
|
||||||
|
"editor.outlinks": "Link in uscita",
|
||||||
|
"editor.backlinks": "Link in entrata",
|
||||||
"editor.untitled": "Senza titolo",
|
"editor.untitled": "Senza titolo",
|
||||||
"editor.createdAt": "Creato",
|
"editor.createdAt": "Creato",
|
||||||
"editor.updatedAt": "Aggiornato",
|
"editor.updatedAt": "Aggiornato",
|
||||||
|
|||||||
Reference in New Issue
Block a user