fix: and even more fixes for M3
This commit is contained in:
@@ -1002,6 +1002,7 @@ impl BdsApp {
|
||||
MediaEditorMsg::AltChanged(s) => { state.alt = s; state.is_dirty = true; }
|
||||
MediaEditorMsg::CaptionChanged(s) => { state.caption = s; state.is_dirty = true; }
|
||||
MediaEditorMsg::AuthorChanged(s) => { state.author = s; state.is_dirty = true; }
|
||||
MediaEditorMsg::SwitchLanguage(lang) => { state.switch_language(&lang); }
|
||||
MediaEditorMsg::Save => {
|
||||
return self.save_media_editor(&tab_id);
|
||||
}
|
||||
@@ -1134,11 +1135,31 @@ impl BdsApp {
|
||||
Message::PostLoaded(result) => {
|
||||
match result {
|
||||
Ok(post) => {
|
||||
let translations = self.db.as_ref()
|
||||
let mut translations = self.db.as_ref()
|
||||
.and_then(|db| bds_core::db::queries::post_translation::list_post_translations_by_post(
|
||||
db.conn(), &post.id,
|
||||
).ok())
|
||||
.unwrap_or_default();
|
||||
// Published translations don't store body in DB — read from file
|
||||
if let Some(ref data_dir) = self.data_dir {
|
||||
for tr in &mut translations {
|
||||
if tr.content.is_none() {
|
||||
let rel = bds_core::util::paths::translation_file_path(
|
||||
post.created_at,
|
||||
&post.slug,
|
||||
&tr.language,
|
||||
);
|
||||
let path = data_dir.join(&rel);
|
||||
if let Ok(raw) = std::fs::read_to_string(&path) {
|
||||
if let Ok((_fm, body)) =
|
||||
bds_core::util::frontmatter::read_translation_file(&raw)
|
||||
{
|
||||
tr.content = Some(body);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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);
|
||||
@@ -1150,7 +1171,12 @@ impl BdsApp {
|
||||
Message::MediaLoaded(result) => {
|
||||
match result {
|
||||
Ok(media) => {
|
||||
let state = MediaEditorState::from_media(&media);
|
||||
let translations = self.db.as_ref()
|
||||
.and_then(|db| bds_core::db::queries::media_translation::list_media_translations_by_media(
|
||||
db.conn(), &media.id,
|
||||
).ok())
|
||||
.unwrap_or_default();
|
||||
let state = MediaEditorState::from_media(&media, &self.blog_languages, &translations);
|
||||
self.media_editors.insert(media.id.clone(), state);
|
||||
}
|
||||
Err(e) => self.notify(ToastLevel::Error, &e),
|
||||
@@ -1873,13 +1899,22 @@ impl BdsApp {
|
||||
fn handle_tags_msg(&mut self, msg: TagsMsg) -> Task<Message> {
|
||||
// Ensure tags view state exists
|
||||
if self.tags_view_state.is_none() {
|
||||
let tags = if let (Some(db), Some(project)) = (&self.db, &self.active_project) {
|
||||
bds_core::db::queries::tag::list_tags_by_project(db.conn(), &project.id)
|
||||
.unwrap_or_default()
|
||||
let (tags, counts) = if let (Some(db), Some(project)) = (&self.db, &self.active_project) {
|
||||
let tags = bds_core::db::queries::tag::list_tags_by_project(db.conn(), &project.id)
|
||||
.unwrap_or_default();
|
||||
let posts = bds_core::db::queries::post::list_posts_by_project(db.conn(), &project.id)
|
||||
.unwrap_or_default();
|
||||
let mut counts = std::collections::HashMap::new();
|
||||
for post in &posts {
|
||||
for tag_name in &post.tags {
|
||||
*counts.entry(tag_name.to_lowercase()).or_insert(0usize) += 1;
|
||||
}
|
||||
}
|
||||
(tags, counts)
|
||||
} else {
|
||||
Vec::new()
|
||||
(Vec::new(), std::collections::HashMap::new())
|
||||
};
|
||||
self.tags_view_state = Some(TagsViewState::new(tags));
|
||||
self.tags_view_state = Some(TagsViewState::new(tags, counts));
|
||||
}
|
||||
let state = self.tags_view_state.as_mut().unwrap();
|
||||
match msg {
|
||||
@@ -2042,6 +2077,18 @@ impl BdsApp {
|
||||
let _ = open::that(dir);
|
||||
}
|
||||
}
|
||||
SettingsMsg::FocusSection(section) => {
|
||||
// Expand the target section, collapse all others
|
||||
use crate::views::settings_view::SettingsSection;
|
||||
let all_others: Vec<SettingsSection> = SettingsSection::all()
|
||||
.iter()
|
||||
.filter(|s| **s != section)
|
||||
.cloned()
|
||||
.collect();
|
||||
state.collapsed = all_others;
|
||||
// Clear search filter to ensure section is visible
|
||||
state.search_query.clear();
|
||||
}
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
@@ -2104,9 +2151,29 @@ impl BdsApp {
|
||||
}
|
||||
}
|
||||
// Load translations for translation flags bar
|
||||
let translations = bds_core::db::queries::post_translation::list_post_translations_by_post(
|
||||
let mut translations = bds_core::db::queries::post_translation::list_post_translations_by_post(
|
||||
db.conn(), &post.id,
|
||||
).unwrap_or_default();
|
||||
// Published translations don't store body in DB — read from file
|
||||
if let Some(ref data_dir) = self.data_dir {
|
||||
for tr in &mut translations {
|
||||
if tr.content.is_none() {
|
||||
let rel = bds_core::util::paths::translation_file_path(
|
||||
post.created_at,
|
||||
&post.slug,
|
||||
&tr.language,
|
||||
);
|
||||
let path = data_dir.join(&rel);
|
||||
if let Ok(raw) = std::fs::read_to_string(&path) {
|
||||
if let Ok((_fm, body)) =
|
||||
bds_core::util::frontmatter::read_translation_file(&raw)
|
||||
{
|
||||
tr.content = Some(body);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let (outlinks, backlinks) = self.load_post_links(&post.id);
|
||||
self.post_editors.insert(
|
||||
post.id.clone(),
|
||||
@@ -2123,7 +2190,10 @@ impl BdsApp {
|
||||
if !self.media_editors.contains_key(&tab.id) {
|
||||
match bds_core::db::queries::media::get_media_by_id(db.conn(), &tab.id) {
|
||||
Ok(media) => {
|
||||
self.media_editors.insert(media.id.clone(), MediaEditorState::from_media(&media));
|
||||
let translations = bds_core::db::queries::media_translation::list_media_translations_by_media(
|
||||
db.conn(), &media.id,
|
||||
).unwrap_or_default();
|
||||
self.media_editors.insert(media.id.clone(), MediaEditorState::from_media(&media, &self.blog_languages, &translations));
|
||||
}
|
||||
Err(e) => {
|
||||
self.notify(ToastLevel::Error, &format!("Failed to load media: {e}"));
|
||||
@@ -2185,17 +2255,30 @@ impl BdsApp {
|
||||
TabType::Tags => {
|
||||
if self.tags_view_state.is_none() {
|
||||
let project_id = self.active_project.as_ref().map(|p| p.id.as_str()).unwrap_or("");
|
||||
// Sync tags from posts first to ensure tags table is populated
|
||||
// Import tags from file first, then sync from posts (additive only)
|
||||
if let Some(ref data_dir) = self.data_dir {
|
||||
let _ = bds_core::engine::tag::sync_tags_from_posts(
|
||||
let _ = bds_core::engine::tag::import_tags_from_file(
|
||||
db.conn(), data_dir, project_id,
|
||||
);
|
||||
let _ = bds_core::engine::tag::sync_tags_from_posts(
|
||||
db.conn(), project_id,
|
||||
);
|
||||
}
|
||||
let tags = bds_core::db::queries::tag::list_tags_by_project(
|
||||
db.conn(),
|
||||
project_id,
|
||||
).unwrap_or_default();
|
||||
self.tags_view_state = Some(TagsViewState::new(tags));
|
||||
// Compute post counts per tag for cloud sizing
|
||||
let posts = bds_core::db::queries::post::list_posts_by_project(
|
||||
db.conn(), project_id,
|
||||
).unwrap_or_default();
|
||||
let mut tag_post_counts = std::collections::HashMap::new();
|
||||
for post in &posts {
|
||||
for tag_name in &post.tags {
|
||||
*tag_post_counts.entry(tag_name.to_lowercase()).or_insert(0usize) += 1;
|
||||
}
|
||||
}
|
||||
self.tags_view_state = Some(TagsViewState::new(tags, tag_post_counts));
|
||||
}
|
||||
}
|
||||
TabType::Settings => {
|
||||
|
||||
@@ -1,14 +1,26 @@
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
|
||||
use iced::widget::{button, column, container, image, row, scrollable, text, Space};
|
||||
use iced::widget::text::Shaping;
|
||||
use iced::{Color, Element, Length};
|
||||
|
||||
use bds_core::i18n::UiLocale;
|
||||
use bds_core::model::Media;
|
||||
use bds_core::i18n::{self, UiLocale};
|
||||
use bds_core::model::{Media, MediaTranslation};
|
||||
|
||||
use crate::app::Message;
|
||||
use crate::components::inputs;
|
||||
use crate::i18n::t;
|
||||
use crate::views::post_editor::TranslationFlag;
|
||||
|
||||
/// Saved draft content for a single media translation language.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MediaTranslationDraft {
|
||||
pub title: String,
|
||||
pub alt: String,
|
||||
pub caption: String,
|
||||
pub is_dirty: bool,
|
||||
}
|
||||
|
||||
/// State for an open media editor.
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -30,10 +42,28 @@ pub struct MediaEditorState {
|
||||
pub created_at: i64,
|
||||
pub updated_at: i64,
|
||||
pub is_dirty: bool,
|
||||
// ── Translation flags ──
|
||||
pub active_language: String,
|
||||
pub canonical_language: String,
|
||||
pub blog_languages: Vec<String>,
|
||||
pub saved_canonical: Option<MediaTranslationDraft>,
|
||||
pub translation_drafts: HashMap<String, MediaTranslationDraft>,
|
||||
}
|
||||
|
||||
impl MediaEditorState {
|
||||
pub fn from_media(media: &Media) -> Self {
|
||||
pub fn from_media(media: &Media, blog_languages: &[String], translations: &[MediaTranslation]) -> Self {
|
||||
let canonical_lang = media.language.clone().unwrap_or_else(|| "en".to_string());
|
||||
|
||||
let mut translation_drafts = HashMap::new();
|
||||
for tr in translations {
|
||||
translation_drafts.insert(tr.language.clone(), MediaTranslationDraft {
|
||||
title: tr.title.clone().unwrap_or_default(),
|
||||
alt: tr.alt.clone().unwrap_or_default(),
|
||||
caption: tr.caption.clone().unwrap_or_default(),
|
||||
is_dirty: false,
|
||||
});
|
||||
}
|
||||
|
||||
Self {
|
||||
media_id: media.id.clone(),
|
||||
filename: media.filename.clone(),
|
||||
@@ -46,14 +76,90 @@ impl MediaEditorState {
|
||||
alt: media.alt.clone().unwrap_or_default(),
|
||||
caption: media.caption.clone().unwrap_or_default(),
|
||||
author: media.author.clone().unwrap_or_default(),
|
||||
language: media.language.clone().unwrap_or_default(),
|
||||
language: canonical_lang.clone(),
|
||||
file_path: media.file_path.clone(),
|
||||
tags: media.tags.clone(),
|
||||
created_at: media.created_at,
|
||||
updated_at: media.updated_at,
|
||||
is_dirty: false,
|
||||
active_language: canonical_lang.clone(),
|
||||
canonical_language: canonical_lang,
|
||||
blog_languages: blog_languages.to_vec(),
|
||||
saved_canonical: None,
|
||||
translation_drafts,
|
||||
}
|
||||
}
|
||||
|
||||
/// Switch to a different language. Saves current fields, loads target.
|
||||
pub fn switch_language(&mut self, target_lang: &str) {
|
||||
if target_lang == self.active_language {
|
||||
return;
|
||||
}
|
||||
// Save current fields
|
||||
if self.active_language == self.canonical_language {
|
||||
self.saved_canonical = Some(MediaTranslationDraft {
|
||||
title: self.title.clone(),
|
||||
alt: self.alt.clone(),
|
||||
caption: self.caption.clone(),
|
||||
is_dirty: self.is_dirty,
|
||||
});
|
||||
} else {
|
||||
self.translation_drafts.insert(self.active_language.clone(), MediaTranslationDraft {
|
||||
title: self.title.clone(),
|
||||
alt: self.alt.clone(),
|
||||
caption: self.caption.clone(),
|
||||
is_dirty: self.is_dirty,
|
||||
});
|
||||
}
|
||||
// Load target fields
|
||||
if target_lang == self.canonical_language {
|
||||
if let Some(saved) = &self.saved_canonical {
|
||||
self.title = saved.title.clone();
|
||||
self.alt = saved.alt.clone();
|
||||
self.caption = saved.caption.clone();
|
||||
self.is_dirty = saved.is_dirty;
|
||||
}
|
||||
} else if let Some(draft) = self.translation_drafts.get(target_lang) {
|
||||
self.title = draft.title.clone();
|
||||
self.alt = draft.alt.clone();
|
||||
self.caption = draft.caption.clone();
|
||||
self.is_dirty = draft.is_dirty;
|
||||
} else {
|
||||
self.title = String::new();
|
||||
self.alt = String::new();
|
||||
self.caption = String::new();
|
||||
self.is_dirty = false;
|
||||
}
|
||||
self.active_language = target_lang.to_string();
|
||||
}
|
||||
|
||||
/// Build translation flags for the view.
|
||||
pub fn translation_flags(&self) -> Vec<TranslationFlag> {
|
||||
if self.translation_drafts.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
let mut flags = Vec::new();
|
||||
let canon = &self.canonical_language;
|
||||
let canon_locale = i18n::normalize_language(canon);
|
||||
flags.push(TranslationFlag {
|
||||
language: canon.clone(),
|
||||
flag_emoji: canon_locale.flag_emoji().to_string(),
|
||||
status: "canonical".to_string(),
|
||||
is_active: self.active_language == *canon,
|
||||
});
|
||||
let mut langs: Vec<&String> = self.translation_drafts.keys().collect();
|
||||
langs.sort();
|
||||
for lang in langs {
|
||||
let locale = i18n::normalize_language(lang);
|
||||
flags.push(TranslationFlag {
|
||||
language: lang.clone(),
|
||||
flag_emoji: locale.flag_emoji().to_string(),
|
||||
status: "translation".to_string(),
|
||||
is_active: self.active_language == **lang,
|
||||
});
|
||||
}
|
||||
flags
|
||||
}
|
||||
}
|
||||
|
||||
/// Media editor messages.
|
||||
@@ -63,6 +169,7 @@ pub enum MediaEditorMsg {
|
||||
AltChanged(String),
|
||||
CaptionChanged(String),
|
||||
AuthorChanged(String),
|
||||
SwitchLanguage(String),
|
||||
Save,
|
||||
Delete,
|
||||
}
|
||||
@@ -91,6 +198,30 @@ pub fn view<'a>(
|
||||
],
|
||||
);
|
||||
|
||||
// Translation flags bar
|
||||
let flags = state.translation_flags();
|
||||
let flags_bar: Element<'a, Message> = if flags.is_empty() {
|
||||
Space::new(0, 0).into()
|
||||
} else {
|
||||
let flag_buttons: Vec<Element<'a, Message>> = flags
|
||||
.iter()
|
||||
.map(|flag| {
|
||||
let label = format!("{} {}", flag.flag_emoji, flag.language);
|
||||
let color = if flag.is_active {
|
||||
Color::WHITE
|
||||
} else {
|
||||
Color::from_rgb(0.55, 0.58, 0.65)
|
||||
};
|
||||
button(text(label).size(12).shaping(Shaping::Advanced).color(color))
|
||||
.on_press(Message::MediaEditor(MediaEditorMsg::SwitchLanguage(flag.language.clone())))
|
||||
.padding([4, 8])
|
||||
.style(|_: &iced::Theme, _| button::Style::default())
|
||||
.into()
|
||||
})
|
||||
.collect();
|
||||
row(flag_buttons).spacing(4).into()
|
||||
};
|
||||
|
||||
// Preview section
|
||||
let preview: Element<'a, Message> = if state.mime_type.starts_with("image/") {
|
||||
if let Some(dir) = data_dir {
|
||||
@@ -166,6 +297,7 @@ pub fn view<'a>(
|
||||
let body = scrollable(
|
||||
column![
|
||||
header,
|
||||
flags_bar,
|
||||
preview,
|
||||
info,
|
||||
inputs::section_header(&t(locale, "editor.metadata")),
|
||||
|
||||
@@ -301,6 +301,7 @@ pub enum PostEditorMsg {
|
||||
pub fn view<'a>(
|
||||
state: &'a PostEditorState,
|
||||
locale: UiLocale,
|
||||
word_wrap: bool,
|
||||
) -> Element<'a, Message> {
|
||||
// ── Header bar ──
|
||||
let dirty_indicator = if state.is_dirty { " \u{25CF}" } else { "" };
|
||||
@@ -521,6 +522,7 @@ pub fn view<'a>(
|
||||
highlighter(),
|
||||
"md",
|
||||
)
|
||||
.word_wrap(word_wrap)
|
||||
.on_change(|msg| match msg {
|
||||
EditorMessage::ContentChanged(s) => Message::PostEditor(PostEditorMsg::ContentChanged(s)),
|
||||
EditorMessage::SaveRequested => Message::PostEditor(PostEditorMsg::Save),
|
||||
|
||||
@@ -174,6 +174,8 @@ pub enum SettingsMsg {
|
||||
RebuildLinks,
|
||||
RegenerateThumbnails,
|
||||
OpenDataFolder,
|
||||
/// Navigate to a specific section from sidebar; expand it, collapse all others.
|
||||
FocusSection(SettingsSection),
|
||||
}
|
||||
|
||||
/// Render the settings view.
|
||||
|
||||
@@ -905,35 +905,41 @@ pub fn view(
|
||||
}
|
||||
SidebarView::Settings => {
|
||||
// Per sidebar_views.allium SettingsNav: 9 fixed-order sections
|
||||
let sections = [
|
||||
"settings.nav.project",
|
||||
"settings.nav.editor",
|
||||
"settings.nav.content",
|
||||
"settings.nav.ai",
|
||||
"settings.nav.technology",
|
||||
"settings.nav.publishing",
|
||||
"settings.nav.data",
|
||||
"settings.nav.mcp",
|
||||
"settings.nav.style",
|
||||
use crate::views::settings_view::{SettingsSection, SettingsMsg};
|
||||
let sections: &[(&str, Option<SettingsSection>)] = &[
|
||||
("settings.nav.project", Some(SettingsSection::Project)),
|
||||
("settings.nav.editor", Some(SettingsSection::Editor)),
|
||||
("settings.nav.content", Some(SettingsSection::Content)),
|
||||
("settings.nav.ai", Some(SettingsSection::AI)),
|
||||
("settings.nav.technology", Some(SettingsSection::Technology)),
|
||||
("settings.nav.publishing", Some(SettingsSection::Publishing)),
|
||||
("settings.nav.data", Some(SettingsSection::Data)),
|
||||
("settings.nav.mcp", Some(SettingsSection::MCP)),
|
||||
("settings.nav.style", None),
|
||||
];
|
||||
let items: Vec<Element<'static, Message>> = sections
|
||||
.iter()
|
||||
.map(|key| {
|
||||
.map(|(key, section_opt)| {
|
||||
let label = t(locale, key);
|
||||
let label_text = text(label)
|
||||
.size(12)
|
||||
.shaping(Shaping::Advanced);
|
||||
button(
|
||||
container(label_text)
|
||||
.width(Length::Fill)
|
||||
)
|
||||
.on_press(Message::OpenTab(Tab {
|
||||
let msg = if let Some(section) = section_opt {
|
||||
Message::Settings(SettingsMsg::FocusSection(section.clone()))
|
||||
} else {
|
||||
Message::OpenTab(Tab {
|
||||
id: "settings".to_string(),
|
||||
tab_type: TabType::Settings,
|
||||
title: t(locale, "common.settings"),
|
||||
is_transient: false,
|
||||
is_dirty: false,
|
||||
}))
|
||||
})
|
||||
};
|
||||
button(
|
||||
container(label_text)
|
||||
.width(Length::Fill)
|
||||
)
|
||||
.on_press(msg)
|
||||
.padding([3, 6])
|
||||
.width(Length::Fill)
|
||||
.style(item_style)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use iced::widget::{button, column, container, row, scrollable, text, text_input, Space};
|
||||
use iced::{Alignment, Background, Color, Element, Length, Theme};
|
||||
|
||||
@@ -23,6 +25,7 @@ pub enum TagsSection {
|
||||
pub struct TagsViewState {
|
||||
pub section: TagsSection,
|
||||
pub tags: Vec<Tag>,
|
||||
pub tag_post_counts: HashMap<String, usize>,
|
||||
pub search_query: String,
|
||||
/// For "Manage": the currently editing tag
|
||||
pub editing_tag: Option<EditingTag>,
|
||||
@@ -40,10 +43,11 @@ pub struct EditingTag {
|
||||
}
|
||||
|
||||
impl TagsViewState {
|
||||
pub fn new(tags: Vec<Tag>) -> Self {
|
||||
pub fn new(tags: Vec<Tag>, tag_post_counts: HashMap<String, usize>) -> Self {
|
||||
Self {
|
||||
section: TagsSection::Cloud,
|
||||
tags,
|
||||
tag_post_counts,
|
||||
search_query: String::new(),
|
||||
editing_tag: None,
|
||||
merge_source: None,
|
||||
@@ -133,14 +137,27 @@ 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))
|
||||
.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 };
|
||||
const MIN_FONT: f32 = 11.0;
|
||||
const MAX_FONT: f32 = 24.0;
|
||||
|
||||
let chips: Vec<Element<'a, Message>> = state
|
||||
.tags
|
||||
.iter()
|
||||
.map(|tag| {
|
||||
let color = parse_tag_color(tag.color.as_deref().unwrap_or(DEFAULT_TAG_COLOR));
|
||||
button(text(&tag.name).size(13).color(Color::WHITE))
|
||||
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([4, 10])
|
||||
.padding([vert_pad, 10])
|
||||
.style(move |_: &Theme, _| button::Style {
|
||||
background: Some(Background::Color(color)),
|
||||
border: iced::Border {
|
||||
|
||||
@@ -310,7 +310,8 @@ fn route_content_area<'a>(
|
||||
match tab.tab_type {
|
||||
TabType::Post => {
|
||||
if let Some(state) = post_editors.get(tab_id) {
|
||||
post_editor::view(state, locale)
|
||||
let wrap = settings_state.map(|s| s.wrap_long_lines).unwrap_or(false);
|
||||
post_editor::view(state, locale, wrap)
|
||||
} else {
|
||||
loading_view(locale)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user