Files
TREK/client/src/native/imageFade.ts
T
Maurice 80c04df4fc feat(native): swipe rows, long-press menus, placeholders while loading, fading pictures, iOS bounce and app icon quick actions
Packing items, tasks and expenses swipe sideways to their actions, as in
Mail or Reminders: done or open one way, delete and edit the other, the
first one run by a full swipe. They are second ways to what each row
already offers; an expense still asks before it goes.

A long press on a trip card lifts it and opens its menu with the card's own
actions. Plain confirmations in MConfirmSheet now also ask through the
system where the app can.

Lists, the atlas and a journey show grey placeholders in their shape while
loading instead of a spinner in an empty screen (MSkeleton); buttons keep
theirs. A picture still loading fades in once it is there; one from the
cache shows at once. In the iOS app the page bounces at its ends again,
except on screens with pull-to-refresh, whose pull it would race; Android
keeps its stretch off.

On iOS a long press on the app icon offers "New trip" and the trip coming
up by its name, set by the UI in its language (TrekShell.setQuickActions,
feature quickActions), and a tap opens it, also from a cold start.

The app's own CSS moves out of index.css into native/nativeApp.css, which
had outgrown its size budget.
2026-11-09 09:37:36 +01:00

40 lines
1.6 KiB
TypeScript

/**
* Pictures that fade in once they are there, instead of building up line by
* line or popping in late, the way a native app shows a cover or a photo.
*
* Only an image that is still loading when it reaches the page fades: it waits
* at opacity 0 (data-fading), and as it loads or fails it is shown with a short
* fade (data-faded, index.css). One that is already in the cache shows at once,
* so nothing on a screen opened a second time flickers. A failed image is shown
* too, so a picture never stays invisible. Map tiles fade by themselves and are
* left alone, as is anything inside [data-no-fade]. Only the app calls this.
*/
const SKIP = '.leaflet-container, .maplibregl-map, [data-no-fade]';
function watch(img: HTMLImageElement): void {
if (img.complete || img.hasAttribute('data-fading') || img.closest(SKIP)) return;
img.setAttribute('data-fading', '');
const show = () => {
img.removeEventListener('load', show);
img.removeEventListener('error', show);
img.removeAttribute('data-fading');
img.setAttribute('data-faded', '');
};
img.addEventListener('load', show);
img.addEventListener('error', show);
}
function scan(node: Node): void {
if (node instanceof HTMLImageElement) watch(node);
else if (node instanceof Element) node.querySelectorAll('img').forEach(watch);
}
export function fadeInImages(root: Element = document.body): () => void {
const observer = new MutationObserver((records) => {
for (const record of records) record.addedNodes.forEach(scan);
});
observer.observe(root, { childList: true, subtree: true });
return () => observer.disconnect();
}