Create a usePopup to simplify popup creation

This commit is contained in:
Zoe Roux 2024-01-22 18:13:47 +01:00
parent d9f4a6ff8d
commit 53ac4a2050
4 changed files with 97 additions and 101 deletions

View File

@ -18,10 +18,10 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { ReactNode } from "react"; import { ReactElement, ReactNode, useCallback, useEffect, useState } from "react";
import { Container } from "./container"; import { Container } from "./container";
import { Portal } from "@gorhom/portal"; import { Portal, usePortal } from "@gorhom/portal";
import { SwitchVariant, YoshikiFunc } from "./themes"; import { ContrastArea, SwitchVariant, YoshikiFunc } from "./themes";
import { View } from "react-native"; import { View } from "react-native";
import { imageBorderRadius } from "./constants"; import { imageBorderRadius } from "./constants";
import { px } from "yoshiki/native"; import { px } from "yoshiki/native";
@ -29,7 +29,7 @@ import { ts } from "./utils";
export const Popup = ({ children, ...props }: { children: ReactNode | YoshikiFunc<ReactNode> }) => { export const Popup = ({ children, ...props }: { children: ReactNode | YoshikiFunc<ReactNode> }) => {
return ( return (
<Portal> <ContrastArea mode="user">
<SwitchVariant> <SwitchVariant>
{({ css, theme }) => ( {({ css, theme }) => (
<View <View
@ -60,6 +60,19 @@ export const Popup = ({ children, ...props }: { children: ReactNode | YoshikiFun
</View> </View>
)} )}
</SwitchVariant> </SwitchVariant>
</Portal> </ContrastArea>
); );
}; };
export const usePopup = () => {
const { addPortal, removePortal } = usePortal();
const [current, setPopup] = useState<ReactNode>();
const close = useCallback(() => setPopup(undefined), []);
useEffect(() => {
addPortal("popup", current);
return () => removePortal("popup");
}, [current, addPortal, removePortal]);
return [setPopup, close];
};

View File

@ -18,7 +18,7 @@
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>. * along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { IconButton, Menu, tooltip } from "@kyoo/primitives"; import { IconButton, Menu, tooltip, usePopup } from "@kyoo/primitives";
import { ComponentProps, ReactElement, useEffect, useState } from "react"; import { ComponentProps, ReactElement, useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import MoreVert from "@material-symbols/svg-400/rounded/more_vert.svg"; import MoreVert from "@material-symbols/svg-400/rounded/more_vert.svg";
@ -51,7 +51,7 @@ export const EpisodesContext = ({
const downloader = useDownloader(); const downloader = useDownloader();
const { css } = useYoshiki(); const { css } = useYoshiki();
const { t } = useTranslation(); const { t } = useTranslation();
const [popup, setPopup] = useState<ReactElement | undefined>(undefined); const [setPopup, close] = usePopup();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const mutation = useMutation({ const mutation = useMutation({
@ -109,19 +109,12 @@ export const EpisodesContext = ({
label={t("home.episodeMore.mediainfo")} label={t("home.episodeMore.mediainfo")}
icon={MovieInfo} icon={MovieInfo}
onSelect={() => onSelect={() =>
setPopup( setPopup(<MediaInfoPopup mediaType={type} mediaSlug={slug} close={close} />)
<MediaInfoPopup
mediaType={type}
mediaSlug={slug}
close={() => setPopup(undefined)}
/>,
)
} }
/> />
</> </>
)} )}
</Menu> </Menu>
{popup}
</> </>
); );
}; };

View File

@ -47,9 +47,9 @@ import {
ts, ts,
Chip, Chip,
DottedSeparator, DottedSeparator,
focusReset, usePopup,
} from "@kyoo/primitives"; } from "@kyoo/primitives";
import { Fragment, ReactElement, useState } from "react"; import { Fragment } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import MovieInfo from "@material-symbols/svg-400/rounded/movie_info.svg"; import MovieInfo from "@material-symbols/svg-400/rounded/movie_info.svg";
import { ImageStyle, Platform, View } from "react-native"; import { ImageStyle, Platform, View } from "react-native";
@ -112,7 +112,7 @@ export const TitleLine = ({
const { css, theme } = useYoshiki(); const { css, theme } = useYoshiki();
const { t } = useTranslation(); const { t } = useTranslation();
const downloader = useDownloader(); const downloader = useDownloader();
const [popup, setPopup] = useState<ReactElement | undefined>(undefined); const [setPopup, close] = usePopup();
return ( return (
<Container <Container
@ -246,30 +246,22 @@ export const TitleLine = ({
/> />
)} )}
{type === "movie" && slug && ( {type === "movie" && slug && (
<IconButton <>
icon={Download} <IconButton
onPress={() => downloader(type, slug)} icon={Download}
color={{ xs: theme.user.contrast, md: theme.colors.white }} onPress={() => downloader(type, slug)}
{...tooltip(t("home.episodeMore.download"))} color={{ xs: theme.user.contrast, md: theme.colors.white }}
/> {...tooltip(t("home.episodeMore.download"))}
/>
<IconButton
icon={MovieInfo}
color={{ xs: theme.user.contrast, md: theme.colors.white }}
onPress={() =>
setPopup(<MediaInfoPopup mediaType={"movie"} mediaSlug={slug!} close={close} />)
}
/>
</>
)} )}
{type === "movie" && (
<IconButton
icon={MovieInfo}
color={{ xs: theme.user.contrast, md: theme.colors.white }}
onPress={() =>
slug &&
setPopup(
<MediaInfoPopup
mediaType={type}
mediaSlug={slug}
close={() => setPopup(undefined)}
/>,
)
}
/>
)}
{popup}
{rating !== null && ( {rating !== null && (
<> <>
<DottedSeparator <DottedSeparator

View File

@ -45,6 +45,7 @@ import {
SwitchVariant, SwitchVariant,
imageBorderRadius, imageBorderRadius,
ts, ts,
usePopup,
} from "@kyoo/primitives"; } from "@kyoo/primitives";
import { DefaultLayout } from "../layout"; import { DefaultLayout } from "../layout";
import { Children, ComponentProps, ReactElement, ReactNode, useState } from "react"; import { Children, ComponentProps, ReactElement, ReactNode, useState } from "react";
@ -257,10 +258,11 @@ const ChangePasswordPopup = ({
); );
}; };
const AccountSettings = ({ setPopup }: { setPopup: (e?: ReactElement) => void }) => { const AccountSettings = () => {
const account = useAccount(); const account = useAccount();
const { css, theme } = useYoshiki(); const { css, theme } = useYoshiki();
const { t } = useTranslation(); const { t } = useTranslation();
const [setPopup, close] = usePopup();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { mutateAsync } = useMutation({ const { mutateAsync } = useMutation({
@ -335,7 +337,7 @@ const AccountSettings = ({ setPopup }: { setPopup: (e?: ReactElement) => void })
label={t("settings.account.username.label")} label={t("settings.account.username.label")}
inital={account.username} inital={account.username}
apply={async (v) => await mutateAsync({ username: v })} apply={async (v) => await mutateAsync({ username: v })}
close={() => setPopup(undefined)} close={close}
/>, />,
) )
} }
@ -356,7 +358,7 @@ const AccountSettings = ({ setPopup }: { setPopup: (e?: ReactElement) => void })
label={t("settings.account.email.label")} label={t("settings.account.email.label")}
inital={account.email} inital={account.email}
apply={async (v) => await mutateAsync({ email: v })} apply={async (v) => await mutateAsync({ email: v })}
close={() => setPopup(undefined)} close={close}
/>, />,
) )
} }
@ -375,7 +377,7 @@ const AccountSettings = ({ setPopup }: { setPopup: (e?: ReactElement) => void })
icon={Password} icon={Password}
label={t("settings.account.password.label")} label={t("settings.account.password.label")}
apply={async (op, np) => await editPassword({ oldPassword: op, newPassword: np })} apply={async (op, np) => await editPassword({ oldPassword: op, newPassword: np })}
close={() => setPopup(undefined)} close={close}
/>, />,
) )
} }
@ -389,67 +391,63 @@ const AccountSettings = ({ setPopup }: { setPopup: (e?: ReactElement) => void })
export const SettingsPage: QueryPage = () => { export const SettingsPage: QueryPage = () => {
const { t, i18n } = useTranslation(); const { t, i18n } = useTranslation();
const languages = new Intl.DisplayNames([i18n.language ?? "en"], { type: "language" }); const languages = new Intl.DisplayNames([i18n.language ?? "en"], { type: "language" });
const [popup, setPopup] = useState<ReactElement | undefined>(undefined);
const theme = useUserTheme("auto"); const theme = useUserTheme("auto");
return ( return (
<> <ScrollView contentContainerStyle={{ gap: ts(4), paddingBottom: ts(4) }}>
<ScrollView contentContainerStyle={{ gap: ts(4), paddingBottom: ts(4) }}> <SettingsContainer title={t("settings.general.label")}>
<SettingsContainer title={t("settings.general.label")}> <Preference
<Preference icon={Theme}
icon={Theme} label={t("settings.general.theme.label")}
description={t("settings.general.theme.description")}
>
<Select
label={t("settings.general.theme.label")} label={t("settings.general.theme.label")}
description={t("settings.general.theme.description")} value={theme}
> onValueChange={(value) => setUserTheme(value)}
<Select values={["auto", "light", "dark"]}
label={t("settings.general.theme.label")} getLabel={(key) => t(`settings.general.theme.${key}`)}
value={theme} />
onValueChange={(value) => setUserTheme(value)} </Preference>
values={["auto", "light", "dark"]} <Preference
getLabel={(key) => t(`settings.general.theme.${key}`)} icon={Language}
/> label={t("settings.general.language.label")}
</Preference> description={t("settings.general.language.description")}
<Preference >
icon={Language} <Select
label={t("settings.general.language.label")} label={t("settings.general.language.label")}
description={t("settings.general.language.description")} value={i18n.resolvedLanguage!}
> onValueChange={(value) =>
<Select i18n.changeLanguage(value !== "system" ? value : (i18n.options.lng as string))
label={t("settings.general.language.label")} }
value={i18n.resolvedLanguage!} values={["system", ...Object.keys(i18n.options.resources!)]}
onValueChange={(value) => getLabel={(key) =>
i18n.changeLanguage(value !== "system" ? value : (i18n.options.lng as string)) key === "system" ? t("settings.general.language.system") : languages.of(key) ?? key
} }
values={["system", ...Object.keys(i18n.options.resources!)]} />
getLabel={(key) => </Preference>
key === "system" ? t("settings.general.language.system") : languages.of(key) ?? key </SettingsContainer>
} <AccountSettings />
/> <SettingsContainer title={t("settings.about.label")}>
</Preference> <Link
</SettingsContainer> href="https://github.com/zoriya/kyoo/releases/latest/download/kyoo.apk"
<AccountSettings setPopup={setPopup} /> target="_blank"
<SettingsContainer title={t("settings.about.label")}> >
<Link <Preference
href="https://github.com/zoriya/kyoo/releases/latest/download/kyoo.apk" icon={Android}
target="_blank" label={t("settings.about.android-app.label")}
> description={t("settings.about.android-app.description")}
<Preference />
icon={Android} </Link>
label={t("settings.about.android-app.label")} <Link href="https://github.com/zoriya/kyoo" target="_blank">
description={t("settings.about.android-app.description")} <Preference
/> icon={Public}
</Link> label={t("settings.about.git.label")}
<Link href="https://github.com/zoriya/kyoo" target="_blank"> description={t("settings.about.git.description")}
<Preference />
icon={Public} </Link>
label={t("settings.about.git.label")} </SettingsContainer>
description={t("settings.about.git.description")} </ScrollView>
/>
</Link>
</SettingsContainer>
</ScrollView>
{popup}
</>
); );
}; };