17 Commits

Author SHA1 Message Date
c0405cd79a Release version v0.1.3 2021-03-22 20:24:10 +01:00
533e319ea0 Merge branch 'error_wrong_pwd_rm_map_files' into 'master'
Error message when password wrong

Closes #66 and #46

See merge request lukas/openmediacenter!42
2021-03-22 19:07:32 +00:00
a2ac188423 remove .map files from production build
show error message if wrong password was entered.
2021-03-22 19:07:32 +00:00
fe349b1fd2 use creat if not exists syntax also on index entries 2021-03-22 18:39:34 +01:00
973a469410 restart service on debian package update instead of only starting it 2021-03-22 17:55:03 +01:00
da04c30148 Merge branch 'reindex_performance_improvement' into 'master'
Reindex Performance Imprvement

Closes #42

See merge request lukas/openmediacenter!43
2021-03-22 16:37:08 +00:00
137d7ed49d use single db query to check whether a url exists already in db or not 2021-03-22 17:22:34 +01:00
f2ec5b644d no implicit any - force return
add some comments
2021-03-19 19:10:13 +01:00
5dbbd34d3a add environment variable to set custom movie backend url
edit readme
handle enter event on AuthenticationPage
2021-03-16 20:44:32 +01:00
e412f699f7 integrate automatic token refresh if current one is invalid
--> load login page if token can't be refreshed
2021-03-16 20:13:12 +01:00
a51176fb93 fix filenames with special characters aren't urlencoded
year parsing does not remove parentheses
2021-03-14 19:01:40 +01:00
b2b0e3a917 fix lukas/openmediacenter#63 2021-03-14 17:29:33 +01:00
e4990bd24e fix not deletable video if a actor is defined
fix wrongly displaying icon if no one exists --> new icon
2021-03-14 16:56:26 +01:00
283d3dc59b Merge branch 'eslint' into 'master'
Eslint

See merge request lukas/openmediacenter!41
2021-03-14 14:51:53 +00:00
1fc67365f0 use eslint to lint project
drop code quality job
2021-03-14 14:51:53 +00:00
ba2704b285 Merge branch 'passwordpage' into 'master'
PasswordPage if enabled in settings

See merge request lukas/openmediacenter!40
2021-03-14 12:49:25 +00:00
059b0af6e7 fix incorrect gui refresh if theme is changed
implement custom clientstore
add new Password page
if password is set force entering password to successfully receive the token
add a new unsafe api call for init call only
2021-03-14 12:49:24 +00:00
62 changed files with 2202 additions and 934 deletions

292
.eslintrc.js Normal file
View File

@ -0,0 +1,292 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
module.exports = {
env: {
es6: true
},
parserOptions: {
sourceType: 'module'
},
extends: [
'plugin:prettier/recommended', // https://github.com/prettier/eslint-plugin-prettier#recommended-configuration
'prettier'
],
plugins: ['eslint-comments', 'react', 'react-hooks', 'jest'],
settings: {
react: {
version: 'detect'
}
},
ignorePatterns: ['node_modules/', '**/*.js'],
overrides: [
{
files: ['*.ts', '*.tsx'],
parser: '@typescript-eslint/parser',
plugins: ['@typescript-eslint/eslint-plugin'],
rules: {
'@typescript-eslint/no-unused-vars': ['error', {argsIgnorePattern: '^_'}],
'no-unused-vars': 'off'
}
}
],
// Map from global var to bool specifying if it can be redefined
globals: {
jest: true,
__DEV__: true,
__dirname: false,
__fbBatchedBridgeConfig: false,
AbortController: false,
alert: false,
cancelAnimationFrame: false,
cancelIdleCallback: false,
clearImmediate: true,
clearInterval: false,
clearTimeout: false,
console: false,
document: false,
ErrorUtils: false,
escape: false,
Event: false,
EventTarget: false,
exports: false,
fetch: false,
FileReader: false,
FormData: false,
global: false,
Headers: false,
Intl: false,
Map: true,
module: false,
navigator: false,
process: false,
Promise: true,
requestAnimationFrame: true,
requestIdleCallback: true,
require: false,
Set: true,
setImmediate: true,
setInterval: false,
setTimeout: false,
URL: false,
URLSearchParams: false,
WebSocket: true,
window: false,
XMLHttpRequest: false,
JSX: true,
KeyboardEvent: true,
MouseEvent: true,
Node: true,
HTMLDivElement: true,
HTMLInputElement: true
},
rules: {
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/explicit-function-return-type": "error",
// General
'comma-dangle': [1, 'never'], // allow or disallow trailing commas
'no-cond-assign': 1, // disallow assignment in conditional expressions
'no-console': 0, // disallow use of console (off by default in the node environment)
'no-const-assign': 2, // disallow assignment to const-declared variables
'no-constant-condition': 0, // disallow use of constant expressions in conditions
'no-control-regex': 1, // disallow control characters in regular expressions
'no-debugger': 1, // disallow use of debugger
'no-dupe-class-members': 2, // Disallow duplicate name in class members
'no-dupe-keys': 2, // disallow duplicate keys when creating object literals
'no-empty': 0, // disallow empty statements
'no-ex-assign': 1, // disallow assigning to the exception in a catch block
'no-extra-boolean-cast': 1, // disallow double-negation boolean casts in a boolean context
'no-extra-parens': 0, // disallow unnecessary parentheses (off by default)
'no-extra-semi': 1, // disallow unnecessary semicolons
'no-func-assign': 1, // disallow overwriting functions written as function declarations
'no-inner-declarations': 0, // disallow function or variable declarations in nested blocks
'no-invalid-regexp': 1, // disallow invalid regular expression strings in the RegExp constructor
'no-negated-in-lhs': 1, // disallow negation of the left operand of an in expression
'no-obj-calls': 1, // disallow the use of object properties of the global object (Math and JSON) as functions
'no-regex-spaces': 1, // disallow multiple spaces in a regular expression literal
'no-reserved-keys': 0, // disallow reserved words being used as object literal keys (off by default)
'no-sparse-arrays': 1, // disallow sparse arrays
'no-unreachable': 2, // disallow unreachable statements after a return, throw, continue, or break statement
'use-isnan': 1, // disallow comparisons with the value NaN
'valid-jsdoc': 0, // Ensure JSDoc comments are valid (off by default)
'valid-typeof': 1, // Ensure that the results of typeof are compared against a valid string
// Best Practices
// These are rules designed to prevent you from making mistakes. They either prescribe a better way of doing something or help you avoid footguns.
'block-scoped-var': 0, // treat var statements as if they were block scoped (off by default)
complexity: 0, // specify the maximum cyclomatic complexity allowed in a program (off by default)
'consistent-return': 0, // require return statements to either always or never specify values
curly: 1, // specify curly brace conventions for all control statements
'default-case': 0, // require default case in switch statements (off by default)
'dot-notation': 1, // encourages use of dot notation whenever possible
eqeqeq: [1, 'allow-null'], // require the use of === and !==
'guard-for-in': 0, // make sure for-in loops have an if statement (off by default)
'no-alert': 1, // disallow the use of alert, confirm, and prompt
'no-caller': 1, // disallow use of arguments.caller or arguments.callee
'no-div-regex': 1, // disallow division operators explicitly at beginning of regular expression (off by default)
'no-else-return': 0, // disallow else after a return in an if (off by default)
'no-eq-null': 0, // disallow comparisons to null without a type-checking operator (off by default)
'no-eval': 2, // disallow use of eval()
'no-extend-native': 1, // disallow adding to native types
'no-extra-bind': 1, // disallow unnecessary function binding
'no-fallthrough': 1, // disallow fallthrough of case statements
'no-floating-decimal': 1, // disallow the use of leading or trailing decimal points in numeric literals (off by default)
'no-implied-eval': 1, // disallow use of eval()-like methods
'no-labels': 1, // disallow use of labeled statements
'no-iterator': 1, // disallow usage of __iterator__ property
'no-lone-blocks': 1, // disallow unnecessary nested blocks
'no-loop-func': 0, // disallow creation of functions within loops
'no-multi-str': 0, // disallow use of multiline strings
'no-native-reassign': 0, // disallow reassignments of native objects
'no-new': 1, // disallow use of new operator when not part of the assignment or comparison
'no-new-func': 2, // disallow use of new operator for Function object
'no-new-wrappers': 1, // disallows creating new instances of String,Number, and Boolean
'no-octal': 1, // disallow use of octal literals
'no-octal-escape': 1, // disallow use of octal escape sequences in string literals, such as var foo = "Copyright \251";
'no-proto': 1, // disallow usage of __proto__ property
'no-redeclare': 0, // disallow declaring the same variable more then once
'no-return-assign': 1, // disallow use of assignment in return statement
'no-script-url': 1, // disallow use of javascript: urls.
'no-self-compare': 1, // disallow comparisons where both sides are exactly the same (off by default)
'no-sequences': 1, // disallow use of comma operator
'no-unused-expressions': 0, // disallow usage of expressions in statement position
'no-useless-escape': 1, // disallow escapes that don't have any effect in literals
'no-void': 1, // disallow use of void operator (off by default)
'no-warning-comments': 0, // disallow usage of configurable warning terms in comments": 1, // e.g. TODO or FIXME (off by default)
'no-with': 1, // disallow use of the with statement
radix: 1, // require use of the second argument for parseInt() (off by default)
'semi-spacing': 1, // require a space after a semi-colon
'vars-on-top': 0, // requires to declare all vars on top of their containing scope (off by default)
'wrap-iife': 0, // require immediate function invocation to be wrapped in parentheses (off by default)
yoda: 1, // require or disallow Yoda conditions
// Variables
// These rules have to do with variable declarations.
'no-catch-shadow': 1, // disallow the catch clause parameter name being the same as a variable in the outer scope (off by default in the node environment)
'no-delete-var': 1, // disallow deletion of variables
'no-label-var': 1, // disallow labels that share a name with a variable
'no-shadow': 1, // disallow declaration of variables already declared in the outer scope
'no-shadow-restricted-names': 1, // disallow shadowing of names such as arguments
'no-undef': 2, // disallow use of undeclared variables unless mentioned in a /*global */ block
'no-undefined': 0, // disallow use of undefined variable (off by default)
'no-undef-init': 1, // disallow use of undefined when initializing variables
'no-unused-vars': [1, {vars: 'all', args: 'none', ignoreRestSiblings: true}], // disallow declaration of variables that are not used in the code
'no-use-before-define': 0, // disallow use of variables before they are defined
// Node.js
// These rules are specific to JavaScript running on Node.js.
'handle-callback-err': 1, // enforces error handling in callbacks (off by default) (on by default in the node environment)
'no-mixed-requires': 1, // disallow mixing regular variable and require declarations (off by default) (on by default in the node environment)
'no-new-require': 1, // disallow use of new operator with the require function (off by default) (on by default in the node environment)
'no-path-concat': 1, // disallow string concatenation with __dirname and __filename (off by default) (on by default in the node environment)
'no-process-exit': 0, // disallow process.exit() (on by default in the node environment)
'no-restricted-modules': 1, // restrict usage of specified node modules (off by default)
'no-sync': 0, // disallow use of synchronous methods (off by default)
// ESLint Comments Plugin
// The following rules are made available via `eslint-plugin-eslint-comments`
'eslint-comments/no-aggregating-enable': 1, // disallows eslint-enable comments for multiple eslint-disable comments
'eslint-comments/no-unlimited-disable': 1, // disallows eslint-disable comments without rule names
'eslint-comments/no-unused-disable': 1, // disallow disables that don't cover any errors
'eslint-comments/no-unused-enable': 1, // // disallow enables that don't enable anything or enable rules that weren't disabled
// Stylistic Issues
// These rules are purely matters of style and are quite subjective.
'key-spacing': 0,
'keyword-spacing': 1, // enforce spacing before and after keywords
'jsx-quotes': [1, 'prefer-single'], // enforces the usage of double quotes for all JSX attribute values which doesnt contain a double quote
'comma-spacing': 0,
'no-multi-spaces': 0,
'brace-style': 0, // enforce one true brace style (off by default)
camelcase: 1, // require camel case names
'consistent-this': 1, // enforces consistent naming when capturing the current execution context (off by default)
'eol-last': 1, // enforce newline at the end of file, with no multiple empty lines
'func-names': 0, // require function expressions to have a name (off by default)
'func-style': 0, // enforces use of function declarations or expressions (off by default)
'new-cap': 0, // require a capital letter for constructors
'new-parens': 1, // disallow the omission of parentheses when invoking a constructor with no arguments
'no-nested-ternary': 0, // disallow nested ternary expressions (off by default)
'no-array-constructor': 1, // disallow use of the Array constructor
'no-empty-character-class': 1, // disallow the use of empty character classes in regular expressions
'no-lonely-if': 0, // disallow if as the only statement in an else block (off by default)
'no-new-object': 1, // disallow use of the Object constructor
'no-spaced-func': 1, // disallow space between function identifier and application
'no-ternary': 0, // disallow the use of ternary operators (off by default)
'no-trailing-spaces': 1, // disallow trailing whitespace at the end of lines
'no-underscore-dangle': 0, // disallow dangling underscores in identifiers
'no-mixed-spaces-and-tabs': 1, // disallow mixed spaces and tabs for indentation
quotes: [1, 'single', 'avoid-escape'], // specify whether double or single quotes should be used
'quote-props': 0, // require quotes around object literal property names (off by default)
semi: 1, // require or disallow use of semicolons instead of ASI
'sort-vars': 0, // sort variables within the same declaration block (off by default)
'space-in-brackets': 0, // require or disallow spaces inside brackets (off by default)
'space-in-parens': 0, // require or disallow spaces inside parentheses (off by default)
'space-infix-ops': 1, // require spaces around operators
'space-unary-ops': [1, {words: true, nonwords: false}], // require or disallow spaces before/after unary operators (words on by default, nonwords off by default)
'max-nested-callbacks': 0, // specify the maximum depth callbacks can be nested (off by default)
'one-var': 0, // allow just one var statement per function (off by default)
'wrap-regex': 0, // require regex literals to be wrapped in parentheses (off by default)
// Legacy
// The following rules are included for compatibility with JSHint and JSLint. While the names of the rules may not match up with the JSHint/JSLint counterpart, the functionality is the same.
'max-depth': 0, // specify the maximum depth that blocks can be nested (off by default)
'max-len': 0, // specify the maximum length of a line in your program (off by default)
'max-params': 0, // limits the number of parameters that can be used in the function declaration. (off by default)
'max-statements': 0, // specify the maximum number of statement allowed in a function (off by default)
'no-bitwise': 1, // disallow use of bitwise operators (off by default)
'no-plusplus': 0, // disallow use of unary operators, ++ and -- (off by default)
// React Plugin
// The following rules are made available via `eslint-plugin-react`.
'react/display-name': 0,
'react/jsx-boolean-value': 0,
'react/jsx-no-comment-textnodes': 2,
'react/jsx-no-duplicate-props': 2,
'react/jsx-no-undef': 2,
'react/jsx-sort-props': 0,
'react/jsx-uses-react': 1,
'react/jsx-uses-vars': 1,
'react/no-did-mount-set-state': 1,
'react/no-did-update-set-state': 1,
'react/no-multi-comp': 0,
'react/no-string-refs': 1,
'react/no-unknown-property': 0,
'react/prop-types': 0,
'react/react-in-jsx-scope': 1,
'react/self-closing-comp': 1,
'react/wrap-multilines': 0,
// React-Hooks Plugin
// The following rules are made available via `eslint-plugin-react-hooks`
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'error',
// Jest Plugin
// The following rules are made available via `eslint-plugin-jest`.
'jest/no-disabled-tests': 1,
'jest/no-focused-tests': 1,
'jest/no-identical-title': 1,
'jest/valid-expect': 1
}
};

View File

@ -6,18 +6,13 @@ stages:
- packaging - packaging
- deploy - deploy
include:
- template: Code-Quality.gitlab-ci.yml
variables:
SAST_DISABLE_DIND: "true"
Minimize_Frontend: Minimize_Frontend:
stage: build stage: build
before_script: before_script:
- yarn install --cache-folder .yarn - yarn install --cache-folder .yarn
script: script:
- yarn run build - yarn run build
- rm build/*/*/*.map
artifacts: artifacts:
expire_in: 2 days expire_in: 2 days
paths: paths:
@ -68,9 +63,21 @@ Backend_Tests:
reports: reports:
junit: ./apiGo/report.xml junit: ./apiGo/report.xml
code_quality: lint:
tags: stage: test
- dind before_script:
- yarn install --cache-folder .yarn
script:
- yarn run lint
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- .yarn/
- ./node_modules/
artifacts:
reports:
codequality: gl-codequality.json
needs: []
Debian_Server: Debian_Server:
stage: packaging stage: packaging

10
.prettierrc.js Normal file
View File

@ -0,0 +1,10 @@
module.exports = {
bracketSpacing: false,
jsxBracketSameLine: true,
singleQuote: true,
tabWidth: 4,
trailingComma: 'none',
printWidth: 135,
semi: true,
jsxSingleQuote: true
};

View File

