mirror of
https://github.com/Radiquum/AniX.git
synced 2025-04-30 18:09:40 +05:00
Merge remote-tracking branch 'origin/feat_player'
This commit is contained in:
parent
bb437fe7ca
commit
25e31a7799
62 changed files with 1508 additions and 701 deletions
|
@ -1,12 +0,0 @@
|
|||
export const Chip = (props) => {
|
||||
return (
|
||||
<div className={`rounded-sm ${props.bg_color || "bg-gray-500"}`}>
|
||||
<p className="px-2 sm:px-4 py-0.5 sm:py-1 text-xs xl:text-base text-white">
|
||||
{props.name}
|
||||
{props.name && props.devider ? props.devider : " "}
|
||||
{props.name_2}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
16
app/components/Chip/Chip.tsx
Normal file
16
app/components/Chip/Chip.tsx
Normal file
|
@ -0,0 +1,16 @@
|
|||
export const Chip = (props: {
|
||||
name?: string;
|
||||
name_2?: string;
|
||||
devider?: string;
|
||||
bg_color?: string;
|
||||
}) => {
|
||||
return (
|
||||
<div className={`rounded-sm ${props.bg_color || "bg-gray-500"}`}>
|
||||
<p className="px-2 sm:px-4 py-0.5 sm:py-1 text-xs xl:text-base text-white">
|
||||
{props.name}
|
||||
{props.name && props.devider ? props.devider : " "}
|
||||
{props.name_2}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -1,14 +1,13 @@
|
|||
"use client";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { useUserStore } from "@/app/store/auth";
|
||||
import { useUserStore } from "#/store/auth";
|
||||
import { Dropdown } from "flowbite-react";
|
||||
|
||||
export const Navbar = () => {
|
||||
const pathname = usePathname();
|
||||
const userStore = useUserStore((state) => state);
|
||||
const userStore: any = useUserStore((state) => state);
|
||||
|
||||
const isNotAuthorizedStyle = "text-gray-700";
|
||||
const navLinks = [
|
||||
{
|
||||
id: 1,
|
||||
|
@ -107,7 +106,7 @@ export const Navbar = () => {
|
|||
}}
|
||||
>
|
||||
<Dropdown.Item className="text-sm md:text-base">
|
||||
<Link href="/profile" className="flex items-center gap-1">
|
||||
<Link href={`/profile/${userStore.user.id}`} className="flex items-center gap-1">
|
||||
<span
|
||||
className={`iconify ${pathname == `/profile/${userStore.user.id}` ? "font-bold mdi--user" : "mdi--user-outline"} w-4 h-4 sm:w-6 sm:h-6`}
|
||||
></span>
|
|
@ -1,7 +1,7 @@
|
|||
import { numberDeclension } from "@/app/api/utils";
|
||||
import { numberDeclension } from "#/api/utils";
|
||||
import Link from "next/link";
|
||||
|
||||
export const RelatedSection = (props) => {
|
||||
export const RelatedSection = (props: any) => {
|
||||
const declension = numberDeclension(
|
||||
props.release_count,
|
||||
"релиз",
|
|
@ -9,9 +9,13 @@ import "swiper/css";
|
|||
import "swiper/css/navigation";
|
||||
import { Navigation } from "swiper/modules";
|
||||
|
||||
export const ReleaseCourusel = (props) => {
|
||||
export const ReleaseCourusel = (props: {
|
||||
sectionTitle: string;
|
||||
showAllLink?: string;
|
||||
content: any;
|
||||
}) => {
|
||||
useEffect(() => {
|
||||
const options = {
|
||||
const options: any = {
|
||||
direction: "horizontal",
|
||||
spaceBetween: 8,
|
||||
allowTouchMove: true,
|
||||
|
@ -39,12 +43,14 @@ export const ReleaseCourusel = (props) => {
|
|||
<h1 className="font-bold text-md sm:text-xl md:text-lg xl:text-xl">
|
||||
{props.sectionTitle}
|
||||
</h1>
|
||||
<Link href={props.showAllLink}>
|
||||
<div className="flex items-center">
|
||||
<p className="hidden text-xl font-bold sm:block">Показать все</p>
|
||||
<span className="w-6 h-6 iconify mdi--arrow-right"></span>
|
||||
</div>
|
||||
</Link>
|
||||
{props.showAllLink && (
|
||||
<Link href={props.showAllLink}>
|
||||
<div className="flex items-center">
|
||||
<p className="hidden text-xl font-bold sm:block">Показать все</p>
|
||||
<span className="w-6 h-6 iconify mdi--arrow-right"></span>
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
<div className="m-4">
|
||||
<div className="swiper">
|
||||
|
@ -65,11 +71,15 @@ export const ReleaseCourusel = (props) => {
|
|||
</div>
|
||||
<div
|
||||
className={`swiper-button-prev ${Styles["swiper-button"]} after:iconify after:material-symbols--chevron-left aspect-square bg-black bg-opacity-25 backdrop-blur rounded-full after:bg-white`}
|
||||
style={{ "--swiper-navigation-size": "64px" }}
|
||||
style={
|
||||
{ "--swiper-navigation-size": "64px" } as React.CSSProperties
|
||||
}
|
||||
></div>
|
||||
<div
|
||||
className={`swiper-button-next ${Styles["swiper-button"]} after:iconify after:material-symbols--chevron-right aspect-square bg-black bg-opacity-25 backdrop-blur rounded-full after:bg-white`}
|
||||
style={{ "--swiper-navigation-size": "64px" }}
|
||||
style={
|
||||
{ "--swiper-navigation-size": "64px" } as React.CSSProperties
|
||||
}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
21
app/components/ReleaseInfo/ReleaseInfo.SearchLink.tsx
Normal file
21
app/components/ReleaseInfo/ReleaseInfo.SearchLink.tsx
Normal file
|
@ -0,0 +1,21 @@
|
|||
import Link from "next/link";
|
||||
|
||||
// const searchBy = {
|
||||
// title: 0,
|
||||
// studio: 1,
|
||||
// director: 2,
|
||||
// author: 3,
|
||||
// genre: 4
|
||||
// }
|
||||
|
||||
// TODO: сделать какую-нибудь анимацию на ссылке при наведении и фокусе
|
||||
export const ReleaseInfoSearchLink = (props: { title: string, searchBy: string | number | null }) => {
|
||||
return (
|
||||
<Link
|
||||
className="underline"
|
||||
href={`/search?q=${props.title}&searchBy=${props.searchBy}`}
|
||||
>
|
||||
{props.title}
|
||||
</Link>
|
||||
);
|
||||
};
|
|
@ -1,18 +1,18 @@
|
|||
import Link from "next/link";
|
||||
import { sinceUnixDate } from "@/app/api/utils";
|
||||
import { Chip } from "@/app/components/Chip/Chip";
|
||||
import { sinceUnixDate } from "#/api/utils";
|
||||
import { Chip } from "#/components/Chip/Chip";
|
||||
|
||||
export const ReleaseLink = (props) => {
|
||||
const profile_lists = {
|
||||
// 0: "Не смотрю",
|
||||
1: { name: "Смотрю", bg_color: "bg-green-500" },
|
||||
2: { name: "В планах", bg_color: "bg-purple-500" },
|
||||
3: { name: "Просмотрено", bg_color: "bg-blue-500" },
|
||||
4: { name: "Отложено", bg_color: "bg-yellow-500" },
|
||||
5: { name: "Брошено", bg_color: "bg-red-500" },
|
||||
};
|
||||
|
||||
export const ReleaseLink169 = (props: any) => {
|
||||
const grade = props.grade.toFixed(1);
|
||||
const profile_lists = {
|
||||
// 0: "Не смотрю",
|
||||
1: { name: "Смотрю", bg_color: "bg-green-500" },
|
||||
2: { name: "В планах", bg_color: "bg-purple-500" },
|
||||
3: { name: "Просмотрено", bg_color: "bg-blue-500" },
|
||||
4: { name: "Отложено", bg_color: "bg-yellow-500" },
|
||||
5: { name: "Брошено", bg_color: "bg-red-500" },
|
||||
};
|
||||
|
||||
const profile_list_status = props.profile_list_status;
|
||||
let user_list = null;
|
||||
if (profile_list_status != null || profile_list_status != 0) {
|
||||
|
@ -79,6 +79,7 @@ export const ReleaseLink = (props) => {
|
|||
devider=", "
|
||||
/>
|
||||
)}
|
||||
{props.category && <Chip name={props.category.name} />}
|
||||
{props.is_favorite && (
|
||||
<div className="flex items-center justify-center bg-pink-500 rounded-sm">
|
||||
<span className="w-3 px-4 py-2.5 text-white sm:px-4 sm:py-3 xl:px-6 xl:py-4 iconify mdi--heart"></span>
|
82
app/components/ReleaseLink/ReleaseLink.Poster.tsx
Normal file
82
app/components/ReleaseLink/ReleaseLink.Poster.tsx
Normal file
|
@ -0,0 +1,82 @@
|
|||
import Link from "next/link";
|
||||
import { sinceUnixDate } from "#/api/utils";
|
||||
import { Chip } from "#/components/Chip/Chip";
|
||||
|
||||
const profile_lists = {
|
||||
// 0: "Не смотрю",
|
||||
1: { name: "Смотрю", bg_color: "bg-green-500" },
|
||||
2: { name: "В планах", bg_color: "bg-purple-500" },
|
||||
3: { name: "Просмотрено", bg_color: "bg-blue-500" },
|
||||
4: { name: "Отложено", bg_color: "bg-yellow-500" },
|
||||
5: { name: "Брошено", bg_color: "bg-red-500" },
|
||||
};
|
||||
|
||||
export const ReleaseLinkPoster = (props: any) => {
|
||||
const grade = props.grade.toFixed(1);
|
||||
const profile_list_status = props.profile_list_status;
|
||||
let user_list = null;
|
||||
if (profile_list_status != null || profile_list_status != 0) {
|
||||
user_list = profile_lists[profile_list_status];
|
||||
}
|
||||
return (
|
||||
<Link href={`/release/${props.id}`}>
|
||||
<div className="flex flex-col w-full h-full gap-4 lg:flex-row">
|
||||
<div
|
||||
className="relative w-full h-64 gap-8 p-4 overflow-hidden bg-white bg-center bg-no-repeat bg-cover border border-gray-200 rounded-lg shadow-md lg:min-w-[300px] lg:min-h-[385px] lg:max-w-[300px] lg:max-h-[385px] lg:bg-top dark:border-gray-700 dark:bg-gray-800"
|
||||
style={{
|
||||
backgroundImage: `linear-gradient(to bottom, rgba(0, 0, 0, 0.1) 0%, rgba(0, 0, 0, 0.9) 100%), url(${props.image})`,
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
<Chip
|
||||
bg_color={
|
||||
props.grade.toFixed(1) == 0
|
||||
? "hidden"
|
||||
: props.grade.toFixed(1) < 2
|
||||
? "bg-red-500"
|
||||
: props.grade.toFixed(1) < 3
|
||||
? "bg-orange-500"
|
||||
: props.grade.toFixed(1) < 4
|
||||
? "bg-yellow-500"
|
||||
: "bg-green-500"
|
||||
}
|
||||
name={props.grade.toFixed(1)}
|
||||
/>
|
||||
{props.status ? (
|
||||
<Chip name={props.status.name} />
|
||||
) : (
|
||||
<Chip
|
||||
name={
|
||||
props.status_id == 1
|
||||
? "Завершено"
|
||||
: props.status_id == 2
|
||||
? "Онгоинг"
|
||||
: "Анонс"
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<Chip
|
||||
name={props.episodes_released && props.episodes_released}
|
||||
name_2={
|
||||
props.episodes_total ? props.episodes_total + " эп." : "? эп."
|
||||
}
|
||||
devider="/"
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute flex flex-col gap-2 text-white bottom-4">
|
||||
{props.title_ru && (
|
||||
<p className="text-xl font-bold text-white md:text-2xl">
|
||||
{props.title_ru}
|
||||
</p>
|
||||
)}
|
||||
{props.title_original && (
|
||||
<p className="text-sm text-gray-300 md:text-base">
|
||||
{props.title_original}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
);
|
||||
};
|
13
app/components/ReleaseLink/ReleaseLink.tsx
Normal file
13
app/components/ReleaseLink/ReleaseLink.tsx
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { ReleaseLink169 } from "./ReleaseLink.16_9";
|
||||
import { ReleaseLinkPoster } from "./ReleaseLink.Poster";
|
||||
|
||||
export const ReleaseLink = (props: any) => {
|
||||
const type = props.type || "16_9";
|
||||
|
||||
if (type == "16_9") {
|
||||
return <ReleaseLink169 {...props} />;
|
||||
}
|
||||
if (type == "poster") {
|
||||
return <ReleaseLinkPoster {...props} />;
|
||||
}
|
||||
};
|
174
app/components/ReleasePlayer/ReleasePlayer.tsx
Normal file
174
app/components/ReleasePlayer/ReleasePlayer.tsx
Normal file
|
@ -0,0 +1,174 @@
|
|||
"use client";
|
||||
|
||||
import { Spinner } from "#/components/Spinner/Spinner";
|
||||
import { useUserStore } from "#/store/auth";
|
||||
import { Card, Dropdown, Button } from "flowbite-react";
|
||||
import { ENDPOINTS } from "#/api/config";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
const DropdownTheme = {
|
||||
floating: {
|
||||
target:
|
||||
"w-full md:w-[256px] bg-blue-600 enabled:hover:bg-blue-700 focus:ring-4 focus:outline-none focus:ring-blue-300 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800",
|
||||
},
|
||||
};
|
||||
|
||||
const ButtonThemeInactive =
|
||||
"bg-blue-600 enabled:hover:bg-blue-700 focus:ring-4 focus:outline-none focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800";
|
||||
const ButtonThemeActive =
|
||||
"bg-blue-800 dark:bg-blue-600 disabled:opacity-100 dark:disabled:opacity-100";
|
||||
|
||||
async function _fetch(url: string) {
|
||||
const data = fetch(url)
|
||||
.then((res) => {
|
||||
if (res.ok) {
|
||||
return res.json();
|
||||
} else {
|
||||
throw new Error("Error fetching data");
|
||||
}
|
||||
})
|
||||
.catch((err) => console.log(err));
|
||||
return data;
|
||||
}
|
||||
|
||||
export const ReleasePlayer = (props: { id: number }) => {
|
||||
const token = useUserStore((state) => state.token);
|
||||
const [voiceoverInfo, setVoiceoverInfo] = useState(null);
|
||||
const [selectedVoiceover, setSelectedVoiceover] = useState(null);
|
||||
const [sourcesInfo, setSourcesInfo] = useState(null);
|
||||
const [selectedSource, setSelectedSource] = useState(null);
|
||||
const [episodeInfo, setEpisodeInfo] = useState(null);
|
||||
const [selectedEpisode, setSelectedEpisode] = useState(null);
|
||||
const [isFirstLoad, setIsFirstLoad] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
async function _fetchInfo() {
|
||||
const voiceover = await _fetch(
|
||||
`${ENDPOINTS.release.episode}/${props.id}`
|
||||
);
|
||||
setVoiceoverInfo(voiceover.types);
|
||||
setSelectedVoiceover(voiceover.types[0]);
|
||||
}
|
||||
_fetchInfo();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
async function _fetchInfo() {
|
||||
const sources = await _fetch(
|
||||
`${ENDPOINTS.release.episode}/${props.id}/${selectedVoiceover.id}`
|
||||
);
|
||||
setSourcesInfo(sources.sources);
|
||||
setSelectedSource(sources.sources[0]);
|
||||
}
|
||||
if (selectedVoiceover) {
|
||||
_fetchInfo();
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedVoiceover]);
|
||||
|
||||
useEffect(() => {
|
||||
async function _fetchInfo(url: string) {
|
||||
const episodes = await _fetch(url);
|
||||
|
||||
setEpisodeInfo(episodes.episodes);
|
||||
setSelectedEpisode(episodes.episodes[0]);
|
||||
}
|
||||
if (selectedSource) {
|
||||
let url = `${ENDPOINTS.release.episode}/${props.id}/${selectedVoiceover.id}/${selectedSource.id}`;
|
||||
if (token) {
|
||||
url = `${ENDPOINTS.release.episode}/${props.id}/${selectedVoiceover.id}/${selectedSource.id}?token=${token}`;
|
||||
}
|
||||
_fetchInfo(url);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedSource, token]);
|
||||
|
||||
useEffect(() => {
|
||||
async function _fetchInfo() {
|
||||
_fetch(`${ENDPOINTS.statistic.addHistory}/${props.id}/${selectedVoiceover.id}/${selectedSource.id}?token=${token}`);
|
||||
_fetch(`${ENDPOINTS.statistic.markWatched}/${props.id}/${selectedVoiceover.id}/${selectedSource.id}?token=${token}`);
|
||||
}
|
||||
if (selectedEpisode && !isFirstLoad && token) {
|
||||
_fetchInfo();
|
||||
}
|
||||
|
||||
if (isFirstLoad) {
|
||||
setIsFirstLoad(false);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedEpisode]);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
{!voiceoverInfo || !sourcesInfo || !episodeInfo ? (
|
||||
<div className="flex items-center justify-center aspect-video">
|
||||
<Spinner />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Dropdown
|
||||
label={`Озвучка: ${selectedVoiceover.name}`}
|
||||
theme={DropdownTheme}
|
||||
>
|
||||
{voiceoverInfo.map((voiceover: any) => (
|
||||
<Dropdown.Item
|
||||
key={voiceover.id}
|
||||
onClick={() => setSelectedVoiceover(voiceover)}
|
||||
>
|
||||
{voiceover.name}
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
</Dropdown>
|
||||
<Dropdown
|
||||
label={`Плеер: ${selectedSource.name}`}
|
||||
theme={DropdownTheme}
|
||||
>
|
||||
{sourcesInfo.map((source: any) => (
|
||||
<Dropdown.Item
|
||||
key={source.id}
|
||||
onClick={() => setSelectedSource(source)}
|
||||
>
|
||||
{source.name}
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
</Dropdown>
|
||||
</div>
|
||||
<div className="aspect-video">
|
||||
<iframe
|
||||
allowFullScreen={true}
|
||||
src={selectedEpisode.url}
|
||||
className="w-full h-full rounded-md"
|
||||
></iframe>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex gap-2 p-2 overflow-x-auto scrollbar-thin">
|
||||
{episodeInfo.map((episode: any) => (
|
||||
<Button
|
||||
className={`text-center min-w-fit ${
|
||||
selectedEpisode.position === episode.position
|
||||
? ButtonThemeActive
|
||||
: ButtonThemeInactive
|
||||
}`}
|
||||
key={episode.id}
|
||||
onClick={() => {
|
||||
setSelectedEpisode(episode);
|
||||
episode.is_watched = true;
|
||||
}}
|
||||
disabled={selectedEpisode.position === episode.position}
|
||||
>
|
||||
{episode.position} серия
|
||||
{episode.is_watched && (
|
||||
<span className="w-5 h-5 ml-2 iconify material-symbols--check-circle"></span>
|
||||
)}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
};
|
|
@ -1,6 +1,6 @@
|
|||
import { ReleaseLink } from "../ReleaseLink/ReleaseLink";
|
||||
|
||||
export const ReleaseSection = (props) => {
|
||||
export const ReleaseSection = (props: any) => {
|
||||
return (
|
||||
<section>
|
||||
{props.sectionTitle && (
|
Loading…
Add table
Add a link
Reference in a new issue