Unread counts, a window margin, blurred dialogs, softer group names
Each rise in an app's unread count while you are elsewhere adds to a tally beside its name, and a dot on its icon in the rail. Looking at the app is the only thing that clears it. The window has no frame of its own, so the shell now keeps a 6px margin in a normal window - the only part of it that is not a web page, and so the only place left to grab. Full screen gives the room back. Dialogs get the app blurred behind them. An app's webview paints above the shell, so it has to be moved aside before a dialog can be seen at all, and once moved there is nothing left to blur - a still taken on the way out is the only way to keep the background there. It runs on a blocking worker: its completion handler is on the main thread, and waiting there deadlocks until the timeout and returns nothing. Settings is much larger, the cog moved to the foot of the nav, group names read as names rather than shouted headings, and "Refresh icons" bumps a version every favicon URL carries, for the ones that cache wrong.
This commit is contained in:
+70
-5
@@ -1,5 +1,6 @@
|
||||
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
|
||||
import * as api from "./api";
|
||||
import Nav from "./components/Nav";
|
||||
@@ -14,6 +15,9 @@ export default function App() {
|
||||
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||
const [focusHidden, setFocusHidden] = useState<string | null>(null);
|
||||
const [theme, setTheme] = useAppearance("system");
|
||||
const [unread, setUnread] = useState<Record<string, number>>({});
|
||||
const [fullscreen, setFullscreen] = useState(false);
|
||||
const [backdrop, setBackdrop] = useState<string | null>(null);
|
||||
|
||||
const stageRef = useRef<HTMLDivElement>(null);
|
||||
const booted = useRef(false);
|
||||
@@ -24,6 +28,20 @@ export default function App() {
|
||||
|
||||
const collapsed = config?.settings.navCollapsed ?? false;
|
||||
|
||||
/* The window has no frame of its own, so in a normal window the shell keeps
|
||||
a margin around itself: somewhere to grab that is not a web page, and the
|
||||
only way to move or place the window by hand. Full screen has no use for
|
||||
it and gives the room back. */
|
||||
useEffect(() => {
|
||||
const w = getCurrentWindow();
|
||||
const check = () => void w.isFullscreen().then(setFullscreen);
|
||||
check();
|
||||
const un = w.onResized(check);
|
||||
return () => {
|
||||
void un.then((f) => f());
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
api.getConfig().then((c) => {
|
||||
setConfig(c);
|
||||
@@ -103,17 +121,37 @@ export default function App() {
|
||||
listen<[string, number]>("zoom-changed", () => {
|
||||
void api.getConfig().then(setConfig);
|
||||
}),
|
||||
listen<[string, number][]>("unread-changed", (e) => {
|
||||
setUnread(Object.fromEntries(e.payload));
|
||||
}),
|
||||
];
|
||||
return () => {
|
||||
unlisten.forEach((p) => p.then((f) => f()));
|
||||
};
|
||||
}, []);
|
||||
|
||||
// A native view paints over anything the shell draws, so a dialog needs the
|
||||
// stage out of the way rather than merely on a higher z-index.
|
||||
/* A native view paints over anything the shell draws, so a dialog needs the
|
||||
app moved out of the way rather than merely a higher z-index — and once it
|
||||
is moved there is nothing left behind the dialog to look at. A still taken
|
||||
on the way out, blurred, puts the background back. */
|
||||
useEffect(() => {
|
||||
if (!config) return;
|
||||
void (settingsOpen ? api.hideStage() : api.showStage());
|
||||
let cancelled = false;
|
||||
|
||||
if (settingsOpen) {
|
||||
void (async () => {
|
||||
const shot = await api.stageSnapshot().catch(() => null);
|
||||
if (cancelled) return;
|
||||
setBackdrop(shot);
|
||||
await api.hideStage();
|
||||
})();
|
||||
} else {
|
||||
setBackdrop(null);
|
||||
void api.showStage();
|
||||
}
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [settingsOpen, config]);
|
||||
|
||||
const toggleCollapse = () => {
|
||||
@@ -137,8 +175,20 @@ export default function App() {
|
||||
|
||||
if (!config) return null;
|
||||
|
||||
const frame = fullscreen ? 0 : 6;
|
||||
|
||||
return (
|
||||
<div className="flex h-screen">
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className="flex h-screen bg-slate-200 dark:bg-black"
|
||||
style={{ padding: frame }}
|
||||
>
|
||||
<div
|
||||
className={
|
||||
"flex min-w-0 flex-1 overflow-hidden " +
|
||||
(fullscreen ? "" : "rounded-xl border border-slate-300 dark:border-slate-800")
|
||||
}
|
||||
>
|
||||
<Nav
|
||||
config={config}
|
||||
activeId={activeId}
|
||||
@@ -150,11 +200,24 @@ export default function App() {
|
||||
onBack={() => activeId && api.historyGo(activeId, -1)}
|
||||
onForward={() => activeId && api.historyGo(activeId, 1)}
|
||||
onReload={() => activeId && api.historyGo(activeId, 0)}
|
||||
unread={unread}
|
||||
/>
|
||||
|
||||
{/* The hole an app's native webview is positioned into. It stays empty
|
||||
on purpose — anything drawn here would be painted over. */}
|
||||
<div ref={stageRef} className="min-w-0 flex-1 bg-slate-100 dark:bg-slate-950">
|
||||
<div
|
||||
ref={stageRef}
|
||||
className="relative min-w-0 flex-1 overflow-hidden bg-slate-100 dark:bg-slate-950"
|
||||
>
|
||||
{backdrop && (
|
||||
<img
|
||||
src={backdrop}
|
||||
alt=""
|
||||
aria-hidden
|
||||
/* Scaled up so the blur has no soft edge to give itself away. */
|
||||
className="absolute inset-0 h-full w-full scale-110 object-cover blur-[16px]"
|
||||
/>
|
||||
)}
|
||||
{config.apps.length === 0 && (
|
||||
<div className="flex h-full flex-col items-center justify-center gap-3 px-6 text-center">
|
||||
<p className="text-[13px] text-slate-500 dark:text-slate-400">
|
||||
@@ -167,6 +230,8 @@ export default function App() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{settingsOpen && (
|
||||
<Settings
|
||||
config={config}
|
||||
|
||||
Reference in New Issue
Block a user