Merge branch 'electronapp' into 'master'

Electronapp

See merge request lukas/openmediacenter!27
This commit is contained in:
Lukas Heiligenbrunner 2020-12-17 20:53:22 +00:00
commit 866d8f72b4
43 changed files with 804 additions and 500 deletions

View File

@ -8,6 +8,7 @@ stages:
- deploy - deploy
cache: cache:
key: "$CI_COMMIT_REF_SLUG" # use per branch caching
paths: paths:
- node_modules/ - node_modules/
@ -76,6 +77,20 @@ package_debian:
- deb/OpenMediaCenter-*.deb - deb/OpenMediaCenter-*.deb
needs: ["build"] 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: deploy_test1:
stage: deploy stage: deploy
image: luki42/alpineopenssh:latest image: luki42/alpineopenssh:latest

21
build.js Normal file
View File

@ -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);
});

View File

@ -2,6 +2,12 @@
"name": "openmediacenter", "name": "openmediacenter",
"version": "0.1.2", "version": "0.1.2",
"private": true, "private": true,
"main": "public/electron.js",
"author": {
"email": "lukas.heiligenbrunner@gmail.com",
"name": "Lukas Heiligenbrunner",
"url": "https://heili.eu"
},
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "^1.2.32", "@fortawesome/fontawesome-svg-core": "^1.2.32",
"@fortawesome/free-regular-svg-icons": "^5.15.1", "@fortawesome/free-regular-svg-icons": "^5.15.1",
@ -11,15 +17,33 @@
"plyr-react": "^3.0.7", "plyr-react": "^3.0.7",
"react": "^17.0.1", "react": "^17.0.1",
"react-bootstrap": "^1.4.0", "react-bootstrap": "^1.4.0",
"react-dom": "^17.0.1" "react-dom": "^17.0.1",
"typescript": "^4.1.3"
}, },
"scripts": { "scripts": {
"start": "react-scripts start", "start": "react-scripts start",
"build": "react-scripts build", "build": "react-scripts build",
"test": "react-scripts test --reporters=jest-junit --reporters=default", "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", "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": { "jest": {
"collectCoverageFrom": [ "collectCoverageFrom": [
@ -32,7 +56,7 @@
] ]
}, },
"proxy": "http://192.168.0.209", "proxy": "http://192.168.0.209",
"homepage": "/", "homepage": "./",
"eslintConfig": { "eslintConfig": {
"extends": "react-app" "extends": "react-app"
}, },
@ -49,12 +73,14 @@
] ]
}, },
"devDependencies": { "devDependencies": {
"react-scripts": "^3.4.4",
"@testing-library/jest-dom": "^5.11.6", "@testing-library/jest-dom": "^5.11.6",
"@testing-library/react": "^11.2.2", "@testing-library/react": "^11.2.2",
"@testing-library/user-event": "^12.2.2", "@testing-library/user-event": "^12.2.2",
"electron": "^11.1.0",
"electron-builder": "^22.1.0",
"enzyme": "^3.11.0", "enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.5", "enzyme-adapter-react-16": "^1.15.5",
"jest-junit": "^12.0.0" "jest-junit": "^12.0.0",
"react-scripts": "^3.4.4"
} }
} }

31
public/electron.js Normal file
View File

