feat(web/radial): global menu pagination

adds a More... button when menu items are over PAGE_ITEMS
This commit is contained in:
Luke
2023-02-23 17:29:38 +01:00
parent c2f5d38108
commit 7ffa12af4f
24 changed files with 96 additions and 37 deletions

View File

@@ -3,6 +3,7 @@
"ui": { "ui": {
"cancel": "Cancel", "cancel": "Cancel",
"close": "Mbylle", "close": "Mbylle",
"confirm": "Konfirmo" "confirm": "Konfirmo",
"more": "More..."
} }
} }

View File

@@ -3,6 +3,7 @@
"ui": { "ui": {
"cancel": "Zrušit", "cancel": "Zrušit",
"close": "Zavřít", "close": "Zavřít",
"confirm": "Potvrdit" "confirm": "Potvrdit",
"more": "More..."
} }
} }

View File

@@ -3,6 +3,7 @@
"ui": { "ui": {
"cancel": "Abbrechen", "cancel": "Abbrechen",
"close": "Schließen", "close": "Schließen",
"confirm": "Bestätigen" "confirm": "Bestätigen",
"more": "More..."
} }
} }

View File

@@ -3,6 +3,7 @@
"ui": { "ui": {
"cancel": "Cancel", "cancel": "Cancel",
"close": "Close", "close": "Close",
"confirm": "Confirm" "confirm": "Confirm",
"more": "More..."
} }
} }

View File

@@ -3,6 +3,7 @@
"ui": { "ui": {
"cancel": "Cancelar", "cancel": "Cancelar",
"close": "Cerrar", "close": "Cerrar",
"confirm": "Confirmar" "confirm": "Confirmar",
"more": "More..."
} }
} }

View File

@@ -3,6 +3,7 @@
"ui": { "ui": {
"cancel": "Tühista", "cancel": "Tühista",
"close": "Sulge", "close": "Sulge",
"confirm": "Kinnita" "confirm": "Kinnita",
"more": "More..."
} }
} }

View File

@@ -3,6 +3,7 @@
"ui": { "ui": {
"cancel": "Annuler", "cancel": "Annuler",
"close": "Fermer", "close": "Fermer",
"confirm": "Confirmer" "confirm": "Confirmer",
"more": "More..."
} }
} }

View File

@@ -3,6 +3,7 @@
"ui": { "ui": {
"cancel": "ביטול", "cancel": "ביטול",
"close": "סגירה", "close": "סגירה",
"confirm": "אישור" "confirm": "אישור",
"more": "More..."
} }
} }

View File

@@ -3,6 +3,7 @@
"ui": { "ui": {
"cancel": "Odustani", "cancel": "Odustani",
"close": "Zatvori", "close": "Zatvori",
"confirm": "Potvrdi" "confirm": "Potvrdi",
"more": "Više..."
} }
} }

View File

@@ -3,6 +3,7 @@
"ui": { "ui": {
"cancel": "Mégse", "cancel": "Mégse",
"close": "Bezárás", "close": "Bezárás",
"confirm": "Megerősít" "confirm": "Megerősít",
"more": "More..."
} }
} }

View File

@@ -3,6 +3,7 @@
"ui": { "ui": {
"cancel": "Batal", "cancel": "Batal",
"close": "Tutup", "close": "Tutup",
"confirm": "Konfirmasi" "confirm": "Konfirmasi",
"more": "More..."
} }
} }

View File

@@ -3,6 +3,7 @@
"ui": { "ui": {
"cancel": "Cancel", "cancel": "Cancel",
"close": "Chiudere", "close": "Chiudere",
"confirm": "Confermare" "confirm": "Confermare",
"more": "More..."
} }
} }

View File

@@ -3,6 +3,7 @@
"ui": { "ui": {
"cancel": "Atšaukti", "cancel": "Atšaukti",
"close": "Uždaryti", "close": "Uždaryti",
"confirm": "Patvirtinti" "confirm": "Patvirtinti",
"more": "More..."
} }
} }

View File

@@ -3,6 +3,7 @@
"ui": { "ui": {
"cancel": "Annuleren", "cancel": "Annuleren",
"close": "Sluiten", "close": "Sluiten",
"confirm": "Bevestig" "confirm": "Bevestig",
"more": "More..."
} }
} }

View File

@@ -3,6 +3,7 @@
"ui": { "ui": {
"cancel": "Anuluj", "cancel": "Anuluj",
"close": "Zamknij", "close": "Zamknij",
"confirm": "Potwierdź" "confirm": "Potwierdź",
"more": "More..."
} }
} }

