From a944e8cb8007f07279411d04eca085a70201d6b8 Mon Sep 17 00:00:00 2001 From: Guus van Meerveld Date: Tue, 12 Mar 2024 22:46:59 +0100 Subject: [PATCH] basic next.js app using nextui --- .dockerignore | 15 +- .eslintrc.json | 10 +- .github/ISSUE_TEMPLATE/bug_report.md | 38 - .github/ISSUE_TEMPLATE/feature_request.md | 20 - .github/workflows/codeql-analysis.yml | 49 - .github/workflows/deploy.yml | 64 - .gitignore | 40 +- Dockerfile | 23 +- README.md | 165 +- app.json | 6 - docker-compose.yml | 8 - flake.lock | 56 + flake.nix | 37 + netlify.toml | 3 - next.config.js | 20 - next.config.mjs | 4 + package.json | 76 +- postcss.config.js | 6 + public/favicon.ico | Bin 935 -> 0 bytes shell.nix | 11 + src/app/globals.css | 3 + src/app/layout.tsx | 22 + src/app/page.tsx | 9 + src/app/providers.tsx | 7 + src/components/Channel/Inline.tsx | 71 - src/components/Layout.tsx | 17 - src/components/Loading.tsx | 19 - .../MaterialColorPicker/ColorBox.tsx | 20 - src/components/MaterialColorPicker/index.tsx | 103 - src/components/ModalBox.ts | 17 - src/components/Navbar/Drawer.tsx | 68 - src/components/Navbar/Search.tsx | 43 - src/components/Navbar/index.tsx | 194 - src/components/Player/Actions.tsx | 89 - src/components/Player/ProgressBar.tsx | 74 - src/components/Player/Time.tsx | 30 - src/components/Player/index.tsx | 212 - src/components/Video/Grid.tsx | 25 - src/components/Video/Inline.tsx | 144 - src/components/Video/index.tsx | 104 - src/globals.css | 4 - src/interfaces/api/channel.ts | 36 - src/interfaces/api/index.ts | 22 - src/interfaces/api/instances.ts | 71 - src/interfaces/api/search.ts | 85 - src/interfaces/api/trending.ts | 23 - src/interfaces/api/video.ts | 60 - src/interfaces/settings.ts | 19 - src/interfaces/video.ts | 102 - src/interfaces/videoPlayer.ts | 11 - src/next-seo.config.ts | 17 - src/pages/404.tsx | 31 - src/pages/_app.tsx | 55 - src/pages/_document.tsx | 22 - src/pages/index.tsx | 46 - src/pages/results.tsx | 163 - src/pages/settings.tsx | 580 -- src/pages/trending.tsx | 120 - src/pages/watch.module.css | 3 - src/pages/watch.tsx | 164 - src/svg/logo.svg | 1 - src/theme.ts | 19 - src/utils/conversions.ts | 78 - src/utils/hooks/useSettings.ts | 29 - src/utils/hooks/useVideoState.ts | 50 - src/utils/index.ts | 30 - src/utils/requests.ts | 57 - tailwind.config.ts | 19 + tsconfig.json | 52 +- yarn.lock | 5484 +++++++++++------ 70 files changed, 3967 insertions(+), 5408 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug_report.md delete mode 100644 .github/ISSUE_TEMPLATE/feature_request.md delete mode 100644 .github/workflows/codeql-analysis.yml delete mode 100644 .github/workflows/deploy.yml delete mode 100644 app.json delete mode 100644 docker-compose.yml create mode 100644 flake.lock create mode 100644 flake.nix delete mode 100644 netlify.toml delete mode 100644 next.config.js create mode 100644 next.config.mjs create mode 100644 postcss.config.js delete mode 100644 public/favicon.ico create mode 100644 shell.nix create mode 100644 src/app/globals.css create mode 100644 src/app/layout.tsx create mode 100644 src/app/page.tsx create mode 100644 src/app/providers.tsx delete mode 100644 src/components/Channel/Inline.tsx delete mode 100644 src/components/Layout.tsx delete mode 100644 src/components/Loading.tsx delete mode 100644 src/components/MaterialColorPicker/ColorBox.tsx delete mode 100644 src/components/MaterialColorPicker/index.tsx delete mode 100644 src/components/ModalBox.ts delete mode 100644 src/components/Navbar/Drawer.tsx delete mode 100644 src/components/Navbar/Search.tsx delete mode 100644 src/components/Navbar/index.tsx delete mode 100644 src/components/Player/Actions.tsx delete mode 100644 src/components/Player/ProgressBar.tsx delete mode 100644 src/components/Player/Time.tsx delete mode 100644 src/components/Player/index.tsx delete mode 100644 src/components/Video/Grid.tsx delete mode 100644 src/components/Video/Inline.tsx delete mode 100644 src/components/Video/index.tsx delete mode 100644 src/globals.css delete mode 100644 src/interfaces/api/channel.ts delete mode 100644 src/interfaces/api/index.ts delete mode 100644 src/interfaces/api/instances.ts delete mode 100644 src/interfaces/api/search.ts delete mode 100644 src/interfaces/api/trending.ts delete mode 100644 src/interfaces/api/video.ts delete mode 100644 src/interfaces/settings.ts delete mode 100644 src/interfaces/video.ts delete mode 100644 src/interfaces/videoPlayer.ts delete mode 100644 src/next-seo.config.ts delete mode 100644 src/pages/404.tsx delete mode 100644 src/pages/_app.tsx delete mode 100644 src/pages/_document.tsx delete mode 100644 src/pages/index.tsx delete mode 100644 src/pages/results.tsx delete mode 100644 src/pages/settings.tsx delete mode 100644 src/pages/trending.tsx delete mode 100644 src/pages/watch.module.css delete mode 100644 src/pages/watch.tsx delete mode 100644 src/svg/logo.svg delete mode 100644 src/theme.ts delete mode 100644 src/utils/conversions.ts delete mode 100644 src/utils/hooks/useSettings.ts delete mode 100644 src/utils/hooks/useVideoState.ts delete mode 100644 src/utils/index.ts delete mode 100644 src/utils/requests.ts create mode 100644 tailwind.config.ts diff --git a/.dockerignore b/.dockerignore index a65f45f..ead7e82 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,8 +1,11 @@ ** -!/next.config.js -!/tsconfig.json -!/package.json -!/yarn.lock -!/public -!/src \ No newline at end of file +!.env +!next.config.js +!tailwind.config.js +!postcss.config.js +!tsconfig.json +!package.json +!yarn.lock +!public +!src \ No newline at end of file diff --git a/.eslintrc.json b/.eslintrc.json index c0faf1d..bffb357 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,11 +1,3 @@ { - "extends": "next/core-web-vitals", - "rules": { - "no-restricted-imports": [ - "error", - { - "patterns": ["@mui/*/*/*", "!@mui/material/test-utils/*"] - } - ] - } + "extends": "next/core-web-vitals" } diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md deleted file mode 100644 index dd84ea7..0000000 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: '' -labels: '' -assignees: '' - ---- - -**Describe the bug** -A clear and concise description of what the bug is. - -**To Reproduce** -Steps to reproduce the behavior: -1. Go to '...' -2. Click on '....' -3. Scroll down to '....' -4. See error - -**Expected behavior** -A clear and concise description of what you expected to happen. - -**Screenshots** -If applicable, add screenshots to help explain your problem. - -**Desktop (please complete the following information):** - - OS: [e.g. iOS] - - Browser [e.g. chrome, safari] - - Version [e.g. 22] - -**Smartphone (please complete the following information):** - - Device: [e.g. iPhone6] - - OS: [e.g. iOS8.1] - - Browser [e.g. stock browser, safari] - - Version [e.g. 22] - -**Additional context** -Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md deleted file mode 100644 index bbcbbe7..0000000 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -name: Feature request -about: Suggest an idea for this project -title: '' -labels: '' -assignees: '' - ---- - -**Is your feature request related to a problem? Please describe.** -A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] - -**Describe the solution you'd like** -A clear and concise description of what you want to happen. - -**Describe alternatives you've considered** -A clear and concise description of any alternative solutions or features you've considered. - -**Additional context** -Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml deleted file mode 100644 index 9587db2..0000000 --- a/.github/workflows/codeql-analysis.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: "CodeQL" - -on: - push: - branches: [master] - paths: - - src/** - - package.json - - tsconfig.json - - .github/workflows/codeql-analysis.yml - pull_request: - # The branches below must be a subset of the branches above - branches: [master] - schedule: - - cron: "27 18 * * 6" - -jobs: - analyze: - name: Analyze - runs-on: ubuntu-latest - permissions: - actions: read - contents: read - security-events: write - - strategy: - fail-fast: false - matrix: - language: ["typescript"] - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v2 - with: - languages: ${{ matrix.language }} - - - name: Autobuild - uses: github/codeql-action/autobuild@v2 - - #- run: | - # make bootstrap - # make release - - - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v2 diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml deleted file mode 100644 index 8b9b167..0000000 --- a/.github/workflows/deploy.yml +++ /dev/null @@ -1,64 +0,0 @@ -name: Deploy to Github Pages / Docker hub - -on: - push: - branches: - - master - paths: - - src/** - - public/** - - Dockerfile - - package.json - - tsconfig.json - - next.config.js - - .github/workflows/deploy.yml - -env: - NEXT_TELEMETRY_DISABLED: 1 - -jobs: - test: - runs-on: ubuntu-latest - steps: - - name: Setup - uses: actions/checkout@v2 - - - name: Setup Node.js - uses: actions/setup-node@v2 - with: - node-version: "16.x" - cache: "yarn" - - - name: Install npm dependencies - run: yarn install - - - name: Lint - run: yarn run lint - - - name: Build - run: yarn run build - - docker: - runs-on: ubuntu-latest - needs: test - steps: - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 - - # Login - - name: Login to DockerHub - uses: docker/login-action@v1 - with: - username: guusvanmeerveld - password: ${{ secrets.DOCKERHUB_TOKEN }} - - # Build & Push - - name: Build Dockerfile and push - uses: docker/build-push-action@v2 - with: - push: true - tags: guusvanmeerveld/materialtube:latest - - # Cache - cache-from: type=gha - cache-to: type=gha,mode=max diff --git a/.gitignore b/.gitignore index 0a152fd..fd3dbb5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,36 @@ -node_modules -.env.local -.next -out \ No newline at end of file +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/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 diff --git a/Dockerfile b/Dockerfile index 8a39276..dff6158 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,25 +1,18 @@ -FROM node:16-alpine AS deps +FROM node:lts-alpine AS deps WORKDIR /app -COPY package.json yarn.lock ./ +COPY package.json yarn.lock prisma ./ RUN yarn install --frozen-lockfile - -FROM node:16-alpine AS builder - +FROM node:lts-alpine AS builder WORKDIR /app - COPY . . - COPY --from=deps /app/node_modules ./node_modules - ENV NEXT_TELEMETRY_DISABLED 1; - +RUN yarn run prisma:generate RUN yarn build && yarn install --production --ignore-scripts --prefer-offline - -FROM node:16-alpine AS runner - +FROM node:lts-alpine AS runner WORKDIR /app ENV NODE_ENV production @@ -28,14 +21,16 @@ RUN addgroup -g 1001 -S nodejs RUN adduser -S nextjs -u 1001 COPY --from=builder /app/next.config.js ./ -COPY --from=builder /app/next-i18next.config.js ./ +COPY --from=builder /app/postcss.config.js ./ +COPY --from=builder /app/tailwind.config.js ./ COPY --from=builder /app/public ./public COPY --from=builder --chown=nextjs:nodejs /app/.next ./.next COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/package.json ./package.json +COPY --from=builder /app/prisma ./prisma USER nextjs EXPOSE 3000 -CMD ["yarn", "start"] +CMD ["yarn", "run", "start:migrate"] \ No newline at end of file diff --git a/README.md b/README.md index f83f902..c403366 100644 --- a/README.md +++ b/README.md @@ -1,157 +1,36 @@ -

