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