diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index d3f0905..9964b88 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -8,6 +8,7 @@ stages: - deploy cache: + key: "$CI_COMMIT_REF_SLUG" # use per branch caching paths: - node_modules/ @@ -76,6 +77,20 @@ package_debian: - deb/OpenMediaCenter-*.deb needs: ["build"] +electron: + stage: packaging + image: electronuserland/builder:wine + script: + - npm run buildlinux + - npm run buildwin + artifacts: + expire_in: 2 days + paths: + - dist/*.rpm + - dist/*.deb + - dist/*.exe + needs: ["build"] + deploy_test1: stage: deploy image: luki42/alpineopenssh:latest diff --git a/build.js b/build.js new file mode 100644 index 0000000..f641e9d --- /dev/null +++ b/build.js @@ -0,0 +1,21 @@ +"use strict"; + +const builder = require("electron-builder"); +const builds = {}; +process.argv.slice(1).forEach(a => { + if (a === "--linux") { + builds.linux = []; + } + if (a === "--win") { + builds.win = []; + } + if (a === "--mac") { + builds.mac = []; + + } +}); +builder.build(builds).then(e => { + console.log(e); +}).catch(e => { + console.error(e); +}); diff --git a/package.json b/package.json index 23eb3f9..1d0287a 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,12 @@ "name": "openmediacenter", "version": "0.1.2", "private": true, + "main": "public/electron.js", + "author": { + "email": "lukas.heiligenbrunner@gmail.com", + "name": "Lukas Heiligenbrunner", + "url": "https://heili.eu" + }, "dependencies": { "@fortawesome/fontawesome-svg-core": "^1.2.32", "@fortawesome/free-regular-svg-icons": "^5.15.1", @@ -11,15 +17,33 @@ "plyr-react": "^3.0.7", "react": "^17.0.1", "react-bootstrap": "^1.4.0", - "react-dom": "^17.0.1" + "react-dom": "^17.0.1", + "typescript": "^4.1.3" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test --reporters=jest-junit --reporters=default", - "testsec": "set NODE_ENV=test && jest --runInBand --ci --all --detectOpenHandles --forceExit --clearMocks --verbose", "coverage": "react-scripts test --coverage --watchAll=false", - "eject": "react-scripts eject" + "eject": "react-scripts eject", + "buildlinux": "node build.js --linux", + "buildwin": "node build.js --win", + "electron-dev": "electron ." + }, + "build": { + "appId": "com.heili.openmediacenter", + "files": [ + "build/**/*" + ], + "directories": { + "buildResources": "assets" + }, + "linux": { + "target": ["rpm","deb","snap","AppImage"] + }, + "win": { + "target": ["nsis"] + } }, "jest": { "collectCoverageFrom": [ @@ -32,7 +56,7 @@ ] }, "proxy": "http://192.168.0.209", - "homepage": "/", + "homepage": "./", "eslintConfig": { "extends": "react-app" }, @@ -49,12 +73,14 @@ ] }, "devDependencies": { - "react-scripts": "^3.4.4", "@testing-library/jest-dom": "^5.11.6", "@testing-library/react": "^11.2.2", "@testing-library/user-event": "^12.2.2", + "electron": "^11.1.0", + "electron-builder": "^22.1.0", "enzyme": "^3.11.0", "enzyme-adapter-react-16": "^1.15.5", - "jest-junit": "^12.0.0" + "jest-junit": "^12.0.0", + "react-scripts": "^3.4.4" } } diff --git a/public/electron.js b/public/electron.js new file mode 100644 index 0000000..442cced --- /dev/null +++ b/public/electron.js @@ -0,0 +1,31 @@ +const electron = require('electron'); +const app = electron.app; +const BrowserWindow = electron.BrowserWindow; + +const path = require('path'); +const url = require('url'); +const isDev = process.env.NODE_ENV === "development"; + +let mainWindow; + +function createWindow() { + mainWindow = new BrowserWindow({width: 1500, height: 880}); + mainWindow.loadURL(isDev ? 'http://localhost:3000' : `file://${path.join(__dirname, '../build/index.html')}`); + mainWindow.on('closed', () => mainWindow = null); +} + +app.on('ready', createWindow); + +console.log( process.env.NODE_ENV); + +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit(); + } +}); + +app.on('activate', () => { + if (mainWindow === null) { + createWindow(); + } +}); diff --git a/src/App.js b/src/App.js index 78a62c9..e8d7d26 100644 --- a/src/App.js +++ b/src/App.js @@ -1,7 +1,7 @@ import React from 'react'; import HomePage from './pages/HomePage/HomePage'; import RandomPage from './pages/RandomPage/RandomPage'; -import GlobalInfos from './GlobalInfos'; +import GlobalInfos from './utils/GlobalInfos'; // include bootstraps css import 'bootstrap/dist/css/bootstrap.min.css'; @@ -9,6 +9,8 @@ import style from './App.module.css'; import SettingsPage from './pages/SettingsPage/SettingsPage'; import CategoryPage from './pages/CategoryPage/CategoryPage'; +import {callAPI} from './utils/Api'; +import {NoBackendConnectionPopup} from './elements/Popups/NoBackendConnectionPopup/NoBackendConnectionPopup'; /** * The main App handles the main tabs and which content to show @@ -22,7 +24,8 @@ class App extends React.Component { page: 'default', generalSettingsLoaded: false, passwordsupport: null, - mediacentername: 'OpenMediaCenter' + mediacentername: 'OpenMediaCenter', + onapierror: false }; // bind this to the method for being able to call methods such as this.setstate @@ -33,24 +36,27 @@ class App extends React.Component { GlobalInfos.setViewBinding(this.constructViewBinding()); } + initialAPICall(){ + // this is the first api call so if it fails we know there is no connection to backend + callAPI('settings.php', {action: 'loadInitialData'}, (result) =>{ + // set theme + GlobalInfos.enableDarkTheme(result.DarkMode); + + this.setState({ + generalSettingsLoaded: true, + passwordsupport: result.passwordEnabled, + mediacentername: result.mediacenter_name, + onapierror: false + }); + // set tab title to received mediacenter name + document.title = result.mediacenter_name; + }, error => { + this.setState({onapierror: true}); + }); + } + componentDidMount() { - const updateRequest = new FormData(); - updateRequest.append('action', 'loadInitialData'); - - fetch('/api/settings.php', {method: 'POST', body: updateRequest}) - .then((response) => response.json() - .then((result) => { - // set theme - GlobalInfos.enableDarkTheme(result.DarkMode); - - this.setState({ - generalSettingsLoaded: true, - passwordsupport: result.passwordEnabled, - mediacentername: result.mediacenter_name - }); - // set tab title to received mediacenter name - document.title = result.mediacenter_name; - })); + this.initialAPICall(); } /** @@ -119,6 +125,7 @@ class App extends React.Component { {this.state.generalSettingsLoaded ? this.MainBody() : 'loading'} + {this.state.onapierror ? this.ApiError() : null} ); } @@ -142,6 +149,11 @@ class App extends React.Component { page: 'lastpage' }); } + + ApiError() { + // on api error show popup and retry and show again if failing.. + return ( this.initialAPICall()}/>); + } } export default App; diff --git a/src/elements/ActorTile/ActorTile.js b/src/elements/ActorTile/ActorTile.js index 383ad35..f124327 100644 --- a/src/elements/ActorTile/ActorTile.js +++ b/src/elements/ActorTile/ActorTile.js @@ -2,7 +2,7 @@ import style from './ActorTile.module.css'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import {faUser} from '@fortawesome/free-solid-svg-icons'; import React from 'react'; -import GlobalInfos from '../../GlobalInfos'; +import GlobalInfos from '../../utils/GlobalInfos'; import ActorPage from '../../pages/ActorPage/ActorPage'; class ActorTile extends React.Component { diff --git a/src/elements/ActorTile/ActorTile.test.js b/src/elements/ActorTile/ActorTile.test.js index e2c5ab3..cfbf37c 100644 --- a/src/elements/ActorTile/ActorTile.test.js +++ b/src/elements/ActorTile/ActorTile.test.js @@ -18,4 +18,17 @@ describe('', function () { expect(func).toBeCalledTimes(1); }); + + it('simulate click with custom handler', function () { + const func = jest.fn((_) => {}); + const wrapper = shallow( func()}/>); + + const func1 = jest.fn(); + prepareViewBinding(func1); + + wrapper.simulate('click'); + + expect(func1).toBeCalledTimes(0); + expect(func).toBeCalledTimes(1); + }); }); diff --git a/src/elements/PageTitle/PageTitle.js b/src/elements/PageTitle/PageTitle.js index a65f893..bddb0b6 100644 --- a/src/elements/PageTitle/PageTitle.js +++ b/src/elements/PageTitle/PageTitle.js @@ -1,6 +1,6 @@ import React from 'react'; import style from './PageTitle.module.css'; -import GlobalInfos from '../../GlobalInfos'; +import GlobalInfos from '../../utils/GlobalInfos'; /** * Component for generating PageTitle with bottom Line diff --git a/src/elements/PageTitle/PageTitle.test.js b/src/elements/PageTitle/PageTitle.test.js index 5b2e22c..71b9a5e 100644 --- a/src/elements/PageTitle/PageTitle.test.js +++ b/src/elements/PageTitle/PageTitle.test.js @@ -1,7 +1,7 @@ import React from 'react'; import {shallow} from 'enzyme'; -import PageTitle from './PageTitle'; +import PageTitle, {Line} from './PageTitle'; describe('', function () { it('renders without crashing ', function () { @@ -29,3 +29,10 @@ describe('', function () { }); }); +describe('', () => { + it('renders without crashing', function () { + const wrapper = shallow(); + wrapper.unmount(); + }); +}); + diff --git a/src/elements/Popups/AddActorPopup/AddActorPopup.js b/src/elements/Popups/AddActorPopup/AddActorPopup.js index 77d5e95..bd88790 100644 --- a/src/elements/Popups/AddActorPopup/AddActorPopup.js +++ b/src/elements/Popups/AddActorPopup/AddActorPopup.js @@ -3,6 +3,7 @@ import React from 'react'; import ActorTile from '../../ActorTile/ActorTile'; import style from './AddActorPopup.module.css'; import {NewActorPopupContent} from '../NewActorPopup/NewActorPopup'; +import {callAPI} from '../../../utils/Api'; /** * Popup for Adding a new Actor to a Video @@ -69,33 +70,21 @@ class AddActorPopup extends React.Component { */ tileClickHandler(actorid) { // fetch the available actors - const req = new FormData(); - req.append('action', 'addActorToVideo'); - req.append('actorid', actorid); - req.append('videoid', this.props.movie_id); - - fetch('/api/actor.php', {method: 'POST', body: req}) - .then((response) => response.json() - .then((result) => { - if (result.result === 'success') { - // return back to player page - this.props.onHide(); - } else { - console.error('an error occured while fetching actors'); - console.error(result); - } - })); + callAPI('actor.php', {action: 'addActorToVideo', actorid: actorid, videoid: this.props.movie_id}, result => { + if (result.result === 'success') { + // return back to player page + this.props.onHide(); + } else { + console.error('an error occured while fetching actors'); + console.error(result); + } + }); } loadActors() { - const req = new FormData(); - req.append('action', 'getAllActors'); - - fetch('/api/actor.php', {method: 'POST', body: req}) - .then((response) => response.json() - .then((result) => { - this.setState({actors: result}); - })); + callAPI('actor.php', {action: 'getAllActors'}, result => { + this.setState({actors: result}); + }); } } diff --git a/src/elements/Popups/AddActorPopup/AddActorPopup.test.js b/src/elements/Popups/AddActorPopup/AddActorPopup.test.js index f5294ad..f4ef4a9 100644 --- a/src/elements/Popups/AddActorPopup/AddActorPopup.test.js +++ b/src/elements/Popups/AddActorPopup/AddActorPopup.test.js @@ -1,6 +1,7 @@ import {shallow} from 'enzyme'; import React from 'react'; import AddActorPopup from './AddActorPopup'; +import {callAPI} from '../../../utils/Api'; describe('', function () { it('renders without crashing ', function () { @@ -8,12 +9,63 @@ describe('', function () { wrapper.unmount(); }); - // it('simulate change to other page', function () { - // const wrapper = shallow(); - // - // console.log(wrapper.find('PopupBase').dive().debug()); - // - // - // console.log(wrapper.debug()); - // }); + it('simulate change to other page', function () { + const wrapper = shallow(); + + expect(wrapper.find('NewActorPopupContent')).toHaveLength(0); + wrapper.find('PopupBase').props().banner.props.onClick(); + + // check if new content is showing + expect(wrapper.find('NewActorPopupContent')).toHaveLength(1); + }); + + it('hide new actor page', function () { + const wrapper = shallow(); + wrapper.find('PopupBase').props().banner.props.onClick(); + + // call onhide event listener manually + wrapper.find('NewActorPopupContent').props().onHide(); + + // expect other page to be hidden again + expect(wrapper.find('NewActorPopupContent')).toHaveLength(0); + }); + + it('test api call and insertion of actor tiles', function () { + global.callAPIMock([{id: 1, actorname: 'test'}, {id: 2, actorname: 'test2'}]); + + const wrapper = shallow(); + + expect(wrapper.find('ActorTile')).toHaveLength(2); + }); + + it('simulate actortile click', function () { + const func = jest.fn(); + const wrapper = shallow( {func()}}/>); + + global.callAPIMock({result: 'success'}); + + wrapper.setState({actors: [{id: 1, actorname: 'test'}]}, () => { + wrapper.find('ActorTile').props().onClick(); + + expect(callAPI).toHaveBeenCalledTimes(1); + + expect(func).toHaveBeenCalledTimes(1); + }); + }); + + it('test failing actortile click', function () { + const func = jest.fn(); + const wrapper = shallow( {func()}}/>); + + global.callAPIMock({result: 'nosuccess'}); + + wrapper.setState({actors: [{id: 1, actorname: 'test'}]}, () => { + wrapper.find('ActorTile').props().onClick(); + + expect(callAPI).toHaveBeenCalledTimes(1); + + // hide funtion should not have been called on error! + expect(func).toHaveBeenCalledTimes(0); + }); + }); }); diff --git a/src/elements/Popups/AddTagPopup/AddTagPopup.js b/src/elements/Popups/AddTagPopup/AddTagPopup.js index 9eb8984..09800e7 100644 --- a/src/elements/Popups/AddTagPopup/AddTagPopup.js +++ b/src/elements/Popups/AddTagPopup/AddTagPopup.js @@ -1,6 +1,7 @@ import React from 'react'; import Tag from '../../Tag/Tag'; import PopupBase from '../PopupBase'; +import {callAPI} from '../../../utils/Api'; /** * component creates overlay to add a new tag to a video @@ -13,16 +14,11 @@ class AddTagPopup extends React.Component { } componentDidMount() { - const updateRequest = new FormData(); - updateRequest.append('action', 'getAllTags'); - - fetch('/api/tags.php', {method: 'POST', body: updateRequest}) - .then((response) => response.json()) - .then((result) => { - this.setState({ - items: result - }); + callAPI('tags.php', {action: 'getAllTags'}, (result) => { + this.setState({ + items: result }); + }); } render() { @@ -44,23 +40,15 @@ class AddTagPopup extends React.Component { * @param tagname tag name to add */ addTag(tagid, tagname) { - console.log(this.props); - const updateRequest = new FormData(); - updateRequest.append('action', 'addTag'); - updateRequest.append('id', tagid); - updateRequest.append('movieid', this.props.movie_id); - - fetch('/api/tags.php', {method: 'POST', body: updateRequest}) - .then((response) => response.json() - .then((result) => { - if (result.result !== 'success') { - console.log('error occured while writing to db -- todo error handling'); - console.log(result.result); - } else { - this.props.submit(tagid, tagname); - } - this.props.onHide(); - })); + callAPI('tags.php', {action: 'addTag', id: tagid, movieid: this.props.movie_id}, result => { + if (result.result !== 'success') { + console.log('error occured while writing to db -- todo error handling'); + console.log(result.result); + } else { + this.props.submit(tagid, tagname); + } + this.props.onHide(); + }); } } diff --git a/src/elements/Popups/NewActorPopup/NewActorPopup.js b/src/elements/Popups/NewActorPopup/NewActorPopup.js index 8e0a85e..35ad563 100644 --- a/src/elements/Popups/NewActorPopup/NewActorPopup.js +++ b/src/elements/Popups/NewActorPopup/NewActorPopup.js @@ -1,6 +1,7 @@ import React from 'react'; import PopupBase from '../PopupBase'; import style from './NewActorPopup.module.css'; +import {callAPI} from '../../../utils/Api'; /** * creates modal overlay to define a new Tag @@ -41,19 +42,13 @@ export class NewActorPopupContent extends React.Component { // check if user typed in name if (this.value === '' || this.value === undefined) return; - const req = new FormData(); - req.append('action', 'createActor'); - req.append('actorname', this.value); - - fetch('/api/actor.php', {method: 'POST', body: req}) - .then((response) => response.json()) - .then((result) => { - if (result.result !== 'success') { - console.log('error occured while writing to db -- todo error handling'); - console.log(result.result); - } - this.props.onHide(); - }); + callAPI('actor.php', {action: 'createActor', actorname: this.value}, (result) => { + if (result.result !== 'success') { + console.log('error occured while writing to db -- todo error handling'); + console.log(result.result); + } + this.props.onHide(); + }); } } diff --git a/src/elements/Popups/NewActorPopup/NewActorPopup.test.js b/src/elements/Popups/NewActorPopup/NewActorPopup.test.js index 8e6d461..a277421 100644 --- a/src/elements/Popups/NewActorPopup/NewActorPopup.test.js +++ b/src/elements/Popups/NewActorPopup/NewActorPopup.test.js @@ -3,6 +3,7 @@ import React from 'react'; import {shallow} from 'enzyme'; import '@testing-library/jest-dom'; import NewActorPopup, {NewActorPopupContent} from './NewActorPopup'; +import {callAPI} from '../../../utils/Api'; describe('', function () { it('renders without crashing ', function () { @@ -18,18 +19,23 @@ describe('', () => { }); it('simulate button click', function () { - const wrapper = shallow(); + global.callAPIMock({}); + + const func = jest.fn(); + const wrapper = shallow( {func()}}/>); // manually set typed in actorname wrapper.instance().value = 'testactorname'; global.fetch = prepareFetchApi({}); - expect(global.fetch).toBeCalledTimes(0); + expect(callAPI).toBeCalledTimes(0); wrapper.find('button').simulate('click'); // fetch should have been called once now - expect(global.fetch).toBeCalledTimes(1); + expect(callAPI).toBeCalledTimes(1); + + expect(func).toHaveBeenCalledTimes(1); }); it('test not allowing request if textfield is empty', function () { diff --git a/src/elements/Popups/NewTagPopup/NewTagPopup.js b/src/elements/Popups/NewTagPopup/NewTagPopup.js index 9d0287c..dd5f548 100644 --- a/src/elements/Popups/NewTagPopup/NewTagPopup.js +++ b/src/elements/Popups/NewTagPopup/NewTagPopup.js @@ -1,6 +1,7 @@ import React from 'react'; import PopupBase from '../PopupBase'; import style from './NewTagPopup.module.css'; +import {callAPI} from '../../../utils/Api'; /** * creates modal overlay to define a new Tag @@ -27,19 +28,13 @@ class NewTagPopup extends React.Component { * store the filled in form to the backend */ storeselection() { - const updateRequest = new FormData(); - updateRequest.append('action', 'createTag'); - updateRequest.append('tagname', this.value); - - fetch('/api/tags.php', {method: 'POST', body: updateRequest}) - .then((response) => response.json()) - .then((result) => { - if (result.result !== 'success') { - console.log('error occured while writing to db -- todo error handling'); - console.log(result.result); - } - this.props.onHide(); - }); + callAPI('tags.php', {action: 'createTag', tagname: this.value}, result => { + if (result.result !== 'success') { + console.log('error occured while writing to db -- todo error handling'); + console.log(result.result); + } + this.props.onHide(); + }); } } diff --git a/src/elements/Popups/NewTagPopup/NewTagPopup.test.js b/src/elements/Popups/NewTagPopup/NewTagPopup.test.js index a996fa6..7c0d66a 100644 --- a/src/elements/Popups/NewTagPopup/NewTagPopup.test.js +++ b/src/elements/Popups/NewTagPopup/NewTagPopup.test.js @@ -3,6 +3,8 @@ import React from 'react'; import {shallow} from 'enzyme'; import '@testing-library/jest-dom'; import NewTagPopup from './NewTagPopup'; +import {NoBackendConnectionPopup} from '../NoBackendConnectionPopup/NoBackendConnectionPopup'; +import {getBackendDomain} from '../../../utils/Api'; describe('', function () { it('renders without crashing ', function () { @@ -33,4 +35,12 @@ describe('', function () { done(); }); }); + + it('simulate textfield change', function () { + const wrapper = shallow(); + + wrapper.find('input').simulate('change', {target: {value: 'testvalue'}}); + + expect(wrapper.instance().value).toBe('testvalue'); + }); }); diff --git a/src/elements/Popups/NoBackendConnectionPopup/NoBackendConnectionPopup.test.js b/src/elements/Popups/NoBackendConnectionPopup/NoBackendConnectionPopup.test.js new file mode 100644 index 0000000..6d3bd98 --- /dev/null +++ b/src/elements/Popups/NoBackendConnectionPopup/NoBackendConnectionPopup.test.js @@ -0,0 +1,29 @@ +import {shallow} from 'enzyme'; +import React from 'react'; +import {NoBackendConnectionPopup} from './NoBackendConnectionPopup'; +import {getBackendDomain} from '../../../utils/Api'; + +describe('', function () { + it('renders without crashing ', function () { + const wrapper = shallow( {}}/>); + wrapper.unmount(); + }); + + it('hides on refresh click', function () { + const func = jest.fn(); + const wrapper = shallow(); + + expect(func).toBeCalledTimes(0); + wrapper.find('button').simulate('click'); + + expect(func).toBeCalledTimes(1); + }); + + it('simulate change of textfield', function () { + const wrapper = shallow( {}}/>); + + wrapper.find('input').simulate('change', {target: {value: 'testvalue'}}); + + expect(getBackendDomain()).toBe('testvalue'); + }); +}); diff --git a/src/elements/Popups/NoBackendConnectionPopup/NoBackendConnectionPopup.tsx b/src/elements/Popups/NoBackendConnectionPopup/NoBackendConnectionPopup.tsx new file mode 100644 index 0000000..ef2670e --- /dev/null +++ b/src/elements/Popups/NoBackendConnectionPopup/NoBackendConnectionPopup.tsx @@ -0,0 +1,20 @@ +import React from "react"; +import PopupBase from "../PopupBase"; +import style from "../NewActorPopup/NewActorPopup.module.css"; +import {setCustomBackendDomain} from "../../../utils/Api"; + +interface NBCProps { + onHide: (_: void) => void +} + +export function NoBackendConnectionPopup(props: NBCProps): JSX.Element { + return ( + +
+ { + setCustomBackendDomain(v.target.value); + }}/>
+ +
+ ); +} diff --git a/src/elements/Popups/PopupBase.js b/src/elements/Popups/PopupBase.js index e720502..dc3fa2f 100644 --- a/src/elements/Popups/PopupBase.js +++ b/src/elements/Popups/PopupBase.js @@ -1,4 +1,4 @@ -import GlobalInfos from '../../GlobalInfos'; +import GlobalInfos from '../../utils/GlobalInfos'; import style from './PopupBase.module.css'; import {Line} from '../PageTitle/PageTitle'; import React from 'react'; @@ -20,7 +20,8 @@ class PopupBase extends React.Component { // parse style props this.framedimensions = { width: (this.props.width ? this.props.width : undefined), - height: (this.props.height ? this.props.height : undefined) + height: (this.props.height ? this.props.height : undefined), + minHeight: (this.props.height ? this.props.height : undefined) }; } diff --git a/src/elements/Popups/PopupBase.module.css b/src/elements/Popups/PopupBase.module.css index d2335d3..6105c65 100644 --- a/src/elements/Popups/PopupBase.module.css +++ b/src/elements/Popups/PopupBase.module.css @@ -1,7 +1,8 @@ .popup { border: 3px #3574fe solid; border-radius: 18px; - height: 80%; + min-height: 80%; + height: fit-content; left: 20%; opacity: 0.95; position: absolute; @@ -40,4 +41,5 @@ margin-right: 20px; margin-top: 10px; opacity: 1; + overflow: auto; } diff --git a/src/elements/Popups/PopupBase.test.js b/src/elements/Popups/PopupBase.test.js index 9e87994..7430a7c 100644 --- a/src/elements/Popups/PopupBase.test.js +++ b/src/elements/Popups/PopupBase.test.js @@ -8,5 +8,19 @@ describe('', function () { wrapper.unmount(); }); + it('simulate keypress', function () { + let events = []; + document.addEventListener = jest.fn((event, cb) => { + events[event] = cb; + }); + + const func = jest.fn(); + shallow( func()}/>); + + // trigger the keypress event + events.keyup({key: 'Escape'}); + + expect(func).toBeCalledTimes(1); + }); }); diff --git a/src/elements/Preview/Preview.js b/src/elements/Preview/Preview.js index 0e58509..ae4f528 100644 --- a/src/elements/Preview/Preview.js +++ b/src/elements/Preview/Preview.js @@ -2,7 +2,8 @@ import React from 'react'; import style from './Preview.module.css'; import Player from '../../pages/Player/Player'; import {Spinner} from 'react-bootstrap'; -import GlobalInfos from '../../GlobalInfos'; +import GlobalInfos from '../../utils/GlobalInfos'; +import {callAPIPlain} from '../../utils/Api'; /** * Component for single preview tile @@ -19,22 +20,12 @@ class Preview extends React.Component { } componentDidMount() { - this.setState({ - previewpicture: null, - name: this.props.name + callAPIPlain('video.php', {action: 'readThumbnail', movieid: this.props.movie_id}, (result) => { + this.setState({ + previewpicture: result, + name: this.props.name + }); }); - - const updateRequest = new FormData(); - updateRequest.append('action', 'readThumbnail'); - updateRequest.append('movieid', this.props.movie_id); - - fetch('/api/video.php', {method: 'POST', body: updateRequest}) - .then((response) => response.text() - .then((result) => { - this.setState({ - previewpicture: result - }); - })); } render() { diff --git a/src/elements/Preview/Previw.test.js b/src/elements/Preview/Previw.test.js index 407dcb8..b661816 100644 --- a/src/elements/Preview/Previw.test.js +++ b/src/elements/Preview/Previw.test.js @@ -9,12 +9,6 @@ describe('', function () { wrapper.unmount(); }); - // check if preview title renders correctly - it('renders title', () => { - const wrapper = shallow(); - expect(wrapper.find('.previewtitle').text()).toBe('test'); - }); - it('click event triggered', () => { const func = jest.fn(); @@ -36,7 +30,7 @@ describe('', function () { }); global.fetch = jest.fn().mockImplementation(() => mockFetchPromise); - const wrapper = shallow(); + const wrapper = shallow(); // now called 1 times expect(global.fetch).toHaveBeenCalledTimes(1); @@ -44,6 +38,8 @@ describe('', function () { process.nextTick(() => { // received picture should be rendered into wrapper expect(wrapper.find('.previewimage').props().src).not.toBeNull(); + // check if preview title renders correctly + expect(wrapper.find('.previewtitle').text()).toBe('test'); global.fetch.mockClear(); done(); diff --git a/src/elements/SideBar/SideBar.js b/src/elements/SideBar/SideBar.js index 8f13fd3..7e24773 100644 --- a/src/elements/SideBar/SideBar.js +++ b/src/elements/SideBar/SideBar.js @@ -1,6 +1,6 @@ import React from 'react'; import style from './SideBar.module.css'; -import GlobalInfos from '../../GlobalInfos'; +import GlobalInfos from '../../utils/GlobalInfos'; /** * component for sidebar-info diff --git a/src/elements/Tag/Tag.js b/src/elements/Tag/Tag.js index c8e4201..c29cf74 100644 --- a/src/elements/Tag/Tag.js +++ b/src/elements/Tag/Tag.js @@ -2,7 +2,7 @@ import React from 'react'; import styles from './Tag.module.css'; import CategoryPage from '../../pages/CategoryPage/CategoryPage'; -import GlobalInfos from '../../GlobalInfos'; +import GlobalInfos from '../../utils/GlobalInfos'; /** * A Component representing a single Category tag diff --git a/src/index.js b/src/index.js index 153a539..17544dd 100644 --- a/src/index.js +++ b/src/index.js @@ -2,7 +2,6 @@ import React from 'react'; import ReactDOM from 'react-dom'; import App from './App'; -// don't allow console logs within production env // don't allow console logs within production env global.console.log = process.env.NODE_ENV !== "development" ? (s) => {} : global.console.log; diff --git a/src/pages/ActorPage/ActorPage.js b/src/pages/ActorPage/ActorPage.js index 8aa825f..6c4e49f 100644 --- a/src/pages/ActorPage/ActorPage.js +++ b/src/pages/ActorPage/ActorPage.js @@ -5,6 +5,7 @@ import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import {faUser} from '@fortawesome/free-solid-svg-icons'; import style from './ActorPage.module.css'; import VideoContainer from '../../elements/VideoContainer/VideoContainer'; +import {callAPI} from '../../utils/Api'; class ActorPage extends React.Component { constructor(props) { @@ -40,17 +41,10 @@ class ActorPage extends React.Component { */ getActorInfo() { // todo 2020-12-4: fetch to db - - const req = new FormData(); - req.append('action', 'getActorInfo'); - req.append('actorid', this.props.actor.actor_id); - - fetch('/api/actor.php', {method: 'POST', body: req}) - .then((response) => response.json() - .then((result) => { - console.log(result); - this.setState({data: result.videos ? result.videos : []}); - })); + callAPI('actor.php', {action: 'getActorInfo', actorid: this.props.actor.actor_id}, result => { + console.log(result); + this.setState({data: result.videos ? result.videos : []}); + }); } } diff --git a/src/pages/CategoryPage/CategoryPage.js b/src/pages/CategoryPage/CategoryPage.js index de320f2..52b135c 100644 --- a/src/pages/CategoryPage/CategoryPage.js +++ b/src/pages/CategoryPage/CategoryPage.js @@ -7,6 +7,7 @@ import {TagPreview} from '../../elements/Preview/Preview'; import NewTagPopup from '../../elements/Popups/NewTagPopup/NewTagPopup'; import PageTitle, {Line} from '../../elements/PageTitle/PageTitle'; import VideoContainer from '../../elements/VideoContainer/VideoContainer'; +import {callAPI} from '../../utils/Api'; /** * Component for Category Page @@ -111,24 +112,11 @@ class CategoryPage extends React.Component { * @param tag tagname */ fetchVideoData(tag) { - console.log(tag); - const updateRequest = new FormData(); - updateRequest.append('action', 'getMovies'); - updateRequest.append('tag', tag); - - console.log('fetching data'); - - // fetch all videos available - fetch('/api/video.php', {method: 'POST', body: updateRequest}) - .then((response) => response.json() - .then((result) => { - this.videodata = result; - this.setState({selected: null}); // needed to trigger the state reload correctly - this.setState({selected: tag}); - })) - .catch(() => { - console.log('no connection to backend'); - }); + callAPI('video.php', {action: 'getMovies', tag: tag}, result => { + this.videodata = result; + this.setState({selected: null}); // needed to trigger the state reload correctly + this.setState({selected: tag}); + }); } /** @@ -143,18 +131,9 @@ class CategoryPage extends React.Component { * load all available tags from db. */ loadTags() { - const updateRequest = new FormData(); - updateRequest.append('action', 'getAllTags'); - - // fetch all videos available - fetch('/api/tags.php', {method: 'POST', body: updateRequest}) - .then((response) => response.json() - .then((result) => { - this.setState({loadedtags: result}); - })) - .catch(() => { - console.log('no connection to backend'); - }); + callAPI('tags.php', {action: 'getAllTags'}, result => { + this.setState({loadedtags: result}); + }); } } diff --git a/src/pages/CategoryPage/CategoryPage.test.js b/src/pages/CategoryPage/CategoryPage.test.js index e824f50..f559756 100644 --- a/src/pages/CategoryPage/CategoryPage.test.js +++ b/src/pages/CategoryPage/CategoryPage.test.js @@ -24,27 +24,6 @@ describe('', function () { }); }); - it('test errored fetch call', done => { - global.fetch = global.prepareFetchApi({}); - - let message; - global.console.log = jest.fn((m) => { - message = m; - }); - - shallow(); - - expect(global.fetch).toHaveBeenCalledTimes(1); - - process.nextTick(() => { - //callback to close window should have called - expect(message).toBe('no connection to backend'); - - global.fetch.mockClear(); - done(); - }); - }); - it('test new tag popup', function () { const wrapper = shallow(); diff --git a/src/pages/HomePage/HomePage.js b/src/pages/HomePage/HomePage.js index fd3dcab..f700824 100644 --- a/src/pages/HomePage/HomePage.js +++ b/src/pages/HomePage/HomePage.js @@ -5,6 +5,7 @@ import VideoContainer from '../../elements/VideoContainer/VideoContainer'; import style from './HomePage.module.css'; import PageTitle, {Line} from '../../elements/PageTitle/PageTitle'; +import {callAPI} from '../../utils/Api'; /** * The home page component showing on the initial pageload @@ -43,54 +44,33 @@ class HomePage extends React.Component { * @param tag tag to fetch videos */ fetchVideoData(tag) { - const updateRequest = new FormData(); - updateRequest.append('action', 'getMovies'); - updateRequest.append('tag', tag); - - console.log('fetching data from' + tag); - - // fetch all videos available - fetch('/api/video.php', {method: 'POST', body: updateRequest}) - .then((response) => response.json() - .then((result) => { - this.setState({ - data: [] - }); - this.setState({ - data: result, - selectionnr: result.length, - tag: tag + ' Videos' - }); - })) - .catch(() => { - console.log('no connection to backend'); + callAPI('video.php', {action: 'getMovies', tag: tag}, (result) => { + this.setState({ + data: [] }); + this.setState({ + data: result, + selectionnr: result.length, + tag: tag + ' Videos' + }); + }); } /** * fetch the necessary data for left info box */ fetchStartData() { - const updateRequest = new FormData(); - updateRequest.append('action', 'getStartData'); - - // fetch all videos available - fetch('/api/video.php', {method: 'POST', body: updateRequest}) - .then((response) => response.json() - .then((result) => { - this.setState({ - sideinfo: { - videonr: result['total'], - fullhdvideonr: result['fullhd'], - hdvideonr: result['hd'], - sdvideonr: result['sd'], - tagnr: result['tags'] - } - }); - })) - .catch(() => { - console.log('no connection to backend'); + callAPI('video.php', {action: 'getStartData'}, (result) => { + this.setState({ + sideinfo: { + videonr: result['total'], + fullhdvideonr: result['fullhd'], + hdvideonr: result['hd'], + sdvideonr: result['sd'], + tagnr: result['tags'] + } }); + }); } /** @@ -101,26 +81,16 @@ class HomePage extends React.Component { searchVideos(keyword) { console.log('search called'); - const updateRequest = new FormData(); - updateRequest.append('action', 'getSearchKeyWord'); - updateRequest.append('keyword', keyword); - - // fetch all videos available - fetch('/api/video.php', {method: 'POST', body: updateRequest}) - .then((response) => response.json() - .then((result) => { - this.setState({ - data: [] - }); - this.setState({ - data: result, - selectionnr: result.length, - tag: 'Search result: ' + keyword - }); - })) - .catch(() => { - console.log('no connection to backend'); + callAPI('video.php', {action: 'getSearchKeyWord', keyword: keyword}, (result) => { + this.setState({ + data: [] }); + this.setState({ + data: result, + selectionnr: result.length, + tag: 'Search result: ' + keyword + }); + }); } render() { diff --git a/src/pages/Player/Player.js b/src/pages/Player/Player.js index 2a53eaf..b59da03 100644 --- a/src/pages/Player/Player.js +++ b/src/pages/Player/Player.js @@ -1,7 +1,7 @@ import React from 'react'; import style from './Player.module.css'; -import plyrstyle from 'plyr-react/dist/plyr.css' +import plyrstyle from 'plyr-react/dist/plyr.css'; import {Plyr} from 'plyr-react'; import SideBar, {SideBarItem, SideBarTitle} from '../../elements/SideBar/SideBar'; @@ -12,7 +12,8 @@ import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import {faPlusCircle} from '@fortawesome/free-solid-svg-icons'; import AddActorPopup from '../../elements/Popups/AddActorPopup/AddActorPopup'; import ActorTile from '../../elements/ActorTile/ActorTile'; -import GlobalInfos from '../../GlobalInfos'; +import GlobalInfos from '../../utils/GlobalInfos'; +import {callAPI, getBackendDomain} from '../../utils/Api'; /** @@ -67,47 +68,40 @@ class Player extends React.Component { * @param tagName name of tag to add */ quickAddTag(tagId, tagName) { - const updateRequest = new FormData(); - updateRequest.append('action', 'addTag'); - updateRequest.append('id', tagId); - updateRequest.append('movieid', this.props.movie_id); + callAPI('tags.php', {action: 'addTag', id: tagId, movieid: this.props.movie_id}, (result) => { + if (result.result !== 'success') { + console.error('error occured while writing to db -- todo error handling'); + console.error(result.result); + } else { + // check if tag has already been added + const tagIndex = this.state.tags.map(function (e) { + return e.tag_name; + }).indexOf(tagName); - fetch('/api/tags.php', {method: 'POST', body: updateRequest}) - .then((response) => response.json() - .then((result) => { - if (result.result !== 'success') { - console.error('error occured while writing to db -- todo error handling'); - console.error(result.result); + // only add tag if it isn't already there + if (tagIndex === -1) { + // update tags if successful + let array = [...this.state.suggesttag]; // make a separate copy of the array (because of setState) + const quickaddindex = this.state.suggesttag.map(function (e) { + return e.tag_id; + }).indexOf(tagId); + + // check if tag is available in quickadds + if (quickaddindex !== -1) { + array.splice(quickaddindex, 1); + + this.setState({ + tags: [...this.state.tags, {tag_name: tagName}], + suggesttag: array + }); } else { - // check if tag has already been added - const tagIndex = this.state.tags.map(function (e) { - return e.tag_name; - }).indexOf(tagName); - - // only add tag if it isn't already there - if (tagIndex === -1) { - // update tags if successful - let array = [...this.state.suggesttag]; // make a separate copy of the array (because of setState) - const quickaddindex = this.state.suggesttag.map(function (e) { - return e.tag_id; - }).indexOf(tagId); - - // check if tag is available in quickadds - if (quickaddindex !== -1) { - array.splice(quickaddindex, 1); - - this.setState({ - tags: [...this.state.tags, {tag_name: tagName}], - suggesttag: array - }); - } else { - this.setState({ - tags: [...this.state.tags, {tag_name: tagName}] - }); - } - } + this.setState({ + tags: [...this.state.tags, {tag_name: tagName}] + }); } - })); + } + } + }); } /** @@ -179,7 +173,7 @@ class Player extends React.Component {
{/* video component is added here */} {this.state.sources ? :
not loaded yet
} @@ -227,36 +221,30 @@ class Player extends React.Component { * fetch all the required infos of a video from backend */ fetchMovieData() { - const updateRequest = new FormData(); - updateRequest.append('action', 'loadVideo'); - updateRequest.append('movieid', this.props.movie_id); - - fetch('/api/video.php', {method: 'POST', body: updateRequest}) - .then((response) => response.json()) - .then((result) => { - this.setState({ - sources: { - type: 'video', - sources: [ - { - src: result.movie_url, - type: 'video/mp4', - size: 1080 - } - ], - poster: result.thumbnail - }, - movie_id: result.movie_id, - movie_name: result.movie_name, - likes: result.likes, - quality: result.quality, - length: result.length, - tags: result.tags, - suggesttag: result.suggesttag, - actors: result.actors - }); - console.log(this.state); + callAPI('video.php', {action: 'loadVideo', movieid: this.props.movie_id}, result => { + this.setState({ + sources: { + type: 'video', + sources: [ + { + src: getBackendDomain() + result.movie_url, + type: 'video/mp4', + size: 1080 + } + ], + poster: result.thumbnail + }, + movie_id: result.movie_id, + movie_name: result.movie_name, + likes: result.likes, + quality: result.quality, + length: result.length, + tags: result.tags, + suggesttag: result.suggesttag, + actors: result.actors }); + console.log(this.state); + }); } @@ -264,21 +252,15 @@ class Player extends React.Component { * click handler for the like btn */ likebtn() { - const updateRequest = new FormData(); - updateRequest.append('action', 'addLike'); - updateRequest.append('movieid', this.props.movie_id); - - fetch('/api/video.php', {method: 'POST', body: updateRequest}) - .then((response) => response.json() - .then((result) => { - if (result.result === 'success') { - // likes +1 --> avoid reload of all data - this.setState({likes: this.state.likes + 1}); - } else { - console.error('an error occured while liking'); - console.error(result); - } - })); + callAPI('video.php', {action: 'addLike', movieid: this.props.movie_id}, result => { + if (result.result === 'success') { + // likes +1 --> avoid reload of all data + this.setState({likes: this.state.likes + 1}); + } else { + console.error('an error occured while liking'); + console.error(result); + } + }); } /** @@ -293,21 +275,15 @@ class Player extends React.Component { * delete the current video and return to last page */ deleteVideo() { - const updateRequest = new FormData(); - updateRequest.append('action', 'deleteVideo'); - updateRequest.append('movieid', this.props.movie_id); - - fetch('/api/video.php', {method: 'POST', body: updateRequest}) - .then((response) => response.json() - .then((result) => { - if (result.result === 'success') { - // return to last element if successful - GlobalInfos.getViewBinding().returnToLastElement(); - } else { - console.error('an error occured while liking'); - console.error(result); - } - })); + callAPI('video.php', {action: 'deleteVideo', movieid: this.props.movie_id}, result => { + if (result.result === 'success') { + // return to last element if successful + GlobalInfos.getViewBinding().returnToLastElement(); + } else { + console.error('an error occured while liking'); + console.error(result); + } + }); } /** @@ -318,17 +294,9 @@ class Player extends React.Component { } refetchActors() { - const req = new FormData(); - req.append('action', 'getActorsOfVideo'); - req.append('videoid', this.props.movie_id); - - console.log('refrething actors'); - - fetch('/api/actor.php', {method: 'POST', body: req}) - .then((response) => response.json() - .then((result) => { - this.setState({actors: result}); - })); + callAPI('actor.php', {action: 'getActorsOfVideo', videoid: this.props.movie_id}, result => { + this.setState({actors: result}); + }); } } diff --git a/src/pages/Player/Player.test.js b/src/pages/Player/Player.test.js index 135e3b8..bb20753 100644 --- a/src/pages/Player/Player.test.js +++ b/src/pages/Player/Player.test.js @@ -1,6 +1,7 @@ import {shallow} from 'enzyme'; import React from 'react'; import Player from './Player'; +import {callAPI} from '../../utils/Api'; describe('', function () { it('renders without crashing ', function () { @@ -81,7 +82,7 @@ describe('', function () { const wrapper = shallow(); const func = jest.fn(); - prepareViewBinding(func) + prepareViewBinding(func); global.fetch = prepareFetchApi({result: 'success'}); @@ -163,6 +164,58 @@ describe('', function () { }); }); + it('showspopups correctly', function () { + const wrapper = shallow(); + + wrapper.setState({popupvisible: true}, () => { + // is the AddTagpopu rendered? + expect(wrapper.find('AddTagPopup')).toHaveLength(1); + wrapper.setState({popupvisible: false, actorpopupvisible: true}, () => { + // actorpopup rendred and tagpopup hidden? + expect(wrapper.find('AddTagPopup')).toHaveLength(0); + expect(wrapper.find('AddActorPopup')).toHaveLength(1); + }); + }); + }); + + + it('quickadd tag correctly', function () { + const wrapper = shallow(); + global.callAPIMock({result: 'success'}); + + wrapper.setState({suggesttag: [{tag_name: 'test', tag_id: 1}]}, () => { + // mock funtion should have not been called + expect(callAPI).toBeCalledTimes(0); + wrapper.find('Tag').findWhere(p => p.text() === 'test').parent().dive().simulate('click'); + // mock function should have been called once + expect(callAPI).toBeCalledTimes(1); + + // expect tag added to video tags + expect(wrapper.state().tags).toMatchObject([{tag_name: 'test'}]); + // expect tag to be removed from tag suggestions + expect(wrapper.state().suggesttag).toHaveLength(0); + }); + }); + + it('test adding of already existing tag', function () { + const wrapper = shallow(); + global.callAPIMock({result: 'success'}); + + wrapper.setState({suggesttag: [{tag_name: 'test', tag_id: 1}], tags: [{tag_name: 'test', tag_id: 1}]}, () => { + // mock funtion should have not been called + expect(callAPI).toBeCalledTimes(0); + wrapper.find('Tag').findWhere(p => p.text() === 'test').last().parent().dive().simulate('click'); + // mock function should have been called once + expect(callAPI).toBeCalledTimes(1); + + // there should not been added a duplicate of tag so object stays same... + expect(wrapper.state().tags).toMatchObject([{tag_name: 'test'}]); + // the suggestion tag shouldn't be removed (this can't actually happen in rl + // because backennd doesn't give dupliacate suggestiontags) + expect(wrapper.state().suggesttag).toHaveLength(1); + }); + }); + function generatetag() { const wrapper = shallow(); @@ -178,4 +231,26 @@ describe('', function () { return wrapper; } + + it('test addactor popup showing', function () { + const wrapper = shallow(); + + expect(wrapper.find('AddActorPopup')).toHaveLength(0); + + wrapper.instance().addActor(); + + // check if popup is visible + expect(wrapper.find('AddActorPopup')).toHaveLength(1); + }); + + it('test hiding of addactor popup', function () { + const wrapper = shallow(); + wrapper.instance().addActor(); + + expect(wrapper.find('AddActorPopup')).toHaveLength(1); + + wrapper.find('AddActorPopup').props().onHide(); + + expect(wrapper.find('AddActorPopup')).toHaveLength(0); + }); }); diff --git a/src/pages/RandomPage/RandomPage.js b/src/pages/RandomPage/RandomPage.js index 0ddfa01..9bb28c5 100644 --- a/src/pages/RandomPage/RandomPage.js +++ b/src/pages/RandomPage/RandomPage.js @@ -4,6 +4,7 @@ import SideBar, {SideBarTitle} from '../../elements/SideBar/SideBar'; import Tag from '../../elements/Tag/Tag'; import PageTitle from '../../elements/PageTitle/PageTitle'; import VideoContainer from '../../elements/VideoContainer/VideoContainer'; +import {callAPI} from '../../utils/Api'; /** * Randompage shuffles random viedeopreviews and provides a shuffle btn @@ -61,25 +62,15 @@ class RandomPage extends React.Component { * @param nr number of videos to load */ loadShuffledvideos(nr) { - const updateRequest = new FormData(); - updateRequest.append('action', 'getRandomMovies'); - updateRequest.append('number', nr); + callAPI('video.php', {action: 'getRandomMovies', number: nr}, result => { + console.log(result); - // fetch all videos available - fetch('/api/video.php', {method: 'POST', body: updateRequest}) - .then((response) => response.json() - .then((result) => { - console.log(result); - - this.setState({videos: []}); // needed to trigger rerender of main videoview - this.setState({ - videos: result.rows, - tags: result.tags - }); - })) - .catch(() => { - console.log('no connection to backend'); + this.setState({videos: []}); // needed to trigger rerender of main videoview + this.setState({ + videos: result.rows, + tags: result.tags }); + }); } } diff --git a/src/pages/SettingsPage/GeneralSettings.js b/src/pages/SettingsPage/GeneralSettings.js index dc0aca1..caa04ac 100644 --- a/src/pages/SettingsPage/GeneralSettings.js +++ b/src/pages/SettingsPage/GeneralSettings.js @@ -1,11 +1,12 @@ import React from 'react'; import {Button, Col, Form} from 'react-bootstrap'; import style from './GeneralSettings.module.css'; -import GlobalInfos from '../../GlobalInfos'; +import GlobalInfos from '../../utils/GlobalInfos'; import InfoHeaderItem from '../../elements/InfoHeaderItem/InfoHeaderItem'; import {faArchive, faBalanceScaleLeft, faRulerVertical} from '@fortawesome/free-solid-svg-icons'; import {faAddressCard} from '@fortawesome/free-regular-svg-icons'; import {version} from '../../../package.json'; +import {callAPI, setCustomBackendDomain} from '../../utils/Api'; /** * Component for Generalsettings tag on Settingspage @@ -18,6 +19,7 @@ class GeneralSettings extends React.Component { this.state = { passwordsupport: false, tmdbsupport: null, + customapi: false, videopath: '', tvshowpath: '', @@ -77,6 +79,31 @@ class GeneralSettings extends React.Component { + { + if (this.state.customapi) { + setCustomBackendDomain(''); + } + + this.setState({customapi: !this.state.customapi}); + }} + /> + {this.state.customapi ? + + API Backend url + { + this.setState({apipath: e.target.value}); + setCustomBackendDomain(e.target.value); + }}/> + : null} + + { + this.setState({ + videopath: result.video_path, + tvshowpath: result.episode_path, + mediacentername: result.mediacenter_name, + password: result.password, + passwordsupport: result.passwordEnabled, + tmdbsupport: result.TMDB_grabbing, - fetch('/api/settings.php', {method: 'POST', body: updateRequest}) - .then((response) => response.json() - .then((result) => { - console.log(result); - this.setState({ - videopath: result.video_path, - tvshowpath: result.episode_path, - mediacentername: result.mediacenter_name, - password: result.password, - passwordsupport: result.passwordEnabled, - tmdbsupport: result.TMDB_grabbing, - - videonr: result.videonr, - dbsize: result.dbsize, - difftagnr: result.difftagnr, - tagsadded: result.tagsadded - }); - })); + videonr: result.videonr, + dbsize: result.dbsize, + difftagnr: result.difftagnr, + tagsadded: result.tagsadded + }); + }); } /** * save the selected and typed settings to the backend */ saveSettings() { - const updateRequest = new FormData(); - updateRequest.append('action', 'saveGeneralSettings'); - - updateRequest.append('password', this.state.passwordsupport ? this.state.password : '-1'); - updateRequest.append('videopath', this.state.videopath); - updateRequest.append('tvshowpath', this.state.tvshowpath); - updateRequest.append('mediacentername', this.state.mediacentername); - updateRequest.append('tmdbsupport', this.state.tmdbsupport); - updateRequest.append('darkmodeenabled', GlobalInfos.isDarkTheme().toString()); - - fetch('/api/settings.php', {method: 'POST', body: updateRequest}) - .then((response) => response.json() - .then((result) => { - if (result.success) { - console.log('successfully saved settings'); - // todo 2020-07-10: popup success - } else { - console.log('failed to save settings'); - // todo 2020-07-10: popup error - } - })); + callAPI('settings.php', { + action: 'saveGeneralSettings', + password: this.state.passwordsupport ? this.state.password : '-1', + videopath: this.state.videopath, + tvshowpath: this.state.tvshowpath, + mediacentername: this.state.mediacentername, + tmdbsupport: this.state.tmdbsupport, + darkmodeenabled: GlobalInfos.isDarkTheme().toString() + }, (result) => { + if (result.success) { + console.log('successfully saved settings'); + // todo 2020-07-10: popup success + } else { + console.log('failed to save settings'); + // todo 2020-07-10: popup error + } + }); } } diff --git a/src/pages/SettingsPage/GeneralSettings.module.css b/src/pages/SettingsPage/GeneralSettings.module.css index 7045c21..5ddf8f1 100644 --- a/src/pages/SettingsPage/GeneralSettings.module.css +++ b/src/pages/SettingsPage/GeneralSettings.module.css @@ -8,6 +8,11 @@ width: 40%; } +.customapiform{ + margin-top: 15px; + width: 40%; +} + .infoheader { display: flex; flex-wrap: wrap; diff --git a/src/pages/SettingsPage/GeneralSettings.test.js b/src/pages/SettingsPage/GeneralSettings.test.js index 0d7a0f4..2f03e34 100644 --- a/src/pages/SettingsPage/GeneralSettings.test.js +++ b/src/pages/SettingsPage/GeneralSettings.test.js @@ -1,7 +1,7 @@ import {shallow} from 'enzyme'; import React from 'react'; import GeneralSettings from './GeneralSettings'; -import GlobalInfos from '../../GlobalInfos'; +import GlobalInfos from '../../utils/GlobalInfos'; describe('', function () { it('renders without crashing ', function () { diff --git a/src/pages/SettingsPage/MovieSettings.js b/src/pages/SettingsPage/MovieSettings.js index cf6c9fc..7ec5886 100644 --- a/src/pages/SettingsPage/MovieSettings.js +++ b/src/pages/SettingsPage/MovieSettings.js @@ -1,5 +1,6 @@ import React from 'react'; import style from './MovieSettings.module.css'; +import {callAPI} from '../../utils/Api'; /** * Component for MovieSettings on Settingspage @@ -50,23 +51,17 @@ class MovieSettings extends React.Component { this.setState({startbtnDisabled: true}); console.log('starting'); - const request = new FormData(); - request.append('action', 'startReindex'); - // fetch all videos available - fetch('/api/settings.php', {method: 'POST', body: request}) - .then((response) => response.json() - .then((result) => { - console.log(result); - if (result.success) { - console.log('started successfully'); - } else { - console.log('error, reindex already running'); - this.setState({startbtnDisabled: true}); - } - })) - .catch(() => { - console.log('no connection to backend'); - }); + + callAPI('settings.php', {action: 'startReindex'}, (result) => { + console.log(result); + if (result.success) { + console.log('started successfully'); + } else { + console.log('error, reindex already running'); + this.setState({startbtnDisabled: true}); + } + }); + if (this.myinterval) { clearInterval(this.myinterval); } @@ -77,49 +72,33 @@ class MovieSettings extends React.Component { * This interval function reloads the current status of reindexing from backend */ updateStatus = () => { - const request = new FormData(); - request.append('action', 'getStatusMessage'); + callAPI('settings.php', {action: 'getStatusMessage'}, (result) => { + if (result.contentAvailable === true) { + console.log(result); + // todo 2020-07-4: scroll to bottom of div here + this.setState({ + // insert a string for each line + text: [...result.message.split('\n'), + ...this.state.text] + }); + } else { + // clear refresh interval if no content available + clearInterval(this.myinterval); - fetch('/api/settings.php', {method: 'POST', body: request}) - .then((response) => response.json() - .then((result) => { - if (result.contentAvailable === true) { - console.log(result); - // todo 2020-07-4: scroll to bottom of div here - this.setState({ - // insert a string for each line - text: [...result.message.split('\n'), - ...this.state.text] - }); - } else { - // clear refresh interval if no content available - clearInterval(this.myinterval); - - this.setState({startbtnDisabled: false}); - } - })) - .catch(() => { - console.log('no connection to backend'); - }); + this.setState({startbtnDisabled: false}); + } + }); }; /** * send request to cleanup db gravity */ cleanupGravity() { - const request = new FormData(); - request.append('action', 'cleanupGravity'); - - fetch('/api/settings.php', {method: 'POST', body: request}) - .then((response) => response.text() - .then((result) => { - this.setState({ - text: ['successfully cleaned up gravity!'] - }); - })) - .catch(() => { - console.log('no connection to backend'); + callAPI('settings.php', {action: 'cleanupGravity'}, (result) => { + this.setState({ + text: ['successfully cleaned up gravity!'] }); + }); } } diff --git a/src/pages/SettingsPage/SettingsPage.js b/src/pages/SettingsPage/SettingsPage.tsx similarity index 86% rename from src/pages/SettingsPage/SettingsPage.js rename to src/pages/SettingsPage/SettingsPage.tsx index 130f3fa..4b8f718 100644 --- a/src/pages/SettingsPage/SettingsPage.js +++ b/src/pages/SettingsPage/SettingsPage.tsx @@ -2,14 +2,18 @@ import React from 'react'; import MovieSettings from './MovieSettings'; import GeneralSettings from './GeneralSettings'; import style from './SettingsPage.module.css'; -import GlobalInfos from '../../GlobalInfos'; +import GlobalInfos from '../../utils/GlobalInfos'; + +type SettingsPageState = { + currentpage: string +} /** * The Settingspage handles all kinds of settings for the mediacenter * and is basically a wrapper for child-tabs */ -class SettingsPage extends React.Component { - constructor(props, context) { +class SettingsPage extends React.Component<{}, SettingsPageState> { + constructor(props: Readonly<{}> | {}, context?: any) { super(props, context); this.state = { @@ -21,7 +25,7 @@ class SettingsPage extends React.Component { * load the selected tab * @returns {JSX.Element|string} the jsx element of the selected tab */ - getContent() { + getContent(): JSX.Element | string { switch (this.state.currentpage) { case 'general': return ; @@ -34,7 +38,7 @@ class SettingsPage extends React.Component { } } - render() { + render() : JSX.Element { const themestyle = GlobalInfos.getThemeStyle(); return (
diff --git a/src/setupTests.js b/src/setupTests.js index 2f52b1f..dabe128 100644 --- a/src/setupTests.js +++ b/src/setupTests.js @@ -6,7 +6,7 @@ import '@testing-library/jest-dom/extend-expect'; import {configure} from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; -import GlobalInfos from './GlobalInfos'; +import GlobalInfos from './utils/GlobalInfos'; configure({adapter: new Adapter()}); @@ -45,3 +45,21 @@ global.prepareViewBinding = (func) => { } }; } + +global.callAPIMock = (resonse) => { + const helpers = require("./utils/Api"); + helpers.callAPI = jest.fn().mockImplementation((_, __, func1) => {func1(resonse)}); +} + +// code to run before each test +global.beforeEach(() => { + // empty fetch response implementation for each test + global.fetch = prepareFetchApi({}); + // todo with callAPIMock +}) + +global.afterEach(() => { + // clear all mocks after each test + jest.resetAllMocks(); +}) + diff --git a/src/utils/Api.ts b/src/utils/Api.ts new file mode 100644 index 0000000..2bd8fc4 --- /dev/null +++ b/src/utils/Api.ts @@ -0,0 +1,88 @@ +let customBackendURL: string; + +/** + * get the domain of the api backend + * @return string domain of backend http://x.x.x.x/bla + */ +export function getBackendDomain(): string { + let userAgent = navigator.userAgent.toLowerCase(); + if (userAgent.indexOf(' electron/') > -1) { + // Electron-specific code - force a custom backendurl + return (customBackendURL); + } else { + // use custom only if defined + if (customBackendURL) { + return (customBackendURL); + } else { + return (window.location.origin); + } + } +} + +/** + * set a custom backend domain + * @param domain a url in format [http://x.x.x.x/somanode] + */ +export function setCustomBackendDomain(domain: string) { + customBackendURL = domain; +} + +/** + * a helper function to get the api path + */ +function getAPIDomain(): string { + return getBackendDomain() + '/api/'; +} + +/** + * interface how an api request should look like + */ +interface ApiBaseRequest { + action: string, + + [_: string]: string +} + +/** + * helper function to build a formdata for requesting post data correctly + * @param args api request object + */ +function buildFormData(args: ApiBaseRequest): FormData { + const req = new FormData(); + + for (const i in args) { + req.append(i, args[i]); + } + return req; +} + +/** + * A backend api call + * @param apinode which api backend handler to call + * @param fd the object to send to backend + * @param callback the callback with json reply from backend + * @param errorcallback a optional callback if an error occured + */ +export function callAPI(apinode: string, fd: ApiBaseRequest, callback: (_: object) => void, errorcallback: (_: object) => void = (_: object) => { +}): void { + fetch(getAPIDomain() + apinode, {method: 'POST', body: buildFormData(fd)}) + .then((response) => response.json() + .then((result) => { + callback(result); + })).catch(reason => errorcallback(reason)); +} + +/** + * A backend api call + * @param apinode which api backend handler to call + * @param fd the object to send to backend + * @param callback the callback with PLAIN text reply from backend + */ +export function callAPIPlain(apinode: string, fd: ApiBaseRequest, callback: (_: any) => void): void { + fetch(getAPIDomain() + apinode, {method: 'POST', body: buildFormData(fd)}) + .then((response) => response.text() + .then((result) => { + callback(result); + })); + +} diff --git a/src/GlobalInfos.js b/src/utils/GlobalInfos.js similarity index 83% rename from src/GlobalInfos.js rename to src/utils/GlobalInfos.js index d8c1094..b5ab724 100644 --- a/src/GlobalInfos.js +++ b/src/utils/GlobalInfos.js @@ -1,5 +1,5 @@ -import darktheme from './AppDarkTheme.module.css'; -import lighttheme from './AppLightTheme.module.css'; +import darktheme from '../AppDarkTheme.module.css'; +import lighttheme from '../AppLightTheme.module.css'; /** * This class is available for all components in project @@ -7,7 +7,7 @@ import lighttheme from './AppLightTheme.module.css'; */ class StaticInfos { #darktheme = true; - #viewbinding = () => {console.warn("Viewbinding not set now!")} + #viewbinding = () => {console.warn('Viewbinding not set now!');}; /** * check if the current theme is the dark theme @@ -37,7 +37,7 @@ class StaticInfos { * set the global Viewbinding for the main Navigation * @param cb */ - setViewBinding(cb){ + setViewBinding(cb) { this.#viewbinding = cb; } @@ -45,7 +45,7 @@ class StaticInfos { * return the Viewbinding for main navigation * @returns {StaticInfos.viewbinding} */ - getViewBinding(){ + getViewBinding() { return this.#viewbinding; } } diff --git a/src/GlobalInfos.test.js b/src/utils/GlobalInfos.test.js similarity index 100% rename from src/GlobalInfos.test.js rename to src/utils/GlobalInfos.test.js diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..7d00a58 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "target": "ES5", + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react" + }, + "include": [ + "src" + ] +}