Shivam commited on
Commit ·
d092f57
1
Parent(s): 4cf14a8
Initial commit: Web-SyncPlay moved into Streamer
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .dockerignore +28 -0
- .env +11 -0
- .eslintrc.json +3 -0
- .github/ISSUE_TEMPLATE/bug-report.md +7 -0
- .github/ISSUE_TEMPLATE/feature-request.md +7 -0
- .github/workflows/codeql-analysis.yml +71 -0
- .github/workflows/docker-publish.yml +57 -0
- .gitignore +29 -0
- .prettierignore +45 -0
- .prettierrc +9 -0
- Dockerfile +61 -0
- LICENSE +21 -0
- README.md +170 -10
- components/Embed.tsx +71 -0
- components/Footer.tsx +39 -0
- components/Head.tsx +44 -0
- components/Layout.tsx +42 -0
- components/Navbar.tsx +69 -0
- components/Room.tsx +113 -0
- components/action/Button.tsx +40 -0
- components/action/DeleteButton.tsx +24 -0
- components/action/DropUp.tsx +67 -0
- components/action/InteractionHandler.tsx +104 -0
- components/action/NewTabLink.tsx +26 -0
- components/alert/Alert.tsx +39 -0
- components/alert/AutoplayAlert.tsx +21 -0
- components/alert/BufferAlert.tsx +17 -0
- components/alert/ConnectingAlert.tsx +19 -0
- components/alert/Loading.module.css +12 -0
- components/alert/NoScriptAlert.tsx +12 -0
- components/icon/Icon.tsx +30 -0
- components/icon/IconBackward.tsx +15 -0
- components/icon/IconBigPause.tsx +17 -0
- components/icon/IconBigPlay.tsx +23 -0
- components/icon/IconCC.tsx +15 -0
- components/icon/IconChevron.tsx +59 -0
- components/icon/IconClipboard.tsx +17 -0
- components/icon/IconClose.tsx +17 -0
- components/icon/IconCog.tsx +15 -0
- components/icon/IconCompress.tsx +15 -0
- components/icon/IconCopyright.tsx +19 -0
- components/icon/IconDelete.tsx +15 -0
- components/icon/IconDisk.tsx +19 -0
- components/icon/IconDrag.tsx +19 -0
- components/icon/IconExpand.tsx +15 -0
- components/icon/IconForward.tsx +15 -0
- components/icon/IconGithub.tsx +19 -0
- components/icon/IconLoading.tsx +24 -0
- components/icon/IconLoop.tsx +15 -0
- components/icon/IconMusic.tsx +15 -0
.dockerignore
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
| 2 |
+
|
| 3 |
+
# dependencies
|
| 4 |
+
node_modules/
|
| 5 |
+
/.pnp
|
| 6 |
+
.pnp.js
|
| 7 |
+
|
| 8 |
+
# testing
|
| 9 |
+
/coverage
|
| 10 |
+
|
| 11 |
+
# production
|
| 12 |
+
/.next
|
| 13 |
+
/build
|
| 14 |
+
/.dist
|
| 15 |
+
|
| 16 |
+
# misc
|
| 17 |
+
.DS_Store
|
| 18 |
+
.env.local
|
| 19 |
+
.env.development.local
|
| 20 |
+
.env.test.local
|
| 21 |
+
.env.production.local
|
| 22 |
+
|
| 23 |
+
npm-debug.log*
|
| 24 |
+
yarn-debug.log*
|
| 25 |
+
yarn-error.log*
|
| 26 |
+
|
| 27 |
+
.idea
|
| 28 |
+
.iml
|
.env
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# to see if your .env file is working correctly by running `docker-compose config`
|
| 2 |
+
|
| 3 |
+
# the site name
|
| 4 |
+
SITE_NAME="Web-SyncPlay"
|
| 5 |
+
|
| 6 |
+
# your domain from which sessions are being served
|
| 7 |
+
# remove trailing slash !!!
|
| 8 |
+
PUBLIC_DOMAIN="https://web-syncplay.de"
|
| 9 |
+
|
| 10 |
+
# modify if you pass your own running redis instance
|
| 11 |
+
REDIS_URL="redis://localhost:6379"
|
.eslintrc.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"extends": "next/core-web-vitals"
|
| 3 |
+
}
|
.github/ISSUE_TEMPLATE/bug-report.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: Bug report
|
| 3 |
+
about: Create a report to help us improve
|
| 4 |
+
title: "[BUG]"
|
| 5 |
+
labels: bug
|
| 6 |
+
assignees: ""
|
| 7 |
+
---
|
.github/ISSUE_TEMPLATE/feature-request.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: Feature request
|
| 3 |
+
about: Suggest an idea for this project
|
| 4 |
+
title: "[FEAT]"
|
| 5 |
+
labels: enhancement
|
| 6 |
+
assignees: ""
|
| 7 |
+
---
|
.github/workflows/codeql-analysis.yml
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# For most projects, this workflow file will not need changing; you simply need
|
| 2 |
+
# to commit it to your repository.
|
| 3 |
+
#
|
| 4 |
+
# You may wish to alter this file to override the set of languages analyzed,
|
| 5 |
+
# or to provide custom queries or build logic.
|
| 6 |
+
#
|
| 7 |
+
# ******** NOTE ********
|
| 8 |
+
# We have attempted to detect the languages in your repository. Please check
|
| 9 |
+
# the `language` matrix defined below to confirm you have the correct set of
|
| 10 |
+
# supported CodeQL languages.
|
| 11 |
+
#
|
| 12 |
+
name: "CodeQL"
|
| 13 |
+
|
| 14 |
+
on:
|
| 15 |
+
push:
|
| 16 |
+
branches: [master]
|
| 17 |
+
pull_request:
|
| 18 |
+
# The branches below must be a subset of the branches above
|
| 19 |
+
branches: [master]
|
| 20 |
+
schedule:
|
| 21 |
+
- cron: "37 6 * * 4"
|
| 22 |
+
|
| 23 |
+
jobs:
|
| 24 |
+
analyze:
|
| 25 |
+
name: Analyze
|
| 26 |
+
runs-on: ubuntu-latest
|
| 27 |
+
permissions:
|
| 28 |
+
actions: read
|
| 29 |
+
contents: read
|
| 30 |
+
security-events: write
|
| 31 |
+
|
| 32 |
+
strategy:
|
| 33 |
+
fail-fast: false
|
| 34 |
+
matrix:
|
| 35 |
+
language: ["javascript"]
|
| 36 |
+
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
| 37 |
+
# Learn more:
|
| 38 |
+
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
| 39 |
+
|
| 40 |
+
steps:
|
| 41 |
+
- name: Checkout repository
|
| 42 |
+
uses: actions/checkout@v2
|
| 43 |
+
|
| 44 |
+
# Initializes the CodeQL tools for scanning.
|
| 45 |
+
- name: Initialize CodeQL
|
| 46 |
+
uses: github/codeql-action/init@v1
|
| 47 |
+
with:
|
| 48 |
+
languages: ${{ matrix.language }}
|
| 49 |
+
# If you wish to specify custom queries, you can do so here or in a config file.
|
| 50 |
+
# By default, queries listed here will override any specified in a config file.
|
| 51 |
+
# Prefix the list here with "+" to use these queries and those in the config file.
|
| 52 |
+
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
| 53 |
+
|
| 54 |
+
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
| 55 |
+
# If this step fails, then you should remove it and run the build manually (see below)
|
| 56 |
+
- name: Autobuild
|
| 57 |
+
uses: github/codeql-action/autobuild@v1
|
| 58 |
+
|
| 59 |
+
# ℹ️ Command-line programs to run using the OS shell.
|
| 60 |
+
# 📚 https://git.io/JvXDl
|
| 61 |
+
|
| 62 |
+
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
| 63 |
+
# and modify them (or add more) to build your code if your project
|
| 64 |
+
# uses a compiled language
|
| 65 |
+
|
| 66 |
+
#- run: |
|
| 67 |
+
# make bootstrap
|
| 68 |
+
# make release
|
| 69 |
+
|
| 70 |
+
- name: Perform CodeQL Analysis
|
| 71 |
+
uses: github/codeql-action/analyze@v1
|
.github/workflows/docker-publish.yml
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Docker
|
| 2 |
+
|
| 3 |
+
on: [push]
|
| 4 |
+
|
| 5 |
+
jobs:
|
| 6 |
+
# Push image to GitHub Packages and docker hub.
|
| 7 |
+
# See also https://docs.docker.com/docker-hub/builds/
|
| 8 |
+
push:
|
| 9 |
+
runs-on: ubuntu-latest
|
| 10 |
+
if: github.event_name == 'push'
|
| 11 |
+
|
| 12 |
+
steps:
|
| 13 |
+
- name: Checkout
|
| 14 |
+
uses: actions/checkout@v2
|
| 15 |
+
|
| 16 |
+
- name: Prepare
|
| 17 |
+
id: prep
|
| 18 |
+
run: |
|
| 19 |
+
DOCKER_IMAGE="websyncplay/websyncplay"
|
| 20 |
+
VERSION=edge
|
| 21 |
+
if [[ $GITHUB_REF == refs/tags/* ]]; then
|
| 22 |
+
VERSION=${GITHUB_REF#refs/tags/}
|
| 23 |
+
elif [[ $GITHUB_REF == refs/heads/* ]]; then
|
| 24 |
+
VERSION=$(echo ${GITHUB_REF#refs/heads/} | sed -r 's#/+#-#g')
|
| 25 |
+
elif [[ $GITHUB_REF == refs/pull/* ]]; then
|
| 26 |
+
VERSION=pr-${{ github.event.number }}
|
| 27 |
+
fi
|
| 28 |
+
|
| 29 |
+
# Use Docker `latest` tag convention
|
| 30 |
+
[ "$VERSION" == "main" ] && VERSION=latest
|
| 31 |
+
|
| 32 |
+
TAG="${DOCKER_IMAGE}:${VERSION}"
|
| 33 |
+
echo ::set-output name=version::${VERSION}
|
| 34 |
+
echo ::set-output name=tag::${TAG}
|
| 35 |
+
echo ::set-output name=created::$(date -u +'%Y-%m-%dT%H:%M:%SZ')
|
| 36 |
+
|
| 37 |
+
- name: Set up Docker Buildx
|
| 38 |
+
uses: docker/setup-buildx-action@v1
|
| 39 |
+
|
| 40 |
+
- name: Login to DockerHub
|
| 41 |
+
if: github.event_name != 'pull_request'
|
| 42 |
+
uses: docker/login-action@v1
|
| 43 |
+
with:
|
| 44 |
+
username: ${{ secrets.DOCKER_USERNAME }}
|
| 45 |
+
password: ${{ secrets.DOCKER_TOKEN }}
|
| 46 |
+
|
| 47 |
+
- name: Build and push to docker hub
|
| 48 |
+
uses: docker/build-push-action@v2
|
| 49 |
+
with:
|
| 50 |
+
context: .
|
| 51 |
+
file: ./Dockerfile
|
| 52 |
+
push: ${{ github.event_name != 'pull_request' }}
|
| 53 |
+
tags: ${{ steps.prep.outputs.tag }}
|
| 54 |
+
labels: |
|
| 55 |
+
org.opencontainers.image.source=${{ github.event.repository.html_url }}
|
| 56 |
+
org.opencontainers.image.created=${{ steps.prep.outputs.created }}
|
| 57 |
+
org.opencontainers.image.revision=${{ github.sha }}
|
.gitignore
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
| 2 |
+
|
| 3 |
+
# dependencies
|
| 4 |
+
node_modules/
|
| 5 |
+
/.pnp
|
| 6 |
+
.pnp.js
|
| 7 |
+
|
| 8 |
+
# testing
|
| 9 |
+
/coverage
|
| 10 |
+
|
| 11 |
+
# production
|
| 12 |
+
/.next
|
| 13 |
+
/build
|
| 14 |
+
|
| 15 |
+
# misc
|
| 16 |
+
.DS_Store
|
| 17 |
+
.env.local
|
| 18 |
+
.env.development.local
|
| 19 |
+
.env.test.local
|
| 20 |
+
.env.production.local
|
| 21 |
+
|
| 22 |
+
npm-debug.log*
|
| 23 |
+
yarn-debug.log*
|
| 24 |
+
yarn-error.log*
|
| 25 |
+
|
| 26 |
+
.idea
|
| 27 |
+
.iml
|
| 28 |
+
log.txt
|
| 29 |
+
*.iml
|
.prettierignore
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
| 2 |
+
|
| 3 |
+
# dependencies
|
| 4 |
+
/node_modules
|
| 5 |
+
/.next
|
| 6 |
+
|
| 7 |
+
# testing
|
| 8 |
+
/db
|
| 9 |
+
/coverage
|
| 10 |
+
|
| 11 |
+
# production
|
| 12 |
+
/build
|
| 13 |
+
public/sw.js
|
| 14 |
+
public/workbox-*.js
|
| 15 |
+
public/fallback-*.js
|
| 16 |
+
public/*.map
|
| 17 |
+
|
| 18 |
+
# misc
|
| 19 |
+
.DS_Store
|
| 20 |
+
.env.local
|
| 21 |
+
.env.development.local
|
| 22 |
+
.env.test.local
|
| 23 |
+
.env.production.local
|
| 24 |
+
|
| 25 |
+
npm-debug.log*
|
| 26 |
+
yarn-debug.log*
|
| 27 |
+
yarn-error.log*
|
| 28 |
+
|
| 29 |
+
# IntelliJ related
|
| 30 |
+
*.iml
|
| 31 |
+
*.ipr
|
| 32 |
+
*.iws
|
| 33 |
+
.idea/
|
| 34 |
+
|
| 35 |
+
.vscode/
|
| 36 |
+
|
| 37 |
+
# Environments
|
| 38 |
+
.env
|
| 39 |
+
.venv
|
| 40 |
+
env/
|
| 41 |
+
venv/
|
| 42 |
+
ENV/
|
| 43 |
+
env.bak/
|
| 44 |
+
venv.bak/
|
| 45 |
+
next-env.d.ts
|
.prettierrc
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"jsxSingleQuote": true,
|
| 3 |
+
"semi": false,
|
| 4 |
+
"singleQuote": false,
|
| 5 |
+
"trailingComma": "es5",
|
| 6 |
+
"useTabs": false,
|
| 7 |
+
"tabWidth": 2,
|
| 8 |
+
"endOfLine": "lf"
|
| 9 |
+
}
|
Dockerfile
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Install dependencies only when needed
|
| 2 |
+
FROM node:21.0-alpine AS deps
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
|
| 6 |
+
RUN apk add --no-cache libc6-compat
|
| 7 |
+
|
| 8 |
+
COPY package.json yarn.lock ./
|
| 9 |
+
RUN yarn install --frozen-lockfile
|
| 10 |
+
|
| 11 |
+
# Rebuild the source code only when needed
|
| 12 |
+
FROM node:21.0-alpine AS builder
|
| 13 |
+
WORKDIR /app
|
| 14 |
+
|
| 15 |
+
COPY --from=deps /app/node_modules ./node_modules
|
| 16 |
+
COPY . .
|
| 17 |
+
RUN yarn build
|
| 18 |
+
|
| 19 |
+
# Production image, copy all the files and run next
|
| 20 |
+
FROM node:21.0-alpine AS runner
|
| 21 |
+
WORKDIR /app
|
| 22 |
+
|
| 23 |
+
ENV SITE_NAME="Web-SyncPlay"
|
| 24 |
+
ENV PUBLIC_DOMAIN="https://web-syncplay.de"
|
| 25 |
+
ENV REDIS_URL="redis://redis:6379"
|
| 26 |
+
|
| 27 |
+
EXPOSE 3000
|
| 28 |
+
|
| 29 |
+
LABEL org.opencontainers.image.url="https://web-syncplay.de" \
|
| 30 |
+
org.opencontainers.image.description="Watch videos or play music in sync with your friends" \
|
| 31 |
+
org.opencontainers.image.title="Web-SyncPlay" \
|
| 32 |
+
maintainer="Yasamato <https://github.com/Yasamato>"
|
| 33 |
+
|
| 34 |
+
RUN addgroup -g 1001 -S nodejs && \
|
| 35 |
+
adduser -S nextjs -u 1001 && \
|
| 36 |
+
apk add --no-cache curl python3 py3-pip && \
|
| 37 |
+
curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -o /usr/local/bin/yt-dlp && \
|
| 38 |
+
chmod a+rx /usr/local/bin/yt-dlp
|
| 39 |
+
|
| 40 |
+
# You only need to copy next.config.js if you are NOT using the default configuration
|
| 41 |
+
# COPY --from=builder /app/next.config.js ./
|
| 42 |
+
COPY --from=builder /app/public ./public
|
| 43 |
+
COPY --from=builder /app/package.json ./package.json
|
| 44 |
+
|
| 45 |
+
# Automatically leverage output traces to reduce image size
|
| 46 |
+
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
| 47 |
+
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
| 48 |
+
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
| 49 |
+
|
| 50 |
+
USER nextjs
|
| 51 |
+
|
| 52 |
+
EXPOSE 8081
|
| 53 |
+
|
| 54 |
+
ENV PORT 8081
|
| 55 |
+
|
| 56 |
+
# Next.js collects completely anonymous telemetry data about general usage.
|
| 57 |
+
# Learn more here: https://nextjs.org/telemetry
|
| 58 |
+
# Uncomment the following line in case you want to disable telemetry.
|
| 59 |
+
# ENV NEXT_TELEMETRY_DISABLED 1
|
| 60 |
+
|
| 61 |
+
CMD ["sh", "-c", "node server.js"]
|
LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2020 Yasamato (Leo Jung)
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
README.md
CHANGED
|
@@ -1,10 +1,170 @@
|
|
| 1 |
-
--
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
-
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[](https://demo.web-syncplay.de)
|
| 2 |
+
[](https://www.codacy.com/gh/Web-SyncPlay/Web-SyncPlay/dashboard?utm_source=github.com&utm_medium=referral&utm_content=Web-SyncPlay/Web-SyncPlay&utm_campaign=Badge_Grade)
|
| 3 |
+
[](https://www.codefactor.io/repository/github/web-syncplay/web-syncplay)
|
| 4 |
+
[](https://hub.docker.com/r/websyncplay/websyncplay)
|
| 5 |
+
|
| 6 |
+
# Web-SyncPlay
|
| 7 |
+
|
| 8 |
+
Watch videos, listen to music or tune in for a live stream and all that with your friends. Web-SyncPlay is a software
|
| 9 |
+
that lets you synchronise your playback with all your friends with a clean modern Web-UI written
|
| 10 |
+
in [React](https://reactjs.org/) for [Next.js](https://nextjs.org), designed
|
| 11 |
+
using [Tailwind CSS](https://tailwindcss.com/) and build on top
|
| 12 |
+
of [react-player](https://github.com/cookpete/react-player).
|
| 13 |
+
|
| 14 |
+
## Supported formats
|
| 15 |
+
|
| 16 |
+
- YouTube videos
|
| 17 |
+
|
| 18 |
+
- Facebook videos
|
| 19 |
+
|
| 20 |
+
- SoundCloud tracks
|
| 21 |
+
|
| 22 |
+
- Streamable videos
|
| 23 |
+
|
| 24 |
+
- Vimeo videos
|
| 25 |
+
|
| 26 |
+
- Wistia videos
|
| 27 |
+
|
| 28 |
+
- Twitch videos
|
| 29 |
+
|
| 30 |
+
- DailyMotion videos
|
| 31 |
+
|
| 32 |
+
- Vidyard videos
|
| 33 |
+
|
| 34 |
+
- Kaltura videos
|
| 35 |
+
|
| 36 |
+
- Files playable via `<video>` or `<audio>` element as well as:
|
| 37 |
+
|
| 38 |
+
- HLS streams
|
| 39 |
+
- DASH streams
|
| 40 |
+
|
| 41 |
+
- Everything that is extractable via [yt-dlp](https://github.com/yt-dlp/yt-dlp) and allowed
|
| 42 |
+
via [CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS)
|
| 43 |
+
|
| 44 |
+
## Known limitations
|
| 45 |
+
|
| 46 |
+
I would have loved to keep the original Player-UIs for ease of use, but they are usually being embedded as an iframe, as
|
| 47 |
+
such the software would be limited to the included API of the vendors. This is sadly not an option if you want to make
|
| 48 |
+
sure that everyone stays synchronised and there is no feedback loop of commands.
|
| 49 |
+
|
| 50 |
+
## Getting started
|
| 51 |
+
|
| 52 |
+
To run this software on your own hardware, you will need to have [Docker](https://www.docker.com/) or any other
|
| 53 |
+
container engine installed.
|
| 54 |
+
|
| 55 |
+
### docker-compose
|
| 56 |
+
|
| 57 |
+
For ease of use there is an
|
| 58 |
+
example [docker-compose.yml](https://github.com/Web-SyncPlay/Web-SyncPlay/docker-compose.yml) file provided, which you
|
| 59 |
+
can copy and adjust to fit your deployment.
|
| 60 |
+
|
| 61 |
+
Simply create a `.env` file in the same directory and run `docker-compose config` to see if the docker-compose file is
|
| 62 |
+
correctly configured.
|
| 63 |
+
|
| 64 |
+
### docker
|
| 65 |
+
|
| 66 |
+
Otherwise, you could simply run the images separately via the `docker` command:
|
| 67 |
+
|
| 68 |
+
To start the temporary in memory database [redis](https://redis.io):
|
| 69 |
+
|
| 70 |
+
```bash
|
| 71 |
+
docker run -d -p 6379:6379 redis
|
| 72 |
+
```
|
| 73 |
+
|
| 74 |
+
Now run the actual service via:
|
| 75 |
+
|
| 76 |
+
```bash
|
| 77 |
+
docker run -d -p 8081:8081 -e REDIS_URL=redis://your-ip:6379 websyncplay/websyncplay
|
| 78 |
+
```
|
| 79 |
+
|
| 80 |
+
### Manual setup
|
| 81 |
+
|
| 82 |
+
To get started with running the project directly via node, clone the repository via:
|
| 83 |
+
|
| 84 |
+
```bash
|
| 85 |
+
git clone https://github.com/Web-SyncPlay/Web-SyncPlay
|
| 86 |
+
```
|
| 87 |
+
|
| 88 |
+
When you are trying to develop on the project simply run
|
| 89 |
+
|
| 90 |
+
```bash
|
| 91 |
+
yarn dev
|
| 92 |
+
```
|
| 93 |
+
|
| 94 |
+
You can now view the project under `http://localhost:3000` with hot reloads
|
| 95 |
+
|
| 96 |
+
To run an optimized deployment you need to run the following two commands:
|
| 97 |
+
|
| 98 |
+
```bash
|
| 99 |
+
yarn build && yarn start
|
| 100 |
+
```
|
| 101 |
+
|
| 102 |
+
### Environment variables
|
| 103 |
+
|
| 104 |
+
| Parameter | Function | Default |
|
| 105 |
+
| --------------- | ---------------------------------------------- | ---------------------- |
|
| 106 |
+
| `SITE_NAME` | The name of your site | `"The Anime Index"` |
|
| 107 |
+
| `PUBLIC_DOMAIN` | Your domain or IP, remove trailing slash | `"https://piracy.moe"` |
|
| 108 |
+
| `REDIS_URL` | Connection string for the redis cache database | `"redis://redis:6379"` |
|
| 109 |
+
|
| 110 |
+
After deployment open your browser and visit http://localhost:8081 or however you address the server. It is
|
| 111 |
+
**_strongly_** recommended putting a reverse proxy using TLS/SSL in front of this service.
|
| 112 |
+
|
| 113 |
+
## Adding synchronised playback to your website
|
| 114 |
+
|
| 115 |
+
> **Warning**: currently not functional, if you want to keep using it use the v1.0 tag
|
| 116 |
+
|
| 117 |
+
A necessary prerequisite is to make your video files available to this service as e.g. HLS/DASH streams or as a simple
|
| 118 |
+
natively playable file via an endpoint publicly accessible via a URL. Make sure your CORS setting allow content to be
|
| 119 |
+
fetched from this service.
|
| 120 |
+
|
| 121 |
+
Having started the service on one of your servers you can then embed the included embed into your website. You don't
|
| 122 |
+
have to manually update the iframe when playing a playlist, as this is already handled automatically.
|
| 123 |
+
|
| 124 |
+
- `<YOUR_ENDPOINT>`: your endpoint from where this service will be accessible, e.g. `https://sync.example.com`
|
| 125 |
+
|
| 126 |
+
- `<ROOM_ID>`: the room ID in which participants will be kept in sync. It is recommended to handle the generation of new
|
| 127 |
+
ID-string on your side, you don't have to do anything on the server side here, the room will be auto created.
|
| 128 |
+
|
| 129 |
+
- `<YOUR_MEDIA_URL>`: publicly accessible media url, from which you serve your video/audio
|
| 130 |
+
|
| 131 |
+
For playing only a single file the service can be embedded via
|
| 132 |
+
|
| 133 |
+
```html
|
| 134 |
+
<iframe
|
| 135 |
+
allow="fullscreen; autoplay; encrypted-media; picture-in-picture"
|
| 136 |
+
style="border:none;"
|
| 137 |
+
width="100%"
|
| 138 |
+
height="100%"
|
| 139 |
+
src="<YOUR_ENDPOINT>/embed/player/<ROOM_ID>?url=<YOUR_MEDIA_URL>"
|
| 140 |
+
>
|
| 141 |
+
</iframe>
|
| 142 |
+
```
|
| 143 |
+
|
| 144 |
+
If you want to sync playback across a playlist, you need to adjust the embed
|
| 145 |
+
|
| 146 |
+
- `<START_INDEX>`: index of the `queue` array, indicates from which point playback should start
|
| 147 |
+
|
| 148 |
+
- `<ITEM_1>`, `<ITEM_2>` ... `<ITEM_N>`: playlist item, the same as `<YOUR_MEDIA_URL>`, they need to be passed in the
|
| 149 |
+
order you want them to be ordered
|
| 150 |
+
|
| 151 |
+
```html
|
| 152 |
+
<iframe
|
| 153 |
+
allow="fullscreen; autoplay; encrypted-media; picture-in-picture"
|
| 154 |
+
style="border:none;"
|
| 155 |
+
width="100%"
|
| 156 |
+
height="100%"
|
| 157 |
+
src="<YOUR_ENDPOINT>/embed/player/<ROOM_ID>?queueIndex=<START_INDEX>&queue=<ITEM_1>&queue=<ITEM_2>...&queue=<ITEM_N>"
|
| 158 |
+
>
|
| 159 |
+
</iframe>
|
| 160 |
+
```
|
| 161 |
+
|
| 162 |
+
You can already disable the player UI by adding `&controlsHidden=true` to the src link of the embed.
|
| 163 |
+
|
| 164 |
+
It is also possible to disable the syncing handler by adding `&showRootPlayer=true`. This is not recommended as this
|
| 165 |
+
will break the sync-process of the playback.
|
| 166 |
+
|
| 167 |
+
## Future developments
|
| 168 |
+
|
| 169 |
+
It is planned to create an api to communicate with the player and be able to use your own custom player to control
|
| 170 |
+
playback.
|
components/Embed.tsx
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client"
|
| 2 |
+
import { FC, useEffect, useState } from "react"
|
| 3 |
+
import Player from "./player/Player"
|
| 4 |
+
import {
|
| 5 |
+
ClientToServerEvents,
|
| 6 |
+
createClientSocket,
|
| 7 |
+
ServerToClientEvents,
|
| 8 |
+
} from "../lib/socket"
|
| 9 |
+
import { Socket } from "socket.io-client"
|
| 10 |
+
import ConnectingAlert from "./alert/ConnectingAlert"
|
| 11 |
+
|
| 12 |
+
interface Props {
|
| 13 |
+
id: string
|
| 14 |
+
}
|
| 15 |
+
|
| 16 |
+
let connecting = false
|
| 17 |
+
|
| 18 |
+
const Embed: FC<Props> = ({ id }) => {
|
| 19 |
+
const [connected, setConnected] = useState(false)
|
| 20 |
+
const [socket, setSocket] = useState<Socket<
|
| 21 |
+
ServerToClientEvents,
|
| 22 |
+
ClientToServerEvents
|
| 23 |
+
> | null>(null)
|
| 24 |
+
|
| 25 |
+
useEffect(() => {
|
| 26 |
+
fetch("/api/socketio").finally(() => {
|
| 27 |
+
if (socket !== null) {
|
| 28 |
+
setConnected(socket.connected)
|
| 29 |
+
} else {
|
| 30 |
+
const newSocket = createClientSocket(id)
|
| 31 |
+
newSocket.on("connect", () => {
|
| 32 |
+
setConnected(true)
|
| 33 |
+
})
|
| 34 |
+
setSocket(newSocket)
|
| 35 |
+
}
|
| 36 |
+
})
|
| 37 |
+
|
| 38 |
+
return () => {
|
| 39 |
+
if (socket !== null) {
|
| 40 |
+
socket.disconnect()
|
| 41 |
+
}
|
| 42 |
+
}
|
| 43 |
+
}, [id, socket])
|
| 44 |
+
|
| 45 |
+
const connectionCheck = () => {
|
| 46 |
+
if (socket !== null && socket.connected) {
|
| 47 |
+
connecting = false
|
| 48 |
+
setConnected(true)
|
| 49 |
+
return
|
| 50 |
+
}
|
| 51 |
+
setTimeout(connectionCheck, 100)
|
| 52 |
+
}
|
| 53 |
+
|
| 54 |
+
if (!connected || socket === null) {
|
| 55 |
+
if (!connecting) {
|
| 56 |
+
connecting = true
|
| 57 |
+
connectionCheck()
|
| 58 |
+
}
|
| 59 |
+
return (
|
| 60 |
+
<div className={"flex justify-center"}>
|
| 61 |
+
<ConnectingAlert />
|
| 62 |
+
</div>
|
| 63 |
+
)
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
return (
|
| 67 |
+
<Player roomId={id} socket={socket} fullHeight={true}/>
|
| 68 |
+
)
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
export default Embed
|
components/Footer.tsx
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { FC } from "react"
|
| 2 |
+
import IconGithub from "./icon/IconGithub"
|
| 3 |
+
import NewTabLink from "./action/NewTabLink"
|
| 4 |
+
import IconCopyright from "./icon/IconCopyright"
|
| 5 |
+
|
| 6 |
+
interface Props {
|
| 7 |
+
error?: number
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
const Footer: FC<Props> = ({ error }) => {
|
| 11 |
+
return (
|
| 12 |
+
<footer className={"flex flex-col bg-dark-900 py-1 px-4"}>
|
| 13 |
+
{error && <div>Error {error}</div>}
|
| 14 |
+
<div className={"text-sm flex flex-col gap-1 sm:flex-row sm:items-center"}>
|
| 15 |
+
<div className={"flex flex-row items-center"}>
|
| 16 |
+
<IconCopyright sizeClassName={"h-3 w-3"}/>
|
| 17 |
+
<NewTabLink href={"https://github.com/Yasamato"}>Yasamato</NewTabLink>
|
| 18 |
+
2022,
|
| 19 |
+
</div>
|
| 20 |
+
|
| 21 |
+
<div>
|
| 22 |
+
Icons by
|
| 23 |
+
<NewTabLink href={"https://heroicons.com"}>Heroicons</NewTabLink>
|
| 24 |
+
and
|
| 25 |
+
<NewTabLink href={"https://fontawesome.com"}>Font Awesome</NewTabLink>
|
| 26 |
+
</div>
|
| 27 |
+
|
| 28 |
+
<NewTabLink
|
| 29 |
+
className={"ml-auto flex items-center"}
|
| 30 |
+
href={"https://github.com/Web-SyncPlay/Web-SyncPlay"}
|
| 31 |
+
>
|
| 32 |
+
<IconGithub className={"mr-1"} /> Github
|
| 33 |
+
</NewTabLink>
|
| 34 |
+
</div>
|
| 35 |
+
</footer>
|
| 36 |
+
)
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
export default Footer
|
components/Head.tsx
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import NextHead from "next/head"
|
| 2 |
+
import { getSiteDomain, getSiteName } from "../lib/env"
|
| 3 |
+
import { useRouter } from "next/router"
|
| 4 |
+
|
| 5 |
+
export interface MetaProps {
|
| 6 |
+
title?: string
|
| 7 |
+
description?: string
|
| 8 |
+
image?: string
|
| 9 |
+
type?: string
|
| 10 |
+
robots?: string
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
const Head = ({ customMeta }: { customMeta?: MetaProps }) => {
|
| 14 |
+
const router = useRouter()
|
| 15 |
+
|
| 16 |
+
const meta: MetaProps = {
|
| 17 |
+
title: getSiteName(),
|
| 18 |
+
description: "Watch videos or play music in sync with your friends",
|
| 19 |
+
type: "website",
|
| 20 |
+
robots: "noindex, noarchive, follow",
|
| 21 |
+
image: getSiteDomain() + "/apple-touch-icon.png",
|
| 22 |
+
...customMeta,
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
return (
|
| 26 |
+
<NextHead>
|
| 27 |
+
<title>{meta.title}</title>
|
| 28 |
+
<meta property='og:url' content={`${getSiteDomain()}${router.asPath}`} />
|
| 29 |
+
<link rel='canonical' href={`${getSiteDomain()}${router.asPath}`} />
|
| 30 |
+
<meta property='og:type' content='website' />
|
| 31 |
+
<meta property='og:site_name' content={getSiteName()} />
|
| 32 |
+
<meta property='og:description' content={meta.description} />
|
| 33 |
+
<meta property='og:title' content={meta.title} />
|
| 34 |
+
{meta.image && <meta property='og:image' content={meta.image} />}
|
| 35 |
+
<meta name='twitter:card' content='summary' />
|
| 36 |
+
<meta name='twitter:title' content={meta.title} />
|
| 37 |
+
<meta name='twitter:description' content={meta.description} />
|
| 38 |
+
{meta.image && <meta name='twitter:image' content={meta.image} />}
|
| 39 |
+
<meta name={"robots"} content={meta.robots} />
|
| 40 |
+
</NextHead>
|
| 41 |
+
)
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
export default Head
|
components/Layout.tsx
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { FC, ReactNode } from "react"
|
| 2 |
+
import Navbar from "./Navbar"
|
| 3 |
+
import NoScriptAlert from "./alert/NoScriptAlert"
|
| 4 |
+
import Footer from "./Footer"
|
| 5 |
+
import Head, { MetaProps } from "./Head"
|
| 6 |
+
|
| 7 |
+
interface Props {
|
| 8 |
+
meta: MetaProps
|
| 9 |
+
showNavbar?: boolean
|
| 10 |
+
error?: number
|
| 11 |
+
roomId?: string
|
| 12 |
+
children?: ReactNode
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
const Layout: FC<Props> = ({
|
| 16 |
+
meta,
|
| 17 |
+
showNavbar = true,
|
| 18 |
+
error,
|
| 19 |
+
roomId,
|
| 20 |
+
children,
|
| 21 |
+
}) => {
|
| 22 |
+
return (
|
| 23 |
+
<div className={"flex flex-col min-h-screen"}>
|
| 24 |
+
<Head customMeta={meta} />
|
| 25 |
+
{showNavbar && (
|
| 26 |
+
<header>
|
| 27 |
+
<Navbar roomId={roomId} />
|
| 28 |
+
</header>
|
| 29 |
+
)}
|
| 30 |
+
|
| 31 |
+
<noscript>
|
| 32 |
+
<NoScriptAlert />
|
| 33 |
+
</noscript>
|
| 34 |
+
|
| 35 |
+
<main className={"relative flex flex-col grow p-2"}>{children}</main>
|
| 36 |
+
|
| 37 |
+
<Footer error={error} />
|
| 38 |
+
</div>
|
| 39 |
+
)
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
export default Layout
|
components/Navbar.tsx
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import Link from "next/link"
|
| 2 |
+
import Image from "next/image"
|
| 3 |
+
import { getSiteDomain, getSiteName } from "../lib/env"
|
| 4 |
+
import Button from "./action/Button"
|
| 5 |
+
import IconShare from "./icon/IconShare"
|
| 6 |
+
import React, { useState } from "react"
|
| 7 |
+
import Modal from "./modal/Modal"
|
| 8 |
+
import InputClipboardCopy from "./input/InputClipboardCopy"
|
| 9 |
+
import { Tooltip } from "react-tooltip"
|
| 10 |
+
|
| 11 |
+
const Navbar = ({ roomId }: { roomId?: string }) => {
|
| 12 |
+
const [showShare, setShowShare] = useState(false)
|
| 13 |
+
|
| 14 |
+
return (
|
| 15 |
+
<div className={"py-1 px-2 flex flex-row gap-1 items-stretch bg-dark-900"}>
|
| 16 |
+
<Link
|
| 17 |
+
href={"/"}
|
| 18 |
+
className={
|
| 19 |
+
"flex p-1 shrink-0 flex-row gap-1 items-center rounded action"
|
| 20 |
+
}
|
| 21 |
+
>
|
| 22 |
+
<Image
|
| 23 |
+
src={"/logo_white.png"}
|
| 24 |
+
alt={"Web-SyncPlay logo"}
|
| 25 |
+
width={36}
|
| 26 |
+
height={36}
|
| 27 |
+
/>
|
| 28 |
+
<span className={"hide-below-sm"}>{getSiteName()}</span>
|
| 29 |
+
</Link>
|
| 30 |
+
{roomId && (
|
| 31 |
+
<>
|
| 32 |
+
<Modal
|
| 33 |
+
title={"Invite your friends"}
|
| 34 |
+
show={showShare}
|
| 35 |
+
close={() => setShowShare(false)}
|
| 36 |
+
>
|
| 37 |
+
<div>Share this link to let more people join in on the fun</div>
|
| 38 |
+
<InputClipboardCopy
|
| 39 |
+
className={"bg-dark-1000"}
|
| 40 |
+
value={getSiteDomain() + "/room/" + roomId}
|
| 41 |
+
/>
|
| 42 |
+
</Modal>
|
| 43 |
+
<Button
|
| 44 |
+
tooltip={"Share the room link"}
|
| 45 |
+
id={"navbar"}
|
| 46 |
+
actionClasses={"hover:bg-primary-800 active:bg-primary-700"}
|
| 47 |
+
className={"ml-auto p-2 bg-primary-900"}
|
| 48 |
+
onClick={() => setShowShare(true)}
|
| 49 |
+
>
|
| 50 |
+
<div className={"flex items-center mx-1"}>
|
| 51 |
+
<IconShare className={"mr-1"} />
|
| 52 |
+
Share
|
| 53 |
+
</div>
|
| 54 |
+
</Button>
|
| 55 |
+
</>
|
| 56 |
+
)}
|
| 57 |
+
|
| 58 |
+
<Tooltip
|
| 59 |
+
anchorId={"navbar"}
|
| 60 |
+
place={"bottom"}
|
| 61 |
+
style={{
|
| 62 |
+
backgroundColor: "var(--dark-700)",
|
| 63 |
+
}}
|
| 64 |
+
/>
|
| 65 |
+
</div>
|
| 66 |
+
)
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
export default Navbar
|
components/Room.tsx
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client"
|
| 2 |
+
import { FC, useEffect, useState } from "react"
|
| 3 |
+
import Player from "./player/Player"
|
| 4 |
+
import {
|
| 5 |
+
ClientToServerEvents,
|
| 6 |
+
createClientSocket,
|
| 7 |
+
ServerToClientEvents,
|
| 8 |
+
} from "../lib/socket"
|
| 9 |
+
import Button from "./action/Button"
|
| 10 |
+
import { Socket } from "socket.io-client"
|
| 11 |
+
import ConnectingAlert from "./alert/ConnectingAlert"
|
| 12 |
+
import PlaylistMenu from "./playlist/PlaylistMenu"
|
| 13 |
+
import IconLoop from "./icon/IconLoop"
|
| 14 |
+
import InputUrl from "./input/InputUrl"
|
| 15 |
+
import UserList from "./user/UserList"
|
| 16 |
+
|
| 17 |
+
interface Props {
|
| 18 |
+
id: string
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
let connecting = false
|
| 22 |
+
|
| 23 |
+
const Room: FC<Props> = ({ id }) => {
|
| 24 |
+
const [connected, setConnected] = useState(false)
|
| 25 |
+
const [socket, setSocket] = useState<Socket<
|
| 26 |
+
ServerToClientEvents,
|
| 27 |
+
ClientToServerEvents
|
| 28 |
+
> | null>(null)
|
| 29 |
+
const [url, setUrl] = useState("")
|
| 30 |
+
|
| 31 |
+
useEffect(() => {
|
| 32 |
+
fetch("/api/socketio").finally(() => {
|
| 33 |
+
if (socket !== null) {
|
| 34 |
+
setConnected(socket.connected)
|
| 35 |
+
} else {
|
| 36 |
+
const newSocket = createClientSocket(id)
|
| 37 |
+
newSocket.on("connect", () => {
|
| 38 |
+
setConnected(true)
|
| 39 |
+
})
|
| 40 |
+
setSocket(newSocket)
|
| 41 |
+
}
|
| 42 |
+
})
|
| 43 |
+
|
| 44 |
+
return () => {
|
| 45 |
+
if (socket !== null) {
|
| 46 |
+
socket.disconnect()
|
| 47 |
+
}
|
| 48 |
+
}
|
| 49 |
+
}, [id, socket])
|
| 50 |
+
|
| 51 |
+
const connectionCheck = () => {
|
| 52 |
+
if (socket !== null && socket.connected) {
|
| 53 |
+
connecting = false
|
| 54 |
+
setConnected(true)
|
| 55 |
+
return
|
| 56 |
+
}
|
| 57 |
+
setTimeout(connectionCheck, 100)
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
if (!connected || socket === null) {
|
| 61 |
+
if (!connecting) {
|
| 62 |
+
connecting = true
|
| 63 |
+
connectionCheck()
|
| 64 |
+
}
|
| 65 |
+
return (
|
| 66 |
+
<div className={"flex justify-center"}>
|
| 67 |
+
<ConnectingAlert />
|
| 68 |
+
</div>
|
| 69 |
+
)
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
return (
|
| 73 |
+
<div className={"flex flex-col sm:flex-row gap-1"}>
|
| 74 |
+
<div className={"grow"}>
|
| 75 |
+
<Player roomId={id} socket={socket} />
|
| 76 |
+
|
| 77 |
+
<div className={"flex flex-row gap-1 p-1"}>
|
| 78 |
+
<Button
|
| 79 |
+
tooltip={"Do a forced manual sync"}
|
| 80 |
+
className={"p-2 flex flex-row gap-1 items-center"}
|
| 81 |
+
onClick={() => {
|
| 82 |
+
console.log("Fetching update", socket?.id)
|
| 83 |
+
socket?.emit("fetch")
|
| 84 |
+
}}
|
| 85 |
+
>
|
| 86 |
+
<IconLoop className={"hover:animate-spin"} />
|
| 87 |
+
<div className={"hidden-below-sm"}>Manual sync</div>
|
| 88 |
+
</Button>
|
| 89 |
+
<InputUrl
|
| 90 |
+
className={"grow"}
|
| 91 |
+
url={url}
|
| 92 |
+
placeholder={"Play url now"}
|
| 93 |
+
tooltip={"Play given url now"}
|
| 94 |
+
onChange={setUrl}
|
| 95 |
+
onSubmit={() => {
|
| 96 |
+
console.log("Requesting", url, "now")
|
| 97 |
+
socket?.emit("playUrl", url)
|
| 98 |
+
setUrl("")
|
| 99 |
+
}}
|
| 100 |
+
>
|
| 101 |
+
Play
|
| 102 |
+
</InputUrl>
|
| 103 |
+
</div>
|
| 104 |
+
|
| 105 |
+
<UserList socket={socket} />
|
| 106 |
+
</div>
|
| 107 |
+
|
| 108 |
+
<PlaylistMenu socket={socket} />
|
| 109 |
+
</div>
|
| 110 |
+
)
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
export default Room
|
components/action/Button.tsx
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { FC, MouseEventHandler, ReactNode } from "react"
|
| 2 |
+
import classNames from "classnames"
|
| 3 |
+
|
| 4 |
+
interface Props {
|
| 5 |
+
id?: string
|
| 6 |
+
tooltip: string
|
| 7 |
+
onClick?: MouseEventHandler<HTMLButtonElement>
|
| 8 |
+
className?: string
|
| 9 |
+
type?: "button" | "submit" | "reset"
|
| 10 |
+
actionClasses?: string
|
| 11 |
+
disabled?: boolean
|
| 12 |
+
children?: ReactNode
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
const Button: FC<Props> = ({
|
| 16 |
+
id,
|
| 17 |
+
tooltip,
|
| 18 |
+
onClick,
|
| 19 |
+
className = "",
|
| 20 |
+
type = "button",
|
| 21 |
+
actionClasses = "action",
|
| 22 |
+
disabled = false,
|
| 23 |
+
children,
|
| 24 |
+
}) => {
|
| 25 |
+
return (
|
| 26 |
+
<button
|
| 27 |
+
id={id}
|
| 28 |
+
data-tooltip-content={tooltip}
|
| 29 |
+
data-tooltip-variant={"dark"}
|
| 30 |
+
onClick={onClick}
|
| 31 |
+
type={type}
|
| 32 |
+
disabled={disabled}
|
| 33 |
+
className={classNames("p-2 rounded", actionClasses, className)}
|
| 34 |
+
>
|
| 35 |
+
{children}
|
| 36 |
+
</button>
|
| 37 |
+
)
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
export default Button
|
components/action/DeleteButton.tsx
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { FC } from "react"
|
| 2 |
+
import ControlButton from "../input/ControlButton"
|
| 3 |
+
import IconDelete from "../icon/IconDelete"
|
| 4 |
+
|
| 5 |
+
interface Props {
|
| 6 |
+
tooltip: string
|
| 7 |
+
onClick: () => void
|
| 8 |
+
className?: string
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
const DeleteButton: FC<Props> = ({ tooltip, onClick }) => {
|
| 12 |
+
return (
|
| 13 |
+
<ControlButton
|
| 14 |
+
className={"transition-colors text-red-600 hover:text-red-500"}
|
| 15 |
+
onClick={onClick}
|
| 16 |
+
interaction={() => {}}
|
| 17 |
+
tooltip={tooltip}
|
| 18 |
+
>
|
| 19 |
+
<IconDelete />
|
| 20 |
+
</ControlButton>
|
| 21 |
+
)
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
export default DeleteButton
|
components/action/DropUp.tsx
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { FC, ReactNode, useEffect, useState } from "react"
|
| 2 |
+
import ControlButton from "../input/ControlButton"
|
| 3 |
+
import classNames from "classnames"
|
| 4 |
+
|
| 5 |
+
interface Props {
|
| 6 |
+
tooltip: string
|
| 7 |
+
open?: boolean
|
| 8 |
+
className?: string
|
| 9 |
+
menuChange?: (open: boolean) => void
|
| 10 |
+
interaction: (touch: boolean) => void
|
| 11 |
+
buttonContent: ReactNode
|
| 12 |
+
children?: ReactNode
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
const DropUp: FC<Props> = ({
|
| 16 |
+
tooltip,
|
| 17 |
+
open,
|
| 18 |
+
className,
|
| 19 |
+
menuChange,
|
| 20 |
+
interaction,
|
| 21 |
+
buttonContent,
|
| 22 |
+
children,
|
| 23 |
+
}) => {
|
| 24 |
+
const [menuOpen, setMenuOpen] = useState(false)
|
| 25 |
+
|
| 26 |
+
useEffect(() => {
|
| 27 |
+
if (typeof open !== "boolean") return
|
| 28 |
+
|
| 29 |
+
if (menuOpen !== open) {
|
| 30 |
+
setMenuOpen(open)
|
| 31 |
+
if (menuChange) {
|
| 32 |
+
menuChange(open)
|
| 33 |
+
}
|
| 34 |
+
}
|
| 35 |
+
}, [open, menuChange, menuOpen])
|
| 36 |
+
|
| 37 |
+
return (
|
| 38 |
+
<div className={"relative"}>
|
| 39 |
+
{menuOpen && (
|
| 40 |
+
<div
|
| 41 |
+
className={classNames(
|
| 42 |
+
"absolute bottom-[60px] rounded bg-dark-900",
|
| 43 |
+
"transition-height transition-width",
|
| 44 |
+
className
|
| 45 |
+
)}
|
| 46 |
+
>
|
| 47 |
+
{children}
|
| 48 |
+
</div>
|
| 49 |
+
)}
|
| 50 |
+
<ControlButton
|
| 51 |
+
tooltip={(menuOpen ? "Close " : "Open ") + tooltip}
|
| 52 |
+
className={menuOpen ? "bg-dark-800" : ""}
|
| 53 |
+
onClick={() => {
|
| 54 |
+
if (menuChange) {
|
| 55 |
+
menuChange(!menuOpen)
|
| 56 |
+
}
|
| 57 |
+
setMenuOpen(!menuOpen)
|
| 58 |
+
}}
|
| 59 |
+
interaction={interaction}
|
| 60 |
+
>
|
| 61 |
+
{buttonContent}
|
| 62 |
+
</ControlButton>
|
| 63 |
+
</div>
|
| 64 |
+
)
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
export default DropUp
|
components/action/InteractionHandler.tsx
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { FC, ReactNode, useState } from "react"
|
| 2 |
+
|
| 3 |
+
export const LEFT_MOUSE_CLICK = 0
|
| 4 |
+
|
| 5 |
+
interface Props {
|
| 6 |
+
tooltip?: string
|
| 7 |
+
className?: string
|
| 8 |
+
prevent?: boolean
|
| 9 |
+
onClick?: (
|
| 10 |
+
e: React.TouchEvent<HTMLDivElement> | React.MouseEvent<HTMLDivElement>,
|
| 11 |
+
touch: boolean
|
| 12 |
+
) => void
|
| 13 |
+
onMove?: (
|
| 14 |
+
e: React.TouchEvent<HTMLDivElement> | React.MouseEvent<HTMLDivElement>,
|
| 15 |
+
touch: boolean
|
| 16 |
+
) => void
|
| 17 |
+
tabIndex?: number
|
| 18 |
+
onKey?: (key: string) => void
|
| 19 |
+
children?: ReactNode
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
const InteractionHandler: FC<Props> = ({
|
| 23 |
+
tooltip,
|
| 24 |
+
className,
|
| 25 |
+
prevent = true,
|
| 26 |
+
onClick,
|
| 27 |
+
onMove,
|
| 28 |
+
onKey,
|
| 29 |
+
tabIndex,
|
| 30 |
+
children,
|
| 31 |
+
}) => {
|
| 32 |
+
const [touched, setTouched] = useState(false)
|
| 33 |
+
const [touchedTime, setTouchedTime] = useState(0)
|
| 34 |
+
|
| 35 |
+
const touch = () => {
|
| 36 |
+
setTouched(true)
|
| 37 |
+
setTouchedTime(new Date().getTime())
|
| 38 |
+
|
| 39 |
+
setTimeout(() => {
|
| 40 |
+
if (new Date().getTime() - touchedTime > 150) {
|
| 41 |
+
setTouched(false)
|
| 42 |
+
}
|
| 43 |
+
}, 200)
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
return (
|
| 47 |
+
<div
|
| 48 |
+
data-tooltip-content={tooltip}
|
| 49 |
+
className={className}
|
| 50 |
+
tabIndex={tabIndex}
|
| 51 |
+
onTouchStart={(e) => {
|
| 52 |
+
touch()
|
| 53 |
+
if (onClick) {
|
| 54 |
+
if (prevent) {
|
| 55 |
+
console.log("Prevent default touch start")
|
| 56 |
+
e.preventDefault()
|
| 57 |
+
e.stopPropagation()
|
| 58 |
+
}
|
| 59 |
+
}
|
| 60 |
+
}}
|
| 61 |
+
onTouchEnd={(e) => {
|
| 62 |
+
touch()
|
| 63 |
+
if (onClick) {
|
| 64 |
+
if (prevent) {
|
| 65 |
+
console.log("Prevent default touch end")
|
| 66 |
+
e.preventDefault()
|
| 67 |
+
e.stopPropagation()
|
| 68 |
+
}
|
| 69 |
+
onClick(e, true)
|
| 70 |
+
}
|
| 71 |
+
}}
|
| 72 |
+
onTouchMove={(e) => {
|
| 73 |
+
if (onMove) {
|
| 74 |
+
onMove(e, true)
|
| 75 |
+
}
|
| 76 |
+
}}
|
| 77 |
+
onMouseDown={(_) => {
|
| 78 |
+
// ignored
|
| 79 |
+
}}
|
| 80 |
+
onMouseUp={(e) => {
|
| 81 |
+
if (e.button !== LEFT_MOUSE_CLICK || touched) {
|
| 82 |
+
return
|
| 83 |
+
}
|
| 84 |
+
if (onClick) {
|
| 85 |
+
onClick(e, false)
|
| 86 |
+
}
|
| 87 |
+
}}
|
| 88 |
+
onMouseMove={(e) => {
|
| 89 |
+
if (onMove) {
|
| 90 |
+
onMove(e, false)
|
| 91 |
+
}
|
| 92 |
+
}}
|
| 93 |
+
onKeyDownCapture={(e) => {
|
| 94 |
+
if (onKey) {
|
| 95 |
+
onKey(e.key)
|
| 96 |
+
}
|
| 97 |
+
}}
|
| 98 |
+
>
|
| 99 |
+
{children}
|
| 100 |
+
</div>
|
| 101 |
+
)
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
export default InteractionHandler
|
components/action/NewTabLink.tsx
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { FC, ReactNode } from "react"
|
| 2 |
+
import classNames from "classnames"
|
| 3 |
+
|
| 4 |
+
interface Props {
|
| 5 |
+
href: string
|
| 6 |
+
className?: string
|
| 7 |
+
children?: ReactNode
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
const NewTabLink: FC<Props> = ({ href, children, className }) => {
|
| 11 |
+
return (
|
| 12 |
+
<a
|
| 13 |
+
href={href}
|
| 14 |
+
className={classNames(
|
| 15 |
+
"mx-1 transition-colors hover:text-primary-900",
|
| 16 |
+
className
|
| 17 |
+
)}
|
| 18 |
+
target={"_blank"}
|
| 19 |
+
rel={"noreferrer"}
|
| 20 |
+
>
|
| 21 |
+
{children}
|
| 22 |
+
</a>
|
| 23 |
+
)
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
export default NewTabLink
|
components/alert/Alert.tsx
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { FC, ReactNode, useState } from "react"
|
| 2 |
+
import IconClose from "../icon/IconClose"
|
| 3 |
+
import Button from "../action/Button"
|
| 4 |
+
import classNames from "classnames"
|
| 5 |
+
|
| 6 |
+
export interface AlertProps {
|
| 7 |
+
canClose?: boolean
|
| 8 |
+
className?: string
|
| 9 |
+
children?: ReactNode
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
const Alert: FC<AlertProps> = ({
|
| 13 |
+
canClose = true,
|
| 14 |
+
className = "",
|
| 15 |
+
children,
|
| 16 |
+
}) => {
|
| 17 |
+
const [closed, setClosed] = useState(false)
|
| 18 |
+
if (closed) {
|
| 19 |
+
return <></>
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
return (
|
| 23 |
+
<div
|
| 24 |
+
className={classNames(
|
| 25 |
+
"rounded bg-dark-800 p-2 flex gap-1 items-center flex-row justify-between",
|
| 26 |
+
className
|
| 27 |
+
)}
|
| 28 |
+
>
|
| 29 |
+
<div className={"flex flex-row gap-1 items-center"}>{children}</div>
|
| 30 |
+
{canClose && (
|
| 31 |
+
<Button tooltip={"Dismiss"} onClick={() => setClosed(true)}>
|
| 32 |
+
<IconClose />
|
| 33 |
+
</Button>
|
| 34 |
+
)}
|
| 35 |
+
</div>
|
| 36 |
+
)
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
export default Alert
|
components/alert/AutoplayAlert.tsx
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { FC } from "react"
|
| 2 |
+
import Alert from "./Alert"
|
| 3 |
+
import Button from "../action/Button"
|
| 4 |
+
import IconSoundMute from "../icon/IconSoundMute"
|
| 5 |
+
|
| 6 |
+
interface Props {
|
| 7 |
+
onClick: () => void
|
| 8 |
+
}
|
| 9 |
+
|
| 10 |
+
const AutoplayAlert: FC<Props> = ({ onClick }) => {
|
| 11 |
+
return (
|
| 12 |
+
<Alert className={"rounded opacity-90"}>
|
| 13 |
+
Sound has been muted for autoplay
|
| 14 |
+
<Button className={"p-2 mr-4"} onClick={onClick} tooltip={"Unmute"}>
|
| 15 |
+
<IconSoundMute />
|
| 16 |
+
</Button>
|
| 17 |
+
</Alert>
|
| 18 |
+
)
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
export default AutoplayAlert
|
components/alert/BufferAlert.tsx
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { FC } from "react"
|
| 2 |
+
import Alert, { AlertProps } from "./Alert"
|
| 3 |
+
import styles from "./Loading.module.css"
|
| 4 |
+
import classNames from "classnames"
|
| 5 |
+
|
| 6 |
+
const BufferAlert: FC<AlertProps> = ({ className, canClose }) => {
|
| 7 |
+
return (
|
| 8 |
+
<Alert
|
| 9 |
+
className={classNames("cursor-progress", className)}
|
| 10 |
+
canClose={canClose}
|
| 11 |
+
>
|
| 12 |
+
<div className={styles.loading}>Buffering ...</div>
|
| 13 |
+
</Alert>
|
| 14 |
+
)
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
export default BufferAlert
|
components/alert/ConnectingAlert.tsx
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { FC } from "react"
|
| 2 |
+
import IconLoading from "../icon/IconLoading"
|
| 3 |
+
import Alert, { AlertProps } from "./Alert"
|
| 4 |
+
import styles from "./Loading.module.css"
|
| 5 |
+
import classNames from "classnames"
|
| 6 |
+
|
| 7 |
+
const ConnectingAlert: FC<AlertProps> = ({
|
| 8 |
+
className = "",
|
| 9 |
+
canClose = false,
|
| 10 |
+
}) => {
|
| 11 |
+
return (
|
| 12 |
+
<Alert canClose={canClose} className={classNames("cursor-wait", className)}>
|
| 13 |
+
<IconLoading className={"hide-below-sm animate-spin"} />
|
| 14 |
+
<div className={styles.loading}>Connecting ...</div>
|
| 15 |
+
</Alert>
|
| 16 |
+
)
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
export default ConnectingAlert
|
components/alert/Loading.module.css
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.loading {
|
| 2 |
+
font-family: monospace;
|
| 3 |
+
display: inline-block;
|
| 4 |
+
clip-path: inset(0 3ch 0 0);
|
| 5 |
+
animation: l 1s steps(4) infinite;
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
@keyframes l {
|
| 9 |
+
to {
|
| 10 |
+
clip-path: inset(0 -1ch 0 0);
|
| 11 |
+
}
|
| 12 |
+
}
|
components/alert/NoScriptAlert.tsx
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { FC } from "react"
|
| 2 |
+
import Alert, { AlertProps } from "./Alert"
|
| 3 |
+
|
| 4 |
+
const NoScriptAlert: FC<AlertProps> = ({ className = "", canClose = true }) => {
|
| 5 |
+
return (
|
| 6 |
+
<Alert className={className} canClose={canClose}>
|
| 7 |
+
Well... it seems like you disabled javascript.
|
| 8 |
+
</Alert>
|
| 9 |
+
)
|
| 10 |
+
}
|
| 11 |
+
|
| 12 |
+
export default NoScriptAlert
|
components/icon/Icon.tsx
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { FC, ReactNode } from "react"
|
| 2 |
+
import classNames from "classnames"
|
| 3 |
+
|
| 4 |
+
export interface IconProps {
|
| 5 |
+
sizeClassName?: string
|
| 6 |
+
className?: string
|
| 7 |
+
viewBox?: string
|
| 8 |
+
children?: ReactNode
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
const Icon: FC<IconProps> = ({
|
| 12 |
+
sizeClassName = "h-5 w-5",
|
| 13 |
+
viewBox = "0 0 24 24",
|
| 14 |
+
className = "",
|
| 15 |
+
children,
|
| 16 |
+
}) => {
|
| 17 |
+
return (
|
| 18 |
+
<svg
|
| 19 |
+
xmlns='http://www.w3.org/2000/svg'
|
| 20 |
+
className={classNames(sizeClassName, className)}
|
| 21 |
+
fill='none'
|
| 22 |
+
viewBox={viewBox}
|
| 23 |
+
stroke='currentColor'
|
| 24 |
+
>
|
| 25 |
+
{children}
|
| 26 |
+
</svg>
|
| 27 |
+
)
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
export default Icon
|
components/icon/IconBackward.tsx
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { FC } from "react"
|
| 2 |
+
import Icon, { IconProps } from "./Icon"
|
| 3 |
+
|
| 4 |
+
const IconBackward: FC<IconProps> = ({ className = "" }) => {
|
| 5 |
+
return (
|
| 6 |
+
<Icon className={className} viewBox='0 0 448 512'>
|
| 7 |
+
<path
|
| 8 |
+
fill='currentColor'
|
| 9 |
+
d='M64 468V44c0-6.6 5.4-12 12-12h48c6.6 0 12 5.4 12 12v176.4l195.5-181C352.1 22.3 384 36.6 384 64v384c0 27.4-31.9 41.7-52.5 24.6L136 292.7V468c0 6.6-5.4 12-12 12H76c-6.6 0-12-5.4-12-12z'
|
| 10 |
+
/>
|
| 11 |
+
</Icon>
|
| 12 |
+
)
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
export default IconBackward
|
components/icon/IconBigPause.tsx
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { FC } from "react"
|
| 2 |
+
import Icon, { IconProps } from "./Icon"
|
| 3 |
+
|
| 4 |
+
const IconBigPause: FC<IconProps> = ({ className = "" }) => {
|
| 5 |
+
return (
|
| 6 |
+
<Icon className={className} sizeClassName={"h-10 w-10"}>
|
| 7 |
+
<path
|
| 8 |
+
strokeLinecap='round'
|
| 9 |
+
strokeLinejoin='round'
|
| 10 |
+
strokeWidth={2}
|
| 11 |
+
d='M10 9v6m4-6v6m7-3a9 9 0 11-18 0 9 9 0 0118 0z'
|
| 12 |
+
/>
|
| 13 |
+
</Icon>
|
| 14 |
+
)
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
export default IconBigPause
|
components/icon/IconBigPlay.tsx
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { FC } from "react"
|
| 2 |
+
import Icon, { IconProps } from "./Icon"
|
| 3 |
+
|
| 4 |
+
const IconBigPlay: FC<IconProps> = ({ className = "" }) => {
|
| 5 |
+
return (
|
| 6 |
+
<Icon className={className} sizeClassName={"h-10 w-10"}>
|
| 7 |
+
<path
|
| 8 |
+
strokeLinecap='round'
|
| 9 |
+
strokeLinejoin='round'
|
| 10 |
+
strokeWidth={2}
|
| 11 |
+
d='M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z'
|
| 12 |
+
/>
|
| 13 |
+
<path
|
| 14 |
+
strokeLinecap='round'
|
| 15 |
+
strokeLinejoin='round'
|
| 16 |
+
strokeWidth={2}
|
| 17 |
+
d='M21 12a9 9 0 11-18 0 9 9 0 0118 0z'
|
| 18 |
+
/>
|
| 19 |
+
</Icon>
|
| 20 |
+
)
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
export default IconBigPlay
|
components/icon/IconCC.tsx
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { FC } from "react"
|
| 2 |
+
import Icon, { IconProps } from "./Icon"
|
| 3 |
+
|
| 4 |
+
const IconCC: FC<IconProps> = ({ className = "" }) => {
|
| 5 |
+
return (
|
| 6 |
+
<Icon className={className} viewBox='0 0 512 512'>
|
| 7 |
+
<path
|
| 8 |
+
fill='currentColor'
|
| 9 |
+
d='M464 64H48C21.5 64 0 85.5 0 112v288c0 26.5 21.5 48 48 48h416c26.5 0 48-21.5 48-48V112c0-26.5-21.5-48-48-48zm-6 336H54c-3.3 0-6-2.7-6-6V118c0-3.3 2.7-6 6-6h404c3.3 0 6 2.7 6 6v276c0 3.3-2.7 6-6 6zm-211.1-85.7c1.7 2.4 1.5 5.6-.5 7.7-53.6 56.8-172.8 32.1-172.8-67.9 0-97.3 121.7-119.5 172.5-70.1 2.1 2 2.5 3.2 1 5.7l-17.5 30.5c-1.9 3.1-6.2 4-9.1 1.7-40.8-32-94.6-14.9-94.6 31.2 0 48 51 70.5 92.2 32.6 2.8-2.5 7.1-2.1 9.2.9l19.6 27.7zm190.4 0c1.7 2.4 1.5 5.6-.5 7.7-53.6 56.9-172.8 32.1-172.8-67.9 0-97.3 121.7-119.5 172.5-70.1 2.1 2 2.5 3.2 1 5.7L420 220.2c-1.9 3.1-6.2 4-9.1 1.7-40.8-32-94.6-14.9-94.6 31.2 0 48 51 70.5 92.2 32.6 2.8-2.5 7.1-2.1 9.2.9l19.6 27.7z'
|
| 10 |
+
/>
|
| 11 |
+
</Icon>
|
| 12 |
+
)
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
export default IconCC
|
components/icon/IconChevron.tsx
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { FC } from "react"
|
| 2 |
+
import Icon, { IconProps } from "./Icon"
|
| 3 |
+
|
| 4 |
+
interface Props extends IconProps {
|
| 5 |
+
direction: "up" | "left" | "right" | "down"
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
const IconChevron: FC<Props> = ({ className = "", direction }) => {
|
| 9 |
+
if (direction === "up") {
|
| 10 |
+
return (
|
| 11 |
+
<Icon
|
| 12 |
+
sizeClassName={"h-6 w-6"}
|
| 13 |
+
className={className}
|
| 14 |
+
viewBox='0 0 448 512'
|
| 15 |
+
>
|
| 16 |
+
<path
|
| 17 |
+
fill='currentColor'
|
| 18 |
+
d='M240.971 130.524l194.343 194.343c9.373 9.373 9.373 24.569 0 33.941l-22.667 22.667c-9.357 9.357-24.522 9.375-33.901.04L224 227.495 69.255 381.516c-9.379 9.335-24.544 9.317-33.901-.04l-22.667-22.667c-9.373-9.373-9.373-24.569 0-33.941L207.03 130.525c9.372-9.373 24.568-9.373 33.941-.001z'
|
| 19 |
+
/>
|
| 20 |
+
</Icon>
|
| 21 |
+
)
|
| 22 |
+
} else if (direction === "left") {
|
| 23 |
+
return (
|
| 24 |
+
<Icon
|
| 25 |
+
sizeClassName={"h-6 w-6"}
|
| 26 |
+
className={className}
|
| 27 |
+
viewBox='0 0 320 512'
|
| 28 |
+
>
|
| 29 |
+
<path
|
| 30 |
+
fill='currentColor'
|
| 31 |
+
d='M34.52 239.03L228.87 44.69c9.37-9.37 24.57-9.37 33.94 0l22.67 22.67c9.36 9.36 9.37 24.52.04 33.9L131.49 256l154.02 154.75c9.34 9.38 9.32 24.54-.04 33.9l-22.67 22.67c-9.37 9.37-24.57 9.37-33.94 0L34.52 272.97c-9.37-9.37-9.37-24.57 0-33.94z'
|
| 32 |
+
/>
|
| 33 |
+
</Icon>
|
| 34 |
+
)
|
| 35 |
+
} else if (direction === "right") {
|
| 36 |
+
return (
|
| 37 |
+
<Icon
|
| 38 |
+
sizeClassName={"h-6 w-6"}
|
| 39 |
+
className={className}
|
| 40 |
+
viewBox='0 0 320 512'
|
| 41 |
+
>
|
| 42 |
+
<path
|
| 43 |
+
fill='currentColor'
|
| 44 |
+
d='M285.476 272.971L91.132 467.314c-9.373 9.373-24.569 9.373-33.941 0l-22.667-22.667c-9.357-9.357-9.375-24.522-.04-33.901L188.505 256 34.484 101.255c-9.335-9.379-9.317-24.544.04-33.901l22.667-22.667c9.373-9.373 24.569-9.373 33.941 0L285.475 239.03c9.373 9.372 9.373 24.568.001 33.941z'
|
| 45 |
+
/>
|
| 46 |
+
</Icon>
|
| 47 |
+
)
|
| 48 |
+
}
|
| 49 |
+
return (
|
| 50 |
+
<Icon sizeClassName={"h-6 w-6"} className={className} viewBox='0 0 448 512'>
|
| 51 |
+
<path
|
| 52 |
+
fill='currentColor'
|
| 53 |
+
d='M207.029 381.476L12.686 187.132c-9.373-9.373-9.373-24.569 0-33.941l22.667-22.667c9.357-9.357 24.522-9.375 33.901-.04L224 284.505l154.745-154.021c9.379-9.335 24.544-9.317 33.901.04l22.667 22.667c9.373 9.373 9.373 24.569 0 33.941L240.971 381.476c-9.373 9.372-24.569 9.372-33.942 0z'
|
| 54 |
+
/>
|
| 55 |
+
</Icon>
|
| 56 |
+
)
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
export default IconChevron
|
components/icon/IconClipboard.tsx
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { FC } from "react"
|
| 2 |
+
import Icon, { IconProps } from "./Icon"
|
| 3 |
+
|
| 4 |
+
const IconClipboard: FC<IconProps> = ({ className = "" }) => {
|
| 5 |
+
return (
|
| 6 |
+
<Icon className={className} sizeClassName={"h-6 w-6"}>
|
| 7 |
+
<path
|
| 8 |
+
strokeLinecap='round'
|
| 9 |
+
strokeLinejoin='round'
|
| 10 |
+
strokeWidth={2}
|
| 11 |
+
d='M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3'
|
| 12 |
+
/>
|
| 13 |
+
</Icon>
|
| 14 |
+
)
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
export default IconClipboard
|
components/icon/IconClose.tsx
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { FC } from "react"
|
| 2 |
+
import Icon, { IconProps } from "./Icon"
|
| 3 |
+
|
| 4 |
+
const IconClose: FC<IconProps> = ({ className = "" }) => {
|
| 5 |
+
return (
|
| 6 |
+
<Icon className={className} sizeClassName={"h-6 w-6"}>
|
| 7 |
+
<path
|
| 8 |
+
strokeLinecap='round'
|
| 9 |
+
strokeLinejoin='round'
|
| 10 |
+
strokeWidth={2}
|
| 11 |
+
d='M6 18L18 6M6 6l12 12'
|
| 12 |
+
/>
|
| 13 |
+
</Icon>
|
| 14 |
+
)
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
export default IconClose
|
components/icon/IconCog.tsx
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { FC } from "react"
|
| 2 |
+
import Icon, { IconProps } from "./Icon"
|
| 3 |
+
|
| 4 |
+
const IconCog: FC<IconProps> = ({ className = "" }) => {
|
| 5 |
+
return (
|
| 6 |
+
<Icon className={className} viewBox='0 0 512 512'>
|
| 7 |
+
<path
|
| 8 |
+
fill='currentColor'
|
| 9 |
+
d='M487.4 315.7l-42.6-24.6c4.3-23.2 4.3-47 0-70.2l42.6-24.6c4.9-2.8 7.1-8.6 5.5-14-11.1-35.6-30-67.8-54.7-94.6-3.8-4.1-10-5.1-14.8-2.3L380.8 110c-17.9-15.4-38.5-27.3-60.8-35.1V25.8c0-5.6-3.9-10.5-9.4-11.7-36.7-8.2-74.3-7.8-109.2 0-5.5 1.2-9.4 6.1-9.4 11.7V75c-22.2 7.9-42.8 19.8-60.8 35.1L88.7 85.5c-4.9-2.8-11-1.9-14.8 2.3-24.7 26.7-43.6 58.9-54.7 94.6-1.7 5.4.6 11.2 5.5 14L67.3 221c-4.3 23.2-4.3 47 0 70.2l-42.6 24.6c-4.9 2.8-7.1 8.6-5.5 14 11.1 35.6 30 67.8 54.7 94.6 3.8 4.1 10 5.1 14.8 2.3l42.6-24.6c17.9 15.4 38.5 27.3 60.8 35.1v49.2c0 5.6 3.9 10.5 9.4 11.7 36.7 8.2 74.3 7.8 109.2 0 5.5-1.2 9.4-6.1 9.4-11.7v-49.2c22.2-7.9 42.8-19.8 60.8-35.1l42.6 24.6c4.9 2.8 11 1.9 14.8-2.3 24.7-26.7 43.6-58.9 54.7-94.6 1.5-5.5-.7-11.3-5.6-14.1zM256 336c-44.1 0-80-35.9-80-80s35.9-80 80-80 80 35.9 80 80-35.9 80-80 80z'
|
| 10 |
+
/>
|
| 11 |
+
</Icon>
|
| 12 |
+
)
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
export default IconCog
|
components/icon/IconCompress.tsx
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { FC } from "react"
|
| 2 |
+
import Icon, { IconProps } from "./Icon"
|
| 3 |
+
|
| 4 |
+
const IconCompress: FC<IconProps> = ({ className = "" }) => {
|
| 5 |
+
return (
|
| 6 |
+
<Icon className={className} viewBox='0 0 448 512'>
|
| 7 |
+
<path
|
| 8 |
+
fill='currentColor'
|
| 9 |
+
d='M436 192H312c-13.3 0-24-10.7-24-24V44c0-6.6 5.4-12 12-12h40c6.6 0 12 5.4 12 12v84h84c6.6 0 12 5.4 12 12v40c0 6.6-5.4 12-12 12zm-276-24V44c0-6.6-5.4-12-12-12h-40c-6.6 0-12 5.4-12 12v84H12c-6.6 0-12 5.4-12 12v40c0 6.6 5.4 12 12 12h124c13.3 0 24-10.7 24-24zm0 300V344c0-13.3-10.7-24-24-24H12c-6.6 0-12 5.4-12 12v40c0 6.6 5.4 12 12 12h84v84c0 6.6 5.4 12 12 12h40c6.6 0 12-5.4 12-12zm192 0v-84h84c6.6 0 12-5.4 12-12v-40c0-6.6-5.4-12-12-12H312c-13.3 0-24 10.7-24 24v124c0 6.6 5.4 12 12 12h40c6.6 0 12-5.4 12-12z'
|
| 10 |
+
/>
|
| 11 |
+
</Icon>
|
| 12 |
+
)
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
export default IconCompress
|
components/icon/IconCopyright.tsx
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { FC } from "react"
|
| 2 |
+
import Icon, { IconProps } from "./Icon"
|
| 3 |
+
|
| 4 |
+
const IconCopyright: FC<IconProps> = ({ className = "", sizeClassName= "h-4 w-4" }) => {
|
| 5 |
+
return (
|
| 6 |
+
<Icon
|
| 7 |
+
className={className}
|
| 8 |
+
sizeClassName={sizeClassName}
|
| 9 |
+
viewBox={"0 0 512 512"}
|
| 10 |
+
>
|
| 11 |
+
<path
|
| 12 |
+
fill='currentColor'
|
| 13 |
+
d='M256 8C119.033 8 8 119.033 8 256s111.033 248 248 248 248-111.033 248-248S392.967 8 256 8zm0 448c-110.532 0-200-89.451-200-200 0-110.531 89.451-200 200-200 110.532 0 200 89.451 200 200 0 110.532-89.451 200-200 200zm107.351-101.064c-9.614 9.712-45.53 41.396-104.065 41.396-82.43 0-140.484-61.425-140.484-141.567 0-79.152 60.275-139.401 139.762-139.401 55.531 0 88.738 26.62 97.593 34.779a11.965 11.965 0 0 1 1.936 15.322l-18.155 28.113c-3.841 5.95-11.966 7.282-17.499 2.921-8.595-6.776-31.814-22.538-61.708-22.538-48.303 0-77.916 35.33-77.916 80.082 0 41.589 26.888 83.692 78.277 83.692 32.657 0 56.843-19.039 65.726-27.225 5.27-4.857 13.596-4.039 17.82 1.738l19.865 27.17a11.947 11.947 0 0 1-1.152 15.518z'
|
| 14 |
+
/>
|
| 15 |
+
</Icon>
|
| 16 |
+
)
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
export default IconCopyright
|
components/icon/IconDelete.tsx
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { FC } from "react"
|
| 2 |
+
import Icon, { IconProps } from "./Icon"
|
| 3 |
+
|
| 4 |
+
const IconDelete: FC<IconProps> = ({ className = "" }) => {
|
| 5 |
+
return (
|
| 6 |
+
<Icon className={className} viewBox='0 0 448 512'>
|
| 7 |
+
<path
|
| 8 |
+
fill='currentColor'
|
| 9 |
+
d='M432 32H312l-9.4-18.7A24 24 0 0 0 281.1 0H166.8a23.72 23.72 0 0 0-21.4 13.3L136 32H16A16 16 0 0 0 0 48v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16V48a16 16 0 0 0-16-16zM53.2 467a48 48 0 0 0 47.9 45h245.8a48 48 0 0 0 47.9-45L416 128H32z'
|
| 10 |
+
/>
|
| 11 |
+
</Icon>
|
| 12 |
+
)
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
export default IconDelete
|
components/icon/IconDisk.tsx
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { FC } from "react"
|
| 2 |
+
import Icon, { IconProps } from "./Icon"
|
| 3 |
+
|
| 4 |
+
const IconDisk: FC<IconProps> = ({ className = "" }) => {
|
| 5 |
+
return (
|
| 6 |
+
<Icon
|
| 7 |
+
className={className}
|
| 8 |
+
sizeClassName={"h-6 w-6"}
|
| 9 |
+
viewBox={"0 0 496 512"}
|
| 10 |
+
>
|
| 11 |
+
<path
|
| 12 |
+
fill='currentColor'
|
| 13 |
+
d='M248 8C111 8 0 119 0 256s111 248 248 248 248-111 248-248S385 8 248 8zM88 256H56c0-105.9 86.1-192 192-192v32c-88.2 0-160 71.8-160 160zm160 96c-53 0-96-43-96-96s43-96 96-96 96 43 96 96-43 96-96 96zm0-128c-17.7 0-32 14.3-32 32s14.3 32 32 32 32-14.3 32-32-14.3-32-32-32z'
|
| 14 |
+
/>
|
| 15 |
+
</Icon>
|
| 16 |
+
)
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
export default IconDisk
|
components/icon/IconDrag.tsx
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { FC } from "react"
|
| 2 |
+
import Icon, { IconProps } from "./Icon"
|
| 3 |
+
|
| 4 |
+
const IconDrag: FC<IconProps> = ({ className = "" }) => {
|
| 5 |
+
return (
|
| 6 |
+
<Icon
|
| 7 |
+
className={className}
|
| 8 |
+
sizeClassName={"h-6 w-6"}
|
| 9 |
+
viewBox={"0 0 320 512"}
|
| 10 |
+
>
|
| 11 |
+
<path
|
| 12 |
+
fill='currentColor'
|
| 13 |
+
d='M96 32H32C14.33 32 0 46.33 0 64v64c0 17.67 14.33 32 32 32h64c17.67 0 32-14.33 32-32V64c0-17.67-14.33-32-32-32zm0 160H32c-17.67 0-32 14.33-32 32v64c0 17.67 14.33 32 32 32h64c17.67 0 32-14.33 32-32v-64c0-17.67-14.33-32-32-32zm0 160H32c-17.67 0-32 14.33-32 32v64c0 17.67 14.33 32 32 32h64c17.67 0 32-14.33 32-32v-64c0-17.67-14.33-32-32-32zM288 32h-64c-17.67 0-32 14.33-32 32v64c0 17.67 14.33 32 32 32h64c17.67 0 32-14.33 32-32V64c0-17.67-14.33-32-32-32zm0 160h-64c-17.67 0-32 14.33-32 32v64c0 17.67 14.33 32 32 32h64c17.67 0 32-14.33 32-32v-64c0-17.67-14.33-32-32-32zm0 160h-64c-17.67 0-32 14.33-32 32v64c0 17.67 14.33 32 32 32h64c17.67 0 32-14.33 32-32v-64c0-17.67-14.33-32-32-32z'
|
| 14 |
+
/>
|
| 15 |
+
</Icon>
|
| 16 |
+
)
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
export default IconDrag
|
components/icon/IconExpand.tsx
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { FC } from "react"
|
| 2 |
+
import Icon, { IconProps } from "./Icon"
|
| 3 |
+
|
| 4 |
+
const IconExpand: FC<IconProps> = ({ className = "" }) => {
|
| 5 |
+
return (
|
| 6 |
+
<Icon className={className} viewBox='0 0 448 512'>
|
| 7 |
+
<path
|
| 8 |
+
fill='currentColor'
|
| 9 |
+
d='M0 180V56c0-13.3 10.7-24 24-24h124c6.6 0 12 5.4 12 12v40c0 6.6-5.4 12-12 12H64v84c0 6.6-5.4 12-12 12H12c-6.6 0-12-5.4-12-12zM288 44v40c0 6.6 5.4 12 12 12h84v84c0 6.6 5.4 12 12 12h40c6.6 0 12-5.4 12-12V56c0-13.3-10.7-24-24-24H300c-6.6 0-12 5.4-12 12zm148 276h-40c-6.6 0-12 5.4-12 12v84h-84c-6.6 0-12 5.4-12 12v40c0 6.6 5.4 12 12 12h124c13.3 0 24-10.7 24-24V332c0-6.6-5.4-12-12-12zM160 468v-40c0-6.6-5.4-12-12-12H64v-84c0-6.6-5.4-12-12-12H12c-6.6 0-12 5.4-12 12v124c0 13.3 10.7 24 24 24h124c6.6 0 12-5.4 12-12z'
|
| 10 |
+
/>
|
| 11 |
+
</Icon>
|
| 12 |
+
)
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
export default IconExpand
|
components/icon/IconForward.tsx
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { FC } from "react"
|
| 2 |
+
import Icon, { IconProps } from "./Icon"
|
| 3 |
+
|
| 4 |
+
const IconForward: FC<IconProps> = ({ className = "" }) => {
|
| 5 |
+
return (
|
| 6 |
+
<Icon className={className} viewBox='0 0 448 512'>
|
| 7 |
+
<path
|
| 8 |
+
fill='currentColor'
|
| 9 |
+
d='M384 44v424c0 6.6-5.4 12-12 12h-48c-6.6 0-12-5.4-12-12V291.6l-195.5 181C95.9 489.7 64 475.4 64 448V64c0-27.4 31.9-41.7 52.5-24.6L312 219.3V44c0-6.6 5.4-12 12-12h48c6.6 0 12 5.4 12 12z'
|
| 10 |
+
/>
|
| 11 |
+
</Icon>
|
| 12 |
+
)
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
export default IconForward
|
components/icon/IconGithub.tsx
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { FC } from "react"
|
| 2 |
+
import Icon, { IconProps } from "./Icon"
|
| 3 |
+
|
| 4 |
+
const IconGithub: FC<IconProps> = ({ className = "", sizeClassName= "h-4 w-4" }) => {
|
| 5 |
+
return (
|
| 6 |
+
<Icon
|
| 7 |
+
className={className}
|
| 8 |
+
sizeClassName={sizeClassName}
|
| 9 |
+
viewBox='0 0 496 512'
|
| 10 |
+
>
|
| 11 |
+
<path
|
| 12 |
+
fill='currentColor'
|
| 13 |
+
d='M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z'
|
| 14 |
+
/>
|
| 15 |
+
</Icon>
|
| 16 |
+
)
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
export default IconGithub
|
components/icon/IconLoading.tsx
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { FC } from "react"
|
| 2 |
+
import Icon, { IconProps } from "./Icon"
|
| 3 |
+
|
| 4 |
+
const IconLoading: FC<IconProps> = ({ className = "" }) => {
|
| 5 |
+
return (
|
| 6 |
+
<Icon className={className}>
|
| 7 |
+
<circle
|
| 8 |
+
className='opacity-25'
|
| 9 |
+
cx='12'
|
| 10 |
+
cy='12'
|
| 11 |
+
r='10'
|
| 12 |
+
stroke='currentColor'
|
| 13 |
+
strokeWidth='4'
|
| 14 |
+
/>
|
| 15 |
+
<path
|
| 16 |
+
className='opacity-75'
|
| 17 |
+
fill='currentColor'
|
| 18 |
+
d='M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z'
|
| 19 |
+
/>
|
| 20 |
+
</Icon>
|
| 21 |
+
)
|
| 22 |
+
}
|
| 23 |
+
|
| 24 |
+
export default IconLoading
|
components/icon/IconLoop.tsx
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { FC } from "react"
|
| 2 |
+
import Icon, { IconProps } from "./Icon"
|
| 3 |
+
|
| 4 |
+
const IconLoop: FC<IconProps> = ({ className = "" }) => {
|
| 5 |
+
return (
|
| 6 |
+
<Icon className={className} viewBox='0 0 512 512'>
|
| 7 |
+
<path
|
| 8 |
+
fill='currentColor'
|
| 9 |
+
d='M440.65 12.57l4 82.77A247.16 247.16 0 0 0 255.83 8C134.73 8 33.91 94.92 12.29 209.82A12 12 0 0 0 24.09 224h49.05a12 12 0 0 0 11.67-9.26 175.91 175.91 0 0 1 317-56.94l-101.46-4.86a12 12 0 0 0-12.57 12v47.41a12 12 0 0 0 12 12H500a12 12 0 0 0 12-12V12a12 12 0 0 0-12-12h-47.37a12 12 0 0 0-11.98 12.57zM255.83 432a175.61 175.61 0 0 1-146-77.8l101.8 4.87a12 12 0 0 0 12.57-12v-47.4a12 12 0 0 0-12-12H12a12 12 0 0 0-12 12V500a12 12 0 0 0 12 12h47.35a12 12 0 0 0 12-12.6l-4.15-82.57A247.17 247.17 0 0 0 255.83 504c121.11 0 221.93-86.92 243.55-201.82a12 12 0 0 0-11.8-14.18h-49.05a12 12 0 0 0-11.67 9.26A175.86 175.86 0 0 1 255.83 432z'
|
| 10 |
+
/>
|
| 11 |
+
</Icon>
|
| 12 |
+
)
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
export default IconLoop
|
components/icon/IconMusic.tsx
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { FC } from "react"
|
| 2 |
+
import Icon, { IconProps } from "./Icon"
|
| 3 |
+
|
| 4 |
+
const IconMusic: FC<IconProps> = ({ className = "" }) => {
|
| 5 |
+
return (
|
| 6 |
+
<Icon className={className} viewBox='0 0 512 512'>
|
| 7 |
+
<path
|
| 8 |
+
fill='currentColor'
|
| 9 |
+
d='M470.38 1.51L150.41 96A32 32 0 0 0 128 126.51v261.41A139 139 0 0 0 96 384c-53 0-96 28.66-96 64s43 64 96 64 96-28.66 96-64V214.32l256-75v184.61a138.4 138.4 0 0 0-32-3.93c-53 0-96 28.66-96 64s43 64 96 64 96-28.65 96-64V32a32 32 0 0 0-41.62-30.49z'
|
| 10 |
+
/>
|
| 11 |
+
</Icon>
|
| 12 |
+
)
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
export default IconMusic
|