Compare commits
8 Commits
rememberSc
...
hoverdelet
Author | SHA1 | Date | |
---|---|---|---|
9f960a2af4 | |||
219124c843 | |||
4de39ab471 | |||
b5220634a2 | |||
d59980460f | |||
4e52688ff9 | |||
009f21390e | |||
62d3e02645 |
84
api/src/handlers/Tags.php
Normal file
84
api/src/handlers/Tags.php
Normal file
@ -0,0 +1,84 @@
|
||||
<?php
|
||||
require_once 'RequestBase.php';
|
||||
|
||||
/**
|
||||
* Class Tags
|
||||
* backend to handle Tag database interactions
|
||||
*/
|
||||
class Tags extends RequestBase {
|
||||
function initHandlers() {
|
||||
$this->addToDB();
|
||||
$this->getFromDB();
|
||||
$this->delete();
|
||||
}
|
||||
|
||||
private function addToDB() {
|
||||
/**
|
||||
* creates a new tag
|
||||
* query requirements:
|
||||
* * tagname -- name of the new tag
|
||||
*/
|
||||
$this->addActionHandler("createTag", function () {
|
||||
// skip tag create if already existing
|
||||
$query = "INSERT IGNORE INTO tags (tag_name) VALUES ('" . $_POST['tagname'] . "')";
|
||||
|
||||
if ($this->conn->query($query) === TRUE) {
|
||||
$this->commitMessage('{"result":"success"}');
|
||||
} else {
|
||||
$this->commitMessage('{"result":"' . $this->conn->error . '"}');
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* adds a new tag to an existing video
|
||||
*
|
||||
* query requirements:
|
||||
* * movieid -- the id of the video to add the tag to
|
||||
* * id -- the tag id which tag to add
|
||||
*/
|
||||
$this->addActionHandler("addTag", function () {
|
||||
$movieid = $_POST['movieid'];
|
||||
$tagid = $_POST['id'];
|
||||
|
||||
// skip tag add if already assigned
|
||||
$query = "INSERT IGNORE INTO video_tags(tag_id, video_id) VALUES ('$tagid','$movieid')";
|
||||
|
||||
if ($this->conn->query($query) === TRUE) {
|
||||
$this->commitMessage('{"result":"success"}');
|
||||
} else {
|
||||
$this->commitMessage('{"result":"' . $this->conn->error . '"}');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private function getFromDB() {
|
||||
/**
|
||||
* returns all available tags from database
|
||||
*/
|
||||
$this->addActionHandler("getAllTags", function () {
|
||||
$query = "SELECT tag_name,tag_id from tags";
|
||||
$result = $this->conn->query($query);
|
||||
|
||||
$rows = array();
|
||||
while ($r = mysqli_fetch_assoc($result)) {
|
||||
array_push($rows, $r);
|
||||
}
|
||||
$this->commitMessage(json_encode($rows));
|
||||
});
|
||||
}
|
||||
|
||||
private function delete() {
|
||||
/**
|
||||
* delete a Tag from a video
|
||||
*/
|
||||
$this->addActionHandler("deleteVideoTag", function () {
|
||||
$movieid = $_POST['video_id'];
|
||||
$tagid = $_POST['tag_id'];
|
||||
|
||||
// skip tag add if already assigned
|
||||
$query = "DELETE FROM video_tags WHERE tag_id=$tagid AND video_id=$movieid";
|
||||
|
||||
$this->commitMessage($this->conn->query($query) ? '{"result":"success"}' : '{"result":"' . $this->conn->error . '"}');
|
||||
});
|
||||
}
|
||||
}
|
@ -6,7 +6,6 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"openmediacenter/apiGo/api/oauth"
|
||||
)
|
||||
|
||||
const APIPREFIX = "/api"
|
||||
@ -37,13 +36,10 @@ func AddHandler(action string, apiNode int, n interface{}, h func() []byte) {
|
||||
}
|
||||
|
||||
func ServerInit(port uint16) {
|
||||
http.Handle(APIPREFIX+"/video", oauth.ValidateToken(videoHandler))
|
||||
http.Handle(APIPREFIX+"/tags", oauth.ValidateToken(tagHandler))
|
||||
http.Handle(APIPREFIX+"/settings", oauth.ValidateToken(settingsHandler))
|
||||
http.Handle(APIPREFIX+"/actor", oauth.ValidateToken(actorHandler))
|
||||
|
||||
// initialize oauth service and add corresponding auth routes
|
||||
oauth.InitOAuth()
|
||||
http.Handle(APIPREFIX+"/video", http.HandlerFunc(videoHandler))
|
||||
http.Handle(APIPREFIX+"/tags", http.HandlerFunc(tagHandler))
|
||||
http.Handle(APIPREFIX+"/settings", http.HandlerFunc(settingsHandler))
|
||||
http.Handle(APIPREFIX+"/actor", http.HandlerFunc(actorHandler))
|
||||
|
||||
fmt.Printf("OpenMediacenter server up and running on port %d\n", port)
|
||||
log.Fatal(http.ListenAndServe(fmt.Sprintf(":%d", port), nil))
|
||||
|
@ -1,67 +0,0 @@
|
||||
package oauth
|
||||
|
||||
import (
|
||||
"gopkg.in/oauth2.v3/errors"
|
||||
"gopkg.in/oauth2.v3/manage"
|
||||
"gopkg.in/oauth2.v3/models"
|
||||
"gopkg.in/oauth2.v3/server"
|
||||
"gopkg.in/oauth2.v3/store"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
var srv *server.Server
|
||||
|
||||
func InitOAuth() {
|
||||
manager := manage.NewDefaultManager()
|
||||
// token store
|
||||
manager.MustTokenStorage(store.NewMemoryTokenStore())
|
||||
|
||||
clientStore := store.NewClientStore()
|
||||
// todo we need to check here if a password is enabled in db -- when yes set it here!
|
||||
clientStore.Set("openmediacenter", &models.Client{
|
||||
ID: "openmediacenter",
|
||||
Secret: "openmediacenter",
|
||||
Domain: "http://localhost:8081",
|
||||
})
|
||||
|
||||
manager.MapClientStorage(clientStore)
|
||||
srv = server.NewServer(server.NewConfig(), manager)
|
||||
srv.SetClientInfoHandler(server.ClientFormHandler)
|
||||
manager.SetRefreshTokenCfg(manage.DefaultRefreshTokenCfg)
|
||||
|
||||
srv.SetInternalErrorHandler(func(err error) (re *errors.Response) {
|
||||
log.Println("Internal Error:", err.Error())
|
||||
return
|
||||
})
|
||||
|
||||
srv.SetResponseErrorHandler(func(re *errors.Response) {
|
||||
log.Println("Response Error:", re.Error.Error())
|
||||
})
|
||||
|
||||
http.HandleFunc("/authorize", func(w http.ResponseWriter, r *http.Request) {
|
||||
err := srv.HandleAuthorizeRequest(w, r)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
}
|
||||
})
|
||||
|
||||
http.HandleFunc("/token", func(w http.ResponseWriter, r *http.Request) {
|
||||
err := srv.HandleTokenRequest(w, r)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func ValidateToken(f http.HandlerFunc) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
_, err := srv.ValidationBearerToken(r)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
f.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
@ -2,8 +2,4 @@ module openmediacenter/apiGo
|
||||
|
||||
go 1.16
|
||||
|
||||
require (
|
||||
github.com/go-session/session v3.1.2+incompatible
|
||||
github.com/go-sql-driver/mysql v1.5.0
|
||||
gopkg.in/oauth2.v3 v3.12.0
|
||||
)
|
||||
require github.com/go-sql-driver/mysql v1.5.0
|
||||
|
109
apiGo/go.sum
109
apiGo/go.sum
@ -1,111 +1,2 @@
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
|
||||
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
||||
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
|
||||
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
|
||||
github.com/fasthttp-contrib/websocket v0.0.0-20160511215533-1f3b11f56072/go.mod h1:duJ4Jxv5lDcvg4QuQr0oowTf7dz4/CR8NtyCooz9HL8=
|
||||
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
|
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
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/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-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
|
||||
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
|
||||
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk=
|
||||
github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA=
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/k0kubun/colorstring v0.0.0-20150214042306-9440f1994b88/go.mod h1:3w7q1U84EfirKl04SVQ/s7nPm1ZPhiXd34z40TNz36k=
|
||||
github.com/klauspost/compress v1.8.2 h1:Bx0qjetmNjdFXASH02NSAREKpiaDwkO1DRZ3dV2KCcs=
|
||||
github.com/klauspost/compress v1.8.2/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
|
||||
github.com/klauspost/cpuid v1.2.1 h1:vJi+O/nMdFt0vqm8NZBI6wzALWdA2X+egi0ogNyrC/w=
|
||||
github.com/klauspost/cpuid v1.2.1/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/moul/http2curl v1.0.0 h1:dRMWoAtb+ePxMlLkrCbAqh4TlPHXvoGUSQ323/9Zahs=
|
||||
github.com/moul/http2curl v1.0.0/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.10.2/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/tidwall/btree v0.0.0-20170113224114-9876f1454cf0 h1:QnyrPZZvPmR0AtJCxxfCtI1qN+fYpKTKJ/5opWmZ34k=
|
||||
github.com/tidwall/btree v0.0.0-20170113224114-9876f1454cf0/go.mod h1:huei1BkDWJ3/sLXmO+bsCNELL+Bp2Kks9OLyQFkzvA8=
|
||||
github.com/tidwall/buntdb v1.1.0 h1:H6LzK59KiNjf1nHVPFrYj4Qnl8d8YLBsYamdL8N+Bao=
|
||||
github.com/tidwall/buntdb v1.1.0/go.mod h1:Y39xhcDW10WlyYXeLgGftXVbjtM0QP+/kpz8xl9cbzE=
|
||||
github.com/tidwall/gjson v1.3.2 h1:+7p3qQFaH3fOMXAJSrdZwGKcOO/lYdGS0HqGhPqDdTI=
|
||||
github.com/tidwall/gjson v1.3.2/go.mod h1:P256ACg0Mn+j1RXIDXoss50DeIABTYK1PULOJHhxOls=
|
||||
github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb h1:5NSYaAdrnblKByzd7XByQEJVT8+9v0W/tIY0Oo4OwrE=
|
||||
github.com/tidwall/grect v0.0.0-20161006141115-ba9a043346eb/go.mod h1:lKYYLFIr9OIgdgrtgkZ9zgRxRdvPYsExnYBsEAd8W5M=
|
||||
github.com/tidwall/match v1.0.1 h1:PnKP62LPNxHKTwvHHZZzdOAOCtsJTjo6dZLCwpKm5xc=
|
||||
github.com/tidwall/match v1.0.1/go.mod h1:LujAq0jyVjBy028G1WhWfIzbpQfMO8bBZ6Tyb0+pL9E=
|
||||
github.com/tidwall/pretty v1.0.0 h1:HsD+QiTn7sK6flMKIvNmpqz1qrpP3Ps6jOKIKMooyg4=
|
||||
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
|
||||
github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e h1:+NL1GDIUOKxVfbp2KoJQD9cTQ6dyP2co9q4yzmT9FZo=
|
||||
github.com/tidwall/rtree v0.0.0-20180113144539-6cd427091e0e/go.mod h1:/h+UnNGt0IhNNJLkGikcdcJqm66zGD/uJGMRxK/9+Ao=
|
||||
github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563 h1:Otn9S136ELckZ3KKDyCkxapfufrqDqwmGjcHfAyXRrE=
|
||||
github.com/tidwall/tinyqueue v0.0.0-20180302190814-1e39f5511563/go.mod h1:mLqSmt7Dv/CNneF2wfcChfN1rvapyQr01LGKnKex0DQ=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.6.0 h1:uWF8lgKmeaIewWVPwi4GRq2P6+R46IgYZdxWtM+GtEY=
|
||||
github.com/valyala/fasthttp v1.6.0/go.mod h1:FstJa9V+Pj9vQ7OJie2qMHdwemEDaDiSdBnvPM1Su9w=
|
||||
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c=
|
||||
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0=
|
||||
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74=
|
||||
github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y=
|
||||
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0 h1:6fRhSjgLCkTD3JnJxvaJ4Sj+TYblw757bqYgZaOq5ZY=
|
||||
github.com/yalp/jsonpath v0.0.0-20180802001716-5cc68e5049a0/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI=
|
||||
github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCOA=
|
||||
github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg=
|
||||
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M=
|
||||
github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM=
|
||||
github.com/yudai/pp v2.0.1+incompatible/go.mod h1:PuxR/8QJ7cyCkFp/aUDS+JY727OFEZkTdatxwunjIkc=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 h1:k7pJ2yAPLPgbskkFdhRCsA77k2fySZ1zf2zCjvQCiIM=
|
||||
golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/oauth2.v3 v3.12.0 h1:yOffAPoolH/i2JxwmC+pgtnY3362iPahsDpLXfDFvNg=
|
||||
gopkg.in/oauth2.v3 v3.12.0/go.mod h1:XEYgKqWX095YiPT+Aw5y3tCn+7/FMnlTFKrupgSiJ3I=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
|
@ -13,7 +13,7 @@ server {
|
||||
try_files $uri /index.html;
|
||||
}
|
||||
|
||||
location ~* ^/(api/|token) {
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.0.1:8081;
|
||||
}
|
||||
}
|
||||
|
10
package.json
10
package.json
@ -73,14 +73,14 @@
|
||||
"@testing-library/react": "^11.2.2",
|
||||
"@testing-library/user-event": "^12.6.0",
|
||||
"@types/jest": "^26.0.19",
|
||||
"@types/node": "^14.14.31",
|
||||
"@types/react": "^17.0.2",
|
||||
"@types/react-dom": "^17.0.1",
|
||||
"@types/react-router": "5.1.12",
|
||||
"@types/node": "^12.19.9",
|
||||
"@types/react": "^16.14.2",
|
||||
"@types/react-dom": "^16.9.10",
|
||||
"@types/react-router": "5.1.8",
|
||||
"@types/react-router-dom": "^5.1.6",
|
||||
"enzyme": "^3.11.0",
|
||||
"enzyme-adapter-react-16": "^1.15.5",
|
||||
"jest-junit": "^12.0.0",
|
||||
"react-scripts": "4.0.3"
|
||||
"react-scripts": "4.0.1"
|
||||
}
|
||||
}
|
||||
|
@ -12,8 +12,12 @@ describe('<ActorTile/>', function () {
|
||||
const func = jest.fn((_) => {});
|
||||
const wrapper = shallow(<ActorTile actor={{Thumbnail: '-1', Name: 'testname', id: 3}} onClick={() => func()}/>);
|
||||
|
||||
const func1 = jest.fn();
|
||||
prepareViewBinding(func1);
|
||||
|
||||
wrapper.simulate('click');
|
||||
|
||||
expect(func1).toBeCalledTimes(0);
|
||||
expect(func).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
@ -30,7 +30,11 @@ class ActorTile extends React.Component<props> {
|
||||
}
|
||||
}
|
||||
|
||||
renderActorTile(customclickhandler: (actor: ActorType) => void): JSX.Element {
|
||||
/**
|
||||
* render the Actor Tile with its pic
|
||||
* @param customclickhandler a custom click handler to be called onclick instead of Link
|
||||
*/
|
||||
private renderActorTile(customclickhandler: (actor: ActorType) => void): JSX.Element {
|
||||
return (
|
||||
<div className={style.actortile} onClick={(): void => customclickhandler(this.props.actor)}>
|
||||
<div className={style.actortile_thumbnail}>
|
||||
|
@ -1,64 +0,0 @@
|
||||
import {shallow} from 'enzyme';
|
||||
import React from 'react';
|
||||
import FilterButton from './FilterButton';
|
||||
import RandomPage from "../../pages/RandomPage/RandomPage";
|
||||
import {callAPI} from "../../utils/Api";
|
||||
|
||||
describe('<FilterButton/>', function () {
|
||||
it('renders without crashing ', function () {
|
||||
const wrapper = shallow(<FilterButton onFilterChange={() => {}}/>);
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
it('test initial render ', function () {
|
||||
const wrapper = shallow(<FilterButton onFilterChange={() => {}}/>);
|
||||
expect(wrapper.find('input')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('test clicking', function () {
|
||||
const wrapper = shallow(<FilterButton onFilterChange={() => {}}/>);
|
||||
wrapper.simulate('click');
|
||||
|
||||
expect(wrapper.find('input')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('test call of callback on textfield change', function () {
|
||||
let val = '';
|
||||
const func = jest.fn((vali => {val = vali}));
|
||||
|
||||
const wrapper = shallow(<FilterButton onFilterChange={func}/>);
|
||||
wrapper.simulate('click');
|
||||
|
||||
wrapper.find('input').simulate('change', {target: {value: 'test'}});
|
||||
|
||||
expect(func).toHaveBeenCalledTimes(1);
|
||||
expect(val).toBe('test')
|
||||
});
|
||||
|
||||
it('test closing on x button click', function () {
|
||||
const wrapper = shallow(<FilterButton onFilterChange={() => {}}/>);
|
||||
wrapper.simulate('click');
|
||||
|
||||
expect(wrapper.find('input')).toHaveLength(1);
|
||||
|
||||
wrapper.find('Button').simulate('click');
|
||||
|
||||
expect(wrapper.find('input')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('test shortkey press', function () {
|
||||
let events = [];
|
||||
document.addEventListener = jest.fn((event, cb) => {
|
||||
events[event] = cb;
|
||||
});
|
||||
|
||||
shallow(<RandomPage/>);
|
||||
|
||||
const wrapper = shallow(<FilterButton onFilterChange={() => {}}/>);
|
||||
expect(wrapper.find('input')).toHaveLength(0);
|
||||
// trigger the keypress event
|
||||
events.keyup({key: 'f'});
|
||||
|
||||
expect(wrapper.find('input')).toHaveLength(1);
|
||||
});
|
||||
});
|
@ -1,99 +0,0 @@
|
||||
import React from "react";
|
||||
import style from "../Popups/AddActorPopup/AddActorPopup.module.css";
|
||||
import {Button} from "../GPElements/Button";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faFilter, faTimes} from "@fortawesome/free-solid-svg-icons";
|
||||
import {addKeyHandler, removeKeyHandler} from "../../utils/ShortkeyHandler";
|
||||
|
||||
interface props {
|
||||
onFilterChange: (filter: string) => void
|
||||
}
|
||||
|
||||
interface state {
|
||||
filtervisible: boolean;
|
||||
filter: string;
|
||||
}
|
||||
|
||||
class FilterButton extends React.Component<props, state> {
|
||||
// filterfield anchor, needed to focus after filter btn click
|
||||
private filterfield: HTMLInputElement | null | undefined;
|
||||
|
||||
|
||||
constructor(props: props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
filtervisible: false,
|
||||
filter: ''
|
||||
}
|
||||
|
||||
this.keypress = this.keypress.bind(this);
|
||||
this.enableFilterField = this.enableFilterField.bind(this);
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
removeKeyHandler(this.keypress);
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
addKeyHandler(this.keypress);
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
if (this.state.filtervisible) {
|
||||
return (
|
||||
<>
|
||||
<input className={'form-control mr-sm-2 ' + style.searchinput}
|
||||
type='text' placeholder='Filter' value={this.state.filter}
|
||||
onChange={(e): void => {
|
||||
this.props.onFilterChange(e.target.value);
|
||||
this.setState({filter: e.target.value});
|
||||
}}
|
||||
ref={(input): void => {
|
||||
this.filterfield = input;
|
||||
}}/>
|
||||
<Button title={<FontAwesomeIcon style={{
|
||||
verticalAlign: 'middle',
|
||||
lineHeight: '130px'
|
||||
}} icon={faTimes} size='1x'/>} color={{backgroundColor: 'red'}} onClick={(): void => {
|
||||
this.setState({filter: '', filtervisible: false});
|
||||
}}/>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return (<Button
|
||||
title={<span>Filter <FontAwesomeIcon
|
||||
style={{
|
||||
verticalAlign: 'middle',
|
||||
lineHeight: '130px'
|
||||
}}
|
||||
icon={faFilter}
|
||||
size='1x'/></span>}
|
||||
color={{backgroundColor: 'cornflowerblue', color: 'white'}}
|
||||
onClick={this.enableFilterField}/>)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* enable filterfield and focus into searchbar
|
||||
*/
|
||||
private enableFilterField(): void {
|
||||
this.setState({filtervisible: true}, () => {
|
||||
// focus filterfield after state update
|
||||
this.filterfield?.focus();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* key event handling
|
||||
* @param event keyevent
|
||||
*/
|
||||
private keypress(event: KeyboardEvent): void {
|
||||
// hide if escape is pressed
|
||||
if (event.key === 'f') {
|
||||
this.enableFilterField();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default FilterButton;
|
@ -6,7 +6,10 @@ import {NewActorPopupContent} from '../NewActorPopup/NewActorPopup';
|
||||
import {APINode, callAPI} from '../../../utils/Api';
|
||||
import {ActorType} from '../../../types/VideoTypes';
|
||||
import {GeneralSuccess} from '../../../types/GeneralTypes';
|
||||
import FilterButton from "../../FilterButton/FilterButton";
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||
import {faFilter, faTimes} from '@fortawesome/free-solid-svg-icons';
|
||||
import {Button} from '../../GPElements/Button';
|
||||
import {addKeyHandler, removeKeyHandler} from '../../../utils/ShortkeyHandler';
|
||||
|
||||
interface props {
|
||||
onHide: () => void;
|
||||
@ -17,6 +20,7 @@ interface state {
|
||||
contentDefault: boolean;
|
||||
actors: ActorType[];
|
||||
filter: string;
|
||||
filtervisible: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -32,14 +36,23 @@ class AddActorPopup extends React.Component<props, state> {
|
||||
this.state = {
|
||||
contentDefault: true,
|
||||
actors: [],
|
||||
filter: ''
|
||||
filter: '',
|
||||
filtervisible: false
|
||||
};
|
||||
|
||||
this.tileClickHandler = this.tileClickHandler.bind(this);
|
||||
this.filterSearch = this.filterSearch.bind(this);
|
||||
this.parentSubmit = this.parentSubmit.bind(this);
|
||||
this.keypress = this.keypress.bind(this);
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
removeKeyHandler(this.keypress);
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
addKeyHandler(this.keypress);
|
||||
|
||||
// fetch the available actors
|
||||
this.loadActors();
|
||||
}
|
||||
@ -81,9 +94,30 @@ class AddActorPopup extends React.Component<props, state> {
|
||||
return (
|
||||
<>
|
||||
<div className={style.searchbar}>
|
||||
<FilterButton onFilterChange={(filter): void => {
|
||||
this.setState({filter: filter})
|
||||
}}/>
|
||||
{
|
||||
this.state.filtervisible ?
|
||||
<>
|
||||
<input className={'form-control mr-sm-2 ' + style.searchinput}
|
||||
type='text' placeholder='Filter' value={this.state.filter}
|
||||
onChange={(e): void => {
|
||||
this.setState({filter: e.target.value});
|
||||
}}
|
||||
ref={(input): void => {this.filterfield = input;}}/>
|
||||
<Button title={<FontAwesomeIcon style={{
|
||||
verticalAlign: 'middle',
|
||||
lineHeight: '130px'
|
||||
}} icon={faTimes} size='1x'/>} color={{backgroundColor: 'red'}} onClick={(): void => {
|
||||
this.setState({filter: '', filtervisible: false});
|
||||
}}/>
|
||||
</> :
|
||||
<Button
|
||||
title={<span>Filter <FontAwesomeIcon style={{
|
||||
verticalAlign: 'middle',
|
||||
lineHeight: '130px'
|
||||
}} icon={faFilter} size='1x'/></span>}
|
||||
color={{backgroundColor: 'cornflowerblue', color: 'white'}}
|
||||
onClick={(): void => this.enableFilterField()}/>
|
||||
}
|
||||
</div>
|
||||
{this.state.actors.filter(this.filterSearch).map((el) => (<ActorTile actor={el} onClick={this.tileClickHandler}/>))}
|
||||
</>
|
||||
@ -121,6 +155,16 @@ class AddActorPopup extends React.Component<props, state> {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* enable filterfield and focus into searchbar
|
||||
*/
|
||||
private enableFilterField(): void {
|
||||
this.setState({filtervisible: true}, () => {
|
||||
// focus filterfield after state update
|
||||
this.filterfield?.focus();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* filter the actor array for search matches
|
||||
* @param actor
|
||||
@ -141,6 +185,17 @@ class AddActorPopup extends React.Component<props, state> {
|
||||
this.tileClickHandler(filteredList[0]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* key event handling
|
||||
* @param event keyevent
|
||||
*/
|
||||
private keypress(event: KeyboardEvent): void {
|
||||
// hide if escape is pressed
|
||||
if (event.key === 'f') {
|
||||
this.enableFilterField();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default AddActorPopup;
|
||||
|
@ -1,3 +1,26 @@
|
||||
.actionbar{
|
||||
margin-bottom: 15px;
|
||||
.popup {
|
||||
border: 3px #3574fe solid;
|
||||
border-radius: 18px;
|
||||
height: 80%;
|
||||
left: 20%;
|
||||
opacity: 0.95;
|
||||
position: absolute;
|
||||
top: 10%;
|
||||
width: 60%;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.header {
|
||||
cursor: move;
|
||||
font-size: x-large;
|
||||
margin-left: 15px;
|
||||
margin-top: 10px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.content {
|
||||
margin-left: 20px;
|
||||
margin-right: 20px;
|
||||
margin-top: 10px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
@ -25,37 +25,11 @@ describe('<AddTagPopup/>', function () {
|
||||
const wrapper = shallow(<AddTagPopup submit={jest.fn()} onHide={jest.fn()}/>);
|
||||
|
||||
wrapper.setState({
|
||||
items: [{TagId: 1, TagName: 'test'}]
|
||||
items: [{tag_id: 1, tag_name: 'test'}]
|
||||
}, () => {
|
||||
wrapper.find('Tag').first().dive().simulate('click');
|
||||
expect(wrapper.instance().props.submit).toHaveBeenCalledTimes(1);
|
||||
expect(wrapper.instance().props.onHide).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('test parent submit if one item left', function () {
|
||||
const onhide = jest.fn();
|
||||
const submit = jest.fn();
|
||||
|
||||
const wrapper = shallow(<AddTagPopup submit={submit} onHide={onhide}/>);
|
||||
|
||||
wrapper.setState({
|
||||
items: [{TagId: 1, TagName: 'test'}]
|
||||
}, () => {
|
||||
wrapper.instance().parentSubmit();
|
||||
|
||||
expect(onhide).toHaveBeenCalledTimes(1);
|
||||
expect(submit).toHaveBeenCalledTimes(1);
|
||||
|
||||
wrapper.setState({
|
||||
items: [{TagId: 1, TagName: 'test'}, {TagId: 3, TagName: 'test3'}]
|
||||
}, () => {
|
||||
wrapper.instance().parentSubmit();
|
||||
|
||||
// expect no submit if there are more than 1 item left...
|
||||
expect(onhide).toHaveBeenCalledTimes(1);
|
||||
expect(submit).toHaveBeenCalledTimes(1);
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -3,17 +3,15 @@ import Tag from '../../Tag/Tag';
|
||||
import PopupBase from '../PopupBase';
|
||||
import {APINode, callAPI} from '../../../utils/Api';
|
||||
import {TagType} from '../../../types/VideoTypes';
|
||||
import FilterButton from "../../FilterButton/FilterButton";
|
||||
import styles from './AddTagPopup.module.css'
|
||||
|
||||
interface props {
|
||||
onHide: () => void;
|
||||
submit: (tagId: number, tagName: string) => void;
|
||||
movie_id: number;
|
||||
}
|
||||
|
||||
interface state {
|
||||
items: TagType[];
|
||||
filter: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -23,11 +21,7 @@ class AddTagPopup extends React.Component<props, state> {
|
||||
constructor(props: props) {
|
||||
super(props);
|
||||
|
||||
this.state = {items: [], filter: ''};
|
||||
|
||||
this.tagFilter = this.tagFilter.bind(this);
|
||||
this.parentSubmit = this.parentSubmit.bind(this);
|
||||
this.onItemClick = this.onItemClick.bind(this);
|
||||
this.state = {items: []};
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
@ -40,37 +34,18 @@ class AddTagPopup extends React.Component<props, state> {
|
||||
|
||||
render(): JSX.Element {
|
||||
return (
|
||||
<PopupBase title='Add a Tag to this Video:' onHide={this.props.onHide} ParentSubmit={this.parentSubmit}>
|
||||
<div className={styles.actionbar}>
|
||||
<FilterButton onFilterChange={(filter): void => this.setState({filter: filter})}/>
|
||||
</div>
|
||||
<PopupBase title='Add a Tag to this Video:' onHide={this.props.onHide}>
|
||||
{this.state.items ?
|
||||
this.state.items.filter(this.tagFilter).map((i) => (
|
||||
this.state.items.map((i) => (
|
||||
<Tag tagInfo={i}
|
||||
onclick={(): void => this.onItemClick(i)}/>
|
||||
onclick={(): void => {
|
||||
this.props.submit(i.TagId, i.TagName);
|
||||
this.props.onHide();
|
||||
}}/>
|
||||
)) : null}
|
||||
</PopupBase>
|
||||
);
|
||||
}
|
||||
|
||||
private onItemClick(tag: TagType): void {
|
||||
this.props.submit(tag.TagId, tag.TagName);
|
||||
this.props.onHide();
|
||||
}
|
||||
|
||||
private tagFilter(tag: TagType): boolean {
|
||||
return tag.TagName.toLowerCase().includes(this.state.filter.toLowerCase());
|
||||
}
|
||||
|
||||
private parentSubmit(): void {
|
||||
// allow submit only if one item is left in selection
|
||||
const filteredList = this.state.items.filter(this.tagFilter);
|
||||
|
||||
if (filteredList.length === 1) {
|
||||
// simulate click if parent submit
|
||||
this.onItemClick(filteredList[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default AddTagPopup;
|
||||
|
@ -72,7 +72,7 @@ class PopupBase extends React.Component<props> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Alert if clicked on outside of element
|
||||
* handle click on outside of element
|
||||
*/
|
||||
handleClickOutside(event: MouseEvent): void {
|
||||
if (this.wrapperRef && this.wrapperRef.current && !this.wrapperRef.current.contains(event.target as Node)) {
|
||||
|
@ -6,6 +6,29 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.videopreview:hover .quickactions {
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
border-radius: 50%;
|
||||
|
||||
color: lightgrey;
|
||||
display: block;
|
||||
|
||||
height: 35px;
|
||||
opacity: 0.7;
|
||||
padding-top: 5px;
|
||||
|
||||
position: absolute;
|
||||
|
||||
right: 5px;
|
||||
text-align: center;
|
||||
top: 5px;
|
||||
width: 35px;
|
||||
}
|
||||
|
||||
.quickactions {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.previewpic {
|
||||
height: 80%;
|
||||
min-height: 150px;
|
||||
@ -38,6 +61,7 @@
|
||||
margin-left: 25px;
|
||||
margin-top: 25px;
|
||||
opacity: 0.85;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.videopreview:hover {
|
||||
|
@ -3,16 +3,19 @@ import style from './Preview.module.css';
|
||||
import {Spinner} from 'react-bootstrap';
|
||||
import {Link} from 'react-router-dom';
|
||||
import GlobalInfos from '../../utils/GlobalInfos';
|
||||
import {faEllipsisV} from '@fortawesome/free-solid-svg-icons';
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||
import QuickActionPop from '../QuickActionPop/QuickActionPop';
|
||||
import {APINode, callAPIPlain} from '../../utils/Api';
|
||||
|
||||
interface PreviewProps {
|
||||
name: string;
|
||||
movie_id: number;
|
||||
onClick?: () => void;
|
||||
}
|
||||
|
||||
interface PreviewState {
|
||||
previewpicture: string | null;
|
||||
optionsvisible: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -24,7 +27,8 @@ class Preview extends React.Component<PreviewProps, PreviewState> {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
previewpicture: null
|
||||
previewpicture: null,
|
||||
optionsvisible: false
|
||||
};
|
||||
}
|
||||
|
||||
@ -39,9 +43,16 @@ class Preview extends React.Component<PreviewProps, PreviewState> {
|
||||
render(): JSX.Element {
|
||||
const themeStyle = GlobalInfos.getThemeStyle();
|
||||
return (
|
||||
<Link to={'/player/' + this.props.movie_id} onClick={this.props.onClick}>
|
||||
<div className={style.videopreview + ' ' + themeStyle.secbackground + ' ' + themeStyle.preview}>
|
||||
<div className={style.previewtitle + ' ' + themeStyle.lighttextcolor}>{this.props.name}</div>
|
||||
<div className={style.videopreview + ' ' + themeStyle.secbackground + ' ' + themeStyle.preview}>
|
||||
<div className={style.quickactions} onClick={(): void => this.setState({optionsvisible: true})}>
|
||||
<FontAwesomeIcon style={{
|
||||
verticalAlign: 'middle',
|
||||
fontSize: '25px'
|
||||
}} icon={faEllipsisV} size='1x'/>
|
||||
</div>
|
||||
{this.popupvisible()}
|
||||
<div className={style.previewtitle + ' ' + themeStyle.lighttextcolor}>{this.props.name}</div>
|
||||
<Link to={'/player/' + this.props.movie_id}>
|
||||
<div className={style.previewpic}>
|
||||
{this.state.previewpicture !== null ?
|
||||
<img className={style.previewimage}
|
||||
@ -50,14 +61,21 @@ class Preview extends React.Component<PreviewProps, PreviewState> {
|
||||
<span className={style.loadAnimation}><Spinner animation='border'/></span>}
|
||||
|
||||
</div>
|
||||
<div className={style.previewbottom}>
|
||||
</Link>
|
||||
<div className={style.previewbottom}>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
popupvisible(): JSX.Element {
|
||||
if (this.state.optionsvisible)
|
||||
return (<QuickActionPop position={{x: 50, y: 50}} onHide={(): void => this.setState({optionsvisible: false})}>heeyyho</QuickActionPop>);
|
||||
else
|
||||
return <></>;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
59
src/elements/QuickActionPop/QuickActionPop.tsx
Normal file
59
src/elements/QuickActionPop/QuickActionPop.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import React, {RefObject} from 'react';
|
||||
import style from './QuickActionPopup.module.css';
|
||||
|
||||
interface props {
|
||||
position: {
|
||||
x: number,
|
||||
y: number
|
||||
},
|
||||
onHide: () => void
|
||||
}
|
||||
|
||||
class QuickActionPop extends React.Component<props> {
|
||||
private readonly wrapperRef: RefObject<HTMLDivElement>;
|
||||
|
||||
constructor(props: props) {
|
||||
super(props);
|
||||
|
||||
this.wrapperRef = React.createRef();
|
||||
|
||||
this.handleClickOutside = this.handleClickOutside.bind(this);
|
||||
}
|
||||
|
||||
|
||||
componentDidMount(): void {
|
||||
document.addEventListener('mousedown', this.handleClickOutside);
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
document.removeEventListener('mousedown', this.handleClickOutside);
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
return (
|
||||
<div ref={this.wrapperRef} className={style.quickaction} style={{top: this.props.position.y, left: this.props.position.x}}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* trigger hide if we click outside the div
|
||||
*/
|
||||
handleClickOutside(event: MouseEvent): void {
|
||||
if (this.wrapperRef && this.wrapperRef.current && !this.wrapperRef.current.contains(event.target as Node)) {
|
||||
this.props.onHide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface Itemprops {
|
||||
title: string;
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
export const ContextItem = (props: Itemprops): JSX.Element => (
|
||||
<div onClick={props.onClick} className={style.ContextItem}>{props.title}</div>
|
||||
);
|
||||
|
||||
export default QuickActionPop;
|
17
src/elements/QuickActionPop/QuickActionPopup.module.css
Normal file
17
src/elements/QuickActionPop/QuickActionPopup.module.css
Normal file
@ -0,0 +1,17 @@
|
||||
.quickaction {
|
||||
background-color: white;
|
||||
height: 120px;
|
||||
position: absolute;
|
||||
width: 90px;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.ContextItem {
|
||||
height: 40px;
|
||||
padding-top: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ContextItem:hover {
|
||||
background-color: lightgray;
|
||||
}
|
@ -1,19 +1,37 @@
|
||||
.tagbtn {
|
||||
background-color: #3574fe;
|
||||
.btnnostyle {
|
||||
background: none;
|
||||
color: inherit;
|
||||
border: none;
|
||||
padding: 0;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
outline: inherit;
|
||||
}
|
||||
|
||||
.tagbtnContainer {
|
||||
background-color: #3574fe;
|
||||
border-radius: 10px;
|
||||
color: white;
|
||||
margin-left: 10px;
|
||||
margin-top: 15px;
|
||||
width: 50px;
|
||||
/*font-weight: bold;*/
|
||||
padding: 5px 15px 5px 15px;
|
||||
}
|
||||
|
||||
.tagbtn:focus {
|
||||
.tagbtnContainer:focus {
|
||||
background-color: #004eff;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.tagbtn:hover {
|
||||
.tagbtnContainer:hover {
|
||||
background-color: #004eff;
|
||||
}
|
||||
|
||||
.deletebtn{
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tagbtnContainer:hover .deletebtn {
|
||||
display: inline;
|
||||
}
|
||||
|
@ -1,18 +1,33 @@
|
||||
import React from 'react';
|
||||
import React, {SyntheticEvent} from 'react';
|
||||
|
||||
import styles from './Tag.module.css';
|
||||
import {Link} from 'react-router-dom';
|
||||
import {TagType} from '../../types/VideoTypes';
|
||||
|
||||
interface props {
|
||||
onclick?: (_: string) => void
|
||||
tagInfo: TagType
|
||||
onclick?: (_: string) => void;
|
||||
tagInfo: TagType;
|
||||
onContextMenu?: (pos: {x: number, y: number}) => void
|
||||
}
|
||||
|
||||
interface state {
|
||||
contextVisible: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* A Component representing a single Category tag
|
||||
*/
|
||||
class Tag extends React.Component<props> {
|
||||
class Tag extends React.Component<props, state> {
|
||||
constructor(props: Readonly<props> | props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
contextVisible: false
|
||||
};
|
||||
|
||||
this.contextmenu = this.contextmenu.bind(this);
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
if (this.props.onclick) {
|
||||
return this.renderButton();
|
||||
@ -27,8 +42,13 @@ class Tag extends React.Component<props> {
|
||||
|
||||
renderButton(): JSX.Element {
|
||||
return (
|
||||
<button className={styles.tagbtn} onClick={(): void => this.TagClick()}
|
||||
data-testid='Test-Tag'>{this.props.tagInfo.TagName}</button>
|
||||
<span className={styles.tagbtnContainer}>
|
||||
<button className={styles.btnnostyle}
|
||||
onClick={(): void => this.TagClick()}
|
||||
onContextMenu={this.contextmenu}
|
||||
data-testid='Test-Tag'>{this.props.tagInfo.TagName}</button>
|
||||
<span className={styles.deletebtn}>X</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@ -42,6 +62,20 @@ class Tag extends React.Component<props> {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* handle a custom contextmenu for this item
|
||||
* @param e
|
||||
* @private
|
||||
*/
|
||||
private contextmenu(e: SyntheticEvent): void {
|
||||
if (!this.props.onContextMenu) return;
|
||||
|
||||
const event = e as unknown as PointerEvent;
|
||||
event.preventDefault();
|
||||
this.props.onContextMenu({x: event.clientX, y: event.clientY});
|
||||
// this.setState({contextVisible: true});
|
||||
}
|
||||
}
|
||||
|
||||
export default Tag;
|
||||
|
@ -4,9 +4,7 @@ import style from './VideoContainer.module.css';
|
||||
import {VideoTypes} from '../../types/ApiTypes';
|
||||
|
||||
interface props {
|
||||
data: VideoTypes.VideoUnloadedType[];
|
||||
onScrollPositionChange?: (scrollPos: number, loadedTiles: number) => void;
|
||||
initialScrollPosition?: {scrollPos: number, loadedTiles: number};
|
||||
data: VideoTypes.VideoUnloadedType[]
|
||||
}
|
||||
|
||||
interface state {
|
||||
@ -34,16 +32,7 @@ class VideoContainer extends React.Component<props, state> {
|
||||
componentDidMount(): void {
|
||||
document.addEventListener('scroll', this.trackScrolling);
|
||||
|
||||
console.log(this.props.initialScrollPosition)
|
||||
if(this.props.initialScrollPosition !== undefined){
|
||||
this.loadPreviewBlock(this.props.initialScrollPosition.loadedTiles, () => {
|
||||
if(this.props.initialScrollPosition !== undefined)
|
||||
window.scrollTo(0, this.props.initialScrollPosition.scrollPos);
|
||||
});
|
||||
}else{
|
||||
this.loadPreviewBlock(16);
|
||||
}
|
||||
|
||||
this.loadPreviewBlock(16);
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
@ -53,11 +42,7 @@ class VideoContainer extends React.Component<props, state> {
|
||||
<Preview
|
||||
key={elem.MovieId}
|
||||
name={elem.MovieName}
|
||||
movie_id={elem.MovieId}
|
||||
onClick={(): void => {
|
||||
if (this.props.onScrollPositionChange !== undefined)
|
||||
this.props.onScrollPositionChange(window.pageYOffset - document.documentElement.clientHeight, this.loadindex)
|
||||
}}/>
|
||||
movie_id={elem.MovieId}/>
|
||||
))}
|
||||
{/*todo css for no items to show*/}
|
||||
{this.state.loadeditems.length === 0 ?
|
||||
@ -77,7 +62,7 @@ class VideoContainer extends React.Component<props, state> {
|
||||
* load previews to the container
|
||||
* @param nr number of previews to load
|
||||
*/
|
||||
loadPreviewBlock(nr: number, callback? : () => void): void {
|
||||
loadPreviewBlock(nr: number): void {
|
||||
console.log('loadpreviewblock called ...');
|
||||
let ret = [];
|
||||
for (let i = 0; i < nr; i++) {
|
||||
@ -92,7 +77,7 @@ class VideoContainer extends React.Component<props, state> {
|
||||
...this.state.loadeditems,
|
||||
...ret
|
||||
]
|
||||
}, callback);
|
||||
});
|
||||
|
||||
|
||||
this.loadindex += nr;
|
||||
@ -103,10 +88,8 @@ class VideoContainer extends React.Component<props, state> {
|
||||
*/
|
||||
trackScrolling = (): void => {
|
||||
// comparison if current scroll position is on bottom --> 200 is bottom offset to trigger load
|
||||
if (document.documentElement.clientHeight + document.documentElement.scrollTop + 200 >= document.documentElement.offsetHeight) {
|
||||
if (window.innerHeight + document.documentElement.scrollTop + 200 >= document.documentElement.offsetHeight) {
|
||||
this.loadPreviewBlock(8);
|
||||
if (this.props.onScrollPositionChange !== undefined)
|
||||
this.props.onScrollPositionChange(document.documentElement.clientHeight + window.pageYOffset, this.loadindex)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -12,8 +12,7 @@ import SearchHandling from './SearchHandling';
|
||||
import {VideoTypes} from '../../types/ApiTypes';
|
||||
import {DefaultTags} from "../../types/GeneralTypes";
|
||||
|
||||
interface props extends RouteComponentProps {
|
||||
}
|
||||
interface props extends RouteComponentProps {}
|
||||
|
||||
interface state {
|
||||
sideinfo: VideoTypes.startDataType
|
||||
@ -51,8 +50,6 @@ export class HomePage extends React.Component<props, state> {
|
||||
// initial get of all videos
|
||||
this.fetchVideoData(DefaultTags.all.TagId);
|
||||
this.fetchStartData();
|
||||
|
||||
console.log(this.props)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -103,8 +100,7 @@ export class HomePage extends React.Component<props, state> {
|
||||
onChange={(e): void => {
|
||||
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>
|
||||
</PageTitle>
|
||||
<SideBar>
|
||||
@ -137,19 +133,7 @@ export class HomePage extends React.Component<props, state> {
|
||||
</SideBar>
|
||||
{this.state.data.length !== 0 ?
|
||||
<VideoContainer
|
||||
data={this.state.data}
|
||||
onScrollPositionChange={(pos, loadedTiles): void => {
|
||||
this.props.location.state = {pos: pos, loaded: loadedTiles};
|
||||
|
||||
console.log("history state update called...")
|
||||
const {state} = this.props.location;
|
||||
const stateCopy: { pos: number, loaded: number } = {...state as { pos: number, loaded: number }};
|
||||
stateCopy.loaded = loadedTiles;
|
||||
stateCopy.pos = pos;
|
||||
this.props.history.replace({state: stateCopy});
|
||||
console.log(this.props)
|
||||
}}
|
||||
initialScrollPosition={this.props.location.state !== null ? {scrollPos: (this.props.location.state as { pos: number, loaded: number }).pos, loadedTiles: (this.props.location.state as { pos: number, loaded: number }).loaded} : undefined}/> :
|
||||
data={this.state.data}/> :
|
||||
<div>No Data found!</div>}
|
||||
<div className={style.rightinfo}>
|
||||
|
||||
|
@ -1,26 +1,25 @@
|
||||
import React from 'react';
|
||||
|
||||
import style from './Player.module.css';
|
||||
import plyrstyle from 'plyr-react/dist/plyr.css';
|
||||
|
||||
import {Plyr} from 'plyr-react';
|
||||
import PlyrJS from 'plyr';
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||
import {faPlusCircle} from '@fortawesome/free-solid-svg-icons';
|
||||
import SideBar, {SideBarItem, SideBarTitle} from '../../elements/SideBar/SideBar';
|
||||
import Tag from '../../elements/Tag/Tag';
|
||||
import AddTagPopup from '../../elements/Popups/AddTagPopup/AddTagPopup';
|
||||
import PageTitle, {Line} from '../../elements/PageTitle/PageTitle';
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||
import {faPlusCircle} from '@fortawesome/free-solid-svg-icons';
|
||||
import AddActorPopup from '../../elements/Popups/AddActorPopup/AddActorPopup';
|
||||
import ActorTile from '../../elements/ActorTile/ActorTile';
|
||||
import {withRouter} from 'react-router-dom';
|
||||
import {APINode, callAPI, getBackendDomain} from '../../utils/Api';
|
||||
import {callAPI, getBackendDomain, APINode} from '../../utils/Api';
|
||||
import {RouteComponentProps} from 'react-router';
|
||||
import {GeneralSuccess} from '../../types/GeneralTypes';
|
||||
import {ActorType, TagType} from '../../types/VideoTypes';
|
||||
import PlyrJS from 'plyr';
|
||||
import {Button} from '../../elements/GPElements/Button';
|
||||
import {VideoTypes} from '../../types/ApiTypes';
|
||||
import GlobalInfos from "../../utils/GlobalInfos";
|
||||
import QuickActionPop, {ContextItem} from '../../elements/QuickActionPop/QuickActionPop';
|
||||
|
||||
interface myprops extends RouteComponentProps<{ id: string }> {}
|
||||
|
||||
@ -35,7 +34,8 @@ interface mystate {
|
||||
suggesttag: TagType[],
|
||||
popupvisible: boolean,
|
||||
actorpopupvisible: boolean,
|
||||
actors: ActorType[]
|
||||
actors: ActorType[],
|
||||
tagContextMenu: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
@ -43,7 +43,7 @@ interface mystate {
|
||||
* and actions such as tag adding and liking
|
||||
*/
|
||||
export class Player extends React.Component<myprops, mystate> {
|
||||
options: PlyrJS.Options = {
|
||||
private options: PlyrJS.Options = {
|
||||
controls: [
|
||||
'play-large', // The large play button in the center
|
||||
'play', // Play/pause playback
|
||||
@ -60,10 +60,13 @@ export class Player extends React.Component<myprops, mystate> {
|
||||
]
|
||||
};
|
||||
|
||||
private contextpos = {x: 0, y: 0, tagid: -1};
|
||||
|
||||
constructor(props: myprops) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
tagContextMenu: false,
|
||||
movie_id: -1,
|
||||
movie_name: '',
|
||||
likes: 0,
|
||||
@ -77,6 +80,7 @@ export class Player extends React.Component<myprops, mystate> {
|
||||
};
|
||||
|
||||
this.quickAddTag = this.quickAddTag.bind(this);
|
||||
this.deleteTag = this.deleteTag.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
@ -132,7 +136,10 @@ export class Player extends React.Component<myprops, mystate> {
|
||||
<Line/>
|
||||
<SideBarTitle>Tags:</SideBarTitle>
|
||||
{this.state.tags.map((m: TagType) => (
|
||||
<Tag key={m.TagId} tagInfo={m}/>
|
||||
<Tag key={m.TagId} tagInfo={m} onContextMenu={(pos): void => {
|
||||
this.setState({tagContextMenu: true});
|
||||
this.contextpos = {...pos, tagid: m.TagId};
|
||||
}}/>
|
||||
))}
|
||||
<Line/>
|
||||
<SideBarTitle>Tag Quickadd:</SideBarTitle>
|
||||
@ -181,10 +188,13 @@ export class Player extends React.Component<myprops, mystate> {
|
||||
handlePopOvers(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
{
|
||||
this.state.popupvisible ?
|
||||
<AddTagPopup onHide={(): void => this.setState({popupvisible: false})}
|
||||
submit={this.quickAddTag}/> : null
|
||||
{this.state.popupvisible ?
|
||||
<AddTagPopup onHide={(): void => {
|
||||
this.setState({popupvisible: false});
|
||||
}}
|
||||
submit={this.quickAddTag}
|
||||
movie_id={this.state.movie_id}/> :
|
||||
null
|
||||
}
|
||||
{
|
||||
this.state.actorpopupvisible ?
|
||||
@ -193,6 +203,7 @@ export class Player extends React.Component<myprops, mystate> {
|
||||
this.setState({actorpopupvisible: false});
|
||||
}} movie_id={this.state.movie_id}/> : null
|
||||
}
|
||||
{this.renderContextMenu()}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -323,10 +334,52 @@ export class Player extends React.Component<myprops, mystate> {
|
||||
* fetch the available video actors again
|
||||
*/
|
||||
refetchActors(): void {
|
||||
callAPI<ActorType[]>(APINode.Actor, {action: 'getActorsOfVideo', MovieId: parseInt(this.props.match.params.id)}, result => {
|
||||
callAPI<ActorType[]>(APINode.Actor, {action: 'getActorsOfVideo', videoid: this.props.match.params.id}, result => {
|
||||
this.setState({actors: result});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* render the Tag context menu
|
||||
*/
|
||||
private renderContextMenu(): JSX.Element {
|
||||
if (this.state.tagContextMenu) {
|
||||
return (
|
||||
<QuickActionPop onHide={(): void => this.setState({tagContextMenu: false})}
|
||||
position={this.contextpos}>
|
||||
<ContextItem title='Delete' onClick={(): void => this.deleteTag(this.contextpos.tagid)}/>
|
||||
</QuickActionPop>);
|
||||
} else {
|
||||
return <></>;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* delete a tag from the current video
|
||||
*/
|
||||
private deleteTag(tag_id: number): void {
|
||||
callAPI<GeneralSuccess>(APINode.Tags,
|
||||
{action: 'deleteVideoTag', video_id: this.props.match.params.id, tag_id: tag_id},
|
||||
(res) => {
|
||||
if (res.result !== 'success') {
|
||||
console.log("deletion errored!");
|
||||
|
||||
this.setState({tagContextMenu: false});
|
||||
}else{
|
||||
// check if tag has already been added
|
||||
const tagIndex = this.state.tags.map(function (e: TagType) {
|
||||
return e.TagId;
|
||||
}).indexOf(tag_id);
|
||||
|
||||
|
||||
// delete tag from array
|
||||
const newTagArray = this.state.tags;
|
||||
newTagArray.splice(tagIndex, 1);
|
||||
|
||||
this.setState({tags: newTagArray, tagContextMenu: false});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(Player);
|
||||
|
@ -19,8 +19,7 @@ global.prepareFetchApi = (response) => {
|
||||
const mockJsonPromise = Promise.resolve(response);
|
||||
const mockFetchPromise = Promise.resolve({
|
||||
json: () => mockJsonPromise,
|
||||
text: () => mockJsonPromise,
|
||||
status: 200
|
||||
text: () => mockJsonPromise
|
||||
});
|
||||
return (jest.fn().mockImplementation(() => mockFetchPromise));
|
||||
};
|
||||
@ -34,6 +33,19 @@ global.prepareFailingFetchApi = () => {
|
||||
return (jest.fn().mockImplementation(() => mockFetchPromise));
|
||||
};
|
||||
|
||||
/**
|
||||
* prepares a viewbinding instance
|
||||
* @param func a mock function to be called
|
||||
*/
|
||||
global.prepareViewBinding = (func) => {
|
||||
GlobalInfos.getViewBinding = () => {
|
||||
return {
|
||||
changeRootElement: func,
|
||||
returnToLastElement: func
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
global.callAPIMock = (resonse) => {
|
||||
const helpers = require('./utils/Api');
|
||||
helpers.callAPI = jest.fn().mockImplementation((_, __, func1) => {func1(resonse);});
|
||||
|
193
src/utils/Api.ts
193
src/utils/Api.ts
@ -43,153 +43,6 @@ interface ApiBaseRequest {
|
||||
[_: string]: string | number | boolean | object
|
||||
}
|
||||
|
||||
// store api token - empty if not set
|
||||
let apiToken = ''
|
||||
|
||||
// a callback que to be called after api token refresh
|
||||
let callQue: (() => void)[] = []
|
||||
// flag to check wheter a api refresh is currently pending
|
||||
let refreshInProcess = false;
|
||||
// store the expire seconds of token
|
||||
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
|
||||
* @param callback to be called after successful refresh
|
||||
*/
|
||||
export function refreshAPIToken(callback: () => void): void {
|
||||
callQue.push(callback);
|
||||
|
||||
// check if already is a token refresh is in process
|
||||
if (refreshInProcess) {
|
||||
// if yes return
|
||||
return;
|
||||
} else {
|
||||
// if not set flat
|
||||
refreshInProcess = true;
|
||||
}
|
||||
|
||||
// check if a cookie with token is available
|
||||
const token = getTokenCookie();
|
||||
if (token !== null) {
|
||||
// check if token is at least valid for the next minute
|
||||
if (token.expire > (new Date().getTime() / 1000) + 60) {
|
||||
apiToken = token.token;
|
||||
expireSeconds = token.expire;
|
||||
callback();
|
||||
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", '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
|
||||
*/
|
||||
function callFuncQue(): void {
|
||||
// call all pending handlers
|
||||
callQue.map(func => {
|
||||
return func();
|
||||
})
|
||||
// reset pending que
|
||||
callQue = []
|
||||
// release flag to be able to start new refresh
|
||||
refreshInProcess = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* set the cookie for the currently gotten token
|
||||
* @param token token string
|
||||
* @param validSec second time when the token will be invalid
|
||||
*/
|
||||
function setTokenCookie(token: string, validSec: number): void {
|
||||
let d = new Date();
|
||||
d.setTime(validSec * 1000);
|
||||
console.log("token set" + d.toUTCString())
|
||||
let expires = "expires=" + d.toUTCString();
|
||||
document.cookie = "token=" + token + ";" + expires + ";path=/";
|
||||
document.cookie = "token_expire=" + validSec + ";" + expires + ";path=/";
|
||||
}
|
||||
|
||||
/**
|
||||
* get all required cookies for the token
|
||||
*/
|
||||
function getTokenCookie(): { token: string, expire: number } | null {
|
||||
const token = decodeCookie('token');
|
||||
const expireInString = decodeCookie('token_expire');
|
||||
const expireIn = parseInt(expireInString, 10) | 0;
|
||||
|
||||
if (expireIn !== 0 && token !== '') {
|
||||
return {token: token, expire: expireIn};
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* decode a simple cookie with key specified
|
||||
* @param key cookie key
|
||||
*/
|
||||
function decodeCookie(key: string): string {
|
||||
let name = key + "=";
|
||||
let decodedCookie = decodeURIComponent(document.cookie);
|
||||
let ca = decodedCookie.split(';');
|
||||
for (let i = 0; i < ca.length; i++) {
|
||||
let c = ca[i];
|
||||
while (c.charAt(0) === ' ') {
|
||||
c = c.substring(1);
|
||||
}
|
||||
if (c.indexOf(name) === 0) {
|
||||
return c.substring(name.length, c.length);
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* check if api token is valid -- if not request new one
|
||||
* when finished call callback
|
||||
* @param callback function to be called afterwards
|
||||
*/
|
||||
function checkAPITokenValid(callback: () => void): void {
|
||||
// check if token is valid and set
|
||||
if (apiToken === '' || expireSeconds <= new Date().getTime() / 1000) {
|
||||
refreshAPIToken(() => {
|
||||
callback()
|
||||
})
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A backend api call
|
||||
* @param apinode which api backend handler to call
|
||||
@ -197,28 +50,12 @@ function checkAPITokenValid(callback: () => void): void {
|
||||
* @param callback the callback with json reply from backend
|
||||
* @param errorcallback a optional callback if an error occured
|
||||
*/
|
||||
export function callAPI<T>(apinode: APINode,
|
||||
fd: ApiBaseRequest,
|
||||
callback: (_: T) => void,
|
||||
errorcallback: (_: string) => void = (_: string): void => {
|
||||
}): void {
|
||||
checkAPITokenValid(() => {
|
||||
fetch(getAPIDomain() + apinode, {
|
||||
method: 'POST', body: JSON.stringify(fd), headers: new Headers({
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + apiToken,
|
||||
}),
|
||||
}).then((response) => {
|
||||
if (response.status !== 200) {
|
||||
console.log('Error: ' + response.statusText);
|
||||
// todo place error popup here
|
||||
} else {
|
||||
response.json().then((result: T) => {
|
||||
callback(result);
|
||||
})
|
||||
}
|
||||
}).catch(reason => errorcallback(reason));
|
||||
})
|
||||
export function callAPI<T>(apinode: APINode, fd: ApiBaseRequest, callback: (_: T) => void, errorcallback: (_: string) => void = (_: string): void => {}): void {
|
||||
fetch(getAPIDomain() + apinode, {method: 'POST', body: JSON.stringify(fd)})
|
||||
.then((response) => response.json()
|
||||
.then((result) => {
|
||||
callback(result);
|
||||
})).catch(reason => errorcallback(reason));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -228,18 +65,12 @@ export function callAPI<T>(apinode: APINode,
|
||||
* @param callback the callback with PLAIN text reply from backend
|
||||
*/
|
||||
export function callAPIPlain(apinode: APINode, fd: ApiBaseRequest, callback: (_: string) => void): void {
|
||||
checkAPITokenValid(() => {
|
||||
fetch(getAPIDomain() + apinode, {
|
||||
method: 'POST', body: JSON.stringify(fd), headers: new Headers({
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + apiToken,
|
||||
})
|
||||
})
|
||||
.then((response) => response.text()
|
||||
.then((result) => {
|
||||
callback(result);
|
||||
}));
|
||||
});
|
||||
fetch(getAPIDomain() + apinode, {method: 'POST', body: JSON.stringify(fd)})
|
||||
.then((response) => response.text()
|
||||
.then((result) => {
|
||||
callback(result);
|
||||
}));
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
Reference in New Issue
Block a user