One AnyBot icon everywhere; the phone page fills the screen and shows the app's bot faces
- New icon: the bot's cloud body and slanted eyes (as in the app's avatars) on a dark tile — the Mac/Linux app, browser tabs, iPhone home screen (PNG: it ignores SVG), Android and notifications. The web had its own ring-and-dot. - anybot --write-icon out.png size [file.svg] draws any SVG (for the web PNGs). - Phone: lists that reach the bottom keep their last row above the home bar and Safari's toolbar (it looked cut off); bots show their shape, colour and eyes instead of a letter. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
@ -0,0 +1,22 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024">
|
||||
<!-- AnyBot's icon edge to edge (web, iPhone home screen, Android): the same drawing as the app icon. -->
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0" stop-color="#3b3e47"/>
|
||||
<stop offset="1" stop-color="#121317"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="glow" cx="0.5" cy="0.18" r="0.75">
|
||||
<stop offset="0" stop-color="#ffffff" stop-opacity="0.16"/>
|
||||
<stop offset="1" stop-color="#ffffff" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="body" gradientUnits="userSpaceOnUse" x1="0" y1="240" x2="0" y2="817">
|
||||
<stop offset="0" stop-color="#ffffff"/>
|
||||
<stop offset="1" stop-color="#dcdde3"/>
|
||||
</linearGradient>
|
||||
<filter id="soft" x="-30%" y="-200%" width="160%" height="500%"><feGaussianBlur stdDeviation="19.8"/></filter>
|
||||
</defs>
|
||||
<rect x="0" y="0" width="1024" height="1024" rx="0" fill="url(#bg)"/>
|
||||
<rect x="0" y="0" width="1024" height="1024" rx="0" fill="url(#glow)"/>
|
||||
<ellipse cx="512.0" cy="826.9" rx="257.4" ry="36.3" fill="#000" fill-opacity="0.45" filter="url(#soft)"/><g fill="url(#body)"><circle cx="363.5" cy="569.5" r="181.5"/><circle cx="660.5" cy="569.5" r="181.5"/><circle cx="512.0" cy="454.0" r="214.5"/><circle cx="512.0" cy="635.5" r="181.5"/></g><ellipse cx="422.9" cy="526.6" rx="36.3" ry="72.6" transform="rotate(-18 422.9 526.6)" fill="#17171a"/><circle cx="411.3" cy="490.3" r="9.9" fill="#ffffff" fill-opacity="0.9"/><ellipse cx="601.1" cy="526.6" rx="36.3" ry="72.6" transform="rotate(-18 601.1 526.6)" fill="#17171a"/><circle cx="589.6" cy="490.3" r="9.9" fill="#ffffff" fill-opacity="0.9"/>
|
||||
<rect x="1.5" y="1.5" width="1021" height="1021" rx="-1.5" fill="none" stroke="#ffffff" stroke-opacity="0.10" stroke-width="3"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
|
|
@ -1,13 +1,22 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
|
||||
<!-- AnyBot's app icon: the bot's face (two eyes) on a dark rounded square. -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024">
|
||||
<!-- AnyBot's app icon (macOS grid: an 824 tile in 1024): the bot's cloud body and slanted eyes, as in the app's avatars. -->
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0" stop-color="#3a3a3a"/>
|
||||
<stop offset="1" stop-color="#1c1c1c"/>
|
||||
<stop offset="0" stop-color="#3b3e47"/>
|
||||
<stop offset="1" stop-color="#121317"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="glow" cx="0.5" cy="0.18" r="0.75">
|
||||
<stop offset="0" stop-color="#ffffff" stop-opacity="0.16"/>
|
||||
<stop offset="1" stop-color="#ffffff" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="body" gradientUnits="userSpaceOnUse" x1="0" y1="309" x2="0" y2="768">
|
||||
<stop offset="0" stop-color="#ffffff"/>
|
||||
<stop offset="1" stop-color="#dcdde3"/>
|
||||
</linearGradient>
|
||||
<filter id="soft" x="-30%" y="-200%" width="160%" height="500%"><feGaussianBlur stdDeviation="15.7"/></filter>
|
||||
</defs>
|
||||
<rect x="32" y="32" width="448" height="448" rx="112" fill="url(#bg)"/>
|
||||
<rect x="32.5" y="32.5" width="447" height="447" rx="111.5" fill="none" stroke="#ffffff" stroke-opacity="0.08"/>
|
||||
<ellipse cx="200" cy="262" rx="30" ry="50" fill="#ffffff" transform="rotate(-12 200 262)"/>
|
||||
<ellipse cx="312" cy="262" rx="30" ry="50" fill="#ffffff" transform="rotate(-12 312 262)"/>
|
||||
<rect x="100" y="100" width="824" height="824" rx="186" fill="url(#bg)"/>
|
||||
<rect x="100" y="100" width="824" height="824" rx="186" fill="url(#glow)"/>
|
||||
<ellipse cx="512.0" cy="775.7" rx="204.4" ry="28.8" fill="#000" fill-opacity="0.45" filter="url(#soft)"/><g fill="url(#body)"><circle cx="394.1" cy="571.3" r="144.1"/><circle cx="629.9" cy="571.3" r="144.1"/><circle cx="512.0" cy="479.6" r="170.3"/><circle cx="512.0" cy="623.7" r="144.1"/></g><ellipse cx="441.3" cy="537.2" rx="28.8" ry="57.6" transform="rotate(-18 441.3 537.2)" fill="#17171a"/><circle cx="432.1" cy="508.4" r="7.9" fill="#ffffff" fill-opacity="0.9"/><ellipse cx="582.7" cy="537.2" rx="28.8" ry="57.6" transform="rotate(-18 582.7 537.2)" fill="#17171a"/><circle cx="573.6" cy="508.4" r="7.9" fill="#ffffff" fill-opacity="0.9"/>
|
||||
<rect x="101.5" y="101.5" width="821" height="821" rx="184.5" fill="none" stroke="#ffffff" stroke-opacity="0.10" stroke-width="3"/>
|
||||
</svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 744 B After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 56 KiB |
|
|
@ -0,0 +1,22 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024">
|
||||
<!-- AnyBot's icon for browser tabs: the same drawing with rounded corners. -->
|
||||
<defs>
|
||||
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0" stop-color="#3b3e47"/>
|
||||
<stop offset="1" stop-color="#121317"/>
|
||||
</linearGradient>
|
||||
<radialGradient id="glow" cx="0.5" cy="0.18" r="0.75">
|
||||
<stop offset="0" stop-color="#ffffff" stop-opacity="0.16"/>
|
||||
<stop offset="1" stop-color="#ffffff" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<linearGradient id="body" gradientUnits="userSpaceOnUse" x1="0" y1="240" x2="0" y2="817">
|
||||
<stop offset="0" stop-color="#ffffff"/>
|
||||
<stop offset="1" stop-color="#dcdde3"/>
|
||||
</linearGradient>
|
||||
<filter id="soft" x="-30%" y="-200%" width="160%" height="500%"><feGaussianBlur stdDeviation="19.8"/></filter>
|
||||
</defs>
|
||||
<rect x="0" y="0" width="1024" height="1024" rx="230" fill="url(#bg)"/>
|
||||
<rect x="0" y="0" width="1024" height="1024" rx="230" fill="url(#glow)"/>
|
||||
<ellipse cx="512.0" cy="826.9" rx="257.4" ry="36.3" fill="#000" fill-opacity="0.45" filter="url(#soft)"/><g fill="url(#body)"><circle cx="363.5" cy="569.5" r="181.5"/><circle cx="660.5" cy="569.5" r="181.5"/><circle cx="512.0" cy="454.0" r="214.5"/><circle cx="512.0" cy="635.5" r="181.5"/></g><ellipse cx="422.9" cy="526.6" rx="36.3" ry="72.6" transform="rotate(-18 422.9 526.6)" fill="#17171a"/><circle cx="411.3" cy="490.3" r="9.9" fill="#ffffff" fill-opacity="0.9"/><ellipse cx="601.1" cy="526.6" rx="36.3" ry="72.6" transform="rotate(-18 601.1 526.6)" fill="#17171a"/><circle cx="589.6" cy="490.3" r="9.9" fill="#ffffff" fill-opacity="0.9"/>
|
||||
<rect x="1.5" y="1.5" width="1021" height="1021" rx="228.5" fill="none" stroke="#ffffff" stroke-opacity="0.10" stroke-width="3"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
|
|
@ -9,8 +9,9 @@
|
|||
<meta name="theme-color" content="#121212" media="(prefers-color-scheme: dark)">
|
||||
<meta name="theme-color" content="#ffffff" media="(prefers-color-scheme: light)">
|
||||
<link rel="manifest" href="manifest.webmanifest">
|
||||
<link rel="icon" href="icon.svg">
|
||||
<link rel="apple-touch-icon" href="icon.svg">
|
||||
<link rel="icon" href="icon.svg" type="image/svg+xml">
|
||||
<link rel="icon" href="icon-192.png" type="image/png" sizes="192x192">
|
||||
<link rel="apple-touch-icon" href="icon-180.png">
|
||||
<title>AnyBot</title>
|
||||
<style>
|
||||
:root{
|
||||
|
|
@ -38,6 +39,9 @@ header .sub{font-size:12px;color:var(--text3);font-weight:400}
|
|||
.icon-btn:active{background:var(--raise)}
|
||||
.icon-btn svg{width:20px;height:20px}
|
||||
.scroll{flex:1;overflow-y:auto;-webkit-overflow-scrolling:touch;overscroll-behavior:contain}
|
||||
/* A list that reaches the bottom of the screen keeps its last row above the iPhone's home bar and
|
||||
Safari's toolbar (it looked cut off). */
|
||||
.view > .scroll:last-child{padding-bottom:calc(env(safe-area-inset-bottom) + 16px)}
|
||||
/* login */
|
||||
.login{flex:1;display:flex;flex-direction:column;justify-content:center;padding:32px 24px calc(env(safe-area-inset-bottom) + 32px);max-width:420px;margin:0 auto;width:100%}
|
||||
.logo{width:56px;height:56px;margin-bottom:20px}
|
||||
|
|
@ -53,6 +57,8 @@ header .sub{font-size:12px;color:var(--text3);font-weight:400}
|
|||
.row{display:flex;gap:12px;align-items:center;padding:12px 16px;border-bottom:1px solid var(--line)}
|
||||
.row:active{background:var(--raise)}
|
||||
.av{width:42px;height:42px;border-radius:50%;flex:none;display:grid;place-items:center;font-weight:600;color:#fff;font-size:16px;position:relative;box-shadow:inset 0 0 0 1px var(--line)}
|
||||
.av.face{background:none !important;box-shadow:none;border-radius:0}
|
||||
.av.face svg{width:100%;height:100%;display:block}
|
||||
.av.sm{width:26px;height:26px;font-size:12px}
|
||||
.row .mid{flex:1;min-width:0}
|
||||
.row .name{font-weight:600;display:flex;gap:6px;align-items:center}
|
||||
|
|
@ -318,6 +324,33 @@ const S = {
|
|||
};
|
||||
// The app sends each bot's colour as #rrggbb (the same palette as the window).
|
||||
const color = (c) => (/^#[0-9a-f]{6}$/i.test(String(c || "")) ? c : "#1c1c1c");
|
||||
// A bot's face as in the app (src/avatar.rs): its shape in its colour, two slanted oval eyes.
|
||||
// Drawn in a -1..1 box; the phone used to show the name's first letter in a circle.
|
||||
function avatarSvg(shape, hex) {
|
||||
const col = color(hex), P = Math.PI, f = (n) => n.toFixed(3);
|
||||
const pts = (a) => `<polygon points="${a.map(([x, y]) => f(x) + "," + f(y)).join(" ")}"/>`;
|
||||
const circ = (x, y, r) => `<circle cx="${f(x)}" cy="${f(y)}" r="${f(r)}"/>`;
|
||||
const range = (n) => [...Array(n).keys()];
|
||||
let body;
|
||||
switch (shape) {
|
||||
case "squircle": body = `<rect x="-0.92" y="-0.92" width="1.84" height="1.84" rx="0.42"/>`; break;
|
||||
case "capsule": body = `<rect x="-0.675" y="-1" width="1.35" height="2" rx="0.66"/>`; break;
|
||||
case "egg": body = pts(range(48).map((i) => { const t = i / 48 * 2 * P; return [0.8 * Math.cos(t), 0.08 + (Math.sin(t) < 0 ? 1 : 0.86) * Math.sin(t)]; })); break;
|
||||
case "hex": body = pts(range(6).map((i) => { const a = i / 6 * 2 * P + P / 6; return [Math.cos(a), Math.sin(a)]; })); break;
|
||||
case "gem": body = pts([[-0.55, -0.8], [0.55, -0.8], [1, -0.2], [0, 1], [-1, -0.2]]); break;
|
||||
case "arch": body = pts(range(25).map((i) => { const a = P + i / 24 * P; return [0.85 * Math.cos(a), -0.15 + 0.85 * Math.sin(a)]; }).concat([[0.85, 1], [-0.85, 1]])); break;
|
||||
case "shield": body = pts([[-0.85, -0.9], [0.85, -0.9], [0.85, 0.05], [0.5, 0.65], [0, 1], [-0.5, 0.65], [-0.85, 0.05]]); break;
|
||||
case "pebble": body = `<ellipse rx="1" ry="0.78" transform="rotate(-14.3)"/>`; break;
|
||||
case "cloud": body = circ(-0.45, 0.15, 0.55) + circ(0.45, 0.15, 0.55) + circ(0, -0.2, 0.65) + circ(0, 0.35, 0.55); break;
|
||||
case "heart": body = circ(-0.45, -0.25, 0.55) + circ(0.45, -0.25, 0.55) + pts([[-0.97, -0.1], [0.97, -0.1], [0, 1]]); break;
|
||||
case "star": body = pts(range(10).map((i) => { const a = -P / 2 + i * P / 5, r = i % 2 ? 0.48 : 1.05; return [r * Math.cos(a), 0.08 + r * Math.sin(a)]; })); break;
|
||||
default: body = circ(0, 0, 0.95);
|
||||
}
|
||||
const rgb = [1, 3, 5].map((i) => parseInt(col.slice(i, i + 2), 16));
|
||||
const eye = rgb[0] + rgb[1] + rgb[2] > 600 ? "#1c1c1c" : "#fcfcfc";
|
||||
const eyes = [-0.27, 0.27].map((x) => `<ellipse cx="${x}" cy="0.02" rx="0.11" ry="0.22" transform="rotate(-20 ${x} 0.02)"/>`).join("");
|
||||
return `<svg viewBox="-1.1 -1.1 2.2 2.2" aria-hidden="true"><g fill="${col}">${body}</g><g fill="${eye}">${eyes}</g></svg>`;
|
||||
}
|
||||
|
||||
function toast(t) { const e = $("toast"); e.textContent = t; e.classList.add("on"); clearTimeout(toast.t); toast.t = setTimeout(() => e.classList.remove("on"), 2600); }
|
||||
function show(v) { document.querySelectorAll(".view").forEach((x) => x.classList.toggle("on", x.id === "v-" + v)); S.view = v; if (v !== "screen") stopFrames(); }
|
||||
|
|
@ -441,7 +474,7 @@ function apply(v) {
|
|||
renderList();
|
||||
if (S.open) {
|
||||
const b = S.bots.find((x) => x.id === S.open);
|
||||
if (b) { $("chat-name").textContent = b.name; $("chat-av").textContent = (b.name || "?")[0]; $("chat-av").style.background = color(b.color); }
|
||||
if (b) { $("chat-name").textContent = b.name; $("chat-av").innerHTML = avatarSvg(b.shape, b.color); $("chat-av").classList.add("face"); }
|
||||
loadChat(); renderAsk();
|
||||
}
|
||||
}
|
||||
|
|
@ -452,7 +485,7 @@ function renderList() {
|
|||
el.innerHTML = S.bots.map((b) => {
|
||||
const waiting = S.approvals.some((a) => a.bot === b.id);
|
||||
const state = waiting ? `<span class="wait">等你決定</span>` : b.busy ? `<span class="busy">${esc(b.status || "工作中…")}</span>` : "";
|
||||
return `<div class="row" data-id="${b.id}"><div class="av" style="background:${color(b.color)}">${esc((b.name || "?")[0])}</div><div class="mid"><div class="name">${esc(b.name)}${b.members ? '<span class="pill">群組</span>' : ""}${b.unread ? '<span class="badge"></span>' : ""}</div><div class="last">${state || esc(b.last || b.description || "")}</div></div></div>`;
|
||||
return `<div class="row" data-id="${b.id}"><div class="av face">${avatarSvg(b.shape, b.color)}</div><div class="mid"><div class="name">${esc(b.name)}${b.members ? '<span class="pill">群組</span>' : ""}${b.unread ? '<span class="badge"></span>' : ""}</div><div class="last">${state || esc(b.last || b.description || "")}</div></div></div>`;
|
||||
}).join("");
|
||||
el.querySelectorAll(".row").forEach((r) => r.onclick = () => openChat(+r.dataset.id));
|
||||
}
|
||||
|
|
@ -461,7 +494,7 @@ function renderList() {
|
|||
function openChat(id) {
|
||||
S.open = id; S.msgs = []; S.pending = []; renderChips();
|
||||
const b = S.bots.find((x) => x.id === id) || {};
|
||||
$("chat-name").textContent = b.name || ""; $("chat-av").textContent = (b.name || "?")[0]; $("chat-av").style.background = color(b.color);
|
||||
$("chat-name").textContent = b.name || ""; $("chat-av").innerHTML = avatarSvg(b.shape, b.color); $("chat-av").classList.add("face");
|
||||
$("msgs").innerHTML = ""; show("chat"); loadChat(true); renderAsk();
|
||||
api(base() + `api/bots/${id}/read`, {json: {}}).catch(() => {});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,8 +29,10 @@ use std::sync::{Arc, Condvar, Mutex};
|
|||
use std::time::{Duration, Instant};
|
||||
|
||||
const PAGE: &str = include_str!("../../mobile/index.html");
|
||||
const MANIFEST: &str = r##"{"name":"AnyBot","short_name":"AnyBot","start_url":"./","display":"standalone","background_color":"#121212","theme_color":"#121212","icons":[{"src":"icon.svg","sizes":"any","type":"image/svg+xml"}]}"##;
|
||||
const ICON: &str = r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96"><rect width="96" height="96" rx="22" fill="#161616"/><circle cx="48" cy="48" r="24" fill="none" stroke="#f5f5f5" stroke-width="7"/><circle cx="48" cy="48" r="7" fill="#f5f5f5"/></svg>"##;
|
||||
const MANIFEST: &str = r##"{"name":"AnyBot","short_name":"AnyBot","start_url":"./","display":"standalone","background_color":"#121212","theme_color":"#121212","icons":[{"src":"icon.svg","sizes":"any","type":"image/svg+xml"},{"src":"icon-192.png","sizes":"192x192","type":"image/png","purpose":"any maskable"},{"src":"icon-512.png","sizes":"512x512","type":"image/png","purpose":"any maskable"}]}"##;
|
||||
/// AnyBot's icon (the same drawing as the app's), and PNGs for the iPhone home screen and Android.
|
||||
const ICON: &str = include_str!("../../mobile/icon.svg");
|
||||
const ICON_PNGS: [(&str, &[u8]); 3] = [("icon-180.png", include_bytes!("../../mobile/icon-180.png")), ("icon-192.png", include_bytes!("../../mobile/icon-192.png")), ("icon-512.png", include_bytes!("../../mobile/icon-512.png"))];
|
||||
|
||||
/// How long a host's "next request?" waits before answering "nothing".
|
||||
const HOST_POLL_SECS: u64 = 25;
|
||||
|
|
@ -124,7 +126,7 @@ self.addEventListener("activate", (e) => e.waitUntil(self.clients.claim()));
|
|||
self.addEventListener("push", (e) => {
|
||||
let d = {};
|
||||
try { d = e.data ? e.data.json() : {}; } catch (_) { d = {body: e.data ? e.data.text() : ""}; }
|
||||
e.waitUntil(self.registration.showNotification(d.title || "AnyBot", {body: d.body || "", tag: "bot-" + (d.bot || 0), renotify: true, data: d, icon: "icon.svg", badge: "icon.svg"}));
|
||||
e.waitUntil(self.registration.showNotification(d.title || "AnyBot", {body: d.body || "", tag: "bot-" + (d.bot || 0), renotify: true, data: d, icon: "icon-192.png", badge: "icon-192.png"}));
|
||||
});
|
||||
self.addEventListener("notificationclick", (e) => {
|
||||
e.notification.close();
|
||||
|
|
@ -497,6 +499,7 @@ impl Relay {
|
|||
.with_header("Strict-Transport-Security", "max-age=31536000"),
|
||||
("GET", ["manifest.webmanifest"]) => Resp::bytes("application/manifest+json", MANIFEST.as_bytes().to_vec()),
|
||||
("GET", ["icon.svg"]) => Resp::bytes("image/svg+xml", ICON.as_bytes().to_vec()),
|
||||
("GET", [name]) if ICON_PNGS.iter().any(|(n, _)| n == name) => Resp::bytes("image/png", ICON_PNGS.iter().find(|(n, _)| n == name).map(|(_, b)| b.to_vec()).unwrap_or_default()),
|
||||
("GET", ["api", "hello"]) => Resp::json(&json!({"app": "anybot-relay"})),
|
||||
("GET", ["sw.js"]) => Resp::bytes("text/javascript; charset=utf-8", SW.as_bytes().to_vec()).with_header("Service-Worker-Allowed", "/"),
|
||||
("GET", ["relay", "push", "key"]) => Resp::json(&json!({"key": self.vapid.public_b64})),
|
||||
|
|
|
|||
20
src/main.rs
|
|
@ -147,8 +147,11 @@ pub fn load_fonts(ctx: &egui::Context) {
|
|||
|
||||
/// The window icon (Linux taskbars and window switchers; macOS uses the app bundle's).
|
||||
fn app_icon() -> egui::IconData {
|
||||
let svg = include_str!("../icons/anybot.svg");
|
||||
let px = 256u32;
|
||||
svg_icon(include_str!("../icons/anybot.svg"), 256)
|
||||
}
|
||||
|
||||
/// An SVG drawn to a square RGBA picture of `px` pixels.
|
||||
fn svg_icon(svg: &str, px: u32) -> egui::IconData {
|
||||
let tree = resvg::usvg::Tree::from_str(svg, &resvg::usvg::Options::default()).expect("icon svg");
|
||||
let mut pm = resvg::tiny_skia::Pixmap::new(px, px).expect("icon pixmap");
|
||||
let s = px as f32 / tree.size().width();
|
||||
|
|
@ -166,11 +169,14 @@ fn app_icon() -> egui::IconData {
|
|||
egui::IconData { rgba, width: px, height: px }
|
||||
}
|
||||
|
||||
/// `anybot --write-icon out.png [size]`: the app icon as a PNG (for Linux packages).
|
||||
fn write_icon(out: &str, px: u32) -> Result<(), String> {
|
||||
let icon = app_icon();
|
||||
/// `anybot --write-icon out.png [size] [file.svg]`: the app icon (or that SVG) as a PNG, drawn at
|
||||
/// that size (for the Mac bundle, Linux packages and the web icons).
|
||||
fn write_icon(out: &str, px: u32, svg: Option<&str>) -> Result<(), String> {
|
||||
let icon = match svg {
|
||||
Some(path) => svg_icon(&std::fs::read_to_string(path).map_err(|e| format!("{path}: {e}"))?, px),
|
||||
None => svg_icon(include_str!("../icons/anybot.svg"), px),
|
||||
};
|
||||
let img = image::RgbaImage::from_raw(icon.width, icon.height, icon.rgba).ok_or("icon")?;
|
||||
let img = if px != icon.width { image::imageops::resize(&img, px, px, image::imageops::FilterType::Lanczos3) } else { img };
|
||||
img.save(out).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
|
|
@ -191,7 +197,7 @@ fn main() -> eframe::Result<()> {
|
|||
if args.get(1).map(String::as_str) == Some("--write-icon") {
|
||||
let out = args.get(2).cloned().unwrap_or_else(|| "anybot.png".into());
|
||||
let px = args.get(3).and_then(|v| v.parse().ok()).unwrap_or(256);
|
||||
if let Err(e) = write_icon(&out, px) {
|
||||
if let Err(e) = write_icon(&out, px, args.get(4).map(String::as_str)) {
|
||||
eprintln!("{e}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -324,6 +324,7 @@ pub fn handle(req: Req, via_relay: bool) -> Resp {
|
|||
("GET", []) | ("GET", ["index.html"]) => return page(),
|
||||
("GET", ["manifest.webmanifest"]) => return Resp::bytes("application/manifest+json", MANIFEST.as_bytes().to_vec()),
|
||||
("GET", ["icon.svg"]) => return Resp::bytes("image/svg+xml", ICON.as_bytes().to_vec()),
|
||||
("GET", [name]) if ICON_PNGS.iter().any(|(n, _)| n == name) => return Resp::bytes("image/png", ICON_PNGS.iter().find(|(n, _)| n == name).map(|(_, b)| b.to_vec()).unwrap_or_default()),
|
||||
("GET", ["api", "hello"]) => {
|
||||
let s = hub().settings();
|
||||
return Resp::json(&json!({"app": "anybot", "host": s.host_name, "relay": via_relay, "needs_password": !via_relay}));
|
||||
|
|
@ -1464,8 +1465,11 @@ pub fn devices_online(s: &Settings) -> Vec<(String, String, bool)> {
|
|||
// ---------- the phone page ----------
|
||||
|
||||
const PAGE: &str = include_str!("../mobile/index.html");
|
||||
pub const MANIFEST: &str = r##"{"name":"AnyBot","short_name":"AnyBot","start_url":"./","display":"standalone","background_color":"#121212","theme_color":"#121212","icons":[{"src":"icon.svg","sizes":"any","type":"image/svg+xml"}]}"##;
|
||||
pub const ICON: &str = r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96"><rect width="96" height="96" rx="22" fill="#161616"/><circle cx="48" cy="48" r="24" fill="none" stroke="#f5f5f5" stroke-width="7"/><circle cx="48" cy="48" r="7" fill="#f5f5f5"/></svg>"##;
|
||||
pub const MANIFEST: &str = r##"{"name":"AnyBot","short_name":"AnyBot","start_url":"./","display":"standalone","background_color":"#121212","theme_color":"#121212","icons":[{"src":"icon.svg","sizes":"any","type":"image/svg+xml"},{"src":"icon-192.png","sizes":"192x192","type":"image/png","purpose":"any maskable"},{"src":"icon-512.png","sizes":"512x512","type":"image/png","purpose":"any maskable"}]}"##;
|
||||
/// The app's icon, the same drawing everywhere (icons/anybot.svg is the Mac/Linux one).
|
||||
pub const ICON: &str = include_str!("../mobile/icon.svg");
|
||||
/// PNG sizes for the iPhone home screen (it ignores SVG) and Android.
|
||||
pub const ICON_PNGS: [(&str, &[u8]); 3] = [("icon-180.png", include_bytes!("../mobile/icon-180.png")), ("icon-192.png", include_bytes!("../mobile/icon-192.png")), ("icon-512.png", include_bytes!("../mobile/icon-512.png"))];
|
||||
|
||||
pub fn page() -> Resp {
|
||||
Resp::bytes("text/html; charset=utf-8", PAGE.as_bytes().to_vec()).with_header("Content-Security-Policy", "default-src 'self'; img-src 'self' blob: data:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'; connect-src 'self'")
|
||||
|
|
|
|||