@ -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();
}
});

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import HomePage from './pages/HomePage/HomePage'; import HomePage from './pages/HomePage/HomePage';
import RandomPage from './pages/RandomPage/RandomPage'; import RandomPage from './pages/RandomPage/RandomPage';
import GlobalInfos from './GlobalInfos'; import GlobalInfos from './utils/GlobalInfos';
// include bootstraps css // include bootstraps css
import 'bootstrap/dist/css/bootstrap.min.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 SettingsPage from './pages/SettingsPage/SettingsPage';
import CategoryPage from './pages/CategoryPage/CategoryPage'; 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 * The main App handles the main tabs and which content to show
@ -22,7 +24,8 @@ class App extends React.Component {
page: 'default', page: 'default',
generalSettingsLoaded: false, generalSettingsLoaded: false,
passwordsupport: null, passwordsupport: null,
mediacentername: 'OpenMediaCenter' mediacentername: 'OpenMediaCenter',
onapierror: false
}; };
// bind this to the method for being able to call methods such as this.setstate // 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()); 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() { componentDidMount() {
const updateRequest = new FormData(); this.initialAPICall();
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;
}));
} }
/** /**
@ -119,6 +125,7 @@ class App extends React.Component {
</div> </div>
</div> </div>
{this.state.generalSettingsLoaded ? this.MainBody() : 'loading'} {this.state.generalSettingsLoaded ? this.MainBody() : 'loading'}
{this.state.onapierror ? this.ApiError() : null}
</div> </div>
); );
} }
@ -142,6 +149,11 @@ class App extends React.Component {
page: 'lastpage' page: 'lastpage'
}); });
} }
ApiError() {
// on api error show popup and retry and show again if failing..
return (<NoBackendConnectionPopup onHide={() => this.initialAPICall()}/>);
}
} }
export default App; export default App;

View File

@ -2,7 +2,7 @@ import style from './ActorTile.module.css';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faUser} from '@fortawesome/free-solid-svg-icons'; import {faUser} from '@fortawesome/free-solid-svg-icons';
import React from 'react'; import React from 'react';
import GlobalInfos from '../../GlobalInfos'; import GlobalInfos from '../../utils/GlobalInfos';
import ActorPage from '../../pages/ActorPage/ActorPage'; import ActorPage from '../../pages/ActorPage/ActorPage';
class ActorTile extends React.Component { class ActorTile extends React.Component {

View File

@ -18,4 +18,17 @@ describe('<ActorTile/>', function () {
expect(func).toBeCalledTimes(1); expect(func).toBeCalledTimes(1);
}); });
it('simulate click with custom handler', function () {
const func = jest.fn((_) => {});
const wrapper = shallow(<ActorTile actor={{thumbnail: "-1", name: "testname", id: 3}} onClick={() => func()}/>);
const func1 = jest.fn();
prepareViewBinding(func1);
wrapper.simulate('click');
expect(func1).toBeCalledTimes(0);
expect(func).toBeCalledTimes(1);
});
}); });

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import style from './PageTitle.module.css'; import style from './PageTitle.module.css';
import GlobalInfos from '../../GlobalInfos'; import GlobalInfos from '../../utils/GlobalInfos';
/** /**
* Component for generating PageTitle with bottom Line * Component for generating PageTitle with bottom Line

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import {shallow} from 'enzyme'; import {shallow} from 'enzyme';
import PageTitle from './PageTitle'; import PageTitle, {Line} from './PageTitle';
describe('<Preview/>', function () { describe('<Preview/>', function () {
it('renders without crashing ', function () { it('renders without crashing ', function () {
@ -29,3 +29,10 @@ describe('<Preview/>', function () {
}); });
}); });
describe('<Line/>', () => {
it('renders without crashing', function () {
const wrapper = shallow(<Line/>);
wrapper.unmount();
});
});

View File

@ -3,6 +3,7 @@ import React from 'react';
import ActorTile from '../../ActorTile/ActorTile'; import ActorTile from '../../ActorTile/ActorTile';
import style from './AddActorPopup.module.css'; import style from './AddActorPopup.module.css';
import {NewActorPopupContent} from '../NewActorPopup/NewActorPopup'; import {NewActorPopupContent} from '../NewActorPopup/NewActorPopup';
import {callAPI} from '../../../utils/Api';
/** /**
* Popup for Adding a new Actor to a Video * Popup for Adding a new Actor to a Video
@ -69,33 +70,21 @@ class AddActorPopup extends React.Component {
*/ */
tileClickHandler(actorid) { tileClickHandler(actorid) {
// fetch the available actors // fetch the available actors
const req = new FormData(); callAPI('actor.php', {action: 'addActorToVideo', actorid: actorid, videoid: this.props.movie_id}, result => {
req.append('action', 'addActorToVideo'); if (result.result === 'success') {
req.append('actorid', actorid); // return back to player page
req.append('videoid', this.props.movie_id); this.props.onHide();
} else {
fetch('/api/actor.php', {method: 'POST', body: req}) console.error('an error occured while fetching actors');
.then((response) => response.json() console.error(result);
.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);
}
}));
} }
loadActors() { loadActors() {
const req = new FormData(); callAPI('actor.php', {action: 'getAllActors'}, result => {
req.append('action', 'getAllActors'); this.setState({actors: result});
});
fetch('/api/actor.php', {method: 'POST', body: req})
.then((response) => response.json()
.then((result) => {
this.setState({actors: result});
}));
} }
} }

View File

