├── .gitignore
├── README.md
├── README.old.md
├── package.json
├── public
├── favicon.ico
├── index.html
└── manifest.json
├── src
├── App.js
├── components
│ ├── Album.js
│ ├── AlbumList.js
│ ├── Background.js
│ ├── DetailPlaying.js
│ ├── InputForm.js
│ └── Player.js
├── global-styles.js
├── handleState
│ ├── handlePlayer.js
│ └── handleStatePlayList.js
└── index.js
└── yarn.lock
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 | .env
21 |
22 | npm-debug.log*
23 | yarn-debug.log*
24 | yarn-error.log*
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | 
2 |
3 | 해당 프로젝트는 React Hook을 공부하기 위해 진행된 사이드 프로젝트입니다.
4 |
5 | ## HookPlayer
6 |
7 | React 의 Hook 기능이 드디어 정상 기능으로 탑재되었습니다.
8 |
9 | 해당 Hook 기능을 사용해보기 위해서, 작은 사이드 프로젝트를 진행해보았습니다.
10 |
11 | Youtube 주소 또는 Video ID 를 활용하여 자신만의 플레이 리스트를 만들고,
12 |
13 | 이를 Local-Storage에 보관, 동기화하여 플레이 리스트가 유지되도록 개발하였습니다.
14 |
15 | Hook에 대해 사용해본 경험은 없었으나, 생각보다도 열띈 반응에 Hook에 대해 알아보고,
16 |
17 | 이해를 돕고자 해당 프로젝트를 진행하였습니다.
18 |
19 |
20 | ### `yarn`
21 |
22 | 명령어 yarn 을 사용하여, 필요한 모듈을 설치합니다.
23 |
24 | ### `yarn start`
25 |
26 | 이후 yarn start로 https://localhost:3000 에서 HookPlayer를 만나보실 수 있습니다.
27 |
--------------------------------------------------------------------------------
/README.old.md:
--------------------------------------------------------------------------------
1 | # hookPlayer-dev4us
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "hook-player",
3 | "version": "0.1.0",
4 | "private": true,
5 | "homepage": "https://dev4us.github.io/hook-player/",
6 | "dependencies": {
7 | "@fortawesome/fontawesome-svg-core": "^1.2.15",
8 | "@fortawesome/free-regular-svg-icons": "^5.7.2",
9 | "@fortawesome/free-solid-svg-icons": "^5.7.2",
10 | "@fortawesome/react-fontawesome": "^0.1.4",
11 | "axios": "^0.18.0",
12 | "gh-pages": "^2.0.1",
13 | "react": "next",
14 | "react-dom": "next",
15 | "react-favicon": "^0.0.14",
16 | "react-gh-corner": "^1.1.2",
17 | "react-helmet": "^5.2.0",
18 | "react-scripts": "2.1.5",
19 | "react-toastify": "^4.5.2",
20 | "react-yt": "^0.1.2",
21 | "styled-components": "^4.1.3",
22 | "styled-reset": "^1.6.8",
23 | "youtube-duration-format": "^0.2.0"
24 | },
25 | "scripts": {
26 | "start": "react-scripts start",
27 | "build": "react-scripts build",
28 | "test": "react-scripts test",
29 | "eject": "react-scripts eject",
30 | "predeploy": "npm run build",
31 | "deploy": "gh-pages -d build"
32 | },
33 | "eslintConfig": {
34 | "extends": "react-app"
35 | },
36 | "browserslist": [
37 | ">0.2%",
38 | "not dead",
39 | "not ie <= 11",
40 | "not op_mini all"
41 | ]
42 | }
43 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dev4us/hook-player/b7dcc9ec26a73f93d2ba4cdef3c1571136393fb2/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
10 |
11 |
15 |
16 |
25 | React App
26 |
27 |
28 |
29 |
30 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/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 | "start_url": ".",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/src/App.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 | import { Helmet } from "react-helmet";
4 | import Favicon from "react-favicon";
5 |
6 | // For handle State
7 | import handleStatePlayList from "./handleState/handleStatePlayList";
8 | import handlePlayer from "./handleState/handlePlayer";
9 |
10 | // Components
11 | import InputForm from "./components/InputForm";
12 | import AlbumList from "./components/AlbumList";
13 | import Player from "./components/Player";
14 | import Background from "./components/Background";
15 | import GHCorner from "react-gh-corner";
16 |
17 | const MainFrame = styled.div`
18 | display: flex;
19 |
20 | justify-content: center;
21 | align-items: center;
22 | position: absolute;
23 | width: 100%;
24 | height: 100%;
25 |
26 | top: 0;
27 | left: 0;
28 | z-index: 2;
29 | `;
30 |
31 | const BodyFrame = styled.div`
32 | display: flex;
33 | justify-content: center;
34 | flex-wrap: wrap;
35 | height: 80%;
36 | width: 70%;
37 | @media (max-width: 500px) {
38 | width: 90%;
39 | }
40 | `;
41 |
42 | const DetailPlayFrame = styled.div`
43 | /*flex: 1;*/
44 | width: 700px;
45 | height: 100%;
46 | @media (max-width: 985px) {
47 | height: 66%;
48 | }
49 | `;
50 | const ControllerPlayFrame = styled.div`
51 | display: flex;
52 | flex-direction: column;
53 | flex: 1;
54 | height: 100%;
55 | background: rgba(50, 50, 50, 0.5);
56 | -webkit-box-shadow: 0 3px 6px rgba(0, 0, 0, 0.12),
57 | 0 3px 6px rgba(0, 0, 0, 0.1725);
58 | box-shadow: 0 3px 6px rgba(0, 0, 0, 0.12), 0 3px 6px rgba(0, 0, 0, 0.1725);
59 | @media (max-width: 986px) {
60 | margin-top: 20%;
61 | margin-bottom: 30px;
62 | }
63 | `;
64 | const ControllerTitle = styled.div`
65 | font-size: 1.2rem;
66 | color: white;
67 | padding-top: 30px;
68 | padding-left: 30px;
69 | `;
70 |
71 | function App() {
72 | const {
73 | statePlayList,
74 | addStatePlayList,
75 | deleteStatePlayList
76 | } = handleStatePlayList();
77 | const { nowPlaying, setNowPlaying } = handlePlayer(statePlayList[0]);
78 |
79 | return (
80 |
81 |
82 |
83 | HookPlayer - dev4us
84 |
85 |
86 |
91 |
92 |
93 |
94 |
99 |
100 |
101 |
102 |
121 |
122 |
123 |
127 |
133 |
134 |
135 |
136 |
137 | );
138 | }
139 |
140 | export default App;
141 |
--------------------------------------------------------------------------------
/src/components/Album.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled, { keyframes, css } from "styled-components";
3 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
4 | import { faTrash } from "@fortawesome/free-solid-svg-icons";
5 | import { toast } from "react-toastify";
6 |
7 | const AlbumItem = styled.div`
8 | display: flex;
9 | position: relative;
10 | align-items: center;
11 | border-bottom: 1px dashed white;
12 | width: 100%;
13 | height: 100px;
14 | padding-left: 15px;
15 | cursor: pointer;
16 | ${props =>
17 | props.isPlaying &&
18 | css`
19 | background: rgba(228, 43, 43, 0.4);
20 | `};
21 |
22 | &:hover {
23 | background: rgba(0, 0, 0, 0.5);
24 | }
25 | `;
26 |
27 | const rotate = keyframes`
28 | 25%{
29 | transform:rotate(90deg);
30 | }
31 | 50%{
32 | transform:rotate(180deg);
33 | }
34 | 75%{
35 | transform:rotate(270deg);
36 | }
37 | 100%{
38 | transform:rotate(360deg);
39 | }
40 | `;
41 | const AlbumThumbnail = styled.div`
42 | display: block;
43 | width: 70px;
44 | height: 75%;
45 | border-radius: 100%;
46 | background-image: url(${props => props.background});
47 | background-size: cover;
48 | background-position: bottom;
49 | margin-right: 40px;
50 | ${props =>
51 | props.isPlaying &&
52 | css`
53 | animation: ${rotate} 5s linear infinite;
54 | `};
55 | @media (max-width: 500px) {
56 | width: 55px;
57 | height: 55px;
58 | margin-right: 10px;
59 | }
60 | `;
61 | const SubContainer = styled.div`
62 | display: block;
63 | float: right;
64 | width: 80%;
65 | height: 100%;
66 | `;
67 |
68 | const SubText = styled.div`
69 | display: flex;
70 | align-items: center;
71 | float: left;
72 | width: 80%;
73 | height: 100%;
74 | `;
75 | const SubIcon = styled.div`
76 | display: flex;
77 | align-items: center;
78 | float: right;
79 | width: 20%;
80 | height: 100%;
81 | `;
82 | const SubSongName = styled.a`
83 | color: white;
84 | font-size: 15px;
85 | font-weight: bold;
86 | `;
87 |
88 | const SubDuration = styled.a`
89 | font-size: 15px;
90 | color: white;
91 | margin-left: 5px;
92 | `;
93 |
94 | const DeleteBtn = styled(FontAwesomeIcon)`
95 | position: absolute;
96 | right: 1rem;
97 | font-size: 1.2rem;
98 | cursor: pointer;
99 | color: #8c8c8c;
100 | &:hover {
101 | color: white;
102 | }
103 | `;
104 |
105 | const Album = ({
106 | thumbnail,
107 | movieId,
108 | statePlayList,
109 | nowPlaying,
110 | setNowPlaying,
111 | deleteStatePlayList,
112 | songName,
113 | singer,
114 | duration
115 | }) => {
116 | const isPlaying = movieId === nowPlaying.videoKey;
117 | const getFullName = () => {
118 | if (songName !== "" && songName !== undefined) {
119 | if (songName.length > 30) {
120 | songName = songName.substring(0, 30) + "...";
121 | }
122 | return songName + " - " + singer;
123 | } else {
124 | return null;
125 | }
126 | };
127 |
128 | return (
129 |
131 | setNowPlaying(statePlayList.find(val => val.videoKey === movieId))
132 | }
133 | isPlaying={isPlaying}
134 | >
135 |
136 |
137 |
138 | {getFullName()}
139 | {`(${duration})`}
140 |
141 |
142 | {
145 | e.stopPropagation();
146 | if (deleteStatePlayList(movieId) !== true) {
147 | toast.error("하나의 항목은 유지하여야 합니다.");
148 | }
149 | }}
150 | />
151 |
152 |
153 |
154 | );
155 | };
156 |
157 | export default Album;
158 |
--------------------------------------------------------------------------------
/src/components/AlbumList.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 | import Album from "./Album";
4 |
5 | const AlbumContainer = styled.div`
6 | flex: 1;
7 | height: 100%;
8 | overflow-y: scroll;
9 | margin-top: 10%;
10 | `;
11 |
12 | const AlbumList = ({
13 | statePlayList,
14 | nowPlaying,
15 | setNowPlaying,
16 | deleteStatePlayList
17 | }) => {
18 | return (
19 |
20 | {statePlayList.map((data, index) => (
21 |
33 | ))}
34 |
35 | );
36 | };
37 |
38 | export default AlbumList;
39 |
--------------------------------------------------------------------------------
/src/components/Background.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 |
4 | const Component = styled.div`
5 | display: flex;
6 | position: relative;
7 | width: 100%;
8 | height: 100%;
9 | top: 0;
10 | left: 0;
11 |
12 | background-image: url(${props => props.backgroundURL});
13 | background-position: center;
14 | background-size: cover;
15 |
16 | filter: blur(8px) opacity(0.8);
17 | -webkit-filter: blur(8px) opacity(0.8);
18 | z-index: 1;
19 | `;
20 |
21 | const Background = ({ backgroundURL }) => {
22 | return ;
23 | };
24 |
25 | export default Background;
26 |
--------------------------------------------------------------------------------
/src/components/DetailPlaying.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import styled from "styled-components";
3 |
4 | const Container = styled.div`
5 | display: flex;
6 | flex: 1;
7 | align-items: center;
8 | flex-direction: column;
9 | width: 100%;
10 | `;
11 |
12 | const PlayingThumbnail = styled.img`
13 | width: 85%;
14 | height: 70%;
15 | -webkit-box-shadow: 0 15px 14px rgba(0, 0, 0, 0.5),
16 | 0 25px 25px rgba(0, 0, 0, 0.075);
17 | box-shadow: 0 15px 14px rgba(0, 0, 0, 0.5), 0 25px 25px rgba(0, 0, 0, 0.075);
18 | `;
19 |
20 | const SongTitle = styled.p`
21 | margin-top: 1.3rem;
22 | color: white;
23 | font-size: 1.2rem;
24 | letter-spacing: 1px;
25 | `;
26 |
27 | const SingerTitle = styled.p`
28 | margin-top: 0.5rem;
29 | color: #cbcbcb;
30 | font-size: 1rem;
31 | `;
32 |
33 | const DetailPlaying = ({ nowPlaying }) => {
34 | const { max_thumbnail, songName, singer } = nowPlaying;
35 | let afterSongName = "";
36 |
37 | const getSongName = () => {
38 | if (songName !== "" && songName !== undefined) {
39 | if (songName.length > 30) {
40 | afterSongName = songName.substring(0, 30) + "...";
41 | }
42 | return afterSongName;
43 | } else {
44 | return null;
45 | }
46 | };
47 | return (
48 |
49 |
50 | {getSongName()}
51 | {singer}
52 |
53 | );
54 | };
55 |
56 | export default DetailPlaying;
57 |
--------------------------------------------------------------------------------
/src/components/InputForm.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import styled, { keyframes } from "styled-components";
3 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
4 | import { faPlus } from "@fortawesome/free-solid-svg-icons";
5 | import axios from "axios";
6 | import ytDurationFormat from "youtube-duration-format";
7 |
8 | const Container = styled.div`
9 | display: flex;
10 | align-items: baseline;
11 | width: 100%;
12 | height: 5%;
13 | margin-top: 3%;
14 | `;
15 |
16 | const Subscription = styled.span`
17 | width: 15%;
18 | margin-left: 2%;
19 | margin-right: 2%;
20 | font-size: 1rem;
21 | font-weight: bold;
22 | color: #ececec;
23 | `;
24 |
25 | const SearchBtnHover = keyframes`
26 | 100%{
27 | background:rgba(255, 255, 255, 0.5);
28 | box-shadow:none;
29 | }
30 | `;
31 |
32 | const SearchBtnFrame = styled.span`
33 | display: flex;
34 | justify-content: center;
35 | align-items: center;
36 | cursor: pointer;
37 | width: 10%;
38 | height: 40px;
39 | border: 2px solid white;
40 | border-radius: 10px;
41 |
42 | -webkit-box-shadow: 0 3px 6px rgba(0, 0, 0, 0.12),
43 | 0 3px 6px rgba(0, 0, 0, 0.1725);
44 | box-shadow: 0 3px 6px rgba(0, 0, 0, 0.12), 0 3px 6px rgba(0, 0, 0, 0.1725);
45 |
46 | &:hover {
47 | animation: ${SearchBtnHover} 0.1s ease;
48 | animation-fill-mode: forwards;
49 | }
50 | `;
51 |
52 | const SearchBtn = styled(FontAwesomeIcon)`
53 | font-size: 1.2rem;
54 | cursor: pointer;
55 | color: #dcdcdc;
56 | `;
57 |
58 | const SearchInput = styled.input`
59 | background: none;
60 | width: 70%;
61 | height: 100%;
62 | color: #ececec;
63 | border: none;
64 | border-bottom: 2px solid #ececec;
65 | `;
66 |
67 | const InputForm = ({ statePlayList, addStatePlayList }) => {
68 | const [inputUrl, setUrl] = useState("");
69 |
70 | const getMovieData = async () => {
71 | if (!inputUrl) {
72 | alert("URL 이나 VideoId를 입력해주세요.");
73 | return false;
74 | }
75 |
76 | const fullUrl = inputUrl;
77 | let video_id = "";
78 |
79 | if (fullUrl.includes("v=") === false) {
80 | video_id = fullUrl;
81 | } else {
82 | video_id = fullUrl.split("v=")[1];
83 | let ampersandPosition = video_id.indexOf("&");
84 |
85 | if (ampersandPosition !== -1) {
86 | video_id = video_id.substring(0, ampersandPosition);
87 | }
88 | }
89 |
90 | const API_KEY = "AIzaSyCC2pMVczqa5crA9qxUFnceNC_0p2gV7gg";
91 | const API_URL = `https://www.googleapis.com/youtube/v3/videos?id=${video_id}&key=${API_KEY}&part=snippet,contentDetails,statistics,status`;
92 | const res = await axios.get(API_URL);
93 | setUrl("");
94 |
95 | if (res.status === 200 && res.data.items.length >= 1) {
96 | const highestThumbnail = () => {
97 | let returnData = "";
98 |
99 | if (res.data.items[0].snippet.thumbnails.maxres) {
100 | returnData = res.data.items[0].snippet.thumbnails.maxres.url;
101 | } else if (res.data.items[0].snippet.thumbnails.high) {
102 | returnData = res.data.items[0].snippet.thumbnails.high.url;
103 | } else if (res.data.items[0].snippet.thumbnails.medium) {
104 | returnData = res.data.items[0].snippet.thumbnails.medium.url;
105 | }
106 | return returnData;
107 | };
108 | const returnData = [
109 | {
110 | id: statePlayList.length || 0,
111 | songName: res.data.items[0].snippet.title,
112 | singer: res.data.items[0].snippet.channelTitle,
113 | videoKey: res.data.items[0].id,
114 | thumbnail: highestThumbnail(),
115 | max_thumbnail: highestThumbnail(),
116 | duration: ytDurationFormat(res.data.items[0].contentDetails.duration)
117 | }
118 | ];
119 |
120 | if (statePlayList.find(val => val.videoKey === res.data.items[0].id)) {
121 | alert("이미 등록된 음원입니다");
122 | return false;
123 | }
124 |
125 | addStatePlayList(returnData);
126 | } else {
127 | alert("존재하지 않거나 적절하지 않은 주소입니다.");
128 | }
129 | };
130 |
131 | return (
132 |
133 | Youtube Link:
134 | setUrl(e.target.value)}
137 | placeholder={`Insert Youtube URL or videoId`}
138 | />
139 | getMovieData()}>
140 |
141 |
142 |
143 | );
144 | };
145 |
146 | export default InputForm;
147 |
--------------------------------------------------------------------------------
/src/components/Player.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import DetailPlaying from "./DetailPlaying";
3 | import YouTube from "react-yt";
4 | import styled from "styled-components";
5 | import { toast } from "react-toastify";
6 |
7 | import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
8 | import {
9 | faPlayCircle,
10 | faPauseCircle,
11 | faChevronCircleLeft,
12 | faChevronCircleRight,
13 | faVolumeMute,
14 | faVolumeUp
15 | } from "@fortawesome/free-solid-svg-icons";
16 |
17 | const Container = styled.div`
18 | display: flex;
19 | flex-direction: column;
20 | align-items: center;
21 | `;
22 | const LeftFrame = styled.div`
23 | width: 100%;
24 | height: 100%;
25 | `;
26 |
27 | const PlayerHiddenFrame = styled.div`
28 | display: none;
29 | width: 1px;
30 | height: 1px;
31 | overflow: hidden;
32 | `;
33 | const Controller = styled.div`
34 | display: flex;
35 | flex-direction: column;
36 | align-items: center;
37 | width: 80%;
38 | padding-top: 5%;
39 | `;
40 | const DurationStatus = styled.div`
41 | width: 100%;
42 | height: 35px;
43 | color: white;
44 | font-size: 0.8rem;
45 | text-align: right;
46 | letter-spacing: 0.1rem;
47 | `;
48 |
49 | const DurationBar = styled.input`
50 | -webkit-appearance: none;
51 | width: 100%;
52 | height: 10px;
53 | border-radius: 5px;
54 | background: rgba(255, 255, 255, 0.5);
55 | outline: none;
56 | cursor: pointer;
57 |
58 | &::-webkit-slider-thumb {
59 | -webkit-appearance: none;
60 | appearance: none;
61 | width: 5px;
62 | height: 20px;
63 | background: white;
64 | cursor: pointer;
65 | }
66 | &::-moz-range-thumb {
67 | width: 5px;
68 | height: 20px;
69 | background: white;
70 | cursor: pointer;
71 | }
72 | `;
73 |
74 | const VolumeBar = styled.input`
75 | -webkit-appearance: none;
76 | width: 40%;
77 | height: 10px;
78 | border-radius: 5px;
79 | background: rgba(255, 255, 255, 0.5);
80 | outline: none;
81 | cursor: pointer;
82 |
83 | &::-webkit-slider-thumb {
84 | -webkit-appearance: none;
85 | appearance: none;
86 | width: 5px;
87 | height: 20px;
88 | background: white;
89 | cursor: pointer;
90 | }
91 | &::-moz-range-thumb {
92 | width: 5px;
93 | height: 20px;
94 | background: white;
95 | cursor: pointer;
96 | }
97 | `;
98 |
99 | const RemotePlayer = styled.div`
100 | display: flex;
101 | flex-direction: row;
102 | align-items: center;
103 | width: 100%;
104 | `;
105 | const PlayPauseBtn = styled(FontAwesomeIcon)`
106 | cursor: pointer;
107 | font-size: 3rem;
108 | color: white;
109 | margin-left: 5px;
110 | margin-right: 5px;
111 | `;
112 |
113 | const ArrowBtn = styled(FontAwesomeIcon)`
114 | cursor: pointer;
115 | font-size: 2rem;
116 | color: white;
117 | `;
118 |
119 | const VolumnIcon = styled(FontAwesomeIcon)`
120 | font-size: 1rem;
121 | color: white;
122 | cursor: pointer;
123 | `;
124 |
125 | const SongController = styled.div`
126 | display: flex;
127 | align-items: center;
128 | justify-content: flex-end;
129 | flex: 6.25;
130 | `;
131 | const VolumeController = styled.div`
132 | display: flex;
133 | align-items: center;
134 | justify-content: flex-end;
135 | flex: 3.75;
136 | `;
137 |
138 | const Player = ({ nowPlaying, statePlayList, setNowPlaying }) => {
139 | return (
140 | <>
141 |
142 | {
159 | const isPlaying = getPlayerState() === 1;
160 | const currentTime = getCurrentTime();
161 | const duration = getDuration();
162 | const nowIndex = statePlayList.findIndex(
163 | val => val.videoKey === nowPlaying.videoKey
164 | );
165 | const formatTime = time => {
166 | if (time === 0) {
167 | return "0:00";
168 | }
169 | const minutes = Math.floor(time / 60);
170 | const seconds = time - minutes * 60;
171 | return `${minutes}:${seconds < 10 ? "0" + seconds : seconds}`;
172 | };
173 | const getProcessPer = (current, duration) => {
174 | if (
175 | typeof current === undefined ||
176 | typeof duration === undefined ||
177 | duration === 0
178 | ) {
179 | return 0;
180 | }
181 |
182 | if (current === duration && duration !== 0) {
183 | let nextIndex = Math.floor(
184 | Math.random() * statePlayList.length
185 | );
186 |
187 | setNowPlaying(statePlayList[nextIndex]);
188 | }
189 |
190 | return Math.ceil((current / duration) * 100);
191 | };
192 |
193 | const moveCurrent = (value, duration) => {
194 | if (typeof value !== undefined && typeof duration !== undefined) {
195 | playVideo();
196 | seekTo(Math.ceil((value / 100) * duration));
197 | }
198 | };
199 | const prevPlay = nowIndex => {
200 | // turn nextMusic
201 | if (0 === nowIndex) {
202 | toast.error("이전 항목이 없습니다.");
203 | } else {
204 | setNowPlaying(statePlayList[nowIndex - 1]);
205 | }
206 | };
207 | const nextPlay = nowIndex => {
208 | // turn nextMusic
209 | if (statePlayList.length - 1 === nowIndex) {
210 | toast.error("다음 항목이 없습니다.");
211 | } else {
212 | setNowPlaying(statePlayList[nowIndex + 1]);
213 | }
214 | };
215 |
216 | return (
217 |
218 | {iframe}
219 |
220 |
221 |
222 | {formatTime(currentTime)} / {formatTime(duration)}
223 | moveCurrent(e.target.value, duration)}
229 | />
230 |
231 |
232 |
233 |
234 | {
237 | prevPlay(nowIndex);
238 | }}
239 | />
240 | {isPlaying ? (
241 | pauseVideo()}
244 | />
245 | ) : (
246 | playVideo()}
249 | />
250 | )}
251 | {
254 | nextPlay(nowIndex);
255 | }}
256 | />
257 |
258 |
259 | {isMuted() && (
260 | unMute()}
263 | />
264 | )}
265 | {!isMuted() && (
266 | mute()}
269 | />
270 | )}
271 | setVolume(e.target.value)}
277 | />
278 |
279 |
280 |
281 |
282 | );
283 | }}
284 | />
285 |
286 | >
287 | );
288 | };
289 |
290 | export default Player;
291 |
--------------------------------------------------------------------------------
/src/global-styles.js:
--------------------------------------------------------------------------------
1 | import { createGlobalStyle } from "styled-components";
2 | import reset from "styled-reset";
3 |
4 | const GlobalStyle = createGlobalStyle`
5 | @import url('https://fonts.googleapis.com/css?family=PT+Sans:400,700');
6 | ${reset}
7 | .App, .root{
8 | width:100%;
9 | height:100%;
10 | }
11 | * {
12 | box-sizing: border-box;
13 | }
14 | *:focus{
15 | outline:none;
16 | }
17 | html,body{
18 | width:100%;
19 | height:100%;
20 | }
21 | body{
22 | font-family: 'PT Sans', sans-serif, -apple-system,system-ui,BlinkMacSystemFont;
23 | font-weight:400;
24 | }
25 | a{
26 | color:inherit;
27 | text-decoration:none;
28 | }
29 | input,
30 | button{
31 | &.focus,
32 | &.active{outline:none}
33 | }
34 | h1,h2,h3,h4,h5,h6{
35 | font-family:'Maven Pro', sans-serif;
36 | }
37 | `;
38 |
39 | export default GlobalStyle;
40 |
--------------------------------------------------------------------------------
/src/handleState/handlePlayer.js:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 |
3 | const handlePlayer = initial => {
4 | const [nowPlaying, setNowPlaying] = useState(initial);
5 |
6 | return {
7 | nowPlaying,
8 | setNowPlaying
9 | };
10 | };
11 |
12 | export default handlePlayer;
13 |
--------------------------------------------------------------------------------
/src/handleState/handleStatePlayList.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 |
3 | const handleStatePlayList = () => {
4 | const initialPlayList = [
5 | {
6 | id: 0,
7 | songName:
8 | "We Can't Stop - Miley Cyrus (Boyce Avenue feat. Bea Miller cover) on Spotify & Apple",
9 | singer: "Boyce Avenue",
10 | videoKey: "uzgp65UnPxA",
11 | thumbnail: "https://i.ytimg.com/vi/uzgp65UnPxA/maxresdefault.jpg",
12 | max_thumbnail: "https://i.ytimg.com/vi/uzgp65UnPxA/maxresdefault.jpg",
13 | duration: "04:30"
14 | },
15 | {
16 | id: 1,
17 | songName: "Charlie Puth - Attention [Official Video]",
18 | singer: "Charlie Puth",
19 | videoKey: "nfs8NYg7yQM",
20 | thumbnail: "https://i.ytimg.com/vi/nfs8NYg7yQM/hqdefault.jpg",
21 | max_thumbnail: "https://i.ytimg.com/vi/nfs8NYg7yQM/hqdefault.jpg",
22 | duration: "03:52"
23 | },
24 | {
25 | id: 2,
26 | songName: "Pharrell Williams - Happy (Official Music Video)",
27 | singer: "PharrellWilliamsVEVO",
28 | videoKey: "ZbZSe6N_BXs",
29 | thumbnail: "https://i.ytimg.com/vi/ZbZSe6N_BXs/maxresdefault.jpg",
30 | max_thumbnail: "https://i.ytimg.com/vi/ZbZSe6N_BXs/maxresdefault.jpg",
31 | duration: "04:01"
32 | }
33 | ];
34 |
35 | const getPlayList = localStorage.getItem("localPlayList")
36 | ? JSON.parse(localStorage.getItem("localPlayList"))
37 | : initialPlayList;
38 |
39 | const [statePlayList, setStatePlayList] = useState(getPlayList);
40 |
41 | useEffect(() => {
42 | localStorage.setItem("localPlayList", JSON.stringify(statePlayList));
43 | });
44 |
45 | return {
46 | statePlayList,
47 | addStatePlayList: contentsData => {
48 | let beforeStateList = statePlayList;
49 | let nextStateList = beforeStateList.concat(contentsData);
50 | setStatePlayList(nextStateList);
51 | },
52 | deleteStatePlayList: conetentsKey => {
53 | if (statePlayList.length !== 1) {
54 | const afterStatePlayList = statePlayList.filter(
55 | (val, index) => val.videoKey !== conetentsKey
56 | );
57 | setStatePlayList(afterStatePlayList);
58 | return true;
59 | } else {
60 | return false;
61 | }
62 | }
63 | };
64 | };
65 |
66 | export default handleStatePlayList;
67 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom";
3 | import App from "./App";
4 | import GlobalStyle from "./global-styles";
5 | import { ToastContainer } from "react-toastify";
6 | import "react-toastify/dist/ReactToastify.min.css";
7 |
8 | ReactDOM.render(
9 | <>
10 |
11 |
12 |
13 | >,
14 | document.getElementById("root")
15 | );
16 |
--------------------------------------------------------------------------------