浏览代码

Some additionals

surstrommed 3 年之前
父节点
当前提交
23380e7675

+ 17 - 0
package-lock.json

@@ -16,6 +16,7 @@
         "@testing-library/jest-dom": "^5.16.1",
         "@testing-library/react": "^12.1.2",
         "@testing-library/user-event": "^13.5.0",
+        "array-move": "^4.0.0",
         "bootstrap": "^5.1.3",
         "cdbreact": "^1.2.1",
         "music-metadata-browser": "^2.5.3",
@@ -4550,6 +4551,17 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/array-move": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/array-move/-/array-move-4.0.0.tgz",
+      "integrity": "sha512-+RY54S8OuVvg94THpneQvFRmqWdAHeqtMzgMW6JNurHxe8rsS07cHQdfGkXnTUXiBcyZ0j3SiDIxxj0RPiqCkQ==",
+      "engines": {
+        "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/array-union": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
@@ -20842,6 +20854,11 @@
         "is-string": "^1.0.7"
       }
     },
+    "array-move": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/array-move/-/array-move-4.0.0.tgz",
+      "integrity": "sha512-+RY54S8OuVvg94THpneQvFRmqWdAHeqtMzgMW6JNurHxe8rsS07cHQdfGkXnTUXiBcyZ0j3SiDIxxj0RPiqCkQ=="
+    },
     "array-union": {
       "version": "2.1.0",
       "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",

+ 1 - 0
package.json

@@ -10,6 +10,7 @@
     "@testing-library/jest-dom": "^5.16.1",
     "@testing-library/react": "^12.1.2",
     "@testing-library/user-event": "^13.5.0",
+    "array-move": "^4.0.0",
     "bootstrap": "^5.1.3",
     "cdbreact": "^1.2.1",
     "music-metadata-browser": "^2.5.3",

+ 54 - 33
src/App.js

@@ -11,6 +11,7 @@ import {
   searchReducer,
   playerReducer,
   routeReducer,
+  scrollTracksReducer,
 } from "./reducers/index";
 import { Sidebar } from "./components/Sidebar";
 import "./App.scss";
@@ -35,7 +36,7 @@ import {
   audioSetDurationWatcher,
   audioSetVolumeWatcher,
   audioSetCurrentTimeWatcher,
-} from "./components/Audio";
+} from "./components/AudioHandler";
 export const history = createBrowserHistory();
 
 const sagaMiddleware = createSagaMiddleware();
@@ -46,6 +47,7 @@ export const store = createStore(
     auth: localStoredReducer(authReducer, "auth"),
     player: localStoredReducer(playerReducer, "player"),
     route: localStoredReducer(routeReducer, "route"),
+    loadedTracks: localStoredReducer(scrollTracksReducer, "loadedTracks"),
     search: searchReducer,
     // изменить условия на страницах на отображения по роутам
   }),
@@ -67,19 +69,22 @@ function* rootSaga() {
     setEmailWatcher(),
     setNewPasswordWatcher(),
     searchWatcher(),
+    audioLoadWatcher(),
+    audioSetDurationWatcher(),
     audioPlayWatcher(),
     audioPauseWatcher(),
     audioPrevTrackWatcher(),
     audioNextTrackWatcher(),
     audioSetCurrentTimeWatcher(),
     audioSetVolumeWatcher(),
-    audioLoadWatcher(),
-    audioSetDurationWatcher(),
     findPlaylistByOwnerWatcher(),
     createPlaylistWatcher(),
     findPlaylistTracksWatcher(),
     loadTracksToPlaylistWatcher(),
     uploadTracksToPlaylistWatcher(),
+    loadNewTracksWatcher(),
+    addSkipTracksWatcher(),
+    clearSkipTracksWatcher(),
   ]);
 }
 
