A logo drawn from the radio's screen, to app icon guidelines (#55)
The 2004 icon is a whole radio on a transparent background and not square, so the tab icon was padded and iOS painted the home-screen icon on white. web/logo.svg is the radio's screen alone, its tuning scale and orange needle, laid out as Apple's app icon guidelines ask: opaque and full-bleed (the system cuts its own corners), a gradient background and flat foreground layers with hard edges, no highlights or shadows of its own, nothing thin enough to vanish at 32px. Each layer is a <g>, ready to split out for Icon Composer. web/logo-dark.svg is the same layers recoloured. favicon.png (128) and apple-touch-icon.png (180) are renders of it. The pages show it from /logo.svg, served outside the auth layer for the sign-in page, with an app icon's rounded corners. The 2004 icon stays at /icon.png for anything outside that links to it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- A new logo, on every page, in the browser tab and on the home screen: the old radio's screen,
|
||||||
|
its tuning scale and orange needle, drawn flat to Apple's app icon guidelines, with a dark
|
||||||
|
version. The 2004 icon is still at /icon.png.
|
||||||
- Words that hide items. Settings has a list for every feed you read, and each feed's settings a
|
- Words that hide items. Settings has a list for every feed you read, and each feed's settings a
|
||||||
list of its own; an item with one of those words or phrases in its title or text is hidden
|
list of its own; an item with one of those words or phrases in its title or text is hidden
|
||||||
from you and is not downloaded on your account. Other people's lists never hide anything from
|
from you and is not downloaded on your account. Other people's lists never hide anything from
|
||||||
|
|||||||
22
src/web.rs
22
src/web.rs
@@ -67,6 +67,7 @@ pub fn router(state: WebState) -> Router {
|
|||||||
.route("/login", get(login_page))
|
.route("/login", get(login_page))
|
||||||
.route("/api/login", post(login))
|
.route("/api/login", post(login))
|
||||||
.route("/icon.png", get(icon))
|
.route("/icon.png", get(icon))
|
||||||
|
.route("/logo.svg", get(logo))
|
||||||
.route("/favicon.ico", get(favicon))
|
.route("/favicon.ico", get(favicon))
|
||||||
.route("/favicon.png", get(favicon))
|
.route("/favicon.png", get(favicon))
|
||||||
.route("/apple-touch-icon.png", get(touch_icon))
|
.route("/apple-touch-icon.png", get(touch_icon))
|
||||||
@@ -552,8 +553,16 @@ fn script(js: &'static str) -> impl IntoResponse {
|
|||||||
([(header::CONTENT_TYPE, "text/javascript; charset=utf-8"), (header::CACHE_CONTROL, SCRIPT_CACHE)], js)
|
([(header::CONTENT_TYPE, "text/javascript; charset=utf-8"), (header::CACHE_CONTROL, SCRIPT_CACHE)], js)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The 2004 icon, served once for both pages rather than inlined as base64 into each. The
|
/// The logo the pages show, served once rather than inlined into each. The sign-in page shows
|
||||||
/// sign-in page shows it, so it sits outside the auth layer with /login.
|
/// it, so it sits outside the auth layer with /login.
|
||||||
|
async fn logo() -> impl IntoResponse {
|
||||||
|
(
|
||||||
|
[(header::CONTENT_TYPE, "image/svg+xml"), (header::CACHE_CONTROL, "max-age=86400")],
|
||||||
|
include_str!("../web/logo.svg"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The 2004 icon. Nothing here shows it any more; it stays for whatever outside ipx links to it.
|
||||||
async fn icon() -> impl IntoResponse {
|
async fn icon() -> impl IntoResponse {
|
||||||
(
|
(
|
||||||
[(header::CONTENT_TYPE, "image/png"), (header::CACHE_CONTROL, "max-age=86400")],
|
[(header::CONTENT_TYPE, "image/png"), (header::CACHE_CONTROL, "max-age=86400")],
|
||||||
@@ -561,9 +570,8 @@ async fn icon() -> impl IntoResponse {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The logo, squared up with transparent padding: it is 128x121, and a tab icon that is not
|
/// web/logo.svg at 128px. /favicon.ico is the same PNG, for a browser that asks for that on its
|
||||||
/// square can be passed over. /favicon.ico is the same PNG, for a browser that asks for that
|
/// own; behind the auth layer it answered 401, and the tab stayed blank.
|
||||||
/// on its own; behind the auth layer it answered 401, and the tab stayed blank.
|
|
||||||
async fn favicon() -> impl IntoResponse {
|
async fn favicon() -> impl IntoResponse {
|
||||||
(
|
(
|
||||||
[(header::CONTENT_TYPE, "image/png"), (header::CACHE_CONTROL, "max-age=86400")],
|
[(header::CONTENT_TYPE, "image/png"), (header::CACHE_CONTROL, "max-age=86400")],
|
||||||
@@ -571,8 +579,8 @@ async fn favicon() -> impl IntoResponse {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// For an iPhone's home screen, which paints a transparent icon's background black, so this
|
/// web/logo.svg at 180px, for an iPhone's home screen. It is opaque edge to edge, as iOS paints
|
||||||
/// one is on white.
|
/// a transparent icon's background black and cuts its own corners.
|
||||||
async fn touch_icon() -> impl IntoResponse {
|
async fn touch_icon() -> impl IntoResponse {
|
||||||
(
|
(
|
||||||
[(header::CONTENT_TYPE, "image/png"), (header::CACHE_CONTROL, "max-age=86400")],
|
[(header::CONTENT_TYPE, "image/png"), (header::CACHE_CONTROL, "max-age=86400")],
|
||||||
|
|||||||
@@ -558,10 +558,10 @@ test('a second person has their own feeds and their own read state', async ({ br
|
|||||||
const ctx = await browser.newContext();
|
const ctx = await browser.newContext();
|
||||||
const page = await ctx.newPage();
|
const page = await ctx.newPage();
|
||||||
await page.goto('/login');
|
await page.goto('/login');
|
||||||
// The sign-in page shows the icon, so it has to load before anyone has signed in.
|
// The sign-in page shows the logo, so it has to load before anyone has signed in.
|
||||||
const icon = await page.request.get('/icon.png');
|
const logo = await page.request.get('/logo.svg');
|
||||||
expect(icon.status()).toBe(200);
|
expect(logo.status()).toBe(200);
|
||||||
expect(icon.headers()['content-type']).toBe('image/png');
|
expect(logo.headers()['content-type']).toBe('image/svg+xml');
|
||||||
await page.locator('#name').fill('sam');
|
await page.locator('#name').fill('sam');
|
||||||
await page.locator('#pw').fill('sampassword');
|
await page.locator('#pw').fill('sampassword');
|
||||||
await page.locator('button[type=submit]').click();
|
await page.locator('button[type=submit]').click();
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
<!-- The server sends this page, and its script, to admins only. -->
|
<!-- The server sends this page, and its script, to admins only. -->
|
||||||
<header id="topbar">
|
<header id="topbar">
|
||||||
<a class="btn ico" href="/" title="Back to iPodderX" aria-label="Back to iPodderX" data-icon="left"></a>
|
<a class="btn ico" href="/" title="Back to iPodderX" aria-label="Back to iPodderX" data-icon="left"></a>
|
||||||
<img class="logo" src="/icon.png" alt="" width="26">
|
<img class="logo" src="/logo.svg" alt="" width="26">
|
||||||
<h1>Admin</h1>
|
<h1>Admin</h1>
|
||||||
<span class="grow"></span>
|
<span class="grow"></span>
|
||||||
<nav class="tabs" id="atabs">
|
<nav class="tabs" id="atabs">
|
||||||
|
|||||||
@@ -495,7 +495,7 @@ a{color:var(--accent)}
|
|||||||
}
|
}
|
||||||
.brand{display:flex;align-items:center;gap:9px;padding:14px 14px 10px}
|
.brand{display:flex;align-items:center;gap:9px;padding:14px 14px 10px}
|
||||||
.brand .logo{
|
.brand .logo{
|
||||||
width:30px;height:30px;flex:none;object-fit:contain;
|
width:30px;height:30px;flex:none;border-radius:7px;
|
||||||
}
|
}
|
||||||
.brand h1{font-size:16px;margin:0;font-weight:650;letter-spacing:-.01em;color:var(--fg);flex:1}
|
.brand h1{font-size:16px;margin:0;font-weight:650;letter-spacing:-.01em;color:var(--fg);flex:1}
|
||||||
.iconbtn{
|
.iconbtn{
|
||||||
@@ -768,7 +768,7 @@ kbd{font:inherit;font-size:12px;color:var(--fg);background:var(--panel2);border:
|
|||||||
body.adminpage{display:block;overflow:auto}
|
body.adminpage{display:block;overflow:auto}
|
||||||
body.adminpage #topbar{position:sticky;top:0;z-index:5}
|
body.adminpage #topbar{position:sticky;top:0;z-index:5}
|
||||||
body.adminpage #topbar h1{font-size:16px;margin:0;font-weight:650}
|
body.adminpage #topbar h1{font-size:16px;margin:0;font-weight:650}
|
||||||
body.adminpage #topbar .logo{border-radius:5px}
|
body.adminpage #topbar .logo{border-radius:6px}
|
||||||
body.adminpage #topbar > a.btn{text-decoration:none}
|
body.adminpage #topbar > a.btn{text-decoration:none}
|
||||||
body.adminpage #admin{max-width:780px;margin:0 auto}
|
body.adminpage #admin{max-width:780px;margin:0 auto}
|
||||||
body.adminpage #admin h2{font-size:18px;margin:0 0 12px}
|
body.adminpage #admin h2{font-size:18px;margin:0 0 12px}
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 5.5 KiB |
BIN
web/favicon.png
BIN
web/favicon.png
Binary file not shown.
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 3.2 KiB |
@@ -34,7 +34,7 @@
|
|||||||
<div id="shell">
|
<div id="shell">
|
||||||
<aside id="sidebar">
|
<aside id="sidebar">
|
||||||
<div class="brand">
|
<div class="brand">
|
||||||
<img class="logo" src="/icon.png" alt="iPodderX" title="The original iPodderX icon, 2004"><h1>iPodderX</h1>
|
<img class="logo" src="/logo.svg" alt="iPodderX"><h1>iPodderX</h1>
|
||||||
</div>
|
</div>
|
||||||
<div class="searchwrap"><input type="search" id="feedFilter" placeholder="Filter feeds…"></div>
|
<div class="searchwrap"><input type="search" id="feedFilter" placeholder="Filter feeds…"></div>
|
||||||
<div id="feedlist"></div>
|
<div id="feedlist"></div>
|
||||||
|
|||||||
@@ -55,9 +55,9 @@ form{
|
|||||||
width:min(360px,100%);background:var(--panel);border:1px solid var(--line);
|
width:min(360px,100%);background:var(--panel);border:1px solid var(--line);
|
||||||
border-radius:14px;padding:22px;box-shadow:var(--shadow);
|
border-radius:14px;padding:22px;box-shadow:var(--shadow);
|
||||||
}
|
}
|
||||||
/* The one place the 2004 icon is shown at the size it was drawn for. */
|
/* The logo is a square app icon, so it gets an app icon's corners. */
|
||||||
.brand{display:flex;flex-direction:column;align-items:center;gap:6px;margin-bottom:20px}
|
.brand{display:flex;flex-direction:column;align-items:center;gap:6px;margin-bottom:20px}
|
||||||
.brand img{width:96px;height:auto}
|
.brand img{width:72px;height:72px;border-radius:16px}
|
||||||
h1{font-size:21px;margin:0;font-weight:650;letter-spacing:-.01em}
|
h1{font-size:21px;margin:0;font-weight:650;letter-spacing:-.01em}
|
||||||
label{display:block;font-size:12px;color:var(--dim);margin:0 0 4px}
|
label{display:block;font-size:12px;color:var(--dim);margin:0 0 4px}
|
||||||
input{
|
input{
|
||||||
@@ -74,7 +74,7 @@ button:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
|
|||||||
</style>
|
</style>
|
||||||
|
|
||||||
<form id="f">
|
<form id="f">
|
||||||
<div class="brand"><img src="/icon.png" alt=""><h1>iPodderX</h1></div>
|
<div class="brand"><img src="/logo.svg" alt=""><h1>iPodderX</h1></div>
|
||||||
<label for="name">Name</label>
|
<label for="name">Name</label>
|
||||||
<input id="name" name="name" autocomplete="username" autofocus required>
|
<input id="name" name="name" autocomplete="username" autofocus required>
|
||||||
<label for="pw">Password</label>
|
<label for="pw">Password</label>
|
||||||
|
|||||||
19
web/logo-dark.svg
Normal file
19
web/logo-dark.svg
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
||||||
|
<!-- logo.svg's dark appearance: the same layers in the same places, only recoloured, as the
|
||||||
|
guidelines ask, so the icon reads as itself in either. -->
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0" stop-color="#1d4468"/>
|
||||||
|
<stop offset="1" stop-color="#0c2238"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<g id="background"><rect width="1024" height="1024" fill="url(#bg)"/></g>
|
||||||
|
<g id="scale" fill="#8fc2ea">
|
||||||
|
<rect x="120" y="520" width="112" height="304" rx="56" opacity=".45"/>
|
||||||
|
<rect x="288" y="360" width="112" height="464" rx="56" opacity=".7"/>
|
||||||
|
<rect x="456" y="440" width="112" height="384" rx="56" opacity=".55"/>
|
||||||
|
<rect x="624" y="280" width="112" height="544" rx="56" opacity=".85"/>
|
||||||
|
<rect x="792" y="600" width="112" height="224" rx="56" opacity=".5"/>
|
||||||
|
</g>
|
||||||
|
<g id="needle"><rect x="728" y="160" width="72" height="704" rx="36" fill="#ff9f2e"/></g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1003 B |
22
web/logo.svg
Normal file
22
web/logo.svg
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
|
||||||
|
<!-- The iPodderX radio's screen, reduced to its tuning scale and needle, laid out as Apple's
|
||||||
|
app icon guidelines ask: square and opaque (the system cuts the corners, and paints
|
||||||
|
transparency black), one background and flat foreground layers with hard edges, no
|
||||||
|
highlights or shadows of its own (the system adds them), nothing thin enough to vanish
|
||||||
|
at 32px. Each <g> is a layer, for splitting out into Icon Composer. -->
|
||||||
|
<defs>
|
||||||
|
<linearGradient id="bg" x1="0" y1="0" x2="0" y2="1">
|
||||||
|
<stop offset="0" stop-color="#dff0fb"/>
|
||||||
|
<stop offset="1" stop-color="#9fccef"/>
|
||||||
|
</linearGradient>
|
||||||
|
</defs>
|
||||||
|
<g id="background"><rect width="1024" height="1024" fill="url(#bg)"/></g>
|
||||||
|
<g id="scale" fill="#2f6aa0">
|
||||||
|
<rect x="120" y="520" width="112" height="304" rx="56" opacity=".45"/>
|
||||||
|
<rect x="288" y="360" width="112" height="464" rx="56" opacity=".7"/>
|
||||||
|
<rect x="456" y="440" width="112" height="384" rx="56" opacity=".55"/>
|
||||||
|
<rect x="624" y="280" width="112" height="544" rx="56" opacity=".85"/>
|
||||||
|
<rect x="792" y="600" width="112" height="224" rx="56" opacity=".5"/>
|
||||||
|
</g>
|
||||||
|
<g id="needle"><rect x="728" y="160" width="72" height="704" rx="36" fill="#f7931e"/></g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.3 KiB |
Reference in New Issue
Block a user