diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 2eff99b..0000000 --- a/.dockerignore +++ /dev/null @@ -1,68 +0,0 @@ -# Python -__pycache__ -venv -.mypy_cache - -# VSCode -.VSCode -*.code-workspace - -# DetaSpace -.space - -# NextJS -## dependencies -standalone -node_modules -.pnp -.pnp.js -.yarn/install-state.gz - -## testing -coverage - -## next.js -.next -out - -## production -build - -## misc -.DS_Store -*.pem - -## debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -## local env files -.env*.local - -## vercel -.vercel - -## typescript -*.tsbuildinfo -next-env.d.ts - -# traefik -traefik/traefik - -old/ -#Trigger Vercel Prod Build - -# next-video -videos/* -!videos/*.json -!videos/*.js -!videos/*.ts -public/_next-video - -API-Trace/* -.env - -player-parsers -docs -.git \ No newline at end of file diff --git a/.env.sample b/.env.sample deleted file mode 100644 index dcb7e8b..0000000 --- a/.env.sample +++ /dev/null @@ -1,5 +0,0 @@ -# пример заполнения: https://example.com, http://0.0.0.0:80 -NEXT_PUBLIC_KODIK_PARSER_URL= # Домен парсера кодика, требуется для просмотра с данного источника -NEXT_PUBLIC_ANILIBRIA_PARSER_URL= # Домен парсера анилибрии, если не заполнено, используется официальное апи -NEXT_PUBLIC_SIBNET_PARSER_URL= # Домен парсера сибнет, требуется для просмотра с данного источника -# --- \ No newline at end of file diff --git a/.github/workflows/DeployPreviewToVercel.yml b/.github/workflows/DeployPreviewToVercel.yml new file mode 100644 index 0000000..d9e97fa --- /dev/null +++ b/.github/workflows/DeployPreviewToVercel.yml @@ -0,0 +1,28 @@ +name: V3 Preview Deployment +env: + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} +on: + push: + # Pattern matched against refs/tags + branches: + - 'V3' + paths-ignore: + - '**/README.md' + - '**/LICENSE' + - '**/TODO.md' + - '**/docs/**' + - '**/extension/**' +jobs: + Deploy-Preview: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Install Vercel CLI + run: npm install --global vercel@latest + - name: Pull Vercel Environment Information + run: vercel pull --yes --environment=preview --token=${{ secrets.VERCEL_TOKEN }} + - name: Build Project Artifacts + run: vercel build --token=${{ secrets.VERCEL_TOKEN }} + - name: Deploy Project Artifacts to Vercel + run: vercel deploy --prebuilt --token=${{ secrets.VERCEL_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/DeployTagToVercel.yml b/.github/workflows/DeployTagToVercel.yml new file mode 100644 index 0000000..fc7ea21 --- /dev/null +++ b/.github/workflows/DeployTagToVercel.yml @@ -0,0 +1,28 @@ +name: Production Tag Deployment +env: + VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }} + VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }} +on: + push: + # Pattern matched against refs/tags + tags: + - '*' # Push events to every tag not containing / + paths-ignore: + - '**/README.md' + - '**/LICENSE' + - '**/TODO.md' + - '**/docs/**' + - '**/extension/**' +jobs: + Deploy-Production: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Install Vercel CLI + run: npm install --global vercel@latest + - name: Pull Vercel Environment Information + run: vercel pull --yes --environment=production --token=${{ secrets.VERCEL_TOKEN }} + - name: Build Project Artifacts + run: vercel build --prod --token=${{ secrets.VERCEL_TOKEN }} + - name: Deploy Project Artifacts to Vercel + run: vercel deploy --prebuilt --prod --token=${{ secrets.VERCEL_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1775543..8595241 100644 --- a/.gitignore +++ b/.gitignore @@ -60,5 +60,4 @@ videos/* !videos/*.ts public/_next-video -API-Trace/* -.env \ No newline at end of file +API-Trace/* \ No newline at end of file diff --git a/DEPLOYMENT.RU.md b/DEPLOYMENT.RU.md deleted file mode 100644 index 78f43a3..0000000 --- a/DEPLOYMENT.RU.md +++ /dev/null @@ -1,180 +0,0 @@ -# Развёртывание приложения AniX - -## Vercel - -Требования: - -- аккаунт GitHub -- аккаунт Vercel - -1. Создайте форк репозитория - - ![fork button](./docs/deploy/fork.png) - -2. Войдите в аккаунт Vercel - -> [!IMPORTANT] -> Аккаунт Vercel должен быть связан с аккаунтом GitHub. -> -> Если у вас нет аккаунта Vercel, то создайте его через вход с помощью GitHub. - -3. Нажмите кнопку создать новый проект - - ![vercel new project button](./docs/deploy/vercel_new_project.png) - -4. Нажмите кнопку импортировать напротив названия репозитория - - ![vercel import button](./docs/deploy/vercel_import.png) - -5. (опционально) добавьте переменные для использования своего плеера: - - - NEXT_PUBLIC_KODIK_PARSER_URL - - NEXT_PUBLIC_ANILIBRIA_PARSER_URL - - NEXT_PUBLIC_SIBNET_PARSER_URL - - на те которые вы получили, если развёртывали [anix-player-parsers](./player-parsers/README.RU.md) - - ![vercel project settings](./docs/deploy/vercel_project.png) - -6. нажмите кнопку "Deploy" и ожидайте пока не появится подтверждение -7. нажмите кнопку "Continue to Dashboard" -8. клиент будет доступен по ссылке такого вида, нажмите на неё чтобы его открыть - ![vercel project url](./docs/deploy/vercel_url.png) - -## Netlify - -Требования: - -- аккаунт GitHub -- аккаунт Netlify - -1. Создайте форк репозитория - - ![fork button](./docs/deploy/fork.png) - -2. Войдите в аккаунт Netlify - -> [!IMPORTANT] -> Аккаунт Netlify должен быть связан с аккаунтом GitHub. -> -> Если у вас нет аккаунта Netlify, то создайте его через вход с помощью GitHub. - -3. Нажмите кнопку создать новый проект - - ![netlify new project button](./docs/deploy/netlify_new_project.png) - -4. Нажмите кнопку GitHub - - ![netlify provider choice](./docs/deploy/netlify_provider.png) - -5. Нажмите на название репозитория - - ![netlify import button](./docs/deploy/netlify_import.png) - -6. (опционально) заполните название проекта - - ![netlify project name](./docs/deploy/netlify_project_name.png) - -7. (опционально) добавьте переменные для использования своего плеера: - - - NEXT_PUBLIC_KODIK_PARSER_URL - - NEXT_PUBLIC_ANILIBRIA_PARSER_URL - - NEXT_PUBLIC_SIBNET_PARSER_URL - - на те которые вы получили, если развёртывали [anix-player-parsers](./player-parsers/README.RU.md) - - 1. ![alt text](./docs/deploy/netlify_env_1.png) - - 2. ![alt text](./docs/deploy/netlify_env_2.png) - -8. нажмите кнопку "Deploy" и ожидайте пока не появится подтверждение - -9. клиент будет доступен по ссылке такого вида, нажмите на неё чтобы его открыть - - ![netlify project url](./docs/deploy/netlify_url.png) - -## Docker - -Требования: - -- [docker](https://docs.docker.com/engine/install/) - -### Пре-билд - -1. выполните команду: - -`docker run -d --name anix -p 3000:3000 radiquum/anix:latest` - -### Ручной билд - -Доп. Требования: - -- [git](https://git-scm.com/) - -1. Клонируйте репозиторий `git clone https://github.com/Radiquum/AniX` -2. Переместитесь в директорию репозитория `cd AniX` -3. Выполните команду `docker build -t anix .` -4. После окончания, выполните команду: `docker run -d --restart always --name anix -p 3000:3000 anix` - -### docker/Обозначения - -- -d - запустить контейнер в фоне -- --restart always - всегда запускать после перезагрузки сервера -- --name - название контейнера -- -p - порт контейнера который будет доступен извне. ПОРТ:3000 - -> [!NOTE] -> для переменных которые вы получили, если развёртывали [anix-player-parsers](./player-parsers/README.RU.md), необходимо использовать `-e ПЕРЕМЕННАЯ=ЗНАЧЕНИЕ` до слова anix - -[команда docker run](https://docs.docker.com/reference/cli/docker/container/run/) - -### docker/После развёртывания - -Сервис будет доступен по адресу: `http://<ВАШ IP><:ВАШ ПОРТ>/` - -### docker/Примечание - -Для использования своего домена и поддержки протокола HTTPS, вы можете использовать Traefik или другой reverse-proxy, с сертификатом SSL. - -Полезные ссылки: - -- [Конвертер из команды docker run в синтакс для docker compose](https://it-tools.tech/docker-run-to-docker-compose-converter) -- [Как настроить Traefik + свой домен + SSL](https://letmegooglethat.com/?q=how+to+setup+traefik+with+custom+domain+and+ssl+certificate+from+lets+encrypt%3F) - -## pm2 - -Требования: - -- [git](https://git-scm.com/) -- [nodejs 23+ с npm](http://nodejs.org/) -- [pm2](https://pm2.keymetrics.io/) - -Инструкция: - -1. Клонируйте репозиторий `git clone https://github.com/Radiquum/AniX` -2. Переместитесь в директорию репозитория `cd AniX` -3. Выполните команду `npm install` -4. (опционально) скопируйте .env.sample как .env и заполните его переменными которые вы получили, если развёртывали [anix-player-parsers](./player-parsers/README.RU.md) -5. Выполните команду `npm run build` -6. создайте новую директорию (далее будем использовать `<имя_новой_директории>` как её имя) -7. переместите в созданную директорию (`<имя_новой_директории>`) - - директорию `public` в `<имя_новой_директории>/public` - - директорию `.next/static` в `<имя_новой_директории>/.next/static` - - файлы из `.next/standalone` в `<имя_новой_директории>` -8. Переместитесь в созданную директорию и выполните команду `pm2 start server.js -n anix` - -### pm2/Обозначения - -- -n - название сервиса в pm2 - -### pm2/После развёртывания - -Сервис будет доступен по адресу: `http://<ВАШ IP>:3000/` - -### pm2/Примечание - -Для автоматического запуска приложения, рекомендуется настроить pm2 на автозапуск, с помощью команды: `pm2 startup` - -Полезные ссылки: - -- [PM2: подходим к вопросу процесс-менеджмента с умом @ Habr](https://habr.com/ru/articles/480670/) diff --git a/DEPLOYMENT.md b/DEPLOYMENT.md deleted file mode 100644 index f7ae21b..0000000 --- a/DEPLOYMENT.md +++ /dev/null @@ -1,180 +0,0 @@ -# AniX Application Deployment - -## Vercel - -Requirements: - -- GitHub account -- Vercel account - -1. Fork the repository - - ![fork button](./docs/deploy/fork.png) - -2. Log in to your Vercel account - -> [!IMPORTANT] -> Your Vercel account must be linked with your GitHub account. -> -> If you don't have a Vercel account, create one by signing in with GitHub. - -3. Click the button to create a new project - - ![vercel new project button](./docs/deploy/vercel_new_project.png) - -4. Click the import button next to the repository name - - ![vercel import button](./docs/deploy/vercel_import.png) - -5. (optional) Add variables to use your own player: - - - NEXT_PUBLIC_KODIK_PARSER_URL - - NEXT_PUBLIC_ANILIBRIA_PARSER_URL - - NEXT_PUBLIC_SIBNET_PARSER_URL - - Use the ones you received if you deployed [anix-player-parsers](./player-parsers/README.md) - - ![vercel project settings](./docs/deploy/vercel_project.png) - -6. Click the "Deploy" button and wait until you see a confirmation -7. Click the "Continue to Dashboard" button -8. The client will be available at a link of this form, click it to open - ![vercel project url](./docs/deploy/vercel_url.png) - -## Netlify - -Requirements: - -- GitHub account -- Netlify account - -1. Fork the repository - - ![fork button](./docs/deploy/fork.png) - -2. Log in to your Netlify account - -> [!IMPORTANT] -> Your Netlify account must be linked with your GitHub account. -> -> If you don't have a Netlify account, create one by signing in with GitHub. - -3. Click the button to create a new project - - ![netlify new project button](./docs/deploy/netlify_new_project.png) - -4. Click the GitHub button - - ![netlify provider choice](./docs/deploy/netlify_provider.png) - -5. Click the repository name - - ![netlify import button](./docs/deploy/netlify_import.png) - -6. (optional) Fill in the project name - - ![netlify project name](./docs/deploy/netlify_project_name.png) - -7. (optional) Add variables to use your own player: - - - NEXT_PUBLIC_KODIK_PARSER_URL - - NEXT_PUBLIC_ANILIBRIA_PARSER_URL - - NEXT_PUBLIC_SIBNET_PARSER_URL - - Use the ones you received if you deployed [anix-player-parsers](./player-parsers/README.md) - - 1. ![alt text](./docs/deploy/netlify_env_1.png) - - 2. ![alt text](./docs/deploy/netlify_env_2.png) - -8. Click the "Deploy" button and wait until you see a confirmation - -9. The client will be available at a link of this form, click it to open - - ![netlify project url](./docs/deploy/netlify_url.png) - -## Docker - -Requirements: - -- [docker](https://docs.docker.com/engine/install/) - -### Pre-built - -1. Run the command: - -`docker run -d --name anix -p 3000:3000 radiquum/anix:latest` - -### Manual build - -Additional Requirements: - -- [git](https://git-scm.com/) - -1. Clone the repository `git clone https://github.com/Radiquum/AniX` -2. Navigate to the repository directory `cd AniX` -3. Run the command `docker build -t anix .` -4. Once finished, run the command: `docker run -d --restart always --name anix -p 3000:3000 anix` - -### docker/Flags - -- -d - run container in the background -- --restart always - always restart after server reboot -- --name - container name -- -p - container port to be exposed externally. PORT:3000 - -> [!NOTE] -> For variables you received if you deployed [anix-player-parsers](./player-parsers/README.md), you need to use `-e VARIABLE=VALUE` before the word anix - -[docker run command](https://docs.docker.com/reference/cli/docker/container/run/) - -### docker/After deployment - -The service will be available at: `http://<:YOUR PORT>/` - -### docker/Note - -To use your own domain and support HTTPS protocol, you can use Traefik or another reverse proxy with SSL certificate. - -Useful links: - -- [Converter from docker run command to docker compose syntax](https://it-tools.tech/docker-run-to-docker-compose-converter) -- [How to setup Traefik + custom domain + SSL](https://letmegooglethat.com/?q=how+to+setup+traefik+with+custom+domain+and+ssl+certificate+from+lets+encrypt%3F) - -## pm2 - -Requirements: - -- [git](https://git-scm.com/) -- [nodejs 23+ with npm](http://nodejs.org/) -- [pm2](https://pm2.keymetrics.io/) - -Instructions: - -1. Clone the repository `git clone https://github.com/Radiquum/AniX` -2. Navigate to the repository directory `cd AniX` -3. Run the command `npm install` -4. (optional) copy `.env.sample` as `.env` and fill it with variables you received if you deployed [anix-player-parsers](./player-parsers/README.md) -5. Run the command `npm run build` -6. Create a new directory (next we will be refer to its name as ``) -7. Move the following files into the new directory (``): - - move `public` directory to `/public` - - move `.next/static` directory to `/.next/static` - - move files from `.next/standalone` to `` -8. Move into the created directory () and run the command `pm2 start server.js -n anix` - -### pm2/Flags - -- -n - service name in pm2 - -### pm2/After deployment - -The service will be available at: `http://:3000/` - -### pm2/Note - -To enable automatic application startup, it is recommended to configure pm2 to start on boot using the command: `pm2 startup` - -Useful links: - -- [PM2: managing processes smartly @ Habr](https://habr.com/ru/articles/480670/) diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 87783ac..0000000 --- a/Dockerfile +++ /dev/null @@ -1,32 +0,0 @@ -FROM node:23-alpine AS base - - -FROM base AS deps -RUN apk add --no-cache libc6-compat -WORKDIR /app -COPY package.json package-lock.json ./ -RUN npm ci - - -FROM base AS builder -WORKDIR /app -COPY --from=deps /app/node_modules ./node_modules -COPY . . -RUN npm run build - - -FROM base AS runner -LABEL org.opencontainers.image.source=https://github.com/radiquum/anix -WORKDIR /app -ENV NODE_ENV=production -RUN addgroup --system --gid 1001 nodejs -RUN adduser --system --uid 1001 nextjs -COPY --from=builder --chown=nextjs:nodejs /app/public ./public -COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ -COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static -USER nextjs - -EXPOSE 3000 -ENV PORT=3000 -ENV HOSTNAME="0.0.0.0" -CMD ["node", "server.js"] diff --git a/README.md b/README.md index 0f965ed..cc7ef10 100644 --- a/README.md +++ b/README.md @@ -2,20 +2,21 @@ AniX is an unofficial web client for the Android application Anixart. It allows you to access and manage your Anixart account from a web browser on your desktop or laptop computer. +[Readme [RU]](./docs/REAME.RU.md) | [Browser Extension [RU]](./extension/README.md) + +## Changelog [RU] + +- [3.6.0](./public/changelog/3.6.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) + +[other versions](./public/changelog) + ## Disclaimer Please note that AniX is an unofficial project and is not affiliated with the developers of Anixart. It is recommended to use the official Anixart app for the most up-to-date features and functionality. ---- - -[[RU] ПРОЧТИ МЕНЯ](./README.RU.md) | [[EN] README](./README.md) - -[[RU] РАЗВЁРТЫВАНИЕ](./DEPLOYMENT.RU.md) | [[EN] DEPLOY](./DEPLOYMENT.md) - -[[RU] Changelogs](./public/changelog) - ---- - ## Screenshots
@@ -53,6 +54,12 @@ Please note that AniX is an unofficial project and is not affiliated with the de
+## Features + +1. Use your existing Anixart account +2. sync lists, watch history, collections and more +3. use almost all features of an android app + ## Contributing -We welcome contributions to this project! If you have any bug fixes, improvements, or new features, please feel free to create a pull request. +We welcome contributions to this project! If you have any bug fixes, improvements, or new features, please feel free to create a pull request. \ No newline at end of file diff --git a/app/App.tsx b/app/App.tsx index 8cccbd0..3733cd1 100644 --- a/app/App.tsx +++ b/app/App.tsx @@ -4,15 +4,10 @@ import { usePreferencesStore } from "./store/preferences"; import { Navbar } from "./components/Navbar/NavbarUpdate"; import { Inter } from "next/font/google"; import { useEffect, useState } from "react"; -import { - Button, - Modal, - ModalBody, - ModalFooter, - ModalHeader, -} from "flowbite-react"; +import { Button, Modal, ModalBody, ModalFooter, ModalHeader } from "flowbite-react"; import { Spinner } from "./components/Spinner/Spinner"; import { ChangelogModal } from "#/components/ChangelogModal/ChangelogModal"; +import PlausibleProvider from "next-plausible"; import { Bounce, ToastContainer } from "react-toastify"; const inter = Inter({ subsets: ["latin"] }); @@ -109,6 +104,14 @@ export const App = (props) => { + {preferencesStore.flags.enableAnalytics && ( + + )} { - if (res.ok) { - return res.json(); - } else { - return { content: [] }; - } - }) + .then((res) => res.json()) .then((data) => { - if (data && data.content) { - setReplies(data.content); - } + setReplies(data.content); }); } if ( @@ -202,9 +194,9 @@ export const CommentsComment = (props: {

