AnyBot/src/app/pages_ui.rs

233 lines
12 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 頁面 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);
}
}