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