mirror of
https://github.com/zoriya/Kyoo.git
synced 2026-05-27 01:22:30 -04:00
Move primitives
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
export * from "./page";
|
||||
export * from "./kyoo-error";
|
||||
export * from "./resources";
|
||||
export * from "./traits";
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// Stolen from https://github.com/necolas/react-native-web/issues/1026#issuecomment-1458279681
|
||||
|
||||
import { type AlertButton, type AlertOptions, Alert as RNAlert } from "react-native";
|
||||
import type { SweetAlertIcon } from "sweetalert2";
|
||||
|
||||
export interface ExtendedAlertStatic {
|
||||
alert: (
|
||||
title: string,
|
||||
message?: string,
|
||||
buttons?: AlertButton[],
|
||||
options?: AlertOptions & { icon?: SweetAlertIcon },
|
||||
) => void;
|
||||
}
|
||||
|
||||
export const Alert: ExtendedAlertStatic = RNAlert as ExtendedAlertStatic;
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// Stolen from https://github.com/necolas/react-native-web/issues/1026#issuecomment-1458279681
|
||||
|
||||
import type { AlertButton, AlertOptions } from "react-native";
|
||||
import Swal, { type SweetAlertIcon } from "sweetalert2";
|
||||
|
||||
// biome-ignore lint/complexity/noStaticOnlyClass: Compatibility with rn
|
||||
export class Alert {
|
||||
static alert(
|
||||
title: string,
|
||||
message?: string,
|
||||
buttons?: AlertButton[],
|
||||
options?: AlertOptions & { icon?: SweetAlertIcon },
|
||||
): void {
|
||||
const confirmButton = buttons
|
||||
? buttons.find((button) => button.style === "default")
|
||||
: undefined;
|
||||
const denyButton = buttons
|
||||
? buttons.find((button) => button.style === "destructive")
|
||||
: undefined;
|
||||
const cancelButton = buttons ? buttons.find((button) => button.style === "cancel") : undefined;
|
||||
|
||||
Swal.fire({
|
||||
title: title,
|
||||
text: message,
|
||||
showConfirmButton: !!confirmButton,
|
||||
showDenyButton: !!denyButton,
|
||||
showCancelButton: !!cancelButton,
|
||||
confirmButtonText: confirmButton?.text,
|
||||
denyButtonText: denyButton?.text,
|
||||
cancelButtonText: cancelButton?.text,
|
||||
icon: options?.icon,
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
if (confirmButton?.onPress !== undefined) {
|
||||
confirmButton.onPress();
|
||||
}
|
||||
} else if (result.isDenied) {
|
||||
if (denyButton?.onPress !== undefined) {
|
||||
denyButton.onPress();
|
||||
}
|
||||
} else if (result.isDismissed) {
|
||||
if (cancelButton?.onPress !== undefined) {
|
||||
cancelButton.onPress();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import AccountCircle from "@material-symbols/svg-400/rounded/account_circle-fill.svg";
|
||||
import { type ComponentType, type RefAttributes, forwardRef } from "react";
|
||||
import { Image, type ImageProps, View, type ViewStyle } from "react-native";
|
||||
import { type Stylable, px, useYoshiki } from "yoshiki/native";
|
||||
import { Icon } from "./icons";
|
||||
import { Skeleton } from "./skeleton";
|
||||
import { P } from "./text";
|
||||
|
||||
const stringToColor = (string: string) => {
|
||||
let hash = 0;
|
||||
|
||||
for (let i = 0; i < string.length; i += 1) {
|
||||
hash = string.charCodeAt(i) + ((hash << 5) - hash);
|
||||
}
|
||||
|
||||
let color = "#";
|
||||
for (let i = 0; i < 3; i += 1) {
|
||||
const value = (hash >> (i * 8)) & 0xff;
|
||||
color += `00${value.toString(16)}`.slice(-2);
|
||||
}
|
||||
return color;
|
||||
};
|
||||
|
||||
const AvatarC = forwardRef<
|
||||
View,
|
||||
{
|
||||
src?: string;
|
||||
alt?: string;
|
||||
size?: number;
|
||||
placeholder?: string;
|
||||
color?: string;
|
||||
fill?: boolean;
|
||||
as?: ComponentType<{ style?: ViewStyle } & RefAttributes<View>>;
|
||||
} & Stylable
|
||||
>(function AvatarI(
|
||||
{ src, alt, size = px(24), color, placeholder, fill = false, as, ...props },
|
||||
ref,
|
||||
) {
|
||||
const { css, theme } = useYoshiki();
|
||||
const col = color ?? theme.overlay0;
|
||||
|
||||
// TODO: Support dark themes when fill === true
|
||||
const Container = as ?? View;
|
||||
return (
|
||||
<Container
|
||||
ref={ref}
|
||||
{...css(
|
||||
[
|
||||
{
|
||||
borderRadius: 999999,
|
||||
overflow: "hidden",
|
||||
height: size,
|
||||
width: size,
|
||||
},
|
||||
fill && {
|
||||
bg: col,
|
||||
},
|
||||
placeholder && {
|
||||
bg: stringToColor(placeholder),
|
||||
},
|
||||
],
|
||||
props,
|
||||
)}
|
||||
>
|
||||
{placeholder ? (
|
||||
<P
|
||||
{...css({
|
||||
marginVertical: 0,
|
||||
lineHeight: size,
|
||||
textAlign: "center",
|
||||
})}
|
||||
>
|
||||
{placeholder[0]}
|
||||
</P>
|
||||
) : (
|
||||
<Icon icon={AccountCircle} size={size} color={fill ? col : theme.colors.white} />
|
||||
)}
|
||||
<Image
|
||||
resizeMode="cover"
|
||||
source={{ uri: src, width: size, height: size }}
|
||||
alt={alt}
|
||||
width={size}
|
||||
height={size}
|
||||
{...(css({ position: "absolute" }) as ImageProps)}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
});
|
||||
|
||||
const AvatarLoader = ({ size = px(24), ...props }: { size?: number }) => {
|
||||
const { css } = useYoshiki();
|
||||
|
||||
return (
|
||||
<Skeleton
|
||||
variant="round"
|
||||
{...css(
|
||||
{
|
||||
height: size,
|
||||
width: size,
|
||||
},
|
||||
props,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const Avatar = Object.assign(AvatarC, { Loader: AvatarLoader });
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { type ComponentType, type ForwardedRef, type ReactElement, forwardRef } from "react";
|
||||
import { type Falsy, type PressableProps, View } from "react-native";
|
||||
import { type Theme, useYoshiki } from "yoshiki/native";
|
||||
import { PressableFeedback } from "./links";
|
||||
import { P } from "./text";
|
||||
import { ts } from "./utils";
|
||||
|
||||
export const Button = forwardRef(function Button<AsProps = PressableProps>(
|
||||
{
|
||||
children,
|
||||
text,
|
||||
icon,
|
||||
licon,
|
||||
disabled,
|
||||
as,
|
||||
...props
|
||||
}: {
|
||||
children?: ReactElement | ReactElement[] | Falsy;
|
||||
text?: string;
|
||||
licon?: ReactElement | Falsy;
|
||||
icon?: ReactElement | Falsy;
|
||||
disabled?: boolean;
|
||||
as?: ComponentType<AsProps>;
|
||||
} & AsProps,
|
||||
ref: ForwardedRef<unknown>,
|
||||
) {
|
||||
const { css } = useYoshiki("button");
|
||||
|
||||
const Container = as ?? PressableFeedback;
|
||||
return (
|
||||
<Container
|
||||
ref={ref as any}
|
||||
disabled={disabled}
|
||||
{...(css(
|
||||
[
|
||||
{
|
||||
flexGrow: 0,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
overflow: "hidden",
|
||||
p: ts(0.5),
|
||||
borderRadius: ts(5),
|
||||
borderColor: (theme: Theme) => theme.accent,
|
||||
borderWidth: ts(0.5),
|
||||
fover: {
|
||||
self: { bg: (theme: Theme) => theme.accent },
|
||||
text: { color: (theme: Theme) => theme.colors.white },
|
||||
},
|
||||
},
|
||||
disabled && {
|
||||
child: {
|
||||
self: {
|
||||
borderColor: (theme) => theme.overlay1,
|
||||
},
|
||||
text: {
|
||||
color: (theme) => theme.overlay1,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
props as any,
|
||||
) as AsProps)}
|
||||
>
|
||||
{(licon || text || icon) != null && (
|
||||
<View
|
||||
{...css({
|
||||
paddingX: ts(3),
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
})}
|
||||
>
|
||||
{licon}
|
||||
{text && <P {...css({ textAlign: "center" }, "text")}>{text}</P>}
|
||||
{icon}
|
||||
</View>
|
||||
)}
|
||||
{children}
|
||||
</Container>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { type TextProps, View } from "react-native";
|
||||
import { type Theme, px, rem, useYoshiki } from "yoshiki/native";
|
||||
import { Link } from "./links";
|
||||
import { Skeleton } from "./skeleton";
|
||||
import { P } from "./text";
|
||||
import { capitalize, ts } from "./utils";
|
||||
|
||||
export const Chip = ({
|
||||
color,
|
||||
size = "medium",
|
||||
outline = false,
|
||||
label,
|
||||
href,
|
||||
replace,
|
||||
target,
|
||||
textProps,
|
||||
...props
|
||||
}: {
|
||||
color?: string;
|
||||
size?: "small" | "medium" | "large";
|
||||
outline?: boolean;
|
||||
label?: string;
|
||||
href?: string;
|
||||
replace?: boolean;
|
||||
target?: string;
|
||||
textProps?: TextProps;
|
||||
}) => {
|
||||
const { css } = useYoshiki("chip");
|
||||
|
||||
textProps ??= {};
|
||||
|
||||
const sizeMult = size === "medium" ? 1 : size === "small" ? 0.5 : 1.5;
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
replace={replace}
|
||||
target={target}
|
||||
{...css(
|
||||
[
|
||||
{
|
||||
pY: ts(1 * sizeMult),
|
||||
pX: ts(2.5 * sizeMult),
|
||||
borderRadius: ts(3),
|
||||
overflow: "hidden",
|
||||
justifyContent: "center",
|
||||
},
|
||||
outline && {
|
||||
borderColor: color ?? ((theme: Theme) => theme.accent),
|
||||
borderStyle: "solid",
|
||||
borderWidth: px(1),
|
||||
fover: {
|
||||
self: {
|
||||
bg: (theme: Theme) => theme.accent,
|
||||
},
|
||||
text: {
|
||||
color: (theme: Theme) => theme.alternate.contrast,
|
||||
},
|
||||
},
|
||||
},
|
||||
!outline && {
|
||||
bg: color ?? ((theme: Theme) => theme.accent),
|
||||
},
|
||||
],
|
||||
props,
|
||||
)}
|
||||
>
|
||||
<P
|
||||
{...css(
|
||||
[
|
||||
"text",
|
||||
{
|
||||
marginVertical: 0,
|
||||
fontSize: rem(0.8),
|
||||
color: (theme: Theme) => (outline ? theme.contrast : theme.alternate.contrast),
|
||||
},
|
||||
],
|
||||
textProps,
|
||||
)}
|
||||
>
|
||||
{label ? capitalize(label) : <Skeleton {...css({ width: rem(3) })} />}
|
||||
</P>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
Chip.Loader = ({
|
||||
color,
|
||||
size = "medium",
|
||||
outline = false,
|
||||
...props
|
||||
}: { color?: string; size?: "small" | "medium" | "large"; outline?: boolean }) => {
|
||||
const { css } = useYoshiki();
|
||||
const sizeMult = size === "medium" ? 1 : size === "small" ? 0.5 : 1.5;
|
||||
|
||||
return (
|
||||
<View
|
||||
{...css(
|
||||
[
|
||||
{
|
||||
pY: ts(1 * sizeMult),
|
||||
pX: ts(2.5 * sizeMult),
|
||||
borderRadius: ts(3),
|
||||
overflow: "hidden",
|
||||
justifyContent: "center",
|
||||
},
|
||||
outline && {
|
||||
borderColor: color ?? ((theme: Theme) => theme.accent),
|
||||
borderStyle: "solid",
|
||||
borderWidth: px(1),
|
||||
},
|
||||
!outline && {
|
||||
bg: color ?? ((theme: Theme) => theme.accent),
|
||||
},
|
||||
],
|
||||
props,
|
||||
)}
|
||||
>
|
||||
<Skeleton {...css({ width: rem(3) })} />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export const imageBorderRadius = 10;
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type { ComponentType } from "react";
|
||||
import { View, type ViewProps } from "react-native";
|
||||
import { percent, px, useYoshiki } from "yoshiki/native";
|
||||
|
||||
export const Container = <AsProps = ViewProps>({
|
||||
as,
|
||||
...props
|
||||
}: { as?: ComponentType<AsProps> } & AsProps) => {
|
||||
const { css } = useYoshiki();
|
||||
|
||||
const As = as ?? View;
|
||||
return (
|
||||
<As
|
||||
{...(css(
|
||||
{
|
||||
display: "flex",
|
||||
paddingHorizontal: px(15),
|
||||
alignSelf: "center",
|
||||
width: {
|
||||
xs: percent(100),
|
||||
sm: px(540),
|
||||
md: px(880),
|
||||
lg: px(1170),
|
||||
},
|
||||
},
|
||||
props,
|
||||
) as any)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { HR as EHR } from "@expo/html-elements";
|
||||
import { type Stylable, px, useYoshiki } from "yoshiki/native";
|
||||
import { ts } from "./utils";
|
||||
|
||||
export const HR = ({
|
||||
orientation = "horizontal",
|
||||
...props
|
||||
}: { orientation?: "vertical" | "horizontal" } & Stylable) => {
|
||||
const { css } = useYoshiki();
|
||||
|
||||
return (
|
||||
<EHR
|
||||
{...css(
|
||||
[
|
||||
{
|
||||
opacity: 0.7,
|
||||
bg: (theme) => theme.overlay0,
|
||||
borderWidth: 0,
|
||||
},
|
||||
orientation === "vertical" && {
|
||||
width: px(1),
|
||||
height: "auto",
|
||||
marginY: ts(1),
|
||||
marginX: ts(2),
|
||||
},
|
||||
orientation === "horizontal" && {
|
||||
height: px(1),
|
||||
width: "auto",
|
||||
marginX: ts(1),
|
||||
marginY: ts(2),
|
||||
},
|
||||
],
|
||||
props,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type React from "react";
|
||||
import { type ComponentProps, type ComponentType, type ForwardedRef, forwardRef } from "react";
|
||||
import { Platform, type PressableProps } from "react-native";
|
||||
import type { SvgProps } from "react-native-svg";
|
||||
import type { YoshikiStyle } from "yoshiki";
|
||||
import { type Stylable, type Theme, px, useYoshiki } from "yoshiki/native";
|
||||
import { PressableFeedback } from "./links";
|
||||
import { P } from "./text";
|
||||
import { type Breakpoint, focusReset, ts } from "./utils";
|
||||
|
||||
declare module "react" {
|
||||
function forwardRef<T, P = {}>(
|
||||
render: (props: P, ref: React.ForwardedRef<T>) => React.ReactElement | null,
|
||||
): (props: P & React.RefAttributes<T>) => React.ReactElement | null;
|
||||
}
|
||||
|
||||
export type Icon = ComponentType<SvgProps>;
|
||||
|
||||
type IconProps = {
|
||||
icon: Icon;
|
||||
color?: Breakpoint<string>;
|
||||
size?: YoshikiStyle<number | string>;
|
||||
};
|
||||
|
||||
export const Icon = ({ icon: Icon, color, size = 24, ...props }: IconProps) => {
|
||||
const { css, theme } = useYoshiki();
|
||||
const computed = css(
|
||||
{ width: size, height: size, fill: color ?? theme.contrast, flexShrink: 0 } as any,
|
||||
props,
|
||||
) as any;
|
||||
|
||||
return (
|
||||
<Icon
|
||||
{...Platform.select<SvgProps>({
|
||||
web: computed,
|
||||
default: {
|
||||
height: computed.style[0]?.height,
|
||||
width: computed.style[0]?.width,
|
||||
fill: computed.style[0]?.fill,
|
||||
...computed,
|
||||
},
|
||||
})}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const IconButton = forwardRef(function IconButton<AsProps = PressableProps>(
|
||||
{
|
||||
icon,
|
||||
size,
|
||||
color,
|
||||
as,
|
||||
...asProps
|
||||
}: IconProps & {
|
||||
as?: ComponentType<AsProps>;
|
||||
} & AsProps,
|
||||
ref: ForwardedRef<unknown>,
|
||||
) {
|
||||
const { css, theme } = useYoshiki();
|
||||
|
||||
const Container = as ?? PressableFeedback;
|
||||
|
||||
return (
|
||||
<Container
|
||||
ref={ref as any}
|
||||
focusRipple
|
||||
{...(css(
|
||||
{
|
||||
alignSelf: "center",
|
||||
p: ts(1),
|
||||
m: px(2),
|
||||
overflow: "hidden",
|
||||
borderRadius: 9999,
|
||||
fover: {
|
||||
self: {
|
||||
...focusReset,
|
||||
bg: (theme: Theme) => theme.overlay0,
|
||||
},
|
||||
},
|
||||
},
|
||||
asProps,
|
||||
) as AsProps)}
|
||||
>
|
||||
<Icon
|
||||
icon={icon}
|
||||
size={size}
|
||||
color={"disabled" in asProps && asProps.disabled ? theme.overlay1 : color}
|
||||
/>
|
||||
</Container>
|
||||
);
|
||||
});
|
||||
|
||||
export const IconFab = <AsProps = PressableProps>(
|
||||
props: ComponentProps<typeof IconButton<AsProps>>,
|
||||
) => {
|
||||
const { css, theme } = useYoshiki();
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
color={theme.colors.black}
|
||||
{...(css(
|
||||
{
|
||||
bg: (theme) => theme.accent,
|
||||
fover: {
|
||||
self: {
|
||||
transform: "scale(1.3)" as any,
|
||||
bg: (theme: Theme) => theme.accent,
|
||||
},
|
||||
},
|
||||
},
|
||||
props,
|
||||
) as any)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const DottedSeparator = (props: Stylable) => {
|
||||
const { css } = useYoshiki();
|
||||
return <P {...css({ mX: ts(1) }, props)}>{String.fromCharCode(0x2022)}</P>;
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type { ReactElement } from "react";
|
||||
import type { ImageStyle } from "react-native";
|
||||
import type { YoshikiStyle } from "yoshiki/src/type";
|
||||
import type { KyooImage } from "~/models";
|
||||
|
||||
export type YoshikiEnhanced<Style> = Style extends any
|
||||
? {
|
||||
[key in keyof Style]: YoshikiStyle<Style[key]>;
|
||||
}
|
||||
: never;
|
||||
|
||||
export type Props = {
|
||||
src?: KyooImage | null;
|
||||
quality: "low" | "medium" | "high";
|
||||
alt?: string;
|
||||
Err?: ReactElement | null;
|
||||
forcedLoading?: boolean;
|
||||
};
|
||||
|
||||
export type ImageLayout = YoshikiEnhanced<
|
||||
| { width: ImageStyle["width"]; height: ImageStyle["height"] }
|
||||
| { width: ImageStyle["width"]; aspectRatio: ImageStyle["aspectRatio"] }
|
||||
| { height: ImageStyle["height"]; aspectRatio: ImageStyle["aspectRatio"] }
|
||||
>;
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type { ReactElement } from "react";
|
||||
import { View } from "react-native";
|
||||
import { Blurhash } from "react-native-blurhash";
|
||||
import { type Stylable, useYoshiki } from "yoshiki/native";
|
||||
|
||||
export const BlurhashContainer = ({
|
||||
blurhash,
|
||||
children,
|
||||
...props
|
||||
}: { blurhash: string; children?: ReactElement | ReactElement[] } & Stylable) => {
|
||||
const { css } = useYoshiki();
|
||||
|
||||
return (
|
||||
<View {...props}>
|
||||
<Blurhash
|
||||
blurhash={blurhash}
|
||||
resizeMode="cover"
|
||||
{...css({ position: "absolute", top: 0, bottom: 0, left: 0, right: 0 })}
|
||||
/>
|
||||
{children}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,322 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { decode } from "blurhash";
|
||||
import {
|
||||
type HTMLAttributes,
|
||||
type ReactElement,
|
||||
forwardRef,
|
||||
useImperativeHandle,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useYoshiki } from "yoshiki";
|
||||
import { nativeStyleToCss } from "yoshiki/native";
|
||||
|
||||
// The blurhashToUrl has been stolen from https://gist.github.com/mattiaz9/53cb67040fa135cb395b1d015a200aff
|
||||
export function blurHashToDataURL(hash: string | undefined): string | undefined {
|
||||
if (!hash) return undefined;
|
||||
|
||||
const pixels = decode(hash, 32, 32);
|
||||
const dataURL = parsePixels(pixels, 32, 32);
|
||||
return dataURL;
|
||||
}
|
||||
|
||||
// thanks to https://github.com/wheany/js-png-encoder
|
||||
function parsePixels(pixels: Uint8ClampedArray, width: number, height: number) {
|
||||
const pixelsString = Array.from(pixels)
|
||||
.map((byte) => String.fromCharCode(byte))
|
||||
.join("");
|
||||
const pngString = generatePng(width, height, pixelsString);
|
||||
const dataURL =
|
||||
typeof Buffer !== "undefined"
|
||||
? Buffer.from(getPngArray(pngString)).toString("base64")
|
||||
: btoa(pngString);
|
||||
return `data:image/png;base64,${dataURL}`;
|
||||
}
|
||||
|
||||
function getPngArray(pngString: string) {
|
||||
const pngArray = new Uint8Array(pngString.length);
|
||||
for (let i = 0; i < pngString.length; i++) {
|
||||
pngArray[i] = pngString.charCodeAt(i);
|
||||
}
|
||||
return pngArray;
|
||||
}
|
||||
|
||||
function generatePng(width: number, height: number, rgbaString: string) {
|
||||
const DEFLATE_METHOD = String.fromCharCode(0x78, 0x01);
|
||||
const CRC_TABLE: number[] = [];
|
||||
const SIGNATURE = String.fromCharCode(137, 80, 78, 71, 13, 10, 26, 10);
|
||||
const NO_FILTER = String.fromCharCode(0);
|
||||
|
||||
// biome-ignore lint: not gonna fix stackowerflow code that works
|
||||
let n, c, k;
|
||||
|
||||
// make crc table
|
||||
for (n = 0; n < 256; n++) {
|
||||
c = n;
|
||||
for (k = 0; k < 8; k++) {
|
||||
if (c & 1) {
|
||||
c = 0xedb88320 ^ (c >>> 1);
|
||||
} else {
|
||||
c = c >>> 1;
|
||||
}
|
||||
}
|
||||
CRC_TABLE[n] = c;
|
||||
}
|
||||
|
||||
// Functions
|
||||
function inflateStore(data: string) {
|
||||
const MAX_STORE_LENGTH = 65535;
|
||||
let storeBuffer = "";
|
||||
// biome-ignore lint: not gonna fix stackowerflow code that works
|
||||
let remaining;
|
||||
// biome-ignore lint: not gonna fix stackowerflow code that works
|
||||
let blockType;
|
||||
|
||||
for (let i = 0; i < data.length; i += MAX_STORE_LENGTH) {
|
||||
remaining = data.length - i;
|
||||
blockType = "";
|
||||
|
||||
if (remaining <= MAX_STORE_LENGTH) {
|
||||
blockType = String.fromCharCode(0x01);
|
||||
} else {
|
||||
remaining = MAX_STORE_LENGTH;
|
||||
blockType = String.fromCharCode(0x00);
|
||||
}
|
||||
// little-endian
|
||||
storeBuffer += blockType + String.fromCharCode(remaining & 0xff, (remaining & 0xff00) >>> 8);
|
||||
storeBuffer += String.fromCharCode(~remaining & 0xff, (~remaining & 0xff00) >>> 8);
|
||||
|
||||
storeBuffer += data.substring(i, i + remaining);
|
||||
}
|
||||
|
||||
return storeBuffer;
|
||||
}
|
||||
|
||||
function adler32(data: string) {
|
||||
const MOD_ADLER = 65521;
|
||||
let a = 1;
|
||||
let b = 0;
|
||||
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
a = (a + data.charCodeAt(i)) % MOD_ADLER;
|
||||
b = (b + a) % MOD_ADLER;
|
||||
}
|
||||
|
||||
return (b << 16) | a;
|
||||
}
|
||||
|
||||
function updateCrc(crc: number, buf: string) {
|
||||
let c = crc;
|
||||
let b: number;
|
||||
|
||||
for (let n = 0; n < buf.length; n++) {
|
||||
b = buf.charCodeAt(n);
|
||||
c = CRC_TABLE[(c ^ b) & 0xff] ^ (c >>> 8);
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
function crc(buf: string) {
|
||||
return updateCrc(0xffffffff, buf) ^ 0xffffffff;
|
||||
}
|
||||
|
||||
function dwordAsString(dword: number) {
|
||||
return String.fromCharCode(
|
||||
(dword & 0xff000000) >>> 24,
|
||||
(dword & 0x00ff0000) >>> 16,
|
||||
(dword & 0x0000ff00) >>> 8,
|
||||
dword & 0x000000ff,
|
||||
);
|
||||
}
|
||||
|
||||
function createChunk(length: number, type: string, data: string) {
|
||||
const CRC = crc(type + data);
|
||||
|
||||
return dwordAsString(length) + type + data + dwordAsString(CRC);
|
||||
}
|
||||
|
||||
function createIHDR(width: number, height: number) {
|
||||
const IHDRdata =
|
||||
dwordAsString(width) +
|
||||
dwordAsString(height) +
|
||||
// bit depth
|
||||
String.fromCharCode(8) +
|
||||
// color type: 6=truecolor with alpha
|
||||
String.fromCharCode(6) +
|
||||
// compression method: 0=deflate, only allowed value
|
||||
String.fromCharCode(0) +
|
||||
// filtering: 0=adaptive, only allowed value
|
||||
String.fromCharCode(0) +
|
||||
// interlacing: 0=none
|
||||
String.fromCharCode(0);
|
||||
|
||||
return createChunk(13, "IHDR", IHDRdata);
|
||||
}
|
||||
|
||||
// PNG creations
|
||||
|
||||
const IEND = createChunk(0, "IEND", "");
|
||||
const IHDR = createIHDR(width, height);
|
||||
|
||||
let scanlines = "";
|
||||
let scanline: string;
|
||||
|
||||
for (let y = 0; y < rgbaString.length; y += width * 4) {
|
||||
scanline = NO_FILTER;
|
||||
if (Array.isArray(rgbaString)) {
|
||||
for (let x = 0; x < width * 4; x++) {
|
||||
scanline += String.fromCharCode(rgbaString[y + x] & 0xff);
|
||||
}
|
||||
} else {
|
||||
scanline += rgbaString.substr(y, width * 4);
|
||||
}
|
||||
scanlines += scanline;
|
||||
}
|
||||
|
||||
const compressedScanlines =
|
||||
DEFLATE_METHOD + inflateStore(scanlines) + dwordAsString(adler32(scanlines));
|
||||
const IDAT = createChunk(compressedScanlines.length, "IDAT", compressedScanlines);
|
||||
|
||||
const pngString = SIGNATURE + IHDR + IDAT + IEND;
|
||||
return pngString;
|
||||
}
|
||||
|
||||
const BlurhashCanvas = forwardRef<
|
||||
HTMLCanvasElement,
|
||||
{
|
||||
blurhash: string;
|
||||
} & HTMLAttributes<HTMLCanvasElement>
|
||||
>(function BlurhashCanvas({ blurhash, ...props }, forwardedRef) {
|
||||
const ref = useRef<HTMLCanvasElement>(null);
|
||||
const { css } = useYoshiki();
|
||||
|
||||
useImperativeHandle(forwardedRef, () => ref.current!, []);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!ref.current) return;
|
||||
const pixels = decode(blurhash, 32, 32);
|
||||
const ctx = ref.current.getContext("2d");
|
||||
if (!ctx) return;
|
||||
const imageData = ctx.createImageData(32, 32);
|
||||
imageData.data.set(pixels);
|
||||
ctx.putImageData(imageData, 0, 0);
|
||||
}, [blurhash]);
|
||||
|
||||
return (
|
||||
<canvas
|
||||
ref={ref}
|
||||
width={32}
|
||||
height={32}
|
||||
{...css(
|
||||
{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
},
|
||||
props,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const BlurhashDiv = forwardRef<
|
||||
HTMLDivElement,
|
||||
{ blurhash: string } & HTMLAttributes<HTMLDivElement>
|
||||
>(function BlurhashDiv({ blurhash, ...props }, ref) {
|
||||
const { css } = useYoshiki();
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
style={{
|
||||
// Use a blurhash here to nicely fade the NextImage when it is loaded completly
|
||||
// (this prevents loading the image line by line which is ugly and buggy on firefox)
|
||||
backgroundImage: `url(${blurHashToDataURL(blurhash)})`,
|
||||
backgroundSize: "cover",
|
||||
backgroundRepeat: "no-repeat",
|
||||
backgroundPosition: "50% 50%",
|
||||
}}
|
||||
{...css(
|
||||
{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
},
|
||||
props,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
export const useRenderType = () => {
|
||||
const [renderType, setRenderType] = useState<"ssr" | "hydratation" | "client">(
|
||||
typeof window === "undefined" ? "ssr" : "hydratation",
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setRenderType("client");
|
||||
}, []);
|
||||
|
||||
return renderType;
|
||||
};
|
||||
|
||||
export const BlurhashContainer = ({
|
||||
blurhash,
|
||||
children,
|
||||
...props
|
||||
}: {
|
||||
blurhash: string;
|
||||
children?: ReactElement | ReactElement[];
|
||||
}) => {
|
||||
const { css } = useYoshiki();
|
||||
const renderType = useRenderType();
|
||||
|
||||
return (
|
||||
<div
|
||||
{...css(
|
||||
{
|
||||
// To reproduce view's behavior
|
||||
boxSizing: "border-box",
|
||||
overflow: "hidden",
|
||||
position: "relative",
|
||||
},
|
||||
nativeStyleToCss(props),
|
||||
)}
|
||||
>
|
||||
{renderType === "ssr" && <BlurhashDiv blurhash={blurhash} />}
|
||||
{renderType === "client" && <BlurhashCanvas blurhash={blurhash} />}
|
||||
{renderType === "hydratation" && (
|
||||
<div dangerouslySetInnerHTML={{ __html: "" }} suppressHydrationWarning />
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { getCurrentToken } from "@kyoo/models";
|
||||
import { type ReactElement, useState } from "react";
|
||||
import { type FlexStyle, type ImageStyle, View, type ViewStyle } from "react-native";
|
||||
import { Blurhash } from "react-native-blurhash";
|
||||
import FastImage from "react-native-fast-image";
|
||||
import { percent, useYoshiki } from "yoshiki/native";
|
||||
import { Skeleton } from "../skeleton";
|
||||
import type { ImageLayout, Props } from "./base-image";
|
||||
|
||||
export const Image = ({
|
||||
src,
|
||||
quality,
|
||||
alt,
|
||||
forcedLoading = false,
|
||||
layout,
|
||||
Err,
|
||||
...props
|
||||
}: Props & { style?: ImageStyle } & { layout: ImageLayout }) => {
|
||||
const { css } = useYoshiki();
|
||||
const [state, setState] = useState<"loading" | "errored" | "finished">(
|
||||
src ? "loading" : "errored",
|
||||
);
|
||||
|
||||
// This could be done with a key but this makes the API easier to use.
|
||||
// This unsures that the state is resetted when the source change (useful for recycler lists.)
|
||||
const [oldSource, setOldSource] = useState(src);
|
||||
if (oldSource !== src) {
|
||||
setState("loading");
|
||||
setOldSource(src);
|
||||
}
|
||||
|
||||
const border = { borderRadius: 6, overflow: "hidden" } satisfies ViewStyle;
|
||||
|
||||
if (forcedLoading) return <Skeleton variant="custom" {...css([layout, border], props)} />;
|
||||
if (!src || state === "errored") {
|
||||
return Err !== undefined ? (
|
||||
Err
|
||||
) : (
|
||||
<View {...css([{ bg: (theme) => theme.overlay0 }, layout, border], props)} />
|
||||
);
|
||||
}
|
||||
|
||||
quality ??= "high";
|
||||
const token = getCurrentToken();
|
||||
return (
|
||||
<View {...css([layout, border], props)}>
|
||||
{state !== "finished" && (
|
||||
<Blurhash
|
||||
blurhash={src.blurhash}
|
||||
resizeMode="cover"
|
||||
{...css({ width: percent(100), height: percent(100) })}
|
||||
/>
|
||||
)}
|
||||
<FastImage
|
||||
source={{
|
||||
uri: src[quality],
|
||||
headers: token
|
||||
? {
|
||||
Authorization: token,
|
||||
}
|
||||
: {},
|
||||
priority: FastImage.priority[quality === "medium" ? "normal" : quality],
|
||||
}}
|
||||
accessibilityLabel={alt}
|
||||
onLoad={() => setState("finished")}
|
||||
onError={() => setState("errored")}
|
||||
resizeMode={FastImage.resizeMode.cover}
|
||||
{...(css({
|
||||
width: percent(100),
|
||||
height: percent(100),
|
||||
}) as { style: FlexStyle })}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
Image.Loader = ({ layout, ...props }: { layout: ImageLayout; children?: ReactElement }) => {
|
||||
const { css } = useYoshiki();
|
||||
const border = { borderRadius: 6, overflow: "hidden" } satisfies ViewStyle;
|
||||
|
||||
return <Skeleton variant="custom" show {...css([layout, border], props)} />;
|
||||
};
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import NextImage from "next/image";
|
||||
import { type ReactElement, useState } from "react";
|
||||
import { type ImageStyle, View, type ViewStyle } from "react-native";
|
||||
import { useYoshiki } from "yoshiki/native";
|
||||
import { imageBorderRadius } from "../constants";
|
||||
import { Skeleton } from "../skeleton";
|
||||
import type { ImageLayout, Props } from "./base-image";
|
||||
import { BlurhashContainer, useRenderType } from "./blurhash.web";
|
||||
|
||||
export const Image = ({
|
||||
src,
|
||||
quality,
|
||||
alt,
|
||||
forcedLoading = false,
|
||||
layout,
|
||||
Err,
|
||||
...props
|
||||
}: Props & { style?: ImageStyle } & { layout: ImageLayout }) => {
|
||||
const { css } = useYoshiki();
|
||||
const [state, setState] = useState<"loading" | "errored" | "finished">(
|
||||
typeof window === "undefined" ? "finished" : "loading",
|
||||
);
|
||||
|
||||
const border = { borderRadius: imageBorderRadius } satisfies ViewStyle;
|
||||
|
||||
if (forcedLoading) return <Skeleton variant="custom" {...css([layout, border], props)} />;
|
||||
if (!src || state === "errored") {
|
||||
return Err !== undefined ? (
|
||||
Err
|
||||
) : (
|
||||
<View {...css([{ bg: (theme) => theme.overlay0 }, layout, border], props)} />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<BlurhashContainer blurhash={src.blurhash} {...css([layout, border], props)}>
|
||||
<img
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
color: "transparent",
|
||||
objectFit: "cover",
|
||||
opacity: state === "loading" ? 0 : 1,
|
||||
transition: "opacity .2s ease-out",
|
||||
}}
|
||||
// It's intended to keep `loading` before `src` because React updates
|
||||
// props in order which causes Safari/Firefox to not lazy load properly.
|
||||
// See https://github.com/facebook/react/issues/25883
|
||||
loading={quality === "high" ? "eager" : "lazy"}
|
||||
decoding="async"
|
||||
fetchpriority={quality === "high" ? "high" : undefined}
|
||||
src={src[quality ?? "high"]}
|
||||
alt={alt!}
|
||||
onLoad={() => setState("finished")}
|
||||
onError={() => setState("errored")}
|
||||
suppressHydrationWarning
|
||||
/>
|
||||
</BlurhashContainer>
|
||||
);
|
||||
};
|
||||
|
||||
Image.Loader = ({ layout, ...props }: { layout: ImageLayout; children?: ReactElement }) => {
|
||||
const { css } = useYoshiki();
|
||||
const border = { borderRadius: 6, overflow: "hidden" } satisfies ViewStyle;
|
||||
|
||||
return <Skeleton variant="custom" show {...css([layout, border], props)} />;
|
||||
};
|
||||
@@ -0,0 +1,170 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { LinearGradient, type LinearGradientProps } from "expo-linear-gradient";
|
||||
import type { ComponentProps, ComponentType, ReactElement, ReactNode } from "react";
|
||||
import { type ImageStyle, View, type ViewProps, type ViewStyle } from "react-native";
|
||||
import { percent } from "yoshiki/native";
|
||||
import { useYoshiki } from "yoshiki/native";
|
||||
import { imageBorderRadius } from "../constants";
|
||||
import { ContrastArea } from "../themes";
|
||||
import type { ImageLayout, Props, YoshikiEnhanced } from "./base-image";
|
||||
import { Image } from "./image";
|
||||
|
||||
export { Sprite } from "./sprite";
|
||||
export { BlurhashContainer } from "./blurhash";
|
||||
export { type Props as ImageProps, Image };
|
||||
|
||||
export const Poster = ({
|
||||
alt,
|
||||
layout,
|
||||
...props
|
||||
}: Props & { style?: ImageStyle } & {
|
||||
layout: YoshikiEnhanced<{ width: ImageStyle["width"] } | { height: ImageStyle["height"] }>;
|
||||
}) => <Image alt={alt!} layout={{ aspectRatio: 2 / 3, ...layout }} {...props} />;
|
||||
|
||||
Poster.Loader = ({
|
||||
layout,
|
||||
...props
|
||||
}: {
|
||||
children?: ReactElement;
|
||||
layout: YoshikiEnhanced<{ width: ImageStyle["width"] } | { height: ImageStyle["height"] }>;
|
||||
}) => <Image.Loader layout={{ aspectRatio: 2 / 3, ...layout }} {...props} />;
|
||||
|
||||
export const PosterBackground = ({
|
||||
alt,
|
||||
layout,
|
||||
...props
|
||||
}: Omit<ComponentProps<typeof ImageBackground>, "layout"> & { style?: ImageStyle } & {
|
||||
layout: YoshikiEnhanced<{ width: ImageStyle["width"] } | { height: ImageStyle["height"] }>;
|
||||
}) => {
|
||||
const { css } = useYoshiki();
|
||||
|
||||
return (
|
||||
<ImageBackground
|
||||
alt={alt!}
|
||||
layout={{ aspectRatio: 2 / 3, ...layout }}
|
||||
{...css({ borderRadius: imageBorderRadius }, props)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
type ImageBackgroundProps = {
|
||||
children?: ReactNode;
|
||||
containerStyle?: YoshikiEnhanced<ViewStyle>;
|
||||
imageStyle?: YoshikiEnhanced<ImageStyle>;
|
||||
layout?: ImageLayout;
|
||||
contrast?: "light" | "dark" | "user";
|
||||
};
|
||||
|
||||
export const ImageBackground = <AsProps = ViewProps>({
|
||||
src,
|
||||
alt,
|
||||
quality,
|
||||
as,
|
||||
children,
|
||||
containerStyle,
|
||||
imageStyle,
|
||||
layout,
|
||||
contrast = "dark",
|
||||
imageSibling,
|
||||
...asProps
|
||||
}: {
|
||||
as?: ComponentType<AsProps>;
|
||||
imageSibling?: ReactElement;
|
||||
} & AsProps &
|
||||
ImageBackgroundProps &
|
||||
Props) => {
|
||||
const Container = as ?? View;
|
||||
|
||||
return (
|
||||
<ContrastArea contrastText mode={contrast}>
|
||||
{({ css }) => (
|
||||
<Container {...(css([layout, { overflow: "hidden" }], asProps) as AsProps)}>
|
||||
<View
|
||||
{...css([
|
||||
{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: -1,
|
||||
bg: (theme) => theme.background,
|
||||
},
|
||||
containerStyle,
|
||||
])}
|
||||
>
|
||||
{src && (
|
||||
<Image
|
||||
src={src}
|
||||
quality={quality}
|
||||
alt={alt!}
|
||||
layout={{ width: percent(100), height: percent(100) }}
|
||||
{...(css([{ borderWidth: 0, borderRadius: 0 }, imageStyle]) as {
|
||||
style: ImageStyle;
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{imageSibling}
|
||||
</View>
|
||||
{children}
|
||||
</Container>
|
||||
)}
|
||||
</ContrastArea>
|
||||
);
|
||||
};
|
||||
|
||||
export const GradientImageBackground = <AsProps = ViewProps>({
|
||||
contrast = "dark",
|
||||
gradient,
|
||||
...props
|
||||
}: {
|
||||
as?: ComponentType<AsProps>;
|
||||
gradient?: Partial<LinearGradientProps>;
|
||||
} & AsProps &
|
||||
ImageBackgroundProps &
|
||||
Props) => {
|
||||
const { css, theme } = useYoshiki();
|
||||
|
||||
return (
|
||||
<ImageBackground
|
||||
contrast={contrast}
|
||||
imageSibling={
|
||||
<LinearGradient
|
||||
start={{ x: 0, y: 0.25 }}
|
||||
end={{ x: 0, y: 1 }}
|
||||
colors={["transparent", theme[contrast].darkOverlay]}
|
||||
{...css(
|
||||
{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
},
|
||||
typeof gradient === "object" ? gradient : undefined,
|
||||
)}
|
||||
/>
|
||||
}
|
||||
{...(props as any)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Image, View } from "react-native";
|
||||
|
||||
export const Sprite = ({
|
||||
src,
|
||||
alt,
|
||||
width,
|
||||
height,
|
||||
x,
|
||||
y,
|
||||
rows,
|
||||
columns,
|
||||
style,
|
||||
...props
|
||||
}: {
|
||||
src: string;
|
||||
alt: string;
|
||||
width: number;
|
||||
height: number;
|
||||
x: number;
|
||||
y: number;
|
||||
rows: number;
|
||||
columns: number;
|
||||
style?: object;
|
||||
}) => {
|
||||
return (
|
||||
<View style={{ width, height, overflow: "hidden", flexGrow: 0, flexShrink: 0 }}>
|
||||
<Image
|
||||
source={{ uri: src }}
|
||||
alt={alt}
|
||||
width={width * columns}
|
||||
height={height * rows}
|
||||
style={{ transform: [{ translateX: -x }, { translateY: -y }] }}
|
||||
{...props}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import NextImage from "next/image";
|
||||
|
||||
export const Sprite = ({
|
||||
src,
|
||||
alt,
|
||||
style,
|
||||
x,
|
||||
y,
|
||||
...props
|
||||
}: {
|
||||
src: string;
|
||||
alt: string;
|
||||
style?: object;
|
||||
width: number;
|
||||
height: number;
|
||||
x: number;
|
||||
y: number;
|
||||
}) => {
|
||||
return (
|
||||
<NextImage
|
||||
src={src}
|
||||
priority={false}
|
||||
alt={alt!}
|
||||
// Don't use next's server to reprocess images, they are already optimized by kyoo.
|
||||
unoptimized={true}
|
||||
style={{
|
||||
objectFit: "none",
|
||||
objectPosition: `${-x}px ${-y}px`,
|
||||
flexGrow: 0,
|
||||
flexShrink: 0,
|
||||
...style,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export { Header, Main, Nav, Footer, UL } from "@expo/html-elements";
|
||||
export * from "./text";
|
||||
export * from "./themes";
|
||||
export * from "./icons";
|
||||
export * from "./links";
|
||||
export * from "./avatar";
|
||||
export * from "./image";
|
||||
export * from "./skeleton";
|
||||
export * from "./tooltip";
|
||||
export * from "./container";
|
||||
export * from "./divider";
|
||||
export * from "./progress";
|
||||
export * from "./slider";
|
||||
export * from "./snackbar";
|
||||
export * from "./alert";
|
||||
export * from "./menu";
|
||||
export * from "./popup";
|
||||
export * from "./select";
|
||||
export * from "./input";
|
||||
export * from "./button";
|
||||
export * from "./chip";
|
||||
|
||||
export * from "./utils";
|
||||
export * from "./constants";
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { type ReactNode, forwardRef, useState } from "react";
|
||||
import { TextInput, type TextInputProps, View, type ViewStyle } from "react-native";
|
||||
import { type Theme, px, useYoshiki } from "yoshiki/native";
|
||||
import type { YoshikiEnhanced } from "./image/base-image";
|
||||
import { focusReset, ts } from "./utils";
|
||||
|
||||
export const Input = forwardRef<
|
||||
TextInput,
|
||||
{
|
||||
variant?: "small" | "big";
|
||||
right?: ReactNode;
|
||||
containerStyle?: YoshikiEnhanced<ViewStyle>;
|
||||
} & TextInputProps
|
||||
>(function Input(
|
||||
{ placeholderTextColor, variant = "small", right, containerStyle, ...props },
|
||||
ref,
|
||||
) {
|
||||
const [focused, setFocused] = useState(false);
|
||||
const { css, theme } = useYoshiki();
|
||||
|
||||
return (
|
||||
<View
|
||||
{...css([
|
||||
{
|
||||
borderColor: (theme) => theme.accent,
|
||||
borderRadius: ts(1),
|
||||
borderWidth: px(1),
|
||||
borderStyle: "solid",
|
||||
padding: ts(0.5),
|
||||
flexDirection: "row",
|
||||
alignContent: "center",
|
||||
alignItems: "center",
|
||||
},
|
||||
variant === "big" && {
|
||||
borderRadius: ts(4),
|
||||
p: ts(1),
|
||||
},
|
||||
focused && {
|
||||
borderWidth: px(2),
|
||||
},
|
||||
containerStyle,
|
||||
])}
|
||||
>
|
||||
<TextInput
|
||||
ref={ref}
|
||||
placeholderTextColor={placeholderTextColor ?? theme.paragraph}
|
||||
onFocus={() => setFocused(true)}
|
||||
onBlur={() => setFocused(false)}
|
||||
{...css(
|
||||
{
|
||||
flexGrow: 1,
|
||||
color: (theme: Theme) => theme.paragraph,
|
||||
borderWidth: 0,
|
||||
...focusReset,
|
||||
},
|
||||
props,
|
||||
)}
|
||||
/>
|
||||
{right}
|
||||
</View>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type { UrlObject } from "node:url";
|
||||
import { type ReactNode, forwardRef } from "react";
|
||||
import {
|
||||
Linking,
|
||||
Platform,
|
||||
Pressable,
|
||||
type PressableProps,
|
||||
type TextProps,
|
||||
type View,
|
||||
} from "react-native";
|
||||
import { TextLink, useLink } from "solito/link";
|
||||
import { parseNextPath } from "solito/router";
|
||||
import { useTheme, useYoshiki } from "yoshiki/native";
|
||||
import { alpha } from "./themes";
|
||||
|
||||
export const A = ({
|
||||
href,
|
||||
replace,
|
||||
children,
|
||||
target,
|
||||
...props
|
||||
}: TextProps & {
|
||||
href?: string | UrlObject | null;
|
||||
target?: string;
|
||||
replace?: boolean;
|
||||
children: ReactNode;
|
||||
}) => {
|
||||
const { css, theme } = useYoshiki();
|
||||
|
||||
return (
|
||||
<TextLink
|
||||
href={href ?? ""}
|
||||
target={target}
|
||||
replace={replace as any}
|
||||
experimental={
|
||||
replace
|
||||
? {
|
||||
nativeBehavior: "stack-replace",
|
||||
isNestedNavigator: true,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
textProps={css(
|
||||
[
|
||||
{
|
||||
fontFamily: theme.font.normal,
|
||||
color: theme.link,
|
||||
},
|
||||
{
|
||||
userSelect: "text",
|
||||
} as any,
|
||||
],
|
||||
{
|
||||
hrefAttrs: { target },
|
||||
...props,
|
||||
},
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</TextLink>
|
||||
);
|
||||
};
|
||||
|
||||
export const PressableFeedback = forwardRef<View, PressableProps>(function Feedback(
|
||||
{ children, ...props },
|
||||
ref,
|
||||
) {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Pressable
|
||||
ref={ref}
|
||||
// TODO: Enable ripple on tv. Waiting for https://github.com/react-native-tvos/react-native-tvos/issues/440
|
||||
{...(Platform.isTV
|
||||
? {}
|
||||
: { android_ripple: { foreground: true, color: alpha(theme.contrast, 0.5) as any } })}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</Pressable>
|
||||
);
|
||||
});
|
||||
|
||||
export const Link = ({
|
||||
href: link,
|
||||
replace,
|
||||
target,
|
||||
children,
|
||||
...props
|
||||
}: { href?: string | UrlObject | null; target?: string; replace?: boolean } & PressableProps) => {
|
||||
const href = link && typeof link === "object" ? parseNextPath(link) : link;
|
||||
const linkProps = useLink({
|
||||
href: href ?? "#",
|
||||
replace,
|
||||
experimental: { nativeBehavior: "stack-replace", isNestedNavigator: true },
|
||||
});
|
||||
// @ts-ignore Missing hrefAttrs type definition.
|
||||
linkProps.hrefAttrs = { ...linkProps.hrefAttrs, target };
|
||||
return (
|
||||
<PressableFeedback
|
||||
{...linkProps}
|
||||
{...props}
|
||||
onPress={(e?: any) => {
|
||||
props?.onPress?.(e);
|
||||
if (e?.defaultPrevented) return;
|
||||
if (Platform.OS !== "web" && href?.includes("://")) Linking.openURL(href);
|
||||
else linkProps.onPress(e);
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</PressableFeedback>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,230 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Portal } from "@gorhom/portal";
|
||||
import Check from "@material-symbols/svg-400/rounded/check-fill.svg";
|
||||
import Close from "@material-symbols/svg-400/rounded/close-fill.svg";
|
||||
import { ScrollView } from "moti";
|
||||
import {
|
||||
type ComponentType,
|
||||
type ReactElement,
|
||||
type ReactNode,
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { Pressable, StyleSheet, View } from "react-native";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import type { SvgProps } from "react-native-svg";
|
||||
import { useRouter } from "solito/router";
|
||||
import { percent, px, sm, useYoshiki, vh, xl } from "yoshiki/native";
|
||||
import { Icon, IconButton } from "./icons";
|
||||
import { PressableFeedback } from "./links";
|
||||
import { P } from "./text";
|
||||
import { ContrastArea, SwitchVariant } from "./themes";
|
||||
import { ts } from "./utils";
|
||||
|
||||
const MenuContext = createContext<((open: boolean) => void) | undefined>(undefined);
|
||||
|
||||
type Optional<T, K extends keyof any> = Omit<T, K> & Partial<T>;
|
||||
|
||||
const Menu = <AsProps,>({
|
||||
Trigger,
|
||||
onMenuOpen,
|
||||
onMenuClose,
|
||||
children,
|
||||
isOpen: outerOpen,
|
||||
setOpen: outerSetOpen,
|
||||
...props
|
||||
}: {
|
||||
Trigger: ComponentType<AsProps>;
|
||||
children?: ReactNode | ReactNode[] | null;
|
||||
onMenuOpen?: () => void;
|
||||
onMenuClose?: () => void;
|
||||
isOpen?: boolean;
|
||||
setOpen?: (v: boolean) => void;
|
||||
} & Optional<AsProps, "onPress">) => {
|
||||
const insets = useSafeAreaInsets();
|
||||
const alreadyRendered = useRef(false);
|
||||
const [isOpen, setOpen] =
|
||||
outerOpen !== undefined && outerSetOpen ? [outerOpen, outerSetOpen] : useState(false);
|
||||
|
||||
// does the same as a useMemo but for props.
|
||||
const memoRef = useRef({ onMenuOpen, onMenuClose });
|
||||
memoRef.current = { onMenuOpen, onMenuClose };
|
||||
useEffect(() => {
|
||||
if (isOpen) memoRef.current.onMenuOpen?.();
|
||||
else if (alreadyRendered.current) memoRef.current.onMenuClose?.();
|
||||
alreadyRendered.current = true;
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Trigger
|
||||
onPress={() => {
|
||||
setOpen(true);
|
||||
}}
|
||||
{...(props as any)}
|
||||
/>
|
||||
{isOpen && (
|
||||
<Portal>
|
||||
<ContrastArea mode="user">
|
||||
<SwitchVariant>
|
||||
{({ css, theme }) => (
|
||||
<MenuContext.Provider value={setOpen}>
|
||||
<Pressable
|
||||
onPress={() => setOpen(false)}
|
||||
tabIndex={-1}
|
||||
{...css({ ...StyleSheet.absoluteFillObject, flexGrow: 1, bg: "transparent" })}
|
||||
/>
|
||||
<View
|
||||
{...css([
|
||||
{
|
||||
bg: (theme) => theme.background,
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
width: percent(100),
|
||||
maxHeight: vh(80),
|
||||
alignSelf: "center",
|
||||
borderTopLeftRadius: px(26),
|
||||
borderTopRightRadius: { xs: px(26), xl: 0 },
|
||||
paddingTop: { xs: px(26), xl: 0 },
|
||||
marginTop: { xs: px(72), xl: 0 },
|
||||
paddingBottom: insets.bottom,
|
||||
},
|
||||
sm({
|
||||
maxWidth: px(640),
|
||||
marginHorizontal: px(56),
|
||||
}),
|
||||
xl({
|
||||
top: 0,
|
||||
right: 0,
|
||||
marginRight: 0,
|
||||
borderBottomLeftRadius: px(26),
|
||||
}),
|
||||
])}
|
||||
>
|
||||
<ScrollView>
|
||||
<IconButton
|
||||
icon={Close}
|
||||
color={theme.colors.black}
|
||||
onPress={() => setOpen(false)}
|
||||
{...css({ alignSelf: "flex-end", display: { xs: "none", xl: "flex" } })}
|
||||
/>
|
||||
{children}
|
||||
</ScrollView>
|
||||
</View>
|
||||
</MenuContext.Provider>
|
||||
)}
|
||||
</SwitchVariant>
|
||||
</ContrastArea>
|
||||
</Portal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const MenuItem = ({
|
||||
label,
|
||||
selected,
|
||||
left,
|
||||
onSelect,
|
||||
href,
|
||||
icon,
|
||||
disabled,
|
||||
...props
|
||||
}: {
|
||||
label: string;
|
||||
selected?: boolean;
|
||||
left?: ReactElement;
|
||||
disabled?: boolean;
|
||||
icon?: ComponentType<SvgProps>;
|
||||
} & ({ onSelect: () => void; href?: undefined } | { href: string; onSelect?: undefined })) => {
|
||||
const { css, theme } = useYoshiki();
|
||||
const setOpen = useContext(MenuContext);
|
||||
const router = useRouter();
|
||||
|
||||
const icn = (icon || selected) && (
|
||||
<Icon
|
||||
icon={icon ?? Check}
|
||||
color={disabled ? theme.overlay0 : theme.paragraph}
|
||||
size={24}
|
||||
{...css({ paddingX: ts(1) })}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<PressableFeedback
|
||||
onPress={() => {
|
||||
setOpen?.call(null, false);
|
||||
onSelect?.call(null);
|
||||
if (href) router.push(href);
|
||||
}}
|
||||
disabled={disabled}
|
||||
{...css(
|
||||
{
|
||||
paddingHorizontal: ts(2),
|
||||
width: percent(100),
|
||||
height: ts(5),
|
||||
alignItems: "center",
|
||||
flexDirection: "row",
|
||||
},
|
||||
props as any,
|
||||
)}
|
||||
>
|
||||
{left && left}
|
||||
{!left && icn && icn}
|
||||
<P
|
||||
{...css([
|
||||
{ paddingLeft: ts(2) + +!(icon || selected || left) * px(24), flexGrow: 1 },
|
||||
disabled && { color: theme.overlay0 },
|
||||
])}
|
||||
>
|
||||
{label}
|
||||
</P>
|
||||
{left && icn && icn}
|
||||
</PressableFeedback>
|
||||
);
|
||||
};
|
||||
Menu.Item = MenuItem;
|
||||
|
||||
const Sub = <AsProps,>({
|
||||
children,
|
||||
...props
|
||||
}: {
|
||||
label: string;
|
||||
selected?: boolean;
|
||||
left?: ReactElement;
|
||||
disabled?: boolean;
|
||||
icon?: ComponentType<SvgProps>;
|
||||
children?: ReactNode | ReactNode[] | null;
|
||||
} & AsProps) => {
|
||||
const setOpen = useContext(MenuContext);
|
||||
return (
|
||||
<Menu Trigger={MenuItem} onMenuClose={() => setOpen?.(false)} {...props}>
|
||||
{children}
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
Menu.Sub = Sub;
|
||||
|
||||
export { Menu };
|
||||
@@ -0,0 +1,246 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Dot from "@material-symbols/svg-400/rounded/fiber_manual_record-fill.svg";
|
||||
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
type ComponentProps,
|
||||
type ComponentType,
|
||||
type ReactElement,
|
||||
type ReactNode,
|
||||
forwardRef,
|
||||
} from "react";
|
||||
import type { PressableProps } from "react-native";
|
||||
import type { SvgProps } from "react-native-svg";
|
||||
import { useYoshiki as useNativeYoshiki } from "yoshiki/native";
|
||||
import { useYoshiki } from "yoshiki/web";
|
||||
import { Icon } from "./icons";
|
||||
import { P } from "./text";
|
||||
import { ContrastArea, SwitchVariant } from "./themes";
|
||||
import { focusReset, ts } from "./utils";
|
||||
|
||||
type YoshikiFunc<T> = (props: ReturnType<typeof useYoshiki>) => T;
|
||||
export const YoshikiProvider = ({ children }: { children: YoshikiFunc<ReactNode> }) => {
|
||||
const yoshiki = useYoshiki();
|
||||
return <>{children(yoshiki)}</>;
|
||||
};
|
||||
export const InternalTriger = forwardRef<unknown, any>(function _Triger(
|
||||
{ Component, ComponentProps, ...props },
|
||||
ref,
|
||||
) {
|
||||
return (
|
||||
<Component ref={ref} {...ComponentProps} {...props} onClickCapture={props.onPointerDown} />
|
||||
);
|
||||
});
|
||||
|
||||
const Menu = <AsProps extends { onPress: PressableProps["onPress"] }>({
|
||||
Trigger,
|
||||
onMenuOpen,
|
||||
onMenuClose,
|
||||
children,
|
||||
isOpen,
|
||||
setOpen,
|
||||
...props
|
||||
}: {
|
||||
Trigger: ComponentType<AsProps>;
|
||||
children: ReactNode | ReactNode[] | null;
|
||||
onMenuOpen?: () => void;
|
||||
onMenuClose?: () => void;
|
||||
isOpen?: boolean;
|
||||
setOpen?: (v: boolean) => void;
|
||||
} & Omit<AsProps, "onPress">) => {
|
||||
return (
|
||||
<DropdownMenu.Root
|
||||
modal
|
||||
open={isOpen}
|
||||
onOpenChange={(newOpen) => {
|
||||
if (setOpen) setOpen(newOpen);
|
||||
if (newOpen) onMenuOpen?.call(null);
|
||||
else onMenuClose?.call(null);
|
||||
}}
|
||||
>
|
||||
<DropdownMenu.Trigger asChild>
|
||||
<InternalTriger Component={Trigger} ComponentProps={props} />
|
||||
</DropdownMenu.Trigger>
|
||||
<ContrastArea mode="user">
|
||||
<SwitchVariant>
|
||||
<YoshikiProvider>
|
||||
{({ css, theme }) => (
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content
|
||||
onFocusOutside={(e) => e.stopImmediatePropagation()}
|
||||
{...css({
|
||||
bg: (theme) => theme.background,
|
||||
overflow: "auto",
|
||||
minWidth: "220px",
|
||||
borderRadius: "8px",
|
||||
boxShadow:
|
||||
"0px 10px 38px -10px rgba(22, 23, 24, 0.35), 0px 10px 20px -15px rgba(22, 23, 24, 0.2)",
|
||||
zIndex: 2,
|
||||
maxHeight: "calc(var(--radix-dropdown-menu-content-available-height) * 0.8)",
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
<DropdownMenu.Arrow fill={theme.background} />
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Portal>
|
||||
)}
|
||||
</YoshikiProvider>
|
||||
</SwitchVariant>
|
||||
</ContrastArea>
|
||||
</DropdownMenu.Root>
|
||||
);
|
||||
};
|
||||
|
||||
const Item = forwardRef<
|
||||
HTMLDivElement,
|
||||
ComponentProps<typeof DropdownMenu.Item> & { href?: string }
|
||||
>(function _Item({ children, href, onSelect, ...props }, ref) {
|
||||
if (href) {
|
||||
return (
|
||||
<DropdownMenu.Item ref={ref} onSelect={onSelect} {...props} asChild>
|
||||
<Link href={href} style={{ textDecoration: "none" }}>
|
||||
{children}
|
||||
</Link>
|
||||
</DropdownMenu.Item>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<DropdownMenu.Item ref={ref} onSelect={onSelect} {...props}>
|
||||
{children}
|
||||
</DropdownMenu.Item>
|
||||
);
|
||||
});
|
||||
|
||||
const MenuItem = forwardRef<
|
||||
HTMLDivElement,
|
||||
{
|
||||
label: string;
|
||||
icon?: ComponentType<SvgProps>;
|
||||
left?: ReactElement;
|
||||
disabled?: boolean;
|
||||
selected?: boolean;
|
||||
} & ({ onSelect: () => void; href?: undefined } | { href: string; onSelect?: undefined })
|
||||
>(function MenuItem({ label, icon, left, selected, onSelect, href, disabled, ...props }, ref) {
|
||||
const { css: nCss } = useNativeYoshiki();
|
||||
const { css, theme } = useYoshiki();
|
||||
|
||||
const icn = (icon || selected) && (
|
||||
<Icon
|
||||
icon={icon ?? Dot}
|
||||
color={disabled ? theme.overlay0 : theme.paragraph}
|
||||
size={icon ? 24 : ts(1)}
|
||||
{...nCss({ paddingX: ts(1) })}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<style jsx global>{`
|
||||
[data-highlighted] {
|
||||
background: ${theme.variant.accent};
|
||||
svg {
|
||||
fill: ${theme.alternate.contrast};
|
||||
}
|
||||
div {
|
||||
color: ${theme.alternate.contrast};
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
<Item
|
||||
ref={ref}
|
||||
onSelect={onSelect}
|
||||
href={href}
|
||||
disabled={disabled}
|
||||
{...css(
|
||||
{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
padding: "8px",
|
||||
height: "32px",
|
||||
...focusReset,
|
||||
},
|
||||
props as any,
|
||||
)}
|
||||
>
|
||||
{left && left}
|
||||
{!left && icn && icn}
|
||||
<P
|
||||
{...nCss([
|
||||
{ paddingLeft: 8 * 2 + +!(icon || selected || left) * 24, flexGrow: 1 },
|
||||
disabled && {
|
||||
color: theme.overlay0,
|
||||
},
|
||||
])}
|
||||
>
|
||||
{label}
|
||||
</P>
|
||||
|
||||
{left && icn && icn}
|
||||
</Item>
|
||||
</>
|
||||
);
|
||||
});
|
||||
Menu.Item = MenuItem;
|
||||
|
||||
const Sub = <AsProps,>({
|
||||
children,
|
||||
disabled,
|
||||
...props
|
||||
}: {
|
||||
label: string;
|
||||
selected?: boolean;
|
||||
left?: ReactElement;
|
||||
disabled?: boolean;
|
||||
icon?: ComponentType<SvgProps>;
|
||||
children: ReactNode | ReactNode[] | null;
|
||||
} & AsProps) => {
|
||||
const { css, theme } = useYoshiki();
|
||||
|
||||
return (
|
||||
<DropdownMenu.Sub>
|
||||
<DropdownMenu.SubTrigger asChild disabled={disabled}>
|
||||
<MenuItem disabled={disabled} {...props} onSelect={(e?: any) => e.preventDefault()} />
|
||||
</DropdownMenu.SubTrigger>
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.SubContent
|
||||
onFocusOutside={(e) => e.stopImmediatePropagation()}
|
||||
{...css({
|
||||
bg: (theme) => theme.background,
|
||||
overflow: "auto",
|
||||
minWidth: "220px",
|
||||
borderRadius: "8px",
|
||||
boxShadow:
|
||||
"0px 10px 38px -10px rgba(22, 23, 24, 0.35), 0px 10px 20px -15px rgba(22, 23, 24, 0.2)",
|
||||
zIndex: 2,
|
||||
maxHeight: "calc(var(--radix-dropdown-menu-content-available-height) * 0.8)",
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
<DropdownMenu.Arrow fill={theme.background} />
|
||||
</DropdownMenu.SubContent>
|
||||
</DropdownMenu.Portal>
|
||||
</DropdownMenu.Sub>
|
||||
);
|
||||
};
|
||||
Menu.Sub = Sub;
|
||||
|
||||
export { Menu };
|
||||
@@ -0,0 +1,91 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { usePortal } from "@gorhom/portal";
|
||||
import { type ReactNode, useCallback, useEffect, useState } from "react";
|
||||
import { ScrollView, View } from "react-native";
|
||||
import { px, vh } from "yoshiki/native";
|
||||
import { imageBorderRadius } from "./constants";
|
||||
import { Container } from "./container";
|
||||
import { ContrastArea, SwitchVariant, type YoshikiFunc } from "./themes";
|
||||
import { ts } from "./utils";
|
||||
|
||||
export const Popup = ({ children, ...props }: { children: ReactNode | YoshikiFunc<ReactNode> }) => {
|
||||
return (
|
||||
<ContrastArea mode="user">
|
||||
<SwitchVariant>
|
||||
{({ css, theme }) => (
|
||||
<View
|
||||
{...css({
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
bg: (theme) => theme.themeOverlay,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
})}
|
||||
>
|
||||
<Container
|
||||
{...css(
|
||||
{
|
||||
borderRadius: px(imageBorderRadius),
|
||||
paddingHorizontal: 0,
|
||||
bg: (theme) => theme.background,
|
||||
overflow: "hidden",
|
||||
},
|
||||
props,
|
||||
)}
|
||||
>
|
||||
<ScrollView
|
||||
contentContainerStyle={{
|
||||
paddingHorizontal: px(15),
|
||||
paddingVertical: ts(4),
|
||||
gap: ts(2),
|
||||
}}
|
||||
{...css({
|
||||
maxHeight: vh(95),
|
||||
flexGrow: 0,
|
||||
flexShrink: 1,
|
||||
})}
|
||||
>
|
||||
{typeof children === "function" ? children({ css, theme }) : children}
|
||||
</ScrollView>
|
||||
</Container>
|
||||
</View>
|
||||
)}
|
||||
</SwitchVariant>
|
||||
</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] as const;
|
||||
};
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { ActivityIndicator, Platform, View } from "react-native";
|
||||
import { Circle, Svg } from "react-native-svg";
|
||||
import { type Stylable, px, useYoshiki } from "yoshiki/native";
|
||||
|
||||
export const CircularProgress = ({
|
||||
size = 48,
|
||||
tickness = 5,
|
||||
color,
|
||||
...props
|
||||
}: { size?: number; tickness?: number; color?: string } & Stylable) => {
|
||||
const { css, theme } = useYoshiki();
|
||||
|
||||
if (Platform.OS !== "web")
|
||||
return <ActivityIndicator size={size} color={color ?? theme.accent} {...props} />;
|
||||
|
||||
return (
|
||||
<View {...css({ width: size, height: size, overflow: "hidden" }, props)}>
|
||||
<style jsx global>{`
|
||||
@keyframes circularProgress-svg {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@keyframes circularProgress-circle {
|
||||
0% {
|
||||
stroke-dasharray: 1px, 200px;
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
50% {
|
||||
stroke-dasharray: 100px, 200px;
|
||||
stroke-dashoffset: -15px;
|
||||
}
|
||||
100% {
|
||||
stroke-dasharray: 100px, 200px;
|
||||
stroke-dashoffset: -125px;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
<Svg
|
||||
viewBox={`${size / 2} ${size / 2} ${size} ${size}`}
|
||||
{...css(
|
||||
// @ts-ignore Web only
|
||||
Platform.OS === "web" && { animation: "circularProgress-svg 1.4s ease-in-out infinite" },
|
||||
)}
|
||||
>
|
||||
<Circle
|
||||
cx={size}
|
||||
cy={size}
|
||||
r={(size - tickness) / 2}
|
||||
strokeWidth={tickness}
|
||||
fill="none"
|
||||
stroke={color ?? theme.accent}
|
||||
strokeDasharray={[px(80), px(200)]}
|
||||
{...css(
|
||||
Platform.OS === "web" && {
|
||||
// @ts-ignore Web only
|
||||
animation: "circularProgress-circle 1.4s ease-in-out infinite",
|
||||
},
|
||||
)}
|
||||
/>
|
||||
</Svg>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import ExpandMore from "@material-symbols/svg-400/rounded/keyboard_arrow_down-fill.svg";
|
||||
import { Button } from "./button";
|
||||
import { Icon } from "./icons";
|
||||
import { Menu } from "./menu";
|
||||
|
||||
export const Select = <Value extends string>({
|
||||
label,
|
||||
value,
|
||||
onValueChange,
|
||||
values,
|
||||
getLabel,
|
||||
}: {
|
||||
label: string;
|
||||
value: Value;
|
||||
onValueChange: (v: Value) => void;
|
||||
values: Value[];
|
||||
getLabel: (key: Value) => string;
|
||||
}) => {
|
||||
return (
|
||||
<Menu Trigger={Button} text={getLabel(value)} icon={<Icon icon={ExpandMore} />}>
|
||||
{values.map((x) => (
|
||||
<Menu.Item
|
||||
key={x}
|
||||
label={getLabel(x)}
|
||||
selected={x === value}
|
||||
onSelect={() => onValueChange(x)}
|
||||
/>
|
||||
))}
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import Check from "@material-symbols/svg-400/rounded/check-fill.svg";
|
||||
import ExpandMore from "@material-symbols/svg-400/rounded/keyboard_arrow_down-fill.svg";
|
||||
import ExpandLess from "@material-symbols/svg-400/rounded/keyboard_arrow_up-fill.svg";
|
||||
import * as RSelect from "@radix-ui/react-select";
|
||||
import { forwardRef } from "react";
|
||||
import { View } from "react-native";
|
||||
import { useYoshiki } from "yoshiki";
|
||||
import { type Theme, px, useYoshiki as useNativeYoshiki } from "yoshiki/native";
|
||||
import { Icon } from "./icons";
|
||||
import { PressableFeedback } from "./links";
|
||||
import { InternalTriger, YoshikiProvider } from "./menu.web";
|
||||
import { P } from "./text";
|
||||
import { ContrastArea, SwitchVariant } from "./themes";
|
||||
import { focusReset, ts } from "./utils";
|
||||
|
||||
export const Select = ({
|
||||
label,
|
||||
value,
|
||||
onValueChange,
|
||||
values,
|
||||
getLabel,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
onValueChange: (v: string) => void;
|
||||
values: string[];
|
||||
getLabel: (key: string) => string;
|
||||
}) => {
|
||||
const { css: wCss } = useYoshiki();
|
||||
const { css } = useNativeYoshiki();
|
||||
|
||||
return (
|
||||
<RSelect.Root value={value} onValueChange={onValueChange}>
|
||||
<RSelect.Trigger aria-label={label} asChild>
|
||||
<InternalTriger
|
||||
Component={PressableFeedback}
|
||||
ComponentProps={css({
|
||||
flexGrow: 0,
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
overflow: "hidden",
|
||||
p: ts(0.5),
|
||||
borderRadius: ts(5),
|
||||
borderColor: (theme) => theme.accent,
|
||||
borderWidth: ts(0.5),
|
||||
fover: {
|
||||
self: { bg: (theme: Theme) => theme.accent },
|
||||
text: { color: (theme: Theme) => theme.colors.white },
|
||||
},
|
||||
})}
|
||||
>
|
||||
<View
|
||||
{...css({
|
||||
paddingX: ts(3),
|
||||
flexDirection: "row",
|
||||
alignItems: "center",
|
||||
})}
|
||||
>
|
||||
<P {...css({ textAlign: "center" }, "text")}>{<RSelect.Value />}</P>
|
||||
<RSelect.Icon {...wCss({ display: "flex", justifyContent: "center" })}>
|
||||
<Icon icon={ExpandMore} />
|
||||
</RSelect.Icon>
|
||||
</View>
|
||||
</InternalTriger>
|
||||
</RSelect.Trigger>
|
||||
<ContrastArea mode="user">
|
||||
<SwitchVariant>
|
||||
<YoshikiProvider>
|
||||
{({ css }) => (
|
||||
<RSelect.Portal>
|
||||
<RSelect.Content
|
||||
{...css({
|
||||
bg: (theme) => theme.background,
|
||||
overflow: "auto",
|
||||
minWidth: "220px",
|
||||
borderRadius: "8px",
|
||||
boxShadow:
|
||||
"0px 10px 38px -10px rgba(22, 23, 24, 0.35), 0px 10px 20px -15px rgba(22, 23, 24, 0.2)",
|
||||
zIndex: 2,
|
||||
maxHeight: "calc(var(--radix-dropdown-menu-content-available-height) * 0.8)",
|
||||
})}
|
||||
>
|
||||
<RSelect.ScrollUpButton>
|
||||
<Icon icon={ExpandLess} />
|
||||
</RSelect.ScrollUpButton>
|
||||
<RSelect.Viewport>
|
||||
{values.map((x) => (
|
||||
<Item key={x} label={getLabel(x)} value={x} />
|
||||
))}
|
||||
</RSelect.Viewport>
|
||||
<RSelect.ScrollDownButton>
|
||||
<Icon icon={ExpandMore} />
|
||||
</RSelect.ScrollDownButton>
|
||||
</RSelect.Content>
|
||||
</RSelect.Portal>
|
||||
)}
|
||||
</YoshikiProvider>
|
||||
</SwitchVariant>
|
||||
</ContrastArea>
|
||||
</RSelect.Root>
|
||||
);
|
||||
};
|
||||
|
||||
const Item = forwardRef<HTMLDivElement, { label: string; value: string }>(function Item(
|
||||
{ label, value, ...props },
|
||||
ref,
|
||||
) {
|
||||
const { css, theme } = useYoshiki();
|
||||
const { css: nCss } = useNativeYoshiki();
|
||||
return (
|
||||
<>
|
||||
<style jsx global>{`
|
||||
[data-highlighted] {
|
||||
background: ${theme.variant.accent};
|
||||
}
|
||||
`}</style>
|
||||
<RSelect.Item
|
||||
ref={ref}
|
||||
value={value}
|
||||
{...css(
|
||||
{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
paddingTop: "8px",
|
||||
paddingBottom: "8px",
|
||||
paddingLeft: "35px",
|
||||
paddingRight: "25px",
|
||||
height: "32px",
|
||||
color: (theme) => theme.paragraph,
|
||||
borderRadius: "4px",
|
||||
position: "relative",
|
||||
userSelect: "none",
|
||||
...focusReset,
|
||||
},
|
||||
props as any,
|
||||
)}
|
||||
>
|
||||
<RSelect.ItemText {...css({ color: (theme) => theme.paragraph })}>{label}</RSelect.ItemText>
|
||||
<RSelect.ItemIndicator asChild>
|
||||
<InternalTriger
|
||||
Component={Icon}
|
||||
icon={Check}
|
||||
ComponentProps={nCss({
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
width: px(25),
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
})}
|
||||
/>
|
||||
</RSelect.ItemIndicator>
|
||||
</RSelect.Item>
|
||||
</>
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { LinearGradient as LG } from "expo-linear-gradient";
|
||||
import { useEffect } from "react";
|
||||
import { StyleSheet, View, type ViewProps } from "react-native";
|
||||
import Animated, {
|
||||
useAnimatedStyle,
|
||||
useSharedValue,
|
||||
withDelay,
|
||||
withRepeat,
|
||||
withTiming,
|
||||
} from "react-native-reanimated";
|
||||
import { em, percent, px, rem, useYoshiki } from "yoshiki/native";
|
||||
|
||||
const LinearGradient = Animated.createAnimatedComponent(LG);
|
||||
|
||||
export const SkeletonCss = () => (
|
||||
<style jsx global>{`
|
||||
@keyframes skeleton {
|
||||
0% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
50% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
);
|
||||
|
||||
export const Skeleton = ({
|
||||
children,
|
||||
show: forcedShow,
|
||||
lines = 1,
|
||||
variant = "text",
|
||||
...props
|
||||
}: Omit<ViewProps, "children"> & {
|
||||
children?: JSX.Element | JSX.Element[] | boolean | null;
|
||||
show?: boolean;
|
||||
lines?: number;
|
||||
variant?: "text" | "header" | "round" | "custom" | "fill" | "filltext";
|
||||
}) => {
|
||||
const { css, theme } = useYoshiki();
|
||||
const width = useSharedValue(-900);
|
||||
const mult = useSharedValue(-1);
|
||||
const animated = useAnimatedStyle(() => ({
|
||||
transform: [
|
||||
{
|
||||
translateX: width.value * mult.value,
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
useEffect(() => {
|
||||
mult.value = withRepeat(withDelay(800, withTiming(1, { duration: 800 })), 0);
|
||||
});
|
||||
|
||||
if (forcedShow === undefined && children && children !== true) return <>{children}</>;
|
||||
|
||||
return (
|
||||
<View
|
||||
{...css(
|
||||
[
|
||||
{
|
||||
position: "relative",
|
||||
},
|
||||
lines === 1 && { overflow: "hidden", borderRadius: px(6) },
|
||||
(variant === "text" || variant === "header") &&
|
||||
lines === 1 && [
|
||||
{
|
||||
width: percent(75),
|
||||
height: rem(1.2),
|
||||
margin: px(2),
|
||||
},
|
||||
variant === "text" && {
|
||||
margin: px(2),
|
||||
},
|
||||
variant === "header" && {
|
||||
marginBottom: rem(0.5),
|
||||
},
|
||||
],
|
||||
|
||||
variant === "round" && {
|
||||
borderRadius: 9999999,
|
||||
},
|
||||
variant === "fill" && {
|
||||
width: percent(100),
|
||||
height: percent(100),
|
||||
},
|
||||
variant === "filltext" && {
|
||||
width: percent(100),
|
||||
height: em(1),
|
||||
},
|
||||
],
|
||||
props,
|
||||
)}
|
||||
>
|
||||
{(forcedShow || !children || children === true) &&
|
||||
[...Array(lines)].map((_, i) => (
|
||||
<View
|
||||
key={`skeleton_${i}`}
|
||||
onLayout={(e) => {
|
||||
width.value = e.nativeEvent.layout.width;
|
||||
}}
|
||||
{...css([
|
||||
{
|
||||
bg: (theme) => theme.overlay0,
|
||||
},
|
||||
lines === 1 && {
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
},
|
||||
lines !== 1 && {
|
||||
width: i === lines - 1 ? percent(40) : percent(100),
|
||||
height: rem(1.2),
|
||||
marginBottom: rem(0.5),
|
||||
overflow: "hidden",
|
||||
borderRadius: px(6),
|
||||
},
|
||||
])}
|
||||
>
|
||||
<LinearGradient
|
||||
start={{ x: 0, y: 0.5 }}
|
||||
end={{ x: 1, y: 0.5 }}
|
||||
colors={["transparent", theme.overlay1, "transparent"]}
|
||||
style={[
|
||||
StyleSheet.absoluteFillObject,
|
||||
{ transform: [{ translateX: -width.value }] },
|
||||
animated,
|
||||
]}
|
||||
/>
|
||||
</View>
|
||||
))}
|
||||
{children}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { LinearGradient } from "expo-linear-gradient";
|
||||
import { View, type ViewProps } from "react-native";
|
||||
import { em, percent, px, rem, useYoshiki } from "yoshiki/native";
|
||||
|
||||
export const SkeletonCss = () => (
|
||||
<style jsx global>{`
|
||||
@keyframes skeleton {
|
||||
0% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
50% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
);
|
||||
|
||||
export const Skeleton = ({
|
||||
children,
|
||||
show: forcedShow,
|
||||
lines = 1,
|
||||
variant = "text",
|
||||
...props
|
||||
}: Omit<ViewProps, "children"> & {
|
||||
children?: JSX.Element | JSX.Element[] | boolean | null;
|
||||
show?: boolean;
|
||||
lines?: number;
|
||||
variant?: "text" | "header" | "round" | "custom" | "fill" | "filltext";
|
||||
}) => {
|
||||
const { css, theme } = useYoshiki();
|
||||
|
||||
if (forcedShow === undefined && children && children !== true) return <>{children}</>;
|
||||
|
||||
return (
|
||||
<View
|
||||
{...css(
|
||||
[
|
||||
lines === 1 && { overflow: "hidden", borderRadius: px(6) },
|
||||
(variant === "text" || variant === "header") &&
|
||||
lines === 1 && [
|
||||
{
|
||||
width: percent(75),
|
||||
height: rem(1.2),
|
||||
margin: px(2),
|
||||
},
|
||||
variant === "text" && {
|
||||
margin: px(2),
|
||||
},
|
||||
variant === "header" && {
|
||||
marginBottom: rem(0.5),
|
||||
},
|
||||
],
|
||||
|
||||
variant === "round" && {
|
||||
borderRadius: 9999999,
|
||||
},
|
||||
variant === "fill" && {
|
||||
width: percent(100),
|
||||
height: percent(100),
|
||||
},
|
||||
variant === "filltext" && {
|
||||
width: percent(100),
|
||||
height: em(1),
|
||||
},
|
||||
],
|
||||
props,
|
||||
)}
|
||||
>
|
||||
{(forcedShow || !children || children === true) &&
|
||||
[...Array(lines)].map((_, i) => (
|
||||
<View
|
||||
key={`skeleton_${i}`}
|
||||
{...css([
|
||||
{
|
||||
bg: (theme) => theme.overlay0,
|
||||
},
|
||||
lines === 1 && {
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
},
|
||||
lines !== 1 && {
|
||||
width: i === lines - 1 ? percent(40) : percent(100),
|
||||
height: rem(1.2),
|
||||
marginBottom: rem(0.5),
|
||||
overflow: "hidden",
|
||||
borderRadius: px(6),
|
||||
},
|
||||
])}
|
||||
>
|
||||
<LinearGradient
|
||||
start={{ x: 0, y: 0.5 }}
|
||||
end={{ x: 1, y: 0.5 }}
|
||||
colors={["transparent", theme.overlay1, "transparent"]}
|
||||
{...css([
|
||||
{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
// @ts-ignore Web only properties
|
||||
animation: "skeleton 1.6s linear 0.5s infinite",
|
||||
transform: "translateX(-100%)",
|
||||
},
|
||||
])}
|
||||
/>
|
||||
</View>
|
||||
))}
|
||||
{children}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,215 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useRef, useState } from "react";
|
||||
import { type GestureResponderEvent, Platform, View } from "react-native";
|
||||
import type { ViewProps } from "react-native-svg/lib/typescript/fabric/utils";
|
||||
import { Stylable, percent, px, useYoshiki } from "yoshiki/native";
|
||||
import { focusReset } from "./utils";
|
||||
|
||||
export const Slider = ({
|
||||
progress,
|
||||
subtleProgress,
|
||||
max = 100,
|
||||
markers,
|
||||
setProgress,
|
||||
startSeek,
|
||||
endSeek,
|
||||
onHover,
|
||||
size = 6,
|
||||
...props
|
||||
}: {
|
||||
progress: number;
|
||||
max?: number;
|
||||
subtleProgress?: number;
|
||||
markers?: number[];
|
||||
setProgress: (progress: number) => void;
|
||||
startSeek?: () => void;
|
||||
endSeek?: () => void;
|
||||
onHover?: (
|
||||
position: number | null,
|
||||
layout: { x: number; y: number; width: number; height: number },
|
||||
) => void;
|
||||
size?: number;
|
||||
} & Partial<ViewProps>) => {
|
||||
const { css } = useYoshiki();
|
||||
const ref = useRef<View>(null);
|
||||
const [layout, setLayout] = useState({ x: 0, y: 0, width: 0, height: 0 });
|
||||
const [isSeeking, setSeek] = useState(false);
|
||||
const [isHover, setHover] = useState(false);
|
||||
const [isFocus, setFocus] = useState(false);
|
||||
const smallBar = !(isSeeking || isHover || isFocus);
|
||||
|
||||
const ts = (value: number) => px(value * size);
|
||||
|
||||
const change = (event: GestureResponderEvent) => {
|
||||
event.preventDefault();
|
||||
const locationX = Platform.select({
|
||||
android: event.nativeEvent.pageX - layout.x,
|
||||
default: event.nativeEvent.locationX,
|
||||
});
|
||||
setProgress(Math.max(0, Math.min(locationX / layout.width, 1)) * max);
|
||||
};
|
||||
|
||||
return (
|
||||
<View
|
||||
ref={ref}
|
||||
// @ts-ignore Web only
|
||||
onMouseEnter={() => setHover(true)}
|
||||
// @ts-ignore Web only
|
||||
onMouseLeave={() => {
|
||||
setHover(false);
|
||||
onHover?.(null, layout);
|
||||
}}
|
||||
// @ts-ignore Web only
|
||||
onMouseMove={(e) =>
|
||||
onHover?.(Math.max(0, Math.min((e.clientX - layout.x) / layout.width, 1) * max), layout)
|
||||
}
|
||||
tabIndex={0}
|
||||
onFocus={() => setFocus(true)}
|
||||
onBlur={() => setFocus(false)}
|
||||
onStartShouldSetResponder={() => true}
|
||||
onResponderGrant={() => {
|
||||
setSeek(true);
|
||||
startSeek?.call(null);
|
||||
}}
|
||||
onResponderRelease={() => {
|
||||
setSeek(false);
|
||||
endSeek?.call(null);
|
||||
}}
|
||||
onResponderStart={change}
|
||||
onResponderMove={change}
|
||||
onLayout={() =>
|
||||
ref.current?.measure((_, __, width, height, pageX, pageY) =>
|
||||
setLayout({ width, height, x: pageX, y: pageY }),
|
||||
)
|
||||
}
|
||||
onKeyDown={(e: KeyboardEvent) => {
|
||||
switch (e.code) {
|
||||
case "ArrowLeft":
|
||||
setProgress(Math.max(progress - 0.05 * max, 0));
|
||||
break;
|
||||
case "ArrowRight":
|
||||
setProgress(Math.min(progress + 0.05 * max, max));
|
||||
break;
|
||||
case "ArrowDown":
|
||||
setProgress(Math.max(progress - 0.1 * max, 0));
|
||||
break;
|
||||
case "ArrowUp":
|
||||
setProgress(Math.min(progress + 0.1 * max, max));
|
||||
break;
|
||||
}
|
||||
}}
|
||||
{...css(
|
||||
{
|
||||
paddingVertical: ts(1),
|
||||
// @ts-ignore Web only
|
||||
cursor: "pointer",
|
||||
...focusReset,
|
||||
},
|
||||
props,
|
||||
)}
|
||||
>
|
||||
<View
|
||||
{...css([
|
||||
{
|
||||
width: percent(100),
|
||||
height: ts(1),
|
||||
bg: (theme) => theme.overlay0,
|
||||
},
|
||||
smallBar && { transform: "scaleY(0.4)" as any },
|
||||
])}
|
||||
>
|
||||
{subtleProgress !== undefined && (
|
||||
<View
|
||||
{...css(
|
||||
{
|
||||
bg: (theme) => theme.overlay1,
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
},
|
||||
{
|
||||
style: {
|
||||
width: percent((subtleProgress / max) * 100),
|
||||
},
|
||||
},
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<View
|
||||
{...css(
|
||||
{
|
||||
bg: (theme) => theme.accent,
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
},
|
||||
{
|
||||
// In an inline style because yoshiki's insertion can not catch up with the constant redraw
|
||||
style: {
|
||||
width: percent((progress / max) * 100),
|
||||
},
|
||||
},
|
||||
)}
|
||||
/>
|
||||
{markers?.map((x) => (
|
||||
<View
|
||||
key={x}
|
||||
{...css({
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
left: percent(Math.min(100, (x / max) * 100)),
|
||||
bg: (theme) => theme.accent,
|
||||
width: ts(0.5),
|
||||
height: ts(1),
|
||||
})}
|
||||
/>
|
||||
))}
|
||||
</View>
|
||||
<View
|
||||
{...css(
|
||||
[
|
||||
{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
marginY: ts(0.5),
|
||||
bg: (theme) => theme.accent,
|
||||
width: ts(2),
|
||||
height: ts(2),
|
||||
borderRadius: ts(1),
|
||||
marginLeft: ts(-1),
|
||||
},
|
||||
smallBar && { opacity: 0 },
|
||||
],
|
||||
{
|
||||
style: {
|
||||
left: percent((progress / max) * 100),
|
||||
},
|
||||
},
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { usePortal } from "@gorhom/portal";
|
||||
import { type ReactElement, createContext, useCallback, useContext, useRef } from "react";
|
||||
import { View } from "react-native";
|
||||
import { percent, px } from "yoshiki/native";
|
||||
import { Button } from "./button";
|
||||
import { imageBorderRadius } from "./constants";
|
||||
import { P } from "./text";
|
||||
import { SwitchVariant } from "./themes";
|
||||
import { ts } from "./utils";
|
||||
|
||||
export type Snackbar = {
|
||||
key?: string;
|
||||
label: string;
|
||||
duration: number;
|
||||
actions?: Action[];
|
||||
};
|
||||
|
||||
export type Action = {
|
||||
label: string;
|
||||
icon: ReactElement;
|
||||
action: () => void;
|
||||
};
|
||||
|
||||
const SnackbarContext = createContext<(snackbar: Snackbar) => void>(null!);
|
||||
|
||||
export const SnackbarProvider = ({ children }: { children: ReactElement | ReactElement[] }) => {
|
||||
const { addPortal, removePortal } = usePortal();
|
||||
const snackbars = useRef<Snackbar[]>([]);
|
||||
const timeout = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const createSnackbar = useCallback(
|
||||
(snackbar: Snackbar) => {
|
||||
if (snackbar.key) snackbars.current = snackbars.current.filter((x) => snackbar.key !== x.key);
|
||||
snackbars.current.unshift(snackbar);
|
||||
|
||||
if (timeout.current) return;
|
||||
const updatePortal = () => {
|
||||
const top = snackbars.current.pop();
|
||||
if (!top) {
|
||||
timeout.current = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const { key, ...props } = top;
|
||||
addPortal("snackbar", <Snackbar key={key} {...props} />);
|
||||
timeout.current = setTimeout(() => {
|
||||
removePortal("snackbar");
|
||||
updatePortal();
|
||||
}, snackbar.duration * 1000);
|
||||
};
|
||||
updatePortal();
|
||||
},
|
||||
[addPortal, removePortal],
|
||||
);
|
||||
|
||||
return <SnackbarContext.Provider value={createSnackbar}>{children}</SnackbarContext.Provider>;
|
||||
};
|
||||
|
||||
export const useSnackbar = () => {
|
||||
return useContext(SnackbarContext);
|
||||
};
|
||||
|
||||
const Snackbar = ({ label, actions }: Snackbar) => {
|
||||
// TODO: take navbar height into account for setting the position of the snacbar.
|
||||
return (
|
||||
<SwitchVariant>
|
||||
{({ css }) => (
|
||||
<View
|
||||
{...css({
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: ts(4),
|
||||
})}
|
||||
>
|
||||
<View
|
||||
{...css({
|
||||
bg: (theme) => theme.background,
|
||||
maxWidth: { sm: percent(75), md: percent(45), lg: px(500) },
|
||||
margin: ts(1),
|
||||
padding: ts(1),
|
||||
flexDirection: "row",
|
||||
borderRadius: imageBorderRadius,
|
||||
})}
|
||||
>
|
||||
<P {...css({ flexGrow: 1, flexShrink: 1 })}>{label}</P>
|
||||
{actions?.map((x, i) => (
|
||||
<Button key={i} text={x.label} icon={x.icon} onPress={x.action} />
|
||||
))}
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
</SwitchVariant>
|
||||
);
|
||||
};
|
||||
Vendored
+26
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
declare module "*.svg" {
|
||||
import type React from "react";
|
||||
import type { SvgProps } from "react-native-svg";
|
||||
const content: React.FC<SvgProps>;
|
||||
export default content;
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import {
|
||||
H1 as EH1,
|
||||
H2 as EH2,
|
||||
H3 as EH3,
|
||||
H4 as EH4,
|
||||
H5 as EH5,
|
||||
H6 as EH6,
|
||||
P as EP,
|
||||
} from "@expo/html-elements";
|
||||
import type { ComponentProps, ComponentType } from "react";
|
||||
import { Platform, type StyleProp, Text, type TextProps, type TextStyle } from "react-native";
|
||||
import { percent, rem, useYoshiki } from "yoshiki/native";
|
||||
import { ts } from "./utils/spacing";
|
||||
|
||||
const styleText = (
|
||||
Component: ComponentType<ComponentProps<typeof EP>>,
|
||||
type?: "header" | "sub",
|
||||
custom?: TextStyle,
|
||||
) => {
|
||||
const Text = (
|
||||
props: Omit<ComponentProps<typeof EP>, "style"> & {
|
||||
style?: StyleProp<TextStyle>;
|
||||
children?: TextProps["children"];
|
||||
},
|
||||
) => {
|
||||
const { css, theme } = useYoshiki();
|
||||
|
||||
return (
|
||||
<Component
|
||||
{...css(
|
||||
[
|
||||
{
|
||||
marginVertical: rem(0.5),
|
||||
color: type === "header" ? theme.heading : theme.paragraph,
|
||||
flexShrink: 1,
|
||||
fontSize: rem(1),
|
||||
fontFamily: theme.font.normal,
|
||||
},
|
||||
type === "sub" && {
|
||||
fontFamily: theme.font["300"] ?? theme.font.normal,
|
||||
fontWeight: "300",
|
||||
opacity: 0.8,
|
||||
fontSize: rem(0.8),
|
||||
},
|
||||
custom?.fontWeight && {
|
||||
fontFamily: theme.font[custom.fontWeight] ?? theme.font.normal,
|
||||
},
|
||||
custom,
|
||||
],
|
||||
props as TextProps,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
return Text;
|
||||
};
|
||||
|
||||
export const H1 = styleText(EH1, "header", { fontSize: rem(3), fontWeight: "900" });
|
||||
export const H2 = styleText(EH2, "header", { fontSize: rem(2) });
|
||||
export const H3 = styleText(EH3, "header");
|
||||
export const H4 = styleText(EH4, "header");
|
||||
export const H5 = styleText(EH5, "header");
|
||||
export const H6 = styleText(EH6, "header");
|
||||
export const Heading = styleText(EP, "header");
|
||||
export const P = styleText(EP, undefined, { fontSize: rem(1) });
|
||||
export const SubP = styleText(EP, "sub");
|
||||
|
||||
export const LI = ({ children, ...props }: TextProps) => {
|
||||
const { css } = useYoshiki();
|
||||
|
||||
return (
|
||||
<P role={Platform.OS === "web" ? "listitem" : props.role} {...props}>
|
||||
<Text
|
||||
{...css({
|
||||
height: percent(100),
|
||||
marginBottom: rem(0.5),
|
||||
paddingRight: ts(1),
|
||||
})}
|
||||
>
|
||||
{String.fromCharCode(0x2022)}
|
||||
</Text>
|
||||
{children}
|
||||
</P>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export * from "./theme";
|
||||
export * from "./catppuccin";
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { ToastAndroid } from "react-native";
|
||||
|
||||
export const tooltip = (tooltip: string, up?: boolean) => ({
|
||||
onLongPress: () => {
|
||||
ToastAndroid.show(tooltip, ToastAndroid.SHORT);
|
||||
},
|
||||
});
|
||||
|
||||
import type { Tooltip as RTooltip } from "react-tooltip";
|
||||
export const Tooltip: typeof RTooltip = (() => null) as any;
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { forwardRef } from "react";
|
||||
import { Tooltip as RTooltip } from "react-tooltip";
|
||||
import type { Theme } from "yoshiki/native";
|
||||
import { ContrastArea } from "./themes";
|
||||
|
||||
export const tooltip = (tooltip: string, up?: boolean) => ({
|
||||
dataSet: {
|
||||
tooltipContent: tooltip,
|
||||
label: tooltip,
|
||||
tooltipPlace: up ? "top" : "bottom",
|
||||
tooltipId: "tooltip",
|
||||
},
|
||||
});
|
||||
|
||||
export const WebTooltip = ({ theme }: { theme: Theme }) => {
|
||||
return (
|
||||
<style jsx global>{`
|
||||
body {
|
||||
--rt-color-white: ${theme.alternate.contrast};
|
||||
--rt-color-dark: ${theme.user.contrast};
|
||||
--rt-opacity: 0.9;
|
||||
--rt-transition-show-delay: 0.15s;
|
||||
--rt-transition-closing-delay: 0.15s;
|
||||
}
|
||||
`}</style>
|
||||
);
|
||||
};
|
||||
|
||||
export const Tooltip = forwardRef(function Tooltip(props, ref) {
|
||||
return (
|
||||
<ContrastArea mode="alternate">
|
||||
<RTooltip {...props} ref={ref} />
|
||||
</ContrastArea>
|
||||
);
|
||||
}) as typeof RTooltip;
|
||||
Vendored
+57
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type React from "react";
|
||||
import "react-native";
|
||||
|
||||
declare module "react-native" {
|
||||
interface PressableStateCallbackType {
|
||||
hovered?: boolean;
|
||||
focused?: boolean;
|
||||
}
|
||||
interface AccessibilityProps {
|
||||
tabIndex?: number;
|
||||
}
|
||||
interface ViewStyle {
|
||||
transitionProperty?: string;
|
||||
transitionDuration?: string;
|
||||
}
|
||||
interface TextProps {
|
||||
hrefAttrs?: {
|
||||
rel?: "noreferrer";
|
||||
target?: string;
|
||||
};
|
||||
}
|
||||
interface ViewProps {
|
||||
dataSet?: Record<string, string>;
|
||||
hrefAttrs?: {
|
||||
rel: "noreferrer";
|
||||
target?: "_blank";
|
||||
};
|
||||
onClick?: (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => void;
|
||||
}
|
||||
}
|
||||
|
||||
declare module "react" {
|
||||
interface StyleHTMLAttributes<T> extends HTMLAttributes<T> {
|
||||
jsx?: boolean;
|
||||
global?: boolean;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useWindowDimensions } from "react-native";
|
||||
import { type Breakpoints as YoshikiBreakpoint, breakpoints, isBreakpoints } from "yoshiki/native";
|
||||
|
||||
type AtLeastOne<T, U = { [K in keyof T]: Pick<T, K> }> = Partial<T> & U[keyof U];
|
||||
export type Breakpoint<T> = T | AtLeastOne<YoshikiBreakpoint<T>>;
|
||||
|
||||
// copied from yoshiki.
|
||||
const useBreakpoint = () => {
|
||||
const { width } = useWindowDimensions();
|
||||
const idx = Object.values(breakpoints).findIndex((x) => width <= x);
|
||||
if (idx === -1) return 0;
|
||||
return idx - 1;
|
||||
};
|
||||
|
||||
const getBreakpointValue = <T>(value: Breakpoint<T>, breakpoint: number): T => {
|
||||
if (!isBreakpoints(value)) return value;
|
||||
const bpKeys = Object.keys(breakpoints) as Array<keyof YoshikiBreakpoint<T>>;
|
||||
for (let i = breakpoint; i >= 0; i--) {
|
||||
if (bpKeys[i] in value) {
|
||||
const val = value[bpKeys[i]];
|
||||
if (val) return val;
|
||||
}
|
||||
}
|
||||
// This should never be reached.
|
||||
return undefined!;
|
||||
};
|
||||
|
||||
export const useBreakpointValue = <T>(value: Breakpoint<T>): T => {
|
||||
const breakpoint = useBreakpoint();
|
||||
return getBreakpointValue(value, breakpoint);
|
||||
};
|
||||
|
||||
export const useBreakpointMap = <T extends Record<string, unknown>>(
|
||||
value: T,
|
||||
): { [key in keyof T]: T[key] extends Breakpoint<infer V> ? V : T } => {
|
||||
const breakpoint = useBreakpoint();
|
||||
// @ts-ignore
|
||||
return Object.fromEntries(
|
||||
Object.entries(value).map(([key, val]) => [key, getBreakpointValue(val, breakpoint)]),
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export const capitalize = (str: string): string => {
|
||||
return str
|
||||
.split(" ")
|
||||
.map((s) => s.trim())
|
||||
.map((s) => {
|
||||
if (s.length > 1) {
|
||||
return s.charAt(0).toUpperCase() + s.slice(1);
|
||||
}
|
||||
return s;
|
||||
})
|
||||
.join(" ");
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export const Head = ({
|
||||
title,
|
||||
description,
|
||||
image,
|
||||
}: {
|
||||
title?: string | null;
|
||||
description?: string | null;
|
||||
image?: string | null;
|
||||
}) => {
|
||||
return null;
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import NextHead from "next/head";
|
||||
|
||||
export const Head = ({
|
||||
title,
|
||||
description,
|
||||
image,
|
||||
}: {
|
||||
title?: string | null;
|
||||
description?: string | null;
|
||||
image?: string | null;
|
||||
}) => {
|
||||
return (
|
||||
<NextHead>
|
||||
{title && <title>{`${title} - Kyoo`}</title>}
|
||||
{description && <meta name="description" content={description} />}
|
||||
{image && <meta property="og:image" content={image} />}
|
||||
</NextHead>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export * from "./breakpoints";
|
||||
export * from "./nojs";
|
||||
export * from "./head";
|
||||
export * from "./spacing";
|
||||
export * from "./capitalize";
|
||||
export * from "./touchonly";
|
||||
export * from "./page-style";
|
||||
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import type { ViewProps } from "react-native";
|
||||
|
||||
export const hiddenIfNoJs: ViewProps = { style: { $$css: true, noJs: "noJsHidden" } as any };
|
||||
|
||||
export const HiddenIfNoJs = () => (
|
||||
<noscript>
|
||||
<style>
|
||||
{`
|
||||
.noJsHidden {
|
||||
display: none;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</noscript>
|
||||
);
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
|
||||
export const usePageStyle = () => {
|
||||
const insets = useSafeAreaInsets();
|
||||
return { paddingBottom: insets.bottom } as const;
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export const usePageStyle = () => {
|
||||
return {} as const;
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Platform } from "react-native";
|
||||
import { px } from "yoshiki/native";
|
||||
|
||||
export const important = <T,>(value: T): T => {
|
||||
return `${value} !important` as T;
|
||||
};
|
||||
|
||||
export const ts = (spacing: number) => {
|
||||
return px(spacing * 8);
|
||||
};
|
||||
|
||||
export const focusReset: object =
|
||||
Platform.OS === "web"
|
||||
? {
|
||||
boxShadow: "unset",
|
||||
outline: "none",
|
||||
}
|
||||
: {};
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Kyoo - A portable and vast media library solution.
|
||||
* Copyright (c) Kyoo.
|
||||
*
|
||||
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
||||
*
|
||||
* Kyoo is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* any later version.
|
||||
*
|
||||
* Kyoo is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Platform, type ViewProps } from "react-native";
|
||||
|
||||
export const TouchOnlyCss = () => {
|
||||
return (
|
||||
<style jsx global>{`
|
||||
:where(body.noHover) .noTouch {
|
||||
display: none;
|
||||
}
|
||||
:where(body:not(.noHover)) .touchOnly {
|
||||
display: none;
|
||||
}
|
||||
`}</style>
|
||||
);
|
||||
};
|
||||
|
||||
export const touchOnly: ViewProps = {
|
||||
style: Platform.OS === "web" ? ({ $$css: true, touchOnly: "touchOnly" } as any) : {},
|
||||
};
|
||||
export const noTouch: ViewProps = {
|
||||
style: Platform.OS === "web" ? ({ $$css: true, noTouch: "noTouch" } as any) : { display: "none" },
|
||||
};
|
||||
|
||||
export const useIsTouch = () => {
|
||||
if (Platform.OS !== "web") return true;
|
||||
if (typeof window === "undefined") return false;
|
||||
// TODO: Subscribe to the change.
|
||||
return document.body.classList.contains("noHover");
|
||||
};
|
||||
@@ -1,9 +1,5 @@
|
||||
import { type ComponentType, type ReactElement, isValidElement } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { View } from "react-native";
|
||||
import { useYoshiki } from "yoshiki/native";
|
||||
import { P } from "~/primitives";
|
||||
import { ErrorView } from "./errors";
|
||||
import type { ReactElement } from "react";
|
||||
import { ErrorView, OfflineView } from "~/ui/errors";
|
||||
import { type QueryIdentifier, useFetch } from "./query";
|
||||
|
||||
export const Fetch = <Data,>({
|
||||
@@ -22,37 +18,3 @@ export const Fetch = <Data,>({
|
||||
if (!data) return <Loader />;
|
||||
return <Render {...data} />;
|
||||
};
|
||||
|
||||
export const OfflineView = () => {
|
||||
const { css } = useYoshiki();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<View
|
||||
{...css({
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
})}
|
||||
>
|
||||
<P {...css({ color: (theme) => theme.colors.white })}>{t("errors.offline")}</P>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
export const EmptyView = ({ message }: { message: string }) => {
|
||||
const { css } = useYoshiki();
|
||||
|
||||
return (
|
||||
<View
|
||||
{...css({
|
||||
flexGrow: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
})}
|
||||
>
|
||||
<P {...css({ color: (theme) => theme.heading })}>{message}</P>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
import { ConnectionErrorContext, useAccount } from "@kyoo/models";
|
||||
import { Button, H1, Icon, Link, P, ts } from "@kyoo/primitives";
|
||||
import Register from "@material-symbols/svg-400/rounded/app_registration.svg";
|
||||
import { useContext } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { View } from "react-native";
|
||||
import { useRouter } from "solito/router";
|
||||
import { useYoshiki } from "yoshiki/native";
|
||||
import { DefaultLayout } from "../../../packages/ui/src/layout";
|
||||
|
||||
export const ConnectionError = () => {
|
||||
const { css } = useYoshiki();
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const { error, retry } = useContext(ConnectionErrorContext);
|
||||
const account = useAccount();
|
||||
|
||||
if (error && (error.status === 401 || error.status === 403)) {
|
||||
if (!account) {
|
||||
return (
|
||||
<View
|
||||
{...css({ flexGrow: 1, flexShrink: 1, justifyContent: "center", alignItems: "center" })}
|
||||
>
|
||||
<P>{t("errors.needAccount")}</P>
|
||||
<Button
|
||||
as={Link}
|
||||
href={"/register"}
|
||||
text={t("login.register")}
|
||||
licon={<Icon icon={Register} {...css({ marginRight: ts(2) })} />}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
if (!account.isVerified) {
|
||||
return (
|
||||
<View
|
||||
{...css({ flexGrow: 1, flexShrink: 1, justifyContent: "center", alignItems: "center" })}
|
||||
>
|
||||
<P>{t("errors.needVerification")}</P>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<View {...css({ padding: ts(2) })}>
|
||||
<H1 {...css({ textAlign: "center" })}>{t("errors.connection")}</H1>
|
||||
<P>{error?.errors[0] ?? t("errors.unknown")}</P>
|
||||
<P>{t("errors.connection-tips")}</P>
|
||||
<Button onPress={retry} text={t("errors.try-again")} {...css({ m: ts(1) })} />
|
||||
<Button
|
||||
onPress={() => router.push("/login")}
|
||||
text={t("errors.re-login")}
|
||||
{...css({ m: ts(1) })}
|
||||
/>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
ConnectionError.getLayout = DefaultLayout;
|
||||
@@ -0,0 +1,19 @@
|
||||
import { View } from "react-native";
|
||||
import { useYoshiki } from "yoshiki/native";
|
||||
import { P } from "~/primitives";
|
||||
|
||||
export const EmptyView = ({ message }: { message: string }) => {
|
||||
const { css } = useYoshiki();
|
||||
|
||||
return (
|
||||
<View
|
||||
{...css({
|
||||
flexGrow: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
})}
|
||||
>
|
||||
<P {...css({ color: (theme) => theme.heading })}>{message}</P>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
import { useContext, useLayoutEffect } from "react";
|
||||
import { View } from "react-native";
|
||||
import { useYoshiki } from "yoshiki/native";
|
||||
import { ConnectionErrorContext, type KyooErrors } from "~/models";
|
||||
import { P } from "~/primitives";
|
||||
|
||||
export const ErrorView = ({
|
||||
error,
|
||||
noBubble = false,
|
||||
}: {
|
||||
error: KyooErrors;
|
||||
noBubble?: boolean;
|
||||
}) => {
|
||||
const { css } = useYoshiki();
|
||||
const { setError } = useContext(ConnectionErrorContext);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
// if this is a permission error, make it go up the tree to have a whole page login screen.
|
||||
if (!noBubble && (error.status === 401 || error.status === 403)) setError(error);
|
||||
}, [error, noBubble, setError]);
|
||||
console.log(error);
|
||||
return (
|
||||
<View
|
||||
{...css({
|
||||
backgroundColor: (theme) => theme.colors.red,
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
})}
|
||||
>
|
||||
{error.errors.map((x, i) => (
|
||||
<P key={i} {...css({ color: (theme) => theme.colors.white })}>
|
||||
{x}
|
||||
</P>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
export * from "./error";
|
||||
export * from "./unauthorized";
|
||||
export * from "./connection";
|
||||
export * from "./setup";
|
||||
export * from "./empty";
|
||||
export * from "./offline";
|
||||
@@ -0,0 +1,22 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { View } from "react-native";
|
||||
import { useYoshiki } from "yoshiki/native";
|
||||
import { P } from "~/primitives";
|
||||
|
||||
export const OfflineView = () => {
|
||||
const { css } = useYoshiki();
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<View
|
||||
{...css({
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
})}
|
||||
>
|
||||
<P {...css({ color: (theme) => theme.colors.white })}>{t("errors.offline")}</P>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Main } from "@expo/html-elements";
|
||||
import { type QueryPage, SetupStep } from "@kyoo/models";
|
||||
import { Button, Icon, Link, P, ts } from "@kyoo/primitives";
|
||||
import Register from "@material-symbols/svg-400/rounded/app_registration.svg";
|
||||
import { useEffect } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useRouter } from "solito/router";
|
||||
import { useYoshiki } from "yoshiki/native";
|
||||
import { Navbar, NavbarProfile } from "../../../packages/ui/src/navbar";
|
||||
import { KyooLongLogo } from "../../../packages/ui/src/navbar/icon";
|
||||
|
||||
export const SetupPage: QueryPage<{ step: SetupStep }> = ({ step }) => {
|
||||
const { css } = useYoshiki();
|
||||
const { t } = useTranslation();
|
||||
const router = useRouter();
|
||||
const isValid = Object.values(SetupStep).includes(step) && step !== SetupStep.Done;
|
||||
|
||||
useEffect(() => {
|
||||
if (!isValid) router.replace("/");
|
||||
}, [isValid, router]);
|
||||
|
||||
if (!isValid) return <P>Loading...</P>;
|
||||
|
||||
return (
|
||||
<Main {...css({ flexGrow: 1, flexShrink: 1, justifyContent: "center", alignItems: "center" })}>
|
||||
<P>{t(`errors.setup.${step}`)}</P>
|
||||
{step === SetupStep.MissingAdminAccount && (
|
||||
<Button
|
||||
as={Link}
|
||||
href={"/register"}
|
||||
text={t("login.register")}
|
||||
licon={<Icon icon={Register} {...css({ marginRight: ts(2) })} />}
|
||||
/>
|
||||
)}
|
||||
</Main>
|
||||
);
|
||||
};
|
||||
|
||||
SetupPage.getLayout = ({ page }) => {
|
||||
const { css } = useYoshiki();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Navbar left={<KyooLongLogo {...css({ marginX: ts(2) })} />} right={<NavbarProfile />} />
|
||||
{page}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { View } from "react-native";
|
||||
import { useYoshiki } from "yoshiki/native";
|
||||
import { P } from "~/primitives";
|
||||
|
||||
export const Unauthorized = ({ missing }: { missing: string[] }) => {
|
||||
const { t } = useTranslation();
|
||||
const { css } = useYoshiki();
|
||||
|
||||
return (
|
||||
<View
|
||||
{...css({
|
||||
flexGrow: 1,
|
||||
flexShrink: 1,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
})}
|
||||
>
|
||||
<P>{t("errors.unauthorized", { permission: missing?.join(", ") })}</P>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user