refactor(app-shell): I.1 — split AppShell.tsx 691 → 248 LOC across 12 files (#671)

* refactor(app-shell): extract appShellHelpers.ts

Move SIDEBAR_COLLAPSED_STORAGE_KEY + read/persist helpers and the
shouldSuppressQueueResizerMouseDown geometry helper out of AppShell.tsx
into utils/appShellHelpers.ts.

AppShell.tsx: 691 → 639 LOC.

* refactor(app-shell): extract usePlatformShellSetup hook

Bundle the one-shot platform/window-shell effects (tiling-WM detection,
no-compositing class, data-platform attr, custom titlebar sync, kinetic
scroll toggle, logging mode push) into hooks/usePlatformShellSetup.ts.
Returns isTilingWm so AppShell can still gate the custom titlebar.

AppShell.tsx: 639 → 604 LOC.

* refactor(app-shell): extract 5 lifecycle hooks

Pull these effect islands into hooks/:
- useOrbitBodyAttrs       — orbit role/phase → <html data-orbit-*> attrs
- useWindowFullscreenState — Tauri isFullscreen() tracker
- useNowPlayingTrayTitle  — title + tray tooltip sync
- useTrayMenuI18n         — tray menu labels via i18n
- useServerCapabilitiesProbe — music folders + rating support + orbit
                              orphan sweep on login

AppShell.tsx: 604 → 497 LOC.

* refactor(app-shell): extract useQueueResizer hook

Bundle queueWidth state, drag listeners, sidebar-aligned handle position,
and the click-vs-drag mousedown handler into hooks/useQueueResizer.ts.
AppShell drops shouldSuppressQueueResizerMouseDown wiring and 4 local
state pieces.

AppShell.tsx: 497 → 403 LOC.

* refactor(app-shell): extract 4 misc lifecycle hooks

- useGlobalDndAndSelectionBlockers — document-level DnD/select-all/
  selectstart blockers (Linux/WebKitGTK + Wayland workarounds).
- useAppActivityTracking — <html data-app-hidden> + <html data-app-blurred>
  so CSS can pause cosmetic animation when the app isn't being looked at.
- useMainScrollingIndicator — scroll-idle tracker for main + np viewports.
- useOfflineAutoNav — connStatus transitions push to /offline or back.

AppShell.tsx: 403 → 282 LOC.

* refactor(app-shell): extract AppShellQueueResizerSeam subcomponent

The 6px resizer strip and the round resize/toggle handle (~50 LOC of JSX
with embedded scrollbar-collision suppression + self-heal logic) move
into components/AppShellQueueResizerSeam.tsx. Desktop-only.

AppShell.tsx: 282 → 248 LOC.
This commit is contained in:
Frank Stellmacher
2026-05-13 23:48:03 +02:00
committed by GitHub
parent 988806e6b1
commit b591a1cb5f
14 changed files with 738 additions and 488 deletions
+51
View File
@@ -0,0 +1,51 @@
import { useEffect, useState } from 'react';
import { APP_MAIN_SCROLL_VIEWPORT_ID } from '../constants/appScroll';
const SCROLL_IDLE_MS = 180;
/**
* `true` while the main route viewport or the Now Playing viewport is
* actively scrolling, falling back to `false` after `SCROLL_IDLE_MS` of
* silence. Used to fade out the queue handle (and similar floating
* controls) while the user is scrolling, so they don't sit on top of the
* overlay scrollbar thumb.
*
* Re-binds on `pathname` change because Now Playing's viewport mounts
* lazily and isn't in the DOM on every route.
*/
export function useMainScrollingIndicator(pathname: string): boolean {
const [isMainScrolling, setIsMainScrolling] = useState(false);
useEffect(() => {
const viewports = new Set<HTMLElement>();
const appViewport = document.getElementById(APP_MAIN_SCROLL_VIEWPORT_ID);
if (appViewport) viewports.add(appViewport);
const nowPlayingViewport = document.querySelector<HTMLElement>('.np-main__viewport');
if (nowPlayingViewport) viewports.add(nowPlayingViewport);
if (viewports.size === 0) return;
let scrollHideTimer: number | null = null;
const onScroll = () => {
setIsMainScrolling(true);
if (scrollHideTimer != null) window.clearTimeout(scrollHideTimer);
scrollHideTimer = window.setTimeout(() => {
setIsMainScrolling(false);
scrollHideTimer = null;
}, SCROLL_IDLE_MS);
};
viewports.forEach(viewport => {
viewport.addEventListener('scroll', onScroll, { passive: true });
});
return () => {
viewports.forEach(viewport => {
viewport.removeEventListener('scroll', onScroll);
});
if (scrollHideTimer != null) window.clearTimeout(scrollHideTimer);
setIsMainScrolling(false);
};
}, [pathname]);
return isMainScrolling;
}