Shivam commited on
Commit
d092f57
·
1 Parent(s): 4cf14a8

Initial commit: Web-SyncPlay moved into Streamer

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .dockerignore +28 -0
  2. .env +11 -0
  3. .eslintrc.json +3 -0
  4. .github/ISSUE_TEMPLATE/bug-report.md +7 -0
  5. .github/ISSUE_TEMPLATE/feature-request.md +7 -0
  6. .github/workflows/codeql-analysis.yml +71 -0
  7. .github/workflows/docker-publish.yml +57 -0
  8. .gitignore +29 -0
  9. .prettierignore +45 -0
  10. .prettierrc +9 -0
  11. Dockerfile +61 -0
  12. LICENSE +21 -0
  13. README.md +170 -10
  14. components/Embed.tsx +71 -0
  15. components/Footer.tsx +39 -0
  16. components/Head.tsx +44 -0
  17. components/Layout.tsx +42 -0
  18. components/Navbar.tsx +69 -0
  19. components/Room.tsx +113 -0
  20. components/action/Button.tsx +40 -0
  21. components/action/DeleteButton.tsx +24 -0
  22. components/action/DropUp.tsx +67 -0
  23. components/action/InteractionHandler.tsx +104 -0
  24. components/action/NewTabLink.tsx +26 -0
  25. components/alert/Alert.tsx +39 -0
  26. components/alert/AutoplayAlert.tsx +21 -0
  27. components/alert/BufferAlert.tsx +17 -0
  28. components/alert/ConnectingAlert.tsx +19 -0
  29. components/alert/Loading.module.css +12 -0
  30. components/alert/NoScriptAlert.tsx +12 -0
  31. components/icon/Icon.tsx +30 -0
  32. components/icon/IconBackward.tsx +15 -0
  33. components/icon/IconBigPause.tsx +17 -0
  34. components/icon/IconBigPlay.tsx +23 -0
  35. components/icon/IconCC.tsx +15 -0
  36. components/icon/IconChevron.tsx +59 -0
  37. components/icon/IconClipboard.tsx +17 -0
  38. components/icon/IconClose.tsx +17 -0
  39. components/icon/IconCog.tsx +15 -0
  40. components/icon/IconCompress.tsx +15 -0
  41. components/icon/IconCopyright.tsx +19 -0
  42. components/icon/IconDelete.tsx +15 -0
  43. components/icon/IconDisk.tsx +19 -0
  44. components/icon/IconDrag.tsx +19 -0
  45. components/icon/IconExpand.tsx +15 -0
  46. components/icon/IconForward.tsx +15 -0
  47. components/icon/IconGithub.tsx +19 -0
  48. components/icon/IconLoading.tsx +24 -0
  49. components/icon/IconLoop.tsx +15 -0
  50. 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
- title: Streamer
3
- emoji: 📚
4
- colorFrom: red
5
- colorTo: green
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [![Website](https://img.shields.io/website?down_color=red&down_message=offline&label=Website&up_color=green&up_message=online&url=https%3A%2F%2Fweb-syncplay.de)](https://demo.web-syncplay.de)
2
+ [![Codacy Badge](https://app.codacy.com/project/badge/Grade/35f7884623744a5c8ad64e184f6f5dcf)](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
+ [![CodeFactor](https://www.codefactor.io/repository/github/web-syncplay/web-syncplay/badge)](https://www.codefactor.io/repository/github/web-syncplay/web-syncplay)
4
+ [![Docker Image Size (tag)](https://img.shields.io/docker/image-size/websyncplay/websyncplay/latest?logo=docker)](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