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:
parent
22e4f0fd8c
commit
58b6804f36
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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}")));
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
|
|
|
|||
|
|
@ -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| {
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
Loading…
Reference in New Issue