ソースを参照

Make an audio controller for tracks

surstrommed 3 年 前
コミット
4f3414ffb6

+ 35 - 1
src/App.js

@@ -8,12 +8,14 @@ import {
   promiseReducer,
   authReducer,
   localStoredReducer,
+  playerReducer,
 } from "./reducers/index";
 import { Sidebar } from "./components/Sidebar";
 import "./App.scss";
 import createSagaMiddleware from "redux-saga";
 import { all, takeEvery, put, call, select } from "redux-saga/effects";
 import * as actions from "./actions";
+import { CAudioController } from "./components/AudioController";
 export const history = createBrowserHistory();
 
 const sagaMiddleware = createSagaMiddleware();
@@ -22,6 +24,7 @@ export const store = createStore(
   combineReducers({
     promise: localStoredReducer(promiseReducer, "promise"),
     auth: localStoredReducer(authReducer, "auth"),
+    player: localStoredReducer(playerReducer, "player"),
   }),
   applyMiddleware(sagaMiddleware)
 );
@@ -45,7 +48,7 @@ function* promiseWatcher() {
 function* aboutMeWorker() {
   // let { id } = getState().auth.payload.sub;
   // await dispatch(actionFindUser(id));
-  let { id } = yield select().auth.payload.sub;
+  let { id } = yield select().auth?.payload?.sub;
   yield call(promiseWatcher, actions.actionFindUser(id));
 }
 
@@ -150,6 +153,33 @@ function* setNewPasswordWatcher() {
   yield takeEvery("SET_NEW_PASSWORD", setNewPasswordWorker);
 }
 
+function* audioPlayWorker({ isPlaying }) {
+  yield put(actions.actionPlayAudio(isPlaying));
+  yield put(actions.actionAboutMe());
+}
+
+function* audioPlayWatcher() {
+  yield takeEvery("PLAY_TRACK", audioPlayWorker);
+}
+
+function* audioPauseWorker({ isPaused }) {
+  yield put(actions.actionPauseAudio(isPaused));
+  yield put(actions.actionAboutMe());
+}
+
+function* audioPauseWatcher() {
+  yield takeEvery("PAUSE_TRACK", audioPauseWorker);
+}
+
+function* audioVolumeWorker({ volume }) {
+  yield put(actions.actionVolumeAudio(volume));
+  yield put(actions.actionAboutMe());
+}
+
+function* audioVolumeWatcher() {
+  yield takeEvery("VOLUME_TRACK", audioVolumeWorker);
+}
+
 if (localStorage.authToken) {
   store.dispatch(actions.actionAboutMe());
 }
@@ -175,6 +205,9 @@ function* rootSaga() {
     setNicknameWatcher(),
     setEmailWatcher(),
     setNewPasswordWatcher(),
+    audioPlayWatcher(),
+    audioPauseWatcher(),
+    audioVolumeWatcher(),
   ]);
 }
 
@@ -194,4 +227,5 @@ function App() {
   );
 }
 
+// <CAudioController />
 export default App;

+ 39 - 6
src/App.scss