@ -1,6 +1,7 @@
import {shallow} from 'enzyme'; import {shallow} from 'enzyme';
import React from 'react'; import React from 'react';
import AddActorPopup from './AddActorPopup'; import AddActorPopup from './AddActorPopup';
import {callAPI} from '../../../utils/Api';
describe('<AddActorPopup/>', function () { describe('<AddActorPopup/>', function () {
it('renders without crashing ', function () { it('renders without crashing ', function () {
@ -8,12 +9,63 @@ describe('<AddActorPopup/>', function () {
wrapper.unmount(); wrapper.unmount();
}); });
// it('simulate change to other page', function () { it('simulate change to other page', function () {
// const wrapper = shallow(<AddActorPopup/>); const wrapper = shallow(<AddActorPopup/>);
//
// console.log(wrapper.find('PopupBase').dive().debug()); expect(wrapper.find('NewActorPopupContent')).toHaveLength(0);
// wrapper.find('PopupBase').props().banner.props.onClick();
//
// console.log(wrapper.debug()); // check if new content is showing
// }); expect(wrapper.find('NewActorPopupContent')).toHaveLength(1);
});
it('hide new actor page', function () {
const wrapper = shallow(<AddActorPopup/>);
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(<AddActorPopup/>);
expect(wrapper.find('ActorTile')).toHaveLength(2);
});
it('simulate actortile click', function () {
const func = jest.fn();
const wrapper = shallow(<AddActorPopup onHide={() => {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(<AddActorPopup onHide={() => {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);
});
});
}); });

View File

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import Tag from '../../Tag/Tag'; import Tag from '../../Tag/Tag';
import PopupBase from '../PopupBase'; import PopupBase from '../PopupBase';
import {callAPI} from '../../../utils/Api';
/** /**
* component creates overlay to add a new tag to a video * component creates overlay to add a new tag to a video
@ -13,16 +14,11 @@ class AddTagPopup extends React.Component {
} }
componentDidMount() { componentDidMount() {
const updateRequest = new FormData(); callAPI('tags.php', {action: 'getAllTags'}, (result) => {
updateRequest.append('action', 'getAllTags'); this.setState({
items: result
fetch('/api/tags.php', {method: 'POST', body: updateRequest})
.then((response) => response.json())
.then((result) => {
this.setState({
items: result
});
}); });
});
} }
render() { render() {
@ -44,23 +40,15 @@ class AddTagPopup extends React.Component {
* @param tagname tag name to add * @param tagname tag name to add
*/ */
addTag(tagid, tagname) { addTag(tagid, tagname) {
console.log(this.props); callAPI('tags.php', {action: 'addTag', id: tagid, movieid: this.props.movie_id}, result => {
const updateRequest = new FormData(); if (result.result !== 'success') {
updateRequest.append('action', 'addTag'); console.log('error occured while writing to db -- todo error handling');
updateRequest.append('id', tagid); console.log(result.result);
updateRequest.append('movieid', this.props.movie_id); } else {
this.props.submit(tagid, tagname);
fetch('/api/tags.php', {method: 'POST', body: updateRequest}) }
.then((response) => response.json() this.props.onHide();
.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();
}));
} }
} }

View File

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import PopupBase from '../PopupBase'; import PopupBase from '../PopupBase';
import style from './NewActorPopup.module.css'; import style from './NewActorPopup.module.css';
import {callAPI} from '../../../utils/Api';
/** /**
* creates modal overlay to define a new Tag * creates modal overlay to define a new Tag
@ -41,19 +42,13 @@ export class NewActorPopupContent extends React.Component {
// check if user typed in name // check if user typed in name
if (this.value === '' || this.value === undefined) return; if (this.value === '' || this.value === undefined) return;
const req = new FormData(); callAPI('actor.php', {action: 'createActor', actorname: this.value}, (result) => {
req.append('action', 'createActor'); if (result.result !== 'success') {
req.append('actorname', this.value); console.log('error occured while writing to db -- todo error handling');
console.log(result.result);
fetch('/api/actor.php', {method: 'POST', body: req}) }
.then((response) => response.json()) this.props.onHide();
.then((result) => { });
if (result.result !== 'success') {
console.log('error occured while writing to db -- todo error handling');
console.log(result.result);
}
this.props.onHide();
});
} }
} }

View File

@ -3,6 +3,7 @@ import React from 'react';
import {shallow} from 'enzyme'; import {shallow} from 'enzyme';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import NewActorPopup, {NewActorPopupContent} from './NewActorPopup'; import NewActorPopup, {NewActorPopupContent} from './NewActorPopup';
import {callAPI} from '../../../utils/Api';
describe('<NewActorPopup/>', function () { describe('<NewActorPopup/>', function () {
it('renders without crashing ', function () { it('renders without crashing ', function () {
@ -18,18 +19,23 @@ describe('<NewActorPopupContent/>', () => {
}); });
it('simulate button click', function () { it('simulate button click', function () {
const wrapper = shallow(<NewActorPopupContent/>); global.callAPIMock({});
const func = jest.fn();
const wrapper = shallow(<NewActorPopupContent onHide={() => {func()}}/>);
// manually set typed in actorname // manually set typed in actorname
wrapper.instance().value = 'testactorname'; wrapper.instance().value = 'testactorname';
global.fetch = prepareFetchApi({}); global.fetch = prepareFetchApi({});
expect(global.fetch).toBeCalledTimes(0); expect(callAPI).toBeCalledTimes(0);
wrapper.find('button').simulate('click'); wrapper.find('button').simulate('click');
// fetch should have been called once now // 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 () { it('test not allowing request if textfield is empty', function () {

View File

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import PopupBase from '../PopupBase'; import PopupBase from '../PopupBase';
import style from './NewTagPopup.module.css'; import style from './NewTagPopup.module.css';
import {callAPI} from '../../../utils/Api';
/** /**
* creates modal overlay to define a new Tag * 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 * store the filled in form to the backend
*/ */
storeselection() { storeselection() {
const updateRequest = new FormData(); callAPI('tags.php', {action: 'createTag', tagname: this.value}, result => {
updateRequest.append('action', 'createTag'); if (result.result !== 'success') {
updateRequest.append('tagname', this.value); console.log('error occured while writing to db -- todo error handling');
console.log(result.result);
fetch('/api/tags.php', {method: 'POST', body: updateRequest}) }
.then((response) => response.json()) this.props.onHide();
.then((result) => { });
if (result.result !== 'success') {
console.log('error occured while writing to db -- todo error handling');
console.log(result.result);
}
this.props.onHide();
});
} }
} }

View File

@ -3,6 +3,8 @@ import React from 'react';
import {shallow} from 'enzyme'; import {shallow} from 'enzyme';
import '@testing-library/jest-dom'; import '@testing-library/jest-dom';
import NewTagPopup from './NewTagPopup'; import NewTagPopup from './NewTagPopup';
import {NoBackendConnectionPopup} from '../NoBackendConnectionPopup/NoBackendConnectionPopup';
import {getBackendDomain} from '../../../utils/Api';
describe('<NewTagPopup/>', function () { describe('<NewTagPopup/>', function () {
it('renders without crashing ', function () { it('renders without crashing ', function () {
@ -33,4 +35,12 @@ describe('<NewTagPopup/>', function () {
done(); done();
}); });
}); });
it('simulate textfield change', function () {
const wrapper = shallow(<NewTagPopup/>);
wrapper.find('input').simulate('change', {target: {value: 'testvalue'}});
expect(wrapper.instance().value).toBe('testvalue');
});
}); });

View File

@ -0,0 +1,29 @@
import {shallow} from 'enzyme';
import React from 'react';
import {NoBackendConnectionPopup} from './NoBackendConnectionPopup';
import {getBackendDomain} from '../../../utils/Api';
describe('<NoBackendConnectionPopup/>', function () {
it('renders without crashing ', function () {
const wrapper = shallow(<NoBackendConnectionPopup onHide={() => {}}/>);
wrapper.unmount();
});
it('hides on refresh click', function () {
const func = jest.fn();
const wrapper = shallow(<NoBackendConnectionPopup onHide={func}/>);
expect(func).toBeCalledTimes(0);
wrapper.find('button').simulate('click');
expect(func).toBeCalledTimes(1);
});
it('simulate change of textfield', function () {
const wrapper = shallow(<NoBackendConnectionPopup onHide={() => {}}/>);
wrapper.find('input').simulate('change', {target: {value: 'testvalue'}});
expect(getBackendDomain()).toBe('testvalue');
});
});

View File