@@ -102,8 +107,6 @@ function* promiseWatcher() {
 }
 
 function* aboutMeWorker() {
-  // let { id } = getState().auth.payload.sub;
-  // await dispatch(actionFindUser(id));
   let { auth } = yield select();
   if (auth) {
     let { id } = auth?.payload?.sub;
@@ -124,6 +127,9 @@ function* aboutAnotherUserWatcher() {
 }
 
 if (localStorage.authToken) {
+  store.dispatch(actions.actionFindUserPlaylists());
+  store.dispatch(actions.actionFullClearTracks());
+  store.dispatch(actions.actionAllTracks());
   store.dispatch(actions.actionAboutMe());
 }
 
@@ -156,6 +162,41 @@ function* loginWatcher() {
   yield takeEvery("FULL_LOGIN", loginWorker);
 }
 
+function* loadNewTracksWorker({ newTracks }) {
+  yield put(actions.actionLoadNewTracks(newTracks));
+}
+
+function* loadNewTracksWatcher() {
+  yield takeEvery("FULL_ADD_TRACKS", loadNewTracksWorker);
+}
+
+function* addSkipTracksWorker({ skipTracks }) {
+  yield put(actions.actionSkipTracks(skipTracks));
+}
+
+function* addSkipTracksWatcher() {
+  yield takeEvery("FULL_ADD_SKIP", addSkipTracksWorker);
+}
+
+function* clearSkipTracksWorker() {
+  yield put(actions.actionClearTracks());
+}
+
+function* clearSkipTracksWatcher() {
+  yield takeEvery("FULL_CLEAR_SKIP", clearSkipTracksWorker);
+}
+
+function* findTracksWorker({ skip }) {
+  let newTracks = yield call(promiseWorker, actions.actionFindTracks(skip));
+  if (newTracks) {
+    yield put(actions.actionFullLoadNewTracks(newTracks));
+  }
+}
+
+function* findTracksWatcher() {
+  yield takeEvery("FIND_ALL_TRACKS", findTracksWorker);
+}
+
 function* registerWorker({ login, password }) {
   yield call(promiseWorker, actions.actionRegister(login, password));
   let token = yield call(promiseWorker, actions.actionLogin(login, password));
@@ -206,8 +247,6 @@ function* setNicknameWatcher() {
 }
 
 function* setEmailWorker({ _id, login }) {
-  //   await dispatch(actionUserUpdate({ _id, nick }));
-  //   await dispatch(actionAboutMe());
   yield call(promiseWorker, actions.actionUserUpdate({ _id, login }));
   yield put(actions.actionAboutMe());
 }
@@ -270,8 +309,6 @@ function* findPlaylistByOwnerWorker() {
   if (auth) {
     let { id } = auth?.payload?.sub;
     yield call(promiseWorker, actions.actionUserPlaylists(id));
-    // yield put(actions.actionFindUserPlaylists());
-    // yield put(actions.actionAboutMe());
   }
 }
 
@@ -282,7 +319,6 @@ function* findPlaylistByOwnerWatcher() {
 function* createPlaylistWorker({ name }) {
   let { auth } = yield select();
   if (auth) {
-    // let { id } = auth?.payload?.sub;
     yield call(promiseWorker, actions.actionCreatePlaylist(name));
     yield put(actions.actionFindUserPlaylists());
   }
@@ -292,19 +328,6 @@ function* createPlaylistWatcher() {
   yield takeEvery("CREATE_PLAYLIST", createPlaylistWorker);
 }
 
-function* findTracksWorker() {
-  yield call(promiseWorker, actions.actionFindTracks());
-}
-
-function* findTracksWatcher() {
-  yield takeEvery("FIND_ALL_TRACKS", findTracksWorker);
-}
-
-if (localStorage.authToken && history.location.pathname === "/search") {
-  console.log("Search");
-  store.dispatch(actions.actionAllTracks());
-}
-
 function* findUserTracksWorker({ _id }) {
   yield call(promiseWorker, actions.actionUserTracks(_id));
 }
@@ -378,10 +401,6 @@ export function* uploadTracksToPlaylistWatcher() {
   yield takeEvery("UPLOAD_TRACKS", uploadTracksToPlaylistWorker);
 }
 
-if (localStorage.authToken && history.location.pathname === "/library") {
-  store.dispatch(actions.actionFindUserPlaylists());
-}
-
 if (localStorage.authToken && history.location.pathname.includes("/profile")) {
   let currentUserId = history.location.pathname.substring(
     history.location.pathname.lastIndexOf("/") + 1
@@ -404,12 +423,14 @@ function App() {
           <Header />
           <Sidebar />
           <Main />
-          <CAudioController
-            name="name"
-            currentTime="currentTime"
-            duration="duration"
-            volume="volume"
-          />
+          {localStorage.authToken ? (
+            <CAudioController
+              name="name"
+              currentTime="currentTime"
+              duration="duration"
+              volume="volume"
+            />
+          ) : null}
         </div>
       </Provider>
     </Router>

+ 1 - 1
src/App.scss

@@ -6,7 +6,7 @@ body {
   background-color: #34393d;
   color: white;
   font-family: "Catamaran", sans-serif;
-  height: 140vh;
+  height: 150vh;
 }
 
 .spoilerText {

+ 30 - 92
src/actions/index.js

@@ -95,7 +95,7 @@ export const actionFindUsers = () =>
     )
   );
 
-export const actionFindTracks = () =>
+export const actionFindTracks = (skip) =>
   actionPromise(
     "tracks",
     gql(
@@ -114,7 +114,17 @@ export const actionFindTracks = () =>
       }
       }
   `,
-      { q: "[{}]" }
+      {
+        // { q: "[{}]" }
+        q: JSON.stringify([
+          {},
+          {
+            sort: [{ _id: -1 }],
+            skip: [skip || 0],
+            limit: [30],
+          },
+        ]),
+      }
     )
   );
 
@@ -233,19 +243,6 @@ export const actionUploadFile = (file, type = "photo") => {
   );
 };
 
-export const actionDeletePlaylist = (idPlaylist) =>
-  actionPromise(
-    "deletePlaylist",
-    gql(
-      `mutation deletePlaylist($playlist:PlaylistInput) {
- PlaylistUpsert(playlist:$playlist) {
- _id name tracks
- }
-}`,
-      { playlist: { _id: idPlaylist } }
-    )
-  );
-
 // ================================================
 
 export const actionPending = (name) => ({
@@ -330,96 +327,37 @@ export const actionSearchResult = (payload) => ({
   payload,
 });
 
-export const actionLoadAudio = ({ track, playlist, indexInPlaylist }) => ({
-  type: "LOAD_TRACK",
-  track,
-  playlist,
-  indexInPlaylist,
-});
-
-export const actionFullLoadAudio = (track, playlist, indexInPlaylist) => ({
-  type: "FULL_LOAD_TRACK",
-  track,
-  playlist,
-  indexInPlaylist,
-});
-
-export const actionPlayAudio = ({ isPlaying }) => ({
-  type: "PLAY_TRACK",
-  isPlaying,
-});
-
-export const actionFullPlayAudio = (isPlaying) => ({
-  type: "FULL_PLAY_TRACK",
-  isPlaying,
-});
-
-export const actionPauseAudio = ({ isPaused }) => ({
-  type: "PAUSE_TRACK",
-  isPaused,
-});
-
-export const actionFullPauseAudio = (isPaused) => ({
-  type: "FULL_PAUSE_TRACK",
-  isPaused,
-});
-
-export const actionPrevTrack = ({ indexInPlaylist, track }) => ({
-  type: "PREV_TRACK",
-  indexInPlaylist,
-  track,
-});
-
-export const actionFullPrevTrack = (indexInPlaylist, track) => ({
-  type: "FULL_PREV_TRACK",
-  indexInPlaylist,
-  track,
-});
-
-export const actionNextTrack = ({ indexInPlaylist, track }) => ({
-  type: "NEXT_TRACK",
-  indexInPlaylist,
-  track,
-});
-
-export const actionFullNextTrack = (indexInPlaylist, track) => ({
-  type: "FULL_NEXT_TRACK",
-  indexInPlaylist,
-  track,
-});
-
-export const actionSetCurrentTimeTrack = ({ currentTime }) => ({
-  type: "SET_CURRENT_TIME_TRACK",
-  currentTime,
+export const actionLoadNewTracks = (newTracks) => ({
+  type: "ADD_TRACKS",
+  newTracks,
 });
 
-export const actionFullSetCurrentTimeTrack = (currentTime) => ({
-  type: "FULL_SET_CURRENT_TIME_TRACK",
-  currentTime,
+export const actionFullLoadNewTracks = (newTracks) => ({
+  type: "FULL_ADD_TRACKS",
+  newTracks,
 });
 
-export const actionSetVolume = ({ volume }) => ({
-  type: "SET_VOLUME",
-  volume,
+export const actionSkipTracks = (skipTracks) => ({
+  type: "ADD_SKIP",
+  skipTracks,
 });
 
-export const actionFullSetVolume = (volume) => ({
-  type: "FULL_SET_VOLUME",
-  volume,
+export const actionFullSkipTracks = (skipTracks) => ({
+  type: "FULL_ADD_SKIP",
+  skipTracks,
 });
 
-export const actionSetDuration = ({ duration }) => ({
-  type: "SET_DURATION",
-  duration,
+export const actionClearTracks = () => ({
+  type: "CLEAR_SKIP",
 });
 
-export const actionFullSetDuration = (duration) => ({
-  type: "FULL_SET_DURATION",
-  duration,
+export const actionFullClearTracks = () => ({
+  type: "FULL_CLEAR_SKIP",
 });
 
-export const actionAllTracks = () => ({
+export const actionAllTracks = (skip) => ({
   type: "FIND_ALL_TRACKS",
+  skip,
 });
 
 export const actionTracksUser = (_id) => ({

+ 11 - 108
src/components/Audio.js

@@ -1,84 +1,8 @@
-import { takeEvery, put } from "redux-saga/effects";
 import { Button } from "react-bootstrap";
-import * as actions from "../actions";
 import { connect } from "react-redux";
 import { backURL } from "../helpers/index";
 import { Link } from "react-router-dom";
-import { useState } from "react";
-
-function* audioLoadWorker(track, playlist, indexInPlaylist) {
-  console.log("Load track");
-  yield put(actions.actionLoadAudio(track, playlist, indexInPlaylist));
-}
-
-export function* audioLoadWatcher() {
-  yield takeEvery("FULL_LOAD_TRACK", audioLoadWorker);
-}
-
-function* audioPlayWorker(isPlaying) {
-  console.log("Play track");
-  yield put(actions.actionPlayAudio(isPlaying));
-}
-
-export function* audioPlayWatcher() {
-  yield takeEvery("FULL_PLAY_TRACK", audioPlayWorker);
-}
-
-function* audioPauseWorker(isPaused) {
-  console.log("Pause track");
-  yield put(actions.actionPauseAudio(isPaused));
-}
-
-export function* audioPauseWatcher() {
-  yield takeEvery("FULL_PAUSE_TRACK", audioPauseWorker);
-}
-
-function* audioPrevTrackWorker(track, indexInPlaylist) {
-  console.log("Prev track");
-  yield put(actions.actionPrevTrack(track, indexInPlaylist));
-}
-
-export function* audioPrevTrackWatcher() {
-  yield takeEvery("FULL_PREV_TRACK", audioPrevTrackWorker);
-}
-
-function* audioNextTrackWorker(track, indexInPlaylist) {
-  console.log("Next track");
-  yield put(actions.actionNextTrack(track, indexInPlaylist));
-}
-
-export function* audioNextTrackWatcher() {
-  yield takeEvery("FULL_NEXT_TRACK", audioNextTrackWorker);
-}
-
-function* audioSetCurrentTimeWorker(currentTime) {
-  console.log("Set current time track");
-  yield put(actions.actionSetCurrentTimeTrack(currentTime));
-}
-
-export function* audioSetCurrentTimeWatcher() {
-  yield takeEvery("FULL_SET_CURRENT_TIME_TRACK", audioSetCurrentTimeWorker);
-}
-
-function* audioSetVolumeWorker(volume) {
-  console.log("Set volume");
-  yield put(actions.actionSetVolume(volume));
-}
-
-export function* audioSetVolumeWatcher() {
-  yield takeEvery("FULL_SET_VOLUME", audioSetVolumeWorker);
-}
-
-function* audioSetDurationWorker(duration) {
-  console.log("Set duration");
-  yield put(actions.actionSetDuration(duration));
-}
-
-export function* audioSetDurationWatcher() {
-  yield takeEvery("FULL_SET_DURATION", audioSetDurationWorker);
-}
-
-let audio = new Audio();
+import * as actions from "./AudioHandler";
 
 const AudioTrack = ({
   personal,
@@ -87,51 +11,33 @@ const AudioTrack = ({
   index,
   playlist,
   loadAudio,
-  playAudio,
-  pauseAudio,
 }) => {
-  const [play, setPlay] = useState(player?.isPlaying);
-
-  audio.src = `${backURL}/${track?.url}`;
-
   function truncText(text) {
     if (text && text.length > 40) {
       return text.substring(0, 40) + "...";
     } else return text;
   }
 
-  let audioPlayPause = (track, playlist) => {
-    let indexInPlaylist = playlist.findIndex(
-      (trackPlaylist) => trackPlaylist?._id === track?._id
-    );
-
-    loadAudio(track, playlist, indexInPlaylist);
-    if (player?.isPaused) {
-      audio.play();
-      setPlay(true);
-      playAudio(true);
-    }
-    if (player?.isPlaying) {
-      audio.pause();
-      setPlay(false);
-      pauseAudio(true);
-    }
-  };
-
   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} |{" "}
+          {!personal ? `${index + 1} | ` : null}
           <span>
             {track?.originalFileName
-              ? truncText(track.originalFileName)
+              ? truncText(track?.originalFileName)
               : "Без названия"}
           </span>
         </span>
       </div>
-      <Button onClick={() => audioPlayPause(track, playlist)}>
-        <i className={`fas ${play ? "fa-pause-circle" : "fa-play-circle"}`}></i>
+      <Button onClick={() => loadAudio(track, playlist, index)}>
+        <i
+          className={`fas ${
+            index === player?.indexInPlaylist && player?.isPlaying
+              ? "fa-pause-circle"
+              : "fa-play-circle"
+          }`}
+        ></i>
       </Button>
       <a
         className="btn btn-primary h-50 ml-1 my-auto"
@@ -140,7 +46,6 @@ const AudioTrack = ({
       >
         <i className="fas fa-download"></i>
       </a>
-      <span>{track?.duration}</span>
       {!personal ? (
         <div className="ml-2">
           <Link to={`/profile/${track?.owner?._id}`}>
@@ -157,7 +62,5 @@ const AudioTrack = ({
 };
 
 export const CAudio = connect((state) => ({ player: state.player }), {
-  playAudio: actions.actionFullPlayAudio,
-  pauseAudio: actions.actionFullPauseAudio,
   loadAudio: actions.actionFullLoadAudio,
 })(AudioTrack);

+ 27 - 49
src/components/AudioController.js

@@ -1,44 +1,22 @@
 import { Button } from "react-bootstrap";
 import { connect } from "react-redux";
 import { useState } from "react";
-import * as actions from "../actions";
-import { backURL } from "../helpers";
-
-let audio = new Audio();
+import * as actions from "./AudioHandler";
 
 const AudioController = ({
-  promise,
   player,
   playAudio,
   pauseAudio,
   prevAudio,
   nextAudio,
-  setDuration,
   setVolume,
   setCurrentTime,
+  loadAudio,
 }) => {
-  const [vol, setVol] = useState(player?.volume ? +player?.volume : 50);
-  const [dur, setDur] = useState(player?.duration ? player?.duration : 0);
-  const [pos, setPos] = useState(player?.currentTime ? player?.currentTime : 0);
   const [indexInPlaylist, setIndexInPlaylist] = useState(
     player?.indexInPlaylist
   );
 
-  let audioInPlaylist = player?.playlist[player?.indexInPlaylist];
-  audio.src = `${backURL}/${audioInPlaylist?.url}`;
-
-  audio.onloadedmetadata = () => {
-    setDur(audio.duration);
-  };
-
-  audio.ondurationchange = () => {
-    // setDuration(dur);
-  };
-
-  audio.ontimeupdate = (event) => {
-    // console.log(audio.currentTime);
-  };
-
   const checkIndexInPlaylist = (type) => {
     if (type === "prev") {
       if (player?.playlist[indexInPlaylist - 1]) {
@@ -60,19 +38,6 @@ const AudioController = ({
     }
   };
 
-  const checkTrackInPlaylist = () => {};
-
-  const audioPlayPause = () => {
-    if (player?.isPlaying) {
-      // audio.pause();
-      pauseAudio(true);
-    }
-    if (player?.isPaused) {
-      // audio.play();
-      playAudio(true);
-    }
-  };
-
   function truncText(text) {
     if (text && text.includes(".mp3")) {
       return text.replace(".mp3", "");
@@ -91,7 +56,7 @@ const AudioController = ({
   return (
     <div className="AudioController">
       <span className="SongName">
-        {truncText(player?.track?.originalFileName)}
+        {player?.track ? truncText(player?.track?.originalFileName) : ""}
       </span>
       <div className="Buttons m-2">
         <Button
@@ -104,7 +69,12 @@ const AudioController = ({
         >
           <i className="fas fa-step-backward"></i>
         </Button>
-        <Button className="mr-2" onClick={() => audioPlayPause()}>
+        <Button
+          className="mr-2"
+          onClick={() =>
+            loadAudio(player?.track, player?.playlist, player?.indexInPlaylist)
+          }
+        >
           <i
             className={`fas fa-${player?.isPlaying ? "pause" : "caret-right"}`}
           ></i>
@@ -127,16 +97,15 @@ const AudioController = ({
             max={
               !player?.duration || Number.isNaN(player?.duration)
                 ? 0
-                : Math.trunc(player?.duration)
+                : player?.duration
             }
-            value={pos}
+            value={player?.currentTime}
             step={1}
             onChange={(e) => {
-              setPos(e.target.value);
-              setCurrentTime(+pos);
+              setCurrentTime(+e.target.value);
             }}
           />
-          <span>{convertTime(dur)}</span>
+          <span>{convertTime(player?.duration)}</span>
         </div>
       </div>
       <div className="Volume m-2">
@@ -144,14 +113,23 @@ const AudioController = ({
           type="range"
           className="form-range"
           min={0}
-          step={1}
           max={100}
-          value={vol}
+          value={player?.volume * 100}
           onChange={(e) => {
-            setVol(e.target.value);
-            setVolume(+vol);
+            let volume = e.target.value / 100;
+            setVolume(volume);
           }}
         />
+        <span
+          style={{
+            fontSize: "14px",
+            position: "absolute",
+            marginTop: "0.5vh",
+            marginLeft: "2vh",
+          }}
+        >
+          {Math.trunc(player?.volume * 100)}%
+        </span>
       </div>
     </div>
   );
@@ -160,12 +138,12 @@ const AudioController = ({
 export const CAudioController = connect(
   (state) => ({ promise: state.promise, player: state.player }),
   {
+    loadAudio: actions.actionFullLoadAudio,
     playAudio: actions.actionFullPlayAudio,
     pauseAudio: actions.actionFullPauseAudio,
     nextAudio: actions.actionFullNextTrack,
     prevAudio: actions.actionFullPrevTrack,
     setVolume: actions.actionFullSetVolume,
-    setDuration: actions.actionFullSetDuration,
     setCurrentTime: actions.actionFullSetCurrentTimeTrack,
   }
 )(AudioController);

+ 190 - 0
src/components/AudioHandler.js

@@ -0,0 +1,190 @@
+import { takeEvery, put, select } from "redux-saga/effects";
+import { store } from "../App";
+import { backURL } from "./../helpers/index";
+
+const audio = new Audio();
+
+function* audioLoadWorker({ track, playlist, indexInPlaylist }) {
+  console.log("Load track");
+  let { player } = yield select();
+  if (player?.indexInPlaylist !== indexInPlaylist) {
+    yield put(actionLoadAudio(track, playlist, indexInPlaylist));
+  }
+  audio.src = `${backURL}/${player?.track?.url}`;
+  if (player?.indexInPlaylist === indexInPlaylist) {
+    if (player?.isPlaying) {
+      yield put(actionFullPauseAudio(true));
+    }
+    if (player?.isPaused) {
+      yield put(actionFullPlayAudio(true));
+    }
+  }
+  audio.onloadedmetadata = (e) => {
+    store.dispatch(actionFullSetDuration(Math.trunc(audio.duration)));
+  };
+  audio.ontimeupdate = (e) => {
+    store.dispatch(
+      actionFullSetCurrentTimeTrack(Math.trunc(e.timeStamp / 1000))
+    );
+  };
+}
+
+export function* audioLoadWatcher() {
+  yield takeEvery("FULL_LOAD_TRACK", audioLoadWorker);
+}
+
+function* audioPlayWorker(isPlaying) {
+  console.log("Play track");
+  audio.play();
+  yield put(actionPlayAudio(isPlaying));
+}
+
+export function* audioPlayWatcher() {
+  yield takeEvery("FULL_PLAY_TRACK", audioPlayWorker);
+}
+
+function* audioPauseWorker(isPaused) {
+  console.log("Pause track");
+  audio.pause();
+  yield put(actionPauseAudio(isPaused));
+}
+
+export function* audioPauseWatcher() {
+  yield takeEvery("FULL_PAUSE_TRACK", audioPauseWorker);
+}
+
+function* audioPrevTrackWorker(track, indexInPlaylist) {
+  console.log("Prev track");
+  yield put(actionPrevTrack(track, indexInPlaylist));
+}
+
+export function* audioPrevTrackWatcher() {
+  yield takeEvery("FULL_PREV_TRACK", audioPrevTrackWorker);
+}
+
+function* audioNextTrackWorker(track, indexInPlaylist) {
+  console.log("Next track");
+  yield put(actionNextTrack(track, indexInPlaylist));
+}
+
+export function* audioNextTrackWatcher() {
+  yield takeEvery("FULL_NEXT_TRACK", audioNextTrackWorker);
+}
+
+function* audioSetCurrentTimeWorker({ currentTime }) {
+  console.log("Set current time track");
+  // audio.currentTime = currentTime;
+  yield put(actionSetCurrentTimeTrack(currentTime));
+}
+
+export function* audioSetCurrentTimeWatcher() {
+  yield takeEvery("FULL_SET_CURRENT_TIME_TRACK", audioSetCurrentTimeWorker);
+}
+
+function* audioSetVolumeWorker({ volume }) {
+  console.log("Set volume");
+  audio.volume = volume;
+  yield put(actionSetVolume(volume));
+}
+
+export function* audioSetVolumeWatcher() {
+  yield takeEvery("FULL_SET_VOLUME", audioSetVolumeWorker);
+}
+
+function* audioSetDurationWorker({ duration }) {
+  console.log("Set duration");
+  // audio.duration = duration;
+  yield put(actionSetDuration(duration));
+}
+
+export function* audioSetDurationWatcher() {
+  yield takeEvery("FULL_SET_DURATION", audioSetDurationWorker);
+}
+
+export const actionLoadAudio = ({ track, playlist, indexInPlaylist }) => ({
+  type: "LOAD_TRACK",
+  track,
+  playlist,
+  indexInPlaylist,
+});
+
+export const actionFullLoadAudio = (track, playlist, indexInPlaylist) => ({
+  type: "FULL_LOAD_TRACK",
+  track,
+  playlist,
+  indexInPlaylist,
+});
+
+export const actionPlayAudio = ({ isPlaying }) => ({
+  type: "PLAY_TRACK",
+  isPlaying,
+});
+
+export const actionFullPlayAudio = (isPlaying) => ({
+  type: "FULL_PLAY_TRACK",
+  isPlaying,
+});
+
+export const actionPauseAudio = ({ isPaused }) => ({
+  type: "PAUSE_TRACK",
+  isPaused,
+});
+
+export const actionFullPauseAudio = (isPaused) => ({
+  type: "FULL_PAUSE_TRACK",
+  isPaused,
+});
+
+export const actionPrevTrack = ({ indexInPlaylist, track }) => ({
+  type: "PREV_TRACK",
+  indexInPlaylist,
+  track,
+});
+
+export const actionFullPrevTrack = (indexInPlaylist, track) => ({
+  type: "FULL_PREV_TRACK",
+  indexInPlaylist,
+  track,
+});
+
+export const actionNextTrack = ({ indexInPlaylist, track }) => ({
+  type: "NEXT_TRACK",
+  indexInPlaylist,
+  track,
+});
+
+export const actionFullNextTrack = (indexInPlaylist, track) => ({
+  type: "FULL_NEXT_TRACK",
+  indexInPlaylist,
+  track,
+});
+
+export const actionSetCurrentTimeTrack = ({ currentTime }) => ({
+  type: "SET_CURRENT_TIME_TRACK",
+  currentTime,
+});
+
+export const actionFullSetCurrentTimeTrack = (currentTime) => ({
+  type: "FULL_SET_CURRENT_TIME_TRACK",
+  currentTime,
+});
+
+export const actionSetVolume = ({ volume }) => ({
+  type: "SET_VOLUME",
+  volume,
+});
+
+export const actionFullSetVolume = (volume) => ({
+  type: "FULL_SET_VOLUME",
+  volume,
+});
+
+export const actionSetDuration = ({ duration }) => ({
+  type: "SET_DURATION",
+  duration,
+});
+
+export const actionFullSetDuration = (duration) => ({
+  type: "FULL_SET_DURATION",
+  duration,
+});

+ 0 - 11
src/components/Dropzone.js

@@ -2,19 +2,8 @@ import React, { useCallback } from "react";
 import { useDropzone } from "react-dropzone";
 import { connect } from "react-redux";
 import { actionSetAvatar, actionUploadTracks } from "../actions";
-import { history } from "./../App";
 
 const MyDropzone = ({ promise, onloadAvatar, onloadMusic }) => {
-  let idPlaylist = history.location.pathname.substring(
-    history.location.pathname.lastIndexOf("/") + 1
-  );
-  let indexPlaylist;
-  if (promise?.userPlaylists?.payload) {
-    indexPlaylist = promise?.userPlaylists?.payload.findIndex(
-      (playlist) => playlist?._id === idPlaylist
-    );
-  }
-
   const onDrop = useCallback(
     (acceptedFiles) => {
       if (acceptedFiles[0].type.includes("audio")) {

+ 2 - 2
src/components/Main.js

@@ -13,13 +13,13 @@ const Content = ({ children }) => <div className="Content">{children}</div>;
 const PageMain = () => {
   return (
     <div className="MainContent">
-      <h1>Главная страница</h1>
+      <h1 className="text-center">Главная страница</h1>
     </div>
   );
 };
 
 export const Main = () => (
-  <main className="Main">
+  <main className="Main" style={{ height: "150vh" }}>
     <Content>
       <Switch>
         <Route path="/" component={withRouter(PageMain)} exact />

+ 1 - 1
src/components/PlayerHeader.js

@@ -14,7 +14,7 @@ export const PlayerHeader = ({ personal }) => {
   return (
     <nav className={`mt-5 navbar ${offset > 50 ? "sticky" : ""}`}>
       <div className="container-fluid player-container">
-        <span>#</span>
+        <span>{personal ? "" : "#"}</span>
         <span>Название</span>
         <span>{personal ? "" : "Владелец"}</span>
       </div>

+ 74 - 62
src/components/Playlist.js

@@ -2,51 +2,90 @@ import { useState } from "react";
 import { connect } from "react-redux";
 import { Link } from "react-router-dom";
 import { CMyDropzone } from "./Dropzone";
-import { sortableContainer, sortableElement } from "react-sortable-hoc";
-import { actionCreateNewPlaylist } from "./../actions/index";
-import { PlayerHeader } from "./PlayerHeader";
-import { Loader } from "./Loader";
-import { Button } from "react-bootstrap";
+import {
+  actionCreateNewPlaylist,
+  actionTracksToPlaylist,
+} from "./../actions/index";
+import { SortableContainer, SortableElement } from "react-sortable-hoc";
+import { arrayMoveImmutable } from "array-move";
 import { CAudio } from "./Audio";
 import { history } from "./../App";
+import { CPreloader } from "./Preloader";
 
-const PlaylistTracks = ({ promise, tracks: { _id, url } = {} }) => {
+const PlaylistTrackItem = ({ track, index, key, playlist }) => {
+  return (
+    <div className="d-block mx-auto mt-2 container w-50" key={key}>
+      <CAudio track={track} playlist={playlist} personal />
+    </div>
+  );
+};
+
+const SortableItem = SortableElement(PlaylistTrackItem);
+
+const PlaylistTracksList = ({ playlistTracks }) => {
+  return (
+    <div>
+      {(playlistTracks || []).map((track, i) => {
+        return (
+          <SortableItem
+            track={track}
+            index={i}
+            key={track._id}
+            playlist={playlistTracks}
+          />
+        );
+      })}
+    </div>
+  );
+};
+
+const SortableList = SortableContainer(PlaylistTracksList);
+
+const TracksInPlaylistSortable = ({ playlistTracks, loadTracksToPlaylist }) => {
   let idPlaylist = history.location.pathname.substring(
     history.location.pathname.lastIndexOf("/") + 1
   );
-  let indexPlaylist;
-  if (promise?.userPlaylists?.payload) {
-    indexPlaylist = promise?.userPlaylists?.payload.findIndex(
-      (playlist) => playlist?._id === idPlaylist
+  const [newPlaylistTracks, setNewPlaylistTracks] = useState(playlistTracks);
+  const onSortEnd = (e) => {
+    var newChangedPlaylistTracks = arrayMoveImmutable(
+      newPlaylistTracks,
+      e.oldIndex,
+      e.newIndex
     );
-  }
+    setNewPlaylistTracks(newChangedPlaylistTracks);
+    const newChangedPlaylistIdTracks = [];
+    newChangedPlaylistTracks.map((newTrack) =>
+      newChangedPlaylistIdTracks.push({ _id: newTrack?._id })
+    );
+    loadTracksToPlaylist(idPlaylist, newChangedPlaylistIdTracks);
+  };
+  return (
+    <SortableList playlistTracks={newPlaylistTracks} onSortEnd={onSortEnd} />
+  );
+};
 
+const CTracksInPlaylistSortable = connect(null, {
+  loadTracksToPlaylist: actionTracksToPlaylist,
+})(TracksInPlaylistSortable);
+
+const PlaylistTracks = ({ promise, tracks: { _id, url } = {} }) => {
   return (
     <div>
-      {promise?.userPlaylists?.payload[indexPlaylist]?.tracks ? (
-        <div className="d-block mx-auto mt-2 container w-50">
-          <PlayerHeader personal />
-        </div>
-      ) : null}
-      {promise?.loadTracksToPlaylist?.status === "PENDING" ||
-      promise?.playlistTracks?.status === "PENDING" ? (
-        <Loader />
-      ) : promise?.playlistTracks?.payload?.tracks ? (
-        promise?.playlistTracks?.payload?.tracks.map((track, index) => (
-          <div
-            className="d-block mx-auto mt-2 container w-50"
-            key={Math.random()}
-          >
-            <CAudio
-              track={track}
-              index={index}
-              playlist={promise?.playlistTracks?.payload?.tracks}
-              personal
-              key={track._id}
-            />
-          </div>
-        ))
-      ) : (
+      <CPreloader
+        promiseName={"playlistTracks"}
+        promiseState={promise}
+        children={
+          <CTracksInPlaylistSortable
+            playlistTracks={promise?.playlistTracks?.payload?.tracks}
+          />
+        }
+      />
+      <CPreloader
+        promiseName={"myUser"}
+        promiseState={promise}
+        children={null}
+      />
+      {promise?.playlistTracks?.payload?.tracks ? null : (
         <h2 className="mt-5 text-center">
           {promise?.myUser?.payload?.nick
             ? promise?.myUser?.payload?.nick
@@ -73,38 +112,11 @@ export const MyPlaylistTracks = () => (
   </>
 );
 
-const PlaylistEditor = ({
-  entity = { array: [] },
-  onSave,
-  onFileDrop,
-  fileStatus,
-}) => {
-  const [state, setState] = useState(entity);
-  //по файлу в дропзоне:
-  //дергать onFileDrop
-  //fileStatus - информация о заливке файла из redux
-  //через useEffect дождаться когда файл зальется
-  //и сделать setState({...state, array: [...state.array, {объект файла с бэка с _id и url}]})
-  //по react-sortable-hoc
-  //делаете как в пример arrayMove для state.array
-  //по кнопке сохранения делаем onSave(state)
-  //где-то рядом остальные поля из state типа title name text
-  //но это вы уже знаете
-  // return (
-
-  // )
-};
-
 const Playlist = ({ playlist: { _id, name } = {} }) => (
   <li className="d-flex">
     <div className="col-lg-6">
       <Link to={`/myplaylist/${_id}`}>{name}</Link>
     </div>
-    <div className="col-lg-6">
-      <Button className="ml-5">
-        <i className="fas fa-trash-alt"></i>
-      </Button>
-    </div>
   </li>
 );
 

+ 19 - 0
src/components/Preloader.js

@@ -0,0 +1,19 @@
+import { Loader } from "./Loader";
+import { connect } from "react-redux";
+
+const RejectedAlert = ({ error }) => <div>Произошла ошибка: {error}</div>;
+
+const Preloader = ({ promiseName, promiseState, children }) => (
+  <>
+    {promiseState[promiseName]?.status === "RESOLVED" ? (
+      children
+    ) : promiseState[promiseName]?.status === "REJECTED" ? (
+      <RejectedAlert error={promiseState[promiseName]?.error} />
+    ) : (
+      <Loader />
+    )}
+  </>
+);
+export const CPreloader = connect((state) => ({ promiseState: state.promise }))(
+  Preloader
+);

+ 0 - 1
src/components/Profile/MyProfile.js

@@ -36,7 +36,6 @@ const MyProfile = ({
     <div className="ProfilePage">
       {auth.token ? (
         <>
-          
           {id === auth?.payload?.sub?.id ? (
             promise?.myUser?.status === "RESOLVED" ? (
               <CUserInfo id={id} />

+ 9 - 5
src/components/SearchResult.js

@@ -1,17 +1,21 @@
 import { connect } from "react-redux";
 import { CTracks } from "./Tracks";
+import { PlayerHeader } from "./PlayerHeader";
 
 const SearchResult = ({ promise, search }) =>
-  search?.searchResult?.payload?.payload ? (
-    <CTracks tracks={search?.searchResult?.payload?.payload} />
+  search?.searchResult?.payload?.payload?.length !== 0 ? (
+    <>
+      <PlayerHeader />
+      <CTracks tracks={search?.searchResult?.payload?.payload} search />
+    </>
   ) : (
     <h2 className="mt-5 text-center">
-      {promise?.user?.payload?.nick ? promise?.user?.payload?.nick : "user"}, по
-      вашему запросу ничего не было найдено.
+      {promise?.myUser?.payload?.nick ? promise?.myUser?.payload?.nick : "user"}
+      , по вашему запросу ничего не было найдено.
     </h2>
   );
 
 export const CSearchResult = connect(
-  (state) => ({ promise: state.promise, search: state.search || [] }),
+  (state) => ({ promise: state.promise, search: state.search }),
   null
 )(SearchResult);

+ 2 - 2
src/components/Sidebar.js

@@ -25,9 +25,9 @@ export const Sidebar = () => {
         </CDBSidebarHeader>
         <CDBSidebarContent className="sidebar-content">
           <CDBSidebarMenu>
-            <NavLink exact to="/" activeClassName="activeClicked">
+            {/*<NavLink exact to="/" activeClassName="activeClicked">
               <CDBSidebarMenuItem icon="columns">Главная</CDBSidebarMenuItem>
-            </NavLink>
+            </NavLink>*/}
             <NavLink exact to="/search" activeClassName="activeClicked">
               <CDBSidebarMenuItem icon="search">Поиск</CDBSidebarMenuItem>
             </NavLink>

+ 65 - 6
src/components/Tracks.js

@@ -1,17 +1,76 @@
 import { connect } from "react-redux";
 import { CAudio } from "./Audio";
+import { useState, useEffect } from "react";
+import { actionAllTracks, actionFullSkipTracks } from "../actions";
+
+const Tracks = ({
+  tracks,
+  skip,
+  getAllTracks,
+  skipAllTracks,
+  search,
+  searchResults,
+}) => {
+  const [flag, setFlag] = useState(false);
+
+  useEffect(() => {
+    if (flag) {
+      window.scrollTo(0, 0);
+      getAllTracks(skip);
+      setFlag(false);
+    }
+  }, [flag]);
+
+  useEffect(() => {
+    document.addEventListener("scroll", scrollHandler);
+
+    return function () {
+      document.removeEventListener("scroll", scrollHandler);
+    };
+  }, [tracks]);
+
+  const scrollHandler = (e) => {
+    if (
+      e.target.documentElement.scrollHeight -
+        (e.target.documentElement.scrollTop + window.innerHeight) ===
+      0
+    ) {
+      setFlag(true);
+      skipAllTracks(30);
+    }
+  };
 
-const Tracks = ({ tracks, player }) => {
   return (
     <>
-      {tracks.map((track, index) => (
-        <CAudio track={track} index={index} playlist={tracks} key={track._id} />
-      ))}
+      {search
+        ? (searchResults || []).map((track, index) => (
+            <CAudio
+              track={track}
+              index={index}
+              playlist={searchResults}
+              key={track._id}
+            />
+          ))
+        : (tracks || []).map((track, index) => (
+            <CAudio
+              track={track}
+              index={index}
+              playlist={tracks}
+              key={track._id}
+            />
+          ))}
     </>
   );
 };
 
 export const CTracks = connect(
-  (state) => ({ player: state.player }),
-  null
+  (state) => ({
+    tracks: state?.loadedTracks?.loadedTracks,
+    skip: state?.loadedTracks?.skipTracks,
+    searchResults: state?.search?.searchResult?.payload?.payload,
+  }),
+  {
+    getAllTracks: actionAllTracks,
+    skipAllTracks: actionFullSkipTracks,
+  }
 )(Tracks);

+ 3 - 2
src/pages/Library.js

@@ -3,13 +3,14 @@ import { AuthCheck } from "./../components/AuthCheck";
 import { history } from "./../App";
 import { CMyPlaylists } from "../components/Playlist";
 
-const Library = ({ auth, promise, actionTracks, actionUser }) => {
+const Library = ({ auth, promise }) => {
   return (
     <div className="SearchPage">
       {auth.token && history.location.pathname === "/library" ? (
         <div className="d-block mx-auto mt-2 container w-50">
           <h1 className="mb-3 text-center">
-            Ваша библиотека плейлистов с музыкой, {promise?.user?.payload?.nick}
+            Ваша библиотека плейлистов с музыкой,{" "}
+            {promise?.myUser?.payload?.nick}
           </h1>
           <CMyPlaylists />
         </div>

+ 27 - 20
src/pages/Search.js

@@ -4,9 +4,9 @@ import { AuthCheck } from "./../components/AuthCheck";
 import { history } from "./../App";
 import { CTracks } from "../components/Tracks";
 import { PlayerHeader } from "./../components/PlayerHeader";
-import { Loader } from "./../components/Loader";
 import { useState } from "react";
 import { actionSearch } from "./../actions/index";
+import { CPreloader } from "./../components/Preloader";
 
 const SearchField = connect(null, { onChangeSearch: actionSearch })(
   ({ onChangeSearch }) => {
@@ -34,25 +34,32 @@ const Search = ({ search, auth, promise }) => {
   return (
     <div className="SearchPage">
       {auth.token && history.location.pathname === "/search" ? (
-        <div className="d-block mx-auto mt-2 container w-50">
-          <h1 className="text-center">Поиск по сайту</h1>
-          <SearchField />
-          {promise?.tracks?.payload?.length !== 0 ? <PlayerHeader /> : null}
-          {search?.searchResult?.payload?.payload ? (
-            <CSearchResult />
-          ) : promise?.tracks?.status === "PENDING" ? (
-            <Loader />
-          ) : promise?.tracks?.payload?.length !== 0 ? (
-            <CTracks tracks={promise.tracks.payload} />
-          ) : (
-            <h2 className="mt-5 text-center">
-              {promise?.user?.payload?.nick
-                ? promise?.user?.payload?.nick
-                : "user"}
-              , на сайте не обнаружено треков.
-            </h2>
-          )}
-        </div>
+        <>
+          <div className="d-block mx-auto mt-2 container w-50">
+            <h1 className="text-center">Поиск по сайту</h1>
+            <SearchField />
+            {search?.searchResult?.payload?.payload ? (
+              <CSearchResult />
+            ) : promise?.tracks?.payload?.length !== 0 ? (
+              <>
+                <PlayerHeader />
+                <CPreloader
+                  promiseName={"tracks"}
+                  promiseState={promise}
+                  children={<CTracks tracks={promise?.tracks?.payload} />}
+                />
+              </>
+            ) : (
+              <h2 className="mt-5 text-center">
+                {promise?.myUser?.payload?.nick
+                  ? promise?.myUser?.payload?.nick
+                  : "user"}
+                , по запросу не было найдено треков.
+              </h2>
+            )}
+          </div>
+          <div className="container" style={{ height: "300px" }}></div>
+        </>
       ) : (
         <div className="d-block mx-auto mt-2 container w-50">
           <AuthCheck header="Поиск по сайту" />

+ 33 - 2
src/reducers/index.js

@@ -60,8 +60,8 @@ let initState = {
   isPlaying: false,
   isPaused: true,
   duration: 0,
-  track: null, // {_id, url, id3.........}
-  playlist: null, // {_id, name, tracks: [{-id}, {_id} ....]}
+  track: null,
+  playlist: null,
   indexInPlaylist: null,
   currentTime: 0,
   volume: 0.5,
@@ -144,3 +144,34 @@ export function routeReducer(state = {}, { type, match }) {
   }
   return state;
 }
+
+export function scrollTracksReducer(
+  state = {},
+  { type, newTracks, skipTracks }
+) {
+  if (type === "ADD_TRACKS") {
+    return {
+      ...state,
+      loadedTracks: [...newTracks],
+    };
+  }
+  if (type === "ADD_SKIP") {
+    return {
+      ...state,
+      skipTracks: state?.skipTracks
+        ? state?.skipTracks + skipTracks
+        : skipTracks,
+    };
+  }
+  if (type === "CLEAR_SKIP") {
+    return {
+      ...state,
+      skipTracks: 0,
+    };
+  }
+  return state;
+}
+
+// loadedTracks:state?.loadedTracks
+//         ? [...state.loadedTracks, ...newTracks]
+//         : [...newTracks],