Finish sort menu

This commit is contained in:
Zoe Roux 2023-03-15 12:09:44 +09:00
parent a716fd1e22
commit 33db01cbbf
4 changed files with 111 additions and 210 deletions

View File

@ -27,10 +27,11 @@ import Close from "@material-symbols/svg-400/rounded/close-fill.svg";
import { Icon, IconButton } from "./icons"; import { Icon, IconButton } from "./icons";
import { PressableFeedback } from "./links"; import { PressableFeedback } from "./links";
import { P } from "./text"; import { P } from "./text";
import { ContrastArea } from "./themes"; import { ContrastArea, SwitchVariant } from "./themes";
import { ts } from "./utils"; import { ts } from "./utils";
import Check from "@material-symbols/svg-400/rounded/check-fill.svg"; import Check from "@material-symbols/svg-400/rounded/check-fill.svg";
import { useRouter } from "solito/router"; import { useRouter } from "solito/router";
import { SvgProps } from "react-native-svg";
const MenuContext = createContext<((open: boolean) => void) | undefined>(undefined); const MenuContext = createContext<((open: boolean) => void) | undefined>(undefined);
@ -60,48 +61,50 @@ const Menu = <AsProps,>({
{isOpen && ( {isOpen && (
<Portal> <Portal>
<ContrastArea mode="user"> <ContrastArea mode="user">
{({ css, theme }) => ( <SwitchVariant>
<MenuContext.Provider value={setOpen}> {({ css, theme }) => (
<Pressable <MenuContext.Provider value={setOpen}>
onPress={() => setOpen(false)} <Pressable
focusable={false}
{...css({ ...StyleSheet.absoluteFillObject, flexGrow: 1, bg: "transparent" })}
/>
<ScrollView
{...css([
{
bg: (theme) => theme.background,
position: "absolute",
bottom: 0,
width: percent(100),
alignSelf: "center",
borderTopLeftRadius: px(26),
borderTopRightRadius: { xs: px(26), xl: 0 },
paddingTop: { xs: px(26), xl: 0 },
marginTop: { xs: px(72), xl: 0 },
},
sm({
maxWidth: px(640),
marginHorizontal: px(56),
}),
xl({
top: 0,
right: 0,
marginRight: 0,
borderBottomLeftRadius: px(26),
}),
])}
>
<IconButton
icon={Close}
color={theme.colors.black}
onPress={() => setOpen(false)} onPress={() => setOpen(false)}
{...css({ alignSelf: "flex-end", display: { xs: "none", xl: "flex" } })} focusable={false}
{...css({ ...StyleSheet.absoluteFillObject, flexGrow: 1, bg: "transparent" })}
/> />
{children} <ScrollView
</ScrollView> {...css([
</MenuContext.Provider> {
)} bg: (theme) => theme.background,
position: "absolute",
bottom: 0,
width: percent(100),
alignSelf: "center",
borderTopLeftRadius: px(26),
borderTopRightRadius: { xs: px(26), xl: 0 },
paddingTop: { xs: px(26), xl: 0 },
marginTop: { xs: px(72), xl: 0 },
},
sm({
maxWidth: px(640),
marginHorizontal: px(56),
}),
xl({
top: 0,
right: 0,
marginRight: 0,
borderBottomLeftRadius: px(26),
}),
])}
>
<IconButton
icon={Close}
color={theme.colors.black}
onPress={() => setOpen(false)}
{...css({ alignSelf: "flex-end", display: { xs: "none", xl: "flex" } })}
/>
{children}
</ScrollView>
</MenuContext.Provider>
)}
</SwitchVariant>
</ContrastArea> </ContrastArea>
</Portal> </Portal>
)} )}
@ -114,10 +117,12 @@ const MenuItem = ({
selected, selected,
onSelect, onSelect,
href, href,
icon,
...props ...props
}: { }: {
label: string; label: string;
selected?: boolean; selected?: boolean;
icon?: ComponentType<SvgProps>;
} & ({ onSelect: () => void; href?: undefined } | { href: string; onSelect?: undefined })) => { } & ({ onSelect: () => void; href?: undefined } | { href: string; onSelect?: undefined })) => {
const { css, theme } = useYoshiki(); const { css, theme } = useYoshiki();
const setOpen = useContext(MenuContext); const setOpen = useContext(MenuContext);
@ -141,7 +146,7 @@ const MenuItem = ({
props as any, props as any,
)} )}
> >
{selected && <Icon icon={Check} color={theme.paragraph} size={24} />} {selected && <Icon icon={icon ?? Check} color={theme.paragraph} size={24} />}
<P {...css({ paddingLeft: ts(2) + +!selected * px(24) })}>{label}</P> <P {...css({ paddingLeft: ts(2) + +!selected * px(24) })}>{label}</P>
</PressableFeedback> </PressableFeedback>
); );

