fix: more gaps in M3
This commit is contained in:
@@ -274,11 +274,21 @@ pub fn sync_tags_from_posts(
|
||||
tag_q::insert_tag(conn, &tag)?;
|
||||
}
|
||||
}
|
||||
|
||||
let all_tags = tag_q::list_tags_by_project(conn, project_id)?;
|
||||
Ok(all_tags)
|
||||
}
|
||||
|
||||
/// Discover tags from posts and rewrite tags.json to match the resulting DB state.
|
||||
pub fn discover_tags(
|
||||
conn: &Connection,
|
||||
data_dir: &Path,
|
||||
project_id: &str,
|
||||
) -> EngineResult<Vec<Tag>> {
|
||||
let tags = sync_tags_from_posts(conn, project_id)?;
|
||||
rewrite_tags_json(conn, data_dir, project_id)?;
|
||||
Ok(tags)
|
||||
}
|
||||
|
||||
/// Rewrite meta/tags.json from DB state.
|
||||
pub fn rewrite_tags_json(
|
||||
conn: &Connection,
|
||||
@@ -520,7 +530,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn sync_tags_from_posts_creates_missing() {
|
||||
let (db, dir) = setup();
|
||||
let (db, _dir) = setup();
|
||||
insert_post(
|
||||
db.conn(),
|
||||
&make_post("x1", "a", vec!["rust".into(), "web".into()]),
|
||||
@@ -540,6 +550,23 @@ mod tests {
|
||||
assert!(names.contains(&"web"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn discover_tags_rewrites_tags_json() {
|
||||
let (db, dir) = setup();
|
||||
insert_post(
|
||||
db.conn(),
|
||||
&make_post("x1", "a", vec!["rust".into(), "web".into()]),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let tags = discover_tags(db.conn(), dir.path(), "p1").unwrap();
|
||||
assert_eq!(tags.len(), 2);
|
||||
|
||||
let entries = meta::read_tags_json(dir.path()).unwrap();
|
||||
let names = entries.into_iter().map(|entry| entry.name).collect::<Vec<_>>();
|
||||
assert_eq!(names, vec!["rust".to_string(), "web".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn import_tags_from_file_preserves_colors() {
|
||||
let (db, dir) = setup();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -135,6 +135,11 @@ impl MenuRegistry {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn empty() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
|
||||
fn register(&mut self, action: MenuAction, item: &MenuItem) {
|
||||
self.action_map.insert(item.id().clone(), action);
|
||||
self.id_map.insert(action, item.id().clone());
|
||||
|
||||
@@ -13,6 +13,12 @@ use crate::components::inputs;
|
||||
use crate::i18n::t;
|
||||
use crate::views::post_editor::TranslationFlag;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LinkedPostItem {
|
||||
pub post_id: String,
|
||||
pub title: String,
|
||||
}
|
||||
|
||||
/// Saved draft content for a single media translation language.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MediaTranslationDraft {
|
||||
@@ -49,10 +55,19 @@ pub struct MediaEditorState {
|
||||
pub blog_languages: Vec<String>,
|
||||
pub saved_canonical: Option<MediaTranslationDraft>,
|
||||
pub translation_drafts: HashMap<String, MediaTranslationDraft>,
|
||||
pub linked_posts: Vec<LinkedPostItem>,
|
||||
pub post_picker_open: bool,
|
||||
pub post_picker_search: String,
|
||||
pub post_picker_results: Vec<LinkedPostItem>,
|
||||
}
|
||||
|
||||
impl MediaEditorState {
|
||||
pub fn from_media(media: &Media, blog_languages: &[String], translations: &[MediaTranslation]) -> Self {
|
||||
pub fn from_media(
|
||||
media: &Media,
|
||||
blog_languages: &[String],
|
||||
translations: &[MediaTranslation],
|
||||
linked_posts: Vec<LinkedPostItem>,
|
||||
) -> Self {
|
||||
let canonical_lang = media.language.clone().unwrap_or_else(|| "en".to_string());
|
||||
|
||||
let mut translation_drafts = HashMap::new();
|
||||
@@ -89,6 +104,10 @@ impl MediaEditorState {
|
||||
blog_languages: blog_languages.to_vec(),
|
||||
saved_canonical: None,
|
||||
translation_drafts,
|
||||
linked_posts,
|
||||
post_picker_open: false,
|
||||
post_picker_search: String::new(),
|
||||
post_picker_results: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,6 +193,11 @@ pub enum MediaEditorMsg {
|
||||
LanguageChanged(String),
|
||||
TagsChanged(String),
|
||||
SwitchLanguage(String),
|
||||
TogglePostPicker,
|
||||
PostPickerSearchChanged(String),
|
||||
LinkPost(String),
|
||||
OpenLinkedPost(String),
|
||||
UnlinkPost(String),
|
||||
Save,
|
||||
Delete,
|
||||
}
|
||||
@@ -308,6 +332,88 @@ pub fn view<'a>(
|
||||
let meta_row2 = row![caption_input, author_input].spacing(16).width(Length::Fill);
|
||||
let meta_row3 = row![tags_input, language_input].spacing(16).width(Length::Fill);
|
||||
|
||||
let linked_posts_header = row![
|
||||
text(t(locale, "editor.linkedPosts"))
|
||||
.size(12)
|
||||
.color(Color::from_rgb(0.55, 0.58, 0.65)),
|
||||
Space::with_width(Length::Fill),
|
||||
button(text(t(locale, "editor.linkToPost")).size(12))
|
||||
.on_press(Message::MediaEditor(MediaEditorMsg::TogglePostPicker))
|
||||
.padding([4, 10]),
|
||||
]
|
||||
.align_y(iced::Alignment::Center)
|
||||
.spacing(8);
|
||||
|
||||
let post_picker: Element<'a, Message> = if state.post_picker_open {
|
||||
let search = inputs::labeled_input(
|
||||
&t(locale, "editor.postPickerSearch"),
|
||||
&t(locale, "editor.postPickerSearchPlaceholder"),
|
||||
&state.post_picker_search,
|
||||
|s| Message::MediaEditor(MediaEditorMsg::PostPickerSearchChanged(s)),
|
||||
);
|
||||
let results: Vec<Element<'a, Message>> = if state.post_picker_results.is_empty() {
|
||||
vec![
|
||||
text(t(locale, "sidebar.filter.noResults"))
|
||||
.size(12)
|
||||
.color(Color::from_rgb(0.55, 0.58, 0.65))
|
||||
.into(),
|
||||
]
|
||||
} else {
|
||||
state
|
||||
.post_picker_results
|
||||
.iter()
|
||||
.map(|post| {
|
||||
button(text(post.title.clone()).size(12))
|
||||
.on_press(Message::MediaEditor(MediaEditorMsg::LinkPost(post.post_id.clone())))
|
||||
.padding([4, 10])
|
||||
.width(Length::Fill)
|
||||
.into()
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
container(column![search, column(results).spacing(4)].spacing(8).padding(8))
|
||||
.style(|_: &iced::Theme| iced::widget::container::Style {
|
||||
background: Some(iced::Background::Color(Color::from_rgb(0.16, 0.18, 0.22))),
|
||||
border: iced::Border {
|
||||
color: Color::from_rgb(0.28, 0.28, 0.32),
|
||||
width: 1.0,
|
||||
radius: 6.0.into(),
|
||||
},
|
||||
..iced::widget::container::Style::default()
|
||||
})
|
||||
.into()
|
||||
} else {
|
||||
Space::new(0, 0).into()
|
||||
};
|
||||
|
||||
let linked_posts_list: Element<'a, Message> = if state.linked_posts.is_empty() {
|
||||
text(t(locale, "editor.linkedPostsEmpty"))
|
||||
.size(12)
|
||||
.color(Color::from_rgb(0.55, 0.58, 0.65))
|
||||
.into()
|
||||
} else {
|
||||
let rows: Vec<Element<'a, Message>> = state
|
||||
.linked_posts
|
||||
.iter()
|
||||
.map(|post| {
|
||||
row![
|
||||
button(text(post.title.clone()).size(12))
|
||||
.on_press(Message::MediaEditor(MediaEditorMsg::OpenLinkedPost(post.post_id.clone())))
|
||||
.padding([4, 0]),
|
||||
Space::with_width(Length::Fill),
|
||||
button(text(t(locale, "editor.unlinkMedia")).size(11))
|
||||
.on_press(Message::MediaEditor(MediaEditorMsg::UnlinkPost(post.post_id.clone())))
|
||||
.padding([3, 8])
|
||||
.style(inputs::danger_button),
|
||||
]
|
||||
.align_y(iced::Alignment::Center)
|
||||
.spacing(8)
|
||||
.into()
|
||||
})
|
||||
.collect();
|
||||
column(rows).spacing(6).into()
|
||||
};
|
||||
|
||||
// Footer
|
||||
let footer = row![
|
||||
inputs::date_label(&t(locale, "editor.createdAt"), state.created_at),
|
||||
@@ -326,6 +432,9 @@ pub fn view<'a>(
|
||||
meta_row1,
|
||||
meta_row2,
|
||||
meta_row3,
|
||||
linked_posts_header,
|
||||
post_picker,
|
||||
linked_posts_list,
|
||||
footer,
|
||||
]
|
||||
.spacing(12)
|
||||
|
||||
@@ -76,8 +76,9 @@ pub enum ConfirmAction {
|
||||
DeleteMedia(String),
|
||||
DeleteScript(String),
|
||||
DeleteTemplate(String),
|
||||
ForceDeleteTemplate(String),
|
||||
DeleteTag(String),
|
||||
MergeTags { source: String, target: String },
|
||||
MergeTags { sources: Vec<String>, target: String },
|
||||
}
|
||||
|
||||
// ── Modal backdrop style ──
|
||||
|
||||
@@ -7,6 +7,8 @@ use bds_core::i18n::UiLocale;
|
||||
use crate::app::Message;
|
||||
use crate::i18n::t;
|
||||
use crate::state::navigation::{OutputEntry, PanelTab, TaskSnapshot};
|
||||
use crate::state::tabs::{Tab, TabType};
|
||||
use crate::views::post_editor::ResolvedPostLink;
|
||||
|
||||
/// Panel background style.
|
||||
fn panel_style(_theme: &Theme) -> container::Style {
|
||||
@@ -66,6 +68,8 @@ pub fn view(
|
||||
panel_tab: PanelTab,
|
||||
task_snapshots: &[TaskSnapshot],
|
||||
output_entries: &[OutputEntry],
|
||||
post_outlinks: &[ResolvedPostLink],
|
||||
post_backlinks: &[ResolvedPostLink],
|
||||
locale: UiLocale,
|
||||
active_tab_is_post: bool,
|
||||
active_tab_is_post_or_media: bool,
|
||||
@@ -183,10 +187,51 @@ pub fn view(
|
||||
}
|
||||
}
|
||||
PanelTab::PostLinks => {
|
||||
// Post Links content populated in M3 (editor integration)
|
||||
container(text(t(locale, "panel.postLinksPlaceholder")).size(12).shaping(Shaping::Advanced).color(muted))
|
||||
.padding(8)
|
||||
if post_outlinks.is_empty() && post_backlinks.is_empty() {
|
||||
container(text(t(locale, "panel.postLinksPlaceholder")).size(12).shaping(Shaping::Advanced).color(muted))
|
||||
.padding(8)
|
||||
.into()
|
||||
} else {
|
||||
let mut items: Vec<Element<'static, Message>> = vec![
|
||||
text(t(locale, "editor.outlinks"))
|
||||
.size(12)
|
||||
.shaping(Shaping::Advanced)
|
||||
.color(Color::from_rgb(0.75, 0.77, 0.82))
|
||||
.into(),
|
||||
];
|
||||
|
||||
if post_outlinks.is_empty() {
|
||||
items.push(text(t(locale, "panel.postLinksPlaceholder")).size(11).color(muted).into());
|
||||
} else {
|
||||
for link in post_outlinks {
|
||||
items.push(post_link_button(locale, link));
|
||||
}
|
||||
}
|
||||
|
||||
items.push(Space::with_height(8.0).into());
|
||||
items.push(
|
||||
text(t(locale, "editor.backlinks"))
|
||||
.size(12)
|
||||
.shaping(Shaping::Advanced)
|
||||
.color(Color::from_rgb(0.75, 0.77, 0.82))
|
||||
.into(),
|
||||
);
|
||||
|
||||
if post_backlinks.is_empty() {
|
||||
items.push(text(t(locale, "panel.postLinksPlaceholder")).size(11).color(muted).into());
|
||||
} else {
|
||||
for link in post_backlinks {
|
||||
items.push(post_link_button(locale, link));
|
||||
}
|
||||
}
|
||||
|
||||
scrollable(
|
||||
iced::widget::Column::with_children(items)
|
||||
.spacing(4)
|
||||
.padding(8),
|
||||
)
|
||||
.into()
|
||||
}
|
||||
}
|
||||
PanelTab::GitLog => {
|
||||
// Git Log content populated in extension bucket A (git integration)
|
||||
@@ -204,3 +249,21 @@ pub fn view(
|
||||
.style(panel_style)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn post_link_button(locale: UiLocale, link: &ResolvedPostLink) -> Element<'static, Message> {
|
||||
button(text(link.title.clone()).size(11).shaping(Shaping::Advanced))
|
||||
.on_press(Message::OpenTab(Tab {
|
||||
id: link.post_id.clone(),
|
||||
title: if link.title.is_empty() {
|
||||
t(locale, "editor.untitled")
|
||||
} else {
|
||||
link.title.clone()
|
||||
},
|
||||
tab_type: TabType::Post,
|
||||
is_transient: false,
|
||||
is_dirty: false,
|
||||
}))
|
||||
.padding([4, 8])
|
||||
.style(tab_inactive)
|
||||
.into()
|
||||
}
|
||||
|
||||
@@ -67,6 +67,7 @@ pub struct PostEditorState {
|
||||
pub updated_at: i64,
|
||||
pub published_at: Option<i64>,
|
||||
pub is_dirty: bool,
|
||||
pub last_edit_at_ms: i64,
|
||||
pub metadata_expanded: bool,
|
||||
pub excerpt_expanded: bool,
|
||||
pub tags_input: String,
|
||||
@@ -120,6 +121,7 @@ impl Clone for PostEditorState {
|
||||
updated_at: self.updated_at,
|
||||
published_at: self.published_at,
|
||||
is_dirty: self.is_dirty,
|
||||
last_edit_at_ms: self.last_edit_at_ms,
|
||||
metadata_expanded: self.metadata_expanded,
|
||||
excerpt_expanded: self.excerpt_expanded,
|
||||
tags_input: self.tags_input.clone(),
|
||||
@@ -180,6 +182,7 @@ impl PostEditorState {
|
||||
updated_at: post.updated_at,
|
||||
published_at: post.published_at,
|
||||
is_dirty: false,
|
||||
last_edit_at_ms: 0,
|
||||
metadata_expanded: title.is_empty(),
|
||||
excerpt_expanded: false,
|
||||
tags_input: String::new(),
|
||||
@@ -196,6 +199,11 @@ impl PostEditorState {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn mark_dirty(&mut self) {
|
||||
self.is_dirty = true;
|
||||
self.last_edit_at_ms = bds_core::util::now_unix_ms();
|
||||
}
|
||||
|
||||
pub fn insert_markdown_at_cursor(&mut self, markdown: &str) {
|
||||
let new_content = {
|
||||
let mut buffer = self.editor_buffer.borrow_mut();
|
||||
@@ -203,7 +211,7 @@ impl PostEditorState {
|
||||
buffer.text()
|
||||
};
|
||||
self.content = new_content;
|
||||
self.is_dirty = true;
|
||||
self.mark_dirty();
|
||||
}
|
||||
|
||||
/// Switch the editor to display a different language.
|
||||
|
||||
@@ -178,12 +178,18 @@ pub fn view<'a>(
|
||||
|k| Message::ScriptEditor(ScriptEditorMsg::KindChanged(k)),
|
||||
);
|
||||
|
||||
// Entrypoint: show discovered functions as a select or text input
|
||||
let entrypoint_input = inputs::labeled_input(
|
||||
let mut entrypoint_options = state.discovered_entrypoints.clone();
|
||||
if !entrypoint_options.iter().any(|entrypoint| entrypoint == &state.entrypoint) {
|
||||
entrypoint_options.push(state.entrypoint.clone());
|
||||
}
|
||||
let selected_entrypoint = entrypoint_options
|
||||
.iter()
|
||||
.find(|entrypoint| *entrypoint == &state.entrypoint);
|
||||
let entrypoint_input = inputs::labeled_select(
|
||||
&t(locale, "editor.entrypoint"),
|
||||
"render",
|
||||
&state.entrypoint,
|
||||
|s| Message::ScriptEditor(ScriptEditorMsg::EntrypointChanged(s)),
|
||||
&entrypoint_options,
|
||||
selected_entrypoint,
|
||||
|entrypoint| Message::ScriptEditor(ScriptEditorMsg::EntrypointChanged(entrypoint)),
|
||||
);
|
||||
|
||||
let enabled_check = inputs::labeled_checkbox(
|
||||
|
||||
@@ -8,16 +8,22 @@ use bds_core::model::Tag;
|
||||
|
||||
use crate::app::Message;
|
||||
use crate::components::inputs;
|
||||
use crate::i18n::t;
|
||||
use crate::i18n::{t, tw};
|
||||
|
||||
const DEFAULT_TAG_COLOR: &str = "#6495ed";
|
||||
const COLOR_PRESETS: [&str; 17] = [
|
||||
"#e63946", "#f77f00", "#fcbf49", "#90be6d", "#43aa8b", "#4d908e",
|
||||
"#577590", "#277da1", "#4361ee", "#7209b7", "#b5179e", "#f72585",
|
||||
"#9c6644", "#6c757d", "#adb5bd", "#2a9d8f", "#264653",
|
||||
];
|
||||
|
||||
/// Active view within the Tags tab.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum TagsSection {
|
||||
Cloud,
|
||||
Manage,
|
||||
Merge,
|
||||
Discover,
|
||||
}
|
||||
|
||||
/// State for the tags view.
|
||||
@@ -27,31 +33,64 @@ pub struct TagsViewState {
|
||||
pub tags: Vec<Tag>,
|
||||
pub tag_post_counts: HashMap<String, usize>,
|
||||
pub search_query: String,
|
||||
/// For "Manage": the currently editing tag
|
||||
pub selected_tags: Vec<String>,
|
||||
pub create_name: String,
|
||||
pub create_color: String,
|
||||
pub editing_tag: Option<EditingTag>,
|
||||
/// For "Merge": source and target tag selection
|
||||
pub merge_source: Option<String>,
|
||||
pub merge_target: Option<String>,
|
||||
pub template_options: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EditingTag {
|
||||
pub id: String,
|
||||
pub original_name: String,
|
||||
pub name: String,
|
||||
pub color: String,
|
||||
pub template_slug: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct TagOption {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for TagOption {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.name)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct TemplateOption {
|
||||
pub slug: String,
|
||||
pub label: String,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for TemplateOption {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{}", self.label)
|
||||
}
|
||||
}
|
||||
|
||||
impl TagsViewState {
|
||||
pub fn new(tags: Vec<Tag>, tag_post_counts: HashMap<String, usize>) -> Self {
|
||||
pub fn new(
|
||||
tags: Vec<Tag>,
|
||||
tag_post_counts: HashMap<String, usize>,
|
||||
template_options: Vec<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
section: TagsSection::Cloud,
|
||||
tags,
|
||||
tag_post_counts,
|
||||
search_query: String::new(),
|
||||
selected_tags: Vec::new(),
|
||||
create_name: String::new(),
|
||||
create_color: String::new(),
|
||||
editing_tag: None,
|
||||
merge_source: None,
|
||||
merge_target: None,
|
||||
template_options,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,28 +100,28 @@ impl TagsViewState {
|
||||
pub enum TagsMsg {
|
||||
SetSection(TagsSection),
|
||||
SearchChanged(String),
|
||||
SelectTag(String),
|
||||
CreateTag(String),
|
||||
ToggleTagSelection(String),
|
||||
ClearSelection,
|
||||
CreateNameChanged(String),
|
||||
CreateColorChanged(String),
|
||||
CreateTag,
|
||||
EditTagName(String),
|
||||
EditTagColor(String),
|
||||
EditTagTemplate(String),
|
||||
EditTagTemplate(TemplateOption),
|
||||
SaveTag,
|
||||
DeleteTag(String),
|
||||
SetMergeSource(String),
|
||||
SetMergeTarget(String),
|
||||
SetMergeTarget(TagOption),
|
||||
MergeTags,
|
||||
SyncTags,
|
||||
}
|
||||
|
||||
/// Render the tags management view.
|
||||
pub fn view<'a>(
|
||||
state: &'a TagsViewState,
|
||||
locale: UiLocale,
|
||||
) -> Element<'a, Message> {
|
||||
// Section navigation tabs
|
||||
pub fn view<'a>(state: &'a TagsViewState, locale: UiLocale) -> Element<'a, Message> {
|
||||
let section_nav = row![
|
||||
section_tab(&t(locale, "tags.nav.cloud"), state.section == TagsSection::Cloud, TagsSection::Cloud),
|
||||
section_tab(&t(locale, "tags.nav.manage"), state.section == TagsSection::Manage, TagsSection::Manage),
|
||||
section_tab(&t(locale, "tags.nav.merge"), state.section == TagsSection::Merge, TagsSection::Merge),
|
||||
section_tab(&t(locale, "tags.nav.discover"), state.section == TagsSection::Discover, TagsSection::Discover),
|
||||
]
|
||||
.spacing(4)
|
||||
.padding(8);
|
||||
@@ -91,6 +130,7 @@ pub fn view<'a>(
|
||||
TagsSection::Cloud => view_cloud(state, locale),
|
||||
TagsSection::Manage => view_manage(state, locale),
|
||||
TagsSection::Merge => view_merge(state, locale),
|
||||
TagsSection::Discover => view_discover(state, locale),
|
||||
};
|
||||
|
||||
column![section_nav, content]
|
||||
@@ -137,13 +177,18 @@ fn view_cloud<'a>(state: &'a TagsViewState, locale: UiLocale) -> Element<'a, Mes
|
||||
.into();
|
||||
}
|
||||
|
||||
// Calculate min/max post counts for font scaling
|
||||
let counts: Vec<usize> = state.tags.iter()
|
||||
.map(|t| *state.tag_post_counts.get(&t.name.to_lowercase()).unwrap_or(&0))
|
||||
let counts: Vec<usize> = state
|
||||
.tags
|
||||
.iter()
|
||||
.map(|tag| *state.tag_post_counts.get(&tag.name.to_lowercase()).unwrap_or(&0))
|
||||
.collect();
|
||||
let min_count = *counts.iter().min().unwrap_or(&0);
|
||||
let max_count = *counts.iter().max().unwrap_or(&1);
|
||||
let count_range = if max_count > min_count { (max_count - min_count) as f32 } else { 1.0 };
|
||||
let count_range = if max_count > min_count {
|
||||
(max_count - min_count) as f32
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
const MIN_FONT: f32 = 11.0;
|
||||
const MAX_FONT: f32 = 24.0;
|
||||
|
||||
@@ -155,28 +200,56 @@ fn view_cloud<'a>(state: &'a TagsViewState, locale: UiLocale) -> Element<'a, Mes
|
||||
let post_count = *state.tag_post_counts.get(&tag.name.to_lowercase()).unwrap_or(&0);
|
||||
let font_size = MIN_FONT + ((post_count - min_count) as f32 / count_range) * (MAX_FONT - MIN_FONT);
|
||||
let vert_pad = ((MAX_FONT - font_size) / 4.0).max(2.0) as u16;
|
||||
button(text(&tag.name).size(font_size).color(Color::WHITE))
|
||||
.on_press(Message::Tags(TagsMsg::SelectTag(tag.id.clone())))
|
||||
.padding([vert_pad, 10])
|
||||
.style(move |_: &Theme, _| button::Style {
|
||||
background: Some(Background::Color(color)),
|
||||
border: iced::Border {
|
||||
radius: 12.0.into(),
|
||||
..iced::Border::default()
|
||||
},
|
||||
text_color: Color::WHITE,
|
||||
..button::Style::default()
|
||||
})
|
||||
.into()
|
||||
let selected = state.selected_tags.iter().any(|selected_id| selected_id == &tag.id);
|
||||
button(
|
||||
row![
|
||||
text(&tag.name).size(font_size).color(Color::WHITE),
|
||||
text(post_count.to_string()).size(11).color(Color::from_rgba(1.0, 1.0, 1.0, 0.85)),
|
||||
]
|
||||
.spacing(6)
|
||||
.align_y(Alignment::Center),
|
||||
)
|
||||
.on_press(Message::Tags(TagsMsg::ToggleTagSelection(tag.id.clone())))
|
||||
.padding([vert_pad, 10])
|
||||
.style(move |_: &Theme, _| button::Style {
|
||||
background: Some(Background::Color(color)),
|
||||
border: iced::Border {
|
||||
radius: 12.0.into(),
|
||||
width: if selected { 2.0 } else { 0.0 },
|
||||
color: if selected { Color::WHITE } else { Color::TRANSPARENT },
|
||||
},
|
||||
text_color: Color::WHITE,
|
||||
..button::Style::default()
|
||||
})
|
||||
.into()
|
||||
})
|
||||
.collect();
|
||||
|
||||
let cloud = row(chips).spacing(6).wrap();
|
||||
let selection_summary: Element<'a, Message> = if state.selected_tags.is_empty() {
|
||||
text(t(locale, "tags.cloudHelp")).size(12).color(Color::from_rgb(0.60, 0.60, 0.65)).into()
|
||||
} else {
|
||||
row![
|
||||
text(tw(locale, "tags.selectedCount", &[("count", &state.selected_tags.len().to_string())]))
|
||||
.size(12)
|
||||
.color(Color::from_rgb(0.75, 0.77, 0.82)),
|
||||
button(text(t(locale, "tags.clearSelection")).size(12))
|
||||
.on_press(Message::Tags(TagsMsg::ClearSelection))
|
||||
.padding([4, 8]),
|
||||
]
|
||||
.spacing(8)
|
||||
.align_y(Alignment::Center)
|
||||
.into()
|
||||
};
|
||||
|
||||
scrollable(
|
||||
container(cloud)
|
||||
.width(Length::Fill)
|
||||
.padding(16),
|
||||
column![
|
||||
inputs::section_header(&t(locale, "tags.cloudSection")),
|
||||
selection_summary,
|
||||
row(chips).spacing(6).wrap(),
|
||||
]
|
||||
.spacing(12)
|
||||
.width(Length::Fill)
|
||||
.padding(16),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
@@ -184,16 +257,38 @@ fn view_cloud<'a>(state: &'a TagsViewState, locale: UiLocale) -> Element<'a, Mes
|
||||
}
|
||||
|
||||
fn view_manage<'a>(state: &'a TagsViewState, locale: UiLocale) -> Element<'a, Message> {
|
||||
let create_panel = column![
|
||||
inputs::section_header(&t(locale, "tags.createSection")),
|
||||
inputs::labeled_input(
|
||||
&t(locale, "tags.name"),
|
||||
&t(locale, "tags.createPlaceholder"),
|
||||
&state.create_name,
|
||||
|value| Message::Tags(TagsMsg::CreateNameChanged(value)),
|
||||
),
|
||||
inputs::labeled_input(
|
||||
&t(locale, "tags.color"),
|
||||
DEFAULT_TAG_COLOR,
|
||||
&state.create_color,
|
||||
|value| Message::Tags(TagsMsg::CreateColorChanged(value)),
|
||||
),
|
||||
color_swatches(locale, true),
|
||||
button(text(t(locale, "tags.createButton")).size(13))
|
||||
.on_press_maybe((!state.create_name.trim().is_empty()).then_some(Message::Tags(TagsMsg::CreateTag)))
|
||||
.style(inputs::primary_button)
|
||||
.padding([6, 16]),
|
||||
]
|
||||
.spacing(8);
|
||||
|
||||
let search = text_input(&t(locale, "sidebar.filter.search"), &state.search_query)
|
||||
.on_input(|s| Message::Tags(TagsMsg::SearchChanged(s)))
|
||||
.on_input(|value| Message::Tags(TagsMsg::SearchChanged(value)))
|
||||
.size(14);
|
||||
|
||||
let filtered: Vec<&Tag> = state
|
||||
.tags
|
||||
.iter()
|
||||
.filter(|t| {
|
||||
.filter(|tag| {
|
||||
state.search_query.is_empty()
|
||||
|| t.name.to_lowercase().contains(&state.search_query.to_lowercase())
|
||||
|| tag.name.to_lowercase().contains(&state.search_query.to_lowercase())
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -201,9 +296,11 @@ fn view_manage<'a>(state: &'a TagsViewState, locale: UiLocale) -> Element<'a, Me
|
||||
.iter()
|
||||
.map(|tag| {
|
||||
let color = parse_tag_color(tag.color.as_deref().unwrap_or(DEFAULT_TAG_COLOR));
|
||||
row![
|
||||
container(Space::new(12, 12))
|
||||
.style(move |_: &Theme| container::Style {
|
||||
let selected = state.selected_tags.iter().any(|selected_id| selected_id == &tag.id);
|
||||
let count = state.tag_post_counts.get(&tag.name.to_lowercase()).copied().unwrap_or(0);
|
||||
button(
|
||||
row![
|
||||
container(Space::new(12, 12)).style(move |_: &Theme| container::Style {
|
||||
background: Some(Background::Color(color)),
|
||||
border: iced::Border {
|
||||
radius: 6.0.into(),
|
||||
@@ -211,65 +308,92 @@ fn view_manage<'a>(state: &'a TagsViewState, locale: UiLocale) -> Element<'a, Me
|
||||
},
|
||||
..container::Style::default()
|
||||
}),
|
||||
text(&tag.name).size(14),
|
||||
Space::with_width(Length::Fill),
|
||||
button(text(t(locale, "modal.confirmDelete.delete")).size(12))
|
||||
.on_press(Message::Tags(TagsMsg::DeleteTag(tag.id.clone())))
|
||||
.style(inputs::danger_button)
|
||||
.padding([3, 8])
|
||||
]
|
||||
.spacing(8)
|
||||
.align_y(Alignment::Center)
|
||||
.padding(4)
|
||||
text(&tag.name).size(14),
|
||||
text(format!("({count})")).size(12).color(Color::from_rgb(0.55, 0.58, 0.65)),
|
||||
Space::with_width(Length::Fill),
|
||||
]
|
||||
.spacing(8)
|
||||
.align_y(Alignment::Center),
|
||||
)
|
||||
.on_press(Message::Tags(TagsMsg::ToggleTagSelection(tag.id.clone())))
|
||||
.padding([6, 8])
|
||||
.style(move |_theme: &Theme, _status| button::Style {
|
||||
background: Some(Background::Color(if selected {
|
||||
Color::from_rgb(0.22, 0.24, 0.30)
|
||||
} else {
|
||||
Color::TRANSPARENT
|
||||
})),
|
||||
border: iced::Border {
|
||||
radius: 6.0.into(),
|
||||
width: if selected { 1.0 } else { 0.0 },
|
||||
color: Color::from_rgb(0.35, 0.45, 0.65),
|
||||
},
|
||||
..button::Style::default()
|
||||
})
|
||||
.into()
|
||||
})
|
||||
.collect();
|
||||
|
||||
let tag_list = iced::widget::Column::with_children(rows).spacing(2);
|
||||
let tag_list = iced::widget::Column::with_children(rows).spacing(4);
|
||||
|
||||
// Edit panel for selected tag
|
||||
let edit_panel: Element<'a, Message> = if let Some(ref editing) = state.editing_tag {
|
||||
let template_options = template_options(state, locale);
|
||||
let selected_template = template_options.iter().find(|option| option.slug == editing.template_slug);
|
||||
let delete_button: Element<'a, Message> = button(text(t(locale, "modal.confirmDelete.delete")).size(13))
|
||||
.on_press(Message::Tags(TagsMsg::DeleteTag(editing.id.clone())))
|
||||
.style(inputs::danger_button)
|
||||
.padding([6, 16])
|
||||
.into();
|
||||
|
||||
column![
|
||||
inputs::section_header(&t(locale, "tags.editTag")),
|
||||
inputs::labeled_input(
|
||||
&t(locale, "tags.name"),
|
||||
"",
|
||||
&editing.name,
|
||||
|s| Message::Tags(TagsMsg::EditTagName(s)),
|
||||
|value| Message::Tags(TagsMsg::EditTagName(value)),
|
||||
),
|
||||
inputs::labeled_input(
|
||||
&t(locale, "tags.color"),
|
||||
"#3498db",
|
||||
DEFAULT_TAG_COLOR,
|
||||
&editing.color,
|
||||
|s| Message::Tags(TagsMsg::EditTagColor(s)),
|
||||
|value| Message::Tags(TagsMsg::EditTagColor(value)),
|
||||
),
|
||||
inputs::labeled_input(
|
||||
color_swatches(locale, false),
|
||||
inputs::labeled_select(
|
||||
&t(locale, "tags.postTemplate"),
|
||||
"",
|
||||
&editing.template_slug,
|
||||
|s| Message::Tags(TagsMsg::EditTagTemplate(s)),
|
||||
&template_options,
|
||||
selected_template,
|
||||
|choice| Message::Tags(TagsMsg::EditTagTemplate(choice)),
|
||||
),
|
||||
button(text(t(locale, "common.save")).size(13))
|
||||
.on_press(Message::Tags(TagsMsg::SaveTag))
|
||||
.style(inputs::primary_button)
|
||||
.padding([6, 16]),
|
||||
row![
|
||||
button(text(t(locale, "common.save")).size(13))
|
||||
.on_press(Message::Tags(TagsMsg::SaveTag))
|
||||
.style(inputs::primary_button)
|
||||
.padding([6, 16]),
|
||||
delete_button,
|
||||
]
|
||||
.spacing(8),
|
||||
]
|
||||
.spacing(8)
|
||||
.padding(12)
|
||||
.into()
|
||||
} else {
|
||||
Space::new(0, 0).into()
|
||||
container(text(t(locale, "tags.selectTag")).size(12).color(Color::from_rgb(0.60, 0.60, 0.65)))
|
||||
.padding([8, 0])
|
||||
.into()
|
||||
};
|
||||
|
||||
scrollable(
|
||||
column![
|
||||
create_panel,
|
||||
inputs::section_header(&t(locale, "tags.manageSection")),
|
||||
search,
|
||||
tag_list,
|
||||
edit_panel,
|
||||
]
|
||||
.spacing(12)
|
||||
.padding(16)
|
||||
.width(Length::Fill)
|
||||
.width(Length::Fill),
|
||||
)
|
||||
.width(Length::Fill)
|
||||
.height(Length::Fill)
|
||||
@@ -277,35 +401,53 @@ fn view_manage<'a>(state: &'a TagsViewState, locale: UiLocale) -> Element<'a, Me
|
||||
}
|
||||
|
||||
fn view_merge<'a>(state: &'a TagsViewState, locale: UiLocale) -> Element<'a, Message> {
|
||||
let source_input = inputs::labeled_input(
|
||||
&t(locale, "tags.mergeSource"),
|
||||
&t(locale, "tags.selectTag"),
|
||||
state.merge_source.as_deref().unwrap_or(""),
|
||||
|s| Message::Tags(TagsMsg::SetMergeSource(s)),
|
||||
);
|
||||
let target_input = inputs::labeled_input(
|
||||
&t(locale, "tags.mergeTarget"),
|
||||
&t(locale, "tags.selectTag"),
|
||||
state.merge_target.as_deref().unwrap_or(""),
|
||||
|s| Message::Tags(TagsMsg::SetMergeTarget(s)),
|
||||
);
|
||||
if state.selected_tags.len() < 2 {
|
||||
return container(text(t(locale, "tags.mergeHelp")).size(12).color(Color::from_rgb(0.60, 0.60, 0.65)))
|
||||
.padding(16)
|
||||
.into();
|
||||
}
|
||||
|
||||
let can_merge = state.merge_source.is_some() && state.merge_target.is_some();
|
||||
let tag_options = selected_tag_options(state);
|
||||
let selected_target = state
|
||||
.merge_target
|
||||
.as_ref()
|
||||
.and_then(|target_id| tag_options.iter().find(|option| &option.id == target_id));
|
||||
|
||||
let merge_btn = if can_merge {
|
||||
button(text(t(locale, "tags.merge")).size(13))
|
||||
.on_press(Message::Tags(TagsMsg::MergeTags))
|
||||
.style(inputs::primary_button)
|
||||
.padding([6, 16])
|
||||
} else {
|
||||
button(text(t(locale, "tags.merge")).size(13))
|
||||
.padding([6, 16])
|
||||
};
|
||||
let merge_preview = state
|
||||
.merge_target
|
||||
.as_ref()
|
||||
.map(|target_id| {
|
||||
selected_tag_options(state)
|
||||
.into_iter()
|
||||
.filter(|option| &option.id != target_id)
|
||||
.map(|option| option.name)
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
column![
|
||||
source_input,
|
||||
target_input,
|
||||
merge_btn,
|
||||
inputs::section_header(&t(locale, "tags.mergeSection")),
|
||||
text(tw(locale, "tags.selectedCount", &[("count", &state.selected_tags.len().to_string())]))
|
||||
.size(12)
|
||||
.color(Color::from_rgb(0.75, 0.77, 0.82)),
|
||||
inputs::labeled_select(
|
||||
&t(locale, "tags.mergeTarget"),
|
||||
&tag_options,
|
||||
selected_target,
|
||||
|choice| Message::Tags(TagsMsg::SetMergeTarget(choice)),
|
||||
),
|
||||
if merge_preview.is_empty() {
|
||||
Element::from(Space::new(0, 0))
|
||||
} else {
|
||||
container(text(tw(locale, "tags.mergePreview", &[("tags", &merge_preview)])).size(12))
|
||||
.padding([4, 0])
|
||||
.into()
|
||||
},
|
||||
button(text(t(locale, "tags.merge")).size(13))
|
||||
.on_press_maybe(state.merge_target.is_some().then_some(Message::Tags(TagsMsg::MergeTags)))
|
||||
.style(inputs::primary_button)
|
||||
.padding([6, 16]),
|
||||
]
|
||||
.spacing(12)
|
||||
.padding(16)
|
||||
@@ -313,6 +455,81 @@ fn view_merge<'a>(state: &'a TagsViewState, locale: UiLocale) -> Element<'a, Mes
|
||||
.into()
|
||||
}
|
||||
|
||||
fn view_discover<'a>(_state: &'a TagsViewState, locale: UiLocale) -> Element<'a, Message> {
|
||||
column![
|
||||
inputs::section_header(&t(locale, "tags.discoverSection")),
|
||||
text(t(locale, "tags.discoverDescription"))
|
||||
.size(12)
|
||||
.color(Color::from_rgb(0.60, 0.60, 0.65)),
|
||||
button(text(t(locale, "tags.discoverButton")).size(13))
|
||||
.on_press(Message::Tags(TagsMsg::SyncTags))
|
||||
.style(inputs::primary_button)
|
||||
.padding([6, 16]),
|
||||
]
|
||||
.spacing(12)
|
||||
.padding(16)
|
||||
.width(Length::Fill)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn template_options(state: &TagsViewState, locale: UiLocale) -> Vec<TemplateOption> {
|
||||
let mut options = vec![TemplateOption {
|
||||
slug: String::new(),
|
||||
label: t(locale, "tags.noTemplate"),
|
||||
}];
|
||||
options.extend(state.template_options.iter().map(|slug| TemplateOption {
|
||||
slug: slug.clone(),
|
||||
label: slug.clone(),
|
||||
}));
|
||||
options
|
||||
}
|
||||
|
||||
fn selected_tag_options(state: &TagsViewState) -> Vec<TagOption> {
|
||||
state
|
||||
.selected_tags
|
||||
.iter()
|
||||
.filter_map(|tag_id| {
|
||||
state.tags.iter().find(|tag| &tag.id == tag_id).map(|tag| TagOption {
|
||||
id: tag.id.clone(),
|
||||
name: tag.name.clone(),
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn color_swatches<'a>(locale: UiLocale, create_mode: bool) -> Element<'a, Message> {
|
||||
let buttons: Vec<Element<'a, Message>> = COLOR_PRESETS
|
||||
.iter()
|
||||
.map(|hex| {
|
||||
let color = parse_tag_color(hex);
|
||||
let msg = if create_mode {
|
||||
TagsMsg::CreateColorChanged((*hex).to_string())
|
||||
} else {
|
||||
TagsMsg::EditTagColor((*hex).to_string())
|
||||
};
|
||||
button(Space::new(18, 18))
|
||||
.on_press(Message::Tags(msg))
|
||||
.padding(0)
|
||||
.style(move |_theme: &Theme, _status| button::Style {
|
||||
background: Some(Background::Color(color)),
|
||||
border: iced::Border {
|
||||
radius: 9.0.into(),
|
||||
..iced::Border::default()
|
||||
},
|
||||
..button::Style::default()
|
||||
})
|
||||
.into()
|
||||
})
|
||||
.collect();
|
||||
|
||||
column![
|
||||
text(t(locale, "tags.colorPresets")).size(12).color(Color::from_rgb(0.55, 0.58, 0.65)),
|
||||
row(buttons).spacing(6).wrap(),
|
||||
]
|
||||
.spacing(6)
|
||||
.into()
|
||||
}
|
||||
|
||||
fn parse_tag_color(hex: &str) -> Color {
|
||||
let hex = hex.trim_start_matches('#');
|
||||
if hex.len() == 6 {
|
||||
@@ -323,4 +540,4 @@ fn parse_tag_color(hex: &str) -> Color {
|
||||
} else {
|
||||
Color::from_rgb(0.4, 0.5, 0.7)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -141,12 +141,18 @@ pub fn view<'a>(
|
||||
let active_tab_is_post = active_tab_type == Some(&TabType::Post);
|
||||
let active_tab_is_post_or_media =
|
||||
active_tab_is_post || active_tab_type == Some(&TabType::Media);
|
||||
let (post_outlinks, post_backlinks) = active_tab
|
||||
.and_then(|id| post_editors.get(id))
|
||||
.map(|editor| (editor.outlinks.as_slice(), editor.backlinks.as_slice()))
|
||||
.unwrap_or((&[], &[]));
|
||||
|
||||
right_col = right_col.push(separator_h());
|
||||
right_col = right_col.push(panel::view(
|
||||
panel_tab,
|
||||
task_snapshots,
|
||||
output_entries,
|
||||
post_outlinks,
|
||||
post_backlinks,
|
||||
locale,
|
||||
active_tab_is_post,
|
||||
active_tab_is_post_or_media,
|
||||
|
||||
@@ -159,6 +159,7 @@
|
||||
"tags.nav.cloud": "Tag-Wolke",
|
||||
"tags.nav.manage": "Tags verwalten",
|
||||
"tags.nav.merge": "Tags zusammenführen",
|
||||
"tags.nav.discover": "Tags entdecken",
|
||||
"sidebar.chatYesterday": "Gestern",
|
||||
"modal.confirmDelete.title": "Löschen bestätigen",
|
||||
"modal.confirmDelete.warning": "Diese Aktion kann nicht rückgängig gemacht werden.",
|
||||
@@ -236,6 +237,11 @@
|
||||
"editor.linkedMedia": "Verknüpfte Medien",
|
||||
"editor.linkedMediaEmpty": "Noch keine verknüpften Medien.",
|
||||
"editor.linkExistingMedia": "Vorhandenes verknüpfen",
|
||||
"editor.linkToPost": "Mit Beitrag verknüpfen",
|
||||
"editor.linkedPosts": "Verknüpfte Beiträge",
|
||||
"editor.linkedPostsEmpty": "Mit keinen Beiträgen verknüpft.",
|
||||
"editor.postPickerSearch": "Beiträge suchen",
|
||||
"editor.postPickerSearchPlaceholder": "Beiträge suchen...",
|
||||
"editor.unlinkMedia": "Verknüpfung lösen",
|
||||
"editor.linkedMediaKindImage": "Bild",
|
||||
"editor.linkedMediaKindFile": "Datei",
|
||||
@@ -244,14 +250,34 @@
|
||||
"editor.updatedAt": "Aktualisiert",
|
||||
"editor.publishedAt": "Veröffentlicht",
|
||||
"tags.noTags": "Noch keine Tags",
|
||||
"tags.cloudSection": "Wolke",
|
||||
"tags.cloudHelp": "Wählen Sie einen oder mehrere Tags aus, um sie zu bearbeiten, zu löschen oder zusammenzuführen.",
|
||||
"tags.selectedCount": "{count} ausgewählt",
|
||||
"tags.clearSelection": "Auswahl aufheben",
|
||||
"tags.createSection": "Tag erstellen",
|
||||
"tags.createPlaceholder": "neues-tag",
|
||||
"tags.createButton": "Erstellen",
|
||||
"tags.editTag": "Tag bearbeiten",
|
||||
"tags.manageSection": "Verwalten",
|
||||
"tags.name": "Name",
|
||||
"tags.color": "Farbe",
|
||||
"tags.colorPresets": "Farbvorgaben",
|
||||
"tags.postTemplate": "Beitragsvorlage",
|
||||
"tags.noTemplate": "Keine",
|
||||
"tags.merge": "Zusammenführen",
|
||||
"tags.mergeSource": "Quell-Tag",
|
||||
"tags.mergeTarget": "Ziel-Tag",
|
||||
"tags.mergeSection": "Zusammenführen",
|
||||
"tags.mergeHelp": "Wählen Sie mindestens zwei Tags aus der Wolke oder Liste aus, um sie zusammenzuführen.",
|
||||
"tags.mergePreview": "Zu löschende Tags: {tags}",
|
||||
"tags.mergeConfirmTitle": "Tags zusammenführen",
|
||||
"tags.mergeConfirmMessage": "{count} Tags in {target} zusammenführen? Dies kann nicht rückgängig gemacht werden.",
|
||||
"tags.deleteUsage": "Dieses Tag wird in {count} Beiträgen verwendet.",
|
||||
"tags.discoverSection": "Entdecken",
|
||||
"tags.discoverDescription": "Tags aus aktuellen Beiträgen entdecken und tags.json passend zur Datenbank neu schreiben.",
|
||||
"tags.discoverButton": "Entdecken",
|
||||
"tags.selectTag": "Tag zum Bearbeiten auswählen",
|
||||
"template.forceDeleteTitle": "Vorlage zwangsweise löschen",
|
||||
"template.forceDeleteMessage": "Diese Vorlage wird von {posts} Beiträgen und {tags} Tags verwendet. Trotzdem löschen?",
|
||||
"settings.projectName": "Projektname",
|
||||
"settings.projectDescription": "Beschreibung",
|
||||
"settings.dataPath": "Datenordner",
|
||||
|
||||
@@ -159,6 +159,7 @@
|
||||
"tags.nav.cloud": "Tag Cloud",
|
||||
"tags.nav.manage": "Manage Tags",
|
||||
"tags.nav.merge": "Merge Tags",
|
||||
"tags.nav.discover": "Discover Tags",
|
||||
"sidebar.chatYesterday": "Yesterday",
|
||||
"modal.confirmDelete.title": "Confirm Delete",
|
||||
"modal.confirmDelete.warning": "This action cannot be undone.",
|
||||
@@ -233,6 +234,11 @@
|
||||
"editor.linkedMedia": "Linked Media",
|
||||
"editor.linkedMediaEmpty": "No linked media yet.",
|
||||
"editor.linkExistingMedia": "Link Existing",
|
||||
"editor.linkToPost": "Link to Post",
|
||||
"editor.linkedPosts": "Linked Posts",
|
||||
"editor.linkedPostsEmpty": "Not linked to any posts.",
|
||||
"editor.postPickerSearch": "Search Posts",
|
||||
"editor.postPickerSearchPlaceholder": "Search posts...",
|
||||
"editor.unlinkMedia": "Unlink",
|
||||
"editor.linkedMediaKindImage": "Image",
|
||||
"editor.linkedMediaKindFile": "File",
|
||||
@@ -244,14 +250,34 @@
|
||||
"editor.insertMedia": "Insert Media",
|
||||
"editor.gallery": "Gallery",
|
||||
"tags.noTags": "No tags yet",
|
||||
"tags.cloudSection": "Cloud",
|
||||
"tags.cloudHelp": "Select one or more tags to edit, delete, or merge them.",
|
||||
"tags.selectedCount": "{count} selected",
|
||||
"tags.clearSelection": "Clear selection",
|
||||
"tags.createSection": "Create Tag",
|
||||
"tags.createPlaceholder": "new-tag",
|
||||
"tags.createButton": "Create",
|
||||
"tags.editTag": "Edit Tag",
|
||||
"tags.manageSection": "Manage",
|
||||
"tags.name": "Name",
|
||||
"tags.color": "Color",
|
||||
"tags.colorPresets": "Color presets",
|
||||
"tags.postTemplate": "Post Template",
|
||||
"tags.noTemplate": "None",
|
||||
"tags.merge": "Merge",
|
||||
"tags.mergeSource": "Source Tag",
|
||||
"tags.mergeTarget": "Target Tag",
|
||||
"tags.mergeSection": "Merge",
|
||||
"tags.mergeHelp": "Select at least two tags from the cloud or list to merge them.",
|
||||
"tags.mergePreview": "Tags to delete: {tags}",
|
||||
"tags.mergeConfirmTitle": "Merge Tags",
|
||||
"tags.mergeConfirmMessage": "Merge {count} tags into {target}? This cannot be undone.",
|
||||
"tags.deleteUsage": "This tag is used in {count} posts.",
|
||||
"tags.discoverSection": "Discover",
|
||||
"tags.discoverDescription": "Discover tags from current posts and rewrite tags.json to match the database.",
|
||||
"tags.discoverButton": "Discover",
|
||||
"tags.selectTag": "Select a tag to edit",
|
||||
"template.forceDeleteTitle": "Force Delete Template",
|
||||
"template.forceDeleteMessage": "This template is used by {posts} posts and {tags} tags. Force delete?",
|
||||
"settings.projectName": "Project Name",
|
||||
"settings.projectDescription": "Description",
|
||||
"settings.dataPath": "Data Folder",
|
||||
|
||||
@@ -159,6 +159,7 @@
|
||||
"tags.nav.cloud": "Nube de etiquetas",
|
||||
"tags.nav.manage": "Gestionar etiquetas",
|
||||
"tags.nav.merge": "Fusionar etiquetas",
|
||||
"tags.nav.discover": "Descubrir etiquetas",
|
||||
"sidebar.chatYesterday": "Ayer",
|
||||
"modal.confirmDelete.title": "Confirmar eliminación",
|
||||
"modal.confirmDelete.warning": "Esta acción no se puede deshacer.",
|
||||
@@ -236,6 +237,11 @@
|
||||
"editor.linkedMedia": "Medios vinculados",
|
||||
"editor.linkedMediaEmpty": "Todavía no hay medios vinculados.",
|
||||
"editor.linkExistingMedia": "Vincular existente",
|
||||
"editor.linkToPost": "Vincular a artículo",
|
||||
"editor.linkedPosts": "Artículos vinculados",
|
||||
"editor.linkedPostsEmpty": "No está vinculado a ningún artículo.",
|
||||
"editor.postPickerSearch": "Buscar artículos",
|
||||
"editor.postPickerSearchPlaceholder": "Buscar artículos...",
|
||||
"editor.unlinkMedia": "Desvincular",
|
||||
"editor.linkedMediaKindImage": "Imagen",
|
||||
"editor.linkedMediaKindFile": "Archivo",
|
||||
@@ -244,14 +250,34 @@
|
||||
"editor.updatedAt": "Actualizado",
|
||||
"editor.publishedAt": "Publicado",
|
||||
"tags.noTags": "Sin etiquetas",
|
||||
"tags.cloudSection": "Nube",
|
||||
"tags.cloudHelp": "Selecciona una o más etiquetas para editarlas, eliminarlas o fusionarlas.",
|
||||
"tags.selectedCount": "{count} seleccionadas",
|
||||
"tags.clearSelection": "Borrar selección",
|
||||
"tags.createSection": "Crear etiqueta",
|
||||
"tags.createPlaceholder": "nueva-etiqueta",
|
||||
"tags.createButton": "Crear",
|
||||
"tags.editTag": "Editar etiqueta",
|
||||
"tags.manageSection": "Gestionar",
|
||||
"tags.name": "Nombre",
|
||||
"tags.color": "Color",
|
||||
"tags.colorPresets": "Colores predefinidos",
|
||||
"tags.postTemplate": "Plantilla de artículo",
|
||||
"tags.noTemplate": "Ninguna",
|
||||
"tags.merge": "Fusionar",
|
||||
"tags.mergeSource": "Etiqueta origen",
|
||||
"tags.mergeTarget": "Etiqueta destino",
|
||||
"tags.mergeSection": "Fusionar",
|
||||
"tags.mergeHelp": "Selecciona al menos dos etiquetas de la nube o la lista para fusionarlas.",
|
||||
"tags.mergePreview": "Etiquetas para eliminar: {tags}",
|
||||
"tags.mergeConfirmTitle": "Fusionar etiquetas",
|
||||
"tags.mergeConfirmMessage": "¿Fusionar {count} etiquetas en {target}? Esta acción no se puede deshacer.",
|
||||
"tags.deleteUsage": "Esta etiqueta se usa en {count} publicaciones.",
|
||||
"tags.discoverSection": "Descubrir",
|
||||
"tags.discoverDescription": "Descubre etiquetas a partir de las publicaciones actuales y reescribe tags.json para reflejar la base de datos.",
|
||||
"tags.discoverButton": "Descubrir",
|
||||
"tags.selectTag": "Seleccione una etiqueta para editar",
|
||||
"template.forceDeleteTitle": "Forzar eliminación de plantilla",
|
||||
"template.forceDeleteMessage": "Esta plantilla se usa en {posts} publicaciones y {tags} etiquetas. ¿Forzar eliminación?",
|
||||
"settings.projectName": "Nombre del proyecto",
|
||||
"settings.projectDescription": "Descripción",
|
||||
"settings.dataPath": "Carpeta de datos",
|
||||
|
||||
@@ -159,6 +159,7 @@
|
||||
"tags.nav.cloud": "Nuage de tags",
|
||||
"tags.nav.manage": "Gérer les tags",
|
||||
"tags.nav.merge": "Fusionner les tags",
|
||||
"tags.nav.discover": "Découvrir les tags",
|
||||
"sidebar.chatYesterday": "Hier",
|
||||
"modal.confirmDelete.title": "Confirmer la suppression",
|
||||
"modal.confirmDelete.warning": "Cette action est irréversible.",
|
||||
@@ -236,6 +237,11 @@
|
||||
"editor.linkedMedia": "Médias liés",
|
||||
"editor.linkedMediaEmpty": "Aucun média lié pour le moment.",
|
||||
"editor.linkExistingMedia": "Lier un existant",
|
||||
"editor.linkToPost": "Lier à un article",
|
||||
"editor.linkedPosts": "Articles liés",
|
||||
"editor.linkedPostsEmpty": "Lié à aucun article.",
|
||||
"editor.postPickerSearch": "Rechercher des articles",
|
||||
"editor.postPickerSearchPlaceholder": "Rechercher des articles...",
|
||||
"editor.unlinkMedia": "Dissocier",
|
||||
"editor.linkedMediaKindImage": "Image",
|
||||
"editor.linkedMediaKindFile": "Fichier",
|
||||
@@ -244,14 +250,34 @@
|
||||
"editor.updatedAt": "Mis à jour",
|
||||
"editor.publishedAt": "Publié",
|
||||
"tags.noTags": "Aucun tag",
|
||||
"tags.cloudSection": "Nuage",
|
||||
"tags.cloudHelp": "Sélectionnez un ou plusieurs tags pour les modifier, les supprimer ou les fusionner.",
|
||||
"tags.selectedCount": "{count} sélectionnés",
|
||||
"tags.clearSelection": "Effacer la sélection",
|
||||
"tags.createSection": "Créer un tag",
|
||||
"tags.createPlaceholder": "nouveau-tag",
|
||||
"tags.createButton": "Créer",
|
||||
"tags.editTag": "Modifier le tag",
|
||||
"tags.manageSection": "Gérer",
|
||||
"tags.name": "Nom",
|
||||
"tags.color": "Couleur",
|
||||
"tags.colorPresets": "Couleurs prédéfinies",
|
||||
"tags.postTemplate": "Modèle d'article",
|
||||
"tags.noTemplate": "Aucun",
|
||||
"tags.merge": "Fusionner",
|
||||
"tags.mergeSource": "Tag source",
|
||||
"tags.mergeTarget": "Tag cible",
|
||||
"tags.mergeSection": "Fusionner",
|
||||
"tags.mergeHelp": "Sélectionnez au moins deux tags dans le nuage ou la liste pour les fusionner.",
|
||||
"tags.mergePreview": "Tags à supprimer : {tags}",
|
||||
"tags.mergeConfirmTitle": "Fusionner les tags",
|
||||
"tags.mergeConfirmMessage": "Fusionner {count} tags dans {target} ? Cette action est irréversible.",
|
||||
"tags.deleteUsage": "Ce tag est utilisé dans {count} articles.",
|
||||
"tags.discoverSection": "Découvrir",
|
||||
"tags.discoverDescription": "Découvrir les tags à partir des articles actuels et réécrire tags.json pour refléter la base de données.",
|
||||
"tags.discoverButton": "Découvrir",
|
||||
"tags.selectTag": "Sélectionnez un tag à modifier",
|
||||
"template.forceDeleteTitle": "Forcer la suppression du modèle",
|
||||
"template.forceDeleteMessage": "Ce modèle est utilisé par {posts} articles et {tags} tags. Forcer la suppression ?",
|
||||
"settings.projectName": "Nom du projet",
|
||||
"settings.projectDescription": "Description",
|
||||
"settings.dataPath": "Dossier de données",
|
||||
|
||||
@@ -159,6 +159,7 @@
|
||||
"tags.nav.cloud": "Nuvola di tag",
|
||||
"tags.nav.manage": "Gestisci tag",
|
||||
"tags.nav.merge": "Unisci tag",
|
||||
"tags.nav.discover": "Scopri tag",
|
||||
"sidebar.chatYesterday": "Ieri",
|
||||
"modal.confirmDelete.title": "Conferma eliminazione",
|
||||
"modal.confirmDelete.warning": "Questa azione non può essere annullata.",
|
||||
@@ -236,6 +237,11 @@
|
||||
"editor.linkedMedia": "Media collegati",
|
||||
"editor.linkedMediaEmpty": "Nessun media collegato.",
|
||||
"editor.linkExistingMedia": "Collega esistente",
|
||||
"editor.linkToPost": "Collega a articolo",
|
||||
"editor.linkedPosts": "Articoli collegati",
|
||||
"editor.linkedPostsEmpty": "Non collegato ad alcun articolo.",
|
||||
"editor.postPickerSearch": "Cerca articoli",
|
||||
"editor.postPickerSearchPlaceholder": "Cerca articoli...",
|
||||
"editor.unlinkMedia": "Scollega",
|
||||
"editor.linkedMediaKindImage": "Immagine",
|
||||
"editor.linkedMediaKindFile": "File",
|
||||
@@ -244,14 +250,34 @@
|
||||
"editor.updatedAt": "Aggiornato",
|
||||
"editor.publishedAt": "Pubblicato",
|
||||
"tags.noTags": "Nessun tag",
|
||||
"tags.cloudSection": "Nuvola",
|
||||
"tags.cloudHelp": "Seleziona uno o più tag per modificarli, eliminarli o unirli.",
|
||||
"tags.selectedCount": "{count} selezionati",
|
||||
"tags.clearSelection": "Cancella selezione",
|
||||
"tags.createSection": "Crea tag",
|
||||
"tags.createPlaceholder": "nuovo-tag",
|
||||
"tags.createButton": "Crea",
|
||||
"tags.editTag": "Modifica tag",
|
||||
"tags.manageSection": "Gestisci",
|
||||
"tags.name": "Nome",
|
||||
"tags.color": "Colore",
|
||||
"tags.colorPresets": "Colori predefiniti",
|
||||
"tags.postTemplate": "Modello articolo",
|
||||
"tags.noTemplate": "Nessuno",
|
||||
"tags.merge": "Unisci",
|
||||
"tags.mergeSource": "Tag sorgente",
|
||||
"tags.mergeTarget": "Tag destinazione",
|
||||
"tags.mergeSection": "Unisci",
|
||||
"tags.mergeHelp": "Seleziona almeno due tag dalla nuvola o dall'elenco per unirli.",
|
||||
"tags.mergePreview": "Tag da eliminare: {tags}",
|
||||
"tags.mergeConfirmTitle": "Unisci tag",
|
||||
"tags.mergeConfirmMessage": "Unire {count} tag in {target}? Questa azione non può essere annullata.",
|
||||
"tags.deleteUsage": "Questo tag è usato in {count} post.",
|
||||
"tags.discoverSection": "Scopri",
|
||||
"tags.discoverDescription": "Scopri i tag dai post correnti e riscrivi tags.json per riflettere il database.",
|
||||
"tags.discoverButton": "Scopri",
|
||||
"tags.selectTag": "Seleziona un tag da modificare",
|
||||
"template.forceDeleteTitle": "Forza eliminazione template",
|
||||
"template.forceDeleteMessage": "Questo template è usato da {posts} post e {tags} tag. Forzare l'eliminazione?",
|
||||
"settings.projectName": "Nome progetto",
|
||||
"settings.projectDescription": "Descrizione",
|
||||
"settings.dataPath": "Cartella dati",
|
||||
|
||||
Reference in New Issue
Block a user