├── .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
for forms, eg. 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 | rm-audio-player 3 |

React Modern Audio Player

4 |

5 | 6 |

7 | 8 | License 9 | 10 | 11 | Version 12 | 13 | 14 | Download 15 | 16 | 17 | BundleSize 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 |
350 | 374 |
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 |
53 |
54 |
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 | 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 for forms, eg. 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 |
24 |
25 | 26 |
27 |
28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /storybook/src/stories/assets/code-brackets.svg: -------------------------------------------------------------------------------- 1 | illustration/code-brackets -------------------------------------------------------------------------------- /storybook/src/stories/assets/colors.svg: -------------------------------------------------------------------------------- 1 | illustration/colors -------------------------------------------------------------------------------- /storybook/src/stories/assets/comments.svg: -------------------------------------------------------------------------------- 1 | illustration/comments -------------------------------------------------------------------------------- /storybook/src/stories/assets/direction.svg: -------------------------------------------------------------------------------- 1 | illustration/direction -------------------------------------------------------------------------------- /storybook/src/stories/assets/flow.svg: -------------------------------------------------------------------------------- 1 | illustration/flow -------------------------------------------------------------------------------- /storybook/src/stories/assets/plugin.svg: -------------------------------------------------------------------------------- 1 | illustration/plugin -------------------------------------------------------------------------------- /storybook/src/stories/assets/repo.svg: -------------------------------------------------------------------------------- 1 | illustration/repo -------------------------------------------------------------------------------- /storybook/src/stories/assets/stackalt.svg: -------------------------------------------------------------------------------- 1 | illustration/stackalt -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------