- -

+This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). -

MaterialTube

+## Getting Started -

- Deploy Site - CodeQL - - Docker pulls - -

+First, run the development server: -

- - Deploy to Heroku - - - Deploy to Netlify - -

- -

- MaterialTube is a simple client-side only web-client for Invidious servers. It supports using an Invidious account, but also allows you to store all of your data locally. It's main goal is to provide an even greater level of privacy and improve on the current Invidious UI. -

- -

Made using

- -

- Typescript - React -

- -## Index -- [Index](#index) -- [(Current) Features](#current-features) -- [Configuration](#configuration) -- [Deploy](#deploy) - - [Using Node.js](#using-nodejs) - - [Using Docker](#using-docker) - - [Locally](#locally) - - [Using Docker Hub](#using-docker-hub) - - [Using Heroku](#using-heroku) - - [Using Netlify](#using-netlify) - -## (Current) Features -- Browse trending -- Watch video's -- Custom settings - -## Configuration -There are a few environmental variables that are able to be set during build time to further customize the application. - -- GIT_URL: Set the url to the git repo. Default: https://github.com/Guusvanmeerveld/MaterialTube -- APP_NAME: Set the app name to show to the users. Default: MaterialTube -- DEFAULT_SERVER: Set the invidious server to use by default. Default: invidious.privacy.gd - -## Deploy - -### Using Node.js - -Requirements: -- Node.js v16.x -- Yarn or NPM -- Git - -```sh -git clone https://github.com/Guusvanmeerveld/MaterialTube MaterialTube - -cd MaterialTube - -# Choose Yarn or NPM -yarn install --frozen-lockfile - -# npm install --frozen-lockfile - -export NEXT_TELEMETRY_DISABLED=1 -``` - -Now you have to choose between export to static HTML (recommended) or running a custom server (improves speed) - -Exporting to static HTML: -```sh -yarn export - -# npm export +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev ``` -The HTML files can be found in the `out` folder. You can now serve them using something like NGINX or Apache -You can also opt to use a custom server, which improves on speed because it will prefetch your request. +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. -Building and starting a custom server: -```sh -yarn build +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. -# npm build +This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. -yarn start - -# npm start -``` - -### Using Docker - -#### Locally - -Requirements: -- Docker -- docker-compose -- Git - -```sh -git clone https://github.com/Guusvanmeerveld/MaterialTube MaterialTube - -cd MaterialTube - -docker build . -t materialtube -``` - -Now update the `docker-compose.yml` to your needs and start the container: - -```sh -docker-compose up -d -``` - -#### Using Docker Hub - -Requirements: -- Docker -- docker-compose - -Simply update the following to your needs and put it in a file named `docker-compose.yml`. - -```yml -version: "3" - -services: - app: - build: guusvanmeerveld/materialtube - container_name: material-tube - ports: - - 3000:80 -``` +## Learn More -Now run `docker-compose up -d` to start the container. +To learn more about Next.js, take a look at the following resources: -### Using Heroku +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. -Deploying to Heroku is a very simple and highly recommended way of deploying. All you have to do is click the button below, create an account (if you don't already have one) and deploy it. +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! -[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy?template=https://github.com/Guusvanmeerveld/MaterialTube) +## Deploy on Vercel -### Using Netlify -Deploying to Netlify is just as easy as deploying to Heroku. Click the button below connect your Git repo and follow the steps to deploy your application. +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. -[![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/Guusvanmeerveld/MaterialTube) \ No newline at end of file +Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/app.json b/app.json deleted file mode 100644 index 65254b5..0000000 --- a/app.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "name": "MaterialTube", - "description": "MaterialTube is a beautiful and elegant web client for Invidious servers, built using Next.js and MUI.", - "repository": "https://github.com/Guusvanmeerveld/MaterialTube", - "keywords": ["react", "youtube", "nextjs", "mui", "invidious"] -} diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 18a333f..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,8 +0,0 @@ -version: "3" - -services: - app: - build: . - container_name: material-tube - ports: - - 3000:3000 diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..d66672e --- /dev/null +++ b/flake.lock @@ -0,0 +1,56 @@ +{ + "nodes": { + "flake-compat": { + "locked": { + "lastModified": 1696426674, + "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", + "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", + "revCount": 57, + "type": "tarball", + "url": "https://api.flakehub.com/f/pinned/edolstra/flake-compat/1.0.1/018afb31-abd1-7bff-a5e4-cff7e18efb7a/source.tar.gz" + }, + "original": { + "type": "tarball", + "url": "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1710252211, + "narHash": "sha256-hQChQpB4LDBaSrNlD6DPLhU9T+R6oyxMCg2V+S7Y1jg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "7eeacecff44e05a9fd61b9e03836b66ecde8a525", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "type": "indirect" + } + }, + "root": { + "inputs": { + "flake-compat": "flake-compat", + "nixpkgs": "nixpkgs", + "systems": "systems" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..df92c58 --- /dev/null +++ b/flake.nix @@ -0,0 +1,37 @@ +{ + description = "Material tube"; + + inputs = { + systems.url = "github:nix-systems/default"; + flake-compat.url = "https://flakehub.com/f/edolstra/flake-compat/1.tar.gz"; + }; + + outputs = + { systems + , nixpkgs + , ... + }: + let + eachSystem = f: + nixpkgs.lib.genAttrs (import systems) ( + system: + f nixpkgs.legacyPackages.${system} + ); + in + { + devShells = eachSystem + (pkgs: { + default = pkgs.mkShell + { + buildInputs = with pkgs; [ + nodejs_20 + + yarn + + nodePackages.typescript + nodePackages.typescript-language-server + ]; + }; + }); + }; +} diff --git a/netlify.toml b/netlify.toml deleted file mode 100644 index 4366f55..0000000 --- a/netlify.toml +++ /dev/null @@ -1,3 +0,0 @@ -[build] - publish = ".next" - command = "yarn build" \ No newline at end of file diff --git a/next.config.js b/next.config.js deleted file mode 100644 index 639e431..0000000 --- a/next.config.js +++ /dev/null @@ -1,20 +0,0 @@ -const packageInfo = require("./package.json"); - -// @ts-check -/** - * @type {import('next').NextConfig} - **/ -module.exports = { - reactStrictMode: true, - eslint: { - ignoreDuringBuilds: true - }, - env: { - NEXT_PUBLIC_GITHUB_URL: process.env.GIT_URL ?? packageInfo.repository.url, - NEXT_PUBLIC_APP_NAME: process.env.APP_NAME ?? packageInfo.displayName, - NEXT_PUBLIC_DEFAULT_SERVER: - process.env.DEFAULT_SERVER ?? "invidious.privacy.gd" - }, - basePath: process.env.BASE_PATH ?? "", - trailingSlash: !(process.env.CI == "true") -}; diff --git a/next.config.mjs b/next.config.mjs new file mode 100644 index 0000000..4678774 --- /dev/null +++ b/next.config.mjs @@ -0,0 +1,4 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = {}; + +export default nextConfig; diff --git a/package.json b/package.json index 7d6741a..d8470f0 100644 --- a/package.json +++ b/package.json @@ -1,51 +1,29 @@ { - "name": "material-tube", - "displayName": "MaterialTube", - "description": "MaterialTube is a beautiful and elegant web client for Invidious servers, built using Next.js and MUI.", - "license": "MIT", - "repository": { - "url": "https://github.com/Guusvanmeerveld/MaterialTube" - }, - "cacheDirectories": [".next/cache"], - "scripts": { - "dev": "next dev", - "build": "next build", - "start": "next start", - "lint": "next lint", - "prettify": "prettier src --write" - }, - "dependencies": { - "@emotion/react": "^11.8.2", - "@emotion/styled": "^11.8.1", - "@mui/icons-material": "^5.5.1", - "@mui/material": "^5.5.1", - "axios": "^0.26.1", - "luxon": "^2.3.1", - "next": "^12.1.0", - "next-seo": "^5.1.0", - "react": "^17.0.2", - "react-dom": "^17.0.2", - "react-intersection-observer": "^8.33.1", - "react-query": "^3.34.16", - "use-local-storage-state": "^16.0.1", - "zustand": "^3.7.2" - }, - "devDependencies": { - "@trivago/prettier-plugin-sort-imports": "^3.2.0", - "@types/luxon": "^2.3.1", - "@types/node": "^17.0.21", - "@types/react": "^17.0.40", - "@types/react-dom": "^17.0.13", - "@typescript-eslint/eslint-plugin": "^5.15.0", - "@typescript-eslint/parser": "^5.15.0", - "eslint": "^8.11.0", - "eslint-config-next": "12.1.0", - "eslint-plugin-css-modules": "^2.11.0", - "eslint-plugin-jsx-a11y": "^6.5.1", - "eslint-plugin-prettier": "^4.0.0", - "eslint-plugin-react": "^7.29.4", - "eslint-plugin-react-hooks": "^4.3.0", - "prettier": "^2.6.0", - "typescript": "^4.6.2" - } + "name": "materialtube", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@nextui-org/react": "^2.2.10", + "framer-motion": "^11.0.12", + "next": "14.1.3", + "react": "^18", + "react-dom": "^18" + }, + "devDependencies": { + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "autoprefixer": "^10.0.1", + "eslint": "^8", + "eslint-config-next": "14.1.3", + "postcss": "^8", + "tailwindcss": "^3.3.0", + "typescript": "^5" + } } diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..12a703d --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/public/favicon.ico b/public/favicon.ico deleted file mode 100644 index 5580bbae131017553009c50fc422fa2fc154bc5e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 935 zcmV;Y16cftP)W?L?-M-{UXb=Y)jI<;D86JoW56r}yd^hW^N!+nRl{v=9GNhDJ43TAwIjZb% z=gMEFJFfKm*l#)X0W0KPqvToPpk*D3=a{ zJ+c<8JRrjwj}B2U8IAVoJEgzHK&Z$AY&lAZY8L3@#5hDVno@uQO=J9%NZtif4zF&5 zbYwG}cKEXFM^I)M7tVJokuMSz-c5^Y*s!yPu&*q{vP>`VYIsz$VM2~@Z%9A3uO%}E z3HKoi41+V@<9YfYX2Cw=jiAA%4W6v1m_fxbYLH5mABKXtq1ph$ykGFcf0(ji-A}hg zK-%zKDEkf4!2i&MUTG_C414}kqM{#3N682V3hpOyKGzaM+Q9$ zfInf^Dh6$3-Z@>~=@T~nRnOdi2x%p%wj3tBlk2v6KjT|KNGp-C?HB6a7f^lrYCZ=3 zh)`CdX0x%k#gw9L@^S$Mgt8JjTdZO*<%hn8HEj?BT8YCzPOu%(E{7Bl*h(yGagMx@ z`gzl6`OpFaT8X?C7u|^q>T!0TNF|IL>-6?u4o{d3(XDgA_ z_ayEV{ror+lK@XvBBOEc+!J`wD5=;4c(4*Fjq&my2oP@}@h`FXWmNo!OnfOJzL*nV zPD|g=h~H9n!lqvQv5NGAE%8T-op87-{disc4~`D_#U%bGUkChV75}5#;aNy(HRyn5 z5Ap3bN{MfH>VTFr@lAh9iEq19N_^w1H4BnkkF9q>a`U%R;@j^n5t1)JSQ2DkL9s+g zz64}RkbMoy5+V5_+y@zQuOfajl-$dppM*%gj+-Dsf&>YK;x9!&k@Gf!d^-RD002ov JPDHLkV1ikcv~K_a diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..f2a32a4 --- /dev/null +++ b/shell.nix @@ -0,0 +1,11 @@ +(import + ( + let lock = builtins.fromJSON (builtins.readFile ./flake.lock); in + fetchTarball { + url = lock.nodes.flake-compat.locked.url or "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz"; + sha256 = lock.nodes.flake-compat.locked.narHash; + } + ) + { src = ./.; } +).shellNix + diff --git a/src/app/globals.css b/src/app/globals.css new file mode 100644 index 0000000..b5c61c9 --- /dev/null +++ b/src/app/globals.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/src/app/layout.tsx b/src/app/layout.tsx new file mode 100644 index 0000000..86ab459 --- /dev/null +++ b/src/app/layout.tsx @@ -0,0 +1,22 @@ +import type { Metadata } from "next"; +import "./globals.css"; +import { Providers } from "./providers"; + +export const metadata: Metadata = { + title: "Create Next App", + description: "Generated by create next app" +}; + +export default function RootLayout({ + children +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + {children} + + + ); +} diff --git a/src/app/page.tsx b/src/app/page.tsx new file mode 100644 index 0000000..e7d4c01 --- /dev/null +++ b/src/app/page.tsx @@ -0,0 +1,9 @@ +import { Button } from "@nextui-org/button"; + +export default function Home() { + return ( + <> + + + ); +} diff --git a/src/app/providers.tsx b/src/app/providers.tsx new file mode 100644 index 0000000..279d01e --- /dev/null +++ b/src/app/providers.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { NextUIProvider } from "@nextui-org/react"; + +export function Providers({ children }: { children: React.ReactNode }) { + return {children}; +} diff --git a/src/components/Channel/Inline.tsx b/src/components/Channel/Inline.tsx deleted file mode 100644 index b2a6933..0000000 --- a/src/components/Channel/Inline.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import Link from "next/link"; - -import { FC } from "react"; - -import Avatar from "@mui/material/Avatar"; -import Box from "@mui/material/Box"; -import Paper from "@mui/material/Paper"; -import Typography from "@mui/material/Typography"; -import { useTheme } from "@mui/material/styles"; - -import { abbreviateNumber, formatNumber } from "@src/utils/"; - -import { ChannelResult } from "@interfaces/api/search"; - -const Channel: FC<{ channel: ChannelResult }> = ({ channel }) => { - const theme = useTheme(); - - const thumbnail = channel.authorThumbnails.find( - (thumbnail) => thumbnail.height == 512 - )?.url as string; - - return ( - - - - - - - - {channel.author} - - {abbreviateNumber(channel.subCount)} subscribers •{" "} - {formatNumber(channel.videoCount)} videos - - - - {channel.description} - - - - - - - ); -}; - -export default Channel; diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx deleted file mode 100644 index cbb1899..0000000 --- a/src/components/Layout.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { FC } from "react"; - -import Box from "@mui/material/Box"; - -import Navbar from "@components/Navbar"; - -const Layout: FC = ({ children }) => ( - <> - - - {children} - -); - -export default Layout; diff --git a/src/components/Loading.tsx b/src/components/Loading.tsx deleted file mode 100644 index 760e9ce..0000000 --- a/src/components/Loading.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { FC } from "react"; - -import Box from "@mui/material/Box"; -import CircularProgress from "@mui/material/CircularProgress"; - -const Loading: FC = () => ( - - - -); - -export default Loading; diff --git a/src/components/MaterialColorPicker/ColorBox.tsx b/src/components/MaterialColorPicker/ColorBox.tsx deleted file mode 100644 index 6591dd3..0000000 --- a/src/components/MaterialColorPicker/ColorBox.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import Box from "@mui/material/Box"; -import { styled } from "@mui/material/styles"; - -interface ColorBoxProps { - color: string; -} - -const ColorBox = styled(Box, { - shouldForwardProp: (prop) => prop !== "color" -})(({ theme, color }) => ({ - width: 24, - height: 24, - backgroundColor: color, - borderRadius: "50%", - borderColor: theme.palette.text.primary, - borderWidth: 2, - borderStyle: "solid" -})); - -export default ColorBox; diff --git a/src/components/MaterialColorPicker/index.tsx b/src/components/MaterialColorPicker/index.tsx deleted file mode 100644 index c470e9a..0000000 --- a/src/components/MaterialColorPicker/index.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { FC } from "react"; - -import Box from "@mui/material/Box"; -import Grid from "@mui/material/Grid"; -import Modal from "@mui/material/Modal"; -import Typography from "@mui/material/Typography"; -import { - blue, - green, - amber, - red, - cyan, - teal, - deepOrange, - indigo, - yellow, - lightBlue, - orange, - lime, - deepPurple, - lightGreen, - pink, - purple -} from "@mui/material/colors"; -import { useTheme } from "@mui/material/styles"; - -import ModalBox from "@components/ModalBox"; - -const colors = [ - red, - deepOrange, - orange, - amber, - yellow, - lime, - lightGreen, - green, - teal, - cyan, - lightBlue, - blue, - indigo, - deepPurple, - purple, - pink -]; - -const MaterialColorPicker: FC<{ - isOpen: boolean; - setState: (isOpen: boolean) => void; - selectedColor: string; - setColor: (color: string) => void; -}> = ({ setState, isOpen, selectedColor, setColor }) => { - const theme = useTheme(); - - return ( - setState(false)} - aria-labelledby="color-picker" - aria-describedby="Pick a material color" - component="div" - > - - - Pick a color - - - {colors.map((color, i) => ( - - {Object.values(color) - .slice(0, 10) - .map((shade, i) => ( - setColor(shade)} - key={i} - sx={{ - width: 24, - height: 24, - backgroundColor: shade, - borderRadius: "50%", - border: - shade == selectedColor - ? { - borderColor: theme.palette.text.primary, - borderWidth: 2, - borderStyle: "solid" - } - : null, - cursor: "pointer", - mb: 1 - }} - > - ))} - - ))} - - - - ); -}; - -export default MaterialColorPicker; diff --git a/src/components/ModalBox.ts b/src/components/ModalBox.ts deleted file mode 100644 index 3b113dd..0000000 --- a/src/components/ModalBox.ts +++ /dev/null @@ -1,17 +0,0 @@ -import Box from "@mui/material/Box"; - -import styled from "@mui/system/styled"; - -const ModalBox = styled(Box)(({ theme }) => ({ - padding: "2rem", - position: "absolute", - left: "50%", - top: "50%", - transform: "translate(-50%, -50%)", - minWidth: "20rem", - backgroundColor: theme.palette.background.paper, - borderRadius: 5, - outline: "none" -})); - -export default ModalBox; diff --git a/src/components/Navbar/Drawer.tsx b/src/components/Navbar/Drawer.tsx deleted file mode 100644 index 9fc3a84..0000000 --- a/src/components/Navbar/Drawer.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import packageInfo from "../../../package.json"; - -import Link from "next/link"; - -import { FC } from "react"; - -import Box from "@mui/material/Box"; -import Divider from "@mui/material/Divider"; -import Drawer from "@mui/material/Drawer"; -import List from "@mui/material/List"; -import ListItem from "@mui/material/ListItem"; -import ListItemIcon from "@mui/material/ListItemIcon"; -import ListItemText from "@mui/material/ListItemText"; -import Typography from "@mui/material/Typography"; - -import Settings from "@mui/icons-material/Settings"; - -const AppDrawer: FC<{ - drawerIsOpen: boolean; - toggleDrawer: ( - isOpen: boolean - ) => (event: React.KeyboardEvent | React.MouseEvent) => void; - width: number; - pages: { - name: string; - icon: JSX.Element; - link: string; - }[]; -}> = ({ drawerIsOpen, toggleDrawer, pages, width }) => { - return ( - - - - - {process.env.NEXT_PUBLIC_APP_NAME} - - - - - {pages.map((page, index) => ( - - - {page.icon} - - - - ))} - - - - - - - - - - - - - ); -}; - -export default AppDrawer; diff --git a/src/components/Navbar/Search.tsx b/src/components/Navbar/Search.tsx deleted file mode 100644 index b8a9c5a..0000000 --- a/src/components/Navbar/Search.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import InputBase from "@mui/material/InputBase"; -import { alpha, styled } from "@mui/material/styles"; - -const Search = styled("div")(({ theme }) => ({ - position: "relative", - borderRadius: theme.shape.borderRadius, - backgroundColor: alpha(theme.palette.common.white, 0.15), - "&:hover": { - backgroundColor: alpha(theme.palette.common.white, 0.25) - }, - marginRight: theme.spacing(2), - marginLeft: 0, - width: "100%", - [theme.breakpoints.up("sm")]: { - marginLeft: theme.spacing(3), - width: "auto" - } -})); - -export const SearchIconWrapper = styled("div")(({ theme }) => ({ - padding: theme.spacing(0, 2), - height: "100%", - position: "absolute", - pointerEvents: "none", - display: "flex", - alignItems: "center", - justifyContent: "center" -})); - -export const StyledInputBase = styled(InputBase)(({ theme }) => ({ - color: "inherit", - "& .MuiInputBase-input": { - padding: theme.spacing(1, 1, 1, 0), - paddingLeft: `calc(1em + ${theme.spacing(4)})`, - transition: theme.transitions.create("width"), - width: "100%", - [theme.breakpoints.up("md")]: { - width: "20ch" - } - } -})); - -export default Search; diff --git a/src/components/Navbar/index.tsx b/src/components/Navbar/index.tsx deleted file mode 100644 index f33ca65..0000000 --- a/src/components/Navbar/index.tsx +++ /dev/null @@ -1,194 +0,0 @@ -import packageInfo from "../../../package.json"; - -import Link from "next/link"; -import { useRouter } from "next/router"; - -import { FC, useState } from "react"; - -import AppBar from "@mui/material/AppBar"; -import Box from "@mui/material/Box"; -import Button from "@mui/material/Button"; -import IconButton from "@mui/material/IconButton"; -import Toolbar from "@mui/material/Toolbar"; -import Tooltip from "@mui/material/Tooltip"; -import Typography from "@mui/material/Typography"; - -import History from "@mui/icons-material/History"; -import Menu from "@mui/icons-material/Menu"; -import PlayCircleOutline from "@mui/icons-material/PlayCircleOutline"; -import PlaylistAddCheck from "@mui/icons-material/PlaylistAddCheck"; -import SearchIcon from "@mui/icons-material/Search"; -import Settings from "@mui/icons-material/Settings"; -import Subscriptions from "@mui/icons-material/Subscriptions"; -import Whatshot from "@mui/icons-material/Whatshot"; - -import Drawer from "@components/Navbar/Drawer"; -import Search, { - SearchIconWrapper, - StyledInputBase -} from "@components/Navbar/Search"; - -export const drawerWidth = 240; - -const Navbar: FC = () => { - const [drawerIsOpen, setDrawerState] = useState(false); - - const router = useRouter(); - - const [search, setSearch] = useState(); - - const toggleDrawer = - (open: boolean) => (event: React.KeyboardEvent | React.MouseEvent) => { - if ( - event.type === "keydown" && - ((event as React.KeyboardEvent).key === "Tab" || - (event as React.KeyboardEvent).key === "Shift") - ) { - return; - } - - setDrawerState(open); - }; - - const pages = [ - { name: "Trending", icon: , link: "/trending" }, - { - name: "Subscriptions", - icon: , - link: "/subscriptions" - }, - { - name: "Watch History", - icon: , - link: "/history" - }, - { - name: "Playlists", - icon: , - link: "/playlists" - } - ]; - - return ( - <> - - - - - setDrawerState(!drawerIsOpen)} - > - - - - - - - - {process.env.NEXT_PUBLIC_APP_NAME} - - - - - - {pages.map((page, i) => ( - - - - - - ))} - - - - - -
{ - e.preventDefault(); - - router.push({ - pathname: "/results", - query: { search_query: search } - }); - }} - method="get" - > - setSearch(e.target.value)} - value={search} - name="search_query" - placeholder="Search…" - inputProps={{ "aria-label": "search" }} - /> - -
-
- - - { - if (router.pathname == "/settings") { - e.preventDefault(); - - router.back(); - } - }} - > - - - - - - - - - - ); -}; - -export default Navbar; diff --git a/src/components/Player/Actions.tsx b/src/components/Player/Actions.tsx deleted file mode 100644 index 78aa9ae..0000000 --- a/src/components/Player/Actions.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import { FC, MutableRefObject } from "react"; - -import Box from "@mui/material/Box"; -import Tooltip from "@mui/material/Tooltip"; - -import Fullscreen from "@mui/icons-material/Fullscreen"; -import Pause from "@mui/icons-material/Pause"; -import PlayArrow from "@mui/icons-material/PlayArrow"; -import Settings from "@mui/icons-material/Settings"; -import SkipNext from "@mui/icons-material/SkipNext"; -import Subtitles from "@mui/icons-material/Subtitles"; -import VolumeUp from "@mui/icons-material/VolumeUp"; - -import { VideoStatus } from "@interfaces/videoPlayer"; - -import useVideoState from "@utils/hooks/useVideoState"; - -import Time from "@components/Player/Time"; - -const iconStyles = { - mr: 1.5, - cursor: "pointer" -}; - -const Actions: FC<{ - duration: number; - videoRef: MutableRefObject; -}> = ({ duration, videoRef }) => { - const togglePlaying = useVideoState((state) => state.togglePlaying); - const playing = useVideoState((state) => state.playing); - - const muted = useVideoState((state) => state.muted); - const toggleMuted = useVideoState((state) => state.toggleMuted); - - return ( - - - - togglePlaying()} - > - {playing == VideoStatus.Playing ? : } - - - - - - - toggleMuted()} sx={iconStyles} /> - - - - - - - - - - - - - - - ); -}; - -export default Actions; diff --git a/src/components/Player/ProgressBar.tsx b/src/components/Player/ProgressBar.tsx deleted file mode 100644 index e9dbd8f..0000000 --- a/src/components/Player/ProgressBar.tsx +++ /dev/null @@ -1,74 +0,0 @@ -import { FC, MutableRefObject, useEffect, useState } from "react"; - -import Box from "@mui/material/Box"; -import { useTheme } from "@mui/material/styles"; - -import useVideoState from "@utils/hooks/useVideoState"; - -const ProgressBar: FC<{ - duration: number; - videoRef: MutableRefObject; -}> = ({ videoRef, duration }) => { - const theme = useTheme(); - - const [buffer, setBuffer] = useState(1); - - const height = 5; - - const bufferColor = "rgba(200, 200, 200, 0.5)"; - const backgroundColor = "rgba(132, 132, 132, 0.5)"; - - const progress = (useVideoState((state) => state.progress) / duration) * 100; - - useEffect(() => { - if (!videoRef.current) return; - - const buffered = videoRef.current.buffered; - - if (buffered.length != 0) { - const newBuffer = - ((buffered.end(0) - buffered.start(0)) / duration) * 100; - - if (newBuffer != buffer) { - setBuffer(newBuffer); - } - } - }, [buffer, duration, videoRef, videoRef.current?.buffered]); - - return ( - - - - - - - ); -}; - -export default ProgressBar; diff --git a/src/components/Player/Time.tsx b/src/components/Player/Time.tsx deleted file mode 100644 index cc64917..0000000 --- a/src/components/Player/Time.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { FC, MutableRefObject } from "react"; - -import Typography from "@mui/material/Typography"; -import { useTheme } from "@mui/material/styles"; - -import { formatTime } from "@src/utils/"; - -import useVideoState from "@utils/hooks/useVideoState"; - -const Time: FC<{ - videoRef: MutableRefObject; - duration: number; -}> = ({ videoRef, duration }) => { - const theme = useTheme(); - - const progress = useVideoState((state) => state.progress); - - return ( - - {formatTime(Math.round(progress))} - <> / - {formatTime(duration)} - - ); -}; - -export default Time; diff --git a/src/components/Player/index.tsx b/src/components/Player/index.tsx deleted file mode 100644 index 5a88e5c..0000000 --- a/src/components/Player/index.tsx +++ /dev/null @@ -1,212 +0,0 @@ -import { FC, MutableRefObject, useEffect, useRef, useState } from "react"; - -import Box from "@mui/material/Box"; -import { SxProps } from "@mui/material/styles"; - -import { AdaptiveFormat, Caption, FormatStream } from "@interfaces/video"; -import { PausedBy, VideoStatus } from "@interfaces/videoPlayer"; - -import useSettings from "@utils/hooks/useSettings"; -import useVideoState from "@utils/hooks/useVideoState"; - -import Actions from "@components/Player/Actions"; -import ProgressBar from "@components/Player/ProgressBar"; - -const Player: FC<{ - formats: AdaptiveFormat[]; - streams: FormatStream[]; - captions: Caption[]; - length: number; - videoId: string; - sx?: SxProps; -}> = ({ formats, length: duration, sx }) => { - const [settings] = useSettings(); - - const playing = useVideoState((state) => state.playing); - const togglePlaying = useVideoState((state) => state.togglePlaying); - const setPlaying = useVideoState((state) => state.setPlaying); - - const speed = useVideoState((state) => state.speed); - const muted = useVideoState((state) => state.muted); - - const error = useVideoState((state) => state.error); - const setError = useVideoState((state) => state.setError); - - const waiting = useVideoState((state) => state.waiting); - const setWaiting = useVideoState((state) => state.setWaiting); - - const setProgress = useVideoState((state) => state.setProgress); - - const pausedBy = useVideoState((state) => state.pausedBy); - - const videoStream = formats.find( - (format) => - format.qualityLabel == "2160p" || - format.qualityLabel == "1080p" - )?.url; - - const audioStream = formats.find((format) => - format.type.includes("audio/mp4") - )?.url as string; - - const videoRef = useRef(null); - const audioRef = useRef(new Audio(audioStream)); - - useEffect(() => { - const audio = audioRef.current; - - audio.volume = 0.25; - - const video = videoRef.current; - - if (!video) return; - - video.playbackRate = speed; - - video.currentTime = 0; - - const handleError = (e: ErrorEvent) => { - setError(e.message || "An unknown error occurred"); - setPlaying(VideoStatus.Paused, PausedBy.Player); - }; - - const handleWaiting = (e: Event) => { - setWaiting(true); - - if (playing == VideoStatus.Playing) - setPlaying(VideoStatus.Paused, PausedBy.Player); - }; - - const handleFinishedWaiting = (e: Event) => { - setWaiting(false); - - if (pausedBy == PausedBy.Player) - setPlaying(VideoStatus.Playing); - }; - - const onTimeUpdate = () => { - setProgress(video.currentTime ?? 0); - }; - - const handlePause = () => { - setPlaying(VideoStatus.Paused); - }; - - if (!videoStream) setError("Could not find video stream"); - - video.addEventListener("waiting", handleWaiting); - video.addEventListener("canplaythrough", handleFinishedWaiting); - video.addEventListener("error", handleError); - video.addEventListener("pause", handlePause); - video.addEventListener("timeupdate", onTimeUpdate); - - audio.addEventListener("waiting", handleWaiting); - audio.addEventListener("canplaythrough", handleFinishedWaiting); - audio.addEventListener("pause", handlePause); - - return () => { - audio.srcObject = null; - - video.removeEventListener("waiting", handleWaiting); - video.removeEventListener( - "canplaythrough", - handleFinishedWaiting - ); - video.removeEventListener("error", handleError); - video.removeEventListener("pause", handlePause); - video.removeEventListener("timeupdate", onTimeUpdate); - - audio.removeEventListener("waiting", handleWaiting); - audio.removeEventListener( - "canplaythrough", - handleFinishedWaiting - ); - audio.removeEventListener("pause", handlePause); - }; - - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - useEffect(() => { - setPlaying( - settings.autoPlay - ? VideoStatus.Playing - : VideoStatus.Paused - ); - }, [setPlaying, settings.autoPlay]); - - useEffect(() => { - if (!videoRef.current || !audioRef.current) return; - - if (playing == VideoStatus.Playing && !error && !waiting) { - videoRef.current.play(); - audioRef.current.play(); - } else { - videoRef.current.pause(); - audioRef.current.pause(); - } - }, [error, playing, waiting]); - - useEffect(() => { - if (!videoRef.current) return; - - videoRef.current.playbackRate = speed; - }, [speed]); - - useEffect(() => { - if (!audioRef.current) return; - - audioRef.current.muted = muted; - }, [muted, audioRef]); - - return ( - - {error && ( - - {error} - - )} - - togglePlaying(PausedBy.User)} - sx={{ - boxShadow: "0px -15px 30px 0px rgba(0,0,0,0.75) inset", - position: "absolute", - top: 0, - left: 0, - width: "100%", - height: "100%" - }} - > - - - - ); -}; - -export default Player; diff --git a/src/components/Video/Grid.tsx b/src/components/Video/Grid.tsx deleted file mode 100644 index 53a3767..0000000 --- a/src/components/Video/Grid.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { FC } from "react"; - -import Grid from "@mui/material/Grid"; - -import VideoModel from "@interfaces/video"; - -import Video from "@components/Video"; - -const VideoGrid: FC<{ videos: VideoModel[] }> = ({ videos }) => { - return ( - - {videos.map((video) => ( - - - ))} - - ); -}; - -export default VideoGrid; diff --git a/src/components/Video/Inline.tsx b/src/components/Video/Inline.tsx deleted file mode 100644 index 6fc591a..0000000 --- a/src/components/Video/Inline.tsx +++ /dev/null @@ -1,144 +0,0 @@ -import Link from "next/link"; - -import { FC } from "react"; - -import Avatar from "@mui/material/Avatar"; -import Box from "@mui/material/Box"; -import CircularProgress from "@mui/material/CircularProgress"; -import Grid from "@mui/material/Grid"; -import Paper from "@mui/material/Paper"; -import Tooltip from "@mui/material/Tooltip"; -import Typography from "@mui/material/Typography"; -import { red } from "@mui/material/colors"; -import { useTheme } from "@mui/material/styles"; - -import { abbreviateNumber } from "@src/utils/"; - -import VideoModel from "@interfaces/video"; - -import { useAuthorThumbnail } from "@utils/requests"; - -const Video: FC<{ video: VideoModel }> = ({ video }) => { - const theme = useTheme(); - - const { - ref, - isLoading, - thumbnail: authorThumbnail - } = useAuthorThumbnail(video.author.id, 176); - - return ( - - - - {video.live && ( - - Live - - )} - {/* eslint-disable-next-line @next/next/no-img-element */} - thumbnail - - - - - - - - {video.title} - - - - - - {!(video.live || video.upcoming) && ( - <> - {abbreviateNumber(video.views)} Views • Published{" "} - {video.published.text} - - )} - {video.live && <>🔴 Live now} - {video.upcoming && video.premiereTimestamp && ( - <> - Premiering on{" "} - {new Date(video.premiereTimestamp * 1000).toLocaleDateString()}{" "} - at{" "} - {new Date(video.premiereTimestamp * 1000).toLocaleTimeString()} - - )} - - - {video.description.text} - - - - - {isLoading && } - {!isLoading && ( - - )} - - {video.author.name} - - - - - - - - ); -}; - -export default Video; diff --git a/src/components/Video/index.tsx b/src/components/Video/index.tsx deleted file mode 100644 index dae52c7..0000000 --- a/src/components/Video/index.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import { DateTime } from "luxon"; - -import Link from "next/link"; - -import { FC } from "react"; - -import Avatar from "@mui/material/Avatar"; -import Box from "@mui/material/Box"; -import Card from "@mui/material/Card"; -import CardActionArea from "@mui/material/CardActionArea"; -import CardContent from "@mui/material/CardContent"; -import CardMedia from "@mui/material/CardMedia"; -import CircularProgress from "@mui/material/CircularProgress"; -import Tooltip from "@mui/material/Tooltip"; -import Typography from "@mui/material/Typography"; -import { useTheme } from "@mui/material/styles"; - -import { abbreviateNumber, formatTime } from "@src/utils"; - -import VideoModel from "@interfaces/video"; - -import { useAuthorThumbnail } from "@utils/requests"; - -const Video: FC = (video) => { - const theme = useTheme(); - - const { - ref, - isLoading, - thumbnail: authorThumbnail - } = useAuthorThumbnail(video.author.id, 100); - - return ( - - - - - - - - {formatTime(video.length)} - - - - - - {video.title} - - - - - - {isLoading && } - {!isLoading && ( - - )} - - {video.author.name} - - - - - - {!(video.live || video.upcoming) && ( - <> - {abbreviateNumber(video.views)} Views • Published{" "} - {video.published.text} - - )} - - - - - - - ); -}; - -export default Video; diff --git a/src/globals.css b/src/globals.css deleted file mode 100644 index e28daa0..0000000 --- a/src/globals.css +++ /dev/null @@ -1,4 +0,0 @@ -a { - text-decoration: none; - color: unset; -} diff --git a/src/interfaces/api/channel.ts b/src/interfaces/api/channel.ts deleted file mode 100644 index b0c9c78..0000000 --- a/src/interfaces/api/channel.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { Quality } from "@interfaces/api"; -import VideoTrending from "@interfaces/api/trending"; - -interface Channel { - author: string; - authorId: string; - authorUrl: string; - authorBanners: AuthorBanner[]; - authorThumbnails: AuthorBanner[]; - subCount: number; - totalViews: number; - joined: number; - autoGenerated: boolean; - isFamilyFriendly: boolean; - description: string; - descriptionHtml: string; - allowedRegions: string[]; - latestVideos: VideoTrending[]; - relatedChannels: RelatedChannel[]; -} - -interface AuthorBanner { - url: string; - width: number; - height: number; - quality?: Quality; -} - -interface RelatedChannel { - author: string; - authorId: string; - authorUrl: string; - authorThumbnails: AuthorBanner[]; -} - -export default Channel; diff --git a/src/interfaces/api/index.ts b/src/interfaces/api/index.ts deleted file mode 100644 index 55986a5..0000000 --- a/src/interfaces/api/index.ts +++ /dev/null @@ -1,22 +0,0 @@ -export interface Error { - error: string; -} - -export interface Thumbnail { - url: string; - width: number; - height: number; - quality?: Quality; -} - -export enum Quality { - Default = "default", - End = "end", - High = "high", - Maxres = "maxres", - Maxresdefault = "maxresdefault", - Medium = "medium", - Middle = "middle", - Sddefault = "sddefault", - Start = "start" -} diff --git a/src/interfaces/api/instances.ts b/src/interfaces/api/instances.ts deleted file mode 100644 index e9aaab0..0000000 --- a/src/interfaces/api/instances.ts +++ /dev/null @@ -1,71 +0,0 @@ -export interface ServerInstance { - flag?: string; - region?: string; - stats?: Stats; - cors?: boolean; - api?: boolean; - type: ServerInstanceType; - uri: string; - monitor?: Monitor; -} - -interface Monitor { - monitorId: number; - createdAt: number; - statusClass: StatusClass; - name: string; - url: null; - type: MonitorType; - dailyRatios: Ratio[]; - "90dRatio": Ratio; - "30dRatio": Ratio; -} - -interface Ratio { - ratio: string; - label: StatusClass; -} - -export enum StatusClass { - Black = "black", - Success = "success", - Warning = "warning" -} - -export enum MonitorType { - HTTPS = "HTTP(s)" -} - -export interface Stats { - version: string; - software: Software; - openRegistrations: boolean; - usage: Usage; - metadata: Metadata; -} - -interface Metadata { - updatedAt: number; - lastChannelRefreshedAt: number; -} - -interface Software { - name: string; - version: string; - branch: string; -} - -interface Usage { - users: Users; -} - -interface Users { - total: number; - activeHalfyear: number; - activeMonth: number; -} - -export enum ServerInstanceType { - HTTPS = "https", - Onion = "onion" -} diff --git a/src/interfaces/api/search.ts b/src/interfaces/api/search.ts deleted file mode 100644 index f35ef8d..0000000 --- a/src/interfaces/api/search.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { Thumbnail } from "@interfaces/api"; -import VideoTrending from "@interfaces/api/trending"; - -interface Results { - type: Type; -} - -export interface ChannelResult extends Results { - type: "channel"; - author: string; - authorId: string; - authorUrl: string; - authorThumbnails: Thumbnail[]; - subCount: number; - videoCount: number; - description: string; - descriptionHtml: string; -} - -export interface VideoResult extends Results { - type: "video"; - title: string; - author: string; - authorId: string; - authorUrl: string; - description: string; - descriptionHtml: string; - videoId: string; - videoThumbnails: Thumbnail[]; - viewCount: number; - published: number; - publishedText: string; - lengthSeconds: number; - isUpcoming: boolean; - premiereTimestamp?: number; - liveNow: boolean; - premium: boolean; -} - -export interface PlaylistResult extends Results { - type: "playlist"; - title: string; - playlistId: string; - author: string; - authorId: string; - authorUrl: string; - videoCount: number; - videos: Video[]; -} - -export interface CategoryResult extends Results { - type: "category"; - title: string; - contents: VideoTrending[]; -} - -export interface Content { - type: Type; - title: string; - videoId: string; - author: string; - authorId: string; - authorUrl: string; - videoThumbnails: Thumbnail[]; - description: string; - descriptionHtml: string; - viewCount: number; - published: number; - publishedText: string; - lengthSeconds: number; - liveNow: boolean; - premium: boolean; - isUpcoming: boolean; -} - -export type Type = "category" | "channel" | "playlist" | "video"; - -export interface Video { - title: string; - videoId: string; - lengthSeconds: number; - videoThumbnails: Thumbnail[]; -} - -export default Results; diff --git a/src/interfaces/api/trending.ts b/src/interfaces/api/trending.ts deleted file mode 100644 index 72495b7..0000000 --- a/src/interfaces/api/trending.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Thumbnail } from "@interfaces/api"; - -interface Trending { - type: string; - title: string; - videoId: string; - author: string; - authorId: string; - authorUrl: string; - videoThumbnails: Thumbnail[]; - description: string; - descriptionHtml: string; - viewCount: number; - published: number; - publishedText: string; - lengthSeconds: number; - liveNow: boolean; - premium: boolean; - isUpcoming: boolean; - premiereTimestamp?: number; -} - -export default Trending; diff --git a/src/interfaces/api/video.ts b/src/interfaces/api/video.ts deleted file mode 100644 index 89aeb8d..0000000 --- a/src/interfaces/api/video.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { Thumbnail } from "@interfaces/api"; -import { - AdaptiveFormat, - Caption, - FormatStream, - RecommendedVideo -} from "@interfaces/video"; - -export interface Video { - type: string; - title: string; - videoId: string; - videoThumbnails: Thumbnail[]; - storyboards: Storyboard[]; - description: string; - descriptionHtml: string; - published: number; - publishedText: string; - keywords: string[]; - viewCount: number; - likeCount: number; - dislikeCount: number; - paid: boolean; - premium: boolean; - isFamilyFriendly: boolean; - allowedRegions: string[]; - premiereTimestamp?: number; - genre: string; - genreUrl: string; - author: string; - authorId: string; - authorUrl: string; - authorThumbnails: Thumbnail[]; - subCountText: string; - lengthSeconds: number; - allowRatings: boolean; - rating: number; - isListed: boolean; - liveNow: boolean; - isUpcoming: boolean; - dashUrl: string; - adaptiveFormats: AdaptiveFormat[]; - formatStreams: FormatStream[]; - captions: Caption[]; - recommendedVideos: RecommendedVideo[]; -} - -interface Storyboard { - url: string; - templateUrl: string; - width: number; - height: number; - count: number; - interval: number; - storyboardWidth: number; - storyboardHeight: number; - storyboardCount: number; -} - -export default Video; diff --git a/src/interfaces/settings.ts b/src/interfaces/settings.ts deleted file mode 100644 index 4ad041e..0000000 --- a/src/interfaces/settings.ts +++ /dev/null @@ -1,19 +0,0 @@ -interface Settings { - theme?: "light" | "dark"; - primaryColor: string; - accentColor: string; - invidiousServer: string; - invidiousUsername?: string; - storageType: StorageType; - customServer?: string; - password?: string; - autoPlay: boolean; -} - -export enum StorageType { - Local = "local", - Invidious = "invidious", - RemoteServer = "remoteserver" -} - -export default Settings; diff --git a/src/interfaces/video.ts b/src/interfaces/video.ts deleted file mode 100644 index 686e9a6..0000000 --- a/src/interfaces/video.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { Thumbnail } from "@interfaces/api"; - -interface Video { - thumbnail: string; - title: string; - description: { - text: string; - html: string; - }; - id: string; - author: { - name: string; - id: string; - url: string; - thumbnail?: string; - }; - views: number; - published: { - time: Date; - text: string; - }; - length: number; - live: boolean; - premium: boolean; - keywords?: string[]; - likes?: number; - dislikes?: number; - familyFriendly?: boolean; - genre?: { - type: string; - url: string; - }; - subscriptions?: string; - rating?: number; - upcoming?: boolean; - premiereTimestamp?: number; - premiered?: Date; - recommendedVideos?: RecommendedVideo[]; - adaptiveFormats?: AdaptiveFormat[]; - formatStreams?: FormatStream[]; - captions?: Caption[]; -} - -export interface RecommendedVideo { - videoId: string; - title: string; - videoThumbnails: Thumbnail[]; - author: string; - authorUrl: string; - authorId: string; - lengthSeconds: number; - viewCountText: string; - viewCount: number; -} - -export interface Caption { - label: string; - language_code: string; - url: string; -} - -enum ProjectionType { - Rectangular = "RECTANGULAR" -} - -export interface AdaptiveFormat { - index: string; - bitrate: string; - init: string; - url: string; - itag: string; - type: string; - clen: string; - lmt: string; - projectionType: ProjectionType; - fps?: number; - container?: Container; - encoding?: string; - resolution?: string; - qualityLabel?: string; -} - -export interface FormatStream { - url: string; - itag: string; - type: string; - quality: string; - fps: number; - container: string; - encoding: string; - resolution: string; - qualityLabel: string; - size: string; -} - -enum Container { - M4A = "m4a", - Mp4 = "mp4", - Webm = "webm" -} - -export default Video; diff --git a/src/interfaces/videoPlayer.ts b/src/interfaces/videoPlayer.ts deleted file mode 100644 index ce6d0d3..0000000 --- a/src/interfaces/videoPlayer.ts +++ /dev/null @@ -1,11 +0,0 @@ -export type VideoSpeed = 0.25 | 0.5 | 1.0 | 1.25 | 1.5 | 1.75 | 2.0; - -export enum VideoStatus { - Playing = "playing", - Paused = "paused" -} - -export enum PausedBy { - User = "user", - Player = "player" -} diff --git a/src/next-seo.config.ts b/src/next-seo.config.ts deleted file mode 100644 index 5e789ec..0000000 --- a/src/next-seo.config.ts +++ /dev/null @@ -1,17 +0,0 @@ -import packageInfo from "../package.json"; - -import type { DefaultSeoProps } from "next-seo"; - -const name = process.env.NEXT_PUBLIC_APP_NAME; - -const SEO: DefaultSeoProps = { - titleTemplate: `%s - ${name}`, - defaultTitle: name, - description: packageInfo.description, - openGraph: { - description: name, - site_name: name - } -}; - -export default SEO; diff --git a/src/pages/404.tsx b/src/pages/404.tsx deleted file mode 100644 index 1a44ab3..0000000 --- a/src/pages/404.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { NextPage } from "next"; -import { NextSeo } from "next-seo"; - -import Box from "@mui/material/Box"; -import Typography from "@mui/material/Typography"; - -import Layout from "@components/Layout"; - -const NotFound: NextPage = () => { - return ( - <> - - - - - Page not found - - - The page may have been moved or - deleted. - - - - - ); -}; - -export default NotFound; diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx deleted file mode 100644 index 11f4b92..0000000 --- a/src/pages/_app.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import { DefaultSeo } from "next-seo"; -import type { AppProps } from "next/app"; - -import { useMemo } from "react"; - -import { QueryClient, QueryClientProvider } from "react-query"; -import { ReactQueryDevtools } from "react-query/devtools"; - -import CssBaseline from "@mui/material/CssBaseline"; -import { ThemeProvider } from "@mui/material/styles"; -import useMediaQuery from "@mui/material/useMediaQuery"; - -import "@src/globals.css"; -import SEO from "@src/next-seo.config"; -import createTheme from "@src/theme"; - -import useSettings from "@utils/hooks/useSettings"; - -const queryClient = new QueryClient({ - defaultOptions: { - queries: { refetchOnWindowFocus: false, retry: 5, retryDelay: 5000 } - } -}); - -const App = ({ Component, pageProps }: AppProps) => { - const [settings] = useSettings(); - - const prefersDarkMode = useMediaQuery("(prefers-color-scheme: dark)"); - - let dark: boolean; - - if (settings.theme) { - if (settings.theme == "dark") dark = true; - else dark = false; - } else { - dark = prefersDarkMode; - } - - const theme = useMemo(() => createTheme(settings, dark), [settings, dark]); - - return ( - - {process.env.NODE_ENV != "production" && ( - - )} - - - - - - - ); -}; - -export default App; diff --git a/src/pages/_document.tsx b/src/pages/_document.tsx deleted file mode 100644 index 82945a4..0000000 --- a/src/pages/_document.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { Html, Head, Main, NextScript } from "next/document"; - -export default function Document() { - return ( - - - - - - -
- - - - ); -} diff --git a/src/pages/index.tsx b/src/pages/index.tsx deleted file mode 100644 index 59c03c1..0000000 --- a/src/pages/index.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import packageInfo from "../../package.json"; - -import { NextPage } from "next"; -import { NextSeo } from "next-seo"; -import Link from "next/link"; - -import Box from "@mui/material/Box"; -import Button from "@mui/material/Button"; -import Typography from "@mui/material/Typography"; - -import PlayCircleOutline from "@mui/icons-material/PlayCircleOutline"; - -import Layout from "@components/Layout"; - -const Index: NextPage = () => ( - <> - - - - - - {process.env.NEXT_PUBLIC_APP_NAME} - - - {packageInfo.description} - - - - - - - - - - - - -); - -export default Index; diff --git a/src/pages/results.tsx b/src/pages/results.tsx deleted file mode 100644 index dd703e2..0000000 --- a/src/pages/results.tsx +++ /dev/null @@ -1,163 +0,0 @@ -import NotFound from "./404"; - -import { NextPage } from "next"; -import { NextSeo } from "next-seo"; -import { useRouter } from "next/router"; - -import { FC, useState } from "react"; - -import { useQuery } from "react-query"; - -import axios from "axios"; - -import Box from "@mui/material/Box"; -import Button from "@mui/material/Button"; -import Container from "@mui/material/Container"; -import Divider from "@mui/material/Divider"; -import Typography from "@mui/material/Typography"; -import { useTheme } from "@mui/material/styles"; - -import Add from "@mui/icons-material/Add"; - -import Result, { - CategoryResult, - ChannelResult, - PlaylistResult, - VideoResult -} from "@interfaces/api/search"; - -import { apiToVideo } from "@utils/conversions"; -import useSettings from "@utils/hooks/useSettings"; - -import Channel from "@components/Channel/Inline"; -import Layout from "@components/Layout"; -import Loading from "@components/Loading"; -import Video from "@components/Video/Inline"; - -const Results: NextPage = () => { - const router = useRouter(); - - const [settings] = useSettings(); - - const query = router.query["search_query"]; - - const { data, isLoading } = useQuery( - ["searchResultsFor", query], - () => - query - ? axios - .get(`https://${settings.invidiousServer}/api/v1/search`, { - params: { - q: query, - type: "all" - } - }) - .then((res) => res.data) - : undefined - ); - - const Category: FC<{ category: CategoryResult }> = ({ category }) => { - const initialCount = 3; - - const [count, setCount] = useState(initialCount); - - const shownVideos = category.contents.slice(0, count); - - return ( - <> - {category.title} - - {shownVideos.map((video, i) => ( -