M16.2: community links — Discord, website, app page and GitHub

Help menu (Join the ArtCraft Discord…, LightCraft Website, LightCraft on GitHub, ArtCraft Website),
a one-click Discord button in the top bar (original chat-bubble icon), and links + a prominent
Discord button in About. Opened through a new open_url service (desktop: system browser; web: new
tab; https only).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas 2026-10-01 14:02:54 -04:00
parent 22e4f0fd8c
commit 58b6804f36
9 changed files with 122 additions and 1 deletions

View File

@ -159,6 +159,10 @@ fn services(originals: Originals, backend: Option<Backend>, ctx: egui::Context)
lightcraft_codecs::encode_png(&lightcraft_codecs::EncodeImage::rgba8(img), &lightcraft_codecs::EncodeMeta::default()).unwrap_or_default()
})),
reveal: None,
open_url: Some(Box::new(|url: &str| {
let w = web_sys::window().ok_or("no window")?;
w.open_with_url_and_target(url, "_blank").map(|_| ()).map_err(|_| "the browser blocked the new tab".to_string())
})),
pick_folder: None,
}
}

View File

@ -82,6 +82,19 @@ fn save_prefs(app: &LightcraftApp) {
fn services() -> Services {
Services {
pick_folder: Some(Box::new(|| rfd::FileDialog::new().set_title("Open Library").pick_folder().map(|p| p.to_string_lossy().to_string()))),
open_url: Some(Box::new(|url: &str| {
if !url.starts_with("https://") {
return Err("only https links are opened".into());
}
let status = if cfg!(target_os = "macos") {
std::process::Command::new("open").arg(url).status()
} else if cfg!(target_os = "windows") {
std::process::Command::new("explorer").arg(url).status()
} else {
std::process::Command::new("xdg-open").arg(url).status()
};
status.map(|_| ()).map_err(|e| e.to_string())
})),
reveal: Some(Box::new(|path: &str| {
let status = if cfg!(target_os = "macos") {
std::process::Command::new("open").args(["-R", path]).status()

View File

@ -71,6 +71,9 @@ pub enum Icon {
Edited,
/// Targeted adjustment: a ring with a centre dot and up/down arrows (drag vertically).
Target,
/// Community chat (opens the ArtCraft Discord): a speech bubble with three dots. Our own
/// generic drawing, not any service's logo.
Chat,
}
struct Pen<'a> {
@ -431,5 +434,24 @@ pub fn paint(p: &Painter, rect: Rect, icon: Icon, color: Color32) {
pen.line(&[(13.5, 5.5), (15.5, 3.5), (17.5, 5.5)]);
pen.line(&[(13.5, 14.5), (15.5, 16.5), (17.5, 14.5)]);
}
Chat => {
// rounded bubble with a tail at the lower left
pen.closed(&[
(4.5, 3.5),
(15.5, 3.5),
(17.0, 5.0),
(17.0, 12.0),
(15.5, 13.5),
(9.0, 13.5),
(5.5, 16.8),
(5.5, 13.5),
(4.5, 13.5),
(3.0, 12.0),
(3.0, 5.0),
]);
for x in [6.8, 10.0, 13.2] {
pen.dot(x, 8.5, 1.1);
}
}
}
}

View File

@ -9,6 +9,7 @@ pub mod control;
pub mod headless;
pub mod icons;
pub mod import;
pub mod links;
pub mod menubar;
pub mod menus;
pub mod merge;
@ -40,6 +41,8 @@ pub type PngEncode = Box<dyn Fn(&lightcraft_raster::Rgba8) -> Vec<u8>>;
pub type PickFolder = Box<dyn FnMut() -> Option<String>>;
/// Reveal a file in the system file manager (Finder / Explorer / the folder on Linux).
pub type RevealFn = Box<dyn FnMut(&str) -> Result<(), String>>;
/// Open a URL in the user's browser.
pub type OpenUrlFn = Box<dyn FnMut(&str) -> Result<(), String>>;
/// Platform services injected by the host app (desktop or web).
#[derive(Default)]
@ -57,6 +60,8 @@ pub struct Services {
pub reveal: Option<RevealFn>,
/// Choose a folder (Settings → General → Open Library…; desktop only).
pub pick_folder: Option<PickFolder>,
/// Open a web link in the browser (Help menu, About, Discord button).
pub open_url: Option<OpenUrlFn>,
}
#[derive(Clone, Copy, Debug, Default)]

View File

@ -0,0 +1,48 @@
//! Community and project links (Help menu, About dialog, top-bar Discord button).
/// The app's id in ArtCraft URLs.
pub const APP: &str = "lightcraft";
/// The ArtCraft community Discord.
pub const DISCORD: &str = "https://discord.gg/artcraft";
/// The ArtCraft website.
pub const WEBSITE: &str = "https://getartcraft.com";
/// This app's page on the ArtCraft website.
pub const APP_PAGE: &str = "https://getartcraft.com/apps/lightcraft";
/// This app's source repository.
pub const GITHUB: &str = "https://github.com/storytold/lightcraft";
/// (UI command id, menu label, URL) for each link, in Help-menu order.
pub const LINKS: &[(&str, &str, &str)] = &[
("app.discord", "Join the ArtCraft Discord…", DISCORD),
("app.website", "LightCraft Website", APP_PAGE),
("app.github", "LightCraft on GitHub", GITHUB),
("app.artcraft", "ArtCraft Website", WEBSITE),
];
/// The URL behind a link command id.
pub fn url_of(cmd: &str) -> Option<&'static str> {
LINKS.iter().find(|(id, _, _)| *id == cmd).map(|(_, _, u)| *u)
}
/// Open `url` in the user's browser (through the host's `open_url` service).
pub fn open(app: &mut crate::LightcraftApp, url: &str) -> Result<serde_json::Value, String> {
let open = app.services.open_url.as_mut().ok_or("can't open links here")?;
open(url)?;
Ok(serde_json::json!({ "url": url }))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn links_are_https_and_name_this_app() {
for (id, label, url) in LINKS {
assert!(url.starts_with("https://"), "{id}");
assert!(!label.is_empty());
assert_eq!(url_of(id), Some(*url));
}
assert!(APP_PAGE.ends_with(&format!("/apps/{APP}")));
assert!(GITHUB.ends_with(&format!("/storytold/{APP}")));
}
}

View File

@ -182,7 +182,7 @@ const LAYOUT: &[(&str, &[&str])] = &[
"@Tools",
],
),
("Help", &["app.shortcuts", "---", "app.about"]),
("Help", &["app.discord", "---", "app.website", "app.github", "app.artcraft", "---", "app.shortcuts", "---", "app.about"]),
];
/// Registry entries that are reached another way (parameterized commands are expanded into

View File

@ -89,6 +89,10 @@ pub const UI_COMMANDS: &[UiCommand] = &[
("app.settings", "Settings…", Some("Cmd+,"), "Edit"),
("app.openLibrary", "Open Library…", None, "File"),
("app.about", "About LightCraft", None, "Help"),
("app.discord", "Join the ArtCraft Discord…", None, "Help"),
("app.website", "LightCraft Website", None, "Help"),
("app.github", "LightCraft on GitHub", None, "Help"),
("app.artcraft", "ArtCraft Website", None, "Help"),
("app.shortcuts", "Keyboard Shortcuts", Some("Cmd+/"), "Help"),
("app.export", "Export Now", None, ""),
("app.showInFinder", "Show in Finder", Some("Cmd+R"), "Photo"),
@ -602,6 +606,10 @@ pub fn run_ui_command(app: &mut LightcraftApp, id: &str, p: &Value) -> Option<Re
}
"app.export" => crate::control::export_active(app, p),
"app.showInFinder" => show_in_finder(app),
"app.discord" | "app.website" | "app.github" | "app.artcraft" => {
let url = crate::links::url_of(id).unwrap_or(crate::links::WEBSITE);
crate::links::open(app, url)
}
"app.exportPrevious" => match app.session.last_export.clone() {
Some(prev) => crate::control::export_active(app, &prev),
None => Err("nothing exported yet — use Export…".into()),

View File

@ -365,6 +365,26 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) {
ui.label(egui::RichText::new("LightCraft").font(t.semibold(20.0)).color(t.text));
ui.label(format!("Version {} — a clean-room, pure-Rust photo library and raw developer.", env!("CARGO_PKG_VERSION")));
ui.label("MIT OR Apache-2.0. Font: Inter (OFL). Icons: original.");
ui.add_space(10.0);
let discord = egui::Button::new(egui::RichText::new("Join the ArtCraft Discord").font(t.semibold(15.0)).color(egui::Color32::WHITE))
.fill(t.accent)
.min_size(egui::vec2(260.0, 34.0));
let r = ui.add(discord).on_hover_text(crate::links::DISCORD);
crate::widgets::register(ui.ctx(), "button:aboutDiscord", r.rect);
if r.clicked() {
let _ = crate::links::open(app, crate::links::DISCORD);
}
ui.add_space(6.0);
for (label, url) in [
("LightCraft website", crate::links::APP_PAGE),
("Source code on GitHub", crate::links::GITHUB),
("ArtCraft — more creative apps", crate::links::WEBSITE),
] {
let r = ui.link(label).on_hover_text(url);
if r.clicked() {
let _ = crate::links::open(app, url);
}
}
}
Dialog::Shortcuts => {
egui::ScrollArea::vertical().max_height(420.0).show(ui, |ui| {

View File

@ -97,6 +97,7 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) {
// right icons
let mut x = full.right() - 18.0;
for (id, icon, tip, cmd) in [
("discord", Icon::Chat, "Join the ArtCraft community on Discord", "app.discord"),
("cloud", Icon::Cloud, "Local library — no cloud account needed", ""),
("help", Icon::Help, "Keyboard shortcuts", "app.shortcuts"),
("share", Icon::Share, "Export", "dialog.export"),