View File

@@ -3,6 +3,7 @@
"ui": { "ui": {
"cancel": "Отменить", "cancel": "Отменить",
"close": "Закрывать", "close": "Закрывать",
"confirm": "Подтверждать" "confirm": "Подтверждать",
"more": "More..."
} }
} }

View File

@@ -3,6 +3,7 @@
"ui": { "ui": {
"cancel": "Zrušiť", "cancel": "Zrušiť",
"close": "Zavrieť", "close": "Zavrieť",
"confirm": "Potvrdiť" "confirm": "Potvrdiť",
"more": "More..."
} }
} }

View File

@@ -3,6 +3,7 @@
"ui": { "ui": {
"cancel": "Cancel", "cancel": "Cancel",
"close": "Zapri", "close": "Zapri",
"confirm": "Potrdi" "confirm": "Potrdi",
"more": "More..."
} }
} }

View File

@@ -3,6 +3,7 @@
"ui": { "ui": {
"cancel": "Avbryt", "cancel": "Avbryt",
"close": "Stäng", "close": "Stäng",
"confirm": "Acceptera" "confirm": "Acceptera",
"more": "More..."
} }
} }

View File

@@ -3,6 +3,7 @@
"ui": { "ui": {
"cancel": "取消", "cancel": "取消",
"close": "关闭", "close": "关闭",
"confirm": "确认" "confirm": "确认",
"more": "More..."
} }
} }

View File

@@ -3,6 +3,7 @@
"ui": { "ui": {
"cancel": "取消", "cancel": "取消",
"close": "關閉", "close": "關閉",
"confirm": "確認" "confirm": "確認",
"more": "More..."
} }
} }

View File