@ -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 (
<PopupBase title='No connection to backend API!' onHide={props.onHide} height='200px' width='600px'>
<div>
<input type='text' placeholder='http://192.168.0.2' onChange={(v) => {
setCustomBackendDomain(v.target.value);
}}/></div>
<button className={style.savebtn} onClick={() => props.onHide()}>Refresh</button>
</PopupBase>
);
}

View File

@ -1,4 +1,4 @@
import GlobalInfos from '../../GlobalInfos'; import GlobalInfos from '../../utils/GlobalInfos';
import style from './PopupBase.module.css'; import style from './PopupBase.module.css';
import {Line} from '../PageTitle/PageTitle'; import {Line} from '../PageTitle/PageTitle';
import React from 'react'; import React from 'react';
@ -20,7 +20,8 @@ class PopupBase extends React.Component {
// parse style props // parse style props
this.framedimensions = { this.framedimensions = {
width: (this.props.width ? this.props.width : undefined), 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)
}; };
} }

View File

@ -1,7 +1,8 @@
.popup { .popup {
border: 3px #3574fe solid; border: 3px #3574fe solid;
border-radius: 18px; border-radius: 18px;
height: 80%; min-height: 80%;
height: fit-content;
left: 20%; left: 20%;
opacity: 0.95; opacity: 0.95;
position: absolute; position: absolute;
@ -40,4 +41,5 @@
margin-right: 20px; margin-right: 20px;
margin-top: 10px; margin-top: 10px;
opacity: 1; opacity: 1;
overflow: auto;
} }

View File

@ -8,5 +8,19 @@ describe('<PopupBase/>', function () {
wrapper.unmount(); wrapper.unmount();
}); });
it('simulate keypress', function () {
let events = [];
document.addEventListener = jest.fn((event, cb) => {
events[event] = cb;
});
const func = jest.fn();
shallow(<PopupBase onHide={() => func()}/>);
// trigger the keypress event
events.keyup({key: 'Escape'});
expect(func).toBeCalledTimes(1);
});
}); });

View File

@ -2,7 +2,8 @@ import React from 'react';
import style from './Preview.module.css'; import style from './Preview.module.css';
import Player from '../../pages/Player/Player'; import Player from '../../pages/Player/Player';
import {Spinner} from 'react-bootstrap'; import {Spinner} from 'react-bootstrap';
import GlobalInfos from '../../GlobalInfos'; import GlobalInfos from '../../utils/GlobalInfos';
import {callAPIPlain} from '../../utils/Api';
/** /**
* Component for single preview tile * Component for single preview tile
@ -19,22 +20,12 @@ class Preview extends React.Component {
} }
componentDidMount() { componentDidMount() {
this.setState({ callAPIPlain('video.php', {action: 'readThumbnail', movieid: this.props.movie_id}, (result) => {
previewpicture: null, this.setState({
name: this.props.name 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() { render() {

View File

@ -9,12 +9,6 @@ describe('<Preview/>', function () {
wrapper.unmount(); wrapper.unmount();
}); });
// check if preview title renders correctly
it('renders title', () => {
const wrapper = shallow(<Preview name='test'/>);
expect(wrapper.find('.previewtitle').text()).toBe('test');
});
it('click event triggered', () => { it('click event triggered', () => {
const func = jest.fn(); const func = jest.fn();
@ -36,7 +30,7 @@ describe('<Preview/>', function () {
}); });
global.fetch = jest.fn().mockImplementation(() => mockFetchPromise); global.fetch = jest.fn().mockImplementation(() => mockFetchPromise);
const wrapper = shallow(<Preview/>); const wrapper = shallow(<Preview name='test'/>);
// now called 1 times // now called 1 times
expect(global.fetch).toHaveBeenCalledTimes(1); expect(global.fetch).toHaveBeenCalledTimes(1);
@ -44,6 +38,8 @@ describe('<Preview/>', function () {
process.nextTick(() => { process.nextTick(() => {
// received picture should be rendered into wrapper // received picture should be rendered into wrapper
expect(wrapper.find('.previewimage').props().src).not.toBeNull(); expect(wrapper.find('.previewimage').props().src).not.toBeNull();
// check if preview title renders correctly
expect(wrapper.find('.previewtitle').text()).toBe('test');
global.fetch.mockClear(); global.fetch.mockClear();
done(); done();

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import style from './SideBar.module.css'; import style from './SideBar.module.css';
import GlobalInfos from '../../GlobalInfos'; import GlobalInfos from '../../utils/GlobalInfos';
/** /**
* component for sidebar-info * component for sidebar-info

View File

@ -2,7 +2,7 @@ import React from 'react';
import styles from './Tag.module.css'; import styles from './Tag.module.css';
import CategoryPage from '../../pages/CategoryPage/CategoryPage'; import CategoryPage from '../../pages/CategoryPage/CategoryPage';
import GlobalInfos from '../../GlobalInfos'; import GlobalInfos from '../../utils/GlobalInfos';
/** /**
* A Component representing a single Category tag * A Component representing a single Category tag

View File

@ -2,7 +2,6 @@ import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import App from './App'; import App from './App';
// don't allow console logs within production env
// 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; global.console.log = process.env.NODE_ENV !== "development" ? (s) => {} : global.console.log;

View File

@ -5,6 +5,7 @@ import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faUser} from '@fortawesome/free-solid-svg-icons'; import {faUser} from '@fortawesome/free-solid-svg-icons';
import style from './ActorPage.module.css'; import style from './ActorPage.module.css';
import VideoContainer from '../../elements/VideoContainer/VideoContainer'; import VideoContainer from '../../elements/VideoContainer/VideoContainer';
import {callAPI} from '../../utils/Api';
class ActorPage extends React.Component { class ActorPage extends React.Component {
constructor(props) { constructor(props) {
@ -40,17 +41,10 @@ class ActorPage extends React.Component {
*/ */
getActorInfo() { getActorInfo() {
// todo 2020-12-4: fetch to db // todo 2020-12-4: fetch to db
callAPI('actor.php', {action: 'getActorInfo', actorid: this.props.actor.actor_id}, result => {
const req = new FormData(); console.log(result);
req.append('action', 'getActorInfo'); this.setState({data: result.videos ? result.videos : []});
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 : []});
}));
} }
} }

