AnyBot/src/app/pages_ui.rs

233 lines
12 KiB
Rust
Raw Normal View History

// 頁面 in the window: the card under a reply (like the original's chat card — the title, a few
// lines of the page fading out, then a footer row with the page icon, its name and 「頁面」), the
// page itself in a large dialog (the whole page drawn with the chat's Markdown, the version picker
// and 存成檔案), and the list under 資料庫 in the right panel.
use super::*;
const CARD_W: f32 = 520.0;
impl App {
/// The card for page `id` under a reply.
pub(super) fn page_card(&mut self, ui: &mut egui::Ui, id: u64) {
let p = self.pal;
let Some(page) = self.state.pages.iter().find(|x| x.id == id).cloned() else {
ui.label(RichText::new(t("(這個頁面已刪除)")).size(12.5).color(p.text3));
return;
};
let now = crate::model::now_ms();
let w = ui.available_width().min(CARD_W);
let resp = Frame::new().fill(p.pane).stroke(Stroke::new(1.0, p.border)).corner_radius(16).show(ui, |ui| {
ui.set_width(w);
ui.spacing_mut().item_spacing.y = 0.0;
// The page's beginning: the title, then a few lines fading out.
Frame::new().inner_margin(Margin { left: 18, right: 18, top: 16, bottom: 12 }).show(ui, |ui| {
ui.set_width(ui.available_width());
ui.add(egui::Label::new(RichText::new(&page.title).size(17.0).strong().color(p.text)).wrap());
ui.add_space(8.0);
let lines = crate::pages::preview(&page, 4);
let n = lines.len().max(1) as f32;
for (i, l) in lines.iter().enumerate() {
let fade = 1.0 - (i as f32 / n) * 0.65;
let c = if i == 0 && !page.summary.is_empty() { p.text } else { p.text2 };
ui.add(egui::Label::new(RichText::new(l).size(13.5).color(c.gamma_multiply(fade))).truncate());
ui.add_space(4.0);
}
});
let r = ui.min_rect();
ui.painter().hline(r.x_range(), r.bottom(), Stroke::new(1.0, p.border));
// The footer row: page icon, name, 「頁面 · 第 N 版 · 更新」.
Frame::new().inner_margin(Margin::symmetric(18, 12)).show(ui, |ui| {
ui.set_width(ui.available_width());
ui.horizontal(|ui| {
let (ir, _) = ui.allocate_exact_size(vec2(30.0, 36.0), egui::Sense::hover());
page_glyph(ui.painter(), ir, p);
ui.add_space(8.0);
ui.vertical(|ui| {
ui.spacing_mut().item_spacing.y = 2.0;
ui.add(egui::Label::new(RichText::new(&page.title).size(14.0).color(p.text)).truncate());
let mut meta = format!("{} · {}", t("頁面"), crate::pages::ago(page.updated, now));
if page.revision > 1 {
meta = format!("{meta} · {}", t("已更新"));
}
ui.label(RichText::new(meta).size(12.0).color(p.text3));
});
});
if page.revision > 1 && !page.note.is_empty() {
ui.add_space(6.0);
ui.add(egui::Label::new(RichText::new(format!("{}:{}", t("最近更新"), page.note)).size(12.0).color(p.text2)).wrap());
}
});
});
let r = ui.interact(resp.response.rect, egui::Id::unique(("page-card", id, self.msg_index)), egui::Sense::click());
if r.hovered() {
ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand);
}
if r.on_hover_text(t("打開頁面")).clicked() {
self.page_view = Some((id, None));
}
}
/// The page, large: everything drawn like the chat, with the version picker and the footer.
pub(super) fn page_viewer(&mut self, ctx: &egui::Context) {
let Some((id, rev)) = self.page_view else { return };
let p = self.pal;
let Some(cur) = self.state.pages.iter().find(|x| x.id == id).cloned() else {
self.page_view = None;
self.notice = Some(t("這個頁面已經刪除了"));
return;
};
let shown = rev.and_then(|r| crate::pages::version_of(&cur, r)).unwrap_or_else(|| cur.clone());
let old = shown.revision != cur.revision;
let bot_name = self.state.bot(cur.bot).map(|b| b.name.clone()).unwrap_or_default();
let w = (ctx.content_rect().width() - 80.0).clamp(360.0, 860.0);
let h = (dialog_max_height(ctx) - 150.0).max(240.0);
let mut keep = true;
let mut delete = false;
let mut pick: Option<Option<u32>> = None;
let close = dialog(ctx, "page-view", "頁面", w, true, p, |ui| {
dialog_body(ui, ("page-body", id), h, |ui| {
ui.spacing_mut().item_spacing.y = 0.0;
ui.add(egui::Label::new(RichText::new(&shown.title).size(26.0).strong().color(p.text)).wrap());
ui.add_space(6.0);
let mut meta = format!("{} · {} {}", bot_name, t("更新於"), crate::pages::when(shown.updated));
if cur.revision > 1 || old {
meta = format!("{meta} · {} {}", t("第"), shown.revision) + &t(" 版");
}
ui.label(RichText::new(meta).size(12.5).color(p.text3));
if old {
ui.add_space(8.0);
Frame::new().fill(p.hover).corner_radius(8).inner_margin(Margin::symmetric(10, 6)).show(ui, |ui| {
ui.horizontal(|ui| {
ui.label(RichText::new(t("你在看舊的版本")).size(12.5).color(p.text2));
if ui.add(secondary_button("回到最新版")).clicked() {
pick = Some(None);
}
});
});
} else if !shown.summary.is_empty() {
ui.add_space(12.0);
ui.add(egui::Label::new(RichText::new(&shown.summary).size(15.0).color(p.text)).wrap());
}
if !shown.note.is_empty() && shown.revision > 1 {
ui.add_space(6.0);
ui.add(egui::Label::new(RichText::new(format!("{}:{}", t("這一版更新了"), shown.note)).size(12.5).color(p.text2)).wrap());
}
ui.add_space(16.0);
let body = shown.body.clone();
self.markdown(ui, &body, p.text, false);
ui.add_space(8.0);
});
dialog_footer(ui, p, |ui| {
if !cur.history.is_empty() {
let label = if old { format!("{} {}", t("第"), shown.revision) + &t(" 版") } else { t("版本紀錄") };
egui::ComboBox::from_id_salt(("page-versions", id)).selected_text(label).width(150.0).show_ui(ui, |ui| {
if ui.selectable_label(!old, format!("{} {} {}({})", t("第"), cur.revision, t("版"), t("最新"))).clicked() {
pick = Some(None);
}
for v in cur.history.iter().rev() {
let text = format!("{} {} {} · {}", t("第"), v.revision, t("版"), crate::pages::when(v.at));
if ui.selectable_label(rev == Some(v.revision), text).clicked() {
pick = Some(Some(v.revision));
}
}
});
}
ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
if ui.add(primary_button("完成", p)).clicked() {
keep = false;
}
if ui.add(secondary_button("存成檔案")).on_hover_text(t("存成 Markdown 到「下載」")).clicked() {
let dir = dirs::download_dir().unwrap_or_else(std::env::temp_dir);
let name: String = shown.title.chars().filter(|c| !"/\\:*?\"<>|".contains(*c)).collect();
let path = dir.join(format!("{name}.md"));
let text = format!("# {}\n\n{}\n", shown.title, shown.body);
match std::fs::write(&path, text) {
Ok(()) => {
crate::preview::reveal(&path);
self.notice = Some(format!("{}{}", t("已存到 "), path.display()));
}
Err(e) => self.notice = Some(format!("{}{e}", t("存不了:"))),
}
}
if ui.add(secondary_button("複製")).on_hover_text(t("複製成 Markdown")).clicked() {
ui.ctx().copy_text(format!("# {}\n\n{}", shown.title, shown.body));
self.notice = Some(t("已複製"));
}
if !old && ui.add(egui::Button::new(RichText::new(t("刪除")).color(p.danger)).frame(false)).clicked() {
delete = true;
}
});
});
});
if let Some(v) = pick {
self.page_view = Some((id, v));
return;
}
if delete {
self.state.pages.retain(|x| x.id != id);
self.page_view = None;
self.notice = Some(format!("{}「{}」", t("已刪除頁面"), cur.title));
self.mark();
return;
}
if close || !keep {
self.page_view = None;
}
}
/// 資料庫 › 頁面: this bot's pages, newest change first. Whether there were any.
pub(super) fn pages_library(&mut self, ui: &mut egui::Ui, bot: &Bot) -> bool {
let p = self.pal;
let mut mine: Vec<crate::pages::Page> = self.state.pages.iter().filter(|x| x.bot == bot.id).cloned().collect();
if mine.is_empty() {
return false;
}
mine.sort_by_key(|x| std::cmp::Reverse(x.updated));
let now = crate::model::now_ms();
let mut open = None;
super::panel_ui::panel_title(ui, "頁面", p);
super::panel_ui::panel_card(ui, p, |ui| {
for pg in &mine {
let (rect, r) = ui.allocate_exact_size(vec2(ui.available_width(), 44.0), egui::Sense::click());
if r.hovered() {
ui.painter().rect_filled(rect, 9, p.hover);
}
let ir = egui::Rect::from_center_size(rect.left_center() + vec2(20.0, 0.0), vec2(18.0, 24.0));
page_glyph(ui.painter(), ir, p);
let x = rect.left() + 38.0;
let title = super::panel_ui::shorten_size(ui, &pg.title, rect.right() - x - 8.0, 13.0);
ui.painter().text(egui::pos2(x, rect.top() + 6.0), egui::Align2::LEFT_TOP, title, egui::FontId::proportional(13.0), p.text);
ui.painter().text(egui::pos2(x, rect.top() + 25.0), egui::Align2::LEFT_TOP, format!("{} · {}", t("頁面"), crate::pages::ago(pg.updated, now)), egui::FontId::proportional(11.5), p.text3);
if r.on_hover_cursor(egui::CursorIcon::PointingHand).clicked() {
open = Some(pg.id);
}
}
});
if let Some(id) = open {
self.page_view = Some((id, None));
}
true
}
}
/// The page icon: a sheet with a folded corner, two text lines and four coloured bars (like the
/// original's page glyph).
pub(super) fn page_glyph(painter: &egui::Painter, r: egui::Rect, p: Palette) {
let w = r.width().min(r.height() * 0.8);
let rect = egui::Rect::from_center_size(r.center(), vec2(w, w * 1.25));
painter.rect_filled(rect, 4, p.hover);
painter.rect_stroke(rect, 4, Stroke::new(1.0, p.border), egui::StrokeKind::Inside);
let u = rect.width() / 10.0;
let l = rect.left() + u * 2.0;
painter.hline(l..=(rect.left() + u * 6.0), rect.top() + u * 2.6, Stroke::new(u * 0.9, p.text3));
painter.hline(l..=(rect.left() + u * 4.6), rect.top() + u * 4.4, Stroke::new(u * 0.9, p.text3));
let colors = [Color32::from_rgb(0x34, 0xa8, 0x53), Color32::from_rgb(0xfb, 0xbc, 0x05), Color32::from_rgb(0xea, 0x43, 0x35), Color32::from_rgb(0x42, 0x85, 0xf4)];
let base = rect.bottom() - u * 2.0;
for (i, c) in colors.iter().enumerate() {
let h = u * (2.0 + i as f32 * 1.1);
let x = l + i as f32 * u * 1.7;
painter.rect_filled(egui::Rect::from_min_max(egui::pos2(x, base - h), egui::pos2(x + u * 1.1, base)), 1, *c);
}
}