Add button to PassagesDisplay to disable passages fetching

This commit is contained in:
2023-02-11 18:10:21 +01:00
parent 7294f35622
commit 3913209b28
3 changed files with 288 additions and 144 deletions

View File

@@ -0,0 +1,19 @@
import { createIcon } from "@hope-ui/solid";
// From https://github.com/hope-ui/hope-ui/blob/main/apps/docs/src/icons/IconHamburgerMenu.tsx
export const IconHamburgerMenu = createIcon({
viewBox: "0 0 15 15",
path: () => (
<path
d="M1.5 3C1.22386 3 1 3.22386 1 3.5C1 3.77614 1.22386 4 1.5 4H13.5C13.7761 4 14 3.77614 14 3.5C14 3.22386
13.7761 3 13.5 3H1.5ZM1 7.5C1 7.22386 1.22386 7 1.5 7H13.5C13.7761 7 14 7.22386 14 7.5C14 7.77614 13.7761 8 13.5
8H1.5C1.22386 8 1 7.77614 1 7.5ZM1 11.5C1 11.2239 1.22386 11 1.5 11H13.5C13.7761 11 14 11.2239 14 11.5C14 11.7761
13.7761 12 13.5 12H1.5C1.22386 12 1 11.7761 1 11.5Z"
fill="currentColor"
fill-rule="evenodd"
clip-rule="evenodd"
/>
),
});

View File