@@ -67,6 +67,7 @@ body {
   padding: 25px 0;
   transition: box-shadow 400ms;
   display: flex;
+  font-size: 16px;
 }
 
 .player-container > span {
@@ -75,12 +76,9 @@ body {
 
 .sticky {
   position: fixed;
-  width: 10%;
-  top: 0;
-}
-
-.sticky > span {
-  text-decoration: none;
+  display: inline-block;
+  width: 50%;
+  font-size: 12px;
 }
 
 .header-section > div {
@@ -91,6 +89,41 @@ body {
   width: 70vh;
 }
 
+.AudioController {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  flex-wrap: nowrap;
+  background-color: #212529;
+  width: 100%;
+  z-index: 1000;
+  height: 15vh;
+  position: fixed;
+  left: 0;
+  bottom: 0;
+  .SongName {
+    margin-left: 1vh;
+    width: 25%;
+  }
+  .Buttons {
+    width: 70%;
+    text-align: center;
+    align-self: center;
+  }
+  .Volume {
+    width: 15%;
+    input {
+      width: 50%;
+    }
+  }
+  .Duration {
+    span {
+      margin-left: 1vh;
+      font-size: 12px;
+    }
+  }
+}
+
 .btn-circle {
   width: 38px;
   height: 38px;

+ 61 - 0
src/actions/index.js

@@ -124,6 +124,44 @@ export const actionUploadPhoto = (file) => {
   );
 };
 
+export const actionUserPlaylists = (_id) => {
+  actionPromise(
+    "userPlaylists",
+    gql(
+      `
+          query getPlaylistByOwnerId($ownerId:String!) {
+              PlaylistFind(query: $ownerId) {
+                  _id, name
+              }
+          }
+      `,
+      { ownerId: JSON.stringify([{ ___owner: _id }]) }
+    )
+  );
+};
+
+export const actionUserTracks = (_id) => {
+  return actionPromise(
+    "userTracks",
+    gql(
+      `
+          query getUserTracks($ownerId: String!) {
+              TrackFind(query: $ownerId) {
+                  _id,
+                  id3 {
+                      title, artist
+                  }
+                  playlists {
+                      _id, name
+                  }
+              }
+          }
+      `,
+      { ownerId: JSON.stringify([{ ___owner: _id }]) }
+    )
+  );
+};
+
 // ================================================
 
 export const actionPending = (name) => ({
@@ -255,3 +293,26 @@ export const actionSetNewPassword = (login, password, newPassword) => ({
 //   await dispatch(actionChangePassword(login, password, newPassword));
 //   await dispatch(actionAboutMe());
 // };
+
+export const actionLoadAudio = (track, duration, playlist, playlistIndex) => ({
+  type: "LOAD_TRACK",
+  track,
+  duration,
+  playlist,
+  playlistIndex,
+});
+
+export const actionPlayAudio = (isPlaying) => ({
+  type: "PLAY_TRACK",
+  isPlaying,
+});
+
+export const actionPauseAudio = (isPaused) => ({
+  type: "PAUSE_TRACK",
+  isPaused,
+});
+
+export const actionVolumeAudio = (volume) => ({
+  type: "VOLUME_TRACK",
+  volume,
+});

+ 83 - 0
src/components/AudioController.js

@@ -0,0 +1,83 @@
+import { Button } from "react-bootstrap";
+import { connect } from "react-redux";
+import { useState } from "react";
+import {
+  actionPlayAudio,
+  actionPauseAudio,
+  actionVolumeAudio,
+} from "./../actions/index";
+
+const AudioController = ({
+  name,
+  currentTime,
+  duration,
+  volume,
+  playAudio,
+  pauseAudio,
+  volumeAudio,
+  player,
+}) => {
+  const [vol, setVol] = useState(volume);
+  const [reproduction, setReproduction] = useState(player?.isPlaying);
+  console.log("audioController", reproduction);
+
+  function truncText(text) {
+    if (text && text.length > 40) {
+      return text.substring(0, 40) + "...";
+    } else return text;
+  }
+
+  function convertTime(dur) {
+    let minutes = Math.floor(dur / 60);
+    let seconds = parseInt((dur % 60) * 100) / 100;
+    return `${minutes}:${seconds}`;
+  }
+
+  return (
+    <div className="AudioController">
+      <span className="SongName">{truncText(name)}</span>
+      <div className="Buttons m-2">
+        <Button className="mr-2">
+          <i className="fas fa-step-backward"></i>
+        </Button>
+        <Button
+          className="mr-2"
+          onClick={() => {
+            setReproduction(!reproduction);
+            reproduction ? pauseAudio(true) : playAudio(true);
+          }}
+        >
+          <i className={`fas fa-${reproduction ? "pause" : "caret-right"}`}></i>
+        </Button>
+        <Button>
+          <i className="fas fa-step-forward"></i>
+        </Button>
+        <div className="Duration">
+          <span>{convertTime(currentTime)}</span>
+          <input type="range" className="form-range mt-3 w-50" />
+          <span>{convertTime(duration)}</span>
+        </div>
+      </div>
+      <div className="Volume m-2">
+        <input
+          type="range"
+          min="0"
+          step="1"
+          max="100"
+          className="form-range"
+          onChange={(e) => {
+            setVol(e.target.value);
+            volumeAudio(vol);
+          }}
+          value={vol}
+        />
+      </div>
+    </div>
+  );
+};
+
+export const CAudioController = connect((state) => ({ player: state.player }), {
+  playAudio: actionPlayAudio,
+  pauseAudio: actionPauseAudio,
+  volumeAudio: actionVolumeAudio,
+})(AudioController);

+ 1 - 1
src/components/Loader.js

@@ -13,7 +13,7 @@ export const Loader = () => {
         size="sm"
         role="status"
         aria-hidden="true"
-      />
+      />{" "}
       Loading...
     </Button>
   );

+ 2 - 5
src/components/PlayerHeader.js

@@ -9,14 +9,11 @@ export const PlayerHeader = ({ personal }) => {
   }, []);
 
   return (
-    <nav
-      className={`mt-5 navbar ${offset > 50 ? "sticky" : ""}`}
-      style={{ width: "110%" }}
-    >
+    <nav className={`mt-5 navbar ${offset > 50 ? "sticky" : ""}`}>
       <div className="container-fluid player-container">
         <span>#</span>
         <span>Название</span>
-        <span>{personal ? "Плейлист" : "Владелец"}</span>
+        <span>{personal ? "Плейлист" : "Информация"}</span>
       </div>
     </nav>
   );

+ 4 - 3
src/components/SearchField.js

@@ -12,9 +12,10 @@ export const SearchField = () => {
         aria-label="Поиск"
         aria-describedby="search-addon"
       />
-      <Button variant="primary" id="search-addon">
-        <FontAwesomeIcon icon={faSearch} />
-      </Button>
     </div>
   );
 };
+
+// <Button variant="primary" id="search-addon">
+// <FontAwesomeIcon icon={faSearch} />
+// </Button>

+ 9 - 9
src/components/Sidebar.js

@@ -1,7 +1,6 @@
 import {
   CDBSidebar,
   CDBSidebarContent,
-  CDBSidebarFooter,
   CDBSidebarHeader,
   CDBSidebarMenu,
   CDBSidebarMenuItem,
@@ -37,15 +36,16 @@ export const Sidebar = () => {
             </NavLink>
           </CDBSidebarMenu>
         </CDBSidebarContent>
-        <CDBSidebarFooter
-          className="m-1"
-          style={{
-            textAlign: "center",
-          }}
-        >
-          Navy Web Player
-        </CDBSidebarFooter>
       </CDBSidebar>
     </div>
   );
 };
+
+// <CDBSidebarFooter
+// className="m-1"
+// style={{
+//   textAlign: "center",
+// }}
+// >
+// Navy Web Player
+// </CDBSidebarFooter>

+ 66 - 28
src/components/Track.js

@@ -2,49 +2,87 @@ import { Button } from "react-bootstrap";
 import { connect } from "react-redux";
 import { backURL } from "../helpers/index";
 import { useState, useEffect, useRef } from "react";
+import { CAudioController } from "./AudioController";
 
-export const Track = ({ audio, index }) => {
+import {
+  actionPauseAudio,
+  actionPlayAudio,
+  actionVolumeAudio,
+  actionLoadAudio,
+} from "./../actions/index";
+
+const Track = ({
+  audio,
+  index,
+  loadAudio,
+  playAudio,
+  pauseAudio,
+  volumeAudio,
+  player,
+}) => {
   const [reproduction, setReproduction] = useState(false);
   let track = audio?.url ? `${backURL}/${audio.url}` : undefined;
   let trackRef = useRef(new Audio(track));
 
   useEffect(() => {
-    reproduction ? trackRef.current.play() : trackRef.current.pause();
-  }, [reproduction]);
+    if (reproduction) {
+      trackRef.current.play();
+    } else {
+      trackRef.current.pause();
+    }
+  }, [reproduction, playAudio, pauseAudio]);
 
   function truncText(text) {
-    if (text.length > 40) {
+    if (text && text.length > 40) {
       return text.substring(0, 40) + "...";
     } else return text;
   }
 
   return (
-    <div className="d-flex mt-5">
-      <div className="customAudio p-2 bg-dark text-white">
-        <span className="ml-3 d-inline-block">
-          {index + 1} |{" "}
-          <span>
-            {audio?.originalFileName
-              ? truncText(audio.originalFileName)
-              : undefined}
+    <>
+      {reproduction ? (
+        <CAudioController
+          name={audio?.originalFileName}
+          currentTime={trackRef.current.currentTime}
+          duration={player.duration}
+          volume={trackRef.current.volume}
+        />
+      ) : null}
+      <div className="d-flex mt-5">
+        <div className="customAudio p-2 bg-dark text-white">
+          <span className="ml-3 d-inline-block">
+            {index + 1} |{" "}
+            <span>
+              {audio?.originalFileName
+                ? truncText(audio.originalFileName)
+                : "Без названия"}
+            </span>
           </span>
-        </span>
+        </div>
+        <Button
+          onClick={() => {
+            setReproduction(!reproduction);
+            loadAudio(trackRef.current, trackRef.current.duration);
+            reproduction ? pauseAudio(true) : playAudio(true);
+          }}
+        >
+          <i
+            className={`fas ${
+              reproduction ? "fa-pause-circle" : "fa-play-circle"
+            }`}
+          ></i>
+        </Button>
+        <Button>
+          <i className="fas fa-plus"></i>
+        </Button>
       </div>
-      <Button onClick={() => setReproduction(!reproduction)}>
-        <i
-          className={`fas ${
-            reproduction ? "fa-pause-circle" : "fa-play-circle"
-          }`}
-        ></i>
-      </Button>
-      <Button>
-        <i className="fas fa-plus"></i>
-      </Button>
-    </div>
+    </>
   );
 };
 
-export const CAudio = connect(
-  (state) => ({ auth: state.auth, promise: state.promise }),
-  null
-)(Audio);
+export const CTrack = connect((state) => ({ player: state.player }), {
+  loadAudio: actionLoadAudio,
+  playAudio: actionPlayAudio,
+  pauseAudio: actionPauseAudio,
+  volumeAudio: actionVolumeAudio,
+})(Track);

+ 2 - 3
src/pages/Library.js

@@ -3,7 +3,7 @@ import { AuthCheck } from "./../components/AuthCheck";
 import { history } from "./../App";
 import { actionFindTracks, actionFindUser } from "./../actions/index";
 import { CMyDropzone } from "../components/Dropzone";
-import { Track } from "../components/Track";
+import { CTrack } from "../components/Track";
 import { PlayerHeader } from "./../components/PlayerHeader";
 import { Loader } from "./../components/Loader";
 
@@ -17,11 +17,10 @@ const Library = ({ auth, promise, actionTracks, actionUser }) => {
           </h1>
           <CMyDropzone />
           <PlayerHeader personal />
-          <Track />
           {promise?.tracks?.payload ? (
             promise.tracks.payload.map((track, index) =>
               track.owner._id === auth.payload.sub.id ? (
-                <Track audio={track} index={index} key={Math.random()} />
+                <CTrack audio={track} index={index} key={Math.random()} />
               ) : (
                 <h2>В вашей библиотеке нет треков.</h2>
               )

+ 3 - 3
src/pages/Search.js

@@ -3,7 +3,7 @@ import { SearchField } from "./../components/SearchField";
 import { AuthCheck } from "./../components/AuthCheck";
 import { history } from "./../App";
 import { actionFindTracks, actionFindUser } from "./../actions/index";
-import { Track } from "../components/Track";
+import { CTrack } from "../components/Track";
 import { PlayerHeader } from "./../components/PlayerHeader";
 import { Loader } from "./../components/Loader";
 
@@ -12,12 +12,12 @@ const Search = ({ auth, promise }) => {
     <div className="SearchPage">
       {auth.token && history.location.pathname === "/search" ? (
         <div className="d-block mx-auto mt-2 container w-50">
-          <h1>Поиск по сайту</h1>
+          <h1 className="text-center">Поиск по сайту</h1>
           <SearchField />
           <PlayerHeader />
           {promise?.tracks?.payload ? (
             promise.tracks.payload.map((track, index) => (
-              <Track audio={track} index={index} key={Math.random()} />
+              <CTrack audio={track} index={index} key={Math.random()} />
             ))
           ) : (
             <Loader />

+ 5 - 5
src/reducers/index.js

@@ -58,7 +58,7 @@ export const playerReducer = (
     type,
     track,
     isPlaying,
-    isStopped,
+    isPaused,
     duration,
     playlist,
     playlistIndex,
@@ -79,14 +79,14 @@ export const playerReducer = (
     return {
       ...state,
       isPlaying,
-      isStopped: !isPlaying,
+      isPaused: !isPlaying,
     };
   }
-  if (type === "STOP_TRACK") {
+  if (type === "PAUSE_TRACK") {
     return {
       ...state,
-      isStopped,
-      isPlaying: !isStopped,
+      isPaused,
+      isPlaying: !isPaused,
     };
   }
   if (type === "VOLUME_TRACK") {