Compare commits
No commits in common. "V3" and "3.5.0" have entirely different histories.
|
@ -1,3 +1,7 @@
|
||||||
{
|
{
|
||||||
"extends": ["next/core-web-vitals"]
|
"extends": ["next/core-web-vitals", "prettier"],
|
||||||
|
"rules": {
|
||||||
|
"prettier/prettier": "error"
|
||||||
|
},
|
||||||
|
"plugins": ["prettier"]
|
||||||
}
|
}
|
||||||
|
|
2
.flowbite-react/.gitignore
vendored
|
@ -1,2 +0,0 @@
|
||||||
class-list.json
|
|
||||||
pid
|
|
|
@ -1,9 +0,0 @@
|
||||||
{
|
|
||||||
"$schema": "https://unpkg.com/flowbite-react/schema.json",
|
|
||||||
"components": [],
|
|
||||||
"dark": true,
|
|
||||||
"prefix": "",
|
|
||||||
"path": "src/components",
|
|
||||||
"tsx": true,
|
|
||||||
"rsc": true
|
|
||||||
}
|
|
2
.gitignore
vendored
|
@ -59,5 +59,3 @@ videos/*
|
||||||
!videos/*.js
|
!videos/*.js
|
||||||
!videos/*.ts
|
!videos/*.ts
|
||||||
public/_next-video
|
public/_next-video
|
||||||
|
|
||||||
API-Trace/*
|
|
|
@ -6,9 +6,10 @@ AniX is an unofficial web client for the Android application Anixart. It allows
|
||||||
|
|
||||||
## Changelog [RU]
|
## Changelog [RU]
|
||||||
|
|
||||||
- [3.7.0](./public/changelog/3.7.0.md)
|
|
||||||
- [3.6.0](./public/changelog/3.6.0.md)
|
|
||||||
- [3.5.0](./public/changelog/3.5.0.md)
|
- [3.5.0](./public/changelog/3.5.0.md)
|
||||||
|
- [3.4.0](./public/changelog/3.4.0.md)
|
||||||
|
- [3.3.0](./public/changelog/3.3.0.md)
|
||||||
|
- [3.2.3](./public/changelog/3.2.3.md)
|
||||||
|
|
||||||
[other versions](./public/changelog)
|
[other versions](./public/changelog)
|
||||||
|
|
||||||
|
|
12
app/App.tsx
|
@ -4,7 +4,7 @@ import { usePreferencesStore } from "./store/preferences";
|
||||||
import { Navbar } from "./components/Navbar/NavbarUpdate";
|
import { Navbar } from "./components/Navbar/NavbarUpdate";
|
||||||
import { Inter } from "next/font/google";
|
import { Inter } from "next/font/google";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from "flowbite-react";
|
import { Button, Modal } from "flowbite-react";
|
||||||
import { Spinner } from "./components/Spinner/Spinner";
|
import { Spinner } from "./components/Spinner/Spinner";
|
||||||
import { ChangelogModal } from "#/components/ChangelogModal/ChangelogModal";
|
import { ChangelogModal } from "#/components/ChangelogModal/ChangelogModal";
|
||||||
import PlausibleProvider from "next-plausible";
|
import PlausibleProvider from "next-plausible";
|
||||||
|
@ -80,8 +80,8 @@ export const App = (props) => {
|
||||||
show={preferencesStore.params.isFirstLaunch}
|
show={preferencesStore.params.isFirstLaunch}
|
||||||
onClose={() => preferencesStore.setParams({ isFirstLaunch: false })}
|
onClose={() => preferencesStore.setParams({ isFirstLaunch: false })}
|
||||||
>
|
>
|
||||||
<ModalHeader>Внимание</ModalHeader>
|
<Modal.Header>Внимание</Modal.Header>
|
||||||
<ModalBody>
|
<Modal.Body>
|
||||||
<p>
|
<p>
|
||||||
Данный сайт не связан с разработчиками приложения Anixart, это
|
Данный сайт не связан с разработчиками приложения Anixart, это
|
||||||
неофициальная имплементация веб клиента для этого приложения.
|
неофициальная имплементация веб клиента для этого приложения.
|
||||||
|
@ -94,15 +94,15 @@ export const App = (props) => {
|
||||||
На сайте могут присутствовать ошибки и не доработки, а так-же
|
На сайте могут присутствовать ошибки и не доработки, а так-же
|
||||||
отсутствующий функционал.
|
отсутствующий функционал.
|
||||||
</p>
|
</p>
|
||||||
</ModalBody>
|
</Modal.Body>
|
||||||
<ModalFooter>
|
<Modal.Footer>
|
||||||
<Button
|
<Button
|
||||||
color={"blue"}
|
color={"blue"}
|
||||||
onClick={() => preferencesStore.setParams({ isFirstLaunch: false })}
|
onClick={() => preferencesStore.setParams({ isFirstLaunch: false })}
|
||||||
>
|
>
|
||||||
Принимаю
|
Принимаю
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</Modal.Footer>
|
||||||
</Modal>
|
</Modal>
|
||||||
{preferencesStore.flags.enableAnalytics && (
|
{preferencesStore.flags.enableAnalytics && (
|
||||||
<PlausibleProvider
|
<PlausibleProvider
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export const CURRENT_APP_VERSION = "3.7.0";
|
export const CURRENT_APP_VERSION = "3.5.0";
|
||||||
|
|
||||||
export const API_URL = "https://api.anixart.tv";
|
export const API_URL = "https://api.anixart.tv";
|
||||||
export const API_PREFIX = "/api/proxy";
|
export const API_PREFIX = "/api/proxy";
|
||||||
|
@ -18,15 +18,6 @@ export const ENDPOINTS = {
|
||||||
bookmark: `${API_PREFIX}/profile/list`,
|
bookmark: `${API_PREFIX}/profile/list`,
|
||||||
history: `${API_PREFIX}/history`,
|
history: `${API_PREFIX}/history`,
|
||||||
favorite: `${API_PREFIX}/favorite`,
|
favorite: `${API_PREFIX}/favorite`,
|
||||||
blocklist: `${API_PREFIX}/profile/blocklist`,
|
|
||||||
friend: {
|
|
||||||
list: `${API_PREFIX}/profile/friend/all`,
|
|
||||||
add: `${API_PREFIX}/profile/friend/request/send`,
|
|
||||||
remove: `${API_PREFIX}/profile/friend/request/remove`,
|
|
||||||
hide: `${API_PREFIX}/profile/friend/request/hide`,
|
|
||||||
in: `${API_PREFIX}/profile/friend/requests/in`,
|
|
||||||
out: `${API_PREFIX}/profile/friend/requests/out`,
|
|
||||||
},
|
|
||||||
settings: {
|
settings: {
|
||||||
my: `${API_PREFIX}/profile/preference/my`,
|
my: `${API_PREFIX}/profile/preference/my`,
|
||||||
login: {
|
login: {
|
||||||
|
|
|
@ -212,9 +212,9 @@ export function unixToDate(
|
||||||
" " +
|
" " +
|
||||||
date.getFullYear() +
|
date.getFullYear() +
|
||||||
", " +
|
", " +
|
||||||
`${date.getHours()}`.padStart(2, "0") +
|
date.getHours() +
|
||||||
":" +
|
":" +
|
||||||
`${date.getMinutes()}`.padStart(2, "0")
|
date.getMinutes()
|
||||||
);
|
);
|
||||||
if (type === "dayMonth")
|
if (type === "dayMonth")
|
||||||
return date.getDate() + " " + months[date.getMonth()];
|
return date.getDate() + " " + months[date.getMonth()];
|
||||||
|
|
|
@ -1,14 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {
|
import { Modal, Accordion } from "flowbite-react";
|
||||||
Accordion,
|
|
||||||
AccordionContent,
|
|
||||||
AccordionPanel,
|
|
||||||
AccordionTitle,
|
|
||||||
Modal,
|
|
||||||
ModalBody,
|
|
||||||
ModalHeader,
|
|
||||||
} from "flowbite-react";
|
|
||||||
import Markdown from "markdown-to-jsx";
|
import Markdown from "markdown-to-jsx";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import Styles from "./ChangelogModal.module.css";
|
import Styles from "./ChangelogModal.module.css";
|
||||||
|
@ -45,8 +37,8 @@ export const ChangelogModal = (props: {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal show={props.isOpen} onClose={() => props.setIsOpen(false)}>
|
<Modal show={props.isOpen} onClose={() => props.setIsOpen(false)}>
|
||||||
<ModalHeader>Список изменений v{props.version}</ModalHeader>
|
<Modal.Header>Список изменений v{props.version}</Modal.Header>
|
||||||
<ModalBody>
|
<Modal.Body>
|
||||||
<Markdown className={Styles.markdown}>
|
<Markdown className={Styles.markdown}>
|
||||||
{currentVersionChangelog}
|
{currentVersionChangelog}
|
||||||
</Markdown>
|
</Markdown>
|
||||||
|
@ -54,8 +46,8 @@ export const ChangelogModal = (props: {
|
||||||
{props.previousVersions.length > 0 &&
|
{props.previousVersions.length > 0 &&
|
||||||
props.previousVersions.map((version) => {
|
props.previousVersions.map((version) => {
|
||||||
return (
|
return (
|
||||||
<AccordionPanel key={version}>
|
<Accordion.Panel key={version}>
|
||||||
<AccordionTitle
|
<Accordion.Title
|
||||||
onClickCapture={(e) => {
|
onClickCapture={(e) => {
|
||||||
if (!previousVersionsChangelog.hasOwnProperty(version)) {
|
if (!previousVersionsChangelog.hasOwnProperty(version)) {
|
||||||
_fetchVersionChangelog(version).then((data) => {
|
_fetchVersionChangelog(version).then((data) => {
|
||||||
|
@ -70,19 +62,19 @@ export const ChangelogModal = (props: {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Список изменений v{version}
|
Список изменений v{version}
|
||||||
</AccordionTitle>
|
</Accordion.Title>
|
||||||
<AccordionContent>
|
<Accordion.Content>
|
||||||
{previousVersionsChangelog.hasOwnProperty(version) ?
|
{previousVersionsChangelog.hasOwnProperty(version) ?
|
||||||
<Markdown className={Styles.markdown}>
|
<Markdown className={Styles.markdown}>
|
||||||
{previousVersionsChangelog[version]}
|
{previousVersionsChangelog[version]}
|
||||||
</Markdown>
|
</Markdown>
|
||||||
: <div>Загрузка ...</div>}
|
: <div>Загрузка ...</div>}
|
||||||
</AccordionContent>
|
</Accordion.Content>
|
||||||
</AccordionPanel>
|
</Accordion.Panel>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Accordion>
|
</Accordion>
|
||||||
</ModalBody>
|
</Modal.Body>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -49,13 +49,11 @@ export const CollectionLink = (props: any) => {
|
||||||
{props.title}
|
{props.title}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{props.description && (
|
|
||||||
<p className="text-xs font-light text-white md:text-sm lg:text-base xl:text-lg">
|
<p className="text-xs font-light text-white md:text-sm lg:text-base xl:text-lg">
|
||||||
{`${props.description.slice(0, 125)}${
|
{`${props.description.slice(0, 125)}${
|
||||||
props.description.length > 125 ? "..." : ""
|
props.description.length > 125 ? "..." : ""
|
||||||
}`}
|
}`}
|
||||||
</p>
|
</p>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Button, Label, Modal, ModalBody, ModalHeader, Textarea, ToggleSwitch } from "flowbite-react";
|
import { Button, Modal, ToggleSwitch, Label, Textarea } from "flowbite-react";
|
||||||
import { CommentsComment } from "./Comments.Comment";
|
import { CommentsComment } from "./Comments.Comment";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { ENDPOINTS } from "#/api/config";
|
import { ENDPOINTS } from "#/api/config";
|
||||||
|
@ -68,12 +68,12 @@ export const CommentsAddModal = (props: {
|
||||||
show={props.isOpen}
|
show={props.isOpen}
|
||||||
onClose={() => props.setIsOpen(false)}
|
onClose={() => props.setIsOpen(false)}
|
||||||
>
|
>
|
||||||
<ModalHeader>
|
<Modal.Header>
|
||||||
<p className="text-lg font-bold text-gray-900 lg:text-2xl dark:text-white">
|
<p className="text-lg font-bold text-gray-900 lg:text-2xl dark:text-white">
|
||||||
{props.isReply ? "Ответ на комментарий" : "Оставить комментарий"}
|
{props.isReply ? "Ответ на комментарий" : "Оставить комментарий"}
|
||||||
</p>
|
</p>
|
||||||
</ModalHeader>
|
</Modal.Header>
|
||||||
<ModalBody>
|
<Modal.Body>
|
||||||
{props.isReply && (
|
{props.isReply && (
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<CommentsComment
|
<CommentsComment
|
||||||
|
@ -98,7 +98,7 @@ export const CommentsAddModal = (props: {
|
||||||
<form className="flex flex-col gap-4" onSubmit={(e) => _sendComment(e)}>
|
<form className="flex flex-col gap-4" onSubmit={(e) => _sendComment(e)}>
|
||||||
<div>
|
<div>
|
||||||
<div className="block mb-2 sr-only">
|
<div className="block mb-2 sr-only">
|
||||||
<Label htmlFor="comment">Ваш комментарий.</Label>
|
<Label htmlFor="comment" value="Ваш комментарий." />
|
||||||
</div>
|
</div>
|
||||||
<Textarea
|
<Textarea
|
||||||
id="comment"
|
id="comment"
|
||||||
|
@ -132,7 +132,7 @@ export const CommentsAddModal = (props: {
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</ModalBody>
|
</Modal.Body>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { unixToDate, sinceUnixDate } from "#/api/utils";
|
import { unixToDate, sinceUnixDate } from "#/api/utils";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { ENDPOINTS } from "#/api/config";
|
import { ENDPOINTS } from "#/api/config";
|
||||||
import { Button, Dropdown, DropdownItem } from "flowbite-react";
|
import { Button, Dropdown } from "flowbite-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { CommentsAddModal } from "./Comments.Add";
|
import { CommentsAddModal } from "./Comments.Add";
|
||||||
import { CommentsEditModal } from "./Comments.Edit";
|
import { CommentsEditModal } from "./Comments.Edit";
|
||||||
|
@ -84,17 +84,9 @@ export const CommentsComment = (props: {
|
||||||
url += `&token=${props.token}`;
|
url += `&token=${props.token}`;
|
||||||
}
|
}
|
||||||
await fetch(url)
|
await fetch(url)
|
||||||
.then((res) => {
|
.then((res) => res.json())
|
||||||
if (res.ok) {
|
|
||||||
return res.json();
|
|
||||||
} else {
|
|
||||||
return { content: [] };
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.then((data) => {
|
.then((data) => {
|
||||||
if (data && data.content) {
|
|
||||||
setReplies(data.content);
|
setReplies(data.content);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
|
@ -191,19 +183,19 @@ export const CommentsComment = (props: {
|
||||||
<span className="w-6 h-6 bg-gray-400 iconify mdi--more-horiz hover:bg-gray-800 active:bg-gray-800"></span>
|
<span className="w-6 h-6 bg-gray-400 iconify mdi--more-horiz hover:bg-gray-800 active:bg-gray-800"></span>
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<DropdownItem onClick={() => setIsEditCommentsOpen(true)}>
|
<Dropdown.Item onClick={() => setIsEditCommentsOpen(true)}>
|
||||||
Редактировать
|
Редактировать
|
||||||
</DropdownItem>
|
</Dropdown.Item>
|
||||||
<DropdownItem onClick={() => _deleteComment()}>
|
<Dropdown.Item onClick={() => _deleteComment()}>
|
||||||
Удалить
|
Удалить
|
||||||
</DropdownItem>
|
</Dropdown.Item>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
)}
|
)}
|
||||||
</footer>
|
</footer>
|
||||||
<div className="relative flex items-center py-2">
|
<div className="relative flex items-center py-2">
|
||||||
<p className="text-gray-800 whitespace-pre-wrap dark:text-gray-400">
|
<p className="text-gray-800 whitespace-pre-wrap dark:text-gray-400">
|
||||||
{!props.comment.isDeleted ?
|
{!props.comment.isDeleted
|
||||||
props.comment.message
|
? props.comment.message
|
||||||
: "Комментарий был удалён."}
|
: "Комментарий был удалён."}
|
||||||
</p>
|
</p>
|
||||||
{isHidden && (
|
{isHidden && (
|
||||||
|
@ -213,8 +205,8 @@ export const CommentsComment = (props: {
|
||||||
>
|
>
|
||||||
<div className="min-w-full min-h-full px-2 py-1.5 rounded-md bg-black text-white bg-opacity-50 backdrop-blur-[8px] flex flex-col justify-center items-center">
|
<div className="min-w-full min-h-full px-2 py-1.5 rounded-md bg-black text-white bg-opacity-50 backdrop-blur-[8px] flex flex-col justify-center items-center">
|
||||||
<p>
|
<p>
|
||||||
{props.comment.likes_count < -5 ?
|
{props.comment.likes_count < -5
|
||||||
"У комментария слишком низкий рейтинг."
|
? "У комментария слишком низкий рейтинг."
|
||||||
: "Данный комментарий может содержать спойлер."}
|
: "Данный комментарий может содержать спойлер."}
|
||||||
</p>
|
</p>
|
||||||
<p className="font-bold">Нажмите, чтобы прочитать</p>
|
<p className="font-bold">Нажмите, чтобы прочитать</p>
|
||||||
|
@ -228,7 +220,7 @@ export const CommentsComment = (props: {
|
||||||
isHidden ? "mt-4" : ""
|
isHidden ? "mt-4" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{props.token ?
|
{props.token ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="flex items-center text-sm font-medium text-gray-500 hover:underline dark:text-gray-400"
|
className="flex items-center text-sm font-medium text-gray-500 hover:underline dark:text-gray-400"
|
||||||
|
@ -251,7 +243,9 @@ export const CommentsComment = (props: {
|
||||||
</svg>
|
</svg>
|
||||||
Ответить
|
Ответить
|
||||||
</button>
|
</button>
|
||||||
: <span></span>}
|
) : (
|
||||||
|
<span></span>
|
||||||
|
)}
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Button
|
<Button
|
||||||
color="inline"
|
color="inline"
|
||||||
|
@ -262,16 +256,18 @@ export const CommentsComment = (props: {
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={`w-6 h-6 iconify mdi--dislike ${
|
className={`w-6 h-6 iconify mdi--dislike ${
|
||||||
vote == 1 ?
|
vote == 1
|
||||||
"text-red-500 dark:text-red-400"
|
? "text-red-500 dark:text-red-400"
|
||||||
: "text-gray-500 dark:text-gray-400"
|
: "text-gray-500 dark:text-gray-400"
|
||||||
}`}
|
}`}
|
||||||
></span>
|
></span>
|
||||||
</Button>
|
</Button>
|
||||||
<p
|
<p
|
||||||
className={`text-sm font-medium ${
|
className={`text-sm font-medium ${
|
||||||
likes > 0 ? "text-green-500 dark:text-green-400"
|
likes > 0
|
||||||
: likes < 0 ? "text-red-500 dark:text-red-400"
|
? "text-green-500 dark:text-green-400"
|
||||||
|
: likes < 0
|
||||||
|
? "text-red-500 dark:text-red-400"
|
||||||
: "text-gray-500 dark:text-gray-400"
|
: "text-gray-500 dark:text-gray-400"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
@ -286,8 +282,8 @@ export const CommentsComment = (props: {
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={`w-6 h-6 iconify mdi--like ${
|
className={`w-6 h-6 iconify mdi--like ${
|
||||||
vote == 2 ?
|
vote == 2
|
||||||
"text-green-500 dark:text-green-400"
|
? "text-green-500 dark:text-green-400"
|
||||||
: "text-gray-500 dark:text-gray-400"
|
: "text-gray-500 dark:text-gray-400"
|
||||||
}`}
|
}`}
|
||||||
></span>
|
></span>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Button, Label, Modal, ModalBody, ModalHeader, Textarea, ToggleSwitch } from "flowbite-react";
|
import { Button, Modal, ToggleSwitch, Label, Textarea } from "flowbite-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { ENDPOINTS } from "#/api/config";
|
import { ENDPOINTS } from "#/api/config";
|
||||||
|
|
||||||
|
@ -56,16 +56,16 @@ export const CommentsEditModal = (props: {
|
||||||
show={props.isOpen}
|
show={props.isOpen}
|
||||||
onClose={() => props.setIsOpen(false)}
|
onClose={() => props.setIsOpen(false)}
|
||||||
>
|
>
|
||||||
<ModalHeader>
|
<Modal.Header>
|
||||||
<p className="text-lg font-bold text-gray-900 lg:text-2xl dark:text-white">
|
<p className="text-lg font-bold text-gray-900 lg:text-2xl dark:text-white">
|
||||||
Редактировать комментарий
|
Редактировать комментарий
|
||||||
</p>
|
</p>
|
||||||
</ModalHeader>
|
</Modal.Header>
|
||||||
<ModalBody>
|
<Modal.Body>
|
||||||
<form className="flex flex-col gap-4" onSubmit={(e) => _sendComment(e)}>
|
<form className="flex flex-col gap-4" onSubmit={(e) => _sendComment(e)}>
|
||||||
<div>
|
<div>
|
||||||
<div className="block mb-2 sr-only">
|
<div className="block mb-2 sr-only">
|
||||||
<Label htmlFor="comment" >Редактировать ваш комментарий.</Label>
|
<Label htmlFor="comment" value="Редактировать ваш комментарий." />
|
||||||
</div>
|
</div>
|
||||||
<Textarea
|
<Textarea
|
||||||
id="comment"
|
id="comment"
|
||||||
|
@ -99,7 +99,7 @@ export const CommentsEditModal = (props: {
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</ModalBody>
|
</Modal.Body>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Button, Card, Modal, ModalHeader, Spinner } from "flowbite-react";
|
import { Card, Button, Modal, Spinner } from "flowbite-react";
|
||||||
import { CommentsComment } from "./Comments.Comment";
|
import { CommentsComment } from "./Comments.Comment";
|
||||||
import { useState, useEffect, useCallback } from "react";
|
import { useState, useEffect, useCallback } from "react";
|
||||||
import { ENDPOINTS } from "#/api/config";
|
import { ENDPOINTS } from "#/api/config";
|
||||||
|
@ -41,7 +41,7 @@ export const CommentsMain = (props: {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
{props.comments && props.comments.map((comment: any) => (
|
{props.comments.map((comment: any) => (
|
||||||
<CommentsComment
|
<CommentsComment
|
||||||
key={comment.id}
|
key={comment.id}
|
||||||
release_id={props.release_id}
|
release_id={props.release_id}
|
||||||
|
@ -149,7 +149,7 @@ const CommentsAllModal = (props: {
|
||||||
show={props.isOpen}
|
show={props.isOpen}
|
||||||
onClose={() => props.setIsOpen(false)}
|
onClose={() => props.setIsOpen(false)}
|
||||||
>
|
>
|
||||||
<ModalHeader>
|
<Modal.Header>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<h2 className="text-lg font-bold text-gray-900 lg:text-2xl dark:text-white">
|
<h2 className="text-lg font-bold text-gray-900 lg:text-2xl dark:text-white">
|
||||||
Все комментарии
|
Все комментарии
|
||||||
|
@ -158,7 +158,7 @@ const CommentsAllModal = (props: {
|
||||||
всего: {isLoading ? "загрузка..." : data[0].total_count}
|
всего: {isLoading ? "загрузка..." : data[0].total_count}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</ModalHeader>
|
</Modal.Header>
|
||||||
<div
|
<div
|
||||||
className="flex flex-col gap-2 p-4 overflow-y-auto"
|
className="flex flex-col gap-2 p-4 overflow-y-auto"
|
||||||
onScroll={handleScroll}
|
onScroll={handleScroll}
|
||||||
|
|
|
@ -1,87 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { Card } from "flowbite-react";
|
|
||||||
import { ReleaseLinkList } from "#/components/ReleaseLink/ReleaseLinkList";
|
|
||||||
import { useUserStore } from "#/store/auth";
|
|
||||||
import { ENDPOINTS } from "#/api/config";
|
|
||||||
import { BookmarksList, useSWRfetcher } from "#/api/utils";
|
|
||||||
import useSWR from "swr";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
|
|
||||||
export const ContinueWatching = () => {
|
|
||||||
const userStore = useUserStore();
|
|
||||||
|
|
||||||
function useFetchReleases(listName: string) {
|
|
||||||
let url: string;
|
|
||||||
if (userStore.token) {
|
|
||||||
url = `${ENDPOINTS.user.bookmark}/all/${BookmarksList[listName]}/0?sort=1&token=${userStore.token}`;
|
|
||||||
}
|
|
||||||
const { data, isLoading, error } = useSWR(url, useSWRfetcher);
|
|
||||||
return [data, isLoading, error];
|
|
||||||
}
|
|
||||||
|
|
||||||
const [watchingData, watchingLoading, watchingError] =
|
|
||||||
useFetchReleases("watching");
|
|
||||||
const [plannedData, plannedLoading, plannedError] =
|
|
||||||
useFetchReleases("planned");
|
|
||||||
const [delayedData, delayedLoading, delayedError] =
|
|
||||||
useFetchReleases("delayed");
|
|
||||||
|
|
||||||
const [releaseData, setReleaseData] = useState<any[]>([]);
|
|
||||||
|
|
||||||
const firstN = (arr, n = 1) => arr.slice(0, n);
|
|
||||||
function _randomize(array: any[], limit: number) {
|
|
||||||
const toRand = array.slice();
|
|
||||||
let currentIndex = toRand.length;
|
|
||||||
while (currentIndex != 0) {
|
|
||||||
let randomIndex = Math.floor(Math.random() * currentIndex);
|
|
||||||
currentIndex--;
|
|
||||||
[toRand[currentIndex], toRand[randomIndex]] = [
|
|
||||||
toRand[randomIndex],
|
|
||||||
toRand[currentIndex],
|
|
||||||
];
|
|
||||||
}
|
|
||||||
return firstN(toRand, limit);
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!watchingLoading && !plannedLoading && !delayedLoading) {
|
|
||||||
const data = [
|
|
||||||
...(watchingData.content || []),
|
|
||||||
...(plannedData.content || []),
|
|
||||||
...(delayedData.content || []),
|
|
||||||
];
|
|
||||||
const randomizedData = _randomize(data, 3);
|
|
||||||
setReleaseData(randomizedData);
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [watchingLoading, plannedLoading, delayedLoading]);
|
|
||||||
|
|
||||||
if (
|
|
||||||
!userStore.isAuth ||
|
|
||||||
watchingLoading ||
|
|
||||||
plannedLoading ||
|
|
||||||
delayedLoading ||
|
|
||||||
releaseData.length == 0
|
|
||||||
)
|
|
||||||
return <></>;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card>
|
|
||||||
<div className="flex justify-between py-2 border-b-2 border-black dark:border-white">
|
|
||||||
<h1>Продолжить просмотр</h1>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-4 mt-2">
|
|
||||||
{releaseData.map((release: any) => {
|
|
||||||
return (
|
|
||||||
<ReleaseLinkList
|
|
||||||
key={release.id}
|
|
||||||
{...release}
|
|
||||||
settings={{ showDescription: false }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,7 +1,7 @@
|
||||||
import React, { useRef } from "react";
|
import React, { useRef } from "react";
|
||||||
import Cropper, { ReactCropperElement } from "react-cropper";
|
import Cropper, { ReactCropperElement } from "react-cropper";
|
||||||
import "cropperjs/dist/cropper.css";
|
import "cropperjs/dist/cropper.css";
|
||||||
import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from "flowbite-react";
|
import { Button, Modal } from "flowbite-react";
|
||||||
|
|
||||||
type CropModalProps = {
|
type CropModalProps = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
|
@ -71,8 +71,8 @@ export const CropModal: React.FC<CropModalProps> = ({
|
||||||
}}
|
}}
|
||||||
size={"7xl"}
|
size={"7xl"}
|
||||||
>
|
>
|
||||||
<ModalHeader>Обрезать изображение</ModalHeader>
|
<Modal.Header>Обрезать изображение</Modal.Header>
|
||||||
<ModalBody>
|
<Modal.Body>
|
||||||
<Cropper
|
<Cropper
|
||||||
src={selectedImage}
|
src={selectedImage}
|
||||||
style={{ height: 400, width: "100%" }}
|
style={{ height: 400, width: "100%" }}
|
||||||
|
@ -95,8 +95,8 @@ export const CropModal: React.FC<CropModalProps> = ({
|
||||||
</p>
|
</p>
|
||||||
<p>Используйте колёсико мыши что-бы изменить масштаб</p>
|
<p>Используйте колёсико мыши что-бы изменить масштаб</p>
|
||||||
</div>
|
</div>
|
||||||
</ModalBody>
|
</Modal.Body>
|
||||||
<ModalFooter>
|
<Modal.Footer>
|
||||||
<Button
|
<Button
|
||||||
color={"blue"}
|
color={"blue"}
|
||||||
disabled={isActionsDisabled}
|
disabled={isActionsDisabled}
|
||||||
|
@ -120,7 +120,7 @@ export const CropModal: React.FC<CropModalProps> = ({
|
||||||
>
|
>
|
||||||
Отменить
|
Отменить
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</Modal.Footer>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,112 +1,49 @@
|
||||||
"use client";
|
"use client";
|
||||||
import { Button, ButtonGroup, Card } from "flowbite-react";
|
import { Card } from "flowbite-react";
|
||||||
import { ProfileActivityCollections } from "./Profile.ActivityCollections";
|
import Link from "next/link";
|
||||||
import { useEffect, useState } from "react";
|
import { numberDeclension } from "#/api/utils";
|
||||||
import { ProfileActivityFriends } from "./Profile.ActivityFriends";
|
|
||||||
import { ProfileActivityComment } from "./Profile.ActivityComment";
|
|
||||||
|
|
||||||
export function ProfileActivity(props: {
|
export function ProfileActivity(props: {
|
||||||
profile_id: number;
|
profile_id: number;
|
||||||
commentCount: number;
|
commentCount: number;
|
||||||
commentPreview: any;
|
videoCount: number;
|
||||||
collectionCount: number;
|
collectionCount: number;
|
||||||
collectionPreview: any;
|
|
||||||
friendsCount: number;
|
friendsCount: number;
|
||||||
friendsPreview: any;
|
|
||||||
token: string;
|
|
||||||
isMyProfile: boolean;
|
|
||||||
}) {
|
}) {
|
||||||
const [tab, setTab] = useState<"collections" | "comments" | "friends">(
|
|
||||||
"collections"
|
|
||||||
);
|
|
||||||
|
|
||||||
const [collections, setCollections] = useState<Record<number, any>>({});
|
|
||||||
|
|
||||||
function _setCollection(array: any[]) {
|
|
||||||
if (array && array.length == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let _coll = array.filter((col) => {
|
|
||||||
if (typeof col == "number") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
_coll.map((col) => {
|
|
||||||
setCollections((prev) => {
|
|
||||||
return {
|
|
||||||
...prev,
|
|
||||||
[col.id]: col,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
if (
|
|
||||||
col.creator.collections_preview &&
|
|
||||||
col.creator.collections_preview.length > 0
|
|
||||||
) {
|
|
||||||
_setCollection(col.creator.collections_preview || []);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
_setCollection(props.collectionPreview || []);
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [props.collectionPreview]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="overflow-hidden h-fit">
|
<Card className="h-fit">
|
||||||
<h1 className="text-2xl font-bold">Активность</h1>
|
<h1 className="text-2xl font-bold">Активность</h1>
|
||||||
<ButtonGroup>
|
<div className="flex items-center gap-4 text-lg">
|
||||||
<Button
|
<div>
|
||||||
color={tab == "collections" ? "blue" : "light"}
|
<p>
|
||||||
onClick={() => setTab("collections")}
|
{props.commentCount}{" "}
|
||||||
>
|
{numberDeclension(
|
||||||
<div className="flex flex-col gap-1 sm:flex-row sm:items-center">
|
props.commentCount,
|
||||||
<p>Коллекции</p>
|
"комментарий",
|
||||||
<p>( {props.collectionCount} )</p>
|
"комментария",
|
||||||
</div>
|
"комментариев"
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
color={tab == "comments" ? "blue" : "light"}
|
|
||||||
onClick={() => setTab("comments")}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col gap-1 sm:flex-row sm:items-center">
|
|
||||||
<p>Комментарии</p>
|
|
||||||
<p>( {props.commentCount} )</p>
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
color={tab == "friends" ? "blue" : "light"}
|
|
||||||
onClick={() => setTab("friends")}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col gap-1 sm:flex-row sm:items-center">
|
|
||||||
<p>Друзья</p>
|
|
||||||
<p>( {props.friendsCount} )</p>
|
|
||||||
</div>
|
|
||||||
</Button>
|
|
||||||
</ButtonGroup>
|
|
||||||
|
|
||||||
{tab == "collections" && (
|
|
||||||
<ProfileActivityCollections
|
|
||||||
content={Object.values(collections) || []}
|
|
||||||
profile_id={props.profile_id}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
{tab == "comments" && (
|
</p>
|
||||||
<ProfileActivityComment
|
<p className="mt-2">{props.videoCount} видео</p>
|
||||||
content={props.commentPreview || []}
|
</div>
|
||||||
profile_id={props.profile_id}
|
<div>
|
||||||
/>
|
<Link href={`/profile/${props.profile_id}/collections`}>
|
||||||
)}
|
<p className="border-b-2 border-gray-300 border-solid dark:border-gray-400 hover:border-gray-500 dark:hover:border-gray-200">
|
||||||
{tab == "friends" && (
|
{props.collectionCount}{" "}
|
||||||
<ProfileActivityFriends
|
{numberDeclension(
|
||||||
token={props.token}
|
props.commentCount,
|
||||||
content={props.friendsPreview || []}
|
"коллекция",
|
||||||
isMyProfile={props.isMyProfile}
|
"коллекции",
|
||||||
profile_id={props.profile_id}
|
"коллекций"
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
|
</p>
|
||||||
|
</Link>
|
||||||
|
<p className="mt-2">
|
||||||
|
{props.friendsCount}{" "}
|
||||||
|
{numberDeclension(props.commentCount, "друзей", "друга", "друзей")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,64 +0,0 @@
|
||||||
import { Swiper, SwiperSlide } from "swiper/react";
|
|
||||||
import "swiper/css";
|
|
||||||
import "swiper/css/navigation";
|
|
||||||
import "swiper/css/mousewheel";
|
|
||||||
import "swiper/css/scrollbar";
|
|
||||||
import { Navigation, Mousewheel, Scrollbar } from "swiper/modules";
|
|
||||||
import { CollectionLink } from "../CollectionLink/CollectionLink";
|
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
export const ProfileActivityCollections = (props: {
|
|
||||||
content: any;
|
|
||||||
profile_id: number;
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<div className="max-w-full">
|
|
||||||
<Swiper
|
|
||||||
modules={[Navigation, Mousewheel, Scrollbar]}
|
|
||||||
spaceBetween={8}
|
|
||||||
slidesPerView={"auto"}
|
|
||||||
direction={"horizontal"}
|
|
||||||
mousewheel={{
|
|
||||||
enabled: true,
|
|
||||||
sensitivity: 4,
|
|
||||||
}}
|
|
||||||
scrollbar={{
|
|
||||||
enabled: true,
|
|
||||||
draggable: true,
|
|
||||||
}}
|
|
||||||
allowTouchMove={true}
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
"--swiper-scrollbar-bottom": "0",
|
|
||||||
} as React.CSSProperties
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{props.content &&
|
|
||||||
props.content.length > 0 &&
|
|
||||||
props.content.map((collection) => {
|
|
||||||
return (
|
|
||||||
<SwiperSlide
|
|
||||||
key={`col-prev-${collection.id}`}
|
|
||||||
style={{ width: "fit-content" }}
|
|
||||||
>
|
|
||||||
<div className="w-[350px] xl:w-[500px] aspect-video">
|
|
||||||
<CollectionLink {...collection} />
|
|
||||||
</div>
|
|
||||||
</SwiperSlide>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
|
|
||||||
{props.content && props.content.length > 0 ?
|
|
||||||
<SwiperSlide style={{ width: "fit-content" }}>
|
|
||||||
<Link href={`/profile/${props.profile_id}/collections`}>
|
|
||||||
<div className="w-[350px] xl:w-[500px] flex flex-col items-center justify-center gap-2 text-black transition-colors bg-gray-100 border hover:bg-gray-200 border-gray-50 hover:border-gray-100 dark:border-gray-700 dark:hover:border-gray-600 dark:hover:bg-gray-500 aspect-video group dark:bg-gray-600 dark:text-white">
|
|
||||||
<span className="w-8 h-8 iconify mdi--arrow-right dark:fill-white"></span>
|
|
||||||
<p>Все коллекции</p>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
</SwiperSlide>
|
|
||||||
: <p className="text-lg">У пользователя нет коллекций</p>}
|
|
||||||
</Swiper>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,92 +0,0 @@
|
||||||
import Link from "next/link";
|
|
||||||
import Image from "next/image";
|
|
||||||
import { sinceUnixDate, unixToDate } from "#/api/utils";
|
|
||||||
|
|
||||||
export const ProfileActivityComment = (props: {
|
|
||||||
content: any;
|
|
||||||
profile_id: number;
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{props.content && props.content.length > 0 ?
|
|
||||||
props.content.map((comment) => {
|
|
||||||
let isHidden = comment.isSpoiler || comment.likes_count < -5 || false;
|
|
||||||
return (
|
|
||||||
<article
|
|
||||||
className="px-4 py-2 text-sm bg-gray-100 rounded-lg sm:text-base dark:bg-gray-900"
|
|
||||||
key={`comment-${comment.id}`}
|
|
||||||
>
|
|
||||||
<footer className="flex items-center justify-between mb-2">
|
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<Link
|
|
||||||
href={`/profile/${comment.profile.id}`}
|
|
||||||
className="inline-flex items-center mr-3 text-sm font-semibold text-gray-900 dark:text-white hover:underline"
|
|
||||||
>
|
|
||||||
<Image
|
|
||||||
className="w-6 h-6 mr-2 rounded-full"
|
|
||||||
width={24}
|
|
||||||
height={24}
|
|
||||||
src={comment.profile.avatar}
|
|
||||||
alt=""
|
|
||||||
/>
|
|
||||||
{comment.profile.login}
|
|
||||||
</Link>
|
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-400">
|
|
||||||
<time
|
|
||||||
dateTime={comment.timestamp.toString()}
|
|
||||||
title={unixToDate(comment.timestamp, "full")}
|
|
||||||
>
|
|
||||||
{sinceUnixDate(comment.timestamp)}
|
|
||||||
</time>
|
|
||||||
</p>
|
|
||||||
<p
|
|
||||||
className={`text-sm font-medium border px-1 py-0.5 rounded-md text-center ml-4 min-w-8 ${
|
|
||||||
comment.likes_count > 0 ?
|
|
||||||
"text-green-500 dark:text-green-400 border-green-500 dark:border-green-400"
|
|
||||||
: comment.likes_count < 0 ?
|
|
||||||
"text-red-500 dark:text-red-400 border-red-500 dark:border-red-400"
|
|
||||||
: "text-gray-500 dark:text-gray-400 border-gray-500 dark:border-gray-400"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{comment.likes_count}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
<div className="relative flex flex-col py-2">
|
|
||||||
{comment.release && typeof comment.release != "number" && (
|
|
||||||
<Link href={`/release/${comment.release.id}`}>
|
|
||||||
<p className="text-gray-900 whitespace-pre-wrap dark:text-gray-500">
|
|
||||||
{!comment.isDeleted ?
|
|
||||||
`К релизу: ${comment.release.title_ru || comment.release.title_alt || comment.release.title_original} (${comment.release.year || "?"}) >>`
|
|
||||||
: ""}
|
|
||||||
</p>
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
<p className="text-gray-800 whitespace-pre-wrap dark:text-gray-400">
|
|
||||||
{!comment.isDeleted ?
|
|
||||||
comment.message
|
|
||||||
: "Комментарий был удалён."}
|
|
||||||
</p>
|
|
||||||
{isHidden && (
|
|
||||||
<button
|
|
||||||
className="absolute top-0 bottom-0 left-0 right-0"
|
|
||||||
onClick={() => isHidden == false}
|
|
||||||
>
|
|
||||||
<div className="min-w-full min-h-full px-2 py-1.5 rounded-md bg-black text-white bg-opacity-50 backdrop-blur-[8px] flex flex-col justify-center items-center">
|
|
||||||
<p>
|
|
||||||
{comment.likes_count < -5 ?
|
|
||||||
"У комментария слишком низкий рейтинг."
|
|
||||||
: "Данный комментарий может содержать спойлер."}
|
|
||||||
</p>
|
|
||||||
<p className="font-bold">Нажмите, чтобы прочитать</p>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
: <p className="text-lg">Пользователь не оставлял комментарии</p>}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,87 +0,0 @@
|
||||||
import { Swiper, SwiperSlide } from "swiper/react";
|
|
||||||
import "swiper/css";
|
|
||||||
import "swiper/css/navigation";
|
|
||||||
import "swiper/css/mousewheel";
|
|
||||||
import "swiper/css/scrollbar";
|
|
||||||
import { Navigation, Mousewheel, Scrollbar } from "swiper/modules";
|
|
||||||
import Link from "next/link";
|
|
||||||
import { Avatar, Button } from "flowbite-react";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { ProfileFriendModal } from "./Profile.FriendsModal";
|
|
||||||
|
|
||||||
export const ProfileActivityFriends = (props: {
|
|
||||||
content: any;
|
|
||||||
token: string;
|
|
||||||
isMyProfile: boolean;
|
|
||||||
profile_id: number;
|
|
||||||
}) => {
|
|
||||||
const [isFriendModalOpen, setIsFriendModalOpen] = useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="max-w-full">
|
|
||||||
<Swiper
|
|
||||||
modules={[Navigation, Mousewheel, Scrollbar]}
|
|
||||||
spaceBetween={8}
|
|
||||||
slidesPerView={"auto"}
|
|
||||||
direction={"horizontal"}
|
|
||||||
mousewheel={{
|
|
||||||
enabled: true,
|
|
||||||
sensitivity: 4,
|
|
||||||
}}
|
|
||||||
scrollbar={{
|
|
||||||
enabled: true,
|
|
||||||
draggable: true,
|
|
||||||
}}
|
|
||||||
allowTouchMove={true}
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
"--swiper-scrollbar-bottom": "0",
|
|
||||||
} as React.CSSProperties
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{props.content &&
|
|
||||||
props.content.length > 0 &&
|
|
||||||
props.content.map((profile) => {
|
|
||||||
return (
|
|
||||||
<SwiperSlide
|
|
||||||
key={`friend-prev-${profile.id}`}
|
|
||||||
style={{ width: "fit-content" }}
|
|
||||||
className="px-2 py-4"
|
|
||||||
>
|
|
||||||
<Link href={`/profile/${profile.id}`}>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Avatar
|
|
||||||
img={profile.avatar}
|
|
||||||
size="md"
|
|
||||||
rounded={true}
|
|
||||||
bordered={true}
|
|
||||||
color={profile.is_online ? "success" : "light"}
|
|
||||||
className="flex-shrink-0"
|
|
||||||
/>
|
|
||||||
<p className="text-lg">{profile.login}</p>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
</SwiperSlide>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{(props.content && props.content.length > 0) || props.isMyProfile ?
|
|
||||||
<SwiperSlide style={{ width: "fit-content" }} className="px-2 py-4">
|
|
||||||
<Button onClick={() => setIsFriendModalOpen(true)}>
|
|
||||||
<p className="text-lg">Все друзья {props.isMyProfile ? "и заявки" : ""}</p>
|
|
||||||
<span className="w-8 h-8 iconify mdi--arrow-right dark:fill-white"></span>
|
|
||||||
</Button>
|
|
||||||
</SwiperSlide>
|
|
||||||
: <p className="text-lg">У пользователя нет друзей</p>}
|
|
||||||
</Swiper>
|
|
||||||
</div>
|
|
||||||
<ProfileFriendModal
|
|
||||||
isOpen={isFriendModalOpen}
|
|
||||||
setIsOpen={setIsFriendModalOpen}
|
|
||||||
token={props.token}
|
|
||||||
isMyProfile={props.isMyProfile}
|
|
||||||
profile_id={props.profile_id}
|
|
||||||
></ProfileFriendModal>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,177 +0,0 @@
|
||||||
import { ENDPOINTS } from "#/api/config";
|
|
||||||
import { tryCatchAPI, unixToDate, useSWRfetcher } from "#/api/utils";
|
|
||||||
import { Avatar, Button, Modal, ModalHeader, useThemeMode } from "flowbite-react";
|
|
||||||
import { useCallback, useEffect, useState } from "react";
|
|
||||||
import useSWRInfinite from "swr/infinite";
|
|
||||||
import { Spinner } from "../Spinner/Spinner";
|
|
||||||
import { toast } from "react-toastify";
|
|
||||||
|
|
||||||
export const ProfileBlockedUsersModal = (props: {
|
|
||||||
isOpen: boolean;
|
|
||||||
setIsOpen: (isOpen: boolean) => void;
|
|
||||||
token: string;
|
|
||||||
profile_id: number;
|
|
||||||
}) => {
|
|
||||||
const [currentRef, setCurrentRef] = useState<any>(null);
|
|
||||||
const theme = useThemeMode();
|
|
||||||
const [actionsDisabled, setActionsDisabled] = useState(false);
|
|
||||||
const [unblockedUsers, setUnblockedUsers] = useState([]);
|
|
||||||
|
|
||||||
const modalRef = useCallback((ref) => {
|
|
||||||
setCurrentRef(ref);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const getKey = (pageIndex: number, previousPageData: any) => {
|
|
||||||
if (previousPageData && !previousPageData.content.length) return null;
|
|
||||||
let url = `${ENDPOINTS.user.blocklist}/all/${pageIndex}?token=${props.token}`;
|
|
||||||
return url;
|
|
||||||
};
|
|
||||||
|
|
||||||
const { data, error, isLoading, size, setSize } = useSWRInfinite(
|
|
||||||
getKey,
|
|
||||||
useSWRfetcher,
|
|
||||||
{ initialSize: 2 }
|
|
||||||
);
|
|
||||||
|
|
||||||
async function _addToBlocklist(profile_id) {
|
|
||||||
setActionsDisabled(true);
|
|
||||||
|
|
||||||
const tid = toast.loading(
|
|
||||||
unblockedUsers.includes(profile_id) ?
|
|
||||||
"Блокируем пользователя..."
|
|
||||||
: "Разблокируем пользователя...",
|
|
||||||
{
|
|
||||||
position: "bottom-center",
|
|
||||||
hideProgressBar: true,
|
|
||||||
closeOnClick: false,
|
|
||||||
pauseOnHover: false,
|
|
||||||
draggable: false,
|
|
||||||
theme: theme.mode == "light" ? "light" : "dark",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
let url = `${ENDPOINTS.user.blocklist}`;
|
|
||||||
unblockedUsers.includes(profile_id) ?
|
|
||||||
(url += "/add/")
|
|
||||||
: (url += "/remove/");
|
|
||||||
url += `${profile_id}?token=${props.token}`;
|
|
||||||
|
|
||||||
const { data, error } = await tryCatchAPI(fetch(url));
|
|
||||||
if (error) {
|
|
||||||
toast.update(tid, {
|
|
||||||
render:
|
|
||||||
unblockedUsers.includes(profile_id) ?
|
|
||||||
"Ошибка блокировки"
|
|
||||||
: "Ошибка разблокировки",
|
|
||||||
type: "error",
|
|
||||||
autoClose: 2500,
|
|
||||||
isLoading: false,
|
|
||||||
closeOnClick: true,
|
|
||||||
draggable: true,
|
|
||||||
});
|
|
||||||
setActionsDisabled(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
toast.update(tid, {
|
|
||||||
render:
|
|
||||||
unblockedUsers.includes(profile_id) ?
|
|
||||||
"Пользователь заблокирован"
|
|
||||||
: "Пользователь разблокирован",
|
|
||||||
type: "success",
|
|
||||||
autoClose: 2500,
|
|
||||||
isLoading: false,
|
|
||||||
closeOnClick: true,
|
|
||||||
draggable: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (unblockedUsers.includes(profile_id)) {
|
|
||||||
setUnblockedUsers((prev) => {
|
|
||||||
return prev.filter((item) => item !== profile_id);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
setUnblockedUsers((prev) => {
|
|
||||||
return [...prev, profile_id];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
setActionsDisabled(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [content, setContent] = useState([]);
|
|
||||||
useEffect(() => {
|
|
||||||
if (data) {
|
|
||||||
let allReleases = [];
|
|
||||||
for (let i = 0; i < data.length; i++) {
|
|
||||||
allReleases.push(...data[i].content);
|
|
||||||
}
|
|
||||||
setContent(allReleases);
|
|
||||||
}
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
const [scrollPosition, setScrollPosition] = useState(0);
|
|
||||||
function handleScroll() {
|
|
||||||
const height = currentRef.scrollHeight - currentRef.clientHeight;
|
|
||||||
const windowScroll = currentRef.scrollTop;
|
|
||||||
const scrolled = (windowScroll / height) * 100;
|
|
||||||
setScrollPosition(Math.floor(scrolled));
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (scrollPosition >= 95 && scrollPosition <= 96) {
|
|
||||||
setSize(size + 1);
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [scrollPosition]);
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Modal
|
|
||||||
dismissible
|
|
||||||
show={props.isOpen}
|
|
||||||
onClose={() => props.setIsOpen(false)}
|
|
||||||
size={"7xl"}
|
|
||||||
>
|
|
||||||
<ModalHeader>Заблокированные пользователи</ModalHeader>
|
|
||||||
<div
|
|
||||||
className="flex flex-col gap-2 p-4 overflow-y-auto"
|
|
||||||
onScroll={handleScroll}
|
|
||||||
ref={modalRef}
|
|
||||||
>
|
|
||||||
{content && content.length > 0 ?
|
|
||||||
content.map((user) => {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-between gap-2" key={`blockeduser-${user.id}`}>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Avatar
|
|
||||||
alt=""
|
|
||||||
img={user.avatar}
|
|
||||||
rounded={true}
|
|
||||||
size={"md"}
|
|
||||||
bordered={true}
|
|
||||||
color={user.is_online ? "success" : "light"}
|
|
||||||
className="flex-shrink-0"
|
|
||||||
/>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<p className="text-lg font-semibold">{user.login}</p>
|
|
||||||
<p>Заблокирован: {unixToDate(user.added_date, "full")}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
color={!unblockedUsers.includes(user.id) ? "blue" : "red"}
|
|
||||||
onClick={() => _addToBlocklist(user.id)}
|
|
||||||
disabled={actionsDisabled}
|
|
||||||
className="flex-grow-0 h-fit"
|
|
||||||
>
|
|
||||||
{!unblockedUsers.includes(user.id) ?
|
|
||||||
"Разблокировать"
|
|
||||||
: "Заблокировать"}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
: "Нет заблокированных пользователей"}
|
|
||||||
{isLoading && <Spinner />}
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Button, Modal, ModalBody, ModalFooter, ModalHeader, Textarea, useThemeMode } from "flowbite-react";
|
import { Button, Modal, Textarea, useThemeMode } from "flowbite-react";
|
||||||
import { ENDPOINTS } from "#/api/config";
|
import { ENDPOINTS } from "#/api/config";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useSWRConfig } from "swr";
|
import { useSWRConfig } from "swr";
|
||||||
|
@ -138,8 +138,8 @@ export const ProfileEditLoginModal = (props: {
|
||||||
onClose={() => props.setIsOpen(false)}
|
onClose={() => props.setIsOpen(false)}
|
||||||
size={"4xl"}
|
size={"4xl"}
|
||||||
>
|
>
|
||||||
<ModalHeader>Изменить никнейм</ModalHeader>
|
<Modal.Header>Изменить никнейм</Modal.Header>
|
||||||
<ModalBody>
|
<Modal.Body>
|
||||||
{loading ?
|
{loading ?
|
||||||
<div className="flex items-center justify-center py-8">
|
<div className="flex items-center justify-center py-8">
|
||||||
<Spinner />
|
<Spinner />
|
||||||
|
@ -173,8 +173,8 @@ export const ProfileEditLoginModal = (props: {
|
||||||
}
|
}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
</ModalBody>
|
</Modal.Body>
|
||||||
<ModalFooter>
|
<Modal.Footer>
|
||||||
{_loginData.is_change_available && (
|
{_loginData.is_change_available && (
|
||||||
<Button
|
<Button
|
||||||
color="blue"
|
color="blue"
|
||||||
|
@ -191,7 +191,7 @@ export const ProfileEditLoginModal = (props: {
|
||||||
>
|
>
|
||||||
Отмена
|
Отмена
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</Modal.Footer>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { FileInput, Label, Modal, ModalBody, ModalHeader, useThemeMode } from "flowbite-react";
|
import { FileInput, Label, Modal, useThemeMode } from "flowbite-react";
|
||||||
import { Spinner } from "../Spinner/Spinner";
|
import { Spinner } from "../Spinner/Spinner";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { ENDPOINTS } from "#/api/config";
|
import { ENDPOINTS } from "#/api/config";
|
||||||
|
@ -14,7 +14,6 @@ import { useSWRConfig } from "swr";
|
||||||
import { useUserStore } from "#/store/auth";
|
import { useUserStore } from "#/store/auth";
|
||||||
import { ProfileEditLoginModal } from "./Profile.EditLoginModal";
|
import { ProfileEditLoginModal } from "./Profile.EditLoginModal";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { ProfileBlockedUsersModal } from "./Profile.BlockedUsersModal";
|
|
||||||
|
|
||||||
export const ProfileEditModal = (props: {
|
export const ProfileEditModal = (props: {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
|
@ -26,7 +25,6 @@ export const ProfileEditModal = (props: {
|
||||||
const [statusModalOpen, setStatusModalOpen] = useState(false);
|
const [statusModalOpen, setStatusModalOpen] = useState(false);
|
||||||
const [socialModalOpen, setSocialModalOpen] = useState(false);
|
const [socialModalOpen, setSocialModalOpen] = useState(false);
|
||||||
const [loginModalOpen, setLoginModalOpen] = useState(false);
|
const [loginModalOpen, setLoginModalOpen] = useState(false);
|
||||||
const [blockedModalOpen, setBlockedModalOpen] = useState(false);
|
|
||||||
const [privacyModalSetting, setPrivacyModalSetting] = useState("none");
|
const [privacyModalSetting, setPrivacyModalSetting] = useState("none");
|
||||||
const [privacySettings, setPrivacySettings] = useState({
|
const [privacySettings, setPrivacySettings] = useState({
|
||||||
privacy_stats: 9,
|
privacy_stats: 9,
|
||||||
|
@ -188,7 +186,6 @@ export const ProfileEditModal = (props: {
|
||||||
if (avatarModalProps.croppedImage) {
|
if (avatarModalProps.croppedImage) {
|
||||||
_uploadAvatar();
|
_uploadAvatar();
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [avatarModalProps.croppedImage]);
|
}, [avatarModalProps.croppedImage]);
|
||||||
|
|
||||||
if (!prefData || !loginData || prefError || loginError) {
|
if (!prefData || !loginData || prefError || loginError) {
|
||||||
|
@ -202,8 +199,8 @@ export const ProfileEditModal = (props: {
|
||||||
onClose={() => props.setIsOpen(false)}
|
onClose={() => props.setIsOpen(false)}
|
||||||
size={"7xl"}
|
size={"7xl"}
|
||||||
>
|
>
|
||||||
<ModalHeader>Редактирование профиля</ModalHeader>
|
<Modal.Header>Редактирование профиля</Modal.Header>
|
||||||
<ModalBody>
|
<Modal.Body>
|
||||||
{prefLoading ?
|
{prefLoading ?
|
||||||
<Spinner />
|
<Spinner />
|
||||||
: <div className="flex flex-col gap-4">
|
: <div className="flex flex-col gap-4">
|
||||||
|
@ -358,18 +355,13 @@ export const ProfileEditModal = (props: {
|
||||||
}
|
}
|
||||||
</p>
|
</p>
|
||||||
</button>
|
</button>
|
||||||
<button
|
{/* <button className="p-2 text-left rounded-md hover:bg-gray-100 dark:hover:bg-gray-900">
|
||||||
className="p-2 text-left rounded-md hover:bg-gray-100 dark:hover:bg-gray-900"
|
|
||||||
onClick={() => {
|
|
||||||
setBlockedModalOpen(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<p className="text-lg">Блоклист</p>
|
<p className="text-lg">Блоклист</p>
|
||||||
<p className="text-base text-gray-500 dark:text-gray-400">
|
<p className="text-base text-gray-500 dark:text-gray-400">
|
||||||
Список пользователей, которым запрещён доступ к вашей
|
Список пользователей, которым запрещён доступ к вашей
|
||||||
странице
|
странице
|
||||||
</p>
|
</p>
|
||||||
</button>
|
</button> */}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
|
@ -395,7 +387,7 @@ export const ProfileEditModal = (props: {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</ModalBody>
|
</Modal.Body>
|
||||||
</Modal>
|
</Modal>
|
||||||
{props.token ?
|
{props.token ?
|
||||||
<>
|
<>
|
||||||
|
@ -439,12 +431,6 @@ export const ProfileEditModal = (props: {
|
||||||
setLogin={setLogin}
|
setLogin={setLogin}
|
||||||
profile_id={props.profile_id}
|
profile_id={props.profile_id}
|
||||||
/>
|
/>
|
||||||
<ProfileBlockedUsersModal
|
|
||||||
isOpen={blockedModalOpen}
|
|
||||||
setIsOpen={setBlockedModalOpen}
|
|
||||||
token={props.token}
|
|
||||||
profile_id={props.profile_id}
|
|
||||||
/>
|
|
||||||
</>
|
</>
|
||||||
: ""}
|
: ""}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Modal, ModalBody, ModalHeader, useThemeMode } from "flowbite-react";
|
import { Modal, useThemeMode } from "flowbite-react";
|
||||||
import { ENDPOINTS } from "#/api/config";
|
import { ENDPOINTS } from "#/api/config";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
|
@ -98,8 +98,8 @@ export const ProfileEditPrivacyModal = (props: {
|
||||||
onClose={() => props.setIsOpen(false)}
|
onClose={() => props.setIsOpen(false)}
|
||||||
size={"4xl"}
|
size={"4xl"}
|
||||||
>
|
>
|
||||||
<ModalHeader>{setting_text[props.setting]}</ModalHeader>
|
<Modal.Header>{setting_text[props.setting]}</Modal.Header>
|
||||||
<ModalBody>
|
<Modal.Body>
|
||||||
{props.setting != "none" ?
|
{props.setting != "none" ?
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
|
@ -202,7 +202,7 @@ export const ProfileEditPrivacyModal = (props: {
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
: ""}
|
: ""}
|
||||||
</ModalBody>
|
</Modal.Body>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,15 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {
|
import { Button, Modal, Label, TextInput, useThemeMode } from "flowbite-react";
|
||||||
Button,
|
|
||||||
Label,
|
|
||||||
Modal,
|
|
||||||
ModalBody,
|
|
||||||
ModalFooter,
|
|
||||||
ModalHeader,
|
|
||||||
TextInput,
|
|
||||||
useThemeMode,
|
|
||||||
} from "flowbite-react";
|
|
||||||
import { Spinner } from "../Spinner/Spinner";
|
import { Spinner } from "../Spinner/Spinner";
|
||||||
import { ENDPOINTS } from "#/api/config";
|
import { ENDPOINTS } from "#/api/config";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
@ -173,8 +164,8 @@ export const ProfileEditSocialModal = (props: {
|
||||||
onClose={() => props.setIsOpen(false)}
|
onClose={() => props.setIsOpen(false)}
|
||||||
size={"4xl"}
|
size={"4xl"}
|
||||||
>
|
>
|
||||||
<ModalHeader>Соц. сети</ModalHeader>
|
<Modal.Header>Соц. сети</Modal.Header>
|
||||||
<ModalBody>
|
<Modal.Body>
|
||||||
<p className="p-2 text-gray-400 border-2 border-gray-200 rounded-md dark:border-gray-500 dark:text-gray-300">
|
<p className="p-2 text-gray-400 border-2 border-gray-200 rounded-md dark:border-gray-500 dark:text-gray-300">
|
||||||
Укажите ссылки на свои социальные сети, чтобы другие пользователи
|
Укажите ссылки на свои социальные сети, чтобы другие пользователи
|
||||||
могли с вами связаться
|
могли с вами связаться
|
||||||
|
@ -186,7 +177,7 @@ export const ProfileEditSocialModal = (props: {
|
||||||
: <div className="flex flex-col gap-4 py-4">
|
: <div className="flex flex-col gap-4 py-4">
|
||||||
<div>
|
<div>
|
||||||
<div className="block mb-2">
|
<div className="block mb-2">
|
||||||
<Label htmlFor="vk-page">ВКонтакте</Label>
|
<Label htmlFor="vk-page" value="ВКонтакте" />
|
||||||
</div>
|
</div>
|
||||||
<TextInput
|
<TextInput
|
||||||
id="vk-page"
|
id="vk-page"
|
||||||
|
@ -198,7 +189,7 @@ export const ProfileEditSocialModal = (props: {
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="block mb-2">
|
<div className="block mb-2">
|
||||||
<Label htmlFor="tg-page">Telegram</Label>
|
<Label htmlFor="tg-page" value="Telegram" />
|
||||||
</div>
|
</div>
|
||||||
<TextInput
|
<TextInput
|
||||||
id="tg-page"
|
id="tg-page"
|
||||||
|
@ -210,7 +201,7 @@ export const ProfileEditSocialModal = (props: {
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="block mb-2">
|
<div className="block mb-2">
|
||||||
<Label htmlFor="discord-page">Discord</Label>
|
<Label htmlFor="discord-page" value="Discord" />
|
||||||
</div>
|
</div>
|
||||||
<TextInput
|
<TextInput
|
||||||
id="discord-page"
|
id="discord-page"
|
||||||
|
@ -222,7 +213,7 @@ export const ProfileEditSocialModal = (props: {
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="block mb-2">
|
<div className="block mb-2">
|
||||||
<Label htmlFor="inst-page">Instagram</Label>
|
<Label htmlFor="inst-page" value="Instagram" />
|
||||||
</div>
|
</div>
|
||||||
<TextInput
|
<TextInput
|
||||||
id="inst-page"
|
id="inst-page"
|
||||||
|
@ -234,7 +225,7 @@ export const ProfileEditSocialModal = (props: {
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="block mb-2">
|
<div className="block mb-2">
|
||||||
<Label htmlFor="tt-page">TikTok</Label>
|
<Label htmlFor="tt-page" value="TikTok" />
|
||||||
</div>
|
</div>
|
||||||
<TextInput
|
<TextInput
|
||||||
id="tt-page"
|
id="tt-page"
|
||||||
|
@ -246,8 +237,8 @@ export const ProfileEditSocialModal = (props: {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</ModalBody>
|
</Modal.Body>
|
||||||
<ModalFooter>
|
<Modal.Footer>
|
||||||
<Button
|
<Button
|
||||||
color="blue"
|
color="blue"
|
||||||
onClick={() => _setSocialSetting()}
|
onClick={() => _setSocialSetting()}
|
||||||
|
@ -262,7 +253,7 @@ export const ProfileEditSocialModal = (props: {
|
||||||
>
|
>
|
||||||
Отмена
|
Отмена
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</Modal.Footer>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Button, Modal, ModalBody, ModalFooter, ModalHeader, Textarea, useThemeMode } from "flowbite-react";
|
import { Button, Modal, Textarea, useThemeMode } from "flowbite-react";
|
||||||
import { ENDPOINTS } from "#/api/config";
|
import { ENDPOINTS } from "#/api/config";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useSWRConfig } from "swr";
|
import { useSWRConfig } from "swr";
|
||||||
|
@ -96,8 +96,8 @@ export const ProfileEditStatusModal = (props: {
|
||||||
onClose={() => props.setIsOpen(false)}
|
onClose={() => props.setIsOpen(false)}
|
||||||
size={"4xl"}
|
size={"4xl"}
|
||||||
>
|
>
|
||||||
<ModalHeader>Изменить статус</ModalHeader>
|
<Modal.Header>Изменить статус</Modal.Header>
|
||||||
<ModalBody>
|
<Modal.Body>
|
||||||
<Textarea
|
<Textarea
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
rows={3}
|
rows={3}
|
||||||
|
@ -111,8 +111,8 @@ export const ProfileEditStatusModal = (props: {
|
||||||
<p className="text-sm text-right text-gray-500 dark:text-gray-300">
|
<p className="text-sm text-right text-gray-500 dark:text-gray-300">
|
||||||
{_stringLength}/80
|
{_stringLength}/80
|
||||||
</p>
|
</p>
|
||||||
</ModalBody>
|
</Modal.Body>
|
||||||
<ModalFooter>
|
<Modal.Footer>
|
||||||
<Button
|
<Button
|
||||||
color="blue"
|
color="blue"
|
||||||
onClick={() => _setStatusSetting()}
|
onClick={() => _setStatusSetting()}
|
||||||
|
@ -123,7 +123,7 @@ export const ProfileEditStatusModal = (props: {
|
||||||
<Button color="red" onClick={() => props.setIsOpen(false)}>
|
<Button color="red" onClick={() => props.setIsOpen(false)}>
|
||||||
Отмена
|
Отмена
|
||||||
</Button>
|
</Button>
|
||||||
</ModalFooter>
|
</Modal.Footer>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,346 +0,0 @@
|
||||||
import { ENDPOINTS } from "#/api/config";
|
|
||||||
import { tryCatchAPI, unixToDate, useSWRfetcher } from "#/api/utils";
|
|
||||||
import {
|
|
||||||
Avatar,
|
|
||||||
Button,
|
|
||||||
Modal,
|
|
||||||
ModalHeader,
|
|
||||||
useThemeMode,
|
|
||||||
} from "flowbite-react";
|
|
||||||
import { useCallback, useEffect, useState } from "react";
|
|
||||||
import useSWRInfinite from "swr/infinite";
|
|
||||||
import { Spinner } from "../Spinner/Spinner";
|
|
||||||
import { toast } from "react-toastify";
|
|
||||||
import useSWR, { mutate } from "swr";
|
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
export const ProfileFriendModal = (props: {
|
|
||||||
isOpen: boolean;
|
|
||||||
setIsOpen: (isOpen: boolean) => void;
|
|
||||||
token: string;
|
|
||||||
isMyProfile: boolean;
|
|
||||||
profile_id: number;
|
|
||||||
}) => {
|
|
||||||
const [currentRef, setCurrentRef] = useState<any>(null);
|
|
||||||
const theme = useThemeMode();
|
|
||||||
const [actionsDisabled, setActionsDisabled] = useState(false);
|
|
||||||
// const [requestInUsers, setRequestInUsers] = useState([]);
|
|
||||||
// const [requestOutUsers, setRequestOutUsers] = useState([]);
|
|
||||||
const [friends, setFriends] = useState([]);
|
|
||||||
|
|
||||||
const modalRef = useCallback((ref) => {
|
|
||||||
setCurrentRef(ref);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const useFetchRequests = (url: string) => {
|
|
||||||
const { data, error, isLoading } = useSWR(url, useSWRfetcher);
|
|
||||||
return [data, error, isLoading];
|
|
||||||
};
|
|
||||||
|
|
||||||
const [requestInUsersData, requestInUsersError, requestInUsersIsLoading] =
|
|
||||||
useFetchRequests(
|
|
||||||
props.isMyProfile ?
|
|
||||||
`${ENDPOINTS.user.friend.in}/last?token=${props.token}&count=8`
|
|
||||||
: ""
|
|
||||||
);
|
|
||||||
|
|
||||||
const [requestOutUsersData, requestOutUsersError, requestOutUsersIsLoading] =
|
|
||||||
useFetchRequests(
|
|
||||||
props.isMyProfile ?
|
|
||||||
`${ENDPOINTS.user.friend.out}/last?token=${props.token}&count=8`
|
|
||||||
: ""
|
|
||||||
);
|
|
||||||
|
|
||||||
async function _hideRequestIn(profile_id) {
|
|
||||||
const tid = toast.loading("Скрываем заявку...", {
|
|
||||||
position: "bottom-center",
|
|
||||||
hideProgressBar: true,
|
|
||||||
closeOnClick: false,
|
|
||||||
pauseOnHover: false,
|
|
||||||
draggable: false,
|
|
||||||
theme: theme.mode == "light" ? "light" : "dark",
|
|
||||||
});
|
|
||||||
|
|
||||||
let url = `${ENDPOINTS.user.friend.hide}/${profile_id}?token=${props.token}`;
|
|
||||||
const { data, error } = await tryCatchAPI(fetch(url));
|
|
||||||
if (error) {
|
|
||||||
toast.update(tid, {
|
|
||||||
render: "Ошибка скрытия заявки",
|
|
||||||
type: "error",
|
|
||||||
autoClose: 2500,
|
|
||||||
isLoading: false,
|
|
||||||
closeOnClick: true,
|
|
||||||
draggable: true,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
toast.update(tid, {
|
|
||||||
render: "Заявка скрыта",
|
|
||||||
type: "success",
|
|
||||||
autoClose: 2500,
|
|
||||||
isLoading: false,
|
|
||||||
closeOnClick: true,
|
|
||||||
draggable: true,
|
|
||||||
});
|
|
||||||
mutate(`${ENDPOINTS.user.friend.in}/last?token=${props.token}&count=8`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function _acceptRequestIn(profile_id) {
|
|
||||||
const tid = toast.loading("Принимаем запрос...", {
|
|
||||||
position: "bottom-center",
|
|
||||||
hideProgressBar: true,
|
|
||||||
closeOnClick: false,
|
|
||||||
pauseOnHover: false,
|
|
||||||
draggable: false,
|
|
||||||
theme: theme.mode == "light" ? "light" : "dark",
|
|
||||||
});
|
|
||||||
|
|
||||||
let url = `${ENDPOINTS.user.friend.add}/${profile_id}?token=${props.token}`;
|
|
||||||
const { data, error } = await tryCatchAPI(fetch(url));
|
|
||||||
if (error) {
|
|
||||||
toast.update(tid, {
|
|
||||||
render: "Ошибка приёма запроса",
|
|
||||||
type: "error",
|
|
||||||
autoClose: 2500,
|
|
||||||
isLoading: false,
|
|
||||||
closeOnClick: true,
|
|
||||||
draggable: true,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
toast.update(tid, {
|
|
||||||
render: "Запрос принят",
|
|
||||||
type: "success",
|
|
||||||
autoClose: 2500,
|
|
||||||
isLoading: false,
|
|
||||||
closeOnClick: true,
|
|
||||||
draggable: true,
|
|
||||||
});
|
|
||||||
mutate(`${ENDPOINTS.user.friend.in}/last?token=${props.token}&count=8`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function _cancelRequestOut(profile_id) {
|
|
||||||
const tid = toast.loading("Отменяем запрос...", {
|
|
||||||
position: "bottom-center",
|
|
||||||
hideProgressBar: true,
|
|
||||||
closeOnClick: false,
|
|
||||||
pauseOnHover: false,
|
|
||||||
draggable: false,
|
|
||||||
theme: theme.mode == "light" ? "light" : "dark",
|
|
||||||
});
|
|
||||||
|
|
||||||
let url = `${ENDPOINTS.user.friend.remove}/${profile_id}?token=${props.token}`;
|
|
||||||
const { data, error } = await tryCatchAPI(fetch(url));
|
|
||||||
if (error) {
|
|
||||||
toast.update(tid, {
|
|
||||||
render: "Ошибка отмена запроса",
|
|
||||||
type: "error",
|
|
||||||
autoClose: 2500,
|
|
||||||
isLoading: false,
|
|
||||||
closeOnClick: true,
|
|
||||||
draggable: true,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
toast.update(tid, {
|
|
||||||
render: "Запрос отменён",
|
|
||||||
type: "success",
|
|
||||||
autoClose: 2500,
|
|
||||||
isLoading: false,
|
|
||||||
closeOnClick: true,
|
|
||||||
draggable: true,
|
|
||||||
});
|
|
||||||
mutate(`${ENDPOINTS.user.friend.out}/last?token=${props.token}&count=8`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const getKey = (pageIndex: number, previousPageData: any) => {
|
|
||||||
if (previousPageData && !previousPageData.content.length) return null;
|
|
||||||
let url = `${ENDPOINTS.user.friend.list}/${props.profile_id}/${pageIndex}?token=${props.token}`;
|
|
||||||
return url;
|
|
||||||
};
|
|
||||||
|
|
||||||
const { data, error, isLoading, size, setSize } = useSWRInfinite(
|
|
||||||
getKey,
|
|
||||||
useSWRfetcher,
|
|
||||||
{ initialSize: 2 }
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (data) {
|
|
||||||
let allFriends = [];
|
|
||||||
for (let i = 0; i < data.length; i++) {
|
|
||||||
allFriends.push(...data[i].content);
|
|
||||||
}
|
|
||||||
setFriends(allFriends);
|
|
||||||
}
|
|
||||||
}, [data]);
|
|
||||||
|
|
||||||
const [scrollPosition, setScrollPosition] = useState(0);
|
|
||||||
function handleScroll() {
|
|
||||||
const height = currentRef.scrollHeight - currentRef.clientHeight;
|
|
||||||
const windowScroll = currentRef.scrollTop;
|
|
||||||
const scrolled = (windowScroll / height) * 100;
|
|
||||||
setScrollPosition(Math.floor(scrolled));
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (scrollPosition >= 95 && scrollPosition <= 96) {
|
|
||||||
setSize(size + 1);
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [scrollPosition]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Modal
|
|
||||||
dismissible
|
|
||||||
show={props.isOpen}
|
|
||||||
onClose={() => props.setIsOpen(false)}
|
|
||||||
size={"4xl"}
|
|
||||||
>
|
|
||||||
<ModalHeader>Друзья</ModalHeader>
|
|
||||||
<div
|
|
||||||
className="flex flex-col gap-4 p-4 overflow-y-auto"
|
|
||||||
onScroll={handleScroll}
|
|
||||||
ref={modalRef}
|
|
||||||
>
|
|
||||||
{props.isMyProfile && (
|
|
||||||
<>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<p className="text-lg font-semibold">Входящие заявки</p>
|
|
||||||
{(
|
|
||||||
requestInUsersData &&
|
|
||||||
requestInUsersData.content &&
|
|
||||||
requestInUsersData.content.length > 0
|
|
||||||
) ?
|
|
||||||
requestInUsersData.content.map((user) => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="flex items-center justify-between gap-2"
|
|
||||||
key={`friend_req_in-${user.id}`}
|
|
||||||
>
|
|
||||||
<Link href={`/profile/${user.id}`}>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Avatar
|
|
||||||
alt=""
|
|
||||||
img={user.avatar}
|
|
||||||
rounded={true}
|
|
||||||
size={"md"}
|
|
||||||
bordered={true}
|
|
||||||
color={user.is_online ? "success" : "light"}
|
|
||||||
className="flex-shrink-0"
|
|
||||||
/>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<p className="text-lg font-semibold">
|
|
||||||
{user.login}
|
|
||||||
</p>
|
|
||||||
<p>Друзей: {user.friend_count}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button
|
|
||||||
color="blue"
|
|
||||||
onClick={() => _acceptRequestIn(user.id)}
|
|
||||||
>
|
|
||||||
Принять
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
color="light"
|
|
||||||
onClick={() => _hideRequestIn(user.id)}
|
|
||||||
>
|
|
||||||
Скрыть
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
: <p className="text-sm">Нет входящих заявок</p>}
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<p className="text-lg font-semibold">Исходящие заявки</p>
|
|
||||||
{(
|
|
||||||
requestOutUsersData &&
|
|
||||||
requestOutUsersData.content &&
|
|
||||||
requestOutUsersData.content.length > 0
|
|
||||||
) ?
|
|
||||||
requestOutUsersData.content.map((user) => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="flex items-center justify-between gap-2"
|
|
||||||
key={`friend_req_out-${user.id}`}
|
|
||||||
>
|
|
||||||
<Link href={`/profile/${user.id}`}>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Avatar
|
|
||||||
alt=""
|
|
||||||
img={user.avatar}
|
|
||||||
rounded={true}
|
|
||||||
size={"md"}
|
|
||||||
bordered={true}
|
|
||||||
color={user.is_online ? "success" : "light"}
|
|
||||||
className="flex-shrink-0"
|
|
||||||
/>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<p className="text-lg font-semibold">
|
|
||||||
{user.login}
|
|
||||||
</p>
|
|
||||||
<p>Друзей: {user.friend_count}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button
|
|
||||||
color="light"
|
|
||||||
onClick={() => _cancelRequestOut(user.id)}
|
|
||||||
>
|
|
||||||
Отменить
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
: <p className="text-sm">Нет исходящих заявок</p>}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<p className="text-lg font-semibold">Все друзья</p>
|
|
||||||
{friends && friends.length > 0 ?
|
|
||||||
friends.map((user) => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="flex items-center justify-between gap-2"
|
|
||||||
key={`friend-${user.id}`}
|
|
||||||
>
|
|
||||||
<Link href={`/profile/${user.id}`}>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Avatar
|
|
||||||
alt=""
|
|
||||||
img={user.avatar}
|
|
||||||
rounded={true}
|
|
||||||
size={"md"}
|
|
||||||
bordered={true}
|
|
||||||
color={user.is_online ? "success" : "light"}
|
|
||||||
className="flex-shrink-0"
|
|
||||||
/>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<p className="text-lg font-semibold">{user.login}</p>
|
|
||||||
<p>Друзей: {user.friend_count}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
: <p className="text-sm">Нет друзей</p>}
|
|
||||||
</div>
|
|
||||||
{isLoading && <Spinner />}
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,79 +1,49 @@
|
||||||
import { Card } from "flowbite-react";
|
import { Card, Carousel } from "flowbite-react";
|
||||||
|
import type {
|
||||||
|
FlowbiteCarouselIndicatorsTheme,
|
||||||
|
FlowbiteCarouselControlTheme,
|
||||||
|
CustomFlowbiteTheme,
|
||||||
|
} from "flowbite-react";
|
||||||
|
import { ReleaseLink } from "../ReleaseLink/ReleaseLinkUpdate";
|
||||||
|
|
||||||
import { ReleaseChips } from "../ReleasePoster/Chips";
|
const CarouselIndicatorsTheme: FlowbiteCarouselIndicatorsTheme = {
|
||||||
import { Poster } from "../ReleasePoster/Poster";
|
active: {
|
||||||
import Link from "next/link";
|
off: "bg-gray-400/50 hover:bg-gray-200",
|
||||||
|
on: "bg-gray-200",
|
||||||
|
},
|
||||||
|
base: "h-3 w-3 rounded-full max-w-[300px]",
|
||||||
|
wrapper: "absolute bottom-5 left-1/2 flex -translate-x-1/2 space-x-3",
|
||||||
|
};
|
||||||
|
|
||||||
const profile_lists = {
|
const CarouselControlsTheme: FlowbiteCarouselControlTheme = {
|
||||||
// 0: "Не смотрю",
|
base: "inline-flex h-8 w-8 items-center justify-center rounded-full group-focus:outline-none group-focus:ring-4 bg-gray-400/30 group-hover:bg-gray-400/60 group-focus:ring-gray-400/70 sm:h-10 sm:w-10",
|
||||||
1: { name: "Смотрю", bg_color: "bg-green-500" },
|
icon: "h-5 w-5 text-gray-400 sm:h-6 sm:w-6",
|
||||||
2: { name: "В планах", bg_color: "bg-purple-500" },
|
};
|
||||||
3: { name: "Просмотрено", bg_color: "bg-blue-500" },
|
|
||||||
4: { name: "Отложено", bg_color: "bg-yellow-500" },
|
const CarouselTheme: CustomFlowbiteTheme["carousel"] = {
|
||||||
5: { name: "Брошено", bg_color: "bg-red-500" },
|
root: {
|
||||||
|
base: "relative h-full w-full max-w-[375px]",
|
||||||
|
},
|
||||||
|
indicators: CarouselIndicatorsTheme,
|
||||||
|
control: CarouselControlsTheme,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ProfileReleaseHistory = (props: any) => {
|
export const ProfileReleaseHistory = (props: any) => {
|
||||||
return (
|
return (
|
||||||
<Card className="h-fit">
|
<Card className="h-fit">
|
||||||
<h1 className="text-2xl font-bold">Недавно просмотренные</h1>
|
<h1 className="text-2xl font-bold">Недавно просмотренные</h1>
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex justify-center">
|
||||||
|
<Carousel theme={CarouselTheme}>
|
||||||
{props.history.map((release) => {
|
{props.history.map((release) => {
|
||||||
const genres = [];
|
|
||||||
const grade = release.grade ? Number(release.grade.toFixed(1)) : null;
|
|
||||||
const profile_list_status = release.profile_list_status || null;
|
|
||||||
let user_list = null;
|
|
||||||
if (profile_list_status != null || profile_list_status != 0) {
|
|
||||||
user_list = profile_lists[profile_list_status];
|
|
||||||
}
|
|
||||||
if (release.genres) {
|
|
||||||
const genres_array = release.genres.split(",");
|
|
||||||
genres_array.forEach((genre) => {
|
|
||||||
genres.push(genre.trim());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<Link href={`/release/${release.id}`} key={`history-${release.id}`}>
|
<ReleaseLink
|
||||||
<div className="flex gap-2">
|
key={`history-${release.id}`}
|
||||||
<div className="flex-shrink-0 w-32">
|
|
||||||
<Poster image={release.image} className="h-auto" />
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<ReleaseChips
|
|
||||||
{...release}
|
{...release}
|
||||||
user_list={user_list}
|
chipsSettings={{ lastWatchedHidden: false }}
|
||||||
grade={grade}
|
|
||||||
settings={{ lastWatchedHidden: false }}
|
|
||||||
/>
|
/>
|
||||||
<div>
|
|
||||||
{genres.length > 0 &&
|
|
||||||
genres.map((genre: string, index: number) => {
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
key={`release_${props.id}_genre_${genre}_${index}`}
|
|
||||||
className="text-sm font-light dark:text-white"
|
|
||||||
>
|
|
||||||
{index > 0 && ", "}
|
|
||||||
{genre}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
{release.title_ru && (
|
|
||||||
<p className="text-lg font-bold dark:text-white">
|
|
||||||
{release.title_ru}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{release.title_original && (
|
|
||||||
<p className="text-sm text-gray-600 dark:text-gray-300">
|
|
||||||
{release.title_original}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
</Carousel>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,5 +1,16 @@
|
||||||
import { Button, Card, Modal, ModalHeader, Rating, RatingStar } from "flowbite-react";
|
import {
|
||||||
|
Card,
|
||||||
|
Carousel,
|
||||||
|
RatingStar,
|
||||||
|
Rating,
|
||||||
|
Modal,
|
||||||
|
Button,
|
||||||
|
} from "flowbite-react";
|
||||||
|
import type {
|
||||||
|
FlowbiteCarouselIndicatorsTheme,
|
||||||
|
FlowbiteCarouselControlTheme,
|
||||||
|
} from "flowbite-react";
|
||||||
|
import Image from "next/image";
|
||||||
import { unixToDate, useSWRfetcher } from "#/api/utils";
|
import { unixToDate, useSWRfetcher } from "#/api/utils";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
@ -8,6 +19,28 @@ import useSWRInfinite from "swr/infinite";
|
||||||
import { Spinner } from "../Spinner/Spinner";
|
import { Spinner } from "../Spinner/Spinner";
|
||||||
import { Poster } from "../ReleasePoster/Poster";
|
import { Poster } from "../ReleasePoster/Poster";
|
||||||
|
|
||||||
|
const CarouselIndicatorsTheme: FlowbiteCarouselIndicatorsTheme = {
|
||||||
|
active: {
|
||||||
|
off: "bg-gray-300/50 hover:bg-gray-400 dark:bg-gray-400/50 dark:hover:bg-gray-200",
|
||||||
|
on: "bg-gray-600 dark:bg-gray-200",
|
||||||
|
},
|
||||||
|
base: "h-3 w-3 rounded-full",
|
||||||
|
wrapper: "absolute bottom-5 left-1/2 flex -translate-x-1/2 space-x-3",
|
||||||
|
};
|
||||||
|
|
||||||
|
const CarouselControlsTheme: FlowbiteCarouselControlTheme = {
|
||||||
|
base: "inline-flex h-8 w-8 items-center justify-center rounded-full bg-gray-600/30 group-hover:bg-gray-600/50 group-focus:outline-none group-focus:ring-4 group-focus:ring-gray-600 dark:bg-gray-400/30 dark:group-hover:bg-gray-400/60 dark:group-focus:ring-gray-400/70 sm:h-10 sm:w-10",
|
||||||
|
icon: "h-5 w-5 text-gray-600 dark:text-gray-400 sm:h-6 sm:w-6",
|
||||||
|
};
|
||||||
|
|
||||||
|
const CarouselTheme = {
|
||||||
|
root: {
|
||||||
|
base: "relative h-full w-full max-w-[700px]",
|
||||||
|
},
|
||||||
|
indicators: CarouselIndicatorsTheme,
|
||||||
|
control: CarouselControlsTheme,
|
||||||
|
};
|
||||||
|
|
||||||
export const ProfileReleaseRatings = (props: any) => {
|
export const ProfileReleaseRatings = (props: any) => {
|
||||||
const [modal, setModal] = useState(false);
|
const [modal, setModal] = useState(false);
|
||||||
return (
|
return (
|
||||||
|
@ -18,16 +51,17 @@ export const ProfileReleaseRatings = (props: any) => {
|
||||||
Посмотреть все
|
Посмотреть все
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col w-full gap-4">
|
<div className="flex min-h-[200px] items-center justify-center">
|
||||||
|
<Carousel theme={CarouselTheme}>
|
||||||
{props.ratings.map((release) => {
|
{props.ratings.map((release) => {
|
||||||
return (
|
return (
|
||||||
<Link href={`/release/${release.id}`} key={`vote-${release.id}`}>
|
<Link href={`/release/${release.id}`} key={`vote-${release.id}`}>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-4 xl:mx-20">
|
||||||
<div className="max-w-32">
|
<div className="max-w-32">
|
||||||
<Poster image={release.image} />
|
<Poster image={release.image} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1 py-4">
|
||||||
<h2 className="text-lg font-bold dark:text-white">{release.title_ru}</h2>
|
<h2 className="text-lg">{release.title_ru}</h2>
|
||||||
<Rating size="md">
|
<Rating size="md">
|
||||||
<RatingStar filled={release.my_vote >= 1} />
|
<RatingStar filled={release.my_vote >= 1} />
|
||||||
<RatingStar filled={release.my_vote >= 2} />
|
<RatingStar filled={release.my_vote >= 2} />
|
||||||
|
@ -43,6 +77,7 @@ export const ProfileReleaseRatings = (props: any) => {
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
</Carousel>
|
||||||
</div>
|
</div>
|
||||||
<ProfileReleaseRatingsModal
|
<ProfileReleaseRatingsModal
|
||||||
profile_id={props.profile_id}
|
profile_id={props.profile_id}
|
||||||
|
@ -113,7 +148,7 @@ const ProfileReleaseRatingsModal = (props: {
|
||||||
onClose={() => props.setIsOpen(false)}
|
onClose={() => props.setIsOpen(false)}
|
||||||
size={"4xl"}
|
size={"4xl"}
|
||||||
>
|
>
|
||||||
<ModalHeader>Оценки</ModalHeader>
|
<Modal.Header>Оценки</Modal.Header>
|
||||||
<div
|
<div
|
||||||
className="flex flex-col gap-2 p-4 overflow-y-auto"
|
className="flex flex-col gap-2 p-4 overflow-y-auto"
|
||||||
onScroll={handleScroll}
|
onScroll={handleScroll}
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
interface UserRoleProps {
|
|
||||||
name: string;
|
|
||||||
color: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const UserRole = ({ name, color }: UserRoleProps) => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`text-[var(--color)] border border-[var(--color)] rounded-md`}
|
|
||||||
style={{ "--color": `#${color}` } as React.CSSProperties}
|
|
||||||
>
|
|
||||||
<p className="px-1.5 py-0.5">{name}</p>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,20 +0,0 @@
|
||||||
interface UserSocialProps {
|
|
||||||
icon: string;
|
|
||||||
url?: string;
|
|
||||||
nickname: string;
|
|
||||||
color: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const UserSocial = ({ nickname, icon, color }: UserSocialProps) => {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`border border-[var(--color)] rounded-md`}
|
|
||||||
style={{ "--color": `#${color}` } as React.CSSProperties}
|
|
||||||
>
|
|
||||||
<div className="flex gap-1 items-center px-1.5 py-1">
|
|
||||||
<span className={`iconify w-6 h-6 bg-[var(--color)] ${icon}`}></span>
|
|
||||||
<p>{nickname}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,152 +1,143 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
import { Avatar, Card, Button } from "flowbite-react";
|
||||||
import { Avatar, Card, useThemeMode } from "flowbite-react";
|
|
||||||
import { UserRole } from "./Profile.Role";
|
|
||||||
import { UserSocial } from "./Profile.Social";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { Chip } from "../Chip/Chip";
|
||||||
|
|
||||||
interface ProfileUserProps {
|
export const ProfileUser = (props: {
|
||||||
|
isOnline: boolean;
|
||||||
avatar: string;
|
avatar: string;
|
||||||
login: string;
|
login: string;
|
||||||
status: string;
|
status: string;
|
||||||
rating: number;
|
socials: {
|
||||||
roles: {
|
isPrivate: boolean;
|
||||||
|
hasSocials: boolean;
|
||||||
|
socials: {
|
||||||
|
name: string;
|
||||||
|
nickname: any;
|
||||||
|
icon: string;
|
||||||
|
urlPrefix?: string | undefined;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
|
chips: {
|
||||||
|
hasChips: boolean;
|
||||||
|
isMyProfile: boolean;
|
||||||
|
isVerified: boolean;
|
||||||
|
isSponsor: boolean;
|
||||||
|
isBlocked: boolean;
|
||||||
|
roles?: {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
color: string;
|
color: string;
|
||||||
}[];
|
}[];
|
||||||
isMyProfile: boolean;
|
|
||||||
isSponsor: boolean;
|
|
||||||
isBlocked: boolean;
|
|
||||||
isVerified: boolean;
|
|
||||||
isOnline: boolean;
|
|
||||||
socials: {
|
|
||||||
vk: string;
|
|
||||||
tg: string;
|
|
||||||
tt: string;
|
|
||||||
inst: string;
|
|
||||||
discord: string;
|
|
||||||
};
|
};
|
||||||
is_social_hidden: boolean;
|
rating: number;
|
||||||
}
|
}) => {
|
||||||
|
const router = useRouter();
|
||||||
export const ProfileUser = ({
|
|
||||||
avatar,
|
|
||||||
login,
|
|
||||||
status,
|
|
||||||
rating,
|
|
||||||
roles,
|
|
||||||
isMyProfile,
|
|
||||||
isVerified,
|
|
||||||
isOnline,
|
|
||||||
isSponsor,
|
|
||||||
isBlocked,
|
|
||||||
socials,
|
|
||||||
is_social_hidden,
|
|
||||||
}: ProfileUserProps) => {
|
|
||||||
const theme = useThemeMode().mode;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card className="h-fit">
|
||||||
{(isMyProfile ||
|
{props.chips.hasChips && (
|
||||||
isVerified ||
|
<div className="flex gap-1 overflow-x-auto scrollbar-thin">
|
||||||
isSponsor ||
|
{props.chips.isMyProfile && (
|
||||||
isBlocked ||
|
<Chip bg_color="bg-blue-500" name="Мой профиль" />
|
||||||
roles.length > 0) && (
|
)}
|
||||||
<div className="flex flex-wrap gap-2">
|
{props.chips.isVerified && (
|
||||||
{isMyProfile && <UserRole name="Мой профиль" color="3f83f8" />}
|
<Chip bg_color="bg-green-500" name="Верифицирован" />
|
||||||
{isBlocked && <UserRole name="Заблокирован" color="f56565" />}
|
)}
|
||||||
{isVerified && <UserRole name="Верифицирован" color="0e9f6e" />}
|
{props.chips.isSponsor && (
|
||||||
{isSponsor && <UserRole name="Спонсор Anixart" color="ecc94b" />}
|
<Chip bg_color="bg-yellow-500" name="Спонсор Anixart" />
|
||||||
{roles.map((role) => (
|
)}
|
||||||
<UserRole key={role.id} name={role.name} color={role.color} />
|
{props.chips.isBlocked && (
|
||||||
|
<Chip bg_color="bg-red-500" name="Заблокирован" />
|
||||||
|
)}
|
||||||
|
{props.chips.roles &&
|
||||||
|
props.chips.roles.length > 0 &&
|
||||||
|
props.chips.roles.map((role: any) => (
|
||||||
|
<Chip
|
||||||
|
key={role.id}
|
||||||
|
bg_color={`bg-[var(--role-color)]`}
|
||||||
|
name={role.name}
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--role-color": `#${role.color}`,
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex flex-col items-center gap-4 sm:items-start sm:flex-row">
|
|
||||||
<Avatar
|
<Avatar
|
||||||
alt=""
|
alt=""
|
||||||
img={avatar}
|
img={props.avatar}
|
||||||
rounded={true}
|
rounded={true}
|
||||||
size={"lg"}
|
size={"lg"}
|
||||||
|
className="relative flex-col items-center justify-center sm:justify-start sm:flex-row"
|
||||||
bordered={true}
|
bordered={true}
|
||||||
color={isOnline ? "success" : "light"}
|
color={props.isOnline ? "success" : "light"}
|
||||||
className="flex-shrink-0"
|
>
|
||||||
/>
|
<div className="space-y-1 text-2xl font-medium whitespace-pre-wrap dark:text-white">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="text-center sm:text-left">
|
||||||
<p className="flex items-center gap-2 text-2xl font-semibold">
|
{props.login}{" "}
|
||||||
{login}
|
|
||||||
<span
|
<span
|
||||||
className={`border rounded-md px-2 py-1 min-w-8 text-sm flex items-center justify-center ${
|
className={`border rounded-md px-2 py-1 text-sm ${
|
||||||
rating > 0 ?
|
props.rating > 0
|
||||||
"border-green-500 text-green-500"
|
? "border-green-500 text-green-500"
|
||||||
: "border-red-500 text-red-500"
|
: "border-red-500 text-red-500"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{rating}
|
{props.rating}
|
||||||
</span>
|
</span>
|
||||||
</p>
|
</div>
|
||||||
<p className="text-sm whitespace-pre-wrap sm:text-md">{status}</p>
|
<div className="text-sm text-gray-500 whitespace-pre-wrap sm:text-md dark:text-gray-400 ">
|
||||||
|
{props.status}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!is_social_hidden &&
|
</Avatar>
|
||||||
(socials.vk ||
|
{props.socials.hasSocials && !props.socials.isPrivate && (
|
||||||
socials.tg ||
|
<div className="flex items-center gap-1 overflow-x-auto scrollbar-thin">
|
||||||
socials.discord ||
|
{props.socials.socials
|
||||||
socials.tt ||
|
.filter((social: any) => {
|
||||||
socials.inst) && (
|
if (social.nickname == "") {
|
||||||
<div className="flex flex-wrap gap-2">
|
return false;
|
||||||
{socials.vk && (
|
}
|
||||||
<Link href={`https://vk.com/${socials.vk}`} target="_blank">
|
return true;
|
||||||
<UserSocial
|
})
|
||||||
nickname={socials.vk}
|
.map((social: any) => {
|
||||||
icon="fa6-brands--vk"
|
if (social.name == "discord" && social.nickname != "")
|
||||||
url={`https://vk.com/${socials.vk}`}
|
return (
|
||||||
color="4a76a8"
|
<Button
|
||||||
/>
|
color="light"
|
||||||
</Link>
|
key={social.name}
|
||||||
)}
|
onClick={() => {
|
||||||
{socials.tg && (
|
window.navigator.clipboard.writeText(social.nickname);
|
||||||
<Link href={`https://t.me/${socials.tg}`} target="_blank">
|
alert("Скопировано!");
|
||||||
<UserSocial
|
}}
|
||||||
nickname={socials.tg}
|
>
|
||||||
icon="fa6-brands--telegram"
|
<div className="flex items-center justify-center gap-2">
|
||||||
url={`https://t.me/${socials.tg}`}
|
<span
|
||||||
color="2aabee"
|
className={`iconify h-4 w-4 sm:h-6 sm:w-6 ${social.icon} dark:fill-white`}
|
||||||
/>
|
></span>
|
||||||
</Link>
|
{social.nickname}
|
||||||
)}
|
</div>
|
||||||
{socials.tt && (
|
</Button>
|
||||||
<Link href={`https://tiktok.com/@${socials.tt}`} target="_blank">
|
);
|
||||||
<UserSocial
|
return (
|
||||||
nickname={socials.tt}
|
|
||||||
icon="fa6-brands--tiktok"
|
|
||||||
url={`https://tiktok.com/@${socials.tt}`}
|
|
||||||
color={theme == "light" ? "000000" : "ffffff"}
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
{socials.inst && (
|
|
||||||
<Link
|
<Link
|
||||||
href={`https://instagram.com/${socials.inst}`}
|
key={social.name}
|
||||||
|
href={`${social.urlPrefix}${social.nickname}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
>
|
>
|
||||||
<UserSocial
|
<Button color="light">
|
||||||
nickname={socials.inst}
|
<div className="flex items-center justify-center gap-2">
|
||||||
icon="fa6-brands--instagram"
|
<span
|
||||||
url={`https://instagram.com/${socials.inst}`}
|
className={`iconify h-4 w-4 sm:h-6 sm:w-6 ${social.icon} dark:fill-white`}
|
||||||
color="c32aa3"
|
></span>
|
||||||
/>
|
{social.nickname}
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
);
|
||||||
{socials.discord && (
|
})}
|
||||||
<UserSocial
|
|
||||||
nickname={socials.discord}
|
|
||||||
icon="fa6-brands--discord"
|
|
||||||
url={`https://discord.com/${socials.discord}`}
|
|
||||||
color="5865f2"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
|
|
|
@ -28,7 +28,6 @@ export const ProfileWatchDynamic = (props: { watchDynamic: Array<any> }) => {
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
theme:"dark",
|
|
||||||
x: {
|
x: {
|
||||||
show: false,
|
show: false,
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { Card, Table, TableBody, TableCell, TableRow } from "flowbite-react";
|
import { Card, Table } from "flowbite-react";
|
||||||
import { ReleaseInfoSearchLink } from "#/components/ReleaseInfo/ReleaseInfo.SearchLink";
|
import { ReleaseInfoSearchLink } from "#/components/ReleaseInfo/ReleaseInfo.SearchLink";
|
||||||
import { unixToDate, minutesToTime } from "#/api/utils";
|
import { unixToDate, minutesToTime } from "#/api/utils";
|
||||||
const weekDay = [
|
const weekDay = [
|
||||||
|
@ -30,9 +30,9 @@ export const ReleaseInfoInfo = (props: {
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<Table>
|
<Table>
|
||||||
<TableBody>
|
<Table.Body>
|
||||||
<TableRow>
|
<Table.Row>
|
||||||
<TableCell className="py-0">
|
<Table.Cell className="py-0">
|
||||||
{props.country ?
|
{props.country ?
|
||||||
props.country.toLowerCase() == "япония" ?
|
props.country.toLowerCase() == "япония" ?
|
||||||
<span className="w-8 h-8 iconify-color twemoji--flag-for-japan"></span>
|
<span className="w-8 h-8 iconify-color twemoji--flag-for-japan"></span>
|
||||||
|
@ -40,45 +40,45 @@ export const ReleaseInfoInfo = (props: {
|
||||||
|
|
||||||
: <span className="w-8 h-8 iconify-color twemoji--flag-for-united-nations "></span>
|
: <span className="w-8 h-8 iconify-color twemoji--flag-for-united-nations "></span>
|
||||||
}
|
}
|
||||||
</TableCell>
|
</Table.Cell>
|
||||||
<TableCell className="font-medium text-gray-900 whitespace-nowrap dark:text-white">
|
<Table.Cell className="font-medium text-gray-900 whitespace-nowrap dark:text-white">
|
||||||
{props.country && props.country}
|
{props.country && props.country}
|
||||||
{(props.aired_on_date != 0 || props.year) && ", "}
|
{(props.aired_on_date != 0 || props.year) && ", "}
|
||||||
{props.season && props.season != 0 ?
|
{props.season && props.season != 0 ?
|
||||||
`${YearSeason[props.season]} `
|
`${YearSeason[props.season]} `
|
||||||
: ""}
|
: ""}
|
||||||
{props.year && `${props.year} г.`}
|
{props.year && `${props.year} г.`}
|
||||||
</TableCell>
|
</Table.Cell>
|
||||||
</TableRow>
|
</Table.Row>
|
||||||
<TableRow>
|
<Table.Row>
|
||||||
<TableCell className="py-0">
|
<Table.Cell className="py-0">
|
||||||
<span className="w-8 h-8 iconify-color mdi--animation-play-outline dark:invert"></span>
|
<span className="w-8 h-8 iconify-color mdi--animation-play-outline dark:invert"></span>
|
||||||
</TableCell>
|
</Table.Cell>
|
||||||
<TableCell className="font-medium text-gray-900 whitespace-nowrap dark:text-white">
|
<Table.Cell className="font-medium text-gray-900 whitespace-nowrap dark:text-white">
|
||||||
{props.episodes.released ? props.episodes.released : "?"}
|
{props.episodes.released ? props.episodes.released : "?"}
|
||||||
{"/"}
|
{"/"}
|
||||||
{props.episodes.total ? props.episodes.total + " эп. " : "? эп. "}
|
{props.episodes.total ? props.episodes.total + " эп. " : "? эп. "}
|
||||||
{props.duration != 0 &&
|
{props.duration != 0 &&
|
||||||
`по ${minutesToTime(props.duration, "daysHours")}`}
|
`по ${minutesToTime(props.duration, "daysHours")}`}
|
||||||
</TableCell>
|
</Table.Cell>
|
||||||
</TableRow>
|
</Table.Row>
|
||||||
<TableRow>
|
<Table.Row>
|
||||||
<TableCell className="py-0">
|
<Table.Cell className="py-0">
|
||||||
<span className="w-8 h-8 iconify-color mdi--calendar-outline dark:invert"></span>
|
<span className="w-8 h-8 iconify-color mdi--calendar-outline dark:invert"></span>
|
||||||
</TableCell>
|
</Table.Cell>
|
||||||
<TableCell className="font-medium text-gray-900 dark:text-white">
|
<Table.Cell className="font-medium text-gray-900 dark:text-white">
|
||||||
{props.category}
|
{props.category}
|
||||||
{", "}
|
{", "}
|
||||||
{props.broadcast == 0 ?
|
{props.broadcast == 0 ?
|
||||||
props.status.toLowerCase()
|
props.status.toLowerCase()
|
||||||
: `выходит ${weekDay[props.broadcast]}`}
|
: `выходит ${weekDay[props.broadcast]}`}
|
||||||
</TableCell>
|
</Table.Cell>
|
||||||
</TableRow>
|
</Table.Row>
|
||||||
<TableRow>
|
<Table.Row>
|
||||||
<TableCell className="py-0">
|
<Table.Cell className="py-0">
|
||||||
<span className="w-8 h-8 iconify-color mdi--people-group-outline dark:invert"></span>
|
<span className="w-8 h-8 iconify-color mdi--people-group-outline dark:invert"></span>
|
||||||
</TableCell>
|
</Table.Cell>
|
||||||
<TableCell className="font-medium text-gray-900 dark:text-white">
|
<Table.Cell className="font-medium text-gray-900 dark:text-white">
|
||||||
{props.studio && (
|
{props.studio && (
|
||||||
<>
|
<>
|
||||||
{"Студия: "}
|
{"Студия: "}
|
||||||
|
@ -117,13 +117,13 @@ export const ReleaseInfoInfo = (props: {
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</Table.Cell>
|
||||||
</TableRow>
|
</Table.Row>
|
||||||
<TableRow>
|
<Table.Row>
|
||||||
<TableCell className="py-0">
|
<Table.Cell className="py-0">
|
||||||
<span className="w-8 h-8 iconify-color mdi--tag-outline dark:invert"></span>
|
<span className="w-8 h-8 iconify-color mdi--tag-outline dark:invert"></span>
|
||||||
</TableCell>
|
</Table.Cell>
|
||||||
<TableCell className="font-medium text-gray-900 dark:text-white">
|
<Table.Cell className="font-medium text-gray-900 dark:text-white">
|
||||||
{props.genres &&
|
{props.genres &&
|
||||||
props.genres.split(", ").map((genre: string, index: number) => {
|
props.genres.split(", ").map((genre: string, index: number) => {
|
||||||
return (
|
return (
|
||||||
|
@ -133,14 +133,14 @@ export const ReleaseInfoInfo = (props: {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</TableCell>
|
</Table.Cell>
|
||||||
</TableRow>
|
</Table.Row>
|
||||||
{props.status.toLowerCase() == "анонс" && (
|
{props.status.toLowerCase() == "анонс" && (
|
||||||
<TableRow>
|
<Table.Row>
|
||||||
<TableCell className="py-0">
|
<Table.Cell className="py-0">
|
||||||
<span className="w-8 h-8 iconify-color mdi--clock-outline dark:invert"></span>
|
<span className="w-8 h-8 iconify-color mdi--clock-outline dark:invert"></span>
|
||||||
</TableCell>
|
</Table.Cell>
|
||||||
<TableCell className="font-medium text-gray-900 whitespace-nowrap dark:text-white">
|
<Table.Cell className="font-medium text-gray-900 whitespace-nowrap dark:text-white">
|
||||||
{props.aired_on_date != 0 ?
|
{props.aired_on_date != 0 ?
|
||||||
unixToDate(props.aired_on_date, "full")
|
unixToDate(props.aired_on_date, "full")
|
||||||
: props.year ?
|
: props.year ?
|
||||||
|
@ -151,10 +151,10 @@ export const ReleaseInfoInfo = (props: {
|
||||||
{props.year && `${props.year} г.`}
|
{props.year && `${props.year} г.`}
|
||||||
</>
|
</>
|
||||||
: "Скоро"}
|
: "Скоро"}
|
||||||
</TableCell>
|
</Table.Cell>
|
||||||
</TableRow>
|
</Table.Row>
|
||||||
)}
|
)}
|
||||||
</TableBody>
|
</Table.Body>
|
||||||
</Table>
|
</Table>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|
|
@ -14,7 +14,7 @@ export const ReleaseInfoStreaming = (props: { release_id: number }) => {
|
||||||
setData(await response.json());
|
setData(await response.json());
|
||||||
};
|
};
|
||||||
_getData();
|
_getData();
|
||||||
}, [props.release_id]);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -32,8 +32,7 @@ export const ReleaseInfoStreaming = (props: { release_id: number }) => {
|
||||||
key={`platform_${item.id}`}
|
key={`platform_${item.id}`}
|
||||||
className="flex items-center gap-2 px-4 py-2 transition-colors bg-gray-100 rounded-lg hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 "
|
className="flex items-center gap-2 px-4 py-2 transition-colors bg-gray-100 rounded-lg hover:bg-gray-300 dark:bg-gray-700 dark:hover:bg-gray-600 "
|
||||||
>
|
>
|
||||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
<img src={item.icon} className="w-6 h-6 rounded-full" />
|
||||||
<img alt="" src={item.icon} className="w-6 h-6 rounded-full" />
|
|
||||||
<p className="text-sm line-clamp-2">{item.name}</p>
|
<p className="text-sm line-clamp-2">{item.name}</p>
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,26 +1,20 @@
|
||||||
import {
|
import {
|
||||||
Button,
|
|
||||||
Card,
|
Card,
|
||||||
Modal,
|
|
||||||
ModalBody,
|
|
||||||
ModalFooter,
|
|
||||||
ModalHeader,
|
|
||||||
Rating,
|
Rating,
|
||||||
RatingAdvanced,
|
Flowbite,
|
||||||
RatingStar,
|
Button,
|
||||||
RatingAdvancedTheme,
|
CustomFlowbiteTheme,
|
||||||
|
Modal,
|
||||||
} from "flowbite-react";
|
} from "flowbite-react";
|
||||||
import { numberDeclension } from "#/api/utils";
|
import { numberDeclension } from "#/api/utils";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { ENDPOINTS } from "#/api/config";
|
import { ENDPOINTS } from "#/api/config";
|
||||||
|
|
||||||
const CustomRatingTheme: RatingAdvancedTheme = {
|
const RatingTheme: CustomFlowbiteTheme = {
|
||||||
base: "flex items-center",
|
ratingAdvanced: {
|
||||||
label: "text-sm font-medium text-cyan-600 dark:text-cyan-500",
|
|
||||||
progress: {
|
progress: {
|
||||||
base: "mx-4 h-5 w-3/4 rounded bg-gray-200 dark:bg-gray-700",
|
base: "mx-4 h-5 w-3/4 rounded bg-gray-200 dark:bg-gray-700",
|
||||||
fill: "h-5 rounded bg-yellow-400",
|
},
|
||||||
label: "text-sm font-medium text-cyan-600 dark:text-cyan-500",
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
export const ReleaseInfoRating = (props: {
|
export const ReleaseInfoRating = (props: {
|
||||||
|
@ -45,7 +39,7 @@ export const ReleaseInfoRating = (props: {
|
||||||
<Card>
|
<Card>
|
||||||
<div className="flex flex-col gap-2 sm:items-center sm:flex-row">
|
<div className="flex flex-col gap-2 sm:items-center sm:flex-row">
|
||||||
<Rating>
|
<Rating>
|
||||||
<RatingStar />
|
<Rating.Star />
|
||||||
<p className="ml-2 text-sm font-bold dark:text-white">
|
<p className="ml-2 text-sm font-bold dark:text-white">
|
||||||
{props.grade.toFixed(2)} из 5
|
{props.grade.toFixed(2)} из 5
|
||||||
</p>
|
</p>
|
||||||
|
@ -54,7 +48,7 @@ export const ReleaseInfoRating = (props: {
|
||||||
<>
|
<>
|
||||||
<span className="mx-1.5 h-1 w-1 rounded-full bg-gray-500 dark:bg-gray-400 hidden lg:block" />
|
<span className="mx-1.5 h-1 w-1 rounded-full bg-gray-500 dark:bg-gray-400 hidden lg:block" />
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{vote ?
|
{vote ? (
|
||||||
<>
|
<>
|
||||||
<p className="text-sm font-medium text-gray-500 dark:text-gray-400">
|
<p className="text-sm font-medium text-gray-500 dark:text-gray-400">
|
||||||
ваша оценка: {vote}
|
ваша оценка: {vote}
|
||||||
|
@ -68,7 +62,8 @@ export const ReleaseInfoRating = (props: {
|
||||||
изменить
|
изменить
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
: <Button
|
) : (
|
||||||
|
<Button
|
||||||
size={"xs"}
|
size={"xs"}
|
||||||
className="text-gray-500 border border-gray-600 rounded-full hover:bg-black hover:text-white hover:border-black dark:text-gray-400 dark:border-gray-500"
|
className="text-gray-500 border border-gray-600 rounded-full hover:bg-black hover:text-white hover:border-black dark:text-gray-400 dark:border-gray-500"
|
||||||
color="inline"
|
color="inline"
|
||||||
|
@ -76,7 +71,7 @@ export const ReleaseInfoRating = (props: {
|
||||||
>
|
>
|
||||||
оценить
|
оценить
|
||||||
</Button>
|
</Button>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -85,50 +80,47 @@ export const ReleaseInfoRating = (props: {
|
||||||
{props.votes.total}{" "}
|
{props.votes.total}{" "}
|
||||||
{numberDeclension(props.votes.total, "голос", "голоса", "голосов")}
|
{numberDeclension(props.votes.total, "голос", "голоса", "голосов")}
|
||||||
</p>
|
</p>
|
||||||
<RatingAdvanced
|
<Flowbite theme={{ theme: RatingTheme }}>
|
||||||
theme={CustomRatingTheme}
|
<Rating.Advanced
|
||||||
percentFilled={Math.floor(
|
percentFilled={Math.floor(
|
||||||
(props.votes["5"] / props.votes.total) * 100
|
(props.votes["5"] / props.votes.total) * 100
|
||||||
)}
|
)}
|
||||||
className="mb-2"
|
className="mb-2"
|
||||||
>
|
>
|
||||||
5
|
5
|
||||||
</RatingAdvanced>
|
</Rating.Advanced>
|
||||||
<RatingAdvanced
|
<Rating.Advanced
|
||||||
theme={CustomRatingTheme}
|
|
||||||
percentFilled={Math.floor(
|
percentFilled={Math.floor(
|
||||||
(props.votes["4"] / props.votes.total) * 100
|
(props.votes["4"] / props.votes.total) * 100
|
||||||
)}
|
)}
|
||||||
className="mb-2"
|
className="mb-2"
|
||||||
>
|
>
|
||||||
4
|
4
|
||||||
</RatingAdvanced>
|
</Rating.Advanced>
|
||||||
<RatingAdvanced
|
<Rating.Advanced
|
||||||
theme={CustomRatingTheme}
|
|
||||||
percentFilled={Math.floor(
|
percentFilled={Math.floor(
|
||||||
(props.votes["3"] / props.votes.total) * 100
|
(props.votes["3"] / props.votes.total) * 100
|
||||||
)}
|
)}
|
||||||
className="mb-2"
|
className="mb-2"
|
||||||
>
|
>
|
||||||
3
|
3
|
||||||
</RatingAdvanced>
|
</Rating.Advanced>
|
||||||
<RatingAdvanced
|
<Rating.Advanced
|
||||||
theme={CustomRatingTheme}
|
|
||||||
percentFilled={Math.floor(
|
percentFilled={Math.floor(
|
||||||
(props.votes["2"] / props.votes.total) * 100
|
(props.votes["2"] / props.votes.total) * 100
|
||||||
)}
|
)}
|
||||||
className="mb-2"
|
className="mb-2"
|
||||||
>
|
>
|
||||||
2
|
2
|
||||||
</RatingAdvanced>
|
</Rating.Advanced>
|
||||||
<RatingAdvanced
|
<Rating.Advanced
|
||||||
theme={CustomRatingTheme}
|
|
||||||
percentFilled={Math.floor(
|
percentFilled={Math.floor(
|
||||||
(props.votes["1"] / props.votes.total) * 100
|
(props.votes["1"] / props.votes.total) * 100
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
1
|
1
|
||||||
</RatingAdvanced>
|
</Rating.Advanced>
|
||||||
|
</Flowbite>
|
||||||
</Card>
|
</Card>
|
||||||
<ReleaseInfoRatingModal
|
<ReleaseInfoRatingModal
|
||||||
isOpen={isRatingModalOpen}
|
isOpen={isRatingModalOpen}
|
||||||
|
@ -186,8 +178,8 @@ const ReleaseInfoRatingModal = (props: {
|
||||||
show={props.isOpen}
|
show={props.isOpen}
|
||||||
onClose={() => props.setIsOpen(false)}
|
onClose={() => props.setIsOpen(false)}
|
||||||
>
|
>
|
||||||
<ModalHeader>Оценка</ModalHeader>
|
<Modal.Header>Оценка</Modal.Header>
|
||||||
<ModalBody>
|
<Modal.Body>
|
||||||
<div>
|
<div>
|
||||||
<div className="block sm:hidden">
|
<div className="block sm:hidden">
|
||||||
<Rating size="md" className="justify-center">
|
<Rating size="md" className="justify-center">
|
||||||
|
@ -199,7 +191,7 @@ const ReleaseInfoRatingModal = (props: {
|
||||||
onMouseOut={() => setCurElement(0)}
|
onMouseOut={() => setCurElement(0)}
|
||||||
onClick={() => setVote(element)}
|
onClick={() => setVote(element)}
|
||||||
>
|
>
|
||||||
<RatingStar
|
<Rating.Star
|
||||||
filled={index + 1 <= curElement || index + 1 <= vote}
|
filled={index + 1 <= curElement || index + 1 <= vote}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -216,7 +208,7 @@ const ReleaseInfoRatingModal = (props: {
|
||||||
onMouseOut={() => setCurElement(0)}
|
onMouseOut={() => setCurElement(0)}
|
||||||
onClick={() => setVote(element)}
|
onClick={() => setVote(element)}
|
||||||
>
|
>
|
||||||
<RatingStar
|
<Rating.Star
|
||||||
filled={index + 1 <= curElement || index + 1 <= vote}
|
filled={index + 1 <= curElement || index + 1 <= vote}
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -224,8 +216,8 @@ const ReleaseInfoRatingModal = (props: {
|
||||||
</Rating>
|
</Rating>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ModalBody>
|
</Modal.Body>
|
||||||
<ModalFooter>
|
<Modal.Footer>
|
||||||
<div className="flex gap-1 ml-auto">
|
<div className="flex gap-1 ml-auto">
|
||||||
<Button
|
<Button
|
||||||
disabled={isSending}
|
disabled={isSending}
|
||||||
|
@ -249,7 +241,7 @@ const ReleaseInfoRatingModal = (props: {
|
||||||
Оценить
|
Оценить
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</ModalFooter>
|
</Modal.Footer>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,9 +1,31 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { Card } from "flowbite-react";
|
import { Card, Carousel, CustomFlowbiteTheme, FlowbiteCarouselControlTheme, FlowbiteCarouselIndicatorsTheme } from "flowbite-react";
|
||||||
import { ReleaseLinkList } from "#/components/ReleaseLink/ReleaseLinkList";
|
import { ReleaseLink } from "#/components/ReleaseLink/ReleaseLinkUpdate";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
|
const CarouselIndicatorsTheme: FlowbiteCarouselIndicatorsTheme = {
|
||||||
|
active: {
|
||||||
|
off: "bg-gray-400/50 hover:bg-gray-200",
|
||||||
|
on: "bg-gray-200",
|
||||||
|
},
|
||||||
|
base: "h-3 w-3 rounded-full max-w-[300px]",
|
||||||
|
wrapper: "absolute bottom-5 left-1/2 flex -translate-x-1/2 space-x-3",
|
||||||
|
};
|
||||||
|
|
||||||
|
const CarouselControlsTheme: FlowbiteCarouselControlTheme = {
|
||||||
|
base: "inline-flex h-8 w-8 items-center justify-center rounded-full group-focus:outline-none group-focus:ring-4 bg-gray-400/30 group-hover:bg-gray-400/60 group-focus:ring-gray-400/70 sm:h-10 sm:w-10",
|
||||||
|
icon: "h-5 w-5 text-gray-400 sm:h-6 sm:w-6",
|
||||||
|
};
|
||||||
|
|
||||||
|
const CarouselTheme: CustomFlowbiteTheme["carousel"] = {
|
||||||
|
root: {
|
||||||
|
base: "relative h-full w-full max-w-[300px]",
|
||||||
|
},
|
||||||
|
indicators: CarouselIndicatorsTheme,
|
||||||
|
control: CarouselControlsTheme,
|
||||||
|
};
|
||||||
|
|
||||||
export const ReleaseInfoRelated = (props: {
|
export const ReleaseInfoRelated = (props: {
|
||||||
release_id: number;
|
release_id: number;
|
||||||
related: any;
|
related: any;
|
||||||
|
@ -22,7 +44,8 @@ export const ReleaseInfoRelated = (props: {
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-4 mt-2">
|
<div className="flex justify-center mt-2">
|
||||||
|
<Carousel pauseOnHover={true} theme={CarouselTheme}>
|
||||||
{props.related_releases
|
{props.related_releases
|
||||||
.filter((release: any) => {
|
.filter((release: any) => {
|
||||||
if (release.id == props.release_id) {
|
if (release.id == props.release_id) {
|
||||||
|
@ -32,13 +55,14 @@ export const ReleaseInfoRelated = (props: {
|
||||||
})
|
})
|
||||||
.map((release: any) => {
|
.map((release: any) => {
|
||||||
return (
|
return (
|
||||||
<ReleaseLinkList
|
<ReleaseLink
|
||||||
key={release.id}
|
key={release.id}
|
||||||
{...release}
|
{...release}
|
||||||
settings={{ showGenres: false, showDescription: false }}
|
settings={{ showGenres: false, showDescription: false }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
</Carousel>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,63 +1,21 @@
|
||||||
import { Card } from "flowbite-react";
|
import { Card, Carousel } from "flowbite-react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
|
||||||
import { Swiper, SwiperSlide } from "swiper/react";
|
|
||||||
import "swiper/css";
|
|
||||||
import "swiper/css/navigation";
|
|
||||||
import "swiper/css/pagination";
|
|
||||||
import "swiper/css/autoplay";
|
|
||||||
import { Navigation, Pagination, Autoplay } from "swiper/modules";
|
|
||||||
|
|
||||||
export const ReleaseInfoScreenshots = (props: { images: string[] }) => {
|
export const ReleaseInfoScreenshots = (props: { images: string[] }) => {
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<Swiper
|
<Carousel className="aspect-[16/10]">
|
||||||
modules={[Navigation, Pagination, Autoplay]}
|
{props.images.map((image: string, index: number) => (
|
||||||
spaceBetween={8}
|
|
||||||
slidesPerView={2}
|
|
||||||
direction={"horizontal"}
|
|
||||||
allowTouchMove={true}
|
|
||||||
autoplay={true}
|
|
||||||
pagination={true}
|
|
||||||
breakpoints={{
|
|
||||||
1024: {
|
|
||||||
slidesPerView: 1,
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
style={{
|
|
||||||
height: "100%",
|
|
||||||
minHeight: 0,
|
|
||||||
maxHeight: "100%",
|
|
||||||
width: "100%",
|
|
||||||
minWidth: 0,
|
|
||||||
maxWidth: "100%",
|
|
||||||
overflow: "hidden",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{props.images.map((image: string, index: number) => {
|
|
||||||
return (
|
|
||||||
<SwiperSlide
|
|
||||||
key={`release-screenshot-${index}`}
|
|
||||||
style={{
|
|
||||||
width: "fit-content",
|
|
||||||
flexShrink: 0,
|
|
||||||
display: "block",
|
|
||||||
height: "100%",
|
|
||||||
maxHeight: "100%",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Image
|
<Image
|
||||||
key={index}
|
key={index}
|
||||||
className="object-cover"
|
className="object-cover"
|
||||||
src={image}
|
src={image}
|
||||||
width={400}
|
width={400}
|
||||||
height={225}
|
height={300}
|
||||||
alt=""
|
alt=""
|
||||||
/>
|
/>
|
||||||
</SwiperSlide>
|
))}
|
||||||
);
|
</Carousel>
|
||||||
})}
|
|
||||||
</Swiper>
|
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,7 +5,7 @@ import useSWRInfinite from "swr/infinite";
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { tryCatchAPI, useSWRfetcher } from "#/api/utils";
|
import { tryCatchAPI, useSWRfetcher } from "#/api/utils";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { DropdownItem, ModalHeader, useThemeMode } from "flowbite-react";
|
import { useThemeMode } from "flowbite-react";
|
||||||
|
|
||||||
const lists = [
|
const lists = [
|
||||||
{ list: 0, name: "Не смотрю" },
|
{ list: 0, name: "Не смотрю" },
|
||||||
|
@ -174,12 +174,12 @@ export const ReleaseInfoUserList = (props: {
|
||||||
disabled={listEventDisabled}
|
disabled={listEventDisabled}
|
||||||
>
|
>
|
||||||
{lists.map((list) => (
|
{lists.map((list) => (
|
||||||
<DropdownItem
|
<Dropdown.Item
|
||||||
key={list.list}
|
key={list.list}
|
||||||
onClick={() => _addToList(list.list)}
|
onClick={() => _addToList(list.list)}
|
||||||
>
|
>
|
||||||
{list.name}
|
{list.name}
|
||||||
</DropdownItem>
|
</Dropdown.Item>
|
||||||
))}
|
))}
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
<Button
|
<Button
|
||||||
|
@ -321,7 +321,7 @@ const AddReleaseToCollectionModal = (props: {
|
||||||
show={props.isOpen}
|
show={props.isOpen}
|
||||||
onClose={() => props.setIsOpen(false)}
|
onClose={() => props.setIsOpen(false)}
|
||||||
>
|
>
|
||||||
<ModalHeader>Выбор коллекции</ModalHeader>
|
<Modal.Header>Выбор коллекции</Modal.Header>
|
||||||
<div
|
<div
|
||||||
className="flex flex-col gap-2 p-4 overflow-y-auto"
|
className="flex flex-col gap-2 p-4 overflow-y-auto"
|
||||||
onScroll={handleScroll}
|
onScroll={handleScroll}
|
||||||
|
|
|
@ -1,118 +0,0 @@
|
||||||
import Link from "next/link";
|
|
||||||
import { Poster } from "../ReleasePoster/Poster";
|
|
||||||
import { ReleaseChips } from "../ReleasePoster/Chips";
|
|
||||||
|
|
||||||
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 ReleaseLinkList = (props: {
|
|
||||||
image: string;
|
|
||||||
title_ru: string;
|
|
||||||
title_original: string;
|
|
||||||
description?: string;
|
|
||||||
genres?: string;
|
|
||||||
grade?: number;
|
|
||||||
id: number;
|
|
||||||
settings?: {
|
|
||||||
showGenres?: boolean;
|
|
||||||
showDescription?: boolean;
|
|
||||||
showOrigTitle?: boolean;
|
|
||||||
};
|
|
||||||
chipsSettings?: {
|
|
||||||
enabled: boolean;
|
|
||||||
gradeHidden?: boolean;
|
|
||||||
statusHidden?: boolean;
|
|
||||||
categoryHidden?: boolean;
|
|
||||||
episodesHidden?: boolean;
|
|
||||||
listHidden?: boolean;
|
|
||||||
favHidden?: boolean;
|
|
||||||
lastWatchedHidden?: boolean;
|
|
||||||
};
|
|
||||||
profile_list_status?: number;
|
|
||||||
status?: {
|
|
||||||
name: string;
|
|
||||||
};
|
|
||||||
category?: {
|
|
||||||
name: string;
|
|
||||||
};
|
|
||||||
status_id?: number;
|
|
||||||
episodes_released?: string;
|
|
||||||
episodes_total?: string;
|
|
||||||
is_favorite?: boolean;
|
|
||||||
}) => {
|
|
||||||
const genres = [];
|
|
||||||
const settings = {
|
|
||||||
showGenres: true,
|
|
||||||
showDescription: true,
|
|
||||||
showOrigTitle: true,
|
|
||||||
...props.settings,
|
|
||||||
};
|
|
||||||
const chipsSettings = props.chipsSettings || {};
|
|
||||||
|
|
||||||
const grade = props.grade ? Number(props.grade.toFixed(1)) : null;
|
|
||||||
const profile_list_status = props.profile_list_status || null;
|
|
||||||
let user_list = null;
|
|
||||||
if (profile_list_status != null || profile_list_status != 0) {
|
|
||||||
user_list = profile_lists[profile_list_status];
|
|
||||||
}
|
|
||||||
if (props.genres) {
|
|
||||||
const genres_array = props.genres.split(",");
|
|
||||||
genres_array.forEach((genre) => {
|
|
||||||
genres.push(genre.trim());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Link href={`/release/${props.id}`}>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<div className="flex-shrink-0 w-32">
|
|
||||||
<Poster image={props.image} className="h-auto" />
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<ReleaseChips
|
|
||||||
{...props}
|
|
||||||
user_list={user_list}
|
|
||||||
grade={grade}
|
|
||||||
settings={{ lastWatchedHidden: false }}
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
{settings.showGenres &&
|
|
||||||
genres.length > 0 &&
|
|
||||||
genres.map((genre: string, index: number) => {
|
|
||||||
return (
|
|
||||||
<span
|
|
||||||
key={`release_${props.id}_genre_${genre}_${index}`}
|
|
||||||
className="text-sm font-light leading-none dark:text-white"
|
|
||||||
>
|
|
||||||
{index > 0 && ", "}
|
|
||||||
{genre}
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
{props.title_ru && (
|
|
||||||
<p className="text-lg font-bold line-clamp-2 dark:text-white">
|
|
||||||
{props.title_ru}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{settings.showOrigTitle && props.title_original && (
|
|
||||||
<p className="text-sm text-gray-600 line-clamp-2 dark:text-gray-300">
|
|
||||||
{props.title_original}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{settings.showDescription && props.description && (
|
|
||||||
<p className="mt-2 text-sm font-light leading-none text-white lg:text-base xl:text-lg line-clamp-4">
|
|
||||||
{props.description}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
};
|
|
131
app/components/ReleasePlayer/EpisodeSelector.tsx
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { ENDPOINTS } from "#/api/config";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { Swiper, SwiperSlide } from "swiper/react";
|
||||||
|
import "swiper/css";
|
||||||
|
import "swiper/css/navigation";
|
||||||
|
import "swiper/css/mousewheel";
|
||||||
|
import "swiper/css/scrollbar";
|
||||||
|
import { Navigation, Mousewheel, Scrollbar } from "swiper/modules";
|
||||||
|
import { Button } from "flowbite-react";
|
||||||
|
|
||||||
|
import {
|
||||||
|
getAnonEpisodesWatched,
|
||||||
|
saveAnonEpisodeWatched,
|
||||||
|
} from "./ReleasePlayer";
|
||||||
|
|
||||||
|
interface Episode {
|
||||||
|
id: number;
|
||||||
|
position: number;
|
||||||
|
name: string;
|
||||||
|
is_watched: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Source {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
episodes_count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EpisodeSelector = (props: {
|
||||||
|
availableEpisodes: Episode[];
|
||||||
|
episode: Episode;
|
||||||
|
setEpisode: any;
|
||||||
|
source: Source;
|
||||||
|
release_id: any;
|
||||||
|
voiceover: any;
|
||||||
|
token: string | null;
|
||||||
|
}) => {
|
||||||
|
let anonEpisodesWatched = getAnonEpisodesWatched(
|
||||||
|
props.release_id,
|
||||||
|
props.source.id,
|
||||||
|
props.voiceover.id
|
||||||
|
);
|
||||||
|
anonEpisodesWatched =
|
||||||
|
anonEpisodesWatched[props.release_id][props.source.id][props.voiceover.id];
|
||||||
|
|
||||||
|
async function saveEpisodeToHistory(episode: Episode) {
|
||||||
|
if (episode && props.token) {
|
||||||
|
fetch(
|
||||||
|
`${ENDPOINTS.statistic.addHistory}/${props.release_id}/${props.source.id}/${episode.position}?token=${props.token}`
|
||||||
|
);
|
||||||
|
fetch(
|
||||||
|
`${ENDPOINTS.statistic.markWatched}/${props.release_id}/${props.source.id}/${episode.position}?token=${props.token}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Swiper
|
||||||
|
modules={[Navigation, Mousewheel, Scrollbar]}
|
||||||
|
spaceBetween={8}
|
||||||
|
slidesPerView={"auto"}
|
||||||
|
direction={"horizontal"}
|
||||||
|
mousewheel={{
|
||||||
|
enabled: true,
|
||||||
|
sensitivity: 4,
|
||||||
|
}}
|
||||||
|
scrollbar={true}
|
||||||
|
allowTouchMove={true}
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--swiper-scrollbar-bottom": "0",
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{props.availableEpisodes.map((episode: Episode) => (
|
||||||
|
<SwiperSlide
|
||||||
|
key={`episode_${episode.position}`}
|
||||||
|
style={{ maxWidth: "fit-content" }}
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
color={
|
||||||
|
props.episode.position === episode.position ? "blue" : "light"
|
||||||
|
}
|
||||||
|
theme={{ base: "w-full disabled:opacity-100" }}
|
||||||
|
onClick={() => {
|
||||||
|
if (["Sibnet"].includes(props.source.name)) {
|
||||||
|
props.availableEpisodes[episode.position].is_watched = true;
|
||||||
|
} else {
|
||||||
|
props.availableEpisodes[episode.position - 1].is_watched =
|
||||||
|
true;
|
||||||
|
}
|
||||||
|
saveAnonEpisodeWatched(
|
||||||
|
props.release_id,
|
||||||
|
props.source.id,
|
||||||
|
props.voiceover.id,
|
||||||
|
episode.position
|
||||||
|
);
|
||||||
|
saveEpisodeToHistory(episode);
|
||||||
|
props.setEpisode({
|
||||||
|
selected: { ...episode, is_watched: true },
|
||||||
|
available: props.availableEpisodes,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
disabled={props.episode.position === episode.position}
|
||||||
|
>
|
||||||
|
<div className="flex items-center">
|
||||||
|
{episode.name ?
|
||||||
|
episode.name
|
||||||
|
: ["Sibnet"].includes(props.source.name) ?
|
||||||
|
`${episode.position + 1} Серия`
|
||||||
|
: `${episode.position} Серия`}
|
||||||
|
{(
|
||||||
|
episode.is_watched ||
|
||||||
|
Object.keys(anonEpisodesWatched).includes(
|
||||||
|
episode.position.toString()
|
||||||
|
)
|
||||||
|
) ?
|
||||||
|
<span className="w-4 h-4 ml-2 iconify material-symbols--check-circle"></span>
|
||||||
|
: <span className="w-4 h-4 ml-2 iconify material-symbols--check-circle-outline"></span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</Button>
|
||||||
|
</SwiperSlide>
|
||||||
|
))}
|
||||||
|
</Swiper>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,133 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { ENDPOINTS } from "#/api/config";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { _fetchAPI } from "./PlayerParsing";
|
|
||||||
|
|
||||||
import { Voiceover } from "./VoiceoverSelectorMenu";
|
|
||||||
import { Source } from "./SourceSelectorMenu";
|
|
||||||
import { getAnonEpisodesWatched } from "./ReleasePlayer";
|
|
||||||
|
|
||||||
export interface Episode {
|
|
||||||
position: number;
|
|
||||||
name: string;
|
|
||||||
is_watched: boolean;
|
|
||||||
}
|
|
||||||
interface EpisodeSelectorMenuProps {
|
|
||||||
release_id: number;
|
|
||||||
voiceover: Voiceover;
|
|
||||||
source: Source;
|
|
||||||
token: string | null;
|
|
||||||
setEpisode: (state) => void;
|
|
||||||
episode: Episode;
|
|
||||||
episodeList: Episode[];
|
|
||||||
setPlayerError: (state) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const EpisodeSelectorMenu = ({
|
|
||||||
release_id,
|
|
||||||
token,
|
|
||||||
voiceover,
|
|
||||||
source,
|
|
||||||
setEpisode,
|
|
||||||
episode,
|
|
||||||
episodeList,
|
|
||||||
setPlayerError,
|
|
||||||
}: EpisodeSelectorMenuProps) => {
|
|
||||||
const [watchedEpisodes, setWatchedEpisodes] = useState([]);
|
|
||||||
useEffect(() => {
|
|
||||||
const __getInfo = async () => {
|
|
||||||
let url = `${ENDPOINTS.release.episode}/${release_id}/${voiceover.id}/${source.id}`;
|
|
||||||
if (token) {
|
|
||||||
url += `?token=${token}`;
|
|
||||||
}
|
|
||||||
const episodes = await _fetchAPI(
|
|
||||||
url,
|
|
||||||
"Не удалось получить информацию о эпизодах",
|
|
||||||
setPlayerError
|
|
||||||
);
|
|
||||||
if (episodes) {
|
|
||||||
let anonEpisodesWatched = getAnonEpisodesWatched(
|
|
||||||
release_id,
|
|
||||||
source.id,
|
|
||||||
voiceover.id
|
|
||||||
);
|
|
||||||
let lastEpisodeWatched = Math.max.apply(
|
|
||||||
0,
|
|
||||||
Object.keys(anonEpisodesWatched[release_id][source.id][voiceover.id])
|
|
||||||
);
|
|
||||||
let selectedEpisode =
|
|
||||||
episodes.episodes.find(
|
|
||||||
(episode: Episode) => episode.position == lastEpisodeWatched
|
|
||||||
) || episodes.episodes[0];
|
|
||||||
|
|
||||||
setEpisode({
|
|
||||||
selected: selectedEpisode,
|
|
||||||
available: episodes.episodes,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if (source) {
|
|
||||||
__getInfo();
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [source]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (release_id && source && voiceover) {
|
|
||||||
const anonEpisodesWatched = getAnonEpisodesWatched(
|
|
||||||
release_id,
|
|
||||||
source.id,
|
|
||||||
voiceover.id
|
|
||||||
);
|
|
||||||
setWatchedEpisodes(
|
|
||||||
anonEpisodesWatched[release_id][source.id][voiceover.id]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}, [release_id, source, voiceover]);
|
|
||||||
|
|
||||||
if (!voiceover || !source || !episode) return <></>
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col items-start justify-start gap-4">
|
|
||||||
<p className="text-[20px] px-2 pt-2 pb-1 font-bold">Эпизод</p>
|
|
||||||
<div className="max-h-full flex flex-col gap-4 items-start justify-start overflow-x-hidden overflow-y-auto px-2 pb-2 scrollbar-thin scrollbar-thumb-[rgb(60_60_60_/_.8)] scrollbar-track-[rgb(30_30_30_/_.8)]">
|
|
||||||
{episodeList && episodeList.length > 0 ?
|
|
||||||
episodeList.map((epis: Episode) => {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={`release-${release_id}-voiceover-${voiceover.id}-source-${source.id}-episode-${epis.position}`}
|
|
||||||
className={`h-fit px-2 justify-start items-start ${episode.position == epis.position ? "text-white" : "text-gray-300 hover:text-gray-100"} transition-colors`}
|
|
||||||
onClick={() => {
|
|
||||||
setEpisode({
|
|
||||||
selected: epis,
|
|
||||||
available: episodeList,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between gap-2 min-w-32">
|
|
||||||
<p className="text-[16px] leading-none whitespace-nowrap">
|
|
||||||
{epis.name ?
|
|
||||||
epis.name
|
|
||||||
: ["Sibnet"].includes(source.name) ?
|
|
||||||
`${epis.position + 1} Серия`
|
|
||||||
: `${epis.position} Серия`}
|
|
||||||
</p>
|
|
||||||
{(
|
|
||||||
epis.is_watched ||
|
|
||||||
Object.keys(watchedEpisodes).includes(
|
|
||||||
epis.position.toString()
|
|
||||||
)
|
|
||||||
) ?
|
|
||||||
<span className="w-4 h-4 ml-2 iconify material-symbols--check-circle"></span>
|
|
||||||
: <span className="w-4 h-4 ml-2 iconify material-symbols--check-circle-outline"></span>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
: ""}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,630 +0,0 @@
|
||||||
.media-controller {
|
|
||||||
--_primary-color: var(--media-primary-color, #fff);
|
|
||||||
--_secondary-color: var(--media-secondary-color, transparent);
|
|
||||||
--_accent-color: var(--media-accent-color, #fff);
|
|
||||||
|
|
||||||
--base: 18px;
|
|
||||||
|
|
||||||
font-size: calc(0.75 * var(--base));
|
|
||||||
font-family: Roboto, Arial, sans-serif;
|
|
||||||
--media-font-family: Roboto, helvetica neue, segoe ui, arial, sans-serif;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
|
|
||||||
--media-primary-color: #fff;
|
|
||||||
--media-secondary-color: transparent;
|
|
||||||
--media-menu-background: rgba(28, 28, 28, 0.8);
|
|
||||||
--media-text-color: var(--_primary-color);
|
|
||||||
--media-control-hover-background: var(--media-secondary-color);
|
|
||||||
|
|
||||||
--media-range-track-height: calc(0.125 * var(--base));
|
|
||||||
--media-range-thumb-height: var(--base);
|
|
||||||
--media-range-thumb-width: var(--base);
|
|
||||||
--media-range-thumb-border-radius: var(--base);
|
|
||||||
|
|
||||||
--media-control-height: calc(2 * var(--base));
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-controller[breakpointmd] {
|
|
||||||
--base: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* The biggest size controller is tied to going fullscreen
|
|
||||||
instead of a player width */
|
|
||||||
.media-controller[mediaisfullscreen] {
|
|
||||||
--base: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-controller:not([mediaisfullscreen]) {
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-control-bar {
|
|
||||||
position: absolute;
|
|
||||||
height: calc(2 * var(--base));
|
|
||||||
line-height: calc(2 * var(--base));
|
|
||||||
bottom: calc(1 * var(--base));
|
|
||||||
left: var(--base);
|
|
||||||
right: var(--base);
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-button {
|
|
||||||
--media-control-hover-background: var(--_secondary-color);
|
|
||||||
--media-tooltip-background: rgb(28 28 28 / 0.24);
|
|
||||||
--media-text-content-height: 1.2;
|
|
||||||
--media-tooltip-padding: 0.7em 1em;
|
|
||||||
--media-tooltip-distance: 8px;
|
|
||||||
--media-tooltip-container-margin: 18px;
|
|
||||||
position: relative;
|
|
||||||
padding: 0;
|
|
||||||
opacity: 0.9;
|
|
||||||
transition: opacity 0.1s cubic-bezier(0.4, 0, 1, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.svg {
|
|
||||||
fill: none;
|
|
||||||
stroke: var(--_primary-color, #fff);
|
|
||||||
stroke-width: 1;
|
|
||||||
stroke-linecap: "round";
|
|
||||||
stroke-linejoin: "round";
|
|
||||||
}
|
|
||||||
|
|
||||||
.svg .svg-shadow {
|
|
||||||
stroke: #000;
|
|
||||||
stroke-opacity: 0.15;
|
|
||||||
stroke-width: 2px;
|
|
||||||
fill: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-gradient-bottom {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: calc(12 * var(--base));
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-gradient-bottom::before {
|
|
||||||
content: "";
|
|
||||||
--gradient-steps:
|
|
||||||
hsl(0 0% 0% / 0) 0%, hsl(0 0% 0% / 0.013) 8.1%, hsl(0 0% 0% / 0.049) 15.5%,
|
|
||||||
hsl(0 0% 0% / 0.104) 22.5%, hsl(0 0% 0% / 0.175) 29%,
|
|
||||||
hsl(0 0% 0% / 0.259) 35.3%, hsl(0 0% 0% / 0.352) 41.2%,
|
|
||||||
hsl(0 0% 0% / 0.45) 47.1%, hsl(0 0% 0% / 0.55) 52.9%,
|
|
||||||
hsl(0 0% 0% / 0.648) 58.8%, hsl(0 0% 0% / 0.741) 64.7%,
|
|
||||||
hsl(0 0% 0% / 0.825) 71%, hsl(0 0% 0% / 0.896) 77.5%,
|
|
||||||
hsl(0 0% 0% / 0.951) 84.5%, hsl(0 0% 0% / 0.987) 91.9%, hsl(0 0% 0%) 100%;
|
|
||||||
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
opacity: 0.7;
|
|
||||||
background: linear-gradient(to bottom, var(--gradient-steps));
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-gradient-top {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: calc(8 * var(--base));
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-gradient-top::before {
|
|
||||||
content: "";
|
|
||||||
--gradient-steps:
|
|
||||||
hsl(0 0% 0% / 0) 0%, hsl(0 0% 0% / 0.013) 8.1%, hsl(0 0% 0% / 0.049) 15.5%,
|
|
||||||
hsl(0 0% 0% / 0.104) 22.5%, hsl(0 0% 0% / 0.175) 29%,
|
|
||||||
hsl(0 0% 0% / 0.259) 35.3%, hsl(0 0% 0% / 0.352) 41.2%,
|
|
||||||
hsl(0 0% 0% / 0.45) 47.1%, hsl(0 0% 0% / 0.55) 52.9%,
|
|
||||||
hsl(0 0% 0% / 0.648) 58.8%, hsl(0 0% 0% / 0.741) 64.7%,
|
|
||||||
hsl(0 0% 0% / 0.825) 71%, hsl(0 0% 0% / 0.896) 77.5%,
|
|
||||||
hsl(0 0% 0% / 0.951) 84.5%, hsl(0 0% 0% / 0.987) 91.9%, hsl(0 0% 0%) 100%;
|
|
||||||
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
opacity: 0.7;
|
|
||||||
background: linear-gradient(to top, var(--gradient-steps));
|
|
||||||
}
|
|
||||||
|
|
||||||
.anime-title {
|
|
||||||
position: absolute;
|
|
||||||
height: calc(2 * var(--base));
|
|
||||||
top: calc(0.5 * var(--base));
|
|
||||||
left: var(--base);
|
|
||||||
right: var(--base);
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-settings-menu {
|
|
||||||
--media-menu-icon-height: 20px;
|
|
||||||
--media-menu-item-icon-height: 20px;
|
|
||||||
--media-settings-menu-min-width: calc(10 * var(--base));
|
|
||||||
--media-menu-transform-in: translateY(0) scale(1);
|
|
||||||
--media-menu-transform-out: translateY(20px) rotate(3deg) scale(1);
|
|
||||||
padding-block: calc(0.15 * var(--base));
|
|
||||||
margin-right: 10px;
|
|
||||||
margin-bottom: 17px;
|
|
||||||
border-radius: 8px;
|
|
||||||
z-index: 2;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-source-dialog {
|
|
||||||
--media-menu-icon-height: 20px;
|
|
||||||
--media-menu-item-icon-height: 20px;
|
|
||||||
--media-settings-menu-min-width: calc(10 * var(--base));
|
|
||||||
--media-settings-menu-min-height: calc(2 * var(--base));
|
|
||||||
--media-menu-transform-in: translateY(0) scale(1);
|
|
||||||
--media-menu-transform-out: translateY(20px) rotate(3deg) scale(1);
|
|
||||||
background: rgba(28, 28, 28, 0.8);
|
|
||||||
min-width: var(--media-settings-menu-min-width, 170px);
|
|
||||||
min-height: var(--media-settings-menu-min-height, 170px);
|
|
||||||
position: absolute;
|
|
||||||
right: 10px;
|
|
||||||
bottom: calc(3 * var(--base));
|
|
||||||
padding: 0;
|
|
||||||
padding-block: calc(0.15 * var(--base));
|
|
||||||
padding-inline: calc(0.6 * var(--base));
|
|
||||||
margin-right: 10px;
|
|
||||||
margin-bottom: 17px;
|
|
||||||
border-radius: 8px;
|
|
||||||
user-select: none;
|
|
||||||
width: fit-content;
|
|
||||||
max-height: fit-content;
|
|
||||||
z-index: 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 640px) {
|
|
||||||
.media-source-dialog {
|
|
||||||
max-height: 50%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 1280px) {
|
|
||||||
.media-controller[mediaisfullscreen] .media-source-dialog {
|
|
||||||
max-height: 30%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-controller media-chrome-dialog > div {
|
|
||||||
word-wrap: normal !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-settings-menu[hidden] {
|
|
||||||
display: block;
|
|
||||||
visibility: visible;
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-settings-menu-item,
|
|
||||||
.media-controller [role="menu"]::part(menu-item) {
|
|
||||||
--media-icon-color: var(--_primary-color);
|
|
||||||
margin-inline: calc(0.45 * var(--base));
|
|
||||||
height: calc(1.6 * var(--base));
|
|
||||||
font-size: calc(0.7 * var(--base));
|
|
||||||
font-weight: 400;
|
|
||||||
padding: 0;
|
|
||||||
padding-left: calc(0.4 * var(--base));
|
|
||||||
padding-right: calc(0.1 * var(--base));
|
|
||||||
border-radius: 6px;
|
|
||||||
text-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-controller [slot="submenu"]::part(back button) {
|
|
||||||
font-size: calc(0.7 * var(--base));
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-settings-menu-item:hover {
|
|
||||||
--media-icon-color: #000;
|
|
||||||
color: #000;
|
|
||||||
background-color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-settings-menu-item:hover [slot="submenu"]::part(menu-item),
|
|
||||||
.media-controller [slot="submenu"]::part(back indicator) {
|
|
||||||
--media-icon-color: var(--_primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-settings-menu-item:hover [slot="submenu"]::part(menu-item):hover {
|
|
||||||
--media-icon-color: #000;
|
|
||||||
color: #000;
|
|
||||||
background-color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-settings-menu-item[submenusize="0"] {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quality-settings[submenusize="1"] {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes bounce-scale-play {
|
|
||||||
0% {
|
|
||||||
transform: scale(0.75, 0.75);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
transform: scale(115%, 115%);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: scale(1, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-button {
|
|
||||||
border-radius: 25%;
|
|
||||||
backdrop-filter: blur(10px) invert(15%) brightness(80%) opacity(0);
|
|
||||||
-webkit-backdrop-filter: blur(10px) invert(15%) brightness(80%) opacity(0);
|
|
||||||
transition:
|
|
||||||
backdrop-filter 0.3s,
|
|
||||||
-webkit-backdrop-filter 0.3s,
|
|
||||||
box-shadow 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-button:hover {
|
|
||||||
/* background-color: rgba(0, 0, 0, 0.05); */
|
|
||||||
box-shadow: rgba(0, 0, 0, 0.3) 0px 0px 5px;
|
|
||||||
/* hue-rotate(120deg) */
|
|
||||||
backdrop-filter: blur(10px) invert(15%) brightness(80%) opacity(1);
|
|
||||||
-webkit-backdrop-filter: blur(10px) invert(15%) brightness(80%) opacity(1);
|
|
||||||
transition:
|
|
||||||
backdrop-filter 0.3s,
|
|
||||||
-webkit-backdrop-filter 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-play-button .icon-play {
|
|
||||||
opacity: 0;
|
|
||||||
transform-box: view-box;
|
|
||||||
transform-origin: center center;
|
|
||||||
transform: scale(0.5, 0.5);
|
|
||||||
transition: all 0.5s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-play-button[mediapaused] .icon-play {
|
|
||||||
opacity: 1;
|
|
||||||
transform: scale(1, 1);
|
|
||||||
animation: 0.35s bounce-scale-play ease-in-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes bounce-pause-left {
|
|
||||||
0% {
|
|
||||||
font-size: 10px;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
font-size: 3px;
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
font-size: 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes bounce-pause-right {
|
|
||||||
0% {
|
|
||||||
font-size: 10px;
|
|
||||||
transform: translateX(-8px);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
font-size: 3px;
|
|
||||||
transform: translateX(1px);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
font-size: 4px;
|
|
||||||
transform: translateX(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-play-button .pause-left,
|
|
||||||
.media-play-button .pause-right {
|
|
||||||
font-size: 4px;
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateX(0);
|
|
||||||
transform-box: view-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-play-button:not([mediapaused]) .pause-left {
|
|
||||||
animation: 0.3s bounce-pause-left ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-play-button:not([mediapaused]) .pause-right {
|
|
||||||
animation: 0.3s bounce-pause-right ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-play-button[mediapaused] .pause-left,
|
|
||||||
.media-play-button[mediapaused] .pause-right {
|
|
||||||
opacity: 0;
|
|
||||||
font-size: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-play-button[mediapaused] .pause-right {
|
|
||||||
transform-origin: right center;
|
|
||||||
transform: translateX(-8px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-settings-menu-button svg {
|
|
||||||
transition: transform 0.1s cubic-bezier(0.4, 0, 1, 1);
|
|
||||||
transform: rotateZ(0deg);
|
|
||||||
}
|
|
||||||
.media-settings-menu-button[aria-expanded="true"] svg {
|
|
||||||
transform: rotateZ(30deg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-time-display {
|
|
||||||
position: relative;
|
|
||||||
padding: calc(0.5 * var(--base));
|
|
||||||
font-size: calc(0.7 * var(--base));
|
|
||||||
border-radius: calc(0.5 * var(--base));
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-controller[breakpointmd] .media-time-display:not([showduration]) {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-controller:not([breakpointmd]) .media-time-display[showduration] {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-time-range {
|
|
||||||
height: calc(2 * var(--base));
|
|
||||||
border-radius: calc(0.25 * var(--base));
|
|
||||||
|
|
||||||
--media-range-track-backdrop-filter: invert(10%) blur(5px) brightness(110%);
|
|
||||||
--media-range-track-background: rgba(255, 255, 255, 0.2);
|
|
||||||
--media-range-track-pointer-background: rgba(255, 255, 255, 0.5);
|
|
||||||
--media-range-track-border-radius: calc(0.25 * var(--base));
|
|
||||||
|
|
||||||
--media-time-range-buffered-color: rgba(255, 255, 255, 0.4);
|
|
||||||
--media-range-bar-color: var(--media-accent-color);
|
|
||||||
|
|
||||||
--media-range-thumb-background: var(--media-accent-color);
|
|
||||||
--media-range-thumb-transition: opacity 0.1s linear;
|
|
||||||
--media-range-thumb-opacity: 0;
|
|
||||||
|
|
||||||
--media-preview-thumbnail-border: calc(0.125 * var(--base)) solid #fff;
|
|
||||||
--media-preview-thumbnail-border-radius: calc(0.5 * var(--base));
|
|
||||||
--media-preview-thumbnail-min-width: calc(8 * var(--base));
|
|
||||||
--media-preview-thumbnail-max-width: calc(10 * var(--base));
|
|
||||||
--media-preview-thumbnail-min-height: calc(5 * var(--base));
|
|
||||||
--media-preview-thumbnail-max-height: calc(7 * var(--base));
|
|
||||||
--media-preview-box-margin: 0 0 -10px;
|
|
||||||
}
|
|
||||||
.media-time-range:hover {
|
|
||||||
--media-range-thumb-opacity: 1;
|
|
||||||
--media-range-track-height: calc(0.25 * var(--base));
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-preview-time-display {
|
|
||||||
font-size: calc(0.65 * var(--base));
|
|
||||||
padding-top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-mute-button {
|
|
||||||
position: relative;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-mute-button .muted-path {
|
|
||||||
transition: clip-path 0.2s ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-mute-button .muted-path-2 {
|
|
||||||
transition-delay: 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-mute-button .muted-path {
|
|
||||||
clip-path: inset(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-mute-button:not([mediavolumelevel="off"]) .muted-path-1 {
|
|
||||||
clip-path: inset(0 0 100% 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-mute-button:not([mediavolumelevel="off"]) .muted-path-2 {
|
|
||||||
clip-path: inset(0 0 100% 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-mute-button .muted-path {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-mute-button[mediavolumelevel="off"] .muted-path {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-mute-button .vol-path {
|
|
||||||
opacity: 1;
|
|
||||||
transition: opacity 0.4s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-mute-button[mediavolumelevel="off"] .vol-path {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-mute-button[mediavolumelevel="low"] .vol-high-path,
|
|
||||||
.media-mute-button[mediavolumelevel="medium"] .vol-high-path {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-volume-range {
|
|
||||||
--media-range-track-background: rgba(255, 255, 255, 0.2);
|
|
||||||
--media-range-thumb-opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes volume-in {
|
|
||||||
0% {
|
|
||||||
visibility: hidden;
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(50%) rotate(1deg);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
visibility: visible;
|
|
||||||
opacity: 1;
|
|
||||||
transform: rotate(-2deg);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
visibility: visible;
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0) rotate(0deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes volume-out {
|
|
||||||
0% {
|
|
||||||
visibility: visible;
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0) rotate(0deg);
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
opacity: 1;
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
visibility: hidden;
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(50%) rotate(1deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-volume-range-wrapper {
|
|
||||||
opacity: 0;
|
|
||||||
visibility: hidden;
|
|
||||||
|
|
||||||
position: absolute;
|
|
||||||
top: -100%;
|
|
||||||
left: calc(4 * var(--base));
|
|
||||||
|
|
||||||
width: calc(10 * var(--base));
|
|
||||||
height: calc(2.5 * var(--base));
|
|
||||||
transform-origin: center left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-volume-range {
|
|
||||||
/*
|
|
||||||
Hide range and animation until mediavolume attribute is set.
|
|
||||||
visibility didn't work, hovering over media-volume-range-wrapper
|
|
||||||
caused it to show. Should require mute-button:hover.
|
|
||||||
*/
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0s 1s;
|
|
||||||
|
|
||||||
width: calc(10 * var(--base));
|
|
||||||
height: var(--base);
|
|
||||||
padding: 0;
|
|
||||||
border-radius: calc(0.25 * var(--base));
|
|
||||||
overflow: hidden;
|
|
||||||
background: rgba(0, 0, 0, 0.2);
|
|
||||||
|
|
||||||
--media-range-bar-color: var(--media-accent-color);
|
|
||||||
|
|
||||||
--media-range-padding-left: 0;
|
|
||||||
--media-range-padding-right: 0;
|
|
||||||
|
|
||||||
--media-range-track-width: calc(10 * var(--base));
|
|
||||||
--media-range-track-height: var(--base);
|
|
||||||
--media-range-track-border-radius: calc(0.25 * var(--base));
|
|
||||||
--media-range-track-backdrop-filter: blur(10px) brightness(80%);
|
|
||||||
|
|
||||||
/* This makes zero volume still show some of the bar.
|
|
||||||
I can't make the bar have curved corners otherwise though. */
|
|
||||||
--media-range-thumb-width: var(--base);
|
|
||||||
--media-range-thumb-border-radius: calc(0.25 * var(--base));
|
|
||||||
|
|
||||||
/* The Sutro design has a gradient like this, but not sure I like it */
|
|
||||||
/* --media-range-thumb-box-shadow: 10px 0px 20px rgba(255, 255, 255, 0.5); */
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-volume-range[mediavolume] {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-controller[keyboardcontrol] .media-volume-range:focus {
|
|
||||||
/* TODO: This appears to be creating a think outline */
|
|
||||||
outline: 1px solid rgba(27, 127, 204, 0.9);
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-mute-button:hover + .media-volume-range-wrapper,
|
|
||||||
.media-mute-button:focus + .media-volume-range-wrapper,
|
|
||||||
.media-mute-button:focus-within + .media-volume-range-wrapper,
|
|
||||||
.media-volume-range-wrapper:hover,
|
|
||||||
.media-volume-range-wrapper:focus,
|
|
||||||
.media-volume-range-wrapper:focus-within {
|
|
||||||
animation: 0.3s volume-in forwards ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-volume-range-wrapper:not(:hover, :focus-within) {
|
|
||||||
animation: 0.3s volume-out ease-out;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* When keyboard navigating the volume range and wrapper need to always be visible
|
|
||||||
otherwise focus state can't land on it. This is ok when keyboard navigating because
|
|
||||||
the hovering issues aren't a concern, unless you happen to be keyboard AND mouse navigating.
|
|
||||||
*/
|
|
||||||
.media-controller[keyboardcontrol] .media-volume-range-wrapper,
|
|
||||||
.media-controller[keyboardcontrol] .media-volume-range-wrapper:focus-within,
|
|
||||||
.media-controller[keyboardcontrol]
|
|
||||||
.media-volume-range-wrapper:focus-within
|
|
||||||
.media-volume-range {
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Having trouble getting @property to work in the shadow dom
|
|
||||||
to clean this up. Like https://codepen.io/luwes/pen/oNRyZyx */
|
|
||||||
|
|
||||||
.media-fullscreen-button .fs-arrow {
|
|
||||||
translate: 0% 0%;
|
|
||||||
}
|
|
||||||
.media-fullscreen-button:hover .fs-arrow {
|
|
||||||
animation: 0.35s up-left-bounce cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
||||||
}
|
|
||||||
.media-fullscreen-button:hover .fs-enter-top,
|
|
||||||
.media-fullscreen-button:hover .fs-exit-bottom {
|
|
||||||
animation-name: up-right-bounce;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-fullscreen-button:hover .fs-enter-bottom,
|
|
||||||
.media-fullscreen-button:hover .fs-exit-top {
|
|
||||||
animation-name: down-left-bounce;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes up-left-bounce {
|
|
||||||
0% {
|
|
||||||
translate: 0 0;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
translate: -4% -4%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@keyframes up-right-bounce {
|
|
||||||
0% {
|
|
||||||
translate: 0 0;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
translate: 4% -4%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@keyframes down-left-bounce {
|
|
||||||
0% {
|
|
||||||
translate: 0 0;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
translate: -4% 4%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@keyframes down-right-bounce {
|
|
||||||
0% {
|
|
||||||
translate: 0 0;
|
|
||||||
}
|
|
||||||
50% {
|
|
||||||
translate: 4% 4%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-controller:not([breakpointmd]) .media-pip-button {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.media-controller media-rendition-menu[mediarenditionunavailable],
|
|
||||||
.media-controller media-volume-range[mediavolumeunavailable],
|
|
||||||
.media-controller media-airplay-button[mediaairplayunavailable],
|
|
||||||
.media-controller media-fullscreen-button[mediafullscreenunavailable],
|
|
||||||
.media-controller media-cast-button[mediacastunavailable],
|
|
||||||
.media-controller media-pip-button[mediapipunavailable] {
|
|
||||||
display: none;
|
|
||||||
}
|
|
760
app/components/ReleasePlayer/MediaThemeSutro.tsx
Normal file
|
@ -0,0 +1,760 @@
|
||||||
|
import "media-chrome/react";
|
||||||
|
import "media-chrome/react/menu";
|
||||||
|
import { MediaTheme } from "media-chrome/react/media-theme";
|
||||||
|
|
||||||
|
export default function Page(props: { children: any, className?: string }) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<template
|
||||||
|
id="media-theme-sutro-player-template"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: `
|
||||||
|
<!-- Sutro -->
|
||||||
|
<style>
|
||||||
|
:host {
|
||||||
|
--_primary-color: var(--media-primary-color, #fff);
|
||||||
|
--_secondary-color: var(--media-secondary-color, transparent);
|
||||||
|
--_accent-color: var(--media-accent-color, #fff);
|
||||||
|
}
|
||||||
|
|
||||||
|
media-controller {
|
||||||
|
--base: 18px;
|
||||||
|
|
||||||
|
font-size: calc(0.75 * var(--base));
|
||||||
|
font-family: Roboto, Arial, sans-serif;
|
||||||
|
--media-font-family: Roboto, helvetica neue, segoe ui, arial, sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
|
||||||
|
--media-primary-color: #fff;
|
||||||
|
--media-secondary-color: transparent;
|
||||||
|
--media-menu-background: rgba(28, 28, 28, 0.6);
|
||||||
|
--media-text-color: var(--_primary-color);
|
||||||
|
--media-control-hover-background: var(--media-secondary-color);
|
||||||
|
|
||||||
|
--media-range-track-height: calc(0.125 * var(--base));
|
||||||
|
--media-range-thumb-height: var(--base);
|
||||||
|
--media-range-thumb-width: var(--base);
|
||||||
|
--media-range-thumb-border-radius: var(--base);
|
||||||
|
|
||||||
|
--media-control-height: calc(2 * var(--base));
|
||||||
|
}
|
||||||
|
|
||||||
|
media-controller[breakpointmd] {
|
||||||
|
--base: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The biggest size controller is tied to going fullscreen
|
||||||
|
instead of a player width */
|
||||||
|
media-controller[mediaisfullscreen] {
|
||||||
|
--base: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-button {
|
||||||
|
--media-control-hover-background: var(--_secondary-color);
|
||||||
|
--media-tooltip-background: rgb(28 28 28 / .24);
|
||||||
|
--media-text-content-height: 1.2;
|
||||||
|
--media-tooltip-padding: .7em 1em;
|
||||||
|
--media-tooltip-distance: 8px;
|
||||||
|
--media-tooltip-container-margin: 18px;
|
||||||
|
position: relative;
|
||||||
|
padding: 0;
|
||||||
|
opacity: 0.9;
|
||||||
|
transition: opacity 0.1s cubic-bezier(0.4, 0, 1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-button svg {
|
||||||
|
fill: none;
|
||||||
|
stroke: var(--_primary-color);
|
||||||
|
stroke-width: 1;
|
||||||
|
stroke-linecap: 'round';
|
||||||
|
stroke-linejoin: 'round';
|
||||||
|
}
|
||||||
|
|
||||||
|
svg .svg-shadow {
|
||||||
|
stroke: #000;
|
||||||
|
stroke-opacity: 0.15;
|
||||||
|
stroke-width: 2px;
|
||||||
|
fill: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<media-controller
|
||||||
|
breakpoints="md:480"
|
||||||
|
defaultstreamtype="on-demand"
|
||||||
|
>
|
||||||
|
<slot name="media" slot="media"></slot>
|
||||||
|
<slot name="poster" slot="poster"></slot>
|
||||||
|
<media-error-dialog slot="dialog"></media-error-dialog>
|
||||||
|
|
||||||
|
<!-- Controls Gradient -->
|
||||||
|
<style>
|
||||||
|
.media-gradient-bottom {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: calc(8 * var(--base));
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-gradient-bottom::before {
|
||||||
|
content: '';
|
||||||
|
--gradient-steps: hsl(0 0% 0% / 0) 0%, hsl(0 0% 0% / 0.013) 8.1%, hsl(0 0% 0% / 0.049) 15.5%,
|
||||||
|
hsl(0 0% 0% / 0.104) 22.5%, hsl(0 0% 0% / 0.175) 29%, hsl(0 0% 0% / 0.259) 35.3%, hsl(0 0% 0% / 0.352) 41.2%,
|
||||||
|
hsl(0 0% 0% / 0.45) 47.1%, hsl(0 0% 0% / 0.55) 52.9%, hsl(0 0% 0% / 0.648) 58.8%, hsl(0 0% 0% / 0.741) 64.7%,
|
||||||
|
hsl(0 0% 0% / 0.825) 71%, hsl(0 0% 0% / 0.896) 77.5%, hsl(0 0% 0% / 0.951) 84.5%, hsl(0 0% 0% / 0.987) 91.9%,
|
||||||
|
hsl(0 0% 0%) 100%;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
opacity: 0.7;
|
||||||
|
background: linear-gradient(to bottom, var(--gradient-steps));
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div class="media-gradient-bottom"></div>
|
||||||
|
|
||||||
|
<!-- Settings Menu -->
|
||||||
|
<style>
|
||||||
|
media-settings-menu {
|
||||||
|
--media-menu-icon-height: 20px;
|
||||||
|
--media-menu-item-icon-height: 20px;
|
||||||
|
--media-settings-menu-min-width: calc(10 * var(--base));
|
||||||
|
--media-menu-transform-in: translateY(0) scale(1);
|
||||||
|
--media-menu-transform-out: translateY(20px) rotate(3deg) scale(1);
|
||||||
|
padding-block: calc(0.15 * var(--base));
|
||||||
|
margin-right: 10px;
|
||||||
|
margin-bottom: 17px;
|
||||||
|
border-radius: 8px;
|
||||||
|
z-index: 2;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
media-settings-menu-item,
|
||||||
|
[role='menu']::part(menu-item) {
|
||||||
|
--media-icon-color: var(--_primary-color);
|
||||||
|
margin-inline: calc(0.45 * var(--base));
|
||||||
|
height: calc(1.6 * var(--base));
|
||||||
|
font-size: calc(0.7 * var(--base));
|
||||||
|
font-weight: 400;
|
||||||
|
padding: 0;
|
||||||
|
padding-left: calc(0.4 * var(--base));
|
||||||
|
padding-right: calc(0.1 * var(--base));
|
||||||
|
border-radius: 6px;
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[slot='submenu']::part(back button) {
|
||||||
|
font-size: calc(0.7 * var(--base));
|
||||||
|
}
|
||||||
|
|
||||||
|
media-settings-menu-item:hover {
|
||||||
|
--media-icon-color: #000;
|
||||||
|
color: #000;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
media-settings-menu-item:hover [slot='submenu']::part(menu-item),
|
||||||
|
[slot='submenu']::part(back indicator) {
|
||||||
|
--media-icon-color: var(--_primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
media-settings-menu-item:hover [slot='submenu']::part(menu-item):hover {
|
||||||
|
--media-icon-color: #000;
|
||||||
|
color: #000;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
media-settings-menu-item[submenusize='0'] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Also hide if only Auto is added. */
|
||||||
|
.quality-settings[submenusize='1'] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<media-settings-menu hidden anchor="auto">
|
||||||
|
<media-settings-menu-item>
|
||||||
|
Playback Speed
|
||||||
|
<media-playback-rate-menu slot="submenu" hidden rates="0.5 0.75 1 1.25 1.5 1.75 2">
|
||||||
|
<div slot="title">Playback Speed</div>
|
||||||
|
</media-playback-rate-menu>
|
||||||
|
</media-settings-menu-item>
|
||||||
|
<media-settings-menu-item class="quality-settings">
|
||||||
|
Quality
|
||||||
|
<media-rendition-menu slot="submenu" hidden>
|
||||||
|
<div slot="title">Quality</div>
|
||||||
|
</media-rendition-menu>
|
||||||
|
</media-settings-menu-item>
|
||||||
|
<media-settings-menu-item>
|
||||||
|
Subtitles/CC
|
||||||
|
<media-captions-menu slot="submenu" hidden>
|
||||||
|
<div slot="title">Subtitles/CC</div>
|
||||||
|
</media-captions-menu>
|
||||||
|
</media-settings-menu-item>
|
||||||
|
</media-settings-menu>
|
||||||
|
|
||||||
|
<!-- Control Bar -->
|
||||||
|
<style>
|
||||||
|
media-control-bar {
|
||||||
|
position: absolute;
|
||||||
|
height: calc(2 * var(--base));
|
||||||
|
line-height: calc(2 * var(--base));
|
||||||
|
bottom: var(--base);
|
||||||
|
left: var(--base);
|
||||||
|
right: var(--base);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<media-control-bar>
|
||||||
|
<!-- Play/Pause -->
|
||||||
|
<style>
|
||||||
|
@keyframes bounce-scale-play {
|
||||||
|
0% {
|
||||||
|
transform: scale(0.75, 0.75);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(115%, 115%);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-button {
|
||||||
|
border-radius: 25%;
|
||||||
|
backdrop-filter: blur(10px) invert(15%) brightness(80%) opacity(0);
|
||||||
|
-webkit-backdrop-filter: blur(10px) invert(15%) brightness(80%) opacity(0);
|
||||||
|
transition: backdrop-filter 0.3s, -webkit-backdrop-filter 0.3s, box-shadow 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-button:hover {
|
||||||
|
/* background-color: rgba(0, 0, 0, 0.05); */
|
||||||
|
box-shadow: rgba(0, 0, 0, 0.3) 0px 0px 5px;
|
||||||
|
/* hue-rotate(120deg) */
|
||||||
|
backdrop-filter: blur(10px) invert(15%) brightness(80%) opacity(1);
|
||||||
|
-webkit-backdrop-filter: blur(10px) invert(15%) brightness(80%) opacity(1);
|
||||||
|
transition: backdrop-filter 0.3s, -webkit-backdrop-filter 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
media-play-button #icon-play {
|
||||||
|
opacity: 0;
|
||||||
|
transform-box: view-box;
|
||||||
|
transform-origin: center center;
|
||||||
|
transform: scale(0.5, 0.5);
|
||||||
|
transition: all 0.5s;
|
||||||
|
}
|
||||||
|
|
||||||
|
media-play-button[mediapaused] #icon-play {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1, 1);
|
||||||
|
animation: 0.35s bounce-scale-play ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bounce-pause-left {
|
||||||
|
0% {
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
font-size: 3px;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
font-size: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bounce-pause-right {
|
||||||
|
0% {
|
||||||
|
font-size: 10px;
|
||||||
|
transform: translateX(-8px);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
font-size: 3px;
|
||||||
|
transform: translateX(1px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
font-size: 4px;
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
media-play-button #pause-left,
|
||||||
|
media-play-button #pause-right {
|
||||||
|
/* Using font-size to animate height because using scale was resulting in unexpected positioning */
|
||||||
|
font-size: 4px;
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0);
|
||||||
|
transform-box: view-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
media-play-button:not([mediapaused]) #pause-left {
|
||||||
|
animation: 0.3s bounce-pause-left ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
media-play-button:not([mediapaused]) #pause-right {
|
||||||
|
animation: 0.3s bounce-pause-right ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
media-play-button[mediapaused] #pause-left,
|
||||||
|
media-play-button[mediapaused] #pause-right {
|
||||||
|
opacity: 0;
|
||||||
|
font-size: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
media-play-button[mediapaused] #pause-right {
|
||||||
|
transform-origin: right center;
|
||||||
|
transform: translateX(-8px);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<media-play-button mediapaused class="media-button">
|
||||||
|
<svg slot="icon" viewBox="0 0 32 32">
|
||||||
|
<!-- <use class="svg-shadow" xlink:href="#icon-play"></use> -->
|
||||||
|
<g>
|
||||||
|
<path
|
||||||
|
id="icon-play"
|
||||||
|
d="M20.7131 14.6976C21.7208 15.2735 21.7208 16.7265 20.7131 17.3024L12.7442 21.856C11.7442 22.4274 10.5 21.7054 10.5 20.5536L10.5 11.4464C10.5 10.2946 11.7442 9.57257 12.7442 10.144L20.7131 14.6976Z"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<!-- <use class="svg-shadow" xlink:href="#icon-pause"></use> -->
|
||||||
|
<g id="icon-pause">
|
||||||
|
<rect id="pause-left" x="10.5" width="1em" y="10.5" height="11" rx="0.5" />
|
||||||
|
<rect id="pause-right" x="17.5" width="1em" y="10.5" height="11" rx="0.5" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</media-play-button>
|
||||||
|
|
||||||
|
<!-- Volume/Mute -->
|
||||||
|
<style>
|
||||||
|
media-mute-button {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
media-mute-button .muted-path {
|
||||||
|
transition: clip-path 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
media-mute-button #muted-path-2 {
|
||||||
|
transition-delay: 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
media-mute-button .muted-path {
|
||||||
|
clip-path: inset(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
media-mute-button:not([mediavolumelevel='off']) #muted-path-1 {
|
||||||
|
clip-path: inset(0 0 100% 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
media-mute-button:not([mediavolumelevel='off']) #muted-path-2 {
|
||||||
|
clip-path: inset(0 0 100% 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
media-mute-button .muted-path {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
media-mute-button[mediavolumelevel='off'] .muted-path {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
media-mute-button .vol-path {
|
||||||
|
opacity: 1;
|
||||||
|
transition: opacity 0.4s;
|
||||||
|
}
|
||||||
|
|
||||||
|
media-mute-button[mediavolumelevel='off'] .vol-path {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
media-mute-button[mediavolumelevel='low'] #vol-high-path,
|
||||||
|
media-mute-button[mediavolumelevel='medium'] #vol-high-path {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
media-volume-range {
|
||||||
|
--media-range-track-background: rgba(255, 255, 255, 0.2);
|
||||||
|
--media-range-thumb-opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes volume-in {
|
||||||
|
0% {
|
||||||
|
visibility: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(50%) rotate(1deg);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
visibility: visible;
|
||||||
|
opacity: 1;
|
||||||
|
transform: rotate(-2deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
visibility: visible;
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) rotate(0deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes volume-out {
|
||||||
|
0% {
|
||||||
|
visibility: visible;
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) rotate(0deg);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
visibility: hidden;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(50%) rotate(1deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-volume-range-wrapper {
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
top: -100%;
|
||||||
|
left: calc(2 * var(--base));
|
||||||
|
|
||||||
|
width: calc(10 * var(--base));
|
||||||
|
height: calc(2.5 * var(--base));
|
||||||
|
transform-origin: center left;
|
||||||
|
}
|
||||||
|
|
||||||
|
media-volume-range {
|
||||||
|
/*
|
||||||
|
Hide range and animation until mediavolume attribute is set.
|
||||||
|
visibility didn't work, hovering over media-volume-range-wrapper
|
||||||
|
caused it to show. Should require mute-button:hover.
|
||||||
|
*/
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0s 1s;
|
||||||
|
|
||||||
|
width: calc(10 * var(--base));
|
||||||
|
height: var(--base);
|
||||||
|
padding: 0;
|
||||||
|
border-radius: calc(0.25 * var(--base));
|
||||||
|
overflow: hidden;
|
||||||
|
background: rgba(0, 0, 0, 0.2);
|
||||||
|
|
||||||
|
--media-range-bar-color: var(--media-accent-color);
|
||||||
|
|
||||||
|
--media-range-padding-left: 0;
|
||||||
|
--media-range-padding-right: 0;
|
||||||
|
|
||||||
|
--media-range-track-width: calc(10 * var(--base));
|
||||||
|
--media-range-track-height: var(--base);
|
||||||
|
--media-range-track-border-radius: calc(0.25 * var(--base));
|
||||||
|
--media-range-track-backdrop-filter: blur(10px) brightness(80%);
|
||||||
|
|
||||||
|
/* This makes zero volume still show some of the bar.
|
||||||
|
I can't make the bar have curved corners otherwise though. */
|
||||||
|
--media-range-thumb-width: var(--base);
|
||||||
|
--media-range-thumb-border-radius: calc(0.25 * var(--base));
|
||||||
|
|
||||||
|
/* The Sutro design has a gradient like this, but not sure I like it */
|
||||||
|
/* --media-range-thumb-box-shadow: 10px 0px 20px rgba(255, 255, 255, 0.5); */
|
||||||
|
}
|
||||||
|
|
||||||
|
media-volume-range[mediavolume] {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
[keyboardcontrol] media-volume-range:focus {
|
||||||
|
/* TODO: This appears to be creating a think outline */
|
||||||
|
outline: 1px solid rgba(27, 127, 204, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
media-mute-button:hover + .media-volume-range-wrapper,
|
||||||
|
media-mute-button:focus + .media-volume-range-wrapper,
|
||||||
|
media-mute-button:focus-within + .media-volume-range-wrapper,
|
||||||
|
.media-volume-range-wrapper:hover,
|
||||||
|
.media-volume-range-wrapper:focus,
|
||||||
|
.media-volume-range-wrapper:focus-within {
|
||||||
|
animation: 0.3s volume-in forwards ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.media-volume-range-wrapper:not(:hover, :focus-within) {
|
||||||
|
animation: 0.3s volume-out ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* When keyboard navigating the volume range and wrapper need to always be visible
|
||||||
|
otherwise focus state can't land on it. This is ok when keyboard navigating because
|
||||||
|
the hovering issues aren't a concern, unless you happen to be keyboard AND mouse navigating.
|
||||||
|
*/
|
||||||
|
[keyboardcontrol] .media-volume-range-wrapper,
|
||||||
|
[keyboardcontrol] .media-volume-range-wrapper:focus-within,
|
||||||
|
[keyboardcontrol] .media-volume-range-wrapper:focus-within media-volume-range {
|
||||||
|
visibility: visible;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<media-mute-button class="media-button" notooltip>
|
||||||
|
<use class="svg-shadow" xlink:href="#vol-paths"></use>
|
||||||
|
<svg slot="icon" viewBox="0 0 32 32">
|
||||||
|
<g id="vol-paths">
|
||||||
|
<path
|
||||||
|
id="speaker-path"
|
||||||
|
d="M16.5 20.486v-8.972c0-1.537-2.037-2.08-2.802-.745l-1.026 1.79a2.5 2.5 0 0 1-.8.85l-1.194.78A1.5 1.5 0 0 0 10 15.446v1.11c0 .506.255.978.678 1.255l1.194.782a2.5 2.5 0 0 1 .8.849l1.026 1.79c.765 1.334 2.802.792 2.802-.745Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
id="vol-low-path"
|
||||||
|
class="vol-path"
|
||||||
|
d="M18.5 18C19.6046 18 20.5 17.1046 20.5 16C20.5 14.8954 19.6046 14 18.5 14"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
id="vol-high-path"
|
||||||
|
class="vol-path"
|
||||||
|
d="M18 21C20.7614 21 23 18.7614 23 16C23 13.2386 20.7614 11 18 11"
|
||||||
|
/>
|
||||||
|
<path id="muted-path-1" class="muted-path" d="M23 18L19 14" />
|
||||||
|
<path id="muted-path-2" class="muted-path" d="M23 14L19 18" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</media-mute-button>
|
||||||
|
<div class="media-volume-range-wrapper">
|
||||||
|
<media-volume-range></media-volume-range>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Time Display -->
|
||||||
|
<style>
|
||||||
|
media-time-display {
|
||||||
|
position: relative;
|
||||||
|
padding: calc(0.5 * var(--base));
|
||||||
|
font-size: calc(0.7 * var(--base));
|
||||||
|
border-radius: calc(0.5 * var(--base));
|
||||||
|
}
|
||||||
|
|
||||||
|
media-controller[breakpointmd] media-time-display:not([showduration]) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
media-controller:not([breakpointmd]) media-time-display[showduration] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<media-time-display></media-time-display>
|
||||||
|
<media-time-display showduration></media-time-display>
|
||||||
|
|
||||||
|
<!-- Time Range / Progress Bar -->
|
||||||
|
<style>
|
||||||
|
media-time-range {
|
||||||
|
height: calc(2 * var(--base));
|
||||||
|
border-radius: calc(0.25 * var(--base));
|
||||||
|
|
||||||
|
--media-range-track-backdrop-filter: invert(10%) blur(5px) brightness(110%);
|
||||||
|
--media-range-track-background: rgba(255, 255, 255, 0.2);
|
||||||
|
--media-range-track-pointer-background: rgba(255, 255, 255, 0.5);
|
||||||
|
--media-range-track-border-radius: calc(0.25 * var(--base));
|
||||||
|
|
||||||
|
--media-time-range-buffered-color: rgba(255, 255, 255, 0.4);
|
||||||
|
--media-range-bar-color: var(--media-accent-color);
|
||||||
|
|
||||||
|
--media-range-thumb-background: var(--media-accent-color);
|
||||||
|
--media-range-thumb-transition: opacity 0.1s linear;
|
||||||
|
--media-range-thumb-opacity: 0;
|
||||||
|
|
||||||
|
--media-preview-thumbnail-border: calc(0.125 * var(--base)) solid #fff;
|
||||||
|
--media-preview-thumbnail-border-radius: calc(0.5 * var(--base));
|
||||||
|
--media-preview-thumbnail-min-width: calc(8 * var(--base));
|
||||||
|
--media-preview-thumbnail-max-width: calc(10 * var(--base));
|
||||||
|
--media-preview-thumbnail-min-height: calc(5 * var(--base));
|
||||||
|
--media-preview-thumbnail-max-height: calc(7 * var(--base));
|
||||||
|
--media-preview-box-margin: 0 0 -10px;
|
||||||
|
}
|
||||||
|
media-time-range:hover {
|
||||||
|
--media-range-thumb-opacity: 1;
|
||||||
|
--media-range-track-height: calc(0.25 * var(--base));
|
||||||
|
}
|
||||||
|
|
||||||
|
media-preview-thumbnail {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
media-preview-chapter-display {
|
||||||
|
font-size: calc(0.6 * var(--base));
|
||||||
|
padding-block: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
media-preview-time-display {
|
||||||
|
font-size: calc(0.65 * var(--base));
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<media-time-range>
|
||||||
|
<media-preview-thumbnail slot="preview"></media-preview-thumbnail>
|
||||||
|
<media-preview-chapter-display slot="preview"></media-preview-chapter-display>
|
||||||
|
<media-preview-time-display slot="preview"></media-preview-time-display>
|
||||||
|
</media-time-range>
|
||||||
|
|
||||||
|
<!-- Skip opening Button -->
|
||||||
|
|
||||||
|
<media-seek-forward-button class="media-button" seekoffset="90">
|
||||||
|
<svg slot="icon" width="256" height="256" viewBox="-65 -75 400 400">
|
||||||
|
<path fill="#fff" d="m246.52 118l-88.19-56.13a12 12 0 0 0-12.18-.39A11.66 11.66 0 0 0 140 71.84v44.59L54.33 61.87a12 12 0 0 0-12.18-.39A11.66 11.66 0 0 0 36 71.84v112.32a11.66 11.66 0 0 0 6.15 10.36a12 12 0 0 0 12.18-.39L140 139.57v44.59a11.66 11.66 0 0 0 6.15 10.36a12 12 0 0 0 12.18-.39L246.52 138a11.81 11.81 0 0 0 0-19.94Zm-108.3 13.19L50 187.38a3.91 3.91 0 0 1-4 .13a3.76 3.76 0 0 1-2-3.35V71.84a3.76 3.76 0 0 1 2-3.35a4 4 0 0 1 1.91-.5a3.94 3.94 0 0 1 2.13.63l88.18 56.16a3.8 3.8 0 0 1 0 6.44Zm104 0L154 187.38a3.91 3.91 0 0 1-4 .13a3.76 3.76 0 0 1-2-3.35V71.84a3.76 3.76 0 0 1 2-3.35a4 4 0 0 1 1.91-.5a3.94 3.94 0 0 1 2.13.63l88.18 56.16a3.8 3.8 0 0 1 0 6.44Z" />
|
||||||
|
</svg>
|
||||||
|
</media-seek-forward-button>
|
||||||
|
|
||||||
|
<!-- Settings Menu Button -->
|
||||||
|
<style>
|
||||||
|
media-settings-menu-button svg {
|
||||||
|
transition: transform 0.1s cubic-bezier(0.4, 0, 1, 1);
|
||||||
|
transform: rotateZ(0deg);
|
||||||
|
}
|
||||||
|
media-settings-menu-button[aria-expanded='true'] svg {
|
||||||
|
transform: rotateZ(30deg);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<media-settings-menu-button class="media-button">
|
||||||
|
<svg slot="icon" viewBox="0 0 32 32">
|
||||||
|
<use class="svg-shadow" xlink:href="#settings-icon"></use>
|
||||||
|
<g id="settings-icon">
|
||||||
|
<path
|
||||||
|
d="M16 18C17.1046 18 18 17.1046 18 16C18 14.8954 17.1046 14 16 14C14.8954 14 14 14.8954 14 16C14 17.1046 14.8954 18 16 18Z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M21.0176 13.0362L20.9715 12.9531C20.8445 12.7239 20.7797 12.4629 20.784 12.1982L20.8049 10.8997C20.8092 10.6343 20.675 10.3874 20.4545 10.2549L18.5385 9.10362C18.3186 8.97143 18.0472 8.9738 17.8293 9.10981L16.7658 9.77382C16.5485 9.90953 16.2999 9.98121 16.0465 9.98121H15.9543C15.7004 9.98121 15.4513 9.90922 15.2336 9.77295L14.1652 9.10413C13.9467 8.96728 13.674 8.96518 13.4535 9.09864L11.5436 10.2545C11.3242 10.3873 11.1908 10.6336 11.1951 10.8981L11.216 12.1982C11.2203 12.4629 11.1555 12.7239 11.0285 12.9531L10.9831 13.0351C10.856 13.2645 10.6715 13.4535 10.4493 13.5819L9.36075 14.2109C9.13763 14.3398 8.99942 14.5851 9 14.8511L9.00501 17.152C9.00559 17.4163 9.1432 17.6597 9.36476 17.7883L10.4481 18.4167C10.671 18.546 10.8559 18.7364 10.9826 18.9673L11.0313 19.0559C11.1565 19.284 11.2203 19.5431 11.2161 19.8059L11.1951 21.1003C11.1908 21.3657 11.325 21.6126 11.5456 21.7452L13.4615 22.8964C13.6814 23.0286 13.9528 23.0262 14.1707 22.8902L15.2342 22.2262C15.4515 22.0905 15.7001 22.0188 15.9535 22.0188H16.0457C16.2996 22.0188 16.5487 22.0908 16.7664 22.227L17.8348 22.8959C18.0534 23.0327 18.326 23.0348 18.5465 22.9014L20.4564 21.7455C20.6758 21.6127 20.8092 21.3664 20.8049 21.1019L20.784 19.8018C20.7797 19.5371 20.8445 19.2761 20.9715 19.0469L21.0169 18.9649C21.144 18.7355 21.3285 18.5465 21.5507 18.4181L22.6393 17.7891C22.8624 17.6602 23.0006 17.4149 23 17.1489L22.995 14.848C22.9944 14.5837 22.8568 14.3403 22.6352 14.2117L21.5493 13.5818C21.328 13.4534 21.1442 13.2649 21.0176 13.0362Z"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</media-settings-menu-button>
|
||||||
|
|
||||||
|
<!-- PIP/Mini Player Button -->
|
||||||
|
<style>
|
||||||
|
media-controller:not([breakpointmd]) media-pip-button {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<media-pip-button class="media-button">
|
||||||
|
<svg slot="icon" viewBox="0 0 32 32">
|
||||||
|
<use class="svg-shadow" xlink:href="#pip-icon"></use>
|
||||||
|
<g id="pip-icon">
|
||||||
|
<path
|
||||||
|
d="M12 22H9.77778C9.34822 22 9 21.6162 9 21.1429V10.8571C9 10.3838 9.34822 10 9.77778 10L22.2222 10C22.6518 10 23 10.3838 23 10.8571V12.5714"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M15 21.5714V16.4286C15 16.1919 15.199 16 15.4444 16H22.5556C22.801 16 23 16.1919 23 16.4286V17V21.5714C23 21.8081 22.801 22 22.5556 22H20.3333H17.6667H15.4444C15.199 22 15 21.8081 15 21.5714Z"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</media-pip-button>
|
||||||
|
|
||||||
|
<!-- Airplay Button -->
|
||||||
|
<media-airplay-button class="media-button">
|
||||||
|
<svg viewBox="0 0 32 32" aria-hidden="true" slot="icon">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M20.5 20h1.722c.43 0 .778-.32.778-.714v-8.572c0-.394-.348-.714-.778-.714H9.778c-.43 0-.778.32-.778.714v1.429"/>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M11.5 20H9.778c-.43 0-.778-.32-.778-.714v-8.572c0-.394.348-.714.778-.714h12.444c.43 0 .778.32.778.714v1.429"/>
|
||||||
|
<path stroke-linejoin="round" d="m16 19 3.464 3.75h-6.928L16 19Z"/>
|
||||||
|
</svg>
|
||||||
|
</media-airplay-button>
|
||||||
|
|
||||||
|
<!-- Cast Button -->
|
||||||
|
<media-cast-button class="media-button">
|
||||||
|
<svg slot="icon" viewBox="0 0 32 32">
|
||||||
|
<use class="svg-shadow" xlink:href="#cast-icon"></use>
|
||||||
|
<g id="cast-icon">
|
||||||
|
<path
|
||||||
|
d="M18.5 21.833h4.167c.46 0 .833-.373.833-.833V11a.833.833 0 0 0-.833-.833H9.333A.833.833 0 0 0 8.5 11v1.111m0 8.056c.92 0 1.667.746 1.667 1.666M8.5 17.667a4.167 4.167 0 0 1 4.167 4.166"
|
||||||
|
/>
|
||||||
|
<path d="M8.5 15.167a6.667 6.667 0 0 1 6.667 6.666" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</media-cast-button>
|
||||||
|
|
||||||
|
<!-- Fullscreen Button -->
|
||||||
|
<style>
|
||||||
|
/* Having trouble getting @property to work in the shadow dom
|
||||||
|
to clean this up. Like https://codepen.io/luwes/pen/oNRyZyx */
|
||||||
|
|
||||||
|
media-fullscreen-button .fs-arrow {
|
||||||
|
translate: 0% 0%;
|
||||||
|
}
|
||||||
|
media-fullscreen-button:hover .fs-arrow {
|
||||||
|
animation: 0.35s up-left-bounce cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
|
}
|
||||||
|
media-fullscreen-button:hover #fs-enter-top,
|
||||||
|
media-fullscreen-button:hover #fs-exit-bottom {
|
||||||
|
animation-name: up-right-bounce;
|
||||||
|
}
|
||||||
|
|
||||||
|
media-fullscreen-button:hover #fs-enter-bottom,
|
||||||
|
media-fullscreen-button:hover #fs-exit-top {
|
||||||
|
animation-name: down-left-bounce;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes up-left-bounce {
|
||||||
|
0% {
|
||||||
|
translate: 0 0;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
translate: -4% -4%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes up-right-bounce {
|
||||||
|
0% {
|
||||||
|
translate: 0 0;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
translate: 4% -4%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes down-left-bounce {
|
||||||
|
0% {
|
||||||
|
translate: 0 0;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
translate: -4% 4%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes down-right-bounce {
|
||||||
|
0% {
|
||||||
|
translate: 0 0;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
translate: 4% 4%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<media-fullscreen-button class="media-button">
|
||||||
|
<svg slot="enter" viewBox="0 0 32 32">
|
||||||
|
<use class="svg-shadow" xlink:href="#fs-enter-paths"></use>
|
||||||
|
<g id="fs-enter-paths">
|
||||||
|
<g id="fs-enter-top" class="fs-arrow">
|
||||||
|
<path d="M18 10H22V14" />
|
||||||
|
<path d="M22 10L18 14" />
|
||||||
|
</g>
|
||||||
|
<g id="fs-enter-bottom" class="fs-arrow">
|
||||||
|
<path d="M14 22L10 22V18" />
|
||||||
|
<path d="M10 22L14 18" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
<svg slot="exit" viewBox="0 0 32 32">
|
||||||
|
<use class="svg-shadow" xlink:href="#fs-exit-paths"></use>
|
||||||
|
<g id="fs-exit-paths">
|
||||||
|
<g id="fs-exit-top" class="fs-arrow">
|
||||||
|
<path d="M22 14H18V10" />
|
||||||
|
<path d="M22 10L18 14" />
|
||||||
|
</g>
|
||||||
|
<g id="fs-exit-bottom" class="fs-arrow">
|
||||||
|
<path d="M10 18L14 18V22" />
|
||||||
|
<path d="M14 18L10 22" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
</media-fullscreen-button>
|
||||||
|
</media-control-bar>
|
||||||
|
</media-controller>`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<MediaTheme
|
||||||
|
className={props.className}
|
||||||
|
// @ts-ignore
|
||||||
|
template="media-theme-sutro-player-template"
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</MediaTheme>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,242 +0,0 @@
|
||||||
import { tryCatchPlayer, tryCatchAPI } from "#/api/utils";
|
|
||||||
|
|
||||||
export async function _fetchAPI(
|
|
||||||
url: string,
|
|
||||||
onErrorMsg: string,
|
|
||||||
setPlayerError: (state) => void,
|
|
||||||
onErrorCodes?: Record<number, string>
|
|
||||||
) {
|
|
||||||
const { data, error } = await tryCatchAPI(fetch(url));
|
|
||||||
if (error) {
|
|
||||||
let errorDetail = "Мы правда не знаем что произошло...";
|
|
||||||
|
|
||||||
if (error.name) {
|
|
||||||
if (error.name == "TypeError") {
|
|
||||||
errorDetail = "Не удалось подключиться к серверу";
|
|
||||||
} else {
|
|
||||||
errorDetail = `Неизвестная ошибка ${error.name}: ${error.message}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (error.code) {
|
|
||||||
if (Object.keys(onErrorCodes).includes(error.code.toString())) {
|
|
||||||
errorDetail = onErrorCodes[error.code.toString()];
|
|
||||||
} else {
|
|
||||||
errorDetail = `API вернуло ошибку: ${error.code}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setPlayerError({
|
|
||||||
message: onErrorMsg,
|
|
||||||
detail: errorDetail,
|
|
||||||
});
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function _fetchPlayer(
|
|
||||||
url: string,
|
|
||||||
setPlayerError: (state) => void
|
|
||||||
) {
|
|
||||||
const { data, error } = (await tryCatchPlayer(fetch(url))) as any;
|
|
||||||
if (error) {
|
|
||||||
let errorDetail = "Мы правда не знаем что произошло...";
|
|
||||||
|
|
||||||
if (error.name) {
|
|
||||||
if (error.name == "TypeError") {
|
|
||||||
errorDetail = "Не удалось подключиться к серверу";
|
|
||||||
} else {
|
|
||||||
errorDetail = `Неизвестная ошибка ${error.name}: ${error.message}`;
|
|
||||||
}
|
|
||||||
} else if (error.message) {
|
|
||||||
errorDetail = error.message;
|
|
||||||
}
|
|
||||||
|
|
||||||
setPlayerError({
|
|
||||||
message: "Не удалось получить ссылку на видео",
|
|
||||||
detail: errorDetail,
|
|
||||||
});
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
function decryptKodikLink(enc: string) {
|
|
||||||
const decryptedBase64 = enc.replace(/[a-zA-Z]/g, (e: any) => {
|
|
||||||
return String.fromCharCode(
|
|
||||||
(e <= "Z" ? 90 : 122) >= (e = e.charCodeAt(0) + 18) ? e : e - 26
|
|
||||||
);
|
|
||||||
});
|
|
||||||
return atob(decryptedBase64);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const _fetchKodikManifest = async (
|
|
||||||
url: string,
|
|
||||||
setPlayerError: (state) => void
|
|
||||||
) => {
|
|
||||||
// Fetch episode links via edge function
|
|
||||||
const data = await _fetchPlayer(
|
|
||||||
`https://anix-player.wah.su/?url=${url}&player=kodik`,
|
|
||||||
setPlayerError
|
|
||||||
);
|
|
||||||
if (data) {
|
|
||||||
let lowQualityLink = data.links["360"][0].src; // we assume that 360p is always present
|
|
||||||
|
|
||||||
if (!lowQualityLink.includes("//")) {
|
|
||||||
// check if link is encrypted, else do nothing
|
|
||||||
lowQualityLink = decryptKodikLink(lowQualityLink);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lowQualityLink.includes("https://")) {
|
|
||||||
// strip the https prefix, since we add it manually
|
|
||||||
lowQualityLink = lowQualityLink.replace("https://", "//");
|
|
||||||
}
|
|
||||||
|
|
||||||
let manifest = `https:${lowQualityLink.replace("360.mp4:hls:", "")}`;
|
|
||||||
let poster = `https:${lowQualityLink.replace("360.mp4:hls:manifest.m3u8", "thumb001.jpg")}`;
|
|
||||||
|
|
||||||
if (
|
|
||||||
lowQualityLink.includes("animetvseries") ||
|
|
||||||
lowQualityLink.includes("tvseries")
|
|
||||||
) {
|
|
||||||
// if link includes "animetvseries" or "tvseries" we need to construct manifest ourselves
|
|
||||||
let blobTxt = "#EXTM3U\n";
|
|
||||||
|
|
||||||
if (data.links.hasOwnProperty("240")) {
|
|
||||||
blobTxt += "#EXT-X-STREAM-INF:RESOLUTION=427x240,BANDWIDTH=200000\n";
|
|
||||||
let link = data.links["240"][0].src;
|
|
||||||
let dec = null;
|
|
||||||
link.includes("//") ?
|
|
||||||
link.startsWith("https:") ?
|
|
||||||
(blobTxt += `${link}\n`)
|
|
||||||
: (blobTxt += `https:${link}\n`)
|
|
||||||
: (dec = decryptKodikLink(link));
|
|
||||||
|
|
||||||
dec ?
|
|
||||||
dec.startsWith("https:") ?
|
|
||||||
(blobTxt += `${dec}\n`)
|
|
||||||
: (blobTxt += `https:${dec}\n`)
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.links.hasOwnProperty("360")) {
|
|
||||||
blobTxt += "#EXT-X-STREAM-INF:RESOLUTION=578x360,BANDWIDTH=400000\n";
|
|
||||||
let link = data.links["360"][0].src;
|
|
||||||
let dec = null;
|
|
||||||
link.includes("//") ?
|
|
||||||
link.startsWith("https:") ?
|
|
||||||
(blobTxt += `${link}\n`)
|
|
||||||
: (blobTxt += `https:${link}\n`)
|
|
||||||
: (dec = decryptKodikLink(link));
|
|
||||||
|
|
||||||
dec ?
|
|
||||||
dec.startsWith("https:") ?
|
|
||||||
(blobTxt += `${dec}\n`)
|
|
||||||
: (blobTxt += `https:${dec}\n`)
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.links.hasOwnProperty("480")) {
|
|
||||||
blobTxt += "#EXT-X-STREAM-INF:RESOLUTION=854x480,BANDWIDTH=596000\n";
|
|
||||||
let link = data.links["480"][0].src;
|
|
||||||
let dec = null;
|
|
||||||
link.includes("//") ?
|
|
||||||
link.startsWith("https:") ?
|
|
||||||
(blobTxt += `${link}\n`)
|
|
||||||
: (blobTxt += `https:${link}\n`)
|
|
||||||
: (dec = decryptKodikLink(link));
|
|
||||||
|
|
||||||
dec ?
|
|
||||||
dec.startsWith("https:") ?
|
|
||||||
(blobTxt += `${dec}\n`)
|
|
||||||
: (blobTxt += `https:${dec}\n`)
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.links.hasOwnProperty("720")) {
|
|
||||||
blobTxt += "#EXT-X-STREAM-INF:RESOLUTION=1280x720,BANDWIDTH=1280000\n";
|
|
||||||
let link = data.links["720"][0].src;
|
|
||||||
let dec = null;
|
|
||||||
link.includes("//") ?
|
|
||||||
link.startsWith("https:") ?
|
|
||||||
(blobTxt += `${link}\n`)
|
|
||||||
: (blobTxt += `https:${link}\n`)
|
|
||||||
: (dec = decryptKodikLink(link));
|
|
||||||
|
|
||||||
dec ?
|
|
||||||
dec.startsWith("https:") ?
|
|
||||||
(blobTxt += `${dec}\n`)
|
|
||||||
: (blobTxt += `https:${dec}\n`)
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.links.hasOwnProperty("1080")) {
|
|
||||||
blobTxt += "#EXT-X-STREAM-INF:RESOLUTION=1920x1080,BANDWIDTH=2560000\n";
|
|
||||||
let link = data.links["1080"][0].src;
|
|
||||||
let dec = null;
|
|
||||||
link.includes("//") ?
|
|
||||||
link.startsWith("https:") ?
|
|
||||||
(blobTxt += `${link}\n`)
|
|
||||||
: (blobTxt += `https:${link}\n`)
|
|
||||||
: (dec = decryptKodikLink(link));
|
|
||||||
|
|
||||||
dec ?
|
|
||||||
dec.startsWith("https:") ?
|
|
||||||
(blobTxt += `${dec}\n`)
|
|
||||||
: (blobTxt += `https:${dec}\n`)
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let file = new File([blobTxt], "manifest.m3u8", {
|
|
||||||
type: "application/x-mpegURL",
|
|
||||||
});
|
|
||||||
manifest = URL.createObjectURL(file);
|
|
||||||
}
|
|
||||||
return { manifest, poster };
|
|
||||||
}
|
|
||||||
return { manifest: null, poster: null };
|
|
||||||
};
|
|
||||||
|
|
||||||
export const _fetchAnilibriaManifest = async (
|
|
||||||
url: string,
|
|
||||||
setPlayerError: (state) => void
|
|
||||||
) => {
|
|
||||||
const id = url.split("?id=")[1].split("&ep=")[0];
|
|
||||||
const epid = url.split("?id=")[1].split("&ep=")[1];
|
|
||||||
const _url = `https://api.anilibria.tv/v3/title?id=${id}`;
|
|
||||||
const data = await _fetchPlayer(
|
|
||||||
`https://anix-player.wah.su/?url=${_url}&player=libria`,
|
|
||||||
setPlayerError
|
|
||||||
);
|
|
||||||
if (data) {
|
|
||||||
const host = `https://${data.player.host}`;
|
|
||||||
const ep = data.player.list[epid];
|
|
||||||
|
|
||||||
// we need to manually construct a manifest file for a hls player
|
|
||||||
const blobTxt = `#EXTM3U\n${ep.hls.sd && `#EXT-X-STREAM-INF:RESOLUTION=854x480,BANDWIDTH=596000\n${host}${ep.hls.sd}\n`}${ep.hls.hd && `#EXT-X-STREAM-INF:RESOLUTION=1280x720,BANDWIDTH=1280000\n${host}${ep.hls.hd}\n`}${ep.hls.fhd && `#EXT-X-STREAM-INF:RESOLUTION=1920x1080,BANDWIDTH=2560000\n${host}${ep.hls.fhd}\n`}`;
|
|
||||||
let file = new File([blobTxt], "manifest.m3u8", {
|
|
||||||
type: "application/x-mpegURL",
|
|
||||||
});
|
|
||||||
let manifest = URL.createObjectURL(file);
|
|
||||||
let poster = `https://anixart.libria.fun${ep.preview}`;
|
|
||||||
return { manifest, poster };
|
|
||||||
}
|
|
||||||
return { manifest: null, poster: null };
|
|
||||||
};
|
|
||||||
|
|
||||||
export const _fetchSibnetManifest = async (
|
|
||||||
url: string,
|
|
||||||
setPlayerError: (state) => void
|
|
||||||
) => {
|
|
||||||
// Fetch data via cloud endpoint
|
|
||||||
const data = await _fetchPlayer(
|
|
||||||
`https://sibnet.anix-player.wah.su/?url=${url}`,
|
|
||||||
setPlayerError
|
|
||||||
);
|
|
||||||
if (data) {
|
|
||||||
let manifest = data.video;
|
|
||||||
let poster = data.poster;
|
|
||||||
return { manifest, poster };
|
|
||||||
}
|
|
||||||
return { manifest: null, poster: null };
|
|
||||||
};
|
|
|
@ -3,7 +3,7 @@
|
||||||
import { Spinner } from "#/components/Spinner/Spinner";
|
import { Spinner } from "#/components/Spinner/Spinner";
|
||||||
import { useUserStore } from "#/store/auth";
|
import { useUserStore } from "#/store/auth";
|
||||||
import { useUserPlayerPreferencesStore } from "#/store/player";
|
import { useUserPlayerPreferencesStore } from "#/store/player";
|
||||||
import { Button, Card, Dropdown, DropdownItem } from "flowbite-react";
|
import { Card, Dropdown, Button } from "flowbite-react";
|
||||||
import { ENDPOINTS } from "#/api/config";
|
import { ENDPOINTS } from "#/api/config";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { Swiper, SwiperSlide } from "swiper/react";
|
import { Swiper, SwiperSlide } from "swiper/react";
|
||||||
|
@ -12,7 +12,6 @@ import "swiper/css/navigation";
|
||||||
import "swiper/css/mousewheel";
|
import "swiper/css/mousewheel";
|
||||||
import "swiper/css/scrollbar";
|
import "swiper/css/scrollbar";
|
||||||
import { Navigation, Mousewheel, Scrollbar } from "swiper/modules";
|
import { Navigation, Mousewheel, Scrollbar } from "swiper/modules";
|
||||||
import { usePreferencesStore } from "#/store/preferences";
|
|
||||||
|
|
||||||
const DropdownTheme = {
|
const DropdownTheme = {
|
||||||
floating: {
|
floating: {
|
||||||
|
@ -133,7 +132,6 @@ export const ReleasePlayer = (props: { id: number }) => {
|
||||||
setSelectedSource(player);
|
setSelectedSource(player);
|
||||||
preferredVoiceoverStore.setPreferredPlayer(props.id, player.name);
|
preferredVoiceoverStore.setPreferredPlayer(props.id, player.name);
|
||||||
};
|
};
|
||||||
const preferenceStore = usePreferencesStore();
|
|
||||||
|
|
||||||
function _setError(error: string) {
|
function _setError(error: string) {
|
||||||
setVoiceoverInfo(null);
|
setVoiceoverInfo(null);
|
||||||
|
@ -246,48 +244,25 @@ export const ReleasePlayer = (props: { id: number }) => {
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [props.id, selectedSource, userStore.token]);
|
}, [props.id, selectedSource, userStore.token]);
|
||||||
|
|
||||||
function _addToHistory(episode: any) {
|
async function _addToHistory(episode: any) {
|
||||||
if (props.id && selectedSource && selectedVoiceover && episode) {
|
if (episode && userStore.token) {
|
||||||
const anonEpisodesWatched = getAnonEpisodesWatched(
|
_fetch(
|
||||||
props.id,
|
|
||||||
selectedSource.id,
|
|
||||||
selectedVoiceover.id
|
|
||||||
);
|
|
||||||
if (
|
|
||||||
preferenceStore.flags.saveWatchHistory &&
|
|
||||||
!episode.is_watched &&
|
|
||||||
!Object.keys(
|
|
||||||
anonEpisodesWatched[props.id][selectedSource.id][selectedVoiceover.id]
|
|
||||||
).includes(episode.position.toString())
|
|
||||||
) {
|
|
||||||
episode.is_watched = true;
|
|
||||||
saveAnonEpisodeWatched(
|
|
||||||
props.id,
|
|
||||||
selectedSource.id,
|
|
||||||
selectedVoiceover.id,
|
|
||||||
episode.position
|
|
||||||
);
|
|
||||||
if (userStore.token) {
|
|
||||||
fetch(
|
|
||||||
`${ENDPOINTS.statistic.addHistory}/${props.id}/${selectedSource.id}/${episode.position}?token=${userStore.token}`
|
`${ENDPOINTS.statistic.addHistory}/${props.id}/${selectedSource.id}/${episode.position}?token=${userStore.token}`
|
||||||
);
|
);
|
||||||
fetch(
|
_fetch(
|
||||||
`${ENDPOINTS.statistic.markWatched}/${props.id}/${selectedSource.id}/${episode.position}?token=${userStore.token}`
|
`${ENDPOINTS.statistic.markWatched}/${props.id}/${selectedSource.id}/${episode.position}?token=${userStore.token}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
{!voiceoverInfo || !sourcesInfo || !episodeInfo ?
|
{!voiceoverInfo || !sourcesInfo || !episodeInfo ? (
|
||||||
<div className="flex items-center justify-center w-full aspect-video">
|
<div className="flex items-center justify-center w-full aspect-video">
|
||||||
{!error ?
|
{!error ? <Spinner /> : <p>{error}</p>}
|
||||||
<Spinner />
|
|
||||||
: <p>{error}</p>}
|
|
||||||
</div>
|
</div>
|
||||||
: <>
|
) : (
|
||||||
|
<>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
<Dropdown
|
<Dropdown
|
||||||
label={`Озвучка: ${selectedVoiceover.name}`}
|
label={`Озвучка: ${selectedVoiceover.name}`}
|
||||||
|
@ -295,14 +270,14 @@ export const ReleasePlayer = (props: { id: number }) => {
|
||||||
theme={DropdownTheme}
|
theme={DropdownTheme}
|
||||||
>
|
>
|
||||||
{voiceoverInfo.map((voiceover: any) => (
|
{voiceoverInfo.map((voiceover: any) => (
|
||||||
<DropdownItem
|
<Dropdown.Item
|
||||||
key={`voiceover_${voiceover.id}`}
|
key={`voiceover_${voiceover.id}`}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setSelectedVoiceoverAndSaveAsPreferred(voiceover)
|
setSelectedVoiceoverAndSaveAsPreferred(voiceover)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{voiceover.name}
|
{voiceover.name}
|
||||||
</DropdownItem>
|
</Dropdown.Item>
|
||||||
))}
|
))}
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
|
@ -311,23 +286,25 @@ export const ReleasePlayer = (props: { id: number }) => {
|
||||||
theme={DropdownTheme}
|
theme={DropdownTheme}
|
||||||
>
|
>
|
||||||
{sourcesInfo.map((source: any) => (
|
{sourcesInfo.map((source: any) => (
|
||||||
<DropdownItem
|
<Dropdown.Item
|
||||||
key={`source_${source.id}`}
|
key={`source_${source.id}`}
|
||||||
onClick={() => setSelectedPlayerAndSaveAsPreferred(source)}
|
onClick={() => setSelectedPlayerAndSaveAsPreferred(source)}
|
||||||
>
|
>
|
||||||
{source.name}
|
{source.name}
|
||||||
</DropdownItem>
|
</Dropdown.Item>
|
||||||
))}
|
))}
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
<div className="aspect-video">
|
<div className="aspect-video">
|
||||||
{selectedEpisode ?
|
{selectedEpisode ? (
|
||||||
<iframe
|
<iframe
|
||||||
allowFullScreen={true}
|
allowFullScreen={true}
|
||||||
src={selectedEpisode.url}
|
src={selectedEpisode.url}
|
||||||
className="w-full h-full rounded-md"
|
className="w-full h-full rounded-md"
|
||||||
></iframe>
|
></iframe>
|
||||||
: <p>Ошибка загрузки плеера</p>}
|
) : (
|
||||||
|
<p>Ошибка загрузки плеера</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Swiper
|
<Swiper
|
||||||
|
@ -337,12 +314,9 @@ export const ReleasePlayer = (props: { id: number }) => {
|
||||||
direction={"horizontal"}
|
direction={"horizontal"}
|
||||||
mousewheel={{
|
mousewheel={{
|
||||||
enabled: true,
|
enabled: true,
|
||||||
sensitivity: 4,
|
sensitivity: 2,
|
||||||
}}
|
|
||||||
scrollbar={{
|
|
||||||
enabled: true,
|
|
||||||
draggable: true,
|
|
||||||
}}
|
}}
|
||||||
|
scrollbar={true}
|
||||||
allowTouchMove={true}
|
allowTouchMove={true}
|
||||||
style={
|
style={
|
||||||
{
|
{
|
||||||
|
@ -354,52 +328,54 @@ export const ReleasePlayer = (props: { id: number }) => {
|
||||||
<SwiperSlide
|
<SwiperSlide
|
||||||
key={`episode_${episode.position}`}
|
key={`episode_${episode.position}`}
|
||||||
style={{ maxWidth: "fit-content" }}
|
style={{ maxWidth: "fit-content" }}
|
||||||
className="pb-2"
|
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
color={
|
color={
|
||||||
selectedEpisode.position === episode.position ?
|
selectedEpisode.position === episode.position
|
||||||
"blue"
|
? "blue"
|
||||||
: "light"
|
: "light"
|
||||||
}
|
}
|
||||||
theme={{ base: "w-full disabled:opacity-100" }}
|
theme={{ base: "w-full disabled:opacity-100" }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedEpisode(episode);
|
setSelectedEpisode(episode);
|
||||||
|
episode.is_watched = true;
|
||||||
_addToHistory(episode);
|
_addToHistory(episode);
|
||||||
|
saveAnonEpisodeWatched(
|
||||||
|
props.id,
|
||||||
|
selectedSource.id,
|
||||||
|
selectedVoiceover.id,
|
||||||
|
episode.position
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
disabled={selectedEpisode.position === episode.position}
|
disabled={selectedEpisode.position === episode.position}
|
||||||
>
|
>
|
||||||
{episode.name ?
|
{episode.name
|
||||||
episode.name
|
? episode.name
|
||||||
: `${
|
: `${
|
||||||
(
|
|
||||||
!["Sibnet", "Sibnet (не работает)"].includes(
|
!["Sibnet", "Sibnet (не работает)"].includes(
|
||||||
selectedSource.name
|
selectedSource.name
|
||||||
)
|
)
|
||||||
) ?
|
? episode.position
|
||||||
episode.position
|
|
||||||
: episode.position + 1
|
: episode.position + 1
|
||||||
} серия`
|
} серия`}
|
||||||
}
|
{episode.is_watched ||
|
||||||
{(
|
|
||||||
episode.is_watched ||
|
|
||||||
getAnonCurrentEpisodeWatched(
|
getAnonCurrentEpisodeWatched(
|
||||||
props.id,
|
props.id,
|
||||||
selectedSource.id,
|
selectedSource.id,
|
||||||
selectedVoiceover.id,
|
selectedVoiceover.id,
|
||||||
episode.position
|
episode.position
|
||||||
)
|
) ? (
|
||||||
) ?
|
|
||||||
<span className="w-5 h-5 ml-2 iconify material-symbols--check-circle"></span>
|
<span className="w-5 h-5 ml-2 iconify material-symbols--check-circle"></span>
|
||||||
: <span className="w-5 h-5 ml-2 opacity-10 iconify material-symbols--check-circle"></span>
|
) : (
|
||||||
}
|
<span className="w-5 h-5 ml-2 opacity-10 iconify material-symbols--check-circle"></span>
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</SwiperSlide>
|
</SwiperSlide>
|
||||||
))}
|
))}
|
||||||
</Swiper>
|
</Swiper>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
76
app/components/ReleasePlayer/SourceSelector.tsx
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Dropdown } from "flowbite-react";
|
||||||
|
import { numberDeclension } from "#/api/utils";
|
||||||
|
import { useUserPlayerPreferencesStore } from "#/store/player";
|
||||||
|
|
||||||
|
interface Source {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
episodes_count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DropdownTrigger = ({ name }: Source) => {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-1 cursor-pointer">
|
||||||
|
<span className="w-6 h-6 iconify material-symbols--motion-play"></span>
|
||||||
|
<p>{name}</p>
|
||||||
|
<span className="w-6 h-6 iconify material-symbols--arrow-drop-down"></span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const DropdownItem = ({ name, episodes_count }: Source) => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2 cursor-pointer">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<p>{name}</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<p>
|
||||||
|
{episodes_count}{" "}
|
||||||
|
{numberDeclension(episodes_count, "серия", "серии", "серий")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SourceSelector = (props: {
|
||||||
|
availableSource: Source[];
|
||||||
|
source: Source;
|
||||||
|
setSource: any;
|
||||||
|
release_id: any;
|
||||||
|
}) => {
|
||||||
|
const playerPreferenceStore = useUserPlayerPreferencesStore();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown
|
||||||
|
label=""
|
||||||
|
dismissOnClick={true}
|
||||||
|
renderTrigger={() => (
|
||||||
|
<span>
|
||||||
|
<DropdownTrigger {...props.source} />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{props.availableSource.map((source: Source) => (
|
||||||
|
<Dropdown.Item
|
||||||
|
key={`source_${source.id}`}
|
||||||
|
onClick={() => {
|
||||||
|
playerPreferenceStore.setPreferredPlayer(
|
||||||
|
props.release_id,
|
||||||
|
source.name
|
||||||
|
);
|
||||||
|
props.setSource({
|
||||||
|
selected: source,
|
||||||
|
available: props.availableSource,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DropdownItem {...source} />
|
||||||
|
</Dropdown.Item>
|
||||||
|
))}
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,117 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { ENDPOINTS } from "#/api/config";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import { _fetchAPI } from "./PlayerParsing";
|
|
||||||
import { useUserPlayerPreferencesStore } from "#/store/player";
|
|
||||||
import { numberDeclension } from "#/api/utils";
|
|
||||||
import { Voiceover } from "./VoiceoverSelectorMenu";
|
|
||||||
|
|
||||||
export interface Source {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
episodes_count: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SourceSelectorMenuProps {
|
|
||||||
release_id: number;
|
|
||||||
setSource: (state) => void;
|
|
||||||
voiceover: Voiceover;
|
|
||||||
source: Source;
|
|
||||||
sourceList: Source[];
|
|
||||||
setPlayerError: (state) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SourceSelectorMenu = ({
|
|
||||||
release_id,
|
|
||||||
setSource,
|
|
||||||
voiceover,
|
|
||||||
source,
|
|
||||||
sourceList,
|
|
||||||
setPlayerError,
|
|
||||||
}: SourceSelectorMenuProps) => {
|
|
||||||
const playerPreferenceStore = useUserPlayerPreferencesStore();
|
|
||||||
const preferredSource = playerPreferenceStore.getPreferredPlayer(release_id);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const __getInfo = async () => {
|
|
||||||
let url = `${ENDPOINTS.release.episode}/${release_id}/${voiceover.id}`;
|
|
||||||
const src = await _fetchAPI(
|
|
||||||
url,
|
|
||||||
"Не удалось получить информацию о источниках",
|
|
||||||
setPlayerError
|
|
||||||
);
|
|
||||||
if (src) {
|
|
||||||
const selectedSrc =
|
|
||||||
src.sources.find(
|
|
||||||
(source: Source) => source.name === preferredSource
|
|
||||||
) || src.sources[0];
|
|
||||||
if (selectedSrc.episodes_count == 0) {
|
|
||||||
const remSources = src.sources.filter(
|
|
||||||
(source: any) => source.id !== selectedSrc.id
|
|
||||||
);
|
|
||||||
setSource({
|
|
||||||
selected: remSources[0],
|
|
||||||
available: remSources,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setSource({
|
|
||||||
selected: selectedSrc,
|
|
||||||
available: src.sources,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if (voiceover) {
|
|
||||||
__getInfo();
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [voiceover]);
|
|
||||||
|
|
||||||
if (!voiceover || !source || !sourceList || sourceList.length <= 1) return <></>
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col items-start justify-start gap-4">
|
|
||||||
<p className="text-[20px] px-2 pt-2 pb-1 font-bold">Источник</p>
|
|
||||||
<div className="max-h-full flex flex-col gap-4 items-start justify-start overflow-x-hidden overflow-y-auto px-2 pb-2 scrollbar-thin scrollbar-thumb-[rgb(60_60_60_/_.8)] scrollbar-track-[rgb(30_30_30_/_.8)]">
|
|
||||||
{sourceList && sourceList.length > 0 ?
|
|
||||||
sourceList.map((src: Source) => {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={`release-${release_id}-voiceover-${voiceover.id}-source-${src.id}`}
|
|
||||||
className={`h-fit ${source.id == src.id ? "text-white" : "text-gray-300 hover:text-gray-100"} transition-colors`}
|
|
||||||
onClick={() => {
|
|
||||||
setSource({
|
|
||||||
selected: src,
|
|
||||||
available: sourceList,
|
|
||||||
});
|
|
||||||
playerPreferenceStore.setPreferredPlayer(
|
|
||||||
release_id,
|
|
||||||
src.name
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col w-full gap-1">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<span className="text-[16px] leading-none">{src.name}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<span>
|
|
||||||
{src.episodes_count || 0}{" "}
|
|
||||||
{numberDeclension(
|
|
||||||
src.episodes_count || 0,
|
|
||||||
"серия",
|
|
||||||
"серии",
|
|
||||||
"серий"
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
: ""}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
102
app/components/ReleasePlayer/VoiceoverSelector.tsx
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Dropdown } from "flowbite-react";
|
||||||
|
import { numberDeclension } from "#/api/utils";
|
||||||
|
import { useUserPlayerPreferencesStore } from "#/store/player";
|
||||||
|
|
||||||
|
interface Voiceover {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
icon: string;
|
||||||
|
episodes_count: number;
|
||||||
|
view_count: number;
|
||||||
|
pinned: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DropdownTrigger = ({ icon, name, pinned }: Voiceover) => {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center gap-2 cursor-pointer">
|
||||||
|
{icon && <img className="w-6 h-6 rounded-full" src={icon}></img>}
|
||||||
|
<p>{name}</p>
|
||||||
|
{pinned && (
|
||||||
|
<span className="h-6 bg-gray-700 dark:bg-gray-300 iconify material-symbols--push-pin"></span>
|
||||||
|
)}
|
||||||
|
<span className="w-6 h-6 -ml-2 iconify material-symbols--arrow-drop-down"></span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const DropdownItem = ({
|
||||||
|
icon,
|
||||||
|
name,
|
||||||
|
pinned,
|
||||||
|
episodes_count,
|
||||||
|
view_count,
|
||||||
|
}: Voiceover) => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-2 cursor-pointer">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{icon && <img className="w-6 h-6 rounded-full" src={icon}></img>}
|
||||||
|
<p>{name}</p>
|
||||||
|
{pinned && (
|
||||||
|
<span className="h-6 iconify material-symbols--push-pin"></span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<p>
|
||||||
|
{episodes_count}{" "}
|
||||||
|
{numberDeclension(episodes_count, "серия", "серии", "серий")}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{view_count}{" "}
|
||||||
|
{numberDeclension(view_count, "просмотр", "просмотра", "просмотров")}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const DropdownTheme = {
|
||||||
|
content: "md:grid md:grid-cols-2 xl:grid-cols-4 gap-2 w-full container",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const VoiceoverSelector = (props: {
|
||||||
|
availableVoiceover: Voiceover[];
|
||||||
|
voiceover: Voiceover;
|
||||||
|
setVoiceover: any;
|
||||||
|
release_id: number;
|
||||||
|
}) => {
|
||||||
|
const playerPreferenceStore = useUserPlayerPreferencesStore();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown
|
||||||
|
theme={DropdownTheme}
|
||||||
|
label=""
|
||||||
|
dismissOnClick={true}
|
||||||
|
renderTrigger={() => (
|
||||||
|
<span>
|
||||||
|
<DropdownTrigger {...props.voiceover} />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{props.availableVoiceover.map((voiceover: Voiceover) => (
|
||||||
|
<Dropdown.Item
|
||||||
|
className="w-fit"
|
||||||
|
key={`voiceover_${voiceover.id}`}
|
||||||
|
onClick={() => {
|
||||||
|
playerPreferenceStore.setPreferredVoiceover(
|
||||||
|
props.release_id,
|
||||||
|
voiceover.name
|
||||||
|
);
|
||||||
|
props.setVoiceover({
|
||||||
|
selected: voiceover,
|
||||||
|
available: props.availableVoiceover,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DropdownItem {...voiceover} />
|
||||||
|
</Dropdown.Item>
|
||||||
|
))}
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,109 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import { ENDPOINTS } from "#/api/config";
|
|
||||||
import { useEffect } from "react";
|
|
||||||
import { _fetchAPI } from "./PlayerParsing";
|
|
||||||
import { useUserPlayerPreferencesStore } from "#/store/player";
|
|
||||||
import { numberDeclension } from "#/api/utils";
|
|
||||||
|
|
||||||
export interface Voiceover {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
icon: string;
|
|
||||||
episodes_count: number;
|
|
||||||
view_count: number;
|
|
||||||
pinned: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface VoiceoverSelectorMenuProps {
|
|
||||||
release_id: number;
|
|
||||||
token: string | null;
|
|
||||||
setVoiceover: (state) => void;
|
|
||||||
voiceover: Voiceover;
|
|
||||||
voiceoverList: Voiceover[];
|
|
||||||
setPlayerError: (state) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const VoiceoverSelectorMenu = ({
|
|
||||||
release_id,
|
|
||||||
token,
|
|
||||||
setVoiceover,
|
|
||||||
voiceover,
|
|
||||||
voiceoverList,
|
|
||||||
setPlayerError,
|
|
||||||
}: VoiceoverSelectorMenuProps) => {
|
|
||||||
const playerPreferenceStore = useUserPlayerPreferencesStore();
|
|
||||||
const preferredVO = playerPreferenceStore.getPreferredVoiceover(release_id);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const __getInfo = async () => {
|
|
||||||
let url = `${ENDPOINTS.release.episode}/${release_id}`;
|
|
||||||
if (token) {
|
|
||||||
url += `?token=${token}`;
|
|
||||||
}
|
|
||||||
const vo = await _fetchAPI(
|
|
||||||
url,
|
|
||||||
"Не удалось получить информацию о озвучках",
|
|
||||||
setPlayerError,
|
|
||||||
{ 1: "Просмотр запрещён" }
|
|
||||||
);
|
|
||||||
if (vo) {
|
|
||||||
const selectedVO =
|
|
||||||
vo.types.find((voiceover: Voiceover) => voiceover.name === preferredVO) ||
|
|
||||||
vo.types[0];
|
|
||||||
setVoiceover({
|
|
||||||
selected: selectedVO,
|
|
||||||
available: vo.types,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
__getInfo();
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [release_id, token]);
|
|
||||||
|
|
||||||
if (!voiceover) return <></>
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col items-start justify-start gap-4">
|
|
||||||
<p className="text-[20px] px-2 pt-2 pb-1 font-bold">Озвучка</p>
|
|
||||||
<div className="max-h-full flex flex-col gap-4 items-start justify-start overflow-x-hidden overflow-y-auto px-2 pb-2 scrollbar-thin scrollbar-thumb-[rgb(60_60_60_/_.8)] scrollbar-track-[rgb(30_30_30_/_.8)]">
|
|
||||||
{voiceoverList && voiceoverList.length > 0 ?
|
|
||||||
voiceoverList.map((vo: Voiceover) => {
|
|
||||||
return (
|
|
||||||
<button key={`release-${release_id}-voiceover-${vo.id}`}
|
|
||||||
className={`h-fit px-2 ${voiceover.id == vo.id ? "text-white" : "text-gray-300 hover:text-gray-100"} transition-colors`}
|
|
||||||
onClick={() => {
|
|
||||||
setVoiceover({
|
|
||||||
selected: vo,
|
|
||||||
available: voiceoverList
|
|
||||||
});
|
|
||||||
playerPreferenceStore.setPreferredVoiceover(
|
|
||||||
release_id,
|
|
||||||
vo.name
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div className="flex flex-col w-full gap-1">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
||||||
{vo.icon ? <img alt="" className="w-6 h-6 rounded-full" src={vo.icon}></img> : ""}
|
|
||||||
<span className="text-[16px] leading-none whitespace-nowrap">{vo.name}</span>
|
|
||||||
{vo.pinned && (
|
|
||||||
<span className={`h-4 iconify material-symbols--push-pin ${voiceover.id == vo.id ? "bg-white" : "bg-gray-300 hover:bg-gray-100"} transition-colors`}></span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
||||||
<span>{vo.episodes_count} {numberDeclension(vo.episodes_count, "серия", "серии", "серий")}</span>
|
|
||||||
<span>{vo.view_count} {numberDeclension(vo.view_count, "просмотр", "просмотра", "просмотров")}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
: ""}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -4,16 +4,12 @@ import { CURRENT_APP_VERSION } from "#/api/config";
|
||||||
import { useUserStore } from "#/store/auth";
|
import { useUserStore } from "#/store/auth";
|
||||||
import { usePreferencesStore } from "#/store/preferences";
|
import { usePreferencesStore } from "#/store/preferences";
|
||||||
import {
|
import {
|
||||||
Button,
|
|
||||||
ButtonGroup,
|
|
||||||
Dropdown,
|
|
||||||
DropdownItem,
|
|
||||||
HR,
|
|
||||||
Modal,
|
Modal,
|
||||||
ModalBody,
|
Button,
|
||||||
ModalHeader,
|
|
||||||
ToggleSwitch,
|
|
||||||
useThemeMode,
|
useThemeMode,
|
||||||
|
ToggleSwitch,
|
||||||
|
HR,
|
||||||
|
Dropdown,
|
||||||
} from "flowbite-react";
|
} from "flowbite-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
|
@ -58,8 +54,8 @@ export const SettingsModal = (props: { isOpen: boolean; setIsOpen: any }) => {
|
||||||
show={props.isOpen}
|
show={props.isOpen}
|
||||||
onClose={() => props.setIsOpen(false)}
|
onClose={() => props.setIsOpen(false)}
|
||||||
>
|
>
|
||||||
<ModalHeader>Настройки</ModalHeader>
|
<Modal.Header>Настройки</Modal.Header>
|
||||||
<ModalBody>
|
<Modal.Body>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span className="w-6 h-6 iconify material-symbols--palette-outline"></span>
|
<span className="w-6 h-6 iconify material-symbols--palette-outline"></span>
|
||||||
|
@ -67,20 +63,20 @@ export const SettingsModal = (props: { isOpen: boolean; setIsOpen: any }) => {
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<p className=" dark:text-white">Тема</p>
|
<p className=" dark:text-white">Тема</p>
|
||||||
<ButtonGroup>
|
<Button.Group>
|
||||||
<Button
|
<Button
|
||||||
color={computedMode == "light" ? "blue" : "light"}
|
color={computedMode == "light" ? "blue" : "gray"}
|
||||||
onClick={() => setMode("light")}
|
onClick={() => setMode("light")}
|
||||||
>
|
>
|
||||||
Светлая
|
Светлая
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
color={computedMode == "dark" ? "blue" : "light"}
|
color={computedMode == "dark" ? "blue" : "gray"}
|
||||||
onClick={() => setMode("dark")}
|
onClick={() => setMode("dark")}
|
||||||
>
|
>
|
||||||
Темная
|
Темная
|
||||||
</Button>
|
</Button>
|
||||||
</ButtonGroup>
|
</Button.Group>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<p className=" dark:text-white max-w-96">
|
<p className=" dark:text-white max-w-96">
|
||||||
|
@ -89,6 +85,15 @@ export const SettingsModal = (props: { isOpen: boolean; setIsOpen: any }) => {
|
||||||
</p>
|
</p>
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
color="blue"
|
color="blue"
|
||||||
|
theme={{
|
||||||
|
toggle: {
|
||||||
|
checked: {
|
||||||
|
color: {
|
||||||
|
blue: "border-blue-700 bg-blue-700",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
onChange={() =>
|
onChange={() =>
|
||||||
preferenceStore.setParams({
|
preferenceStore.setParams({
|
||||||
skipToCategory: {
|
skipToCategory: {
|
||||||
|
@ -116,7 +121,7 @@ export const SettingsModal = (props: { isOpen: boolean; setIsOpen: any }) => {
|
||||||
>
|
>
|
||||||
{Object.keys(HomeCategory).map((key) => {
|
{Object.keys(HomeCategory).map((key) => {
|
||||||
return (
|
return (
|
||||||
<DropdownItem
|
<Dropdown.Item
|
||||||
key={key}
|
key={key}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
preferenceStore.setParams({
|
preferenceStore.setParams({
|
||||||
|
@ -128,7 +133,7 @@ export const SettingsModal = (props: { isOpen: boolean; setIsOpen: any }) => {
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{HomeCategory[key]}
|
{HomeCategory[key]}
|
||||||
</DropdownItem>
|
</Dropdown.Item>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
@ -147,7 +152,7 @@ export const SettingsModal = (props: { isOpen: boolean; setIsOpen: any }) => {
|
||||||
>
|
>
|
||||||
{Object.keys(BookmarksCategory).map((key) => {
|
{Object.keys(BookmarksCategory).map((key) => {
|
||||||
return (
|
return (
|
||||||
<DropdownItem
|
<Dropdown.Item
|
||||||
key={key}
|
key={key}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
preferenceStore.setParams({
|
preferenceStore.setParams({
|
||||||
|
@ -159,7 +164,7 @@ export const SettingsModal = (props: { isOpen: boolean; setIsOpen: any }) => {
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{BookmarksCategory[key]}
|
{BookmarksCategory[key]}
|
||||||
</DropdownItem>
|
</Dropdown.Item>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
@ -177,7 +182,7 @@ export const SettingsModal = (props: { isOpen: boolean; setIsOpen: any }) => {
|
||||||
{Object.keys(NavbarTitles).map(
|
{Object.keys(NavbarTitles).map(
|
||||||
(key: "always" | "links" | "selected" | "never") => {
|
(key: "always" | "links" | "selected" | "never") => {
|
||||||
return (
|
return (
|
||||||
<DropdownItem
|
<Dropdown.Item
|
||||||
className={`${key == "links" ? "hidden lg:flex" : ""}`}
|
className={`${key == "links" ? "hidden lg:flex" : ""}`}
|
||||||
key={`navbar-titles-${key}`}
|
key={`navbar-titles-${key}`}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
|
@ -187,7 +192,7 @@ export const SettingsModal = (props: { isOpen: boolean; setIsOpen: any }) => {
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{NavbarTitles[key]}
|
{NavbarTitles[key]}
|
||||||
</DropdownItem>
|
</Dropdown.Item>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
|
@ -206,7 +211,7 @@ export const SettingsModal = (props: { isOpen: boolean; setIsOpen: any }) => {
|
||||||
: "Нет"
|
: "Нет"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<DropdownItem
|
<Dropdown.Item
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
preferenceStore.setFlags({
|
preferenceStore.setFlags({
|
||||||
showFifthButton: null,
|
showFifthButton: null,
|
||||||
|
@ -214,10 +219,10 @@ export const SettingsModal = (props: { isOpen: boolean; setIsOpen: any }) => {
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Не показывать
|
Не показывать
|
||||||
</DropdownItem>
|
</Dropdown.Item>
|
||||||
{Object.keys(FifthButton).map((key) => {
|
{Object.keys(FifthButton).map((key) => {
|
||||||
return (
|
return (
|
||||||
<DropdownItem
|
<Dropdown.Item
|
||||||
key={`navbar-fifthbutton-${key}`}
|
key={`navbar-fifthbutton-${key}`}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
preferenceStore.setFlags({
|
preferenceStore.setFlags({
|
||||||
|
@ -226,7 +231,7 @@ export const SettingsModal = (props: { isOpen: boolean; setIsOpen: any }) => {
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{FifthButton[key]}
|
{FifthButton[key]}
|
||||||
</DropdownItem>
|
</Dropdown.Item>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
@ -241,6 +246,15 @@ export const SettingsModal = (props: { isOpen: boolean; setIsOpen: any }) => {
|
||||||
<p className=" dark:text-white">Показывать список изменений</p>
|
<p className=" dark:text-white">Показывать список изменений</p>
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
color="blue"
|
color="blue"
|
||||||
|
theme={{
|
||||||
|
toggle: {
|
||||||
|
checked: {
|
||||||
|
color: {
|
||||||
|
blue: "border-blue-700 bg-blue-700",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
onChange={() =>
|
onChange={() =>
|
||||||
preferenceStore.setFlags({
|
preferenceStore.setFlags({
|
||||||
showChangelog: !preferenceStore.flags.showChangelog,
|
showChangelog: !preferenceStore.flags.showChangelog,
|
||||||
|
@ -249,24 +263,6 @@ export const SettingsModal = (props: { isOpen: boolean; setIsOpen: any }) => {
|
||||||
checked={preferenceStore.flags.showChangelog}
|
checked={preferenceStore.flags.showChangelog}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p className=" dark:text-white">Сохранять историю просмотра</p>
|
|
||||||
<p className="max-w-sm text-gray-500 dark:text-gray-300">
|
|
||||||
При отключении, история не будет сохранятся как локально, так и
|
|
||||||
на аккаунте
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<ToggleSwitch
|
|
||||||
color="blue"
|
|
||||||
onChange={() =>
|
|
||||||
preferenceStore.setFlags({
|
|
||||||
saveWatchHistory: !preferenceStore.flags.saveWatchHistory,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
checked={preferenceStore.flags.saveWatchHistory}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className=" dark:text-white">Отправка аналитики</p>
|
<p className=" dark:text-white">Отправка аналитики</p>
|
||||||
|
@ -276,6 +272,15 @@ export const SettingsModal = (props: { isOpen: boolean; setIsOpen: any }) => {
|
||||||
</div>
|
</div>
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
color="blue"
|
color="blue"
|
||||||
|
theme={{
|
||||||
|
toggle: {
|
||||||
|
checked: {
|
||||||
|
color: {
|
||||||
|
blue: "border-blue-700 bg-blue-700",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
onChange={() =>
|
onChange={() =>
|
||||||
preferenceStore.setFlags({
|
preferenceStore.setFlags({
|
||||||
enableAnalytics: !preferenceStore.flags.enableAnalytics,
|
enableAnalytics: !preferenceStore.flags.enableAnalytics,
|
||||||
|
@ -298,6 +303,15 @@ export const SettingsModal = (props: { isOpen: boolean; setIsOpen: any }) => {
|
||||||
</div>
|
</div>
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
color="blue"
|
color="blue"
|
||||||
|
theme={{
|
||||||
|
toggle: {
|
||||||
|
checked: {
|
||||||
|
color: {
|
||||||
|
blue: "border-blue-700 bg-blue-700",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
onChange={() =>
|
onChange={() =>
|
||||||
preferenceStore.setParams({
|
preferenceStore.setParams({
|
||||||
experimental: {
|
experimental: {
|
||||||
|
@ -326,7 +340,7 @@ export const SettingsModal = (props: { isOpen: boolean; setIsOpen: any }) => {
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</ModalBody>
|
</Modal.Body>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,17 +1,10 @@
|
||||||
export const Spinner = (props: { size?: "base" | "md" | "lg" }) => {
|
export const Spinner = () => {
|
||||||
let size = "w-8 h-8";
|
|
||||||
if (props.size == "md") {
|
|
||||||
size = "w-12 h-12";
|
|
||||||
}
|
|
||||||
if (props.size == "lg") {
|
|
||||||
size = "w-16 h-16";
|
|
||||||
}
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div role="status">
|
<div role="status">
|
||||||
<svg
|
<svg
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className={`inline ${size} text-gray-200 animate-spin dark:text-gray-600 fill-blue-600`}
|
className="inline w-8 h-8 text-gray-200 animate-spin dark:text-gray-600 fill-blue-600"
|
||||||
viewBox="0 0 100 101"
|
viewBox="0 0 100 101"
|
||||||
fill="none"
|
fill="none"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
|
|
@ -15,6 +15,7 @@ import {
|
||||||
AccordionPanel,
|
AccordionPanel,
|
||||||
AccordionTitle,
|
AccordionTitle,
|
||||||
} from "flowbite-react";
|
} from "flowbite-react";
|
||||||
|
import { version } from "node:os";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
export const AboutPage = () => {
|
export const AboutPage = () => {
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { Spinner } from "#/components/Spinner/Spinner";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useScrollPosition } from "#/hooks/useScrollPosition";
|
import { useScrollPosition } from "#/hooks/useScrollPosition";
|
||||||
import { useUserStore } from "../store/auth";
|
import { useUserStore } from "../store/auth";
|
||||||
import { Button, ButtonGroup, Dropdown, DropdownItem } from "flowbite-react";
|
import { Dropdown, Button } from "flowbite-react";
|
||||||
import { sort } from "./common";
|
import { sort } from "./common";
|
||||||
import { ENDPOINTS } from "#/api/config";
|
import { ENDPOINTS } from "#/api/config";
|
||||||
import { BookmarksList, useSWRfetcher } from "#/api/utils";
|
import { BookmarksList, useSWRfetcher } from "#/api/utils";
|
||||||
|
@ -151,7 +151,7 @@ export function BookmarksCategoryPage(props: any) {
|
||||||
</form>
|
</form>
|
||||||
: ""}
|
: ""}
|
||||||
<div className="m-4 overflow-auto">
|
<div className="m-4 overflow-auto">
|
||||||
<ButtonGroup>
|
<Button.Group>
|
||||||
<Button
|
<Button
|
||||||
className="whitespace-nowrap"
|
className="whitespace-nowrap"
|
||||||
disabled={props.slug == "watching"}
|
disabled={props.slug == "watching"}
|
||||||
|
@ -222,7 +222,7 @@ export function BookmarksCategoryPage(props: any) {
|
||||||
>
|
>
|
||||||
{props.SectionTitleMapping["abandoned"]}
|
{props.SectionTitleMapping["abandoned"]}
|
||||||
</Button>
|
</Button>
|
||||||
</ButtonGroup>
|
</Button.Group>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center justify-between px-4 py-2 border-b-2 border-black dark:border-white">
|
<div className="flex items-center justify-between px-4 py-2 border-b-2 border-black dark:border-white">
|
||||||
<h1 className="font-bold text-md sm:text-xl md:text-lg xl:text-xl">
|
<h1 className="font-bold text-md sm:text-xl md:text-lg xl:text-xl">
|
||||||
|
@ -237,7 +237,7 @@ export function BookmarksCategoryPage(props: any) {
|
||||||
theme={DropdownTheme}
|
theme={DropdownTheme}
|
||||||
>
|
>
|
||||||
{sort.values.map((item, index) => (
|
{sort.values.map((item, index) => (
|
||||||
<DropdownItem key={index} onClick={() => setSelectedSort(index)}>
|
<Dropdown.Item key={index} onClick={() => setSelectedSort(index)}>
|
||||||
<span
|
<span
|
||||||
className={`w-6 h-6 iconify ${
|
className={`w-6 h-6 iconify ${
|
||||||
sort.values[index].value.split("_")[1] == "descending" ?
|
sort.values[index].value.split("_")[1] == "descending" ?
|
||||||
|
@ -246,7 +246,7 @@ export function BookmarksCategoryPage(props: any) {
|
||||||
}`}
|
}`}
|
||||||
></span>
|
></span>
|
||||||
{item.name}
|
{item.name}
|
||||||
</DropdownItem>
|
</Dropdown.Item>
|
||||||
))}
|
))}
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,15 +5,14 @@ import { useEffect, useState, useCallback } from "react";
|
||||||
import { useSearchParams, useRouter } from "next/navigation";
|
import { useSearchParams, useRouter } from "next/navigation";
|
||||||
import { ENDPOINTS } from "#/api/config";
|
import { ENDPOINTS } from "#/api/config";
|
||||||
import {
|
import {
|
||||||
Button,
|
|
||||||
Card,
|
Card,
|
||||||
|
Button,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
|
TextInput,
|
||||||
|
Textarea,
|
||||||
FileInput,
|
FileInput,
|
||||||
Label,
|
Label,
|
||||||
Modal,
|
Modal,
|
||||||
ModalHeader,
|
|
||||||
Textarea,
|
|
||||||
TextInput,
|
|
||||||
useThemeMode,
|
useThemeMode,
|
||||||
} from "flowbite-react";
|
} from "flowbite-react";
|
||||||
import { PosterWithStuff } from "#/components/ReleasePoster/PosterWithStuff";
|
import { PosterWithStuff } from "#/components/ReleasePoster/PosterWithStuff";
|
||||||
|
@ -34,7 +33,6 @@ export const CreateCollectionPage = () => {
|
||||||
if (userStore.state === "finished" && !userStore.token) {
|
if (userStore.state === "finished" && !userStore.token) {
|
||||||
router.push("/login?redirect=/collections/create");
|
router.push("/login?redirect=/collections/create");
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [userStore]);
|
}, [userStore]);
|
||||||
|
|
||||||
const [edit, setEdit] = useState(false);
|
const [edit, setEdit] = useState(false);
|
||||||
|
@ -390,9 +388,10 @@ export const CreateCollectionPage = () => {
|
||||||
</Label>
|
</Label>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="block mb-2">
|
<div className="block mb-2">
|
||||||
<Label htmlFor="title">
|
<Label
|
||||||
Название (минимум 10, максимум 60 символов)
|
htmlFor="title"
|
||||||
</Label>
|
value="Название (минимум 10, максимум 60 символов)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<TextInput
|
<TextInput
|
||||||
id="title"
|
id="title"
|
||||||
|
@ -409,9 +408,10 @@ export const CreateCollectionPage = () => {
|
||||||
{stringLength.title}/60
|
{stringLength.title}/60
|
||||||
</p>
|
</p>
|
||||||
<div className="block mt-2 mb-2">
|
<div className="block mt-2 mb-2">
|
||||||
<Label htmlFor="description">
|
<Label
|
||||||
Описание (максимум 1000 символов)
|
htmlFor="description"
|
||||||
</Label>
|
value="Описание (максимум 1000 символов)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Textarea
|
<Textarea
|
||||||
rows={4}
|
rows={4}
|
||||||
|
@ -434,7 +434,7 @@ export const CreateCollectionPage = () => {
|
||||||
checked={isPrivate}
|
checked={isPrivate}
|
||||||
onChange={(e) => setIsPrivate(e.target.checked)}
|
onChange={(e) => setIsPrivate(e.target.checked)}
|
||||||
/>
|
/>
|
||||||
<Label htmlFor="private">Приватная коллекция</Label>
|
<Label htmlFor="private" value="Приватная коллекция" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
|
@ -603,7 +603,7 @@ export const ReleasesEditModal = (props: {
|
||||||
onClose={() => props.setIsOpen(false)}
|
onClose={() => props.setIsOpen(false)}
|
||||||
size={"7xl"}
|
size={"7xl"}
|
||||||
>
|
>
|
||||||
<ModalHeader>Изменить релизы в коллекции</ModalHeader>
|
<Modal.Header>Изменить релизы в коллекции</Modal.Header>
|
||||||
<div
|
<div
|
||||||
onScroll={handleScroll}
|
onScroll={handleScroll}
|
||||||
ref={modalRef}
|
ref={modalRef}
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { Spinner } from "#/components/Spinner/Spinner";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useScrollPosition } from "#/hooks/useScrollPosition";
|
import { useScrollPosition } from "#/hooks/useScrollPosition";
|
||||||
import { useUserStore } from "../store/auth";
|
import { useUserStore } from "../store/auth";
|
||||||
import { Button, Dropdown, DropdownItem } from "flowbite-react";
|
import { Dropdown, Button } from "flowbite-react";
|
||||||
import { sort } from "./common";
|
import { sort } from "./common";
|
||||||
import { ENDPOINTS } from "#/api/config";
|
import { ENDPOINTS } from "#/api/config";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
@ -126,7 +126,7 @@ export function FavoritesPage() {
|
||||||
theme={DropdownTheme}
|
theme={DropdownTheme}
|
||||||
>
|
>
|
||||||
{sort.values.map((item, index) => (
|
{sort.values.map((item, index) => (
|
||||||
<DropdownItem key={index} onClick={() => setSelectedSort(index)}>
|
<Dropdown.Item key={index} onClick={() => setSelectedSort(index)}>
|
||||||
<span
|
<span
|
||||||
className={`w-6 h-6 iconify ${
|
className={`w-6 h-6 iconify ${
|
||||||
sort.values[index].value.split("_")[1] == "descending"
|
sort.values[index].value.split("_")[1] == "descending"
|
||||||
|
@ -135,7 +135,7 @@ export function FavoritesPage() {
|
||||||
}`}
|
}`}
|
||||||
></span>
|
></span>
|
||||||
{item.name}
|
{item.name}
|
||||||
</DropdownItem>
|
</Dropdown.Item>
|
||||||
))}
|
))}
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { useState, useEffect } from "react";
|
||||||
import { useScrollPosition } from "#/hooks/useScrollPosition";
|
import { useScrollPosition } from "#/hooks/useScrollPosition";
|
||||||
import { useUserStore } from "../store/auth";
|
import { useUserStore } from "../store/auth";
|
||||||
import { _FetchHomePageReleases } from "#/api/utils";
|
import { _FetchHomePageReleases } from "#/api/utils";
|
||||||
import { Button, ButtonGroup } from "flowbite-react";
|
import { Button } from "flowbite-react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
export function IndexCategoryPage(props) {
|
export function IndexCategoryPage(props) {
|
||||||
|
@ -53,13 +53,13 @@ export function IndexCategoryPage(props) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="mb-4 overflow-auto">
|
<div className="mb-4 overflow-auto">
|
||||||
<ButtonGroup>
|
<Button.Group>
|
||||||
<Button className="whitespace-nowrap" disabled={props.slug == "last"} color="light" onClick={() => router.push("/home/last")}>{props.SectionTitleMapping["last"]}</Button>
|
<Button className="whitespace-nowrap" disabled={props.slug == "last"} color="light" onClick={() => router.push("/home/last")}>{props.SectionTitleMapping["last"]}</Button>
|
||||||
<Button className="whitespace-nowrap" disabled={props.slug == "finished"} color="light" onClick={() => router.push("/home/finished")}>{props.SectionTitleMapping["finished"]}</Button>
|
<Button className="whitespace-nowrap" disabled={props.slug == "finished"} color="light" onClick={() => router.push("/home/finished")}>{props.SectionTitleMapping["finished"]}</Button>
|
||||||
<Button className="whitespace-nowrap" disabled={props.slug == "ongoing"} color="light" onClick={() => router.push("/home/ongoing")}>{props.SectionTitleMapping["ongoing"]}</Button>
|
<Button className="whitespace-nowrap" disabled={props.slug == "ongoing"} color="light" onClick={() => router.push("/home/ongoing")}>{props.SectionTitleMapping["ongoing"]}</Button>
|
||||||
<Button className="whitespace-nowrap" disabled={props.slug == "announce"} color="light" onClick={() => router.push("/home/announce")}>{props.SectionTitleMapping["announce"]}</Button>
|
<Button className="whitespace-nowrap" disabled={props.slug == "announce"} color="light" onClick={() => router.push("/home/announce")}>{props.SectionTitleMapping["announce"]}</Button>
|
||||||
<Button className="whitespace-nowrap" disabled={props.slug == "films"} color="light" onClick={() => router.push("/home/films")}>{props.SectionTitleMapping["films"]}</Button>
|
<Button className="whitespace-nowrap" disabled={props.slug == "films"} color="light" onClick={() => router.push("/home/films")}>{props.SectionTitleMapping["films"]}</Button>
|
||||||
</ButtonGroup>
|
</Button.Group>
|
||||||
</div>
|
</div>
|
||||||
{content && content.length > 0 ? (
|
{content && content.length > 0 ? (
|
||||||
<ReleaseSection
|
<ReleaseSection
|
||||||
|
|
|
@ -59,6 +59,53 @@ export const ProfilePage = (props: any) => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasSocials =
|
||||||
|
user.vk_page != "" ||
|
||||||
|
user.tg_page != "" ||
|
||||||
|
user.tt_page != "" ||
|
||||||
|
user.inst_page != "" ||
|
||||||
|
user.discord_page != "" ||
|
||||||
|
false;
|
||||||
|
const socials = [
|
||||||
|
{
|
||||||
|
name: "vk",
|
||||||
|
nickname: user.vk_page,
|
||||||
|
icon: "fa6-brands--vk",
|
||||||
|
urlPrefix: "https://vk.com/",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "telegram",
|
||||||
|
nickname: user.tg_page,
|
||||||
|
icon: "fa6-brands--telegram",
|
||||||
|
urlPrefix: "https://t.me/",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "discord",
|
||||||
|
nickname: user.discord_page,
|
||||||
|
icon: "fa6-brands--discord",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "tiktok",
|
||||||
|
nickname: user.tt_page,
|
||||||
|
icon: "fa6-brands--tiktok",
|
||||||
|
urlPrefix: "https://tiktok.com/@",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "instagram",
|
||||||
|
nickname: user.inst_page,
|
||||||
|
icon: "fa6-brands--instagram",
|
||||||
|
urlPrefix: "https://instagram.com/",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const hasChips =
|
||||||
|
user.is_verified ||
|
||||||
|
user.is_blocked ||
|
||||||
|
(user.roles && user.roles.length > 0) ||
|
||||||
|
isMyProfile;
|
||||||
|
const isPrivacy =
|
||||||
|
user.is_stats_hidden || user.is_counts_hidden || user.is_social_hidden;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
|
@ -69,48 +116,61 @@ export const ProfilePage = (props: any) => {
|
||||||
ban_expires={user.ban_expires}
|
ban_expires={user.ban_expires}
|
||||||
/>
|
/>
|
||||||
<ProfilePrivacyBanner
|
<ProfilePrivacyBanner
|
||||||
is_privacy={
|
is_privacy={isPrivacy}
|
||||||
user.is_stats_hidden ||
|
|
||||||
user.is_counts_hidden ||
|
|
||||||
user.is_social_hidden
|
|
||||||
}
|
|
||||||
is_me_blocked={user.is_me_blocked}
|
is_me_blocked={user.is_me_blocked}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={`grid grid-cols-1 gap-2 lg:grid-cols-2 ${
|
className={`flex flex-wrap gap-2 ${
|
||||||
(
|
isPrivacy || user.is_banned || user.is_perm_banned ? "mt-4" : ""
|
||||||
user.is_banned ||
|
}`}
|
||||||
user.is_perm_banned ||
|
|
||||||
user.is_stats_hidden ||
|
|
||||||
user.is_counts_hidden ||
|
|
||||||
user.is_social_hidden
|
|
||||||
) ?
|
|
||||||
"mt-4"
|
|
||||||
: ""
|
|
||||||
} mb-4`}
|
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2 w-full xl:w-[50%]">
|
||||||
<ProfileUser
|
<ProfileUser
|
||||||
avatar={user.avatar || ""}
|
isOnline={user.is_online}
|
||||||
login={user.login || ""}
|
avatar={user.avatar}
|
||||||
status={user.status || ""}
|
login={user.login}
|
||||||
roles={user.roles || []}
|
status={user.status}
|
||||||
rating={user.rating_score || 0}
|
|
||||||
isMyProfile={isMyProfile || false}
|
|
||||||
isVerified={user.is_verified || false}
|
|
||||||
isOnline={user.is_online || false}
|
|
||||||
isSponsor={user.is_sponsor || false}
|
|
||||||
isBlocked={user.is_blocked || false}
|
|
||||||
socials={{
|
socials={{
|
||||||
vk: user.vk_page || null,
|
isPrivate: user.is_social_hidden,
|
||||||
tg: user.tg_page || null,
|
hasSocials: hasSocials,
|
||||||
tt: user.tt_page || null,
|
socials: socials,
|
||||||
inst: user.inst_page || null,
|
|
||||||
discord: user.discord_page || null,
|
|
||||||
}}
|
}}
|
||||||
is_social_hidden={user.is_social_hidden}
|
chips={{
|
||||||
|
hasChips: hasChips,
|
||||||
|
isMyProfile: isMyProfile,
|
||||||
|
isVerified: user.is_verified,
|
||||||
|
isSponsor: user.is_sponsor,
|
||||||
|
isBlocked: user.is_blocked,
|
||||||
|
roles: user.roles,
|
||||||
|
}}
|
||||||
|
rating={user.rating_score}
|
||||||
/>
|
/>
|
||||||
|
{!user.is_counts_hidden && (
|
||||||
|
<ProfileActivity
|
||||||
|
profile_id={user.id}
|
||||||
|
commentCount={user.comment_count}
|
||||||
|
videoCount={user.video_count}
|
||||||
|
collectionCount={user.collection_count}
|
||||||
|
friendsCount={user.friend_count}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!user.is_stats_hidden && (
|
||||||
|
<div className="flex-col hidden gap-2 xl:flex">
|
||||||
|
{user.votes && user.votes.length > 0 && (
|
||||||
|
<ProfileReleaseRatings
|
||||||
|
ratings={user.votes}
|
||||||
|
token={authUser.token}
|
||||||
|
profile_id={user.id}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{user.history && user.history.length > 0 && (
|
||||||
|
<ProfileReleaseHistory history={user.history} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col w-full gap-2 xl:flex-1 xl:w-auto ">
|
||||||
{authUser.token && (
|
{authUser.token && (
|
||||||
<ProfileActions
|
<ProfileActions
|
||||||
isMyProfile={isMyProfile}
|
isMyProfile={isMyProfile}
|
||||||
|
@ -125,32 +185,6 @@ export const ProfilePage = (props: any) => {
|
||||||
edit_setIsOpen={setIsOpen}
|
edit_setIsOpen={setIsOpen}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!user.is_counts_hidden && (
|
|
||||||
<ProfileActivity
|
|
||||||
profile_id={user.id}
|
|
||||||
commentCount={user.comment_count}
|
|
||||||
commentPreview={user.release_comments_preview || []}
|
|
||||||
collectionCount={user.collection_count}
|
|
||||||
collectionPreview={user.collections_preview || []}
|
|
||||||
friendsCount={user.friend_count}
|
|
||||||
friendsPreview={user.friends_preview || []}
|
|
||||||
token={authUser.token}
|
|
||||||
isMyProfile={isMyProfile || false}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{!user.is_stats_hidden && (
|
|
||||||
<div className="flex-col hidden gap-2 lg:flex">
|
|
||||||
{user.votes && user.votes.length > 0 && (
|
|
||||||
<ProfileReleaseRatings
|
|
||||||
ratings={user.votes}
|
|
||||||
token={authUser.token}
|
|
||||||
profile_id={user.id}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
{!user.is_stats_hidden && (
|
{!user.is_stats_hidden && (
|
||||||
<>
|
<>
|
||||||
<ProfileStats
|
<ProfileStats
|
||||||
|
@ -166,7 +200,7 @@ export const ProfilePage = (props: any) => {
|
||||||
profile_id={user.id}
|
profile_id={user.id}
|
||||||
/>
|
/>
|
||||||
<ProfileWatchDynamic watchDynamic={user.watch_dynamics || []} />
|
<ProfileWatchDynamic watchDynamic={user.watch_dynamics || []} />
|
||||||
<div className="flex flex-col gap-2 lg:hidden">
|
<div className="flex flex-col gap-2 xl:hidden">
|
||||||
{user.votes && user.votes.length > 0 && (
|
{user.votes && user.votes.length > 0 && (
|
||||||
<ProfileReleaseRatings
|
<ProfileReleaseRatings
|
||||||
ratings={user.votes}
|
ratings={user.votes}
|
||||||
|
@ -174,10 +208,10 @@ export const ProfilePage = (props: any) => {
|
||||||
profile_id={user.id}
|
profile_id={user.id}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
{user.history && user.history.length > 0 && (
|
{user.history && user.history.length > 0 && (
|
||||||
<ProfileReleaseHistory history={user.history} />
|
<ProfileReleaseHistory history={user.history} />
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -18,7 +18,6 @@ import { CommentsMain } from "#/components/Comments/Comments.Main";
|
||||||
import { InfoLists } from "#/components/InfoLists/InfoLists";
|
import { InfoLists } from "#/components/InfoLists/InfoLists";
|
||||||
import { ENDPOINTS } from "#/api/config";
|
import { ENDPOINTS } from "#/api/config";
|
||||||
import { usePreferencesStore } from "#/store/preferences";
|
import { usePreferencesStore } from "#/store/preferences";
|
||||||
import { ContinueWatching } from "#/components/ContinueWatching/ContinueWatching";
|
|
||||||
|
|
||||||
export const ReleasePage = (props: any) => {
|
export const ReleasePage = (props: any) => {
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
@ -72,7 +71,7 @@ export const ReleasePage = (props: any) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-2 pb-8">
|
<div className="flex flex-col gap-2">
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-[70%_30%] gap-2 grid-flow-row-dense">
|
<div className="grid grid-cols-1 lg:grid-cols-[70%_30%] gap-2 grid-flow-row-dense">
|
||||||
<ReleaseInfoBasics
|
<ReleaseInfoBasics
|
||||||
image={data.release.image}
|
image={data.release.image}
|
||||||
|
@ -119,18 +118,16 @@ export const ReleasePage = (props: any) => {
|
||||||
data.release.status.name.toLowerCase() != "анонс" && (
|
data.release.status.name.toLowerCase() != "анонс" && (
|
||||||
<>
|
<>
|
||||||
{preferenceStore.params.experimental.newPlayer ?
|
{preferenceStore.params.experimental.newPlayer ?
|
||||||
<ReleasePlayerCustom id={props.id} token={userStore.token} title={data.release.title_ru || data.release.title_original || ""} />
|
<ReleasePlayerCustom id={props.id} token={userStore.token} />
|
||||||
: <ReleasePlayer id={props.id} />}
|
: <ReleasePlayer id={props.id} />}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<div className="hidden lg:block">
|
|
||||||
<CommentsMain
|
<CommentsMain
|
||||||
release_id={props.id}
|
release_id={props.id}
|
||||||
token={userStore.token}
|
token={userStore.token}
|
||||||
comments={data.release.comments}
|
comments={data.release.comments}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
{data.release.status &&
|
{data.release.status &&
|
||||||
data.release.status.name.toLowerCase() != "анонс" && (
|
data.release.status.name.toLowerCase() != "анонс" && (
|
||||||
|
@ -168,14 +165,6 @@ export const ReleasePage = (props: any) => {
|
||||||
related_releases={data.release.related_releases}
|
related_releases={data.release.related_releases}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{userStore.token && <ContinueWatching />}
|
|
||||||
<div className="block lg:hidden">
|
|
||||||
<CommentsMain
|
|
||||||
release_id={props.id}
|
|
||||||
token={userStore.token}
|
|
||||||
comments={data.release.comments}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { useScrollPosition } from "#/hooks/useScrollPosition";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useSearchParams } from "next/navigation";
|
import { useSearchParams } from "next/navigation";
|
||||||
import { useUserStore } from "../store/auth";
|
import { useUserStore } from "../store/auth";
|
||||||
import { Button, Dropdown, DropdownItem, Modal, ModalBody, ModalFooter, ModalHeader } from "flowbite-react";
|
import { Button, Dropdown, Modal } from "flowbite-react";
|
||||||
import { CollectionsSection } from "#/components/CollectionsSection/CollectionsSection";
|
import { CollectionsSection } from "#/components/CollectionsSection/CollectionsSection";
|
||||||
import { UserSection } from "#/components/UserSection/UserSection";
|
import { UserSection } from "#/components/UserSection/UserSection";
|
||||||
import { useSWRfetcher } from "#/api/utils";
|
import { useSWRfetcher } from "#/api/utils";
|
||||||
|
@ -360,8 +360,8 @@ const FiltersModal = (props: {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal show={props.isOpen} onClose={() => _cancel()}>
|
<Modal show={props.isOpen} onClose={() => _cancel()}>
|
||||||
<ModalHeader>Фильтры</ModalHeader>
|
<Modal.Header>Фильтры</Modal.Header>
|
||||||
<ModalBody>
|
<Modal.Body>
|
||||||
<div className="my-4">
|
<div className="my-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<p className="font-bold dark:text-white">Искать в</p>
|
<p className="font-bold dark:text-white">Искать в</p>
|
||||||
|
@ -376,12 +376,12 @@ const FiltersModal = (props: {
|
||||||
return <></>;
|
return <></>;
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<DropdownItem
|
<Dropdown.Item
|
||||||
onClick={() => setWhere(item)}
|
onClick={() => setWhere(item)}
|
||||||
key={`where--${item}`}
|
key={`where--${item}`}
|
||||||
>
|
>
|
||||||
{WhereMapping[item]}
|
{WhereMapping[item]}
|
||||||
</DropdownItem>
|
</Dropdown.Item>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
})}
|
})}
|
||||||
|
@ -395,12 +395,12 @@ const FiltersModal = (props: {
|
||||||
<Dropdown label={ListsMapping[list].name} color="blue">
|
<Dropdown label={ListsMapping[list].name} color="blue">
|
||||||
{Object.keys(ListsMapping).map((item) => {
|
{Object.keys(ListsMapping).map((item) => {
|
||||||
return (
|
return (
|
||||||
<DropdownItem
|
<Dropdown.Item
|
||||||
onClick={() => setList(item)}
|
onClick={() => setList(item)}
|
||||||
key={`list--${item}`}
|
key={`list--${item}`}
|
||||||
>
|
>
|
||||||
{ListsMapping[item].name}
|
{ListsMapping[item].name}
|
||||||
</DropdownItem>
|
</Dropdown.Item>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
@ -414,20 +414,20 @@ const FiltersModal = (props: {
|
||||||
<Dropdown label={TagMapping[searchBy].name} color="blue">
|
<Dropdown label={TagMapping[searchBy].name} color="blue">
|
||||||
{Object.keys(TagMapping).map((item) => {
|
{Object.keys(TagMapping).map((item) => {
|
||||||
return (
|
return (
|
||||||
<DropdownItem
|
<Dropdown.Item
|
||||||
onClick={() => setSearchBy(item)}
|
onClick={() => setSearchBy(item)}
|
||||||
key={`tag--${item}`}
|
key={`tag--${item}`}
|
||||||
>
|
>
|
||||||
{TagMapping[item].name}
|
{TagMapping[item].name}
|
||||||
</DropdownItem>
|
</Dropdown.Item>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
: ""}
|
: ""}
|
||||||
</ModalBody>
|
</Modal.Body>
|
||||||
<ModalFooter>
|
<Modal.Footer>
|
||||||
<div className="flex justify-end w-full gap-2">
|
<div className="flex justify-end w-full gap-2">
|
||||||
<Button color="red" onClick={() => _cancel()}>
|
<Button color="red" onClick={() => _cancel()}>
|
||||||
Отменить
|
Отменить
|
||||||
|
@ -436,7 +436,7 @@ const FiltersModal = (props: {
|
||||||
Применить
|
Применить
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</ModalFooter>
|
</Modal.Footer>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -6,9 +6,10 @@ AniX - это неофициальный веб-клиент для Android-пр
|
||||||
|
|
||||||
## Список изменений
|
## Список изменений
|
||||||
|
|
||||||
- [3.7.0](/public/changelog/3.7.0.md)
|
|
||||||
- [3.6.0](/public/changelog/3.6.0.md)
|
|
||||||
- [3.5.0](/public/changelog/3.5.0.md)
|
- [3.5.0](/public/changelog/3.5.0.md)
|
||||||
|
- [3.4.0](/public/changelog/3.4.0.md)
|
||||||
|
- [3.3.0](/public/changelog/3.3.0.md)
|
||||||
|
- [3.2.3](/public/changelog/3.2.3.md)
|
||||||
|
|
||||||
[другие версии](/public/changelog)
|
[другие версии](/public/changelog)
|
||||||
|
|
||||||
|
|
Before Width: | Height: | Size: 529 KiB After Width: | Height: | Size: 120 KiB |
Before Width: | Height: | Size: 632 KiB After Width: | Height: | Size: 212 KiB |
Before Width: | Height: | Size: 731 KiB After Width: | Height: | Size: 204 KiB |
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 267 KiB |
Before Width: | Height: | Size: 1.8 MiB After Width: | Height: | Size: 225 KiB |
|
@ -1,7 +1,8 @@
|
||||||
const { withPlausibleProxy } = require("next-plausible");
|
const { withPlausibleProxy } = require("next-plausible");
|
||||||
const withFlowbiteReact = require("flowbite-react/plugin/nextjs");
|
|
||||||
/** @type {import('next').NextConfig} */
|
module.exports = withPlausibleProxy({
|
||||||
const NextConfig = {
|
customDomain: "https://analytics.wah.su",
|
||||||
|
})({
|
||||||
reactStrictMode: false,
|
reactStrictMode: false,
|
||||||
images: {
|
images: {
|
||||||
unoptimized: true,
|
unoptimized: true,
|
||||||
|
@ -9,74 +10,68 @@ const NextConfig = {
|
||||||
async headers() {
|
async headers() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
source: "/bookmarks/:slug*",
|
source: '/bookmarks/:slug*',
|
||||||
headers: [
|
headers: [
|
||||||
{
|
{
|
||||||
key: "Cache-Control",
|
key: 'Cache-Control',
|
||||||
value: "s-maxage=2592000, stale-while-revalidate=86400",
|
value: 's-maxage=2592000, stale-while-revalidate=86400',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
source: "/collection/:slug*",
|
source: '/collection/:slug*',
|
||||||
headers: [
|
headers: [
|
||||||
{
|
{
|
||||||
key: "Cache-Control",
|
key: 'Cache-Control',
|
||||||
value: "s-maxage=2592000, stale-while-revalidate=86400",
|
value: 's-maxage=2592000, stale-while-revalidate=86400',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
source: "/home/:slug*",
|
source: '/home/:slug*',
|
||||||
headers: [
|
headers: [
|
||||||
{
|
{
|
||||||
key: "Cache-Control",
|
key: 'Cache-Control',
|
||||||
value: "s-maxage=2592000, stale-while-revalidate=86400",
|
value: 's-maxage=2592000, stale-while-revalidate=86400',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
source: "/profile/:slug*",
|
source: '/profile/:slug*',
|
||||||
headers: [
|
headers: [
|
||||||
{
|
{
|
||||||
key: "Cache-Control",
|
key: 'Cache-Control',
|
||||||
value: "s-maxage=2592000, stale-while-revalidate=86400",
|
value: 's-maxage=2592000, stale-while-revalidate=86400',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
source: "/release/:slug*",
|
source: '/release/:slug*',
|
||||||
headers: [
|
headers: [
|
||||||
{
|
{
|
||||||
key: "Cache-Control",
|
key: 'Cache-Control',
|
||||||
value: "s-maxage=2592000, stale-while-revalidate=86400",
|
value: 's-maxage=2592000, stale-while-revalidate=86400',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
source: "/related/:slug*",
|
source: '/related/:slug*',
|
||||||
headers: [
|
headers: [
|
||||||
{
|
{
|
||||||
key: "Cache-Control",
|
key: 'Cache-Control',
|
||||||
value: "s-maxage=2592000, stale-while-revalidate=86400",
|
value: 's-maxage=2592000, stale-while-revalidate=86400',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
source: "/search",
|
source: '/search',
|
||||||
headers: [
|
headers: [
|
||||||
{
|
{
|
||||||
key: "Cache-Control",
|
key: 'Cache-Control',
|
||||||
value: "s-maxage=2592000, stale-while-revalidate=86400",
|
value: 's-maxage=2592000, stale-while-revalidate=86400',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
};
|
});
|
||||||
|
|
||||||
const config = withPlausibleProxy({
|
|
||||||
customDomain: "https://analytics.wah.su",
|
|
||||||
})(withFlowbiteReact(NextConfig));
|
|
||||||
|
|
||||||
module.exports = config;
|
|
||||||
|
|
1849
package-lock.json
generated
13
package.json
|
@ -6,20 +6,18 @@
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "next lint"
|
||||||
"postinstall": "flowbite-react patch"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"apexcharts": "^3.52.0",
|
"apexcharts": "^3.52.0",
|
||||||
"deepmerge-ts": "^7.1.0",
|
"deepmerge-ts": "^7.1.0",
|
||||||
"flowbite": "^2.4.1",
|
"flowbite": "^2.4.1",
|
||||||
"flowbite-react": "^0.11.7",
|
"flowbite-react": "^0.10.1",
|
||||||
"hls-video-element": "^1.5.0",
|
"hls-video-element": "^1.5.0",
|
||||||
"markdown-to-jsx": "^7.4.7",
|
"markdown-to-jsx": "^7.4.7",
|
||||||
"media-chrome": "^4.9.0",
|
"media-chrome": "^4.8.0",
|
||||||
"next": "^14.2.26",
|
"next": "^14.2.13",
|
||||||
"next-plausible": "^3.12.1",
|
"next-plausible": "^3.12.1",
|
||||||
"prettier": "^3.5.3",
|
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-cropper": "^2.3.3",
|
"react-cropper": "^2.3.3",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
|
@ -33,7 +31,6 @@
|
||||||
"@iconify-json/fa6-brands": "^1.1.21",
|
"@iconify-json/fa6-brands": "^1.1.21",
|
||||||
"@iconify-json/material-symbols": "^1.1.83",
|
"@iconify-json/material-symbols": "^1.1.83",
|
||||||
"@iconify-json/mdi": "^1.1.67",
|
"@iconify-json/mdi": "^1.1.67",
|
||||||
"@iconify-json/solar": "^1.2.2",
|
|
||||||
"@iconify-json/twemoji": "^1.1.15",
|
"@iconify-json/twemoji": "^1.1.15",
|
||||||
"@iconify/tailwind": "^1.1.1",
|
"@iconify/tailwind": "^1.1.1",
|
||||||
"@types/node": "20.14.12",
|
"@types/node": "20.14.12",
|
||||||
|
@ -41,8 +38,8 @@
|
||||||
"copy-webpack-plugin": "^12.0.2",
|
"copy-webpack-plugin": "^12.0.2",
|
||||||
"eslint": "^8",
|
"eslint": "^8",
|
||||||
"eslint-config-next": "14.2.5",
|
"eslint-config-next": "14.2.5",
|
||||||
"eslint-plugin-react-refresh": "^0.4.19",
|
|
||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
|
"prettier": "^3.5.3",
|
||||||
"tailwind-scrollbar": "^3.1.0",
|
"tailwind-scrollbar": "^3.1.0",
|
||||||
"tailwindcss": "^3.4.1"
|
"tailwindcss": "^3.4.1"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,22 +0,0 @@
|
||||||
# 3.6.0
|
|
||||||
|
|
||||||
## Добавлено
|
|
||||||
|
|
||||||
- Добавлена возможность смотреть заблокированных пользователей (в меню редактирования профиля)
|
|
||||||
- Добавлена возможность смотреть друзей пользователя
|
|
||||||
- Добавлена возможность смотреть, принимать и скрывать заявки в друзья
|
|
||||||
- Добавлен просмотр популярных комментариев пользователя
|
|
||||||
- Добавлен блок "Продолжить просмотр" на странице релиза если был произведён вход в аккаунт (3 рандомных релиза из списков сморю, в планах и отложено)
|
|
||||||
|
|
||||||
## Изменено
|
|
||||||
|
|
||||||
- Стиль некоторых элементов был изменён в связи с обновление библиотеки компонентов
|
|
||||||
- Блок скриншотов был перенесён на другую библиотеку
|
|
||||||
- Изменён вид блока связанных релизов на странице релиза с карусели карточек на список
|
|
||||||
- Изменён блок активности на странице профиля
|
|
||||||
- Изменены виды блоков "Оценки" и "Недавно просмотренные" на странице профиля с карусели карточек на список
|
|
||||||
- Изменён вид блока профиля (соц. сети и роли)
|
|
||||||
|
|
||||||
## Исправлено
|
|
||||||
|
|
||||||
- Получение прямых ссылок на источник кодик в собственном плеере, если кодик отдал зашифрованные ссылки
|
|
|
@ -1,12 +0,0 @@
|
||||||
# 3.7.0
|
|
||||||
|
|
||||||
## Добавлено
|
|
||||||
|
|
||||||
- Настройка сохранения истории просмотра
|
|
||||||
- Кнопки след./пред. серия в плеере
|
|
||||||
- Название, озвучка и серия написаны в плеере
|
|
||||||
|
|
||||||
## Изменено
|
|
||||||
|
|
||||||
- Выбор озвучки, источника и серии теперь находится в плеере
|
|
||||||
- Серия теперь засчитывается просмотренной только после нажатия кнопки воспроизведения
|
|
|
@ -1,9 +1,5 @@
|
||||||
const { addIconSelectors } = require("@iconify/tailwind");
|
const { addIconSelectors } = require("@iconify/tailwind");
|
||||||
const flowbiteReact = require("flowbite-react/plugin/tailwindcss");
|
import flowbite from "flowbite-react/tailwind";
|
||||||
|
|
||||||
flowbiteReact.config = {
|
|
||||||
charts: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
|
@ -11,12 +7,14 @@ module.exports = {
|
||||||
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
|
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
".flowbite-react\\class-list.json"
|
flowbite.content(),
|
||||||
],
|
],
|
||||||
plugins: [
|
plugins: [
|
||||||
addIconSelectors(["mdi", "material-symbols", "twemoji", "fa6-brands", "solar"]),
|
addIconSelectors(["mdi", "material-symbols", "twemoji", "fa6-brands"]),
|
||||||
flowbiteReact,
|
require("tailwind-scrollbar"),
|
||||||
require("tailwind-scrollbar")
|
flowbite.plugin()({
|
||||||
|
charts: true,
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
darkMode: "selector",
|
darkMode: "selector",
|
||||||
theme: {
|
theme: {
|
||||||
|
|