@ -22,24 +22,30 @@ and in dark mode:
![](https://i.ibb.co/xzhdsbJ/Screenshot-20200812-172926.png) ![](https://i.ibb.co/xzhdsbJ/Screenshot-20200812-172926.png)
## Installation ## Installation
First of all clone the repository.
`git clone https://gitlab.heili.eu/lukas/openmediacenter.git` Download the latest release .deb file from the Releases page and install it via `apt install ./OpenMediaCenter-0.1.x_amd64.deb`
Then build a production build via npm.
`npm run build`
Afterwards you can copy the content of the generated `build` folder as well as the `api` folder to your webserver root.
You need also to setup a Database with the structure described in [SQL Style Reference](https://gitlab.heili.eu/lukas/openmediacenter/-/blob/master/database.sql).
The login data to this database needs to be specified in the `api/Database.php` file.
Now you could optionally check if the service is up and running: `systemctl status OpenMediaCenter`
## Usage ## Usage
Now you can access your MediaCenter via your servers global ip (: Now you can access your MediaCenter via your servers global ip on port 8080 (:
At the settings tab you can set the correct videopath on server and click reindex afterwards. At the settings tab you can set the correct videopath on server and click reindex afterwards.
## Development
Build and start the go backend:
`go build`
Start frontend dev server:
`npm start`
### Environent Variables:
`REACT_APP_CUST_BACK_DOMAIN` :: Set a custom movie domain
## Contact ## Contact
Any contribution is appreciated. Any contribution is appreciated.
Feel free to contact me (lukas.heiligenbrunner@gmail.com), open an issue or request a new feature. Feel free to contact me (lukas.heiligenbrunner@gmail.com), open an issue or request a new feature.

View File

@ -16,6 +16,7 @@ const (
TagNode = iota TagNode = iota
SettingsNode = iota SettingsNode = iota
ActorNode = iota ActorNode = iota
InitNode = iota
) )
type actionStruct struct { type actionStruct struct {
@ -42,6 +43,9 @@ func ServerInit(port uint16) {
http.Handle(APIPREFIX+"/settings", oauth.ValidateToken(settingsHandler)) http.Handle(APIPREFIX+"/settings", oauth.ValidateToken(settingsHandler))
http.Handle(APIPREFIX+"/actor", oauth.ValidateToken(actorHandler)) http.Handle(APIPREFIX+"/actor", oauth.ValidateToken(actorHandler))
// initialization api calls to check if password is neccessaray
http.Handle(APIPREFIX+"/init", http.HandlerFunc(initHandler))
// initialize oauth service and add corresponding auth routes // initialize oauth service and add corresponding auth routes
oauth.InitOAuth() oauth.InitOAuth()
@ -85,6 +89,10 @@ func settingsHandler(rw http.ResponseWriter, req *http.Request) {
handlefunc(rw, req, SettingsNode) handlefunc(rw, req, SettingsNode)
} }
func initHandler(rw http.ResponseWriter, req *http.Request) {
handlefunc(rw, req, InitNode)
}
func handlefunc(rw http.ResponseWriter, req *http.Request, node int) { func handlefunc(rw http.ResponseWriter, req *http.Request, node int) {
// only allow post requests // only allow post requests
if req.Method != "POST" { if req.Method != "POST" {

33
apiGo/api/Init.go Normal file
View File

@ -0,0 +1,33 @@
package api
import (
"encoding/json"
"openmediacenter/apiGo/database/settings"
)
func AddInitHandlers() {
passwordNeeded()
}
func passwordNeeded() {
AddHandler("loadInitialData", InitNode, nil, func() []byte {
sett := settings.LoadSettings()
type InitialDataTypeResponse struct {
DarkMode bool
Pasword bool
MediacenterName string
VideoPath string
}
res := InitialDataTypeResponse{
DarkMode: sett.DarkMode,
Pasword: sett.Pasword != "-1",
MediacenterName: sett.Mediacenter_name,
VideoPath: sett.VideoPath,
}
str, _ := json.Marshal(res)
return str
})
}

View File

@ -1,8 +1,6 @@
package api package api
import ( import (
"encoding/json"
"fmt"
"openmediacenter/apiGo/api/types" "openmediacenter/apiGo/api/types"
"openmediacenter/apiGo/database" "openmediacenter/apiGo/database"
"openmediacenter/apiGo/videoparser" "openmediacenter/apiGo/videoparser"
@ -15,41 +13,6 @@ func AddSettingsHandlers() {
} }
func getSettingsFromDB() { func getSettingsFromDB() {
AddHandler("loadInitialData", SettingsNode, nil, func() []byte {
query := "SELECT DarkMode, password, mediacenter_name, video_path from settings"
type InitialDataType struct {
DarkMode int
Pasword int
Mediacenter_name string
VideoPath string
}
result := InitialDataType{}
err := database.QueryRow(query).Scan(&result.DarkMode, &result.Pasword, &result.Mediacenter_name, &result.VideoPath)
if err != nil {
fmt.Println("error while parsing db data: " + err.Error())
}
type InitialDataTypeResponse struct {
DarkMode bool
Pasword bool
Mediacenter_name string
VideoPath string
}
res := InitialDataTypeResponse{
DarkMode: result.DarkMode != 0,
Pasword: result.Pasword != -1,
Mediacenter_name: result.Mediacenter_name,
VideoPath: result.VideoPath,
}
str, _ := json.Marshal(res)
return str
})
AddHandler("loadGeneralSettings", SettingsNode, nil, func() []byte { AddHandler("loadGeneralSettings", SettingsNode, nil, func() []byte {
result := database.GetSettings() result := database.GetSettings()
return jsonify(result) return jsonify(result)

View File

@ -227,7 +227,20 @@ func addToVideoHandlers() {
MovieId int MovieId int
} }
AddHandler("deleteVideo", VideoNode, &dv, func() []byte { AddHandler("deleteVideo", VideoNode, &dv, func() []byte {
query := fmt.Sprintf("DELETE FROM videos WHERE movie_id=%d", dv.MovieId) // delete tag constraints
query := fmt.Sprintf("DELETE FROM video_tags WHERE video_id=%d", dv.MovieId)
err := database.Edit(query)
// delete actor constraints
query = fmt.Sprintf("DELETE FROM actors_videos WHERE video_id=%d", dv.MovieId)
err = database.Edit(query)
// respond only if result not successful
if err != nil {
return database.ManualSuccessResponse(err)
}
query = fmt.Sprintf("DELETE FROM videos WHERE movie_id=%d", dv.MovieId)
return database.SuccessQuery(query) return database.SuccessQuery(query)
}) })
} }

View File

@ -0,0 +1,57 @@
package oauth
import (
"gopkg.in/oauth2.v3"
"openmediacenter/apiGo/database/settings"
)
type CustomClientStore struct {
oauth2.ClientStore
}
type CustomClientInfo struct {
oauth2.ClientInfo
ID string
Secret string
Domain string
UserID string
}
func NewCustomStore() oauth2.ClientStore {
s := new(CustomClientStore)
return s
}
func (a *CustomClientStore) GetByID(id string) (oauth2.ClientInfo, error) {
password := settings.GetPassword()
// if password not set assign default password
if password == nil {
defaultpassword := "openmediacenter"
password = &defaultpassword
}
clientinfo := CustomClientInfo{
ID: "openmediacenter",
Secret: *password,
Domain: "http://localhost:8081",
UserID: "openmediacenter",
}
return &clientinfo, nil
}
func (a *CustomClientInfo) GetID() string {
return a.ID
}
func (a *CustomClientInfo) GetSecret() string {
return a.Secret
}
func (a *CustomClientInfo) GetDomain() string {
return a.Domain
}
func (a *CustomClientInfo) GetUserID() string {
return a.UserID
}

View File

@ -3,7 +3,6 @@ package oauth
import ( import (
"gopkg.in/oauth2.v3/errors" "gopkg.in/oauth2.v3/errors"
"gopkg.in/oauth2.v3/manage" "gopkg.in/oauth2.v3/manage"
"gopkg.in/oauth2.v3/models"
"gopkg.in/oauth2.v3/server" "gopkg.in/oauth2.v3/server"
"gopkg.in/oauth2.v3/store" "gopkg.in/oauth2.v3/store"
"log" "log"
@ -17,15 +16,10 @@ func InitOAuth() {
// token store // token store
manager.MustTokenStorage(store.NewMemoryTokenStore()) manager.MustTokenStorage(store.NewMemoryTokenStore())
clientStore := store.NewClientStore() // create new secretstore
// todo we need to check here if a password is enabled in db -- when yes set it here! clientStore := NewCustomStore()
clientStore.Set("openmediacenter", &models.Client{
ID: "openmediacenter",
Secret: "openmediacenter",
Domain: "http://localhost:8081",
})
manager.MapClientStorage(clientStore) manager.MapClientStorage(clientStore)
srv = server.NewServer(server.NewConfig(), manager) srv = server.NewServer(server.NewConfig(), manager)
srv.SetClientInfoHandler(server.ClientFormHandler) srv.SetClientInfoHandler(server.ClientFormHandler)
manager.SetRefreshTokenCfg(manage.DefaultRefreshTokenCfg) manager.SetRefreshTokenCfg(manage.DefaultRefreshTokenCfg)

View File

@ -0,0 +1,49 @@
package settings
import (
"fmt"
"openmediacenter/apiGo/database"
)
func GetPassword() *string {
pwd := LoadSettings().Pasword
if pwd == "-1" {
return nil
} else {
return &pwd
}
}
type SettingsType struct {
DarkMode bool
Pasword string
Mediacenter_name string
VideoPath string
}
func LoadSettings() *SettingsType {
query := "SELECT DarkMode, password, mediacenter_name, video_path from settings"
type RawSettingsType struct {
DarkMode int
Pasword string
Mediacenter_name string
VideoPath string
}
result := RawSettingsType{}
err := database.QueryRow(query).Scan(&result.DarkMode, &result.Pasword, &result.Mediacenter_name, &result.VideoPath)
if err != nil {
fmt.Println("error while parsing db data: " + err.Error())
}
res := SettingsType{
DarkMode: result.DarkMode != 0,
Pasword: result.Pasword,
Mediacenter_name: result.Mediacenter_name,
VideoPath: result.VideoPath,
}
return &res
}

View File

@ -3,7 +3,6 @@ module openmediacenter/apiGo
go 1.16 go 1.16
require ( require (
github.com/go-session/session v3.1.2+incompatible
github.com/go-sql-driver/mysql v1.5.0 github.com/go-sql-driver/mysql v1.5.0
gopkg.in/oauth2.v3 v3.12.0 gopkg.in/oauth2.v3 v3.12.0
) )

View File

@ -11,7 +11,6 @@ github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/gavv/httpexpect v2.0.0+incompatible h1:1X9kcRshkSKEjNJJxX9Y9mQ5BRfbxU5kORdjhlA1yX8= github.com/gavv/httpexpect v2.0.0+incompatible h1:1X9kcRshkSKEjNJJxX9Y9mQ5BRfbxU5kORdjhlA1yX8=
github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc= github.com/gavv/httpexpect v2.0.0+incompatible/go.mod h1:x+9tiU1YnrOvnB725RkpoLv1M62hOWzwo5OXotisrKc=
github.com/go-session/session v3.1.2+incompatible h1:yStchEObKg4nk2F7JGE7KoFIrA/1Y078peagMWcrncg=
github.com/go-session/session v3.1.2+incompatible/go.mod h1:8B3iivBQjrz/JtC68Np2T1yBBLxTan3mn/3OM0CyRt0= github.com/go-session/session v3.1.2+incompatible/go.mod h1:8B3iivBQjrz/JtC68Np2T1yBBLxTan3mn/3OM0CyRt0=
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=

View File

@ -26,6 +26,7 @@ func main() {
api.AddSettingsHandlers() api.AddSettingsHandlers()
api.AddTagHandlers() api.AddTagHandlers()
api.AddActorsHandlers() api.AddActorsHandlers()
api.AddInitHandlers()
api.ServerInit(8081) api.ServerInit(8081)
} }

View File

@ -11,6 +11,7 @@ import (
"os/exec" "os/exec"
"regexp" "regexp"
"strconv" "strconv"
"strings"
) )
var mSettings types.SettingsType var mSettings types.SettingsType
@ -41,7 +42,12 @@ func ReIndexVideos(path []string, sett types.SettingsType) {
fmt.Printf("FFMPEG support: %t\n", mExtDepsAvailable.FFMpeg) fmt.Printf("FFMPEG support: %t\n", mExtDepsAvailable.FFMpeg)
fmt.Printf("MediaInfo support: %t\n", mExtDepsAvailable.MediaInfo) fmt.Printf("MediaInfo support: %t\n", mExtDepsAvailable.MediaInfo)
for _, s := range path { // filter out those urls which are already existing in db
nonExisting := filterExisting(path)
fmt.Printf("There are %d videos not existing in db.\n", len(*nonExisting))
for _, s := range *nonExisting {
processVideo(s) processVideo(s)
} }
@ -51,6 +57,45 @@ func ReIndexVideos(path []string, sett types.SettingsType) {
fmt.Println("Reindexing finished!") fmt.Println("Reindexing finished!")
} }
// filter those entries from array which are already existing!
func filterExisting(paths []string) *[]string {
var nameStr string
// build the query string with files on disk
for i, s := range paths {
// escape ' in url name
s = strings.Replace(s, "'", "\\'", -1)
nameStr += "SELECT '" + s + "' "
// if first index add as url
if i == 0 {
nameStr += "AS url "
}
// if not last index add union all
if i != len(paths)-1 {
nameStr += "UNION ALL "
}
}
query := fmt.Sprintf("SELECT * FROM (%s) urls WHERE urls.url NOT IN(SELECT movie_url FROM videos)", nameStr)
rows := database.Query(query)
var resultarr []string
// parse the result rows into a array
for rows.Next() {
var url string
err := rows.Scan(&url)
if err != nil {
continue
}
resultarr = append(resultarr, url)
}
rows.Close()
return &resultarr
}
func processVideo(fileNameOrig string) { func processVideo(fileNameOrig string) {
fmt.Printf("Processing %s video-", fileNameOrig) fmt.Printf("Processing %s video-", fileNameOrig)
@ -58,18 +103,11 @@ func processVideo(fileNameOrig string) {
r, _ := regexp.Compile(`\.[a-zA-Z0-9]+$`) r, _ := regexp.Compile(`\.[a-zA-Z0-9]+$`)
fileName := r.ReplaceAllString(fileNameOrig, "") fileName := r.ReplaceAllString(fileNameOrig, "")
// match the year and cut year from name
year, fileName := matchYear(fileName) year, fileName := matchYear(fileName)
// now we should look if this video already exists in db fmt.Printf("The Video %s doesn't exist! Adding it to database.\n", fileName)
query := "SELECT * FROM videos WHERE movie_name = ?"
err := database.QueryRow(query, fileName).Scan()
if err == sql.ErrNoRows {
fmt.Printf("The Video %s does't exist! Adding it to database.\n", fileName)
addVideo(fileName, fileNameOrig, year) addVideo(fileName, fileNameOrig, year)
} else {
fmt.Println(" :existing!")
}
} }
// add a video to the database // add a video to the database
@ -139,8 +177,9 @@ func matchYear(fileName string) (int, string) {
if len(years) == 0 { if len(years) == 0 {
return -1, fileName return -1, fileName
} }
yearStr := years[len(years)-1]
year, err := strconv.Atoi(years[len(years)-1]) // get last year occurance and cut first and last char
year, err := strconv.Atoi(yearStr[1 : len(yearStr)-1])
if err != nil { if err != nil {
return -1, fileName return -1, fileName
@ -172,7 +211,11 @@ func parseFFmpegPic(fileName string) (*string, error) {
return nil, err return nil, err
} }
backpic64 := "data:image/jpeg;base64," + base64.StdEncoding.EncodeToString(stdout) strEncPic := base64.StdEncoding.EncodeToString(stdout)
if strEncPic == "" {
return nil, nil
}
backpic64 := fmt.Sprintf("data:image/jpeg;base64,%s", strEncPic)
return &backpic64, nil return &backpic64, nil
} }

View File

@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"net/http" "net/http"
"net/url"
"regexp" "regexp"
) )
@ -43,8 +44,9 @@ type TMDBGenre struct {
} }
func SearchVideo(MovieName string, year int) *VideoTMDB { func SearchVideo(MovieName string, year int) *VideoTMDB {
url := fmt.Sprintf("%ssearch/movie?api_key=%s&query=%s", baseUrl, apiKey, MovieName) fmt.Printf("Searching TMDB for: Moviename: %s, year:%d \n", MovieName, year)
resp, err := http.Get(url) queryURL := fmt.Sprintf("%ssearch/movie?api_key=%s&query=%s", baseUrl, apiKey, url.QueryEscape(MovieName))
resp, err := http.Get(queryURL)
if err != nil { if err != nil {
fmt.Println(err.Error()) fmt.Println(err.Error())
return nil return nil
@ -63,6 +65,11 @@ func SearchVideo(MovieName string, year int) *VideoTMDB {
fmt.Println(len(t.Results)) fmt.Println(len(t.Results))
// if there was no match with tmdb return 0
if len(t.Results) == 0 {
return nil
}
var tmdbVid tmdbVidResult var tmdbVid tmdbVidResult
if year != -1 { if year != -1 {
for _, result := range t.Results { for _, result := range t.Results {

View File

@ -48,10 +48,10 @@ create table if not exists actors_videos
foreign key (video_id) references videos (movie_id) foreign key (video_id) references videos (movie_id)
); );
create index actors_videos_actor_id_index create index if not exists actors_videos_actor_id_index
on actors_videos (actor_id); on actors_videos (actor_id);
create index actors_videos_video_id_index create index if not exists actors_videos_video_id_index
on actors_videos (video_id); on actors_videos (video_id);
create table if not exists video_tags create table if not exists video_tags

View File

@ -18,4 +18,4 @@ chown -R www-data:www-data /var/www/openmediacenter
systemctl restart nginx systemctl restart nginx
systemctl enable OpenMediaCenter.service systemctl enable OpenMediaCenter.service
systemctl start OpenMediaCenter.service systemctl restart OpenMediaCenter.service

View File

@ -1,8 +1,7 @@
{ {
"name": "openmediacenter", "name": "openmediacenter",
"version": "0.1.2", "version": "0.1.3",
"private": true, "private": true,
"main": "public/electron.js",
"author": { "author": {
"email": "lukas.heiligenbrunner@gmail.com", "email": "lukas.heiligenbrunner@gmail.com",
"name": "Lukas Heiligenbrunner", "name": "Lukas Heiligenbrunner",
@ -24,8 +23,9 @@
}, },
"scripts": { "scripts": {
"start": "react-scripts start", "start": "react-scripts start",
"build": "react-scripts build", "build": "CI=false react-scripts build",
"test": "CI=true react-scripts test --reporters=jest-junit --verbose --silent --coverage --reporters=default" "test": "CI=true react-scripts test --reporters=jest-junit --verbose --silent --coverage --reporters=default",
"lint": "eslint --format gitlab src/"
}, },
"jest": { "jest": {
"collectCoverageFrom": [ "collectCoverageFrom": [
@ -39,23 +39,6 @@
}, },
"proxy": "http://127.0.0.1:8081", "proxy": "http://127.0.0.1:8081",
"homepage": "/", "homepage": "/",
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
],
"overrides": [
{
"files": [
"**/*.ts?(x)"
],
"rules": {
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/explicit-function-return-type": "error"
}
}
]
},
"browserslist": { "browserslist": {
"production": [ "production": [
">0.2%", ">0.2%",
@ -78,9 +61,21 @@
"@types/react-dom": "^17.0.1", "@types/react-dom": "^17.0.1",
"@types/react-router": "5.1.12", "@types/react-router": "5.1.12",
"@types/react-router-dom": "^5.1.6", "@types/react-router-dom": "^5.1.6",
"@typescript-eslint/eslint-plugin": "^4.17.0",
"@typescript-eslint/parser": "^4.17.0",
"enzyme": "^3.11.0", "enzyme": "^3.11.0",
"enzyme-adapter-react-16": "^1.15.5", "enzyme-adapter-react-16": "^1.15.5",
"eslint": "^7.22.0",
"eslint-config-prettier": "^8.1.0",
"eslint-formatter-gitlab": "^2.2.0",
"eslint-plugin-eslint-comments": "^3.2.0",
"eslint-plugin-jest": "^24.3.1",
"eslint-plugin-prettier": "^3.3.1",
"eslint-plugin-react": "^7.22.0",
"eslint-plugin-react-hooks": "^4.2.0",
"jest-junit": "^12.0.0", "jest-junit": "^12.0.0",
"prettier": "^2.2.1",
"prettier-config": "^1.0.0",
"react-scripts": "4.0.3" "react-scripts": "4.0.3"
} }
} }

View File

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import App from './App'; import App from './App';
import {shallow} from 'enzyme'; import {shallow} from 'enzyme';
import GlobalInfos from "./utils/GlobalInfos";
describe('<App/>', function () { describe('<App/>', function () {
it('renders without crashing ', function () { it('renders without crashing ', function () {
@ -10,34 +11,37 @@ describe('<App/>', function () {
it('renders title', () => { it('renders title', () => {
const wrapper = shallow(<App/>); const wrapper = shallow(<App/>);
wrapper.setState({password: false});
expect(wrapper.find('.navbrand').text()).toBe('OpenMediaCenter'); expect(wrapper.find('.navbrand').text()).toBe('OpenMediaCenter');
}); });
it('are navlinks correct', function () { it('are navlinks correct', function () {
const wrapper = shallow(<App/>); const wrapper = shallow(<App/>);
wrapper.setState({password: false});
expect(wrapper.find('.navitem')).toHaveLength(4); expect(wrapper.find('.navitem')).toHaveLength(4);
}); });
it('test initial fetch from api', done => { it('test initial fetch from api', done => {
global.fetch = global.prepareFetchApi({ callAPIMock({
generalSettingsLoaded: true, MediacenterName: 'testname'
passwordsupport: true, })
mediacentername: 'testname'
}); GlobalInfos.enableDarkTheme = jest.fn((r) => {})
const wrapper = shallow(<App/>); const wrapper = shallow(<App/>);
const func = jest.fn();
wrapper.instance().setState = func;
expect(global.fetch).toBeCalledTimes(1);
process.nextTick(() => { process.nextTick(() => {
expect(func).toBeCalledTimes(1); expect(document.title).toBe('testname');
global.fetch.mockClear(); global.fetch.mockClear();
done(); done();
}); });
}); });
it('test render of password page', function () {
const wrapper = shallow(<App/>);
wrapper.setState({password: true});
expect(wrapper.find('AuthenticationPage')).toHaveLength(1);
});
}); });

View File

@ -9,7 +9,7 @@ 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 {APINode, callAPI} from './utils/Api'; import {APINode, apiTokenValid, callApiUnsafe, refreshAPIToken} from './utils/Api';
import {NoBackendConnectionPopup} from './elements/Popups/NoBackendConnectionPopup/NoBackendConnectionPopup'; import {NoBackendConnectionPopup} from './elements/Popups/NoBackendConnectionPopup/NoBackendConnectionPopup';
import {BrowserRouter as Router, NavLink, Route, Switch} from 'react-router-dom'; import {BrowserRouter as Router, NavLink, Route, Switch} from 'react-router-dom';
@ -17,10 +17,10 @@ import Player from './pages/Player/Player';
import ActorOverviewPage from './pages/ActorOverviewPage/ActorOverviewPage'; import ActorOverviewPage from './pages/ActorOverviewPage/ActorOverviewPage';
import ActorPage from './pages/ActorPage/ActorPage'; import ActorPage from './pages/ActorPage/ActorPage';
import {SettingsTypes} from './types/ApiTypes'; import {SettingsTypes} from './types/ApiTypes';
import AuthenticationPage from './pages/AuthenticationPage/AuthenticationPage';
interface state { interface state {
generalSettingsLoaded: boolean; password: boolean | null; // null if uninitialized - true if pwd needed false if not needed
passwordsupport: boolean;
mediacentername: string; mediacentername: string;
onapierror: boolean; onapierror: boolean;
} }
@ -31,86 +31,153 @@ interface state {
class App extends React.Component<{}, state> { class App extends React.Component<{}, state> {
constructor(props: {}) { constructor(props: {}) {
super(props); super(props);
let pwdneeded: boolean | null = null;
if (apiTokenValid()) {
pwdneeded = false;
} else {
refreshAPIToken((err) => {
if (err === 'invalid_client') {
this.setState({password: true});
} else if (err === '') {
this.setState({password: false});
} else {
console.log('unimplemented token error: ' + err);
}
});
}
this.state = { this.state = {
generalSettingsLoaded: false,
passwordsupport: false,
mediacentername: 'OpenMediaCenter', mediacentername: 'OpenMediaCenter',
onapierror: false onapierror: false,
password: pwdneeded
};
// force an update on theme change
GlobalInfos.onThemeChange(() => {
this.forceUpdate();
});
// set the hook to load passwordfield on global func call
GlobalInfos.loadPasswordPage = (callback?: () => void): void => {
// try refreshing the token
refreshAPIToken((err) => {
if (err !== '') {
this.setState({password: true});
} else {
// call callback if request was successful
if (callback) {
callback();
}
}
}, true);
}; };
} }
initialAPICall(): void { initialAPICall(): void {
// this is the first api call so if it fails we know there is no connection to backend // this is the first api call so if it fails we know there is no connection to backend
callAPI(APINode.Settings, {action: 'loadInitialData'}, (result: SettingsTypes.initialApiCallData) => { callApiUnsafe(
APINode.Init,
{action: 'loadInitialData'},
(result: SettingsTypes.initialApiCallData) => {
// set theme // set theme
GlobalInfos.enableDarkTheme(result.DarkMode); GlobalInfos.enableDarkTheme(result.DarkMode);
GlobalInfos.setVideoPath(result.VideoPath); GlobalInfos.setVideoPath(result.VideoPath);
this.setState({ this.setState({
generalSettingsLoaded: true, mediacentername: result.MediacenterName,
passwordsupport: result.Password,
mediacentername: result.Mediacenter_name,
onapierror: false onapierror: false
}); });
// set tab title to received mediacenter name // set tab title to received mediacenter name
document.title = result.Mediacenter_name; document.title = result.MediacenterName;
}, error => { },
() => {
this.setState({onapierror: true}); this.setState({onapierror: true});
}); }
);
} }
componentDidMount(): void { componentDidMount(): void {
this.initialAPICall(); this.initialAPICall();
} }
render(): JSX.Element { render(): JSX.Element {
const themeStyle = GlobalInfos.getThemeStyle(); const themeStyle = GlobalInfos.getThemeStyle();
// add the main theme to the page body // add the main theme to the page body
document.body.className = themeStyle.backgroundcolor; document.body.className = themeStyle.backgroundcolor;
if (this.state.password === true) {
// render authentication page if auth is neccessary
return <AuthenticationPage onSuccessLogin={(): void => this.setState({password: false})} />;
} else if (this.state.password === false) {
return ( return (
<Router> <Router>
<div className={style.app}> <div className={style.app}>
<div className={[style.navcontainer, themeStyle.backgroundcolor, themeStyle.textcolor, themeStyle.hrcolor].join(' ')}> <div
className={[style.navcontainer, themeStyle.backgroundcolor, themeStyle.textcolor, themeStyle.hrcolor].join(
' '
)}>
<div className={style.navbrand}>{this.state.mediacentername}</div> <div className={style.navbrand}>{this.state.mediacentername}</div>
<NavLink className={[style.navitem, themeStyle.navitem].join(' ')} to={'/'} activeStyle={{opacity: '0.85'}}>Home</NavLink> <NavLink
<NavLink className={[style.navitem, themeStyle.navitem].join(' ')} to={'/random'} activeStyle={{opacity: '0.85'}}>Random className={[style.navitem, themeStyle.navitem].join(' ')}
Video</NavLink> to={'/'}
activeStyle={{opacity: '0.85'}}>
Home
</NavLink>
<NavLink
className={[style.navitem, themeStyle.navitem].join(' ')}
to={'/random'}
activeStyle={{opacity: '0.85'}}>
Random Video
</NavLink>
<NavLink className={[style.navitem, themeStyle.navitem].join(' ')} to={'/categories'} activeStyle={{opacity: '0.85'}}>Categories</NavLink> <NavLink
<NavLink className={[style.navitem, themeStyle.navitem].join(' ')} to={'/settings'} activeStyle={{opacity: '0.85'}}>Settings</NavLink> className={[style.navitem, themeStyle.navitem].join(' ')}
to={'/categories'}
activeStyle={{opacity: '0.85'}}>
Categories
</NavLink>
<NavLink
className={[style.navitem, themeStyle.navitem].join(' ')}
to={'/settings'}
activeStyle={{opacity: '0.85'}}>
Settings
</NavLink>
</div> </div>
{this.routing()} {this.routing()}
</div> </div>
{this.state.onapierror ? this.ApiError() : null} {this.state.onapierror ? this.ApiError() : null}
</Router> </Router>
); );
} else {
return <>still loading...</>;
}
} }
routing(): JSX.Element { routing(): JSX.Element {
return ( return (
<Switch> <Switch>
<Route path="/random"> <Route path='/random'>
<RandomPage /> <RandomPage />
</Route> </Route>
<Route path="/categories"> <Route path='/categories'>
<CategoryPage /> <CategoryPage />
</Route> </Route>
<Route path="/settings"> <Route path='/settings'>
<SettingsPage /> <SettingsPage />
</Route> </Route>
<Route exact path="/player/:id"> <Route exact path='/player/:id'>
<Player /> <Player />
</Route> </Route>
<Route exact path="/actors"> <Route exact path='/actors'>
<ActorOverviewPage /> <ActorOverviewPage />
</Route> </Route>
<Route path="/actors/:id"> <Route path='/actors/:id'>
<ActorPage /> <ActorPage />
</Route> </Route>
<Route path="/"> <Route path='/'>
<HomePage /> <HomePage />
</Route> </Route>
</Switch> </Switch>
@ -119,7 +186,7 @@ class App extends React.Component<{}, state> {
ApiError(): JSX.Element { ApiError(): JSX.Element {
// on api error show popup and retry and show again if failing.. // on api error show popup and retry and show again if failing..
return (<NoBackendConnectionPopup onHide={(): void => this.initialAPICall()}/>); return <NoBackendConnectionPopup onHide={(): void => this.initialAPICall()} />;
} }
} }

View File

@ -5,13 +5,13 @@ import React from 'react';
import {Link} from 'react-router-dom'; import {Link} from 'react-router-dom';
import {ActorType} from '../../types/VideoTypes'; import {ActorType} from '../../types/VideoTypes';
interface props { interface Props {
actor: ActorType; actor: ActorType;
onClick?: (actor: ActorType) => void onClick?: (actor: ActorType) => void;
} }
class ActorTile extends React.Component<props> { class ActorTile extends React.Component<Props> {
constructor(props: props) { constructor(props: Props) {
super(props); super(props);
this.state = {}; this.state = {};
@ -21,12 +21,7 @@ class ActorTile extends React.Component<props> {
if (this.props.onClick) { if (this.props.onClick) {
return this.renderActorTile(this.props.onClick); return this.renderActorTile(this.props.onClick);
} else { } else {
return ( return <Link to={{pathname: '/actors/' + this.props.actor.ActorId}}>{this.renderActorTile(() => {})}</Link>;
<Link to={{pathname: '/actors/' + this.props.actor.ActorId}}>
{this.renderActorTile(() => {
})}
</Link>
);
} }
} }
@ -34,9 +29,19 @@ class ActorTile extends React.Component<props> {
return ( return (
<div className={style.actortile} onClick={(): void => customclickhandler(this.props.actor)}> <div className={style.actortile} onClick={(): void => customclickhandler(this.props.actor)}>
<div className={style.actortile_thumbnail}> <div className={style.actortile_thumbnail}>
{this.props.actor.Thumbnail === '' ? <FontAwesomeIcon style={{ {
this.props.actor.Thumbnail === '' ? (
<FontAwesomeIcon
style={{
lineHeight: '130px' lineHeight: '130px'
}} icon={faUser} size='5x'/> : 'dfdf' /* todo render picture provided here! */} }}
icon={faUser}
size='5x'
/>
) : (
'dfdf'
) /* todo render picture provided here! */
}
</div> </div>
<div className={style.actortile_name}>{this.props.actor.Name}</div> <div className={style.actortile_name}>{this.props.actor.Name}</div>
</div> </div>

View File

@ -1,12 +1,12 @@
import React from "react"; import React from 'react';
import style from "../Popups/AddActorPopup/AddActorPopup.module.css"; import style from '../Popups/AddActorPopup/AddActorPopup.module.css';
import {Button} from "../GPElements/Button"; import {Button} from '../GPElements/Button';
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faFilter, faTimes} from "@fortawesome/free-solid-svg-icons"; import {faFilter, faTimes} from '@fortawesome/free-solid-svg-icons';
import {addKeyHandler, removeKeyHandler} from "../../utils/ShortkeyHandler"; import {addKeyHandler, removeKeyHandler} from '../../utils/ShortkeyHandler';
interface props { interface Props {
onFilterChange: (filter: string) => void onFilterChange: (filter: string) => void;
} }
interface state { interface state {
@ -14,18 +14,17 @@ interface state {
filter: string; filter: string;
} }
class FilterButton extends React.Component<props, state> { class FilterButton extends React.Component<Props, state> {
// filterfield anchor, needed to focus after filter btn click // filterfield anchor, needed to focus after filter btn click
private filterfield: HTMLInputElement | null | undefined; private filterfield: HTMLInputElement | null | undefined;
constructor(props: Props) {
constructor(props: props) {
super(props); super(props);
this.state = { this.state = {
filtervisible: false, filtervisible: false,
filter: '' filter: ''
} };
this.keypress = this.keypress.bind(this); this.keypress = this.keypress.bind(this);
this.enableFilterField = this.enableFilterField.bind(this); this.enableFilterField = this.enableFilterField.bind(this);
@ -43,34 +42,57 @@ class FilterButton extends React.Component<props, state> {
if (this.state.filtervisible) { if (this.state.filtervisible) {
return ( return (
<> <>
<input className={'form-control mr-sm-2 ' + style.searchinput} <input
type='text' placeholder='Filter' value={this.state.filter} className={'form-control mr-sm-2 ' + style.searchinput}
type='text'
placeholder='Filter'
value={this.state.filter}
onChange={(e): void => { onChange={(e): void => {
this.props.onFilterChange(e.target.value); this.props.onFilterChange(e.target.value);
this.setState({filter: e.target.value}); this.setState({filter: e.target.value});
}} }}
ref={(input): void => { ref={(input): void => {
this.filterfield = input; this.filterfield = input;
}}/> }}
<Button title={<FontAwesomeIcon style={{ />
<Button
title={
<FontAwesomeIcon
style={{
verticalAlign: 'middle', verticalAlign: 'middle',
lineHeight: '130px' lineHeight: '130px'
}} icon={faTimes} size='1x'/>} color={{backgroundColor: 'red'}} onClick={(): void => { }}
icon={faTimes}
size='1x'
/>
}
color={{backgroundColor: 'red'}}
onClick={(): void => {
this.setState({filter: '', filtervisible: false}); this.setState({filter: '', filtervisible: false});
}}/> }}
/>
</> </>
); );
} else { } else {
return (<Button return (
title={<span>Filter <FontAwesomeIcon <Button
title={
<span>
Filter{' '}
<FontAwesomeIcon
style={{ style={{
verticalAlign: 'middle', verticalAlign: 'middle',
lineHeight: '130px' lineHeight: '130px'
}} }}
icon={faFilter} icon={faFilter}
size='1x'/></span>} size='1x'
/>
</span>
}
color={{backgroundColor: 'cornflowerblue', color: 'white'}} color={{backgroundColor: 'cornflowerblue', color: 'white'}}
onClick={this.enableFilterField}/>) onClick={this.enableFilterField}
/>
);
} }
} }

View File

@ -5,11 +5,11 @@ import {Spinner} from 'react-bootstrap';
import {IconDefinition} from '@fortawesome/fontawesome-common-types'; import {IconDefinition} from '@fortawesome/fontawesome-common-types';
interface props { interface props {
onClick?: () => void onClick?: () => void;
backColor: string backColor: string;
icon: IconDefinition icon: IconDefinition;
text: string | number text: string | number;
subtext: string | number subtext: string | number;
} }
/** /**
@ -18,23 +18,35 @@ interface props {
class InfoHeaderItem extends React.Component<props> { class InfoHeaderItem extends React.Component<props> {
render(): JSX.Element { render(): JSX.Element {
return ( return (
<div onClick={(): void => { <div
onClick={(): void => {
// call clicklistener if defined // call clicklistener if defined
if (this.props.onClick != null) this.props.onClick(); if (this.props.onClick != null) {
}} className={style.infoheaderitem} style={{backgroundColor: this.props.backColor}}> this.props.onClick();
}
}}
className={style.infoheaderitem}
style={{backgroundColor: this.props.backColor}}>
<div className={style.icon}> <div className={style.icon}>
<FontAwesomeIcon style={{ <FontAwesomeIcon
style={{
verticalAlign: 'middle', verticalAlign: 'middle',
lineHeight: '130px' lineHeight: '130px'
}} icon={this.props.icon} size='5x'/> }}
icon={this.props.icon}
size='5x'
/>
</div> </div>
{this.props.text !== null && this.props.text !== undefined ? {this.props.text !== null && this.props.text !== undefined ? (
<> <>
<div className={style.maintext}>{this.props.text}</div> <div className={style.maintext}>{this.props.text}</div>
<div className={style.subtext}>{this.props.subtext}</div> <div className={style.subtext}>{this.props.subtext}</div>
</> </>
: <span className={style.loadAnimation}><Spinner animation='border'/></span> ) : (
} <span className={style.loadAnimation}>
<Spinner animation='border' />
</span>
)}
</div> </div>
); );
} }

View File

@ -17,9 +17,7 @@ class PageTitle extends React.Component<props> {
<div className={style.pageheader + ' ' + themeStyle.backgroundcolor}> <div className={style.pageheader + ' ' + themeStyle.backgroundcolor}>
<span className={style.pageheadertitle + ' ' + themeStyle.textcolor}>{this.props.title}</span> <span className={style.pageheadertitle + ' ' + themeStyle.textcolor}>{this.props.title}</span>
<span className={style.pageheadersubtitle + ' ' + themeStyle.textcolor}>{this.props.subtitle}</span> <span className={style.pageheadersubtitle + ' ' + themeStyle.textcolor}>{this.props.subtitle}</span>
<> <>{this.props.children}</>
{this.props.children}
</>
<Line /> <Line />
</div> </div>
); );

View File

@ -40,7 +40,7 @@ describe('<AddActorPopup/>', function () {
it('simulate actortile click', function () { it('simulate actortile click', function () {
const func = jest.fn(); const func = jest.fn();
const wrapper = shallow(<AddActorPopup onHide={() => {func();}} movie_id={1}/>); const wrapper = shallow(<AddActorPopup onHide={() => {func();}} movieId={1}/>);
global.callAPIMock({result: 'success'}); global.callAPIMock({result: 'success'});

View File

@ -6,11 +6,11 @@ import {NewActorPopupContent} from '../NewActorPopup/NewActorPopup';
import {APINode, callAPI} from '../../../utils/Api'; import {APINode, callAPI} from '../../../utils/Api';
import {ActorType} from '../../../types/VideoTypes'; import {ActorType} from '../../../types/VideoTypes';
import {GeneralSuccess} from '../../../types/GeneralTypes'; import {GeneralSuccess} from '../../../types/GeneralTypes';
import FilterButton from "../../FilterButton/FilterButton"; import FilterButton from '../../FilterButton/FilterButton';
interface props { interface Props {
onHide: () => void; onHide: () => void;
movie_id: number; movieId: number;
} }
interface state { interface state {
@ -22,11 +22,11 @@ interface state {
/** /**
* Popup for Adding a new Actor to a Video * Popup for Adding a new Actor to a Video
*/ */
class AddActorPopup extends React.Component<props, state> { class AddActorPopup extends React.Component<Props, state> {
// filterfield anchor, needed to focus after filter btn click // filterfield anchor, needed to focus after filter btn click
private filterfield: HTMLInputElement | null | undefined; private filterfield: HTMLInputElement | null | undefined;
constructor(props: props) { constructor(props: Props) {
super(props); super(props);
this.state = { this.state = {
@ -48,12 +48,19 @@ class AddActorPopup extends React.Component<props, state> {
return ( return (
<> <>
{/* todo render actor tiles here and add search field*/} {/* todo render actor tiles here and add search field*/}
<PopupBase title='Add new Actor to Video' onHide={this.props.onHide} banner={ <PopupBase
title='Add new Actor to Video'
onHide={this.props.onHide}
banner={
<button <button
className={style.newactorbutton} className={style.newactorbutton}
onClick={(): void => { onClick={(): void => {
this.setState({contentDefault: false}); this.setState({contentDefault: false});
}}>Create new Actor</button>} ParentSubmit={this.parentSubmit}> }}>
Create new Actor
</button>
}
ParentSubmit={this.parentSubmit}>
{this.resolvePage()} {this.resolvePage()}
</PopupBase> </PopupBase>
</> </>
@ -65,11 +72,18 @@ class AddActorPopup extends React.Component<props, state> {
* @returns {JSX.Element} * @returns {JSX.Element}
*/ */
resolvePage(): JSX.Element { resolvePage(): JSX.Element {
if (this.state.contentDefault) return (this.getContent()); if (this.state.contentDefault) {
else return (<NewActorPopupContent onHide={(): void => { return this.getContent();
} else {
return (
<NewActorPopupContent
onHide={(): void => {
this.loadActors(); this.loadActors();
this.setState({contentDefault: true}); this.setState({contentDefault: true});
}}/>); }}
/>
);
}
} }
/** /**
@ -81,15 +95,19 @@ class AddActorPopup extends React.Component<props, state> {
return ( return (
<> <>
<div className={style.searchbar}> <div className={style.searchbar}>
<FilterButton onFilterChange={(filter): void => { <FilterButton
this.setState({filter: filter}) onFilterChange={(filter): void => {
}}/> this.setState({filter: filter});
}}
/>
</div> </div>
{this.state.actors.filter(this.filterSearch).map((el) => (<ActorTile actor={el} onClick={this.tileClickHandler}/>))} {this.state.actors.filter(this.filterSearch).map((el) => (
<ActorTile actor={el} onClick={this.tileClickHandler} />
))}
</> </>
); );
} else { } else {
return (<div>somekind of loading</div>); return <div>somekind of loading</div>;
} }
} }
@ -98,25 +116,29 @@ class AddActorPopup extends React.Component<props, state> {
*/ */
tileClickHandler(actor: ActorType): void { tileClickHandler(actor: ActorType): void {
// fetch the available actors // fetch the available actors
callAPI<GeneralSuccess>(APINode.Actor, { callAPI<GeneralSuccess>(
APINode.Actor,
{
action: 'addActorToVideo', action: 'addActorToVideo',
ActorId: actor.ActorId, ActorId: actor.ActorId,
MovieId: this.props.movie_id MovieId: this.props.movieId
}, result => { },
(result) => {
if (result.result === 'success') { if (result.result === 'success') {
// return back to player page // return back to player page
this.props.onHide(); this.props.onHide();
} else { } else {
console.error('an error occured while fetching actors: ' + result); console.error('an error occured while fetching actors: ' + result);
} }
}); }
);
} }
/** /**
* load the actors from backend and set state * load the actors from backend and set state
*/ */
loadActors(): void { loadActors(): void {
callAPI<ActorType[]>(APINode.Actor, {action: 'getAllActors'}, result => { callAPI<ActorType[]>(APINode.Actor, {action: 'getAllActors'}, (result) => {
this.setState({actors: result}); this.setState({actors: result});
}); });
} }

View File

@ -3,10 +3,10 @@ import Tag from '../../Tag/Tag';
import PopupBase from '../PopupBase'; import PopupBase from '../PopupBase';
import {APINode, callAPI} from '../../../utils/Api'; import {APINode, callAPI} from '../../../utils/Api';
import {TagType} from '../../../types/VideoTypes'; import {TagType} from '../../../types/VideoTypes';
import FilterButton from "../../FilterButton/FilterButton"; import FilterButton from '../../FilterButton/FilterButton';
import styles from './AddTagPopup.module.css' import styles from './AddTagPopup.module.css';
interface props { interface Props {
onHide: () => void; onHide: () => void;
submit: (tagId: number, tagName: string) => void; submit: (tagId: number, tagName: string) => void;
} }
@ -19,8 +19,8 @@ interface state {
/** /**
* component creates overlay to add a new tag to a video * component creates overlay to add a new tag to a video
*/ */
class AddTagPopup extends React.Component<props, state> { class AddTagPopup extends React.Component<Props, state> {
constructor(props: props) { constructor(props: Props) {
super(props); super(props);
this.state = {items: [], filter: ''}; this.state = {items: [], filter: ''};
@ -44,11 +44,9 @@ class AddTagPopup extends React.Component<props, state> {
<div className={styles.actionbar}> <div className={styles.actionbar}>
<FilterButton onFilterChange={(filter): void => this.setState({filter: filter})} /> <FilterButton onFilterChange={(filter): void => this.setState({filter: filter})} />
</div> </div>
{this.state.items ? {this.state.items
this.state.items.filter(this.tagFilter).map((i) => ( ? this.state.items.filter(this.tagFilter).map((i) => <Tag tagInfo={i} onclick={(): void => this.onItemClick(i)} />)
<Tag tagInfo={i} : null}
onclick={(): void => this.onItemClick(i)}/>
)) : null}
</PopupBase> </PopupBase>
); );
} }

View File

@ -28,10 +28,17 @@ export class NewActorPopupContent extends React.Component<NewActorPopupProps> {
return ( return (
<> <>
<div> <div>
<input type='text' placeholder='Actor Name' onChange={(v): void => { <input
type='text'
placeholder='Actor Name'
onChange={(v): void => {
this.value = v.target.value; this.value = v.target.value;
}}/></div> }}
<button className={style.savebtn} onClick={(): void => this.storeselection()}>Save</button> />
</div>
<button className={style.savebtn} onClick={(): void => this.storeselection()}>
Save
</button>
</> </>
); );
} }
@ -41,7 +48,9 @@ export class NewActorPopupContent extends React.Component<NewActorPopupProps> {
*/ */
storeselection(): void { storeselection(): void {
// 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;
}
callAPI(APINode.Actor, {action: 'createActor', actorname: this.value}, (result: GeneralSuccess) => { callAPI(APINode.Actor, {action: 'createActor', actorname: this.value}, (result: GeneralSuccess) => {
if (result.result !== 'success') { if (result.result !== 'success') {

View File

@ -5,7 +5,7 @@ import {APINode, callAPI} from '../../../utils/Api';
import {GeneralSuccess} from '../../../types/GeneralTypes'; import {GeneralSuccess} from '../../../types/GeneralTypes';
interface props { interface props {
onHide: () => void onHide: () => void;
} }
/** /**
@ -16,11 +16,24 @@ class NewTagPopup extends React.Component<props> {
render(): JSX.Element { render(): JSX.Element {
return ( return (
<PopupBase title='Add new Tag' onHide={this.props.onHide} height='200px' width='400px' ParentSubmit={(): void => this.storeselection()}> <PopupBase
<div><input type='text' placeholder='Tagname' onChange={(v): void => { title='Add new Tag'
onHide={this.props.onHide}
height='200px'
width='400px'
ParentSubmit={(): void => this.storeselection()}>
<div>
<input
type='text'
placeholder='Tagname'
onChange={(v): void => {
this.value = v.target.value; this.value = v.target.value;
}}/></div> }}
<button className={style.savebtn} onClick={(): void => this.storeselection()}>Save</button> />
</div>
<button className={style.savebtn} onClick={(): void => this.storeselection()}>
Save
</button>
</PopupBase> </PopupBase>
); );
} }

View File

@ -4,17 +4,24 @@ import style from '../NewActorPopup/NewActorPopup.module.css';
import {setCustomBackendDomain} from '../../../utils/Api'; import {setCustomBackendDomain} from '../../../utils/Api';
interface NBCProps { interface NBCProps {
onHide: (_: void) => void onHide: (_: void) => void;
} }
export function NoBackendConnectionPopup(props: NBCProps): JSX.Element { export function NoBackendConnectionPopup(props: NBCProps): JSX.Element {
return ( return (
<PopupBase title='No connection to backend API!' onHide={props.onHide} height='200px' width='600px'> <PopupBase title='No connection to backend API!' onHide={props.onHide} height='200px' width='600px'>
<div> <div>
<input type='text' placeholder='http://192.168.0.2' onChange={(v): void => { <input
type='text'
placeholder='http://192.168.0.2'
onChange={(v): void => {
setCustomBackendDomain(v.target.value); setCustomBackendDomain(v.target.value);
}}/></div> }}
<button className={style.savebtn} onClick={(): void => props.onHide()}>Refresh</button> />
</div>
<button className={style.savebtn} onClick={(): void => props.onHide()}>
Refresh
</button>
</PopupBase> </PopupBase>
); );
} }

View File

@ -8,18 +8,11 @@ describe('<PopupBase/>', function () {
wrapper.unmount(); wrapper.unmount();
}); });
let events;
function mockKeyPress() {
events = [];
document.addEventListener = jest.fn((event, cb) => {
events[event] = cb;
});
}
it('simulate keypress', function () { it('simulate keypress', function () {
mockKeyPress(); mockKeyPress();
const func = jest.fn(); const func = jest.fn();
const events = mockKeyPress();
shallow(<PopupBase onHide={() => func()}/>); shallow(<PopupBase onHide={() => func()}/>);
// trigger the keypress event // trigger the keypress event
@ -29,7 +22,7 @@ describe('<PopupBase/>', function () {
}); });
it('test an Enter sumit', function () { it('test an Enter sumit', function () {
mockKeyPress(); const events = mockKeyPress();
const func = jest.fn(); const func = jest.fn();
shallow(<PopupBase ParentSubmit={() => func()}/>); shallow(<PopupBase ParentSubmit={() => func()}/>);

View File

@ -4,7 +4,7 @@ import {Line} from '../PageTitle/PageTitle';
import React, {RefObject} from 'react'; import React, {RefObject} from 'react';
import {addKeyHandler, removeKeyHandler} from '../../utils/ShortkeyHandler'; import {addKeyHandler, removeKeyHandler} from '../../utils/ShortkeyHandler';
interface props { interface Props {
width?: string; width?: string;
height?: string; height?: string;
banner?: JSX.Element; banner?: JSX.Element;
@ -16,11 +16,11 @@ interface props {
/** /**
* wrapper class for generic types of popups * wrapper class for generic types of popups
*/ */
class PopupBase extends React.Component<props> { class PopupBase extends React.Component<Props> {
private wrapperRef: RefObject<HTMLDivElement>; private wrapperRef: RefObject<HTMLDivElement>;
private framedimensions: {minHeight: string | undefined; width: string | undefined; height: string | undefined}; private framedimensions: {minHeight: string | undefined; width: string | undefined; height: string | undefined};
constructor(props: props) { constructor(props: Props) {
super(props); super(props);
this.state = {items: []}; this.state = {items: []};
@ -32,9 +32,9 @@ class PopupBase extends React.Component<props> {
// 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) minHeight: this.props.height ? this.props.height : undefined
}; };
} }
@ -64,9 +64,7 @@ class PopupBase extends React.Component<props> {
</div> </div>
<Line /> <Line />
<div className={style.content}> <div className={style.content}>{this.props.children}</div>
{this.props.children}
</div>
</div> </div>
); );
} }
@ -90,7 +88,9 @@ class PopupBase extends React.Component<props> {
this.props.onHide(); this.props.onHide();
} else if (event.key === 'Enter') { } else if (event.key === 'Enter') {
// call a parentsubmit if defined // call a parentsubmit if defined
if (this.props.ParentSubmit) this.props.ParentSubmit(); if (this.props.ParentSubmit) {
this.props.ParentSubmit();
}
} }
} }
@ -98,15 +98,19 @@ class PopupBase extends React.Component<props> {
* make the element drag and droppable * make the element drag and droppable
*/ */
dragElement(): void { dragElement(): void {
let xOld = 0, yOld = 0; let xOld = 0,
yOld = 0;
const elmnt = this.wrapperRef.current; const elmnt = this.wrapperRef.current;
if (elmnt === null) return; if (elmnt === null) {
if (elmnt.firstChild === null) return; return;
}
if (elmnt.firstChild === null) {
return;
}
(elmnt.firstChild as HTMLDivElement).onmousedown = dragMouseDown; (elmnt.firstChild as HTMLDivElement).onmousedown = dragMouseDown;
function dragMouseDown(e: MouseEvent): void { function dragMouseDown(e: MouseEvent): void {
e.preventDefault(); e.preventDefault();
// get the mouse cursor position at startup: // get the mouse cursor position at startup:
@ -125,9 +129,11 @@ class PopupBase extends React.Component<props> {
xOld = e.clientX; xOld = e.clientX;
yOld = e.clientY; yOld = e.clientY;
// set the element's new position: // set the element's new position:
if (elmnt === null) return; if (elmnt === null) {
elmnt.style.top = (elmnt.offsetTop - dy) + 'px'; return;
elmnt.style.left = (elmnt.offsetLeft - dx) + 'px'; }
elmnt.style.top = elmnt.offsetTop - dy + 'px';
elmnt.style.left = elmnt.offsetLeft - dx + 'px';
} }
function closeDragElement(): void { function closeDragElement(): void {

View File

@ -2,17 +2,16 @@ import React from 'react';
import PopupBase from '../PopupBase'; import PopupBase from '../PopupBase';
import {Button} from '../../GPElements/Button'; import {Button} from '../../GPElements/Button';
interface props { interface Props {
onHide: (_: void) => void; onHide: (_: void) => void;
submit: (_: void) => void; submit: (_: void) => void;
} }
export default function SubmitPopup(props: props): JSX.Element { export default function SubmitPopup(props: Props): JSX.Element {
return ( return (
<PopupBase title='Are you sure?' onHide={props.onHide} height='160px' width='300px'> <PopupBase title='Are you sure?' onHide={props.onHide} height='160px' width='300px'>
<Button title='Submit' color={{backgroundColor: 'green'}} onClick={(): void => props.submit()} /> <Button title='Submit' color={{backgroundColor: 'green'}} onClick={(): void => props.submit()} />
<Button title='Cancel' color={{backgroundColor: 'red'}} onClick={(): void => props.onHide()} /> <Button title='Cancel' color={{backgroundColor: 'red'}} onClick={(): void => props.onHide()} />
</PopupBase> </PopupBase>
); );
} }

View File

@ -4,10 +4,12 @@ import {Spinner} from 'react-bootstrap';
import {Link} from 'react-router-dom'; import {Link} from 'react-router-dom';
import GlobalInfos from '../../utils/GlobalInfos'; import GlobalInfos from '../../utils/GlobalInfos';
import {APINode, callAPIPlain} from '../../utils/Api'; import {APINode, callAPIPlain} from '../../utils/Api';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faPhotoVideo} from '@fortawesome/free-solid-svg-icons';
interface PreviewProps { interface PreviewProps {
name: string; name: string;
movie_id: number; movieId: number;
} }
interface PreviewState { interface PreviewState {
@ -28,7 +30,7 @@ class Preview extends React.Component<PreviewProps, PreviewState> {
} }
componentDidMount(): void { componentDidMount(): void {
callAPIPlain(APINode.Video, {action: 'readThumbnail', movieid: this.props.movie_id}, (result) => { callAPIPlain(APINode.Video, {action: 'readThumbnail', movieid: this.props.movieId}, (result) => {
this.setState({ this.setState({
previewpicture: result previewpicture: result
}); });
@ -38,23 +40,30 @@ class Preview extends React.Component<PreviewProps, PreviewState> {
render(): JSX.Element { render(): JSX.Element {
const themeStyle = GlobalInfos.getThemeStyle(); const themeStyle = GlobalInfos.getThemeStyle();
return ( return (
<Link to={'/player/' + this.props.movie_id}> <Link to={'/player/' + this.props.movieId}>
<div className={style.videopreview + ' ' + themeStyle.secbackground + ' ' + themeStyle.preview}> <div className={style.videopreview + ' ' + themeStyle.secbackground + ' ' + themeStyle.preview}>
<div className={style.previewtitle + ' ' + themeStyle.lighttextcolor}>{this.props.name}</div> <div className={style.previewtitle + ' ' + themeStyle.lighttextcolor}>{this.props.name}</div>
<div className={style.previewpic}> <div className={style.previewpic}>
{this.state.previewpicture !== null ? {this.state.previewpicture === '' ? (
<img className={style.previewimage} <FontAwesomeIcon
src={this.state.previewpicture} style={{
alt='Pic loading.'/> : color: 'white',
<span className={style.loadAnimation}><Spinner animation='border'/></span>} marginTop: '55px'
}}
</div> icon={faPhotoVideo}
<div className={style.previewbottom}> size='5x'
/>
) : this.state.previewpicture === null ? (
<span className={style.loadAnimation}>
<Spinner animation='border' />
</span>
) : (
<img className={style.previewimage} src={this.state.previewpicture} alt='Pic loading.' />
)}
</div> </div>
<div className={style.previewbottom} />
</div> </div>
</Link> </Link>
); );
} }
} }
@ -66,11 +75,8 @@ export class TagPreview extends React.Component<{ name: string }> {
render(): JSX.Element { render(): JSX.Element {
const themeStyle = GlobalInfos.getThemeStyle(); const themeStyle = GlobalInfos.getThemeStyle();
return ( return (
<div <div className={style.videopreview + ' ' + style.tagpreview + ' ' + themeStyle.secbackground + ' ' + themeStyle.preview}>
className={style.videopreview + ' ' + style.tagpreview + ' ' + themeStyle.secbackground + ' ' + themeStyle.preview}> <div className={style.tagpreviewtitle + ' ' + themeStyle.lighttextcolor}>{this.props.name}</div>
<div className={style.tagpreviewtitle + ' ' + themeStyle.lighttextcolor}>
{this.props.name}
</div>
</div> </div>
); );
} }

View File

@ -5,7 +5,7 @@ import Preview, {TagPreview} from './Preview';
describe('<Preview/>', function () { describe('<Preview/>', function () {
it('renders without crashing ', function () { it('renders without crashing ', function () {
const wrapper = shallow(<Preview movie_id={1}/>); const wrapper = shallow(<Preview movieId={1}/>);
wrapper.unmount(); wrapper.unmount();
}); });
@ -17,7 +17,7 @@ describe('<Preview/>', function () {
}); });
global.fetch = jest.fn().mockImplementation(() => mockFetchPromise); global.fetch = jest.fn().mockImplementation(() => mockFetchPromise);
const wrapper = shallow(<Preview name='test' movie_id={1}/>); const wrapper = shallow(<Preview name='test' movieId={1}/>);
// now called 1 times // now called 1 times
expect(global.fetch).toHaveBeenCalledTimes(1); expect(global.fetch).toHaveBeenCalledTimes(1);
@ -35,7 +35,7 @@ describe('<Preview/>', function () {
}); });
it('spinner loads correctly', function () { it('spinner loads correctly', function () {
const wrapper = shallow(<Preview movie_id={1}/>); const wrapper = shallow(<Preview movieId={1}/>);
// expect load animation to be visible // expect load animation to be visible
expect(wrapper.find('.loadAnimation')).toHaveLength(1); expect(wrapper.find('.loadAnimation')).toHaveLength(1);

View File

@ -13,11 +13,16 @@ interface SideBarProps {
class SideBar extends React.Component<SideBarProps> { class SideBar extends React.Component<SideBarProps> {
render(): JSX.Element { render(): JSX.Element {
const themeStyle = GlobalInfos.getThemeStyle(); const themeStyle = GlobalInfos.getThemeStyle();
const classnn = style.sideinfogeometry + ' ' + (this.props.hiddenFrame === undefined ? style.sideinfo + ' ' + themeStyle.secbackground : ''); const classnn =
style.sideinfogeometry +
' ' +
(this.props.hiddenFrame === undefined ? style.sideinfo + ' ' + themeStyle.secbackground : '');
return (<div className={classnn} style={{width: this.props.width}}> return (
<div className={classnn} style={{width: this.props.width}}>
{this.props.children} {this.props.children}
</div>); </div>
);
} }
} }
@ -27,9 +32,7 @@ class SideBar extends React.Component<SideBarProps> {
export class SideBarTitle extends React.Component { export class SideBarTitle extends React.Component {
render(): JSX.Element { render(): JSX.Element {
const themeStyle = GlobalInfos.getThemeStyle(); const themeStyle = GlobalInfos.getThemeStyle();
return ( return <div className={style.sidebartitle + ' ' + themeStyle.subtextcolor}>{this.props.children}</div>;
<div className={style.sidebartitle + ' ' + themeStyle.subtextcolor}>{this.props.children}</div>
);
} }
} }
@ -40,8 +43,9 @@ export class SideBarItem extends React.Component {
render(): JSX.Element { render(): JSX.Element {
const themeStyle = GlobalInfos.getThemeStyle(); const themeStyle = GlobalInfos.getThemeStyle();
return ( return (
<div <div className={style.sidebarinfo + ' ' + themeStyle.thirdbackground + ' ' + themeStyle.lighttextcolor}>
className={style.sidebarinfo + ' ' + themeStyle.thirdbackground + ' ' + themeStyle.lighttextcolor}>{this.props.children}</div> {this.props.children}
</div>
); );
} }
} }

View File

@ -5,8 +5,8 @@ import {Link} from 'react-router-dom';
import {TagType} from '../../types/VideoTypes'; import {TagType} from '../../types/VideoTypes';
interface props { interface props {
onclick?: (_: string) => void onclick?: (_: string) => void;
tagInfo: TagType tagInfo: TagType;
} }
/** /**
@ -17,18 +17,15 @@ class Tag extends React.Component<props> {
if (this.props.onclick) { if (this.props.onclick) {
return this.renderButton(); return this.renderButton();
} else { } else {
return ( return <Link to={'/categories/' + this.props.tagInfo.TagId}>{this.renderButton()}</Link>;
<Link to={'/categories/' + this.props.tagInfo.TagId}>
{this.renderButton()}
</Link>
);
} }
} }
renderButton(): JSX.Element { renderButton(): JSX.Element {
return ( return (
<button className={styles.tagbtn} onClick={(): void => this.TagClick()} <button className={styles.tagbtn} onClick={(): void => this.TagClick()} data-testid='Test-Tag'>
data-testid='Test-Tag'>{this.props.tagInfo.TagName}</button> {this.props.tagInfo.TagName}
</button>
); );
} }

View File

@ -3,8 +3,8 @@ import Preview from '../Preview/Preview';
import style from './VideoContainer.module.css'; import style from './VideoContainer.module.css';
import {VideoTypes} from '../../types/ApiTypes'; import {VideoTypes} from '../../types/ApiTypes';
interface props { interface Props {
data: VideoTypes.VideoUnloadedType[] data: VideoTypes.VideoUnloadedType[];
} }
interface state { interface state {
@ -16,11 +16,11 @@ interface state {
* A videocontainer storing lots of Preview elements * A videocontainer storing lots of Preview elements
* includes scroll handling and loading of preview infos * includes scroll handling and loading of preview infos
*/ */
class VideoContainer extends React.Component<props, state> { class VideoContainer extends React.Component<Props, state> {
// stores current index of loaded elements // stores current index of loaded elements
loadindex: number = 0; loadindex: number = 0;
constructor(props: props) { constructor(props: Props) {
super(props); super(props);
this.state = { this.state = {
@ -38,15 +38,11 @@ class VideoContainer extends React.Component<props, state> {
render(): JSX.Element { render(): JSX.Element {
return ( return (
<div className={style.maincontent}> <div className={style.maincontent}>
{this.state.loadeditems.map(elem => ( {this.state.loadeditems.map((elem) => (
<Preview <Preview key={elem.MovieId} name={elem.MovieName} movieId={elem.MovieId} />
key={elem.MovieId}
name={elem.MovieName}
movie_id={elem.MovieId}/>
))} ))}
{/*todo css for no items to show*/} {/*todo css for no items to show*/}
{this.state.loadeditems.length === 0 ? {this.state.loadeditems.length === 0 ? 'no items to show!' : null}
'no items to show!' : null}
{this.props.children} {this.props.children}
</div> </div>
); );
@ -73,13 +69,9 @@ class VideoContainer extends React.Component<props, state> {
} }
this.setState({ this.setState({
loadeditems: [ loadeditems: [...this.state.loadeditems, ...ret]
...this.state.loadeditems,
...ret
]
}); });
this.loadindex += nr; this.loadindex += nr;
} }

View File

@ -3,7 +3,7 @@ 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
global.console.log = process.env.NODE_ENV !== 'development' ? (s: string | number | boolean): void => {} : global.console.log; global.console.log = process.env.NODE_ENV !== 'development' ? (_: string | number | boolean): void => {} : global.console.log;
ReactDOM.render( ReactDOM.render(
<React.StrictMode> <React.StrictMode>

View File

@ -8,16 +8,15 @@ import style from './ActorOverviewPage.module.css';
import {Button} from '../../elements/GPElements/Button'; import {Button} from '../../elements/GPElements/Button';
import NewActorPopup from '../../elements/Popups/NewActorPopup/NewActorPopup'; import NewActorPopup from '../../elements/Popups/NewActorPopup/NewActorPopup';
interface props { interface Props {}
}
interface state { interface state {
actors: ActorType[]; actors: ActorType[];
NActorPopupVisible: boolean NActorPopupVisible: boolean;
} }
class ActorOverviewPage extends React.Component<props, state> { class ActorOverviewPage extends React.Component<Props, state> {
constructor(props: props) { constructor(props: Props) {
super(props); super(props);
this.state = { this.state = {
@ -38,19 +37,24 @@ class ActorOverviewPage extends React.Component<props, state> {
<Button title='Add Actor' onClick={(): void => this.setState({NActorPopupVisible: true})} /> <Button title='Add Actor' onClick={(): void => this.setState({NActorPopupVisible: true})} />
</SideBar> </SideBar>
<div className={style.container}> <div className={style.container}>
{this.state.actors.map((el) => (<ActorTile key={el.ActorId} actor={el}/>))} {this.state.actors.map((el) => (
<ActorTile key={el.ActorId} actor={el} />
))}
</div> </div>
{this.state.NActorPopupVisible ? {this.state.NActorPopupVisible ? (
<NewActorPopup onHide={(): void => { <NewActorPopup
onHide={(): void => {
this.setState({NActorPopupVisible: false}); this.setState({NActorPopupVisible: false});
this.fetchAvailableActors(); // refetch actors this.fetchAvailableActors(); // refetch actors
}}/> : null} }}
/>
) : null}
</> </>
); );
} }
fetchAvailableActors(): void { fetchAvailableActors(): void {
callAPI<ActorType[]>(APINode.Actor, {action: 'getAllActors'}, result => { callAPI<ActorType[]>(APINode.Actor, {action: 'getAllActors'}, (result) => {
this.setState({actors: result}); this.setState({actors: result});
}); });
} }

View File

@ -13,22 +13,20 @@ import {Button} from '../../elements/GPElements/Button';
import {ActorTypes, VideoTypes} from '../../types/ApiTypes'; import {ActorTypes, VideoTypes} from '../../types/ApiTypes';
interface state { interface state {
data: VideoTypes.VideoUnloadedType[], data: VideoTypes.VideoUnloadedType[];
actor: ActorType actor: ActorType;
} }
/** /**
* empty default props with id in url * empty default props with id in url
*/ */
interface props extends RouteComponentProps<{ id: string }> { interface Props extends RouteComponentProps<{id: string}> {}
}
/** /**
* info page about a specific actor and a list of all its videos * info page about a specific actor and a list of all its videos
*/ */
export class ActorPage extends React.Component<props, state> { export class ActorPage extends React.Component<Props, state> {
constructor(props: props) { constructor(props: Props) {
super(props); super(props);
this.state = {data: [], actor: {ActorId: 0, Name: '', Thumbnail: ''}}; this.state = {data: [], actor: {ActorId: 0, Name: '', Thumbnail: ''}};
@ -50,10 +48,7 @@ export class ActorPage extends React.Component<props, state> {
</div> </div>
<SideBarTitle>Attention: This is an early preview!</SideBarTitle> <SideBarTitle>Attention: This is an early preview!</SideBarTitle>
</SideBar> </SideBar>
{this.state.data.length !== 0 ? {this.state.data.length !== 0 ? <VideoContainer data={this.state.data} /> : <div>No Data found!</div>}
<VideoContainer
data={this.state.data}/> :
<div>No Data found!</div>}
</> </>
); );
} }
@ -66,15 +61,19 @@ export class ActorPage extends React.Component<props, state> {
* request more actor info from backend * request more actor info from backend
*/ */
getActorInfo(): void { getActorInfo(): void {
callAPI(APINode.Actor, { callAPI(
APINode.Actor,
{
action: 'getActorInfo', action: 'getActorInfo',
ActorId: parseInt(this.props.match.params.id) ActorId: parseInt(this.props.match.params.id, 10)
}, (result: ActorTypes.videofetchresult) => { },
(result: ActorTypes.videofetchresult) => {
this.setState({ this.setState({
data: result.Videos ? result.Videos : [], data: result.Videos ? result.Videos : [],
actor: result.Info actor: result.Info
}); });
}); }
);
} }
} }

View File

@ -0,0 +1,56 @@
.main {
background-color: #00b3ff;
margin-left: calc(50% - 125px);
margin-top: 5%;
padding-bottom: 15px;
width: 250px;
text-align: center;
border-radius: 10px;
}
.loginText {
font-size: xx-large;
text-align: center;
margin-bottom: 15px;
font-weight: bolder;
}
.openmediacenterlabel {
margin-top: 5%;
text-align: center;
font-size: xxx-large;
font-weight: bold;
text-transform: capitalize;
color: white;
}
.input {
margin-left: 10px;
margin-right: 10px;
width: calc(100% - 20px);
background: transparent;
border-width: 0 0 1px 0;
color: #505050;
border-color: #505050;
text-align: center;
margin-bottom: 25px;
font-size: larger;
}
.submitbtn {
margin-top: 10px;
}
::placeholder {
color: #505050;
opacity: 1;
}
*:focus {
outline: none;
}
.input:focus {
color: black;
border-color: black;
}

View File

@ -0,0 +1,56 @@
import React from 'react';
import AuthenticationPage from './AuthenticationPage';
import {shallow} from 'enzyme';
describe('<AuthenticationPage/>', function () {
it('renders without crashing ', function () {
const wrapper = shallow(<AuthenticationPage submit={() => {
}}/>);
wrapper.unmount();
});
it('test button click', function () {
const func = jest.fn();
const wrapper = shallow(<AuthenticationPage onSuccessLogin={func}/>);
wrapper.instance().authenticate = jest.fn(() => {
wrapper.instance().props.onSuccessLogin()
});
wrapper.setState({pwdText: 'testpwd'});
wrapper.find('Button').simulate('click');
expect(func).toHaveBeenCalledTimes(1);
});
it('test fail authenticate', function () {
const events = mockKeyPress();
const helpers = require('../../utils/Api');
helpers.refreshAPIToken = jest.fn().mockImplementation((callback, force, pwd) => {
callback('there was an error')
});
const wrapper = shallow(<AuthenticationPage/>);
events.keyup({key: 'Enter'});
expect(wrapper.state().wrongPWDInfo).toBe(true);
});
it('test success authenticate', function () {
const events = mockKeyPress();
const func = jest.fn()
const helpers = require('../../utils/Api');
helpers.refreshAPIToken = jest.fn().mockImplementation((callback, force, pwd) => {
callback('')
});
const wrapper = shallow(<AuthenticationPage onSuccessLogin={func}/>);
events.keyup({key: 'Enter'});
expect(wrapper.state().wrongPWDInfo).toBe(false);
expect(func).toHaveBeenCalledTimes(1);
});
});

View File

@ -0,0 +1,110 @@
import React from 'react';
import {Button} from '../../elements/GPElements/Button';
import style from './AuthenticationPage.module.css';
import {addKeyHandler, removeKeyHandler} from '../../utils/ShortkeyHandler';
import {refreshAPIToken} from '../../utils/Api';
import {faTimes} from '@fortawesome/free-solid-svg-icons';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
interface state {
pwdText: string;
wrongPWDInfo: boolean;
}
interface Props {
onSuccessLogin: () => void;
}
class AuthenticationPage extends React.Component<Props, state> {
constructor(props: Props) {
super(props);
this.state = {
pwdText: '',
wrongPWDInfo: false
};
this.keypress = this.keypress.bind(this);
this.authenticate = this.authenticate.bind(this);
}
componentDidMount(): void {
addKeyHandler(this.keypress);
}
componentWillUnmount(): void {
removeKeyHandler(this.keypress);
}
render(): JSX.Element {
return (
<>
<div className={style.openmediacenterlabel}>OpenMediaCenter</div>
<div className={style.main}>
<div className={style.loginText}>Login</div>
<div>
<input
className={style.input}
placeholder='Password'
type='password'
onChange={(ch): void => this.setState({pwdText: ch.target.value})}
value={this.state.pwdText}
/>
{this.state.wrongPWDInfo ? (
<div>
<FontAwesomeIcon
style={{
color: 'red',
marginRight: '7px'
}}
icon={faTimes}
size='1x'
/>
wrong password!
</div>
) : null}
</div>
<div className={style.submitbtn}>
<Button title='Submit' onClick={this.authenticate} />
</div>
</div>
</>
);
}
/**
* request a new token and check if pwd was valid
*/
authenticate(): void {
refreshAPIToken(
(error) => {
if (error !== '') {
this.setState({wrongPWDInfo: true});
// set timeout to make the info auto-disappearing
setTimeout(() => {
this.setState({wrongPWDInfo: false});
}, 2000);
} else {
this.props.onSuccessLogin();
}
},
true,
this.state.pwdText
);
}
/**
* key event handling
* @param event keyevent
*/
keypress(event: KeyboardEvent): void {
// hide if escape is pressed
if (event.key === 'Enter') {
// call submit
this.authenticate();
}
}
}
export default AuthenticationPage;

View File

@ -34,23 +34,25 @@ export class CategoryView extends React.Component<CategoryViewProps, CategoryVie
} }
componentDidMount(): void { componentDidMount(): void {
this.fetchVideoData(parseInt(this.props.match.params.id)); this.fetchVideoData(parseInt(this.props.match.params.id, 10));
} }
componentDidUpdate(prevProps: Readonly<CategoryViewProps>, prevState: Readonly<CategoryViewState>): void { componentDidUpdate(prevProps: Readonly<CategoryViewProps>): void {
// trigger video refresh if id changed // trigger video refresh if id changed
if (prevProps.match.params.id !== this.props.match.params.id) { if (prevProps.match.params.id !== this.props.match.params.id) {
this.setState({loaded: false}); this.reloadVideoData();
this.fetchVideoData(parseInt(this.props.match.params.id));
} }
} }
reloadVideoData(): void {
this.setState({loaded: false});
this.fetchVideoData(parseInt(this.props.match.params.id, 10));
}
render(): JSX.Element { render(): JSX.Element {
return ( return (
<> <>
<PageTitle <PageTitle title='Categories' subtitle={this.videodata.length + ' Videos'} />
title='Categories'
subtitle={this.videodata.length + ' Videos'}/>
<SideBar> <SideBar>
<SideBarTitle>Default Tags:</SideBarTitle> <SideBarTitle>Default Tags:</SideBarTitle>
@ -60,16 +62,23 @@ export class CategoryView extends React.Component<CategoryViewProps, CategoryVie
<Tag tagInfo={DefaultTags.lowq} /> <Tag tagInfo={DefaultTags.lowq} />
<Line /> <Line />
<Button title='Delete Tag' onClick={(): void => {this.deleteTag(false);}} color={{backgroundColor: 'red'}}/> <Button
title='Delete Tag'
onClick={(): void => {
this.deleteTag(false);
}}
color={{backgroundColor: 'red'}}
/>
</SideBar> </SideBar>
{this.state.loaded ? {this.state.loaded ? <VideoContainer data={this.videodata} /> : null}
<VideoContainer
data={this.videodata}/> : null}
<button data-testid='backbtn' className='btn btn-success' <button
data-testid='backbtn'
className='btn btn-success'
onClick={(): void => { onClick={(): void => {
this.props.history.push('/categories'); this.props.history.push('/categories');
}}>Back to Categories }}>
Back to Categories
</button> </button>
{this.handlePopups()} {this.handlePopups()}
</> </>
@ -78,9 +87,14 @@ export class CategoryView extends React.Component<CategoryViewProps, CategoryVie
private handlePopups(): JSX.Element { private handlePopups(): JSX.Element {
if (this.state.submitForceDelete) { if (this.state.submitForceDelete) {
return (<SubmitPopup return (
<SubmitPopup
onHide={(): void => this.setState({submitForceDelete: false})} onHide={(): void => this.setState({submitForceDelete: false})}
submit={(): void => {this.deleteTag(true);}}/>); submit={(): void => {
this.deleteTag(true);
}}
/>
);
} else { } else {
return <></>; return <></>;
} }
@ -91,7 +105,7 @@ export class CategoryView extends React.Component<CategoryViewProps, CategoryVie
* @param id tagid * @param id tagid
*/ */
private fetchVideoData(id: number): void { private fetchVideoData(id: number): void {
callAPI<VideoTypes.VideoUnloadedType[]>(APINode.Video, {action: 'getMovies', tag: id}, result => { callAPI<VideoTypes.VideoUnloadedType[]>(APINode.Video, {action: 'getMovies', tag: id}, (result) => {
this.videodata = result; this.videodata = result;
this.setState({loaded: true}); this.setState({loaded: true});
}); });
@ -101,11 +115,14 @@ export class CategoryView extends React.Component<CategoryViewProps, CategoryVie
* delete the current tag * delete the current tag
*/ */
private deleteTag(force: boolean): void { private deleteTag(force: boolean): void {
callAPI<GeneralSuccess>(APINode.Tags, { callAPI<GeneralSuccess>(
APINode.Tags,
{
action: 'deleteTag', action: 'deleteTag',
TagId: parseInt(this.props.match.params.id), TagId: parseInt(this.props.match.params.id, 10),
Force: force Force: force
}, result => { },
(result) => {
console.log(result.result); console.log(result.result);
if (result.result === 'success') { if (result.result === 'success') {
this.props.history.push('/categories'); this.props.history.push('/categories');
@ -113,7 +130,8 @@ export class CategoryView extends React.Component<CategoryViewProps, CategoryVie
// show submisison tag to ask if really delete // show submisison tag to ask if really delete
this.setState({submitForceDelete: true}); this.setState({submitForceDelete: true});
} }
}); }
);
} }
} }

View File

@ -15,10 +15,10 @@ interface TagViewState {
popupvisible: boolean; popupvisible: boolean;
} }
interface props {} interface Props {}
class TagView extends React.Component<props, TagViewState> { class TagView extends React.Component<Props, TagViewState> {
constructor(props: props) { constructor(props: Props) {
super(props); super(props);
this.state = { this.state = {
@ -34,9 +34,7 @@ class TagView extends React.Component<props, TagViewState> {
render(): JSX.Element { render(): JSX.Element {
return ( return (
<> <>
<PageTitle <PageTitle title='Categories' subtitle={this.state.loadedtags.length + ' different Tags'} />
title='Categories'
subtitle={this.state.loadedtags.length + ' different Tags'}/>
<SideBar> <SideBar>
<SideBarTitle>Default Tags:</SideBarTitle> <SideBarTitle>Default Tags:</SideBarTitle>
@ -46,18 +44,23 @@ class TagView extends React.Component<props, TagViewState> {
<Tag tagInfo={DefaultTags.lowq} /> <Tag tagInfo={DefaultTags.lowq} />
<Line /> <Line />
<button data-testid='btnaddtag' className='btn btn-success' onClick={(): void => { <button
data-testid='btnaddtag'
className='btn btn-success'
onClick={(): void => {
this.setState({popupvisible: true}); this.setState({popupvisible: true});
}}>Add a new Tag! }}>
Add a new Tag!
</button> </button>
</SideBar> </SideBar>
<div className={videocontainerstyle.maincontent}> <div className={videocontainerstyle.maincontent}>
{this.state.loadedtags ? {this.state.loadedtags
this.state.loadedtags.map((m) => ( ? this.state.loadedtags.map((m) => (
<Link to={'/categories/' + m.TagId} key={m.TagId}> <Link to={'/categories/' + m.TagId} key={m.TagId}>
<TagPreview name={m.TagName}/></Link> <TagPreview name={m.TagName} />
)) : </Link>
'loading'} ))
: 'loading'}
</div> </div>
{this.handlePopups()} {this.handlePopups()}
</> </>
@ -68,7 +71,7 @@ class TagView extends React.Component<props, TagViewState> {
* load all available tags from db. * load all available tags from db.
*/ */
loadTags(): void { loadTags(): void {
callAPI<TagType[]>(APINode.Tags, {action: 'getAllTags'}, result => { callAPI<TagType[]>(APINode.Tags, {action: 'getAllTags'}, (result) => {
this.setState({loadedtags: result}); this.setState({loadedtags: result});
}); });
} }
@ -76,13 +79,15 @@ class TagView extends React.Component<props, TagViewState> {
private handlePopups(): JSX.Element { private handlePopups(): JSX.Element {
if (this.state.popupvisible) { if (this.state.popupvisible) {
return ( return (
<NewTagPopup onHide={(): void => { <NewTagPopup
onHide={(): void => {
this.setState({popupvisible: false}); this.setState({popupvisible: false});
this.loadTags(); this.loadTags();
}}/> }}
/>
); );
} else { } else {
return (<></>); return <></>;
} }
} }
} }

View File

@ -10,25 +10,25 @@ import {Route, Switch, withRouter} from 'react-router-dom';
import {RouteComponentProps} from 'react-router'; import {RouteComponentProps} from 'react-router';
import SearchHandling from './SearchHandling'; import SearchHandling from './SearchHandling';
import {VideoTypes} from '../../types/ApiTypes'; import {VideoTypes} from '../../types/ApiTypes';
import {DefaultTags} from "../../types/GeneralTypes"; import {DefaultTags} from '../../types/GeneralTypes';
interface props extends RouteComponentProps {} interface Props extends RouteComponentProps {}
interface state { interface state {
sideinfo: VideoTypes.startDataType sideinfo: VideoTypes.startDataType;
subtitle: string, subtitle: string;
data: VideoTypes.VideoUnloadedType[], data: VideoTypes.VideoUnloadedType[];
selectionnr: number selectionnr: number;
} }
/** /**
* The home page component showing on the initial pageload * The home page component showing on the initial pageload
*/ */
export class HomePage extends React.Component<props, state> { export class HomePage extends React.Component<Props, state> {
/** keyword variable needed temporary store search keyword */ /** keyword variable needed temporary store search keyword */
keyword = ''; keyword = '';
constructor(props: props) { constructor(props: Props) {
super(props); super(props);
this.state = { this.state = {
@ -38,7 +38,7 @@ export class HomePage extends React.Component<props, state> {
HDNr: 0, HDNr: 0,
SDNr: 0, SDNr: 0,
DifferentTags: 0, DifferentTags: 0,
Tagged: 0, Tagged: 0
}, },
subtitle: 'All Videos', subtitle: 'All Videos',
data: [], data: [],
@ -65,7 +65,7 @@ export class HomePage extends React.Component<props, state> {
}); });
this.setState({ this.setState({
data: result, data: result,
selectionnr: result.length, selectionnr: result.length
}); });
}); });
} }
@ -79,7 +79,6 @@ export class HomePage extends React.Component<props, state> {
}); });
} }
render(): JSX.Element { render(): JSX.Element {
return ( return (
<> <>
@ -88,56 +87,78 @@ export class HomePage extends React.Component<props, state> {
<SearchHandling /> <SearchHandling />
</Route> </Route>
<Route path='/'> <Route path='/'>
<PageTitle <PageTitle title='Home Page' subtitle={this.state.subtitle + ' - ' + this.state.selectionnr}>
title='Home Page' <form
subtitle={this.state.subtitle + ' - ' + this.state.selectionnr}> className={'form-inline ' + style.searchform}
<form className={'form-inline ' + style.searchform} onSubmit={(e): void => { onSubmit={(e): void => {
e.preventDefault(); e.preventDefault();
this.props.history.push('/search/' + this.keyword); this.props.history.push('/search/' + this.keyword);
}}> }}>
<input data-testid='searchtextfield' className='form-control mr-sm-2' <input
type='text' placeholder='Search' data-testid='searchtextfield'
className='form-control mr-sm-2'
type='text'
placeholder='Search'
onChange={(e): void => { onChange={(e): void => {
this.keyword = e.target.value; this.keyword = e.target.value;
}}/> }}
<button data-testid='searchbtnsubmit' className='btn btn-success' type='submit'>Search</button> />
<button data-testid='searchbtnsubmit' className='btn btn-success' type='submit'>
Search
</button>
</form> </form>
</PageTitle> </PageTitle>
<SideBar> <SideBar>
<SideBarTitle>Infos:</SideBarTitle> <SideBarTitle>Infos:</SideBarTitle>
<Line /> <Line />
<SideBarItem><b>{this.state.sideinfo.VideoNr}</b> Videos Total!</SideBarItem> <SideBarItem>
<SideBarItem><b>{this.state.sideinfo.FullHdNr}</b> FULL-HD Videos!</SideBarItem> <b>{this.state.sideinfo.VideoNr}</b> Videos Total!
<SideBarItem><b>{this.state.sideinfo.HDNr}</b> HD Videos!</SideBarItem> </SideBarItem>
<SideBarItem><b>{this.state.sideinfo.SDNr}</b> SD Videos!</SideBarItem> <SideBarItem>
<SideBarItem><b>{this.state.sideinfo.DifferentTags}</b> different Tags!</SideBarItem> <b>{this.state.sideinfo.FullHdNr}</b> FULL-HD Videos!
</SideBarItem>
<SideBarItem>
<b>{this.state.sideinfo.HDNr}</b> HD Videos!
</SideBarItem>
<SideBarItem>
<b>{this.state.sideinfo.SDNr}</b> SD Videos!
</SideBarItem>
<SideBarItem>
<b>{this.state.sideinfo.DifferentTags}</b> different Tags!
</SideBarItem>
<Line /> <Line />
<SideBarTitle>Default Tags:</SideBarTitle> <SideBarTitle>Default Tags:</SideBarTitle>
<Tag tagInfo={{TagName: 'All', TagId: DefaultTags.all.TagId}} onclick={(): void => { <Tag
tagInfo={{TagName: 'All', TagId: DefaultTags.all.TagId}}
onclick={(): void => {
this.fetchVideoData(DefaultTags.all.TagId); this.fetchVideoData(DefaultTags.all.TagId);
this.setState({subtitle: `All Videos`}); this.setState({subtitle: 'All Videos'});
}}/> }}
<Tag tagInfo={{TagName: 'Full Hd', TagId: DefaultTags.fullhd.TagId}} onclick={(): void => { />
<Tag
tagInfo={{TagName: 'Full Hd', TagId: DefaultTags.fullhd.TagId}}
onclick={(): void => {
this.fetchVideoData(DefaultTags.fullhd.TagId); this.fetchVideoData(DefaultTags.fullhd.TagId);
this.setState({subtitle: `Full Hd Videos`}); this.setState({subtitle: 'Full Hd Videos'});
}}/> }}
<Tag tagInfo={{TagName: 'Low Quality', TagId: DefaultTags.lowq.TagId}} />
<Tag
tagInfo={{TagName: 'Low Quality', TagId: DefaultTags.lowq.TagId}}
onclick={(): void => { onclick={(): void => {
this.fetchVideoData(DefaultTags.lowq.TagId); this.fetchVideoData(DefaultTags.lowq.TagId);
this.setState({subtitle: `Low Quality Videos`}); this.setState({subtitle: 'Low Quality Videos'});
}}/> }}
<Tag tagInfo={{TagName: 'HD', TagId: DefaultTags.hd.TagId}} onclick={(): void => { />
<Tag
tagInfo={{TagName: 'HD', TagId: DefaultTags.hd.TagId}}
onclick={(): void => {
this.fetchVideoData(DefaultTags.hd.TagId); this.fetchVideoData(DefaultTags.hd.TagId);
this.setState({subtitle: `HD Videos`}); this.setState({subtitle: 'HD Videos'});
}}/> }}
/>
</SideBar> </SideBar>
{this.state.data.length !== 0 ? {this.state.data.length !== 0 ? <VideoContainer data={this.state.data} /> : <div>No Data found!</div>}
<VideoContainer <div className={style.rightinfo} />
data={this.state.data}/> :
<div>No Data found!</div>}
<div className={style.rightinfo}>
</div>
</Route> </Route>
</Switch> </Switch>
</> </>

View File

@ -11,14 +11,14 @@ interface params {
name: string; name: string;
} }
interface props extends RouteComponentProps<params> {} interface Props extends RouteComponentProps<params> {}
interface state { interface state {
data: VideoTypes.VideoUnloadedType[]; data: VideoTypes.VideoUnloadedType[];
} }
export class SearchHandling extends React.Component<props, state> { export class SearchHandling extends React.Component<Props, state> {
constructor(props: props) { constructor(props: Props) {
super(props); super(props);
this.state = { this.state = {
@ -45,11 +45,9 @@ export class SearchHandling extends React.Component<props, state> {
*/ */
getVideoData(): JSX.Element { getVideoData(): JSX.Element {
if (this.state.data.length !== 0) { if (this.state.data.length !== 0) {
return ( return <VideoContainer data={this.state.data} />;
<VideoContainer data={this.state.data}/>
);
} else { } else {
return (<div>No Data found!</div>); return <div>No Data found!</div>;
} }
} }

View File

@ -20,22 +20,22 @@ import {ActorType, TagType} from '../../types/VideoTypes';
import PlyrJS from 'plyr'; import PlyrJS from 'plyr';
import {Button} from '../../elements/GPElements/Button'; import {Button} from '../../elements/GPElements/Button';
import {VideoTypes} from '../../types/ApiTypes'; import {VideoTypes} from '../../types/ApiTypes';
import GlobalInfos from "../../utils/GlobalInfos"; import GlobalInfos from '../../utils/GlobalInfos';
interface myprops extends RouteComponentProps<{id: string}> {} interface myprops extends RouteComponentProps<{id: string}> {}
interface mystate { interface mystate {
sources?: PlyrJS.SourceInfo, sources?: PlyrJS.SourceInfo;
movie_id: number, movieId: number;
movie_name: string, movieName: string;
likes: number, likes: number;
quality: number, quality: number;
length: number, length: number;
tags: TagType[], tags: TagType[];
suggesttag: TagType[], suggesttag: TagType[];
popupvisible: boolean, popupvisible: boolean;
actorpopupvisible: boolean, actorpopupvisible: boolean;
actors: ActorType[] actors: ActorType[];
} }
/** /**
@ -64,8 +64,8 @@ export class Player extends React.Component<myprops, mystate> {
super(props); super(props);
this.state = { this.state = {
movie_id: -1, movieId: -1,
movie_name: '', movieName: '',
likes: 0, likes: 0,
quality: 0, quality: 0,
length: 0, length: 0,
@ -87,27 +87,37 @@ export class Player extends React.Component<myprops, mystate> {
render(): JSX.Element { render(): JSX.Element {
return ( return (
<div id='videocontainer'> <div id='videocontainer'>
<PageTitle <PageTitle title='Watch' subtitle={this.state.movieName} />
title='Watch'
subtitle={this.state.movie_name}/>
{this.assembleSideBar()} {this.assembleSideBar()}
<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 ? (
style={plyrstyle} <Plyr style={plyrstyle} source={this.state.sources} options={this.options} />
source={this.state.sources} ) : (
options={this.options}/> : <div>not loaded yet</div>
<div>not loaded yet</div>} )}
<div className={style.videoactions}> <div className={style.videoactions}>
<Button onClick={(): void => this.likebtn()} title='Like this Video!' color={{backgroundColor: 'green'}} /> <Button onClick={(): void => this.likebtn()} title='Like this Video!' color={{backgroundColor: 'green'}} />
<Button onClick={(): void => this.setState({popupvisible: true})} title='Give this Video a Tag' color={{backgroundColor: '#3574fe'}}/> <Button
<Button title='Delete Video' onClick={(): void => {this.deleteVideo();}} color={{backgroundColor: 'red'}}/> onClick={(): void => this.setState({popupvisible: true})}
title='Give this Video a Tag'
color={{backgroundColor: '#3574fe'}}
/>
<Button
title='Delete Video'
onClick={(): void => {
this.deleteVideo();
}}
color={{backgroundColor: 'red'}}
/>
</div> </div>
{this.assembleActorTiles()} {this.assembleActorTiles()}
</div> </div>
<button className={style.closebutton} onClick={(): void => this.closebtn()}>Close</button> <button className={style.closebutton} onClick={(): void => this.closebtn()}>
Close
</button>
{ {
// handle the popovers switched on and off according to state changes // handle the popovers switched on and off according to state changes
this.handlePopOvers() this.handlePopOvers()
@ -124,11 +134,19 @@ export class Player extends React.Component<myprops, mystate> {
<SideBar> <SideBar>
<SideBarTitle>Infos:</SideBarTitle> <SideBarTitle>Infos:</SideBarTitle>
<Line /> <Line />
<SideBarItem><b>{this.state.likes}</b> Likes!</SideBarItem> <SideBarItem>
{this.state.quality !== 0 ? <b>{this.state.likes}</b> Likes!
<SideBarItem><b>{this.state.quality}p</b> Quality!</SideBarItem> : null} </SideBarItem>
{this.state.length !== 0 ? {this.state.quality !== 0 ? (
<SideBarItem><b>{Math.round(this.state.length / 60)}</b> Minutes of length!</SideBarItem> : null} <SideBarItem>
<b>{this.state.quality}p</b> Quality!
</SideBarItem>
) : null}
{this.state.length !== 0 ? (
<SideBarItem>
<b>{Math.round(this.state.length / 60)}</b> Minutes of length!
</SideBarItem>
) : null}
<Line /> <Line />
<SideBarTitle>Tags:</SideBarTitle> <SideBarTitle>Tags:</SideBarTitle>
{this.state.tags.map((m: TagType) => ( {this.state.tags.map((m: TagType) => (
@ -142,7 +160,8 @@ export class Player extends React.Component<myprops, mystate> {
key={m.TagName} key={m.TagName}
onclick={(): void => { onclick={(): void => {
this.quickAddTag(m.TagId, m.TagName); this.quickAddTag(m.TagId, m.TagName);
}}/> }}
/>
))} ))}
</SideBar> </SideBar>
); );
@ -154,18 +173,20 @@ export class Player extends React.Component<myprops, mystate> {
private assembleActorTiles(): JSX.Element { private assembleActorTiles(): JSX.Element {
return ( return (
<div className={style.actorcontainer}> <div className={style.actorcontainer}>
{this.state.actors ? {this.state.actors ? this.state.actors.map((actr: ActorType) => <ActorTile key={actr.ActorId} actor={actr} />) : <></>}
this.state.actors.map((actr: ActorType) => ( <div
<ActorTile key={actr.ActorId} actor={actr}/> className={style.actorAddTile}
)) : <></> onClick={(): void => {
}
<div className={style.actorAddTile} onClick={(): void => {
this.addActor(); this.addActor();
}}> }}>
<div className={style.actorAddTile_thumbnail}> <div className={style.actorAddTile_thumbnail}>
<FontAwesomeIcon style={{ <FontAwesomeIcon
style={{
lineHeight: '130px' lineHeight: '130px'
}} icon={faPlusCircle} size='5x'/> }}
icon={faPlusCircle}
size='5x'
/>
</div> </div>
<div className={style.actorAddTile_name}>Add Actor</div> <div className={style.actorAddTile_name}>Add Actor</div>
</div> </div>
@ -173,7 +194,6 @@ export class Player extends React.Component<myprops, mystate> {
); );
} }
/** /**
* handle the popovers generated according to state changes * handle the popovers generated according to state changes
* @returns {JSX.Element} * @returns {JSX.Element}
@ -181,18 +201,18 @@ export class Player extends React.Component<myprops, mystate> {
handlePopOvers(): JSX.Element { handlePopOvers(): JSX.Element {
return ( return (
<> <>
{ {this.state.popupvisible ? (
this.state.popupvisible ? <AddTagPopup onHide={(): void => this.setState({popupvisible: false})} submit={this.quickAddTag} />
<AddTagPopup onHide={(): void => this.setState({popupvisible: false})} ) : null}
submit={this.quickAddTag}/> : null {this.state.actorpopupvisible ? (
} <AddActorPopup
{ onHide={(): void => {
this.state.actorpopupvisible ?
<AddActorPopup onHide={(): void => {
this.refetchActors(); this.refetchActors();
this.setState({actorpopupvisible: false}); this.setState({actorpopupvisible: false});
}} movie_id={this.state.movie_id}/> : null }}
} movieId={this.state.movieId}
/>
) : null}
</> </>
); );
} }
@ -203,27 +223,34 @@ export class Player extends React.Component<myprops, mystate> {
* @param tagName name of tag to add * @param tagName name of tag to add
*/ */
quickAddTag(tagId: number, tagName: string): void { quickAddTag(tagId: number, tagName: string): void {
callAPI(APINode.Tags, { callAPI(
APINode.Tags,
{
action: 'addTag', action: 'addTag',
TagId: tagId, TagId: tagId,
MovieId: parseInt(this.props.match.params.id) MovieId: parseInt(this.props.match.params.id, 10)
}, (result: GeneralSuccess) => { },
(result: GeneralSuccess) => {
if (result.result !== 'success') { if (result.result !== 'success') {
console.error('error occured while writing to db -- todo error handling'); console.error('error occured while writing to db -- todo error handling');
console.error(result.result); console.error(result.result);
} else { } else {
// check if tag has already been added // check if tag has already been added
const tagIndex = this.state.tags.map(function (e: TagType) { const tagIndex = this.state.tags
.map(function (e: TagType) {
return e.TagName; return e.TagName;
}).indexOf(tagName); })
.indexOf(tagName);
// only add tag if it isn't already there // only add tag if it isn't already there
if (tagIndex === -1) { if (tagIndex === -1) {
// update tags if successful // update tags if successful
let array = [...this.state.suggesttag]; // make a separate copy of the array (because of setState) let array = [...this.state.suggesttag]; // make a separate copy of the array (because of setState)
const quickaddindex = this.state.suggesttag.map(function (e: TagType) { const quickaddindex = this.state.suggesttag
.map(function (e: TagType) {
return e.TagId; return e.TagId;
}).indexOf(tagId); })
.indexOf(tagId);
// check if tag is available in quickadds // check if tag is available in quickadds
if (quickaddindex !== -1) { if (quickaddindex !== -1) {
@ -240,29 +267,39 @@ export class Player extends React.Component<myprops, mystate> {
} }
} }
} }
}); }
);
} }
/** /**
* fetch all the required infos of a video from backend * fetch all the required infos of a video from backend
*/ */
fetchMovieData(): void { fetchMovieData(): void {
callAPI(APINode.Video, {action: 'loadVideo', MovieId: parseInt(this.props.match.params.id)}, (result: VideoTypes.loadVideoType) => { callAPI(
console.log(result) APINode.Video,
{action: 'loadVideo', MovieId: parseInt(this.props.match.params.id, 10)},
(result: VideoTypes.loadVideoType) => {
console.log(result);
console.log(process.env.REACT_APP_CUST_BACK_DOMAIN);
this.setState({ this.setState({
sources: { sources: {
type: 'video', type: 'video',
sources: [ sources: [
{ {
src: getBackendDomain() + GlobalInfos.getVideoPath() + result.MovieUrl, src:
(process.env.REACT_APP_CUST_BACK_DOMAIN
? process.env.REACT_APP_CUST_BACK_DOMAIN
: getBackendDomain()) +
GlobalInfos.getVideoPath() +
result.MovieUrl,
type: 'video/mp4', type: 'video/mp4',
size: 1080 size: 1080
} }
], ],
poster: result.Poster poster: result.Poster
}, },
movie_id: result.MovieId, movieId: result.MovieId,
movie_name: result.MovieName, movieName: result.MovieName,
likes: result.Likes, likes: result.Likes,
quality: result.Quality, quality: result.Quality,
length: result.Length, length: result.Length,
@ -270,15 +307,15 @@ export class Player extends React.Component<myprops, mystate> {
suggesttag: result.SuggestedTag, suggesttag: result.SuggestedTag,
actors: result.Actors actors: result.Actors
}); });
});
} }
);
}
/** /**
* click handler for the like btn * click handler for the like btn
*/ */
likebtn(): void { likebtn(): void {
callAPI(APINode.Video, {action: 'addLike', MovieId: parseInt(this.props.match.params.id)}, (result: GeneralSuccess) => { callAPI(APINode.Video, {action: 'addLike', MovieId: parseInt(this.props.match.params.id, 10)}, (result: GeneralSuccess) => {
if (result.result === 'success') { if (result.result === 'success') {
// likes +1 --> avoid reload of all data // likes +1 --> avoid reload of all data
this.setState({likes: this.state.likes + 1}); this.setState({likes: this.state.likes + 1});
@ -301,7 +338,10 @@ export class Player extends React.Component<myprops, mystate> {
* delete the current video and return to last page * delete the current video and return to last page
*/ */
deleteVideo(): void { deleteVideo(): void {
callAPI(APINode.Video, {action: 'deleteVideo', MovieId: parseInt(this.props.match.params.id)}, (result: GeneralSuccess) => { callAPI(
APINode.Video,
{action: 'deleteVideo', MovieId: parseInt(this.props.match.params.id, 10)},
(result: GeneralSuccess) => {
if (result.result === 'success') { if (result.result === 'success') {
// return to last element if successful // return to last element if successful
this.props.history.goBack(); this.props.history.goBack();
@ -309,7 +349,8 @@ export class Player extends React.Component<myprops, mystate> {
console.error('an error occured while liking'); console.error('an error occured while liking');
console.error(result); console.error(result);
} }
}); }
);
} }
/** /**
@ -323,11 +364,14 @@ export class Player extends React.Component<myprops, mystate> {
* fetch the available video actors again * fetch the available video actors again
*/ */
refetchActors(): void { refetchActors(): void {
callAPI<ActorType[]>(APINode.Actor, {action: 'getActorsOfVideo', MovieId: parseInt(this.props.match.params.id)}, result => { callAPI<ActorType[]>(
APINode.Actor,
{action: 'getActorsOfVideo', MovieId: parseInt(this.props.match.params.id, 10)},
(result) => {
this.setState({actors: result}); this.setState({actors: result});
}); }
);
} }
} }
export default withRouter(Player); export default withRouter(Player);

View File

@ -47,8 +47,7 @@ class RandomPage extends React.Component<{}, state> {
render(): JSX.Element { render(): JSX.Element {
return ( return (
<div> <div>
<PageTitle title='Random Videos' <PageTitle title='Random Videos' subtitle='4pc' />
subtitle='4pc'/>
<SideBar> <SideBar>
<SideBarTitle>Visible Tags:</SideBarTitle> <SideBarTitle>Visible Tags:</SideBarTitle>
@ -57,16 +56,17 @@ class RandomPage extends React.Component<{}, state> {
))} ))}
</SideBar> </SideBar>
{this.state.videos.length !== 0 ? {this.state.videos.length !== 0 ? (
<VideoContainer <VideoContainer data={this.state.videos}>
data={this.state.videos}>
<div className={style.Shufflebutton}> <div className={style.Shufflebutton}>
<button onClick={(): void => this.shuffleclick()} className={style.btnshuffle}>Shuffle</button> <button onClick={(): void => this.shuffleclick()} className={style.btnshuffle}>
Shuffle
</button>
</div> </div>
</VideoContainer> </VideoContainer>
: ) : (
<div>No Data found!</div>} <div>No Data found!</div>
)}
</div> </div>
); );
} }
@ -83,8 +83,8 @@ class RandomPage extends React.Component<{}, state> {
* @param nr number of videos to load * @param nr number of videos to load
*/ */
loadShuffledvideos(nr: number): void { loadShuffledvideos(nr: number): void {
callAPI<GetRandomMoviesType>(APINode.Video, {action: 'getRandomMovies', number: nr}, result => { callAPI<GetRandomMoviesType>(APINode.Video, {action: 'getRandomMovies', number: nr}, (result) => {
console.log(result) console.log(result);
this.setState({videos: []}); // needed to trigger rerender of main videoview this.setState({videos: []}); // needed to trigger rerender of main videoview
this.setState({ this.setState({
videos: result.Videos, videos: result.Videos,

View File

@ -11,20 +11,19 @@ import {SettingsTypes} from '../../types/ApiTypes';
import {GeneralSuccess} from '../../types/GeneralTypes'; import {GeneralSuccess} from '../../types/GeneralTypes';
interface state { interface state {
customapi: boolean customapi: boolean;
apipath: string apipath: string;
generalSettings: SettingsTypes.loadGeneralSettingsType generalSettings: SettingsTypes.loadGeneralSettingsType;
} }
interface props { interface Props {}
}
/** /**
* Component for Generalsettings tag on Settingspage * Component for Generalsettings tag on Settingspage
* handles general settings of mediacenter which concerns to all pages * handles general settings of mediacenter which concerns to all pages
*/ */
class GeneralSettings extends React.Component<props, state> { class GeneralSettings extends React.Component<Props, state> {
constructor(props: props) { constructor(props: Props) {
super(props); super(props);
this.state = { this.state = {
@ -34,14 +33,14 @@ class GeneralSettings extends React.Component<props, state> {
DarkMode: true, DarkMode: true,
DBSize: 0, DBSize: 0,
DifferentTags: 0, DifferentTags: 0,
EpisodePath: "", EpisodePath: '',
MediacenterName: "", MediacenterName: '',
Password: "", Password: '',
PasswordEnabled: false, PasswordEnabled: false,
TagsAdded: 0, TagsAdded: 0,
TMDBGrabbing: false, TMDBGrabbing: false,
VideoNr: 0, VideoNr: 0,
VideoPath: "" VideoPath: ''
} }
}; };
} }
@ -55,51 +54,71 @@ class GeneralSettings extends React.Component<props, state> {
return ( return (
<> <>
<div className={style.infoheader}> <div className={style.infoheader}>
<InfoHeaderItem backColor='lightblue' <InfoHeaderItem
backColor='lightblue'
text={this.state.generalSettings.VideoNr} text={this.state.generalSettings.VideoNr}
subtext='Videos in Gravity' subtext='Videos in Gravity'
icon={faArchive}/> icon={faArchive}
<InfoHeaderItem backColor='yellow' />
<InfoHeaderItem
backColor='yellow'
text={this.state.generalSettings.DBSize + ' MB'} text={this.state.generalSettings.DBSize + ' MB'}
subtext='Database size' subtext='Database size'
icon={faRulerVertical}/> icon={faRulerVertical}
<InfoHeaderItem backColor='green' />
<InfoHeaderItem
backColor='green'
text={this.state.generalSettings.DifferentTags} text={this.state.generalSettings.DifferentTags}
subtext='different Tags' subtext='different Tags'
icon={faAddressCard}/> icon={faAddressCard}
<InfoHeaderItem backColor='orange' />
<InfoHeaderItem
backColor='orange'
text={this.state.generalSettings.TagsAdded} text={this.state.generalSettings.TagsAdded}
subtext='tags added' subtext='tags added'
icon={faBalanceScaleLeft}/> icon={faBalanceScaleLeft}
/>
</div> </div>
<div className={style.GeneralForm + ' ' + themeStyle.subtextcolor}> <div className={style.GeneralForm + ' ' + themeStyle.subtextcolor}>
<Form data-testid='mainformsettings' onSubmit={(e): void => { <Form
data-testid='mainformsettings'
onSubmit={(e): void => {
e.preventDefault(); e.preventDefault();
this.saveSettings(); this.saveSettings();
}}> }}>
<Form.Row> <Form.Row>
<Form.Group as={Col} data-testid='videpathform'> <Form.Group as={Col} data-testid='videpathform'>
<Form.Label>Video Path</Form.Label> <Form.Label>Video Path</Form.Label>
<Form.Control type='text' placeholder='/var/www/html/video' <Form.Control
type='text'
placeholder='/var/www/html/video'
value={this.state.generalSettings.VideoPath} value={this.state.generalSettings.VideoPath}
onChange={(ee): void => this.setState({ onChange={(ee): void =>
this.setState({
generalSettings: { generalSettings: {
...this.state.generalSettings, ...this.state.generalSettings,
VideoPath: ee.target.value VideoPath: ee.target.value
} }
})}/> })
}
/>
</Form.Group> </Form.Group>
<Form.Group as={Col} data-testid='tvshowpath'> <Form.Group as={Col} data-testid='tvshowpath'>
<Form.Label>TV Show Path</Form.Label> <Form.Label>TV Show Path</Form.Label>
<Form.Control type='text' placeholder='/var/www/html/tvshow' <Form.Control
type='text'
placeholder='/var/www/html/tvshow'
value={this.state.generalSettings.EpisodePath} value={this.state.generalSettings.EpisodePath}
onChange={(e): void => this.setState({ onChange={(e): void =>
this.setState({
generalSettings: { generalSettings: {
...this.state.generalSettings, ...this.state.generalSettings,
EpisodePath: e.target.value EpisodePath: e.target.value
} }
})}/> })
}
/>
</Form.Group> </Form.Group>
</Form.Row> </Form.Row>
@ -116,17 +135,20 @@ class GeneralSettings extends React.Component<props, state> {
this.setState({customapi: !this.state.customapi}); this.setState({customapi: !this.state.customapi});
}} }}
/> />
{this.state.customapi ? {this.state.customapi ? (
<Form.Group className={style.customapiform} data-testid='apipath'> <Form.Group className={style.customapiform} data-testid='apipath'>
<Form.Label>API Backend url</Form.Label> <Form.Label>API Backend url</Form.Label>
<Form.Control type='text' placeholder='https://127.0.0.1' <Form.Control
type='text'
placeholder='https://127.0.0.1'
value={this.state.apipath} value={this.state.apipath}
onChange={(e): void => { onChange={(e): void => {
this.setState({apipath: e.target.value}); this.setState({apipath: e.target.value});
setCustomBackendDomain(e.target.value); setCustomBackendDomain(e.target.value);
}}/> }}
</Form.Group> : null} />
</Form.Group>
) : null}
<Form.Check <Form.Check
type='switch' type='switch'
@ -144,19 +166,24 @@ class GeneralSettings extends React.Component<props, state> {
}} }}
/> />
{this.state.generalSettings.PasswordEnabled ? {this.state.generalSettings.PasswordEnabled ? (
<Form.Group data-testid='passwordfield'> <Form.Group data-testid='passwordfield'>
<Form.Label>Password</Form.Label> <Form.Label>Password</Form.Label>
<Form.Control type='password' placeholder='**********' <Form.Control
type='password'
placeholder='**********'
value={this.state.generalSettings.Password} value={this.state.generalSettings.Password}
onChange={(e): void => this.setState({ onChange={(e): void =>
this.setState({
generalSettings: { generalSettings: {
...this.state.generalSettings, ...this.state.generalSettings,
Password: e.target.value Password: e.target.value
} }
})}/> })
</Form.Group> : null
} }
/>
</Form.Group>
) : null}
<Form.Check <Form.Check
type='switch' type='switch'
@ -189,14 +216,19 @@ class GeneralSettings extends React.Component<props, state> {
<Form.Group className={style.mediacenternameform} data-testid='nameform'> <Form.Group className={style.mediacenternameform} data-testid='nameform'>
<Form.Label>The name of the Mediacenter</Form.Label> <Form.Label>The name of the Mediacenter</Form.Label>
<Form.Control type='text' placeholder='Mediacentername' <Form.Control
type='text'
placeholder='Mediacentername'
value={this.state.generalSettings.MediacenterName} value={this.state.generalSettings.MediacenterName}
onChange={(e): void => this.setState({ onChange={(e): void =>
this.setState({
generalSettings: { generalSettings: {
...this.state.generalSettings, ...this.state.generalSettings,
MediacenterName: e.target.value MediacenterName: e.target.value
} }
})}/> })
}
/>
</Form.Group> </Form.Group>
<Button variant='primary' type='submit'> <Button variant='primary' type='submit'>
@ -204,9 +236,7 @@ class GeneralSettings extends React.Component<props, state> {
</Button> </Button>
</Form> </Form>
</div> </div>
<div className={style.footer}> <div className={style.footer}>Version: {version}</div>
Version: {version}
</div>
</> </>
); );
} }
@ -228,12 +258,15 @@ class GeneralSettings extends React.Component<props, state> {
if (!this.state.generalSettings.PasswordEnabled) { if (!this.state.generalSettings.PasswordEnabled) {
settings.Password = '-1'; settings.Password = '-1';
} }
settings.DarkMode = GlobalInfos.isDarkTheme() settings.DarkMode = GlobalInfos.isDarkTheme();
callAPI(APINode.Settings, { callAPI(
APINode.Settings,
{
action: 'saveGeneralSettings', action: 'saveGeneralSettings',
Settings: settings Settings: settings
}, (result: GeneralSuccess) => { },
(result: GeneralSuccess) => {
if (result.result) { if (result.result) {
console.log('successfully saved settings'); console.log('successfully saved settings');
// todo 2020-07-10: popup success // todo 2020-07-10: popup success
@ -241,7 +274,8 @@ class GeneralSettings extends React.Component<props, state> {
console.log('failed to save settings'); console.log('failed to save settings');
// todo 2020-07-10: popup error // todo 2020-07-10: popup error
} }
}); }
);
} }
} }

View File

@ -5,20 +5,20 @@ import {GeneralSuccess} from '../../types/GeneralTypes';
import {SettingsTypes} from '../../types/ApiTypes'; import {SettingsTypes} from '../../types/ApiTypes';
interface state { interface state {
text: string[] text: string[];
startbtnDisabled: boolean startbtnDisabled: boolean;
} }
interface props {} interface Props {}
/** /**
* Component for MovieSettings on Settingspage * Component for MovieSettings on Settingspage
* handles settings concerning to movies in general * handles settings concerning to movies in general
*/ */
class MovieSettings extends React.Component<props, state> { class MovieSettings extends React.Component<Props, state> {
myinterval: number = -1; myinterval: number = -1;
constructor(props: props) { constructor(props: Props) {
super(props); super(props);
this.state = { this.state = {
@ -32,23 +32,36 @@ class MovieSettings extends React.Component<props, state> {
} }
componentWillUnmount(): void { componentWillUnmount(): void {
if (this.myinterval !== -1) if (this.myinterval !== -1) {
clearInterval(this.myinterval); clearInterval(this.myinterval);
} }
}
render(): JSX.Element { render(): JSX.Element {
return ( return (
<> <>
<button disabled={this.state.startbtnDisabled} <button
disabled={this.state.startbtnDisabled}
className='btn btn-success' className='btn btn-success'
onClick={(): void => {this.startReindex();}}>Reindex Movie onClick={(): void => {
this.startReindex();
}}>
Reindex Movie
</button> </button>
<button className='btn btn-warning' <button
onClick={(): void => {this.cleanupGravity();}}>Cleanup Gravity className='btn btn-warning'
onClick={(): void => {
this.cleanupGravity();
}}>
Cleanup Gravity
</button> </button>
<div className={style.indextextarea}>{this.state.text.map(m => ( <div className={style.indextextarea}>
<div key={m} className='textarea-element'>{m}</div> {this.state.text.map((m) => (
))}</div> <div key={m} className='textarea-element'>
{m}
</div>
))}
</div>
</> </>
); );
} }
@ -99,7 +112,7 @@ class MovieSettings extends React.Component<props, state> {
* send request to cleanup db gravity * send request to cleanup db gravity
*/ */
cleanupGravity(): void { cleanupGravity(): void {
callAPI(APINode.Settings, {action: 'cleanupGravity'}, (result) => { callAPI(APINode.Settings, {action: 'cleanupGravity'}, () => {
this.setState({ this.setState({
text: ['successfully cleaned up gravity!'] text: ['successfully cleaned up gravity!']
}); });

View File

@ -28,16 +28,16 @@ class SettingsPage extends React.Component {
</div> </div>
<div className={style.SettingsContent}> <div className={style.SettingsContent}>
<Switch> <Switch>
<Route path="/settings/general"> <Route path='/settings/general'>
<GeneralSettings /> <GeneralSettings />
</Route> </Route>
<Route path="/settings/movies"> <Route path='/settings/movies'>
<MovieSettings /> <MovieSettings />
</Route> </Route>
<Route path="/settings/tv"> <Route path='/settings/tv'>
<span /> <span />
</Route> </Route>
<Route path="/settings"> <Route path='/settings'>
<Redirect to='/settings/general' /> <Redirect to='/settings/general' />
</Route> </Route>
</Switch> </Switch>

View File

@ -37,6 +37,7 @@ global.prepareFailingFetchApi = () => {
global.callAPIMock = (resonse) => { global.callAPIMock = (resonse) => {
const helpers = require('./utils/Api'); const helpers = require('./utils/Api');
helpers.callAPI = jest.fn().mockImplementation((_, __, func1) => {func1(resonse);}); helpers.callAPI = jest.fn().mockImplementation((_, __, func1) => {func1(resonse);});
helpers.callApiUnsafe = jest.fn().mockImplementation((_, __, func1) => {func1(resonse);});
}; };
// code to run before each test // code to run before each test
@ -51,3 +52,12 @@ global.afterEach(() => {
jest.resetAllMocks(); jest.resetAllMocks();
}); });
global.mockKeyPress = () => {
let events = [];
document.addEventListener = jest.fn((event, cb) => {
events[event] = cb;
});
return events;
}

View File

@ -2,16 +2,16 @@ import {ActorType, TagType} from './VideoTypes';
export namespace VideoTypes { export namespace VideoTypes {
export interface loadVideoType { export interface loadVideoType {
MovieUrl: string MovieUrl: string;
Poster: string Poster: string;
MovieId: number MovieId: number;
MovieName: string MovieName: string;
Likes: number Likes: number;
Quality: number Quality: number;
Length: number Length: number;
Tags: TagType[] Tags: TagType[];
SuggestedTag: TagType[] SuggestedTag: TagType[];
Actors: ActorType[] Actors: ActorType[];
} }
export interface startDataType { export interface startDataType {
@ -25,7 +25,7 @@ export namespace VideoTypes {
export interface VideoUnloadedType { export interface VideoUnloadedType {
MovieId: number; MovieId: number;
MovieName: string MovieName: string;
} }
} }
@ -33,23 +33,23 @@ export namespace SettingsTypes {
export interface initialApiCallData { export interface initialApiCallData {
DarkMode: boolean; DarkMode: boolean;
Password: boolean; Password: boolean;
Mediacenter_name: string; MediacenterName: string;
VideoPath: string; VideoPath: string;
} }
export interface loadGeneralSettingsType { export interface loadGeneralSettingsType {
VideoPath: string, VideoPath: string;
EpisodePath: string, EpisodePath: string;
MediacenterName: string, MediacenterName: string;
Password: string, Password: string;
PasswordEnabled: boolean, PasswordEnabled: boolean;
TMDBGrabbing: boolean, TMDBGrabbing: boolean;
DarkMode: boolean, DarkMode: boolean;
VideoNr: number, VideoNr: number;
DBSize: number, DBSize: number;
DifferentTags: number, DifferentTags: number;
TagsAdded: number TagsAdded: number;
} }
export interface getStatusMessageType { export interface getStatusMessageType {

View File

@ -1,11 +1,11 @@
import {TagType} from './VideoTypes'; import {TagType} from './VideoTypes';
export interface GeneralSuccess { export interface GeneralSuccess {
result: string result: string;
} }
interface TagarrayType { interface TagarrayType {
[_: string]: TagType [_: string]: TagType;
} }
export const DefaultTags: TagarrayType = { export const DefaultTags: TagarrayType = {

View File

@ -2,8 +2,8 @@
* type accepted by Tag component * type accepted by Tag component
*/ */
export interface TagType { export interface TagType {
TagName: string TagName: string;
TagId: number TagId: number;
} }
export interface ActorType { export interface ActorType {

View File

@ -1,3 +1,5 @@
import GlobalInfos from './GlobalInfos';
let customBackendURL: string; let customBackendURL: string;
/** /**
@ -8,13 +10,13 @@ export function getBackendDomain(): string {
let userAgent = navigator.userAgent.toLowerCase(); let userAgent = navigator.userAgent.toLowerCase();
if (userAgent.indexOf(' electron/') > -1) { if (userAgent.indexOf(' electron/') > -1) {
// Electron-specific code - force a custom backendurl // Electron-specific code - force a custom backendurl
return (customBackendURL); return customBackendURL;
} else { } else {
// use custom only if defined // use custom only if defined
if (customBackendURL) { if (customBackendURL) {
return (customBackendURL); return customBackendURL;
} else { } else {
return (window.location.origin); return window.location.origin;
} }
} }
} }
@ -38,33 +40,28 @@ function getAPIDomain(): string {
* interface how an api request should look like * interface how an api request should look like
*/ */
interface ApiBaseRequest { interface ApiBaseRequest {
action: string | number, action: string | number;
[_: string]: string | number | boolean | object [_: string]: string | number | boolean | object;
} }
// store api token - empty if not set // store api token - empty if not set
let apiToken = '' let apiToken = '';
// a callback que to be called after api token refresh // a callback que to be called after api token refresh
let callQue: (() => void)[] = [] let callQue: ((error: string) => void)[] = [];
// flag to check wheter a api refresh is currently pending // flag to check wheter a api refresh is currently pending
let refreshInProcess = false; let refreshInProcess = false;
// store the expire seconds of token // store the expire seconds of token
let expireSeconds = -1; let expireSeconds = -1;
interface APIToken {
access_token: string;
expires_in: number;
scope: string;
token_type: string;
}
/** /**
* refresh the api token or use that one in cookie if still valid * refresh the api token or use that one in cookie if still valid
* @param callback to be called after successful refresh * @param callback to be called after successful refresh
* @param password
* @param force
*/ */
export function refreshAPIToken(callback: () => void): void { export function refreshAPIToken(callback: (error: string) => void, force?: boolean, password?: string): void {
callQue.push(callback); callQue.push(callback);
// check if already is a token refresh is in process // check if already is a token refresh is in process
@ -76,51 +73,72 @@ export function refreshAPIToken(callback: () => void): void {
refreshInProcess = true; refreshInProcess = true;
} }
if (apiTokenValid() && !force) {
console.log('token still valid...');
callFuncQue('');
return;
}
const formData = new FormData();
formData.append('grant_type', 'client_credentials');
formData.append('client_id', 'openmediacenter');
formData.append('client_secret', password ? password : 'openmediacenter');
formData.append('scope', 'all');
interface APIToken {
error?: string;
// eslint-disable-next-line camelcase
access_token: string; // no camel case allowed because of backendlib
// eslint-disable-next-line camelcase
expires_in: number; // no camel case allowed because of backendlib
scope: string;
// eslint-disable-next-line camelcase
token_type: string; // no camel case allowed because of backendlib
}
fetch(getBackendDomain() + '/token', {method: 'POST', body: formData}).then((response) =>
response.json().then((result: APIToken) => {
if (result.error) {
callFuncQue(result.error);
return;
}
console.log(result);
// set api token
apiToken = result.access_token;
// set expire time
expireSeconds = new Date().getTime() / 1000 + result.expires_in;
setTokenCookie(apiToken, expireSeconds);
// call all handlers and release flag
callFuncQue('');
})
);
}
export function apiTokenValid(): boolean {
// check if a cookie with token is available // check if a cookie with token is available
const token = getTokenCookie(); const token = getTokenCookie();
if (token !== null) { if (token !== null) {
// check if token is at least valid for the next minute // check if token is at least valid for the next minute
if (token.expire > (new Date().getTime() / 1000) + 60) { if (token.expire > new Date().getTime() / 1000 + 60) {
apiToken = token.token; apiToken = token.token;
expireSeconds = token.expire; expireSeconds = token.expire;
callback();
console.log("token still valid...") return true;
callFuncQue();
return;
} }
} }
return false;
const formData = new FormData();
formData.append("grant_type", "client_credentials");
formData.append("client_id", "openmediacenter");
formData.append("client_secret", 'openmediacenter');
formData.append("scope", 'all');
fetch(getBackendDomain() + '/token', {method: 'POST', body: formData})
.then((response) => response.json()
.then((result: APIToken) => {
console.log(result)
// set api token
apiToken = result.access_token;
// set expire time
expireSeconds = (new Date().getTime() / 1000) + result.expires_in;
setTokenCookie(apiToken, expireSeconds);
// call all handlers and release flag
callFuncQue();
}));
} }
/** /**
* call all qued callbacks * call all qued callbacks
*/ */
function callFuncQue(): void { function callFuncQue(error: string): void {
// call all pending handlers // call all pending handlers
callQue.map(func => { callQue.map((func) => {
return func(); return func(error);
}) });
// reset pending que // reset pending que
callQue = [] callQue = [];
// release flag to be able to start new refresh // release flag to be able to start new refresh
refreshInProcess = false; refreshInProcess = false;
} }
@ -133,24 +151,24 @@ function callFuncQue(): void {
function setTokenCookie(token: string, validSec: number): void { function setTokenCookie(token: string, validSec: number): void {
let d = new Date(); let d = new Date();
d.setTime(validSec * 1000); d.setTime(validSec * 1000);
console.log("token set" + d.toUTCString()) console.log('token set' + d.toUTCString());
let expires = "expires=" + d.toUTCString(); let expires = 'expires=' + d.toUTCString();
document.cookie = "token=" + token + ";" + expires + ";path=/"; document.cookie = 'token=' + token + ';' + expires + ';path=/';
document.cookie = "token_expire=" + validSec + ";" + expires + ";path=/"; document.cookie = 'token_expire=' + validSec + ';' + expires + ';path=/';
} }
/** /**
* get all required cookies for the token * get all required cookies for the token
*/ */
function getTokenCookie(): { token: string, expire: number } | null { function getTokenCookie(): {token: string; expire: number} | null {
const token = decodeCookie('token'); const token = decodeCookie('token');
const expireInString = decodeCookie('token_expire'); const expireInString = decodeCookie('token_expire');
const expireIn = parseInt(expireInString, 10) | 0; const expireIn = parseInt(expireInString, 10);
if (expireIn !== 0 && token !== '') { if (expireIn !== 0 && token !== '') {
return {token: token, expire: expireIn}; return {token: token, expire: expireIn};
} else { } else {
return null return null;
} }
} }
@ -159,7 +177,7 @@ function getTokenCookie(): { token: string, expire: number } | null {
* @param key cookie key * @param key cookie key
*/ */
function decodeCookie(key: string): string { function decodeCookie(key: string): string {
let name = key + "="; let name = key + '=';
let decodedCookie = decodeURIComponent(document.cookie); let decodedCookie = decodeURIComponent(document.cookie);
let ca = decodedCookie.split(';'); let ca = decodedCookie.split(';');
for (let i = 0; i < ca.length; i++) { for (let i = 0; i < ca.length; i++) {
@ -171,7 +189,7 @@ function decodeCookie(key: string): string {
return c.substring(name.length, c.length); return c.substring(name.length, c.length);
} }
} }
return ""; return '';
} }
/** /**
@ -183,10 +201,10 @@ function checkAPITokenValid(callback: () => void): void {
// check if token is valid and set // check if token is valid and set
if (apiToken === '' || expireSeconds <= new Date().getTime() / 1000) { if (apiToken === '' || expireSeconds <= new Date().getTime() / 1000) {
refreshAPIToken(() => { refreshAPIToken(() => {
callback() callback();
}) });
} else { } else {
callback() callback();
} }
} }
@ -197,28 +215,70 @@ function checkAPITokenValid(callback: () => void): void {
* @param callback the callback with json reply from backend * @param callback the callback with json reply from backend
* @param errorcallback a optional callback if an error occured * @param errorcallback a optional callback if an error occured
*/ */
export function callAPI<T>(apinode: APINode, export function callAPI<T>(
apinode: APINode,
fd: ApiBaseRequest, fd: ApiBaseRequest,
callback: (_: T) => void, callback: (_: T) => void,
errorcallback: (_: string) => void = (_: string): void => { errorcallback: (_: string) => void = (_: string): void => {}
}): void { ): void {
checkAPITokenValid(() => { checkAPITokenValid(() => {
console.log(apiToken);
fetch(getAPIDomain() + apinode, { fetch(getAPIDomain() + apinode, {
method: 'POST', body: JSON.stringify(fd), headers: new Headers({ method: 'POST',
body: JSON.stringify(fd),
headers: new Headers({
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': 'Bearer ' + apiToken, Authorization: 'Bearer ' + apiToken
}), })
}).then((response) => { })
.then((response) => {
if (response.status === 200) {
// success
response.json().then((result: T) => {
callback(result);
});
} else if (response.status === 400) {
// Bad Request --> invalid token
console.log('loading Password page.');
// load password page
if (GlobalInfos.loadPasswordPage) {
GlobalInfos.loadPasswordPage(() => {
callAPI(apinode, fd, callback, errorcallback);
});
}
} else {
console.log('Error: ' + response.statusText);
}
})
.catch((reason) => errorcallback(reason));
});
}
/**
* make a public unsafe api call (without token) -- use as rare as possible only for initialization (eg. check if pwd is neccessary)
* @param apinode
* @param fd
* @param callback
* @param errorcallback
*/
export function callApiUnsafe<T>(
apinode: APINode,
fd: ApiBaseRequest,
callback: (_: T) => void,
errorcallback?: (_: string) => void
): void {
fetch(getAPIDomain() + apinode, {method: 'POST', body: JSON.stringify(fd)})
.then((response) => {
if (response.status !== 200) { if (response.status !== 200) {
console.log('Error: ' + response.statusText); console.log('Error: ' + response.statusText);
// todo place error popup here // todo place error popup here
} else { } else {
response.json().then((result: T) => { response.json().then((result: T) => {
callback(result); callback(result);
}) });
} }
}).catch(reason => errorcallback(reason));
}) })
.catch((reason) => (errorcallback ? errorcallback(reason) : {}));
} }
/** /**
@ -230,24 +290,29 @@ export function callAPI<T>(apinode: APINode,
export function callAPIPlain(apinode: APINode, fd: ApiBaseRequest, callback: (_: string) => void): void { export function callAPIPlain(apinode: APINode, fd: ApiBaseRequest, callback: (_: string) => void): void {
checkAPITokenValid(() => { checkAPITokenValid(() => {
fetch(getAPIDomain() + apinode, { fetch(getAPIDomain() + apinode, {
method: 'POST', body: JSON.stringify(fd), headers: new Headers({ method: 'POST',
body: JSON.stringify(fd),
headers: new Headers({
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': 'Bearer ' + apiToken, Authorization: 'Bearer ' + apiToken
}) })
}) }).then((response) =>
.then((response) => response.text() response.text().then((result) => {
.then((result) => {
callback(result); callback(result);
})); })
);
}); });
} }
/** /**
* API nodes definitions * API nodes definitions
*/ */
// eslint-disable-next-line no-shadow
export enum APINode { export enum APINode {
Settings = 'settings', Settings = 'settings',
Tags = 'tags', Tags = 'tags',
Actor = 'actor', Actor = 'actor',
Video = 'video' Video = 'video',
Init = 'init'
} }

View File

@ -7,7 +7,7 @@ import lighttheme from '../AppLightTheme.module.css';
*/ */
class StaticInfos { class StaticInfos {
private darktheme: boolean = true; private darktheme: boolean = true;
private videopath: string = "" private videopath: string = '';
/** /**
* check if the current theme is the dark theme * check if the current theme is the dark theme
@ -15,7 +15,7 @@ class StaticInfos {
*/ */
isDarkTheme(): boolean { isDarkTheme(): boolean {
return this.darktheme; return this.darktheme;
}; }
/** /**
* setter to enable or disable the dark or light theme * setter to enable or disable the dark or light theme
@ -23,6 +23,9 @@ class StaticInfos {
*/ */
enableDarkTheme(enable = true): void { enableDarkTheme(enable = true): void {
this.darktheme = enable; this.darktheme = enable;
this.handlers.map((func) => {
return func();
});
} }
/** /**
@ -33,6 +36,11 @@ class StaticInfos {
return this.isDarkTheme() ? darktheme : lighttheme; return this.isDarkTheme() ? darktheme : lighttheme;
} }
handlers: (() => void)[] = [];
onThemeChange(func: () => void): void {
this.handlers.push(func);
}
/** /**
* set the current videopath * set the current videopath
* @param vidpath videopath with beginning and ending slash * @param vidpath videopath with beginning and ending slash
@ -47,7 +55,11 @@ class StaticInfos {
getVideoPath(): string { getVideoPath(): string {
return this.videopath; return this.videopath;
} }
/**
* load the Password page manually
*/
loadPasswordPage: ((callback?: () => void) => void) | undefined = undefined;
} }
const GlobalInfos = new StaticInfos(); export default new StaticInfos();
export default GlobalInfos;

199
yarn.lock
View File

@ -1977,6 +1977,20 @@
dependencies: dependencies:
"@types/yargs-parser" "*" "@types/yargs-parser" "*"
"@typescript-eslint/eslint-plugin@^4.17.0":
version "4.17.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.17.0.tgz#6f856eca4e6a52ce9cf127dfd349096ad936aa2d"
integrity sha512-/fKFDcoHg8oNan39IKFOb5WmV7oWhQe1K6CDaAVfJaNWEhmfqlA24g+u1lqU5bMH7zuNasfMId4LaYWC5ijRLw==
dependencies:
"@typescript-eslint/experimental-utils" "4.17.0"
"@typescript-eslint/scope-manager" "4.17.0"
debug "^4.1.1"
functional-red-black-tree "^1.0.1"
lodash "^4.17.15"
regexpp "^3.0.0"
semver "^7.3.2"
tsutils "^3.17.1"
"@typescript-eslint/eslint-plugin@^4.5.0": "@typescript-eslint/eslint-plugin@^4.5.0":
version "4.16.1" version "4.16.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.16.1.tgz#2caf6a79dd19c3853b8d39769a27fccb24e4e651" resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.16.1.tgz#2caf6a79dd19c3853b8d39769a27fccb24e4e651"
@ -2003,6 +2017,18 @@
eslint-scope "^5.0.0" eslint-scope "^5.0.0"
eslint-utils "^2.0.0" eslint-utils "^2.0.0"
"@typescript-eslint/experimental-utils@4.17.0":
version "4.17.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.17.0.tgz#762c44aaa1a6a3c05b6d63a8648fb89b89f84c80"
integrity sha512-ZR2NIUbnIBj+LGqCFGQ9yk2EBQrpVVFOh9/Kd0Lm6gLpSAcCuLLe5lUCibKGCqyH9HPwYC0GIJce2O1i8VYmWA==
dependencies:
"@types/json-schema" "^7.0.3"
"@typescript-eslint/scope-manager" "4.17.0"
"@typescript-eslint/types" "4.17.0"
"@typescript-eslint/typescript-estree" "4.17.0"
eslint-scope "^5.0.0"
eslint-utils "^2.0.0"
"@typescript-eslint/experimental-utils@^3.10.1": "@typescript-eslint/experimental-utils@^3.10.1":
version "3.10.1" version "3.10.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-3.10.1.tgz#e179ffc81a80ebcae2ea04e0332f8b251345a686" resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-3.10.1.tgz#e179ffc81a80ebcae2ea04e0332f8b251345a686"
@ -2014,6 +2040,16 @@
eslint-scope "^5.0.0" eslint-scope "^5.0.0"
eslint-utils "^2.0.0" eslint-utils "^2.0.0"
"@typescript-eslint/parser@^4.17.0":
version "4.17.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.17.0.tgz#141b647ffc72ebebcbf9b0fe6087f65b706d3215"
integrity sha512-KYdksiZQ0N1t+6qpnl6JeK9ycCFprS9xBAiIrw4gSphqONt8wydBw4BXJi3C11ywZmyHulvMaLjWsxDjUSDwAw==
dependencies:
"@typescript-eslint/scope-manager" "4.17.0"
"@typescript-eslint/types" "4.17.0"
"@typescript-eslint/typescript-estree" "4.17.0"
debug "^4.1.1"
"@typescript-eslint/parser@^4.5.0": "@typescript-eslint/parser@^4.5.0":
version "4.16.1" version "4.16.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.16.1.tgz#3bbd3234dd3c5b882b2bcd9899bc30e1e1586d2a" resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.16.1.tgz#3bbd3234dd3c5b882b2bcd9899bc30e1e1586d2a"
@ -2032,6 +2068,14 @@
"@typescript-eslint/types" "4.16.1" "@typescript-eslint/types" "4.16.1"
"@typescript-eslint/visitor-keys" "4.16.1" "@typescript-eslint/visitor-keys" "4.16.1"
"@typescript-eslint/scope-manager@4.17.0":
version "4.17.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.17.0.tgz#f4edf94eff3b52a863180f7f89581bf963e3d37d"
integrity sha512-OJ+CeTliuW+UZ9qgULrnGpPQ1bhrZNFpfT/Bc0pzNeyZwMik7/ykJ0JHnQ7krHanFN9wcnPK89pwn84cRUmYjw==
dependencies:
"@typescript-eslint/types" "4.17.0"
"@typescript-eslint/visitor-keys" "4.17.0"
"@typescript-eslint/types@3.10.1": "@typescript-eslint/types@3.10.1":
version "3.10.1" version "3.10.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-3.10.1.tgz#1d7463fa7c32d8a23ab508a803ca2fe26e758727" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-3.10.1.tgz#1d7463fa7c32d8a23ab508a803ca2fe26e758727"
@ -2042,6 +2086,11 @@
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.16.1.tgz#5ba2d3e38b1a67420d2487519e193163054d9c15" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.16.1.tgz#5ba2d3e38b1a67420d2487519e193163054d9c15"
integrity sha512-nnKqBwMgRlhzmJQF8tnFDZWfunXmJyuXj55xc8Kbfup4PbkzdoDXZvzN8//EiKR27J6vUSU8j4t37yUuYPiLqA== integrity sha512-nnKqBwMgRlhzmJQF8tnFDZWfunXmJyuXj55xc8Kbfup4PbkzdoDXZvzN8//EiKR27J6vUSU8j4t37yUuYPiLqA==
"@typescript-eslint/types@4.17.0":
version "4.17.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.17.0.tgz#f57d8fc7f31b348db946498a43050083d25f40ad"
integrity sha512-RN5z8qYpJ+kXwnLlyzZkiJwfW2AY458Bf8WqllkondQIcN2ZxQowAToGSd9BlAUZDB5Ea8I6mqL2quGYCLT+2g==
"@typescript-eslint/typescript-estree@3.10.1": "@typescript-eslint/typescript-estree@3.10.1":
version "3.10.1" version "3.10.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-3.10.1.tgz#fd0061cc38add4fad45136d654408569f365b853" resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-3.10.1.tgz#fd0061cc38add4fad45136d654408569f365b853"
@ -2069,6 +2118,19 @@
semver "^7.3.2" semver "^7.3.2"
tsutils "^3.17.1" tsutils "^3.17.1"
"@typescript-eslint/typescript-estree@4.17.0":
version "4.17.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.17.0.tgz#b835d152804f0972b80dbda92477f9070a72ded1"
integrity sha512-lRhSFIZKUEPPWpWfwuZBH9trYIEJSI0vYsrxbvVvNyIUDoKWaklOAelsSkeh3E2VBSZiNe9BZ4E5tYBZbUczVQ==
dependencies:
"@typescript-eslint/types" "4.17.0"
"@typescript-eslint/visitor-keys" "4.17.0"
debug "^4.1.1"
globby "^11.0.1"
is-glob "^4.0.1"
semver "^7.3.2"
tsutils "^3.17.1"
"@typescript-eslint/visitor-keys@3.10.1": "@typescript-eslint/visitor-keys@3.10.1":
version "3.10.1" version "3.10.1"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-3.10.1.tgz#cd4274773e3eb63b2e870ac602274487ecd1e931" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-3.10.1.tgz#cd4274773e3eb63b2e870ac602274487ecd1e931"
@ -2084,6 +2146,14 @@
"@typescript-eslint/types" "4.16.1" "@typescript-eslint/types" "4.16.1"
eslint-visitor-keys "^2.0.0" eslint-visitor-keys "^2.0.0"
"@typescript-eslint/visitor-keys@4.17.0":
version "4.17.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.17.0.tgz#9c304cfd20287c14a31d573195a709111849b14d"
integrity sha512-WfuMN8mm5SSqXuAr9NM+fItJ0SVVphobWYkWOwQ1odsfC014Vdxk/92t4JwS1Q6fCA/ABfCKpa3AVtpUKTNKGQ==
dependencies:
"@typescript-eslint/types" "4.17.0"
eslint-visitor-keys "^2.0.0"
"@webassemblyjs/ast@1.9.0": "@webassemblyjs/ast@1.9.0":
version "1.9.0" version "1.9.0"
resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.9.0.tgz#bd850604b4042459a5a41cd7d338cbed695ed964" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.9.0.tgz#bd850604b4042459a5a41cd7d338cbed695ed964"
@ -2430,6 +2500,11 @@ argparse@^1.0.7:
dependencies: dependencies:
sprintf-js "~1.0.2" sprintf-js "~1.0.2"
argparse@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
aria-query@^4.2.2: aria-query@^4.2.2:
version "4.2.2" version "4.2.2"
resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-4.2.2.tgz#0d2ca6c9aceb56b8977e9fed6aed7e15bbd2f83b" resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-4.2.2.tgz#0d2ca6c9aceb56b8977e9fed6aed7e15bbd2f83b"
@ -4719,6 +4794,11 @@ escodegen@^1.14.1:
optionalDependencies: optionalDependencies:
source-map "~0.6.1" source-map "~0.6.1"
eslint-config-prettier@^8.1.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.1.0.tgz#4ef1eaf97afe5176e6a75ddfb57c335121abc5a6"
integrity sha512-oKMhGv3ihGbCIimCAjqkdzx2Q+jthoqnXSP+d86M9tptwugycmTFdVR4IpLgq2c4SHifbwO90z2fQ8/Aio73yw==
eslint-config-react-app@^6.0.0: eslint-config-react-app@^6.0.0:
version "6.0.0" version "6.0.0"
resolved "https://registry.yarnpkg.com/eslint-config-react-app/-/eslint-config-react-app-6.0.0.tgz#ccff9fc8e36b322902844cbd79197982be355a0e" resolved "https://registry.yarnpkg.com/eslint-config-react-app/-/eslint-config-react-app-6.0.0.tgz#ccff9fc8e36b322902844cbd79197982be355a0e"
@ -4726,6 +4806,13 @@ eslint-config-react-app@^6.0.0:
dependencies: dependencies:
confusing-browser-globals "^1.0.10" confusing-browser-globals "^1.0.10"
eslint-formatter-gitlab@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/eslint-formatter-gitlab/-/eslint-formatter-gitlab-2.2.0.tgz#e91bf9e22d04ae54561bf84788ed509c32a26e01"
integrity sha512-iLQB4Fp8CFAB2PiHVGMx58Zukzx5ZiNZa8MUKWzEkYwohkv6ZoutMi1JQWG89x1I+26EsvleTMoF2QkZco/XKA==
dependencies:
js-yaml "^4.0.0"
eslint-import-resolver-node@^0.3.4: eslint-import-resolver-node@^0.3.4:
version "0.3.4" version "0.3.4"
resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz#85ffa81942c25012d8231096ddf679c03042c717" resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz#85ffa81942c25012d8231096ddf679c03042c717"
@ -4742,6 +4829,14 @@ eslint-module-utils@^2.6.0:
debug "^2.6.9" debug "^2.6.9"
pkg-dir "^2.0.0" pkg-dir "^2.0.0"
eslint-plugin-eslint-comments@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-eslint-comments/-/eslint-plugin-eslint-comments-3.2.0.tgz#9e1cd7b4413526abb313933071d7aba05ca12ffa"
integrity sha512-0jkOl0hfojIHHmEHgmNdqv4fmh7300NdpA9FFpF7zaoLvB/QeXOGNLIo86oAveJFrfB1p05kC8hpEMHM8DwWVQ==
dependencies:
escape-string-regexp "^1.0.5"
ignore "^5.0.5"
eslint-plugin-flowtype@^5.2.0: eslint-plugin-flowtype@^5.2.0:
version "5.3.1" version "5.3.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-flowtype/-/eslint-plugin-flowtype-5.3.1.tgz#df6227e28c61d967b825c1327a27818bbb2ad325" resolved "https://registry.yarnpkg.com/eslint-plugin-flowtype/-/eslint-plugin-flowtype-5.3.1.tgz#df6227e28c61d967b825c1327a27818bbb2ad325"
@ -4776,6 +4871,13 @@ eslint-plugin-jest@^24.1.0:
dependencies: dependencies:
"@typescript-eslint/experimental-utils" "^4.0.1" "@typescript-eslint/experimental-utils" "^4.0.1"
eslint-plugin-jest@^24.3.1:
version "24.3.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-jest/-/eslint-plugin-jest-24.3.1.tgz#c8df037847b83397940bef7fbc2cc168ab466bcc"
integrity sha512-RQt59rfMSHyvedImT72iaf8JcvCcR4P7Uq499dALtjY8mrCjbwWrFi1UceG4sid2wVIeDi+0tjxXZ8CZEVO7Zw==
dependencies:
"@typescript-eslint/experimental-utils" "^4.0.1"
eslint-plugin-jsx-a11y@^6.3.1: eslint-plugin-jsx-a11y@^6.3.1:
version "6.4.1" version "6.4.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.4.1.tgz#a2d84caa49756942f42f1ffab9002436391718fd" resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.4.1.tgz#a2d84caa49756942f42f1ffab9002436391718fd"
@ -4793,12 +4895,19 @@ eslint-plugin-jsx-a11y@^6.3.1:
jsx-ast-utils "^3.1.0" jsx-ast-utils "^3.1.0"
language-tags "^1.0.5" language-tags "^1.0.5"
eslint-plugin-prettier@^3.3.1:
version "3.3.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-3.3.1.tgz#7079cfa2497078905011e6f82e8dd8453d1371b7"
integrity sha512-Rq3jkcFY8RYeQLgk2cCwuc0P7SEFwDravPhsJZOQ5N4YI4DSg50NyqJ/9gdZHzQlHf8MvafSesbNJCcP/FF6pQ==
dependencies:
prettier-linter-helpers "^1.0.0"
eslint-plugin-react-hooks@^4.2.0: eslint-plugin-react-hooks@^4.2.0:
version "4.2.0" version "4.2.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.2.0.tgz#8c229c268d468956334c943bb45fc860280f5556" resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.2.0.tgz#8c229c268d468956334c943bb45fc860280f5556"
integrity sha512-623WEiZJqxR7VdxFCKLI6d6LLpwJkGPYKODnkH3D7WpOG5KM8yWueBd8TLsNAetEJNF5iJmolaAKO3F8yzyVBQ== integrity sha512-623WEiZJqxR7VdxFCKLI6d6LLpwJkGPYKODnkH3D7WpOG5KM8yWueBd8TLsNAetEJNF5iJmolaAKO3F8yzyVBQ==
eslint-plugin-react@^7.21.5: eslint-plugin-react@^7.21.5, eslint-plugin-react@^7.22.0:
version "7.22.0" version "7.22.0"
resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.22.0.tgz#3d1c542d1d3169c45421c1215d9470e341707269" resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.22.0.tgz#3d1c542d1d3169c45421c1215d9470e341707269"
integrity sha512-p30tuX3VS+NWv9nQot9xIGAHBXR0+xJVaZriEsHoJrASGCJZDJ8JLNM0YqKqI0AKm6Uxaa1VUHoNEibxRCMQHA== integrity sha512-p30tuX3VS+NWv9nQot9xIGAHBXR0+xJVaZriEsHoJrASGCJZDJ8JLNM0YqKqI0AKm6Uxaa1VUHoNEibxRCMQHA==
@ -4909,6 +5018,49 @@ eslint@^7.11.0:
text-table "^0.2.0" text-table "^0.2.0"
v8-compile-cache "^2.0.3" v8-compile-cache "^2.0.3"
eslint@^7.22.0:
version "7.22.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-7.22.0.tgz#07ecc61052fec63661a2cab6bd507127c07adc6f"
integrity sha512-3VawOtjSJUQiiqac8MQc+w457iGLfuNGLFn8JmF051tTKbh5/x/0vlcEj8OgDCaw7Ysa2Jn8paGshV7x2abKXg==
dependencies:
"@babel/code-frame" "7.12.11"
"@eslint/eslintrc" "^0.4.0"
ajv "^6.10.0"
chalk "^4.0.0"
cross-spawn "^7.0.2"
debug "^4.0.1"
doctrine "^3.0.0"
enquirer "^2.3.5"
eslint-scope "^5.1.1"
eslint-utils "^2.1.0"
eslint-visitor-keys "^2.0.0"
espree "^7.3.1"
esquery "^1.4.0"
esutils "^2.0.2"
file-entry-cache "^6.0.1"
functional-red-black-tree "^1.0.1"
glob-parent "^5.0.0"
globals "^13.6.0"
ignore "^4.0.6"
import-fresh "^3.0.0"
imurmurhash "^0.1.4"
is-glob "^4.0.0"
js-yaml "^3.13.1"
json-stable-stringify-without-jsonify "^1.0.1"
levn "^0.4.1"
lodash "^4.17.21"
minimatch "^3.0.4"
natural-compare "^1.4.0"
optionator "^0.9.1"
progress "^2.0.0"
regexpp "^3.1.0"
semver "^7.2.1"
strip-ansi "^6.0.0"
strip-json-comments "^3.1.0"
table "^6.0.4"
text-table "^0.2.0"
v8-compile-cache "^2.0.3"
espree@^7.3.0, espree@^7.3.1: espree@^7.3.0, espree@^7.3.1:
version "7.3.1" version "7.3.1"
resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.1.tgz#f2df330b752c6f55019f8bd89b7660039c1bbbb6" resolved "https://registry.yarnpkg.com/espree/-/espree-7.3.1.tgz#f2df330b752c6f55019f8bd89b7660039c1bbbb6"
@ -5147,6 +5299,11 @@ fast-deep-equal@^3.1.1:
resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525"
integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
fast-diff@^1.1.2:
version "1.2.0"
resolved "https://registry.yarnpkg.com/fast-diff/-/fast-diff-1.2.0.tgz#73ee11982d86caaf7959828d519cfe927fac5f03"
integrity sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==
fast-glob@^3.1.1: fast-glob@^3.1.1:
version "3.2.5" version "3.2.5"
resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.5.tgz#7939af2a656de79a4f1901903ee8adcaa7cb9661" resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.5.tgz#7939af2a656de79a4f1901903ee8adcaa7cb9661"
@ -5576,6 +5733,13 @@ globals@^12.1.0:
dependencies: dependencies:
type-fest "^0.8.1" type-fest "^0.8.1"
globals@^13.6.0:
version "13.6.0"
resolved "https://registry.yarnpkg.com/globals/-/globals-13.6.0.tgz#d77138e53738567bb96a3916ff6f6b487af20ef7"
integrity sha512-YFKCX0SiPg7l5oKYCJ2zZGxcXprVXHcSnVuvzrT3oSENQonVLqM5pf9fN5dLGZGyCjhw8TN8Btwe/jKnZ0pjvQ==
dependencies:
type-fest "^0.20.2"
globby@11.0.1: globby@11.0.1:
version "11.0.1" version "11.0.1"
resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.1.tgz#9a2bf107a068f3ffeabc49ad702c79ede8cfd357" resolved "https://registry.yarnpkg.com/globby/-/globby-11.0.1.tgz#9a2bf107a068f3ffeabc49ad702c79ede8cfd357"
@ -5991,7 +6155,7 @@ ignore@^4.0.6:
resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc"
integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==
ignore@^5.1.4: ignore@^5.0.5, ignore@^5.1.4:
version "5.1.8" version "5.1.8"
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57"
integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==
@ -7013,6 +7177,13 @@ js-yaml@^3.13.1:
argparse "^1.0.7" argparse "^1.0.7"
esprima "^4.0.0" esprima "^4.0.0"
js-yaml@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.0.0.tgz#f426bc0ff4b4051926cd588c71113183409a121f"
integrity sha512-pqon0s+4ScYUvX30wxQi3PogGFAlUyH0awepWvwkj4jD4v+ova3RiYw8bmA6x2rDrEaj8i/oWKoRxpVNW+Re8Q==
dependencies:
argparse "^2.0.1"
jsbn@~0.1.0: jsbn@~0.1.0:
version "0.1.1" version "0.1.1"
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
@ -7363,7 +7534,7 @@ lodash.uniq@^4.5.0:
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=
"lodash@>=3.5 <5", lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.5: "lodash@>=3.5 <5", lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.5:
version "4.17.21" version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@ -9202,6 +9373,23 @@ prepend-http@^1.0.0:
resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc" resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-1.0.4.tgz#d4f4562b0ce3696e41ac52d0e002e57a635dc6dc"
integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw= integrity sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=
prettier-config@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/prettier-config/-/prettier-config-1.0.0.tgz#f8eed3916369b81678acaa33dc0298147d1958c8"
integrity sha1-+O7TkWNpuBZ4rKoz3AKYFH0ZWMg=
prettier-linter-helpers@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz#d23d41fe1375646de2d0104d3454a3008802cf7b"
integrity sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==
dependencies:
fast-diff "^1.1.2"
prettier@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.2.1.tgz#795a1a78dd52f073da0cd42b21f9c91381923ff5"
integrity sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q==
pretty-bytes@^5.3.0: pretty-bytes@^5.3.0:
version "5.6.0" version "5.6.0"
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb"
@ -11280,6 +11468,11 @@ type-fest@^0.11.0:
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.11.0.tgz#97abf0872310fed88a5c466b25681576145e33f1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.11.0.tgz#97abf0872310fed88a5c466b25681576145e33f1"
integrity sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ== integrity sha512-OdjXJxnCN1AvyLSzeKIgXTXxV+99ZuXl3Hpo9XpJAv9MBcHrrJOQ5kV7ypXOuQie+AmWG25hLbiKdwYTifzcfQ==
type-fest@^0.20.2:
version "0.20.2"
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4"
integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==
type-fest@^0.3.1: type-fest@^0.3.1:
version "0.3.1" version "0.3.1"
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.3.1.tgz#63d00d204e059474fe5e1b7c011112bbd1dc29e1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.3.1.tgz#63d00d204e059474fe5e1b7c011112bbd1dc29e1"