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
|
||||
|
||||
- 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
|
||||
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
|
||||
|
||||
22
src/web.rs
22
src/web.rs
@@ -67,6 +67,7 @@ pub fn router(state: WebState) -> Router {
|
||||
.route("/login", get(login_page))
|
||||
.route("/api/login", post(login))
|
||||
.route("/icon.png", get(icon))
|
||||
.route("/logo.svg", get(logo))
|
||||
.route("/favicon.ico", get(favicon))
|
||||
.route("/favicon.png", get(favicon))
|
||||
.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)
|
||||
}
|
||||
|
||||
/// The 2004 icon, served once for both pages rather than inlined as base64 into each. The
|
||||
/// sign-in page shows it, so it sits outside the auth layer with /login.
|
||||
/// The logo the pages show, served once rather than inlined into each. The sign-in page shows
|
||||
/// 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 {
|
||||
(
|
||||
[(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
|
||||
/// square can be passed over. /favicon.ico is the same PNG, for a browser that asks for that
|
||||
/// on its own; behind the auth layer it answered 401, and the tab stayed blank.
|
||||
/// web/logo.svg at 128px. /favicon.ico is the same PNG, for a browser that asks for that on its
|
||||
/// own; behind the auth layer it answered 401, and the tab stayed blank.
|
||||
async fn favicon() -> impl IntoResponse {
|
||||
(
|
||||
[(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
|
||||
/// one is on white.
|
||||
/// web/logo.svg at 180px, for an iPhone's home screen. It is opaque edge to edge, as iOS paints
|
||||
/// a transparent icon's background black and cuts its own corners.
|
||||
async fn touch_icon() -> impl IntoResponse {
|
||||
(
|
||||
[(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 page = await ctx.newPage();
|
||||
await page.goto('/login');
|
||||
// The sign-in page shows the icon, so it has to load before anyone has signed in.
|
||||
const icon = await page.request.get('/icon.png');
|
||||
expect(icon.status()).toBe(200);
|
||||
expect(icon.headers()['content-type']).toBe('image/png');
|
||||
// The sign-in page shows the logo, so it has to load before anyone has signed in.
|
||||
const logo = await page.request.get('/logo.svg');
|
||||
expect(logo.status()).toBe(200);
|
||||
expect(logo.headers()['content-type']).toBe('image/svg+xml');
|
||||
await page.locator('#name').fill('sam');
|
||||
await page.locator('#pw').fill('sampassword');
|
||||
await page.locator('button[type=submit]').click();
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<!-- The server sends this page, and its script, to admins only. -->
|
||||
<header id="topbar">
|
||||
<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>
|
||||
<span class="grow"></span>
|
||||
<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 .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}
|
||||
.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 #topbar{position:sticky;top:0;z-index:5}
|
||||
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 #admin{max-width:780px;margin:0 auto}
|
||||
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">
|
||||
<aside id="sidebar">
|
||||
<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 class="searchwrap"><input type="search" id="feedFilter" placeholder="Filter feeds…"></div>
|
||||
<div id="feedlist"></div>
|
||||
|
||||
@@ -55,9 +55,9 @@ form{
|
||||
width:min(360px,100%);background:var(--panel);border:1px solid var(--line);
|
||||
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 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}
|
||||
label{display:block;font-size:12px;color:var(--dim);margin:0 0 4px}
|
||||
input{
|
||||
@@ -74,7 +74,7 @@ button:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
|
||||
</style>
|
||||
|
||||
<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>
|
||||
<input id="name" name="name" autocomplete="username" autofocus required>
|
||||
<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