- {!props.comment.isDeleted ? - props.comment.message - : "Комментарий был удалён."} + {!props.comment.isDeleted + ? props.comment.message + : "Комментарий был удалён."}

{isHidden && ( - : } + ) : ( + + )}

0 ? "text-green-500 dark:text-green-400" - : likes < 0 ? "text-red-500 dark:text-red-400" - : "text-gray-500 dark:text-gray-400" + likes > 0 + ? "text-green-500 dark:text-green-400" + : likes < 0 + ? "text-red-500 dark:text-red-400" + : "text-gray-500 dark:text-gray-400" }`} > {likes} @@ -286,9 +282,9 @@ export const CommentsComment = (props: { > diff --git a/app/components/Comments/Comments.Main.tsx b/app/components/Comments/Comments.Main.tsx index c4c1ec3..3411918 100644 --- a/app/components/Comments/Comments.Main.tsx +++ b/app/components/Comments/Comments.Main.tsx @@ -41,7 +41,7 @@ export const CommentsMain = (props: {

- {props.comments && props.comments.map((comment: any) => ( + {props.comments.map((comment: any) => ( { + 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 ( +
+ + {props.availableEpisodes.map((episode: Episode) => ( + + + + ))} + +
+ ); +}; diff --git a/app/components/ReleasePlayer/EpisodeSelectorMenu.tsx b/app/components/ReleasePlayer/EpisodeSelectorMenu.tsx deleted file mode 100644 index ab03540..0000000 --- a/app/components/ReleasePlayer/EpisodeSelectorMenu.tsx +++ /dev/null @@ -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 ( -
-

Эпизод

-
- {episodeList && episodeList.length > 0 ? - episodeList.map((epis: Episode) => { - return ( - - ); - }) - : ""} -
-
- ); -}; diff --git a/app/components/ReleasePlayer/MediaPlayer.module.css b/app/components/ReleasePlayer/MediaPlayer.module.css deleted file mode 100644 index 28b47a9..0000000 --- a/app/components/ReleasePlayer/MediaPlayer.module.css +++ /dev/null @@ -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; -} diff --git a/app/components/ReleasePlayer/MediaThemeSutro.tsx b/app/components/ReleasePlayer/MediaThemeSutro.tsx new file mode 100644 index 0000000..7ebb900 --- /dev/null +++ b/app/components/ReleasePlayer/MediaThemeSutro.tsx @@ -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 ( + <> +