├── .changeset
├── config.json
└── real-jars-shake.md
├── .eslintignore
├── .eslintrc.json
├── .github
├── README.md
└── workflows
│ └── release.yaml
├── .gitignore
├── .husky
├── .gitignore
└── pre-commit
├── .prettierignore
├── .prettierrc.json
├── LICENSE
├── package.json
├── package
├── .npmignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── index.html
├── package.json
├── preview
│ ├── App.tsx
│ ├── assets
│ │ ├── audio
│ │ │ ├── audio-1.mp3
│ │ │ ├── audio-2.mp3
│ │ │ ├── audio-3.mp3
│ │ │ ├── audio-4.mp3
│ │ │ └── audio-5.mp3
│ │ └── images
│ │ │ ├── audio-1.jpg
│ │ │ ├── audio-2.jpg
│ │ │ ├── audio-3.jpg
│ │ │ ├── audio-4.jpg
│ │ │ ├── audio-5.jpg
│ │ │ └── noname.png
│ ├── index.d.ts
│ └── main.tsx
├── src
│ ├── components
│ │ ├── AudioPlayer
│ │ │ ├── Audio
│ │ │ │ ├── index.tsx
│ │ │ │ └── useAudio.ts
│ │ │ ├── Context
│ │ │ │ ├── StateContext
│ │ │ │ │ ├── audio.ts
│ │ │ │ │ ├── element.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── placement.ts
│ │ │ │ ├── dispatchContext.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── reducer.ts
│ │ │ ├── Interface
│ │ │ │ ├── Controller
│ │ │ │ │ ├── Button
│ │ │ │ │ │ ├── PlayBtn.tsx
│ │ │ │ │ │ ├── PlayListTriggerBtn.tsx
│ │ │ │ │ │ ├── PrevNnextBtn.tsx
│ │ │ │ │ │ ├── RepeatTypeBtn.tsx
│ │ │ │ │ │ ├── StyledBtn.ts
│ │ │ │ │ │ ├── VolumeTriggerBtn.tsx
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── Drawer
│ │ │ │ │ │ ├── SortablePlayList
│ │ │ │ │ │ │ ├── Content
│ │ │ │ │ │ │ │ ├── PlayListItem.tsx
│ │ │ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ │ │ └── usePlayList.ts
│ │ │ │ │ │ │ └── index.tsx
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── Icon.tsx
│ │ │ │ │ ├── Input
│ │ │ │ │ │ ├── Progress
│ │ │ │ │ │ │ ├── BarProgress.tsx
│ │ │ │ │ │ │ ├── WaveformProgress.tsx
│ │ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ │ ├── useProgress.ts
│ │ │ │ │ │ │ └── useWavesurfer.ts
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── Tooltip
│ │ │ │ │ │ ├── Volume
│ │ │ │ │ │ │ ├── Content.tsx
│ │ │ │ │ │ │ ├── Trigger.tsx
│ │ │ │ │ │ │ ├── index.tsx
│ │ │ │ │ │ │ └── useVolume.ts
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── CustomComponent
│ │ │ │ │ └── index.tsx
│ │ │ │ ├── Information
│ │ │ │ │ ├── Artwork.tsx
│ │ │ │ │ ├── TrackInfo.tsx
│ │ │ │ │ ├── TrackTime
│ │ │ │ │ │ ├── Current.tsx
│ │ │ │ │ │ ├── Duration.tsx
│ │ │ │ │ │ ├── Styles.ts
│ │ │ │ │ │ ├── Types.ts
│ │ │ │ │ │ └── index.tsx
│ │ │ │ │ └── index.tsx
│ │ │ │ └── index.tsx
│ │ │ ├── Player
│ │ │ │ ├── index.tsx
│ │ │ │ └── usePropsStateEffect.ts
│ │ │ └── index.tsx
│ │ ├── CssTransition.tsx
│ │ ├── Drawer
│ │ │ ├── Drawer.tsx
│ │ │ ├── DrawerContent.tsx
│ │ │ ├── DrawerContext.ts
│ │ │ ├── DrawerTrigger.tsx
│ │ │ └── index.ts
│ │ ├── Dropdown
│ │ │ ├── Dropdown.tsx
│ │ │ ├── DropdownContent.tsx
│ │ │ ├── DropdownContext.ts
│ │ │ ├── DropdownTrigger.tsx
│ │ │ ├── index.ts
│ │ │ └── useDropdown.ts
│ │ ├── Grid
│ │ │ ├── Grid.tsx
│ │ │ ├── Item.tsx
│ │ │ └── index.ts
│ │ ├── Provider
│ │ │ ├── AudioPlayerProvider.tsx
│ │ │ ├── SpectrumProvider.tsx
│ │ │ └── index.ts
│ │ └── SortableList
│ │ │ ├── SortableList.tsx
│ │ │ ├── SortableListItem.tsx
│ │ │ ├── index.ts
│ │ │ └── useSortableListItem.ts
│ ├── hooks
│ │ ├── index.ts
│ │ ├── useClickOutside.ts
│ │ ├── useGridTemplate.ts
│ │ ├── useNonNullableContext.ts
│ │ ├── useRefsDispatch.ts
│ │ └── useVariableColor.ts
│ ├── index.ts
│ ├── styles
│ │ ├── GlobalStyle.ts
│ │ └── vars.css
│ └── utils
│ │ ├── generateUnionNumType.ts
│ │ ├── getRandomNumber.ts
│ │ ├── getTime.ts
│ │ ├── refs.ts
│ │ └── resetAudioValues.ts
├── tsconfig.json
└── vite.config.ts
├── storybook
├── .eslintrc.json
├── .storybook
│ ├── main.js
│ └── preview.js
├── README.md
├── package.json
├── public
│ ├── audio
│ │ ├── audio-1.mp3
│ │ ├── audio-2.mp3
│ │ ├── audio-3.mp3
│ │ ├── audio-4.mp3
│ │ └── audio-5.mp3
│ ├── favicon.ico
│ ├── images
│ │ ├── audio-1.jpg
│ │ ├── audio-2.jpg
│ │ ├── audio-3.jpg
│ │ ├── audio-4.jpg
│ │ └── audio-5.jpg
│ ├── index.html
│ ├── logo192.png
│ ├── logo512.png
│ ├── manifest.json
│ └── robots.txt
├── src
│ ├── App.css
│ ├── App.test.tsx
│ ├── App.tsx
│ ├── index.css
│ ├── index.tsx
│ ├── logo.svg
│ ├── react-app-env.d.ts
│ ├── reportWebVitals.ts
│ ├── setupTests.ts
│ └── stories
│ │ ├── Test.stories.tsx
│ │ ├── Test.tsx
│ │ ├── assets
│ │ ├── code-brackets.svg
│ │ ├── colors.svg
│ │ ├── comments.svg
│ │ ├── direction.svg
│ │ ├── flow.svg
│ │ ├── plugin.svg
│ │ ├── repo.svg
│ │ └── stackalt.svg
│ │ ├── playList.ts
│ │ └── playerMode.ts
├── tsconfig.json
└── yarn.lock
└── yarn.lock
/.changeset/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://unpkg.com/@changesets/config@1.6.3/schema.json",
3 | "changelog": [
4 | "@changesets/changelog-github",
5 | { "repo": "slash9494/react-modern-audio-player" }
6 | ],
7 | "commit": false,
8 | "linked": [],
9 | "access": "public",
10 | "baseBranch": "main",
11 | "updateInternalDependencies": "patch",
12 | "ignore": []
13 | }
14 |
--------------------------------------------------------------------------------
/.changeset/real-jars-shake.md:
--------------------------------------------------------------------------------
1 | ---
2 | "react-modern-audio-player": major
3 | ---
4 |
5 | React modern audio player
6 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /package/dist
3 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es2021": true
5 | },
6 | "extends": [
7 | "eslint:recommended",
8 | "plugin:@typescript-eslint/recommended",
9 | "plugin:prettier/recommended",
10 | "plugin:react/recommended",
11 | "plugin:react/jsx-runtime"
12 | ],
13 | "parser": "@typescript-eslint/parser",
14 | "parserOptions": {
15 | "ecmaVersion": "latest",
16 | "sourceType": "module"
17 | },
18 | "plugins": [
19 | "@typescript-eslint",
20 | "react-hooks"
21 | ],
22 | "rules": {
23 | "react-hooks/rules-of-hooks": "error",
24 | "react-hooks/exhaustive-deps": "warn",
25 | "no-console":[
26 | "error",
27 | {
28 | "allow": ["warn", "error"]
29 | }
30 | ],
31 | "no-unused-vars": "off",
32 | "@typescript-eslint/no-unused-vars": ["error"]
33 | },
34 |
35 | // for new JSX transform from react17
36 | "settings": {
37 | "import/parsers": {
38 | "@typescript-eslint/parser": [".ts", ".tsx", ".js"]
39 | },
40 | "import/resolver": {
41 | "typescript": "./tsconfig.json"
42 | },
43 | "react": {
44 | "createClass": "createReactClass", // Regex for Component Factory to use,
45 | // default to "createReactClass"
46 | "pragma": "React", // Pragma to use, default to "React"
47 | "fragment": "Fragment", // Fragment to use (may be a property of ), default to "Fragment"
48 | "version": "detect", // React version. "detect" automatically picks the version you have installed.
49 | // You can also use `16.0`, `16.3`, etc, if you want to override the detected value.
50 | // It will default to "latest" and warn if missing, and to "detect" in the future
51 | "flowVersion": "0.53" // Flow version
52 | },
53 | "propWrapperFunctions": [
54 | // The names of any function used to wrap propTypes, e.g. `forbidExtraProps`. If this isn't set, any propTypes wrapped in a function will be skipped.
55 | "forbidExtraProps",
56 | {"property": "freeze", "object": "Object"},
57 | {"property": "myFavoriteWrapper"},
58 | // for rules that check exact prop wrappers
59 | {"property": "forbidExtraProps", "exact": true}
60 | ],
61 | "componentWrapperFunctions": [
62 | // The name of any function used to wrap components, e.g. Mobx `observer` function. If this isn't set, components wrapped by these functions will be skipped.
63 | "observer", // `property`
64 | {"property": "styled"}, // `object` is optional
65 | {"property": "observer", "object": "Mobx"},
66 | {"property": "observer", "object": ""} // sets `object` to whatever value `settings.react.pragma` is set to
67 | ],
68 | "formComponents": [
69 | // Components used as alternatives to
70 | "CustomForm",
71 | {"name": "Form", "formAttribute": "endpoint"}
72 | ],
73 | "linkComponents": [
74 | // Components used as alternatives to for linking, eg.
75 | "Hyperlink",
76 | {"name": "Link", "linkAttribute": "to"}
77 | ]
78 |
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/.github/README.md:
--------------------------------------------------------------------------------
1 | ../package/README.md
--------------------------------------------------------------------------------
/.github/workflows/release.yaml:
--------------------------------------------------------------------------------
1 | name: release
2 |
3 | on:
4 | push:
5 | branches: [main]
6 | jobs:
7 | publish:
8 | runs-on: ubuntu-latest
9 | if: ${{ github.event_name == 'push' }}
10 | steps:
11 | - uses: actions/checkout@v2
12 | - uses: actions/setup-node@v2
13 | with:
14 | cache: 'yarn'
15 | node-version: '16'
16 |
17 | - name: Configure git
18 | run: |
19 | git config user.email "slash9494@naver.com"
20 | git config user.name "Yun Hyeon Lee"
21 |
22 | - id: yarn-cache-dir-path
23 | run: echo "::set-output name=dir::$(yarn cache dir)"
24 | - uses: actions/cache@v2
25 | with:
26 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
27 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
28 | restore-keys: |
29 | ${{ runner.os }}-yarn-
30 |
31 | - name: Install dependencies
32 | run: yarn --frozen-lockfile
33 |
34 | - name: Build
35 | run: yarn build
36 |
37 | - name: Creating .npmrc
38 | run: |
39 | cat << EOF > "$HOME/.npmrc"
40 | email=slash9494@naver.com
41 | //registry.npmjs.org/:_authToken=$NPM_TOKEN
42 | EOF
43 | env:
44 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
45 |
46 | - name: publish
47 | uses: JS-DevTools/npm-publish@v1
48 | with:
49 | token: ${{ secrets.NPM_TOKEN }}
50 | package: './package/package.json'
51 | access: 'public'
52 |
53 | - name: Tag new version
54 | if: steps.publish.outputs.type != 'none'
55 | uses: Klemensas/action-autotag@stable
56 | with:
57 | GITHUB_TOKEN: ${{ github.token }}
58 | tag_prefix: "v"
59 | package_root: "./package"
60 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | node_modules
5 |
6 | # build
7 | dist
8 |
9 | # misc
10 | .DS_Store
11 | .env.local
12 | .env.development.local
13 | .env.test.local
14 | .env.production.local
15 |
16 | npm-debug.log*
17 | yarn-debug.log*
18 | yarn-error.log*
19 |
20 | *.drawio
21 | releaseNote.md
--------------------------------------------------------------------------------
/.husky/.gitignore:
--------------------------------------------------------------------------------
1 | _
2 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npx lint-staged
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /package/dist
3 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "arrowParens": "always",
3 | "bracketSameLine": false,
4 | "bracketSpacing": true,
5 | "embeddedLanguageFormatting": "auto",
6 | "htmlWhitespaceSensitivity": "css",
7 | "insertPragma": false,
8 | "jsxSingleQuote": false,
9 | "printWidth": 80,
10 | "proseWrap": "preserve",
11 | "quoteProps": "as-needed",
12 | "requirePragma": false,
13 | "semi": true,
14 | "singleQuote": false,
15 | "tabWidth": 2,
16 | "trailingComma": "es5",
17 | "useTabs": false,
18 | "vueIndentScriptAndStyle": false
19 | }
20 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 LYH
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.
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-modern-audio-player-root",
3 | "version": "0.0.1",
4 | "workspaces": [
5 | "package",
6 | "storybook"
7 | ],
8 | "repository": {
9 | "type": "git",
10 | "url": "git+https://github.com/slash9494/react-simple-audio-player.git",
11 | "directory": "package"
12 | },
13 | "private": true,
14 | "scripts": {
15 | "build": "cd package && yarn build",
16 | "release": "changeset publish",
17 | "version-packages": "changeset version",
18 | "prepare": "husky install",
19 | "lint": "eslint --fix ."
20 | },
21 | "devDependencies": {
22 | "@typescript-eslint/eslint-plugin": "^5.30.5",
23 | "@typescript-eslint/parser": "^5.30.5",
24 | "eslint": "^8.19.0",
25 | "eslint-config-prettier": "^8.5.0",
26 | "eslint-plugin-prettier": "^4.2.1",
27 | "eslint-plugin-react": "^7.30.1",
28 | "eslint-plugin-react-hooks": "^4.6.0",
29 | "husky": "^8.0.1",
30 | "lint-staged": "^13.0.3",
31 | "prettier": "^2.7.1",
32 | "typescript": "^4.7.4"
33 | },
34 | "dependencies": {
35 | "@changesets/cli": "^2.24.0",
36 | "react": "^18.2.0",
37 | "react-dom": "^18.2.0"
38 | },
39 | "lint-staged": {
40 | "*.{js,css,mdx,jsx,tsx,ts}": [
41 | "prettier --list-different",
42 | "eslint"
43 | ]
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/package/.npmignore:
--------------------------------------------------------------------------------
1 | coverage
2 | docs
3 | media
4 | node_modules
5 | src
6 | preview
7 | storybook
8 | .eslintignore
9 | .eslintrc
10 | .gitattributes
11 | .gitignore
12 | .vscode
13 | tsconfig.json
14 | yarn.lock
15 | vite.config.ts
16 | index.html
--------------------------------------------------------------------------------
/package/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # React-modern-audio-player `v1.2.1`
2 |
3 | ### apply release git action
--------------------------------------------------------------------------------
/package/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 LYH
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.
22 |
--------------------------------------------------------------------------------
/package/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
React Modern Audio Player
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | ## DEMO
22 | https://codesandbox.io/s/basic-91y82y?file=/src/App.tsx
23 |
24 | # ****Flexible and Customizable UI****
25 | ## This can offer waveform by `wavesurfer.js`
26 |
27 |
28 | ## This can offer various UI and you can also customize each component position
29 | > Full View
30 | >
31 |
32 | > Position Change
33 | >
34 |
35 |
36 | > Particular View
37 | >
38 | >
39 | >
40 | >
41 | >
42 | >
43 |
44 |
45 | # ****Installation****
46 |
47 | ```tsx
48 | npm install --save react-modern-audio-player
49 | ```
50 |
51 | # ****Quick Start****
52 |
53 | ```tsx
54 | import AudioPlayer from 'react-modern-audio-player';
55 |
56 | const playList = [
57 | {
58 | name: 'name',
59 | writer: 'writer',
60 | img: 'image.jpg',
61 | src: 'audio.mp3',
62 | id: 1,
63 | },
64 | ]
65 | function Player (){
66 | return (
67 |
68 | )
69 | }
70 | ```
71 |
72 | # Props
73 |
74 | ```tsx
75 | interface AudioPlayerProps {
76 | playList: PlayList;
77 | audioInitialState?: InitialStates;
78 | audioRef?: React.MutableRefObject;
79 | activeUI?: ActiveUI;
80 | customIcons?: CustomIcons;
81 | coverImgsCss?: CoverImgsCss;
82 | placement?: {
83 | player?: PlayerPlacement;
84 | playList?: PlayListPlacement;
85 | interface?: InterfacePlacement;
86 | volumeSlider?: VolumeSliderPlacement;
87 | };
88 | rootContainerProps?: RootContainerProps
89 | }
90 | ```
91 |
92 | Prop | Type | Default
93 | --- | --- | ---
94 | `playList` | [PlayList](#playlist) | [ ]
95 | `audioInitialState` | [InitialStates](#InitialStates) | isPlaying: false repeatType: "ALL" volume: 1
96 | `activeUI` | [ActiveUI](#activeui) | playButton : true
97 | `customIcons` | [CustomIcons](#customicons) | undefined
98 | `coverImgsCss` | [CoverImgsCss](#coverimgscss) | undefined
99 | `placement` | [Placement](#placement) | playListPlacement : "bottom" interfacePlacement :[DefaultInterfacePlacement](#default-interface-placement)
100 | `rootContainerProps` | [RootContainerProps](#rootcontainerprops) | theme: spectrum-theme-default
width: 100%
position: 'static'
UNSAFE_className: rm-audio-player-provider
101 |
102 | ## PlayList
103 |
104 | ```tsx
105 | type PlayList = Array;
106 | type AudioData = {
107 | src: string;
108 | id: number;
109 | name?: string | ReactNode;
110 | writer?: string | ReactNode;
111 | img?: string;
112 | description?: string | ReactNode;
113 | customTrackInfo?: string | ReactNode;
114 | };
115 | ```
116 |
117 | ## InitialStates
118 |
119 | ```tsx
120 | type InitialStates = Omit<
121 | React.AudioHTMLAttributes,
122 | "autoPlay"
123 | > & {
124 | isPlaying?: boolean;
125 | repeatType?: RepeatType;
126 | volume?: number;
127 | currentTime?: number;
128 | duration?: number;
129 | curPlayId: number;
130 | };
131 | ```
132 |
133 | ## ActiveUI
134 |
135 | ```tsx
136 | type ActiveUI = {
137 | all: boolean;
138 | playButton: boolean;
139 | playList: PlayListUI;
140 | prevNnext: boolean;
141 | volume: boolean;
142 | volumeSlider: boolean;
143 | repeatType: boolean;
144 | trackTime: boolean;
145 | trackInfo: boolean;
146 | artwork: boolean;
147 | progress: ProgressUI;
148 | };
149 | type ProgressUI = "waveform" | "bar" | false;
150 | type PlayListUI = "sortable" | "unSortable" | false;
151 | ```
152 |
153 | ## CustomIcons
154 |
155 | ```tsx
156 | type CustomIcons = {
157 | play: ReactNode;
158 | pause: ReactNode;
159 | prev: ReactNode;
160 | next: ReactNode;
161 | repeatOne: ReactNode;
162 | repeatAll: ReactNode;
163 | repeatNone: ReactNode;
164 | repeatShuffle: ReactNode;
165 | volumeFull: ReactNode;
166 | volumeHalf: ReactNode;
167 | volumeMuted: ReactNode;
168 | playList: ReactNode;
169 | };
170 | ```
171 |
172 | ## CoverImgsCss
173 |
174 | ```tsx
175 | interface CoverImgsCss {
176 | artwork?: React.CSSProperties;
177 | listThumbnail?: React.CSSProperties;
178 | }
179 | ```
180 |
181 | ## Placement
182 |
183 | ```tsx
184 | type PlayerPlacement =
185 | | "bottom"
186 | | "top"
187 | | "bottom-left"
188 | | "bottom-right"
189 | | "top-left"
190 | | "top-right";
191 |
192 | type VolumeSliderPlacement = "bottom" | "top" | 'left' | 'right';
193 |
194 | type PlayListPlacement = "bottom" | "top";
195 |
196 | type InterfacePlacement = {
197 | templateArea?: InterfaceGridTemplateArea;
198 | customComponentsArea?: InterfaceGridCustomComponentsArea;
199 | itemCustomArea?: InterfaceGridItemArea;
200 | };
201 |
202 | type InterfacePlacementKey =
203 | | Exclude
204 | | "trackTimeCurrent"
205 | | "trackTimeDuration";
206 |
207 | type InterfacePlacementValue = "row1-1" | "row1-2" | "row1-3" | "row1-4" | ... more ... | "row9-9"
208 | /** if you apply custom components, values must be "row1-1" ~ any more */
209 |
210 | type InterfaceGridTemplateArea = Record;
211 |
212 | type InterfaceGridCustomComponentsArea = Record;
213 |
214 | type InterfaceGridItemArea = Partial>;
215 | /** example
216 | * progress : 2-4
217 | * repeatBtn : row1-4 / 2 / row1-4 / 10
218 | *
219 | * check MDN - grid area
220 | * https://developer.mozilla.org/ko/docs/Web/CSS/grid-area
221 | */
222 | ```
223 |
224 | ### Default interface placement
225 | ```tsx
226 | const defaultInterfacePlacement = {
227 | templateArea: {
228 | artwork: "row1-1",
229 | trackInfo: "row1-2",
230 | trackTimeCurrent: "row1-3",
231 | trackTimeDuration: "row1-4",
232 | progress: "row1-5",
233 | repeatType: "row1-6",
234 | volume: "row1-7",
235 | playButton: "row1-8",
236 | playList: "row1-9",
237 | },
238 | };
239 | ```
240 |
241 | ## RootContainerProps
242 | > it is same with spectrum provider props
243 | >
244 | > https://react-spectrum.adobe.com/react-spectrum/Provider.html#themes
245 |
246 |
247 | # Override Style
248 |
249 | ### Theme mode ( dark-mode )
250 |
251 | > it apply dark-mode depending on `system-theme`
252 | >
253 | > you can customize color-theme by `css-variable` of `react-spectrum` `theme-default`
254 |
255 |
256 | ## ID & Classnames
257 |
258 | ### root ID
259 |
260 | - rm-audio-player
261 |
262 | ### root ClassName
263 |
264 | - rm-audio-player-provider
265 |
266 | ### color variables
267 |
268 | ```tsx
269 | --rm-audio-player-interface-container:var(--spectrum-global-color-gray-100);
270 | --rm-audio-player-volume-background: #ccc;
271 | --rm-audio-player-volume-panel-background:#f2f2f2;
272 | --rm-audio-player-volume-panel-border:#ccc;
273 | --rm-audio-player-volume-thumb: #d3d3d3;
274 | --rm-audio-player-volume-fill:rgba(0, 0, 0, 0.5);
275 | --rm-audio-player-volume-track:#ababab;
276 | --rm-audio-player-track-current-time:#0072F5;
277 | --rm-audio-player-track-duration:#8c8c8c;
278 | --rm-audio-player-progress-bar:#0072F5;
279 | --rm-audio-player-progress-bar-background:#D1D1D1;
280 | --rm-audio-player-waveform-cursor:var(--spectrum-global-color-gray-800);
281 | --rm-audio-player-waveform-background:var(--rm-audio-player-progress-bar-background);
282 | --rm-audio-player-waveform-bar:var(--rm-audio-player-progress-bar);
283 | --rm-audio-player-sortable-list:var(--spectrum-global-color-gray-200);
284 | --rm-audio-player-sortable-list-button-active:#0072F5;
285 | --rm-audio-player-selected-list-item-background:var(--spectrum-global-color-gray-500);
286 |
287 | // ...spectrum theme palette and so on... //
288 | ```
289 | # Custom Component
290 | > you can apply custom component to `AudioPlayer` by `CustomComponent`
291 | >
292 | > you can also set `viewProps` to `CustomComponent`
293 | >
294 | > (https://react-spectrum.adobe.com/react-spectrum/View.html#props)
295 |
296 | ``` tsx
297 | const activeUI: ActiveUI = {
298 | all: true,
299 | };
300 |
301 | const placement = {
302 | interface: {
303 | customComponentsArea: {
304 | playerCustomComponent: "row1-10",
305 | },
306 | } as InterfacePlacement<11>,
307 | /**
308 | * you should set generic value of `InterfacePlacement` as interfaces max length for auto-complete aria type such as "row-1-10"
309 | * generic value must plus 1 than interfaces length because of 0 index
310 | */
311 | };
312 |
313 | /** you can get audioPlayerState by props */
314 | const CustomComponent = ({
315 | audioPlayerState,
316 | }: {
317 | audioPlayerState?: AudioPlayerStateContext;
318 | }) => {
319 | const audioEl = audioPlayerState?.elementRefs?.audioEl;
320 | const handOverTime = () => {
321 | if (audioEl) {
322 | audioEl.currentTime += 30;
323 | }
324 | };
325 | return (
326 | <>
327 |
328 | >
329 | );
330 | };
331 |
332 |
337 |
338 |
339 |
340 |
341 | ```
342 |
343 |
344 |
345 | # ****Example****
346 | ```tsx
347 | function App() {
348 | return (
349 |
375 | );
376 | }
377 | ```
378 |
--------------------------------------------------------------------------------
/package/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | React-modern-audio-player
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/package/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-modern-audio-player",
3 | "version": "1.4.0-rc.2",
4 | "author": {
5 | "name": "LYH",
6 | "email": "slash9494@naver.com",
7 | "url": "https://github.com/slash9494"
8 | },
9 | "homepage": "https://github.com/slash9494/react-modern-audio-player/",
10 | "module": "dist/index.es.js",
11 | "main": "dist/index.es.js",
12 | "esnext": "dist/index.es.js",
13 | "typings": "dist/types/index.d.ts",
14 | "files": [
15 | "dist"
16 | ],
17 | "license": "MIT",
18 | "repository": {
19 | "type": "git",
20 | "url": "git+https://github.com/slash9494/react-modern-audio-player.git"
21 | },
22 | "scripts": {
23 | "dev": "vite --host",
24 | "build": "rm -rf dist && tsc && vite build",
25 | "preview": "vite preview",
26 | "typeCheck": "tsc --project ./tsconfig.json --noEmit"
27 | },
28 | "peerDependencies": {
29 | "react": ">=16.8.0",
30 | "react-dom": ">=16.8.0"
31 | },
32 | "dependencies": {
33 | "@react-spectrum/layout": "^3.3.1",
34 | "@react-spectrum/provider": "^3.4.1",
35 | "@react-spectrum/theme-default": "^3.3.1",
36 | "@react-spectrum/view": "^3.2.1",
37 | "classnames": "^2.3.1",
38 | "react-icons": "^4.4.0",
39 | "styled-components": "^5.3.5",
40 | "wavesurfer.js": "^6.2.0"
41 | },
42 | "devDependencies": {
43 | "@types/node": "^17.0.25",
44 | "@types/react": "^18.0.1",
45 | "@types/react-dom": "^18.0.0",
46 | "@types/styled-components": "^5.1.26",
47 | "@types/wavesurfer.js": "^6.0.3",
48 | "@vitejs/plugin-react": "^1.0.7",
49 | "typescript": "^4.6.3",
50 | "vite": "^2.9.0",
51 | "vite-plugin-dts": "^1.1.0",
52 | "vite-plugin-libcss": "^1.0.5"
53 | },
54 | "keywords": [
55 | "audio",
56 | "music",
57 | "player",
58 | "media",
59 | "react",
60 | "react-modern-audio-player"
61 | ]
62 | }
63 |
--------------------------------------------------------------------------------
/package/preview/App.tsx:
--------------------------------------------------------------------------------
1 | import PlayerLogo from "./assets/images/noname.png";
2 | import { useState } from "react";
3 | import AudioPlayerWithProviders, {
4 | ActiveUI,
5 | AudioPlayerStateContext,
6 | InterfacePlacement,
7 | PlayerPlacement,
8 | PlayList,
9 | } from "../src";
10 | const playList: PlayList = [
11 | {
12 | name: "React Modern Audio Player-1",
13 | writer: "LYH",
14 | img: `${PlayerLogo}`,
15 | src: "https://cdn.pixabay.com/audio/2022/08/23/audio_d16737dc28.mp3",
16 | id: 1,
17 | },
18 | {
19 | name: "React Modern Audio Player-1",
20 | writer: "LYH",
21 | img: `${PlayerLogo}`,
22 | src: "https://cdn.pixabay.com/audio/2022/01/21/audio_c44fddb424.mp3",
23 | id: 2,
24 | },
25 | {
26 | name: "React Modern Audio Player-1",
27 | writer: "LYH",
28 | img: `${PlayerLogo}`,
29 | src: "https://cdn.pixabay.com/audio/2022/08/03/audio_54ca0ffa52.mp3",
30 | id: 3,
31 | },
32 | {
33 | name: "React Modern Audio Player-1",
34 | writer: "LYH",
35 | img: `${PlayerLogo}`,
36 | src: "https://cdn.pixabay.com/audio/2022/07/25/audio_3266b47d61.mp3",
37 | id: 4,
38 | },
39 | {
40 | name: "React Modern Audio Player-1",
41 | writer: "LYH",
42 | img: `${PlayerLogo}`,
43 | src: "https://cdn.pixabay.com/audio/2022/08/02/audio_884fe92c21.mp3",
44 | id: 5,
45 | },
46 | ];
47 |
48 | const initialState = {
49 | muted: true,
50 | volume: 0.2,
51 | curPlayId: 1,
52 | };
53 |
54 | function App() {
55 | const [progressType, setProgressType] = useState("bar");
56 | const [playerPlacement, setPlayerPlacement] = useState("static");
57 |
58 | const placement = {
59 | interface: {
60 | templateArea: {
61 | // playList: "row1-3",
62 | // progress: "row2-1",
63 | // playButton: "row1-1",
64 | // repeatType: "row2-10",
65 | // volume: "row1-3",
66 | // trackTimeCurrent: "row2-1",
67 | // trackTimeDuration: "row2-3",
68 | },
69 | customComponentsArea: {
70 | test1: "row1-10",
71 | },
72 | } as InterfacePlacement<11>,
73 | player: playerPlacement as PlayerPlacement,
74 | };
75 |
76 | const activeUI: ActiveUI = {
77 | all: true,
78 | progress: progressType as "bar" | "waveform",
79 | // playButton: true,
80 | // repeatType: true,
81 | // volume: true,
82 | // playList: "sortable",
83 | // prevNnext: true,
84 | // trackTime: true,
85 | };
86 |
87 | const CustomComponent = ({
88 | audioPlayerState,
89 | }: {
90 | audioPlayerState?: AudioPlayerStateContext;
91 | }) => {
92 | const audioEl = audioPlayerState?.elementRefs?.audioEl;
93 | const handOverTime = () => {
94 | if (audioEl) {
95 | audioEl.currentTime += 30;
96 | }
97 | };
98 | return (
99 | <>
100 |
101 | >
102 | );
103 | };
104 |
105 | return (
106 |
117 |
122 | React modern audio player
123 |
130 |
149 |
150 |
151 |
157 |
158 |
159 |
160 |
161 |
162 |
163 | );
164 | }
165 |
166 | export default App;
167 |
--------------------------------------------------------------------------------
/package/preview/assets/audio/audio-1.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/slash9494/react-modern-audio-player/c2f9d4ca25779e00d32a9fdda751fcbdf36a5eeb/package/preview/assets/audio/audio-1.mp3
--------------------------------------------------------------------------------
/package/preview/assets/audio/audio-2.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/slash9494/react-modern-audio-player/c2f9d4ca25779e00d32a9fdda751fcbdf36a5eeb/package/preview/assets/audio/audio-2.mp3
--------------------------------------------------------------------------------
/package/preview/assets/audio/audio-3.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/slash9494/react-modern-audio-player/c2f9d4ca25779e00d32a9fdda751fcbdf36a5eeb/package/preview/assets/audio/audio-3.mp3
--------------------------------------------------------------------------------
/package/preview/assets/audio/audio-4.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/slash9494/react-modern-audio-player/c2f9d4ca25779e00d32a9fdda751fcbdf36a5eeb/package/preview/assets/audio/audio-4.mp3
--------------------------------------------------------------------------------
/package/preview/assets/audio/audio-5.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/slash9494/react-modern-audio-player/c2f9d4ca25779e00d32a9fdda751fcbdf36a5eeb/package/preview/assets/audio/audio-5.mp3
--------------------------------------------------------------------------------
/package/preview/assets/images/audio-1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/slash9494/react-modern-audio-player/c2f9d4ca25779e00d32a9fdda751fcbdf36a5eeb/package/preview/assets/images/audio-1.jpg
--------------------------------------------------------------------------------
/package/preview/assets/images/audio-2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/slash9494/react-modern-audio-player/c2f9d4ca25779e00d32a9fdda751fcbdf36a5eeb/package/preview/assets/images/audio-2.jpg
--------------------------------------------------------------------------------
/package/preview/assets/images/audio-3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/slash9494/react-modern-audio-player/c2f9d4ca25779e00d32a9fdda751fcbdf36a5eeb/package/preview/assets/images/audio-3.jpg
--------------------------------------------------------------------------------
/package/preview/assets/images/audio-4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/slash9494/react-modern-audio-player/c2f9d4ca25779e00d32a9fdda751fcbdf36a5eeb/package/preview/assets/images/audio-4.jpg
--------------------------------------------------------------------------------
/package/preview/assets/images/audio-5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/slash9494/react-modern-audio-player/c2f9d4ca25779e00d32a9fdda751fcbdf36a5eeb/package/preview/assets/images/audio-5.jpg
--------------------------------------------------------------------------------
/package/preview/assets/images/noname.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/slash9494/react-modern-audio-player/c2f9d4ca25779e00d32a9fdda751fcbdf36a5eeb/package/preview/assets/images/noname.png
--------------------------------------------------------------------------------
/package/preview/index.d.ts:
--------------------------------------------------------------------------------
1 | declare module "*.jpg";
2 | declare module "*.png";
3 | declare module "*.mp3";
4 |
--------------------------------------------------------------------------------
/package/preview/main.tsx:
--------------------------------------------------------------------------------
1 | import { StrictMode } from "react";
2 | import { createRoot } from "react-dom/client";
3 | import App from "./App";
4 | const container = document.getElementById("root");
5 | const root = createRoot(container as HTMLElement, {
6 | onRecoverableError: (error) => console.log("recovering", error),
7 | });
8 |
9 | root.render(
10 |
11 |
12 |
13 | );
14 |
--------------------------------------------------------------------------------
/package/src/components/AudioPlayer/Audio/index.tsx:
--------------------------------------------------------------------------------
1 | import { useNonNullableContext } from "@/hooks/useNonNullableContext";
2 | import { audioPlayerDispatchContext } from "@/components/AudioPlayer/Context/dispatchContext";
3 | import {
4 | audioPlayerStateContext,
5 | AudioNativeProps,
6 | } from "@/components/AudioPlayer/Context/StateContext";
7 | import React, { FC, useEffect, useRef } from "react";
8 | import { useAudio } from "./useAudio";
9 |
10 | // TODO : optimize large audio files
11 |
12 | export const Audio: FC<{
13 | audioRef?: React.MutableRefObject;
14 | }> = ({ audioRef: propsAudioRef }) => {
15 | const audioRef = useRef(null);
16 | const { curAudioState, curPlayId, playList } = useNonNullableContext(
17 | audioPlayerStateContext
18 | );
19 | const audioPlayerDispatch = useNonNullableContext(audioPlayerDispatchContext);
20 |
21 | const curPlayedAudioData = playList.find(
22 | (audioData) => audioData.id === curPlayId
23 | );
24 | const audioNativeStates: AudioNativeProps = Object.fromEntries(
25 | Object.entries(curAudioState).filter((state) => {
26 | if (
27 | state[0] === "isPlaying" ||
28 | state[0] === "repeatType" ||
29 | state[0] === "curPlayId" ||
30 | state[0] === "isLoadedMetaData"
31 | ) {
32 | return false;
33 | }
34 | return true;
35 | })
36 | );
37 |
38 | const useAudioEventProps = useAudio();
39 |
40 | useEffect(() => {
41 | if (!audioRef.current) return;
42 |
43 | audioPlayerDispatch({
44 | type: "SET_ELEMENT_REFS",
45 | elementRefs: { audioEl: audioRef.current },
46 | });
47 |
48 | if (propsAudioRef) {
49 | propsAudioRef.current = audioRef.current;
50 | }
51 | }, [audioPlayerDispatch, propsAudioRef]);
52 |
53 | return (
54 |
62 | );
63 | };
64 |
--------------------------------------------------------------------------------
/package/src/components/AudioPlayer/Audio/useAudio.ts:
--------------------------------------------------------------------------------
1 | import { useNonNullableContext } from "@/hooks/useNonNullableContext";
2 | import { getTimeWithPadStart } from "@/utils/getTime";
3 | import { resetAudioValues } from "@/utils/resetAudioValues";
4 | import { HTMLAttributes, SyntheticEvent, useCallback, useEffect } from "react";
5 | import {
6 | audioPlayerStateContext,
7 | audioPlayerDispatchContext,
8 | } from "../Context";
9 |
10 | export const useAudio = (): HTMLAttributes => {
11 | const { curAudioState, elementRefs } = useNonNullableContext(
12 | audioPlayerStateContext
13 | );
14 | const audioPlayerDispatch = useNonNullableContext(audioPlayerDispatchContext);
15 |
16 | // TODO : refactor dependency by exporting
17 | const onTimeUpdate = useCallback(
18 | (event: SyntheticEvent) => {
19 | if (event.currentTarget.readyState === 0 || !elementRefs) return;
20 | const currentTime = event.currentTarget.currentTime;
21 | const duration = event.currentTarget.duration;
22 |
23 | const {
24 | trackCurTimeEl,
25 | progressBarEl,
26 | progressValueEl,
27 | progressHandleEl,
28 | } = elementRefs;
29 | if (trackCurTimeEl) {
30 | trackCurTimeEl.innerText = getTimeWithPadStart(currentTime);
31 | }
32 |
33 | if (progressBarEl && progressValueEl && progressHandleEl) {
34 | const progressBarWidth = progressBarEl.clientWidth;
35 | const progressHandlePosition =
36 | (currentTime / duration) * progressBarWidth;
37 |
38 | progressValueEl.style.transform = `scaleX(${currentTime / duration})`;
39 | progressHandleEl.style.transform = `translateX(${progressHandlePosition}px)`;
40 | }
41 | },
42 | [elementRefs]
43 | );
44 | const onEnded = useCallback(() => {
45 | if (!elementRefs?.audioEl) return;
46 | if (curAudioState.repeatType === "ONE") {
47 | elementRefs.audioEl.currentTime = 0;
48 | elementRefs.audioEl.play();
49 | return;
50 | }
51 | audioPlayerDispatch({ type: "NEXT_AUDIO" });
52 | }, [audioPlayerDispatch, curAudioState.repeatType, elementRefs?.audioEl]);
53 | const onLoadedMetadata = useCallback(
54 | (e: SyntheticEvent) => {
55 | if (!elementRefs) return;
56 |
57 | const { duration } = e.currentTarget;
58 | resetAudioValues(elementRefs, duration);
59 |
60 | audioPlayerDispatch({
61 | type: "SET_AUDIO_STATE",
62 | audioState: { isLoadedMetaData: true },
63 | });
64 | },
65 | [elementRefs]
66 | );
67 |
68 | /** play */
69 | useEffect(() => {
70 | if (!elementRefs?.audioEl) return;
71 | if (curAudioState.isPlaying) {
72 | elementRefs?.audioEl.play();
73 | } else {
74 | elementRefs?.audioEl.pause();
75 | }
76 | }, [elementRefs?.audioEl, curAudioState.isPlaying, audioPlayerDispatch]);
77 |
78 | /** volume */
79 | useEffect(() => {
80 | if (!elementRefs?.audioEl || !curAudioState.volume) return;
81 | elementRefs.audioEl.volume = curAudioState.volume;
82 | }, [elementRefs?.audioEl, curAudioState.volume]);
83 |
84 | return {
85 | onTimeUpdate,
86 | onEnded,
87 | onLoadedMetadata,
88 | };
89 | };
90 |
--------------------------------------------------------------------------------
/package/src/components/AudioPlayer/Context/StateContext/audio.ts:
--------------------------------------------------------------------------------
1 | import { ReactNode } from "react";
2 |
3 | export type AudioNativeProps = Omit<
4 | React.AudioHTMLAttributes,
5 | "autoPlay"
6 | >;
7 |
8 | export type RepeatType = "ALL" | "SHUFFLE" | "ONE" | "NONE";
9 | export type AudioCustomProps = {
10 | isLoadedMetaData?: boolean;
11 | isPlaying?: boolean;
12 | repeatType?: RepeatType;
13 | volume?: number;
14 | currentTime?: number;
15 | duration?: number;
16 | };
17 |
18 | export type AudioData = {
19 | src: string;
20 | id: number;
21 | name?: string | ReactNode;
22 | writer?: string | ReactNode;
23 | img?: string;
24 | description?: string | ReactNode;
25 | customTrackInfo?: string | ReactNode;
26 | };
27 |
28 | export type AudioState = AudioNativeProps & AudioCustomProps;
29 |
--------------------------------------------------------------------------------
/package/src/components/AudioPlayer/Context/StateContext/element.ts:
--------------------------------------------------------------------------------
1 | import { ReactNode } from "react";
2 | import { AudioData } from "./audio";
3 |
4 | export type PlayList = Array;
5 |
6 | export type ProgressUI = "waveform" | "bar" | false;
7 | export type PlayListUI = "sortable" | "unSortable" | false;
8 | export type ActiveUI = Partial<{
9 | all: boolean;
10 | playButton: boolean;
11 | playList: PlayListUI;
12 | prevNnext: boolean;
13 | volume: boolean;
14 | volumeSlider: boolean;
15 | repeatType: boolean;
16 | trackTime: boolean;
17 | trackInfo: boolean;
18 | artwork: boolean;
19 | progress: ProgressUI;
20 | }>;
21 |
22 | export type CustomIcons = Partial<{
23 | play: ReactNode;
24 | pause: ReactNode;
25 | prev: ReactNode;
26 | next: ReactNode;
27 | repeatOne: ReactNode;
28 | repeatAll: ReactNode;
29 | repeatNone: ReactNode;
30 | repeatShuffle: ReactNode;
31 | volumeFull: ReactNode;
32 | volumeHalf: ReactNode;
33 | volumeMuted: ReactNode;
34 | playList: ReactNode;
35 | }>;
36 |
37 | export type ElementRefs = Partial<{
38 | audioEl: HTMLAudioElement;
39 | trackCurTimeEl: HTMLSpanElement;
40 | trackDurationEl: HTMLSpanElement;
41 | progressBarEl: HTMLDivElement;
42 | progressValueEl: HTMLDivElement;
43 | progressHandleEl: HTMLDivElement;
44 | waveformInst: WaveSurfer;
45 | }>;
46 |
47 | export interface CoverImgsCss {
48 | artwork?: React.CSSProperties;
49 | listThumbnail?: React.CSSProperties;
50 | }
51 |
--------------------------------------------------------------------------------
/package/src/components/AudioPlayer/Context/StateContext/index.ts:
--------------------------------------------------------------------------------
1 | import { createContext } from "react";
2 | import { AudioState } from "./audio";
3 | import {
4 | ActiveUI,
5 | CoverImgsCss,
6 | ElementRefs,
7 | CustomIcons,
8 | PlayList,
9 | } from "./element";
10 | import {
11 | PlayListPlacement,
12 | InterfacePlacement,
13 | PlayerPlacement,
14 | VolumeSliderPlacement,
15 | } from "./placement";
16 |
17 | export interface AudioPlayerStateContext {
18 | playList: PlayList;
19 | curPlayId: number;
20 | curIdx: number;
21 | curAudioState: AudioState;
22 | activeUI: ActiveUI;
23 | playListPlacement: PlayListPlacement;
24 | playerPlacement?: PlayerPlacement;
25 | interfacePlacement?: InterfacePlacement;
26 | volumeSliderPlacement?: VolumeSliderPlacement;
27 | elementRefs?: ElementRefs;
28 | customIcons?: CustomIcons;
29 | coverImgsCss?: CoverImgsCss;
30 | }
31 |
32 | export type InitialStates = AudioState & {
33 | curPlayId: number;
34 | };
35 |
36 | export * from "./audio";
37 | export * from "./element";
38 | export * from "./placement";
39 | export const audioPlayerStateContext =
40 | createContext(null);
41 |
--------------------------------------------------------------------------------
/package/src/components/AudioPlayer/Context/StateContext/placement.ts:
--------------------------------------------------------------------------------
1 | import { DropdownContentPlacement } from "@/components/Dropdown";
2 | import { NumbersToUnionNum } from "@/utils/generateUnionNumType";
3 | import { ActiveUI } from "./element";
4 |
5 | export type VolumeSliderPlacement = DropdownContentPlacement;
6 | export type PlayListPlacement = "bottom" | "top";
7 | export type PlayerPlacement =
8 | | "bottom"
9 | | "top"
10 | | "bottom-left"
11 | | "bottom-right"
12 | | "top-left"
13 | | "top-right"
14 | | "static";
15 |
16 | //TODO : declare dynamic length type depending on the number of activeUI;
17 | export const defaultInterfacePlacementMaxLength = 10; // plus 1 for deleted number 0;
18 |
19 | export type InterfacePlacementKey =
20 | | Exclude
21 | | "trackTimeCurrent"
22 | | "trackTimeDuration";
23 |
24 | export type InterfaceGridTemplateArea = Partial<
25 | Record<
26 | InterfacePlacementKey,
27 | `row${NumbersToUnionNum}-${NumbersToUnionNum}`
28 | >
29 | >;
30 |
31 | export type InterfaceGridCustomComponentsArea =
32 | Partial<
33 | Record<
34 | string,
35 | `row${NumbersToUnionNum}-${NumbersToUnionNum}`
36 | >
37 | >;
38 |
39 | export type InterfaceGridItemArea = Partial<
40 | Record
41 | >;
42 |
43 | export type InterfacePlacement<
44 | TMaxLength extends number = typeof defaultInterfacePlacementMaxLength
45 | > = {
46 | templateArea?: InterfaceGridTemplateArea;
47 | customComponentsArea?: InterfaceGridCustomComponentsArea;
48 | itemCustomArea?: InterfaceGridItemArea;
49 | };
50 |
51 | export const defaultInterfacePlacement: {
52 | templateArea: Required<
53 | InterfaceGridTemplateArea
54 | >;
55 | } = {
56 | templateArea: {
57 | artwork: "row1-1",
58 | trackInfo: "row1-2",
59 | trackTimeCurrent: "row1-3",
60 | trackTimeDuration: "row1-4",
61 | progress: "row1-5",
62 | repeatType: "row1-6",
63 | volume: "row1-7",
64 | playButton: "row1-8",
65 | playList: "row1-9",
66 | },
67 | };
68 |
69 | export interface Placements<
70 | TInterfacePlacementLength extends number = typeof defaultInterfacePlacementMaxLength
71 | > {
72 | playListPlacement: PlayListPlacement;
73 | interfacePlacement: InterfacePlacement;
74 | volumeSliderPlacement: VolumeSliderPlacement | undefined;
75 | playerPlacement: PlayerPlacement | undefined;
76 | }
77 |
--------------------------------------------------------------------------------
/package/src/components/AudioPlayer/Context/dispatchContext.ts:
--------------------------------------------------------------------------------
1 | import { Dispatch, createContext } from "react";
2 | import {
3 | RepeatType,
4 | PlayList,
5 | PlayListPlacement,
6 | PlayerPlacement,
7 | ActiveUI,
8 | ElementRefs,
9 | CustomIcons,
10 | InterfacePlacement,
11 | CoverImgsCss,
12 | VolumeSliderPlacement,
13 | defaultInterfacePlacementMaxLength,
14 | AudioState,
15 | } from "./StateContext";
16 |
17 | export type AudioContextAction<
18 | TInterfacePlacementLength extends number = typeof defaultInterfacePlacementMaxLength
19 | > =
20 | | { type: "NEXT_AUDIO" }
21 | | { type: "PREV_AUDIO" }
22 | | { type: "UPDATE_PLAY_LIST"; playList: PlayList }
23 | | { type: "SET_AUDIO_STATE"; audioState: AudioState }
24 | | { type: "SET_INITIAL_STATES"; audioState: AudioState; curPlayId: number }
25 | | { type: "CHANGE_PLAYING_STATE"; state?: boolean }
26 | | { type: "SET_CURRENT_AUDIO"; currentIndex: number; currentAudioId: number }
27 | | { type: "SET_REPEAT_TYPE"; repeatType: RepeatType }
28 | | { type: "SET_VOLUME"; volume: number }
29 | | { type: "SET_MUTED"; muted: boolean }
30 | | { type: "SET_ACTIVE_UI"; activeUI: ActiveUI }
31 | | { type: "SET_ELEMENT_REFS"; elementRefs: ElementRefs }
32 | | { type: "SET_CUSTOM_ICONS"; customIcons: CustomIcons }
33 | | { type: "SET_COVER_IMGS_CSS"; coverImgsCss: CoverImgsCss }
34 | | {
35 | type: "SET_PLACEMENTS";
36 | playerPlacement?: PlayerPlacement;
37 | playListPlacement?: PlayListPlacement;
38 | interfacePlacement?: InterfacePlacement;
39 | volumeSliderPlacement?: VolumeSliderPlacement;
40 | };
41 | export type AudioPlayerDispatchContext = Dispatch;
42 |
43 | export const audioPlayerDispatchContext =
44 | createContext(null);
45 |
--------------------------------------------------------------------------------
/package/src/components/AudioPlayer/Context/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./StateContext";
2 | export * from "./dispatchContext";
3 | export * from "./reducer";
4 |
--------------------------------------------------------------------------------
/package/src/components/AudioPlayer/Context/reducer.ts:
--------------------------------------------------------------------------------
1 | import { getRandomNumber } from "@/utils/getRandomNumber";
2 | import { resetAudioValues } from "@/utils/resetAudioValues";
3 | import { AudioContextAction } from "./dispatchContext";
4 | import { AudioPlayerStateContext } from "./StateContext";
5 |
6 | const getRandomIdx = (curIdx: number, minNumber: number, maxNumber: number) => {
7 | let nextIdx = getRandomNumber(minNumber, maxNumber);
8 | while (nextIdx === curIdx) {
9 | nextIdx = getRandomNumber(minNumber, maxNumber);
10 | }
11 | return nextIdx;
12 | };
13 |
14 | export const audioPlayerReducer = (
15 | state: AudioPlayerStateContext,
16 | action: AudioContextAction
17 | ): AudioPlayerStateContext => {
18 | switch (action.type) {
19 | case "NEXT_AUDIO": {
20 | resetAudioValues(state.elementRefs, undefined, true);
21 |
22 | if (
23 | state.curAudioState.repeatType === "NONE" &&
24 | state.curIdx + 1 === state.playList.length
25 | ) {
26 | return {
27 | ...state,
28 | curAudioState: { ...state.curAudioState, isPlaying: false },
29 | };
30 | }
31 | if (state.curAudioState.repeatType === "SHUFFLE") {
32 | const randomIdx = getRandomIdx(
33 | state.curIdx,
34 | 0,
35 | state.playList.length - 1
36 | );
37 | return {
38 | ...state,
39 | curPlayId: state.playList[randomIdx].id,
40 | curIdx: randomIdx,
41 | curAudioState: {
42 | ...state.curAudioState,
43 | isLoadedMetaData: false,
44 | },
45 | };
46 | }
47 | const infiniteLoopNextIdx = (state.curIdx + 1) % state.playList.length;
48 | return {
49 | ...state,
50 | curIdx: infiniteLoopNextIdx,
51 | curPlayId: state.playList[infiniteLoopNextIdx].id,
52 | };
53 | }
54 | case "PREV_AUDIO": {
55 | if (
56 | (state.elementRefs?.audioEl &&
57 | state.elementRefs?.audioEl.currentTime > 1) ||
58 | (state.elementRefs?.waveformInst &&
59 | state.elementRefs?.waveformInst.getCurrentTime() > 1) ||
60 | (state.curAudioState.repeatType === "NONE" && state.curIdx === 0)
61 | ) {
62 | resetAudioValues(state.elementRefs, undefined, true);
63 | return state;
64 | }
65 | if (state.curAudioState.repeatType === "SHUFFLE") {
66 | const randomIdx = getRandomIdx(
67 | state.curIdx,
68 | 0,
69 | state.playList.length - 1
70 | );
71 | return {
72 | ...state,
73 | curPlayId: state.playList[randomIdx].id,
74 | curIdx: randomIdx,
75 | };
76 | }
77 | const infiniteLoopPrevIdx =
78 | (state.curIdx - 1 + state.playList.length) % state.playList.length;
79 | return {
80 | ...state,
81 | curPlayId: state.playList[infiniteLoopPrevIdx].id,
82 | curIdx: infiniteLoopPrevIdx,
83 | curAudioState: {
84 | ...state.curAudioState,
85 | isLoadedMetaData: false,
86 | },
87 | };
88 | }
89 | case "UPDATE_PLAY_LIST": {
90 | const curPlayListItem = action.playList.find(
91 | (item) => item.id === state.curPlayId
92 | );
93 | if (!curPlayListItem) {
94 | console.error(
95 | "UPDATE_PLAY_LIST ERROR - curPlayId is not found on playList"
96 | );
97 | return state;
98 | }
99 |
100 | const curIdx = action.playList.findIndex(
101 | (item) => item.id === state.curPlayId
102 | );
103 |
104 | return {
105 | ...state,
106 | playList: action.playList,
107 | curIdx,
108 | };
109 | }
110 | case "SET_VOLUME":
111 | return {
112 | ...state,
113 | curAudioState: {
114 | ...state.curAudioState,
115 | volume: action.volume,
116 | },
117 | };
118 | case "SET_AUDIO_STATE":
119 | return {
120 | ...state,
121 | curAudioState: { ...state.curAudioState, ...action.audioState },
122 | };
123 | case "SET_INITIAL_STATES":
124 | return {
125 | ...state,
126 | curAudioState: { ...state.curAudioState, ...action.audioState },
127 | curPlayId: action.curPlayId,
128 | };
129 | case "CHANGE_PLAYING_STATE":
130 | if (action.state !== undefined) {
131 | return {
132 | ...state,
133 | curAudioState: {
134 | ...state.curAudioState,
135 | isPlaying: action.state,
136 | },
137 | };
138 | }
139 | return {
140 | ...state,
141 | curAudioState: {
142 | ...state.curAudioState,
143 | isPlaying: !state.curAudioState.isPlaying,
144 | },
145 | };
146 | case "SET_CURRENT_AUDIO":
147 | return {
148 | ...state,
149 | curPlayId: action.currentAudioId,
150 | curIdx: action.currentIndex,
151 | curAudioState: {
152 | ...state.curAudioState,
153 | isLoadedMetaData: false,
154 | },
155 | };
156 | case "SET_REPEAT_TYPE":
157 | return {
158 | ...state,
159 | curAudioState: {
160 | ...state.curAudioState,
161 | repeatType: action.repeatType,
162 | },
163 | };
164 | case "SET_PLACEMENTS":
165 | return {
166 | ...state,
167 | playerPlacement: action.playerPlacement || state.playerPlacement,
168 | playListPlacement: action.playListPlacement || state.playListPlacement,
169 | interfacePlacement: action.interfacePlacement,
170 | volumeSliderPlacement: action.volumeSliderPlacement,
171 | };
172 | case "SET_MUTED":
173 | return {
174 | ...state,
175 | curAudioState: {
176 | ...state.curAudioState,
177 | muted: action.muted,
178 | },
179 | };
180 | case "SET_ACTIVE_UI":
181 | return {
182 | ...state,
183 | activeUI: { ...action.activeUI },
184 | };
185 | case "SET_ELEMENT_REFS":
186 | return {
187 | ...state,
188 | elementRefs: { ...state.elementRefs, ...action.elementRefs },
189 | };
190 | case "SET_CUSTOM_ICONS":
191 | return {
192 | ...state,
193 | customIcons: { ...state.customIcons, ...action.customIcons },
194 | };
195 | case "SET_COVER_IMGS_CSS":
196 | return {
197 | ...state,
198 | coverImgsCss: { ...state.coverImgsCss, ...action.coverImgsCss },
199 | };
200 | default:
201 | throw new Error("Unhandled action");
202 | }
203 | };
204 |
--------------------------------------------------------------------------------
/package/src/components/AudioPlayer/Interface/Controller/Button/PlayBtn.tsx:
--------------------------------------------------------------------------------
1 | import { FC, useMemo } from "react";
2 | import styled from "styled-components";
3 | import { useNonNullableContext } from "@/hooks/useNonNullableContext";
4 | import { audioPlayerDispatchContext } from "@/components/AudioPlayer/Context/dispatchContext";
5 | import { audioPlayerStateContext } from "@/components/AudioPlayer/Context/StateContext";
6 | import { StyledBtn } from "./StyledBtn";
7 | import { MdPauseCircleFilled, MdPlayCircleFilled } from "react-icons/md";
8 | import { Icon } from "../Icon";
9 |
10 | const StyledPlayBtn = styled(StyledBtn)`
11 | width: 35px;
12 | `;
13 |
14 | export const PlayBtn: FC = () => {
15 | const { curAudioState, customIcons } = useNonNullableContext(
16 | audioPlayerStateContext
17 | );
18 | const audioPlayerDispatch = useNonNullableContext(audioPlayerDispatchContext);
19 |
20 | const changePlayState = () =>
21 | audioPlayerDispatch({ type: "CHANGE_PLAYING_STATE" });
22 | const PlayIcon = useMemo(() => {
23 | if (curAudioState.isPlaying)
24 | return (
25 | }
27 | customIcon={customIcons?.pause}
28 | />
29 | );
30 | return (
31 | } customIcon={customIcons?.play} />
32 | );
33 | }, [curAudioState.isPlaying, customIcons?.pause, customIcons?.play]);
34 |
35 | return (
36 |
37 | {PlayIcon}
38 |
39 | );
40 | };
41 |
--------------------------------------------------------------------------------
/package/src/components/AudioPlayer/Interface/Controller/Button/PlayListTriggerBtn.tsx:
--------------------------------------------------------------------------------
1 | import { audioPlayerStateContext } from "@/components/AudioPlayer/Context";
2 | import { useNonNullableContext } from "@/hooks/useNonNullableContext";
3 | import { FC } from "react";
4 | import { MdPlaylistPlay } from "react-icons/md";
5 | import { Icon } from "../Icon";
6 | import { StyledBtn } from "./StyledBtn";
7 |
8 | export interface PlayListTriggerBtnProps {
9 | isOpen: boolean;
10 | }
11 |
12 | export const PlayListTriggerBtn: FC = ({ isOpen }) => {
13 | const { customIcons } = useNonNullableContext(audioPlayerStateContext);
14 | return (
15 |
16 |
26 | }
27 | customIcon={customIcons?.playList}
28 | />
29 |
30 | );
31 | };
32 |
--------------------------------------------------------------------------------
/package/src/components/AudioPlayer/Interface/Controller/Button/PrevNnextBtn.tsx:
--------------------------------------------------------------------------------
1 | import { useNonNullableContext } from "@/hooks/useNonNullableContext";
2 | import { audioPlayerDispatchContext } from "@/components/AudioPlayer/Context/dispatchContext";
3 | import { audioPlayerStateContext } from "@/components/AudioPlayer/Context/StateContext";
4 | import { FC, useMemo } from "react";
5 | import { StyledBtn } from "./StyledBtn";
6 | import { ImPrevious, ImNext } from "react-icons/im";
7 | import { Icon } from "../Icon";
8 |
9 | interface PrevNnextBtnProps {
10 | type: "prev" | "next";
11 | visible: boolean;
12 | }
13 |
14 | export const PrevNnextBtn: FC = ({ type, visible }) => {
15 | const { customIcons } = useNonNullableContext(audioPlayerStateContext);
16 | const audioPlayerDispatch = useNonNullableContext(audioPlayerDispatchContext);
17 | const changeAudio = () => {
18 | if (type === "next") {
19 | audioPlayerDispatch({ type: "NEXT_AUDIO" });
20 | }
21 | if (type === "prev") {
22 | audioPlayerDispatch({ type: "PREV_AUDIO" });
23 | }
24 | };
25 | const PrevNnextIcon = useMemo(() => {
26 | if (type === "next") {
27 | return } customIcon={customIcons?.next} />;
28 | }
29 | if (type === "prev") {
30 | return } customIcon={customIcons?.prev} />;
31 | }
32 | return null;
33 | }, [customIcons?.next, customIcons?.prev, type]);
34 |
35 | return visible ? (
36 |
37 | {PrevNnextIcon}
38 |
39 | ) : null;
40 | };
41 |
--------------------------------------------------------------------------------
/package/src/components/AudioPlayer/Interface/Controller/Button/RepeatTypeBtn.tsx:
--------------------------------------------------------------------------------
1 | import { FC, useCallback, useMemo } from "react";
2 | import { useNonNullableContext } from "@/hooks/useNonNullableContext";
3 | import { audioPlayerDispatchContext } from "@/components/AudioPlayer/Context/dispatchContext";
4 | import { audioPlayerStateContext } from "@/components/AudioPlayer/Context/StateContext";
5 | import { StyledBtn } from "./StyledBtn";
6 | import {
7 | TbRepeatOff,
8 | TbRepeatOnce,
9 | TbRepeat,
10 | TbArrowsShuffle,
11 | } from "react-icons/tb";
12 | import { Icon } from "../Icon";
13 |
14 | export const RepeatTypeBtn: FC = () => {
15 | const { curAudioState, customIcons } = useNonNullableContext(
16 | audioPlayerStateContext
17 | );
18 | const audioPlayerDispatch = useNonNullableContext(audioPlayerDispatchContext);
19 | const changeRepeatType = useCallback(() => {
20 | switch (curAudioState.repeatType) {
21 | case "ALL":
22 | audioPlayerDispatch({ type: "SET_REPEAT_TYPE", repeatType: "ONE" });
23 | break;
24 | case "ONE":
25 | audioPlayerDispatch({ type: "SET_REPEAT_TYPE", repeatType: "NONE" });
26 | break;
27 | case "NONE":
28 | audioPlayerDispatch({ type: "SET_REPEAT_TYPE", repeatType: "SHUFFLE" });
29 | break;
30 | case "SHUFFLE":
31 | audioPlayerDispatch({ type: "SET_REPEAT_TYPE", repeatType: "ALL" });
32 | break;
33 | default:
34 | break;
35 | }
36 | }, [curAudioState.repeatType, audioPlayerDispatch]);
37 | const RepeatIcon = useMemo(() => {
38 | switch (curAudioState.repeatType) {
39 | case "ALL":
40 | return (
41 | } customIcon={customIcons?.repeatAll} />
42 | );
43 | case "ONE":
44 | return (
45 | } customIcon={customIcons?.repeatOne} />
46 | );
47 | case "NONE":
48 | return (
49 | } customIcon={customIcons?.repeatNone} />
50 | );
51 | case "SHUFFLE":
52 | return (
53 | }
55 | customIcon={customIcons?.repeatShuffle}
56 | />
57 | );
58 | default:
59 | null;
60 | }
61 | }, [
62 | curAudioState.repeatType,
63 | customIcons?.repeatAll,
64 | customIcons?.repeatNone,
65 | customIcons?.repeatOne,
66 | customIcons?.repeatShuffle,
67 | ]);
68 |
69 | return (
70 |
71 | {RepeatIcon}
72 |
73 | );
74 | };
75 |
--------------------------------------------------------------------------------
/package/src/components/AudioPlayer/Interface/Controller/Button/StyledBtn.ts:
--------------------------------------------------------------------------------
1 | import styled from "styled-components";
2 |
3 | export const StyledBtn = styled.button`
4 | display: flex;
5 | width: 20px;
6 | height: 100%;
7 | svg {
8 | width: 100%;
9 | height: 100%;
10 | pointer-events: none;
11 | }
12 |
13 | /** //TODO : animation on off */
14 | &:hover {
15 | transform: scale(1.1);
16 | }
17 | &:active {
18 | transform: scale(0.8);
19 | opacity: 0.5;
20 | transition: all 0.2s ease-out;
21 | }
22 | `;
23 |
--------------------------------------------------------------------------------
/package/src/components/AudioPlayer/Interface/Controller/Button/VolumeTriggerBtn.tsx:
--------------------------------------------------------------------------------
1 | import { useNonNullableContext } from "@/hooks/useNonNullableContext";
2 | import { audioPlayerDispatchContext } from "@/components/AudioPlayer/Context/dispatchContext";
3 | import { audioPlayerStateContext } from "@/components/AudioPlayer/Context/StateContext";
4 | import { forwardRef, useCallback, useMemo } from "react";
5 | import { IconBaseProps } from "react-icons/lib";
6 | import { TbVolume3, TbVolume2, TbVolume } from "react-icons/tb";
7 | import { Icon } from "../Icon";
8 | import { StyledBtn } from "./StyledBtn";
9 |
10 | export const VolumeTriggerBtn = forwardRef((_, ref) => {
11 | const { curAudioState, customIcons, elementRefs } = useNonNullableContext(
12 | audioPlayerStateContext
13 | );
14 | const audioPlayerDispatch = useNonNullableContext(audioPlayerDispatchContext);
15 | const changeMuteState = useCallback(
16 | () =>
17 | audioPlayerDispatch({ type: "SET_MUTED", muted: !curAudioState.muted }),
18 | [audioPlayerDispatch, curAudioState.muted]
19 | );
20 |
21 | const VolumeIcon = useMemo(() => {
22 | const volumeOpt: IconBaseProps = {
23 | size: "100%",
24 | };
25 | if (curAudioState.muted)
26 | return (
27 | }
29 | customIcon={customIcons?.volumeMuted}
30 | />
31 | );
32 | const volumeState = (value: number) => {
33 | if (value === 0) return "mute";
34 | if (value <= 0.5) return "low";
35 | if (value > 0.5) return "high";
36 | };
37 | switch (
38 | volumeState(curAudioState.volume || elementRefs?.audioEl?.volume || 0)
39 | ) {
40 | case "mute":
41 | return (
42 | }
44 | customIcon={customIcons?.volumeMuted}
45 | />
46 | );
47 | case "low":
48 | return (
49 | }
51 | customIcon={customIcons?.volumeHalf}
52 | />
53 | );
54 | case "high":
55 | return (
56 | }
58 | customIcon={customIcons?.volumeFull}
59 | />
60 | );
61 | default:
62 | return null;
63 | }
64 | }, [
65 | curAudioState.muted,
66 | curAudioState.volume,
67 | customIcons?.volumeMuted,
68 | customIcons?.volumeFull,
69 | customIcons?.volumeHalf,
70 | ]);
71 | return (
72 |
77 | {VolumeIcon}
78 |
79 | );
80 | });
81 | VolumeTriggerBtn.displayName = "VolumeTriggerBtn";
82 |
--------------------------------------------------------------------------------
/package/src/components/AudioPlayer/Interface/Controller/Button/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./PlayBtn";
2 | export * from "./PrevNnextBtn";
3 | export * from "./RepeatTypeBtn";
4 | export * from "./PlayListTriggerBtn";
5 | export * from "./VolumeTriggerBtn";
6 |
--------------------------------------------------------------------------------
/package/src/components/AudioPlayer/Interface/Controller/Drawer/SortablePlayList/Content/PlayListItem.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | AudioData,
3 | audioPlayerStateContext,
4 | } from "@/components/AudioPlayer/Context";
5 | import { useNonNullableContext } from "@/hooks/useNonNullableContext";
6 | import classNames from "classnames";
7 | import styled from "styled-components";
8 |
9 | export const PlayListItem = ({ data }: { data: AudioData }) => {
10 | const { curPlayId, coverImgsCss } = useNonNullableContext(
11 | audioPlayerStateContext
12 | );
13 | return (
14 |
19 |
20 |
21 |

22 |
23 |
24 | {data.writer &&
{data.writer}}
25 | {data.name &&
{data.name}}
26 | {data.description && (
27 |
{data.description}
28 | )}
29 |
30 |
31 |
32 | );
33 | };
34 |
35 | const ListItemContainer = styled.div`
36 | width: 100%;
37 | height: 100%;
38 | display: flex;
39 | align-items: center;
40 | padding: 10px 20px;
41 | &.curPlayed {
42 | background: var(--rm-audio-player-selected-list-item-background);
43 | }
44 | .list-item-contents-wrapper {
45 | width: 100%;
46 | display: flex;
47 | gap: 10px;
48 | }
49 | .album-cover-wrapper {
50 | display: flex;
51 | align-items: center;
52 | img {
53 | width: 35px;
54 | height: 35px;
55 | }
56 | }
57 | .album-info-wrapper {
58 | display: grid;
59 | min-width: 10px;
60 | font-size: 13px;
61 | margin-right: 1.5rem;
62 | padding: 2px 0%;
63 | span {
64 | align-self: center;
65 | text-overflow: ellipsis;
66 | white-space: nowrap;
67 | overflow: hidden;
68 | }
69 | }
70 | `;
71 |
--------------------------------------------------------------------------------
/package/src/components/AudioPlayer/Interface/Controller/Drawer/SortablePlayList/Content/index.tsx:
--------------------------------------------------------------------------------
1 | import { audioPlayerStateContext } from "@/components/AudioPlayer/Context";
2 | import { CssTransition } from "@/components/CssTransition";
3 | import SortableList from "@/components/SortableList";
4 | import { useNonNullableContext } from "@/hooks/useNonNullableContext";
5 | import { FC } from "react";
6 | import ReactDOM from "react-dom";
7 | import styled from "styled-components";
8 | import { PlayListItem } from "./PlayListItem";
9 | import { usePlayList } from "./usePlayList";
10 |
11 | export interface SortablePlayListProps {
12 | isOpen: boolean;
13 | setIsOpen: (isOpen: boolean) => void;
14 | }
15 |
16 | export const PlayList: FC = ({ isOpen, setIsOpen }) => {
17 | const { playList } = useNonNullableContext(audioPlayerStateContext);
18 | const { cssTransitionEventProps, sortableItemEventProps } = usePlayList({
19 | setIsOpen,
20 | });
21 | const {
22 | onClick: onClickItem,
23 | onDragStart: onDragStartItem,
24 | ...otherSortableItemEventProps
25 | } = sortableItemEventProps;
26 |
27 | return playList.length !== 0 ? (
28 | ReactDOM.createPortal(
29 |
37 |
38 |
39 | {/** //TODO : change props event to context */}
40 | {playList.map((data, index) => (
41 | onClickItem(index)}
46 | onDragStart={() => onDragStartItem(index)}
47 | {...otherSortableItemEventProps}
48 | >
49 |
50 |
51 | ))}
52 |
53 |
54 | ,
55 | document.querySelector(".sortable-play-list") as HTMLDivElement
56 | )
57 | ) : (
58 | <>>
59 | );
60 | };
61 |
62 | const PlayListContainer = styled.div`
63 | transition-property: max-height, opacity;
64 | overflow-x: hidden;
65 | overflow-y: auto;
66 |
67 | &.playlist-content-enter {
68 | opacity: 0;
69 | max-height: 0;
70 | }
71 | &.playlist-content-enter-active {
72 | opacity: 1;
73 | max-height: 20vh;
74 | transition-duration: 0.5s;
75 | transition-timing-function: cubic-bezier(0, 0, 0, 1.2);
76 | }
77 | &.playlist-content-leave {
78 | opacity: 1;
79 | max-height: 20vh;
80 | }
81 | &.playlist-content-leave-active {
82 | opacity: 0;
83 | max-height: 0;
84 | transition-duration: 0.25s;
85 | transition-timing-function: 0.2s cubic-bezier(0.66, -0.41, 1, 1);
86 | }
87 | `;
88 |
--------------------------------------------------------------------------------
/package/src/components/AudioPlayer/Interface/Controller/Drawer/SortablePlayList/Content/usePlayList.ts:
--------------------------------------------------------------------------------
1 | import {
2 | AudioData,
3 | audioPlayerDispatchContext,
4 | audioPlayerStateContext,
5 | } from "@/components/AudioPlayer/Context";
6 | import { CssTransitionProps } from "@/components/CssTransition";
7 | import { UseSortableListItemProps } from "@/components/SortableList/useSortableListItem";
8 | import { useNonNullableContext } from "@/hooks/useNonNullableContext";
9 | import { useCallback, useState } from "react";
10 | import { SortablePlayListProps } from ".";
11 |
12 | interface UsePlayListReturn {
13 | cssTransitionEventProps: Partial;
14 | sortableItemEventProps: Omit<
15 | UseSortableListItemProps,
16 | "onDragStart" | "onClick" | "index" | "listData"
17 | > & {
18 | onDragStart: (index: number) => void;
19 | onClick: (index: number) => void;
20 | };
21 | }
22 |
23 | export const usePlayList = ({
24 | setIsOpen,
25 | }: {
26 | setIsOpen: SortablePlayListProps["setIsOpen"];
27 | }): UsePlayListReturn => {
28 | const { playList, activeUI } = useNonNullableContext(audioPlayerStateContext);
29 | const audioPlayerDispatch = useNonNullableContext(audioPlayerDispatchContext);
30 |
31 | const [dragStartIdx, setDragStartIdx] = useState(0);
32 |
33 | const onClickItem = useCallback(
34 | (index: number) => {
35 | audioPlayerDispatch({
36 | type: "SET_CURRENT_AUDIO",
37 | currentIndex: index,
38 | currentAudioId: playList[index].id,
39 | });
40 | },
41 | [audioPlayerDispatch, playList]
42 | );
43 | return {
44 | cssTransitionEventProps: {
45 | onExited: () => setIsOpen(false),
46 | onEntered: () => setIsOpen(true),
47 | },
48 | sortableItemEventProps: {
49 | draggable: activeUI.playList !== "unSortable" ?? true,
50 | dragStartIdx,
51 | onDragStart: (index) => setDragStartIdx(index),
52 | onDrop: (e, newPlayList) =>
53 | audioPlayerDispatch({
54 | type: "UPDATE_PLAY_LIST",
55 | playList: newPlayList,
56 | }),
57 | onClick: (index) => onClickItem(index),
58 | },
59 | };
60 | };
61 |
--------------------------------------------------------------------------------
/package/src/components/AudioPlayer/Interface/Controller/Drawer/SortablePlayList/index.tsx:
--------------------------------------------------------------------------------
1 | import { FC, useState } from "react";
2 | import Drawer from "@/components/Drawer";
3 | import { PlayList } from "./Content";
4 | import { PlayListTriggerBtn } from "../../Button";
5 |
6 | export const SortablePlayList: FC = () => {
7 | const [isOpen, setIsOpen] = useState(false);
8 |
9 | return (
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | );
19 | };
20 |
--------------------------------------------------------------------------------
/package/src/components/AudioPlayer/Interface/Controller/Drawer/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./SortablePlayList";
2 |
--------------------------------------------------------------------------------
/package/src/components/AudioPlayer/Interface/Controller/Icon.tsx:
--------------------------------------------------------------------------------
1 | import { FC, ReactElement, ReactNode } from "react";
2 | import { IconType } from "react-icons/lib";
3 |
4 | interface _IconProps {
5 | render: ReactElement;
6 | customIcon?: ReactNode;
7 | }
8 |
9 | export const Icon: FC<_IconProps> = ({ render, customIcon }) => {
10 | return <>{customIcon ?? render}>;
11 | };
12 |
--------------------------------------------------------------------------------
/package/src/components/AudioPlayer/Interface/Controller/Input/Progress/BarProgress.tsx:
--------------------------------------------------------------------------------
1 | import { audioPlayerStateContext } from "@/components/AudioPlayer/Context";
2 | import { useNonNullableContext } from "@/hooks/useNonNullableContext";
3 | import { useRefsDispatch } from "@/hooks/useRefsDispatch";
4 | import { FC, useEffect, useRef } from "react";
5 | import styled from "styled-components";
6 | import { useProgress } from "./useProgress";
7 |
8 | export const BarProgress: FC<{ isActive: boolean }> = ({ isActive }) => {
9 | const progressBarRef = useRef(null);
10 | const progressValueRef = useRef(null);
11 | const progressHandleRef = useRef(null);
12 | useRefsDispatch(
13 | {
14 | refs: {
15 | progressBarEl: progressBarRef,
16 | progressValueEl: progressValueRef,
17 | progressHandleEl: progressHandleRef,
18 | },
19 | },
20 | [isActive]
21 | );
22 |
23 | const { elementRefs, curAudioState } = useNonNullableContext(
24 | audioPlayerStateContext
25 | );
26 | useEffect(() => {
27 | if (
28 | !progressBarRef.current ||
29 | !progressValueRef.current ||
30 | !progressHandleRef.current ||
31 | !elementRefs?.audioEl ||
32 | !curAudioState.isLoadedMetaData ||
33 | curAudioState.isPlaying
34 | )
35 | return;
36 |
37 | const progressBarWidth = progressBarRef.current.clientWidth;
38 | const progressHandlePosition =
39 | (elementRefs.audioEl.currentTime / elementRefs.audioEl.duration) *
40 | progressBarWidth;
41 |
42 | progressValueRef.current.style.transform = `scaleX(${
43 | elementRefs.audioEl.currentTime / elementRefs.audioEl.duration
44 | })`;
45 | progressHandleRef.current.style.transform = `translateX(${progressHandlePosition}px)`;
46 | }, [isActive, curAudioState.isLoadedMetaData]);
47 |
48 | const eventProps = useProgress();
49 |
50 | return isActive ? (
51 |
52 |
55 |
56 |
57 | ) : null;
58 | };
59 |
60 | const BarProgressWrapper = styled.div`
61 | display: flex;
62 | width: 100%;
63 | height: 18px;
64 | padding: 8px 0;
65 | cursor: pointer;
66 | position: relative;
67 | align-items: center;
68 | .rm-player-progress-bar {
69 | position: relative;
70 | width: 100%;
71 | height: 100%;
72 | overflow: hidden;
73 | background-color: var(--rm-audio-player-progress-bar-background);
74 | }
75 | .rm-player-progress {
76 | position: absolute;
77 | left: 0;
78 | width: 100%;
79 | height: 100%;
80 | background-color: var(--rm-audio-player-progress-bar);
81 | transform-origin: 0 0;
82 | transform: scaleX(0);
83 | }
84 | .rm-player-progress-handle {
85 | position: absolute;
86 | left: -4px;
87 | background-color: var(--rm-audio-player-progress-bar);
88 | border-radius: 100%;
89 | height: 8px;
90 | width: 8px;
91 | opacity: 0;
92 | transition: opacity 0.2s ease-in-out;
93 | }
94 | &:hover {
95 | .rm-player-progress-handle {
96 | opacity: 1;
97 | }
98 | }
99 | `;
100 |
--------------------------------------------------------------------------------
/package/src/components/AudioPlayer/Interface/Controller/Input/Progress/WaveformProgress.tsx:
--------------------------------------------------------------------------------
1 | import { audioPlayerStateContext } from "@/components/AudioPlayer/Context";
2 | import { useNonNullableContext } from "@/hooks/useNonNullableContext";
3 | import { FC, useEffect, useRef } from "react";
4 | import styled, { css } from "styled-components";
5 | import { useProgress } from "./useProgress";
6 | import { useWaveSurfer } from "./useWavesurfer";
7 |
8 | const WaveformWrapper = styled.div`
9 | ${({ isActive }: { isActive: boolean }) => css`
10 | display: flex;
11 | width: 100%;
12 | #rm-waveform {
13 | width: 100%;
14 | wave {
15 | cursor: pointer !important;
16 | }
17 |
18 | ${!isActive &&
19 | css`
20 | height: 0;
21 | opacity: 0;
22 | pointer-events: none;
23 | `}
24 | }
25 | `}
26 | `;
27 |
28 | export const WaveformProgress: FC<{ isActive: boolean }> = ({ isActive }) => {
29 | const waveformRef = useRef(null);
30 | const { elementRefs, curAudioState } = useNonNullableContext(
31 | audioPlayerStateContext
32 | );
33 |
34 | useWaveSurfer(waveformRef);
35 |
36 | // apply current time to waveform when progress is active
37 | useEffect(() => {
38 | if (
39 | !isActive ||
40 | !elementRefs?.waveformInst ||
41 | !elementRefs?.audioEl ||
42 | !curAudioState.isLoadedMetaData ||
43 | curAudioState.isPlaying
44 | )
45 | return;
46 |
47 | elementRefs.waveformInst.seekTo(
48 | elementRefs.audioEl.currentTime / elementRefs.audioEl.duration
49 | );
50 | }, [isActive, curAudioState.isLoadedMetaData]);
51 |
52 | const eventProps = useProgress();
53 | return (
54 |
55 |
56 |
57 | );
58 | };
59 |
--------------------------------------------------------------------------------
/package/src/components/AudioPlayer/Interface/Controller/Input/Progress/index.tsx:
--------------------------------------------------------------------------------
1 | import { useNonNullableContext } from "@/hooks/useNonNullableContext";
2 | import { audioPlayerStateContext } from "@/components/AudioPlayer/Context/StateContext";
3 | import { FC } from "react";
4 | import styled from "styled-components";
5 | import { BarProgress } from "./BarProgress";
6 | import { WaveformProgress } from "./WaveformProgress";
7 |
8 | const ProgressContainer = styled.div`
9 | min-width: 100px;
10 | `;
11 |
12 | export const Progress: FC = () => {
13 | const { activeUI } = useNonNullableContext(audioPlayerStateContext);
14 |
15 | return (
16 |
17 |
18 |
19 |
20 | );
21 | };
22 |
--------------------------------------------------------------------------------
/package/src/components/AudioPlayer/Interface/Controller/Input/Progress/useProgress.ts:
--------------------------------------------------------------------------------
1 | import { audioPlayerStateContext } from "@/components/AudioPlayer/Context";
2 | import { useNonNullableContext } from "@/hooks/useNonNullableContext";
3 | import { HTMLAttributes, useCallback, useState, MouseEvent } from "react";
4 |
5 | export const useProgress = (): HTMLAttributes => {
6 | const { elementRefs, curAudioState } = useNonNullableContext(
7 | audioPlayerStateContext
8 | );
9 | const [isTimeChangeActive, setTimeChangeActive] = useState(false);
10 |
11 | const moveAudioTime = useCallback(
12 | (e: MouseEvent) => {
13 | if (!elementRefs?.audioEl || !curAudioState?.isLoadedMetaData) return;
14 | const { clientX } = e;
15 | const { clientWidth } = e.currentTarget;
16 | const boundingRect = e.currentTarget.getBoundingClientRect();
17 | const curPositionX = clientX - boundingRect.x;
18 | const curPositionPercent = curPositionX / clientWidth;
19 | const curPositionTime = curPositionPercent * elementRefs.audioEl.duration;
20 | elementRefs.audioEl.currentTime = curPositionTime;
21 | },
22 | [curAudioState?.isLoadedMetaData, elementRefs?.audioEl]
23 | );
24 |
25 | const setSelectStartActive = useCallback(
26 | (state: boolean) => (document.onselectstart = () => state),
27 | []
28 | );
29 |
30 | return {
31 | onMouseDown: () => setTimeChangeActive(true),
32 | onMouseUp: () => setTimeChangeActive(false),
33 | onMouseLeave: () => setTimeChangeActive(false),
34 | onMouseMove: isTimeChangeActive ? moveAudioTime : undefined,
35 | onClick: moveAudioTime,
36 | onMouseOver: () => setSelectStartActive(false),
37 | onMouseOut: () => isTimeChangeActive && setSelectStartActive(true),
38 | };
39 | };
40 |
--------------------------------------------------------------------------------
/package/src/components/AudioPlayer/Interface/Controller/Input/Progress/useWavesurfer.ts:
--------------------------------------------------------------------------------
1 | import { useNonNullableContext } from "@/hooks/useNonNullableContext";
2 | import { useVariableColor } from "@/hooks/useVariableColor";
3 | import { audioPlayerDispatchContext } from "@/components/AudioPlayer/Context/dispatchContext";
4 | import { audioPlayerStateContext } from "@/components/AudioPlayer/Context/StateContext";
5 | import { useEffect } from "react";
6 | import WaveSurfer from "wavesurfer.js";
7 |
8 | const waveformColors = {
9 | progressColor: "--rm-audio-player-waveform-bar",
10 | waveColor: "--rm-audio-player-waveform-background",
11 | };
12 |
13 | // TODO : dynamic drawing form from large files
14 |
15 | export const useWaveSurfer = (waveformRef: React.RefObject) => {
16 | const audioPlayerDispatch = useNonNullableContext(audioPlayerDispatchContext);
17 | const { elementRefs, curPlayId, curAudioState } = useNonNullableContext(
18 | audioPlayerStateContext
19 | );
20 | const colorsRef = useVariableColor(waveformColors);
21 |
22 | /** init waveSurfer */
23 | useEffect(() => {
24 | if (elementRefs?.waveformInst || !colorsRef.current) return;
25 |
26 | const waveSurfer = WaveSurfer.create({
27 | barWidth: 1,
28 | cursorWidth: 2,
29 | container: "#rm-waveform",
30 | height: 80,
31 | progressColor: `${colorsRef.current.progressColor}`,
32 | responsive: true,
33 | waveColor: `${colorsRef.current.waveColor}`,
34 | cursorColor: "var(--rm-audio-player-waveform-cursor)",
35 | backend: "MediaElement",
36 | removeMediaElementOnDestroy: false,
37 | });
38 |
39 | audioPlayerDispatch({
40 | type: "SET_ELEMENT_REFS",
41 | elementRefs: { waveformInst: waveSurfer },
42 | });
43 | }, [elementRefs?.waveformInst, audioPlayerDispatch, colorsRef]);
44 |
45 | // TODO : preserve audio state when loading new audio
46 | /** load audio */
47 | useEffect(() => {
48 | if (!elementRefs?.audioEl || !elementRefs?.waveformInst) return;
49 | elementRefs.audioEl.pause();
50 | elementRefs.waveformInst.load(elementRefs?.audioEl);
51 |
52 | if (curAudioState.volume) {
53 | elementRefs.audioEl.volume = curAudioState.volume;
54 | }
55 |
56 | if (curAudioState.isPlaying) elementRefs?.audioEl?.play();
57 | }, [curPlayId, elementRefs?.audioEl, elementRefs?.waveformInst]);
58 |
59 | // set waveform responsively
60 | useEffect(() => {
61 | if (!waveformRef.current || !elementRefs?.waveformInst) return;
62 |
63 | const redrawWaveform = () => {
64 | elementRefs.waveformInst?.drawBuffer();
65 | };
66 | const resizeObserver = new ResizeObserver(redrawWaveform);
67 | resizeObserver.observe(waveformRef.current);
68 |
69 | return () => {
70 | resizeObserver.disconnect();
71 | };
72 | }, [elementRefs?.waveformInst, waveformRef]);
73 |
74 | /** delete empty wave surfer */
75 | useEffect(
76 | () => () => {
77 | const waveEl = document.getElementsByTagName("wave");
78 | if (waveEl.length) {
79 | waveEl[0].remove();
80 | audioPlayerDispatch({
81 | type: "SET_ELEMENT_REFS",
82 | elementRefs: { waveformInst: undefined },
83 | });
84 | elementRefs?.waveformInst?.destroy();
85 | }
86 | },
87 | []
88 | );
89 | };
90 |
--------------------------------------------------------------------------------
/package/src/components/AudioPlayer/Interface/Controller/Input/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./Progress";
2 |
--------------------------------------------------------------------------------
/package/src/components/AudioPlayer/Interface/Controller/Tooltip/Volume/Content.tsx:
--------------------------------------------------------------------------------
1 | import { useNonNullableContext } from "@/hooks/useNonNullableContext";
2 | import { audioPlayerDispatchContext } from "@/components/AudioPlayer/Context/dispatchContext";
3 | import {
4 | audioPlayerStateContext,
5 | VolumeSliderPlacement,
6 | } from "@/components/AudioPlayer/Context/StateContext";
7 | import { ChangeEvent, FC, useCallback, useRef } from "react";
8 | import styled, { css } from "styled-components";
9 |
10 | export const VolumeSlider: FC<{ placement: VolumeSliderPlacement }> = ({
11 | placement,
12 | }) => {
13 | const contentRef = useRef(null);
14 | const { curAudioState, elementRefs } = useNonNullableContext(
15 | audioPlayerStateContext
16 | );
17 | const audioPlayerDispatch = useNonNullableContext(audioPlayerDispatchContext);
18 |
19 | const onChangeVolume = useCallback(
20 | (e: ChangeEvent) => {
21 | e.stopPropagation();
22 | e.preventDefault();
23 | if (curAudioState.muted) {
24 | audioPlayerDispatch({ type: "SET_MUTED", muted: false });
25 | }
26 |
27 | const { value } = e.target;
28 | const parsedValue = parseFloat(value);
29 | audioPlayerDispatch({
30 | type: "SET_VOLUME",
31 | volume: parsedValue,
32 | });
33 | },
34 | [curAudioState.muted, audioPlayerDispatch]
35 | );
36 | return (
37 |
45 |
46 |
56 |
57 |
58 | );
59 | };
60 |
61 | const VolumeSliderContainer = styled.div`
62 | ${({
63 | contentPlacement,
64 | volumeValue,
65 | }: {
66 | contentPlacement?: VolumeSliderPlacement;
67 | volumeValue: number;
68 | }) => css`
69 | --rm-audio-player-volume-value: ${volumeValue}%;
70 | position: relative;
71 | height: 119px;
72 | width: 32px;
73 | display: flex;
74 | align-items: center;
75 | justify-content: center;
76 | ${contentPlacement === "top" &&
77 | css`
78 | bottom: auto;
79 | `}
80 |
81 | ${contentPlacement === "left" &&
82 | css`
83 | transform: rotate(-90deg);
84 | right: 50px;
85 | `}
86 |
87 | ${contentPlacement === "right" &&
88 | css`
89 | transform: rotate(90deg);
90 | left: 50px;
91 | `}
92 |
93 | ${contentPlacement === "bottom" &&
94 | css`
95 | transform: rotateX(180deg);
96 | top: 5px;
97 | `}
98 |
99 | .volume-panel-wrapper {
100 | width: 30px;
101 | background-color: var(--rm-audio-player-volume-panel-background);
102 | border: 1px solid var(--rm-audio-player-volume-panel-border);
103 | border-radius: 5px;
104 | height: 118px;
105 | box-shadow: 0 2px 4px rgb(0 0 0 /10%);
106 | position: absolute;
107 | bottom: 5px;
108 |
109 | &:before {
110 | content: "";
111 | bottom: -10px;
112 | left: 7.9px;
113 | border-color: transparent transparent
114 | var(--rm-audio-player-volume-panel-border)
115 | var(--rm-audio-player-volume-panel-border);
116 | border-style: solid;
117 | border-width: 5px;
118 | box-shadow: -3px 3px 4px rgb(0 0 0 / 10%);
119 | position: absolute;
120 | width: 0;
121 | height: 0;
122 | box-sizing: border-box;
123 | -webkit-transform-origin: 0 0;
124 | transform-origin: 0 0;
125 | -webkit-transform: rotate(-45deg);
126 | transform: rotate(-45deg);
127 | pointer-events: none;
128 | z-index: 0;
129 | }
130 | &:after {
131 | content: "";
132 | bottom: -8px;
133 | left: 9px;
134 | border-color: transparent transparent
135 | var(--rm-audio-player-volume-panel-background)
136 | var(--rm-audio-player-volume-panel-background);
137 | border-style: solid;
138 | border-width: 4px;
139 | z-index: 1;
140 | position: absolute;
141 | width: 0;
142 | height: 0;
143 | box-sizing: border-box;
144 | -webkit-transform-origin: 0 0;
145 | transform-origin: 0 0;
146 | -webkit-transform: rotate(-45deg);
147 | transform: rotate(-45deg);
148 | pointer-events: none;
149 | }
150 | }
151 |
152 | input {
153 | &[type="range"] {
154 | margin-left: 14px;
155 | position: absolute;
156 | display: block;
157 | top: -45px;
158 | left: 0;
159 | height: 2px;
160 | width: 92px;
161 | -webkit-appearance: none;
162 | background-color: var(--rm-audio-player-volume-background);
163 | outline-color: transparent;
164 | transform-origin: 75px 75px;
165 | transform: rotate(-90deg);
166 | }
167 |
168 | &:focus {
169 | outline-color: transparent;
170 | }
171 |
172 | &::-webkit-slider-thumb {
173 | -webkit-appearance: none;
174 | width: 16px;
175 | height: 16px;
176 | border-radius: 12px;
177 | overflow: visible;
178 | background: var(--rm-audio-player-volume-thumb);
179 | }
180 |
181 | &::-moz-range-thumb {
182 | width: 16px;
183 | height: 16px;
184 | border-radius: 12px;
185 | overflow: visible;
186 | background: var(--rm-audio-player-volume-thumb);
187 | border: none;
188 | }
189 | &::-moz-range-track {
190 | -webkit-appearance: none;
191 | appearance: none;
192 | display: block;
193 | overflow: visible;
194 | color: transparent;
195 | cursor: pointer;
196 | border-radius: 2%/50%;
197 | border-color: transparent;
198 | background-color: transparent;
199 | background-position: center;
200 | background-repeat: no-repeat;
201 | background-size: 100% 3px;
202 | background-image: linear-gradient(
203 | 90deg,
204 | var(--rm-audio-player-volume-fill) var(--rm-audio-player-volume-value),
205 | var(--rm-audio-player-volume-track)
206 | var(--rm-audio-player-volume-value)
207 | );
208 | }
209 |
210 | &::-webkit-slider-runnable-track {
211 | -webkit-appearance: none;
212 | appearance: none;
213 | display: block;
214 | overflow: visible;
215 | color: transparent;
216 | cursor: pointer;
217 | border-radius: 2%/50%;
218 | border-color: transparent;
219 | background-color: transparent;
220 | background-position: center;
221 | background-repeat: no-repeat;
222 | background-size: 100% 3px;
223 | background-image: linear-gradient(
224 | 90deg,
225 | var(--rm-audio-player-volume-fill) var(--rm-audio-player-volume-value),
226 | var(--rm-audio-player-volume-track)
227 | var(--rm-audio-player-volume-value)
228 | );
229 | }
230 | }
231 | `}
232 | `;
233 |
--------------------------------------------------------------------------------
/package/src/components/AudioPlayer/Interface/Controller/Tooltip/Volume/Trigger.tsx:
--------------------------------------------------------------------------------
1 | import { useNonNullableContext } from "@/hooks/useNonNullableContext";
2 | import { audioPlayerDispatchContext } from "@/components/AudioPlayer/Context/dispatchContext";
3 | import { audioPlayerStateContext } from "@/components/AudioPlayer/Context/StateContext";
4 | import { forwardRef, useCallback, useMemo } from "react";
5 | import { IconBaseProps } from "react-icons/lib";
6 | import { TbVolume3, TbVolume2, TbVolume } from "react-icons/tb";
7 | import styled from "styled-components";
8 | import { Icon } from "../../Icon";
9 |
10 | const TriggerContainer = styled.div`
11 | width: 20px;
12 | height: 20px;
13 | display: flex;
14 | align-items: center;
15 | justify-content: center;
16 | `;
17 | export const Trigger = forwardRef((_, ref) => {
18 | const { curAudioState, customIcons, elementRefs } = useNonNullableContext(
19 | audioPlayerStateContext
20 | );
21 | const audioPlayerDispatch = useNonNullableContext(audioPlayerDispatchContext);
22 | const changeMuteState = useCallback(
23 | () =>
24 | audioPlayerDispatch({ type: "SET_MUTED", muted: !curAudioState.muted }),
25 | [audioPlayerDispatch, curAudioState.muted]
26 | );
27 |
28 | const VolumeIcon = useMemo(() => {
29 | const volumeOpt: IconBaseProps = {
30 | size: "100%",
31 | };
32 | if (curAudioState.muted)
33 | return (
34 | }
36 | customIcon={customIcons?.volumeMuted}
37 | />
38 | );
39 | const volumeState = (value: number) => {
40 | if (value === 0) return "mute";
41 | if (value <= 0.5) return "low";
42 | if (value > 0.5) return "high";
43 | };
44 | switch (
45 | volumeState(curAudioState.volume || elementRefs?.audioEl?.volume || 0)
46 | ) {
47 | case "mute":
48 | return (
49 | }
51 | customIcon={customIcons?.volumeMuted}
52 | />
53 | );
54 | case "low":
55 | return (
56 | }
58 | customIcon={customIcons?.volumeHalf}
59 | />
60 | );
61 | case "high":
62 | return (
63 | }
65 | customIcon={customIcons?.volumeFull}
66 | />
67 | );
68 | default:
69 | return null;
70 | }
71 | }, [
72 | curAudioState.muted,
73 | curAudioState.volume,
74 | customIcons?.volumeMuted,
75 | customIcons?.volumeFull,
76 | customIcons?.volumeHalf,
77 | elementRefs?.audioEl?.volume,
78 | ]);
79 | return (
80 |
85 | {VolumeIcon}
86 |
87 | );
88 | });
89 | Trigger.displayName = "Trigger";
90 |
--------------------------------------------------------------------------------
/package/src/components/AudioPlayer/Interface/Controller/Tooltip/Volume/index.tsx:
--------------------------------------------------------------------------------
1 | import { FC, useRef } from "react";
2 | import { VolumeSlider } from "./Content";
3 | import Dropdown from "@/components/Dropdown";
4 | import { VolumeTriggerBtn } from "../../Button";
5 | import { useNonNullableContext } from "@/hooks/useNonNullableContext";
6 | import { audioPlayerStateContext } from "@/components/AudioPlayer/Context";
7 | import { useVolumeSliderPlacement } from "./useVolume";
8 |
9 | // TODO : apply event callback props
10 |
11 | export const Volume: FC = () => {
12 | const triggerRef = useRef(null);
13 | const {
14 | activeUI: { volumeSlider: volumeSliderEl },
15 | volumeSliderPlacement: contextVolumePlacement,
16 | } = useNonNullableContext(audioPlayerStateContext);
17 | const volumeSliderPlacement = useVolumeSliderPlacement({
18 | triggerRef,
19 | initialState: "bottom",
20 | });
21 |
22 | return (
23 |
28 |
29 |
30 |
31 |
32 |
35 |
36 |
37 | );
38 | };
39 |
--------------------------------------------------------------------------------
/package/src/components/AudioPlayer/Interface/Controller/Tooltip/Volume/useVolume.ts:
--------------------------------------------------------------------------------
1 | import {
2 | audioPlayerStateContext,
3 | VolumeSliderPlacement,
4 | } from "@/components/AudioPlayer/Context";
5 | import { useNonNullableContext } from "@/hooks/useNonNullableContext";
6 | import { useState, useEffect } from "react";
7 |
8 | export const useVolumeSliderPlacement = ({
9 | triggerRef,
10 | initialState,
11 | }: {
12 | triggerRef: React.RefObject;
13 | initialState: VolumeSliderPlacement;
14 | }) => {
15 | const { playerPlacement } = useNonNullableContext(audioPlayerStateContext);
16 | const [volumeSliderPlacement, setVolumeSliderPlacement] =
17 | useState(initialState);
18 |
19 | useEffect(() => {
20 | if (triggerRef.current) {
21 | const placementValidation = () => {
22 | if (
23 | triggerRef.current!.getBoundingClientRect().top <
24 | window.innerHeight / 2
25 | ) {
26 | return "bottom";
27 | }
28 | return "top";
29 | };
30 |
31 | const volumeSliderPlacementTimeout = setTimeout(() => {
32 | setVolumeSliderPlacement(placementValidation());
33 | }, 0);
34 | return () => {
35 | clearTimeout(volumeSliderPlacementTimeout);
36 | };
37 | }
38 | }, [playerPlacement, triggerRef]);
39 | return volumeSliderPlacement;
40 | };
41 |
--------------------------------------------------------------------------------
/package/src/components/AudioPlayer/Interface/Controller/Tooltip/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./Volume";
2 |
--------------------------------------------------------------------------------
/package/src/components/AudioPlayer/Interface/Controller/index.tsx:
--------------------------------------------------------------------------------
1 | import { FC } from "react";
2 | import { useNonNullableContext } from "@/hooks/useNonNullableContext";
3 | import {
4 | audioPlayerStateContext,
5 | defaultInterfacePlacement,
6 | } from "@/components/AudioPlayer/Context/StateContext";
7 | import { PlayBtn, PrevNnextBtn, RepeatTypeBtn } from "./Button";
8 | import { SortablePlayList } from "./Drawer";
9 | import { Progress } from "./Input";
10 | import { Flex } from "@react-spectrum/layout";
11 | import Grid from "@/components/Grid";
12 | import { Volume } from "./Tooltip";
13 |
14 | export const Controller: FC = () => {
15 | const { interfacePlacement, activeUI } = useNonNullableContext(
16 | audioPlayerStateContext
17 | );
18 |
19 | return (
20 | <>
21 |
32 |
33 |
34 |
42 |
43 |
44 |
52 |
53 |
57 |
58 |
62 |
63 |
64 |
72 |
73 |
74 |
82 |
83 |
84 | >
85 | );
86 | };
87 |
--------------------------------------------------------------------------------
/package/src/components/AudioPlayer/Interface/CustomComponent/index.tsx:
--------------------------------------------------------------------------------
1 | import Grid from "@/components/Grid";
2 | import { GridItemProps } from "@/components/Grid/Item";
3 | import { useNonNullableContext } from "@/hooks/useNonNullableContext";
4 | import React, { FC } from "react";
5 | import { audioPlayerStateContext } from "../../Context";
6 |
7 | // TODO : apply collection component
8 |
9 | export type CustomComponentProps = {
10 | children?: React.ReactNode;
11 | id: string;
12 | } & GridItemProps;
13 |
14 | export const CustomComponent: FC = ({
15 | children,
16 | id,
17 | ...gridItemProps
18 | }) => {
19 | const audioPlayerState = useNonNullableContext(audioPlayerStateContext);
20 |
21 | const placement = audioPlayerState.interfacePlacement;
22 | const gridArea = placement?.customComponentsArea?.[id];
23 |
24 | return (
25 |
30 | {React.cloneElement(children as React.ReactElement, { audioPlayerState })}
31 |
32 | );
33 | };
34 |
--------------------------------------------------------------------------------
/package/src/components/AudioPlayer/Interface/Information/Artwork.tsx:
--------------------------------------------------------------------------------
1 | import { useNonNullableContext } from "@/hooks/useNonNullableContext";
2 | import { audioPlayerStateContext } from "@/components/AudioPlayer/Context/StateContext";
3 | import { FC } from "react";
4 | import styled from "styled-components";
5 |
6 | const ArtworkContainer = styled.div`
7 | display: flex;
8 | align-items: center;
9 | width: 100%;
10 | height: 100%;
11 | img {
12 | width: 50px;
13 | height: 50px;
14 | }
15 | `;
16 |
17 | export const Artwork: FC = () => {
18 | const { playList, curIdx, coverImgsCss } = useNonNullableContext(
19 | audioPlayerStateContext
20 | );
21 |
22 | return (
23 |
24 |
25 |
26 | );
27 | };
28 |
--------------------------------------------------------------------------------
/package/src/components/AudioPlayer/Interface/Information/TrackInfo.tsx:
--------------------------------------------------------------------------------
1 | import { useNonNullableContext } from "@/hooks/useNonNullableContext";
2 | import { audioPlayerStateContext } from "@/components/AudioPlayer/Context/StateContext";
3 | import { FC } from "react";
4 | import styled from "styled-components";
5 |
6 | const TrackInfoContainer = styled.div`
7 | display: grid;
8 | align-items: center;
9 | row-gap: 5px;
10 | width: 200px;
11 | span {
12 | text-overflow: ellipsis;
13 | white-space: nowrap;
14 | overflow: hidden;
15 | }
16 | .title {
17 | font-size: 16px;
18 | }
19 | .writer {
20 | font-size: 12px;
21 | }
22 | `;
23 |
24 | export const TrackInfo: FC = () => {
25 | const { playList, curIdx } = useNonNullableContext(audioPlayerStateContext);
26 | const curPlayData = playList[curIdx];
27 | return (
28 |
29 | {curPlayData?.customTrackInfo ? (
30 | curPlayData.customTrackInfo
31 | ) : (
32 | <>
33 | {curPlayData?.name && (
34 | {curPlayData.name}
35 | )}
36 | {curPlayData?.writer && (
37 | {curPlayData.writer}
38 | )}
39 | >
40 | )}
41 |
42 | );
43 | };
44 |
--------------------------------------------------------------------------------
/package/src/components/AudioPlayer/Interface/Information/TrackTime/Current.tsx:
--------------------------------------------------------------------------------
1 | import { FC, useRef } from "react";
2 | import styled from "styled-components";
3 | import { TrackTimeContainer } from "./Styles";
4 | import { TrackTimeChildrenProps } from "./Types";
5 | import { useRefsDispatch } from "@/hooks/useRefsDispatch";
6 | import { audioPlayerStateContext } from "@/components/AudioPlayer/Context";
7 | import { useNonNullableContext } from "@/hooks/useNonNullableContext";
8 | import { getTimeWithPadStart } from "@/utils/getTime";
9 |
10 | export const Current: FC = ({ position }) => {
11 | const trackCurTimeRef = useRef(null);
12 | const { elementRefs } = useNonNullableContext(audioPlayerStateContext);
13 |
14 | useRefsDispatch(
15 | {
16 | refs: { trackCurTimeEl: trackCurTimeRef },
17 | },
18 | []
19 | );
20 |
21 | return (
22 |
27 |
28 | {elementRefs?.audioEl?.currentTime
29 | ? getTimeWithPadStart(elementRefs.audioEl.currentTime)
30 | : "00:00"}
31 |
32 |
33 | );
34 | };
35 |
36 | const TrackTimeCurrentContainer = styled(TrackTimeContainer)`
37 | .track-current-time {
38 | font-weight: 700;
39 | letter-spacing: -0.1rem;
40 | color: var(--rm-audio-player-track-current-time);
41 | }
42 | `;
43 |
--------------------------------------------------------------------------------
/package/src/components/AudioPlayer/Interface/Information/TrackTime/Duration.tsx:
--------------------------------------------------------------------------------
1 | import { FC, useRef } from "react";
2 | import styled from "styled-components";
3 | import { TrackTimeContainer } from "./Styles";
4 | import { TrackTimeChildrenProps } from "./Types";
5 | import { useRefsDispatch } from "@/hooks/useRefsDispatch";
6 | import { useNonNullableContext } from "@/hooks/useNonNullableContext";
7 | import { audioPlayerStateContext } from "@/components/AudioPlayer/Context";
8 | import { getTimeWithPadStart } from "@/utils/getTime";
9 |
10 | export const Duration: FC = ({ position }) => {
11 | const trackDurationRef = useRef(null);
12 | const { elementRefs } = useNonNullableContext(audioPlayerStateContext);
13 |
14 | useRefsDispatch(
15 | {
16 | refs: { trackDurationEl: trackDurationRef },
17 | },
18 | []
19 | );
20 |
21 | return (
22 |
27 |
28 | {elementRefs?.audioEl?.duration
29 | ? getTimeWithPadStart(elementRefs.audioEl.duration)
30 | : "00:00"}
31 |
32 |
33 | );
34 | };
35 |
36 | const TrackTimeDurationContainer = styled(TrackTimeContainer)`
37 | .track-duration {
38 | display: flex;
39 | color: var(--rm-audio-player-track-duration);
40 | letter-spacing: -0.1rem;
41 | }
42 | `;
43 |
--------------------------------------------------------------------------------
/package/src/components/AudioPlayer/Interface/Information/TrackTime/Styles.ts:
--------------------------------------------------------------------------------
1 | import styled, { css } from "styled-components";
2 | import { TrackTimePosition } from "./Types";
3 |
4 | export interface TrackTimeContainerProps {
5 | position: TrackTimePosition;
6 | childrenClassName: string;
7 | }
8 |
9 | export const TrackTimeContainer = styled.div`
10 | ${({ position, childrenClassName }) => css`
11 | display: flex;
12 | align-items: center;
13 | justify-content: center;
14 | min-height: 16px;
15 | font-family: monospace !important;
16 | font-size: 16px !important;
17 |
18 | .${childrenClassName} {
19 | margin-right: ${position === "left" && "-10px"};
20 | }
21 |
22 | ${position === "right" &&
23 | css`
24 | .${childrenClassName}:before {
25 | content: "/";
26 | margin: 0 0.3rem;
27 | }
28 | `}
29 | `}
30 | `;
31 |
--------------------------------------------------------------------------------
/package/src/components/AudioPlayer/Interface/Information/TrackTime/Types.ts:
--------------------------------------------------------------------------------
1 | export type TrackTimePosition = "left" | "right" | "separation";
2 | export interface TrackTimeChildrenProps {
3 | position: TrackTimePosition;
4 | }
5 |
--------------------------------------------------------------------------------
/package/src/components/AudioPlayer/Interface/Information/TrackTime/index.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | audioPlayerStateContext,
3 | defaultInterfacePlacement,
4 | } from "@/components/AudioPlayer/Context";
5 | import Grid from "@/components/Grid";
6 | import { useNonNullableContext } from "@/hooks/useNonNullableContext";
7 | import { FC, useCallback } from "react";
8 | import { Current } from "./Current";
9 | import { Duration } from "./Duration";
10 | import { TrackTimePosition } from "./Types";
11 |
12 | export const TrackTime: FC = () => {
13 | const { interfacePlacement, activeUI } = useNonNullableContext(
14 | audioPlayerStateContext
15 | );
16 |
17 | const parsePosition = useCallback(
18 | (str: string) => +str.split(/[^\d]/).join(""),
19 | []
20 | );
21 | const currentTimePosition = parsePosition(
22 | interfacePlacement?.itemCustomArea?.trackTimeCurrent ||
23 | interfacePlacement?.templateArea?.trackTimeCurrent ||
24 | defaultInterfacePlacement.templateArea.trackTimeCurrent
25 | );
26 | const durationTimePosition = parsePosition(
27 | interfacePlacement?.itemCustomArea?.trackTimeDuration ||
28 | interfacePlacement?.templateArea?.trackTimeDuration ||
29 | defaultInterfacePlacement.templateArea.trackTimeDuration
30 | );
31 |
32 | const getPosition = useCallback(
33 | (positionNumber: number): TrackTimePosition => {
34 | switch (positionNumber) {
35 | case 1:
36 | return "right";
37 | case -1:
38 | return "left";
39 | default:
40 | return "separation";
41 | }
42 | },
43 | []
44 | );
45 | const positions = {
46 | current: getPosition(currentTimePosition - durationTimePosition),
47 | duration: getPosition(durationTimePosition - currentTimePosition),
48 | };
49 |
50 | return (
51 | <>
52 |
60 |
61 |
62 |
70 |
71 |
72 | >
73 | );
74 | };
75 |
--------------------------------------------------------------------------------
/package/src/components/AudioPlayer/Interface/Information/index.tsx:
--------------------------------------------------------------------------------
1 | import Grid from "@/components/Grid";
2 | import { useNonNullableContext } from "@/hooks/useNonNullableContext";
3 | import {
4 | audioPlayerStateContext,
5 | defaultInterfacePlacement,
6 | } from "@/components/AudioPlayer/Context/StateContext";
7 | import { FC } from "react";
8 | import { Artwork } from "./Artwork";
9 | import { TrackInfo } from "./TrackInfo";
10 | import { TrackTime } from "./TrackTime";
11 |
12 | export const Information: FC = () => {
13 | const { interfacePlacement, playList, curIdx, activeUI } =
14 | useNonNullableContext(audioPlayerStateContext);
15 |
16 | const isTrackInfoActive =
17 | Boolean(
18 | playList[curIdx]?.customTrackInfo ??
19 | playList[curIdx]?.writer ??
20 | playList[curIdx]?.name
21 | ) && Boolean(activeUI.trackInfo ?? activeUI.all);
22 |
23 | return (
24 | <>
25 |
35 |
36 |
37 |
45 |
46 |
47 |
48 |
49 | >
50 | );
51 | };
52 |
--------------------------------------------------------------------------------
/package/src/components/AudioPlayer/Interface/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { FC } from "react";
2 | import styled from "styled-components";
3 | import { Controller } from "./Controller";
4 | import { Information } from "./Information";
5 |
6 | import { audioPlayerStateContext } from "@/components/AudioPlayer/Context/StateContext";
7 | import Grid from "@/components/Grid";
8 |
9 | import { useNonNullableContext } from "@/hooks/useNonNullableContext";
10 | import { useGridTemplate } from "@/hooks/useGridTemplate";
11 |
12 | interface InterfaceProps {
13 | children: React.ReactNode;
14 | }
15 |
16 | export const Interface: FC = ({ children }) => {
17 | const { interfacePlacement, activeUI, playListPlacement } =
18 | useNonNullableContext(audioPlayerStateContext);
19 |
20 | const CustomComponents = React.Children.toArray(children);
21 |
22 | const [gridAreas, gridColumns] = useGridTemplate(
23 | activeUI,
24 | interfacePlacement?.templateArea,
25 | interfacePlacement?.customComponentsArea
26 | );
27 |
28 | return (
29 |
30 | {playListPlacement === "top" && }
31 |
39 |
40 |
41 |
42 | {CustomComponents}
43 |
44 | {playListPlacement === "bottom" && }
45 |
46 | );
47 | };
48 |
49 | const InterfaceContainer = styled.div`
50 | .interface-grid {
51 | background: var(--rm-audio-player-interface-container);
52 | }
53 | .interface-grid {
54 | padding: 0.5rem 10px;
55 | }
56 | .sortable-play-list {
57 | background: var(--rm-audio-player-sortable-list);
58 | box-shadow: -5px 2px 4px 0px rgb(0 0 0 / 4%) inset;
59 | }
60 | `;
61 |
--------------------------------------------------------------------------------
/package/src/components/AudioPlayer/Player/index.tsx:
--------------------------------------------------------------------------------
1 | import { View } from "@react-spectrum/view";
2 | import {
3 | ActiveUI,
4 | PlayListPlacement,
5 | CustomIcons,
6 | PlayerPlacement,
7 | PlayList,
8 | InitialStates,
9 | InterfacePlacement,
10 | CoverImgsCss,
11 | VolumeSliderPlacement,
12 | defaultInterfacePlacementMaxLength,
13 | } from "@/components/AudioPlayer/Context";
14 | import { Audio } from "../Audio";
15 | import { Interface } from "../Interface";
16 | import { usePropsStateEffect } from "./usePropsStateEffect";
17 |
18 | // TODO : feature - add Equalizer component
19 | // TODO : feature - add dynamic spectrum form
20 |
21 | export interface AudioPlayerProps {
22 | children?: React.ReactNode;
23 | playList: PlayList;
24 | audioInitialState?: InitialStates;
25 | audioRef?: React.MutableRefObject;
26 | activeUI?: ActiveUI;
27 | customIcons?: CustomIcons;
28 | coverImgsCss?: CoverImgsCss;
29 | placement?: {
30 | player?: PlayerPlacement;
31 | playList?: PlayListPlacement;
32 | interface?: InterfacePlacement;
33 | volumeSlider?: VolumeSliderPlacement;
34 | };
35 | }
36 |
37 | export const AudioPlayer = <
38 | TInterfacePlacementLength extends number = typeof defaultInterfacePlacementMaxLength
39 | >({
40 | audioRef,
41 | children,
42 | ...restProps
43 | }: AudioPlayerProps) => {
44 | usePropsStateEffect(restProps);
45 |
46 | return (
47 |
48 |
49 | {children}
50 |
51 | );
52 | };
53 |
--------------------------------------------------------------------------------
/package/src/components/AudioPlayer/Player/usePropsStateEffect.ts:
--------------------------------------------------------------------------------
1 | import { useLayoutEffect, useEffect, useState } from "react";
2 | import { useNonNullableContext } from "@/hooks/useNonNullableContext";
3 | import { AudioPlayerProps } from ".";
4 | import {
5 | audioPlayerDispatchContext,
6 | InterfacePlacement,
7 | PlayerPlacement,
8 | PlayListPlacement,
9 | VolumeSliderPlacement,
10 | } from "../Context";
11 |
12 | export const usePropsStateEffect = ({
13 | placement = {},
14 | activeUI,
15 | coverImgsCss,
16 | audioInitialState,
17 | playList,
18 | customIcons,
19 | }: Omit, "children">) => {
20 | const [isMounted, setIsMounted] = useState(false);
21 | const audioPlayerDispatch = useNonNullableContext(audioPlayerDispatchContext);
22 | useLayoutEffect(() => {
23 | if (!isMounted) return;
24 | const {
25 | player: playerPlacement,
26 | playList: playListPlacement,
27 | interface: interfacePlacement,
28 | volumeSlider: volumeSliderPlacement,
29 | } = placement as {
30 | player?: PlayerPlacement;
31 | playList?: PlayListPlacement;
32 | interface?: InterfacePlacement;
33 | volumeSlider?: VolumeSliderPlacement;
34 | };
35 | audioPlayerDispatch({
36 | type: "SET_PLACEMENTS",
37 | playerPlacement,
38 | playListPlacement,
39 | interfacePlacement,
40 | volumeSliderPlacement,
41 | });
42 | }, [audioPlayerDispatch, placement]);
43 |
44 | useLayoutEffect(() => {
45 | if (!isMounted || !activeUI) return;
46 |
47 | audioPlayerDispatch({ type: "SET_ACTIVE_UI", activeUI });
48 | }, [activeUI, audioPlayerDispatch]);
49 |
50 | useLayoutEffect(() => {
51 | if (!isMounted || !coverImgsCss) return;
52 |
53 | audioPlayerDispatch({ type: "SET_COVER_IMGS_CSS", coverImgsCss });
54 | }, [audioPlayerDispatch, coverImgsCss]);
55 |
56 | useEffect(() => {
57 | if (!isMounted || !audioInitialState) return;
58 |
59 | audioPlayerDispatch({
60 | type: "SET_INITIAL_STATES",
61 | audioState: audioInitialState,
62 | curPlayId: audioInitialState.curPlayId,
63 | });
64 | }, [audioInitialState, audioPlayerDispatch]);
65 |
66 | useEffect(() => {
67 | if (!isMounted) return;
68 |
69 | audioPlayerDispatch({ type: "UPDATE_PLAY_LIST", playList });
70 | }, [audioPlayerDispatch, playList]);
71 |
72 | useEffect(() => {
73 | if (!isMounted || !customIcons) return;
74 |
75 | audioPlayerDispatch({ type: "SET_CUSTOM_ICONS", customIcons });
76 | }, [customIcons, audioPlayerDispatch]);
77 |
78 | useEffect(() => {
79 | setIsMounted(true);
80 | }, []);
81 | };
82 |
--------------------------------------------------------------------------------
/package/src/components/AudioPlayer/index.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | AudioPlayerProvider,
3 | SpectrumProvider,
4 | SpectrumProviderProps,
5 | } from "@/components/Provider";
6 | import { GlobalStyle } from "../../styles/GlobalStyle";
7 | import { defaultInterfacePlacementMaxLength } from "./Context";
8 | import { CustomComponent } from "./Interface/CustomComponent";
9 | import { AudioPlayer, AudioPlayerProps } from "./Player";
10 |
11 | export type RMAudioPlayerProps<
12 | TInterfacePlacementLength extends number = typeof defaultInterfacePlacementMaxLength
13 | > = AudioPlayerProps & SpectrumProviderProps;
14 |
15 | const AudioPlayerWithProviders = ({
16 | rootContainerProps,
17 | ...audioPlayProps
18 | }: RMAudioPlayerProps) => {
19 | return (
20 |
21 |
22 |
23 |
24 |
25 |
26 | );
27 | };
28 |
29 | type AudioPlayerComponent = typeof AudioPlayerWithProviders & {
30 | CustomComponent: typeof CustomComponent;
31 | };
32 |
33 | export default AudioPlayerWithProviders as AudioPlayerComponent;
34 |
--------------------------------------------------------------------------------
/package/src/components/CssTransition.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | cloneElement,
3 | FC,
4 | PropsWithChildren,
5 | useLayoutEffect,
6 | useState,
7 | } from "react";
8 | import { keyframes } from "styled-components";
9 |
10 | export const appearanceIn = keyframes({
11 | "0%": {
12 | opacity: 0,
13 | transform: "scale(0.95)",
14 | },
15 | "60%": {
16 | opacity: 0.75,
17 | transform: "scale(1.05)",
18 | },
19 | "100%": {
20 | opacity: 1,
21 | transform: "scale(1)",
22 | },
23 | });
24 |
25 | export const appearanceOut = keyframes({
26 | "0%": {
27 | opacity: 1,
28 | transform: "scale(1)",
29 | },
30 | "100%": {
31 | opacity: 0,
32 | transform: "scale(0.5)",
33 | },
34 | });
35 |
36 | export interface CssTransitionProps {
37 | visible: boolean;
38 | name: string;
39 | leaveTime: number;
40 | enterTime: number;
41 | clearTime: number;
42 | onExited?: () => void;
43 | onEntered?: () => void;
44 | }
45 |
46 | export const CssTransition: FC> = ({
47 | visible,
48 | name,
49 | leaveTime,
50 | enterTime,
51 | clearTime,
52 | onExited,
53 | onEntered,
54 | children,
55 | }) => {
56 | const [classNames, setClassNames] = useState("");
57 | const [renderable, setRenderable] = useState(false);
58 |
59 | useLayoutEffect(() => {
60 | const statusClassName = visible ? "enter" : "leave";
61 | const time = visible ? enterTime : leaveTime;
62 | if (visible && !renderable) {
63 | setRenderable(true);
64 | }
65 |
66 | setClassNames(`${name}-${statusClassName}`);
67 |
68 | // set class to active
69 | const timer = setTimeout(() => {
70 | setClassNames(
71 | `${name}-${statusClassName} ${name}-${statusClassName}-active`
72 | );
73 | if (statusClassName === "leave") {
74 | onExited?.();
75 | } else {
76 | onEntered?.();
77 | }
78 | clearTimeout(timer);
79 | }, time);
80 |
81 | // remove classNames when animation over
82 | const clearClassesTimer = setTimeout(() => {
83 | if (!visible) {
84 | setClassNames(name);
85 | setRenderable(false);
86 | }
87 | clearTimeout(clearClassesTimer);
88 | }, time + clearTime);
89 |
90 | return () => {
91 | clearTimeout(timer);
92 | clearTimeout(clearClassesTimer);
93 | };
94 | }, [visible, renderable]);
95 |
96 | if (!renderable) return null;
97 |
98 | return cloneElement(children as React.ReactElement, {
99 | className: `${
100 | (children as React.ReactElement).props.className
101 | } ${classNames}`,
102 | });
103 | };
104 |
--------------------------------------------------------------------------------
/package/src/components/Drawer/Drawer.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | PropsWithChildren,
3 | useRef,
4 | FC,
5 | useLayoutEffect,
6 | useState,
7 | } from "react";
8 | import styled from "styled-components";
9 | import { DrawerContext, drawerContext } from "./DrawerContext";
10 | import { DrawerTrigger } from "./DrawerTrigger";
11 | import { DrawerContent } from "./DrawerContent";
12 | import { appearanceIn, appearanceOut } from "../CssTransition";
13 | import useClickOutside from "@/hooks/useClickOutside";
14 |
15 | export interface DrawerProps extends Omit, "setIsOpen"> {
16 | outboundClickActive?: boolean;
17 | placement?: "bottom" | "top";
18 | }
19 |
20 | const Drawer: FC> = ({
21 | outboundClickActive = false,
22 | placement = "bottom",
23 | children,
24 | isOpen: isOpenProp,
25 | onOpenChange,
26 | }) => {
27 | const drawerRef = useRef(null);
28 | const [trigger, content] = React.Children.toArray(children);
29 | const [isOpen, setIsOpen] = useState(false);
30 |
31 | useClickOutside(drawerRef, () => {
32 | if (!outboundClickActive) return;
33 | setIsOpen(false);
34 | onOpenChange && onOpenChange(false);
35 | });
36 | useLayoutEffect(() => {
37 | if (isOpenProp !== undefined) {
38 | setIsOpen(isOpenProp);
39 | }
40 | }, [isOpenProp]);
41 |
42 | return (
43 |
44 |
45 | <>
46 | {placement === "top" && content}
47 | {trigger}
48 | {placement === "bottom" && content}
49 | >
50 |
51 |
52 | );
53 | };
54 |
55 | type DrawerComponent = typeof Drawer & {
56 | Trigger: typeof DrawerTrigger;
57 | Content: typeof DrawerContent;
58 | };
59 |
60 | export default Drawer as DrawerComponent;
61 |
62 | export const DrawerContainer = styled.div`
63 | position: relative;
64 | min-width: 20px;
65 | min-height: 20px;
66 | .drawer-trigger-wrapper {
67 | width: 100%;
68 | height: 100%;
69 | cursor: pointer;
70 | position: absolute;
71 | display: flex;
72 | }
73 |
74 | .drawer-content-wrapper {
75 | transform-origin: center top;
76 | }
77 |
78 | .drawer-content-wrapper-enter {
79 | animation: ${appearanceIn} 0.25s ease-out normal forwards;
80 | }
81 |
82 | .drawer-content-wrapper-leave {
83 | animation: ${appearanceOut} 0.1s ease-in forwards;
84 | }
85 | `;
86 |
--------------------------------------------------------------------------------
/package/src/components/Drawer/DrawerContent.tsx:
--------------------------------------------------------------------------------
1 | import { useNonNullableContext } from "@/hooks/useNonNullableContext";
2 | import { FC, PropsWithChildren, useMemo } from "react";
3 | import { CssTransition } from "../CssTransition";
4 | import { drawerContext } from "./DrawerContext";
5 |
6 | export type DrawerContentPlacement = "top" | "bottom" | "left" | "right";
7 |
8 | export type DrawerContentProps = {
9 | isWithAnimation?: boolean;
10 | };
11 |
12 | export const DrawerContent: FC> = ({
13 | children,
14 | isWithAnimation = true,
15 | }) => {
16 | const { isOpen, setIsOpen } = useNonNullableContext(drawerContext);
17 | const onExited = () => setIsOpen(false);
18 | const onEntered = () => setIsOpen(true);
19 |
20 | const Content = useMemo(
21 | () => {children}
,
22 |
23 | [children]
24 | );
25 |
26 | return isWithAnimation ? (
27 |
36 | {Content}
37 |
38 | ) : (
39 | Content
40 | );
41 | };
42 |
--------------------------------------------------------------------------------
/package/src/components/Drawer/DrawerContext.ts:
--------------------------------------------------------------------------------
1 | import { createContext } from "react";
2 |
3 | export interface DrawerContext {
4 | isOpen: boolean;
5 | setIsOpen: React.Dispatch>;
6 | onOpenChange?: (isOpen: boolean) => void;
7 | }
8 |
9 | export const drawerContext = createContext(null);
10 |
--------------------------------------------------------------------------------
/package/src/components/Drawer/DrawerTrigger.tsx:
--------------------------------------------------------------------------------
1 | import { useNonNullableContext } from "@/hooks/useNonNullableContext";
2 | import { FC, PropsWithChildren } from "react";
3 | import { drawerContext } from "./DrawerContext";
4 |
5 | export const DrawerTrigger: FC> = ({ children }) => {
6 | const { isOpen, setIsOpen, onOpenChange } =
7 | useNonNullableContext(drawerContext);
8 | return (
9 | {
12 | setIsOpen(!isOpen);
13 | onOpenChange && onOpenChange(!isOpen);
14 | }}
15 | >
16 | {children}
17 |
18 | );
19 | };
20 |
--------------------------------------------------------------------------------
/package/src/components/Drawer/index.ts:
--------------------------------------------------------------------------------
1 | import Drawer from "./Drawer";
2 | import { DrawerContent } from "./DrawerContent";
3 | import { DrawerTrigger } from "./DrawerTrigger";
4 |
5 | export default Drawer;
6 | Drawer.Content = DrawerContent;
7 | Drawer.Trigger = DrawerTrigger;
8 |
9 | export type {
10 | DrawerContentProps,
11 | DrawerContentPlacement,
12 | } from "./DrawerContent";
13 |
--------------------------------------------------------------------------------
/package/src/components/Dropdown/Dropdown.tsx:
--------------------------------------------------------------------------------
1 | import React, {
2 | PropsWithChildren,
3 | useRef,
4 | FC,
5 | useLayoutEffect,
6 | useState,
7 | } from "react";
8 | import styled from "styled-components";
9 | import { DropdownContext, dropdownContext } from "./DropdownContext";
10 | import { DropdownTrigger } from "./DropdownTrigger";
11 | import { DropdownContent } from "./DropdownContent";
12 | import { appearanceIn, appearanceOut } from "../CssTransition";
13 | import useClickOutside from "@/hooks/useClickOutside";
14 | import { useDropdown } from "./useDropdown";
15 |
16 | export interface DropdownProps
17 | extends Omit, "setIsOpen"> {
18 | triggerType?: "click" | "hover";
19 | outboundClickActive?: boolean;
20 | placement?: DropdownContext["placement"];
21 | disabled?: boolean;
22 | }
23 |
24 | const Dropdown: FC> = ({
25 | triggerType = "click",
26 | outboundClickActive = true,
27 | children,
28 | isOpen: isOpenProp,
29 | placement = "bottom",
30 | disabled = false,
31 | onOpenChange,
32 | }) => {
33 | const dropdownRef = useRef(null);
34 | const [trigger, content] = React.Children.toArray(children);
35 | const [isOpen, setIsOpen] = useState(false);
36 | const dropdownEventProps = useDropdown({
37 | setIsOpen,
38 | isOpen,
39 | triggerType,
40 | clickArea: "root",
41 | });
42 |
43 | useClickOutside(dropdownRef, () => outboundClickActive && setIsOpen(false));
44 | useLayoutEffect(() => {
45 | if (isOpenProp !== undefined) {
46 | setIsOpen(isOpenProp);
47 | }
48 | }, [isOpenProp]);
49 |
50 | return (
51 |
54 |
59 | <>
60 | {trigger}
61 | {!disabled && content}
62 | >
63 |
64 |
65 | );
66 | };
67 |
68 | type DropdownComponent = typeof Dropdown & {
69 | Trigger: typeof DropdownTrigger;
70 | Content: typeof DropdownContent;
71 | };
72 |
73 | export default Dropdown as DropdownComponent;
74 |
75 | export const DropdownContainer = styled.div`
76 | position: relative;
77 | display: flex;
78 | align-items: center;
79 | justify-content: center;
80 | min-width: 20px;
81 | min-height: 20px;
82 | .dropdown-trigger-wrapper {
83 | width: 100%;
84 | height: 100%;
85 | cursor: pointer;
86 | position: absolute;
87 | display: flex;
88 | }
89 |
90 | .dropdown-content-wrapper {
91 | transform-origin: center top;
92 | }
93 |
94 | .dropdown-content-wrapper-enter {
95 | animation: ${appearanceIn} 0.25s ease-out normal forwards;
96 | }
97 |
98 | .dropdown-content-wrapper-leave {
99 | animation: ${appearanceOut} 0.1s ease-in forwards;
100 | }
101 | `;
102 |
--------------------------------------------------------------------------------
/package/src/components/Dropdown/DropdownContent.tsx:
--------------------------------------------------------------------------------
1 | import { useNonNullableContext } from "@/hooks/useNonNullableContext";
2 | import {
3 | FC,
4 | HTMLAttributes,
5 | PropsWithChildren,
6 | useEffect,
7 | useMemo,
8 | useState,
9 | } from "react";
10 | import styled, { css } from "styled-components";
11 | import { CssTransition } from "../CssTransition";
12 | import { dropdownContext } from "./DropdownContext";
13 | import { useDropdown } from "./useDropdown";
14 |
15 | export type DropdownContentPlacement = "top" | "bottom" | "left" | "right";
16 |
17 | export type DropdownContentProps = HTMLAttributes & {
18 | isWithAnimation?: boolean;
19 | };
20 |
21 | export interface DropdownSize {
22 | width: number;
23 | height: number;
24 | }
25 |
26 | export const DropdownContent: FC> = ({
27 | children,
28 | isWithAnimation = true,
29 | ...dropdownContentProps
30 | }) => {
31 | const { dropdownRef, placement, isOpen, setIsOpen } =
32 | useNonNullableContext(dropdownContext);
33 | const [dropdownSize, setDropdownSize] = useState();
34 | const { onClick } = useDropdown({
35 | setIsOpen,
36 | isOpen,
37 | clickArea: "content",
38 | });
39 | const onExited = () => setIsOpen(false);
40 | const onEntered = () => setIsOpen(true);
41 |
42 | useEffect(() => {
43 | if (dropdownRef.current) {
44 | setDropdownSize({
45 | width: dropdownRef.current.offsetWidth,
46 | height: dropdownRef.current.offsetHeight,
47 | });
48 | }
49 | }, [dropdownRef]);
50 |
51 | const Content = useMemo(
52 | () =>
53 | dropdownSize ? (
54 |
60 | {children}
61 |
62 | ) : null,
63 | [children, dropdownContentProps, dropdownSize, placement, onClick]
64 | );
65 |
66 | return isWithAnimation ? (
67 |
76 | {Content}
77 |
78 | ) : isOpen ? (
79 | Content
80 | ) : null;
81 | };
82 |
83 | interface DropdownContainerProps {
84 | placement: DropdownContentPlacement;
85 | dropdownSize: DropdownSize;
86 | }
87 |
88 | const DropdownContentContainer = styled.div`
89 | ${({ placement, dropdownSize }: DropdownContainerProps) => css`
90 | position: absolute;
91 | top: ${placement === "bottom" ? `${dropdownSize.height}px` : undefined};
92 | margin-top: ${placement === "bottom" ? `5px` : undefined};
93 | bottom: ${placement === "top" ? `${dropdownSize.height}px` : undefined};
94 | margin-bottom: ${placement === "top" ? `5px` : undefined};
95 | left: ${placement === "right" ? `${dropdownSize.width}px` : undefined};
96 | right: ${placement === "left" ? `${dropdownSize.width}px` : undefined};
97 | `}
98 | `;
99 |
--------------------------------------------------------------------------------
/package/src/components/Dropdown/DropdownContext.ts:
--------------------------------------------------------------------------------
1 | import { createContext } from "react";
2 | export interface DropdownContext {
3 | dropdownRef: React.RefObject;
4 | isOpen: boolean;
5 | placement: "top" | "bottom" | "left" | "right";
6 | setIsOpen: React.Dispatch>;
7 | onOpenChange?: (isOpen: boolean) => void;
8 | }
9 |
10 | export const dropdownContext = createContext(null);
11 |
--------------------------------------------------------------------------------
/package/src/components/Dropdown/DropdownTrigger.tsx:
--------------------------------------------------------------------------------
1 | import { FC, PropsWithChildren } from "react";
2 |
3 | export const DropdownTrigger: FC> = ({
4 | children,
5 | }) => {
6 | return {children}
;
7 | };
8 |
--------------------------------------------------------------------------------
/package/src/components/Dropdown/index.ts:
--------------------------------------------------------------------------------
1 | import Dropdown from "./Dropdown";
2 | import { DropdownContent } from "./DropdownContent";
3 | import { DropdownTrigger } from "./DropdownTrigger";
4 |
5 | export default Dropdown;
6 | Dropdown.Content = DropdownContent;
7 | Dropdown.Trigger = DropdownTrigger;
8 |
9 | export type {
10 | DropdownContentProps,
11 | DropdownContentPlacement,
12 | } from "./DropdownContent";
13 |
--------------------------------------------------------------------------------
/package/src/components/Dropdown/useDropdown.ts:
--------------------------------------------------------------------------------
1 | import { HTMLAttributes, useRef } from "react";
2 | import { DropdownProps } from "./Dropdown";
3 |
4 | export const useDropdown = ({
5 | clickArea,
6 | triggerType,
7 | isOpen,
8 | setIsOpen,
9 | onOpenChange,
10 | }: {
11 | clickArea: "root" | "content";
12 | triggerType?: DropdownProps["triggerType"];
13 | isOpen: boolean;
14 | setIsOpen: React.Dispatch>;
15 | onOpenChange?: (isOpen: boolean) => void;
16 | }): HTMLAttributes => {
17 | const timer = useRef();
18 | const lazyChangeVisible = (nextState: boolean) => {
19 | const clear = () => {
20 | clearTimeout(timer.current);
21 | timer.current = undefined;
22 | };
23 | const handler = (nextState: boolean) => {
24 | setIsOpen(nextState);
25 | onOpenChange && onOpenChange(nextState);
26 | clear();
27 | };
28 | clear();
29 | if (nextState) {
30 | timer.current = window.setTimeout(() => handler(true), 100);
31 | return;
32 | }
33 | timer.current = window.setTimeout(() => handler(false), 100);
34 | };
35 | const mouseEventHandler = (next: boolean) => {
36 | triggerType === "hover" && lazyChangeVisible(next);
37 | };
38 | const clickEventHandler = () => {
39 | if (triggerType !== "click") return;
40 | setIsOpen(!isOpen);
41 | onOpenChange && onOpenChange(!isOpen);
42 | };
43 | const preventHandler = (event: React.MouseEvent) => {
44 | event.stopPropagation();
45 | event.nativeEvent.stopImmediatePropagation();
46 | };
47 | return {
48 | onClick: clickArea === "content" ? preventHandler : clickEventHandler,
49 | onKeyUp: () => mouseEventHandler(true),
50 | onMouseEnter: () => mouseEventHandler(true),
51 | onMouseLeave: () => mouseEventHandler(false),
52 | onFocus: () => mouseEventHandler(true),
53 | onBlur: () => mouseEventHandler(false),
54 | };
55 | };
56 |
--------------------------------------------------------------------------------
/package/src/components/Grid/Grid.tsx:
--------------------------------------------------------------------------------
1 | import { Grid as SpectrumGrid } from "@react-spectrum/layout";
2 | import { GridItem } from "./Item";
3 |
4 | export const Grid = SpectrumGrid;
5 |
6 | type GridComponent = typeof SpectrumGrid & {
7 | Item: typeof GridItem;
8 | };
9 |
10 | export default Grid as GridComponent;
11 |
--------------------------------------------------------------------------------
/package/src/components/Grid/Item.tsx:
--------------------------------------------------------------------------------
1 | import { View, ViewProps } from "@react-spectrum/view";
2 | import { forwardRef } from "react";
3 | import { DOMRefValue } from "@react-types/shared";
4 |
5 | export interface GridItemProps extends Omit {
6 | visible?: boolean;
7 | children: React.ReactNode;
8 | }
9 |
10 | export const GridItem = forwardRef<
11 | React.RefAttributes>,
12 | GridItemProps
13 | >(({ children, visible = true, ...viewProps }, ref) => {
14 | return (
15 |
21 | {visible && children}
22 |
23 | );
24 | });
25 | GridItem.displayName = "GridItem";
26 |
--------------------------------------------------------------------------------
/package/src/components/Grid/index.ts:
--------------------------------------------------------------------------------
1 | import Grid from "./Grid";
2 | import { GridItem } from "./Item";
3 |
4 | Grid.Item = GridItem;
5 | export default Grid;
6 |
--------------------------------------------------------------------------------
/package/src/components/Provider/AudioPlayerProvider.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | audioPlayerDispatchContext,
3 | audioPlayerReducer,
4 | audioPlayerStateContext,
5 | AudioState,
6 | defaultInterfacePlacement,
7 | defaultInterfacePlacementMaxLength,
8 | Placements,
9 | } from "@/components/AudioPlayer/Context";
10 | import { PropsWithChildren, useReducer } from "react";
11 | import { AudioPlayerProps } from "../AudioPlayer/Player";
12 |
13 | export const AudioPlayerProvider = <
14 | TInterfacePlacementLength extends number = typeof defaultInterfacePlacementMaxLength
15 | >({
16 | children,
17 | ...props
18 | }: PropsWithChildren>) => {
19 | const {
20 | playList,
21 | audioInitialState,
22 | activeUI: activeUIProp,
23 | placement: placementProp,
24 | ...otherProps
25 | } = props;
26 |
27 | const curAudioState: AudioState = {
28 | isPlaying: audioInitialState?.isPlaying || false,
29 | repeatType: audioInitialState?.repeatType || "ALL",
30 | volume: audioInitialState?.volume || 1,
31 | muted: audioInitialState?.muted,
32 | };
33 |
34 | const activeUI = activeUIProp || {
35 | playButton: true,
36 | };
37 |
38 | const placement: Placements = {
39 | playerPlacement: placementProp?.player,
40 | playListPlacement: placementProp?.playList || "bottom",
41 | interfacePlacement: placementProp?.interface || {
42 | templateArea: {
43 | playButton: defaultInterfacePlacement.templateArea["playButton"],
44 | },
45 | },
46 | volumeSliderPlacement: placementProp?.volumeSlider,
47 | };
48 |
49 | const [audioContextState, dispatchAudioContextState] = useReducer(
50 | audioPlayerReducer,
51 | {
52 | playList,
53 | curPlayId: audioInitialState?.curPlayId || 1,
54 | curIdx: audioInitialState?.curPlayId
55 | ? playList.findIndex(
56 | (audioData) => audioData.id === audioInitialState?.curPlayId
57 | )
58 | : 0,
59 | curAudioState,
60 | activeUI,
61 | ...(placement as Placements<10>),
62 | ...otherProps,
63 | }
64 | );
65 |
66 | return (
67 |
68 |
69 | {children}
70 |
71 |
72 | );
73 | };
74 |
--------------------------------------------------------------------------------
/package/src/components/Provider/SpectrumProvider.tsx:
--------------------------------------------------------------------------------
1 | import { FC, PropsWithChildren, useLayoutEffect, useState } from "react";
2 | import { Provider } from "@react-spectrum/provider";
3 | import { theme } from "@react-spectrum/theme-default";
4 | import { useNonNullableContext } from "@/hooks/useNonNullableContext";
5 | import { audioPlayerStateContext } from "@/components/AudioPlayer/Context/StateContext";
6 | import { DOMRefValue } from "@react-types/shared";
7 | import { ProviderProps } from "@react-types/provider";
8 |
9 | export interface SpectrumProviderProps {
10 | rootContainerProps?: Omit<
11 | ProviderProps & React.RefAttributes>,
12 | "children"
13 | >;
14 | }
15 |
16 | export const SpectrumProvider: FC> = ({
17 | children,
18 | rootContainerProps,
19 | }) => {
20 | const { playerPlacement: contextPlayerPlacement } = useNonNullableContext(
21 | audioPlayerStateContext
22 | );
23 | const [placementState, setPlacementState] = useState<{
24 | bottom?: number;
25 | top?: number;
26 | left?: number;
27 | right?: number;
28 | }>();
29 | useLayoutEffect(() => {
30 | if (contextPlayerPlacement) {
31 | const placementValidation = () => {
32 | switch (contextPlayerPlacement) {
33 | case "bottom":
34 | return { bottom: 0 };
35 | case "top":
36 | return { top: 0 };
37 | case "bottom-left":
38 | return { bottom: 0, left: 0 };
39 | case "bottom-right":
40 | return { bottom: 0, right: 0 };
41 | case "top-left":
42 | return { top: 0, left: 0 };
43 | case "top-right":
44 | return { top: 0, right: 0 };
45 | default:
46 | break;
47 | }
48 | };
49 | setPlacementState(placementValidation());
50 | }
51 | }, [contextPlayerPlacement]);
52 |
53 | return (
54 |
66 | {children}
67 |
68 | );
69 | };
70 |
--------------------------------------------------------------------------------
/package/src/components/Provider/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./SpectrumProvider";
2 | export * from "./AudioPlayerProvider";
3 |
--------------------------------------------------------------------------------
/package/src/components/SortableList/SortableList.tsx:
--------------------------------------------------------------------------------
1 | import { FC, PropsWithChildren } from "react";
2 | import { SortableListItem } from "./SortableListItem";
3 | import styled from "styled-components";
4 |
5 | const SortableList: FC> = ({ children }) => {
6 | return (
7 |
8 | {children}
9 |
10 | );
11 | };
12 |
13 | const SortableListContainer = styled.ul`
14 | ul,
15 | li {
16 | list-style-type: none;
17 | }
18 | cursor: pointer;
19 | `;
20 |
21 | type SortableListComponent = typeof SortableList & {
22 | Item: typeof SortableListItem;
23 | };
24 |
25 | export default SortableList as SortableListComponent;
26 |
--------------------------------------------------------------------------------
/package/src/components/SortableList/SortableListItem.tsx:
--------------------------------------------------------------------------------
1 | import { Flex } from "@react-spectrum/layout";
2 | import { PropsWithChildren } from "react";
3 | import styled from "styled-components";
4 | import { ListItem } from "./index";
5 | import {
6 | useSortableListItem,
7 | UseSortableListItemProps,
8 | } from "./useSortableListItem";
9 |
10 | export type SortableListItemProps = UseSortableListItemProps;
11 |
12 | export const SortableListItem = (
13 | props: PropsWithChildren>
14 | ) => {
15 | const { children, ...useListItemProps } = props;
16 | const eventProps = useSortableListItem(useListItemProps);
17 |
18 | return (
19 |
23 | {children}
24 |
25 | );
26 | };
27 |
28 | const SortableListItemContainer = styled.li`
29 | border-top: 2px solid transparent;
30 | border-bottom: 2px solid transparent;
31 | transition: all 0.3s ease-in-out;
32 |
33 | & * {
34 | pointer-events: none;
35 | }
36 |
37 | &.dragstart {
38 | opacity: 0.5;
39 | }
40 |
41 | &.dragover {
42 | transform: scale(1.02);
43 | backdrop-filter: blur(20px);
44 | box-shadow: 0px 3.58195px 22.3872px -2.68646px rgb(0 0 0 / 20%);
45 | }
46 | `;
47 |
--------------------------------------------------------------------------------
/package/src/components/SortableList/index.ts:
--------------------------------------------------------------------------------
1 | import SortableList from "./SortableList";
2 | import { SortableListItem } from "./SortableListItem";
3 |
4 | SortableList.Item = SortableListItem;
5 | export default SortableList;
6 |
7 | export type ListData = Array;
8 | export type ListItem = Record;
9 |
10 | export type { UseSortableListItemProps } from "./useSortableListItem";
11 | export type { SortableListItemProps } from "./SortableListItem";
12 |
--------------------------------------------------------------------------------
/package/src/components/SortableList/useSortableListItem.ts:
--------------------------------------------------------------------------------
1 | import { ListData } from "./index";
2 |
3 | export interface UseSortableListItemProps {
4 | index: number;
5 | dragStartIdx: number;
6 | listData: ListData;
7 | draggable?: boolean;
8 | onDragStart?: (e: React.DragEvent) => void;
9 | onDragOver?: (e: React.DragEvent) => void;
10 | onDrop?: (
11 | e: React.DragEvent,
12 | newListData: ListData
13 | ) => void;
14 | onClick?: (e: React.MouseEvent) => void;
15 | }
16 |
17 | export const useSortableListItem: (
18 | props: UseSortableListItemProps
19 | ) => React.LiHTMLAttributes = ({
20 | index,
21 | dragStartIdx,
22 | listData,
23 | draggable = true,
24 | onDragStart: onDragStartCb,
25 | onDragOver: onDragOverCb,
26 | onDrop: onDropCb,
27 | onClick: onClickCb,
28 | }) => {
29 | return {
30 | draggable,
31 | onDragStart: (e: React.DragEvent) => {
32 | e.stopPropagation();
33 | e.currentTarget.classList.add("dragstart");
34 | onDragStartCb && onDragStartCb(e);
35 | },
36 | onDragEnd: (e: React.DragEvent) => {
37 | e.stopPropagation();
38 | e.currentTarget.classList.remove("dragstart");
39 | },
40 | onDragEnter: (e: React.DragEvent) => {
41 | e.stopPropagation();
42 | e.currentTarget.classList.add("dragover");
43 | },
44 | onDragLeave: (e: React.DragEvent) => {
45 | e.stopPropagation();
46 | e.currentTarget.classList.remove("dragover");
47 | },
48 | onDragOver: (e: React.DragEvent) => {
49 | e.preventDefault();
50 | e.stopPropagation();
51 | onDragOverCb && onDragOverCb(e);
52 | },
53 | onDrop: (e: React.DragEvent) => {
54 | e.stopPropagation();
55 | e.currentTarget.classList.remove("dragover");
56 | const curListData = [...listData];
57 | const draggedItem = listData[dragStartIdx];
58 | curListData.splice(dragStartIdx, 1);
59 | const newListData = (
60 | dragStartIdx < index
61 | ? [
62 | ...curListData.slice(0, index),
63 | draggedItem,
64 | ...curListData.slice(index, curListData.length),
65 | ]
66 | : [
67 | ...curListData.slice(0, index),
68 | draggedItem,
69 | ...curListData.slice(index, curListData.length),
70 | ]
71 | ).map((item, idx) => ({ ...item, index: idx }));
72 |
73 | onDropCb && onDropCb(e, newListData);
74 | },
75 | onClick: (e: React.MouseEvent) => {
76 | e.stopPropagation();
77 | onClickCb && onClickCb(e);
78 | },
79 | };
80 | };
81 |
--------------------------------------------------------------------------------
/package/src/hooks/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./useNonNullableContext";
2 | export * from "./useVariableColor";
3 | export * from "./useClickOutside";
4 | export * from "./useRefsDispatch";
5 |
--------------------------------------------------------------------------------
/package/src/hooks/useClickOutside.ts:
--------------------------------------------------------------------------------
1 | import { MutableRefObject, useEffect } from "react";
2 |
3 | const useClickOutside = (
4 | ref: MutableRefObject,
5 | handler: (event: MouseEvent) => void
6 | ) => {
7 | useEffect(() => {
8 | const callback = (event: MouseEvent) => {
9 | const el = ref.current;
10 | const { target } = event;
11 | if (!event || !el || el.contains(target as Node)) return;
12 | handler(event);
13 | };
14 |
15 | document.addEventListener("click", callback);
16 | return () => document.removeEventListener("click", callback);
17 | }, [ref, handler]);
18 | };
19 |
20 | export default useClickOutside;
21 |
--------------------------------------------------------------------------------
/package/src/hooks/useGridTemplate.ts:
--------------------------------------------------------------------------------
1 | import {
2 | ActiveUI,
3 | defaultInterfacePlacement,
4 | InterfacePlacement,
5 | } from "@/components/AudioPlayer/Context";
6 | import { useCallback, useState } from "react";
7 |
8 | export const useGridTemplate = (
9 | activeUI: ActiveUI,
10 | templateArea: InterfacePlacement["templateArea"] | undefined,
11 | customComponentsArea?: InterfacePlacement["customComponentsArea"]
12 | ) => {
13 | const generateGridTemplateValues = useCallback(
14 | (
15 | activeUi: ActiveUI,
16 | templatePlacement?: InterfacePlacement["templateArea"],
17 | customComponentsPlacement?: InterfacePlacement["customComponentsArea"]
18 | ) => {
19 | const activeUIAllKeys = Object.keys(
20 | defaultInterfacePlacement.templateArea
21 | ).filter((key) => {
22 | if (
23 | (key === "trackTimeCurrent" || key === "trackTimeDuration") &&
24 | activeUi.trackTime === false
25 | ) {
26 | return false;
27 | }
28 |
29 | if (activeUi[key as keyof ActiveUI] !== undefined) {
30 | return activeUi[key as keyof ActiveUI];
31 | }
32 | return true;
33 | });
34 |
35 | const activeUIKeysArr = activeUi.all
36 | ? activeUIAllKeys
37 | : Object.entries(activeUi)
38 | .filter(([, value]) => value)
39 | .map(([key]) => key);
40 |
41 | const renameTrackTime = () => {
42 | if (activeUIKeysArr.find((key) => key === "trackTime")) {
43 | activeUIKeysArr.splice(activeUIKeysArr.indexOf("trackTime"), 1);
44 | activeUIKeysArr.push("trackTimeCurrent");
45 | activeUIKeysArr.push("trackTimeDuration");
46 | }
47 | };
48 | renameTrackTime();
49 |
50 | const totalTemplatePlacement = {
51 | ...defaultInterfacePlacement.templateArea,
52 | ...templatePlacement,
53 | };
54 | const activeTemplatePlacementArr = Object.entries(
55 | totalTemplatePlacement
56 | ).filter(([key]) => activeUIKeysArr.includes(key));
57 |
58 | let maxRow = 1;
59 | const colsCntRecord: Record = {};
60 |
61 | const totalPlacementArr = [
62 | ...activeTemplatePlacementArr,
63 | ...Object.entries(customComponentsPlacement || {}),
64 | ]
65 | .map(([key, value]) => {
66 | const [rowWithText, colStrNum] = value!.split("-");
67 | const row = +rowWithText.split("row")[1];
68 |
69 | maxRow = Math.max(maxRow, row);
70 | colsCntRecord[row] = colsCntRecord[row] ? colsCntRecord[row] + 1 : 1;
71 | return {
72 | key,
73 | row,
74 | col: +colStrNum,
75 | };
76 | })
77 | .sort((a, b) => a.col - b.col);
78 |
79 | const maxCol = Math.max(...Object.values(colsCntRecord));
80 |
81 | let progressColIdx: number | undefined;
82 | const gridAreas = new Array(maxRow).fill("").map((_, rowIdx) => {
83 | let cols = "";
84 | let isWithProgress = false;
85 |
86 | const curRowPlacementArr = totalPlacementArr.filter(({ key, row }) => {
87 | if (row === rowIdx + 1) {
88 | if (key === "progress") {
89 | isWithProgress = true;
90 | }
91 | return true;
92 | }
93 | return false;
94 | });
95 |
96 | if (isWithProgress) {
97 | const progressCnt = maxCol - (curRowPlacementArr.length - 1);
98 |
99 | for (let i = 0; i < maxCol - (progressCnt - 1); i++) {
100 | if (curRowPlacementArr[i]?.key === "progress") {
101 | cols += ` row${rowIdx + 1}-${curRowPlacementArr[i].col} `.repeat(
102 | progressCnt
103 | );
104 | progressColIdx = Math.ceil(progressCnt / 2) + i - 1;
105 | } else {
106 | cols += ` row${rowIdx + 1}-${
107 | curRowPlacementArr[i] ? curRowPlacementArr[i].col : i + 1
108 | }`;
109 | }
110 | }
111 | } else {
112 | for (let i = 0; i < maxCol; i++) {
113 | cols += ` row${rowIdx + 1}-${
114 | curRowPlacementArr[i] ? curRowPlacementArr[i].col : i + 1
115 | }`;
116 | }
117 | }
118 |
119 | return cols.trimStart();
120 | });
121 |
122 | const maxWidth = window ? window.innerWidth - 100 : 1500;
123 | const gridColumns = new Array(maxRow).fill("").map((_, rowIdx) => {
124 | let cols = "";
125 | for (let i = 0; i < maxCol; i++) {
126 | if (progressColIdx === i && rowIdx === 0) {
127 | cols += ` 1fr`;
128 | continue;
129 | }
130 |
131 | cols += ` fit-content(${maxWidth}px)`;
132 | }
133 | return cols.trimStart();
134 | });
135 |
136 | return { gridAreas, gridColumns };
137 | },
138 | []
139 | );
140 |
141 | const [curActiveUI, setCurActiveUI] = useState(activeUI);
142 | const [curPlacementArea, setCurPlacementArea] = useState({
143 | templateArea,
144 | customComponentsArea,
145 | });
146 | const [curPlacementAreaValues, setCurPlacementAreaValues] = useState<{
147 | gridAreas: string[];
148 | gridColumns: string[];
149 | }>();
150 |
151 | if (!curPlacementAreaValues) {
152 | const { gridAreas, gridColumns } = generateGridTemplateValues(
153 | curActiveUI,
154 | curPlacementArea.templateArea,
155 | curPlacementArea.customComponentsArea
156 | );
157 | setCurPlacementAreaValues({ gridAreas, gridColumns });
158 | return [gridAreas, gridColumns] as const;
159 | }
160 |
161 | if (
162 | curActiveUI !== activeUI ||
163 | curPlacementArea.templateArea !== templateArea ||
164 | curPlacementArea.customComponentsArea !== customComponentsArea
165 | ) {
166 | setCurActiveUI(activeUI);
167 | setCurPlacementArea({ templateArea, customComponentsArea });
168 |
169 | const { gridAreas, gridColumns } = generateGridTemplateValues(
170 | activeUI,
171 | templateArea,
172 | customComponentsArea
173 | );
174 | setCurPlacementAreaValues({ gridAreas, gridColumns });
175 | }
176 |
177 | const { gridAreas, gridColumns } = curPlacementAreaValues;
178 | return [gridAreas, gridColumns] as const;
179 | };
180 |
--------------------------------------------------------------------------------
/package/src/hooks/useNonNullableContext.ts:
--------------------------------------------------------------------------------
1 | import { Context, useContext } from "react";
2 |
3 | export const useNonNullableContext = (context: Context): T => {
4 | const state = useContext(context);
5 | if (!state) throw new Error(`${context} is not provided or null`);
6 | return state;
7 | };
8 |
--------------------------------------------------------------------------------
/package/src/hooks/useRefsDispatch.ts:
--------------------------------------------------------------------------------
1 | import {
2 | audioPlayerDispatchContext,
3 | ElementRefs,
4 | } from "@/components/AudioPlayer/Context";
5 | import { useEffect } from "react";
6 | import { useNonNullableContext } from ".";
7 |
8 | export type ElementRefsRecord = Partial<
9 | Record>
10 | >;
11 |
12 | export const useRefsDispatch = (
13 | { refs }: { refs: ElementRefsRecord },
14 | deps: Array
15 | ) => {
16 | const audioPlayerDispatch = useNonNullableContext(audioPlayerDispatchContext);
17 |
18 | useEffect(() => {
19 | const isAllExist = Object.values(refs).every((ref) => ref.current);
20 | if (isAllExist) {
21 | const elementRefs = Object.entries(refs).reduce(
22 | (accObj, [key, ref]) => ({ ...accObj, [key]: ref.current }),
23 | {}
24 | ) as ElementRefs;
25 |
26 | audioPlayerDispatch({
27 | type: "SET_ELEMENT_REFS",
28 | elementRefs,
29 | });
30 | }
31 | }, [audioPlayerDispatch, ...Object.values(refs), ...deps]);
32 | };
33 |
--------------------------------------------------------------------------------
/package/src/hooks/useVariableColor.ts:
--------------------------------------------------------------------------------
1 | import { useLayoutEffect, useRef } from "react";
2 |
3 | export type VariableColors = Record;
4 |
5 | export const useVariableColor = (
6 | variableColors: VariableColors
7 | ) => {
8 | const colorsRef = useRef>();
9 | useLayoutEffect(() => {
10 | const parsedColors: VariableColors = Object.entries(
11 | variableColors
12 | ).reduce(
13 | (acc, [key, varName]) => ({
14 | ...acc,
15 | [key]: window
16 | .getComputedStyle(
17 | document.getElementsByClassName("rm-audio-player-provider")[0]
18 | )
19 | .getPropertyValue(`${varName}`),
20 | }),
21 | {} as VariableColors
22 | );
23 | colorsRef.current = parsedColors;
24 | }, [variableColors]);
25 |
26 | return colorsRef;
27 | };
28 |
--------------------------------------------------------------------------------
/package/src/index.ts:
--------------------------------------------------------------------------------
1 | import AudioPlayerWithProviders from "./components/AudioPlayer";
2 | import { CustomComponent } from "./components/AudioPlayer/Interface/CustomComponent";
3 |
4 | export default AudioPlayerWithProviders;
5 | AudioPlayerWithProviders.CustomComponent = CustomComponent;
6 |
7 | export * from "./components/AudioPlayer";
8 | export * from "./components/AudioPlayer/Context";
9 | export * from "./components/AudioPlayer/Player";
10 | export * from "./components/Provider/SpectrumProvider";
11 |
--------------------------------------------------------------------------------
/package/src/styles/GlobalStyle.ts:
--------------------------------------------------------------------------------
1 | import { createGlobalStyle } from "styled-components";
2 | import "./vars.css";
3 | export const GlobalStyle = createGlobalStyle`
4 |
5 | .rm-audio-player-provider {
6 | * {
7 | box-sizing: border-box;
8 | font: inherit;
9 | font-size: 100%;
10 | }
11 |
12 | ol,
13 | ul {
14 | list-style: none;
15 | margin: 0;
16 | padding: 0;
17 | }
18 | blockquote,
19 | q {
20 | quotes: none;
21 | }
22 | blockquote:before,
23 | blockquote:after,
24 | q:before,
25 | q:after {
26 | content: '';
27 | content: none;
28 | }
29 | table {
30 | border-collapse: collapse;
31 | border-spacing: 0;
32 | }
33 | button {
34 | margin: 0;
35 | padding: 0;
36 | background: transparent;
37 | cursor: pointer;
38 | vertical-align: baseline;
39 | border: 0;
40 | }
41 | }`;
42 |
--------------------------------------------------------------------------------
/package/src/styles/vars.css:
--------------------------------------------------------------------------------
1 | .spectrum--medium_9e130c, .spectrum--large_9e130c {
2 | --rm-audio-player-interface-container:var(--spectrum-global-color-gray-100);
3 | --rm-audio-player-volume-background: #ccc;
4 | --rm-audio-player-volume-panel-background:#f2f2f2;
5 | --rm-audio-player-volume-panel-border:#ccc;
6 | --rm-audio-player-volume-thumb: #d3d3d3;
7 | --rm-audio-player-volume-fill:rgba(0, 0, 0, 0.5);
8 | --rm-audio-player-volume-track:#ababab;
9 | --rm-audio-player-track-current-time:#0072F5;
10 | --rm-audio-player-track-duration:#8c8c8c;
11 | --rm-audio-player-progress-bar:#0072F5;
12 | --rm-audio-player-progress-bar-background:#D1D1D1;
13 | --rm-audio-player-waveform-cursor:var(--spectrum-global-color-gray-800);
14 | --rm-audio-player-waveform-background:var(--rm-audio-player-progress-bar-background);
15 | --rm-audio-player-waveform-bar:var(--rm-audio-player-progress-bar);
16 | --rm-audio-player-sortable-list:var(--spectrum-global-color-gray-200);
17 | --rm-audio-player-sortable-list-button-active:#0072F5;
18 | --rm-audio-player-selected-list-item-background:var(--spectrum-global-color-gray-500);
19 | }
20 |
--------------------------------------------------------------------------------
/package/src/utils/generateUnionNumType.ts:
--------------------------------------------------------------------------------
1 | type _GenerateUnionNum<
2 | Length extends number,
3 | Counter extends number[],
4 | Accumulator extends number
5 | > = Counter["length"] extends Length
6 | ? Accumulator
7 | : _GenerateUnionNum<
8 | Length,
9 | [number, ...Counter],
10 | Accumulator | Counter["length"]
11 | >;
12 |
13 | export type NumbersToUnionNum = Num extends number
14 | ? Num extends 0
15 | ? never
16 | : Exclude<_GenerateUnionNum, 0>
17 | : never;
18 |
--------------------------------------------------------------------------------
/package/src/utils/getRandomNumber.ts:
--------------------------------------------------------------------------------
1 | export const getRandomNumber = (min: number, max: number) => {
2 | return Math.round(Math.random() * (max - min) + min);
3 | };
4 |
--------------------------------------------------------------------------------
/package/src/utils/getTime.ts:
--------------------------------------------------------------------------------
1 | export const getTimeWithPadStart = (time: number) => {
2 | const minutes = `${Math.floor(time / 60)}`.padStart(2, "0");
3 | const seconds = `${Math.floor(time % 60)}`.padStart(2, "0");
4 |
5 | return `${minutes}:${seconds}`;
6 | };
7 |
--------------------------------------------------------------------------------
/package/src/utils/refs.ts:
--------------------------------------------------------------------------------
1 | export type ReactRef =
2 | | React.Ref
3 | | React.RefObject
4 | | React.MutableRefObject;
5 |
--------------------------------------------------------------------------------
/package/src/utils/resetAudioValues.ts:
--------------------------------------------------------------------------------
1 | import { ElementRefs } from "@/components/AudioPlayer/Context/StateContext";
2 | import { getTimeWithPadStart } from "./getTime";
3 |
4 | export const resetAudioValues = (
5 | elementRefs?: ElementRefs,
6 | duration?: number,
7 | restart?: boolean
8 | ) => {
9 | if (!elementRefs) return;
10 |
11 | const {
12 | progressHandleEl,
13 | progressValueEl,
14 | trackCurTimeEl,
15 | trackDurationEl,
16 | audioEl,
17 | } = elementRefs;
18 | if (restart) {
19 | if (audioEl) {
20 | audioEl.currentTime = 0;
21 | }
22 | }
23 | if (progressHandleEl && progressValueEl) {
24 | progressValueEl.style.transform = `scaleX(0)`;
25 | progressHandleEl.style.transform = `translateX(0px)`;
26 | }
27 |
28 | if (trackCurTimeEl && trackDurationEl) {
29 | trackCurTimeEl.innerHTML = "00:00";
30 | if (!restart) {
31 | trackDurationEl.innerHTML = duration
32 | ? getTimeWithPadStart(duration)
33 | : "00:00";
34 | }
35 | }
36 | };
37 |
--------------------------------------------------------------------------------
/package/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "useDefineForClassFields": true,
5 | "lib": ["DOM", "DOM.Iterable", "ESNext"],
6 | "allowJs": false,
7 | "skipLibCheck": false,
8 | "esModuleInterop": false,
9 | "allowSyntheticDefaultImports": true,
10 | "strict": true,
11 | "forceConsistentCasingInFileNames": true,
12 | "module": "ESNext",
13 | "moduleResolution": "Node",
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "noEmit": true,
17 | "jsx": "react-jsx",
18 | "paths":{
19 | "@/components/*": ["./src/components/*"],
20 | "@/hooks/*": ["./src/hooks/*"],
21 | "@/utils/*": ["./src/utils/*"],
22 | "@/styles/*": ["./src/styles/*"],
23 | }
24 | },
25 | "include": ["**/*.d.ts","src","preview"],
26 | }
27 |
--------------------------------------------------------------------------------
/package/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import react from "@vitejs/plugin-react";
3 | import dts from "vite-plugin-dts";
4 | import path from "node:path";
5 | import libCss from "vite-plugin-libcss";
6 |
7 | export default defineConfig({
8 | plugins: [
9 | react(),
10 | dts({ outputDir: "dist/types", include: "src" }),
11 | libCss(),
12 | ],
13 | resolve: {
14 | alias: {
15 | "@/components": path.resolve(__dirname, "./src/components"),
16 | "@/hooks": path.resolve(__dirname, "./src/hooks"),
17 | "@/utils": path.resolve(__dirname, "./src/utils"),
18 | "@/styles/*": path.resolve(__dirname, "./src/styles/*"),
19 | },
20 | },
21 | build: {
22 | lib: {
23 | entry: path.resolve(__dirname, "src/index.ts"),
24 | name: "react-modern-audio-player",
25 | formats: ["es"],
26 | fileName: (format) =>
27 | format === "es" ? `index.es.js` : `index.${format}`,
28 | },
29 | rollupOptions: {
30 | external: ["react", "react-dom", "styled-components"],
31 | output: {
32 | globals: {
33 | react: "React",
34 | "react-dom": "ReactDOM",
35 | "styled-components": "styled",
36 | },
37 | exports: "named",
38 | },
39 | },
40 | cssCodeSplit: true,
41 | },
42 | });
43 |
--------------------------------------------------------------------------------
/storybook/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es2021": true
5 | },
6 | "extends": [
7 | "eslint:recommended",
8 | "plugin:@typescript-eslint/recommended",
9 | "plugin:prettier/recommended",
10 | "plugin:react/recommended",
11 | "plugin:react/jsx-runtime"
12 | ],
13 | "parser": "@typescript-eslint/parser",
14 | "parserOptions": {
15 | "ecmaVersion": "latest",
16 | "sourceType": "module"
17 | },
18 | "plugins": [
19 | "@typescript-eslint",
20 | "react-hooks"
21 | ],
22 | "rules": {
23 | "react-hooks/rules-of-hooks": "error",
24 | "react-hooks/exhaustive-deps": "warn",
25 | "no-console":[
26 | "error",
27 | {
28 | "allow": ["warn", "error"]
29 | }
30 | ],
31 | "no-unused-vars": "off",
32 | "@typescript-eslint/no-unused-vars": ["error"]
33 | },
34 |
35 | // for new JSX transform from react17
36 | "settings": {
37 | "import/parsers": {
38 | "@typescript-eslint/parser": [".ts", ".tsx", ".js"]
39 | },
40 | "import/resolver": {
41 | "typescript": "./tsconfig.json"
42 | },
43 | "react": {
44 | "createClass": "createReactClass", // Regex for Component Factory to use,
45 | // default to "createReactClass"
46 | "pragma": "React", // Pragma to use, default to "React"
47 | "fragment": "Fragment", // Fragment to use (may be a property of ), default to "Fragment"
48 | "version": "detect", // React version. "detect" automatically picks the version you have installed.
49 | // You can also use `16.0`, `16.3`, etc, if you want to override the detected value.
50 | // It will default to "latest" and warn if missing, and to "detect" in the future
51 | "flowVersion": "0.53" // Flow version
52 | },
53 | "propWrapperFunctions": [
54 | // The names of any function used to wrap propTypes, e.g. `forbidExtraProps`. If this isn't set, any propTypes wrapped in a function will be skipped.
55 | "forbidExtraProps",
56 | {"property": "freeze", "object": "Object"},
57 | {"property": "myFavoriteWrapper"},
58 | // for rules that check exact prop wrappers
59 | {"property": "forbidExtraProps", "exact": true}
60 | ],
61 | "componentWrapperFunctions": [
62 | // The name of any function used to wrap components, e.g. Mobx `observer` function. If this isn't set, components wrapped by these functions will be skipped.
63 | "observer", // `property`
64 | {"property": "styled"}, // `object` is optional
65 | {"property": "observer", "object": "Mobx"},
66 | {"property": "observer", "object": ""} // sets `object` to whatever value `settings.react.pragma` is set to
67 | ],
68 | "formComponents": [
69 | // Components used as alternatives to
70 | "CustomForm",
71 | {"name": "Form", "formAttribute": "endpoint"}
72 | ],
73 | "linkComponents": [
74 | // Components used as alternatives to for linking, eg.
75 | "Hyperlink",
76 | {"name": "Link", "linkAttribute": "to"}
77 | ]
78 |
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/storybook/.storybook/main.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "stories": [
3 | "../src/**/*.stories.mdx",
4 | "../src/**/*.stories.@(js|jsx|ts|tsx)"
5 | ],
6 | "addons": [
7 | "@storybook/addon-links",
8 | "@storybook/addon-essentials",
9 | "@storybook/addon-interactions",
10 | "@storybook/preset-create-react-app"
11 | ],
12 | "framework": "@storybook/react",
13 | "core": {
14 | "builder": "@storybook/builder-webpack5"
15 | }
16 | }
--------------------------------------------------------------------------------
/storybook/.storybook/preview.js:
--------------------------------------------------------------------------------
1 | export const parameters = {
2 | actions: { argTypesRegex: "^on[A-Z].*" },
3 | controls: {
4 | matchers: {
5 | color: /(background|color)$/i,
6 | date: /Date$/,
7 | },
8 | },
9 | }
--------------------------------------------------------------------------------
/storybook/README.md:
--------------------------------------------------------------------------------
1 | # Player Test
2 |
--------------------------------------------------------------------------------
/storybook/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "storybook",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/jest-dom": "^5.16.4",
7 | "@testing-library/react": "^13.3.0",
8 | "@testing-library/user-event": "^13.5.0",
9 | "@types/jest": "^27.5.2",
10 | "@types/node": "^16.11.45",
11 | "@types/react": "17.0.33",
12 | "@types/react-dom": "17.0.10",
13 | "react": "17.0.2",
14 | "react-dom": "17.0.2",
15 | "react-modern-audio-player": "^1.2.4",
16 | "react-scripts": "5.0.1",
17 | "typescript": "^4.7.4",
18 | "web-vitals": "^2.1.4"
19 | },
20 | "scripts": {
21 | "start": "react-scripts start",
22 | "build": "react-scripts build",
23 | "test": "react-scripts test",
24 | "eject": "react-scripts eject",
25 | "sb": "start-storybook -p 6006 -s public",
26 | "build-storybook": "build-storybook -s public"
27 | },
28 | "eslintConfig": {
29 | "extends": [
30 | "react-app",
31 | "react-app/jest"
32 | ],
33 | "overrides": [
34 | {
35 | "files": [
36 | "**/*.stories.*"
37 | ],
38 | "rules": {
39 | "import/no-anonymous-default-export": "off"
40 | }
41 | }
42 | ]
43 | },
44 | "browserslist": {
45 | "production": [
46 | ">0.2%",
47 | "not dead",
48 | "not op_mini all"
49 | ],
50 | "development": [
51 | "last 1 chrome version",
52 | "last 1 firefox version",
53 | "last 1 safari version"
54 | ]
55 | },
56 | "devDependencies": {
57 | "@storybook/addon-actions": "^6.5.9",
58 | "@storybook/addon-essentials": "^6.5.9",
59 | "@storybook/addon-interactions": "^6.5.9",
60 | "@storybook/addon-links": "^6.5.9",
61 | "@storybook/builder-webpack5": "^6.5.9",
62 | "@storybook/manager-webpack5": "^6.5.9",
63 | "@storybook/node-logger": "^6.5.9",
64 | "@storybook/preset-create-react-app": "^4.1.2",
65 | "@storybook/react": "^6.5.9",
66 | "@storybook/testing-library": "^0.0.13",
67 | "babel-plugin-named-exports-order": "^0.0.2",
68 | "prop-types": "^15.8.1",
69 | "webpack": "^5.73.0"
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/storybook/public/audio/audio-1.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/slash9494/react-modern-audio-player/c2f9d4ca25779e00d32a9fdda751fcbdf36a5eeb/storybook/public/audio/audio-1.mp3
--------------------------------------------------------------------------------
/storybook/public/audio/audio-2.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/slash9494/react-modern-audio-player/c2f9d4ca25779e00d32a9fdda751fcbdf36a5eeb/storybook/public/audio/audio-2.mp3
--------------------------------------------------------------------------------
/storybook/public/audio/audio-3.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/slash9494/react-modern-audio-player/c2f9d4ca25779e00d32a9fdda751fcbdf36a5eeb/storybook/public/audio/audio-3.mp3
--------------------------------------------------------------------------------
/storybook/public/audio/audio-4.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/slash9494/react-modern-audio-player/c2f9d4ca25779e00d32a9fdda751fcbdf36a5eeb/storybook/public/audio/audio-4.mp3
--------------------------------------------------------------------------------
/storybook/public/audio/audio-5.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/slash9494/react-modern-audio-player/c2f9d4ca25779e00d32a9fdda751fcbdf36a5eeb/storybook/public/audio/audio-5.mp3
--------------------------------------------------------------------------------
/storybook/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/slash9494/react-modern-audio-player/c2f9d4ca25779e00d32a9fdda751fcbdf36a5eeb/storybook/public/favicon.ico
--------------------------------------------------------------------------------
/storybook/public/images/audio-1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/slash9494/react-modern-audio-player/c2f9d4ca25779e00d32a9fdda751fcbdf36a5eeb/storybook/public/images/audio-1.jpg
--------------------------------------------------------------------------------
/storybook/public/images/audio-2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/slash9494/react-modern-audio-player/c2f9d4ca25779e00d32a9fdda751fcbdf36a5eeb/storybook/public/images/audio-2.jpg
--------------------------------------------------------------------------------
/storybook/public/images/audio-3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/slash9494/react-modern-audio-player/c2f9d4ca25779e00d32a9fdda751fcbdf36a5eeb/storybook/public/images/audio-3.jpg
--------------------------------------------------------------------------------
/storybook/public/images/audio-4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/slash9494/react-modern-audio-player/c2f9d4ca25779e00d32a9fdda751fcbdf36a5eeb/storybook/public/images/audio-4.jpg
--------------------------------------------------------------------------------
/storybook/public/images/audio-5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/slash9494/react-modern-audio-player/c2f9d4ca25779e00d32a9fdda751fcbdf36a5eeb/storybook/public/images/audio-5.jpg
--------------------------------------------------------------------------------
/storybook/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | React App
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/storybook/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/slash9494/react-modern-audio-player/c2f9d4ca25779e00d32a9fdda751fcbdf36a5eeb/storybook/public/logo192.png
--------------------------------------------------------------------------------
/storybook/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/slash9494/react-modern-audio-player/c2f9d4ca25779e00d32a9fdda751fcbdf36a5eeb/storybook/public/logo512.png
--------------------------------------------------------------------------------
/storybook/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/storybook/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/storybook/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .App-logo {
6 | height: 40vmin;
7 | pointer-events: none;
8 | }
9 |
10 | @media (prefers-reduced-motion: no-preference) {
11 | .App-logo {
12 | animation: App-logo-spin infinite 20s linear;
13 | }
14 | }
15 |
16 | .App-header {
17 | background-color: #282c34;
18 | min-height: 100vh;
19 | display: flex;
20 | flex-direction: column;
21 | align-items: center;
22 | justify-content: center;
23 | font-size: calc(10px + 2vmin);
24 | color: white;
25 | }
26 |
27 | .App-link {
28 | color: #61dafb;
29 | }
30 |
31 | @keyframes App-logo-spin {
32 | from {
33 | transform: rotate(0deg);
34 | }
35 | to {
36 | transform: rotate(360deg);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/storybook/src/App.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render, screen } from '@testing-library/react';
3 | import App from './App';
4 |
5 | test('renders learn react link', () => {
6 | render();
7 | const linkElement = screen.getByText(/learn react/i);
8 | expect(linkElement).toBeInTheDocument();
9 | });
10 |
--------------------------------------------------------------------------------
/storybook/src/App.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import logo from './logo.svg';
3 | import './App.css';
4 |
5 | function App() {
6 | return (
7 |
23 | );
24 | }
25 |
26 | export default App;
27 |
--------------------------------------------------------------------------------
/storybook/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
14 |
--------------------------------------------------------------------------------
/storybook/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import './index.css'
4 | import App from './App'
5 |
6 | ReactDOM.render(
7 |
8 |
9 | ,
10 | document.getElementById('root')
11 | )
12 |
--------------------------------------------------------------------------------
/storybook/src/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/storybook/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/storybook/src/reportWebVitals.ts:
--------------------------------------------------------------------------------
1 | import { ReportHandler } from 'web-vitals';
2 |
3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => {
4 | if (onPerfEntry && onPerfEntry instanceof Function) {
5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
6 | getCLS(onPerfEntry);
7 | getFID(onPerfEntry);
8 | getFCP(onPerfEntry);
9 | getLCP(onPerfEntry);
10 | getTTFB(onPerfEntry);
11 | });
12 | }
13 | };
14 |
15 | export default reportWebVitals;
16 |
--------------------------------------------------------------------------------
/storybook/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/storybook/src/stories/Test.stories.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { ComponentStory, ComponentMeta } from "@storybook/react";
3 | import { Test } from "./Test";
4 |
5 | export default {
6 | title: "Example/Test",
7 | component: Test,
8 | argTypes: {
9 | playerPlacement: {
10 | options: [
11 | "bottom",
12 | "top",
13 | "bottom-left",
14 | "bottom-right",
15 | "top-left",
16 | "top-right",
17 | ],
18 | control: { type: "select" },
19 | },
20 | mode: {
21 | options: ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"],
22 | control: { type: "select" },
23 | defaultValue: "0",
24 | },
25 | progressType: {
26 | options: ["bar", "waveform"],
27 | control: { type: "select" },
28 | },
29 | },
30 | } as ComponentMeta;
31 |
32 | const Template: ComponentStory = (args) => ;
33 |
34 | export const PlayerTest = Template.bind({});
35 |
--------------------------------------------------------------------------------
/storybook/src/stories/Test.tsx:
--------------------------------------------------------------------------------
1 | import { FC, useEffect, useState } from "react";
2 | import AudioPlayer from "../../../package/dist/index.es.js";
3 | import { PlayList } from "../../../package/dist/types/components/AudioPlayer/Context";
4 | import { playerMode } from "./playerMode.ts";
5 | // import AudioPlayer, { PlayList } from "react-modern-audio-player";
6 |
7 | const initialState = {
8 | volume: 0.2,
9 | curPlayId: 3,
10 | };
11 |
12 | export const Test: FC<{ mode: string }> = ({ mode }) => {
13 | const curPlayerMode = playerMode[+mode];
14 |
15 | return (
16 |
28 | );
29 | };
30 |
--------------------------------------------------------------------------------
/storybook/src/stories/assets/code-brackets.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/storybook/src/stories/assets/colors.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/storybook/src/stories/assets/comments.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/storybook/src/stories/assets/direction.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/storybook/src/stories/assets/flow.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/storybook/src/stories/assets/plugin.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/storybook/src/stories/assets/repo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/storybook/src/stories/assets/stackalt.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/storybook/src/stories/playList.ts:
--------------------------------------------------------------------------------
1 | import { PlayList } from "react-modern-audio-player";
2 |
3 | export const playList: PlayList = [
4 | {
5 | name: "Relax And Sleep",
6 | writer: "Anton Vlasov",
7 | img: "/images/audio-1.jpg",
8 | src: "/audio/audio-1.mp3",
9 | id: 1,
10 | },
11 | {
12 | name: "Don't You Think Lose",
13 | writer: "Anton Vlasov",
14 | img: "/images/audio-2.jpg",
15 | src: "https://cdn.pixabay.com/audio/2022/01/21/audio_c44fddb424.mp3",
16 | id: 2,
17 | },
18 | {
19 | name: "The Cradle of Your Soul dsdasdasdas",
20 | writer: "lemonaudiostudio",
21 | img: "/images/audio-3.jpg",
22 | src: "/audio/audio-3.mp3",
23 | id: 3,
24 | },
25 | {
26 | name: "Spirit Blossom",
27 | writer: "RomanBelov",
28 | img: "/images/audio-4.jpg",
29 | src: "/audio/audio-4.mp3",
30 | id: 4,
31 | },
32 | {
33 | name: "Everything Feels New",
34 | writer: "EvgenyBardyuzha",
35 | img: "/images/audio-5.jpg",
36 | src: "/audio/audio-5.mp3",
37 | id: 5,
38 | },
39 | ];
40 |
--------------------------------------------------------------------------------
/storybook/src/stories/playerMode.ts:
--------------------------------------------------------------------------------
1 |
2 | import {RMAudioPlayerProps} from '../../../package/dist/types';
3 | import { playList } from './playList';
4 |
5 | export const playerMode: Record<
6 | number,
7 | RMAudioPlayerProps
8 | > = {
9 | 0: {
10 | playList:playList,
11 | placement:{
12 | interface: {
13 | templateArea: {
14 | artwork: "row1-1",
15 | trackInfo: "row1-2",
16 | trackTimeCurrent: "row1-3",
17 | trackTimeDuration: "row1-4",
18 | progress: "row1-5",
19 | repeatType: "row1-6",
20 | volume: "row1-7",
21 | playButton: "row1-8",
22 | playList: "row1-9"
23 | },
24 | },
25 | player: 'bottom-left',
26 | },
27 | activeUI:{
28 | all: true,
29 | progress: "waveform",
30 | }
31 | },
32 | 1: {
33 | playList:playList,
34 | placement:{
35 | interface: {
36 | templateArea: {
37 | trackTimeDuration: "row1-5",
38 | progress: "row1-4",
39 | playButton: "row1-6",
40 | repeatType: "row1-7",
41 | volume: "row1-8"
42 | },
43 | },
44 | player: 'static',
45 | },
46 | activeUI:{
47 | progress: 'bar',
48 | playButton: true,
49 | repeatType: true,
50 | volume: true,
51 | trackTime: true,
52 | playList: 'unSortable',
53 | },
54 | rootContainerProps:{
55 | colorScheme:'light'
56 | }
57 | },
58 | 2: {
59 | playList:playList,
60 | placement:{
61 | interface: {
62 | templateArea: {
63 | artwork: "row1-2",
64 | playList: "row1-3",
65 | trackInfo: "row2-2",
66 | trackTimeCurrent: "row3-1",
67 | progress: "row3-2",
68 | trackTimeDuration: "row3-3",
69 | playButton: "row4-2",
70 | repeatType: "row4-1",
71 | volume: "row4-3"
72 | },
73 | },
74 | player: 'top-left',
75 | volumeSlider:'left',
76 | playList:'top'
77 | },
78 | activeUI:{
79 | all: true,
80 | progress: 'bar',
81 | },
82 | },
83 | 3: {
84 | playList:playList,
85 | placement:{
86 | interface: {
87 | templateArea: {
88 | progress:"row1-1",
89 | playButton:'row2-2',
90 | playList:'row2-1',
91 | volume: 'row2-3',
92 | },
93 | },
94 | playList:'bottom',
95 | player: 'static',
96 | },
97 | activeUI:{
98 | progress: 'bar',
99 | playButton: true,
100 | volume: true,
101 | playList: 'sortable',
102 | prevNnext: true,
103 | },
104 | },
105 | 4: {
106 | playList:playList,
107 | placement:undefined,
108 | activeUI:{
109 | all: true,
110 | progress: 'waveform',
111 | },
112 | }
113 | };
--------------------------------------------------------------------------------
/storybook/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "noFallthroughCasesInSwitch": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "react-jsx"
22 | },
23 | "include": [
24 | "src"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------