Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1721,9 +1721,8 @@ async fn update(app: &App, short: &str) -> Result<(bool, String), anyhow::Error>
Ok((true, theme.version))
}

/// The thumbnail the theme list displays, where the theme provides one. A theme
/// without one returns 404, on which the panel hides the image, so nothing need
/// report whether a preview exists.
/// The thumbnail the theme list displays, where the theme provides one; the list
/// reports which do.
pub async fn theme_preview(_: Admin, State(app): State<Shared>, Path(short): Path<String>) -> Response {
match crate::frontend::preview(&app.themes, &short) {
// Not cached: reinstalling a theme under the same name also replaces the
Expand Down Expand Up @@ -1807,7 +1806,14 @@ pub async fn save_theme_config(

pub async fn themes(_: Admin, State(app): State<Shared>) -> Response {
match crate::frontend::themes(&app) {
Ok(themes) => Json(json!({"themes": themes})).into_response(),
Ok(mut themes) => {
// Reads each image to answer, as serving it would: a handful of
// themes, each image capped at 8 MiB.
for theme in &mut themes {
theme.preview = crate::frontend::preview(&app.themes, &theme.short).is_some();
}
Json(json!({"themes": themes})).into_response()
}
Err(e) => fail(e),
}
}
Expand Down
4 changes: 4 additions & 0 deletions src/frontend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ pub struct Theme {
/// deleted.
#[serde(skip_deserializing)]
pub builtin: bool,
/// Whether [`preview`] has an image for it. Filled in for the panel's list
/// only, so it lays each card out once rather than when the image arrives.
#[serde(skip_deserializing)]
pub preview: bool,
}

pub async fn serve(State(app): State<Shared>, headers: HeaderMap, uri: Uri) -> Response {
Expand Down
110 changes: 63 additions & 47 deletions web-admin/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Admin } from "@/components/Admin"
import { Login } from "@/components/Login"
import { Button } from "@/components/ui/button"
import { Skeleton } from "@/components/ui/skeleton"
import { TooltipProvider } from "@/components/ui/tooltip"
import { api, provisionRefusal, useNodes } from "@/lib/api"

type Me = { authed: boolean; github: boolean; site_name: string; public_page: boolean; site: string }
Expand Down Expand Up @@ -61,8 +62,18 @@ function useTheme() {
)
const dark = saved ? saved === "dark" : system

// Switched with every transition held. Buttons, badges and table rows fade
// their colours over 150 ms while everything else changes at once, leaving
// the page in both palettes for that long.
useEffect(() => {
const hold = document.createElement("style")
hold.textContent = "*,*::before,*::after{transition:none!important}"
document.head.append(hold)
document.documentElement.classList.toggle("dark", dark)
// Resolves the new colours while transitions are off, so removing the
// hold starts none.
void document.body.offsetWidth
hold.remove()
}, [dark])

return [
Expand Down Expand Up @@ -128,53 +139,58 @@ export default function App() {
}

return (
<div className="min-h-svh">
<header className="sticky top-0 z-10 border-b bg-background/80 backdrop-blur">
<div className="mx-auto flex max-w-7xl items-center gap-3 px-4 py-3">
{/* The site name is the way back to the status page, as in the
theme's own header. */}
<a href="/" className="font-semibold transition-opacity hover:opacity-70">
{me.site_name || "Monitor"}
</a>
<span className="text-xs text-muted-foreground">后台</span>
<div className="flex-1" />
{/* The status page is a separate app, so this is a navigation. */}
<Button variant="ghost" size="sm" asChild>
<a href="/">
<ExternalLink /> 状态面板
// The browser's own title tooltip waits a second or more and cannot be
// shortened; these open after 300 ms.
<TooltipProvider delayDuration={300}>
<div className="min-h-svh">
<header className="sticky top-0 z-10 border-b bg-background/80 backdrop-blur">
<div className="mx-auto flex max-w-7xl items-center gap-3 px-4 py-3">
{/* The site name is the way back to the status page, as in the
theme's own header. */}
<a href="/" className="font-semibold transition-opacity hover:opacity-70">
{me.site_name || "Monitor"}
</a>
</Button>
<Button variant="ghost" size="icon" onClick={toggleTheme} title="切换主题">
{dark ? <Sun /> : <Moon />}
</Button>
<Button variant="ghost" size="icon" onClick={signOut} title="退出登录">
<LogOut />
</Button>
</div>
</header>

<main className="mx-auto max-w-7xl space-y-5 px-4 py-6">
{error && <p className="text-sm text-destructive">{error}</p>}
{!nodes ? (
<Skeleton className="h-64" />
) : (
<Admin
path={path}
go={go}
nodes={sorted}
refresh={refresh}
// The hub's own public URL rather than this browser's address: the
// panel is frequently reached over a loopback port behind a proxy,
// while the install command and OAuth callback need the real one.
site={me.site || location.origin}
// Why this page cannot add nodes, measured by the rule the hub applies
// to the `Origin` it receives; empty when it can.
refusal={provisionRefusal(location.origin, me.site)}
/>
)}
</main>

<Toaster position="top-center" theme={dark ? "dark" : "light"} />
</div>
<span className="text-xs text-muted-foreground">后台</span>
<div className="flex-1" />
{/* The status page is a separate app, so this is a navigation. */}
<Button variant="ghost" size="sm" asChild>
<a href="/">
<ExternalLink /> 状态面板
</a>
</Button>
<Button variant="ghost" size="icon" onClick={toggleTheme} title="切换主题">
{dark ? <Sun /> : <Moon />}
</Button>
<Button variant="ghost" size="icon" onClick={signOut} title="退出登录">
<LogOut />
</Button>
</div>
</header>

<main className="mx-auto max-w-7xl space-y-5 px-4 py-6">
{error && <p className="text-sm text-destructive">{error}</p>}
{!nodes ? (
<Skeleton className="h-64" />
) : (
<Admin
path={path}
go={go}
nodes={sorted}
refresh={refresh}
// The hub's own public URL rather than this browser's address: the
// panel is frequently reached over a loopback port behind a proxy,
// while the install command and OAuth callback need the real one.
site={me.site || location.origin}
// Why this page cannot add nodes, measured by the rule the hub applies
// to the `Origin` it receives; empty when it can.
refusal={provisionRefusal(location.origin, me.site)}
reloadMe={loadMe}
/>
)}
</main>

<Toaster position="top-center" theme={dark ? "dark" : "light"} />
</div>
</TooltipProvider>
)
}
Loading
Loading