@@ -1,10 +1,11 @@
import { Box, createStyles } from '@mantine/core'; import { Box, createStyles } from '@mantine/core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { useState } from 'react'; import { useEffect, useState } from 'react';
import { useNuiEvent } from '../../../hooks/useNuiEvent'; import { useNuiEvent } from '../../../hooks/useNuiEvent';
import { fetchNui } from '../../../utils/fetchNui'; import { fetchNui } from '../../../utils/fetchNui';
import ScaleFade from '../../../transitions/ScaleFade'; import ScaleFade from '../../../transitions/ScaleFade';
import type { RadialMenuItem } from '../../../typings'; import type { RadialMenuItem } from '../../../typings';
import { useLocales } from '../../../providers/LocaleProvider';
const useStyles = createStyles((theme) => ({ const useStyles = createStyles((theme) => ({
wrapper: { wrapper: {
@@ -51,23 +52,41 @@ const useStyles = createStyles((theme) => ({
}, },
})); }));
// PAGE_ITEMS + 1 = More... button
const PAGE_ITEMS = 7;
const degToRad = (deg: number) => deg * (Math.PI / 180); const degToRad = (deg: number) => deg * (Math.PI / 180);
const RadialMenu: React.FC = () => { const RadialMenu: React.FC = () => {
const { classes } = useStyles(); const { classes } = useStyles();
const { locale } = useLocales();
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const [menu, setMenu] = useState<{ items: RadialMenuItem[]; sub?: boolean }>({ const [menuItems, setMenuItems] = useState<RadialMenuItem[]>([]);
const [menu, setMenu] = useState<{ items: RadialMenuItem[]; sub?: boolean; page: number }>({
items: [], items: [],
sub: false, sub: false,
page: 1,
}); });
const changePage = async (increment?: boolean) => {
setVisible(false);
// May cause issues if user toggles off the menu while in transition?
await new Promise((resolve) => setTimeout(resolve, 100));
setVisible(true);
setMenu({ ...menu, page: increment ? menu.page + 1 : menu.page - 1 });
};
useEffect(() => {
if (menu.items.length < PAGE_ITEMS) return setMenuItems(menu.items);
const items = menu.items.slice(PAGE_ITEMS * (menu.page - 1), PAGE_ITEMS * menu.page);
PAGE_ITEMS * menu.page < menu.items.length &&
items.push({ icon: 'ellipsis-h', label: locale.ui.more, isMore: true });
setMenuItems(items);
}, [menu.items, menu.page]);
useNuiEvent('openRadialMenu', async (data: { items: RadialMenuItem[]; sub?: boolean } | false) => { useNuiEvent('openRadialMenu', async (data: { items: RadialMenuItem[]; sub?: boolean } | false) => {
if (!data) return setVisible(false); if (!data) return setVisible(false);
// if (visible) { setMenu({ ...data, page: 1 });
// setVisible(false);
// await new Promise((resolve) => setTimeout(resolve, 100));
// }
setMenu(data);
setVisible(true); setVisible(true);
}); });
@@ -77,16 +96,22 @@ const RadialMenu: React.FC = () => {
return ( return (
<> <>
<Box className={classes.wrapper} onContextMenu={() => menu.sub && fetchNui('radialBack')}> <Box
className={classes.wrapper}
onContextMenu={async () => {
if (menu.page > 1) await changePage();
else if (menu.sub) fetchNui('radialBack');
}}
>
<ScaleFade visible={visible}> <ScaleFade visible={visible}>
<svg width="350px" height="350px" transform="rotate(90)"> <svg width="350px" height="350px" transform="rotate(90)">
{/*Fixed issues with background circle extending the circle when there's less than 3 items*/} {/*Fixed issues with background circle extending the circle when there's less than 3 items*/}
<g transform="translate(175, 175)"> <g transform="translate(175, 175)">
<circle r={175} className={classes.backgroundCircle} /> <circle r={175} className={classes.backgroundCircle} />
</g> </g>
{menu.items.map((item, index) => { {menuItems.map((item, index) => {
// Always draw full circle to avoid elipse circles with 2 or less items // Always draw full circle to avoid elipse circles with 2 or less items
const pieAngle = 360 / (menu.items.length < 3 ? 3 : menu.items.length); const pieAngle = 360 / (menuItems.length < 3 ? 3 : menuItems.length);
const angle = degToRad(pieAngle / 2 + 90); const angle = degToRad(pieAngle / 2 + 90);
const gap = 0; const gap = 0;
const radius = 175 * 0.65 - gap; const radius = 175 * 0.65 - gap;
@@ -100,7 +125,13 @@ const RadialMenu: React.FC = () => {
<g <g
transform={`rotate(-${index * pieAngle} 175 175) translate(${sinAngle * gap}, ${cosAngle * gap})`} transform={`rotate(-${index * pieAngle} 175 175) translate(${sinAngle * gap}, ${cosAngle * gap})`}
className={classes.sector} className={classes.sector}
onClick={() => fetchNui('radialClick', index)} onClick={async () => {
const clickIndex = menu.page === 1 ? index : PAGE_ITEMS * (menu.page - 1) + index;
if (!item.isMore) fetchNui('radialClick', clickIndex);
else {
await changePage(true);
}
}}
> >
<path <path
d={`M175.01,175.01 l${175 - gap},0 A175.01,175.01 0 0,0 ${ d={`M175.01,175.01 l${175 - gap},0 A175.01,175.01 0 0,0 ${
@@ -126,11 +157,14 @@ const RadialMenu: React.FC = () => {
})} })}
<g <g
transform={`translate(175, 175)`} transform={`translate(175, 175)`}
onClick={() => { onClick={async () => {
if (menu.sub) fetchNui('radialBack'); if (menu.page > 1) await changePage();
else { else {
setVisible(false); if (menu.sub) fetchNui('radialBack');
fetchNui('radialClose'); else {
setVisible(false);
fetchNui('radialClose');
}
} }
}} }}
> >
@@ -139,7 +173,7 @@ const RadialMenu: React.FC = () => {
</svg> </svg>
<div className={classes.centerIconContainer}> <div className={classes.centerIconContainer}>
<FontAwesomeIcon <FontAwesomeIcon
icon={!menu.sub ? 'xmark' : 'arrow-rotate-left'} icon={!menu.sub && menu.page < 2 ? 'xmark' : 'arrow-rotate-left'}
fixedWidth fixedWidth
className={classes.centerIcon} className={classes.centerIcon}
color="#fff" color="#fff"

View File

@@ -18,6 +18,7 @@ debugData([
cancel: 'Cancel', cancel: 'Cancel',
close: 'Close', close: 'Close',
confirm: 'Confirm', confirm: 'Confirm',
more: 'More...',
}, },
}, },
}, },
@@ -29,6 +30,7 @@ interface Locale {
cancel: string; cancel: string;
close: string; close: string;
confirm: string; confirm: string;
more: string;
}; };
} }
@@ -46,6 +48,7 @@ const LocaleProvider: React.FC<{ children: React.ReactNode }> = ({ children }) =
cancel: '', cancel: '',
close: '', close: '',
confirm: '', confirm: '',
more: '',
}, },
}); });

View File

@@ -3,4 +3,5 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core';
export interface RadialMenuItem { export interface RadialMenuItem {
icon: IconProp; icon: IconProp;
label: string; label: string;
isMore?: boolean;
} }