View File

@ -25,10 +25,11 @@ import { PressableProps } from "react-native";
import { useYoshiki } from "yoshiki/web"; import { useYoshiki } from "yoshiki/web";
import { px, useYoshiki as useNativeYoshiki } from "yoshiki/native"; import { px, useYoshiki as useNativeYoshiki } from "yoshiki/native";
import { P } from "./text"; import { P } from "./text";
import { ContrastArea } from "./themes"; import { ContrastArea, SwitchVariant } from "./themes";
import { Icon } from "./icons"; import { Icon } from "./icons";
import Dot from "@material-symbols/svg-400/rounded/fiber_manual_record-fill.svg"; import Dot from "@material-symbols/svg-400/rounded/fiber_manual_record-fill.svg";
import { focusReset } from "./utils"; import { focusReset, ts } from "./utils";
import { SvgProps } from "react-native-svg";
type YoshikiFunc<T> = (props: ReturnType<typeof useYoshiki>) => T; type YoshikiFunc<T> = (props: ReturnType<typeof useYoshiki>) => T;
const YoshikiProvider = ({ children }: { children: YoshikiFunc<ReactNode> }) => { const YoshikiProvider = ({ children }: { children: YoshikiFunc<ReactNode> }) => {
@ -68,26 +69,28 @@ const Menu = <AsProps extends { onPress: PressableProps["onPress"] }>({
<InternalTriger Component={Trigger} ComponentProps={props} /> <InternalTriger Component={Trigger} ComponentProps={props} />
</DropdownMenu.Trigger> </DropdownMenu.Trigger>
<ContrastArea mode="user"> <ContrastArea mode="user">
<YoshikiProvider> <SwitchVariant>
{({ css }) => ( <YoshikiProvider>
<DropdownMenu.Portal> {({ css }) => (
<DropdownMenu.Content <DropdownMenu.Portal>
onFocusOutside={(e) => e.stopImmediatePropagation()} <DropdownMenu.Content
{...css({ onFocusOutside={(e) => e.stopImmediatePropagation()}
bg: (theme) => theme.background, {...css({
overflow: "hidden", bg: (theme) => theme.background,
minWidth: "220px", overflow: "hidden",
borderRadius: "8px", minWidth: "220px",
boxShadow: borderRadius: "8px",
"0px 10px 38px -10px rgba(22, 23, 24, 0.35), 0px 10px 20px -15px rgba(22, 23, 24, 0.2)", boxShadow:
zIndex: 2, "0px 10px 38px -10px rgba(22, 23, 24, 0.35), 0px 10px 20px -15px rgba(22, 23, 24, 0.2)",
})} zIndex: 2,
> })}
{children} >
</DropdownMenu.Content> {children}
</DropdownMenu.Portal> </DropdownMenu.Content>
)} </DropdownMenu.Portal>
</YoshikiProvider> )}
</YoshikiProvider>
</SwitchVariant>
</ContrastArea> </ContrastArea>
</DropdownMenu.Root> </DropdownMenu.Root>
); );
@ -122,7 +125,7 @@ const MenuItem = ({
...props ...props
}: { }: {
label: string; label: string;
icon?: JSX.Element; icon?: ComponentType<SvgProps>;
selected?: boolean; selected?: boolean;
} & ({ onSelect: () => void; href?: undefined } | { href: string; onSelect?: undefined })) => { } & ({ onSelect: () => void; href?: undefined } | { href: string; onSelect?: undefined })) => {
const { css: nCss } = useNativeYoshiki(); const { css: nCss } = useNativeYoshiki();
@ -132,7 +135,7 @@ const MenuItem = ({
<> <>
<style jsx global>{` <style jsx global>{`
[data-highlighted] { [data-highlighted] {
background: ${theme.alternate.accent}; background: ${theme.variant.accent};
} }
`}</style> `}</style>
<Item <Item
@ -151,13 +154,13 @@ const MenuItem = ({
> >
{selected && ( {selected && (
<Icon <Icon
icon={Dot} icon={icon ?? Dot}
color={theme.paragraph} color={theme.paragraph}
size={px(8)} size={ts(icon ? 2 : 1)}
{...nCss({ paddingRight: px(8) })} {...nCss({ paddingRight: ts(1) })}
/> />
)} )}
{<P {...nCss(!selected && { paddingLeft: px(8 * 2) })}>{label}</P>} {<P {...nCss(!selected && { paddingLeft: ts(1 + (icon ? 2 : 1)) })}>{label}</P>}
</Item> </Item>
</> </>
); );

View File

@ -24,82 +24,12 @@ import { useYoshiki } from "yoshiki/native";
import GridView from "@material-symbols/svg-400/rounded/grid_view.svg"; import GridView from "@material-symbols/svg-400/rounded/grid_view.svg";
import ViewList from "@material-symbols/svg-400/rounded/view_list.svg"; import ViewList from "@material-symbols/svg-400/rounded/view_list.svg";
import Sort from "@material-symbols/svg-400/rounded/sort.svg"; import Sort from "@material-symbols/svg-400/rounded/sort.svg";
import ArrowUpward from "@material-symbols/svg-400/rounded/arrow_upward.svg";
import ArrowDownward from "@material-symbols/svg-400/rounded/arrow_downward.svg";
import { Layout, SortBy, SortOrd } from "./types"; import { Layout, SortBy, SortOrd } from "./types";
import { forwardRef } from "react"; import { forwardRef } from "react";
import { View, PressableProps } from "react-native"; import { View, PressableProps } from "react-native";
// const SortByMenu = ({
// sortKey,
// setSort,
// sortOrd,
// setSortOrd,
// anchor,
// onClose,
// }: {
// sortKey: SortBy;
// setSort: (sort: SortBy) => void;
// sortOrd: SortOrd;
// setSortOrd: (sort: SortOrd) => void;
// anchor: HTMLElement;
// onClose: () => void;
// }) => {
// const router = useRouter();
// const { t } = useTranslation("browse");
//
// return (
// <Menu
// id="sortby-menu"
// MenuListProps={{
// "aria-labelledby": "sortby",
// }}
// anchorEl={anchor}
// open={!!anchor}
// onClose={onClose}
// >
// {Object.values(SortBy).map((x) => (
// <MenuItem
// key={x}
// selected={sortKey === x}
// onClick={() => setSort(x)}
// component={Link}
// to={{ query: { ...router.query, sortBy: `${sortKey}-${sortOrd}` } }}
// shallow
// replace
// >
// <ListItemText>{t(`browse.sortkey.${x}`)}</ListItemText>
// </MenuItem>
// ))}
// <Divider />
// <MenuItem
// selected={sortOrd === SortOrd.Asc}
// onClick={() => setSortOrd(SortOrd.Asc)}
// component={Link}
// to={{ query: { ...router.query, sortBy: `${sortKey}-${sortOrd}` } }}
// shallow
// replace
// >
// <ListItemIcon>
// <South fontSize="small" />
// </ListItemIcon>
// <ListItemText>{t("browse.sortord.asc")}</ListItemText>
// </MenuItem>
// <MenuItem
// selected={sortOrd === SortOrd.Desc}
// onClick={() => setSortOrd(SortOrd.Desc)}
// component={Link}
// to={{ query: { ...router.query, sortBy: `${sortKey}-${sortOrd}` } }}
// shallow
// replace
// >
// <ListItemIcon>
// <North fontSize="small" />
// </ListItemIcon>
// <ListItemText>{t("browse.sortord.desc")}</ListItemText>
// </MenuItem>
// </Menu>
// );
// };
const SortTrigger = forwardRef<View, PressableProps & { sortKey: SortBy }>(function _SortTrigger( const SortTrigger = forwardRef<View, PressableProps & { sortKey: SortBy }>(function _SortTrigger(
{ sortKey, ...props }, { sortKey, ...props },
ref, ref,
@ -121,20 +51,17 @@ const SortTrigger = forwardRef<View, PressableProps & { sortKey: SortBy }>(funct
export const BrowseSettings = ({ export const BrowseSettings = ({
sortKey, sortKey,
setSort,
sortOrd, sortOrd,
setSortOrd, setSort,
layout, layout,
setLayout, setLayout,
}: { }: {
sortKey: SortBy; sortKey: SortBy;
setSort: (sort: SortBy) => void;
sortOrd: SortOrd; sortOrd: SortOrd;
setSortOrd: (sort: SortOrd) => void; setSort: (sort: SortBy, ord: SortOrd) => void;
layout: Layout; layout: Layout;
setLayout: (layout: Layout) => void; setLayout: (layout: Layout) => void;
}) => { }) => {
// const [sortAnchor, setSortAnchor] = useState<HTMLElement | null>(null);
const { css, theme } = useYoshiki(); const { css, theme } = useYoshiki();
const { t } = useTranslation(); const { t } = useTranslation();
@ -148,10 +75,10 @@ export const BrowseSettings = ({
key={x} key={x}
label={t(`browse.sortkey.${x}`)} label={t(`browse.sortkey.${x}`)}
selected={sortKey === x} selected={sortKey === x}
onSelect={() => setSort(x)} icon={sortOrd === SortOrd.Asc ? ArrowUpward : ArrowDownward}
// component={Link} onSelect={() =>
// to={{ query: { ...router.query, sortBy: `${sortKey}-${sortOrd}` } }} setSort(x, sortKey === x && sortOrd === SortOrd.Asc ? SortOrd.Desc : SortOrd.Asc)
// TODO: Set query param for sort. }
/> />
))} ))}
</Menu> </Menu>
@ -173,40 +100,4 @@ export const BrowseSettings = ({
</View> </View>
</View> </View>
); );
// return (
// <>
// <Box sx={{ display: "flex", justifyContent: "space-around" }}>
// <ButtonGroup sx={{ m: 1 }}>
// <Button disabled>
// <FilterList />
// </Button>
// <Tooltip title={t("browse.sortby-tt")}>
// <Button
// id="sortby"
// aria-label={t("browse.sortby-tt")}
// aria-controls={sortAnchor ? "sorby-menu" : undefined}
// aria-haspopup="true"
// aria-expanded={sortAnchor ? "true" : undefined}
// onClick={(event) => setSortAnchor(event.currentTarget)}
// >
// <Sort />
// {t("browse.sortby", { key: t(`browse.sortkey.${sortKey}`) })}
// {sortOrd === SortOrd.Asc ? <South fontSize="small" /> : <North fontSize="small" />}
// </Button>
// </Tooltip>
// </ButtonGroup>
// </Box>
// {sortAnchor && (
// <SortByMenu
// sortKey={sortKey}
// sortOrd={sortOrd}
// setSort={setSort}
// setSortOrd={setSortOrd}
// anchor={sortAnchor}
// onClose={() => setSortAnchor(null)}
// />
// )}
// </>
// );
}; };

View File

@ -18,7 +18,6 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { ComponentProps, useState } from "react";
import { import {
QueryIdentifier, QueryIdentifier,
QueryPage, QueryPage,
@ -27,6 +26,8 @@ import {
ItemType, ItemType,
getDisplayDate, getDisplayDate,
} from "@kyoo/models"; } from "@kyoo/models";
import { ComponentProps, useState } from "react";
import { createParam } from "solito";
import { DefaultLayout } from "../layout"; import { DefaultLayout } from "../layout";
import { WithLoading } from "../fetch"; import { WithLoading } from "../fetch";
import { InfiniteFetch } from "../fetch-infinite"; import { InfiniteFetch } from "../fetch-infinite";
@ -35,6 +36,8 @@ import { ItemList } from "./list";
import { SortBy, SortOrd, Layout } from "./types"; import { SortBy, SortOrd, Layout } from "./types";
import { BrowseSettings } from "./header"; import { BrowseSettings } from "./header";
const { useParam } = createParam<{ sortBy?: string }>();
export const itemMap = ( export const itemMap = (
item: WithLoading<LibraryItem>, item: WithLoading<LibraryItem>,
): WithLoading<ComponentProps<typeof ItemGrid> & ComponentProps<typeof ItemList>> => { ): WithLoading<ComponentProps<typeof ItemGrid> & ComponentProps<typeof ItemList>> => {
@ -72,33 +75,32 @@ const query = (
}); });
export const BrowsePage: QueryPage<{ slug?: string }> = ({ slug }) => { export const BrowsePage: QueryPage<{ slug?: string }> = ({ slug }) => {
const [sortKey, setSort] = useState(SortBy.Name); const [sort, setSort] = useParam("sortBy");
const [sortOrd, setSortOrd] = useState(SortOrd.Asc); const sortKey = (sort?.split(":")[0] as SortBy) || SortBy.Name;
const sortOrd = (sort?.split(":")[1] as SortOrd) || SortOrd.Asc;
const [layout, setLayout] = useState(Layout.Grid); const [layout, setLayout] = useState(Layout.Grid);
const LayoutComponent = layout === Layout.Grid ? ItemGrid : ItemList; const LayoutComponent = layout === Layout.Grid ? ItemGrid : ItemList;
// TODO list header to seet sort things, filter and layout.
return ( return (
<> <InfiniteFetch
<InfiniteFetch query={query(slug, sortKey, sortOrd)}
query={query(slug, sortKey, sortOrd)} placeholderCount={15}
placeholderCount={15} layout={LayoutComponent.layout}
layout={LayoutComponent.layout} Header={
Header={ <BrowseSettings
<BrowseSettings sortKey={sortKey}
sortKey={sortKey} sortOrd={sortOrd}
setSort={setSort} setSort={(key, ord) => {
sortOrd={sortOrd} setSort(`${key}:${ord}`);
setSortOrd={setSortOrd} }}
layout={layout} layout={layout}
setLayout={setLayout} setLayout={setLayout}
/> />
} }
> >
{(item) => <LayoutComponent {...itemMap(item)} />} {(item) => <LayoutComponent {...itemMap(item)} />}
</InfiniteFetch> </InfiniteFetch>
</>
); );
}; };