View File

@ -7,6 +7,7 @@ import {TagPreview} from '../../elements/Preview/Preview';
import NewTagPopup from '../../elements/Popups/NewTagPopup/NewTagPopup'; import NewTagPopup from '../../elements/Popups/NewTagPopup/NewTagPopup';
import PageTitle, {Line} from '../../elements/PageTitle/PageTitle'; import PageTitle, {Line} from '../../elements/PageTitle/PageTitle';
import VideoContainer from '../../elements/VideoContainer/VideoContainer'; import VideoContainer from '../../elements/VideoContainer/VideoContainer';
import {callAPI} from '../../utils/Api';
/** /**
* Component for Category Page * Component for Category Page
@ -111,24 +112,11 @@ class CategoryPage extends React.Component {
* @param tag tagname * @param tag tagname
*/ */
fetchVideoData(tag) { fetchVideoData(tag) {
console.log(tag); callAPI('video.php', {action: 'getMovies', tag: tag}, result => {
const updateRequest = new FormData(); this.videodata = result;
updateRequest.append('action', 'getMovies'); this.setState({selected: null}); // needed to trigger the state reload correctly
updateRequest.append('tag', tag); this.setState({selected: 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');
});
} }
/** /**
@ -143,18 +131,9 @@ class CategoryPage extends React.Component {
* load all available tags from db. * load all available tags from db.
*/ */
loadTags() { loadTags() {
const updateRequest = new FormData(); callAPI('tags.php', {action: 'getAllTags'}, result => {
updateRequest.append('action', 'getAllTags'); this.setState({loadedtags: result});
});
// 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');
});
} }
} }

View File

