Compare commits
10 Commits
6c6ba6c4c9
...
fa4ef77a9e
Author | SHA1 | Date |
---|---|---|
zisra | fa4ef77a9e | |
Jorrin | f3dd80f42b | |
William Oldham | cfc74dfa78 | |
William Oldham | 1a3144a872 | |
Jorrin | ae81832037 | |
Jorrin | 3da8955607 | |
zisra | a9f6b518aa | |
Isra | 25452fa01a | |
Isra | 7de093ef20 | |
Isra | 6fe063db4d |
|
@ -156,7 +156,8 @@
|
|||
},
|
||||
"navigation": {
|
||||
"banner": {
|
||||
"offline": "Check your internet connection"
|
||||
"offline": "Check your internet connection",
|
||||
"mobile": "Open in app"
|
||||
},
|
||||
"menu": {
|
||||
"about": "About us",
|
||||
|
@ -278,7 +279,8 @@
|
|||
"loadingError": "Error loading season",
|
||||
"loadingList": "Loading...",
|
||||
"loadingTitle": "Loading...",
|
||||
"unairedEpisodes": "One or more episodes in this season have been disabled because they haven't been aired yet."
|
||||
"unairedEpisodes": "One or more episodes in this season have been disabled because they haven't been aired yet.",
|
||||
"seasons": "Seasons"
|
||||
},
|
||||
"playback": {
|
||||
"speedLabel": "Playback speed",
|
||||
|
|
|
@ -65,6 +65,7 @@ export enum Icons {
|
|||
CIRCLE_QUESTION = "circle_question",
|
||||
BRUSH = "brush",
|
||||
UPLOAD = "upload",
|
||||
OPEN_IN_APP = "open_in_app",
|
||||
}
|
||||
|
||||
export interface IconProps {
|
||||
|
@ -136,6 +137,7 @@ const iconList: Record<Icons, string> = {
|
|||
circle_question: `<svg xmlns="http://www.w3.org/2000/svg" height="1em" width="1em" viewBox="0 0 512 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.--><path opacity="1" fill="currentColor" d="M464 256A208 208 0 1 0 48 256a208 208 0 1 0 416 0zM0 256a256 256 0 1 1 512 0A256 256 0 1 1 0 256zm169.8-90.7c7.9-22.3 29.1-37.3 52.8-37.3h58.3c34.9 0 63.1 28.3 63.1 63.1c0 22.6-12.1 43.5-31.7 54.8L280 264.4c-.2 13-10.9 23.6-24 23.6c-13.3 0-24-10.7-24-24V250.5c0-8.6 4.6-16.5 12.1-20.8l44.3-25.4c4.7-2.7 7.6-7.7 7.6-13.1c0-8.4-6.8-15.1-15.1-15.1H222.6c-3.4 0-6.4 2.1-7.5 5.3l-.4 1.2c-4.4 12.5-18.2 19-30.6 14.6s-19-18.2-14.6-30.6l.4-1.2zM224 352a32 32 0 1 1 64 0 32 32 0 1 1 -64 0z"/></svg>`,
|
||||
brush: `<svg xmlns="http://www.w3.org/2000/svg" height="1em" width="1em" viewBox="0 0 384 512"><!--!Font Awesome Free 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2023 Fonticons, Inc.--><path d="M162.4 6c-1.5-3.6-5-6-8.9-6h-19c-3.9 0-7.5 2.4-8.9 6L104.9 57.7c-3.2 8-14.6 8-17.8 0L66.4 6c-1.5-3.6-5-6-8.9-6H48C21.5 0 0 21.5 0 48V224v22.4V256H9.6 374.4 384v-9.6V224 48c0-26.5-21.5-48-48-48H230.5c-3.9 0-7.5 2.4-8.9 6L200.9 57.7c-3.2 8-14.6 8-17.8 0L162.4 6zM0 288v32c0 35.3 28.7 64 64 64h64v64c0 35.3 28.7 64 64 64s64-28.7 64-64V384h64c35.3 0 64-28.7 64-64V288H0zM192 432a16 16 0 1 1 0 32 16 16 0 1 1 0-32z" fill="currentColor"/></svg>`,
|
||||
upload: `<svg xmlns="http://www.w3.org/2000/svg" height="1em" width="1em" viewBox="0 0 384 512"><!--! Font Awesome Pro 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path opacity="1" fill="currentColor" d="M320 480H64c-17.7 0-32-14.3-32-32V64c0-17.7 14.3-32 32-32H192V144c0 26.5 21.5 48 48 48H352V448c0 17.7-14.3 32-32 32zM240 160c-8.8 0-16-7.2-16-16V32.5c2.8 .7 5.4 2.1 7.4 4.2L347.3 152.6c2.1 2.1 3.5 4.6 4.2 7.4H240zM64 0C28.7 0 0 28.7 0 64V448c0 35.3 28.7 64 64 64H320c35.3 0 64-28.7 64-64V163.9c0-12.7-5.1-24.9-14.1-33.9L254.1 14.1c-9-9-21.2-14.1-33.9-14.1H64zM208 278.6l52.7 52.7c6.2 6.2 16.4 6.2 22.6 0s6.2-16.4 0-22.6l-80-80c-6.2-6.2-16.4-6.2-22.6 0l-80 80c-6.2 6.2-6.2 16.4 0 22.6s16.4 6.2 22.6 0L176 278.6V400c0 8.8 7.2 16 16 16s16-7.2 16-16V278.6z"/></svg>`,
|
||||
open_in_app: `<svg xmlns="http://www.w3.org/2000/svg" height="1em" width="1em" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.5.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2024 Fonticons, Inc. --><path fill="currentColor" d="M304 41c0 10.9 4.3 21.3 12 29L362.1 116 207 271c-9.4 9.4-9.4 24.6 0 33.9s24.6 9.4 33.9 0l155-155L442.1 196c7.7 7.7 18.1 12 29 12c22.6 0 41-18.3 41-41V40c0-22.1-17.9-40-40-40H345c-22.6 0-41 18.3-41 41zm57.9 7H464V150.1L361.9 48zM72 32C32.2 32 0 64.2 0 104V440c0 39.8 32.2 72 72 72H408c39.8 0 72-32.2 72-72V312c0-13.3-10.7-24-24-24s-24 10.7-24 24V440c0 13.3-10.7 24-24 24H72c-13.3 0-24-10.7-24-24V104c0-13.3 10.7-24 24-24H200c13.3 0 24-10.7 24-24s-10.7-24-24-24H72z"/></svg>`,
|
||||
};
|
||||
|
||||
function ChromeCastButton() {
|
||||
|
|
|
@ -25,7 +25,7 @@ export function EditButton(props: EditButtonProps) {
|
|||
>
|
||||
<span ref={parent}>
|
||||
{props.editing ? (
|
||||
<span className="mx-4 whitespace-nowrap">
|
||||
<span className="mx-2 sm:mx-4 whitespace-nowrap">
|
||||
{t("home.mediaList.stopEditing")}
|
||||
</span>
|
||||
) : (
|
||||
|
|
|
@ -212,9 +212,16 @@ function EpisodesView({
|
|||
|
||||
return (
|
||||
<Menu.CardWithScrollable>
|
||||
<Menu.BackLink onClick={goBack}>
|
||||
{loadingState?.value?.season.title ||
|
||||
t("player.menus.episodes.loadingTitle")}
|
||||
<Menu.BackLink
|
||||
onClick={goBack}
|
||||
rightSide={
|
||||
<span>
|
||||
{loadingState?.value?.season.title ||
|
||||
t("player.menus.episodes.loadingTitle")}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
{t("player.menus.episodes.seasons")}
|
||||
</Menu.BackLink>
|
||||
{content}
|
||||
</Menu.CardWithScrollable>
|
||||
|
|
|
@ -24,7 +24,14 @@ export function useIsMobile(horizontal?: boolean) {
|
|||
};
|
||||
}, [horizontal]);
|
||||
|
||||
const userAgent = window.navigator.userAgent;
|
||||
const hasMobileUserAgent =
|
||||
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
|
||||
userAgent,
|
||||
);
|
||||
|
||||
return {
|
||||
isMobile,
|
||||
hasMobileUserAgent,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,21 +1,27 @@
|
|||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { Icon, Icons } from "@/components/Icon";
|
||||
import { useIsMobile } from "@/hooks/useIsMobile";
|
||||
import { useBannerStore, useRegisterBanner } from "@/stores/banner";
|
||||
import { getMobileAppLink } from "@/utils/mobileAppLink";
|
||||
|
||||
import { usePlayerStore } from "../player/store";
|
||||
|
||||
export function Banner(props: {
|
||||
children: React.ReactNode;
|
||||
type: "error";
|
||||
type: "error" | "open_app";
|
||||
id: string;
|
||||
}) {
|
||||
const [ref] = useRegisterBanner<HTMLDivElement>(props.id);
|
||||
const hideBanner = useBannerStore((s) => s.hideBanner);
|
||||
const styles = {
|
||||
error: "bg-[#C93957] text-white",
|
||||
open_app: "bg-[#4169E1] text-white", // blue
|
||||
};
|
||||
const icons = {
|
||||
error: Icons.CIRCLE_EXCLAMATION,
|
||||
open_app: Icons.OPEN_IN_APP,
|
||||
};
|
||||
|
||||
return (
|
||||
|
@ -23,15 +29,15 @@ export function Banner(props: {
|
|||
<div
|
||||
className={[
|
||||
styles[props.type],
|
||||
"flex items-center justify-center p-1",
|
||||
"flex items-center justify-center p-4",
|
||||
].join(" ")}
|
||||
>
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex items-center space-x-3 text-lg">
|
||||
<Icon icon={icons[props.type]} />
|
||||
<div>{props.children}</div>
|
||||
</div>
|
||||
<span
|
||||
className="absolute right-4 hover:cursor-pointer"
|
||||
className="absolute right-4 hover:cursor-pointer cursor-pointer"
|
||||
onClick={() => hideBanner(props.id, true)}
|
||||
>
|
||||
<Icon icon={Icons.X} />
|
||||
|
@ -41,12 +47,39 @@ export function Banner(props: {
|
|||
);
|
||||
}
|
||||
|
||||
export function LinkedBanner(props: {
|
||||
href: string;
|
||||
type: "error" | "open_app";
|
||||
id: string;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<a href={props.href} rel="noreferrer">
|
||||
<Banner type={props.type} id={props.id}>
|
||||
{props.children}
|
||||
</Banner>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
export function BannerLocation(props: { location?: string }) {
|
||||
const { t } = useTranslation();
|
||||
const isOnline = useBannerStore((s) => s.isOnline);
|
||||
const setLocation = useBannerStore((s) => s.setLocation);
|
||||
const ignoredBannerIds = useBannerStore((s) => s.ignoredBannerIds);
|
||||
const currentLocation = useBannerStore((s) => s.location);
|
||||
const { hasMobileUserAgent } = useIsMobile();
|
||||
const meta = usePlayerStore((s) => s.meta);
|
||||
const [appLink, setAppLink] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (meta) {
|
||||
setAppLink(getMobileAppLink(meta));
|
||||
} else {
|
||||
setAppLink("");
|
||||
}
|
||||
}, [meta]);
|
||||
|
||||
const loc = props.location ?? null;
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -66,6 +99,15 @@ export function BannerLocation(props: { location?: string }) {
|
|||
{t("navigation.banner.offline")}
|
||||
</Banner>
|
||||
) : null}
|
||||
{hasMobileUserAgent && !ignoredBannerIds.includes("open_app") ? (
|
||||
<LinkedBanner
|
||||
id="open_app"
|
||||
type="open_app"
|
||||
href={meta ? appLink : "movieweb://"}
|
||||
>
|
||||
{t("navigation.banner.mobile")}
|
||||
</LinkedBanner>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
import { PlayerMeta } from "@/stores/player/slices/source";
|
||||
|
||||
export function getMobileAppLink(meta: PlayerMeta) {
|
||||
const url = new URL("movieweb://videoPlayer");
|
||||
|
||||
url.searchParams.set("type", meta.type === "movie" ? "movie" : "tv");
|
||||
url.searchParams.set("id", meta.tmdbId);
|
||||
|
||||
const isShow = meta.type === "show";
|
||||
if (isShow && meta.season?.number) {
|
||||
url.searchParams.set("season", meta.season?.number.toString());
|
||||
}
|
||||
if (isShow && meta.episode?.number) {
|
||||
url.searchParams.set("episode", meta.episode.number.toString());
|
||||
}
|
||||
|
||||
return url.toString();
|
||||
}
|
Loading…
Reference in New Issue