@@ -39,6 +39,21 @@
margin-right: auto; margin-right: auto;
} }
.header .menu {
height: calc(80/100*100%);
aspect-ratio: 1;
margin-right: calc(30/1920*100%);
}
.header .menu button {
height: 100%;
aspect-ratio: 1;
background-color: var(--idfm-black);
border: solid var(--idfm-white) 3px;
border-radius: calc(9/86*100%);
}
.header .clock { .header .clock {
width: calc(175/1920*100%); width: calc(175/1920*100%);
height: calc(80/100*100%); height: calc(80/100*100%);

View File

@@ -1,6 +1,7 @@
import { createEffect, createResource, createSignal, For, JSX, ParentComponent, Show, useContext, VoidComponent } from "solid-js"; import { createContext, createEffect, createResource, createSignal, For, JSX, ParentComponent, Show, useContext, VoidComponent, VoidProps } from "solid-js";
import { createStore } from "solid-js/store"; import { createStore } from "solid-js/store";
import { createDateNow } from "@solid-primitives/date"; import { createDateNow } from "@solid-primitives/date";
import { IconButton, Menu, MenuTrigger, MenuContent, MenuItem } from "@hope-ui/solid";
import { format } from "date-fns"; import { format } from "date-fns";
import { BusinessDataContext, BusinessDataStore } from "./businessData"; import { BusinessDataContext, BusinessDataStore } from "./businessData";
@@ -9,45 +10,224 @@ import { SearchContext, SearchStore } from "./search";
import { Passage, Passages } from "./types"; import { Passage, Passages } from "./types";
import { getTransportModeSrc } from "./utils"; import { getTransportModeSrc } from "./utils";
import { PassagesPanel } from "./passagesPanel"; import { PassagesPanel } from "./passagesPanel";
import { IconHamburgerMenu } from './extra/iconHamburgerMenu';
import styles from "./passagesDisplay.module.css"; import styles from "./passagesDisplay.module.css";
export const PassagesDisplay: ParentComponent = () => { type PositionnedPanel = {
position: number;
// TODO: Should be PassagesPanelComponent ?
panel: JSX.Element;
};
const maxPassagePerPanel = 5;
const syncPeriodMsec = 20 * 1000; interface PassagesDisplayStore {
const panelSwitchPeriodMsec = 4 * 1000; isPassagesRefreshEnabled: () => boolean;
enablePassagesRefresh: () => void;
disablePassagesRefresh: () => void;
togglePassagesRefresh: () => void;
getPanels: () => Array<PositionnedPanel>;
setPanels: (panels: Array<PositionnedPanel>) => void;
getDisplayedPanelId: () => number;
setDisplayedPanelId: (panelId: number) => void;
};
const PassagesDisplayContext = createContext<PassagesDisplayStore>();
function PassagesDisplayProvider(props: { children: JSX.Element }) {
type Store = {
refreshEnabled: boolean;
panels: Array<PositionnedPanel>;
displayedPanelId: number;
};
const [store, setStore] = createStore<Store>({ refreshEnabled: true, panels: [], displayedPanelId: 0 });
const isPassagesRefreshEnabled = (): boolean => {
return store.refreshEnabled;
}
const enablePassagesRefresh = (): void => {
setStore('refreshEnabled', true);
}
const disablePassagesRefresh = (): void => {
setStore('refreshEnabled', false);
}
const togglePassagesRefresh = (): void => {
setStore('refreshEnabled', !store.refreshEnabled);
}
const getPanels = (): Array<PositionnedPanel> => {
return store.panels;
}
const setPanels = (panels: Array<PositionnedPanel>): void => {
setStore('panels', panels);
}
const getDisplayedPanelId = (): number => {
return store.displayedPanelId;
}
const setDisplayedPanelId = (panelId: number): void => {
setStore('displayedPanelId', panelId);
}
return (
<PassagesDisplayContext.Provider value={{
isPassagesRefreshEnabled, enablePassagesRefresh,
disablePassagesRefresh, togglePassagesRefresh,
getPanels, setPanels,
getDisplayedPanelId, setDisplayedPanelId
}}>
{props.children}
</PassagesDisplayContext.Provider>
);
}
// TODO: Sort transport modes by weight
const Header: VoidComponent<{ title: string }> = (props) => {
const businessDataStore: BusinessDataStore | undefined = useContext(BusinessDataContext); const businessDataStore: BusinessDataStore | undefined = useContext(BusinessDataContext);
// TODO: Use props instead const passagesDisplayStore: PassagesDisplayStore | undefined = useContext(PassagesDisplayContext);
const searchStore: SearchStore | undefined = useContext(SearchContext);
if (businessDataStore === undefined || searchStore === undefined) if (businessDataStore === undefined || passagesDisplayStore === undefined)
return <div />; return <div />;
const { passages, getLine, getLinePassages, refreshPassages, clearPassages } = businessDataStore; const { getLine, passages } = businessDataStore;
const { getDisplayedStops } = searchStore; const { isPassagesRefreshEnabled, togglePassagesRefresh } = passagesDisplayStore;
const [displayedPanelId, setDisplayedPanelId] = createSignal<number>(0);
type PositionnedPanel = {
position: number;
// TODO: Should be PassagesPanelComponent ?
panel: JSX.Element;
};
const [panels, setPanels] = createStore<PositionnedPanel[]>([]);
const [dateNow] = createDateNow(1000); const [dateNow] = createDateNow(1000);
const computeTransportModes = async (lineIds: string[]): Promise<string[]> => {
const lines = await Promise.all(lineIds.map((lineId) => getLine(lineId)));
const urls: Set<string> = new Set();
for (const line of lines) {
const src = getTransportModeSrc(line.transportMode, false);
if (src !== undefined) {
urls.add(src);
}
}
return Array.from(urls);
}
const [linesIds, setLinesIds] = createSignal<string[]>([]);
const [transportModeUrls] = createResource<string[], string[]>(linesIds, computeTransportModes);
createEffect(() => {
setLinesIds(Object.keys(passages()));
});
return (
<div class={styles.header}>
<Show when={transportModeUrls() !== undefined} >
<For each={transportModeUrls()}>
{(url) =>
<div class={styles.transportMode}>
<img src={url} />
</div>
}
</For>
</Show>
<div class={styles.title}>
<svg viewBox="0 0 1260 50">
<text x="0" y="50%" dominant-baseline="middle" font-size="50" style="fill: #ffffff">
{props.title}
</text>
</svg>
</div>
<div class={styles.menu}>
<Menu>
<MenuTrigger
as={IconButton}
icon=<IconHamburgerMenu />
/>
<MenuContent>
<MenuItem onSelect={() => togglePassagesRefresh()}>{isPassagesRefreshEnabled() ? "Disable" : "Enable"}</MenuItem>
</MenuContent>
</Menu>
</div>
<div class={styles.clock}>
<svg viewBox="0 0 115 43">
<text x="50%" y="55%" dominant-baseline="middle" text-anchor="middle" font-size="43" style="fill: #ffffff">
{format(dateNow(), "HH:mm")}
</text>
</svg>
</div>
</div >
);
};
const Footer: VoidComponent<{}> = () => {
const passagesDisplayStore: PassagesDisplayStore | undefined = useContext(PassagesDisplayContext);
if (passagesDisplayStore === undefined)
return <div />;
const { getDisplayedPanelId, getPanels } = passagesDisplayStore;
return (
<div class={styles.footer}>
<For each={getPanels()}>
{(panel) => {
const position = panel.position;
return (
<div>
<svg viewBox="0 0 29 29">
<circle cx="50%" cy="50%" r="13" stroke="#ffffff" stroke-width="3"
style={{ fill: `var(--idfm-${position == getDisplayedPanelId() ? "white" : "black"})` }}
/>
</svg>
</div>
);
}}
</For>
</div>
);
}
const Body: ParentComponent<{ maxPassagesPerPanel: number, syncPeriodMsec: number, panelSwitchPeriodMsec: number }> = (props) => {
const businessDataStore: BusinessDataStore | undefined = useContext(BusinessDataContext);
const passagesDisplayStore: PassagesDisplayStore | undefined = useContext(PassagesDisplayContext);
// TODO: Use props instead
const searchStore: SearchStore | undefined = useContext(SearchContext);
if (businessDataStore === undefined || passagesDisplayStore === undefined || searchStore === undefined) {
return <div />;
}
const { getLinePassages, passages, clearPassages, refreshPassages } = businessDataStore;
const { isPassagesRefreshEnabled, getDisplayedPanelId, setDisplayedPanelId, getPanels, setPanels } = passagesDisplayStore;
const { getDisplayedStops } = searchStore;
setInterval(() => { setInterval(() => {
let nextPanelId = displayedPanelId() + 1; let nextPanelId = getDisplayedPanelId() + 1;
if (nextPanelId >= panels.length) { if (nextPanelId >= getPanels().length) {
nextPanelId = 0; nextPanelId = 0;
} }
setDisplayedPanelId(nextPanelId); setDisplayedPanelId(nextPanelId);
}, panelSwitchPeriodMsec); }, props.panelSwitchPeriodMsec);
setInterval(
async () => {
if (isPassagesRefreshEnabled()) {
const stops = getDisplayedStops();
if (stops.length > 0) {
refreshPassages(stops[0].id);
}
}
else {
console.log("Passages refresh disabled... skip it.");
}
},
props.syncPeriodMsec
);
createEffect(() => { createEffect(() => {
console.log("######### onStopIdUpdate #########"); console.log("######### onStopIdUpdate #########");
@@ -64,137 +244,67 @@ export const PassagesDisplay: ParentComponent = () => {
} }
}); });
setInterval( return (
async () => { <div class={styles.panelsContainer}>
const stops = getDisplayedStops(); {() => {
if (stops.length > 0) { setPanels([]);
refreshPassages(stops[0].id);
}
},
syncPeriodMsec
);
// TODO: Sort transport modes by weight let newPanels = [];
const Header: VoidComponent<{ passages: Passages, title: string }> = (props) => { let positioneds: PositionnedPanel[] = [];
let index = 0;
const computeTransportModes = async (lineIds: string[]): Promise<string[]> => { let chunk: Record<string, Record<string, Passage[]>> = {};
const lines = await Promise.all(lineIds.map((lineId) => getLine(lineId))); let chunkSize = 0;
const urls: Set<string> = new Set();
for (const line of lines) { for (const lineId of Object.keys(passages())) {
const src = getTransportModeSrc(line.transportMode, false); const byLinePassages = getLinePassages(lineId);
if (src !== undefined) { const byLinePassagesKeys = Object.keys(byLinePassages);
urls.add(src);
if (byLinePassagesKeys.length <= props.maxPassagesPerPanel - chunkSize) {
chunk[lineId] = byLinePassages;
chunkSize += byLinePassagesKeys.length;
}
else {
const panelid = index++;
const panel = <PassagesPanel show={panelid == getDisplayedPanelId()} passages={chunk} />;
newPanels.push(panel);
positioneds.push({ position: panelid, panel: panel });
chunk = {};
chunk[lineId] = byLinePassages;
chunkSize = byLinePassagesKeys.length;
}
}
if (chunkSize) {
const panelId = index++;
const panel = <PassagesPanel show={panelId == getDisplayedPanelId()} passages={chunk} />;
newPanels.push(panel);
positioneds.push({ position: panelId, panel: panel });
} }
}
return Array.from(urls);
}
const [linesIds, setLinesIds] = createSignal<string[]>([]); setPanels(positioneds);
const [transportModeUrls] = createResource<string[], string[]>(linesIds, computeTransportModes);
createEffect(() => { return newPanels;
setLinesIds(Object.keys(props.passages)); }}
}); </div>
);
}
return ( export const PassagesDisplay: ParentComponent = () => {
<div class={styles.header}>
<Show when={transportModeUrls() !== undefined} >
<For each={transportModeUrls()}>
{(url) =>
<div class={styles.transportMode}>
<img src={url} />
</div>
}
</For>
</Show>
<div class={styles.title}>
<svg viewBox="0 0 1260 50">
<text x="0" y="50%" dominant-baseline="middle" font-size="50" style="fill: #ffffff">
{props.title}
</text>
</svg>
</div>
<div class={styles.clock}>
<svg viewBox="0 0 115 43">
<text x="50%" y="55%" dominant-baseline="middle" text-anchor="middle" font-size="43" style="fill: #ffffff">
{format(dateNow(), "HH:mm")}
</text>
</svg>
</div>
</div >
);
};
const Footer: VoidComponent<{ panels: PositionnedPanel[] }> = (props) => { const MAX_PASSAGES_PER_PANEL = 5;
return (
<div class={styles.footer}> // TODO: Use props.
<For each={props.panels}> const syncPeriodMsec = 20 * 1000;
{(panel) => { const panelSwitchPeriodMsec = 4 * 1000;
const position = panel.position;
return (
<div>
<svg viewBox="0 0 29 29">
<circle cx="50%" cy="50%" r="13" stroke="#ffffff" stroke-width="3"
style={{ fill: `var(--idfm-${position == displayedPanelId() ? "white" : "black"})` }}
/>
</svg>
</div>
);
}}
</For>
</div>
);
}
return ( return (
<div class={styles.passagesDisplay}> <div class={styles.passagesDisplay}>
<Header title="Prochains passages" passages={passages()} /> <PassagesDisplayProvider>
<div class={styles.panelsContainer}> <Header title="Prochains passages" />
{() => { <Body maxPassagesPerPanel={MAX_PASSAGES_PER_PANEL} syncPeriodMsec={syncPeriodMsec} panelSwitchPeriodMsec={panelSwitchPeriodMsec} />
setPanels([]); <Footer />
</PassagesDisplayProvider>
let newPanels = [];
let positioneds: PositionnedPanel[] = [];
let index = 0;
let chunk: Record<string, Record<string, Passage[]>> = {};
let chunkSize = 0;
console.log("passages=", passages());
for (const lineId of Object.keys(passages())) {
console.log("lineId=", lineId);
const byLinePassages = getLinePassages(lineId);
console.log("byLinePassages=", byLinePassages);
const byLinePassagesKeys = Object.keys(byLinePassages);
console.log("byLinePassagesKeys=", byLinePassagesKeys);
if (byLinePassagesKeys.length <= maxPassagePerPanel - chunkSize) {
chunk[lineId] = byLinePassages;
chunkSize += byLinePassagesKeys.length;
}
else {
const panelid = index++;
const panel = <PassagesPanel show={panelid == displayedPanelId()} passages={chunk} />;
newPanels.push(panel);
positioneds.push({ position: panelid, panel: panel });
chunk = {};
chunk[lineId] = byLinePassages;
chunkSize = byLinePassagesKeys.length;
}
}
if (chunkSize) {
const panelId = index++;
const panel = <PassagesPanel show={panelId == displayedPanelId()} passages={chunk} />;
newPanels.push(panel);
positioneds.push({ position: panelId, panel: panel });
}
setPanels(positioneds);
return newPanels;
}}
</div>
<Footer panels={panels} />
</div> </div>
); );
}; };