@ -24,27 +24,6 @@ describe('<CategoryPage/>', function () {
}); });
}); });
it('test errored fetch call', done => {
global.fetch = global.prepareFetchApi({});
let message;
global.console.log = jest.fn((m) => {
message = m;
});
shallow(<CategoryPage/>);
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 () { it('test new tag popup', function () {
const wrapper = shallow(<CategoryPage/>); const wrapper = shallow(<CategoryPage/>);

View File

@ -5,6 +5,7 @@ import VideoContainer from '../../elements/VideoContainer/VideoContainer';
import style from './HomePage.module.css'; import style from './HomePage.module.css';
import PageTitle, {Line} from '../../elements/PageTitle/PageTitle'; import PageTitle, {Line} from '../../elements/PageTitle/PageTitle';
import {callAPI} from '../../utils/Api';
/** /**
* The home page component showing on the initial pageload * The home page component showing on the initial pageload
@ -43,54 +44,33 @@ class HomePage extends React.Component {
* @param tag tag to fetch videos * @param tag tag to fetch videos
*/ */
fetchVideoData(tag) { fetchVideoData(tag) {
const updateRequest = new FormData(); callAPI('video.php', {action: 'getMovies', tag: tag}, (result) => {
updateRequest.append('action', 'getMovies'); this.setState({
updateRequest.append('tag', tag); data: []
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');
}); });
this.setState({
data: result,
selectionnr: result.length,
tag: tag + ' Videos'
});
});
} }
/** /**
* fetch the necessary data for left info box * fetch the necessary data for left info box
*/ */
fetchStartData() { fetchStartData() {
const updateRequest = new FormData(); callAPI('video.php', {action: 'getStartData'}, (result) => {
updateRequest.append('action', 'getStartData'); this.setState({
sideinfo: {
// fetch all videos available videonr: result['total'],
fetch('/api/video.php', {method: 'POST', body: updateRequest}) fullhdvideonr: result['fullhd'],
.then((response) => response.json() hdvideonr: result['hd'],
.then((result) => { sdvideonr: result['sd'],
this.setState({ tagnr: result['tags']
sideinfo: { }
videonr: result['total'],
fullhdvideonr: result['fullhd'],
hdvideonr: result['hd'],
sdvideonr: result['sd'],
tagnr: result['tags']
}
});
}))
.catch(() => {
console.log('no connection to backend');
}); });
});
} }
/** /**
@ -101,26 +81,16 @@ class HomePage extends React.Component {
searchVideos(keyword) { searchVideos(keyword) {
console.log('search called'); console.log('search called');
const updateRequest = new FormData(); callAPI('video.php', {action: 'getSearchKeyWord', keyword: keyword}, (result) => {
updateRequest.append('action', 'getSearchKeyWord'); this.setState({
updateRequest.append('keyword', keyword); data: []
// 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');
}); });
this.setState({
data: result,
selectionnr: result.length,
tag: 'Search result: ' + keyword
});
});
} }
render() { render() {

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import style from './Player.module.css'; 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 {Plyr} from 'plyr-react';
import SideBar, {SideBarItem, SideBarTitle} from '../../elements/SideBar/SideBar'; 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 {faPlusCircle} from '@fortawesome/free-solid-svg-icons';
import AddActorPopup from '../../elements/Popups/AddActorPopup/AddActorPopup'; import AddActorPopup from '../../elements/Popups/AddActorPopup/AddActorPopup';
import ActorTile from '../../elements/ActorTile/ActorTile'; 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 * @param tagName name of tag to add
*/ */
quickAddTag(tagId, tagName) { quickAddTag(tagId, tagName) {
const updateRequest = new FormData(); callAPI('tags.php', {action: 'addTag', id: tagId, movieid: this.props.movie_id}, (result) => {
updateRequest.append('action', 'addTag'); if (result.result !== 'success') {
updateRequest.append('id', tagId); console.error('error occured while writing to db -- todo error handling');
updateRequest.append('movieid', this.props.movie_id); 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}) // only add tag if it isn't already there
.then((response) => response.json() if (tagIndex === -1) {
.then((result) => { // update tags if successful
if (result.result !== 'success') { let array = [...this.state.suggesttag]; // make a separate copy of the array (because of setState)
console.error('error occured while writing to db -- todo error handling'); const quickaddindex = this.state.suggesttag.map(function (e) {
console.error(result.result); 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 { } else {
// check if tag has already been added this.setState({
const tagIndex = this.state.tags.map(function (e) { tags: [...this.state.tags, {tag_name: tagName}]
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}]
});
}
}
} }
})); }
}
});
} }
/** /**
@ -179,7 +173,7 @@ class Player extends React.Component {
<div className={style.videowrapper}> <div className={style.videowrapper}>
{/* video component is added here */} {/* video component is added here */}
{this.state.sources ? <Plyr {this.state.sources ? <Plyr
style={plyrstyle} style={plyrstyle}
source={this.state.sources} source={this.state.sources}
options={this.options}/> : options={this.options}/> :
<div>not loaded yet</div>} <div>not loaded yet</div>}
@ -227,36 +221,30 @@ class Player extends React.Component {
* fetch all the required infos of a video from backend * fetch all the required infos of a video from backend
*/ */
fetchMovieData() { fetchMovieData() {
const updateRequest = new FormData(); callAPI('video.php', {action: 'loadVideo', movieid: this.props.movie_id}, result => {
updateRequest.append('action', 'loadVideo'); this.setState({
updateRequest.append('movieid', this.props.movie_id); sources: {
type: 'video',
fetch('/api/video.php', {method: 'POST', body: updateRequest}) sources: [
.then((response) => response.json()) {
.then((result) => { src: getBackendDomain() + result.movie_url,
this.setState({ type: 'video/mp4',
sources: { size: 1080
type: 'video', }
sources: [ ],
{ poster: result.thumbnail
src: result.movie_url, },
type: 'video/mp4', movie_id: result.movie_id,
size: 1080 movie_name: result.movie_name,
} likes: result.likes,
], quality: result.quality,
poster: result.thumbnail length: result.length,
}, tags: result.tags,
movie_id: result.movie_id, suggesttag: result.suggesttag,
movie_name: result.movie_name, actors: result.actors
likes: result.likes,
quality: result.quality,
length: result.length,
tags: result.tags,
suggesttag: result.suggesttag,
actors: result.actors
});
console.log(this.state);
}); });
console.log(this.state);
});
} }
@ -264,21 +252,15 @@ class Player extends React.Component {
* click handler for the like btn * click handler for the like btn
*/ */
likebtn() { likebtn() {
const updateRequest = new FormData(); callAPI('video.php', {action: 'addLike', movieid: this.props.movie_id}, result => {
updateRequest.append('action', 'addLike'); if (result.result === 'success') {
updateRequest.append('movieid', this.props.movie_id); // likes +1 --> avoid reload of all data
this.setState({likes: this.state.likes + 1});
fetch('/api/video.php', {method: 'POST', body: updateRequest}) } else {
.then((response) => response.json() console.error('an error occured while liking');
.then((result) => { console.error(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 * delete the current video and return to last page
*/ */
deleteVideo() { deleteVideo() {
const updateRequest = new FormData(); callAPI('video.php', {action: 'deleteVideo', movieid: this.props.movie_id}, result => {
updateRequest.append('action', 'deleteVideo'); if (result.result === 'success') {
updateRequest.append('movieid', this.props.movie_id); // return to last element if successful
GlobalInfos.getViewBinding().returnToLastElement();
fetch('/api/video.php', {method: 'POST', body: updateRequest}) } else {
.then((response) => response.json() console.error('an error occured while liking');
.then((result) => { console.error(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() { refetchActors() {
const req = new FormData(); callAPI('actor.php', {action: 'getActorsOfVideo', videoid: this.props.movie_id}, result => {
req.append('action', 'getActorsOfVideo'); this.setState({actors: result});
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});
}));
} }
} }

View File

@ -1,6 +1,7 @@
import {shallow} from 'enzyme'; import {shallow} from 'enzyme';
import React from 'react'; import React from 'react';
import Player from './Player'; import Player from './Player';
import {callAPI} from '../../utils/Api';
describe('<Player/>', function () { describe('<Player/>', function () {
it('renders without crashing ', function () { it('renders without crashing ', function () {
@ -81,7 +82,7 @@ describe('<Player/>', function () {
const wrapper = shallow(<Player/>); const wrapper = shallow(<Player/>);
const func = jest.fn(); const func = jest.fn();
prepareViewBinding(func) prepareViewBinding(func);
global.fetch = prepareFetchApi({result: 'success'}); global.fetch = prepareFetchApi({result: 'success'});
@ -163,6 +164,58 @@ describe('<Player/>', function () {
}); });
}); });
it('showspopups correctly', function () {
const wrapper = shallow(<Player/>);
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(<Player/>);
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(<Player/>);
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() { function generatetag() {
const wrapper = shallow(<Player/>); const wrapper = shallow(<Player/>);
@ -178,4 +231,26 @@ describe('<Player/>', function () {
return wrapper; return wrapper;
} }
it('test addactor popup showing', function () {
const wrapper = shallow(<Player/>);
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(<Player/>);
wrapper.instance().addActor();
expect(wrapper.find('AddActorPopup')).toHaveLength(1);
wrapper.find('AddActorPopup').props().onHide();
expect(wrapper.find('AddActorPopup')).toHaveLength(0);
});
}); });

View File

@ -4,6 +4,7 @@ import SideBar, {SideBarTitle} from '../../elements/SideBar/SideBar';
import Tag from '../../elements/Tag/Tag'; import Tag from '../../elements/Tag/Tag';
import PageTitle from '../../elements/PageTitle/PageTitle'; import PageTitle from '../../elements/PageTitle/PageTitle';
import VideoContainer from '../../elements/VideoContainer/VideoContainer'; import VideoContainer from '../../elements/VideoContainer/VideoContainer';
import {callAPI} from '../../utils/Api';
/** /**
* Randompage shuffles random viedeopreviews and provides a shuffle btn * 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 * @param nr number of videos to load
*/ */
loadShuffledvideos(nr) { loadShuffledvideos(nr) {
const updateRequest = new FormData(); callAPI('video.php', {action: 'getRandomMovies', number: nr}, result => {
updateRequest.append('action', 'getRandomMovies'); console.log(result);
updateRequest.append('number', nr);
// fetch all videos available this.setState({videos: []}); // needed to trigger rerender of main videoview
fetch('/api/video.php', {method: 'POST', body: updateRequest}) this.setState({
.then((response) => response.json() videos: result.rows,
.then((result) => { tags: result.tags
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');
}); });
});
} }
} }

View File

@ -1,11 +1,12 @@
import React from 'react'; import React from 'react';
import {Button, Col, Form} from 'react-bootstrap'; import {Button, Col, Form} from 'react-bootstrap';
import style from './GeneralSettings.module.css'; import style from './GeneralSettings.module.css';
import GlobalInfos from '../../GlobalInfos'; import GlobalInfos from '../../utils/GlobalInfos';
import InfoHeaderItem from '../../elements/InfoHeaderItem/InfoHeaderItem'; import InfoHeaderItem from '../../elements/InfoHeaderItem/InfoHeaderItem';
import {faArchive, faBalanceScaleLeft, faRulerVertical} from '@fortawesome/free-solid-svg-icons'; import {faArchive, faBalanceScaleLeft, faRulerVertical} from '@fortawesome/free-solid-svg-icons';
import {faAddressCard} from '@fortawesome/free-regular-svg-icons'; import {faAddressCard} from '@fortawesome/free-regular-svg-icons';
import {version} from '../../../package.json'; import {version} from '../../../package.json';
import {callAPI, setCustomBackendDomain} from '../../utils/Api';
/** /**
* Component for Generalsettings tag on Settingspage * Component for Generalsettings tag on Settingspage
@ -18,6 +19,7 @@ class GeneralSettings extends React.Component {
this.state = { this.state = {
passwordsupport: false, passwordsupport: false,
tmdbsupport: null, tmdbsupport: null,
customapi: false,
videopath: '', videopath: '',
tvshowpath: '', tvshowpath: '',
@ -77,6 +79,31 @@ class GeneralSettings extends React.Component {
</Form.Group> </Form.Group>
</Form.Row> </Form.Row>
<Form.Check
type='switch'
id='custom-switch-api'
label='Use custom API url'
checked={this.state.customapi}
onChange={() => {
if (this.state.customapi) {
setCustomBackendDomain('');
}
this.setState({customapi: !this.state.customapi});
}}
/>
{this.state.customapi ?
<Form.Group className={style.customapiform} data-testid='apipath'>
<Form.Label>API Backend url</Form.Label>
<Form.Control type='text' placeholder='https://127.0.0.1'
value={this.state.apipath}
onChange={(e) => {
this.setState({apipath: e.target.value});
setCustomBackendDomain(e.target.value);
}}/>
</Form.Group> : null}
<Form.Check <Form.Check
type='switch' type='switch'
id='custom-switch' id='custom-switch'
@ -142,54 +169,44 @@ class GeneralSettings extends React.Component {
* inital load of already specified settings from backend * inital load of already specified settings from backend
*/ */
loadSettings() { loadSettings() {
const updateRequest = new FormData(); callAPI('settings.php', {action: 'loadGeneralSettings'}, (result) => {
updateRequest.append('action', 'loadGeneralSettings'); 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}) videonr: result.videonr,
.then((response) => response.json() dbsize: result.dbsize,
.then((result) => { difftagnr: result.difftagnr,
console.log(result); tagsadded: result.tagsadded
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
});
}));
} }
/** /**
* save the selected and typed settings to the backend * save the selected and typed settings to the backend
*/ */
saveSettings() { saveSettings() {
const updateRequest = new FormData(); callAPI('settings.php', {
updateRequest.append('action', 'saveGeneralSettings'); action: 'saveGeneralSettings',
password: this.state.passwordsupport ? this.state.password : '-1',
updateRequest.append('password', this.state.passwordsupport ? this.state.password : '-1'); videopath: this.state.videopath,
updateRequest.append('videopath', this.state.videopath); tvshowpath: this.state.tvshowpath,
updateRequest.append('tvshowpath', this.state.tvshowpath); mediacentername: this.state.mediacentername,
updateRequest.append('mediacentername', this.state.mediacentername); tmdbsupport: this.state.tmdbsupport,
updateRequest.append('tmdbsupport', this.state.tmdbsupport); darkmodeenabled: GlobalInfos.isDarkTheme().toString()
updateRequest.append('darkmodeenabled', GlobalInfos.isDarkTheme().toString()); }, (result) => {
if (result.success) {
fetch('/api/settings.php', {method: 'POST', body: updateRequest}) console.log('successfully saved settings');
.then((response) => response.json() // todo 2020-07-10: popup success
.then((result) => { } else {
if (result.success) { console.log('failed to save settings');
console.log('successfully saved settings'); // todo 2020-07-10: popup error
// todo 2020-07-10: popup success }
} else { });
console.log('failed to save settings');
// todo 2020-07-10: popup error
}
}));
} }
} }

View File

@ -8,6 +8,11 @@
width: 40%; width: 40%;
} }
.customapiform{
margin-top: 15px;
width: 40%;
}
.infoheader { .infoheader {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;

View File

@ -1,7 +1,7 @@
import {shallow} from 'enzyme'; import {shallow} from 'enzyme';
import React from 'react'; import React from 'react';
import GeneralSettings from './GeneralSettings'; import GeneralSettings from './GeneralSettings';
import GlobalInfos from '../../GlobalInfos'; import GlobalInfos from '../../utils/GlobalInfos';
describe('<GeneralSettings/>', function () { describe('<GeneralSettings/>', function () {
it('renders without crashing ', function () { it('renders without crashing ', function () {

View File

@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import style from './MovieSettings.module.css'; import style from './MovieSettings.module.css';
import {callAPI} from '../../utils/Api';
/** /**
* Component for MovieSettings on Settingspage * Component for MovieSettings on Settingspage
@ -50,23 +51,17 @@ class MovieSettings extends React.Component {
this.setState({startbtnDisabled: true}); this.setState({startbtnDisabled: true});
console.log('starting'); console.log('starting');
const request = new FormData();
request.append('action', 'startReindex'); callAPI('settings.php', {action: 'startReindex'}, (result) => {
// fetch all videos available console.log(result);
fetch('/api/settings.php', {method: 'POST', body: request}) if (result.success) {
.then((response) => response.json() console.log('started successfully');
.then((result) => { } else {
console.log(result); console.log('error, reindex already running');
if (result.success) { this.setState({startbtnDisabled: true});
console.log('started successfully'); }
} else { });
console.log('error, reindex already running');
this.setState({startbtnDisabled: true});
}
}))
.catch(() => {
console.log('no connection to backend');
});
if (this.myinterval) { if (this.myinterval) {
clearInterval(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 * This interval function reloads the current status of reindexing from backend
*/ */
updateStatus = () => { updateStatus = () => {
const request = new FormData(); callAPI('settings.php', {action: 'getStatusMessage'}, (result) => {
request.append('action', 'getStatusMessage'); 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}) this.setState({startbtnDisabled: false});
.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');
});
}; };
/** /**
* send request to cleanup db gravity * send request to cleanup db gravity
*/ */
cleanupGravity() { cleanupGravity() {
const request = new FormData(); callAPI('settings.php', {action: 'cleanupGravity'}, (result) => {
request.append('action', 'cleanupGravity'); this.setState({
text: ['successfully cleaned up gravity!']
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');
}); });
});
} }
} }

View File

@ -2,14 +2,18 @@ import React from 'react';
import MovieSettings from './MovieSettings'; import MovieSettings from './MovieSettings';
import GeneralSettings from './GeneralSettings'; import GeneralSettings from './GeneralSettings';
import style from './SettingsPage.module.css'; 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 * The Settingspage handles all kinds of settings for the mediacenter
* and is basically a wrapper for child-tabs * and is basically a wrapper for child-tabs
*/ */
class SettingsPage extends React.Component { class SettingsPage extends React.Component<{}, SettingsPageState> {
constructor(props, context) { constructor(props: Readonly<{}> | {}, context?: any) {
super(props, context); super(props, context);
this.state = { this.state = {
@ -21,7 +25,7 @@ class SettingsPage extends React.Component {
* load the selected tab * load the selected tab
* @returns {JSX.Element|string} the jsx element of the selected tab * @returns {JSX.Element|string} the jsx element of the selected tab
*/ */
getContent() { getContent(): JSX.Element | string {
switch (this.state.currentpage) { switch (this.state.currentpage) {
case 'general': case 'general':
return <GeneralSettings/>; return <GeneralSettings/>;
@ -34,7 +38,7 @@ class SettingsPage extends React.Component {
} }
} }
render() { render() : JSX.Element {
const themestyle = GlobalInfos.getThemeStyle(); const themestyle = GlobalInfos.getThemeStyle();
return ( return (
<div> <div>

View File

@ -6,7 +6,7 @@ import '@testing-library/jest-dom/extend-expect';
import {configure} from 'enzyme'; import {configure} from 'enzyme';
import Adapter from 'enzyme-adapter-react-16'; import Adapter from 'enzyme-adapter-react-16';
import GlobalInfos from './GlobalInfos'; import GlobalInfos from './utils/GlobalInfos';
configure({adapter: new Adapter()}); 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();
})

88
src/utils/Api.ts Normal file
View File

@ -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);
}));
}

View File

@ -1,5 +1,5 @@
import darktheme from './AppDarkTheme.module.css'; import darktheme from '../AppDarkTheme.module.css';
import lighttheme from './AppLightTheme.module.css'; import lighttheme from '../AppLightTheme.module.css';
/** /**
* This class is available for all components in project * This class is available for all components in project
@ -7,7 +7,7 @@ import lighttheme from './AppLightTheme.module.css';
*/ */
class StaticInfos { class StaticInfos {
#darktheme = true; #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 * check if the current theme is the dark theme
@ -37,7 +37,7 @@ class StaticInfos {
* set the global Viewbinding for the main Navigation * set the global Viewbinding for the main Navigation
* @param cb * @param cb
*/ */
setViewBinding(cb){ setViewBinding(cb) {
this.#viewbinding = cb; this.#viewbinding = cb;
} }
@ -45,7 +45,7 @@ class StaticInfos {
* return the Viewbinding for main navigation * return the Viewbinding for main navigation
* @returns {StaticInfos.viewbinding} * @returns {StaticInfos.viewbinding}
*/ */
getViewBinding(){ getViewBinding() {
return this.#viewbinding; return this.#viewbinding;
} }
} }

25
tsconfig.json Normal file
View File

@ -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"
]
}