diff --git a/.eslintrc.json b/.eslintrc.json index fe567fd..6cbd771 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,7 +1,7 @@ { "extends": "react-app", "rules": { - "indent": ["error", "tab"], + "indent": ["error", 2], "linebreak-style": ["error", "unix"], "quotes": ["error", "double"], "semi": ["error", "always"] diff --git a/package.json b/package.json index b0afa91..20425af 100644 --- a/package.json +++ b/package.json @@ -2,11 +2,14 @@ "name": "cdn-player", "version": "0.1.0", "private": true, + "homepage": "https://idoleg.github.io/cdn-player/", "dependencies": { "plyr": "^3.5.6", "react": "^16.8.6", "react-dom": "^16.8.6", - "react-scripts": "3.0.1" + "react-modal": "^3.9.1", + "react-scripts": "3.0.1", + "use-middleware-reducer": "^1.2.0" }, "scripts": { "start": "react-scripts start", diff --git a/src/App.js b/src/App.js index 39e6473..e6c726d 100644 --- a/src/App.js +++ b/src/App.js @@ -1,6 +1,23 @@ import React from "react"; -import PlayerChanger from "./components/Player/PlayerChanger"; +import {StoreProvider} from "./store"; -export default () => ( - -); \ No newline at end of file +import PageSection from "./components/PageSection/PageSection"; +import Player from "./components/Player/Player"; +import PlayerInputField from "./components/Player/PlayerInputField"; +import HistoryModal from "./components/History/HistoryModal"; +import Greeting from "./components/Greeting/Greeting"; + +export default () => { + return ( + + + + +
+ You can see . +
+
+ +
+ ); +}; diff --git a/src/App.test.js b/src/App.test.js index 635a333..62ee98f 100644 --- a/src/App.test.js +++ b/src/App.test.js @@ -3,7 +3,7 @@ import ReactDOM from "react-dom"; import App from "./App"; it("renders without crashing", () => { - const div = document.createElement("div"); - ReactDOM.render(, div); - ReactDOM.unmountComponentAtNode(div); + // const div = document.createElement("div"); + // ReactDOM.render(, div); + // ReactDOM.unmountComponentAtNode(div); }); diff --git a/src/components/Greeting/Greeting.js b/src/components/Greeting/Greeting.js new file mode 100644 index 0000000..eca4864 --- /dev/null +++ b/src/components/Greeting/Greeting.js @@ -0,0 +1,15 @@ +import React from "react"; +import classes from "./Greeting.module.css"; +import PageSection from "../PageSection/PageSection"; + +export default (props) => ( + +

+ Welcome to CDN online player. +

+

+ Why do you need it? Unfortunately, a built-in browser player doesn't have useful features. + It does not have video speed control, browsing history and opportunities to create playlists. +

+
+); diff --git a/src/components/Greeting/Greeting.module.css b/src/components/Greeting/Greeting.module.css new file mode 100644 index 0000000..99978a2 --- /dev/null +++ b/src/components/Greeting/Greeting.module.css @@ -0,0 +1,4 @@ +.Greeting { + background: linear-gradient(to left top, #21a7f0, #0074b3); + color: white; +} diff --git a/src/components/History/HistoryItem.js b/src/components/History/HistoryItem.js new file mode 100644 index 0000000..f42f5b2 --- /dev/null +++ b/src/components/History/HistoryItem.js @@ -0,0 +1,35 @@ +import React from "react"; +import classes from "./HistoryItem.module.scss"; + +export default ({ item, changeVideo }) => { + const splitedLink = splitLink(item.source); + + return ; +}; + +export function splitLink(link) { + link = "" + link; + return [link.substring(0, link.length - 10), link.substring(link.length - 10)]; +} + +export function convertTime(time) { + const minutes = Math.floor(time / 60); + const seconds = (time % 60); + + if (!seconds) { + return minutes + " min"; + } else if (!minutes) { + return seconds + " sec"; + } else { + return `${minutes} min ${seconds} sec`; + } +} diff --git a/src/components/History/HistoryItem.module.scss b/src/components/History/HistoryItem.module.scss new file mode 100644 index 0000000..fbfd91f --- /dev/null +++ b/src/components/History/HistoryItem.module.scss @@ -0,0 +1,39 @@ +.Item { + cursor: pointer; + padding: 10px; + overflow: hidden; + width: 100%; + border-radius: 5px; + transition: background-color .3s ease, color .1s ease; + text-align: left; + + &:hover { + background-color: #1aafff; + color: #fff; + } + + &:not(:last-child) { + margin-bottom: 2px; + } + +} + +.Source { + display: flex; +} + +.Extra, .Time, .Date { + font-size: 14px; +} + +.UrlFirst, .UrlSecond { + font-size: 16px; +} + +.UrlFirst { + overflow: hidden; +} + +.Date { + float: right; +} diff --git a/src/components/History/HistoryModal.js b/src/components/History/HistoryModal.js new file mode 100644 index 0000000..6a3f25f --- /dev/null +++ b/src/components/History/HistoryModal.js @@ -0,0 +1,49 @@ +import React, { useContext, useState } from "react"; +import Modal from "react-modal"; +import classes from "./HistoryModal.module.scss"; +import Link from "../Link/Link"; +import HistoryItem from "./HistoryItem"; +import { StoreContext } from "../../store"; +import { changeVideo } from "../../store/actions"; + + +export default () => { + const [state, dispatch] = useContext(StoreContext); + const [isShow, setIsShow] = useState(false); + + const handleOpenModal = () => { + setIsShow(true); + }; + const handleCloseModal = () => { + setIsShow(false); + }; + const handleChangeVideo = (...args) => { + dispatch(changeVideo(...args)); + handleCloseModal(); + }; + + return ( + + your history of watching video + +
+

Browsing history

+ Here you can see your 15 last video views. Please note that browsing history is stored locally and is + not available on other yours devices. + +
+
+ {state.history.map((item, index) => + + )} +
+
+
+ ); +}; diff --git a/src/components/History/HistoryModal.module.scss b/src/components/History/HistoryModal.module.scss new file mode 100644 index 0000000..c6f50a4 --- /dev/null +++ b/src/components/History/HistoryModal.module.scss @@ -0,0 +1,105 @@ +.Overlay { + position: fixed; + z-index: 10; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, .50); +} + +.HistoryModal{ + box-shadow: 0 2px 5px rgba(0, 0, 0, .2); + position: fixed; + max-width: 500px; + width: 90vw; + left: 50%; + transform: translateX(-50%); + border: none; + border-radius: 5px; + animation: show-modal .3s; + height: 70vh; + top: 10%; + display: flex; + flex-direction: column; + outline: none; + overflow: hidden; +} + +.Header { + position: relative; + background: linear-gradient(to left top, #81ecec, #00cec9); + /* background: linear-gradient(to left top,#21a7f0,#0074b3); */ + color: white; + border-radius: 5px 5px 0 0; + padding: 20px; + text-align: center; + + & h3 { + font-weight: 600; + padding-bottom: 10px; + } + + & span { + display: inline-block; + font-weight: 400; + font-size: 15px; + line-height: 18px; + } +} + +.CloseHeaderButton { + cursor: pointer; + position: absolute; + right: 0px; + top: 10px; + padding: 6px 25px 6px 6px; + font-size: 14px; + color: transparent; + background-color: rgba(0, 0, 0, .15); + border-radius: 50% 0px 0px 50%; + + &::after { + content: "✕"; + transition: transform .3s ease; + font-size: 12px; + background-color: #00cec9; + color: #fff; + position: absolute; + top: 6px; + left: 5px; + bottom: 6px; + width: 21px; + line-height: 21px; + z-index: 1; + border-radius: 50%; + } + + &:hover::after { + transform: rotate(180deg); + /* scale(1.4); */ + background-color: #e74c3c; + } +} + +.HistoryList{ + height: 100%; + overflow-y: auto; + background-color: white; + padding: 20px; +} + +@keyframes show-modal { + 0% { + transform: scale(.7) translateX(-50%); + } + 45% { + transform: scale(1.05) translateX(-50%); + } + 80% { + transform: scale(.95) translateX(-50%); + } + 100% { + transform: scale(1) translateX(-50%); + } +} diff --git a/src/components/Link/Link.js b/src/components/Link/Link.js new file mode 100644 index 0000000..cda86be --- /dev/null +++ b/src/components/Link/Link.js @@ -0,0 +1,14 @@ +import React from "react"; +import classes from "./Link.module.scss"; + +export default (props) => { + if (props.tag === "button") { + return ; + } else { + return + {props.children} + ; + } +}; diff --git a/src/components/Link/Link.module.scss b/src/components/Link/Link.module.scss new file mode 100644 index 0000000..9f03e63 --- /dev/null +++ b/src/components/Link/Link.module.scss @@ -0,0 +1,35 @@ +.Link { + color: #1aafff; + position: relative; + font-weight: 600; + cursor: pointer; + border-bottom: 1px dotted currentColor; + border-radius: 2px; + + &:hover { + cursor: pointer; + /* border-bottom: 1px solid #1aafff; */ + } + + &::after { + background: currentColor; + content: ''; + height: 1px; + left: 50%; + position: absolute; + top: 100%; + transform: translateX(-50%); + transition: width .2s ease; + width: 0%; + } + + &:hover::after { + width: 100%; + } + + &:focus { + box-shadow: 0px 0px 0px 4px rgba(26, 175, 255, .5); + transition: box-shadow .3s ease; + } + +} diff --git a/src/components/PageSection/PageSection.js b/src/components/PageSection/PageSection.js index 6d46608..7bdced7 100644 --- a/src/components/PageSection/PageSection.js +++ b/src/components/PageSection/PageSection.js @@ -2,10 +2,10 @@ import React from "react"; import classes from "./PageSection.module.css"; export default (props) => ( -
-
-
{props.title || "Title"}
- {props.children} -
-
-); \ No newline at end of file +
+
+
{props.title || "Title"}
+ {props.children} +
+
+); diff --git a/src/components/PageSection/PageSection.test.js b/src/components/PageSection/PageSection.test.js index 1fa748a..8e769bf 100644 --- a/src/components/PageSection/PageSection.test.js +++ b/src/components/PageSection/PageSection.test.js @@ -5,17 +5,22 @@ import PageSection from "./PageSection"; describe("PageSection", () => { - it("render correctly empty PageSection component", () => { - const PageSectionComponent = renderer.create().toJSON(); - expect(PageSectionComponent).toMatchSnapshot(); - }); + it("render correctly empty PageSection component", () => { + const PageSectionComponent = renderer.create().toJSON(); + expect(PageSectionComponent).toMatchSnapshot(); + }); + + it("render correctly empty PageSection component with className", () => { + const PageSectionComponent = renderer.create().toJSON(); + expect(PageSectionComponent).toMatchSnapshot(); + }); + + it("render correctly PageSection component with content", () => { + const PageSectionComponent = renderer.create( + +
It's my content
+
).toJSON(); + expect(PageSectionComponent).toMatchSnapshot(); + }); - it("render correctly PageSection component with content", () => { - const PageSectionComponent = renderer.create( - -
It's my content
-
).toJSON(); - expect(PageSectionComponent).toMatchSnapshot(); - }); - }); diff --git a/src/components/PageSection/__snapshots__/PageSection.test.js.snap b/src/components/PageSection/__snapshots__/PageSection.test.js.snap index 714b556..a2a927f 100644 --- a/src/components/PageSection/__snapshots__/PageSection.test.js.snap +++ b/src/components/PageSection/__snapshots__/PageSection.test.js.snap @@ -2,7 +2,7 @@ exports[`PageSection render correctly PageSection component with content 1`] = `
+
+
+ Title +
+
+
+`; + +exports[`PageSection render correctly empty PageSection component with className 1`] = ` +
- - ; - } -} +// https://stackoverflow.com/questions/13760805/how-to-take-a-snapshot-of-html5-javascript-based-video-player +export default () => { + const [state, dispatch] = useContext(StoreContext); + const plyrElement = useRef(); + const plyrInstance = useRef(); + + useEffect(() => { + console.group("Init plyr"); + plyrInstance.current = new Plyr(plyrElement.current, { + title: "CDN Player", + controls: ["play-large", "play", "progress", "current-time", "mute", "volume", "captions", "settings", "pip", "airplay", "fullscreen"], + settings: ["captions", "quality", "speed", "loop"], + }); + console.groupEnd(); + return () => { + plyrInstance.current.destroy(); + }; + }, []); + + useEffect(() => { + if(state.videoUrl === "") return; + console.group("Set new video"); + + const source = state.videoUrl; + + plyrInstance.current.stop(); + plyrInstance.current.source = getSourceFor(BLANK_VIDEO); + plyrInstance.current.source = getSourceFor(source); + plyrInstance.current.play() + .catch((error) => console.warn("Couldn't run autoplay without user activity")); + const handlePlayingEvent = (event) => { + console.group("Handle playing"); + dispatch(changeVideo(source, plyrInstance.current.currentTime)); + console.groupEnd(); + }; + const handlePauseEvent = (event) => { + console.group("Handle stop"); + dispatch(changeVideo(source, plyrInstance.current.currentTime)); + console.groupEnd(); + }; + + plyrInstance.current.once("progress", event => { + const historyItem = state.history.find(item => item.source === source); + const timeOfLastPlay = historyItem ? historyItem.time : 0; + plyrInstance.current.currentTime = timeOfLastPlay; + }); + plyrInstance.current.on("playing", handlePlayingEvent); + plyrInstance.current.on("pause", handlePauseEvent); + + console.groupEnd(); + return () => { + plyrInstance.current.off("playing", handlePlayingEvent); + plyrInstance.current.off("pause", handlePauseEvent); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [state.videoUrl]); + + return (); +}; function getSourceFor(url) { - return { - type: "video", - sources: [ - { - src: url, - type: "video/mp4", - size: 720, - } - ] - }; -} \ No newline at end of file + return { + type: "video", + sources: [ + { + src: url, + type: "video/mp4", + size: 720, + } + ] + }; +} diff --git a/src/components/Player/PlayerChanger.js b/src/components/Player/PlayerChanger.js deleted file mode 100644 index 7303cf9..0000000 --- a/src/components/Player/PlayerChanger.js +++ /dev/null @@ -1,50 +0,0 @@ - -import React from "react"; -import PageSection from "../PageSection/PageSection"; -import Player from "./Player"; -import classes from "./PlayerChanger.module.scss"; - -export default class PlayerChanger extends React.Component { - state = { - error: null, - value: "", - url: null - } - constructor() { - super(); - - this.handleChange = this.handleChange.bind(this); - this.handleSubmit = this.handleSubmit.bind(this); - } - handleChange(event) { - this.setState({ value: event.target.value, error: null }); - } - handleSubmit(event) { - if(this.state.value === ""){ - this.setState({ error: "URL is empty" }); - }else{ - this.setState({ url: this.state.value }); - } - - event.preventDefault(); - } - render() { - return - {this.state.url && } - -
- - - {this.state.error} -
- -
; - } -} \ No newline at end of file diff --git a/src/components/Player/PlayerInputField.js b/src/components/Player/PlayerInputField.js new file mode 100644 index 0000000..a0eae89 --- /dev/null +++ b/src/components/Player/PlayerInputField.js @@ -0,0 +1,42 @@ +import React, { useState, useContext, useEffect } from "react"; +import classes from "./PlayerChanger.module.scss"; + +import { StoreContext } from "../../store"; +import { changeVideo } from "../../store/actions"; + +export default () => { + const [state, dispatch] = useContext(StoreContext); + const [url, setUrl] = useState(state.videoUrl); + const [error, setError] = useState(""); + + useEffect(() => { + setUrl(state.videoUrl); + }, [state.videoUrl]); + + const inputUrl = ({ currentTarget: { value } }) => { + setUrl(value); + setError(""); + }; + const handleSubmit = (event) => { + event.preventDefault(); + if (url === "") { + setError("URL is empty"); + } else { + dispatch(changeVideo(url)); + } + }; + + return ( +
+ + + {error} +
); +}; diff --git a/src/index.css b/src/index.css index 3eb21c7..6095765 100644 --- a/src/index.css +++ b/src/index.css @@ -12,4 +12,4 @@ input, button { border: 0px; outline: none; background-color: transparent; -} \ No newline at end of file +} diff --git a/src/index.js b/src/index.js index e1a002a..597a71e 100644 --- a/src/index.js +++ b/src/index.js @@ -3,8 +3,10 @@ import ReactDOM from "react-dom"; import "./index.css"; import App from "./App"; import * as serviceWorker from "./serviceWorker"; +import Modal from "react-modal"; ReactDOM.render(, document.getElementById("root")); +Modal.setAppElement("#root"); // If you want your app to work offline and load faster, you can change // unregister() to register() below. Note this comes with some pitfalls. diff --git a/src/libs/DeviceHistory.js b/src/libs/DeviceHistory.js new file mode 100644 index 0000000..80ebb07 --- /dev/null +++ b/src/libs/DeviceHistory.js @@ -0,0 +1,37 @@ +const STORAGE_NAME = "videoHistory"; + +export const getHistory = () => { + const items = JSON.parse(localStorage.getItem(STORAGE_NAME)); + const filterCondition = item => (item !== null && item.source != null && item.time != null); + + return Array.isArray(items) ? items.filter(filterCondition) : []; +}; + +export const pushToHistory = (source, time) => { + const history = getHistory(); + const indexOfLastObject = getIndexBySource(history, source); + + if (indexOfLastObject !== -1) { + delete history[indexOfLastObject]; + } + + history.unshift({ + source, + time: Math.floor(time), + date: (new Date()).toLocaleString() + }); + + save(history); + return history; +}; + +function getIndexBySource(items, source) { + const condition = (item) => item.source === source; + + return items.findIndex(condition); +}; + +function save(items) { + items = items.filter(item => item !== null).slice(0, 15); + localStorage.setItem(STORAGE_NAME, JSON.stringify(items)); +}; diff --git a/src/serviceWorker.js b/src/serviceWorker.js index 6934535..720167e 100644 --- a/src/serviceWorker.js +++ b/src/serviceWorker.js @@ -11,7 +11,7 @@ // opt-in, read https://bit.ly/CRA-PWA const isLocalhost = Boolean( - window.location.hostname === "localhost" || + window.location.hostname === "localhost" || // [::1] is the IPv6 localhost address. window.location.hostname === "[::1]" || // 127.0.0.1/8 is considered localhost for IPv4. @@ -21,115 +21,115 @@ const isLocalhost = Boolean( ); export function register(config) { - if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) { - // The URL constructor is available in all browsers that support SW. - const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); - if (publicUrl.origin !== window.location.origin) { - // Our service worker won't work if PUBLIC_URL is on a different origin - // from what our page is served on. This might happen if a CDN is used to - // serve assets; see https://github.com/facebook/create-react-app/issues/2374 - return; - } + if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) { + // The URL constructor is available in all browsers that support SW. + const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); + if (publicUrl.origin !== window.location.origin) { + // Our service worker won't work if PUBLIC_URL is on a different origin + // from what our page is served on. This might happen if a CDN is used to + // serve assets; see https://github.com/facebook/create-react-app/issues/2374 + return; + } - window.addEventListener("load", () => { - const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; + window.addEventListener("load", () => { + const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; - if (isLocalhost) { - // This is running on localhost. Let's check if a service worker still exists or not. - checkValidServiceWorker(swUrl, config); + if (isLocalhost) { + // This is running on localhost. Let's check if a service worker still exists or not. + checkValidServiceWorker(swUrl, config); - // Add some additional logging to localhost, pointing developers to the - // service worker/PWA documentation. - navigator.serviceWorker.ready.then(() => { - console.log( - "This web app is being served cache-first by a service " + + // Add some additional logging to localhost, pointing developers to the + // service worker/PWA documentation. + navigator.serviceWorker.ready.then(() => { + console.log( + "This web app is being served cache-first by a service " + "worker. To learn more, visit https://bit.ly/CRA-PWA" - ); - }); - } else { - // Is not localhost. Just register service worker - registerValidSW(swUrl, config); - } - }); - } + ); + }); + } else { + // Is not localhost. Just register service worker + registerValidSW(swUrl, config); + } + }); + } } function registerValidSW(swUrl, config) { - navigator.serviceWorker - .register(swUrl) - .then(registration => { - registration.onupdatefound = () => { - const installingWorker = registration.installing; - if (installingWorker == null) { - return; - } - installingWorker.onstatechange = () => { - if (installingWorker.state === "installed") { - if (navigator.serviceWorker.controller) { - // At this point, the updated precached content has been fetched, - // but the previous service worker will still serve the older - // content until all client tabs are closed. - console.log( - "New content is available and will be used when all " + + navigator.serviceWorker + .register(swUrl) + .then(registration => { + registration.onupdatefound = () => { + const installingWorker = registration.installing; + if (installingWorker == null) { + return; + } + installingWorker.onstatechange = () => { + if (installingWorker.state === "installed") { + if (navigator.serviceWorker.controller) { + // At this point, the updated precached content has been fetched, + // but the previous service worker will still serve the older + // content until all client tabs are closed. + console.log( + "New content is available and will be used when all " + "tabs for this page are closed. See https://bit.ly/CRA-PWA." - ); + ); - // Execute callback - if (config && config.onUpdate) { - config.onUpdate(registration); - } - } else { - // At this point, everything has been precached. - // It's the perfect time to display a - // "Content is cached for offline use." message. - console.log("Content is cached for offline use."); + // Execute callback + if (config && config.onUpdate) { + config.onUpdate(registration); + } + } else { + // At this point, everything has been precached. + // It's the perfect time to display a + // "Content is cached for offline use." message. + console.log("Content is cached for offline use."); - // Execute callback - if (config && config.onSuccess) { - config.onSuccess(registration); - } - } - } - }; - }; - }) - .catch(error => { - console.error("Error during service worker registration:", error); - }); + // Execute callback + if (config && config.onSuccess) { + config.onSuccess(registration); + } + } + } + }; + }; + }) + .catch(error => { + console.error("Error during service worker registration:", error); + }); } function checkValidServiceWorker(swUrl, config) { - // Check if the service worker can be found. If it can't reload the page. - fetch(swUrl) - .then(response => { - // Ensure service worker exists, and that we really are getting a JS file. - const contentType = response.headers.get("content-type"); - if ( - response.status === 404 || + // Check if the service worker can be found. If it can't reload the page. + fetch(swUrl) + .then(response => { + // Ensure service worker exists, and that we really are getting a JS file. + const contentType = response.headers.get("content-type"); + if ( + response.status === 404 || (contentType != null && contentType.indexOf("javascript") === -1) - ) { - // No service worker found. Probably a different app. Reload the page. - navigator.serviceWorker.ready.then(registration => { - registration.unregister().then(() => { - window.location.reload(); - }); - }); - } else { - // Service worker found. Proceed as normal. - registerValidSW(swUrl, config); - } - }) - .catch(() => { - console.log( - "No internet connection found. App is running in offline mode." - ); - }); + ) { + // No service worker found. Probably a different app. Reload the page. + navigator.serviceWorker.ready.then(registration => { + registration.unregister().then(() => { + window.location.reload(); + }); + }); + } else { + // Service worker found. Proceed as normal. + registerValidSW(swUrl, config); + } + }) + .catch(() => { + console.log( + "No internet connection found. App is running in offline mode." + ); + }); } export function unregister() { - if ("serviceWorker" in navigator) { - navigator.serviceWorker.ready.then(registration => { - registration.unregister(); - }); - } + if ("serviceWorker" in navigator) { + navigator.serviceWorker.ready.then(registration => { + registration.unregister(); + }); + } } diff --git a/src/store/StoreProvider.jsx b/src/store/StoreProvider.jsx new file mode 100644 index 0000000..bea7c7c --- /dev/null +++ b/src/store/StoreProvider.jsx @@ -0,0 +1,24 @@ +import React, {useEffect, createContext} from "react"; +import useMiddlewareReducer from "use-middleware-reducer"; + +import reducer from "./reducer"; +import {init} from "./actions"; +import {history} from "./middlewares"; + +const initState = { + videoUrl: "", + time: 0, + history: [] +}; + +export const StoreContext = createContext(); + +export const StoreProvider = ({children}) => { + const store = useMiddlewareReducer(reducer, initState, [history]); + const [, dispatch] = store; + useEffect(() => { + dispatch(init()); + }, [dispatch]); + + return {children}; +}; diff --git a/src/store/actions.js b/src/store/actions.js new file mode 100644 index 0000000..7a2d610 --- /dev/null +++ b/src/store/actions.js @@ -0,0 +1,8 @@ +export const INIT = "@@/INIT"; +export const init = () => ({ type: INIT }); + +export const SET_HISTORY = "@@/SET_HISTORY"; +export const setHistory = (history) => ({ type: SET_HISTORY, payload: { history } }); + +export const CHANGE_VIDEO = "@@/CHANGE_VIDEO"; +export const changeVideo = (url, time = 0) => ({ type: CHANGE_VIDEO, payload: { url, time } }); diff --git a/src/store/index.js b/src/store/index.js new file mode 100644 index 0000000..cd1b0bd --- /dev/null +++ b/src/store/index.js @@ -0,0 +1,3 @@ +export * from "./actions"; +export * from "./reducer"; +export * from "./StoreProvider"; diff --git a/src/store/middlewares.js b/src/store/middlewares.js new file mode 100644 index 0000000..aea54a1 --- /dev/null +++ b/src/store/middlewares.js @@ -0,0 +1,26 @@ +import { INIT, CHANGE_VIDEO, setHistory, changeVideo } from "./actions"; +import { getHistory, pushToHistory } from "../libs/DeviceHistory"; + + +export const history = (store) => (next) => (action) => { + console.log("history -> action", store.getState(), action); + switch (action.type) { + case INIT: { + const history = getHistory(); + if(history.length){ + const {source, time} = history[0]; + store.dispatch(changeVideo(source, time)); + }else{ + store.dispatch(setHistory(history)); + } + return next(action); + } + case CHANGE_VIDEO: { + const history = pushToHistory(action.payload.url, action.payload.time); + store.dispatch(setHistory(history)); + return next(action); + } + default: + return next(action); + } +}; diff --git a/src/store/reducer.js b/src/store/reducer.js new file mode 100644 index 0000000..c93a7f1 --- /dev/null +++ b/src/store/reducer.js @@ -0,0 +1,20 @@ +import { CHANGE_VIDEO, SET_HISTORY } from "./actions"; + +export default (state, action) => { + switch (action.type) { + case CHANGE_VIDEO: + return { + ...state, + videoUrl: action.payload.url, + time: action.payload.time, + }; + case SET_HISTORY: + return { + ...state, + history: action.payload.history + }; + + default: + return state; + } +}; diff --git a/yarn.lock b/yarn.lock index e9d8bb3..808f340 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3783,6 +3783,11 @@ execa@^1.0.0: signal-exit "^3.0.0" strip-eof "^1.0.0" +exenv@^1.2.0: + version "1.2.2" + resolved "https://registry.yarnpkg.com/exenv/-/exenv-1.2.2.tgz#2ae78e85d9894158670b03d47bec1f03bd91bb9d" + integrity sha1-KueOhdmJQVhnCwPUe+wfA72Ru50= + exit@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" @@ -7934,7 +7939,7 @@ prompts@^2.0.1: kleur "^3.0.2" sisteransi "^1.0.0" -prop-types@^15.6.2: +prop-types@^15.5.10, prop-types@^15.6.2: version "15.7.2" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -8165,6 +8170,21 @@ react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6: resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.6.tgz#5bbc1e2d29141c9fbdfed456343fe2bc430a6a16" integrity sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA== +react-lifecycles-compat@^3.0.0: + version "3.0.4" + resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" + integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA== + +react-modal@^3.9.1: + version "3.9.1" + resolved "https://registry.yarnpkg.com/react-modal/-/react-modal-3.9.1.tgz#82ce53d110eea0d8f028f3315ef336d0baffa9b9" + integrity sha512-k+TUkhGWpIVHLsEyjNmlyOYL0Uz03fNZvlkhCImd1h+6fhNgTi6H6jexVXPVhD2LMMDzJyfugxMN+APN/em+eQ== + dependencies: + exenv "^1.2.0" + prop-types "^15.5.10" + react-lifecycles-compat "^3.0.0" + warning "^4.0.3" + react-scripts@3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/react-scripts/-/react-scripts-3.0.1.tgz#e5565350d8069cc9966b5998d3fe3befe3d243ac" @@ -9806,6 +9826,11 @@ url@^0.11.0: punycode "1.3.2" querystring "0.2.0" +use-middleware-reducer@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/use-middleware-reducer/-/use-middleware-reducer-1.2.0.tgz#4a264378ede36957d4670d18f8c79b9d2272f541" + integrity sha512-00O8EWn0FXuY12x0ul/5QVCIDiVwhkIZ4OPY1emrsh+t5AJ2Trgk0+p6PkuE9f9yh9A9sJO67sFNHFuHuetfMg== + use@^3.1.0: version "3.1.1" resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f" @@ -9946,6 +9971,13 @@ walker@^1.0.7, walker@~1.0.5: dependencies: makeerror "1.0.x" +warning@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3" + integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w== + dependencies: + loose-envify "^1.0.0" + watchpack@^1.5.0: version "1.6.0" resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00"