import { useEffect, useReducer, useRef, useState } from 'react'; import { useI18n, LANGS } from '../i18n/I18nProvider'; import { ACCENTS, FONT_SETS, useTheme, type FontSetId } from '../theme/ThemeProvider'; import { scanSession, watchScan } from '../engine/library'; import { Avatar } from './Avatar'; import type { User } from '../api'; // Header: the app's TopBar minus everything camera-only, plus the two controls // the web build adds — theme (light/dark + accent group) and language. export function TopBar({ recipeName, pro, user, exporting, savingPhoto, canUndo, canRedo, onUndo, onRedo, onReset, onSavePhoto, onExport, photoCount, photoMax, onAuth, onSignup, onLogout, hasImmich, uploadingImmich, onUploadImmich, }: { recipeName: string; // Saving into the account's own folder is the account's own act, so the // button wears the same PRO marker the chips do until the address is proven. pro: boolean; user: User | null; exporting: boolean; savingPhoto: boolean; canUndo: boolean; canRedo: boolean; onUndo: () => void; onRedo: () => void; onReset: () => void; onSavePhoto: () => void; onExport: () => void; // This account's own folder: the SAVE PHOTO button states the count, and the // API refuses past the cap. A guest sees 0/12 and is sent to sign in. null is // the admin, who the API lets past the cap — the button then just counts. photoCount: number; photoMax: number | null; onAuth: () => void; onSignup: () => void; onLogout: () => void; hasImmich?: boolean; uploadingImmich?: boolean; onUploadImmich?: () => void; }) { const { t, lang, setLang } = useI18n(); const { mode, accent, fonts, setMode, setAccent, setFonts } = useTheme(); const [menu, setMenu] = useState<'theme' | 'lang' | 'account' | null>(null); // The phone's one button: on a narrow screen the row of controls is the whole // of a second and a third line, so it folds away into a panel under the // hamburger in the corner (see .bar-menu in app.css). A wide screen never // opens it — there the panel is the row itself. const [bar, setBar] = useState(false); const wrap = useRef(null); // The catalogue's own reading runs in this tab whatever screen is up, so the // header says so while it does. const live = scanSession(); const [, bump] = useReducer((n: number) => n + 1, 0); useEffect(() => { const unwatch = watchScan(() => bump()); bump(); // a scan already running when the studio comes up return unwatch; }, []); useEffect(() => { if (!menu && !bar) return; const away = (e: MouseEvent) => { if (wrap.current?.contains(e.target as Node)) return; setMenu(null); setBar(false); }; document.addEventListener('mousedown', away); return () => document.removeEventListener('mousedown', away); }, [menu, bar]); return (
RecipesCam {t('nav.studio')} {recipeName} {/* Everything the bar carries, in one box so a phone can fold it away: see `.bar-menu` — `display: contents` wherever the row has the room for the buttons, a panel under the hamburger where it has not. */} {/* A menu of the bar's own (theme, language, account) draws its popover from inside this box, so the box is held open while one is: the popover would be a child of a box that is not on the screen. */}
setBar(false)} > {/* First of the controls, on purpose: sending the frame to Immich Cloud is what the phone's header puts at the head of its row, and it is the one control here that hands the photo to another box. Drawn only once the account has a server to send to. The pinwheel is the label — the word beside it is a desktop convenience the phone drops (see app.css). */} {hasImmich && onUploadImmich ? ( ) : null} {/* Both arrows are one icon wide, so the pair reads as one control: the counter-clockwise arrow steps back, the clockwise one forward. They sit with the name they would take back — after the mark and the preset. */} {/* The catalogue's own reading runs in this tab whatever screen is up, so the header says so while it does. The shelf itself is the accounts' (Library.tsx turns a guest away), which is why the link waits for one. */} {live ? ( ) : null} {user ? ( {t('nav.library')} ) : ( /* Not a link. An anchor wearing `aria-disabled` still navigates on a click, and the screen behind it would only send the guest home again — so a guest gets the shape of the button and nothing to press. */ {t('nav.library')} )}
{menu === 'theme' ? (
{/* The name is a span, not a bare text node: a chip is a flex row, and a loose text node in one is an anonymous item — the shape iOS stops painting until the page is touched (same as the panel heads and ChipRow's chips). */}
{ACCENTS.map((entry) => ( ))}
{/* A dropdown keeps the group compact — the labels are long and the current choice is the only one that needs to be visible. */}
) : null}
{menu === 'lang' ? (
{LANGS.map((entry) => ( ))}
) : null}
{menu === 'account' ? (
{user ? ( <>

{t('auth.loggedInAs', { email: user.email })}

{/* The account control opens the one page that account can act on: moderation for an admin, its own profile for a member. */} {user.admin ? ( {t('nav.admin')} ) : ( <> {t('nav.photos')} {t('nav.profile')} )} ) : ( <> )}
) : null}
{/* Last in the row on purpose: EXPORT is the end of the job, so it sits at the end of the bar — the one button the eye lands on when the edit is done. */}
{/* The corner the panel hangs from, and only on a phone: a wide screen draws the row itself instead (see .bar-toggle). */}
); }