From 81a4098c6fb8533660d650d5c2d11e5dd577810f Mon Sep 17 00:00:00 2001 From: Olivier Bado Date: Fri, 4 Dec 2020 10:53:57 +0100 Subject: [PATCH 001/145] Replace default comments in po files + Some Code formatting --- pod/locale/fr/LC_MESSAGES/django.po | 6 +- pod/locale/fr/LC_MESSAGES/djangojs.po | 6 +- pod/locale/nl/LC_MESSAGES/django.po | 6 +- pod/locale/nl/LC_MESSAGES/djangojs.po | 6 +- pod/main/static/css/pod.css | 218 +++++++++++++------------- 5 files changed, 117 insertions(+), 125 deletions(-) diff --git a/pod/locale/fr/LC_MESSAGES/django.po b/pod/locale/fr/LC_MESSAGES/django.po index 7079ad8441..305b1cf82e 100755 --- a/pod/locale/fr/LC_MESSAGES/django.po +++ b/pod/locale/fr/LC_MESSAGES/django.po @@ -1,7 +1,5 @@ -# SOME DESCRIPTIVE TITLE. -# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the PACKAGE package. -# FIRST AUTHOR , YEAR. +# Esup Pod main French strings. +# This file is distributed under the same license as the POD package. # msgid "" msgstr "" diff --git a/pod/locale/fr/LC_MESSAGES/djangojs.po b/pod/locale/fr/LC_MESSAGES/djangojs.po index 8497114e07..4c620f1b18 100644 --- a/pod/locale/fr/LC_MESSAGES/djangojs.po +++ b/pod/locale/fr/LC_MESSAGES/djangojs.po @@ -1,7 +1,5 @@ -# SOME DESCRIPTIVE TITLE. -# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the PACKAGE package. -# FIRST AUTHOR , YEAR. +# Esup Pod JS French strings. +# This file is distributed under the same license as the POD package. # msgid "" msgstr "" diff --git a/pod/locale/nl/LC_MESSAGES/django.po b/pod/locale/nl/LC_MESSAGES/django.po index 5cfd07bacd..af1ee586e6 100644 --- a/pod/locale/nl/LC_MESSAGES/django.po +++ b/pod/locale/nl/LC_MESSAGES/django.po @@ -1,7 +1,5 @@ -# SOME DESCRIPTIVE TITLE. -# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the PACKAGE package. -# FIRST AUTHOR , YEAR. +# Esup Pod main dutch strings. +# This file is distributed under the same license as the POD package. # msgid "" msgstr "" diff --git a/pod/locale/nl/LC_MESSAGES/djangojs.po b/pod/locale/nl/LC_MESSAGES/djangojs.po index ad6dd8c85b..4d349b999a 100644 --- a/pod/locale/nl/LC_MESSAGES/djangojs.po +++ b/pod/locale/nl/LC_MESSAGES/djangojs.po @@ -1,7 +1,5 @@ -# SOME DESCRIPTIVE TITLE. -# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER -# This file is distributed under the same license as the PACKAGE package. -# FIRST AUTHOR , YEAR. +# Esup Pod JS Dutch strings. +# This file is distributed under the same license as the POD package. # #, fuzzy msgid "" diff --git a/pod/main/static/css/pod.css b/pod/main/static/css/pod.css index 1a2a677990..f835fe2ae1 100755 --- a/pod/main/static/css/pod.css +++ b/pod/main/static/css/pod.css @@ -17,29 +17,29 @@ body[data-admin-utc-offset] } body { -padding-top: 3.5rem; + padding-top: 3.5rem; } .jumbotron { padding: 1rem 2rem; } h1{ - font-size: 2rem; + font-size: 2rem; } label { - font-variant: small-caps; + font-variant: small-caps; } .pod-mt-7{ - margin-top: 3.5rem; + margin-top: 3.5rem; } .breadcrumb, .jumbotron{ - background: #f8f9fa; + background: #f8f9fa; } .list-group-item{ -padding:.25rem 1.25rem; + padding:.25rem 1.25rem; } .container-pod{ - max-width: 1400px; + max-width: 1400px; } /*** video ***/ @@ -86,13 +86,13 @@ padding:.25rem 1.25rem; /** modal **/ .modal-title svg { - height: 40px; - width: 40px; - /*color:#b03c7e;*/ + height: 40px; + width: 40px; + /*color:#b03c7e;*/ } .modal-title{ - display: flex; + display: flex; align-items: center; } @@ -100,20 +100,20 @@ padding:.25rem 1.25rem; max-width: 90%; }*/ .modal-pod-full .close{ -font-size:3rem; + font-size:3rem; } .title-chaine{ - margin-right: 0.5rem; - font-weight: 600; + margin-right: 0.5rem; + font-weight: 600; } #listeChaines{ - max-height: 30rem; - overflow: auto; - border-bottom: 1px solid black; + max-height: 30rem; + overflow: auto; + border-bottom: 1px solid black; } .liste-collapse{ - z-index: 900; + z-index: 900; } /**end modal **/ /*** tab **/ @@ -123,19 +123,19 @@ font-size:3rem; background: white; } .nav-tabs{ - border: 0; + border: 0; } /***aside ***/ .open-aside{ - margin-right: 15px; + margin-right: 15px; } #card-takenote .dropdown-menu{ border: none; } /*** modif taille logo ***/ @media (max-width: 576px ){ - .navbar-brand img{ + .navbar-brand img{ height: 35px; } .navbar-brand{ @@ -155,18 +155,18 @@ font-size:3rem; font-size: 0.9rem; } .navbar-items-pod{ - position: absolute; - top:53px; - left:0; - padding: 1rem; - width: 100%; + position: absolute; + top:53px; + left:0; + padding: 1rem; + width: 100%; } .navbar-nav .login .dropdown-menu { position: absolute; float: left; } #s::-webkit-input-placeholder { /* Chrome/Opera/Safari */ - color: white; + color: white; } #s::-moz-placeholder { /* Firefox 19+ */ color: white; @@ -178,16 +178,16 @@ font-size:3rem; color: white; } #s{ - width:1px; - border-radius:0; - box-shadow:none; - outline: none; - padding:0; - margin:0; - border:0; - background-color: transparent; - opacity:0; - position: absolute; + width:1px; + border-radius:0; + box-shadow:none; + outline: none; + padding:0; + margin:0; + border:0; + background-color: transparent; + opacity:0; + position: absolute; } #s:focus{ width:100%; @@ -221,8 +221,8 @@ font-size:3rem; /*background: #f8f9fa;*/ color: white !important; font-weight: 600; - height: 3rem; - width: 3rem; + height: 3rem; + width: 3rem; } .nav-link{ padding: 0.25rem 0.5rem; @@ -235,13 +235,13 @@ button.nav-link{ /*** CARD ***/ .infinite-item .card-body { - padding: 0.9rem; - height: 3.5rem; - overflow: hidden; + padding: 0.9rem; + height: 3.5rem; + overflow: hidden; } .infinite-item .card-header .octicon { - height:0.9rem; - margin:0.1rem; + height:0.9rem; + margin:0.1rem; } #videos_list .card-header, .playlist-videos .card-header { @@ -252,7 +252,7 @@ button.nav-link{ z-index: 9; } #videos_list .card-header .text-muted { - color:white !important; + color:white !important; } #videos_list .video-title{ @@ -261,35 +261,35 @@ button.nav-link{ } div.video-card .d-flex { - min-height:70%; - /*position:relative;*/ - top:0; + min-height:70%; + /*position:relative;*/ + top:0; } div.card a{ - overflow:hidden; + overflow:hidden; } div.card a img { - transition:-webkit-transform .5s ease; - transition:transform .5s ease; - transition:transform .5s ease, -webkit-transform .5s ease + transition:-webkit-transform .5s ease; + transition:transform .5s ease; + transition:transform .5s ease, -webkit-transform .5s ease } div.card a:hover>img { - -webkit-transform:scale(1.1); - transform:scale(1.1); - z-index:0; + -webkit-transform:scale(1.1); + transform:scale(1.1); + z-index:0; } .card-footer-pod { - position: absolute; - bottom: 57px; - right: 2px; - /*width: 100%; - text-align: center;*/ - border:none; - /*background: rgba(220,220,220);*/ - /*height: 1rem;*/ + position: absolute; + bottom: 57px; + right: 2px; + /*width: 100%; + text-align: center;*/ + border:none; + /*background: rgba(220,220,220);*/ + /*height: 1rem;*/ } .card-footer .feather, .card-header .feather{ - height: 1rem; + height: 1rem; } /* menu channels */ .dropdown-submenu > .dropdown-menu { @@ -300,7 +300,7 @@ div.card a:hover>img { border-radius: 0 6px 6px 6px; } .dropdown-submenu:hover { - background-color: white; + background-color: white; } .dropdown-submenu:hover > .dropdown-menu { display: block; @@ -321,98 +321,98 @@ div.card a:hover>img { /** aside list video **/ #filterType div.collapse:not(.show), #filterDiscipline div.collapse:not(.show), #filterTag div.collapse:not(.show) { - height: 148px !important; - overflow: hidden; + height: 148px !important; + overflow: hidden; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; } #filterType div.collapsing, #filterDiscipline div.collapsing, #filterTag div.collapsing{ - min-height: 148px !important; + min-height: 148px !important; } #filterType a.collapsed:after, #filterDiscipline a.collapsed:after, #filterTag a.collapsed:after { - content: '+'; + content: '+'; } #filterType a:not(.collapsed):after, #filterDiscipline a:not(.collapsed):after, #filterTag a:not(.collapsed):after { - content: '-'; + content: '-'; } /** ckeditor **/ .django-ckeditor-widget { - width: 100%; + width: 100%; } /** video form **/ .required { - color:red; - font-size: 1.3rem; - } + color:red; + font-size: 1.3rem; +} /*.form-group-required { border-left: 3px solid rgba(201, 76, 76, 0.3); padding-left : 5px; }*/ .form-group-required .errors { - background-color: rgba(201, 76, 76, 0.3); + background-color: rgba(201, 76, 76, 0.3); } #base-message-alert, #formalertdiv{ - position:fixed; - top:50%; - text-align: center; - margin:0 auto; - left: 50%; - transform: translate(-50%, 0); - z-index:1000; + position:fixed; + top:50%; + text-align: center; + margin:0 auto; + left: 50%; + transform: translate(-50%, 0); + z-index:1000; } /** channel form **/ #channel_form .selector, #enrichment_group_form .selector{ - width: 100% !important; + width: 100% !important; } #channel_form .selector select, #enrichment_group_form .selector select { - width:100% !important; + width:100% !important; } #channel_form .selector-available, #channel_form .selector-chosen, #enrichment_group_form .selector-available, #enrichment_group_form .selector-chosen { - width: 47% !important; + width: 47% !important; } #channel_form .selector-available h2, #channel_form .selector-chosen h2, #enrichment_group_form .selector-available h2, #enrichment_group_form .selector-chosen h2 { - margin: 0; - padding: 8px; - font-weight: 400; - font-size: 13px; - text-align: left; + margin: 0; + padding: 8px; + font-weight: 400; + font-size: 13px; + text-align: left; } /* vignettes video */ @media (max-width: 575.98px) { - .pod-flex-direction{ - flex-direction: column; - } + .pod-flex-direction{ + flex-direction: column; + } } @media screen and (min-width:768px){ - .card-img-top { - max-height:184.13px; - } + .card-img-top { + max-height:184.13px; + } } @media screen and (min-width:992px){ - .card-img-top { - max-height:195.36px; - } - .flex-2{ + .card-img-top { + max-height:195.36px; + } + .flex-2{ flex: 2; - } + } } /*** footer **/ footer{ - color:#aaa; + color:#aaa; } footer a, footer a:hover{ - color: white; + color: white; } /*** Useful for additional owners ***/ input.select2-input{ - width: 100% !important; + width: 100% !important; } #view-counter-icon{ @@ -421,10 +421,10 @@ input.select2-input{ #viewers-list { - position: absolute; + position: absolute; right: 0; margin-right: 80px; - color: #FFF; + color: #FFF; text-align: center; font-size: 20px; background-color: rgba(40,48,56, 0.5); From 9cf07899b685cd77d7ac7f067602111a08f408f8 Mon Sep 17 00:00:00 2001 From: Olivier Bado Date: Fri, 4 Dec 2020 10:55:52 +0100 Subject: [PATCH 002/145] Footer : replace inline ">" by css (better for accessibility and more customisable) + add css class on "back to top" link, so it's more customisable too. --- pod/main/static/css/pod.css | 18 ++++++++++++++++++ pod/main/templates/footer.html | 20 ++++++++++++-------- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/pod/main/static/css/pod.css b/pod/main/static/css/pod.css index f835fe2ae1..41bf374ff3 100755 --- a/pod/main/static/css/pod.css +++ b/pod/main/static/css/pod.css @@ -410,6 +410,24 @@ footer{ footer a, footer a:hover{ color: white; } + +.footer_links{ + list-style:none; +} +.footer_links>li:before{ + margin-right:.5em; + content: ">"; + font-weight: bold; +} + +.top_link:before{ + content: "\21E7"; + padding-right: 4px; + text-decoration: none; + display: inline-block; + font-size: 1.2em; +} + /*** Useful for additional owners ***/ input.select2-input{ width: 100% !important; diff --git a/pod/main/templates/footer.html b/pod/main/templates/footer.html index dc2bb3cc26..b4a00fdc90 100644 --- a/pod/main/templates/footer.html +++ b/pod/main/templates/footer.html @@ -3,7 +3,7 @@ {% spaceless %}

-Haut de page + Haut de page

@@ -16,15 +16,19 @@

- {% for link in LINK_FOOTER %} - - > {{link.title}} -
- {% endfor %} + {% if LINK_FOOTER %} + + {% endif %}

{{TITLE_SITE}} {% trans "video platform of" %} {{TITLE_ETB}} - {% trans "Release" %} {{VERSION}} - {{VIDEOS_COUNT}} {% trans "videos availables" %} [ {{VIDEOS_DURATION}} ]

From 62a1bf463f193a13fbe7263224c2af2b594ae001 Mon Sep 17 00:00:00 2001 From: Olivier Bado Date: Fri, 4 Dec 2020 12:05:06 +0100 Subject: [PATCH 003/145] Corrects some i18n english strings + Add some pydocs in import_data.py --- pod/video/management/commands/import_data.py | 62 +++++++++++++++----- pod/video/templates/videos/video-info.html | 45 +++++++------- 2 files changed, 69 insertions(+), 38 deletions(-) diff --git a/pod/video/management/commands/import_data.py b/pod/video/management/commands/import_data.py index e32ea3cc16..c5ced53075 100755 --- a/pod/video/management/commands/import_data.py +++ b/pod/video/management/commands/import_data.py @@ -1,3 +1,5 @@ +"""POD V1 DATA IMPORTER.""" + from django.utils import translation from django.core.management.base import BaseCommand from django.core import serializers @@ -40,6 +42,8 @@ class Command(BaseCommand): + """Main command class. To be called by a 'python manage.py import_data $type'.""" + args = 'Channel Theme Type User Discipline Pod tags Chapter Contributor...' help = 'Import from V1' valid_args = ['User', 'Channel', 'Theme', 'Type', 'Discipline', 'FlatPage', @@ -47,9 +51,11 @@ class Command(BaseCommand): 'Overlay', 'docpods', 'trackpods', 'enrichpods', 'Pod'] def add_arguments(self, parser): + """Add 'import' argument.""" parser.add_argument('import') def handle(self, *args, **options): + """Handle the command call.""" # Activate a fixed locale fr translation.activate('fr') if options['import'] and options['import'] in self.valid_args: @@ -81,6 +87,7 @@ def handle(self, *args, **options): % self.valid_args) def save_object(self, type_to_import, obj): + """Save object obj of type 'type_to_import'.""" if AUTHENTICATION and type_to_import == 'UserProfile': owner = Owner.objects.get( user_id=obj.object.user_id) @@ -97,11 +104,11 @@ def save_object(self, type_to_import, obj): else: try: if ( - type_to_import == 'Pod' or - not hasattr(obj.object, 'video') or - ( - hasattr(obj.object, 'video') and - obj.object.video.id not in VIDEO_ID_TO_EXCLUDE + type_to_import == 'Pod' + or not hasattr(obj.object, 'video') + or ( + hasattr(obj.object, 'video') + and obj.object.video.id not in VIDEO_ID_TO_EXCLUDE ) ): obj.object.headband = None @@ -112,6 +119,7 @@ def save_object(self, type_to_import, obj): print("Objects related does not exist %s" % e) def migrate_to_v2(self, filepath, type_to_import): + """Convert data type in specified filepath.""" f = open(filepath, 'r') filedata = f.read() f.close() @@ -121,36 +129,42 @@ def migrate_to_v2(self, filepath, type_to_import): f.close() def Channel(self, filedata): + """Rename channel model in specified filedata.""" return filedata.replace( "pods.channel", "video.channel" ) def Theme(self, filedata): + """Rename theme model in specified filedata.""" return filedata.replace( "pods.theme", "video.theme" ) def Type(self, filedata): + """Rename type model in specified filedata.""" return filedata.replace( "pods.type", "video.type" ).replace("headband", "icon") def Discipline(self, filedata): + """Rename discipline model in specified filedata.""" return filedata.replace( "pods.discipline", "video.discipline" ).replace("headband", "icon") def Pod(self, filedata): + """Rename pod model in specified filedata.""" return filedata.replace( "pods.pod", "video.video" ) def Chapter(self, filedata): + """Rename chapter model in specified filedata.""" return filedata.replace( "pods.chapterpods", "chapter.chapter" @@ -160,18 +174,21 @@ def Chapter(self, filedata): ) def Contributor(self, filedata): + """Rename contributor model in specified filedata.""" return filedata.replace( "pods.contributorpods", "completion.contributor" ) def Overlay(self, filedata): + """Rename overlay model in specified filedata.""" return filedata.replace( "pods.overlaypods", "completion.overlay" ) def get_new_data(self, type_to_import, filedata): + """Get new data.""" newdata = filedata type_import = { "Channel": self.Channel, @@ -194,6 +211,7 @@ def get_new_data(self, type_to_import, filedata): return newdata def add_data_to_video(self, type_to_import, obj, data): + """Add related data (docs, tags, track, enrich) to video.""" if type_to_import in ('docpods',): self.add_doc_to_video(obj, data) if type_to_import in ('tags',): @@ -204,6 +222,7 @@ def add_data_to_video(self, type_to_import, obj, data): self.add_enrich_to_video(obj, data) def add_tag_to_video(self, video_id, list_tag): + """Add tags to video.""" try: video = Video.objects.get(id=video_id) video.tags = ', '.join(list_tag) @@ -212,6 +231,7 @@ def add_tag_to_video(self, video_id, list_tag): print(video_id, " does not exist") def add_doc_to_video(self, video_id, list_doc): + """Add docs to video.""" print(video_id, list_doc) try: video = Video.objects.get(id=video_id) @@ -224,6 +244,7 @@ def add_doc_to_video(self, video_id, list_doc): print(video_id, " does not exist") def add_track_to_video(self, video_id, list_doc): + """Add tracks to video.""" print(video_id, list_doc) try: video = Video.objects.get(id=video_id) @@ -247,6 +268,7 @@ def add_track_to_video(self, video_id, list_doc): print(video_id, " does not exist") def convert_to_vtt(self, new_file): + """Convert subtitles to VTT.""" try: webvtt.from_srt(new_file).save(new_file[:-3] + "vtt") new_file = new_file[:-3] + "vtt" @@ -274,6 +296,7 @@ def convert_to_vtt(self, new_file): return "" def add_enrich_to_video(self, video_id, list_doc): + """Add enrichments to video.""" print(video_id) try: video = Video.objects.get(id=video_id) @@ -301,6 +324,7 @@ def add_enrich_to_video(self, video_id, list_doc): print(video_id, " does not exist") def download_doc(self, doc): + """Download doc from pod v1.""" source_url = FROM_URL + doc dest_file = os.path.join( settings.MEDIA_ROOT, @@ -312,6 +336,7 @@ def download_doc(self, doc): return new_file def create_and_save_doc(self, new_file, video): + """Create and save doc.""" if FILEPICKER: homedir, created = UserFolder.objects.get_or_create( name='home', @@ -338,6 +363,7 @@ def create_and_save_doc(self, new_file, video): return document def create_and_save_image(self, new_file, video): + """Create and save image.""" if FILEPICKER: homedir, created = UserFolder.objects.get_or_create( name='home', @@ -365,7 +391,8 @@ def create_and_save_image(self, new_file, video): """ -SAVE FROM PODV1 +# POD V1 DATA EXPORTER. +# To be executed inside 'python manage.py shell' on a pod server v1.x from pods.models import Channel from pods.models import Theme @@ -393,9 +420,9 @@ def create_and_save_image(self, new_file, video): with open("Type.json", "w") as out: json_serializer.serialize(Type.objects.all(), indent=2, stream=out) ->>> owners = set(Channel.objects.all().values_list("owners", flat=True)) ->>> users = set(Channel.objects.all().values_list("users", flat=True)) ->>> list_user = owners.union(users) +owners = set(Channel.objects.all().values_list("owners", flat=True)) +users = set(Channel.objects.all().values_list("users", flat=True)) +list_user = owners.union(users) with open("User.json", "w") as out: json_serializer.serialize(User.objects.filter(id__in=list_user), indent=2, @@ -417,7 +444,7 @@ def create_and_save_image(self, new_file, video): json_serializer.serialize(User.objects.filter(id__in=podowner), indent=2, stream=out) -video_fields = ('video', 'allow_donwloading', 'is_360', 'title', 'slug', +video_fields = ('video', 'allow_downloading', 'is_360', 'title', 'slug', 'owner', 'date_added', 'date_evt', 'cursus', 'main_lang', 'description', 'duration', 'type', 'discipline', 'channel', 'theme', 'is_draft', 'is_restricted', 'password') @@ -428,11 +455,11 @@ def create_and_save_image(self, new_file, video): list_tag = {} for p in Pod.objects.all(): - list_tag["%s" %p.id] = [] - for t in p.tags.all(): - list_tag["%s" %p.id].append(t.name) + list_tag["%s" %p.id] = [] + for t in p.tags.all(): + list_tag["%s" %p.id].append(t.name) with open("tags.json", "w") as out: - out.write(json.dumps(list_tag, indent=2)) + out.write(json.dumps(list_tag, indent=2)) with open("Chapter.json", "w") as out: @@ -448,6 +475,8 @@ def create_and_save_image(self, new_file, video): list_doc["%s" %p.id] = [] for d in p.docpods_set.all(): list_doc["%s" %p.id].append(d.document.file.name) +# -- PRESS enter AND WAIT FOR THE END OF PROCESSING +# BEFORE STARTING THE FOLLOWING 2 LINES -- with open("docpods.json", "w") as out: out.write(json.dumps(list_doc, indent=2)) @@ -462,6 +491,8 @@ def create_and_save_image(self, new_file, video): data['lang'] = d.lang data['src'] = d.src.file.name list_track["%s" %p.id].append(data) +# -- PRESS enter AND WAIT FOR THE END OF PROCESSING +# BEFORE STARTING THE FOLLOWING 2 LINES -- with open("trackpods.json", "w") as out: out.write(json.dumps(list_track, indent=2)) @@ -482,7 +513,8 @@ def create_and_save_image(self, new_file, video): data['document'] = d.document.file.name if d.document else "" data['embed'] = d.embed list_enrich["%s" %p.id].append(data) - +# -- PRESS enter AND WAIT FOR THE END OF PROCESSING +# BEFORE STARTING THE FOLLOWING 2 LINES -- with open("enrichpods.json", "w") as out: out.write(json.dumps(list_enrich, indent=2)) diff --git a/pod/video/templates/videos/video-info.html b/pod/video/templates/videos/video-info.html index 7d9bd62a8d..a060d6184f 100644 --- a/pod/video/templates/videos/video-info.html +++ b/pod/video/templates/videos/video-info.html @@ -12,7 +12,7 @@
 {% trans 'Summary' %}
{% if video.description or tag_list %}

{{ video.description|safe }}

{% if tag_list %} -

{% trans 'Tags' %} :  +

{% trans 'Tags:' %}  {% for tag in tag_list %} {{ tag }} @@ -33,7 +33,7 @@

 {% trans 'Summary' %}
 {% trans 'Infos' %}
-

{% trans 'Updated on' %} :
{{ video.date_added }}

-

{% trans 'Duration' %} :
{{ video.duration_in_time }}

-

{% trans 'Number of view' %} :
{{ video.get_viewcount }}{% if USE_STATS_VIEW %} ({% trans 'Show details views' %})

{% endif %} +

{% trans 'Updated on:' %}
{{ video.date_added }}

+

{% trans 'Duration:' %}
{{ video.duration_in_time }}

+

{% trans 'Number of view:' %}
{{ video.get_viewcount }}{% if USE_STATS_VIEW %} ({% trans 'Show details views' %})

{% endif %}
-

{% trans 'Type' %} :
{{ video.type.title }}

-

{% trans "Main language" %} :
{{video.get_main_lang}}

+

{% trans 'Type:' %}
{{ video.type.title }}

+

{% trans "Main language:" %}
{{video.get_main_lang}}

{% if video.cursus and video.cursus != '0' %} -

{% trans 'Audience' %} :
{{video.get_cursus}} +

{% trans 'Audience:' %}
{{video.get_cursus}} {%endif%} {% if video.discipline.all %} -

{% trans 'Disciplines' %} :
+

{% trans 'Disciplines:' %}
{% for disc in video.discipline.all %} {{ disc.title }} @@ -96,7 +96,7 @@

 {% trans 'Infos' %}

{%endif%} {% if request.GET.is_iframe and video.licence %} -

{% trans 'Licence' %} :
+

{% trans 'Licence:' %}
{% include "videos/video_licencebox.html" %}

{% endif %} @@ -105,11 +105,11 @@
 {% trans 'Infos' %}
-
 {% trans 'Downloads' %}
+
 {% trans 'Downloads' %}
{% if video.allow_downloading %} -
{% trans 'Video file(s)' %} :
+
{% trans 'Video file(s):' %}
{% for vid in video.get_video_mp4 %}
{% csrf_token %} @@ -123,7 +123,7 @@
 {% trans 'Downloads' %}
{% if video.allow_downloading %} {% if video.get_video_mp3 %} -
{% trans 'Audio file' %} :
+
{% trans 'Audio file:' %}
{% csrf_token %} @@ -135,7 +135,7 @@
 {% trans 'Downloads' %}
{% if video.document_set.all %} -
{% trans 'Document' %} :
+
{% trans 'Document:' %}
{% for doc in video.document_set.all %} {% csrf_token %} @@ -157,7 +157,7 @@
 {% if video.is_draft %}{% trans 'Embed/S - +


{% endif %} @@ -190,19 +190,18 @@
 {% if video.is_draft %}{% trans 'Embed/S {% trans 'Check the box to indicate the beginning of playing desired.' %}
- +
- +
- + qrcode
-
{%endif%}
From d39b693cd714a0cad20ddce5cdb66b705133e772 Mon Sep 17 00:00:00 2001 From: Olivier Bado Date: Fri, 11 Dec 2020 11:26:48 +0100 Subject: [PATCH 004/145] "username" was not displayed in "not authorized" message on login screen. --- pod/authentication/templates/registration/login.html | 3 +-- pod/custom/settings_local.py.example | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/pod/authentication/templates/registration/login.html b/pod/authentication/templates/registration/login.html index 1b73d7b173..dafd4f0302 100644 --- a/pod/authentication/templates/registration/login.html +++ b/pod/authentication/templates/registration/login.html @@ -31,7 +31,7 @@

{% trans 'Log in' %}

{% if user.is_authenticated %}

{% blocktrans trimmed %} - You are authenticated as {{ username }}, but are not authorized to + You are authenticated as {{ user }}, but are not authorized to access this page. Would you like to login to a different account? {% endblocktrans %}

@@ -52,7 +52,6 @@

{% trans 'Log in' %}

- {% url 'admin_password_reset' as password_reset_url %} {% if password_reset_url %} diff --git a/pod/custom/settings_local.py.example b/pod/custom/settings_local.py.example index 637b8c9f16..ebfb5ad64a 100644 --- a/pod/custom/settings_local.py.example +++ b/pod/custom/settings_local.py.example @@ -557,7 +557,7 @@ VIDEOS_DIR = "videos" ENCODE_VIDEO = "start_encode" """ -# pourl'encodage distant, il faut préciser le login, l'host et la clé pour appeler cet encodage +# pour l'encodage distant, il faut préciser le login, l'host et la clé pour appeler cet encodage # REMOTE ENCODING SETTINGS # SSH_REMOTE_USER = "" # SSH_REMOTE_HOST = "xxx.univ.fr" From 840573f2bb21f2d6505ebe17e6c093c5829ab91e Mon Sep 17 00:00:00 2001 From: ptitloup Date: Mon, 14 Dec 2020 11:28:08 +0100 Subject: [PATCH 005/145] bump version of Pillow et Deepspeech --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 8f69d0195b..fc516cbe2f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ Django==1.11.29 django-ckeditor==5.3.1 -Pillow==7.1.0 +Pillow==8.0.1 django-tagging==0.4.6 django-modeltranslation==0.12.2 webvtt-py==0.4.5 @@ -18,7 +18,7 @@ django-auth-mixins==1.0.1 celery==4.3.0 vine==1.3.0 pandas -deepspeech==0.9.1 +deepspeech==0.9.3 billiard<4.0,>=3.6.0 webrtcvad django-select2-forms==2.1.0 From eaab2ebd70cc77ccf56ecfcf4b8890f56865a629 Mon Sep 17 00:00:00 2001 From: ptitloup Date: Thu, 17 Dec 2020 17:24:55 +0100 Subject: [PATCH 006/145] add chardet and requests, bump urllib3 to 1.26.2 --- pod/custom/admin.py | 6 +- pod/custom/apps.py | 10 +- pod/custom/models.py | 6 +- pod/custom/settings_local.py.example | 2026 +++++++++++++------------- pod/custom/tests.py | 6 +- pod/custom/urls.py | 16 +- pod/custom/views.py | 6 +- requirements.txt | 4 +- 8 files changed, 1041 insertions(+), 1039 deletions(-) diff --git a/pod/custom/admin.py b/pod/custom/admin.py index 4185d360e9..e813532def 100644 --- a/pod/custom/admin.py +++ b/pod/custom/admin.py @@ -1,3 +1,3 @@ -# from django.contrib import admin - -# Register your models here. +# from django.contrib import admin + +# Register your models here. diff --git a/pod/custom/apps.py b/pod/custom/apps.py index 245de53f9d..5e0d8e1c89 100644 --- a/pod/custom/apps.py +++ b/pod/custom/apps.py @@ -1,5 +1,5 @@ -from django.apps import AppConfig - - -class CustomConfig(AppConfig): - name = 'custom' +from django.apps import AppConfig + + +class CustomConfig(AppConfig): + name = 'custom' diff --git a/pod/custom/models.py b/pod/custom/models.py index 0b4331b362..812d9cc8ae 100644 --- a/pod/custom/models.py +++ b/pod/custom/models.py @@ -1,3 +1,3 @@ -# from django.db import models - -# Create your models here. +# from django.db import models + +# Create your models here. diff --git a/pod/custom/settings_local.py.example b/pod/custom/settings_local.py.example index 637b8c9f16..2b23e2034b 100644 --- a/pod/custom/settings_local.py.example +++ b/pod/custom/settings_local.py.example @@ -1,1013 +1,1013 @@ -""" -# ********************************************************** ATTENTION ********************************************************** -# -# Attention ! Veuillez ne pas renommer ce fichier en settings_local.py mais vous en inspirer pour configurer votre instance Pod. -# -# ********************************************************** WARNING ************************************************************ -# -# Warning ! Please do not rename this file to settings_local.py but be inspired by it to configure your Pod instance. -# -#******************************************************************************************************************************** -""" - - -# configuration principale -""" -# La clé secrète d’une installation Django. -# Elle est utilisée dans le contexte de la signature cryptographique, -# et doit être définie à une valeur unique et non prédictible. -# https://docs.djangoproject.com/fr/1.11/ref/settings/#secret-key -""" -SECRET_KEY = 'A_CHANGER' - -""" -# Une valeur booléenne qui active ou désactive le mode de débogage. -# Ne déployez jamais de site en production avec le réglage DEBUG activé. -# https://docs.djangoproject.com/fr/1.11/ref/settings/#debug -""" -DEBUG = True - -""" -Pour forcer le https -""" -SECURE_SSL_REDIRECT = True -SESSION_COOKIE_SECURE = True -CSRF_COOKIE_SECURE = True - -""" -# Une liste de chaînes représentant des noms de domaine/d’hôte que ce site Django peut servir. -# C’est une mesure de sécurité pour empêcher les attaques d’en-tête Host HTTP, -# qui sont possibles même avec bien des configurations de serveur Web apparemment sécurisées. -# https://docs.djangoproject.com/fr/1.11/ref/settings/#allowed-hosts -""" -ALLOWED_HOSTS = ['localhost'] - -""" -# L’âge des cookies de sessions, en secondes. -# https://docs.djangoproject.com/fr/1.11/ref/settings/#session-cookie-age -""" -SESSION_COOKIE_AGE = 14400 - -""" -# Indique s’il faut que la session expire lorsque l’utilisateur ferme son navigateur. -# https://docs.djangoproject.com/fr/1.11/ref/settings/#session-cookie-age -""" -SESSION_EXPIRE_AT_BROWSER_CLOSE = True - -""" -# Une liste de toutes les personnes qui reçoivent les notifications d’erreurs dans le code. -# Lorsque DEBUG=False et qu’une vue lève une exception, -# Django envoie un courriel à ces personnes contenant les informations complètes de l’exception. -# Chaque élément de la liste doit être un tuple au format « (nom complet, adresse électronique) ». -# Exemple : [('John', 'john@example.com'), ('Mary', 'mary@example.com')] Dans Pod, -# les "admins" sont également destinataires des courriels de contact, -# d'encodage ou de flux rss si la variable CONTACT_US_EMAIL n'est pas renseignée. -""" -ADMINS = ( ('Name', 'adminmail@univ.fr'),) - -""" -# Dans Pod, les "managers" sont destinataires des courriels de fin d'encodage (et ainsi des vidéos déposées sur la plateforme). -# Le premier managers renseigné est également contact des flus rss. -# Ils sont aussi destinataires des courriels de contact si la variable CONTACT_US_EMAIL n'est pas renseignée. -""" -MANAGERS = ADMINS - -""" -# Un dictionnaire contenant les réglages de toutes les bases de données à utiliser avec Django. -# C’est un dictionnaire imbriqué dont les contenus font correspondre l’alias de base de données avec un dictionnaire contenant les options de chacune des bases de données. -# https://docs.djangoproject.com/fr/1.11/ref/settings/#databases -""" -DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), - } -} - -""" -# Langue par défaut si non détectée -""" -LANGUAGE_CODE = "fr" - -""" -# Langue disponible et traduite -""" -LANGUAGES = ( - ('fr', 'Français'), ('en', 'English'), ('nl', 'Dutch (Netherlands)') -) - -""" -# Une chaîne représentant le fuseau horaire pour cette installation. -# https://docs.djangoproject.com/fr/1.11/ref/settings/#std:setting-TIME_ZONE -""" -TIME_ZONE = "UTC" - -""" -# Le répertoire dans lequel stocker temporairement les données (typiquement pour les fichiers plus grands que FILE_UPLOAD_MAX_MEMORY_SIZE) lors des téléversements de fichiers. -# https://docs.djangoproject.com/fr/1.11/ref/settings/#file-upload-temp-dir -""" -FILE_UPLOAD_TEMP_DIR = "/var/tmp" - -""" -# Le chemin absolu vers le répertoire dans lequel collectstatic rassemble les fichiers statiques en vue du déploiement. -# https://docs.djangoproject.com/fr/1.11/ref/settings/#std:setting-STATIC_ROOT -""" -STATIC_ROOT = "/pod/static" - -""" -# Chemin absolu de système de fichiers pointant vers le répertoire qui contiendra les fichiers téléversés par les utilisateurs. -# https://docs.djangoproject.com/fr/1.11/ref/settings/#std:setting-MEDIA_ROOT -""" -MEDIA_ROOT = "/pod/media" - -""" -# nom du serveur smtp -""" -EMAIL_HOST = "smtp.univ.fr" - -""" -# port d'écoute du serveur smtp -""" -EMAIL_PORT = 25 - -""" -# courriel utilisé par défaut pour les envois automatique (erreur de code etc.) -""" -SERVER_EMAIL = "noreply@univ.fr" - -""" -# courriel utilisé par défaut pour les envois de courriel (contact, encodage etc.) -""" -DEFAULT_FROM_EMAIL= "noreply@univ.fr" - -""" -# Les utilisateurs inactif ne sont plus affichés dans la barre de menu utilisateur -""" -MENUBAR_HIDE_INACTIVE_OWNERS = True - -""" -# Les utilisateurs non staff ne sont plus affichés dans la barre de menu utilisateur -""" -MENUBAR_SHOW_STAFF_OWNERS_ONLY = False - -""" -# Afficher les vidéos dont l'accès est protégé par mot de passe sur la page d'accueil -""" -HOMEPAGE_SHOWS_PASSWORDED = False - -""" -# Afficher les vidéos dont l'accès est protégé par authentification sur la page d'accueil -""" -HOMEPAGE_SHOWS_RESTRICTED = False - -""" -# Les mots clés saisis lors de l'ajout de vidéo sont convertis automatiquement en minuscule -""" -FORCE_LOWERCASE_TAGS = True - -""" -# Les mots clés saisis lors de l'ajout de vidéo ne peuvent dépassé la longueur saisie -""" -MAX_TAG_LENGTH = 50 - -""" -# Utiliser l'application de gestion de fichier fourni avec le projet. Si False, chaque fichier envoyé ne pourra être utilisé qu'une seule fois. -""" -USE_PODFILE = False - -""" -# Liste des applications tierces accessibles. -""" -THIRD_PARTY_APPS = [] - -""" -# Nom du répertoire racine ou les fichiers "complémentaires" (hors vidéos etc.) sont téléversés. -""" -FILES_DIR = "files" - -""" -# Choix de sujet pour les courriels envoyés depuis la plateforme -""" -SUBJECT_CHOICES = ( - ('', '-----'), - ('info', ('Request more information')), - ('contribute', ('Learn more about how to contribute')), - ('request_password', ('Password request for a video')), - ('inappropriate_content', ('Report inappropriate content')), - ('bug', ('Correction or bug report')), - ('other', ('Other (please specify)')) -) - -""" -# Si valeur vaut 'True', le username de l'utilisateur ne sera pas visible sur la plate-forme Pod et -# si la valeur vaut 'False' le username sera affichés aux utilisateurs authentifiés. (par soucis du respect du RGPD) -""" -HIDE_USERNAME = False - -""" -# Si valeur vaut 'True', l'onglet Utilisateur ne sera pas visible et si la valeur vaut 'False' l'onglet Utilisateur ne sera visible qu'aux personnes authentifiées. -# (par soucis du respect du RGPD) -""" -HIDE_USER_TAB = False - -""" -# Si la valeur vaut 'True', le filtre des vidéos par utilisateur ne sera plus visible sur la plate-forme Pod et -# si la valeur vaut 'False' le filtre sera visible qu'aux personnes authentifiées. (par soucis du respect du RGPD) -""" -HIDE_USER_FILTER = False - -""" -# Si valeur vaut 'True', les URLs contenues dans le texte de superposition seront transformées, à la lecture de la vidéo, en lien cliquable. -""" -LINK_SUPERPOSITION = False - -""" -# Si valeur vaut 'True', les e-mails de contacts seront adressés, selon le sujet, soit au propriétaire de la vidéo soit au(x) manageur(s) des vidéos Pod. -# (voir USER_CONTACT_EMAIL_CASE et USE_ESTABLISHMENT_FIELD ) -""" -CUSTOM_CONTACT_US = False - -""" -# Une liste contenant les sujets de contact dont l'utilisateur sera seul destinataire plutôt que le(s) manageur(s). -# Si la liste est vide, les mails de contact seront envoyés au(x) manageur(s). Valeurs possibles : -# 'info', 'contribute', 'request_password', 'inapropriate_content', 'bug', 'other' -""" -USER_CONTACT_EMAIL_CASE = [] - -""" -# Si valeur vaut 'True', rajoute un attribut 'establishment' à l'utilisateur Pod ce qui permet de gérer plus d'un établissement pouvant utiliser Pod. -# Dans ce cas les emails de contact par exemple seront envoyés soit à l'utilisateur soit au(x) manageur(s) de l'établissement de l'utilisateur. -# (voir USER_CONTACT_EMAIL_CASE ) -# Egalement les emails de fin d'encodage seront envoyés au(x) manageur(s) de l'établissement du propriétaire de la vidéo encodée, -# en plus d'un email au propriétaire, en confirmation de la fin d'encodage de sa vidéo. -""" -USE_ESTABLISHMENT_FIELD = False - -""" -# Permet d'activer la possibilité de voir en details le nombre de visualisation d'une vidéo durant un jour donné ou mois, -# année ou encore le nombre de vue total depuis la création de la vidéo. un lien est rajouté dans la partie info lors de la lecture d'une vidéo, -# un lien est rajouté dans la page de visualisation d'une chaîne ou un theme ou encore toutes les vidéos présentes sur la plateforme. -""" -USE_STATS_VIEW = False - -""" -# affichage de la statistique de vue des videos si utilisateur authentifié -""" -VIEW_STATS_AUTH = False - -""" -# Fixe une année maximale que la date de suppression d'une vidéo ne peut dépasser. Par défaut MAX_DURATION_DATE_DELETE = 10. (Année courante + 10 ans). -""" -MAX_DURATION_DATE_DELETE = 10 - -""" -# Permet d'utiliser la mise en ligne fragmentée (qui permet de reprendre la mise en ligne lors de problèmes de connexion) -""" -USE_CHUNKED_UPLOAD = False - -""" -# Taille d'un fragment (le fichier sera mis en ligne par fragment de cette taille) -""" -CHUNK_SIZE = 1000000 - -""" -# Masquer l'authentification locale -""" -HIDE_LOCAL_LOGIN = False - -""" -# Afficher uniquement les thèmes de premier niveau dans l'onglet 'Chaîne' -""" -SHOW_ONLY_PARENT_THEMES = False - -""" -# Organiser l'affichage des vidéos dans des themes par sections pliables -""" -ORGANIZE_BY_THEME = False - -""" -# Configuration des templates / de l'affichage -# L'ensemble des variables ci-après doivent être contnu dans un dictionnnaire TEMPLATE_VISIBLE_SETTINGS. -# Voici sa valeur par défaut : -""" -TEMPLATE_VISIBLE_SETTINGS = { - 'TITLE_SITE': 'Pod', - 'TITLE_ETB': 'University name', - 'LOGO_SITE': 'img/logoPod.svg', - 'LOGO_ETB': 'img/logo_etb.svg', - 'LOGO_PLAYER': 'img/logoPod.svg', - 'LINK_PLAYER': '', - 'FOOTER_TEXT': ('',), - 'FAVICON': 'img/logoPod.svg', - 'CSS_OVERRIDE' : '', - 'PRE_HEADER_TEMPLATE' : '', - 'POST_FOOTER_TEMPLATE' : '', - 'TRACKING_TEMPLATE' : '', -} - - -""" -# Titre du site. -TITLE_SITE = "Pod" -""" - -""" -# Titre de l’établissement. -""" -TITLE_ETB = "University name" - -""" -# Logo affiché en haut à gauche sur toutes les pages. Doit se situer dans le répertoire static -""" -LOGO_SITE = "img/logoPod.svg" - -""" -# Logo affiché dans le footer sur toutes les pages. Doit se situer dans le répertoire static -""" -LOGO_ETB = "img/logo_etb.svg" - -""" -# Logo affiché sur le player video. Doit se situer dans le répertoire static -""" -LOGO_PLAYER = "img/logoPod.svg" - -""" -# Lien de destination du logo affiché sur le player -""" -LINK_PLAYER = "" - -""" -# Texte affiché dans le footer. Une ligne par entrée, accepte du code html. -# Par exmple : ( '42, rue Paul Duez', '59000 Lille - France', ('Google maps') ) -""" -FOOTER_TEXT = ('',) - -""" -# Icon affiché dans la barre d'adresse du navigateur -""" -FAVICON = "img/logoPod.svg" - -""" -# Si souhaitée, à créer et sauvegarder dans le répertoire static de l'application custom et préciser le chemin d'accès. Par exemple : "custom/etab.css" -""" -CSS_OVERRIDE = "" - -""" -# vous pouvez créer un template dans votre application custom et indiquer son chemin dans cette variable pour que ce code html, -# ce template soit affiché en haut de votre page, le code est ajouté juste après la balise body.(Or iframe) -# Si le fichié créé est '/usr/local/django_projects/podv2/pod/custom/templates/custom/preheader.html' alors la varaible doit prendre la valeur 'custom/preheader.html' -""" -PRE_HEADER_TEMPLATE = "" - -""" -# Idem que pre-header, le code contenu dans le template sera affiché juste avant la fermeture du body. (Or iframe) -""" -POST_FOOTER_TEMPLATE = "" - -""" -# vous pouvez créer un template dans votre application custom pour y intégrer votre code Piwik ou Google analytics. -# Ce template est inséré dans toutes les pages de la plateforme, y compris en mode iframe -""" -TRACKING_TEMPLATE = "" - -# Configuration application recherche -# Pour mettre à jour le moteur de recherche, je lance toutes les nuits la tache cron suivante : -# 0 5 * * * cd /home/pod/django_projects/podv2 && /home/pod/.virtualenvs/django_pod/bin/python manage.py index_videos --all - -""" -# adresse du ou des instances d'Elasticsearch utilisées pour l'indexation et la recherche de vidéo. -""" -ES_URL = ['http://127.0.0.1:9200/'] - -""" -# Valeur pour l’index de ElasticSearch -""" -ES_INDEX = "pod" - - - -# Configuration encodage - -""" -# commande ffmpeg -""" -FFMPEG = "ffmpeg" - -""" -# commande ffprobe -""" -FFPROBE = "ffprobe" - -""" -# durée en seconde des segment HLS -""" -SEGMENT_TARGET_DURATION = 2 - -""" -# la taille du buffer est égale au bitrate vidéo du rendu multiplié par cette valeur -""" -RATE_MONITOR_BUFFER_RATIO = 2 - -""" -# nombre de thread possible pour ffmpeg (0 égale maximum possible) -""" -FFMPEG_NB_THREADS = 0 - -""" -# Commande utilisée pour récupérer les informations de la première piste video du fichier envoyé -""" -GET_INFO_VIDEO = "%(ffprobe)s -v quiet -show_format -show_streams -select_streams v:0 -print_format json -i %(source)s" - -""" -# Commande utilisée pour récupérer les informations de la première piste audio du fichier envoyé -""" -GET_INFO_AUDIO = "%(ffprobe)s -v quiet -show_format -show_streams -select_streams a:0 -print_format json -i %(source)s" - -""" -# paramètres de la commande ffmpeg utilisés pour encoder toutes les vidéos, peu importe le rendu -""" -FFMPEG_STATIC_PARAMS = " -c:a aac -ar 48000 -c:v h264 -profile:v high -pix_fmt yuv420p -crf 20 -sc_threshold 0 -force_key_frames \"expr:gte(t,n_forced*1)\" -deinterlace -threads %(nb_threads)s " - -""" -# autres paramètres qui sont placés au début de la commande -""" -FFMPEG_MISC_PARAMS = " -hide_banner -y " - -""" -# bitrate audio pour l'encodage M4A (encodage des fichiers audio envoyés sur la plateforme) -""" -AUDIO_BITRATE = "192k" - -""" -# commande utilisée pour l'encodage des fichiers audio envoyés sur la plateforme -""" -ENCODING_M4A = "%(ffmpeg)s -i %(source)s %(misc_params)s -c:a aac -b:a %(audio_bitrate)s -vn -threads %(nb_threads)s \"%(output_dir)s/audio_%(audio_bitrate)s.m4a\"" - -""" -# commande utilisée pour l'encodage audio pour tous les fichiers envoyés sur la plateforme -""" -ENCODE_MP3_CMD = "%(ffmpeg)s -i %(source)s %(misc_params)s -vn -b:a %(audio_bitrate)s -vn -f mp3 -threads %(nb_threads)s \"%(output_dir)s/audio_%(audio_bitrate)s.mp3\"" - -""" -# Si True, un courriel est envoyé aux managers et à l'auteur (si DEBUG est à False) à la fin de l'encodage -""" -EMAIL_ON_ENCODING_COMPLETION = True - -""" -# Répertoire temporaire pour la création des thumbnails -""" -FILE_UPLOAD_TEMP_DIR = "/tmp" # déjà défini au dessus - -""" -# Utilisation de Celery pour la gestion des taches d'encodage -""" -CELERY_TO_ENCODE = False -CELERY_BROKER_URL = "amqp://pod:xxx@localhost/rabbitpod" - -# Configuration flux RSS - -""" -# couverture du droit pour chaque vidéo -""" -DEFAULT_DC_COVERAGE = TITLE_ETB + " - Town - Country" - -""" -# droit par défaut affichés dans le flux RSS si non renseigné -""" -DEFAULT_DC_RIGHTS = "BY-NC-SA" - -# Configuration application vidéo - -""" -# Si True, seule les personnes "Staff" peuvent déposer des vidéos sur la plateforme -""" -RESTRICT_EDIT_VIDEO_ACCESS_TO_STAFF_ONLY = False - -""" -# image par défaut affichée comme poster ou vignette, utilisée pour présenter la vidéo. Cette image doit se situer dans le répertoire static. -""" -DEFAULT_THUMBNAIL = "img/default.png" - -""" -# Encodage possible sur la plateforme. Associé à un rendu dans le cas d'une vidéo. -""" -ENCODING_CHOICES = ( - ("audio", "audio"),* - ("360p", "360p"), - ("480p", "480p"), - ("720p", "720p"), - ("1080p", "1080p"), - ("playlist", "playlist") -) - -""" -# Format d'encodage réalisé sur la plateforme. -""" -FORMAT_CHOICES = ( - ("video/mp4", 'video/mp4'), - ("video/mp2t", 'video/mp2t'), - ("video/webm", 'video/webm'), - ("audio/mp3", "audio/mp3"), - ("audio/wav", "audio/wav"), - ("application/x-mpegURL", "application/x-mpegURL"), -) - -""" -# Licence proposées pour les vidéos. -""" -LICENCE_CHOICES = ( - ('by', ("Attribution 4.0 International (CC BY 4.0)")), - ('by-nd', ("Attribution-NoDerivatives 4.0 " "International (CC BY-ND 4.0)" )), - ('by-nc-nd', ( "Attribution-NonCommercial-NoDerivatives 4.0 " "International (CC BY-NC-ND 4.0)" )), - ('by-nc', ("Attribution-NonCommercial 4.0 " "International (CC BY-NC 4.0)")), - ('by-nc-sa', ( "Attribution-NonCommercial-ShareAlike 4.0 " "International (CC BY-NC-SA 4.0)" )), - ('by-sa', ( "Attribution-ShareAlike 4.0 International (CC BY-SA 4.0)")) -) - -""" -# Les vidéos créées sans type (par importation par exemple) seront affectées au type par défaut (en général, le type ayant pour identifiant '1' est 'Other') -""" -DEFAULT_TYPE_ID = 1 - -""" -# Liste des cursus proposés lors de l'ajout des vidéos. Affichés en dessous d'une vidéos, ils sont aussi utilisés pour affiner la recherche. -""" -CURSUS_CODES = ( - ('0', ("None / All")), - ('L', ("Bachelor’s Degree")), - ('M', ("Master’s Degree")), - ('D', ("Doctorate")), - ('1', _("Other")) -) - -""" -# Liste des langues proposées lors de l'ajout des vidéos. Affichés en dessous d'une vidéos, ils sont aussi utilisés pour affiner la recherche. -""" -LANG_CHOICES = ( settings.PREF_LANG_CHOICES + (('', '----------'),) + settings.ALL_LANG_CHOICES ) - -""" -# Répertoire par défaut pour le téléversement des vidéos. -""" -VIDEOS_DIR = "videos" - -""" -# Fonction appelée pour lancer l'encodage des vidéos -# Pour l'encodage distant, il faut préciser "start_remote_encode" -""" -ENCODE_VIDEO = "start_encode" - -""" -# pourl'encodage distant, il faut préciser le login, l'host et la clé pour appeler cet encodage -# REMOTE ENCODING SETTINGS -# SSH_REMOTE_USER = "" -# SSH_REMOTE_HOST = "xxx.univ.fr" -# SSH_REMOTE_KEY = "/home/pod/.ssh/id_rsa" -""" - - -""" -# Extension autorisée pour le téléversement sur la plateforme -""" -VIDEO_ALLOWED_EXTENSIONS = ( '3gp', 'avi', 'divx', 'flv', 'm2p', 'm4v', 'mkv', 'mov', 'mp4', 'mpeg', 'mpg', 'mts', 'wmv', 'mp3', 'ogg', 'wav', 'wma' ) - -""" -# Taille maximum en Go des fichiers téléversés sur la plateforme -""" -VIDEO_MAX_UPLOAD_SIZE = 1 - -""" -# Ensemble des textes d'aide affichés avec le formulaire d'envoi de vidéo -# voir pod/video/forms.py -""" -VIDEO_FORM_FIELDS_HELP_TEXT = "" - -""" -# Liste des champs du formulaire d'édition de vidéos affichés -""" -VIDEO_FORM_FIELDS = __all__ - -""" -# Ensemble des textes d'aide affichés avec le formulaire d'édition de chaine. -# voir pod/video/forms.py -""" -CHANNEL_FORM_FIELDS_HELP_TEXT = "" - -""" -# Ensemble des textes d'aide affichés avec le formulaire d'édition de theme. -# voir pod/video/forms.py -""" -THEME_FORM_FIELDS_HELP_TEXT = "" - -# Configuration application recorder (enregistreur) - -""" -# Type d'enregistrement géré par la plateforme. -# Un enregistreur ne peut déposer que des fichiers de type proposé par la plateforme. -# Le traitement se fait en fonction du type de fichier déposé. -""" -RECORDER_TYPE = ( ('video', _('Video')), ('audiovideocast', _('Audiovideocast')), ) - -""" -# Chemin racine du répertoire où sont déposés les enregistrements (chemin du serveur FTP). -""" -DEFAULT_RECORDER_PATH = "/data/ftp-pod/ftp/" - -""" -# Identifiant du propriétaire par défaut (si non spécifié) des enregistrements déposés. -""" -DEFAULT_RECORDER_USER_ID = 1 - -""" -# Ajoute un enregistreur par défaut à un enregistrement non identifiable (mauvais chemin dans le dépôt FTP). -""" -DEFAULT_RECORDER_ID = 1 - -""" -# Identifiant du type de vidéo par défaut (si non spécifié). (Exemple : 3 pour Colloque/conférence, 4 pour Cours...) -""" -DEFAULT_RECORDER_TYPE_ID = 1 - -""" -# Si True, le manager de l'enregistreur pourra choisir un propriétaire de l'enregistrement. -""" -ALLOW_RECORDER_MANAGER_CHOICE_VID_OWNER = True - -""" -# Si True, une page apparaîtra dans le menu du profil de l'utilisateur autorisé permettant de s'attribuer un enregistrement. -""" -ALLOW_MANUAL_RECORDING_CLAIMING = False - -""" -# Liste des champs supplémentaires pour le formulaire des enregistreurs -# Cette liste reprend le nom des champs correspondants aux paramètres d'édition d'une vidéo (Discipline, Chaine, Theme, mots clés...) -# L'exemple suivant comporte l'ensemble des champs possibles mais peut être allégée en fonction des besoins. -# Les vidéos seront alors générées avec les valeurs des champs supplémentaires telles que définies dans leur enregistreur. -""" -RECORDER_ADDITIONAL_FIELDS = ('additional_users', 'is_draft', 'password', - 'is_restricted', 'restrict_access_to_groups', - 'cursus','main_lang', 'tags','discipline', - 'licence', 'channel', 'theme', 'transcript' - 'allow_downloading', 'is_360', 'disable_comment',) - -""" -# Si True, permet de ne pas prendre en compte la 1° image lors du traitement d'une fichier d'enregistrement de type AudioVideoCast. -""" -RECORDER_SKIP_FIRST_IMAGE = False - -""" -# Précise les proxy à utiliser pour une requête vers l'application elle même, par défaut force la non utilisation de proxy -""" -SELF_REQUESTS_PROXIES = { "http": None, "https": None} - -""" -# Autorise la requête sur l'application en elle même sans vérifier le certificat SSL -""" -ALLOW_INSECURE_REQUESTS = False - -""" -# Chemin d'accès web (public) au répertoire de dépot des enregistrements (DEFAULT_RECORDER_PATH). Attention à penser à modifier la conf de NGINX -""" -PUBLIC_RECORD_DIR = "records" - -""" -# Si True, affiche l'icone de prévisualisation des vidéos dans la page "Revendiquer un enregistrement" -""" -USE_RECORD_PREVIEW = False - -# Configuration application podfile (gestion de fichier) - -""" -# Extensions autorisées pour les documents téléversés dans le gestionnaire de fichier -""" -FILE_ALLOWED_EXTENSIONS = ( 'doc', 'docx', 'odt', 'pdf', 'xls', 'xlsx', 'ods', 'ppt', 'pptx', 'txt', 'html', 'htm', 'vtt', 'srt', 'webm', 'ts', ) - -""" -# Extensions autorisées pour les images téléversés dans le gestionnaire de fichier -""" -IMAGE_ALLOWED_EXTENSIONS = ( 'jpg', 'jpeg', 'bmp', 'png', 'gif', 'tiff', ) - -""" -# Poids maximum en Mo par fichier téléversé dans le gestionnaire de fichier -""" -FILE_MAX_UPLOAD_SIZE = 10 - -# Configuration application completion (contributeur, sous-titre, document à télécharger, superposition) - -""" -# Liste de rôle possible pour un contributeur -""" -ROLE_CHOICES = ( - ('actor', ('actor')), - ('author', ('author')), - ('designer', ('designer')), - ('consultant', ('consultant')), - ('contributor', ('contributor')), - ('editor', ('editor')), - ('speaker', ('speaker')), - ('soundman', ('soundman')), - ('director', ('director')), - ('writer', ('writer')), - ('technician', ('technician')), - ('voice-over', ('voice-over')), -) - -""" -# Liste de type de piste possible pour une vidéo (sous-titre, légende etc.) -""" -KIND_CHOICES = ( ('subtitles', ('subtitles')), ('captions', ('captions')), ) - -# Configuration application authentification (Local, CAS et LDAP) - -""" -# Type d'authentification possible sur votre instance. Pour l'instant local ou cas -""" -AUTH_TYPE = (('local', _('local')), ('CAS', 'CAS')) - -""" -# Activation de l'authentification CAS en plus de l'authentification locale -""" -USE_CAS = False - -""" -# Url du serveur cas de l'établissement. Format http://url_cas -""" -CAS_SERVER_URL = "sso_cas" - -""" -# Si True, authentifie automatiquement l'individu si déjà authentifié sur le serveur CAS -""" -CAS_GATEWAY = False - -""" -# Si utilisation de la connection CAS, renseigne les champs du compte de la personne depuis une source externe. -# Valeur possible : None (pas de renseignement), CAS (renseigne les champs de la personne depuis les informations renvoyées par le CAS), -# LDAP (Interroge le serveur LDAP pour renseigner les champs du compte de la personne) -""" -POPULATE_USER = None - -""" -# variable utilisée pour trouver les informations de l'individu connecté dans le fichier renvoyé par le CAS lors de l'authentification -""" -AUTH_CAS_USER_SEARCH = "user" - -""" -# liste de correspondance entre les champs d'un compte de Pod et les champs renvoyés par le CAS -""" -USER_CAS_MAPPING_ATTRIBUTES = { - "uid": "uid", - "mail": "mail", - "last_name": "sn", - "first_name": "givenname", - "affiliation": "eduPersonAffiliation" -} - -""" -# Si True, des groupes sont créés automatiquement à partir des affiliations des individus qui se connectent sur la plateforme et -# l'individu qui se connecte est ajouté automatiquement à ses groupes -""" -CREATE_GROUP_FROM_AFFILIATION = False - -""" -# Les personnes ayant pour affiliation les valeurs renseignées dans cette variable sont automatiquement la valeur staff de leur compte à True -""" -AFFILIATION_STAFF = ('faculty', 'employee', 'staff') - -""" -# Valeurs possibles pour l'Affiliation du compte -""" -AFFILIATION = ( - ('student', ('student')), - ('faculty', ('faculty')), - ('staff', ('staff')), - ('employee', ('employee')), - ('member', ('member')), - ('affiliate', ('affiliate')), - ('alum', ('alum')), - ('library-walk-in', ('library-walk-in')), - ('researcher', ('researcher')), - ('retired', ('retired')), - ('emeritus', ('emeritus')), - ('teacher', ('teacher')), - ('registered-reader', _('registered-reader')) -) - -""" -# Information de connection au serveur LDAP. Le champ url peut contenir un ou plusieurs url pour ajouter des hôtes de référence, exemple : -# Si un seul host : {'url': "ldap.univ.fr'', 'port': 389, 'use_ssl': False} -# Si plusieurs : {'url': ("ldap.univ.fr'',"ldap2.univ.fr"), 'port': 389, 'use_ssl': False} -""" -LDAP_SERVER = {'url': '', 'port': 389, 'use_ssl': False} - -""" -# Identifiant (DN) du compte pour se connecter au serveur LDAP -""" -AUTH_LDAP_BIND_DN = "" - -""" -# Mot de passe du compte pour se connecter au serveur LDAP -""" -AUTH_LDAP_BIND_PASSWORD = "" - -""" -# Filtre LDAP permettant la recherche de l'individu dans le serveur LDAP -""" -AUTH_LDAP_USER_SEARCH = ('ou=people,dc=univ,dc=fr', "(uid=%(uid)s)") - -""" -# liste de correspondance entre les champs d'un compte de Pod et les champs renvoyés par le LDAP -""" -USER_LDAP_MAPPING_ATTRIBUTES = { - "uid": "uid", - "mail": "mail", - "last_name": "sn", - "first_name": "givenname", - "primaryAffiliation": "eduPersonPrimaryAffiliation", - "affiliations": "eduPersonAffiliation" -} - -""" -# Utiliser l'authentification Shibboleth -""" -USE_SHIB = False - -""" -# Nom de la fédération d'identité utilisée -""" -SHIB_NAME = "" - -""" -# Mapping des attributs entre shibboleth et la classe utilisateur -""" -SHIBBOLETH_ATTRIBUTE_MAP = { - "shib-user": (True, "username"), - "shib-given-name": (True, "first_name"), - "shib-sn": (True, "last_name"), - "shib-mail": (False, "email"), -} - -""" -# Nom de l'attribut dans les headers qui sert à identifier l'utilisateur connecté avec Shibboleth -""" -REMOTE_USER_HEADER = REMOTE_USER - -""" -# URL de connexion à votre instance Shibboleth -""" - -SHIB_URL = "" - -""" -# URL de déconnexion à votre instance Shibboleth -""" -SHIB_LOGOUT_URL = "" - -""" -# Forcer le passage en minuscule du nom d'utilisateur CAS (permet de prévenir des doubles créations de comptes dans certain cas). -""" -CAS_FORCE_LOWERCASE_USERNAME = False - -""" -# Permettre l'usage du oembed, partage dans moodle, facebook, twitter etc. -""" -OEMBED = True - -""" -# Activer les commentaires au niveau de la plateforme -""" -ACTIVE_VIDEO_COMMENT = True - -""" -# Transcription -""" -USE_TRANSCRIPTION = True - -""" -# decoupage de l'audio pour la transcription -""" -AUDIO_SPLIT_TIME = 300 - -""" -# paramétrage de deepspeech pour le modele -""" -DS_PARAM = { - 'fr': { - # le modèle deepspeech - 'model': "/usr/local/django_projects/transcription/model_tensorflow_fr/output_graph.pbmm", - 'scorer': "/usr/local/django_projects/transcription/model_tensorflow_fr/kenlm.scorer", - - } -} - -""" -# La liste des utilisateurs regardant le direct sera réservée au staff -""" -VIEWERS_ONLY_FOR_STAFF = False - -""" -# Temps (en seconde) entre deux envois d'un signal au serveur, pour signaler la présence sur un live -# Peut être augmenté en cas de perte de performance mais au détriment de la qualité du comptage des valeurs -""" -HEARTBEAT_DELAY = 45 - -""" -# Délai (en seconde) selon lequel une vue est considérée comme expirée si elle n'as pas renvoyé de signal depuis -""" -VIEW_EXPIRATION_DELAY = 60 - -""" -# Utilisation de BigBlueButton -""" -USE_BBB = True - -""" -# Répertoire du plugin bbb-recorder (voir documentation https://github.com/jibon57/bbb-recorder) -# bbb-recorder doit être installé dans ce répertoire, sur tous les serveurs d'encodage -# bbb-recorder crée un répertoire 'homedir' / Downloads qui a besoin d'espace disque -""" -DEFAULT_BBB_PLUGIN = '/home/podtest/bbb-recorder/' - -""" -# Répertoire qui contiendra les fichiers vidéo générés par bbb-recorder -""" -DEFAULT_BBB_PATH = '/data/www/podtest/bbb-recorder/' - -""" -# URL du serveur BigBlueButton ou Scalelite, où se trouvent les présentations Web BBB et l'API -""" -BBB_SERVER_URL = 'https://bbb.univ.fr/' - -""" -# Clé BigBlueButton ou Scalelite LOADBALANCER_SECRET -""" -BBB_SECRET_KEY = 'abcdef' - -""" -# Type de vidéo générée par défaut -""" -DEFAULT_BBB_TYPE_ID = 1 - -""" -# Format du nom d'utilisateur dans BBB. -# Valeurs possibles : 'first_name last_name', 'last_name first_name' -# Exemple1 : si dans le serveur BBB, vous avez un utilisateur au format 'John Doe', mettez 'first_name last_name' -# Exemple2 : si dans le serveur BBB, vous avez un utilisateur au format 'Doe John', mettez 'last_name first_name' -""" -BBB_USERNAME_FORMAT = 'first_name last_name' - -""" -# Nombre de jours avant la suppression des sessions et des utilisateurs associés, non déjà publiés -# Pour ne pas supprimer les anciennes sessions, utiliser la valeur 0 -""" -BBB_NUMBER_DAYS_BEFORE_DELETE = 0 - -""" -# -# Permet d'activer le fonctionnement de categorie au niveau de ses vidéos -# Vous pouvez créer des catégories pour pouvoir ranger vos propres vidéos -# Les catégories sont liées à l'utilisateur -""" -USER_VIDEO_CATEGORY = False - -""" -# gestion de l'oboslesence des vidéos -""" -USE_OBSOLESCENCE = True - -WARN_DEADLINES = [60, 30, 7] - -POD_ARCHIVE_AFFILIATION = ['faculty', - 'staff', - 'employee', - 'affiliate', - 'alum', - 'library-walk-in', - 'researcher', - 'retired', - 'emeritus', - 'teacher', - 'registered-reader', - 'member'] - -ACCOMMODATION_YEARS = { - 'affiliate': 1 -} -DEFAULT_YEAR_DATE_DELETE = 2 - -""" -# Configuration / Activtion du LTI -# voir pod/main/settings.py L.224 -""" -LTI_ENABLED = True # False par défaut - -""" -# Configuration H5P -# les videos interactive ne sont plus maintenu dans Pod -""" -H5P_ENABLED = True # False -H5P_DEV_MODE = 0 -H5P_URL = '/h5p/' -H5P_SAVE = 30 -H5P_EXPORT = '/exports/' -H5P_LANGUAGE = 'fr' -BASE_URL = 'https://pod.univ.fr/' - +""" +# ********************************************************** ATTENTION ********************************************************** +# +# Attention ! Veuillez ne pas renommer ce fichier en settings_local.py mais vous en inspirer pour configurer votre instance Pod. +# +# ********************************************************** WARNING ************************************************************ +# +# Warning ! Please do not rename this file to settings_local.py but be inspired by it to configure your Pod instance. +# +#******************************************************************************************************************************** +""" + + +# configuration principale +""" +# La clé secrète d’une installation Django. +# Elle est utilisée dans le contexte de la signature cryptographique, +# et doit être définie à une valeur unique et non prédictible. +# https://docs.djangoproject.com/fr/1.11/ref/settings/#secret-key +""" +SECRET_KEY = 'A_CHANGER' + +""" +# Une valeur booléenne qui active ou désactive le mode de débogage. +# Ne déployez jamais de site en production avec le réglage DEBUG activé. +# https://docs.djangoproject.com/fr/1.11/ref/settings/#debug +""" +DEBUG = True + +""" +Pour forcer le https +""" +SECURE_SSL_REDIRECT = True +SESSION_COOKIE_SECURE = True +CSRF_COOKIE_SECURE = True + +""" +# Une liste de chaînes représentant des noms de domaine/d’hôte que ce site Django peut servir. +# C’est une mesure de sécurité pour empêcher les attaques d’en-tête Host HTTP, +# qui sont possibles même avec bien des configurations de serveur Web apparemment sécurisées. +# https://docs.djangoproject.com/fr/1.11/ref/settings/#allowed-hosts +""" +ALLOWED_HOSTS = ['localhost'] + +""" +# L’âge des cookies de sessions, en secondes. +# https://docs.djangoproject.com/fr/1.11/ref/settings/#session-cookie-age +""" +SESSION_COOKIE_AGE = 14400 + +""" +# Indique s’il faut que la session expire lorsque l’utilisateur ferme son navigateur. +# https://docs.djangoproject.com/fr/1.11/ref/settings/#session-cookie-age +""" +SESSION_EXPIRE_AT_BROWSER_CLOSE = True + +""" +# Une liste de toutes les personnes qui reçoivent les notifications d’erreurs dans le code. +# Lorsque DEBUG=False et qu’une vue lève une exception, +# Django envoie un courriel à ces personnes contenant les informations complètes de l’exception. +# Chaque élément de la liste doit être un tuple au format « (nom complet, adresse électronique) ». +# Exemple : [('John', 'john@example.com'), ('Mary', 'mary@example.com')] Dans Pod, +# les "admins" sont également destinataires des courriels de contact, +# d'encodage ou de flux rss si la variable CONTACT_US_EMAIL n'est pas renseignée. +""" +ADMINS = ( ('Name', 'adminmail@univ.fr'),) + +""" +# Dans Pod, les "managers" sont destinataires des courriels de fin d'encodage (et ainsi des vidéos déposées sur la plateforme). +# Le premier managers renseigné est également contact des flus rss. +# Ils sont aussi destinataires des courriels de contact si la variable CONTACT_US_EMAIL n'est pas renseignée. +""" +MANAGERS = ADMINS + +""" +# Un dictionnaire contenant les réglages de toutes les bases de données à utiliser avec Django. +# C’est un dictionnaire imbriqué dont les contenus font correspondre l’alias de base de données avec un dictionnaire contenant les options de chacune des bases de données. +# https://docs.djangoproject.com/fr/1.11/ref/settings/#databases +""" +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), + } +} + +""" +# Langue par défaut si non détectée +""" +LANGUAGE_CODE = "fr" + +""" +# Langue disponible et traduite +""" +LANGUAGES = ( + ('fr', 'Français'), ('en', 'English'), ('nl', 'Dutch (Netherlands)') +) + +""" +# Une chaîne représentant le fuseau horaire pour cette installation. +# https://docs.djangoproject.com/fr/1.11/ref/settings/#std:setting-TIME_ZONE +""" +TIME_ZONE = "UTC" + +""" +# Le répertoire dans lequel stocker temporairement les données (typiquement pour les fichiers plus grands que FILE_UPLOAD_MAX_MEMORY_SIZE) lors des téléversements de fichiers. +# https://docs.djangoproject.com/fr/1.11/ref/settings/#file-upload-temp-dir +""" +FILE_UPLOAD_TEMP_DIR = "/var/tmp" + +""" +# Le chemin absolu vers le répertoire dans lequel collectstatic rassemble les fichiers statiques en vue du déploiement. +# https://docs.djangoproject.com/fr/1.11/ref/settings/#std:setting-STATIC_ROOT +""" +STATIC_ROOT = "/pod/static" + +""" +# Chemin absolu de système de fichiers pointant vers le répertoire qui contiendra les fichiers téléversés par les utilisateurs. +# https://docs.djangoproject.com/fr/1.11/ref/settings/#std:setting-MEDIA_ROOT +""" +MEDIA_ROOT = "/pod/media" + +""" +# nom du serveur smtp +""" +EMAIL_HOST = "smtp.univ.fr" + +""" +# port d'écoute du serveur smtp +""" +EMAIL_PORT = 25 + +""" +# courriel utilisé par défaut pour les envois automatique (erreur de code etc.) +""" +SERVER_EMAIL = "noreply@univ.fr" + +""" +# courriel utilisé par défaut pour les envois de courriel (contact, encodage etc.) +""" +DEFAULT_FROM_EMAIL= "noreply@univ.fr" + +""" +# Les utilisateurs inactif ne sont plus affichés dans la barre de menu utilisateur +""" +MENUBAR_HIDE_INACTIVE_OWNERS = True + +""" +# Les utilisateurs non staff ne sont plus affichés dans la barre de menu utilisateur +""" +MENUBAR_SHOW_STAFF_OWNERS_ONLY = False + +""" +# Afficher les vidéos dont l'accès est protégé par mot de passe sur la page d'accueil +""" +HOMEPAGE_SHOWS_PASSWORDED = False + +""" +# Afficher les vidéos dont l'accès est protégé par authentification sur la page d'accueil +""" +HOMEPAGE_SHOWS_RESTRICTED = False + +""" +# Les mots clés saisis lors de l'ajout de vidéo sont convertis automatiquement en minuscule +""" +FORCE_LOWERCASE_TAGS = True + +""" +# Les mots clés saisis lors de l'ajout de vidéo ne peuvent dépassé la longueur saisie +""" +MAX_TAG_LENGTH = 50 + +""" +# Utiliser l'application de gestion de fichier fourni avec le projet. Si False, chaque fichier envoyé ne pourra être utilisé qu'une seule fois. +""" +USE_PODFILE = False + +""" +# Liste des applications tierces accessibles. +""" +THIRD_PARTY_APPS = [] + +""" +# Nom du répertoire racine ou les fichiers "complémentaires" (hors vidéos etc.) sont téléversés. +""" +FILES_DIR = "files" + +""" +# Choix de sujet pour les courriels envoyés depuis la plateforme +""" +SUBJECT_CHOICES = ( + ('', '-----'), + ('info', ('Request more information')), + ('contribute', ('Learn more about how to contribute')), + ('request_password', ('Password request for a video')), + ('inappropriate_content', ('Report inappropriate content')), + ('bug', ('Correction or bug report')), + ('other', ('Other (please specify)')) +) + +""" +# Si valeur vaut 'True', le username de l'utilisateur ne sera pas visible sur la plate-forme Pod et +# si la valeur vaut 'False' le username sera affichés aux utilisateurs authentifiés. (par soucis du respect du RGPD) +""" +HIDE_USERNAME = False + +""" +# Si valeur vaut 'True', l'onglet Utilisateur ne sera pas visible et si la valeur vaut 'False' l'onglet Utilisateur ne sera visible qu'aux personnes authentifiées. +# (par soucis du respect du RGPD) +""" +HIDE_USER_TAB = False + +""" +# Si la valeur vaut 'True', le filtre des vidéos par utilisateur ne sera plus visible sur la plate-forme Pod et +# si la valeur vaut 'False' le filtre sera visible qu'aux personnes authentifiées. (par soucis du respect du RGPD) +""" +HIDE_USER_FILTER = False + +""" +# Si valeur vaut 'True', les URLs contenues dans le texte de superposition seront transformées, à la lecture de la vidéo, en lien cliquable. +""" +LINK_SUPERPOSITION = False + +""" +# Si valeur vaut 'True', les e-mails de contacts seront adressés, selon le sujet, soit au propriétaire de la vidéo soit au(x) manageur(s) des vidéos Pod. +# (voir USER_CONTACT_EMAIL_CASE et USE_ESTABLISHMENT_FIELD ) +""" +CUSTOM_CONTACT_US = False + +""" +# Une liste contenant les sujets de contact dont l'utilisateur sera seul destinataire plutôt que le(s) manageur(s). +# Si la liste est vide, les mails de contact seront envoyés au(x) manageur(s). Valeurs possibles : +# 'info', 'contribute', 'request_password', 'inapropriate_content', 'bug', 'other' +""" +USER_CONTACT_EMAIL_CASE = [] + +""" +# Si valeur vaut 'True', rajoute un attribut 'establishment' à l'utilisateur Pod ce qui permet de gérer plus d'un établissement pouvant utiliser Pod. +# Dans ce cas les emails de contact par exemple seront envoyés soit à l'utilisateur soit au(x) manageur(s) de l'établissement de l'utilisateur. +# (voir USER_CONTACT_EMAIL_CASE ) +# Egalement les emails de fin d'encodage seront envoyés au(x) manageur(s) de l'établissement du propriétaire de la vidéo encodée, +# en plus d'un email au propriétaire, en confirmation de la fin d'encodage de sa vidéo. +""" +USE_ESTABLISHMENT_FIELD = False + +""" +# Permet d'activer la possibilité de voir en details le nombre de visualisation d'une vidéo durant un jour donné ou mois, +# année ou encore le nombre de vue total depuis la création de la vidéo. un lien est rajouté dans la partie info lors de la lecture d'une vidéo, +# un lien est rajouté dans la page de visualisation d'une chaîne ou un theme ou encore toutes les vidéos présentes sur la plateforme. +""" +USE_STATS_VIEW = False + +""" +# affichage de la statistique de vue des videos si utilisateur authentifié +""" +VIEW_STATS_AUTH = False + +""" +# Fixe une année maximale que la date de suppression d'une vidéo ne peut dépasser. Par défaut MAX_DURATION_DATE_DELETE = 10. (Année courante + 10 ans). +""" +MAX_DURATION_DATE_DELETE = 10 + +""" +# Permet d'utiliser la mise en ligne fragmentée (qui permet de reprendre la mise en ligne lors de problèmes de connexion) +""" +USE_CHUNKED_UPLOAD = False + +""" +# Taille d'un fragment (le fichier sera mis en ligne par fragment de cette taille) +""" +CHUNK_SIZE = 1000000 + +""" +# Masquer l'authentification locale +""" +HIDE_LOCAL_LOGIN = False + +""" +# Afficher uniquement les thèmes de premier niveau dans l'onglet 'Chaîne' +""" +SHOW_ONLY_PARENT_THEMES = False + +""" +# Organiser l'affichage des vidéos dans des themes par sections pliables +""" +ORGANIZE_BY_THEME = False + +""" +# Configuration des templates / de l'affichage +# L'ensemble des variables ci-après doivent être contnu dans un dictionnnaire TEMPLATE_VISIBLE_SETTINGS. +# Voici sa valeur par défaut : +""" +TEMPLATE_VISIBLE_SETTINGS = { + 'TITLE_SITE': 'Pod', + 'TITLE_ETB': 'University name', + 'LOGO_SITE': 'img/logoPod.svg', + 'LOGO_ETB': 'img/logo_etb.svg', + 'LOGO_PLAYER': 'img/logoPod.svg', + 'LINK_PLAYER': '', + 'FOOTER_TEXT': ('',), + 'FAVICON': 'img/logoPod.svg', + 'CSS_OVERRIDE' : '', + 'PRE_HEADER_TEMPLATE' : '', + 'POST_FOOTER_TEMPLATE' : '', + 'TRACKING_TEMPLATE' : '', +} + + +""" +# Titre du site. +TITLE_SITE = "Pod" +""" + +""" +# Titre de l’établissement. +""" +TITLE_ETB = "University name" + +""" +# Logo affiché en haut à gauche sur toutes les pages. Doit se situer dans le répertoire static +""" +LOGO_SITE = "img/logoPod.svg" + +""" +# Logo affiché dans le footer sur toutes les pages. Doit se situer dans le répertoire static +""" +LOGO_ETB = "img/logo_etb.svg" + +""" +# Logo affiché sur le player video. Doit se situer dans le répertoire static +""" +LOGO_PLAYER = "img/logoPod.svg" + +""" +# Lien de destination du logo affiché sur le player +""" +LINK_PLAYER = "" + +""" +# Texte affiché dans le footer. Une ligne par entrée, accepte du code html. +# Par exmple : ( '42, rue Paul Duez', '59000 Lille - France', ('Google maps') ) +""" +FOOTER_TEXT = ('',) + +""" +# Icon affiché dans la barre d'adresse du navigateur +""" +FAVICON = "img/logoPod.svg" + +""" +# Si souhaitée, à créer et sauvegarder dans le répertoire static de l'application custom et préciser le chemin d'accès. Par exemple : "custom/etab.css" +""" +CSS_OVERRIDE = "" + +""" +# vous pouvez créer un template dans votre application custom et indiquer son chemin dans cette variable pour que ce code html, +# ce template soit affiché en haut de votre page, le code est ajouté juste après la balise body.(Or iframe) +# Si le fichié créé est '/usr/local/django_projects/podv2/pod/custom/templates/custom/preheader.html' alors la varaible doit prendre la valeur 'custom/preheader.html' +""" +PRE_HEADER_TEMPLATE = "" + +""" +# Idem que pre-header, le code contenu dans le template sera affiché juste avant la fermeture du body. (Or iframe) +""" +POST_FOOTER_TEMPLATE = "" + +""" +# vous pouvez créer un template dans votre application custom pour y intégrer votre code Piwik ou Google analytics. +# Ce template est inséré dans toutes les pages de la plateforme, y compris en mode iframe +""" +TRACKING_TEMPLATE = "" + +# Configuration application recherche +# Pour mettre à jour le moteur de recherche, je lance toutes les nuits la tache cron suivante : +# 0 5 * * * cd /home/pod/django_projects/podv2 && /home/pod/.virtualenvs/django_pod/bin/python manage.py index_videos --all + +""" +# adresse du ou des instances d'Elasticsearch utilisées pour l'indexation et la recherche de vidéo. +""" +ES_URL = ['http://127.0.0.1:9200/'] + +""" +# Valeur pour l’index de ElasticSearch +""" +ES_INDEX = "pod" + + + +# Configuration encodage + +""" +# commande ffmpeg +""" +FFMPEG = "ffmpeg" + +""" +# commande ffprobe +""" +FFPROBE = "ffprobe" + +""" +# durée en seconde des segment HLS +""" +SEGMENT_TARGET_DURATION = 2 + +""" +# la taille du buffer est égale au bitrate vidéo du rendu multiplié par cette valeur +""" +RATE_MONITOR_BUFFER_RATIO = 2 + +""" +# nombre de thread possible pour ffmpeg (0 égale maximum possible) +""" +FFMPEG_NB_THREADS = 0 + +""" +# Commande utilisée pour récupérer les informations de la première piste video du fichier envoyé +""" +GET_INFO_VIDEO = "%(ffprobe)s -v quiet -show_format -show_streams -select_streams v:0 -print_format json -i %(source)s" + +""" +# Commande utilisée pour récupérer les informations de la première piste audio du fichier envoyé +""" +GET_INFO_AUDIO = "%(ffprobe)s -v quiet -show_format -show_streams -select_streams a:0 -print_format json -i %(source)s" + +""" +# paramètres de la commande ffmpeg utilisés pour encoder toutes les vidéos, peu importe le rendu +""" +FFMPEG_STATIC_PARAMS = " -c:a aac -ar 48000 -c:v h264 -profile:v high -pix_fmt yuv420p -crf 20 -sc_threshold 0 -force_key_frames \"expr:gte(t,n_forced*1)\" -deinterlace -threads %(nb_threads)s " + +""" +# autres paramètres qui sont placés au début de la commande +""" +FFMPEG_MISC_PARAMS = " -hide_banner -y " + +""" +# bitrate audio pour l'encodage M4A (encodage des fichiers audio envoyés sur la plateforme) +""" +AUDIO_BITRATE = "192k" + +""" +# commande utilisée pour l'encodage des fichiers audio envoyés sur la plateforme +""" +ENCODING_M4A = "%(ffmpeg)s -i %(source)s %(misc_params)s -c:a aac -b:a %(audio_bitrate)s -vn -threads %(nb_threads)s \"%(output_dir)s/audio_%(audio_bitrate)s.m4a\"" + +""" +# commande utilisée pour l'encodage audio pour tous les fichiers envoyés sur la plateforme +""" +ENCODE_MP3_CMD = "%(ffmpeg)s -i %(source)s %(misc_params)s -vn -b:a %(audio_bitrate)s -vn -f mp3 -threads %(nb_threads)s \"%(output_dir)s/audio_%(audio_bitrate)s.mp3\"" + +""" +# Si True, un courriel est envoyé aux managers et à l'auteur (si DEBUG est à False) à la fin de l'encodage +""" +EMAIL_ON_ENCODING_COMPLETION = True + +""" +# Répertoire temporaire pour la création des thumbnails +""" +FILE_UPLOAD_TEMP_DIR = "/tmp" # déjà défini au dessus + +""" +# Utilisation de Celery pour la gestion des taches d'encodage +""" +CELERY_TO_ENCODE = False +CELERY_BROKER_URL = "amqp://pod:xxx@localhost/rabbitpod" + +# Configuration flux RSS + +""" +# couverture du droit pour chaque vidéo +""" +DEFAULT_DC_COVERAGE = TITLE_ETB + " - Town - Country" + +""" +# droit par défaut affichés dans le flux RSS si non renseigné +""" +DEFAULT_DC_RIGHTS = "BY-NC-SA" + +# Configuration application vidéo + +""" +# Si True, seule les personnes "Staff" peuvent déposer des vidéos sur la plateforme +""" +RESTRICT_EDIT_VIDEO_ACCESS_TO_STAFF_ONLY = False + +""" +# image par défaut affichée comme poster ou vignette, utilisée pour présenter la vidéo. Cette image doit se situer dans le répertoire static. +""" +DEFAULT_THUMBNAIL = "img/default.png" + +""" +# Encodage possible sur la plateforme. Associé à un rendu dans le cas d'une vidéo. +""" +ENCODING_CHOICES = ( + ("audio", "audio"),* + ("360p", "360p"), + ("480p", "480p"), + ("720p", "720p"), + ("1080p", "1080p"), + ("playlist", "playlist") +) + +""" +# Format d'encodage réalisé sur la plateforme. +""" +FORMAT_CHOICES = ( + ("video/mp4", 'video/mp4'), + ("video/mp2t", 'video/mp2t'), + ("video/webm", 'video/webm'), + ("audio/mp3", "audio/mp3"), + ("audio/wav", "audio/wav"), + ("application/x-mpegURL", "application/x-mpegURL"), +) + +""" +# Licence proposées pour les vidéos. +""" +LICENCE_CHOICES = ( + ('by', ("Attribution 4.0 International (CC BY 4.0)")), + ('by-nd', ("Attribution-NoDerivatives 4.0 " "International (CC BY-ND 4.0)" )), + ('by-nc-nd', ( "Attribution-NonCommercial-NoDerivatives 4.0 " "International (CC BY-NC-ND 4.0)" )), + ('by-nc', ("Attribution-NonCommercial 4.0 " "International (CC BY-NC 4.0)")), + ('by-nc-sa', ( "Attribution-NonCommercial-ShareAlike 4.0 " "International (CC BY-NC-SA 4.0)" )), + ('by-sa', ( "Attribution-ShareAlike 4.0 International (CC BY-SA 4.0)")) +) + +""" +# Les vidéos créées sans type (par importation par exemple) seront affectées au type par défaut (en général, le type ayant pour identifiant '1' est 'Other') +""" +DEFAULT_TYPE_ID = 1 + +""" +# Liste des cursus proposés lors de l'ajout des vidéos. Affichés en dessous d'une vidéos, ils sont aussi utilisés pour affiner la recherche. +""" +CURSUS_CODES = ( + ('0', ("None / All")), + ('L', ("Bachelor’s Degree")), + ('M', ("Master’s Degree")), + ('D', ("Doctorate")), + ('1', _("Other")) +) + +""" +# Liste des langues proposées lors de l'ajout des vidéos. Affichés en dessous d'une vidéos, ils sont aussi utilisés pour affiner la recherche. +""" +LANG_CHOICES = ( settings.PREF_LANG_CHOICES + (('', '----------'),) + settings.ALL_LANG_CHOICES ) + +""" +# Répertoire par défaut pour le téléversement des vidéos. +""" +VIDEOS_DIR = "videos" + +""" +# Fonction appelée pour lancer l'encodage des vidéos +# Pour l'encodage distant, il faut préciser "start_remote_encode" +""" +ENCODE_VIDEO = "start_encode" + +""" +# pourl'encodage distant, il faut préciser le login, l'host et la clé pour appeler cet encodage +# REMOTE ENCODING SETTINGS +# SSH_REMOTE_USER = "" +# SSH_REMOTE_HOST = "xxx.univ.fr" +# SSH_REMOTE_KEY = "/home/pod/.ssh/id_rsa" +""" + + +""" +# Extension autorisée pour le téléversement sur la plateforme +""" +VIDEO_ALLOWED_EXTENSIONS = ( '3gp', 'avi', 'divx', 'flv', 'm2p', 'm4v', 'mkv', 'mov', 'mp4', 'mpeg', 'mpg', 'mts', 'wmv', 'mp3', 'ogg', 'wav', 'wma' ) + +""" +# Taille maximum en Go des fichiers téléversés sur la plateforme +""" +VIDEO_MAX_UPLOAD_SIZE = 1 + +""" +# Ensemble des textes d'aide affichés avec le formulaire d'envoi de vidéo +# voir pod/video/forms.py +""" +VIDEO_FORM_FIELDS_HELP_TEXT = "" + +""" +# Liste des champs du formulaire d'édition de vidéos affichés +""" +VIDEO_FORM_FIELDS = __all__ + +""" +# Ensemble des textes d'aide affichés avec le formulaire d'édition de chaine. +# voir pod/video/forms.py +""" +CHANNEL_FORM_FIELDS_HELP_TEXT = "" + +""" +# Ensemble des textes d'aide affichés avec le formulaire d'édition de theme. +# voir pod/video/forms.py +""" +THEME_FORM_FIELDS_HELP_TEXT = "" + +# Configuration application recorder (enregistreur) + +""" +# Type d'enregistrement géré par la plateforme. +# Un enregistreur ne peut déposer que des fichiers de type proposé par la plateforme. +# Le traitement se fait en fonction du type de fichier déposé. +""" +RECORDER_TYPE = ( ('video', _('Video')), ('audiovideocast', _('Audiovideocast')), ) + +""" +# Chemin racine du répertoire où sont déposés les enregistrements (chemin du serveur FTP). +""" +DEFAULT_RECORDER_PATH = "/data/ftp-pod/ftp/" + +""" +# Identifiant du propriétaire par défaut (si non spécifié) des enregistrements déposés. +""" +DEFAULT_RECORDER_USER_ID = 1 + +""" +# Ajoute un enregistreur par défaut à un enregistrement non identifiable (mauvais chemin dans le dépôt FTP). +""" +DEFAULT_RECORDER_ID = 1 + +""" +# Identifiant du type de vidéo par défaut (si non spécifié). (Exemple : 3 pour Colloque/conférence, 4 pour Cours...) +""" +DEFAULT_RECORDER_TYPE_ID = 1 + +""" +# Si True, le manager de l'enregistreur pourra choisir un propriétaire de l'enregistrement. +""" +ALLOW_RECORDER_MANAGER_CHOICE_VID_OWNER = True + +""" +# Si True, une page apparaîtra dans le menu du profil de l'utilisateur autorisé permettant de s'attribuer un enregistrement. +""" +ALLOW_MANUAL_RECORDING_CLAIMING = False + +""" +# Liste des champs supplémentaires pour le formulaire des enregistreurs +# Cette liste reprend le nom des champs correspondants aux paramètres d'édition d'une vidéo (Discipline, Chaine, Theme, mots clés...) +# L'exemple suivant comporte l'ensemble des champs possibles mais peut être allégée en fonction des besoins. +# Les vidéos seront alors générées avec les valeurs des champs supplémentaires telles que définies dans leur enregistreur. +""" +RECORDER_ADDITIONAL_FIELDS = ('additional_users', 'is_draft', 'password', + 'is_restricted', 'restrict_access_to_groups', + 'cursus','main_lang', 'tags','discipline', + 'licence', 'channel', 'theme', 'transcript' + 'allow_downloading', 'is_360', 'disable_comment',) + +""" +# Si True, permet de ne pas prendre en compte la 1° image lors du traitement d'une fichier d'enregistrement de type AudioVideoCast. +""" +RECORDER_SKIP_FIRST_IMAGE = False + +""" +# Précise les proxy à utiliser pour une requête vers l'application elle même, par défaut force la non utilisation de proxy +""" +SELF_REQUESTS_PROXIES = { "http": None, "https": None} + +""" +# Autorise la requête sur l'application en elle même sans vérifier le certificat SSL +""" +ALLOW_INSECURE_REQUESTS = False + +""" +# Chemin d'accès web (public) au répertoire de dépot des enregistrements (DEFAULT_RECORDER_PATH). Attention à penser à modifier la conf de NGINX +""" +PUBLIC_RECORD_DIR = "records" + +""" +# Si True, affiche l'icone de prévisualisation des vidéos dans la page "Revendiquer un enregistrement" +""" +USE_RECORD_PREVIEW = False + +# Configuration application podfile (gestion de fichier) + +""" +# Extensions autorisées pour les documents téléversés dans le gestionnaire de fichier +""" +FILE_ALLOWED_EXTENSIONS = ( 'doc', 'docx', 'odt', 'pdf', 'xls', 'xlsx', 'ods', 'ppt', 'pptx', 'txt', 'html', 'htm', 'vtt', 'srt', 'webm', 'ts', ) + +""" +# Extensions autorisées pour les images téléversés dans le gestionnaire de fichier +""" +IMAGE_ALLOWED_EXTENSIONS = ( 'jpg', 'jpeg', 'bmp', 'png', 'gif', 'tiff', ) + +""" +# Poids maximum en Mo par fichier téléversé dans le gestionnaire de fichier +""" +FILE_MAX_UPLOAD_SIZE = 10 + +# Configuration application completion (contributeur, sous-titre, document à télécharger, superposition) + +""" +# Liste de rôle possible pour un contributeur +""" +ROLE_CHOICES = ( + ('actor', ('actor')), + ('author', ('author')), + ('designer', ('designer')), + ('consultant', ('consultant')), + ('contributor', ('contributor')), + ('editor', ('editor')), + ('speaker', ('speaker')), + ('soundman', ('soundman')), + ('director', ('director')), + ('writer', ('writer')), + ('technician', ('technician')), + ('voice-over', ('voice-over')), +) + +""" +# Liste de type de piste possible pour une vidéo (sous-titre, légende etc.) +""" +KIND_CHOICES = ( ('subtitles', ('subtitles')), ('captions', ('captions')), ) + +# Configuration application authentification (Local, CAS et LDAP) + +""" +# Type d'authentification possible sur votre instance. Pour l'instant local ou cas +""" +AUTH_TYPE = (('local', _('local')), ('CAS', 'CAS')) + +""" +# Activation de l'authentification CAS en plus de l'authentification locale +""" +USE_CAS = False + +""" +# Url du serveur cas de l'établissement. Format http://url_cas +""" +CAS_SERVER_URL = "sso_cas" + +""" +# Si True, authentifie automatiquement l'individu si déjà authentifié sur le serveur CAS +""" +CAS_GATEWAY = False + +""" +# Si utilisation de la connection CAS, renseigne les champs du compte de la personne depuis une source externe. +# Valeur possible : None (pas de renseignement), CAS (renseigne les champs de la personne depuis les informations renvoyées par le CAS), +# LDAP (Interroge le serveur LDAP pour renseigner les champs du compte de la personne) +""" +POPULATE_USER = None + +""" +# variable utilisée pour trouver les informations de l'individu connecté dans le fichier renvoyé par le CAS lors de l'authentification +""" +AUTH_CAS_USER_SEARCH = "user" + +""" +# liste de correspondance entre les champs d'un compte de Pod et les champs renvoyés par le CAS +""" +USER_CAS_MAPPING_ATTRIBUTES = { + "uid": "uid", + "mail": "mail", + "last_name": "sn", + "first_name": "givenname", + "affiliation": "eduPersonAffiliation" +} + +""" +# Si True, des groupes sont créés automatiquement à partir des affiliations des individus qui se connectent sur la plateforme et +# l'individu qui se connecte est ajouté automatiquement à ses groupes +""" +CREATE_GROUP_FROM_AFFILIATION = False + +""" +# Les personnes ayant pour affiliation les valeurs renseignées dans cette variable sont automatiquement la valeur staff de leur compte à True +""" +AFFILIATION_STAFF = ('faculty', 'employee', 'staff') + +""" +# Valeurs possibles pour l'Affiliation du compte +""" +AFFILIATION = ( + ('student', ('student')), + ('faculty', ('faculty')), + ('staff', ('staff')), + ('employee', ('employee')), + ('member', ('member')), + ('affiliate', ('affiliate')), + ('alum', ('alum')), + ('library-walk-in', ('library-walk-in')), + ('researcher', ('researcher')), + ('retired', ('retired')), + ('emeritus', ('emeritus')), + ('teacher', ('teacher')), + ('registered-reader', _('registered-reader')) +) + +""" +# Information de connection au serveur LDAP. Le champ url peut contenir un ou plusieurs url pour ajouter des hôtes de référence, exemple : +# Si un seul host : {'url': "ldap.univ.fr'', 'port': 389, 'use_ssl': False} +# Si plusieurs : {'url': ("ldap.univ.fr'',"ldap2.univ.fr"), 'port': 389, 'use_ssl': False} +""" +LDAP_SERVER = {'url': '', 'port': 389, 'use_ssl': False} + +""" +# Identifiant (DN) du compte pour se connecter au serveur LDAP +""" +AUTH_LDAP_BIND_DN = "" + +""" +# Mot de passe du compte pour se connecter au serveur LDAP +""" +AUTH_LDAP_BIND_PASSWORD = "" + +""" +# Filtre LDAP permettant la recherche de l'individu dans le serveur LDAP +""" +AUTH_LDAP_USER_SEARCH = ('ou=people,dc=univ,dc=fr', "(uid=%(uid)s)") + +""" +# liste de correspondance entre les champs d'un compte de Pod et les champs renvoyés par le LDAP +""" +USER_LDAP_MAPPING_ATTRIBUTES = { + "uid": "uid", + "mail": "mail", + "last_name": "sn", + "first_name": "givenname", + "primaryAffiliation": "eduPersonPrimaryAffiliation", + "affiliations": "eduPersonAffiliation" +} + +""" +# Utiliser l'authentification Shibboleth +""" +USE_SHIB = False + +""" +# Nom de la fédération d'identité utilisée +""" +SHIB_NAME = "" + +""" +# Mapping des attributs entre shibboleth et la classe utilisateur +""" +SHIBBOLETH_ATTRIBUTE_MAP = { + "shib-user": (True, "username"), + "shib-given-name": (True, "first_name"), + "shib-sn": (True, "last_name"), + "shib-mail": (False, "email"), +} + +""" +# Nom de l'attribut dans les headers qui sert à identifier l'utilisateur connecté avec Shibboleth +""" +REMOTE_USER_HEADER = REMOTE_USER + +""" +# URL de connexion à votre instance Shibboleth +""" + +SHIB_URL = "" + +""" +# URL de déconnexion à votre instance Shibboleth +""" +SHIB_LOGOUT_URL = "" + +""" +# Forcer le passage en minuscule du nom d'utilisateur CAS (permet de prévenir des doubles créations de comptes dans certain cas). +""" +CAS_FORCE_LOWERCASE_USERNAME = False + +""" +# Permettre l'usage du oembed, partage dans moodle, facebook, twitter etc. +""" +OEMBED = True + +""" +# Activer les commentaires au niveau de la plateforme +""" +ACTIVE_VIDEO_COMMENT = True + +""" +# Transcription +""" +USE_TRANSCRIPTION = True + +""" +# decoupage de l'audio pour la transcription +""" +AUDIO_SPLIT_TIME = 300 + +""" +# paramétrage de deepspeech pour le modele +""" +DS_PARAM = { + 'fr': { + # le modèle deepspeech + 'model': "/usr/local/django_projects/transcription/model_tensorflow_fr/output_graph.pbmm", + 'scorer': "/usr/local/django_projects/transcription/model_tensorflow_fr/kenlm.scorer", + + } +} + +""" +# La liste des utilisateurs regardant le direct sera réservée au staff +""" +VIEWERS_ONLY_FOR_STAFF = False + +""" +# Temps (en seconde) entre deux envois d'un signal au serveur, pour signaler la présence sur un live +# Peut être augmenté en cas de perte de performance mais au détriment de la qualité du comptage des valeurs +""" +HEARTBEAT_DELAY = 45 + +""" +# Délai (en seconde) selon lequel une vue est considérée comme expirée si elle n'as pas renvoyé de signal depuis +""" +VIEW_EXPIRATION_DELAY = 60 + +""" +# Utilisation de BigBlueButton +""" +USE_BBB = True + +""" +# Répertoire du plugin bbb-recorder (voir documentation https://github.com/jibon57/bbb-recorder) +# bbb-recorder doit être installé dans ce répertoire, sur tous les serveurs d'encodage +# bbb-recorder crée un répertoire 'homedir' / Downloads qui a besoin d'espace disque +""" +DEFAULT_BBB_PLUGIN = '/home/podtest/bbb-recorder/' + +""" +# Répertoire qui contiendra les fichiers vidéo générés par bbb-recorder +""" +DEFAULT_BBB_PATH = '/data/www/podtest/bbb-recorder/' + +""" +# URL du serveur BigBlueButton ou Scalelite, où se trouvent les présentations Web BBB et l'API +""" +BBB_SERVER_URL = 'https://bbb.univ.fr/' + +""" +# Clé BigBlueButton ou Scalelite LOADBALANCER_SECRET +""" +BBB_SECRET_KEY = 'abcdef' + +""" +# Type de vidéo générée par défaut +""" +DEFAULT_BBB_TYPE_ID = 1 + +""" +# Format du nom d'utilisateur dans BBB. +# Valeurs possibles : 'first_name last_name', 'last_name first_name' +# Exemple1 : si dans le serveur BBB, vous avez un utilisateur au format 'John Doe', mettez 'first_name last_name' +# Exemple2 : si dans le serveur BBB, vous avez un utilisateur au format 'Doe John', mettez 'last_name first_name' +""" +BBB_USERNAME_FORMAT = 'first_name last_name' + +""" +# Nombre de jours avant la suppression des sessions et des utilisateurs associés, non déjà publiés +# Pour ne pas supprimer les anciennes sessions, utiliser la valeur 0 +""" +BBB_NUMBER_DAYS_BEFORE_DELETE = 0 + +""" +# +# Permet d'activer le fonctionnement de categorie au niveau de ses vidéos +# Vous pouvez créer des catégories pour pouvoir ranger vos propres vidéos +# Les catégories sont liées à l'utilisateur +""" +USER_VIDEO_CATEGORY = False + +""" +# gestion de l'oboslesence des vidéos +""" +USE_OBSOLESCENCE = True + +WARN_DEADLINES = [60, 30, 7] + +POD_ARCHIVE_AFFILIATION = ['faculty', + 'staff', + 'employee', + 'affiliate', + 'alum', + 'library-walk-in', + 'researcher', + 'retired', + 'emeritus', + 'teacher', + 'registered-reader', + 'member'] + +ACCOMMODATION_YEARS = { + 'affiliate': 1 +} +DEFAULT_YEAR_DATE_DELETE = 2 + +""" +# Configuration / Activtion du LTI +# voir pod/main/settings.py L.224 +""" +LTI_ENABLED = True # False par défaut + +""" +# Configuration H5P +# les videos interactive ne sont plus maintenu dans Pod +""" +H5P_ENABLED = True # False +H5P_DEV_MODE = 0 +H5P_URL = '/h5p/' +H5P_SAVE = 30 +H5P_EXPORT = '/exports/' +H5P_LANGUAGE = 'fr' +BASE_URL = 'https://pod.univ.fr/' + diff --git a/pod/custom/tests.py b/pod/custom/tests.py index a79ca8be56..e9137c85ea 100644 --- a/pod/custom/tests.py +++ b/pod/custom/tests.py @@ -1,3 +1,3 @@ -# from django.test import TestCase - -# Create your tests here. +# from django.test import TestCase + +# Create your tests here. diff --git a/pod/custom/urls.py b/pod/custom/urls.py index 8a62d709b9..90f371b68c 100644 --- a/pod/custom/urls.py +++ b/pod/custom/urls.py @@ -1,8 +1,8 @@ -# from django.conf.urls import url - -# from . import views - -app_name = "custom" - -urlpatterns = [ -] +# from django.conf.urls import url + +# from . import views + +app_name = "custom" + +urlpatterns = [ +] diff --git a/pod/custom/views.py b/pod/custom/views.py index fd0e044955..2ecb8171f3 100644 --- a/pod/custom/views.py +++ b/pod/custom/views.py @@ -1,3 +1,3 @@ -# from django.shortcuts import render - -# Create your views here. +# from django.shortcuts import render + +# Create your views here. diff --git a/requirements.txt b/requirements.txt index fc516cbe2f..0865d4251f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,7 +8,7 @@ django-cas-client==1.5.2 ldap3==2.5 django-progressbarupload==0.1.7 django-simple-captcha==0.5.12 -urllib3==1.25.9 +urllib3==1.26.2 elasticsearch==6.3.1 djangorestframework==3.9.1 django-filter==1.1.0 @@ -24,3 +24,5 @@ webrtcvad django-select2-forms==2.1.0 django-shibboleth-remoteuser django-chunked-upload==2.0.0 +requests==2.25.1 +chardet==4.0.0 \ No newline at end of file From e0006b7e5ed6daf0d3c1dcc01e0cce2f96762b93 Mon Sep 17 00:00:00 2001 From: ptitloup Date: Thu, 17 Dec 2020 17:59:43 +0100 Subject: [PATCH 007/145] add remote command to specify wich command to launch and just specify filename for video input argument to remote command --- pod/custom/settings_local.py.example | 3 +++ pod/video/remote_encode.py | 7 +++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/pod/custom/settings_local.py.example b/pod/custom/settings_local.py.example index 2b23e2034b..b652b4dc0c 100644 --- a/pod/custom/settings_local.py.example +++ b/pod/custom/settings_local.py.example @@ -562,6 +562,9 @@ ENCODE_VIDEO = "start_encode" # SSH_REMOTE_USER = "" # SSH_REMOTE_HOST = "xxx.univ.fr" # SSH_REMOTE_KEY = "/home/pod/.ssh/id_rsa" +# Remote command to launch, it will be call with this arguments : +# -n encoding- -i -v -u -d +# SSH_REMOTE_CMD = "" """ diff --git a/pod/video/remote_encode.py b/pod/video/remote_encode.py index 7b73d10978..a81c777acf 100644 --- a/pod/video/remote_encode.py +++ b/pod/video/remote_encode.py @@ -44,6 +44,8 @@ settings, 'SSH_REMOTE_HOST', "") SSH_REMOTE_KEY = getattr( settings, 'SSH_REMOTE_KEY', "") +SSH_REMOTE_CMD = getattr( + settings, 'SSH_REMOTE_CMD', "") # ########################################################################## @@ -91,11 +93,12 @@ def remote_encode_video(video_id): f.write("%s\n" % start) # launch remote encoding - cmd = "./pod-encoding/submit.sh \ + cmd = "{remote_cmd} \ -n encoding-{video_id} -i {video_input} \ -v {video_id} -u {user_hashkey} -d {debug}".format( + remote_cmd=SSH_REMOTE_CMD, video_id=video_id, - video_input=video_to_encode.video, + video_input=os.path.basename(video_to_encode.video.name), user_hashkey=video_to_encode.owner.owner.hashkey, debug=DEBUG ) From 517cb35330d5acdf76f5962568eef76a4a99eaaa Mon Sep 17 00:00:00 2001 From: Ptitloup Date: Thu, 17 Dec 2020 18:05:33 +0100 Subject: [PATCH 008/145] Update requirements.txt Downgrade Pillow to run with Python3.5 --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 0865d4251f..37244e9f81 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ Django==1.11.29 django-ckeditor==5.3.1 -Pillow==8.0.1 +Pillow==7.2.0 django-tagging==0.4.6 django-modeltranslation==0.12.2 webvtt-py==0.4.5 @@ -25,4 +25,4 @@ django-select2-forms==2.1.0 django-shibboleth-remoteuser django-chunked-upload==2.0.0 requests==2.25.1 -chardet==4.0.0 \ No newline at end of file +chardet==4.0.0 From c1758825b99061aef364963c350f449f6d80cc3a Mon Sep 17 00:00:00 2001 From: ptitloup Date: Thu, 17 Dec 2020 18:45:05 +0100 Subject: [PATCH 009/145] create remote_transcript.py --- pod/video/remote_transcript.py | 123 +++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 pod/video/remote_transcript.py diff --git a/pod/video/remote_transcript.py b/pod/video/remote_transcript.py new file mode 100644 index 0000000000..3917f01db5 --- /dev/null +++ b/pod/video/remote_transcript.py @@ -0,0 +1,123 @@ +from django.conf import settings +from django.core.files import File + +import time +import logging +import threading +import subprocess +import shlex +import os +import json +import re + +from .models import Video + +from .utils import change_encoding_step, add_encoding_log, check_file +from .utils import create_outputdir, send_email, send_email_encoding + +if getattr(settings, 'USE_PODFILE', False): + FILEPICKER = True + from pod.podfile.models import CustomImageModel + from pod.podfile.models import UserFolder +else: + FILEPICKER = False + from pod.main.models import CustomImageModel + +log = logging.getLogger(__name__) + +DEBUG = getattr(settings, 'DEBUG', True) + +SSH_TRANSCRIPT_REMOTE_USER = getattr( + settings, 'SSH_TRANSCRIPT_REMOTE_USER', "") +SSH_TRANSCRIPT_REMOTE_HOST = getattr( + settings, 'SSH_TRANSCRIPT_REMOTE_HOST', "") +SSH_TRANSCRIPT_REMOTE_KEY = getattr( + settings, 'SSH_TRANSCRIPT_REMOTE_KEY', "") +SSH_TRANSCRIPT_REMOTE_CMD = getattr( + settings, 'SSH_TRANSCRIPT_REMOTE_CMD', "") + + +# ########################################################################## +# REMOTE TRANSCRIPT VIDEO : MAIN FUNCTION +# ########################################################################## + + +def remote_transcript_video(video_id): + start = "Start transcript at : %s" % time.ctime() + msg = "" + change_encoding_step( + video_id, 5, + "transcripting audio") + + video_to_encode = Video.objects.get(id=video_id) + + encoding_log, created = EncodingLog.objects.get_or_create( + video=video_to_encode) + encoding_log.log = "%s\n%s" % (encoding_log.log, start) + encoding_log.save() + + mp3file = video_to_encode.get_video_mp3( + ).source_file if video_to_encode.get_video_mp3() else None + + if mp3file and check_file(mp3file.path): + + # launch remote encoding + cmd = "{remote_cmd} \ + -n encoding-{video_id} -i {video_input} \ + -v {video_id} -u {user_hashkey} -d {debug}".format( + remote_cmd=SSH_TRANSCRIPT_REMOTE_CMD, + video_id=video_id, + video_input=os.path.basename(video_to_encode.video.name), + user_hashkey=video_to_encode.owner.owner.hashkey, + debug=DEBUG + ) + + key = " -i %s " % SSH_TRANSCRIPT_REMOTE_KEY if SSH_TRANSCRIPT_REMOTE_KEY != "" else "" + + remote_cmd = "ssh {key} {user}@{host} \"{cmd}\"".format( + key=key, user=SSH_TRANSCRIPT_REMOTE_USER, host=SSH_TRANSCRIPT_REMOTE_HOST, cmd=cmd) + + # launch remote encode + change_encoding_step(video_id, 5, "process remote transcipt") + add_encoding_log(video_id, "process remote transcipt : %s" % remote_cmd) + process_cmd(remote_cmd, video_id) + + else: + msg += "Wrong mp3 file or path : "\ + + "\n%s" % mp3file.path + add_encoding_log(video_id, msg) + change_encoding_step(video_id, -1, msg) + send_email(msg, video_id) + + +def process_cmd(remote_cmd, video_id): + msg = "" + try: + output = subprocess.run( + shlex.split(remote_cmd), stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + add_encoding_log(video_id, "slurm output : %s" % output.stdout) + if DEBUG: + print(output.stdout) + if output.returncode != 0: + msg += 20*"////" + "\n" + msg += "ERROR RETURN CODE: {0}\n".format(output.returncode) + msg += output + add_encoding_log(video_id, msg) + change_encoding_step(video_id, -1, msg) + send_email(msg, video_id) + except subprocess.CalledProcessError as e: + # raise RuntimeError('ffprobe returned non-zero status: {}'.format( + # e.stderr)) + msg += 20*"////" + "\n" + msg += "Runtime Error: {0}\n".format(e) + add_encoding_log(video_id, msg) + change_encoding_step(video_id, -1, msg) + send_email(msg, video_id) + except OSError as err: + # raise OSError(e.errno, 'ffprobe not found: {}'.format(e.strerror) + msg += 20*"////" + "\n" + msg += "OS error: {0}\n".format(err) + add_encoding_log(video_id, msg) + change_encoding_step(video_id, -1, msg) + send_email(msg, video_id) \ No newline at end of file From cad152b120927316242d1b269aef0c2ac58882d3 Mon Sep 17 00:00:00 2001 From: ptitloup Date: Thu, 17 Dec 2020 18:53:33 +0100 Subject: [PATCH 010/145] add remote transcript settings, fix flake8 in remote transcript module --- pod/custom/settings_local.py.example | 10 ++++++++ pod/video/remote_transcript.py | 36 +++++++++++----------------- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/pod/custom/settings_local.py.example b/pod/custom/settings_local.py.example index b652b4dc0c..b1cb0f4733 100644 --- a/pod/custom/settings_local.py.example +++ b/pod/custom/settings_local.py.example @@ -566,6 +566,16 @@ ENCODE_VIDEO = "start_encode" # -n encoding- -i -v -u -d # SSH_REMOTE_CMD = "" """ +""" +# pourl'encodage distant, il faut préciser le login, l'host et la clé pour appeler cet encodage +# REMOTE ENCODING SETTINGS +# SSH_TRANSCRIPT_REMOTE_USER = "pod" +# SSH_TRANSCRIPT_REMOTE_HOST = "xxx.univ.fr" +# SSH_TRANSCRIPT_REMOTE_KEY = "/home/pod/.ssh/id_rsa" +# Remote command to launch, it will be call with this arguments : +# -n transcoding- -i -v -u -d +# SSH_TRANSCRIPT_REMOTE_CMD = "" +""" """ diff --git a/pod/video/remote_transcript.py b/pod/video/remote_transcript.py index 3917f01db5..c53ac98249 100644 --- a/pod/video/remote_transcript.py +++ b/pod/video/remote_transcript.py @@ -1,27 +1,15 @@ from django.conf import settings -from django.core.files import File import time import logging -import threading import subprocess import shlex import os -import json -import re -from .models import Video +from .models import Video, EncodingLog from .utils import change_encoding_step, add_encoding_log, check_file -from .utils import create_outputdir, send_email, send_email_encoding - -if getattr(settings, 'USE_PODFILE', False): - FILEPICKER = True - from pod.podfile.models import CustomImageModel - from pod.podfile.models import UserFolder -else: - FILEPICKER = False - from pod.main.models import CustomImageModel +from .utils import send_email log = logging.getLogger(__name__) @@ -36,7 +24,6 @@ SSH_TRANSCRIPT_REMOTE_CMD = getattr( settings, 'SSH_TRANSCRIPT_REMOTE_CMD', "") - # ########################################################################## # REMOTE TRANSCRIPT VIDEO : MAIN FUNCTION # ########################################################################## @@ -57,13 +44,13 @@ def remote_transcript_video(video_id): encoding_log.save() mp3file = video_to_encode.get_video_mp3( - ).source_file if video_to_encode.get_video_mp3() else None + ).source_file if video_to_encode.get_video_mp3() else None if mp3file and check_file(mp3file.path): - # launch remote encoding + # launch remote transcoding cmd = "{remote_cmd} \ - -n encoding-{video_id} -i {video_input} \ + -n transcoding-{video_id} -i {video_input} \ -v {video_id} -u {user_hashkey} -d {debug}".format( remote_cmd=SSH_TRANSCRIPT_REMOTE_CMD, video_id=video_id, @@ -72,14 +59,19 @@ def remote_transcript_video(video_id): debug=DEBUG ) - key = " -i %s " % SSH_TRANSCRIPT_REMOTE_KEY if SSH_TRANSCRIPT_REMOTE_KEY != "" else "" + key = " -i %s " % SSH_TRANSCRIPT_REMOTE_KEY if ( + SSH_TRANSCRIPT_REMOTE_KEY != "") else "" remote_cmd = "ssh {key} {user}@{host} \"{cmd}\"".format( - key=key, user=SSH_TRANSCRIPT_REMOTE_USER, host=SSH_TRANSCRIPT_REMOTE_HOST, cmd=cmd) + key=key, + user=SSH_TRANSCRIPT_REMOTE_USER, + host=SSH_TRANSCRIPT_REMOTE_HOST, + cmd=cmd) # launch remote encode change_encoding_step(video_id, 5, "process remote transcipt") - add_encoding_log(video_id, "process remote transcipt : %s" % remote_cmd) + add_encoding_log( + video_id, "process remote transcipt : %s" % remote_cmd) process_cmd(remote_cmd, video_id) else: @@ -120,4 +112,4 @@ def process_cmd(remote_cmd, video_id): msg += "OS error: {0}\n".format(err) add_encoding_log(video_id, msg) change_encoding_step(video_id, -1, msg) - send_email(msg, video_id) \ No newline at end of file + send_email(msg, video_id) From 865c03fe55e960690dbed05809d152d81b2b460c Mon Sep 17 00:00:00 2001 From: ptitloup Date: Mon, 21 Dec 2020 14:58:14 +0100 Subject: [PATCH 011/145] create import transcript command --- .../commands/import_transcripted_video.py | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100755 pod/video/management/commands/import_transcripted_video.py diff --git a/pod/video/management/commands/import_transcripted_video.py b/pod/video/management/commands/import_transcripted_video.py new file mode 100755 index 0000000000..fde4cf579e --- /dev/null +++ b/pod/video/management/commands/import_transcripted_video.py @@ -0,0 +1,38 @@ +from django.conf import settings + +from django.utils import translation +from django.core.management.base import BaseCommand +from django.core.exceptions import ObjectDoesNotExist + +from pod.video.models import Video +from pod.video.remote_transcript import store_remote_transcripting_video + + +LANGUAGE_CODE = getattr(settings, "LANGUAGE_CODE", 'fr') + + +class Command(BaseCommand): + # args = 'video_id' + help = 'Import recorded video into Pod' + + def add_arguments(self, parser): + parser.add_argument( + 'video_id', + type=int, + help='Indicates the id of the video to import encoded files' + ) + + def handle(self, *args, **options): + # Activate a fixed locale fr + translation.activate(LANGUAGE_CODE) + video_id = options['video_id'] + try: + video = Video.objects.get(id=video_id) + store_remote_transcripting_video(video_id) + self.stdout.write(self.style.SUCCESS( + 'Successfully import transcripted video "%s"' % (video))) + except ObjectDoesNotExist: + self.stdout.write(self.style.ERROR( + "******* Video id matching query does not exist: %s *******" + % video_id + )) From d52ed7980432611531667dc0e83b03e81fdea8e6 Mon Sep 17 00:00:00 2001 From: ptitloup Date: Mon, 21 Dec 2020 15:07:05 +0100 Subject: [PATCH 012/145] imrpove remote transcrip, add store remote transcripting function --- pod/video/remote_transcript.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/pod/video/remote_transcript.py b/pod/video/remote_transcript.py index c53ac98249..769d7e9757 100644 --- a/pod/video/remote_transcript.py +++ b/pod/video/remote_transcript.py @@ -1,6 +1,7 @@ from django.conf import settings import time +import json import logging import subprocess import shlex @@ -9,7 +10,7 @@ from .models import Video, EncodingLog from .utils import change_encoding_step, add_encoding_log, check_file -from .utils import send_email +from .utils import send_email, create_outputdir log = logging.getLogger(__name__) @@ -113,3 +114,26 @@ def process_cmd(remote_cmd, video_id): add_encoding_log(video_id, msg) change_encoding_step(video_id, -1, msg) send_email(msg, video_id) + + +def store_remote_transcripting_video(video_id): + # + msg = "" + video_to_encode = Video.objects.get(id=video_id) + output_dir = create_outputdir(video_id, video_to_encode.video.path) + info_video = {} + + if check_file(output_dir + "/transcript.json"): + with open(output_dir + "/transcript.json") as json_file: + info_video = json.load(json_file) + + if DEBUG: + print(output_dir) + print(json.dumps(info_video, indent=2)) + + else: + msg += "Wrong file or path : "\ + + "\n%s" % video_to_encode.video.path + add_encoding_log(video_id, msg) + change_encoding_step(video_id, -1, msg) + send_email(msg, video_id) From 3f617abfafe520fec1468d2725c3849b6348ca56 Mon Sep 17 00:00:00 2001 From: ptitloup Date: Mon, 21 Dec 2020 17:23:24 +0100 Subject: [PATCH 013/145] add thread to launch remote transcript, work on store remote transcripting by convert json to webvtt --- pod/video/remote_transcript.py | 44 +++++++++++++++++++++++++++++++++- pod/video/transcript.py | 14 +++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/pod/video/remote_transcript.py b/pod/video/remote_transcript.py index 769d7e9757..d9e082ce12 100644 --- a/pod/video/remote_transcript.py +++ b/pod/video/remote_transcript.py @@ -6,12 +6,16 @@ import subprocess import shlex import os +import datetime as dt +from datetime import timedelta from .models import Video, EncodingLog from .utils import change_encoding_step, add_encoding_log, check_file from .utils import send_email, create_outputdir +from webvtt import WebVTT, Caption + log = logging.getLogger(__name__) DEBUG = getattr(settings, 'DEBUG', True) @@ -25,6 +29,8 @@ SSH_TRANSCRIPT_REMOTE_CMD = getattr( settings, 'SSH_TRANSCRIPT_REMOTE_CMD', "") +SENTENCE_MAX_LENGTH = getattr(settings, 'SENTENCE_MAX_LENGTH', 3) + # ########################################################################## # REMOTE TRANSCRIPT VIDEO : MAIN FUNCTION # ########################################################################## @@ -55,7 +61,7 @@ def remote_transcript_video(video_id): -v {video_id} -u {user_hashkey} -d {debug}".format( remote_cmd=SSH_TRANSCRIPT_REMOTE_CMD, video_id=video_id, - video_input=os.path.basename(video_to_encode.video.name), + video_input=os.path.basename(mp3file.name), user_hashkey=video_to_encode.owner.owner.hashkey, debug=DEBUG ) @@ -131,9 +137,45 @@ def store_remote_transcripting_video(video_id): print(output_dir) print(json.dumps(info_video, indent=2)) + webvtt = WebVTT() + + words = info_video["transcripts"][0]["words"] + """ + for transcript in info_video["transcripts"]: + for word in transcript["words"]: + words.append(word) + """ + text_caption = [] + start_caption = None + duration = 0 + for word in words: + text_caption.append(word['word']) + if start_caption is None: + start_caption = word['start_time'] + if duration + word['duration'] > SENTENCE_MAX_LENGTH: + caption = Caption( + format_time_caption(start_caption), + format_time_caption( + start_caption + duration + word['duration']), + " ".join(text_caption) + ) + webvtt.captions.append(caption) + text_caption = [] + start_caption = None + duration = 0 + else: + duration += word['duration'] + print(webvtt) + else: msg += "Wrong file or path : "\ + "\n%s" % video_to_encode.video.path add_encoding_log(video_id, msg) change_encoding_step(video_id, -1, msg) send_email(msg, video_id) + + +def format_time_caption(time_caption): + return (dt.datetime.utcfromtimestamp(0) + + timedelta(seconds=float(time_caption)) + ).strftime('%H:%M:%S.%f')[:-3] diff --git a/pod/video/transcript.py b/pod/video/transcript.py index 7da9d8459f..042a781308 100644 --- a/pod/video/transcript.py +++ b/pod/video/transcript.py @@ -67,6 +67,20 @@ print(webvtt) """ +# ########################################################################## +# ENCODE VIDEO: THREAD TO LAUNCH ENCODE +# ########################################################################## + + +def start_remote_transcript(video_id): + # load module here to prevent circular import + from .remote_transcript import remote_transcript_video + log.info("START ENCODE VIDEO ID %s" % video_id) + t = threading.Thread(target=remote_transcript_video, + args=[video_id]) + t.setDaemon(True) + t.start() + def start_transcript(video_id): log.info("START TRANSCRIPT VIDEO %s" % video_id) From 49ceacd82f171c4c6ffa41ce3d76af7e2d22d132 Mon Sep 17 00:00:00 2001 From: Eliam LOTONGA Date: Wed, 6 Jan 2021 14:24:29 +0100 Subject: [PATCH 014/145] Fix: bug if domain name contains 'video' word --- pod/video/static/js/comment-script.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pod/video/static/js/comment-script.js b/pod/video/static/js/comment-script.js index 4a2e6f62f8..a98c074449 100644 --- a/pod/video/static/js/comment-script.js +++ b/pod/video/static/js/comment-script.js @@ -1,4 +1,4 @@ -let base_url = window.location.href.replace("video", "comment"); +let base_url = window.location.href.replace("/video/", "/comment/"); let base_vote_url = base_url.replace('comment', 'comment/vote'); let base_delete_url = base_url.replace('comment', 'comment/del'); let all_comment = null; From 88b843ac4da468260b522e8a2a31bbec93cdf5f0 Mon Sep 17 00:00:00 2001 From: Eliam LOTONGA Date: Thu, 7 Jan 2021 00:37:36 +0100 Subject: [PATCH 015/145] Add CookieSamesite middleware --- pod/settings.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pod/settings.py b/pod/settings.py index e004eba06c..d991bb4da2 100644 --- a/pod/settings.py +++ b/pod/settings.py @@ -67,6 +67,8 @@ 'django.middleware.locale.LocaleMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', + # Django 3.1 starts to support SameSite middleware + 'django_cookies_samesite.middleware.CookiesSameSite', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', From 4248140a713796372c1a830bf04a8bc8515edf6a Mon Sep 17 00:00:00 2001 From: Eliam LOTONGA Date: Thu, 7 Jan 2021 00:38:33 +0100 Subject: [PATCH 016/145] Add dependence django-cookies-samesite==0.8.0 --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 37244e9f81..db442a6c18 100644 --- a/requirements.txt +++ b/requirements.txt @@ -26,3 +26,4 @@ django-shibboleth-remoteuser django-chunked-upload==2.0.0 requests==2.25.1 chardet==4.0.0 +django-cookies-samesite==0.8.0 From 92383e8c66cb9c82f0d76460e829d6a69a7afc87 Mon Sep 17 00:00:00 2001 From: Eliam LOTONGA Date: Thu, 7 Jan 2021 09:56:34 +0100 Subject: [PATCH 017/145] Add Samesite settings variable --- pod/custom/settings_local.py.example | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pod/custom/settings_local.py.example b/pod/custom/settings_local.py.example index b652b4dc0c..9cc8919edd 100644 --- a/pod/custom/settings_local.py.example +++ b/pod/custom/settings_local.py.example @@ -1014,3 +1014,8 @@ H5P_EXPORT = '/exports/' H5P_LANGUAGE = 'fr' BASE_URL = 'https://pod.univ.fr/' +""" +#Vous pouvez définir le drapeau SameSite sur tous les cookies +#(même sur ceux provenant d'applications Django tierces) +""" +SESSION_COOKIE_SAMESITE_FORCE_ALL = True From 93153a6d897a6f6ce265dc007298c3a2820c5bbd Mon Sep 17 00:00:00 2001 From: Eliam LOTONGA Date: Thu, 7 Jan 2021 09:58:36 +0100 Subject: [PATCH 018/145] Typo --- pod/custom/settings_local.py.example | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pod/custom/settings_local.py.example b/pod/custom/settings_local.py.example index 9cc8919edd..4b5c8d1d3a 100644 --- a/pod/custom/settings_local.py.example +++ b/pod/custom/settings_local.py.example @@ -1015,7 +1015,7 @@ H5P_LANGUAGE = 'fr' BASE_URL = 'https://pod.univ.fr/' """ -#Vous pouvez définir le drapeau SameSite sur tous les cookies -#(même sur ceux provenant d'applications Django tierces) +# Vous pouvez définir le drapeau SameSite sur tous les cookies +# (même sur ceux provenant d'applications Django tierces) """ SESSION_COOKIE_SAMESITE_FORCE_ALL = True From 220dc8f956f1e301f404db78c361218da5e04491 Mon Sep 17 00:00:00 2001 From: Eliam LOTONGA Date: Thu, 7 Jan 2021 10:26:45 +0100 Subject: [PATCH 019/145] Add Samesite settings variable --- pod/custom/settings_local.py.example | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pod/custom/settings_local.py.example b/pod/custom/settings_local.py.example index 4b5c8d1d3a..058d91f347 100644 --- a/pod/custom/settings_local.py.example +++ b/pod/custom/settings_local.py.example @@ -1015,7 +1015,12 @@ H5P_LANGUAGE = 'fr' BASE_URL = 'https://pod.univ.fr/' """ -# Vous pouvez définir le drapeau SameSite sur tous les cookies +# Vous pouvez définir le drapeau Http cookie SameSite sur tous les cookies # (même sur ceux provenant d'applications Django tierces) """ SESSION_COOKIE_SAMESITE_FORCE_ALL = True +""" +# Vous pouvez définir le drapeau Http cookie SameSite à: +# 'Strict', 'Lax' ou 'None' +""" +SESSION_COOKIE_SAMESITE = 'Lax' From 0e8428dbcd518f71c90523c684f06aaf9f1c52e3 Mon Sep 17 00:00:00 2001 From: Eliam LOTONGA Date: Fri, 8 Jan 2021 18:45:05 +0100 Subject: [PATCH 020/145] fixed bug parent id --- pod/video/models.py | 42 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/pod/video/models.py b/pod/video/models.py index a5eb479b29..061cdd665c 100755 --- a/pod/video/models.py +++ b/pod/video/models.py @@ -34,7 +34,7 @@ from sorl.thumbnail import get_thumbnail from pod.main.models import get_nextautoincrement from pod.main.lang_settings import ALL_LANG_CHOICES, PREF_LANG_CHOICES -from django.db.models import Q +from django.db.models import Count, Q if getattr(settings, 'USE_PODFILE', False): from pod.podfile.models import CustomImageModel @@ -1470,9 +1470,47 @@ class Meta: verbose_name_plural = _("Comments") @property - def numberVote(self): + def number_vote(self): self.vote_set.all().count() + @property + def get_children(self): + all_c = Comment.objects.filter( + video=self.video + ).order_by('added').annotate( + nbr_vote=Count('vote') + ) + return list( + map( + lambda c: { + 'id': c.id, + 'parent__id': c.parent.id if c.parent else None, + 'top_parent__id': c.top_parent_comment.id, + 'author__id': c.author.id, + 'author__first_name': c.author.first_name, + 'author__last_name': c.author.last_name, + 'content': c.content, + 'added': c.added, + 'nbr_vote': c.nbr_vote}, + filter( + lambda c: ( + c.id != self.id and + c.top_parent_comment.id == self.id), + all_c + ) + ) + ) + + @property + def top_parent_comment(self): + parent_c = self + while parent_c: + if not parent_c.parent: + break + parent_c = parent_c.parent + + return parent_c + def __str__(self): return self.content From 70d18d1ac2c5d370317476864ae25ae27d2c8beb Mon Sep 17 00:00:00 2001 From: Eliam LOTONGA Date: Fri, 8 Jan 2021 18:45:48 +0100 Subject: [PATCH 021/145] Review code --- pod/video/views.py | 44 ++++++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/pod/video/views.py b/pod/video/views.py index 7499edfb3d..9214a44d50 100644 --- a/pod/video/views.py +++ b/pod/video/views.py @@ -1690,12 +1690,17 @@ def add_comment(request, video_slug, comment_id=None): if request.method == "POST": c_video = get_object_or_404(Video, slug=video_slug) c_user = request.user + c_top_parent = get_object_or_404( + Comment, id=request.POST.get('top_parent'))if( + request.POST.get('top_parent')) else None c_parent = get_object_or_404( Comment, id=comment_id)if comment_id else None c_content = request.POST.get('content', '') c_date = request.POST.get('date_added', None) if c_content: c = Comment() + if c_top_parent: + c.top_parent = c_top_parent if c_parent: c.parent = c_parent if c_date: @@ -1718,31 +1723,30 @@ def add_comment(request, video_slug, comment_id=None): def get_comments(request, video_slug): v = get_object_or_404(Video, slug=video_slug) - comments = Comment.objects.filter(video=v).order_by('added').annotate( - nbr_vote=Count('vote')) + # extract parent comments - p_c = comments.filter(parent=None).values( - 'id', - 'author__first_name', - 'author__last_name', - 'author__id', - 'content', - 'added', - 'nbr_vote') + p_c = Comment.objects.filter( + video=v, parent=None).order_by('added').annotate( + nbr_vote=Count('vote')) + # organize comments => parent with children comment_org = [] for c in p_c: - filter_data = comments.filter( - parent__id=c['id']).values( - 'id', - 'author__first_name', - 'author__last_name', - 'author__id', - 'content', - 'added', - 'nbr_vote') + children_comments = c.get_children + c = { + 'id': c.id, + 'parent__id': None, + 'top_parent__id': None, + 'author__first_name': c.author.first_name, + 'author__last_name': c.author.last_name, + 'author__id': c.author.id, + 'content': c.content, + 'added': c.added, + 'nbr_vote': c.nbr_vote + } comment_org.append({ - 'parent_comment': c, 'children': list(filter_data)}) + 'parent_comment': c, 'children': children_comments}) + return HttpResponse( json.dumps(comment_org, cls=DjangoJSONEncoder), content_type="application/json") From d19c6b6092c096ba43599b697c6e717b0337026e Mon Sep 17 00:00:00 2001 From: Eliam LOTONGA Date: Fri, 8 Jan 2021 18:46:27 +0100 Subject: [PATCH 022/145] tag comment on reply --- pod/video/static/js/comment-script.js | 193 +++++++++++++++++++------- 1 file changed, 142 insertions(+), 51 deletions(-) diff --git a/pod/video/static/js/comment-script.js b/pod/video/static/js/comment-script.js index 4a2e6f62f8..4eca0ae441 100644 --- a/pod/video/static/js/comment-script.js +++ b/pod/video/static/js/comment-script.js @@ -1,4 +1,4 @@ -let base_url = window.location.href.replace("video", "comment"); +let base_url = window.location.href.replace("/video/", "/comment/"); let base_vote_url = base_url.replace('comment', 'comment/vote'); let base_delete_url = base_url.replace('comment', 'comment/del'); let all_comment = null; @@ -83,7 +83,7 @@ class ConfirmModal extends HTMLElement e.preventDefault(); document.querySelector("#custom_element_confirm_modal .confirm_delete").classList.remove("show"); ACTION_COMMENT.comment_to_delete = null; - }) + }); this.innerHTML = modal; this.querySelector(".actions").appendChild(delete_btn); this.querySelector(".actions").appendChild(cancel_btn); @@ -121,22 +121,25 @@ class Comment extends HTMLElement comment.setAttribute("class", "comment"); let comment_container = document.createElement("DIV"); comment_container.setAttribute("class", "comment_container"); - let comment_content = ` -
-
-

${owner}

- -
-
-

${content}

-
- + let comment_content = document.createElement('div'); + comment_content.setAttribute('class', 'comment_content'); + comment_content.innerHTML = ` +
+

${owner}

+ +
+
+
+ `; - comment_container.innerHTML = comment_content; + comment_container.appendChild(comment_content); + if(is_parent) + comment_container.querySelector('.comment_content_body').innerHTML = content; + else + comment_container.querySelector('.comment_content_body').appendChild(content); if(user_id) { let svg_icon = ``; @@ -166,7 +169,7 @@ class Comment extends HTMLElement gettext("Delete"), id ); - delete_action.addEventListener("click", ()=> + delete_action.addEventListener("click", () => { // display confirm modal document.querySelector("#custom_element_confirm_modal .confirm_delete").classList.add("show"); @@ -195,19 +198,19 @@ class Comment extends HTMLElement child_container.classList.add("show"); if(this.value.trim() !== "") { - add_comment.parentElement.classList.toggle("show"); - add_child_comment( this ); let comment_parent = get_node(this, "comment_element", "comment_child" ); + add_comment.parentElement.classList.toggle("show"); if(!comment_parent.classList.contains("show")) comment_parent.classList.add("show") hide_show_child_comment_text(this) + add_child_comment( this, child_container, comment_parent ); this.value = ""; } } }); comment_container.querySelector(".comment_content_footer .form").appendChild(add_comment); } - if(is_parent)// child comment doesn't have an id + if(is_parent) { let comment_icon_svg = ``; let comment_icon = document.createElement("DIV"); @@ -274,11 +277,12 @@ function hide_or_add_show_children_btn(parent_comment=null) let children_container = parent_comment.querySelector(".comments_children_container"); if(!parent_comment.querySelector(".actions .comment_show_children_action") && children_container.childElementCount >0) { - if(!is_comment_owner || is_video_owner || is_superuser){ + if(is_comment_owner || is_video_owner || is_superuser){ parent_comment.querySelector(".comment_content_footer .actions").insertBefore( children_action, parent_comment.querySelector(".comment_content_footer .actions .comment_delete_action") ); } else{ + console.log("NOt owner , Not video owner, Not super user") parent_comment.querySelector(".comment_content_footer .actions").appendChild(children_action ); } } @@ -360,7 +364,7 @@ function vote(comment_id, target_html_el) /**************** Save comment into the server **************** ***************************************************************/ -function save_comment(content, date, parent_id=null) +function save_comment(content, date, parent_id=null, top_parent_id=null) { let post_url = base_url.replace("comment", "comment/add"); post_url = parent_id?post_url+parent_id+'/':post_url @@ -393,7 +397,7 @@ function save_comment(content, date, parent_id=null) else { all_comment = all_comment.map((comment)=>{ - if(comment.parent_comment.id === parent_id) + if(comment.parent_comment.id === top_parent_id) comment.children.push(c); return comment; }); @@ -434,6 +438,59 @@ function delete_comment(target_comment_html) }) } +/*********** Scroll to a specific comment ************* + * ****************************************************/ +function scrollToComment(targetComment) +{ + targetComment.scrollIntoView({ + behavior: "smooth", + block: "center", + inline: "nearest" + }); + let htmlTarget = targetComment.querySelector(".comment_content"); + if(htmlTarget.classList.contains("scroll_to")) + htmlTarget.classList.remove("scroll_to"); + + window.setTimeout(() => { + htmlTarget.classList.remove('scroll_to'); + }, 8000); + + htmlTarget.classList.add('scroll_to'); +} + + +/****** Add the owner of parent comment as a tag ****** + ******************************************************/ +function add_user_tag(comment_value, parent_comment) +{ + let reply_to = get_comment_attribute( + parent_comment, + attr="author__first_name" + ); + reply_content = get_comment_attribute( parent_comment, attr="content" ); + let htmlTarget = parent_comment.querySelector(".comment_content"); + let tag = document.createElement("a"); + tag.setAttribute("href", "#"); + tag.addEventListener("click", (e) => { + e.preventDefault(); + e.stopPropagation(); + scrollToComment(parent_comment) + }); + tag.innerHTML =` + + @${reply_to} + ${reply_content} + `; + let comment_text = document.createElement("span"); + comment_text.setAttribute('class', 'comment_text'); + comment_text.innerText = comment_value; + + let comment_content = document.createElement("p"); + comment_content.appendChild(tag); + comment_content.appendChild(comment_text); + return comment_content; +} + /**************** Add parent Comment **************** ******************************************************/ if(is_authenticated){ @@ -445,7 +502,7 @@ if(is_authenticated){ let el = add_parent_comment.querySelector(".new_parent_comment") if(el.value.trim() != "") { - let comment_content = encodeHTML(el.value); + let comment_content = el.value; let c = new Comment( owner=user_fullName, content=comment_content, @@ -463,74 +520,96 @@ if(is_authenticated){ } /**************** Add child Comment ***************** ******************************************************/ -function add_child_comment(el) +function add_child_comment(el, container_el, parent_comment) { let date_added = new Date(); if(el.value.trim() !== "") { - let parent_el = get_node(el, "comments_children_container"); - let comment_content = encodeHTML(el.value) + let child_direct_parent = document.querySelector( + `#${el.parentElement.parentElement.dataset.comment}`) + + let comment_child_content = add_user_tag( + el.value, + child_direct_parent, + ); let c = new Comment( owner=user_fullName, - content=comment_content, + content=comment_child_content, likes=0, added_since=date_added, id = `comment_${date_added.getTime()}`, is_parent=false, is_comment_owner=true); - parent_el.prepend(c); - hide_or_add_show_children_btn(parent_el.parentElement); + container_el.prepend(c); + hide_or_add_show_children_btn(container_el.parentElement); + // INSERT INTO DATABASE THE CURRENT COMMENT CHILD - let parent_com = get_node(parent_el, 'comment_element', 'comment_child'); - let p_id = get_comment_attribute(parent_com); - save_comment(comment_content, date_added.toISOString(), p_id); + let p_id = get_comment_attribute( child_direct_parent ); + let t_p_id = get_comment_attribute( + get_node(el, 'comment_element', 'comment_child') // get the top parent element + ) + + // Scroll to the comment child + if(window.scrollY > 1180) + scrollToComment( c ); + + save_comment(el.value, date_added.toISOString(), p_id, t_p_id); } } /********* Return backend comment attribute ********* ******************************************************/ -function get_comment_attribute(comment_html, attr='id') +function get_comment_attribute(comment_html, attr="id") { let curr_html_id = comment_html.getAttribute('id'); - let comment_attr = null; + let comment_attr = null all_comment.forEach(comment=> { let comment_htmlid = `comment_${new Date(comment.parent_comment.added).getTime()}`; if(comment_htmlid == curr_html_id) + { comment_attr = comment.parent_comment[attr]; + } comment.children.forEach(c_comment=> { let c_comment_htmlid = `comment_${new Date(c_comment.added).getTime()}`; if(c_comment_htmlid == curr_html_id) comment_attr = c_comment[attr]; }); - if(comment_attr) return comment_attr; }); return comment_attr; } -/****************** HTML encoder ******************** - ******************************************************/ -function encodeHTML(s) +function htmlContainsClass(html_el, classes) { - return s.replace(/&/g, '&').replace(/{ + if( !html_el.classList.contains(cls)) + ctn = false; + }); + return ctn; } -/**************** Get parentNode or sibling node **************** +/********** Get parentNode or sibling node ********** ******************************************************/ -function get_node(el, class_name=null, not=null) +function get_node(el, class_name, not) { - class_name = class_name?class_name :"comment_container"; - let selector = not !== null?`.${class_name}:not(.${not})`: `.${class_name}`; - let childElement = el.querySelector(selector); - if(el.classList.contains(class_name) && !el.classList.contains(not)) + class_name = class_name || "comment_container"; + not = not || ""; + let selector = !!not?`.${class_name}:not(.${not})`: `.${class_name}`; + let foundedElement = el.querySelector(selector); + if(htmlContainsClass(el, class_name) && !htmlContainsClass(el, not)) { return el; - else if(childElement) - return childElement; - return get_node(el.parentElement, class_name, not); + } + if(foundedElement){ + return foundedElement; + } + else{ + return get_node(el.parentElement, class_name, not); + } } -/**************** Manage hide/show child comment text **************** +/******* Manage hide/show child comment text ******** ******************************************************/ function hide_show_child_comment_text(el) { @@ -588,9 +667,21 @@ fetch(base_vote_url).then(response=>{ children_data.forEach(child_data=>{ date_added = new Date(child_data.added); html_id =`comment_${date_added.getTime().toString()}`; + let parent_to_scroll = parent_c; + if(child_data.parent__id !== child_data.top_parent__id) + { + let direct_parent_comment = children_data.filter((c_obj) => c_obj.id === child_data.parent__id )[0]; + let direct_parent_html_id = `comment_${new Date(direct_parent_comment.added).getTime().toString()}`; + parent_to_scroll = parent_c.querySelector(`#${direct_parent_html_id}`); + } + let comment_child_content = add_user_tag( + child_data.content, + parent_to_scroll, + comment_data.parent_comment.author__first_name + ); let child_c = new Comment( owner=`${child_data.author__first_name} ${child_data.author__last_name.toUpperCase()}`, - content=child_data.content, + content=comment_child_content, likes=child_data.nbr_vote, added_since=date_added, id=html_id, From 7880ef7d385d0cf8230c4629ceeb1db0ee44602e Mon Sep 17 00:00:00 2001 From: Eliam LOTONGA Date: Fri, 8 Jan 2021 18:46:50 +0100 Subject: [PATCH 023/145] css reply tag --- pod/video/static/css/comment-style.css | 35 ++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/pod/video/static/css/comment-style.css b/pod/video/static/css/comment-style.css index b5fcbeaf83..a66c58c82c 100644 --- a/pod/video/static/css/comment-style.css +++ b/pod/video/static/css/comment-style.css @@ -31,13 +31,30 @@ .comment_main h6 { font-weight: 600; - color: var(--secondary) !important; + color: var(--padding-comment-content) !important; font-size: 16px; } .comment_main .icon { - width: 15px; + width: 18px; } +.reply_to{ + color: var(--secondary); + letter-spacing: 1.1px; + width: 200px; + max-width: 100%; + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: var(--content-third-color) !important; + margin-bottom: .8em; + margin-left: .8em; +} +.reply_author{ + font-weight: 600; +} +.reply_content:{font-weight: 500;} .comment_main .icon.comment_vote_icon svg { -webkit-transform: translateY(-1px); @@ -576,3 +593,17 @@ padding-left: .8em; } } +/* ***** scroll to comment background */ +.scroll_to{ + animation-name: scrolltocomment; + animation-duration: 8s; + animation-fill-mode: forwards; +} +@keyframes scrolltocomment{ + 0% { + background-color: rgba(255, 234, 59); + } + 100% { + background-color: var(--third-background-color); + } + } From 8e7e95b6f21d3490bdc43e13ef492219f63beaf6 Mon Sep 17 00:00:00 2001 From: Eliam LOTONGA Date: Sat, 9 Jan 2021 22:18:22 +0100 Subject: [PATCH 024/145] Add reply tag svg --- pod/video/static/js/comment-script.js | 1 + 1 file changed, 1 insertion(+) diff --git a/pod/video/static/js/comment-script.js b/pod/video/static/js/comment-script.js index 4eca0ae441..47d84d1b47 100644 --- a/pod/video/static/js/comment-script.js +++ b/pod/video/static/js/comment-script.js @@ -478,6 +478,7 @@ function add_user_tag(comment_value, parent_comment) }); tag.innerHTML =` + @${reply_to} ${reply_content} `; From 86b3159c70fdc6d4b945b2ea9bb77f427fe1b97b Mon Sep 17 00:00:00 2001 From: Eliam LOTONGA Date: Sat, 9 Jan 2021 22:18:41 +0100 Subject: [PATCH 025/145] Css to reply tag svg --- pod/video/static/css/comment-style.css | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pod/video/static/css/comment-style.css b/pod/video/static/css/comment-style.css index a66c58c82c..7ced38df9e 100644 --- a/pod/video/static/css/comment-style.css +++ b/pod/video/static/css/comment-style.css @@ -41,7 +41,7 @@ .reply_to{ color: var(--secondary); letter-spacing: 1.1px; - width: 200px; + width: 230px; max-width: 100%; display: block; overflow: hidden; @@ -51,6 +51,9 @@ margin-bottom: .8em; margin-left: .8em; } +.reply_to svg { + width: 16px; +} .reply_author{ font-weight: 600; } From c76255b913073e4754cb42cc322d7618fd1be74b Mon Sep 17 00:00:00 2001 From: Eliam LOTONGA Date: Sat, 9 Jan 2021 23:57:16 +0100 Subject: [PATCH 026/145] Add dynamic border color of comment child, change comment children order by --- pod/video/static/js/comment-script.js | 51 +++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 6 deletions(-) diff --git a/pod/video/static/js/comment-script.js b/pod/video/static/js/comment-script.js index 47d84d1b47..0673b2305d 100644 --- a/pod/video/static/js/comment-script.js +++ b/pod/video/static/js/comment-script.js @@ -4,6 +4,17 @@ let base_delete_url = base_url.replace('comment', 'comment/del'); let all_comment = null; let lang_btn = document.querySelector(".btn-lang.btn-lang-active"); let VOTED_USERS = []; +const COLORS = [ + "#212529", + "#343A40", + "#495057", + "#6C757D", + "#ADB5BD", + "#CED4DA", + "#DEE2E6", + "#E9ECEF", + "#F8F9FA", +] const LANG = lang_btn?lang_btn.textContent.trim() : "fr"; const ACTION_COMMENT = { comment_to_delete: null @@ -453,7 +464,7 @@ function scrollToComment(targetComment) window.setTimeout(() => { htmlTarget.classList.remove('scroll_to'); - }, 8000); + }, 3000); htmlTarget.classList.add('scroll_to'); } @@ -492,6 +503,28 @@ function add_user_tag(comment_value, parent_comment) return comment_content; } +/**************** return color index **************** + ******************************************************/ +function setBorderLeftColor(comment, parent_element, is_parent=false){ + if(is_parent) return 0; + try{ + let index = Number.parseInt(parent_element.dataset.level) + 1; + if(index >= COLORS.length){ + comment.dataset.level = COLORS.length - 1; + comment.querySelector(".comment_content") + .style.borderLeft = `4px solid ${COLORS[COLORS.length - 1]}`; + } + else{ + comment.dataset.level = index; + comment.querySelector(".comment_content") + .style.borderLeft = `4px solid ${COLORS[index]}`; + } + }catch(e){ + comment.dataset.level = COLORS.length - 1; + comment.querySelector(".comment_content") + .style.borderLeft = `4px solid ${COLORS[COLORS.length - 1]}`; + } +} /**************** Add parent Comment **************** ******************************************************/ if(is_authenticated){ @@ -512,6 +545,7 @@ if(is_authenticated){ id=`comment_${date_added.getTime()}`, is_parent=true, is_comment_owner=true); + c.dataset.level = '-1'; document.querySelector(".comment_container .comment_content form.add_parent_comment").after(c); el.value = ""; // INSERT INTO DATABASE THE CURRENT COMMENT CHILD @@ -528,7 +562,7 @@ function add_child_comment(el, container_el, parent_comment) { let child_direct_parent = document.querySelector( `#${el.parentElement.parentElement.dataset.comment}`) - + let comment_child_content = add_user_tag( el.value, child_direct_parent, @@ -541,7 +575,9 @@ function add_child_comment(el, container_el, parent_comment) id = `comment_${date_added.getTime()}`, is_parent=false, is_comment_owner=true); - container_el.prepend(c); + setBorderLeftColor(c, child_direct_parent, is_parent=false) + //container_el.prepend(c); + container_el.appendChild(c); hide_or_add_show_children_btn(container_el.parentElement); // INSERT INTO DATABASE THE CURRENT COMMENT CHILD @@ -551,8 +587,8 @@ function add_child_comment(el, container_el, parent_comment) ) // Scroll to the comment child - if(window.scrollY > 1180) - scrollToComment( c ); + //if(window.scrollY > 10) + scrollToComment( c ); save_comment(el.value, date_added.toISOString(), p_id, t_p_id); } @@ -662,6 +698,7 @@ fetch(base_vote_url).then(response=>{ is_parent=true, is_comment_owner= user_id===parent_data.author__id ); + parent_c.dataset.level = '-1'; manage_vote_frontend(parent_data.id, parent_c); if(children_data.length>0) { @@ -689,7 +726,9 @@ fetch(base_vote_url).then(response=>{ is_parent=false, is_comment_owner= user_id===child_data.author__id ); - parent_c.querySelector(".comments_children_container").prepend(child_c); + setBorderLeftColor(child_c, parent_to_scroll, is_parent=false) + //parent_c.querySelector(".comments_children_container").prepend(child_c); + parent_c.querySelector(".comments_children_container").appendChild(child_c); manage_vote_frontend(child_data.id, child_c); }); } From 5f7627f103d26d09eddc2d0a7d2230c49874d372 Mon Sep 17 00:00:00 2001 From: Eliam LOTONGA Date: Sat, 9 Jan 2021 23:57:57 +0100 Subject: [PATCH 027/145] scroll_to duration of animation --- pod/video/static/css/comment-style.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pod/video/static/css/comment-style.css b/pod/video/static/css/comment-style.css index 7ced38df9e..30d1cb654f 100644 --- a/pod/video/static/css/comment-style.css +++ b/pod/video/static/css/comment-style.css @@ -599,7 +599,7 @@ /* ***** scroll to comment background */ .scroll_to{ animation-name: scrolltocomment; - animation-duration: 8s; + animation-duration: 5s; animation-fill-mode: forwards; } @keyframes scrolltocomment{ From 0fb68f7a25875b73006b7144f0d4c59af99ebd62 Mon Sep 17 00:00:00 2001 From: Eliam LOTONGA Date: Sun, 10 Jan 2021 11:43:01 +0100 Subject: [PATCH 028/145] Add icon to comment answer, change color gradient of answerw --- pod/video/static/js/comment-script.js | 41 +++++++++++++++++---------- 1 file changed, 26 insertions(+), 15 deletions(-) diff --git a/pod/video/static/js/comment-script.js b/pod/video/static/js/comment-script.js index 0673b2305d..126a131287 100644 --- a/pod/video/static/js/comment-script.js +++ b/pod/video/static/js/comment-script.js @@ -5,15 +5,15 @@ let all_comment = null; let lang_btn = document.querySelector(".btn-lang.btn-lang-active"); let VOTED_USERS = []; const COLORS = [ - "#212529", - "#343A40", - "#495057", - "#6C757D", - "#ADB5BD", - "#CED4DA", - "#DEE2E6", - "#E9ECEF", - "#F8F9FA", + "#5F616D", + "#6B6D79", + "#767984", + "#828590", + "#8D929B", + "#999EA7", + "#A4AAB2", + "#B0B6BE", + "#BBC2C9", ] const LANG = lang_btn?lang_btn.textContent.trim() : "fr"; const ACTION_COMMENT = { @@ -236,8 +236,14 @@ class Comment extends HTMLElement } else { + let comment_icon_svg = ``; + let comment_icon = document.createElement("DIV"); + comment_icon.innerHTML = comment_icon_svg; + comment_icon.setAttribute("class", "icon comments_icon"); + // add show children button action - comment_container.setAttribute("class", ""); + comment_container.setAttribute("class", "comment_child_container"); + comment_container.prepend(comment_icon) comment_container.querySelector(".comment_content").classList.add("comment_content_child"); this.classList.add("comment_child"); this.appendChild(comment_container); @@ -464,7 +470,7 @@ function scrollToComment(targetComment) window.setTimeout(() => { htmlTarget.classList.remove('scroll_to'); - }, 3000); + }, 4000); htmlTarget.classList.add('scroll_to'); } @@ -505,24 +511,29 @@ function add_user_tag(comment_value, parent_comment) /**************** return color index **************** ******************************************************/ -function setBorderLeftColor(comment, parent_element, is_parent=false){ - if(is_parent) return 0; +function setBorderLeftColor(comment, parent_element){ try{ let index = Number.parseInt(parent_element.dataset.level) + 1; if(index >= COLORS.length){ comment.dataset.level = COLORS.length - 1; comment.querySelector(".comment_content") .style.borderLeft = `4px solid ${COLORS[COLORS.length - 1]}`; + comment.querySelector(".comments_icon") + .style.color = `${COLORS[COLORS.length - 1]}`; } else{ comment.dataset.level = index; comment.querySelector(".comment_content") .style.borderLeft = `4px solid ${COLORS[index]}`; + comment.querySelector(".comments_icon") + .style.color = `${COLORS[index]}`; } }catch(e){ comment.dataset.level = COLORS.length - 1; comment.querySelector(".comment_content") .style.borderLeft = `4px solid ${COLORS[COLORS.length - 1]}`; + comment.querySelector(".comments_icon") + .style.color = `${COLORS[COLORS.length - 1]}`; } } /**************** Add parent Comment **************** @@ -575,7 +586,7 @@ function add_child_comment(el, container_el, parent_comment) id = `comment_${date_added.getTime()}`, is_parent=false, is_comment_owner=true); - setBorderLeftColor(c, child_direct_parent, is_parent=false) + setBorderLeftColor(c, child_direct_parent) //container_el.prepend(c); container_el.appendChild(c); hide_or_add_show_children_btn(container_el.parentElement); @@ -726,7 +737,7 @@ fetch(base_vote_url).then(response=>{ is_parent=false, is_comment_owner= user_id===child_data.author__id ); - setBorderLeftColor(child_c, parent_to_scroll, is_parent=false) + setBorderLeftColor(child_c, parent_to_scroll) //parent_c.querySelector(".comments_children_container").prepend(child_c); parent_c.querySelector(".comments_children_container").appendChild(child_c); manage_vote_frontend(child_data.id, child_c); From a80e2b09f10a125e30f4bcc6cebee0e953b8144c Mon Sep 17 00:00:00 2001 From: Eliam LOTONGA Date: Sun, 10 Jan 2021 11:44:37 +0100 Subject: [PATCH 029/145] Add comment_child_container style, remove unused style --- pod/video/static/css/comment-style.css | 31 ++++++++++++-------------- 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/pod/video/static/css/comment-style.css b/pod/video/static/css/comment-style.css index 30d1cb654f..99df252492 100644 --- a/pod/video/static/css/comment-style.css +++ b/pod/video/static/css/comment-style.css @@ -76,6 +76,7 @@ transform: translateX( calc(-50% - var(--padding-comment-content) ) );*/ width: var(--padding-comment-content); margin:0 6px; + flex: none; } .comment_main .inline_flex_space{display: -webkit-box;display: -ms-flexbox;display: flex; -webkit-box-align: center; -ms-flex-align: center; align-items: center; -webkit-box-pack: justify; -ms-flex-pack: justify; justify-content: space-between; -ms-flex-wrap: wrap; flex-wrap: wrap;} .comment_main .comment_disable::before{ @@ -99,6 +100,12 @@ color: var(--primary); padding-top: 2em; } +@media only screen and (max-width: 512px) +{ + .jumbotron{ + padding: 0 !important; + } +} .comment_main { overflow-y: auto; @@ -425,7 +432,7 @@ -ms-flex-pack: justify; justify-content: space-between; border-left: 4px solid var(--primary); - padding: .6em; + padding: .4em; background-color: var(--third-background-color); -webkit-box-shadow: 1px 1px 4px var(--background-color); box-shadow: 1px 1px 4px var(--background-color); @@ -434,26 +441,27 @@ } .comment_main .comment_element .comment_container > .comments_children_container { - padding-left: 1em; height: 0; overflow: hidden; -webkit-transition: .5s height; -o-transition: .5s height; transition: .5s height; } -.comment_main .comment_element.show .comment_container > .comments_children_container -{ - height: auto; +.comment_main .comment_element.show .comment_container > .comments_children_container{ height: auto; } +.comment_main .comment_element .comment_container .comment_element > .comment_child_container{ + display: flex; + margin-top: calc(var(--margin-between-comment) - .2em); } +.comment_main .comment_element .comment_container .comment_element > .comment_child_container .comments_icon{margin-left: 0; flex: none;} .comment_main .comment .comment_content_child { border-left: 4px solid var(--content-secondary-color); border-radius: 5px; - margin-top: calc(var(--margin-between-comment) - .2em); border-top: 1px solid var(--primary-background-color); border-bottom: 1px solid var(--primary-background-color); border-right: 1px solid var(--primary-background-color); + flex-grow: 1; } .comment_main .comment .comment_content_header .comment_since @@ -585,17 +593,6 @@ padding: 0; } } -@media only screen and (max-width: 550px) -{ - .comment_main .comment_container - { - padding: 0 .8em; - } - .comment_main .comment .comment_container > .comments_children_container - { - padding-left: .8em; - } -} /* ***** scroll to comment background */ .scroll_to{ animation-name: scrolltocomment; From fa9d9cc2ba03213fa8b4fde55b30d02b3c967c5e Mon Sep 17 00:00:00 2001 From: Eliam LOTONGA Date: Mon, 11 Jan 2021 09:29:01 +0100 Subject: [PATCH 030/145] Fixed remove comment child on cascade --- pod/video/static/js/comment-script.js | 67 +++++++++++++++++++-------- 1 file changed, 47 insertions(+), 20 deletions(-) diff --git a/pod/video/static/js/comment-script.js b/pod/video/static/js/comment-script.js index 126a131287..e1212860ef 100644 --- a/pod/video/static/js/comment-script.js +++ b/pod/video/static/js/comment-script.js @@ -81,9 +81,11 @@ class ConfirmModal extends HTMLElement // remove show children btn if no child in the node if(parent_el.classList.contains("comments_children_container") && parent_el.childElementCount===0) { - hide_or_add_show_children_btn(parent_el.parentElement); + parent_e = get_node(parent_el, "comment_element", "comment_child"); + hide_or_add_show_children_btn(parent_e); } - delete_comment(ACTION_COMMENT.comment_to_delete); + delete_comment(); + delete_comment_child_DOM(); ACTION_COMMENT.comment_to_delete = null; }); let cancel_btn = document.createElement("BUTTON"); @@ -288,18 +290,18 @@ function hide_or_add_show_children_btn(parent_comment=null) get_node(this.parentElement, "comment_element", "comment_child").classList.toggle("show"); hide_show_child_comment_text(this); }); - + if(parent_comment) { + let is_parent_comment_owner = user_id === get_comment_attribute(parent_comment, "author__id"); let children_container = parent_comment.querySelector(".comments_children_container"); if(!parent_comment.querySelector(".actions .comment_show_children_action") && children_container.childElementCount >0) { - if(is_comment_owner || is_video_owner || is_superuser){ + if(is_parent_comment_owner || is_video_owner || is_superuser){ parent_comment.querySelector(".comment_content_footer .actions").insertBefore( children_action, parent_comment.querySelector(".comment_content_footer .actions .comment_delete_action") ); } else{ - console.log("NOt owner , Not video owner, Not super user") parent_comment.querySelector(".comment_content_footer .actions").appendChild(children_action ); } } @@ -319,7 +321,8 @@ function hide_or_add_show_children_btn(parent_comment=null) let comments_children_container= comment.querySelector(".comment .comment_container .comments_children_container"); if(!comment.querySelector(".comment_content_footer .actions .comment_show_children_action") && comments_children_container.childElementCount > 0) { - if(!is_comment_owner || is_video_owner || is_superuser){ + let is_parent_comment_owner = user_id === get_comment_attribute(comment, "author__id"); + if(is_parent_comment_owner || is_video_owner || is_superuser){ comment.querySelector(".comment_content_footer .actions").insertBefore( children_action, comment.querySelector(".comment_content_footer .actions .comment_delete_action") ); } @@ -401,10 +404,13 @@ function save_comment(content, date, parent_id=null, top_parent_id=null) author__last_name: data.author__last_name, content: content, id: data.id, - added: date + added: date, + parent__id: parent_id, + top_parent__id: top_parent_id } if(!parent_id) { + // update all_comment data let p_c = { parent_comment: c, children: [] @@ -413,10 +419,11 @@ function save_comment(content, date, parent_id=null, top_parent_id=null) } else { + // update all_comment data all_comment = all_comment.map((comment)=>{ if(comment.parent_comment.id === top_parent_id) - comment.children.push(c); - return comment; + comment.children = [...comment.children, c]; + return comment; }); } }); @@ -427,14 +434,13 @@ function save_comment(content, date, parent_id=null, top_parent_id=null) console.log(error) console.log(error.message) }); - } /**************** Delete Comment **************** ******************************************************/ -function delete_comment(target_comment_html) +function delete_comment() { - let comment_id = get_comment_attribute(target_comment_html); + let comment_id = get_comment_attribute(ACTION_COMMENT.comment_to_delete); let url = base_delete_url + comment_id + '/'; let data = new FormData(); data.append('csrfmiddlewaretoken', Cookies.get('csrftoken')); @@ -455,6 +461,22 @@ function delete_comment(target_comment_html) }) } +function delete_comment_child_DOM() +{ + let comment_id = get_comment_attribute(ACTION_COMMENT.comment_to_delete); + let comment_top_parent_id = get_comment_attribute(ACTION_COMMENT.comment_to_delete, attr='top_parent__id'); + all_comment + .filter(p_comment => p_comment.parent_comment.id === comment_top_parent_id)[0] + .children + .forEach( c_comment => { + if(c_comment.parent__id === comment_id){ + let html_id = `#comment_${new Date(c_comment.added).getTime()}`; + let c = document.querySelector(html_id) + c.parentElement.removeChild(c); + } + }); +} + /*********** Scroll to a specific comment ************* * ****************************************************/ function scrollToComment(targetComment) @@ -589,7 +611,7 @@ function add_child_comment(el, container_el, parent_comment) setBorderLeftColor(c, child_direct_parent) //container_el.prepend(c); container_el.appendChild(c); - hide_or_add_show_children_btn(container_el.parentElement); + hide_or_add_show_children_btn(get_node(container_el, "comment_element", "comment_child")); // INSERT INTO DATABASE THE CURRENT COMMENT CHILD let p_id = get_comment_attribute( child_direct_parent ); @@ -611,20 +633,25 @@ function get_comment_attribute(comment_html, attr="id") { let curr_html_id = comment_html.getAttribute('id'); let comment_attr = null - all_comment.forEach(comment=> - { - let comment_htmlid = `comment_${new Date(comment.parent_comment.added).getTime()}`; + for(const comment of all_comment){ + let comment_htmlid = `comment_${new Date(comment.parent_comment.added).getTime()}`; + if(comment_htmlid == curr_html_id) { comment_attr = comment.parent_comment[attr]; + break; } - comment.children.forEach(c_comment=> + for(const c_comment of comment.children) { + let c_comment_htmlid = `comment_${new Date(c_comment.added).getTime()}`; - if(c_comment_htmlid == curr_html_id) + if(c_comment_htmlid == curr_html_id){ comment_attr = c_comment[attr]; - }); - }); + break; + } + } + if(comment_attr) break; + } return comment_attr; } From b1fc0e70ccedbed2b057ab4cfcdf21e3b2745539 Mon Sep 17 00:00:00 2001 From: Eliam LOTONGA Date: Mon, 11 Jan 2021 09:29:43 +0100 Subject: [PATCH 031/145] change bg color on scrolling to a comment --- pod/video/static/css/comment-style.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pod/video/static/css/comment-style.css b/pod/video/static/css/comment-style.css index 99df252492..f9882d8afc 100644 --- a/pod/video/static/css/comment-style.css +++ b/pod/video/static/css/comment-style.css @@ -601,7 +601,7 @@ } @keyframes scrolltocomment{ 0% { - background-color: rgba(255, 234, 59); + background-color: #f9f99b; /* rgba(255, 234, 59)*/ } 100% { background-color: var(--third-background-color); From 3a36dff82b01ce429f409279fa751a57b01f2923 Mon Sep 17 00:00:00 2001 From: Eliam LOTONGA Date: Mon, 11 Jan 2021 11:21:47 +0100 Subject: [PATCH 032/145] update locale fr --- pod/locale/fr/LC_MESSAGES/djangojs.mo | Bin 9545 -> 9509 bytes pod/locale/fr/LC_MESSAGES/djangojs.po | 501 +++++++++++++++++++++----- 2 files changed, 417 insertions(+), 84 deletions(-) diff --git a/pod/locale/fr/LC_MESSAGES/djangojs.mo b/pod/locale/fr/LC_MESSAGES/djangojs.mo index 0f157e8c557346094c4ab2bedd402747e38c0b29..a2686fca6e0c2158e9f043698b95e40ed5591f50 100644 GIT binary patch delta 2606 zcmZA1eN5F=9LMnk7mOzb<;lS0hY#@}PnR?vL?uE6gaR}X743$<1OqPIi-2==+uU+X z*m5`jEKAMJIk(#VLu*!2^PyIMXj^mS^pDosbjvNFtx;R=&%HmIb;kGiI_L0vIOluL zx!*K>)D*b8DCUHr93YkueJ*3vad{pWN=m#jbeV;ihUu7sg_wbBu@c+y2|R|y7{qd% z#8RAg`d1_vvx;^*78(;UZ%`?s;~bXYkGKG16OGXr(r`Iu<5Rf7u?ur)A4c8x9`a|- za7n_CF%>_@#ds6r@IF3`k1(0mK}9dVjhe{}#$r4Rt+h_(!dQ%jN?{2y zNmGHES-sP4blTfdnc0ObyBR{VZ4RRba1<->dF{(J=lOB;zqoW z`B=6nGT=_kqCJ9|$Z^yrJ&&5e71UB)Lrv@+YG990_r<4@|2is5`ELPwa19>Aa-75} zoJKuZ!c$t~den=XQJcwj`p2FAcTqFG;P@#jV^?t`%9vZI_cR7raII|{@(trfEyWRh z0Y5;c@F6NAf1(Cdo*5ZvA1alna5YXjCNny<>v0_(LM`D%)Q7L)CJcN}#iEi%{_zYVD{3;o~T78Y5b`FPasJ&-yye}Ur@);#ia!|SWI_J_(#5yDq( zK4oc()3+X9By`-AEtRcA8=>^81hf&fwsXZzMQf$?n=AQLwh^4n@JUg>PKwH_gx20g zbQ3C`2;m#SZg;iY9V$=X8I@h1 z=jrbD`u16#5xd*!{&ysvUSII-j1HII_6LVD3qpCBKSc*~vWh}SvOb9l)h!uwRUaH2 p^!4`JzW+XDoA$5I4SEOm`5voTo`Z8kHMrM8sHa_JQ5jka_bgq9+;Pz$9fluJb`P)m187u#*wMX1DN z9w0#zRLoMN1|u&TD;h6LB$zZY@B)ZRVnU)Az(kCRyeN^72_RP#V z|8r*ge*2kr=Tbq&sG&rN=|ni)7&Ux6frEme06zAee%)>v?9Yc;W4|AS{GqBooC6;o&4b|@zb*Hxk>7)15!#yp;HMyOQae(#4*Q7Jo(T9TVM1&c_FGEjqM*zTPV zqx!vu79PbQotmDoXKjoQfAc|3=+-5(~~4Obsf9jmYFo z3vzGMff?A1nt2#mZnFnTuGxpW|3R$BV;I6~`Q%@fndH;L4%DXFfo=E^w&HcH#6<VMP)k;3-A!m!?U;uZ=)`( z=PIrJlc*baVmU^=_EE3>UDS-f^gM~m)OWbcM`ls?S;u;6DZ3mhJj0COYCM2S-9^;Q zuA(yXFKR#y#i@bDP^tV7AHgf0MT}18?YIOT)Y6Tk9{de1#~;u_C%{v*Hj6#Gkt~_L zo^K-Cz>J{=dIGiEzeNr7CTfqQu^!b}f~B|$mAOG=^~_#W|50SD<~?LDIp#POb^IEe z@d7qr@ib#*VFy|mM{U+as7-hZwKvWof2M#@kXWX?Pj zH{nzGI&Q?@P?>BZy^d})NJS%l%ky*8TAxL|PM1(A{uQ-1lqL^E8o0sDx8+B>!47l{G2M3f$(^TXBL{XI0!+DFe09RQgl6M-6lt zv5?Rp|6eL-c*bk2#}HA@^WC-T!a715Nk!SBLq-->852Cp$(xA!{fwbgo@tR9->z#6umZigm%5Y5xi+`Y4g

a&G&pES zcUqC&p=e@lc4;!{Z%fMw+XMDaJJtACPF1znDc(P{+v@F)41{BL)cw`8ct7nL^7<#I zPF(2AwqkaCcqkgT_IRYhIC#E~hT zGq|-`$zZJ5sWJBscZb*$iP?!zaaFRk_+r|0w@dtm;b9&WkI>zlh}-L*skQZPhn~K^ zNN<17Hv9gcEj!xR6Wy75iaYN6u1!`1zVPK)19tr00OH9Xr;Vg1UM*=%o-gUl{0~9J BEVKXs diff --git a/pod/locale/fr/LC_MESSAGES/djangojs.po b/pod/locale/fr/LC_MESSAGES/djangojs.po index 8497114e07..cd59e685db 100644 --- a/pod/locale/fr/LC_MESSAGES/djangojs.po +++ b/pod/locale/fr/LC_MESSAGES/djangojs.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: Esup-Pod\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2020-12-01 18:18+0100\n" +"POT-Creation-Date: 2021-01-11 11:11+0100\n" "PO-Revision-Date: 2020-04-30 12:12+0200\n" "Last-Translator: Nicolas \n" "Language-Team: \n" @@ -22,19 +22,24 @@ msgstr "" #: pod/enrichment/static/js/enrichment.js:5 #: pod/enrichment/static/js/enrichment.js:7 #: pod/enrichment/static/js/enrichment.js:183 -#: pod/enrichment/static/js/enrichment.js:185 +#: pod/enrichment/static/js/enrichment.js:185 pod/static/js/chapters.js:5 +#: pod/static/js/chapters.js:7 pod/static/js/enrichment.js:5 +#: pod/static/js/enrichment.js:7 pod/static/js/enrichment.js:183 +#: pod/static/js/enrichment.js:185 msgid "Get time from the player" msgstr "Obtenir le temps du lecteur" #: pod/chapter/static/js/chapters.js:16 #: pod/completion/static/js/completion.js:50 -#: pod/enrichment/static/js/enrichment.js:17 +#: pod/enrichment/static/js/enrichment.js:17 pod/static/js/chapters.js:16 +#: pod/static/js/completion.js:50 pod/static/js/enrichment.js:17 msgid "Error getting form." msgstr "Erreur lors de la récupération du formulaire" #: pod/chapter/static/js/chapters.js:16 #: pod/completion/static/js/completion.js:50 -#: pod/enrichment/static/js/enrichment.js:17 +#: pod/enrichment/static/js/enrichment.js:17 pod/static/js/chapters.js:16 +#: pod/static/js/completion.js:50 pod/static/js/enrichment.js:17 msgid "The form could not be recovered." msgstr "Le formulaire ne peut pas être récupéré." @@ -53,40 +58,56 @@ msgstr "Le formulaire ne peut pas être récupéré." #: pod/main/static/js/main.js:300 pod/main/static/js/main.js:311 #: pod/main/static/js/main.js:325 #: pod/podfile/static/podfile/js/filewidget.js:365 -#: pod/podfile/static/podfile/js/filewidget.js:398 +#: pod/podfile/static/podfile/js/filewidget.js:398 pod/static/js/chapters.js:52 +#: pod/static/js/chapters.js:72 pod/static/js/chapters.js:93 +#: pod/static/js/chapters.js:125 pod/static/js/chapters.js:159 +#: pod/static/js/completion.js:92 pod/static/js/completion.js:123 +#: pod/static/js/completion.js:166 pod/static/js/completion.js:198 +#: pod/static/js/enrichment.js:52 pod/static/js/enrichment.js:72 +#: pod/static/js/enrichment.js:93 pod/static/js/enrichment.js:120 +#: pod/static/js/enrichment.js:149 pod/static/js/main.js:293 +#: pod/static/js/main.js:300 pod/static/js/main.js:311 +#: pod/static/js/main.js:325 pod/static/podfile/js/filewidget.js:365 +#: pod/static/podfile/js/filewidget.js:398 msgid "You are no longer authenticated. Please log in again." msgstr "Vous n'êtes plus authentifié. Veuillez vous reconnectez." #: pod/chapter/static/js/chapters.js:146 #: pod/enrichment/static/js/enrichment.js:136 pod/main/static/js/main.js:263 -#: pod/main/static/js/main.js:317 +#: pod/main/static/js/main.js:317 pod/static/js/chapters.js:146 +#: pod/static/js/enrichment.js:136 pod/static/js/main.js:263 +#: pod/static/js/main.js:317 msgid "One or more errors have been found in the form." msgstr "Une ou plusieurs erreurs ont été trouvées dans le formulaire." #: pod/chapter/static/js/chapters.js:181 -#: pod/enrichment/static/js/enrichment.js:223 +#: pod/enrichment/static/js/enrichment.js:223 pod/static/js/chapters.js:181 +#: pod/static/js/enrichment.js:223 msgid "Please enter a title from 2 to 100 characters." msgstr "Veuillez entrer un titre contenant entre 2 et 100 caractères." -#: pod/chapter/static/js/chapters.js:187 +#: pod/chapter/static/js/chapters.js:187 pod/static/js/chapters.js:187 msgid "Please enter a correct start field between 0 and" msgstr "Veuillez entrer un correct temps de début compris entre 0 et" -#: pod/chapter/static/js/chapters.js:200 +#: pod/chapter/static/js/chapters.js:200 pod/static/js/chapters.js:200 msgid "The chapter" msgstr "Le chapitre" -#: pod/chapter/static/js/chapters.js:200 +#: pod/chapter/static/js/chapters.js:200 pod/static/js/chapters.js:200 msgid "starts at the same time." msgstr "commencent au même moment." #: pod/chapter/static/js/videojs-chapters.js:22 #: pod/chapter/static/js/videojs-chapters.js:24 #: pod/chapter/static/js/videojs-chapters.js:26 +#: pod/static/js/videojs-chapters.js:22 pod/static/js/videojs-chapters.js:24 +#: pod/static/js/videojs-chapters.js:26 msgid "Chapters" msgstr "Chapitres" #: pod/completion/static/js/caption_maker.js:22 +#: pod/static/js/caption_maker.js:22 msgid "" "WEBVTT\n" "\n" @@ -99,332 +120,375 @@ msgstr "" "texte de sous-titre" #: pod/completion/static/js/caption_maker.js:34 +#: pod/static/js/caption_maker.js:34 msgid "There is no captions to save" msgstr "Il n’y a aucun sous-titre à sauvegarder" #: pod/completion/static/js/caption_maker.js:82 +#: pod/static/js/caption_maker.js:82 msgid "error during exchange" msgstr "erreur durant le téléversement" #: pod/completion/static/js/caption_maker.js:82 +#: pod/static/js/caption_maker.js:82 msgid "no data could be stored." msgstr "aucune donnée ne peut être stockée." #: pod/completion/static/js/caption_maker.js:113 +#: pod/static/js/caption_maker.js:113 msgid "Are you sure you want to delete all caption ?" msgstr "Êtes-vous sûr(e) de vouloir supprimer les sous-titre ?" -#: pod/completion/static/js/completion.js:145 +#: pod/completion/static/js/completion.js:145 pod/static/js/completion.js:145 msgid "Are you sure you want to delete this file ?" msgstr "Êtes-vous sûr(e) de vouloir supprimer ce fichier ?" -#: pod/completion/static/js/completion.js:147 +#: pod/completion/static/js/completion.js:147 pod/static/js/completion.js:147 msgid "Are you sure you want to delete this contributor ?" msgstr "Êtes-vous sûr(e) de vouloir supprimer ce contributeur ?" -#: pod/completion/static/js/completion.js:149 +#: pod/completion/static/js/completion.js:149 pod/static/js/completion.js:149 msgid "Are you sure you want to delete this document ?" msgstr "Êtes-vous sûr(e) de vouloir supprimer ce document ?" -#: pod/completion/static/js/completion.js:151 +#: pod/completion/static/js/completion.js:151 pod/static/js/completion.js:151 msgid "Are you sure you want to delete this overlay ?" msgstr "Êtes-vous sûr(e) de vouloir supprimer cette superposition ?" #: pod/completion/static/js/completion.js:195 pod/main/static/js/main.js:265 +#: pod/static/js/completion.js:195 pod/static/js/main.js:265 msgid "Changes have been saved." msgstr "Les modifications ont été sauvegardées." -#: pod/completion/static/js/completion.js:224 +#: pod/completion/static/js/completion.js:224 pod/static/js/completion.js:224 msgid "Display" msgstr "Afficher" -#: pod/completion/static/js/completion.js:224 +#: pod/completion/static/js/completion.js:224 pod/static/js/completion.js:224 msgid "section" msgstr "section" -#: pod/completion/static/js/completion.js:246 +#: pod/completion/static/js/completion.js:246 pod/static/js/completion.js:246 msgid "Please enter a name from 2 to 100 caracteres." msgstr "Veuillez entrer un nom entre 2 et 200 caractères." -#: pod/completion/static/js/completion.js:252 +#: pod/completion/static/js/completion.js:252 pod/static/js/completion.js:252 msgid "You cannot enter a weblink with more than 200 caracteres." msgstr "Vous ne pouvez pas entrer un lien web avec plus de 200 caractères." -#: pod/completion/static/js/completion.js:258 +#: pod/completion/static/js/completion.js:258 pod/static/js/completion.js:258 msgid "Please enter a role." msgstr "Veuillez entrer un rôle." -#: pod/completion/static/js/completion.js:267 +#: pod/completion/static/js/completion.js:267 pod/static/js/completion.js:267 msgid "" "There is already a contributor with this same name and role in the list." msgstr "" "Il existe déjà un contributeur avec le même nom et le même rôle dans cette " "liste." -#: pod/completion/static/js/completion.js:278 +#: pod/completion/static/js/completion.js:278 pod/static/js/completion.js:278 msgid "Please enter a correct kind." msgstr "Veuillez entrer un genre correct." -#: pod/completion/static/js/completion.js:286 +#: pod/completion/static/js/completion.js:286 pod/static/js/completion.js:286 msgid "Please select a language." msgstr "Veuillez renseigner une langue." -#: pod/completion/static/js/completion.js:293 +#: pod/completion/static/js/completion.js:293 pod/static/js/completion.js:293 msgid "Please specify a track file." msgstr "Veuillez joindre un fichier de piste vidéo." -#: pod/completion/static/js/completion.js:299 +#: pod/completion/static/js/completion.js:299 pod/static/js/completion.js:299 msgid "Only .vtt format is allowed." msgstr "Seulement le format \".vtt\" est autorisé." -#: pod/completion/static/js/completion.js:316 +#: pod/completion/static/js/completion.js:316 pod/static/js/completion.js:316 msgid "There is already a track with the same kind and language in the list." msgstr "" "Il existe déjà une piste vidéo avec le même genre et la même langue dans la " "liste." #: pod/completion/static/js/completion.js:323 -#: pod/enrichment/static/js/enrichment.js:274 +#: pod/enrichment/static/js/enrichment.js:274 pod/static/js/completion.js:323 +#: pod/static/js/enrichment.js:274 msgid "Please select a document." msgstr "Veuillez joindre un document." -#: pod/completion/static/js/completion.js:331 +#: pod/completion/static/js/completion.js:331 pod/static/js/completion.js:331 msgid "Iframe and Script tags are not allowed." msgstr "Les balises Iframe et Script ne sont pas autorisées." -#: pod/enrichment/static/js/enrichment.js:229 +#: pod/enrichment/static/js/enrichment.js:229 pod/static/js/enrichment.js:229 msgid "Please enter a correct start from 0 to " msgstr "Veuillez entrer un correct temps de début compris entre 0 et " -#: pod/enrichment/static/js/enrichment.js:235 +#: pod/enrichment/static/js/enrichment.js:235 pod/static/js/enrichment.js:235 msgid "Please enter a correct end from 1 to " msgstr "Veuillez entrer un correct temps de fin compris entre 1 et " -#: pod/enrichment/static/js/enrichment.js:243 +#: pod/enrichment/static/js/enrichment.js:243 pod/static/js/enrichment.js:243 msgid "Please enter a correct image." msgstr "Veuillez joindre une image correct." -#: pod/enrichment/static/js/enrichment.js:251 +#: pod/enrichment/static/js/enrichment.js:251 pod/static/js/enrichment.js:251 msgid "Please enter a correct richtext." msgstr "Veuillez entrer un texte riche correct." -#: pod/enrichment/static/js/enrichment.js:259 +#: pod/enrichment/static/js/enrichment.js:259 pod/static/js/enrichment.js:259 msgid "Please enter a correct weblink." msgstr "Veuillez entrer un lien web correct." -#: pod/enrichment/static/js/enrichment.js:265 +#: pod/enrichment/static/js/enrichment.js:265 pod/static/js/enrichment.js:265 msgid "Weblink must be less than 200 characters." msgstr "Un lien web doit contenir moins de 200 caractères." -#: pod/enrichment/static/js/enrichment.js:282 +#: pod/enrichment/static/js/enrichment.js:282 pod/static/js/enrichment.js:282 msgid "Please enter a correct embed." msgstr "Veuillez entrer une intégration correct." -#: pod/enrichment/static/js/enrichment.js:288 +#: pod/enrichment/static/js/enrichment.js:288 pod/static/js/enrichment.js:288 msgid "Embed field must be less than 300 characters." msgstr "Un lien d'intégration doit être inférieur à 300 caractères." -#: pod/enrichment/static/js/enrichment.js:296 +#: pod/enrichment/static/js/enrichment.js:296 pod/static/js/enrichment.js:296 msgid "Please enter a type in index field." msgstr "Veuillez entrer un type." -#: pod/enrichment/static/js/enrichment.js:308 +#: pod/enrichment/static/js/enrichment.js:308 pod/static/js/enrichment.js:308 msgid "The start field value is greater than the end field one." msgstr "Le temps de début est supérieur au temps de fin." -#: pod/enrichment/static/js/enrichment.js:310 +#: pod/enrichment/static/js/enrichment.js:310 pod/static/js/enrichment.js:310 msgid "The end field value is greater than the video duration." msgstr "La temps de fin est supérieur à la durée de la vidéo." -#: pod/enrichment/static/js/enrichment.js:312 +#: pod/enrichment/static/js/enrichment.js:312 pod/static/js/enrichment.js:312 msgid "End field and start field cannot be equal." msgstr "Le commencement et la fin ne peuvent être équivalents." -#: pod/enrichment/static/js/enrichment.js:330 +#: pod/enrichment/static/js/enrichment.js:330 pod/static/js/enrichment.js:330 msgid "There is an overlap with the enrichment " msgstr "Il y a un chevauchement avec l'enrichissement " -#: pod/enrichment/static/js/enrichment.js:331 +#: pod/enrichment/static/js/enrichment.js:331 pod/static/js/enrichment.js:331 msgid "please change start and/or end values." msgstr "veuillez changer de valeur de début et/ou de fin." #: pod/main/static/js/jquery.jqgrid.min.js:10 +#: pod/static/js/jquery.jqgrid.min.js:10 msgid "gView" msgstr "" #: pod/main/static/js/jquery.jqgrid.min.js:10 +#: pod/static/js/jquery.jqgrid.min.js:10 msgid "gBox" msgstr "" #: pod/main/static/js/jquery.jqgrid.min.js:10 +#: pod/static/js/jquery.jqgrid.min.js:10 msgid "overlay" msgstr "" #: pod/main/static/js/jquery.jqgrid.min.js:10 +#: pod/static/js/jquery.jqgrid.min.js:10 msgid "loading" msgstr "chargement" #: pod/main/static/js/jquery.jqgrid.min.js:10 +#: pod/static/js/jquery.jqgrid.min.js:10 msgid "states.hover" msgstr "" #: pod/main/static/js/jquery.jqgrid.min.js:10 +#: pod/static/js/jquery.jqgrid.min.js:10 msgid "states.disabled" msgstr "" #: pod/main/static/js/jquery.jqgrid.min.js:10 +#: pod/static/js/jquery.jqgrid.min.js:10 msgid "pager.pagerSelect" msgstr "" #: pod/main/static/js/jquery.jqgrid.min.js:10 +#: pod/static/js/jquery.jqgrid.min.js:10 msgid "pager.pagerInput" msgstr "" #: pod/main/static/js/jquery.jqgrid.min.js:10 +#: pod/static/js/jquery.jqgrid.min.js:10 msgid "pager.pagerButton" msgstr "" #: pod/main/static/js/jquery.jqgrid.min.js:10 +#: pod/static/js/jquery.jqgrid.min.js:10 msgid "states.hoverTh" msgstr "" #: pod/main/static/js/jquery.jqgrid.min.js:10 +#: pod/static/js/jquery.jqgrid.min.js:10 msgid "colHeaders" msgstr "" #: pod/main/static/js/jquery.jqgrid.min.js:10 +#: pod/static/js/jquery.jqgrid.min.js:10 msgid "hTable" msgstr "" #: pod/main/static/js/jquery.jqgrid.min.js:10 +#: pod/static/js/jquery.jqgrid.min.js:10 msgid "states.select" msgstr "" #: pod/main/static/js/jquery.jqgrid.min.js:10 +#: pod/static/js/jquery.jqgrid.min.js:10 msgid "resizer" msgstr "" #: pod/main/static/js/jquery.jqgrid.min.js:10 +#: pod/static/js/jquery.jqgrid.min.js:10 msgid "gridFooter" msgstr "" #: pod/main/static/js/jquery.jqgrid.min.js:10 +#: pod/static/js/jquery.jqgrid.min.js:10 msgid "rowFooter" msgstr "" #: pod/main/static/js/jquery.jqgrid.min.js:10 +#: pod/static/js/jquery.jqgrid.min.js:10 msgid "gridRow" msgstr "" #: pod/main/static/js/jquery.jqgrid.min.js:10 +#: pod/static/js/jquery.jqgrid.min.js:10 msgid "grid" msgstr "" #: pod/main/static/js/jquery.jqgrid.min.js:10 +#: pod/static/js/jquery.jqgrid.min.js:10 msgid "top" msgstr "" #: pod/main/static/js/jquery.jqgrid.min.js:10 +#: pod/static/js/jquery.jqgrid.min.js:10 msgid "bottom" msgstr "" #: pod/main/static/js/jquery.jqgrid.min.js:10 +#: pod/static/js/jquery.jqgrid.min.js:10 msgid "hDiv" msgstr "" #: pod/main/static/js/jquery.jqgrid.min.js:10 +#: pod/static/js/jquery.jqgrid.min.js:10 msgid "pager.pager" msgstr "" #: pod/main/static/js/jquery.jqgrid.min.js:10 +#: pod/static/js/jquery.jqgrid.min.js:10 msgid "titleButton" msgstr "" #: pod/main/static/js/jquery.jqgrid.min.js:10 +#: pod/static/js/jquery.jqgrid.min.js:10 msgid "gridTitle" msgstr "" #: pod/main/static/js/jquery.jqgrid.min.js:10 +#: pod/static/js/jquery.jqgrid.min.js:10 msgid "toolbarUpper" msgstr "" #: pod/main/static/js/jquery.jqgrid.min.js:10 +#: pod/static/js/jquery.jqgrid.min.js:10 msgid "toolbarBottom" msgstr "" #: pod/main/static/js/jquery.jqgrid.min.js:10 +#: pod/static/js/jquery.jqgrid.min.js:10 msgid "rowNum" msgstr "" #: pod/main/static/js/jquery.jqgrid.min.js:10 +#: pod/static/js/jquery.jqgrid.min.js:10 msgid "gridError" msgstr "" #: pod/main/static/js/jquery.jqgrid.min.js:10 +#: pod/static/js/jquery.jqgrid.min.js:10 msgid "gridErrorText" msgstr "" -#: pod/main/static/js/main.js:244 +#: pod/main/static/js/main.js:244 pod/static/js/main.js:244 msgid "Are you sure you want to delete this element?" msgstr "Êtes-vous sûr(e) de vouloir supprimer cet élément ?" #: pod/main/static/js/main.js:286 -#: pod/podfile/static/podfile/js/filewidget.js:391 +#: pod/podfile/static/podfile/js/filewidget.js:391 pod/static/js/main.js:286 +#: pod/static/podfile/js/filewidget.js:391 msgid "Error during exchange" msgstr "Erreur durant le téléversement" -#: pod/main/static/js/main.js:286 +#: pod/main/static/js/main.js:286 pod/static/js/main.js:286 msgid "No data could be stored." msgstr "Aucune donnée ne peut être stockée." -#: pod/main/static/js/main.js:339 +#: pod/main/static/js/main.js:339 pod/static/js/main.js:339 msgid "Change your picture" msgstr "Changer votre image" -#: pod/main/static/js/main.js:345 +#: pod/main/static/js/main.js:345 pod/static/js/main.js:345 msgid "Add your picture" msgstr "Ajouter votre image" -#: pod/main/static/js/main.js:443 +#: pod/main/static/js/main.js:443 pod/static/js/main.js:443 msgid "text copied" msgstr "texte copié" -#: pod/main/static/js/main.js:490 +#: pod/main/static/js/main.js:490 pod/static/js/main.js:490 msgid "Errors appear in the form, please correct them" msgstr "Des erreurs sont présentes dans le formulaire, veuillez les corriger" -#: pod/main/static/js/main.js:513 +#: pod/main/static/js/main.js:513 pod/static/js/main.js:513 msgid "The file size exceeds the maximum allowed value :" msgstr "La fichier dépasse la taille maximum autorisée :" -#: pod/main/static/js/main.js:528 +#: pod/main/static/js/main.js:528 pod/static/js/main.js:528 msgid "The file extension not in the allowed extension :" msgstr "" "Cette extension de fichier n'est pas présente dans les extensions " "autorisées :" #: pod/podfile/static/podfile/js/filewidget.js:11 +#: pod/static/podfile/js/filewidget.js:11 msgid "Change image" msgstr "Changer d'image" #: pod/podfile/static/podfile/js/filewidget.js:13 +#: pod/static/podfile/js/filewidget.js:13 msgid "Change file" msgstr "Changer de fichier" #: pod/podfile/static/podfile/js/filewidget.js:28 +#: pod/static/podfile/js/filewidget.js:28 msgid "Open file in a new tab" msgstr "Ouvrir le fichier dans un nouvel onglet" #: pod/podfile/static/podfile/js/filewidget.js:55 +#: pod/static/podfile/js/filewidget.js:55 msgid "This folder is empty" msgstr "Ce dossier est vide" #: pod/podfile/static/podfile/js/filewidget.js:134 #: pod/podfile/static/podfile/js/filewidget.js:141 +#: pod/static/podfile/js/filewidget.js:134 +#: pod/static/podfile/js/filewidget.js:141 msgid "Change" msgstr "Changer de fichier" #: pod/podfile/static/podfile/js/filewidget.js:149 +#: pod/static/podfile/js/filewidget.js:149 msgid "Enter new name of folder" msgstr "Indiquer un nouveau nom au dossier" @@ -432,168 +496,437 @@ msgstr "Indiquer un nouveau nom au dossier" #: pod/podfile/static/podfile/js/filewidget.js:202 #: pod/podfile/static/podfile/js/filewidget.js:235 #: pod/podfile/static/podfile/js/filewidget.js:254 +#: pod/static/podfile/js/filewidget.js:180 +#: pod/static/podfile/js/filewidget.js:202 +#: pod/static/podfile/js/filewidget.js:235 +#: pod/static/podfile/js/filewidget.js:254 msgid "Server error" msgstr "Erreur du serveur" #: pod/podfile/static/podfile/js/filewidget.js:188 +#: pod/static/podfile/js/filewidget.js:188 msgid "Add" msgstr "Ajouter" #: pod/podfile/static/podfile/js/filewidget.js:268 +#: pod/static/podfile/js/filewidget.js:268 msgid "Are you sure you want to delete this folder?" msgstr "Êtes-vous sûr(e) de vouloir supprimer ce dossier ?" #: pod/podfile/static/podfile/js/filewidget.js:277 +#: pod/static/podfile/js/filewidget.js:277 msgid "Are you sure you want to delete this file?" msgstr "Êtes-vous sûr(e) de vouloir supprimer ce fichier ?" #: pod/podfile/static/podfile/js/filewidget.js:477 +#: pod/static/podfile/js/filewidget.js:477 msgid "See more" msgstr "Voir plus" -#: pod/video/static/js/comment-script.js:61 -#: pod/video/static/js/comment-script.js:166 -msgid "Delete" -msgstr "Supprimer" +#: pod/static/admin/js/SelectFilter2.js:47 +#, javascript-format +msgid "Available %s" +msgstr "" + +#: pod/static/admin/js/SelectFilter2.js:53 +#, javascript-format +msgid "" +"This is the list of available %s. You may choose some by selecting them in " +"the box below and then clicking the \"Choose\" arrow between the two boxes." +msgstr "" + +#: pod/static/admin/js/SelectFilter2.js:69 +#, javascript-format +msgid "Type into this box to filter down the list of available %s." +msgstr "" + +#: pod/static/admin/js/SelectFilter2.js:74 +msgid "Filter" +msgstr "" + +#: pod/static/admin/js/SelectFilter2.js:78 +msgid "Choose all" +msgstr "" + +#: pod/static/admin/js/SelectFilter2.js:78 +#, javascript-format +msgid "Click to choose all %s at once." +msgstr "" + +#: pod/static/admin/js/SelectFilter2.js:84 +msgid "Choose" +msgstr "" + +#: pod/static/admin/js/SelectFilter2.js:86 pod/static/js/comment-script.js:182 +#: pod/video/static/js/comment-script.js:182 +msgid "Remove" +msgstr "" + +#: pod/static/admin/js/SelectFilter2.js:92 +#, javascript-format +msgid "Chosen %s" +msgstr "" + +#: pod/static/admin/js/SelectFilter2.js:98 +#, javascript-format +msgid "" +"This is the list of chosen %s. You may remove some by selecting them in the " +"box below and then clicking the \"Remove\" arrow between the two boxes." +msgstr "" + +#: pod/static/admin/js/SelectFilter2.js:108 +msgid "Remove all" +msgstr "" + +#: pod/static/admin/js/SelectFilter2.js:108 +#, javascript-format +msgid "Click to remove all chosen %s at once." +msgstr "" + +#: pod/static/admin/js/actions.js:48 pod/static/admin/js/actions.min.js:2 +msgid "%(sel)s of %(cnt)s selected" +msgid_plural "%(sel)s of %(cnt)s selected" +msgstr[0] "" +msgstr[1] "" + +#: pod/static/admin/js/actions.js:117 pod/static/admin/js/actions.min.js:4 +msgid "" +"You have unsaved changes on individual editable fields. If you run an " +"action, your unsaved changes will be lost." +msgstr "" + +#: pod/static/admin/js/actions.js:129 pod/static/admin/js/actions.min.js:5 +msgid "" +"You have selected an action, but you haven't saved your changes to " +"individual fields yet. Please click OK to save. You'll need to re-run the " +"action." +msgstr "" + +#: pod/static/admin/js/actions.js:131 pod/static/admin/js/actions.min.js:5 +msgid "" +"You have selected an action, and you haven't made any changes on individual " +"fields. You're probably looking for the Go button rather than the Save " +"button." +msgstr "" + +#: pod/static/admin/js/admin/DateTimeShortcuts.js:74 +#, javascript-format +msgid "Note: You are %s hour ahead of server time." +msgid_plural "Note: You are %s hours ahead of server time." +msgstr[0] "" +msgstr[1] "" + +#: pod/static/admin/js/admin/DateTimeShortcuts.js:82 +#, javascript-format +msgid "Note: You are %s hour behind server time." +msgid_plural "Note: You are %s hours behind server time." +msgstr[0] "" +msgstr[1] "" + +#: pod/static/admin/js/admin/DateTimeShortcuts.js:109 +#: pod/static/admin/js/admin/DateTimeShortcuts.js:160 +msgid "Now" +msgstr "" + +#: pod/static/admin/js/admin/DateTimeShortcuts.js:127 +msgid "Choose a Time" +msgstr "" + +#: pod/static/admin/js/admin/DateTimeShortcuts.js:157 +msgid "Choose a time" +msgstr "" + +#: pod/static/admin/js/admin/DateTimeShortcuts.js:165 +msgid "Midnight" +msgstr "" + +#: pod/static/admin/js/admin/DateTimeShortcuts.js:170 +msgid "6 a.m." +msgstr "" + +#: pod/static/admin/js/admin/DateTimeShortcuts.js:175 +msgid "Noon" +msgstr "" + +#: pod/static/admin/js/admin/DateTimeShortcuts.js:180 +msgid "6 p.m." +msgstr "" -#: pod/video/static/js/comment-script.js:80 +#: pod/static/admin/js/admin/DateTimeShortcuts.js:188 +#: pod/static/admin/js/admin/DateTimeShortcuts.js:348 +#: pod/static/js/comment-script.js:93 pod/video/static/js/comment-script.js:93 msgid "Cancel" msgstr "Annuler" -#: pod/video/static/js/comment-script.js:143 -msgid "Answer comment icon" -msgstr "Icon réponse commentaire" +#: pod/static/admin/js/admin/DateTimeShortcuts.js:253 +#: pod/static/admin/js/admin/DateTimeShortcuts.js:333 +msgid "Today" +msgstr "" + +#: pod/static/admin/js/admin/DateTimeShortcuts.js:270 +msgid "Choose a Date" +msgstr "" + +#: pod/static/admin/js/admin/DateTimeShortcuts.js:327 +msgid "Yesterday" +msgstr "" + +#: pod/static/admin/js/admin/DateTimeShortcuts.js:339 +msgid "Tomorrow" +msgstr "" + +#: pod/static/admin/js/calendar.js:12 +msgid "January" +msgstr "" + +#: pod/static/admin/js/calendar.js:13 +msgid "February" +msgstr "" + +#: pod/static/admin/js/calendar.js:14 +msgid "March" +msgstr "" + +#: pod/static/admin/js/calendar.js:15 +msgid "April" +msgstr "" + +#: pod/static/admin/js/calendar.js:16 +msgid "May" +msgstr "" + +#: pod/static/admin/js/calendar.js:17 +msgid "June" +msgstr "" + +#: pod/static/admin/js/calendar.js:18 +msgid "July" +msgstr "" + +#: pod/static/admin/js/calendar.js:19 +msgid "August" +msgstr "" + +#: pod/static/admin/js/calendar.js:20 +msgid "September" +msgstr "" + +#: pod/static/admin/js/calendar.js:21 +msgid "October" +msgstr "" -#: pod/video/static/js/comment-script.js:143 -msgid "Answer" +#: pod/static/admin/js/calendar.js:22 +msgid "November" +msgstr "" + +#: pod/static/admin/js/calendar.js:23 +msgid "December" +msgstr "" + +#: pod/static/admin/js/calendar.js:26 +msgctxt "one letter Sunday" +msgid "S" +msgstr "" + +#: pod/static/admin/js/calendar.js:27 +msgctxt "one letter Monday" +msgid "M" +msgstr "" + +#: pod/static/admin/js/calendar.js:28 +msgctxt "one letter Tuesday" +msgid "T" +msgstr "" + +#: pod/static/admin/js/calendar.js:29 +msgctxt "one letter Wednesday" +msgid "W" +msgstr "" + +#: pod/static/admin/js/calendar.js:30 +msgctxt "one letter Thursday" +msgid "T" +msgstr "" + +#: pod/static/admin/js/calendar.js:31 +msgctxt "one letter Friday" +msgid "F" +msgstr "" + +#: pod/static/admin/js/calendar.js:32 +msgctxt "one letter Saturday" +msgid "S" +msgstr "" + +#: pod/static/admin/js/collapse.js:10 pod/static/admin/js/collapse.js:21 +#: pod/static/admin/js/collapse.min.js:1 +msgid "Show" +msgstr "Afficher" + +#: pod/static/admin/js/collapse.js:18 pod/static/admin/js/collapse.min.js:1 +msgid "Hide" +msgstr "Cacher" + +#: pod/static/js/comment-script.js:72 pod/video/static/js/comment-script.js:72 +msgid "Delete" +msgstr "Supprimer" + +#: pod/static/js/comment-script.js:159 +#: pod/video/static/js/comment-script.js:159 +msgid "Reply to comment" +msgstr "Répondre au commentaire" + +#: pod/static/js/comment-script.js:159 +#: pod/video/static/js/comment-script.js:159 +msgid "Reply" msgstr "Répondre" -#: pod/video/static/js/comment-script.js:152 -msgid "Comment vote icon" -msgstr "Icon vote commentaire" +#: pod/static/js/comment-script.js:168 +#: pod/video/static/js/comment-script.js:168 +msgid "Agree with the comment" +msgstr "D'accord avec ce commentaire" -#: pod/video/static/js/comment-script.js:163 -msgid "Comment delete icon" -msgstr "Icon suppression commentaire" +#: pod/static/js/comment-script.js:179 +#: pod/video/static/js/comment-script.js:179 +msgid "Remove this comment" +msgstr "Supprimer ce commentaire" -#: pod/video/static/js/comment-script.js:180 +#: pod/static/js/comment-script.js:196 +#: pod/video/static/js/comment-script.js:196 msgid "Add a public comment" msgstr "Ajouter un commentaire public" -#: pod/video/static/js/comment-script.js:261 -msgid "Comment show children icon" -msgstr "Icon affichage commentaire enfant" +#: pod/static/js/comment-script.js:280 +#: pod/video/static/js/comment-script.js:280 +msgid "Answers" +msgstr "Réponses" -#: pod/video/static/js/comment-script.js:264 -#: pod/video/static/js/comment-script.js:571 -msgid "Show responses" +#: pod/static/js/comment-script.js:285 +#: pod/video/static/js/comment-script.js:285 +msgid "Show answers" msgstr "Afficher les réponses" -#: pod/video/static/js/comment-script.js:427 +#: pod/static/js/comment-script.js:456 +#: pod/video/static/js/comment-script.js:456 msgid "Comment has been deleted successfully." msgstr "Commentaire a été supprimé avec succès." -#: pod/video/static/js/comment-script.js:569 -msgid "Hide responses" -msgstr "Cacher les réponses" - +#: pod/static/js/validate-date_delete-field.js:26 #: pod/video/static/js/validate-date_delete-field.js:26 msgid "The date must be before or equal to" msgstr "La date doit être anterieure ou égale à" -#: pod/video/static/js/video_category.js:25 +#: pod/static/js/video_category.js:25 pod/video/static/js/video_category.js:25 msgid "Category changes saved successfully" msgstr "Les changements sur la catégorie ont été sauvegardés avec succès." -#: pod/video/static/js/video_category.js:26 +#: pod/static/js/video_category.js:26 pod/video/static/js/video_category.js:26 msgid "You cannot add two categories with the same title." msgstr "Vous ne pouvez pas ajouter deux catégories avec le même titre." -#: pod/video/static/js/video_category.js:27 +#: pod/static/js/video_category.js:27 pod/video/static/js/video_category.js:27 msgid "Category deleted successfully" msgstr "Catégorie supprimée avec succès." -#: pod/video/static/js/video_category.js:28 +#: pod/static/js/video_category.js:28 pod/video/static/js/video_category.js:28 msgid "An error occured, please refresh the page and try again." msgstr "" "Une erreur est survenue, Veuillez recharger la page et rééssayer à nouveau." -#: pod/video/static/js/video_category.js:29 +#: pod/static/js/video_category.js:29 pod/video/static/js/video_category.js:29 msgid "Category title field is required." msgstr "Le champ category titre est requis." -#: pod/video/static/js/video_category.js:30 +#: pod/static/js/video_category.js:30 pod/video/static/js/video_category.js:30 msgid "Save category" msgstr "Sauvegarder catégorie" +#: pod/static/js/video_category.js:147 #: pod/video/static/js/video_category.js:147 msgid "videos found" msgstr "vidéos trouvées" +#: pod/static/js/video_category.js:147 #: pod/video/static/js/video_category.js:147 msgid "video found" msgstr "vidéo trouvée" +#: pod/static/js/video_category.js:152 #: pod/video/static/js/video_category.js:152 msgid "Sorry, no video found" msgstr "Désolé, aucune vidéo trouvée" +#: pod/static/js/video_category.js:391 #: pod/video/static/js/video_category.js:391 msgid "Edit the category" msgstr "Éditer la catégorie" +#: pod/static/js/video_category.js:404 #: pod/video/static/js/video_category.js:404 msgid "Delete the category" msgstr "Supprimer la catégorie" +#: pod/static/js/video_category.js:465 #: pod/video/static/js/video_category.js:465 msgid "This content is chaptered." msgstr "Ce contenu est chapitré" +#: pod/static/js/video_category.js:474 #: pod/video/static/js/video_category.js:474 msgid "This content is in draft." msgstr "Ce contenu est en privé" +#: pod/static/js/video_category.js:492 #: pod/video/static/js/video_category.js:492 msgid "Audio content." msgstr "Contenu audio" +#: pod/static/js/video_category.js:549 #: pod/video/static/js/video_category.js:549 msgid "Success.." msgstr "Succès.." +#: pod/static/js/video_category.js:550 #: pod/video/static/js/video_category.js:550 msgid "Error.." msgstr "Erreur.." +#: pod/static/js/video_category.js:768 #: pod/video/static/js/video_category.js:768 msgid "Category created successfully" msgstr "Catégorie créée avec succès." +#: pod/static/js/video_category.js:788 #: pod/video/static/js/video_category.js:788 msgid "Create category" msgstr "Créer catégorie" +#: pod/static/js/video_stats_view.js:14 #: pod/video/static/js/video_stats_view.js:14 msgid "Title" msgstr "Titre" +#: pod/static/js/video_stats_view.js:15 #: pod/video/static/js/video_stats_view.js:15 msgid "View during the day" msgstr "Vue de la journée" +#: pod/static/js/video_stats_view.js:16 #: pod/video/static/js/video_stats_view.js:16 msgid "View during the month" msgstr "Vue du mois" +#: pod/static/js/video_stats_view.js:17 #: pod/video/static/js/video_stats_view.js:17 msgid "View during the year" msgstr "Vue de l'année" +#: pod/static/js/video_stats_view.js:18 #: pod/video/static/js/video_stats_view.js:18 msgid "Total view from creation" msgstr "Vue totale depuis création" +#: pod/static/js/video_stats_view.js:19 #: pod/video/static/js/video_stats_view.js:19 msgid "Slug" msgstr "Titre court" From 8d0c0513da66859a19b700443099a3edb7a4d260 Mon Sep 17 00:00:00 2001 From: Eliam LOTONGA Date: Mon, 11 Jan 2021 11:22:34 +0100 Subject: [PATCH 033/145] Show number of comment replies --- pod/video/static/js/comment-script.js | 45 ++++++++++++++++----------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/pod/video/static/js/comment-script.js b/pod/video/static/js/comment-script.js index e1212860ef..aad425c83e 100644 --- a/pod/video/static/js/comment-script.js +++ b/pod/video/static/js/comment-script.js @@ -82,7 +82,7 @@ class ConfirmModal extends HTMLElement if(parent_el.classList.contains("comments_children_container") && parent_el.childElementCount===0) { parent_e = get_node(parent_el, "comment_element", "comment_child"); - hide_or_add_show_children_btn(parent_e); + hide_or_add_show_children_btn(parent_e, parent_el.childElementCount); } delete_comment(); delete_comment_child_DOM(); @@ -156,7 +156,7 @@ class Comment extends HTMLElement if(user_id) { let svg_icon = ``; - let response_action = createFooterBtnAction( "comment_actions comment_response_action", "icon comment_response_icon", gettext("Answer comment icon"), svg_icon, "comment_response_btn", gettext("Answer")); + let response_action = createFooterBtnAction( "comment_actions comment_response_action", "icon comment_response_icon", gettext("Reply to comment"), svg_icon, "comment_response_btn", gettext("Reply")); response_action.addEventListener("click", function() { let target_node = get_node(this, "form"); @@ -165,7 +165,7 @@ class Comment extends HTMLElement }); comment_container.querySelector(".comment_content_footer .actions").appendChild(response_action); svg_icon = [``, ``]; - let vote_action = createFooterBtnAction( "comment_actions comment_vote_action", "icon comment_vote_icon", gettext("Comment vote icon"), svg_icon, "comment_vote_btn", likes, id ); + let vote_action = createFooterBtnAction( "comment_actions comment_vote_action", "icon comment_vote_icon", gettext("Agree with the comment"), svg_icon, "comment_vote_btn", likes, id ); vote_action.addEventListener("click", ()=>{ let comment_id = get_comment_attribute(document.getElementById(id)); vote(comment_id, vote_action); @@ -176,7 +176,7 @@ class Comment extends HTMLElement let delete_action = createFooterBtnAction( "comment_actions comment_delete_action", "icon comment_delete_icon", - gettext("Comment delete icon"), + gettext("Remove this comment"), svg_icon, "comment_delete_btn", gettext("Delete"), @@ -215,7 +215,7 @@ class Comment extends HTMLElement add_comment.parentElement.classList.toggle("show"); if(!comment_parent.classList.contains("show")) comment_parent.classList.add("show") - hide_show_child_comment_text(this) + hide_show_child_comment_text(this, comment_parent.querySelector("comments_children_container").childElementCount) add_child_comment( this, child_container, comment_parent ); this.value = ""; } @@ -273,22 +273,24 @@ function createFooterBtnAction( classes, icon_classes, title, svg, span_classes, el.appendChild(el_span); return el; } -function hide_or_add_show_children_btn(parent_comment=null) +function hide_or_add_show_children_btn(parent_comment=null, nb_child=null) { let svg_icon_show = `` let svg_icon_hide = `` + let txt = gettext("Answers"); + txt = nb_child?`${nb_child} ${txt}`: txt; let children_action = createFooterBtnAction( "comment_actions comment_show_children_action", "icon comment_show_children_icon", - gettext("Comment show children icon"), + gettext("Show answers"), [svg_icon_show, svg_icon_hide], "comment_show_children_btn", - gettext("Show responses") + txt ); children_action.addEventListener("click", function() { get_node(this.parentElement, "comment_element", "comment_child").classList.toggle("show"); - hide_show_child_comment_text(this); + hide_show_child_comment_text(this, nb_child); }); if(parent_comment) @@ -337,7 +339,7 @@ function hide_or_add_show_children_btn(parent_comment=null) comment_children_container.querySelector(".actions .comment_show_children_action") ) } - hide_show_child_comment_text(comments_children_container); + hide_show_child_comment_text(comments_children_container, nb_child); }) } } @@ -611,16 +613,17 @@ function add_child_comment(el, container_el, parent_comment) setBorderLeftColor(c, child_direct_parent) //container_el.prepend(c); container_el.appendChild(c); - hide_or_add_show_children_btn(get_node(container_el, "comment_element", "comment_child")); + + hide_or_add_show_children_btn( + get_node(container_el, "comment_element", "comment_child"), + parent_comment.querySelector('.comments_children_container').childElementCount + ); // INSERT INTO DATABASE THE CURRENT COMMENT CHILD let p_id = get_comment_attribute( child_direct_parent ); - let t_p_id = get_comment_attribute( - get_node(el, 'comment_element', 'comment_child') // get the top parent element - ) + let t_p_id = get_comment_attribute(parent_comment); // Scroll to the comment child - //if(window.scrollY > 10) scrollToComment( c ); save_comment(el.value, date_added.toISOString(), p_id, t_p_id); @@ -686,14 +689,18 @@ function get_node(el, class_name, not) /******* Manage hide/show child comment text ******** ******************************************************/ -function hide_show_child_comment_text(el) +function hide_show_child_comment_text(el, nb_child=null) { + nb_child = nb_child?nb_child:''; let node = get_node(el, "comment_element", "comment_child"); let target_node = get_node(el, "comment_show_children_btn"); if(node.classList.contains("show")) target_node.innerText = gettext("Hide responses"); - else - target_node.innerText = gettext("Show responses"); + else{ + let txt = gettext("Answers"); + txt = `${nb_child} ${txt}`; + target_node.innerText = txt; + } } /***************** Manage vote svg ****************** @@ -774,7 +781,7 @@ fetch(base_vote_url).then(response=>{ parent_container.querySelector('form.add_parent_comment').after(parent_c); else parent_container.prepend(parent_c); - hide_or_add_show_children_btn(parent_c); + hide_or_add_show_children_btn(parent_c, children_data.length); }); }); }); From 12c4610f74f969b6c9e5bc96beb2bbc98f4953fe Mon Sep 17 00:00:00 2001 From: ptitloup Date: Mon, 11 Jan 2021 16:13:06 +0100 Subject: [PATCH 034/145] add save vtt function to store webvtt file, send email on transcript completion --- pod/video/remote_transcript.py | 72 ++++++++++++++++++++++++++++++++-- 1 file changed, 69 insertions(+), 3 deletions(-) diff --git a/pod/video/remote_transcript.py b/pod/video/remote_transcript.py index d9e082ce12..2349662870 100644 --- a/pod/video/remote_transcript.py +++ b/pod/video/remote_transcript.py @@ -1,4 +1,6 @@ from django.conf import settings +from django.core.files import File + import time import json @@ -12,14 +14,25 @@ from .models import Video, EncodingLog from .utils import change_encoding_step, add_encoding_log, check_file -from .utils import send_email, create_outputdir +from .utils import send_email, create_outputdir, send_email_transcript + +from pod.completion.models import Track from webvtt import WebVTT, Caption +from tempfile import NamedTemporaryFile log = logging.getLogger(__name__) DEBUG = getattr(settings, 'DEBUG', True) +if getattr(settings, 'USE_PODFILE', False): + from pod.podfile.models import CustomFileModel + from pod.podfile.models import UserFolder + FILEPICKER = True +else: + FILEPICKER = False + from pod.main.models import CustomFileModel + SSH_TRANSCRIPT_REMOTE_USER = getattr( settings, 'SSH_TRANSCRIPT_REMOTE_USER', "") SSH_TRANSCRIPT_REMOTE_HOST = getattr( @@ -31,6 +44,9 @@ SENTENCE_MAX_LENGTH = getattr(settings, 'SENTENCE_MAX_LENGTH', 3) +EMAIL_ON_TRANSCRIPTING_COMPLETION = getattr( + settings, 'EMAIL_ON_TRANSCRIPTING_COMPLETION', True) + # ########################################################################## # REMOTE TRANSCRIPT VIDEO : MAIN FUNCTION # ########################################################################## @@ -138,7 +154,7 @@ def store_remote_transcripting_video(video_id): print(json.dumps(info_video, indent=2)) webvtt = WebVTT() - + # They're sorted by confidence. First one is highest confidence result. words = info_video["transcripts"][0]["words"] """ for transcript in info_video["transcripts"]: @@ -165,7 +181,14 @@ def store_remote_transcripting_video(video_id): duration = 0 else: duration += word['duration'] - print(webvtt) + if DEBUG: + print(webvtt) + msg += saveVTT(video_to_encode, webvtt) + add_encoding_log(video_id, msg) + change_encoding_step(video_id, 0, "done") + # envois mail fin transcription + if EMAIL_ON_TRANSCRIPTING_COMPLETION: + send_email_transcript(video_to_encode) else: msg += "Wrong file or path : "\ @@ -179,3 +202,46 @@ def format_time_caption(time_caption): return (dt.datetime.utcfromtimestamp(0) + timedelta(seconds=float(time_caption)) ).strftime('%H:%M:%S.%f')[:-3] + + +def saveVTT(video, webvtt): + msg = "\nSAVE TRANSCRIPT WEBVTT : %s" % time.ctime() + lang = video.main_lang + temp_vtt_file = NamedTemporaryFile(suffix='.vtt') + webvtt.save(temp_vtt_file.name) + if webvtt.captions: + msg += "\nstore vtt file in bdd with CustomFileModel model file field" + if FILEPICKER: + videodir, created = UserFolder.objects.get_or_create( + name='%s' % video.slug, + owner=video.owner) + """ + previousSubtitleFile = CustomFileModel.objects.filter( + name__startswith="subtitle_%s" % lang, + folder=videodir, + created_by=video.owner + ) + """ + # for subt in previousSubtitleFile: + # subt.delete() + subtitleFile, created = CustomFileModel.objects.get_or_create( + name="subtitle_%s_%s" % (lang, time.strftime("%Y%m%d-%H%M%S")), + folder=videodir, + created_by=video.owner) + if subtitleFile.file and os.path.isfile(subtitleFile.file.path): + os.remove(subtitleFile.file.path) + else: + subtitleFile, created = CustomFileModel.objects.get_or_create() + + subtitleFile.file.save("subtitle_%s_%s.vtt" % ( + lang, time.strftime("%Y%m%d-%H%M%S")), File(temp_vtt_file)) + msg += "\nstore vtt file in bdd with Track model src field" + + subtitleVtt, created = Track.objects.get_or_create( + video=video, lang=lang) + subtitleVtt.src = subtitleFile + subtitleVtt.lang = lang + subtitleVtt.save() + else: + msg += "\nERROR SUBTITLES Output size is 0" + return msg From 028079db4c1bac42d727917933e2c9953002ff4d Mon Sep 17 00:00:00 2001 From: Eliam LOTONGA Date: Mon, 11 Jan 2021 16:20:20 +0100 Subject: [PATCH 035/145] Add counter comments --- pod/video/static/js/comment-script.js | 71 ++++++++++++++++++++------- 1 file changed, 52 insertions(+), 19 deletions(-) diff --git a/pod/video/static/js/comment-script.js b/pod/video/static/js/comment-script.js index aad425c83e..7f4abb0f04 100644 --- a/pod/video/static/js/comment-script.js +++ b/pod/video/static/js/comment-script.js @@ -2,7 +2,8 @@ let base_url = window.location.href.replace("/video/", "/comment/"); let base_vote_url = base_url.replace('comment', 'comment/vote'); let base_delete_url = base_url.replace('comment', 'comment/del'); let all_comment = null; -let lang_btn = document.querySelector(".btn-lang.btn-lang-active"); +const lang_btn = document.querySelector(".btn-lang.btn-lang-active"); +const comment_label = document.querySelector('.comment_label'); let VOTED_USERS = []; const COLORS = [ "#5F616D", @@ -84,8 +85,7 @@ class ConfirmModal extends HTMLElement parent_e = get_node(parent_el, "comment_element", "comment_child"); hide_or_add_show_children_btn(parent_e, parent_el.childElementCount); } - delete_comment(); - delete_comment_child_DOM(); + delete_comment(ACTION_COMMENT.comment_to_delete); ACTION_COMMENT.comment_to_delete = null; }); let cancel_btn = document.createElement("BUTTON"); @@ -215,7 +215,7 @@ class Comment extends HTMLElement add_comment.parentElement.classList.toggle("show"); if(!comment_parent.classList.contains("show")) comment_parent.classList.add("show") - hide_show_child_comment_text(this, comment_parent.querySelector("comments_children_container").childElementCount) + hide_show_child_comment_text(this, comment_parent.querySelector(".comments_children_container").childElementCount) add_child_comment( this, child_container, comment_parent ); this.value = ""; } @@ -417,7 +417,7 @@ function save_comment(content, date, parent_id=null, top_parent_id=null) parent_comment: c, children: [] } - all_comment.push(p_c); + all_comment = [...all_comment, p_c]; } else { @@ -428,6 +428,7 @@ function save_comment(content, date, parent_id=null, top_parent_id=null) return comment; }); } + set_comments_number() }); } else{ console.log("Mauvaise réponse du réseau", response); } @@ -440,9 +441,10 @@ function save_comment(content, date, parent_id=null, top_parent_id=null) /**************** Delete Comment **************** ******************************************************/ -function delete_comment() +function delete_comment(comment) { - let comment_id = get_comment_attribute(ACTION_COMMENT.comment_to_delete); + let comment_id = get_comment_attribute(comment); + let is_child = !!get_comment_attribute(comment, 'parent__id'); let url = base_delete_url + comment_id + '/'; let data = new FormData(); data.append('csrfmiddlewaretoken', Cookies.get('csrftoken')); @@ -452,8 +454,15 @@ function delete_comment() } ).then(response=>{ response.json().then( data=>{ - if(data.deleted) + if(data.deleted){ document.body.appendChild(new AlertMessage(gettext("Comment has been deleted successfully."))); + + if(is_child) delete_comment_child_DOM(comment, is_child); + + else all_comment = all_comment.filter(c => c.parent_comment.id != data.comment_deleted ); + + set_comments_number(); + } else document.body.appendChild(new AlertMessage(data.message)); }); @@ -463,20 +472,33 @@ function delete_comment() }) } -function delete_comment_child_DOM() +function delete_comment_child_DOM(comment, is_child) { - let comment_id = get_comment_attribute(ACTION_COMMENT.comment_to_delete); - let comment_top_parent_id = get_comment_attribute(ACTION_COMMENT.comment_to_delete, attr='top_parent__id'); - all_comment - .filter(p_comment => p_comment.parent_comment.id === comment_top_parent_id)[0] - .children - .forEach( c_comment => { - if(c_comment.parent__id === comment_id){ - let html_id = `#comment_${new Date(c_comment.added).getTime()}`; - let c = document.querySelector(html_id) - c.parentElement.removeChild(c); + if(is_child){ + let comment_id = get_comment_attribute(comment); + let comment_top_parent_id = get_comment_attribute(comment, attr='top_parent__id'); + + all_comment = all_comment.map(p_comment => { + if(p_comment.parent_comment.id === comment_top_parent_id){ + console.log(p_comment); + p_comment.children = p_comment.children + .filter(c_comment => c_comment.id !== comment_id) + .filter( c_comment => { + console.log(c_comment.parent__id, comment_id) + console.log(p_comment.children) + if(c_comment.parent__id === comment_id){ + // Remove comment html element from DOM + let html_id = `#comment_${new Date(c_comment.added).getTime()}`; + let c = document.querySelector(html_id) + c.parentElement.removeChild(c); + } + return c_comment.parent__id !== comment_id; // Remove comment data from all_comment + }); + return p_comment; } + return p_comment; }); + } } /*********** Scroll to a specific comment ************* @@ -715,6 +737,16 @@ function manage_vote_frontend(id, el) }); } +/******* Set number comments in comment label ********* + * ****************************************************/ +function set_comments_number() +{ + let label_text = comment_label.innerText.replace(/\d+\s+/, ''); + let nb_comments = all_comment.reduce( (acc, curr) => acc += curr.children.length, 0) + all_comment.length; + console.log("number comments", nb_comments); + comment_label.innerText = `${nb_comments} ${label_text}`; +} + /************ Get vote from the server ************** ******************************************************/ fetch(base_vote_url).then(response=>{ @@ -727,6 +759,7 @@ fetch(base_vote_url).then(response=>{ fetch(base_url).then(response=>{ response.json().then(data=>{ all_comment = data; + set_comments_number() data.forEach(comment_data=> { let parent_container = document.querySelector('.comment_container .comment_content'); From 0e6b88dc6c65f8cf5c9fa58ba89c750f77f9dae0 Mon Sep 17 00:00:00 2001 From: ptitloup Date: Mon, 11 Jan 2021 16:25:13 +0100 Subject: [PATCH 036/145] fix flake8 compliant --- pod/video/remote_transcript.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/pod/video/remote_transcript.py b/pod/video/remote_transcript.py index 2349662870..9016234a40 100644 --- a/pod/video/remote_transcript.py +++ b/pod/video/remote_transcript.py @@ -149,9 +149,8 @@ def store_remote_transcripting_video(video_id): with open(output_dir + "/transcript.json") as json_file: info_video = json.load(json_file) - if DEBUG: - print(output_dir) - print(json.dumps(info_video, indent=2)) + print_if_debug(output_dir) + print_if_debug(json.dumps(info_video, indent=2)) webvtt = WebVTT() # They're sorted by confidence. First one is highest confidence result. @@ -181,8 +180,7 @@ def store_remote_transcripting_video(video_id): duration = 0 else: duration += word['duration'] - if DEBUG: - print(webvtt) + print_if_debug(webvtt) msg += saveVTT(video_to_encode, webvtt) add_encoding_log(video_id, msg) change_encoding_step(video_id, 0, "done") @@ -198,6 +196,11 @@ def store_remote_transcripting_video(video_id): send_email(msg, video_id) +def print_if_debug(msg): + if DEBUG: + print(msg) + + def format_time_caption(time_caption): return (dt.datetime.utcfromtimestamp(0) + timedelta(seconds=float(time_caption)) From 885b82bd5500b5f3097cb7e3419fc145a2c6083d Mon Sep 17 00:00:00 2001 From: Eliam LOTONGA Date: Mon, 11 Jan 2021 17:21:33 +0100 Subject: [PATCH 037/145] Using js after function instead of insertBefore --- pod/video/static/js/comment-script.js | 28 +++++---------------------- 1 file changed, 5 insertions(+), 23 deletions(-) diff --git a/pod/video/static/js/comment-script.js b/pod/video/static/js/comment-script.js index 7f4abb0f04..3dc4d90750 100644 --- a/pod/video/static/js/comment-script.js +++ b/pod/video/static/js/comment-script.js @@ -295,17 +295,10 @@ function hide_or_add_show_children_btn(parent_comment=null, nb_child=null) if(parent_comment) { - let is_parent_comment_owner = user_id === get_comment_attribute(parent_comment, "author__id"); let children_container = parent_comment.querySelector(".comments_children_container"); if(!parent_comment.querySelector(".actions .comment_show_children_action") && children_container.childElementCount >0) { - if(is_parent_comment_owner || is_video_owner || is_superuser){ - parent_comment.querySelector(".comment_content_footer .actions").insertBefore( - children_action, parent_comment.querySelector(".comment_content_footer .actions .comment_delete_action") ); - } - else{ - parent_comment.querySelector(".comment_content_footer .actions").appendChild(children_action ); - } + parent_comment.querySelector(".comment_content_footer .actions .comment_vote_action").after( children_action ); } else if(parent_comment.querySelector(".actions .comment_show_children_action") && children_container.childElementCount===0) { @@ -323,14 +316,7 @@ function hide_or_add_show_children_btn(parent_comment=null, nb_child=null) let comments_children_container= comment.querySelector(".comment .comment_container .comments_children_container"); if(!comment.querySelector(".comment_content_footer .actions .comment_show_children_action") && comments_children_container.childElementCount > 0) { - let is_parent_comment_owner = user_id === get_comment_attribute(comment, "author__id"); - if(is_parent_comment_owner || is_video_owner || is_superuser){ - comment.querySelector(".comment_content_footer .actions").insertBefore( - children_action, comment.querySelector(".comment_content_footer .actions .comment_delete_action") ); - } - else{ - comment.querySelector(".comment_content_footer .actions").appendChild(children_action ); - } + comment.querySelector(".comment_content_footer .actions .comment_vote_action").after( children_action ); } else if(comment.querySelector(".comment_content_footer .actions .comment_show_children_action") && comments_children_container.childElementCount===0) { @@ -716,13 +702,9 @@ function hide_show_child_comment_text(el, nb_child=null) nb_child = nb_child?nb_child:''; let node = get_node(el, "comment_element", "comment_child"); let target_node = get_node(el, "comment_show_children_btn"); - if(node.classList.contains("show")) - target_node.innerText = gettext("Hide responses"); - else{ - let txt = gettext("Answers"); - txt = `${nb_child} ${txt}`; - target_node.innerText = txt; - } + let txt = gettext("Answers"); + txt = `${nb_child} ${txt}`; + target_node.innerText = txt; } /***************** Manage vote svg ****************** From fa92205d8a61a250028abf00e198f9a18a8869d3 Mon Sep 17 00:00:00 2001 From: Eliam LOTONGA Date: Mon, 11 Jan 2021 17:21:52 +0100 Subject: [PATCH 038/145] Size text --- pod/video/static/css/comment-style.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pod/video/static/css/comment-style.css b/pod/video/static/css/comment-style.css index f9882d8afc..ca64e40c6a 100644 --- a/pod/video/static/css/comment-style.css +++ b/pod/video/static/css/comment-style.css @@ -476,7 +476,7 @@ margin: 1em 0 2em 0; font-weight: 400; word-break: break-all; - + font-size: 14px; } .comment_main .comment .comment_content_footer { @@ -500,6 +500,7 @@ -ms-flex-align: center; align-items: center; cursor: pointer; + font-size: 13px; } .comment_main .comment .comment_content_footer >* .icon { From 36f5a53c43adf59d6cc59825cef32ea532c2f9c7 Mon Sep 17 00:00:00 2001 From: Eliam LOTONGA Date: Mon, 11 Jan 2021 17:42:14 +0100 Subject: [PATCH 039/145] Review css --- pod/video/static/css/comment-style.css | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pod/video/static/css/comment-style.css b/pod/video/static/css/comment-style.css index ca64e40c6a..92076917b8 100644 --- a/pod/video/static/css/comment-style.css +++ b/pod/video/static/css/comment-style.css @@ -434,8 +434,8 @@ border-left: 4px solid var(--primary); padding: .4em; background-color: var(--third-background-color); - -webkit-box-shadow: 1px 1px 4px var(--background-color); - box-shadow: 1px 1px 4px var(--background-color); + -webkit-box-shadow: 2px 2px 12px #e6e2e2; + box-shadow: 2px 2px 12px #e6e2e2; border-top-right-radius: 4px; border-bottom-right-radius: 4px; } @@ -452,7 +452,7 @@ display: flex; margin-top: calc(var(--margin-between-comment) - .2em); } -.comment_main .comment_element .comment_container .comment_element > .comment_child_container .comments_icon{margin-left: 0; flex: none;} +.comment_main .comment_element .comment_container .comment_element > .comment_child_container .comments_icon{margin-left: 0; flex: none; align-self: center} .comment_main .comment .comment_content_child { @@ -467,6 +467,7 @@ .comment_main .comment .comment_content_header .comment_since { color: var(--content-third-color); + font-size: 14px; } .comment_main .comment .comment_content_body { From 2a192fb2858fa96dd2ee56543c320e647fae3ae2 Mon Sep 17 00:00:00 2001 From: Eliam LOTONGA Date: Tue, 12 Jan 2021 07:00:28 +0100 Subject: [PATCH 040/145] Manage[ctrl+Enter, shift+Enter] new line on replying to a comment --- pod/video/static/js/comment-script.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/pod/video/static/js/comment-script.js b/pod/video/static/js/comment-script.js index 3dc4d90750..2fb8594fae 100644 --- a/pod/video/static/js/comment-script.js +++ b/pod/video/static/js/comment-script.js @@ -205,7 +205,15 @@ class Comment extends HTMLElement }); new_comment.addEventListener("keydown", function(e) { - if (!((e.ctrlKey && e.keyCode === 13) || (e.shiftKey && e.keyCode === 13)) && e.keyCode === 13) { + if ( + (e.ctrlKey && e.keyCode === 13) || + (e.shiftKey && e.keyCode === 13) + ){ + e.preventDefault(); + this.value = this.value + '\r\n'; + this.scrollTop = this.scrollHeight; + } + else if (e.keyCode === 13) { let child_container = get_node(this, "comments_children_container"); if(!child_container.classList.contains("show")) child_container.classList.add("show"); @@ -466,12 +474,9 @@ function delete_comment_child_DOM(comment, is_child) all_comment = all_comment.map(p_comment => { if(p_comment.parent_comment.id === comment_top_parent_id){ - console.log(p_comment); p_comment.children = p_comment.children .filter(c_comment => c_comment.id !== comment_id) .filter( c_comment => { - console.log(c_comment.parent__id, comment_id) - console.log(p_comment.children) if(c_comment.parent__id === comment_id){ // Remove comment html element from DOM let html_id = `#comment_${new Date(c_comment.added).getTime()}`; @@ -725,7 +730,6 @@ function set_comments_number() { let label_text = comment_label.innerText.replace(/\d+\s+/, ''); let nb_comments = all_comment.reduce( (acc, curr) => acc += curr.children.length, 0) + all_comment.length; - console.log("number comments", nb_comments); comment_label.innerText = `${nb_comments} ${label_text}`; } From 6c24d325640d16726f9c7a1bc7f5b701d058b890 Mon Sep 17 00:00:00 2001 From: Eliam LOTONGA Date: Tue, 12 Jan 2021 10:04:01 +0100 Subject: [PATCH 041/145] Fix get vote not need auth, format code --- pod/video/static/js/comment-script.js | 814 ++++++++++++-------------- 1 file changed, 382 insertions(+), 432 deletions(-) diff --git a/pod/video/static/js/comment-script.js b/pod/video/static/js/comment-script.js index 2fb8594fae..a688bf4d3c 100644 --- a/pod/video/static/js/comment-script.js +++ b/pod/video/static/js/comment-script.js @@ -4,7 +4,7 @@ let base_delete_url = base_url.replace('comment', 'comment/del'); let all_comment = null; const lang_btn = document.querySelector(".btn-lang.btn-lang-active"); const comment_label = document.querySelector('.comment_label'); -let VOTED_USERS = []; +let VOTED_USERS = []; const COLORS = [ "#5F616D", "#6B6D79", @@ -16,45 +16,44 @@ const COLORS = [ "#B0B6BE", "#BBC2C9", ] -const LANG = lang_btn?lang_btn.textContent.trim() : "fr"; +const LANG = lang_btn ? lang_btn.textContent.trim() : "fr"; const ACTION_COMMENT = { comment_to_delete: null } -class AlertMessage extends HTMLElement -{ - constructor(message, alert_class="alert_success") - { - super(); - this.setAttribute('class', "alert "+`alert_${alert_class}`); - let html = document.createElement('DIV'); - html.setAttribute("class", "alert_content"); - let content = document.createElement('DIV'); - content.setAttribute('class', 'alert_message'); - content.textContent = message; - html.appendChild(content); - this.appendChild(html); +if (!is_authenticated) + document.querySelector(".comment_counter_container").style.margin = "2em 0"; + +class AlertMessage extends HTMLElement { + constructor(message, alert_class = "alert_success") { + super(); + this.setAttribute('class', "alert " + `alert_${alert_class}`); + let html = document.createElement('DIV'); + html.setAttribute("class", "alert_content"); + let content = document.createElement('DIV'); + content.setAttribute('class', 'alert_message'); + content.textContent = message; + html.appendChild(content); + this.appendChild(html); } - connectedCallback(){ - super.connectedCallback && super.connectedCallback(); - window.setTimeout(()=>{ - this.classList.add('alert_close'); - window.setTimeout(()=>{ - this.parentElement.removeChild(this); - },1000); - }, 3000); + connectedCallback() { + super.connectedCallback && super.connectedCallback(); + window.setTimeout(() => { + this.classList.add('alert_close'); + window.setTimeout(() => { + this.parentElement.removeChild(this); + }, 1000); + }, 3000); } } customElements.define("alert-message", AlertMessage); -class ConfirmModal extends HTMLElement -{ - constructor(title=null, message=null, delete_text=null, cancel_text=null) - { +class ConfirmModal extends HTMLElement { + constructor(title = null, message = null, delete_text = null, cancel_text = null) { super(); - title = title?title: this.getAttribute('confirm_title'); - message = message?message: this.getAttribute("message"); - delete_text = delete_text? delete_text: this.getAttribute("delete_text"); - cancel_text = cancel_text? cancel_text: this.getAttribute("cancel_text"); + title = title ? title : this.getAttribute('confirm_title'); + message = message ? message : this.getAttribute("message"); + delete_text = delete_text ? delete_text : this.getAttribute("delete_text"); + cancel_text = cancel_text ? cancel_text : this.getAttribute("cancel_text"); let modal = `

@@ -70,29 +69,26 @@ class ConfirmModal extends HTMLElement `; let delete_btn = document.createElement("BUTTON"); delete_btn.setAttribute("class", "delete"); - delete_btn.innerHTML = delete_text?delete_text:gettext("Delete"); + delete_btn.innerHTML = delete_text ? delete_text : gettext("Delete"); - delete_btn.addEventListener("click", (e)=> - { + delete_btn.addEventListener("click", (e) => { e.preventDefault(); document.querySelector("#custom_element_confirm_modal .confirm_delete").classList.remove("show"); // Delete comment let parent_el = ACTION_COMMENT.comment_to_delete.parentElement parent_el.removeChild(ACTION_COMMENT.comment_to_delete); // remove show children btn if no child in the node - if(parent_el.classList.contains("comments_children_container") && parent_el.childElementCount===0) - { - parent_e = get_node(parent_el, "comment_element", "comment_child"); + if (parent_el.classList.contains("comments_children_container") && parent_el.childElementCount === 0) { + let parent_e = get_node(parent_el, "comment_element", "comment_child"); hide_or_add_show_children_btn(parent_e, parent_el.childElementCount); } - delete_comment(ACTION_COMMENT.comment_to_delete); + delete_comment(ACTION_COMMENT.comment_to_delete); ACTION_COMMENT.comment_to_delete = null; }); let cancel_btn = document.createElement("BUTTON"); cancel_btn.setAttribute("class", "cancel"); - cancel_btn.innerHTML = cancel_text?cancel_text:gettext("Cancel"); - cancel_btn.addEventListener("click", (e)=> - { + cancel_btn.innerHTML = cancel_text ? cancel_text : gettext("Cancel"); + cancel_btn.addEventListener("click", (e) => { e.preventDefault(); document.querySelector("#custom_element_confirm_modal .confirm_delete").classList.remove("show"); ACTION_COMMENT.comment_to_delete = null; @@ -104,29 +100,25 @@ class ConfirmModal extends HTMLElement } customElements.define("confirm-modal", ConfirmModal); -class CommentSince extends HTMLElement -{ - constructor(since) - { - super(); - since = this.getAttribute("since")? this.getAttribute("since"): since; - since = typeof(since) === "string"? new Date(since): since; - let date_since = moment(since).locale(LANG).fromNow(); - let div = document.createElement("DIV"); - div.setAttribute("class", "comment_since"); - div.innerText = date_since; - this.appendChild(div); - window.setInterval(()=>{ - date_since = moment(since).locale(LANG).fromNow(); - div.innerText = date_since; - }, 60000); +class CommentSince extends HTMLElement { + constructor(since) { + super(); + since = this.getAttribute("since") ? this.getAttribute("since") : since; + since = typeof (since) === "string" ? new Date(since) : since; + let date_since = moment(since).locale(LANG).fromNow(); + let div = document.createElement("DIV"); + div.setAttribute("class", "comment_since"); + div.innerText = date_since; + this.appendChild(div); + window.setInterval(() => { + date_since = moment(since).locale(LANG).fromNow(); + div.innerText = date_since; + }, 60000); } } customElements.define("comment-since", CommentSince); -class Comment extends HTMLElement -{ - constructor( owner, content, likes, added_since, id, is_parent=null, is_comment_owner=false) - { +class Comment extends HTMLElement { + constructor(owner, content, likes, added_since, id, is_parent = null, is_comment_owner = false) { super(); this.setAttribute("class", "comment_element"); this.setAttribute("id", id); @@ -134,8 +126,8 @@ class Comment extends HTMLElement comment.setAttribute("class", "comment"); let comment_container = document.createElement("DIV"); comment_container.setAttribute("class", "comment_container"); - let comment_content = document.createElement('div'); - comment_content.setAttribute('class', 'comment_content'); + let comment_content = document.createElement('div'); + comment_content.setAttribute('class', 'comment_content'); comment_content.innerHTML = `

${owner}

@@ -148,30 +140,32 @@ class Comment extends HTMLElement
`; - comment_container.appendChild(comment_content); - if(is_parent) - comment_container.querySelector('.comment_content_body').innerHTML = content; - else - comment_container.querySelector('.comment_content_body').appendChild(content); - if(user_id) - { - let svg_icon = ``; - let response_action = createFooterBtnAction( "comment_actions comment_response_action", "icon comment_response_icon", gettext("Reply to comment"), svg_icon, "comment_response_btn", gettext("Reply")); - response_action.addEventListener("click", function() - { + comment_container.appendChild(comment_content); + if (is_parent) + comment_container.querySelector('.comment_content_body').innerHTML = content; + else + comment_container.querySelector('.comment_content_body').appendChild(content); + let svg_icon = [``, ``]; + let vote_action = createFooterBtnAction("comment_actions comment_vote_action", "icon comment_vote_icon", gettext("Agree with the comment"), svg_icon, "comment_vote_btn", `${likes} votes`, id); + if (is_authenticated) { + vote_action.addEventListener("click", () => { + let comment_id = get_comment_attribute(document.getElementById(id)); + vote(comment_id, vote_action); + }); + } + comment_container.querySelector(".comment_content_footer .actions").appendChild(vote_action); + + if (user_id) { + svg_icon = ``; + let response_action = createFooterBtnAction("comment_actions comment_response_action", "icon comment_response_icon", gettext("Reply to comment"), svg_icon, "comment_response_btn", gettext("Reply")); + response_action.addEventListener("click", function () { let target_node = get_node(this, "form"); target_node.classList.toggle("show"); this.parentElement.nextElementSibling.querySelector(".new_comment").focus(); }); comment_container.querySelector(".comment_content_footer .actions").appendChild(response_action); - svg_icon = [``, ``]; - let vote_action = createFooterBtnAction( "comment_actions comment_vote_action", "icon comment_vote_icon", gettext("Agree with the comment"), svg_icon, "comment_vote_btn", likes, id ); - vote_action.addEventListener("click", ()=>{ - let comment_id = get_comment_attribute(document.getElementById(id)); - vote(comment_id, vote_action); - }); - comment_container.querySelector(".comment_content_footer .actions").appendChild(vote_action); - if(is_comment_owner || is_video_owner || is_superuser){ + + if (is_comment_owner || is_video_owner || is_superuser) { svg_icon = ``; let delete_action = createFooterBtnAction( "comment_actions comment_delete_action", @@ -182,57 +176,51 @@ class Comment extends HTMLElement gettext("Delete"), id ); - delete_action.addEventListener("click", () => - { + delete_action.addEventListener("click", () => { // display confirm modal document.querySelector("#custom_element_confirm_modal .confirm_delete").classList.add("show"); let el = document.getElementById(id); ACTION_COMMENT.comment_to_delete = el; }); - comment_container.querySelector(".comment_content_footer .actions").appendChild(delete_action); - } + comment_container.querySelector(".comment_content_footer .actions").appendChild(delete_action); + } let add_comment = document.createElement("DIV"); add_comment.setAttribute("class", "add_comment"); - add_comment.innerHTML =''; + add_comment.innerHTML = ''; let new_comment = add_comment.querySelector(".new_comment"); - new_comment.addEventListener("keyup", function(e) - { - if (!((e.ctrlKey && e.keyCode === 13) || (e.shiftKey && e.keyCode === 13)) && e.keyCode === 13) - { + new_comment.addEventListener("keyup", function (e) { + if (!((e.ctrlKey && e.keyCode === 13) || (e.shiftKey && e.keyCode === 13)) && e.keyCode === 13) { this.value = ""; - this.blur(); + this.blur(); } }); - new_comment.addEventListener("keydown", function(e) - { - if ( - (e.ctrlKey && e.keyCode === 13) || - (e.shiftKey && e.keyCode === 13) - ){ - e.preventDefault(); - this.value = this.value + '\r\n'; - this.scrollTop = this.scrollHeight; - } - else if (e.keyCode === 13) { + new_comment.addEventListener("keydown", function (e) { + if ( + (e.ctrlKey && e.keyCode === 13) || + (e.shiftKey && e.keyCode === 13) + ) { + e.preventDefault(); + this.value = this.value + '\r\n'; + this.scrollTop = this.scrollHeight; + } + else if (e.keyCode === 13) { let child_container = get_node(this, "comments_children_container"); - if(!child_container.classList.contains("show")) + if (!child_container.classList.contains("show")) child_container.classList.add("show"); - if(this.value.trim() !== "") - { - let comment_parent = get_node(this, "comment_element", "comment_child" ); + if (this.value.trim() !== "") { + let comment_parent = get_node(this, "comment_element", "comment_child"); add_comment.parentElement.classList.toggle("show"); - if(!comment_parent.classList.contains("show")) + if (!comment_parent.classList.contains("show")) comment_parent.classList.add("show") hide_show_child_comment_text(this, comment_parent.querySelector(".comments_children_container").childElementCount) - add_child_comment( this, child_container, comment_parent ); + add_child_comment(this, child_container, comment_parent); this.value = ""; } } }); comment_container.querySelector(".comment_content_footer .form").appendChild(add_comment); - } - if(is_parent) - { + } + if (is_parent) { let comment_icon_svg = ``; let comment_icon = document.createElement("DIV"); comment_icon.innerHTML = comment_icon_svg; @@ -244,8 +232,7 @@ class Comment extends HTMLElement comment.appendChild(comment_container); this.appendChild(comment); } - else - { + else { let comment_icon_svg = ``; let comment_icon = document.createElement("DIV"); comment_icon.innerHTML = comment_icon_svg; @@ -260,15 +247,14 @@ class Comment extends HTMLElement } } } -function createFooterBtnAction( classes, icon_classes, title, svg, span_classes, span_text, comment_id=null ) -{ +function createFooterBtnAction(classes, icon_classes, title, svg, span_classes, span_text, comment_id = null) { let el = document.createElement("DIV"); el.setAttribute("class", classes); - if(comment_id) el.setAttribute("data-comment", comment_id); + if (comment_id) el.setAttribute("data-comment", comment_id); let el_icon = document.createElement("DIV"); el_icon.setAttribute("class", icon_classes) el_icon.setAttribute("title", title) - if(Array.isArray(svg)) + if (Array.isArray(svg)) el_icon.innerHTML = svg.join(" ") else el_icon.innerHTML = svg @@ -281,35 +267,30 @@ function createFooterBtnAction( classes, icon_classes, title, svg, span_classes, el.appendChild(el_span); return el; } -function hide_or_add_show_children_btn(parent_comment=null, nb_child=null) -{ +function hide_or_add_show_children_btn(parent_comment = null, nb_child = null) { let svg_icon_show = `` let svg_icon_hide = `` let txt = gettext("Answers"); - txt = nb_child?`${nb_child} ${txt}`: txt; + txt = nb_child ? `${nb_child} ${txt}` : txt; let children_action = createFooterBtnAction( "comment_actions comment_show_children_action", "icon comment_show_children_icon", gettext("Show answers"), [svg_icon_show, svg_icon_hide], "comment_show_children_btn", - txt + txt ); - children_action.addEventListener("click", function() - { + children_action.addEventListener("click", function () { get_node(this.parentElement, "comment_element", "comment_child").classList.toggle("show"); hide_show_child_comment_text(this, nb_child); }); - - if(parent_comment) - { + + if (parent_comment) { let children_container = parent_comment.querySelector(".comments_children_container"); - if(!parent_comment.querySelector(".actions .comment_show_children_action") && children_container.childElementCount >0) - { - parent_comment.querySelector(".comment_content_footer .actions .comment_vote_action").after( children_action ); + if (!parent_comment.querySelector(".actions .comment_show_children_action") && children_container.childElementCount > 0) { + parent_comment.querySelector(".comment_content_footer .actions .comment_vote_action").after(children_action); } - else if(parent_comment.querySelector(".actions .comment_show_children_action") && children_container.childElementCount===0) - { + else if (parent_comment.querySelector(".actions .comment_show_children_action") && children_container.childElementCount === 0) { // remove action if exist parent_comment.querySelector(".comment_content_footer .actions").removeChild( parent_comment.querySelector(".comment_content_footer .actions .comment_show_children_action") @@ -319,15 +300,12 @@ function hide_or_add_show_children_btn(parent_comment=null, nb_child=null) else // check all comments { let all_comments = document.querySelectorAll(".comment_element:not(.comment_child)"); - all_comments.forEach(comment=> - { - let comments_children_container= comment.querySelector(".comment .comment_container .comments_children_container"); - if(!comment.querySelector(".comment_content_footer .actions .comment_show_children_action") && comments_children_container.childElementCount > 0) - { - comment.querySelector(".comment_content_footer .actions .comment_vote_action").after( children_action ); + all_comments.forEach(comment => { + let comments_children_container = comment.querySelector(".comment .comment_container .comments_children_container"); + if (!comment.querySelector(".comment_content_footer .actions .comment_show_children_action") && comments_children_container.childElementCount > 0) { + comment.querySelector(".comment_content_footer .actions .comment_vote_action").after(children_action); } - else if(comment.querySelector(".comment_content_footer .actions .comment_show_children_action") && comments_children_container.childElementCount===0) - { + else if (comment.querySelector(".comment_content_footer .actions .comment_show_children_action") && comments_children_container.childElementCount === 0) { // remove action if exist comment.querySelector(".comment_content_footer .actions").removeChild( comment_children_container.querySelector(".actions .comment_show_children_action") @@ -341,168 +319,158 @@ customElements.define("comment-element", Comment) /******************* Voting for a comment ******************** ***************************************************************/ -function vote(comment_id, target_html_el) -{ +function vote(comment_id, target_html_el) { // send request to the server to check if the current user already vote - let vote_url = base_vote_url + comment_id +"/"; + let vote_url = base_vote_url + comment_id + "/"; let btn = target_html_el.querySelector('.comment_vote_btn') let data = new FormData(); data.append('csrfmiddlewaretoken', Cookies.get('csrftoken')); fetch(vote_url, { - method: "POST", - body: data - }).then(response=>{ - response.json().then(data=>{ - if(data.voted === true) - { - VOTED_USERS.push({ - 'user__id': user_id, - 'comment__id': comment_id, - }); - btn.innerHTML = parseInt(btn.textContent)+1; - if(!target_html_el.classList.contains('voted')) - target_html_el.classList.add('voted'); - } - else - { - VOTED_USERS = VOTED_USERS.filter(obj=>{ - obj.comment__id !== comment_id - }) - btn.innerHTML = parseInt(btn.textContent)-1; - if(target_html_el.classList.contains('voted')) - target_html_el.classList.remove('voted'); - } - }) - }).catch(error=>{ - console.log(error); + method: "POST", + body: data + }).then(response => { + response.json().then(data => { + if (data.voted === true) { + VOTED_USERS.push({ + 'user__id': user_id, + 'comment__id': comment_id, + }); + btn.innerHTML = parseInt(btn.textContent) + 1; + if (!target_html_el.classList.contains('voted')) + target_html_el.classList.add('voted'); + } + else { + VOTED_USERS = VOTED_USERS.filter(obj => { + obj.comment__id !== comment_id + }) + btn.innerHTML = parseInt(btn.textContent) - 1; + if (target_html_el.classList.contains('voted')) + target_html_el.classList.remove('voted'); + } + }) + }).catch(error => { + console.log(error); }); } /**************** Save comment into the server **************** ***************************************************************/ -function save_comment(content, date, parent_id=null, top_parent_id=null) -{ +function save_comment(content, date, parent_id = null, top_parent_id = null) { let post_url = base_url.replace("comment", "comment/add"); - post_url = parent_id?post_url+parent_id+'/':post_url + post_url = parent_id ? post_url + parent_id + '/' : post_url let data = new FormData(); data.append('content', content); data.append('date_added', date); data.append('csrfmiddlewaretoken', Cookies.get('csrftoken')); - fetch(post_url,{ + fetch(post_url, { method: "POST", body: data - }).then(response=>{ - if(response.ok) - { - response.json().then(data=>{ - let c = { - author__first_name: data.author__first_name, - author__last_name: data.author__last_name, - content: content, - id: data.id, - added: date, - parent__id: parent_id, - top_parent__id: top_parent_id - } - if(!parent_id) - { - // update all_comment data - let p_c = { - parent_comment: c, - children: [] - } - all_comment = [...all_comment, p_c]; - } - else - { - // update all_comment data - all_comment = all_comment.map((comment)=>{ - if(comment.parent_comment.id === top_parent_id) - comment.children = [...comment.children, c]; - return comment; - }); - } - set_comments_number() - }); - } - else{ console.log("Mauvaise réponse du réseau", response); } + }).then(response => { + if (response.ok) { + response.json().then(data => { + let c = { + author__first_name: data.author__first_name, + author__last_name: data.author__last_name, + content: content, + id: data.id, + added: date, + parent__id: parent_id, + top_parent__id: top_parent_id + } + if (!parent_id) { + // update all_comment data + let p_c = { + parent_comment: c, + children: [] + } + all_comment = [...all_comment, p_c]; + } + else { + // update all_comment data + all_comment = all_comment.map((comment) => { + if (comment.parent_comment.id === top_parent_id) + comment.children = [...comment.children, c]; + return comment; + }); + } + set_comments_number() + }); + } + else { console.log("Mauvaise réponse du réseau", response); } }) - .catch(error =>{ - console.log(error) - console.log(error.message) - }); + .catch(error => { + console.log(error) + console.log(error.message) + }); } /**************** Delete Comment **************** ******************************************************/ -function delete_comment(comment) -{ +function delete_comment(comment) { let comment_id = get_comment_attribute(comment); let is_child = !!get_comment_attribute(comment, 'parent__id'); let url = base_delete_url + comment_id + '/'; let data = new FormData(); data.append('csrfmiddlewaretoken', Cookies.get('csrftoken')); fetch(url, { - method: "POST", - body: data + method: "POST", + body: data } - ).then(response=>{ - response.json().then( data=>{ - if(data.deleted){ - document.body.appendChild(new AlertMessage(gettext("Comment has been deleted successfully."))); - - if(is_child) delete_comment_child_DOM(comment, is_child); - - else all_comment = all_comment.filter(c => c.parent_comment.id != data.comment_deleted ); - - set_comments_number(); - } - else - document.body.appendChild(new AlertMessage(data.message)); - }); - }).catch(error=>{ - console.log(error); - console.log(error.message); + ).then(response => { + response.json().then(data => { + if (data.deleted) { + document.body.appendChild(new AlertMessage(gettext("Comment has been deleted successfully."))); + + if (is_child) delete_comment_child_DOM(comment, is_child); + + else all_comment = all_comment.filter(c => c.parent_comment.id != data.comment_deleted); + + set_comments_number(); + } + else + document.body.appendChild(new AlertMessage(data.message)); + }); + }).catch(error => { + console.log(error); + console.log(error.message); }) } -function delete_comment_child_DOM(comment, is_child) -{ - if(is_child){ +function delete_comment_child_DOM(comment, is_child) { + if (is_child) { let comment_id = get_comment_attribute(comment); - let comment_top_parent_id = get_comment_attribute(comment, attr='top_parent__id'); - + let comment_top_parent_id = get_comment_attribute(comment, attr = 'top_parent__id'); + all_comment = all_comment.map(p_comment => { - if(p_comment.parent_comment.id === comment_top_parent_id){ - p_comment.children = p_comment.children - .filter(c_comment => c_comment.id !== comment_id) - .filter( c_comment => { - if(c_comment.parent__id === comment_id){ - // Remove comment html element from DOM + if (p_comment.parent_comment.id === comment_top_parent_id) { + p_comment.children = p_comment.children + .filter(c_comment => c_comment.id !== comment_id) + .filter(c_comment => { + if (c_comment.parent__id === comment_id) { + // Remove comment html element from DOM let html_id = `#comment_${new Date(c_comment.added).getTime()}`; - let c = document.querySelector(html_id) - c.parentElement.removeChild(c); - } - return c_comment.parent__id !== comment_id; // Remove comment data from all_comment - }); - return p_comment; - } - return p_comment; - }); + let c = document.querySelector(html_id) + c.parentElement.removeChild(c); + } + return c_comment.parent__id !== comment_id; // Remove comment data from all_comment + }); + return p_comment; + } + return p_comment; + }); } } /*********** Scroll to a specific comment ************* * ****************************************************/ -function scrollToComment(targetComment) -{ +function scrollToComment(targetComment) { targetComment.scrollIntoView({ - behavior: "smooth", - block: "center", - inline: "nearest" + behavior: "smooth", + block: "center", + inline: "nearest" }); let htmlTarget = targetComment.querySelector(".comment_content"); - if(htmlTarget.classList.contains("scroll_to")) + if (htmlTarget.classList.contains("scroll_to")) htmlTarget.classList.remove("scroll_to"); window.setTimeout(() => { @@ -515,22 +483,21 @@ function scrollToComment(targetComment) /****** Add the owner of parent comment as a tag ****** ******************************************************/ -function add_user_tag(comment_value, parent_comment) -{ +function add_user_tag(comment_value, parent_comment) { let reply_to = get_comment_attribute( - parent_comment, - attr="author__first_name" + parent_comment, + attr = "author__first_name" ); - reply_content = get_comment_attribute( parent_comment, attr="content" ); + reply_content = get_comment_attribute(parent_comment, attr = "content"); let htmlTarget = parent_comment.querySelector(".comment_content"); let tag = document.createElement("a"); tag.setAttribute("href", "#"); tag.addEventListener("click", (e) => { - e.preventDefault(); - e.stopPropagation(); - scrollToComment(parent_comment) + e.preventDefault(); + e.stopPropagation(); + scrollToComment(parent_comment) }); - tag.innerHTML =` + tag.innerHTML = ` @${reply_to} @@ -548,163 +515,153 @@ function add_user_tag(comment_value, parent_comment) /**************** return color index **************** ******************************************************/ -function setBorderLeftColor(comment, parent_element){ - try{ +function setBorderLeftColor(comment, parent_element) { + try { let index = Number.parseInt(parent_element.dataset.level) + 1; - if(index >= COLORS.length){ - comment.dataset.level = COLORS.length - 1; - comment.querySelector(".comment_content") - .style.borderLeft = `4px solid ${COLORS[COLORS.length - 1]}`; - comment.querySelector(".comments_icon") - .style.color = `${COLORS[COLORS.length - 1]}`; - } - else{ - comment.dataset.level = index; - comment.querySelector(".comment_content") - .style.borderLeft = `4px solid ${COLORS[index]}`; - comment.querySelector(".comments_icon") - .style.color = `${COLORS[index]}`; - } - }catch(e){ - comment.dataset.level = COLORS.length - 1; - comment.querySelector(".comment_content") - .style.borderLeft = `4px solid ${COLORS[COLORS.length - 1]}`; - comment.querySelector(".comments_icon") - .style.color = `${COLORS[COLORS.length - 1]}`; + if (index >= COLORS.length) { + comment.dataset.level = COLORS.length - 1; + comment.querySelector(".comment_content") + .style.borderLeft = `4px solid ${COLORS[COLORS.length - 1]}`; + comment.querySelector(".comments_icon") + .style.color = `${COLORS[COLORS.length - 1]}`; + } + else { + comment.dataset.level = index; + comment.querySelector(".comment_content") + .style.borderLeft = `4px solid ${COLORS[index]}`; + comment.querySelector(".comments_icon") + .style.color = `${COLORS[index]}`; + } + } catch (e) { + comment.dataset.level = COLORS.length - 1; + comment.querySelector(".comment_content") + .style.borderLeft = `4px solid ${COLORS[COLORS.length - 1]}`; + comment.querySelector(".comments_icon") + .style.color = `${COLORS[COLORS.length - 1]}`; } } /**************** Add parent Comment **************** ******************************************************/ -if(is_authenticated){ +if (is_authenticated) { let add_parent_comment = document.querySelector("form.add_parent_comment"); - add_parent_comment.addEventListener("submit", (e)=> - { + add_parent_comment.addEventListener("submit", (e) => { e.preventDefault(); let date_added = new Date(); - let el = add_parent_comment.querySelector(".new_parent_comment") - if(el.value.trim() != "") - { - let comment_content = el.value; + let el = add_parent_comment.querySelector(".new_parent_comment") + if (el.value.trim() != "") { + let comment_content = el.value; let c = new Comment( - owner=user_fullName, - content=comment_content, - likes=0, - added_since=date_added, - id=`comment_${date_added.getTime()}`, - is_parent=true, - is_comment_owner=true); - c.dataset.level = '-1'; + owner = user_fullName, + content = comment_content, + likes = 0, + added_since = date_added, + id = `comment_${date_added.getTime()}`, + is_parent = true, + is_comment_owner = true); + c.dataset.level = '-1'; document.querySelector(".comment_container .comment_content form.add_parent_comment").after(c); el.value = ""; // INSERT INTO DATABASE THE CURRENT COMMENT CHILD - save_comment(comment_content, date_added.toISOString()); + save_comment(comment_content, date_added.toISOString()); } }); } /**************** Add child Comment ***************** ******************************************************/ -function add_child_comment(el, container_el, parent_comment) -{ +function add_child_comment(el, container_el, parent_comment) { let date_added = new Date(); - if(el.value.trim() !== "") - { - let child_direct_parent = document.querySelector( - `#${el.parentElement.parentElement.dataset.comment}`) - - let comment_child_content = add_user_tag( - el.value, - child_direct_parent, - ); + if (el.value.trim() !== "") { + let child_direct_parent = document.querySelector( + `#${el.parentElement.parentElement.dataset.comment}`) + + let comment_child_content = add_user_tag( + el.value, + child_direct_parent, + ); let c = new Comment( - owner=user_fullName, - content=comment_child_content, - likes=0, - added_since=date_added, + owner = user_fullName, + content = comment_child_content, + likes = 0, + added_since = date_added, id = `comment_${date_added.getTime()}`, - is_parent=false, - is_comment_owner=true); - setBorderLeftColor(c, child_direct_parent) - //container_el.prepend(c); - container_el.appendChild(c); - + is_parent = false, + is_comment_owner = true); + setBorderLeftColor(c, child_direct_parent) + //container_el.prepend(c); + container_el.appendChild(c); + hide_or_add_show_children_btn( - get_node(container_el, "comment_element", "comment_child"), - parent_comment.querySelector('.comments_children_container').childElementCount - ); + get_node(container_el, "comment_element", "comment_child"), + parent_comment.querySelector('.comments_children_container').childElementCount + ); // INSERT INTO DATABASE THE CURRENT COMMENT CHILD - let p_id = get_comment_attribute( child_direct_parent ); - let t_p_id = get_comment_attribute(parent_comment); + let p_id = get_comment_attribute(child_direct_parent); + let t_p_id = get_comment_attribute(parent_comment); - // Scroll to the comment child - scrollToComment( c ); + // Scroll to the comment child + scrollToComment(c); - save_comment(el.value, date_added.toISOString(), p_id, t_p_id); + save_comment(el.value, date_added.toISOString(), p_id, t_p_id); } } /********* Return backend comment attribute ********* ******************************************************/ -function get_comment_attribute(comment_html, attr="id") -{ +function get_comment_attribute(comment_html, attr = "id") { let curr_html_id = comment_html.getAttribute('id'); let comment_attr = null - for(const comment of all_comment){ + for (const comment of all_comment) { let comment_htmlid = `comment_${new Date(comment.parent_comment.added).getTime()}`; - if(comment_htmlid == curr_html_id) - { - comment_attr = comment.parent_comment[attr]; - break; - } - for(const c_comment of comment.children) - { - - let c_comment_htmlid = `comment_${new Date(c_comment.added).getTime()}`; - if(c_comment_htmlid == curr_html_id){ - comment_attr = c_comment[attr]; - break; - } - } - if(comment_attr) break; + if (comment_htmlid == curr_html_id) { + comment_attr = comment.parent_comment[attr]; + break; + } + for (const c_comment of comment.children) { + + let c_comment_htmlid = `comment_${new Date(c_comment.added).getTime()}`; + if (c_comment_htmlid == curr_html_id) { + comment_attr = c_comment[attr]; + break; + } + } + if (comment_attr) break; } return comment_attr; } -function htmlContainsClass(html_el, classes) -{ +function htmlContainsClass(html_el, classes) { let ctn = true; - classes.split('.').forEach(cls =>{ - if( !html_el.classList.contains(cls)) - ctn = false; + classes.split('.').forEach(cls => { + if (!html_el.classList.contains(cls)) + ctn = false; }); return ctn; } /********** Get parentNode or sibling node ********** ******************************************************/ -function get_node(el, class_name, not) -{ +function get_node(el, class_name, not) { class_name = class_name || "comment_container"; not = not || ""; - let selector = !!not?`.${class_name}:not(.${not})`: `.${class_name}`; + let selector = !!not ? `.${class_name}:not(.${not})` : `.${class_name}`; let foundedElement = el.querySelector(selector); - if(htmlContainsClass(el, class_name) && !htmlContainsClass(el, not)) { + if (htmlContainsClass(el, class_name) && !htmlContainsClass(el, not)) { return el; } - if(foundedElement){ + if (foundedElement) { return foundedElement; } - else{ + else { return get_node(el.parentElement, class_name, not); } } /******* Manage hide/show child comment text ******** ******************************************************/ -function hide_show_child_comment_text(el, nb_child=null) -{ - nb_child = nb_child?nb_child:''; +function hide_show_child_comment_text(el, nb_child = null) { + nb_child = nb_child ? nb_child : ''; let node = get_node(el, "comment_element", "comment_child"); let target_node = get_node(el, "comment_show_children_btn"); let txt = gettext("Answers"); @@ -714,92 +671,85 @@ function hide_show_child_comment_text(el, nb_child=null) /***************** Manage vote svg ****************** ******************************************************/ -function manage_vote_frontend(id, el) -{ - VOTED_USERS.forEach(obj =>{ - if(obj.comment__id === id && obj.user__id === user_id) - { - el.querySelector(".comment_vote_action").classList.add('voted'); - } +function manage_vote_frontend(id, el) { + VOTED_USERS.forEach(obj => { + if (obj.comment__id === id && obj.user__id === user_id) { + el.querySelector(".comment_vote_action").classList.add('voted'); + } }); } /******* Set number comments in comment label ********* * ****************************************************/ -function set_comments_number() -{ +function set_comments_number() { let label_text = comment_label.innerText.replace(/\d+\s+/, ''); - let nb_comments = all_comment.reduce( (acc, curr) => acc += curr.children.length, 0) + all_comment.length; + let nb_comments = all_comment.reduce((acc, curr) => acc += curr.children.length, 0) + all_comment.length; comment_label.innerText = `${nb_comments} ${label_text}`; } /************ Get vote from the server ************** ******************************************************/ -fetch(base_vote_url).then(response=>{ - response.json().then(data=>{ - VOTED_USERS = data.votes; +fetch(base_vote_url).then(response => { + response.json().then(data => { + VOTED_USERS = data.votes; }); -}).finally(function(){ +}).finally(function () { /************ Get data from the server ************** ******************************************************/ - fetch(base_url).then(response=>{ - response.json().then(data=>{ - all_comment = data; - set_comments_number() - data.forEach(comment_data=> - { + fetch(base_url).then(response => { + response.json().then(data => { + all_comment = data; + set_comments_number() + data.forEach(comment_data => { let parent_container = document.querySelector('.comment_container .comment_content'); let children_data = comment_data.children; - let parent_data = comment_data.parent_comment; + let parent_data = comment_data.parent_comment; let date_added = new Date(parent_data.added); - let html_id =`comment_${date_added.getTime().toString()}`; + let html_id = `comment_${date_added.getTime().toString()}`; let parent_c = new Comment( - owner=`${parent_data.author__first_name} ${parent_data.author__last_name.toUpperCase()}`, - content=parent_data.content, - likes=parent_data.nbr_vote, - added_since=date_added, - id=html_id, - is_parent=true, - is_comment_owner= user_id===parent_data.author__id + owner = `${parent_data.author__first_name} ${parent_data.author__last_name.toUpperCase()}`, + content = parent_data.content, + likes = parent_data.nbr_vote, + added_since = date_added, + id = html_id, + is_parent = true, + is_comment_owner = user_id === parent_data.author__id ); - parent_c.dataset.level = '-1'; - manage_vote_frontend(parent_data.id, parent_c); - if(children_data.length>0) - { - children_data.forEach(child_data=>{ - date_added = new Date(child_data.added); - html_id =`comment_${date_added.getTime().toString()}`; - let parent_to_scroll = parent_c; - if(child_data.parent__id !== child_data.top_parent__id) - { - let direct_parent_comment = children_data.filter((c_obj) => c_obj.id === child_data.parent__id )[0]; - let direct_parent_html_id = `comment_${new Date(direct_parent_comment.added).getTime().toString()}`; - parent_to_scroll = parent_c.querySelector(`#${direct_parent_html_id}`); - } - let comment_child_content = add_user_tag( - child_data.content, - parent_to_scroll, - comment_data.parent_comment.author__first_name - ); - let child_c = new Comment( - owner=`${child_data.author__first_name} ${child_data.author__last_name.toUpperCase()}`, - content=comment_child_content, - likes=child_data.nbr_vote, - added_since=date_added, - id=html_id, - is_parent=false, - is_comment_owner= user_id===child_data.author__id - ); - setBorderLeftColor(child_c, parent_to_scroll) - //parent_c.querySelector(".comments_children_container").prepend(child_c); - parent_c.querySelector(".comments_children_container").appendChild(child_c); - manage_vote_frontend(child_data.id, child_c); - }); + parent_c.dataset.level = '-1'; + manage_vote_frontend(parent_data.id, parent_c); + if (children_data.length > 0) { + children_data.forEach(child_data => { + date_added = new Date(child_data.added); + html_id = `comment_${date_added.getTime().toString()}`; + let parent_to_scroll = parent_c; + if (child_data.parent__id !== child_data.top_parent__id) { + let direct_parent_comment = children_data.filter((c_obj) => c_obj.id === child_data.parent__id)[0]; + let direct_parent_html_id = `comment_${new Date(direct_parent_comment.added).getTime().toString()}`; + parent_to_scroll = parent_c.querySelector(`#${direct_parent_html_id}`); + } + let comment_child_content = add_user_tag( + child_data.content, + parent_to_scroll, + comment_data.parent_comment.author__first_name + ); + let child_c = new Comment( + owner = `${child_data.author__first_name} ${child_data.author__last_name.toUpperCase()}`, + content = comment_child_content, + likes = child_data.nbr_vote, + added_since = date_added, + id = html_id, + is_parent = false, + is_comment_owner = user_id === child_data.author__id + ); + setBorderLeftColor(child_c, parent_to_scroll) + parent_c.querySelector(".comments_children_container").appendChild(child_c); + manage_vote_frontend(child_data.id, child_c); + }); } - if(is_authenticated) + if (is_authenticated) parent_container.querySelector('form.add_parent_comment').after(parent_c); - else - parent_container.prepend(parent_c); + else + parent_container.querySelector(".comment_counter_container").after(parent_c); hide_or_add_show_children_btn(parent_c, children_data.length); }); }); From 33632df47a612770826b200f4e28704a60639b46 Mon Sep 17 00:00:00 2001 From: Eliam LOTONGA Date: Tue, 12 Jan 2021 10:04:25 +0100 Subject: [PATCH 042/145] remove login required on vote function --- pod/video/views.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pod/video/views.py b/pod/video/views.py index 9214a44d50..e1bf2e73e5 100644 --- a/pod/video/views.py +++ b/pod/video/views.py @@ -1649,7 +1649,6 @@ def video_add(request): 'TRANSCRIPT': TRANSCRIPT}) -@login_required(redirect_field_name='referrer') @csrf_protect def vote(request, video_slug, comment_id=None): c_video = get_object_or_404(Video, slug=video_slug) From 4f93e7c569bd261b327786612e9025df6c540445 Mon Sep 17 00:00:00 2001 From: Eliam LOTONGA Date: Tue, 12 Jan 2021 10:04:57 +0100 Subject: [PATCH 043/145] remove unused css rules --- pod/video/static/css/comment-style.css | 3 --- 1 file changed, 3 deletions(-) diff --git a/pod/video/static/css/comment-style.css b/pod/video/static/css/comment-style.css index 92076917b8..ddd95e7060 100644 --- a/pod/video/static/css/comment-style.css +++ b/pod/video/static/css/comment-style.css @@ -318,10 +318,7 @@ { width: 100%; border-bottom: 1px solid var(--background-color); - margin: 1em 0; padding: 1em 0; - /*margin-top: 1em; - padding-top: 0;*/ } .comment_main .form-wrapper { From aba6ddd3793fc675b7c46c7660b23afeb12fec14 Mon Sep 17 00:00:00 2001 From: ptitloup Date: Tue, 12 Jan 2021 11:43:59 +0100 Subject: [PATCH 044/145] defin function to transcript video, call it from admin and remote_encode --- pod/video/admin.py | 45 +++++++++++++++++++++++--------------- pod/video/remote_encode.py | 23 ++++++++++++------- 2 files changed, 42 insertions(+), 26 deletions(-) diff --git a/pod/video/admin.py b/pod/video/admin.py index 6cf829c445..d261b21849 100644 --- a/pod/video/admin.py +++ b/pod/video/admin.py @@ -5,6 +5,7 @@ from django.utils.html import format_html from django.utils.translation import ugettext_lazy as _ from modeltranslation.admin import TranslationAdmin + from .models import Video from .models import Channel from .models import Theme @@ -20,7 +21,6 @@ from .models import ViewCount from .models import VideoToDelete from .models import VideoVersion -from .transcript import start_transcript from .forms import VideoForm, VideoVersionForm from .forms import ChannelForm @@ -46,6 +46,14 @@ TRANSCRIPT = getattr(settings, 'USE_TRANSCRIPTION', False) +if TRANSCRIPT: + from . import transcript + TRANSCRIPT_VIDEO = getattr( + settings, + 'TRANSCRIPT_VIDEO', + 'start_transcript' + ) + USE_OBSOLESCENCE = getattr( settings, "USE_OBSOLESCENCE", False) @@ -75,10 +83,10 @@ def queryset(self, request, queryset): value = self.value() if value == 'Yes': queryset = queryset.exclude( - pk__in=[vid.id for vid in queryset if not vid.encoded]) + pk__in=[vid.id for vid in queryset if not vid.encoded]) elif value == 'No': queryset = queryset.exclude( - pk__in=[vid.id for vid in queryset if vid.encoded]) + pk__in=[vid.id for vid in queryset if vid.encoded]) return queryset @@ -199,7 +207,8 @@ def transcript_video(self, request, queryset): if CELERY_TO_ENCODE: task_start_transcript.delay(item.id) else: - start_transcript(item.id) + transcript_video = getattr(transcript, TRANSCRIPT_VIDEO) + transcript_video(item.id) transcript_video.short_description = _('Transcript selected') def get_queryset(self, request): @@ -212,9 +221,9 @@ def get_queryset(self, request): def change_view(self, request, object_id, form_url='', extra_context=None): extra_context = extra_context or {} extra_context['max_duration_date_delete'] = getattr( - settings, 'MAX_DURATION_DATE_DELETE', 10) + settings, 'MAX_DURATION_DATE_DELETE', 10) return super(VideoAdmin, self).change_view( - request, object_id, form_url, extra_context) + request, object_id, form_url, extra_context) def save_model(self, request, obj, form, change): super().save_model(request, obj, form, change) @@ -342,10 +351,10 @@ def get_queryset(self, request): def formfield_for_foreignkey(self, db_field, request, **kwargs): if (db_field.name) == "parentId": kwargs["queryset"] = Theme.objects.filter( - channel__sites=Site.objects.get_current()) + channel__sites=Site.objects.get_current()) if (db_field.name) == "channel": kwargs["queryset"] = Channel.objects.filter( - sites=Site.objects.get_current()) + sites=Site.objects.get_current()) return super().formfield_for_foreignkey(db_field, request, **kwargs) @@ -447,10 +456,10 @@ def get_queryset(self, request): def formfield_for_foreignkey(self, db_field, request, **kwargs): if (db_field.name) == "video": kwargs["queryset"] = Video.objects.filter( - sites=Site.objects.get_current()) + sites=Site.objects.get_current()) if (db_field.name) == "rendition": kwargs["queryset"] = VideoRendition.objects.filter( - sites=Site.objects.get_current()) + sites=Site.objects.get_current()) return super().formfield_for_foreignkey(db_field, request, **kwargs) @@ -467,7 +476,7 @@ def get_queryset(self, request): def formfield_for_foreignkey(self, db_field, request, **kwargs): if (db_field.name) == "video": kwargs["queryset"] = Video.objects.filter( - sites=Site.objects.get_current()) + sites=Site.objects.get_current()) return super().formfield_for_foreignkey(db_field, request, **kwargs) @@ -484,7 +493,7 @@ def get_queryset(self, request): def formfield_for_foreignkey(self, db_field, request, **kwargs): if (db_field.name) == "video": kwargs["queryset"] = Video.objects.filter( - sites=Site.objects.get_current()) + sites=Site.objects.get_current()) return super().formfield_for_foreignkey(db_field, request, **kwargs) @@ -560,10 +569,10 @@ def get_queryset(self, request): def formfield_for_foreignkey(self, db_field, request, **kwargs): if (db_field.name) == "user": kwargs["queryset"] = User.objects.filter( - owner__sites=Site.objects.get_current()) + owner__sites=Site.objects.get_current()) if (db_field.name) == "video": kwargs["queryset"] = Video.objects.filter( - sites=Site.objects.get_current()) + sites=Site.objects.get_current()) return super().formfield_for_foreignkey(db_field, request, **kwargs) @@ -589,10 +598,10 @@ def get_queryset(self, request): def formfield_for_foreignkey(self, db_field, request, **kwargs): if (db_field.name) == "user": kwargs["queryset"] = User.objects.filter( - owner__sites=Site.objects.get_current()) + owner__sites=Site.objects.get_current()) if (db_field.name) == "video": kwargs["queryset"] = Video.objects.filter( - sites=Site.objects.get_current()) + sites=Site.objects.get_current()) return super().formfield_for_foreignkey(db_field, request, **kwargs) @@ -617,10 +626,10 @@ def get_queryset(self, request): def formfield_for_foreignkey(self, db_field, request, **kwargs): if (db_field.name) == "user": kwargs["queryset"] = User.objects.filter( - owner__sites=Site.objects.get_current()) + owner__sites=Site.objects.get_current()) if (db_field.name) == "parentNote": kwargs["queryset"] = AdvancedNotes.objects.filter( - video__owner__owner__sites=Site.objects.get_current()) + video__owner__owner__sites=Site.objects.get_current()) if (db_field.name) == "parentCom": kwargs["queryset"] = NoteComments. \ objects.filter( diff --git a/pod/video/remote_encode.py b/pod/video/remote_encode.py index a81c777acf..3ea783132a 100644 --- a/pod/video/remote_encode.py +++ b/pod/video/remote_encode.py @@ -35,6 +35,14 @@ DEBUG = getattr(settings, 'DEBUG', True) +TRANSCRIPT = getattr(settings, 'USE_TRANSCRIPTION', False) + +if TRANSCRIPT: + from . import transcript + TRANSCRIPT_VIDEO = getattr(settings, + 'TRANSCRIPT_VIDEO', + 'start_transcript') + EMAIL_ON_ENCODING_COMPLETION = getattr( settings, 'EMAIL_ON_ENCODING_COMPLETION', True) @@ -163,9 +171,9 @@ def store_remote_encoding_video(video_id): with open(output_dir + "/info_video.json") as json_file: info_video = json.load(json_file) - if DEBUG: - print(output_dir) - print(json.dumps(info_video, indent=2)) + # if DEBUG: + # print(output_dir) + # print(json.dumps(info_video, indent=2)) video_to_encode.duration = info_video["duration"] video_to_encode.encoding_in_progress = True @@ -197,11 +205,10 @@ def store_remote_encoding_video(video_id): send_email_encoding(video_encoding) # Transcript - """ - main_threaded_transcript(video_id) if ( - TRANSCRIPT and video_to_encode.transcript - ) else False - """ + if (TRANSCRIPT and video_encoding.transcript): + transcript_video = getattr(transcript, TRANSCRIPT_VIDEO) + transcript_video(video_id) + print('ALL is DONE') From c1a8b2e9a81ec176b9e41fdfe6a2fcff6454e7e1 Mon Sep 17 00:00:00 2001 From: ptitloup Date: Tue, 12 Jan 2021 12:12:59 +0100 Subject: [PATCH 045/145] add threaded option to start encode or transcript, call it directly in each module --- pod/video/admin.py | 9 ++------ pod/video/encode.py | 23 ++++++++++++++------- pod/video/remote_encode.py | 2 +- pod/video/transcript.py | 42 ++++++++++++++++++++++++-------------- 4 files changed, 46 insertions(+), 30 deletions(-) diff --git a/pod/video/admin.py b/pod/video/admin.py index d261b21849..a879d98f65 100644 --- a/pod/video/admin.py +++ b/pod/video/admin.py @@ -36,8 +36,6 @@ from django.contrib.sites.shortcuts import get_current_site from pod.chapter.admin import ChapterInline -from pod.main.tasks import task_start_transcript - # Ordering user by username ! User._meta.ordering = ["username"] # SET USE_ESTABLISHMENT_FIELD @@ -204,11 +202,8 @@ def encode_video(self, request, queryset): def transcript_video(self, request, queryset): for item in queryset: if item.get_video_mp3() and not item.encoding_in_progress: - if CELERY_TO_ENCODE: - task_start_transcript.delay(item.id) - else: - transcript_video = getattr(transcript, TRANSCRIPT_VIDEO) - transcript_video(item.id) + transcript_video = getattr(transcript, TRANSCRIPT_VIDEO) + transcript_video(item.id) transcript_video.short_description = _('Transcript selected') def get_queryset(self, request): diff --git a/pod/video/encode.py b/pod/video/encode.py index 54237ec18d..d4dfd41b98 100755 --- a/pod/video/encode.py +++ b/pod/video/encode.py @@ -34,10 +34,15 @@ FILEPICKER = False from pod.main.models import CustomImageModel -TRANSCRIPT = False -if getattr(settings, 'USE_TRANSCRIPTION', False): - TRANSCRIPT = True - from .transcript import main_threaded_transcript +TRANSCRIPT = getattr(settings, 'USE_TRANSCRIPTION', False) + +if TRANSCRIPT: + from . import transcript + TRANSCRIPT_VIDEO = getattr( + settings, + 'TRANSCRIPT_VIDEO', + 'start_transcript' + ) USE_ESTABLISHMENT = getattr( settings, 'USE_ESTABLISHMENT_FIELD', False) @@ -356,9 +361,7 @@ def encode_video(video_id): if EMAIL_ON_ENCODING_COMPLETION: send_email_encoding(video_to_encode) - main_threaded_transcript(video_id) if ( - TRANSCRIPT and video_to_encode.transcript - ) else False + transcript_video(video_id) else: msg = "Wrong file or path:"\ @@ -368,6 +371,12 @@ def encode_video(video_id): send_email(msg, video_id) +def transcript_video(video_id): + video = Video.objects.get(id=video_id) + if (TRANSCRIPT and video.transcript): + start_transcript_video = getattr(transcript, TRANSCRIPT_VIDEO) + start_transcript_video(video_id, False) + # ########################################################################## # ENCODE VIDEO: GET VIDEO DATA # ########################################################################## diff --git a/pod/video/remote_encode.py b/pod/video/remote_encode.py index 3ea783132a..2a054d0422 100644 --- a/pod/video/remote_encode.py +++ b/pod/video/remote_encode.py @@ -207,7 +207,7 @@ def store_remote_encoding_video(video_id): # Transcript if (TRANSCRIPT and video_encoding.transcript): transcript_video = getattr(transcript, TRANSCRIPT_VIDEO) - transcript_video(video_id) + transcript_video(video_id, False) print('ALL is DONE') diff --git a/pod/video/transcript.py b/pod/video/transcript.py index 042a781308..1589613ce6 100644 --- a/pod/video/transcript.py +++ b/pod/video/transcript.py @@ -1,6 +1,7 @@ from django.conf import settings from django.core.files import File from pod.completion.models import Track +from pod.main.tasks import task_start_transcript from .utils import change_encoding_step, add_encoding_log from .utils import send_email, send_email_transcript @@ -53,6 +54,8 @@ EMAIL_ON_TRANSCRIPTING_COMPLETION = getattr( settings, 'EMAIL_ON_TRANSCRIPTING_COMPLETION', True) +CELERY_TO_ENCODE = getattr(settings, 'CELERY_TO_ENCODE', False) + log = logging.getLogger(__name__) """ @@ -68,26 +71,35 @@ """ # ########################################################################## -# ENCODE VIDEO: THREAD TO LAUNCH ENCODE +# TRANSCRIPT VIDEO: THREAD TO LAUNCH TRANSCRIPT # ########################################################################## -def start_remote_transcript(video_id): +def start_remote_transcript(video_id, threaded=True): # load module here to prevent circular import from .remote_transcript import remote_transcript_video - log.info("START ENCODE VIDEO ID %s" % video_id) - t = threading.Thread(target=remote_transcript_video, - args=[video_id]) - t.setDaemon(True) - t.start() - - -def start_transcript(video_id): - log.info("START TRANSCRIPT VIDEO %s" % video_id) - t = threading.Thread(target=main_threaded_transcript, - args=[video_id]) - t.setDaemon(True) - t.start() + if threaded: + log.info("START ENCODE VIDEO ID %s" % video_id) + t = threading.Thread(target=remote_transcript_video, + args=[video_id]) + t.setDaemon(True) + t.start() + else: + remote_transcript_video(video_id) + + +def start_transcript(video_id, threaded=True): + if threaded: + if CELERY_TO_ENCODE: + task_start_transcript.delay(video_id) + else: + log.info("START TRANSCRIPT VIDEO %s" % video_id) + t = threading.Thread(target=main_threaded_transcript, + args=[video_id]) + t.setDaemon(True) + t.start() + else: + main_threaded_transcript(video_id) def get_model(lang): From 108166f931705fdbf7d09c4fd2d1f1b6b2d3d463 Mon Sep 17 00:00:00 2001 From: Eliam LOTONGA Date: Tue, 12 Jan 2021 15:32:51 +0100 Subject: [PATCH 046/145] Add loader element --- pod/video/templates/videos/video-comment.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pod/video/templates/videos/video-comment.html b/pod/video/templates/videos/video-comment.html index b7daa7b6aa..7db4c4aa21 100644 --- a/pod/video/templates/videos/video-comment.html +++ b/pod/video/templates/videos/video-comment.html @@ -5,13 +5,12 @@
+
+ +
{% if is_auth %}
-
- - -
{% endif %} +
From d5776cc22872b0109c6f82d33fa42e230904a33c Mon Sep 17 00:00:00 2001 From: Eliam LOTONGA Date: Tue, 12 Jan 2021 15:33:01 +0100 Subject: [PATCH 047/145] Css loader --- pod/video/static/css/comment-style.css | 40 ++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/pod/video/static/css/comment-style.css b/pod/video/static/css/comment-style.css index ddd95e7060..43de68b35b 100644 --- a/pod/video/static/css/comment-style.css +++ b/pod/video/static/css/comment-style.css @@ -606,3 +606,43 @@ background-color: var(--third-background-color); } } +/* ************ Loader ************* */ +.lds-ring.hide{display: none; opacity: 0; visibility: none;} +.lds-ring { + position: relative; + width: 50px; + height: 50px; + margin: 0 auto; + display: flex; + justify-content: center; +} +.lds-ring div { + box-sizing: border-box; + display: block; + position: absolute; + width: 34px; + height: 34px; + margin: 8px; + border: 4px solid #fff; + border-radius: 50%; + animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite; + border-color: var(--background-color) transparent transparent transparent; +} +.lds-ring div:nth-child(1) { + animation-delay: -0.45s; +} +.lds-ring div:nth-child(2) { + animation-delay: -0.3s; +} +.lds-ring div:nth-child(3) { + animation-delay: -0.15s; +} +@keyframes lds-ring { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + From ab14aefa8e033e09026c71d28333cdff64ed49e4 Mon Sep 17 00:00:00 2001 From: Eliam LOTONGA Date: Tue, 12 Jan 2021 15:33:45 +0100 Subject: [PATCH 048/145] toggle hide/show loader --- pod/video/static/js/comment-script.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pod/video/static/js/comment-script.js b/pod/video/static/js/comment-script.js index a688bf4d3c..6d3c566ba9 100644 --- a/pod/video/static/js/comment-script.js +++ b/pod/video/static/js/comment-script.js @@ -687,9 +687,13 @@ function set_comments_number() { comment_label.innerText = `${nb_comments} ${label_text}`; } +// Loader Element +const loader = document.querySelector(".comment_content > .lds-ring"); + /************ Get vote from the server ************** ******************************************************/ fetch(base_vote_url).then(response => { + loader.classList.remove('hide'); // show loader response.json().then(data => { VOTED_USERS = data.votes; }); @@ -698,6 +702,7 @@ fetch(base_vote_url).then(response => { ******************************************************/ fetch(base_url).then(response => { response.json().then(data => { + loader.classList.add('hide'); // hide loader all_comment = data; set_comments_number() data.forEach(comment_data => { From 2b1559436f0ab28120b68657e5b17c1da225e4a7 Mon Sep 17 00:00:00 2001 From: Eliam LOTONGA Date: Tue, 12 Jan 2021 15:52:21 +0100 Subject: [PATCH 049/145] import vote_post and vote_get --- pod/urls.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pod/urls.py b/pod/urls.py index 6768f7bbf2..3f6c988839 100644 --- a/pod/urls.py +++ b/pod/urls.py @@ -31,7 +31,8 @@ from pod.video.views import video_count, video_version from pod.video.views import video_oembed from pod.video.views import stats_view -from pod.video.views import get_comments, add_comment, delete_comment, vote +from pod.video.views import get_comments, add_comment, delete_comment +from pod.video.views import vote_get, vote_post from pod.video.views import get_categories, add_category from pod.video.views import edit_category, delete_category from pod.video.feeds import RssSiteVideosFeed, RssSiteAudiosFeed @@ -238,9 +239,9 @@ url(r'^comment/del/(?P[\-\d\w]+)/(?P[\d]+)/$', delete_comment, name='delete_comment'), url(r'^comment/vote/(?P[\-\d\w]+)/$', - vote, name='get_votes'), + vote_get, name='get_votes'), url(r'^comment/vote/(?P[\-\d\w]+)/(?P[\d]+)/$', - vote, name='add_vote'), + vote_post, name='add_vote'), ] # VIDEO CATEGORY From 787541e375cbb7ed71e1fe5b2b40dab90a618ca4 Mon Sep 17 00:00:00 2001 From: Eliam LOTONGA Date: Tue, 12 Jan 2021 15:54:03 +0100 Subject: [PATCH 050/145] extract vote post code to vote_post function, rename vote function to vote_get --- pod/video/views.py | 52 ++++++++++++++++++++++++++-------------------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/pod/video/views.py b/pod/video/views.py index e1bf2e73e5..3abf74a97b 100644 --- a/pod/video/views.py +++ b/pod/video/views.py @@ -1650,31 +1650,9 @@ def video_add(request): @csrf_protect -def vote(request, video_slug, comment_id=None): +def vote_get(request, video_slug): c_video = get_object_or_404(Video, slug=video_slug) if request.method == "POST": - c = get_object_or_404( - Comment, video=c_video, id=comment_id)if comment_id else None - c_user = request.user - if not c_user: - return HttpResponse('

Bad Request

', status=400) - response = {} - c_vote = Vote.objects.filter( - user=c_user, comment=c, comment__video=c_video).first() - if c_vote: - c_vote.delete() - response['voted'] = False - else: - c_vote = Vote() - c_vote.comment = c - c_vote.user = c_user - c_vote.save() - response['voted'] = True - - return HttpResponse( - json.dumps(response), - content_type="application/json") - if comment_id: return HttpResponseNotFound('

Method Not Allowed

', status=405) else: votes = Vote.objects.filter( @@ -1683,6 +1661,34 @@ def vote(request, video_slug, comment_id=None): return HttpResponse(json.dumps(data), content_type='application/json') +@login_required(redirect_field_name='referrer') +def vote_post(request, video_slug, comment_id): + if request.method == "GET": + return HttpResponseNotFound('

Method Not Allowed

', status=405) + + c_video = get_object_or_404(Video, slug=video_slug) + c = get_object_or_404( + Comment, video=c_video, id=comment_id)if comment_id else None + c_user = request.user + if not c_user: + return HttpResponse('

Bad Request

', status=400) + response = {} + c_vote = Vote.objects.filter( + user=c_user, comment=c, comment__video=c_video).first() + if c_vote: + c_vote.delete() + response['voted'] = False + else: + c_vote = Vote() + c_vote.comment = c + c_vote.user = c_user + c_vote.save() + response['voted'] = True + + return HttpResponse( + json.dumps(response), content_type="application/json") + + @login_required(redirect_field_name='referrer') @csrf_protect def add_comment(request, video_slug, comment_id=None): From ca6fa238bc56fd1a5408003dd05ed09e9f220fa8 Mon Sep 17 00:00:00 2001 From: Eliam LOTONGA Date: Tue, 12 Jan 2021 16:28:52 +0100 Subject: [PATCH 051/145] change COLORS --- pod/video/static/js/comment-script.js | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/pod/video/static/js/comment-script.js b/pod/video/static/js/comment-script.js index 6d3c566ba9..d0b0b03f28 100644 --- a/pod/video/static/js/comment-script.js +++ b/pod/video/static/js/comment-script.js @@ -6,15 +6,15 @@ const lang_btn = document.querySelector(".btn-lang.btn-lang-active"); const comment_label = document.querySelector('.comment_label'); let VOTED_USERS = []; const COLORS = [ - "#5F616D", - "#6B6D79", - "#767984", - "#828590", - "#8D929B", - "#999EA7", - "#A4AAB2", - "#B0B6BE", - "#BBC2C9", + "#050710", + "#0E112A", + "#1D1543", + "#302163", + "#432B82", + "#53358F", + "#643FBD", + "#755CC9", + "#877CD3", ] const LANG = lang_btn ? lang_btn.textContent.trim() : "fr"; const ACTION_COMMENT = { @@ -587,7 +587,6 @@ function add_child_comment(el, container_el, parent_comment) { is_parent = false, is_comment_owner = true); setBorderLeftColor(c, child_direct_parent) - //container_el.prepend(c); container_el.appendChild(c); hide_or_add_show_children_btn( From 0e081d3dfead007b0969b599ca42c41151955aee Mon Sep 17 00:00:00 2001 From: Eliam LOTONGA Date: Tue, 12 Jan 2021 16:45:23 +0100 Subject: [PATCH 052/145] fix missing vote --- pod/video/static/js/comment-script.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pod/video/static/js/comment-script.js b/pod/video/static/js/comment-script.js index d0b0b03f28..00d27aaf85 100644 --- a/pod/video/static/js/comment-script.js +++ b/pod/video/static/js/comment-script.js @@ -146,7 +146,7 @@ class Comment extends HTMLElement { else comment_container.querySelector('.comment_content_body').appendChild(content); let svg_icon = [``, ``]; - let vote_action = createFooterBtnAction("comment_actions comment_vote_action", "icon comment_vote_icon", gettext("Agree with the comment"), svg_icon, "comment_vote_btn", `${likes} votes`, id); + let vote_action = createFooterBtnAction("comment_actions comment_vote_action", "icon comment_vote_icon", gettext("Agree with the comment"), is_authenticated ? svg_icon : '', "comment_vote_btn", `${likes} votes`, id); if (is_authenticated) { vote_action.addEventListener("click", () => { let comment_id = get_comment_attribute(document.getElementById(id)); @@ -335,7 +335,7 @@ function vote(comment_id, target_html_el) { 'user__id': user_id, 'comment__id': comment_id, }); - btn.innerHTML = parseInt(btn.textContent) + 1; + btn.innerHTML = parseInt(btn.textContent) + 1 + " votes"; if (!target_html_el.classList.contains('voted')) target_html_el.classList.add('voted'); } @@ -343,7 +343,7 @@ function vote(comment_id, target_html_el) { VOTED_USERS = VOTED_USERS.filter(obj => { obj.comment__id !== comment_id }) - btn.innerHTML = parseInt(btn.textContent) - 1; + btn.innerHTML = parseInt(btn.textContent) - 1 + " votes"; if (target_html_el.classList.contains('voted')) target_html_el.classList.remove('voted'); } From a7b2b9d75f70764d99804550dbf3703834f6e1c9 Mon Sep 17 00:00:00 2001 From: Eliam LOTONGA Date: Tue, 12 Jan 2021 16:50:43 +0100 Subject: [PATCH 053/145] margin bottom comment form --- pod/video/static/css/comment-style.css | 1 + 1 file changed, 1 insertion(+) diff --git a/pod/video/static/css/comment-style.css b/pod/video/static/css/comment-style.css index 43de68b35b..b77387f65b 100644 --- a/pod/video/static/css/comment-style.css +++ b/pod/video/static/css/comment-style.css @@ -319,6 +319,7 @@ width: 100%; border-bottom: 1px solid var(--background-color); padding: 1em 0; + margin-bottom: 2em; } .comment_main .form-wrapper { From e171eaca3321a39b714fcebcade54dd88af3a681 Mon Sep 17 00:00:00 2001 From: Eliam LOTONGA Date: Tue, 12 Jan 2021 16:51:49 +0100 Subject: [PATCH 054/145] fix bug css --- pod/video/static/css/comment-style.css | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pod/video/static/css/comment-style.css b/pod/video/static/css/comment-style.css index b77387f65b..b11da1940a 100644 --- a/pod/video/static/css/comment-style.css +++ b/pod/video/static/css/comment-style.css @@ -57,7 +57,9 @@ .reply_author{ font-weight: 600; } -.reply_content:{font-weight: 500;} +.reply_content{ + font-weight: 500; +} .comment_main .icon.comment_vote_icon svg { -webkit-transform: translateY(-1px); From 18b73c1e225a03bb794fd00a5ca70e54ffdc721b Mon Sep 17 00:00:00 2001 From: Eliam LOTONGA Date: Wed, 13 Jan 2021 10:28:43 +0100 Subject: [PATCH 055/145] Fix bug update counter of comment children and delete nested comment children --- pod/video/static/js/comment-script.js | 65 ++++++++++++++++----------- 1 file changed, 38 insertions(+), 27 deletions(-) diff --git a/pod/video/static/js/comment-script.js b/pod/video/static/js/comment-script.js index 00d27aaf85..361df10c7e 100644 --- a/pod/video/static/js/comment-script.js +++ b/pod/video/static/js/comment-script.js @@ -75,13 +75,6 @@ class ConfirmModal extends HTMLElement { e.preventDefault(); document.querySelector("#custom_element_confirm_modal .confirm_delete").classList.remove("show"); // Delete comment - let parent_el = ACTION_COMMENT.comment_to_delete.parentElement - parent_el.removeChild(ACTION_COMMENT.comment_to_delete); - // remove show children btn if no child in the node - if (parent_el.classList.contains("comments_children_container") && parent_el.childElementCount === 0) { - let parent_e = get_node(parent_el, "comment_element", "comment_child"); - hide_or_add_show_children_btn(parent_e, parent_el.childElementCount); - } delete_comment(ACTION_COMMENT.comment_to_delete); ACTION_COMMENT.comment_to_delete = null; }); @@ -212,7 +205,6 @@ class Comment extends HTMLElement { add_comment.parentElement.classList.toggle("show"); if (!comment_parent.classList.contains("show")) comment_parent.classList.add("show") - hide_show_child_comment_text(this, comment_parent.querySelector(".comments_children_container").childElementCount) add_child_comment(this, child_container, comment_parent); this.value = ""; } @@ -282,9 +274,7 @@ function hide_or_add_show_children_btn(parent_comment = null, nb_child = null) { ); children_action.addEventListener("click", function () { get_node(this.parentElement, "comment_element", "comment_child").classList.toggle("show"); - hide_show_child_comment_text(this, nb_child); }); - if (parent_comment) { let children_container = parent_comment.querySelector(".comments_children_container"); if (!parent_comment.querySelector(".actions .comment_show_children_action") && children_container.childElementCount > 0) { @@ -311,7 +301,7 @@ function hide_or_add_show_children_btn(parent_comment = null, nb_child = null) { comment_children_container.querySelector(".actions .comment_show_children_action") ) } - hide_show_child_comment_text(comments_children_container, nb_child); + update_answer_text(comment); }) } } @@ -355,7 +345,7 @@ function vote(comment_id, target_html_el) { /**************** Save comment into the server **************** ***************************************************************/ -function save_comment(content, date, parent_id = null, top_parent_id = null) { +function save_comment(content, date, parent_id = null, parent_comment = null) { let post_url = base_url.replace("comment", "comment/add"); post_url = parent_id ? post_url + parent_id + '/' : post_url let data = new FormData(); @@ -367,6 +357,7 @@ function save_comment(content, date, parent_id = null, top_parent_id = null) { body: data }).then(response => { if (response.ok) { + let top_parent_id = get_comment_attribute(parent_comment); response.json().then(data => { let c = { author__first_name: data.author__first_name, @@ -393,6 +384,12 @@ function save_comment(content, date, parent_id = null, top_parent_id = null) { return comment; }); } + console.log(parent_comment); + console.log(parent_comment.querySelector('.comments_children_container').childElementCount); + hide_or_add_show_children_btn( + parent_comment, + parent_comment.querySelector('.comments_children_container').childElementCount + ); set_comments_number() }); } @@ -420,11 +417,18 @@ function delete_comment(comment) { response.json().then(data => { if (data.deleted) { document.body.appendChild(new AlertMessage(gettext("Comment has been deleted successfully."))); - - if (is_child) delete_comment_child_DOM(comment, is_child); - - else all_comment = all_comment.filter(c => c.parent_comment.id != data.comment_deleted); - + let parent_el = null + if (is_child) { + let remaining_children = delete_comment_child_DOM(comment, is_child); + parent_el = get_node(comment, "comment_element", "comment_child"); + comment.parentElement.removeChild(comment); + hide_or_add_show_children_btn(parent_el, remaining_children); + update_answer_text(parent_el); + set_comments_number(); + return; + } + all_comment = all_comment.filter(c => c.parent_comment.id != data.comment_deleted); + comment.parentElement.removeChild(comment); set_comments_number(); } else @@ -437,28 +441,33 @@ function delete_comment(comment) { } function delete_comment_child_DOM(comment, is_child) { + let remaining_children = 0; if (is_child) { let comment_id = get_comment_attribute(comment); let comment_top_parent_id = get_comment_attribute(comment, attr = 'top_parent__id'); + let list_parent_id = [comment_id]; all_comment = all_comment.map(p_comment => { if (p_comment.parent_comment.id === comment_top_parent_id) { p_comment.children = p_comment.children .filter(c_comment => c_comment.id !== comment_id) .filter(c_comment => { - if (c_comment.parent__id === comment_id) { + if (list_parent_id.includes(c_comment.parent__id)) { + list_parent_id = [...list_parent_id, c_comment.id]; // Remove comment html element from DOM let html_id = `#comment_${new Date(c_comment.added).getTime()}`; let c = document.querySelector(html_id) c.parentElement.removeChild(c); } - return c_comment.parent__id !== comment_id; // Remove comment data from all_comment + return !list_parent_id.includes(c_comment.parent__id); // Remove comment data from all_comment }); + remaining_children = p_comment.children.length; return p_comment; } return p_comment; }); } + return remaining_children; } /*********** Scroll to a specific comment ************* @@ -590,18 +599,18 @@ function add_child_comment(el, container_el, parent_comment) { container_el.appendChild(c); hide_or_add_show_children_btn( - get_node(container_el, "comment_element", "comment_child"), + parent_comment, parent_comment.querySelector('.comments_children_container').childElementCount ); + update_answer_text(parent_comment) // INSERT INTO DATABASE THE CURRENT COMMENT CHILD let p_id = get_comment_attribute(child_direct_parent); - let t_p_id = get_comment_attribute(parent_comment); // Scroll to the comment child scrollToComment(c); - save_comment(el.value, date_added.toISOString(), p_id, t_p_id); + save_comment(el.value, date_added.toISOString(), p_id, parent_comment); } } @@ -659,13 +668,15 @@ function get_node(el, class_name, not) { /******* Manage hide/show child comment text ******** ******************************************************/ -function hide_show_child_comment_text(el, nb_child = null) { - nb_child = nb_child ? nb_child : ''; - let node = get_node(el, "comment_element", "comment_child"); - let target_node = get_node(el, "comment_show_children_btn"); +function update_answer_text(el) { + let children_container = get_node(el, "comments_children_container"); + let nb_child = children_container.childElementCount; + nb_child = nb_child === 0 ? '' : nb_child; let txt = gettext("Answers"); txt = `${nb_child} ${txt}`; - target_node.innerText = txt; + let answer_text_element = el.querySelector(".comment_show_children_btn"); + if (answer_text_element) + answer_text_element.innerText = txt; } /***************** Manage vote svg ****************** From 65b2ac87803d7a8ca9dda5c41fee7cfe70550ee0 Mon Sep 17 00:00:00 2001 From: ptitloup Date: Thu, 14 Jan 2021 12:05:44 +0100 Subject: [PATCH 056/145] add action to video view set to show list of video for username pass in get request - create VideoUserSerializer to serialize it --- pod/video/rest_views.py | 42 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/pod/video/rest_views.py b/pod/video/rest_views.py index d465686f50..a259c0c650 100644 --- a/pod/video/rest_views.py +++ b/pod/video/rest_views.py @@ -5,6 +5,8 @@ # from rest_framework import authentication, permissions from rest_framework import renderers from rest_framework.decorators import api_view +from rest_framework.decorators import action + from django.template.loader import render_to_string from django.shortcuts import get_object_or_404 @@ -70,6 +72,32 @@ class Meta: ) +class VideoUserSerializer(serializers.ModelSerializer): + + def to_representation(self, instance): + data = super(VideoUserSerializer, self).to_representation(instance) + import json + video_data = json.loads(instance.get_json_to_index()) + video_data.update({"encoded": instance.encoded}) + video_data.update( + {"encoding_in_progress": instance.encoding_in_progress}) + video_data.update( + {"get_thumbnail_admin": instance.get_thumbnail_admin}) + video_data.update({"mp4_file": instance.get_video_mp4_json()}) + video_data.update({"mp3_file": instance.get_video_mp3( + ).source_file.url if instance.get_video_mp3() else ""}) + video_data.update({"m4a_file": instance.get_video_m4a( + ).source_file.url if instance.get_video_m4a() else ""}) + data["video_data"] = video_data + return data + + class Meta: + model = Video + fields = ( + 'id', 'url', 'get_version' + ) + + class VideoRenditionSerializer(serializers.HyperlinkedModelSerializer): class Meta: @@ -136,6 +164,20 @@ class VideoViewSet(viewsets.ModelViewSet): serializer_class = VideoSerializer filter_fields = ('owner', 'type', 'date_added', 'channel', 'discipline') + @action(detail=False, methods=['get']) + def user_videos(self, request): + user_videos = Video.objects.filter( + owner__username=request.GET.get('username')) + page = self.paginate_queryset(user_videos) + if page is not None: + serializer = VideoUserSerializer( + page, many=True, context={'request': request}) + return self.get_paginated_response(serializer.data) + + serializer = VideoUserSerializer( + user_videos, many=True, context={'request': request}) + return Response(serializer.data) + class VideoRenditionViewSet(viewsets.ModelViewSet): queryset = VideoRendition.objects.all() From 9952fbb6d2118ff35d372db885c7b4d7ccdd0ba1 Mon Sep 17 00:00:00 2001 From: ptitloup Date: Thu, 14 Jan 2021 15:00:29 +0100 Subject: [PATCH 057/145] add filter in list video for user --- pod/video/rest_views.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/pod/video/rest_views.py b/pod/video/rest_views.py index a259c0c650..4ffbd67e6f 100644 --- a/pod/video/rest_views.py +++ b/pod/video/rest_views.py @@ -17,6 +17,8 @@ from .views import VIDEOS from .remote_encode import start_store_remote_encoding_video +import json + # Serializers define the API representation. @@ -65,7 +67,6 @@ class Meta: 'encoding_in_progress', 'duration', 'sites', 'disable_comment', 'get_encoding_step', 'get_version', 'encoded', 'duration_in_time' ) - filter_fields = ('owner', 'type', 'date_added') read_only_fields = ('encoding_in_progress', 'duration', 'get_encoding_step', 'get_version', 'encoded', 'duration_in_time' @@ -76,7 +77,6 @@ class VideoUserSerializer(serializers.ModelSerializer): def to_representation(self, instance): data = super(VideoUserSerializer, self).to_representation(instance) - import json video_data = json.loads(instance.get_json_to_index()) video_data.update({"encoded": instance.encoded}) video_data.update( @@ -94,8 +94,13 @@ def to_representation(self, instance): class Meta: model = Video fields = ( - 'id', 'url', 'get_version' + 'id', 'url', 'get_version', 'type', 'date_added', 'is_draft', + 'is_restricted', 'encoding_in_progress', 'encoded', ) + read_only_fields = ('encoding_in_progress', 'duration', + 'get_encoding_step', + 'get_version', 'encoded', 'duration_in_time' + ) class VideoRenditionSerializer(serializers.HyperlinkedModelSerializer): @@ -162,11 +167,14 @@ class DisciplineViewSet(viewsets.ModelViewSet): class VideoViewSet(viewsets.ModelViewSet): queryset = Video.objects.all() serializer_class = VideoSerializer - filter_fields = ('owner', 'type', 'date_added', 'channel', 'discipline') + filter_fields = ('owner', 'type', 'date_added', 'is_draft', + 'is_restricted', 'encoding_in_progress') @action(detail=False, methods=['get']) def user_videos(self, request): - user_videos = Video.objects.filter( + # user_videos = Video.objects.filter( + # owner__username=request.GET.get('username')) + user_videos = self.filter_queryset(self.get_queryset()).filter( owner__username=request.GET.get('username')) page = self.paginate_queryset(user_videos) if page is not None: From f579eac59078fe5ea5a4dd18f333fbc5eec1012c Mon Sep 17 00:00:00 2001 From: ptitloup Date: Thu, 14 Jan 2021 15:27:30 +0100 Subject: [PATCH 058/145] add filter to get only encoded video --- pod/video/rest_views.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pod/video/rest_views.py b/pod/video/rest_views.py index 4ffbd67e6f..018686de21 100644 --- a/pod/video/rest_views.py +++ b/pod/video/rest_views.py @@ -81,6 +81,8 @@ def to_representation(self, instance): video_data.update({"encoded": instance.encoded}) video_data.update( {"encoding_in_progress": instance.encoding_in_progress}) + video_data.update( + {"get_encoding_step": instance.get_encoding_step}) video_data.update( {"get_thumbnail_admin": instance.get_thumbnail_admin}) video_data.update({"mp4_file": instance.get_video_mp4_json()}) @@ -176,6 +178,9 @@ def user_videos(self, request): # owner__username=request.GET.get('username')) user_videos = self.filter_queryset(self.get_queryset()).filter( owner__username=request.GET.get('username')) + if request.GET.get('encoded') and request.GET.get('encoded') == "true": + user_videos = user_videos.exclude( + pk__in=[vid.id for vid in VIDEOS if not vid.encoded]) page = self.paginate_queryset(user_videos) if page is not None: serializer = VideoUserSerializer( From 3af30f537bb931cb8d0048aea0570cb4ce2fb70e Mon Sep 17 00:00:00 2001 From: ptitloup Date: Thu, 14 Jan 2021 17:10:10 +0100 Subject: [PATCH 059/145] add rest view to launch transcript, rename store_encoded_video in store_remote_encoded_video --- pod/main/rest_router.py | 7 +++++-- pod/video/rest_views.py | 16 +++++++++++++++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/pod/main/rest_router.py b/pod/main/rest_router.py index d1ec2bfbad..e30a9559e4 100644 --- a/pod/main/rest_router.py +++ b/pod/main/rest_router.py @@ -60,8 +60,11 @@ name='dublincore'), url(r'launch_encode_view/$', video_views.launch_encode_view, name='launch_encode_view'), - url(r'store_encoded_video/$', video_views.store_encoded_video, - name='store_encoded_video'), + url(r'launch_transcript_view/$', video_views.launch_transcript_view, + name='launch_transcript_view'), + url(r'store_remote_encoded_video/$', + video_views.store_remote_encoded_video, + name='store_remote_encoded_video'), ] for apps in settings.THIRD_PARTY_APPS: diff --git a/pod/video/rest_views.py b/pod/video/rest_views.py index 018686de21..11ea3c6701 100644 --- a/pod/video/rest_views.py +++ b/pod/video/rest_views.py @@ -16,6 +16,7 @@ from .models import PlaylistVideo from .views import VIDEOS from .remote_encode import start_store_remote_encoding_video +from .transcript import start_transcript import json @@ -279,7 +280,20 @@ def launch_encode_view(request): @api_view(['GET']) -def store_encoded_video(request): +def launch_transcript_view(request): + video = get_object_or_404(Video, slug=request.GET.get('slug')) + if ( + video is not None + and video.get_video_mp3() + ): + start_transcript(video.id, threaded=True) + return Response( + VideoSerializer(instance=video, context={'request': request}).data + ) + + +@api_view(['GET']) +def store_remote_encoded_video(request): video_id = request.GET.get('id', 0) video = get_object_or_404(Video, id=video_id) start_store_remote_encoding_video(video_id) From 9ce5031d9041af2c01ed5fe6cea5144e26cb913c Mon Sep 17 00:00:00 2001 From: ptitloup Date: Fri, 15 Jan 2021 09:25:45 +0100 Subject: [PATCH 060/145] update settings example --- pod/custom/settings_local.py.example | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/pod/custom/settings_local.py.example b/pod/custom/settings_local.py.example index 4ca7b29949..f6c1892e96 100644 --- a/pod/custom/settings_local.py.example +++ b/pod/custom/settings_local.py.example @@ -566,8 +566,15 @@ ENCODE_VIDEO = "start_encode" # -n encoding- -i -v -u -d # SSH_REMOTE_CMD = "" """ + """ -# pourl'encodage distant, il faut préciser le login, l'host et la clé pour appeler cet encodage +# Fonction appelée pour lancer la transcription des vidéos +# Pour la transcription distant, il faut préciser "start_remote_transcript" +""" +TRANSCRIPT_VIDEO = "start_transcript" + +""" +# pour la transcription distant, il faut préciser le login, l'host et la clé pour appeler la fonction # REMOTE ENCODING SETTINGS # SSH_TRANSCRIPT_REMOTE_USER = "pod" # SSH_TRANSCRIPT_REMOTE_HOST = "xxx.univ.fr" @@ -801,7 +808,7 @@ AFFILIATION = ( ('retired', ('retired')), ('emeritus', ('emeritus')), ('teacher', ('teacher')), - ('registered-reader', _('registered-reader')) + ('registered-reader', ('registered-reader')) ) """ From 5b7a3dbc8de94e542fa61da9110016aebe94b5a1 Mon Sep 17 00:00:00 2001 From: ptitloup Date: Fri, 15 Jan 2021 14:54:39 +0100 Subject: [PATCH 061/145] remove video-js-6.8.0, add video-js-7.10.2, call it in header and update script to use it --- .../video-js-6.8.0/alt/video-js-cdn.css | 1446 - .../video-js-6.8.0/alt/video-js-cdn.min.css | 1 - .../static/video-js-6.8.0/alt/video.novtt.js | 24545 -------------- .../video-js-6.8.0/alt/video.novtt.min.js | 6 - .../examples/elephantsdream/captions.ar.vtt | 330 - .../examples/elephantsdream/captions.en.vtt | 334 - .../examples/elephantsdream/captions.ja.vtt | 326 - .../examples/elephantsdream/captions.ru.vtt | 356 - .../examples/elephantsdream/captions.sv.vtt | 349 - .../examples/elephantsdream/chapters.en.vtt | 44 - .../elephantsdream/descriptions.en.vtt | 280 - .../examples/elephantsdream/index.html | 42 - .../static/video-js-6.8.0/examples/index.html | 18 - .../examples/shared/example-captions.vtt | 41 - .../examples/simple-embed/index.html | 26 - .../static/video-js-6.8.0/font/VideoJS.eot | Bin 6952 -> 0 bytes .../static/video-js-6.8.0/font/VideoJS.svg | 108 - .../static/video-js-6.8.0/font/VideoJS.ttf | Bin 6788 -> 0 bytes .../static/video-js-6.8.0/font/VideoJS.woff | Bin 4168 -> 0 bytes .../static/video-js-6.8.0/ie8/videojs-ie8.js | 2600 -- .../video-js-6.8.0/ie8/videojs-ie8.min.js | 1 - pod/main/static/video-js-6.8.0/lang/ar.js | 34 - pod/main/static/video-js-6.8.0/lang/ba.js | 26 - pod/main/static/video-js-6.8.0/lang/bg.js | 26 - pod/main/static/video-js-6.8.0/lang/ca.js | 26 - pod/main/static/video-js-6.8.0/lang/cs.js | 26 - pod/main/static/video-js-6.8.0/lang/da.js | 26 - pod/main/static/video-js-6.8.0/lang/de.js | 84 - pod/main/static/video-js-6.8.0/lang/el.js | 40 - pod/main/static/video-js-6.8.0/lang/en.js | 85 - pod/main/static/video-js-6.8.0/lang/es.js | 27 - pod/main/static/video-js-6.8.0/lang/fa.js | 84 - pod/main/static/video-js-6.8.0/lang/fi.js | 26 - pod/main/static/video-js-6.8.0/lang/fr.js | 84 - pod/main/static/video-js-6.8.0/lang/gl.js | 27 - pod/main/static/video-js-6.8.0/lang/he.js | 84 - pod/main/static/video-js-6.8.0/lang/hr.js | 26 - pod/main/static/video-js-6.8.0/lang/hu.js | 26 - pod/main/static/video-js-6.8.0/lang/it.js | 26 - pod/main/static/video-js-6.8.0/lang/ja.js | 26 - pod/main/static/video-js-6.8.0/lang/ko.js | 26 - pod/main/static/video-js-6.8.0/lang/nb.js | 26 - pod/main/static/video-js-6.8.0/lang/nl.js | 84 - pod/main/static/video-js-6.8.0/lang/nn.js | 26 - pod/main/static/video-js-6.8.0/lang/pl.js | 34 - pod/main/static/video-js-6.8.0/lang/pt-BR.js | 26 - pod/main/static/video-js-6.8.0/lang/pt-PT.js | 41 - pod/main/static/video-js-6.8.0/lang/ru.js | 84 - pod/main/static/video-js-6.8.0/lang/sk.js | 84 - pod/main/static/video-js-6.8.0/lang/sr.js | 26 - pod/main/static/video-js-6.8.0/lang/sv.js | 26 - pod/main/static/video-js-6.8.0/lang/tr.js | 76 - pod/main/static/video-js-6.8.0/lang/uk.js | 40 - pod/main/static/video-js-6.8.0/lang/vi.js | 84 - pod/main/static/video-js-6.8.0/lang/zh-CN.js | 83 - pod/main/static/video-js-6.8.0/lang/zh-TW.js | 83 - pod/main/static/video-js-6.8.0/video-js.css | 1458 - .../static/video-js-6.8.0/video-js.min.css | 1 - pod/main/static/video-js-6.8.0/video.cjs.js | 24104 -------------- pod/main/static/video-js-6.8.0/video.es.js | 24102 -------------- pod/main/static/video-js-6.8.0/video.js | 26371 ---------------- pod/main/static/video-js-6.8.0/video.min.js | 7 - .../videojs-contrib-quality-levels.min.js | 2 + .../player/videojs-hls-quality-selector.css | 7 + .../videojs-hls-quality-selector.min.js | 7 + .../static/player/videojs-seek-buttons.css | 2 + .../static/player/videojs-seek-buttons.min.js | 2 + pod/video/static/player/videojs/lang/fr.js | 84 + pod/video/static/player/videojs/lang/fr.json | 84 + .../static/player/videojs/video-js.min.css | 1 + pod/video/static/player/videojs/video.min.js | 25 + pod/video/templates/videos/video-header.html | 48 +- pod/video/templates/videos/video-script.html | 60 +- 73 files changed, 292 insertions(+), 108584 deletions(-) delete mode 100755 pod/main/static/video-js-6.8.0/alt/video-js-cdn.css delete mode 100755 pod/main/static/video-js-6.8.0/alt/video-js-cdn.min.css delete mode 100755 pod/main/static/video-js-6.8.0/alt/video.novtt.js delete mode 100755 pod/main/static/video-js-6.8.0/alt/video.novtt.min.js delete mode 100755 pod/main/static/video-js-6.8.0/examples/elephantsdream/captions.ar.vtt delete mode 100755 pod/main/static/video-js-6.8.0/examples/elephantsdream/captions.en.vtt delete mode 100755 pod/main/static/video-js-6.8.0/examples/elephantsdream/captions.ja.vtt delete mode 100755 pod/main/static/video-js-6.8.0/examples/elephantsdream/captions.ru.vtt delete mode 100755 pod/main/static/video-js-6.8.0/examples/elephantsdream/captions.sv.vtt delete mode 100755 pod/main/static/video-js-6.8.0/examples/elephantsdream/chapters.en.vtt delete mode 100755 pod/main/static/video-js-6.8.0/examples/elephantsdream/descriptions.en.vtt delete mode 100755 pod/main/static/video-js-6.8.0/examples/elephantsdream/index.html delete mode 100755 pod/main/static/video-js-6.8.0/examples/index.html delete mode 100755 pod/main/static/video-js-6.8.0/examples/shared/example-captions.vtt delete mode 100755 pod/main/static/video-js-6.8.0/examples/simple-embed/index.html delete mode 100755 pod/main/static/video-js-6.8.0/font/VideoJS.eot delete mode 100755 pod/main/static/video-js-6.8.0/font/VideoJS.svg delete mode 100755 pod/main/static/video-js-6.8.0/font/VideoJS.ttf delete mode 100755 pod/main/static/video-js-6.8.0/font/VideoJS.woff delete mode 100755 pod/main/static/video-js-6.8.0/ie8/videojs-ie8.js delete mode 100755 pod/main/static/video-js-6.8.0/ie8/videojs-ie8.min.js delete mode 100755 pod/main/static/video-js-6.8.0/lang/ar.js delete mode 100755 pod/main/static/video-js-6.8.0/lang/ba.js delete mode 100755 pod/main/static/video-js-6.8.0/lang/bg.js delete mode 100755 pod/main/static/video-js-6.8.0/lang/ca.js delete mode 100755 pod/main/static/video-js-6.8.0/lang/cs.js delete mode 100755 pod/main/static/video-js-6.8.0/lang/da.js delete mode 100755 pod/main/static/video-js-6.8.0/lang/de.js delete mode 100755 pod/main/static/video-js-6.8.0/lang/el.js delete mode 100755 pod/main/static/video-js-6.8.0/lang/en.js delete mode 100755 pod/main/static/video-js-6.8.0/lang/es.js delete mode 100755 pod/main/static/video-js-6.8.0/lang/fa.js delete mode 100755 pod/main/static/video-js-6.8.0/lang/fi.js delete mode 100755 pod/main/static/video-js-6.8.0/lang/fr.js delete mode 100755 pod/main/static/video-js-6.8.0/lang/gl.js delete mode 100755 pod/main/static/video-js-6.8.0/lang/he.js delete mode 100755 pod/main/static/video-js-6.8.0/lang/hr.js delete mode 100755 pod/main/static/video-js-6.8.0/lang/hu.js delete mode 100755 pod/main/static/video-js-6.8.0/lang/it.js delete mode 100755 pod/main/static/video-js-6.8.0/lang/ja.js delete mode 100755 pod/main/static/video-js-6.8.0/lang/ko.js delete mode 100755 pod/main/static/video-js-6.8.0/lang/nb.js delete mode 100755 pod/main/static/video-js-6.8.0/lang/nl.js delete mode 100755 pod/main/static/video-js-6.8.0/lang/nn.js delete mode 100755 pod/main/static/video-js-6.8.0/lang/pl.js delete mode 100755 pod/main/static/video-js-6.8.0/lang/pt-BR.js delete mode 100755 pod/main/static/video-js-6.8.0/lang/pt-PT.js delete mode 100755 pod/main/static/video-js-6.8.0/lang/ru.js delete mode 100755 pod/main/static/video-js-6.8.0/lang/sk.js delete mode 100755 pod/main/static/video-js-6.8.0/lang/sr.js delete mode 100755 pod/main/static/video-js-6.8.0/lang/sv.js delete mode 100755 pod/main/static/video-js-6.8.0/lang/tr.js delete mode 100755 pod/main/static/video-js-6.8.0/lang/uk.js delete mode 100755 pod/main/static/video-js-6.8.0/lang/vi.js delete mode 100755 pod/main/static/video-js-6.8.0/lang/zh-CN.js delete mode 100755 pod/main/static/video-js-6.8.0/lang/zh-TW.js delete mode 100755 pod/main/static/video-js-6.8.0/video-js.css delete mode 100755 pod/main/static/video-js-6.8.0/video-js.min.css delete mode 100755 pod/main/static/video-js-6.8.0/video.cjs.js delete mode 100755 pod/main/static/video-js-6.8.0/video.es.js delete mode 100755 pod/main/static/video-js-6.8.0/video.js delete mode 100755 pod/main/static/video-js-6.8.0/video.min.js create mode 100644 pod/video/static/player/videojs-contrib-quality-levels.min.js create mode 100644 pod/video/static/player/videojs-hls-quality-selector.css create mode 100644 pod/video/static/player/videojs-hls-quality-selector.min.js create mode 100644 pod/video/static/player/videojs-seek-buttons.css create mode 100644 pod/video/static/player/videojs-seek-buttons.min.js create mode 100644 pod/video/static/player/videojs/lang/fr.js create mode 100644 pod/video/static/player/videojs/lang/fr.json create mode 100644 pod/video/static/player/videojs/video-js.min.css create mode 100644 pod/video/static/player/videojs/video.min.js diff --git a/pod/main/static/video-js-6.8.0/alt/video-js-cdn.css b/pod/main/static/video-js-6.8.0/alt/video-js-cdn.css deleted file mode 100755 index b2ce11a9fe..0000000000 --- a/pod/main/static/video-js-6.8.0/alt/video-js-cdn.css +++ /dev/null @@ -1,1446 +0,0 @@ -.video-js .vjs-big-play-button .vjs-icon-placeholder:before, .vjs-button > .vjs-icon-placeholder:before, .video-js .vjs-modal-dialog, .vjs-modal-dialog .vjs-modal-dialog-content { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; } - -.video-js .vjs-big-play-button .vjs-icon-placeholder:before, .vjs-button > .vjs-icon-placeholder:before { - text-align: center; } - -@font-face { - font-family: VideoJS; - src: url("//vjs.zencdn.net/font/1.5.1/VideoJS.eot?#iefix") format("eot"); } - -@font-face { - font-family: VideoJS; - src: url(data:application/font-woff;charset=utf-8;base64,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) format("woff"), url(data:application/x-font-ttf;charset=utf-8;base64,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AG8AbgB0AGUAbABsAG8AIABwAHIAbwBqAGUAYwB0AC4AaAB0AHQAcAA6AC8ALwBmAG8AbgB0AGUAbABsAG8ALgBjAG8AbQAAAAIAAAAAAAAAEQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIQECAQMBBAEFAQYBBwEIAQkBCgELAQwBDQEOAQ8BEAERARIBEwEUARUBFgEXARgBGQEaARsBHAEdAR4BHwEgASEBIgAEcGxheQtwbGF5LWNpcmNsZQVwYXVzZQt2b2x1bWUtbXV0ZQp2b2x1bWUtbG93CnZvbHVtZS1taWQLdm9sdW1lLWhpZ2gQZnVsbHNjcmVlbi1lbnRlcg9mdWxsc2NyZWVuLWV4aXQGc3F1YXJlB3NwaW5uZXIJc3VidGl0bGVzCGNhcHRpb25zCGNoYXB0ZXJzBXNoYXJlA2NvZwZjaXJjbGUOY2lyY2xlLW91dGxpbmUTY2lyY2xlLWlubmVyLWNpcmNsZQJoZAZjYW5jZWwGcmVwbGF5CGZhY2Vib29rBWdwbHVzCGxpbmtlZGluB3R3aXR0ZXIGdHVtYmxyCXBpbnRlcmVzdBFhdWRpby1kZXNjcmlwdGlvbgVhdWRpbwluZXh0LWl0ZW0NcHJldmlvdXMtaXRlbQAAAAA=) format("truetype"); - font-weight: normal; - font-style: normal; } - -.vjs-icon-play, .video-js .vjs-big-play-button .vjs-icon-placeholder:before, .video-js .vjs-play-control .vjs-icon-placeholder { - font-family: VideoJS; - font-weight: normal; - font-style: normal; } - .vjs-icon-play:before, .video-js .vjs-big-play-button .vjs-icon-placeholder:before, .video-js .vjs-play-control .vjs-icon-placeholder:before { - content: "\f101"; } - -.vjs-icon-play-circle { - font-family: VideoJS; - font-weight: normal; - font-style: normal; } - .vjs-icon-play-circle:before { - content: "\f102"; } - -.vjs-icon-pause, .video-js .vjs-play-control.vjs-playing .vjs-icon-placeholder { - font-family: VideoJS; - font-weight: normal; - font-style: normal; } - .vjs-icon-pause:before, .video-js .vjs-play-control.vjs-playing .vjs-icon-placeholder:before { - content: "\f103"; } - -.vjs-icon-volume-mute, .video-js .vjs-mute-control.vjs-vol-0 .vjs-icon-placeholder { - font-family: VideoJS; - font-weight: normal; - font-style: normal; } - .vjs-icon-volume-mute:before, .video-js .vjs-mute-control.vjs-vol-0 .vjs-icon-placeholder:before { - content: "\f104"; } - -.vjs-icon-volume-low, .video-js .vjs-mute-control.vjs-vol-1 .vjs-icon-placeholder { - font-family: VideoJS; - font-weight: normal; - font-style: normal; } - .vjs-icon-volume-low:before, .video-js .vjs-mute-control.vjs-vol-1 .vjs-icon-placeholder:before { - content: "\f105"; } - -.vjs-icon-volume-mid, .video-js .vjs-mute-control.vjs-vol-2 .vjs-icon-placeholder { - font-family: VideoJS; - font-weight: normal; - font-style: normal; } - .vjs-icon-volume-mid:before, .video-js .vjs-mute-control.vjs-vol-2 .vjs-icon-placeholder:before { - content: "\f106"; } - -.vjs-icon-volume-high, .video-js .vjs-mute-control .vjs-icon-placeholder { - font-family: VideoJS; - font-weight: normal; - font-style: normal; } - .vjs-icon-volume-high:before, .video-js .vjs-mute-control .vjs-icon-placeholder:before { - content: "\f107"; } - -.vjs-icon-fullscreen-enter, .video-js .vjs-fullscreen-control .vjs-icon-placeholder { - font-family: VideoJS; - font-weight: normal; - font-style: normal; } - .vjs-icon-fullscreen-enter:before, .video-js .vjs-fullscreen-control .vjs-icon-placeholder:before { - content: "\f108"; } - -.vjs-icon-fullscreen-exit, .video-js.vjs-fullscreen .vjs-fullscreen-control .vjs-icon-placeholder { - font-family: VideoJS; - font-weight: normal; - font-style: normal; } - .vjs-icon-fullscreen-exit:before, .video-js.vjs-fullscreen .vjs-fullscreen-control .vjs-icon-placeholder:before { - content: "\f109"; } - -.vjs-icon-square { - font-family: VideoJS; - font-weight: normal; - font-style: normal; } - .vjs-icon-square:before { - content: "\f10a"; } - -.vjs-icon-spinner { - font-family: VideoJS; - font-weight: normal; - font-style: normal; } - .vjs-icon-spinner:before { - content: "\f10b"; } - -.vjs-icon-subtitles, .video-js .vjs-subtitles-button .vjs-icon-placeholder, .video-js .vjs-subs-caps-button .vjs-icon-placeholder, -.video-js.video-js:lang(en-GB) .vjs-subs-caps-button .vjs-icon-placeholder, -.video-js.video-js:lang(en-IE) .vjs-subs-caps-button .vjs-icon-placeholder, -.video-js.video-js:lang(en-AU) .vjs-subs-caps-button .vjs-icon-placeholder, -.video-js.video-js:lang(en-NZ) .vjs-subs-caps-button .vjs-icon-placeholder { - font-family: VideoJS; - font-weight: normal; - font-style: normal; } - .vjs-icon-subtitles:before, .video-js .vjs-subtitles-button .vjs-icon-placeholder:before, .video-js .vjs-subs-caps-button .vjs-icon-placeholder:before, - .video-js.video-js:lang(en-GB) .vjs-subs-caps-button .vjs-icon-placeholder:before, - .video-js.video-js:lang(en-IE) .vjs-subs-caps-button .vjs-icon-placeholder:before, - .video-js.video-js:lang(en-AU) .vjs-subs-caps-button .vjs-icon-placeholder:before, - .video-js.video-js:lang(en-NZ) .vjs-subs-caps-button .vjs-icon-placeholder:before { - content: "\f10c"; } - -.vjs-icon-captions, .video-js .vjs-captions-button .vjs-icon-placeholder, .video-js:lang(en) .vjs-subs-caps-button .vjs-icon-placeholder, -.video-js:lang(fr-CA) .vjs-subs-caps-button .vjs-icon-placeholder { - font-family: VideoJS; - font-weight: normal; - font-style: normal; } - .vjs-icon-captions:before, .video-js .vjs-captions-button .vjs-icon-placeholder:before, .video-js:lang(en) .vjs-subs-caps-button .vjs-icon-placeholder:before, - .video-js:lang(fr-CA) .vjs-subs-caps-button .vjs-icon-placeholder:before { - content: "\f10d"; } - -.vjs-icon-chapters, .video-js .vjs-chapters-button .vjs-icon-placeholder { - font-family: VideoJS; - font-weight: normal; - font-style: normal; } - .vjs-icon-chapters:before, .video-js .vjs-chapters-button .vjs-icon-placeholder:before { - content: "\f10e"; } - -.vjs-icon-share { - font-family: VideoJS; - font-weight: normal; - font-style: normal; } - .vjs-icon-share:before { - content: "\f10f"; } - -.vjs-icon-cog { - font-family: VideoJS; - font-weight: normal; - font-style: normal; } - .vjs-icon-cog:before { - content: "\f110"; } - -.vjs-icon-circle, .video-js .vjs-play-progress, .video-js .vjs-volume-level { - font-family: VideoJS; - font-weight: normal; - font-style: normal; } - .vjs-icon-circle:before, .video-js .vjs-play-progress:before, .video-js .vjs-volume-level:before { - content: "\f111"; } - -.vjs-icon-circle-outline { - font-family: VideoJS; - font-weight: normal; - font-style: normal; } - .vjs-icon-circle-outline:before { - content: "\f112"; } - -.vjs-icon-circle-inner-circle { - font-family: VideoJS; - font-weight: normal; - font-style: normal; } - .vjs-icon-circle-inner-circle:before { - content: "\f113"; } - -.vjs-icon-hd { - font-family: VideoJS; - font-weight: normal; - font-style: normal; } - .vjs-icon-hd:before { - content: "\f114"; } - -.vjs-icon-cancel, .video-js .vjs-control.vjs-close-button .vjs-icon-placeholder { - font-family: VideoJS; - font-weight: normal; - font-style: normal; } - .vjs-icon-cancel:before, .video-js .vjs-control.vjs-close-button .vjs-icon-placeholder:before { - content: "\f115"; } - -.vjs-icon-replay, .video-js .vjs-play-control.vjs-ended .vjs-icon-placeholder { - font-family: VideoJS; - font-weight: normal; - font-style: normal; } - .vjs-icon-replay:before, .video-js .vjs-play-control.vjs-ended .vjs-icon-placeholder:before { - content: "\f116"; } - -.vjs-icon-facebook { - font-family: VideoJS; - font-weight: normal; - font-style: normal; } - .vjs-icon-facebook:before { - content: "\f117"; } - -.vjs-icon-gplus { - font-family: VideoJS; - font-weight: normal; - font-style: normal; } - .vjs-icon-gplus:before { - content: "\f118"; } - -.vjs-icon-linkedin { - font-family: VideoJS; - font-weight: normal; - font-style: normal; } - .vjs-icon-linkedin:before { - content: "\f119"; } - -.vjs-icon-twitter { - font-family: VideoJS; - font-weight: normal; - font-style: normal; } - .vjs-icon-twitter:before { - content: "\f11a"; } - -.vjs-icon-tumblr { - font-family: VideoJS; - font-weight: normal; - font-style: normal; } - .vjs-icon-tumblr:before { - content: "\f11b"; } - -.vjs-icon-pinterest { - font-family: VideoJS; - font-weight: normal; - font-style: normal; } - .vjs-icon-pinterest:before { - content: "\f11c"; } - -.vjs-icon-audio-description, .video-js .vjs-descriptions-button .vjs-icon-placeholder { - font-family: VideoJS; - font-weight: normal; - font-style: normal; } - .vjs-icon-audio-description:before, .video-js .vjs-descriptions-button .vjs-icon-placeholder:before { - content: "\f11d"; } - -.vjs-icon-audio, .video-js .vjs-audio-button .vjs-icon-placeholder { - font-family: VideoJS; - font-weight: normal; - font-style: normal; } - .vjs-icon-audio:before, .video-js .vjs-audio-button .vjs-icon-placeholder:before { - content: "\f11e"; } - -.vjs-icon-next-item { - font-family: VideoJS; - font-weight: normal; - font-style: normal; } - .vjs-icon-next-item:before { - content: "\f11f"; } - -.vjs-icon-previous-item { - font-family: VideoJS; - font-weight: normal; - font-style: normal; } - .vjs-icon-previous-item:before { - content: "\f120"; } - -.video-js { - display: block; - vertical-align: top; - box-sizing: border-box; - color: #fff; - background-color: #000; - position: relative; - padding: 0; - font-size: 10px; - line-height: 1; - font-weight: normal; - font-style: normal; - font-family: Arial, Helvetica, sans-serif; - word-break: initial; } - .video-js:-moz-full-screen { - position: absolute; } - .video-js:-webkit-full-screen { - width: 100% !important; - height: 100% !important; } - -.video-js[tabindex="-1"] { - outline: none; } - -.video-js *, -.video-js *:before, -.video-js *:after { - box-sizing: inherit; } - -.video-js ul { - font-family: inherit; - font-size: inherit; - line-height: inherit; - list-style-position: outside; - margin-left: 0; - margin-right: 0; - margin-top: 0; - margin-bottom: 0; } - -.video-js.vjs-fluid, -.video-js.vjs-16-9, -.video-js.vjs-4-3 { - width: 100%; - max-width: 100%; - height: 0; } - -.video-js.vjs-16-9 { - padding-top: 56.25%; } - -.video-js.vjs-4-3 { - padding-top: 75%; } - -.video-js.vjs-fill { - width: 100%; - height: 100%; } - -.video-js .vjs-tech { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; } - -body.vjs-full-window { - padding: 0; - margin: 0; - height: 100%; - overflow-y: auto; } - -.vjs-full-window .video-js.vjs-fullscreen { - position: fixed; - overflow: hidden; - z-index: 1000; - left: 0; - top: 0; - bottom: 0; - right: 0; } - -.video-js.vjs-fullscreen { - width: 100% !important; - height: 100% !important; - padding-top: 0 !important; } - -.video-js.vjs-fullscreen.vjs-user-inactive { - cursor: none; } - -.vjs-hidden { - display: none !important; } - -.vjs-disabled { - opacity: 0.5; - cursor: default; } - -.video-js .vjs-offscreen { - height: 1px; - left: -9999px; - position: absolute; - top: 0; - width: 1px; } - -.vjs-lock-showing { - display: block !important; - opacity: 1; - visibility: visible; } - -.vjs-no-js { - padding: 20px; - color: #fff; - background-color: #000; - font-size: 18px; - font-family: Arial, Helvetica, sans-serif; - text-align: center; - width: 300px; - height: 150px; - margin: 0px auto; } - -.vjs-no-js a, -.vjs-no-js a:visited { - color: #66A8CC; } - -.video-js .vjs-big-play-button { - font-size: 3em; - line-height: 1.5em; - height: 1.5em; - width: 3em; - display: block; - position: absolute; - top: 10px; - left: 10px; - padding: 0; - cursor: pointer; - opacity: 1; - border: 0.06666em solid #fff; - background-color: #2B333F; - background-color: rgba(43, 51, 63, 0.7); - -webkit-border-radius: 0.3em; - -moz-border-radius: 0.3em; - border-radius: 0.3em; - -webkit-transition: all 0.4s; - -moz-transition: all 0.4s; - -ms-transition: all 0.4s; - -o-transition: all 0.4s; - transition: all 0.4s; } - -.vjs-big-play-centered .vjs-big-play-button { - top: 50%; - left: 50%; - margin-top: -0.75em; - margin-left: -1.5em; } - -.video-js:hover .vjs-big-play-button, -.video-js .vjs-big-play-button:focus { - border-color: #fff; - background-color: #73859f; - background-color: rgba(115, 133, 159, 0.5); - -webkit-transition: all 0s; - -moz-transition: all 0s; - -ms-transition: all 0s; - -o-transition: all 0s; - transition: all 0s; } - -.vjs-controls-disabled .vjs-big-play-button, -.vjs-has-started .vjs-big-play-button, -.vjs-using-native-controls .vjs-big-play-button, -.vjs-error .vjs-big-play-button { - display: none; } - -.vjs-has-started.vjs-paused.vjs-show-big-play-button-on-pause .vjs-big-play-button { - display: block; } - -.video-js button { - background: none; - border: none; - color: inherit; - display: inline-block; - overflow: visible; - font-size: inherit; - line-height: inherit; - text-transform: none; - text-decoration: none; - transition: none; - -webkit-appearance: none; - -moz-appearance: none; - appearance: none; } - -.vjs-control .vjs-button { - width: 100%; - height: 100%; } - -.video-js .vjs-control.vjs-close-button { - cursor: pointer; - height: 3em; - position: absolute; - right: 0; - top: 0.5em; - z-index: 2; } - -.video-js .vjs-modal-dialog { - background: rgba(0, 0, 0, 0.8); - background: -webkit-linear-gradient(-90deg, rgba(0, 0, 0, 0.8), rgba(255, 255, 255, 0)); - background: linear-gradient(180deg, rgba(0, 0, 0, 0.8), rgba(255, 255, 255, 0)); - overflow: auto; - box-sizing: content-box; } - -.video-js .vjs-modal-dialog > * { - box-sizing: border-box; } - -.vjs-modal-dialog .vjs-modal-dialog-content { - font-size: 1.2em; - line-height: 1.5; - padding: 20px 24px; - z-index: 1; } - -.vjs-menu-button { - cursor: pointer; } - -.vjs-menu-button.vjs-disabled { - cursor: default; } - -.vjs-workinghover .vjs-menu-button.vjs-disabled:hover .vjs-menu { - display: none; } - -.vjs-menu .vjs-menu-content { - display: block; - padding: 0; - margin: 0; - font-family: Arial, Helvetica, sans-serif; - overflow: auto; - box-sizing: content-box; } - -.vjs-menu .vjs-menu-content > * { - box-sizing: border-box; } - -.vjs-scrubbing .vjs-menu-button:hover .vjs-menu { - display: none; } - -.vjs-menu li { - list-style: none; - margin: 0; - padding: 0.2em 0; - line-height: 1.4em; - font-size: 1.2em; - text-align: center; - text-transform: lowercase; } - -.vjs-menu li.vjs-menu-item:focus, -.vjs-menu li.vjs-menu-item:hover { - background-color: #73859f; - background-color: rgba(115, 133, 159, 0.5); } - -.vjs-menu li.vjs-selected, -.vjs-menu li.vjs-selected:focus, -.vjs-menu li.vjs-selected:hover { - background-color: #fff; - color: #2B333F; } - -.vjs-menu li.vjs-menu-title { - text-align: center; - text-transform: uppercase; - font-size: 1em; - line-height: 2em; - padding: 0; - margin: 0 0 0.3em 0; - font-weight: bold; - cursor: default; } - -.vjs-menu-button-popup .vjs-menu { - display: none; - position: absolute; - bottom: 0; - width: 10em; - left: -3em; - height: 0em; - margin-bottom: 1.5em; - border-top-color: rgba(43, 51, 63, 0.7); } - -.vjs-menu-button-popup .vjs-menu .vjs-menu-content { - background-color: #2B333F; - background-color: rgba(43, 51, 63, 0.7); - position: absolute; - width: 100%; - bottom: 1.5em; - max-height: 15em; } - -.vjs-workinghover .vjs-menu-button-popup:hover .vjs-menu, -.vjs-menu-button-popup .vjs-menu.vjs-lock-showing { - display: block; } - -.video-js .vjs-menu-button-inline { - -webkit-transition: all 0.4s; - -moz-transition: all 0.4s; - -ms-transition: all 0.4s; - -o-transition: all 0.4s; - transition: all 0.4s; - overflow: hidden; } - -.video-js .vjs-menu-button-inline:before { - width: 2.222222222em; } - -.video-js .vjs-menu-button-inline:hover, -.video-js .vjs-menu-button-inline:focus, -.video-js .vjs-menu-button-inline.vjs-slider-active, -.video-js.vjs-no-flex .vjs-menu-button-inline { - width: 12em; } - -.vjs-menu-button-inline .vjs-menu { - opacity: 0; - height: 100%; - width: auto; - position: absolute; - left: 4em; - top: 0; - padding: 0; - margin: 0; - -webkit-transition: all 0.4s; - -moz-transition: all 0.4s; - -ms-transition: all 0.4s; - -o-transition: all 0.4s; - transition: all 0.4s; } - -.vjs-menu-button-inline:hover .vjs-menu, -.vjs-menu-button-inline:focus .vjs-menu, -.vjs-menu-button-inline.vjs-slider-active .vjs-menu { - display: block; - opacity: 1; } - -.vjs-no-flex .vjs-menu-button-inline .vjs-menu { - display: block; - opacity: 1; - position: relative; - width: auto; } - -.vjs-no-flex .vjs-menu-button-inline:hover .vjs-menu, -.vjs-no-flex .vjs-menu-button-inline:focus .vjs-menu, -.vjs-no-flex .vjs-menu-button-inline.vjs-slider-active .vjs-menu { - width: auto; } - -.vjs-menu-button-inline .vjs-menu-content { - width: auto; - height: 100%; - margin: 0; - overflow: hidden; } - -.video-js .vjs-control-bar { - display: none; - width: 100%; - position: absolute; - bottom: 0; - left: 0; - right: 0; - height: 3.0em; - background-color: #2B333F; - background-color: rgba(43, 51, 63, 0.7); } - -.vjs-has-started .vjs-control-bar { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - visibility: visible; - opacity: 1; - -webkit-transition: visibility 0.1s, opacity 0.1s; - -moz-transition: visibility 0.1s, opacity 0.1s; - -ms-transition: visibility 0.1s, opacity 0.1s; - -o-transition: visibility 0.1s, opacity 0.1s; - transition: visibility 0.1s, opacity 0.1s; } - -.vjs-has-started.vjs-user-inactive.vjs-playing .vjs-control-bar { - visibility: visible; - opacity: 0; - -webkit-transition: visibility 1s, opacity 1s; - -moz-transition: visibility 1s, opacity 1s; - -ms-transition: visibility 1s, opacity 1s; - -o-transition: visibility 1s, opacity 1s; - transition: visibility 1s, opacity 1s; } - -.vjs-controls-disabled .vjs-control-bar, -.vjs-using-native-controls .vjs-control-bar, -.vjs-error .vjs-control-bar { - display: none !important; } - -.vjs-audio.vjs-has-started.vjs-user-inactive.vjs-playing .vjs-control-bar { - opacity: 1; - visibility: visible; } - -.vjs-has-started.vjs-no-flex .vjs-control-bar { - display: table; } - -.video-js .vjs-control { - position: relative; - text-align: center; - margin: 0; - padding: 0; - height: 100%; - width: 4em; - -webkit-box-flex: none; - -moz-box-flex: none; - -webkit-flex: none; - -ms-flex: none; - flex: none; } - -.vjs-button > .vjs-icon-placeholder:before { - font-size: 1.8em; - line-height: 1.67; } - -.video-js .vjs-control:focus:before, -.video-js .vjs-control:hover:before, -.video-js .vjs-control:focus { - text-shadow: 0em 0em 1em white; } - -.video-js .vjs-control-text { - border: 0; - clip: rect(0 0 0 0); - height: 1px; - overflow: hidden; - padding: 0; - position: absolute; - width: 1px; } - -.vjs-no-flex .vjs-control { - display: table-cell; - vertical-align: middle; } - -.video-js .vjs-custom-control-spacer { - display: none; } - -.video-js .vjs-progress-control { - cursor: pointer; - -webkit-box-flex: auto; - -moz-box-flex: auto; - -webkit-flex: auto; - -ms-flex: auto; - flex: auto; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-box-align: center; - -webkit-align-items: center; - -ms-flex-align: center; - align-items: center; - min-width: 4em; } - -.video-js .vjs-progress-control.disabled { - cursor: default; } - -.vjs-live .vjs-progress-control { - display: none; } - -.vjs-no-flex .vjs-progress-control { - width: auto; } - -.video-js .vjs-progress-holder { - -webkit-box-flex: auto; - -moz-box-flex: auto; - -webkit-flex: auto; - -ms-flex: auto; - flex: auto; - -webkit-transition: all 0.2s; - -moz-transition: all 0.2s; - -ms-transition: all 0.2s; - -o-transition: all 0.2s; - transition: all 0.2s; - height: 0.3em; } - -.video-js .vjs-progress-control .vjs-progress-holder { - margin: 0 10px; } - -.video-js .vjs-progress-control:hover .vjs-progress-holder { - font-size: 1.666666666666666666em; } - -.video-js .vjs-progress-control:hover .vjs-progress-holder.disabled { - font-size: 1em; } - -.video-js .vjs-progress-holder .vjs-play-progress, -.video-js .vjs-progress-holder .vjs-load-progress, -.video-js .vjs-progress-holder .vjs-load-progress div { - position: absolute; - display: block; - height: 100%; - margin: 0; - padding: 0; - width: 0; - left: 0; - top: 0; } - -.video-js .vjs-play-progress { - background-color: #fff; } - .video-js .vjs-play-progress:before { - font-size: 0.9em; - position: absolute; - right: -0.5em; - top: -0.333333333333333em; - z-index: 1; } - -.video-js .vjs-load-progress { - background: #bfc7d3; - background: rgba(115, 133, 159, 0.5); } - -.video-js .vjs-load-progress div { - background: white; - background: rgba(115, 133, 159, 0.75); } - -.video-js .vjs-time-tooltip { - background-color: #fff; - background-color: rgba(255, 255, 255, 0.8); - -webkit-border-radius: 0.3em; - -moz-border-radius: 0.3em; - border-radius: 0.3em; - color: #000; - float: right; - font-family: Arial, Helvetica, sans-serif; - font-size: 1em; - padding: 6px 8px 8px 8px; - pointer-events: none; - position: relative; - top: -3.4em; - visibility: hidden; - z-index: 1; } - -.video-js .vjs-progress-holder:focus .vjs-time-tooltip { - display: none; } - -.video-js .vjs-progress-control:hover .vjs-time-tooltip, -.video-js .vjs-progress-control:hover .vjs-progress-holder:focus .vjs-time-tooltip { - display: block; - font-size: 0.6em; - visibility: visible; } - -.video-js .vjs-progress-control.disabled:hover .vjs-time-tooltip { - font-size: 1em; } - -.video-js .vjs-progress-control .vjs-mouse-display { - display: none; - position: absolute; - width: 1px; - height: 100%; - background-color: #000; - z-index: 1; } - -.vjs-no-flex .vjs-progress-control .vjs-mouse-display { - z-index: 0; } - -.video-js .vjs-progress-control:hover .vjs-mouse-display { - display: block; } - -.video-js.vjs-user-inactive .vjs-progress-control .vjs-mouse-display { - visibility: hidden; - opacity: 0; - -webkit-transition: visibility 1s, opacity 1s; - -moz-transition: visibility 1s, opacity 1s; - -ms-transition: visibility 1s, opacity 1s; - -o-transition: visibility 1s, opacity 1s; - transition: visibility 1s, opacity 1s; } - -.video-js.vjs-user-inactive.vjs-no-flex .vjs-progress-control .vjs-mouse-display { - display: none; } - -.vjs-mouse-display .vjs-time-tooltip { - color: #fff; - background-color: #000; - background-color: rgba(0, 0, 0, 0.8); } - -.video-js .vjs-slider { - position: relative; - cursor: pointer; - padding: 0; - margin: 0 0.45em 0 0.45em; - /* iOS Safari */ - -webkit-touch-callout: none; - /* Safari */ - -webkit-user-select: none; - /* Konqueror HTML */ - -khtml-user-select: none; - /* Firefox */ - -moz-user-select: none; - /* Internet Explorer/Edge */ - -ms-user-select: none; - /* Non-prefixed version, currently supported by Chrome and Opera */ - user-select: none; - background-color: #73859f; - background-color: rgba(115, 133, 159, 0.5); } - -.video-js .vjs-slider.disabled { - cursor: default; } - -.video-js .vjs-slider:focus { - text-shadow: 0em 0em 1em white; - -webkit-box-shadow: 0 0 1em #fff; - -moz-box-shadow: 0 0 1em #fff; - box-shadow: 0 0 1em #fff; } - -.video-js .vjs-mute-control { - cursor: pointer; - -webkit-box-flex: none; - -moz-box-flex: none; - -webkit-flex: none; - -ms-flex: none; - flex: none; - padding-left: 2em; - padding-right: 2em; - padding-bottom: 3em; } - -.video-js .vjs-volume-control { - cursor: pointer; - margin-right: 1em; - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; } - -.video-js .vjs-volume-control.vjs-volume-horizontal { - width: 5em; } - -.video-js .vjs-volume-panel .vjs-volume-control { - visibility: visible; - opacity: 0; - width: 1px; - height: 1px; - margin-left: -1px; } - -.vjs-no-flex .vjs-volume-panel .vjs-volume-control.vjs-volume-vertical { - -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; } - .vjs-no-flex .vjs-volume-panel .vjs-volume-control.vjs-volume-vertical .vjs-volume-bar, - .vjs-no-flex .vjs-volume-panel .vjs-volume-control.vjs-volume-vertical .vjs-volume-level { - -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; } - -.video-js .vjs-volume-panel { - -webkit-transition: width 1s; - -moz-transition: width 1s; - -ms-transition: width 1s; - -o-transition: width 1s; - transition: width 1s; } - .video-js .vjs-volume-panel:hover .vjs-volume-control, - .video-js .vjs-volume-panel:active .vjs-volume-control, - .video-js .vjs-volume-panel:focus .vjs-volume-control, - .video-js .vjs-volume-panel .vjs-volume-control:hover, - .video-js .vjs-volume-panel .vjs-volume-control:active, - .video-js .vjs-volume-panel .vjs-mute-control:hover ~ .vjs-volume-control, - .video-js .vjs-volume-panel .vjs-volume-control.vjs-slider-active { - visibility: visible; - opacity: 1; - position: relative; - -webkit-transition: visibility 0.1s, opacity 0.1s, height 0.1s, width 0.1s, left 0s, top 0s; - -moz-transition: visibility 0.1s, opacity 0.1s, height 0.1s, width 0.1s, left 0s, top 0s; - -ms-transition: visibility 0.1s, opacity 0.1s, height 0.1s, width 0.1s, left 0s, top 0s; - -o-transition: visibility 0.1s, opacity 0.1s, height 0.1s, width 0.1s, left 0s, top 0s; - transition: visibility 0.1s, opacity 0.1s, height 0.1s, width 0.1s, left 0s, top 0s; } - .video-js .vjs-volume-panel:hover .vjs-volume-control.vjs-volume-horizontal, - .video-js .vjs-volume-panel:active .vjs-volume-control.vjs-volume-horizontal, - .video-js .vjs-volume-panel:focus .vjs-volume-control.vjs-volume-horizontal, - .video-js .vjs-volume-panel .vjs-volume-control:hover.vjs-volume-horizontal, - .video-js .vjs-volume-panel .vjs-volume-control:active.vjs-volume-horizontal, - .video-js .vjs-volume-panel .vjs-mute-control:hover ~ .vjs-volume-control.vjs-volume-horizontal, - .video-js .vjs-volume-panel .vjs-volume-control.vjs-slider-active.vjs-volume-horizontal { - width: 5em; - height: 3em; } - .video-js .vjs-volume-panel:hover .vjs-volume-control.vjs-volume-vertical, - .video-js .vjs-volume-panel:active .vjs-volume-control.vjs-volume-vertical, - .video-js .vjs-volume-panel:focus .vjs-volume-control.vjs-volume-vertical, - .video-js .vjs-volume-panel .vjs-volume-control:hover.vjs-volume-vertical, - .video-js .vjs-volume-panel .vjs-volume-control:active.vjs-volume-vertical, - .video-js .vjs-volume-panel .vjs-mute-control:hover ~ .vjs-volume-control.vjs-volume-vertical, - .video-js .vjs-volume-panel .vjs-volume-control.vjs-slider-active.vjs-volume-vertical { - -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=100)"; } - .video-js .vjs-volume-panel:hover .vjs-volume-control.vjs-volume-vertical .vjs-volume-bar, - .video-js .vjs-volume-panel:hover .vjs-volume-control.vjs-volume-vertical .vjs-volume-level, - .video-js .vjs-volume-panel:active .vjs-volume-control.vjs-volume-vertical .vjs-volume-bar, - .video-js .vjs-volume-panel:active .vjs-volume-control.vjs-volume-vertical .vjs-volume-level, - .video-js .vjs-volume-panel:focus .vjs-volume-control.vjs-volume-vertical .vjs-volume-bar, - .video-js .vjs-volume-panel:focus .vjs-volume-control.vjs-volume-vertical .vjs-volume-level, - .video-js .vjs-volume-panel .vjs-volume-control:hover.vjs-volume-vertical .vjs-volume-bar, - .video-js .vjs-volume-panel .vjs-volume-control:hover.vjs-volume-vertical .vjs-volume-level, - .video-js .vjs-volume-panel .vjs-volume-control:active.vjs-volume-vertical .vjs-volume-bar, - .video-js .vjs-volume-panel .vjs-volume-control:active.vjs-volume-vertical .vjs-volume-level, - .video-js .vjs-volume-panel .vjs-mute-control:hover ~ .vjs-volume-control.vjs-volume-vertical .vjs-volume-bar, - .video-js .vjs-volume-panel .vjs-mute-control:hover ~ .vjs-volume-control.vjs-volume-vertical .vjs-volume-level, - .video-js .vjs-volume-panel .vjs-volume-control.vjs-slider-active.vjs-volume-vertical .vjs-volume-bar, - .video-js .vjs-volume-panel .vjs-volume-control.vjs-slider-active.vjs-volume-vertical .vjs-volume-level { - -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=100)"; } - .video-js .vjs-volume-panel.vjs-volume-panel-horizontal:hover, .video-js .vjs-volume-panel.vjs-volume-panel-horizontal:active, .video-js .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-slider-active { - width: 9em; - -webkit-transition: width 0.1s; - -moz-transition: width 0.1s; - -ms-transition: width 0.1s; - -o-transition: width 0.1s; - transition: width 0.1s; } - -.video-js .vjs-volume-panel .vjs-volume-control.vjs-volume-vertical { - height: 8em; - width: 3em; - left: -3.5em; - -webkit-transition: visibility 1s, opacity 1s, height 1s 1s, width 1s 1s, left 1s 1s, top 1s 1s; - -moz-transition: visibility 1s, opacity 1s, height 1s 1s, width 1s 1s, left 1s 1s, top 1s 1s; - -ms-transition: visibility 1s, opacity 1s, height 1s 1s, width 1s 1s, left 1s 1s, top 1s 1s; - -o-transition: visibility 1s, opacity 1s, height 1s 1s, width 1s 1s, left 1s 1s, top 1s 1s; - transition: visibility 1s, opacity 1s, height 1s 1s, width 1s 1s, left 1s 1s, top 1s 1s; } - -.video-js .vjs-volume-panel .vjs-volume-control.vjs-volume-horizontal { - -webkit-transition: visibility 1s, opacity 1s, height 1s 1s, width 1s, left 1s 1s, top 1s 1s; - -moz-transition: visibility 1s, opacity 1s, height 1s 1s, width 1s, left 1s 1s, top 1s 1s; - -ms-transition: visibility 1s, opacity 1s, height 1s 1s, width 1s, left 1s 1s, top 1s 1s; - -o-transition: visibility 1s, opacity 1s, height 1s 1s, width 1s, left 1s 1s, top 1s 1s; - transition: visibility 1s, opacity 1s, height 1s 1s, width 1s, left 1s 1s, top 1s 1s; } - -.video-js.vjs-no-flex .vjs-volume-panel .vjs-volume-control.vjs-volume-horizontal { - width: 5em; - height: 3em; - visibility: visible; - opacity: 1; - position: relative; - -webkit-transition: none; - -moz-transition: none; - -ms-transition: none; - -o-transition: none; - transition: none; } - -.video-js.vjs-no-flex .vjs-volume-control.vjs-volume-vertical, -.video-js.vjs-no-flex .vjs-volume-panel .vjs-volume-control.vjs-volume-vertical { - position: absolute; - bottom: 3em; - left: 0.5em; } - -.video-js .vjs-volume-panel { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; } - -.video-js .vjs-volume-bar { - margin: 1.35em 0.45em; } - -.vjs-volume-bar.vjs-slider-horizontal { - width: 5em; - height: 0.3em; } - -.vjs-volume-bar.vjs-slider-vertical { - width: 0.3em; - height: 5em; - margin: 1.35em auto; } - -.video-js .vjs-volume-level { - position: absolute; - bottom: 0; - left: 0; - background-color: #fff; } - .video-js .vjs-volume-level:before { - position: absolute; - font-size: 0.9em; } - -.vjs-slider-vertical .vjs-volume-level { - width: 0.3em; } - .vjs-slider-vertical .vjs-volume-level:before { - top: -0.5em; - left: -0.3em; } - -.vjs-slider-horizontal .vjs-volume-level { - height: 0.3em; } - .vjs-slider-horizontal .vjs-volume-level:before { - top: -0.3em; - right: -0.5em; } - -.video-js .vjs-volume-panel.vjs-volume-panel-vertical { - width: 4em; } - -.vjs-volume-bar.vjs-slider-vertical .vjs-volume-level { - height: 100%; } - -.vjs-volume-bar.vjs-slider-horizontal .vjs-volume-level { - width: 100%; } - -.video-js .vjs-volume-vertical { - width: 3em; - height: 8em; - bottom: 8em; - background-color: #2B333F; - background-color: rgba(43, 51, 63, 0.7); } - -.video-js .vjs-volume-horizontal .vjs-menu { - left: -2em; } - -.vjs-poster { - display: inline-block; - vertical-align: middle; - background-repeat: no-repeat; - background-position: 50% 50%; - background-size: contain; - background-color: #000000; - cursor: pointer; - margin: 0; - padding: 0; - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - height: 100%; } - -.vjs-poster img { - display: block; - vertical-align: middle; - margin: 0 auto; - max-height: 100%; - padding: 0; - width: 100%; } - -.vjs-has-started .vjs-poster { - display: none; } - -.vjs-audio.vjs-has-started .vjs-poster { - display: block; } - -.vjs-using-native-controls .vjs-poster { - display: none; } - -.video-js .vjs-live-control { - display: -webkit-box; - display: -webkit-flex; - display: -ms-flexbox; - display: flex; - -webkit-box-align: flex-start; - -webkit-align-items: flex-start; - -ms-flex-align: flex-start; - align-items: flex-start; - -webkit-box-flex: auto; - -moz-box-flex: auto; - -webkit-flex: auto; - -ms-flex: auto; - flex: auto; - font-size: 1em; - line-height: 3em; } - -.vjs-no-flex .vjs-live-control { - display: table-cell; - width: auto; - text-align: left; } - -.video-js .vjs-time-control { - -webkit-box-flex: none; - -moz-box-flex: none; - -webkit-flex: none; - -ms-flex: none; - flex: none; - font-size: 1em; - line-height: 3em; - min-width: 2em; - width: auto; - padding-left: 1em; - padding-right: 1em; } - -.vjs-live .vjs-time-control { - display: none; } - -.video-js .vjs-current-time, -.vjs-no-flex .vjs-current-time { - display: none; } - -.vjs-no-flex .vjs-remaining-time.vjs-time-control.vjs-control { - width: 0px !important; - white-space: nowrap; } - -.video-js .vjs-duration, -.vjs-no-flex .vjs-duration { - display: none; } - -.vjs-time-divider { - display: none; - line-height: 3em; } - -.vjs-live .vjs-time-divider { - display: none; } - -.video-js .vjs-play-control .vjs-icon-placeholder { - cursor: pointer; - -webkit-box-flex: none; - -moz-box-flex: none; - -webkit-flex: none; - -ms-flex: none; - flex: none; } - -.vjs-text-track-display { - position: absolute; - bottom: 3em; - left: 0; - right: 0; - top: 0; - pointer-events: none; } - -.video-js.vjs-user-inactive.vjs-playing .vjs-text-track-display { - bottom: 1em; } - -.video-js .vjs-text-track { - font-size: 1.4em; - text-align: center; - margin-bottom: 0.1em; - background-color: #000; - background-color: rgba(0, 0, 0, 0.5); } - -.vjs-subtitles { - color: #fff; } - -.vjs-captions { - color: #fc6; } - -.vjs-tt-cue { - display: block; } - -video::-webkit-media-text-track-display { - -moz-transform: translateY(-3em); - -ms-transform: translateY(-3em); - -o-transform: translateY(-3em); - -webkit-transform: translateY(-3em); - transform: translateY(-3em); } - -.video-js.vjs-user-inactive.vjs-playing video::-webkit-media-text-track-display { - -moz-transform: translateY(-1.5em); - -ms-transform: translateY(-1.5em); - -o-transform: translateY(-1.5em); - -webkit-transform: translateY(-1.5em); - transform: translateY(-1.5em); } - -.video-js .vjs-fullscreen-control { - cursor: pointer; - -webkit-box-flex: none; - -moz-box-flex: none; - -webkit-flex: none; - -ms-flex: none; - flex: none; } - -.vjs-playback-rate > .vjs-menu-button, -.vjs-playback-rate .vjs-playback-rate-value { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; } - -.vjs-playback-rate .vjs-playback-rate-value { - pointer-events: none; - font-size: 1.5em; - line-height: 2; - text-align: center; } - -.vjs-playback-rate .vjs-menu { - width: 4em; - left: 0em; } - -.vjs-error .vjs-error-display .vjs-modal-dialog-content { - font-size: 1.4em; - text-align: center; } - -.vjs-error .vjs-error-display:before { - color: #fff; - content: 'X'; - font-family: Arial, Helvetica, sans-serif; - font-size: 4em; - left: 0; - line-height: 1; - margin-top: -0.5em; - position: absolute; - text-shadow: 0.05em 0.05em 0.1em #000; - text-align: center; - top: 50%; - vertical-align: middle; - width: 100%; } - -.vjs-loading-spinner { - display: none; - position: absolute; - top: 50%; - left: 50%; - margin: -25px 0 0 -25px; - opacity: 0.85; - text-align: left; - border: 6px solid rgba(43, 51, 63, 0.7); - box-sizing: border-box; - background-clip: padding-box; - width: 50px; - height: 50px; - border-radius: 25px; - visibility: hidden; } - -.vjs-seeking .vjs-loading-spinner, -.vjs-waiting .vjs-loading-spinner { - display: block; - animation: 0s linear 0.3s forwards vjs-spinner-show; } - -.vjs-loading-spinner:before, -.vjs-loading-spinner:after { - content: ""; - position: absolute; - margin: -6px; - box-sizing: inherit; - width: inherit; - height: inherit; - border-radius: inherit; - opacity: 1; - border: inherit; - border-color: transparent; - border-top-color: white; } - -.vjs-seeking .vjs-loading-spinner:before, -.vjs-seeking .vjs-loading-spinner:after, -.vjs-waiting .vjs-loading-spinner:before, -.vjs-waiting .vjs-loading-spinner:after { - -webkit-animation: vjs-spinner-spin 1.1s cubic-bezier(0.6, 0.2, 0, 0.8) infinite, vjs-spinner-fade 1.1s linear infinite; - animation: vjs-spinner-spin 1.1s cubic-bezier(0.6, 0.2, 0, 0.8) infinite, vjs-spinner-fade 1.1s linear infinite; } - -.vjs-seeking .vjs-loading-spinner:before, -.vjs-waiting .vjs-loading-spinner:before { - border-top-color: white; } - -.vjs-seeking .vjs-loading-spinner:after, -.vjs-waiting .vjs-loading-spinner:after { - border-top-color: white; - -webkit-animation-delay: 0.44s; - animation-delay: 0.44s; } - -@keyframes vjs-spinner-show { - to { - visibility: visible; } } - -@-webkit-keyframes vjs-spinner-show { - to { - visibility: visible; } } - -@keyframes vjs-spinner-spin { - 100% { - transform: rotate(360deg); } } - -@-webkit-keyframes vjs-spinner-spin { - 100% { - -webkit-transform: rotate(360deg); } } - -@keyframes vjs-spinner-fade { - 0% { - border-top-color: #73859f; } - 20% { - border-top-color: #73859f; } - 35% { - border-top-color: white; } - 60% { - border-top-color: #73859f; } - 100% { - border-top-color: #73859f; } } - -@-webkit-keyframes vjs-spinner-fade { - 0% { - border-top-color: #73859f; } - 20% { - border-top-color: #73859f; } - 35% { - border-top-color: white; } - 60% { - border-top-color: #73859f; } - 100% { - border-top-color: #73859f; } } - -.vjs-chapters-button .vjs-menu ul { - width: 24em; } - -.video-js .vjs-subs-caps-button + .vjs-menu .vjs-captions-menu-item .vjs-menu-item-text .vjs-icon-placeholder { - position: absolute; } - -.video-js .vjs-subs-caps-button + .vjs-menu .vjs-captions-menu-item .vjs-menu-item-text .vjs-icon-placeholder:before { - font-family: VideoJS; - content: "\f10d"; - font-size: 1.5em; - line-height: inherit; } - -.video-js.vjs-layout-tiny:not(.vjs-fullscreen) .vjs-custom-control-spacer { - -webkit-box-flex: auto; - -moz-box-flex: auto; - -webkit-flex: auto; - -ms-flex: auto; - flex: auto; } - -.video-js.vjs-layout-tiny:not(.vjs-fullscreen).vjs-no-flex .vjs-custom-control-spacer { - width: auto; } - -.video-js.vjs-layout-tiny:not(.vjs-fullscreen) .vjs-current-time, .video-js.vjs-layout-tiny:not(.vjs-fullscreen) .vjs-time-divider, .video-js.vjs-layout-tiny:not(.vjs-fullscreen) .vjs-duration, .video-js.vjs-layout-tiny:not(.vjs-fullscreen) .vjs-remaining-time, -.video-js.vjs-layout-tiny:not(.vjs-fullscreen) .vjs-playback-rate, .video-js.vjs-layout-tiny:not(.vjs-fullscreen) .vjs-progress-control, -.video-js.vjs-layout-tiny:not(.vjs-fullscreen) .vjs-mute-control, .video-js.vjs-layout-tiny:not(.vjs-fullscreen) .vjs-volume-control, -.video-js.vjs-layout-tiny:not(.vjs-fullscreen) .vjs-chapters-button, .video-js.vjs-layout-tiny:not(.vjs-fullscreen) .vjs-descriptions-button, .video-js.vjs-layout-tiny:not(.vjs-fullscreen) .vjs-captions-button, -.video-js.vjs-layout-tiny:not(.vjs-fullscreen) .vjs-subtitles-button, .video-js.vjs-layout-tiny:not(.vjs-fullscreen) .vjs-audio-button { - display: none; } - -.video-js.vjs-layout-x-small:not(.vjs-fullscreen) .vjs-current-time, .video-js.vjs-layout-x-small:not(.vjs-fullscreen) .vjs-time-divider, .video-js.vjs-layout-x-small:not(.vjs-fullscreen) .vjs-duration, .video-js.vjs-layout-x-small:not(.vjs-fullscreen) .vjs-remaining-time, -.video-js.vjs-layout-x-small:not(.vjs-fullscreen) .vjs-playback-rate, -.video-js.vjs-layout-x-small:not(.vjs-fullscreen) .vjs-mute-control, .video-js.vjs-layout-x-small:not(.vjs-fullscreen) .vjs-volume-control, -.video-js.vjs-layout-x-small:not(.vjs-fullscreen) .vjs-chapters-button, .video-js.vjs-layout-x-small:not(.vjs-fullscreen) .vjs-descriptions-button, .video-js.vjs-layout-x-small:not(.vjs-fullscreen) .vjs-captions-button, -.video-js.vjs-layout-x-small:not(.vjs-fullscreen) .vjs-subtitles-button, .video-js.vjs-layout-x-small:not(.vjs-fullscreen) .vjs-audio-button { - display: none; } - -.video-js.vjs-layout-small:not(.vjs-fullscreen) .vjs-current-time, .video-js.vjs-layout-small:not(.vjs-fullscreen) .vjs-time-divider, .video-js.vjs-layout-small:not(.vjs-fullscreen) .vjs-duration, .video-js.vjs-layout-small:not(.vjs-fullscreen) .vjs-remaining-time, -.video-js.vjs-layout-small:not(.vjs-fullscreen) .vjs-playback-rate, -.video-js.vjs-layout-small:not(.vjs-fullscreen) .vjs-mute-control, .video-js.vjs-layout-small:not(.vjs-fullscreen) .vjs-volume-control, -.video-js.vjs-layout-small:not(.vjs-fullscreen) .vjs-chapters-button, .video-js.vjs-layout-small:not(.vjs-fullscreen) .vjs-descriptions-button, .video-js.vjs-layout-small:not(.vjs-fullscreen) .vjs-captions-button, -.video-js.vjs-layout-small:not(.vjs-fullscreen) .vjs-subtitles-button .vjs-audio-button { - display: none; } - -.vjs-modal-dialog.vjs-text-track-settings { - background-color: #2B333F; - background-color: rgba(43, 51, 63, 0.75); - color: #fff; - height: 70%; } - -.vjs-text-track-settings .vjs-modal-dialog-content { - display: table; } - -.vjs-text-track-settings .vjs-track-settings-colors, -.vjs-text-track-settings .vjs-track-settings-font, -.vjs-text-track-settings .vjs-track-settings-controls { - display: table-cell; } - -.vjs-text-track-settings .vjs-track-settings-controls { - text-align: right; - vertical-align: bottom; } - -@supports (display: grid) { - .vjs-text-track-settings .vjs-modal-dialog-content { - display: grid; - grid-template-columns: 1fr 1fr; - grid-template-rows: 1fr auto; } - .vjs-text-track-settings .vjs-track-settings-colors { - display: block; - grid-column: 1; - grid-row: 1; } - .vjs-text-track-settings .vjs-track-settings-font { - grid-column: 2; - grid-row: 1; } - .vjs-text-track-settings .vjs-track-settings-controls { - grid-column: 2; - grid-row: 2; } } - -.vjs-track-setting > select { - margin-right: 5px; } - -.vjs-text-track-settings fieldset { - margin: 5px; - padding: 3px; - border: none; } - -.vjs-text-track-settings fieldset span { - display: inline-block; } - -.vjs-text-track-settings legend { - color: #fff; - margin: 0 0 5px 0; } - -.vjs-text-track-settings .vjs-label { - position: absolute; - clip: rect(1px 1px 1px 1px); - clip: rect(1px, 1px, 1px, 1px); - display: block; - margin: 0 0 5px 0; - padding: 0; - border: 0; - height: 1px; - width: 1px; - overflow: hidden; } - -.vjs-track-settings-controls button:focus, -.vjs-track-settings-controls button:active { - outline-style: solid; - outline-width: medium; - background-image: linear-gradient(0deg, #fff 88%, #73859f 100%); } - -.vjs-track-settings-controls button:hover { - color: rgba(43, 51, 63, 0.75); } - -.vjs-track-settings-controls button { - background-color: #fff; - background-image: linear-gradient(-180deg, #fff 88%, #73859f 100%); - color: #2B333F; - cursor: pointer; - border-radius: 2px; } - -.vjs-track-settings-controls .vjs-default-button { - margin-right: 1em; } - -@media print { - .video-js > *:not(.vjs-tech):not(.vjs-poster) { - visibility: hidden; } } - -.vjs-resize-manager { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - border: none; - visibility: hidden; } diff --git a/pod/main/static/video-js-6.8.0/alt/video-js-cdn.min.css b/pod/main/static/video-js-6.8.0/alt/video-js-cdn.min.css deleted file mode 100755 index 414fd4c7a6..0000000000 --- a/pod/main/static/video-js-6.8.0/alt/video-js-cdn.min.css +++ /dev/null @@ -1 +0,0 @@ -.video-js .vjs-big-play-button .vjs-icon-placeholder:before,.video-js .vjs-modal-dialog,.vjs-button>.vjs-icon-placeholder:before,.vjs-modal-dialog .vjs-modal-dialog-content{position:absolute;top:0;left:0;width:100%;height:100%}.video-js .vjs-big-play-button .vjs-icon-placeholder:before,.vjs-button>.vjs-icon-placeholder:before{text-align:center}@font-face{font-family:VideoJS;src:url(//vjs.zencdn.net/font/1.5.1/VideoJS.eot?#iefix) format("eot")}@font-face{font-family:VideoJS;src:url(data:application/font-woff;charset=utf-8;base64,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) format("woff"),url(data:application/x-font-ttf;charset=utf-8;base64,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AG8AbgB0AGUAbABsAG8AIABwAHIAbwBqAGUAYwB0AC4AaAB0AHQAcAA6AC8ALwBmAG8AbgB0AGUAbABsAG8ALgBjAG8AbQAAAAIAAAAAAAAAEQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIQECAQMBBAEFAQYBBwEIAQkBCgELAQwBDQEOAQ8BEAERARIBEwEUARUBFgEXARgBGQEaARsBHAEdAR4BHwEgASEBIgAEcGxheQtwbGF5LWNpcmNsZQVwYXVzZQt2b2x1bWUtbXV0ZQp2b2x1bWUtbG93CnZvbHVtZS1taWQLdm9sdW1lLWhpZ2gQZnVsbHNjcmVlbi1lbnRlcg9mdWxsc2NyZWVuLWV4aXQGc3F1YXJlB3NwaW5uZXIJc3VidGl0bGVzCGNhcHRpb25zCGNoYXB0ZXJzBXNoYXJlA2NvZwZjaXJjbGUOY2lyY2xlLW91dGxpbmUTY2lyY2xlLWlubmVyLWNpcmNsZQJoZAZjYW5jZWwGcmVwbGF5CGZhY2Vib29rBWdwbHVzCGxpbmtlZGluB3R3aXR0ZXIGdHVtYmxyCXBpbnRlcmVzdBFhdWRpby1kZXNjcmlwdGlvbgVhdWRpbwluZXh0LWl0ZW0NcHJldmlvdXMtaXRlbQAAAAA=) format("truetype");font-weight:400;font-style:normal}.video-js .vjs-big-play-button .vjs-icon-placeholder:before,.video-js .vjs-play-control .vjs-icon-placeholder,.vjs-icon-play{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-big-play-button .vjs-icon-placeholder:before,.video-js .vjs-play-control .vjs-icon-placeholder:before,.vjs-icon-play:before{content:"\f101"}.vjs-icon-play-circle{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-play-circle:before{content:"\f102"}.video-js .vjs-play-control.vjs-playing .vjs-icon-placeholder,.vjs-icon-pause{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-play-control.vjs-playing .vjs-icon-placeholder:before,.vjs-icon-pause:before{content:"\f103"}.video-js .vjs-mute-control.vjs-vol-0 .vjs-icon-placeholder,.vjs-icon-volume-mute{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-mute-control.vjs-vol-0 .vjs-icon-placeholder:before,.vjs-icon-volume-mute:before{content:"\f104"}.video-js .vjs-mute-control.vjs-vol-1 .vjs-icon-placeholder,.vjs-icon-volume-low{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-mute-control.vjs-vol-1 .vjs-icon-placeholder:before,.vjs-icon-volume-low:before{content:"\f105"}.video-js .vjs-mute-control.vjs-vol-2 .vjs-icon-placeholder,.vjs-icon-volume-mid{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-mute-control.vjs-vol-2 .vjs-icon-placeholder:before,.vjs-icon-volume-mid:before{content:"\f106"}.video-js .vjs-mute-control .vjs-icon-placeholder,.vjs-icon-volume-high{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-mute-control .vjs-icon-placeholder:before,.vjs-icon-volume-high:before{content:"\f107"}.video-js .vjs-fullscreen-control .vjs-icon-placeholder,.vjs-icon-fullscreen-enter{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-fullscreen-control .vjs-icon-placeholder:before,.vjs-icon-fullscreen-enter:before{content:"\f108"}.video-js.vjs-fullscreen .vjs-fullscreen-control .vjs-icon-placeholder,.vjs-icon-fullscreen-exit{font-family:VideoJS;font-weight:400;font-style:normal}.video-js.vjs-fullscreen .vjs-fullscreen-control .vjs-icon-placeholder:before,.vjs-icon-fullscreen-exit:before{content:"\f109"}.vjs-icon-square{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-square:before{content:"\f10a"}.vjs-icon-spinner{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-spinner:before{content:"\f10b"}.video-js .vjs-subs-caps-button .vjs-icon-placeholder,.video-js .vjs-subtitles-button .vjs-icon-placeholder,.video-js.video-js:lang(en-AU) .vjs-subs-caps-button .vjs-icon-placeholder,.video-js.video-js:lang(en-GB) .vjs-subs-caps-button .vjs-icon-placeholder,.video-js.video-js:lang(en-IE) .vjs-subs-caps-button .vjs-icon-placeholder,.video-js.video-js:lang(en-NZ) .vjs-subs-caps-button .vjs-icon-placeholder,.vjs-icon-subtitles{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-subs-caps-button .vjs-icon-placeholder:before,.video-js .vjs-subtitles-button .vjs-icon-placeholder:before,.video-js.video-js:lang(en-AU) .vjs-subs-caps-button .vjs-icon-placeholder:before,.video-js.video-js:lang(en-GB) .vjs-subs-caps-button .vjs-icon-placeholder:before,.video-js.video-js:lang(en-IE) .vjs-subs-caps-button .vjs-icon-placeholder:before,.video-js.video-js:lang(en-NZ) .vjs-subs-caps-button .vjs-icon-placeholder:before,.vjs-icon-subtitles:before{content:"\f10c"}.video-js .vjs-captions-button .vjs-icon-placeholder,.video-js:lang(en) .vjs-subs-caps-button .vjs-icon-placeholder,.video-js:lang(fr-CA) .vjs-subs-caps-button .vjs-icon-placeholder,.vjs-icon-captions{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-captions-button .vjs-icon-placeholder:before,.video-js:lang(en) .vjs-subs-caps-button .vjs-icon-placeholder:before,.video-js:lang(fr-CA) .vjs-subs-caps-button .vjs-icon-placeholder:before,.vjs-icon-captions:before{content:"\f10d"}.video-js .vjs-chapters-button .vjs-icon-placeholder,.vjs-icon-chapters{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-chapters-button .vjs-icon-placeholder:before,.vjs-icon-chapters:before{content:"\f10e"}.vjs-icon-share{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-share:before{content:"\f10f"}.vjs-icon-cog{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-cog:before{content:"\f110"}.video-js .vjs-play-progress,.video-js .vjs-volume-level,.vjs-icon-circle{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-play-progress:before,.video-js .vjs-volume-level:before,.vjs-icon-circle:before{content:"\f111"}.vjs-icon-circle-outline{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-circle-outline:before{content:"\f112"}.vjs-icon-circle-inner-circle{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-circle-inner-circle:before{content:"\f113"}.vjs-icon-hd{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-hd:before{content:"\f114"}.video-js .vjs-control.vjs-close-button .vjs-icon-placeholder,.vjs-icon-cancel{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-control.vjs-close-button .vjs-icon-placeholder:before,.vjs-icon-cancel:before{content:"\f115"}.video-js .vjs-play-control.vjs-ended .vjs-icon-placeholder,.vjs-icon-replay{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-play-control.vjs-ended .vjs-icon-placeholder:before,.vjs-icon-replay:before{content:"\f116"}.vjs-icon-facebook{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-facebook:before{content:"\f117"}.vjs-icon-gplus{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-gplus:before{content:"\f118"}.vjs-icon-linkedin{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-linkedin:before{content:"\f119"}.vjs-icon-twitter{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-twitter:before{content:"\f11a"}.vjs-icon-tumblr{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-tumblr:before{content:"\f11b"}.vjs-icon-pinterest{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-pinterest:before{content:"\f11c"}.video-js .vjs-descriptions-button .vjs-icon-placeholder,.vjs-icon-audio-description{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-descriptions-button .vjs-icon-placeholder:before,.vjs-icon-audio-description:before{content:"\f11d"}.video-js .vjs-audio-button .vjs-icon-placeholder,.vjs-icon-audio{font-family:VideoJS;font-weight:400;font-style:normal}.video-js .vjs-audio-button .vjs-icon-placeholder:before,.vjs-icon-audio:before{content:"\f11e"}.vjs-icon-next-item{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-next-item:before{content:"\f11f"}.vjs-icon-previous-item{font-family:VideoJS;font-weight:400;font-style:normal}.vjs-icon-previous-item:before{content:"\f120"}.video-js{display:block;vertical-align:top;box-sizing:border-box;color:#fff;background-color:#000;position:relative;padding:0;font-size:10px;line-height:1;font-weight:400;font-style:normal;font-family:Arial,Helvetica,sans-serif;word-break:initial}.video-js:-moz-full-screen{position:absolute}.video-js:-webkit-full-screen{width:100%!important;height:100%!important}.video-js[tabindex="-1"]{outline:0}.video-js *,.video-js :after,.video-js :before{box-sizing:inherit}.video-js ul{font-family:inherit;font-size:inherit;line-height:inherit;list-style-position:outside;margin-left:0;margin-right:0;margin-top:0;margin-bottom:0}.video-js.vjs-16-9,.video-js.vjs-4-3,.video-js.vjs-fluid{width:100%;max-width:100%;height:0}.video-js.vjs-16-9{padding-top:56.25%}.video-js.vjs-4-3{padding-top:75%}.video-js.vjs-fill{width:100%;height:100%}.video-js .vjs-tech{position:absolute;top:0;left:0;width:100%;height:100%}body.vjs-full-window{padding:0;margin:0;height:100%;overflow-y:auto}.vjs-full-window .video-js.vjs-fullscreen{position:fixed;overflow:hidden;z-index:1000;left:0;top:0;bottom:0;right:0}.video-js.vjs-fullscreen{width:100%!important;height:100%!important;padding-top:0!important}.video-js.vjs-fullscreen.vjs-user-inactive{cursor:none}.vjs-hidden{display:none!important}.vjs-disabled{opacity:.5;cursor:default}.video-js .vjs-offscreen{height:1px;left:-9999px;position:absolute;top:0;width:1px}.vjs-lock-showing{display:block!important;opacity:1;visibility:visible}.vjs-no-js{padding:20px;color:#fff;background-color:#000;font-size:18px;font-family:Arial,Helvetica,sans-serif;text-align:center;width:300px;height:150px;margin:0 auto}.vjs-no-js a,.vjs-no-js a:visited{color:#66a8cc}.video-js .vjs-big-play-button{font-size:3em;line-height:1.5em;height:1.5em;width:3em;display:block;position:absolute;top:10px;left:10px;padding:0;cursor:pointer;opacity:1;border:.06666em solid #fff;background-color:#2b333f;background-color:rgba(43,51,63,.7);-webkit-border-radius:.3em;-moz-border-radius:.3em;border-radius:.3em;-webkit-transition:all .4s;-moz-transition:all .4s;-ms-transition:all .4s;-o-transition:all .4s;transition:all .4s}.vjs-big-play-centered .vjs-big-play-button{top:50%;left:50%;margin-top:-.75em;margin-left:-1.5em}.video-js .vjs-big-play-button:focus,.video-js:hover .vjs-big-play-button{border-color:#fff;background-color:#73859f;background-color:rgba(115,133,159,.5);-webkit-transition:all 0s;-moz-transition:all 0s;-ms-transition:all 0s;-o-transition:all 0s;transition:all 0s}.vjs-controls-disabled .vjs-big-play-button,.vjs-error .vjs-big-play-button,.vjs-has-started .vjs-big-play-button,.vjs-using-native-controls .vjs-big-play-button{display:none}.vjs-has-started.vjs-paused.vjs-show-big-play-button-on-pause .vjs-big-play-button{display:block}.video-js button{background:0 0;border:none;color:inherit;display:inline-block;overflow:visible;font-size:inherit;line-height:inherit;text-transform:none;text-decoration:none;transition:none;-webkit-appearance:none;-moz-appearance:none;appearance:none}.vjs-control .vjs-button{width:100%;height:100%}.video-js .vjs-control.vjs-close-button{cursor:pointer;height:3em;position:absolute;right:0;top:.5em;z-index:2}.video-js .vjs-modal-dialog{background:rgba(0,0,0,.8);background:-webkit-linear-gradient(-90deg,rgba(0,0,0,.8),rgba(255,255,255,0));background:linear-gradient(180deg,rgba(0,0,0,.8),rgba(255,255,255,0));overflow:auto;box-sizing:content-box}.video-js .vjs-modal-dialog>*{box-sizing:border-box}.vjs-modal-dialog .vjs-modal-dialog-content{font-size:1.2em;line-height:1.5;padding:20px 24px;z-index:1}.vjs-menu-button{cursor:pointer}.vjs-menu-button.vjs-disabled{cursor:default}.vjs-workinghover .vjs-menu-button.vjs-disabled:hover .vjs-menu{display:none}.vjs-menu .vjs-menu-content{display:block;padding:0;margin:0;font-family:Arial,Helvetica,sans-serif;overflow:auto;box-sizing:content-box}.vjs-menu .vjs-menu-content>*{box-sizing:border-box}.vjs-scrubbing .vjs-menu-button:hover .vjs-menu{display:none}.vjs-menu li{list-style:none;margin:0;padding:.2em 0;line-height:1.4em;font-size:1.2em;text-align:center;text-transform:lowercase}.vjs-menu li.vjs-menu-item:focus,.vjs-menu li.vjs-menu-item:hover{background-color:#73859f;background-color:rgba(115,133,159,.5)}.vjs-menu li.vjs-selected,.vjs-menu li.vjs-selected:focus,.vjs-menu li.vjs-selected:hover{background-color:#fff;color:#2b333f}.vjs-menu li.vjs-menu-title{text-align:center;text-transform:uppercase;font-size:1em;line-height:2em;padding:0;margin:0 0 .3em 0;font-weight:700;cursor:default}.vjs-menu-button-popup .vjs-menu{display:none;position:absolute;bottom:0;width:10em;left:-3em;height:0;margin-bottom:1.5em;border-top-color:rgba(43,51,63,.7)}.vjs-menu-button-popup .vjs-menu .vjs-menu-content{background-color:#2b333f;background-color:rgba(43,51,63,.7);position:absolute;width:100%;bottom:1.5em;max-height:15em}.vjs-menu-button-popup .vjs-menu.vjs-lock-showing,.vjs-workinghover .vjs-menu-button-popup:hover .vjs-menu{display:block}.video-js .vjs-menu-button-inline{-webkit-transition:all .4s;-moz-transition:all .4s;-ms-transition:all .4s;-o-transition:all .4s;transition:all .4s;overflow:hidden}.video-js .vjs-menu-button-inline:before{width:2.222222222em}.video-js .vjs-menu-button-inline.vjs-slider-active,.video-js .vjs-menu-button-inline:focus,.video-js .vjs-menu-button-inline:hover,.video-js.vjs-no-flex .vjs-menu-button-inline{width:12em}.vjs-menu-button-inline .vjs-menu{opacity:0;height:100%;width:auto;position:absolute;left:4em;top:0;padding:0;margin:0;-webkit-transition:all .4s;-moz-transition:all .4s;-ms-transition:all .4s;-o-transition:all .4s;transition:all .4s}.vjs-menu-button-inline.vjs-slider-active .vjs-menu,.vjs-menu-button-inline:focus .vjs-menu,.vjs-menu-button-inline:hover .vjs-menu{display:block;opacity:1}.vjs-no-flex .vjs-menu-button-inline .vjs-menu{display:block;opacity:1;position:relative;width:auto}.vjs-no-flex .vjs-menu-button-inline.vjs-slider-active .vjs-menu,.vjs-no-flex .vjs-menu-button-inline:focus .vjs-menu,.vjs-no-flex .vjs-menu-button-inline:hover .vjs-menu{width:auto}.vjs-menu-button-inline .vjs-menu-content{width:auto;height:100%;margin:0;overflow:hidden}.video-js .vjs-control-bar{display:none;width:100%;position:absolute;bottom:0;left:0;right:0;height:3em;background-color:#2b333f;background-color:rgba(43,51,63,.7)}.vjs-has-started .vjs-control-bar{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;visibility:visible;opacity:1;-webkit-transition:visibility .1s,opacity .1s;-moz-transition:visibility .1s,opacity .1s;-ms-transition:visibility .1s,opacity .1s;-o-transition:visibility .1s,opacity .1s;transition:visibility .1s,opacity .1s}.vjs-has-started.vjs-user-inactive.vjs-playing .vjs-control-bar{visibility:visible;opacity:0;-webkit-transition:visibility 1s,opacity 1s;-moz-transition:visibility 1s,opacity 1s;-ms-transition:visibility 1s,opacity 1s;-o-transition:visibility 1s,opacity 1s;transition:visibility 1s,opacity 1s}.vjs-controls-disabled .vjs-control-bar,.vjs-error .vjs-control-bar,.vjs-using-native-controls .vjs-control-bar{display:none!important}.vjs-audio.vjs-has-started.vjs-user-inactive.vjs-playing .vjs-control-bar{opacity:1;visibility:visible}.vjs-has-started.vjs-no-flex .vjs-control-bar{display:table}.video-js .vjs-control{position:relative;text-align:center;margin:0;padding:0;height:100%;width:4em;-webkit-box-flex:none;-moz-box-flex:none;-webkit-flex:none;-ms-flex:none;flex:none}.vjs-button>.vjs-icon-placeholder:before{font-size:1.8em;line-height:1.67}.video-js .vjs-control:focus,.video-js .vjs-control:focus:before,.video-js .vjs-control:hover:before{text-shadow:0 0 1em #fff}.video-js .vjs-control-text{border:0;clip:rect(0 0 0 0);height:1px;overflow:hidden;padding:0;position:absolute;width:1px}.vjs-no-flex .vjs-control{display:table-cell;vertical-align:middle}.video-js .vjs-custom-control-spacer{display:none}.video-js .vjs-progress-control{cursor:pointer;-webkit-box-flex:auto;-moz-box-flex:auto;-webkit-flex:auto;-ms-flex:auto;flex:auto;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center;min-width:4em}.video-js .vjs-progress-control.disabled{cursor:default}.vjs-live .vjs-progress-control{display:none}.vjs-no-flex .vjs-progress-control{width:auto}.video-js .vjs-progress-holder{-webkit-box-flex:auto;-moz-box-flex:auto;-webkit-flex:auto;-ms-flex:auto;flex:auto;-webkit-transition:all .2s;-moz-transition:all .2s;-ms-transition:all .2s;-o-transition:all .2s;transition:all .2s;height:.3em}.video-js .vjs-progress-control .vjs-progress-holder{margin:0 10px}.video-js .vjs-progress-control:hover .vjs-progress-holder{font-size:1.666666666666666666em}.video-js .vjs-progress-control:hover .vjs-progress-holder.disabled{font-size:1em}.video-js .vjs-progress-holder .vjs-load-progress,.video-js .vjs-progress-holder .vjs-load-progress div,.video-js .vjs-progress-holder .vjs-play-progress{position:absolute;display:block;height:100%;margin:0;padding:0;width:0;left:0;top:0}.video-js .vjs-play-progress{background-color:#fff}.video-js .vjs-play-progress:before{font-size:.9em;position:absolute;right:-.5em;top:-.333333333333333em;z-index:1}.video-js .vjs-load-progress{background:#bfc7d3;background:rgba(115,133,159,.5)}.video-js .vjs-load-progress div{background:#fff;background:rgba(115,133,159,.75)}.video-js .vjs-time-tooltip{background-color:#fff;background-color:rgba(255,255,255,.8);-webkit-border-radius:.3em;-moz-border-radius:.3em;border-radius:.3em;color:#000;float:right;font-family:Arial,Helvetica,sans-serif;font-size:1em;padding:6px 8px 8px 8px;pointer-events:none;position:relative;top:-3.4em;visibility:hidden;z-index:1}.video-js .vjs-progress-holder:focus .vjs-time-tooltip{display:none}.video-js .vjs-progress-control:hover .vjs-progress-holder:focus .vjs-time-tooltip,.video-js .vjs-progress-control:hover .vjs-time-tooltip{display:block;font-size:.6em;visibility:visible}.video-js .vjs-progress-control.disabled:hover .vjs-time-tooltip{font-size:1em}.video-js .vjs-progress-control .vjs-mouse-display{display:none;position:absolute;width:1px;height:100%;background-color:#000;z-index:1}.vjs-no-flex .vjs-progress-control .vjs-mouse-display{z-index:0}.video-js .vjs-progress-control:hover .vjs-mouse-display{display:block}.video-js.vjs-user-inactive .vjs-progress-control .vjs-mouse-display{visibility:hidden;opacity:0;-webkit-transition:visibility 1s,opacity 1s;-moz-transition:visibility 1s,opacity 1s;-ms-transition:visibility 1s,opacity 1s;-o-transition:visibility 1s,opacity 1s;transition:visibility 1s,opacity 1s}.video-js.vjs-user-inactive.vjs-no-flex .vjs-progress-control .vjs-mouse-display{display:none}.vjs-mouse-display .vjs-time-tooltip{color:#fff;background-color:#000;background-color:rgba(0,0,0,.8)}.video-js .vjs-slider{position:relative;cursor:pointer;padding:0;margin:0 .45em 0 .45em;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:#73859f;background-color:rgba(115,133,159,.5)}.video-js .vjs-slider.disabled{cursor:default}.video-js .vjs-slider:focus{text-shadow:0 0 1em #fff;-webkit-box-shadow:0 0 1em #fff;-moz-box-shadow:0 0 1em #fff;box-shadow:0 0 1em #fff}.video-js .vjs-mute-control{cursor:pointer;-webkit-box-flex:none;-moz-box-flex:none;-webkit-flex:none;-ms-flex:none;flex:none;padding-left:2em;padding-right:2em;padding-bottom:3em}.video-js .vjs-volume-control{cursor:pointer;margin-right:1em;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}.video-js .vjs-volume-control.vjs-volume-horizontal{width:5em}.video-js .vjs-volume-panel .vjs-volume-control{visibility:visible;opacity:0;width:1px;height:1px;margin-left:-1px}.video-js .vjs-volume-panel{-webkit-transition:width 1s;-moz-transition:width 1s;-ms-transition:width 1s;-o-transition:width 1s;transition:width 1s}.video-js .vjs-volume-panel .vjs-mute-control:hover~.vjs-volume-control,.video-js .vjs-volume-panel .vjs-volume-control.vjs-slider-active,.video-js .vjs-volume-panel .vjs-volume-control:active,.video-js .vjs-volume-panel .vjs-volume-control:hover,.video-js .vjs-volume-panel:active .vjs-volume-control,.video-js .vjs-volume-panel:focus .vjs-volume-control,.video-js .vjs-volume-panel:hover .vjs-volume-control{visibility:visible;opacity:1;position:relative;-webkit-transition:visibility .1s,opacity .1s,height .1s,width .1s,left 0s,top 0s;-moz-transition:visibility .1s,opacity .1s,height .1s,width .1s,left 0s,top 0s;-ms-transition:visibility .1s,opacity .1s,height .1s,width .1s,left 0s,top 0s;-o-transition:visibility .1s,opacity .1s,height .1s,width .1s,left 0s,top 0s;transition:visibility .1s,opacity .1s,height .1s,width .1s,left 0s,top 0s}.video-js .vjs-volume-panel .vjs-mute-control:hover~.vjs-volume-control.vjs-volume-horizontal,.video-js .vjs-volume-panel .vjs-volume-control.vjs-slider-active.vjs-volume-horizontal,.video-js .vjs-volume-panel .vjs-volume-control:active.vjs-volume-horizontal,.video-js .vjs-volume-panel .vjs-volume-control:hover.vjs-volume-horizontal,.video-js .vjs-volume-panel:active .vjs-volume-control.vjs-volume-horizontal,.video-js .vjs-volume-panel:focus .vjs-volume-control.vjs-volume-horizontal,.video-js .vjs-volume-panel:hover .vjs-volume-control.vjs-volume-horizontal{width:5em;height:3em}.video-js .vjs-volume-panel.vjs-volume-panel-horizontal.vjs-slider-active,.video-js .vjs-volume-panel.vjs-volume-panel-horizontal:active,.video-js .vjs-volume-panel.vjs-volume-panel-horizontal:hover{width:9em;-webkit-transition:width .1s;-moz-transition:width .1s;-ms-transition:width .1s;-o-transition:width .1s;transition:width .1s}.video-js .vjs-volume-panel .vjs-volume-control.vjs-volume-vertical{height:8em;width:3em;left:-3.5em;-webkit-transition:visibility 1s,opacity 1s,height 1s 1s,width 1s 1s,left 1s 1s,top 1s 1s;-moz-transition:visibility 1s,opacity 1s,height 1s 1s,width 1s 1s,left 1s 1s,top 1s 1s;-ms-transition:visibility 1s,opacity 1s,height 1s 1s,width 1s 1s,left 1s 1s,top 1s 1s;-o-transition:visibility 1s,opacity 1s,height 1s 1s,width 1s 1s,left 1s 1s,top 1s 1s;transition:visibility 1s,opacity 1s,height 1s 1s,width 1s 1s,left 1s 1s,top 1s 1s}.video-js .vjs-volume-panel .vjs-volume-control.vjs-volume-horizontal{-webkit-transition:visibility 1s,opacity 1s,height 1s 1s,width 1s,left 1s 1s,top 1s 1s;-moz-transition:visibility 1s,opacity 1s,height 1s 1s,width 1s,left 1s 1s,top 1s 1s;-ms-transition:visibility 1s,opacity 1s,height 1s 1s,width 1s,left 1s 1s,top 1s 1s;-o-transition:visibility 1s,opacity 1s,height 1s 1s,width 1s,left 1s 1s,top 1s 1s;transition:visibility 1s,opacity 1s,height 1s 1s,width 1s,left 1s 1s,top 1s 1s}.video-js.vjs-no-flex .vjs-volume-panel .vjs-volume-control.vjs-volume-horizontal{width:5em;height:3em;visibility:visible;opacity:1;position:relative;-webkit-transition:none;-moz-transition:none;-ms-transition:none;-o-transition:none;transition:none}.video-js.vjs-no-flex .vjs-volume-control.vjs-volume-vertical,.video-js.vjs-no-flex .vjs-volume-panel .vjs-volume-control.vjs-volume-vertical{position:absolute;bottom:3em;left:.5em}.video-js .vjs-volume-panel{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}.video-js .vjs-volume-bar{margin:1.35em .45em}.vjs-volume-bar.vjs-slider-horizontal{width:5em;height:.3em}.vjs-volume-bar.vjs-slider-vertical{width:.3em;height:5em;margin:1.35em auto}.video-js .vjs-volume-level{position:absolute;bottom:0;left:0;background-color:#fff}.video-js .vjs-volume-level:before{position:absolute;font-size:.9em}.vjs-slider-vertical .vjs-volume-level{width:.3em}.vjs-slider-vertical .vjs-volume-level:before{top:-.5em;left:-.3em}.vjs-slider-horizontal .vjs-volume-level{height:.3em}.vjs-slider-horizontal .vjs-volume-level:before{top:-.3em;right:-.5em}.video-js .vjs-volume-panel.vjs-volume-panel-vertical{width:4em}.vjs-volume-bar.vjs-slider-vertical .vjs-volume-level{height:100%}.vjs-volume-bar.vjs-slider-horizontal .vjs-volume-level{width:100%}.video-js .vjs-volume-vertical{width:3em;height:8em;bottom:8em;background-color:#2b333f;background-color:rgba(43,51,63,.7)}.video-js .vjs-volume-horizontal .vjs-menu{left:-2em}.vjs-poster{display:inline-block;vertical-align:middle;background-repeat:no-repeat;background-position:50% 50%;background-size:contain;background-color:#000;cursor:pointer;margin:0;padding:0;position:absolute;top:0;right:0;bottom:0;left:0;height:100%}.vjs-poster img{display:block;vertical-align:middle;margin:0 auto;max-height:100%;padding:0;width:100%}.vjs-has-started .vjs-poster{display:none}.vjs-audio.vjs-has-started .vjs-poster{display:block}.vjs-using-native-controls .vjs-poster{display:none}.video-js .vjs-live-control{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-align:flex-start;-webkit-align-items:flex-start;-ms-flex-align:flex-start;align-items:flex-start;-webkit-box-flex:auto;-moz-box-flex:auto;-webkit-flex:auto;-ms-flex:auto;flex:auto;font-size:1em;line-height:3em}.vjs-no-flex .vjs-live-control{display:table-cell;width:auto;text-align:left}.video-js .vjs-time-control{-webkit-box-flex:none;-moz-box-flex:none;-webkit-flex:none;-ms-flex:none;flex:none;font-size:1em;line-height:3em;min-width:2em;width:auto;padding-left:1em;padding-right:1em}.vjs-live .vjs-time-control{display:none}.video-js .vjs-current-time,.vjs-no-flex .vjs-current-time{display:none}.vjs-no-flex .vjs-remaining-time.vjs-time-control.vjs-control{width:0!important;white-space:nowrap}.video-js .vjs-duration,.vjs-no-flex .vjs-duration{display:none}.vjs-time-divider{display:none;line-height:3em}.vjs-live .vjs-time-divider{display:none}.video-js .vjs-play-control .vjs-icon-placeholder{cursor:pointer;-webkit-box-flex:none;-moz-box-flex:none;-webkit-flex:none;-ms-flex:none;flex:none}.vjs-text-track-display{position:absolute;bottom:3em;left:0;right:0;top:0;pointer-events:none}.video-js.vjs-user-inactive.vjs-playing .vjs-text-track-display{bottom:1em}.video-js .vjs-text-track{font-size:1.4em;text-align:center;margin-bottom:.1em;background-color:#000;background-color:rgba(0,0,0,.5)}.vjs-subtitles{color:#fff}.vjs-captions{color:#fc6}.vjs-tt-cue{display:block}video::-webkit-media-text-track-display{-moz-transform:translateY(-3em);-ms-transform:translateY(-3em);-o-transform:translateY(-3em);-webkit-transform:translateY(-3em);transform:translateY(-3em)}.video-js.vjs-user-inactive.vjs-playing video::-webkit-media-text-track-display{-moz-transform:translateY(-1.5em);-ms-transform:translateY(-1.5em);-o-transform:translateY(-1.5em);-webkit-transform:translateY(-1.5em);transform:translateY(-1.5em)}.video-js .vjs-fullscreen-control{cursor:pointer;-webkit-box-flex:none;-moz-box-flex:none;-webkit-flex:none;-ms-flex:none;flex:none}.vjs-playback-rate .vjs-playback-rate-value,.vjs-playback-rate>.vjs-menu-button{position:absolute;top:0;left:0;width:100%;height:100%}.vjs-playback-rate .vjs-playback-rate-value{pointer-events:none;font-size:1.5em;line-height:2;text-align:center}.vjs-playback-rate .vjs-menu{width:4em;left:0}.vjs-error .vjs-error-display .vjs-modal-dialog-content{font-size:1.4em;text-align:center}.vjs-error .vjs-error-display:before{color:#fff;content:'X';font-family:Arial,Helvetica,sans-serif;font-size:4em;left:0;line-height:1;margin-top:-.5em;position:absolute;text-shadow:.05em .05em .1em #000;text-align:center;top:50%;vertical-align:middle;width:100%}.vjs-loading-spinner{display:none;position:absolute;top:50%;left:50%;margin:-25px 0 0 -25px;opacity:.85;text-align:left;border:6px solid rgba(43,51,63,.7);box-sizing:border-box;background-clip:padding-box;width:50px;height:50px;border-radius:25px;visibility:hidden}.vjs-seeking .vjs-loading-spinner,.vjs-waiting .vjs-loading-spinner{display:block;animation:0s linear .3s forwards vjs-spinner-show}.vjs-loading-spinner:after,.vjs-loading-spinner:before{content:"";position:absolute;margin:-6px;box-sizing:inherit;width:inherit;height:inherit;border-radius:inherit;opacity:1;border:inherit;border-color:transparent;border-top-color:#fff}.vjs-seeking .vjs-loading-spinner:after,.vjs-seeking .vjs-loading-spinner:before,.vjs-waiting .vjs-loading-spinner:after,.vjs-waiting .vjs-loading-spinner:before{-webkit-animation:vjs-spinner-spin 1.1s cubic-bezier(.6,.2,0,.8) infinite,vjs-spinner-fade 1.1s linear infinite;animation:vjs-spinner-spin 1.1s cubic-bezier(.6,.2,0,.8) infinite,vjs-spinner-fade 1.1s linear infinite}.vjs-seeking .vjs-loading-spinner:before,.vjs-waiting .vjs-loading-spinner:before{border-top-color:#fff}.vjs-seeking .vjs-loading-spinner:after,.vjs-waiting .vjs-loading-spinner:after{border-top-color:#fff;-webkit-animation-delay:.44s;animation-delay:.44s}@keyframes vjs-spinner-show{to{visibility:visible}}@-webkit-keyframes vjs-spinner-show{to{visibility:visible}}@keyframes vjs-spinner-spin{100%{transform:rotate(360deg)}}@-webkit-keyframes vjs-spinner-spin{100%{-webkit-transform:rotate(360deg)}}@keyframes vjs-spinner-fade{0%{border-top-color:#73859f}20%{border-top-color:#73859f}35%{border-top-color:#fff}60%{border-top-color:#73859f}100%{border-top-color:#73859f}}@-webkit-keyframes vjs-spinner-fade{0%{border-top-color:#73859f}20%{border-top-color:#73859f}35%{border-top-color:#fff}60%{border-top-color:#73859f}100%{border-top-color:#73859f}}.vjs-chapters-button .vjs-menu ul{width:24em}.video-js .vjs-subs-caps-button+.vjs-menu .vjs-captions-menu-item .vjs-menu-item-text .vjs-icon-placeholder{position:absolute}.video-js .vjs-subs-caps-button+.vjs-menu .vjs-captions-menu-item .vjs-menu-item-text .vjs-icon-placeholder:before{font-family:VideoJS;content:"\f10d";font-size:1.5em;line-height:inherit}.video-js.vjs-layout-tiny:not(.vjs-fullscreen) .vjs-custom-control-spacer{-webkit-box-flex:auto;-moz-box-flex:auto;-webkit-flex:auto;-ms-flex:auto;flex:auto}.video-js.vjs-layout-tiny:not(.vjs-fullscreen).vjs-no-flex .vjs-custom-control-spacer{width:auto}.video-js.vjs-layout-tiny:not(.vjs-fullscreen) .vjs-audio-button,.video-js.vjs-layout-tiny:not(.vjs-fullscreen) .vjs-captions-button,.video-js.vjs-layout-tiny:not(.vjs-fullscreen) .vjs-chapters-button,.video-js.vjs-layout-tiny:not(.vjs-fullscreen) .vjs-current-time,.video-js.vjs-layout-tiny:not(.vjs-fullscreen) .vjs-descriptions-button,.video-js.vjs-layout-tiny:not(.vjs-fullscreen) .vjs-duration,.video-js.vjs-layout-tiny:not(.vjs-fullscreen) .vjs-mute-control,.video-js.vjs-layout-tiny:not(.vjs-fullscreen) .vjs-playback-rate,.video-js.vjs-layout-tiny:not(.vjs-fullscreen) .vjs-progress-control,.video-js.vjs-layout-tiny:not(.vjs-fullscreen) .vjs-remaining-time,.video-js.vjs-layout-tiny:not(.vjs-fullscreen) .vjs-subtitles-button,.video-js.vjs-layout-tiny:not(.vjs-fullscreen) .vjs-time-divider,.video-js.vjs-layout-tiny:not(.vjs-fullscreen) .vjs-volume-control{display:none}.video-js.vjs-layout-x-small:not(.vjs-fullscreen) .vjs-audio-button,.video-js.vjs-layout-x-small:not(.vjs-fullscreen) .vjs-captions-button,.video-js.vjs-layout-x-small:not(.vjs-fullscreen) .vjs-chapters-button,.video-js.vjs-layout-x-small:not(.vjs-fullscreen) .vjs-current-time,.video-js.vjs-layout-x-small:not(.vjs-fullscreen) .vjs-descriptions-button,.video-js.vjs-layout-x-small:not(.vjs-fullscreen) .vjs-duration,.video-js.vjs-layout-x-small:not(.vjs-fullscreen) .vjs-mute-control,.video-js.vjs-layout-x-small:not(.vjs-fullscreen) .vjs-playback-rate,.video-js.vjs-layout-x-small:not(.vjs-fullscreen) .vjs-remaining-time,.video-js.vjs-layout-x-small:not(.vjs-fullscreen) .vjs-subtitles-button,.video-js.vjs-layout-x-small:not(.vjs-fullscreen) .vjs-time-divider,.video-js.vjs-layout-x-small:not(.vjs-fullscreen) .vjs-volume-control{display:none}.video-js.vjs-layout-small:not(.vjs-fullscreen) .vjs-captions-button,.video-js.vjs-layout-small:not(.vjs-fullscreen) .vjs-chapters-button,.video-js.vjs-layout-small:not(.vjs-fullscreen) .vjs-current-time,.video-js.vjs-layout-small:not(.vjs-fullscreen) .vjs-descriptions-button,.video-js.vjs-layout-small:not(.vjs-fullscreen) .vjs-duration,.video-js.vjs-layout-small:not(.vjs-fullscreen) .vjs-mute-control,.video-js.vjs-layout-small:not(.vjs-fullscreen) .vjs-playback-rate,.video-js.vjs-layout-small:not(.vjs-fullscreen) .vjs-remaining-time,.video-js.vjs-layout-small:not(.vjs-fullscreen) .vjs-subtitles-button .vjs-audio-button,.video-js.vjs-layout-small:not(.vjs-fullscreen) .vjs-time-divider,.video-js.vjs-layout-small:not(.vjs-fullscreen) .vjs-volume-control{display:none}.vjs-modal-dialog.vjs-text-track-settings{background-color:#2b333f;background-color:rgba(43,51,63,.75);color:#fff;height:70%}.vjs-text-track-settings .vjs-modal-dialog-content{display:table}.vjs-text-track-settings .vjs-track-settings-colors,.vjs-text-track-settings .vjs-track-settings-controls,.vjs-text-track-settings .vjs-track-settings-font{display:table-cell}.vjs-text-track-settings .vjs-track-settings-controls{text-align:right;vertical-align:bottom}@supports (display:grid){.vjs-text-track-settings .vjs-modal-dialog-content{display:grid;grid-template-columns:1fr 1fr;grid-template-rows:1fr auto}.vjs-text-track-settings .vjs-track-settings-colors{display:block;grid-column:1;grid-row:1}.vjs-text-track-settings .vjs-track-settings-font{grid-column:2;grid-row:1}.vjs-text-track-settings .vjs-track-settings-controls{grid-column:2;grid-row:2}}.vjs-track-setting>select{margin-right:5px}.vjs-text-track-settings fieldset{margin:5px;padding:3px;border:none}.vjs-text-track-settings fieldset span{display:inline-block}.vjs-text-track-settings legend{color:#fff;margin:0 0 5px 0}.vjs-text-track-settings .vjs-label{position:absolute;clip:rect(1px 1px 1px 1px);clip:rect(1px,1px,1px,1px);display:block;margin:0 0 5px 0;padding:0;border:0;height:1px;width:1px;overflow:hidden}.vjs-track-settings-controls button:active,.vjs-track-settings-controls button:focus{outline-style:solid;outline-width:medium;background-image:linear-gradient(0deg,#fff 88%,#73859f 100%)}.vjs-track-settings-controls button:hover{color:rgba(43,51,63,.75)}.vjs-track-settings-controls button{background-color:#fff;background-image:linear-gradient(-180deg,#fff 88%,#73859f 100%);color:#2b333f;cursor:pointer;border-radius:2px}.vjs-track-settings-controls .vjs-default-button{margin-right:1em}@media print{.video-js>:not(.vjs-tech):not(.vjs-poster){visibility:hidden}}.vjs-resize-manager{position:absolute;top:0;left:0;width:100%;height:100%;border:none;visibility:hidden} \ No newline at end of file diff --git a/pod/main/static/video-js-6.8.0/alt/video.novtt.js b/pod/main/static/video-js-6.8.0/alt/video.novtt.js deleted file mode 100755 index 78167933ba..0000000000 --- a/pod/main/static/video-js-6.8.0/alt/video.novtt.js +++ /dev/null @@ -1,24545 +0,0 @@ -/** - * @license - * Video.js 6.8.0 - * Copyright Brightcove, Inc. - * Available under Apache License Version 2.0 - * - */ - -(function (global, factory) { - typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : - typeof define === 'function' && define.amd ? define(factory) : - (global.videojs = factory()); -}(this, (function () { - -var version = "6.8.0"; - -var commonjsGlobal = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; - - - - - -function createCommonjsModule(fn, module) { - return module = { exports: {} }, fn(module, module.exports), module.exports; -} - -var win; - -if (typeof window !== "undefined") { - win = window; -} else if (typeof commonjsGlobal !== "undefined") { - win = commonjsGlobal; -} else if (typeof self !== "undefined"){ - win = self; -} else { - win = {}; -} - -var window_1 = win; - -var empty = {}; - - -var empty$1 = (Object.freeze || Object)({ - 'default': empty -}); - -var minDoc = ( empty$1 && empty ) || empty$1; - -var topLevel = typeof commonjsGlobal !== 'undefined' ? commonjsGlobal : - typeof window !== 'undefined' ? window : {}; - - -var doccy; - -if (typeof document !== 'undefined') { - doccy = document; -} else { - doccy = topLevel['__GLOBAL_DOCUMENT_CACHE@4']; - - if (!doccy) { - doccy = topLevel['__GLOBAL_DOCUMENT_CACHE@4'] = minDoc; - } -} - -var document_1 = doccy; - -/** - * @file browser.js - * @module browser - */ -var USER_AGENT = window_1.navigator && window_1.navigator.userAgent || ''; -var webkitVersionMap = /AppleWebKit\/([\d.]+)/i.exec(USER_AGENT); -var appleWebkitVersion = webkitVersionMap ? parseFloat(webkitVersionMap.pop()) : null; - -/* - * Device is an iPhone - * - * @type {Boolean} - * @constant - * @private - */ -var IS_IPAD = /iPad/i.test(USER_AGENT); - -// The Facebook app's UIWebView identifies as both an iPhone and iPad, so -// to identify iPhones, we need to exclude iPads. -// http://artsy.github.io/blog/2012/10/18/the-perils-of-ios-user-agent-sniffing/ -var IS_IPHONE = /iPhone/i.test(USER_AGENT) && !IS_IPAD; -var IS_IPOD = /iPod/i.test(USER_AGENT); -var IS_IOS = IS_IPHONE || IS_IPAD || IS_IPOD; - -var IOS_VERSION = function () { - var match = USER_AGENT.match(/OS (\d+)_/i); - - if (match && match[1]) { - return match[1]; - } - return null; -}(); - -var IS_ANDROID = /Android/i.test(USER_AGENT); -var ANDROID_VERSION = function () { - // This matches Android Major.Minor.Patch versions - // ANDROID_VERSION is Major.Minor as a Number, if Minor isn't available, then only Major is returned - var match = USER_AGENT.match(/Android (\d+)(?:\.(\d+))?(?:\.(\d+))*/i); - - if (!match) { - return null; - } - - var major = match[1] && parseFloat(match[1]); - var minor = match[2] && parseFloat(match[2]); - - if (major && minor) { - return parseFloat(match[1] + '.' + match[2]); - } else if (major) { - return major; - } - return null; -}(); - -// Old Android is defined as Version older than 2.3, and requiring a webkit version of the android browser -var IS_OLD_ANDROID = IS_ANDROID && /webkit/i.test(USER_AGENT) && ANDROID_VERSION < 2.3; -var IS_NATIVE_ANDROID = IS_ANDROID && ANDROID_VERSION < 5 && appleWebkitVersion < 537; - -var IS_FIREFOX = /Firefox/i.test(USER_AGENT); -var IS_EDGE = /Edge/i.test(USER_AGENT); -var IS_CHROME = !IS_EDGE && /Chrome/i.test(USER_AGENT); -var CHROME_VERSION = function () { - var match = USER_AGENT.match(/Chrome\/(\d+)/); - - if (match && match[1]) { - return parseFloat(match[1]); - } - return null; -}(); -var IS_IE8 = /MSIE\s8\.0/.test(USER_AGENT); -var IE_VERSION = function () { - var result = /MSIE\s(\d+)\.\d/.exec(USER_AGENT); - var version = result && parseFloat(result[1]); - - if (!version && /Trident\/7.0/i.test(USER_AGENT) && /rv:11.0/.test(USER_AGENT)) { - // IE 11 has a different user agent string than other IE versions - version = 11.0; - } - - return version; -}(); - -var IS_SAFARI = /Safari/i.test(USER_AGENT) && !IS_CHROME && !IS_ANDROID && !IS_EDGE; -var IS_ANY_SAFARI = IS_SAFARI || IS_IOS; - -var TOUCH_ENABLED = isReal() && ('ontouchstart' in window_1 || window_1.DocumentTouch && window_1.document instanceof window_1.DocumentTouch); - -var BACKGROUND_SIZE_SUPPORTED = isReal() && 'backgroundSize' in window_1.document.createElement('video').style; - -var browser = (Object.freeze || Object)({ - IS_IPAD: IS_IPAD, - IS_IPHONE: IS_IPHONE, - IS_IPOD: IS_IPOD, - IS_IOS: IS_IOS, - IOS_VERSION: IOS_VERSION, - IS_ANDROID: IS_ANDROID, - ANDROID_VERSION: ANDROID_VERSION, - IS_OLD_ANDROID: IS_OLD_ANDROID, - IS_NATIVE_ANDROID: IS_NATIVE_ANDROID, - IS_FIREFOX: IS_FIREFOX, - IS_EDGE: IS_EDGE, - IS_CHROME: IS_CHROME, - CHROME_VERSION: CHROME_VERSION, - IS_IE8: IS_IE8, - IE_VERSION: IE_VERSION, - IS_SAFARI: IS_SAFARI, - IS_ANY_SAFARI: IS_ANY_SAFARI, - TOUCH_ENABLED: TOUCH_ENABLED, - BACKGROUND_SIZE_SUPPORTED: BACKGROUND_SIZE_SUPPORTED -}); - -var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { - return typeof obj; -} : function (obj) { - return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; -}; - - - - - - - - - - - -var classCallCheck = function (instance, Constructor) { - if (!(instance instanceof Constructor)) { - throw new TypeError("Cannot call a class as a function"); - } -}; - - - - - - - - - - - -var inherits = function (subClass, superClass) { - if (typeof superClass !== "function" && superClass !== null) { - throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); - } - - subClass.prototype = Object.create(superClass && superClass.prototype, { - constructor: { - value: subClass, - enumerable: false, - writable: true, - configurable: true - } - }); - if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; -}; - - - - - - - - - - - -var possibleConstructorReturn = function (self, call) { - if (!self) { - throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); - } - - return call && (typeof call === "object" || typeof call === "function") ? call : self; -}; - - - - - - - - - - - -var taggedTemplateLiteralLoose = function (strings, raw) { - strings.raw = raw; - return strings; -}; - -/** - * @file obj.js - * @module obj - */ - -/** - * @callback obj:EachCallback - * - * @param {Mixed} value - * The current key for the object that is being iterated over. - * - * @param {string} key - * The current key-value for object that is being iterated over - */ - -/** - * @callback obj:ReduceCallback - * - * @param {Mixed} accum - * The value that is accumulating over the reduce loop. - * - * @param {Mixed} value - * The current key for the object that is being iterated over. - * - * @param {string} key - * The current key-value for object that is being iterated over - * - * @return {Mixed} - * The new accumulated value. - */ -var toString = Object.prototype.toString; - -/** - * Get the keys of an Object - * - * @param {Object} - * The Object to get the keys from - * - * @return {string[]} - * An array of the keys from the object. Returns an empty array if the - * object passed in was invalid or had no keys. - * - * @private - */ -var keys = function keys(object) { - return isObject(object) ? Object.keys(object) : []; -}; - -/** - * Array-like iteration for objects. - * - * @param {Object} object - * The object to iterate over - * - * @param {obj:EachCallback} fn - * The callback function which is called for each key in the object. - */ -function each(object, fn) { - keys(object).forEach(function (key) { - return fn(object[key], key); - }); -} - -/** - * Array-like reduce for objects. - * - * @param {Object} object - * The Object that you want to reduce. - * - * @param {Function} fn - * A callback function which is called for each key in the object. It - * receives the accumulated value and the per-iteration value and key - * as arguments. - * - * @param {Mixed} [initial = 0] - * Starting value - * - * @return {Mixed} - * The final accumulated value. - */ -function reduce(object, fn) { - var initial = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 0; - - return keys(object).reduce(function (accum, key) { - return fn(accum, object[key], key); - }, initial); -} - -/** - * Object.assign-style object shallow merge/extend. - * - * @param {Object} target - * @param {Object} ...sources - * @return {Object} - */ -function assign(target) { - for (var _len = arguments.length, sources = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { - sources[_key - 1] = arguments[_key]; - } - - if (Object.assign) { - return Object.assign.apply(Object, [target].concat(sources)); - } - - sources.forEach(function (source) { - if (!source) { - return; - } - - each(source, function (value, key) { - target[key] = value; - }); - }); - - return target; -} - -/** - * Returns whether a value is an object of any kind - including DOM nodes, - * arrays, regular expressions, etc. Not functions, though. - * - * This avoids the gotcha where using `typeof` on a `null` value - * results in `'object'`. - * - * @param {Object} value - * @return {Boolean} - */ -function isObject(value) { - return !!value && (typeof value === 'undefined' ? 'undefined' : _typeof(value)) === 'object'; -} - -/** - * Returns whether an object appears to be a "plain" object - that is, a - * direct instance of `Object`. - * - * @param {Object} value - * @return {Boolean} - */ -function isPlain(value) { - return isObject(value) && toString.call(value) === '[object Object]' && value.constructor === Object; -} - -/** - * @file log.js - * @module log - */ -var log = void 0; - -// This is the private tracking variable for logging level. -var level = 'info'; - -// This is the private tracking variable for the logging history. -var history = []; - -/** - * Log messages to the console and history based on the type of message - * - * @private - * @param {string} type - * The name of the console method to use. - * - * @param {Array} args - * The arguments to be passed to the matching console method. - * - * @param {boolean} [stringify] - * By default, only old IEs should get console argument stringification, - * but this is exposed as a parameter to facilitate testing. - */ -var logByType = function logByType(type, args) { - var stringify = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : !!IE_VERSION && IE_VERSION < 11; - - var lvl = log.levels[level]; - var lvlRegExp = new RegExp('^(' + lvl + ')$'); - - if (type !== 'log') { - - // Add the type to the front of the message when it's not "log". - args.unshift(type.toUpperCase() + ':'); - } - - // Add a clone of the args at this point to history. - if (history) { - history.push([].concat(args)); - } - - // Add console prefix after adding to history. - args.unshift('VIDEOJS:'); - - // If there's no console then don't try to output messages, but they will - // still be stored in history. - if (!window_1.console) { - return; - } - - // Was setting these once outside of this function, but containing them - // in the function makes it easier to test cases where console doesn't exist - // when the module is executed. - var fn = window_1.console[type]; - - if (!fn && type === 'debug') { - // Certain browsers don't have support for console.debug. For those, we - // should default to the closest comparable log. - fn = window_1.console.info || window_1.console.log; - } - - // Bail out if there's no console or if this type is not allowed by the - // current logging level. - if (!fn || !lvl || !lvlRegExp.test(type)) { - return; - } - - // IEs previous to 11 log objects uselessly as "[object Object]"; so, JSONify - // objects and arrays for those less-capable browsers. - if (stringify) { - args = args.map(function (a) { - if (isObject(a) || Array.isArray(a)) { - try { - return JSON.stringify(a); - } catch (x) { - return String(a); - } - } - - // Cast to string before joining, so we get null and undefined explicitly - // included in output (as we would in a modern console). - return String(a); - }).join(' '); - } - - // Old IE versions do not allow .apply() for console methods (they are - // reported as objects rather than functions). - if (!fn.apply) { - fn(args); - } else { - fn[Array.isArray(args) ? 'apply' : 'call'](window_1.console, args); - } -}; - -/** - * Logs plain debug messages. Similar to `console.log`. - * - * @class - * @param {Mixed[]} args - * One or more messages or objects that should be logged. - */ -log = function log() { - for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { - args[_key] = arguments[_key]; - } - - logByType('log', args); -}; - -/** - * Enumeration of available logging levels, where the keys are the level names - * and the values are `|`-separated strings containing logging methods allowed - * in that logging level. These strings are used to create a regular expression - * matching the function name being called. - * - * Levels provided by video.js are: - * - * - `off`: Matches no calls. Any value that can be cast to `false` will have - * this effect. The most restrictive. - * - `all`: Matches only Video.js-provided functions (`debug`, `log`, - * `log.warn`, and `log.error`). - * - `debug`: Matches `log.debug`, `log`, `log.warn`, and `log.error` calls. - * - `info` (default): Matches `log`, `log.warn`, and `log.error` calls. - * - `warn`: Matches `log.warn` and `log.error` calls. - * - `error`: Matches only `log.error` calls. - * - * @type {Object} - */ -log.levels = { - all: 'debug|log|warn|error', - off: '', - debug: 'debug|log|warn|error', - info: 'log|warn|error', - warn: 'warn|error', - error: 'error', - DEFAULT: level -}; - -/** - * Get or set the current logging level. If a string matching a key from - * {@link log.levels} is provided, acts as a setter. Regardless of argument, - * returns the current logging level. - * - * @param {string} [lvl] - * Pass to set a new logging level. - * - * @return {string} - * The current logging level. - */ -log.level = function (lvl) { - if (typeof lvl === 'string') { - if (!log.levels.hasOwnProperty(lvl)) { - throw new Error('"' + lvl + '" in not a valid log level'); - } - level = lvl; - } - return level; -}; - -/** - * Returns an array containing everything that has been logged to the history. - * - * This array is a shallow clone of the internal history record. However, its - * contents are _not_ cloned; so, mutating objects inside this array will - * mutate them in history. - * - * @return {Array} - */ -log.history = function () { - return history ? [].concat(history) : []; -}; - -/** - * Clears the internal history tracking, but does not prevent further history - * tracking. - */ -log.history.clear = function () { - if (history) { - history.length = 0; - } -}; - -/** - * Disable history tracking if it is currently enabled. - */ -log.history.disable = function () { - if (history !== null) { - history.length = 0; - history = null; - } -}; - -/** - * Enable history tracking if it is currently disabled. - */ -log.history.enable = function () { - if (history === null) { - history = []; - } -}; - -/** - * Logs error messages. Similar to `console.error`. - * - * @param {Mixed[]} args - * One or more messages or objects that should be logged as an error - */ -log.error = function () { - for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { - args[_key2] = arguments[_key2]; - } - - return logByType('error', args); -}; - -/** - * Logs warning messages. Similar to `console.warn`. - * - * @param {Mixed[]} args - * One or more messages or objects that should be logged as a warning. - */ -log.warn = function () { - for (var _len3 = arguments.length, args = Array(_len3), _key3 = 0; _key3 < _len3; _key3++) { - args[_key3] = arguments[_key3]; - } - - return logByType('warn', args); -}; - -/** - * Logs debug messages. Similar to `console.debug`, but may also act as a comparable - * log if `console.debug` is not available - * - * @param {Mixed[]} args - * One or more messages or objects that should be logged as debug. - */ -log.debug = function () { - for (var _len4 = arguments.length, args = Array(_len4), _key4 = 0; _key4 < _len4; _key4++) { - args[_key4] = arguments[_key4]; - } - - return logByType('debug', args); -}; - -var log$1 = log; - -function clean (s) { - return s.replace(/\n\r?\s*/g, '') -} - - -var tsml = function tsml (sa) { - var s = '' - , i = 0; - - for (; i < arguments.length; i++) - s += clean(sa[i]) + (arguments[i + 1] || ''); - - return s -}; - -/** - * @file computed-style.js - * @module computed-style - */ -/** - * A safe getComputedStyle with an IE8 fallback. - * - * This is needed because in Firefox, if the player is loaded in an iframe with - * `display:none`, then `getComputedStyle` returns `null`, so, we do a null-check to - * make sure that the player doesn't break in these cases. - * - * @param {Element} el - * The element you want the computed style of - * - * @param {string} prop - * The property name you want - * - * @see https://bugzilla.mozilla.org/show_bug.cgi?id=548397 - * - * @static - * @const - */ -function computedStyle(el, prop) { - if (!el || !prop) { - return ''; - } - - if (typeof window_1.getComputedStyle === 'function') { - var cs = window_1.getComputedStyle(el); - - return cs ? cs[prop] : ''; - } - - return el.currentStyle[prop] || ''; -} - -var _templateObject = taggedTemplateLiteralLoose(['Setting attributes in the second argument of createEl()\n has been deprecated. Use the third argument instead.\n createEl(type, properties, attributes). Attempting to set ', ' to ', '.'], ['Setting attributes in the second argument of createEl()\n has been deprecated. Use the third argument instead.\n createEl(type, properties, attributes). Attempting to set ', ' to ', '.']); - -/** - * @file dom.js - * @module dom - */ -/** - * Detect if a value is a string with any non-whitespace characters. - * - * @param {string} str - * The string to check - * - * @return {boolean} - * - True if the string is non-blank - * - False otherwise - * - */ -function isNonBlankString(str) { - return typeof str === 'string' && /\S/.test(str); -} - -/** - * Throws an error if the passed string has whitespace. This is used by - * class methods to be relatively consistent with the classList API. - * - * @param {string} str - * The string to check for whitespace. - * - * @throws {Error} - * Throws an error if there is whitespace in the string. - * - */ -function throwIfWhitespace(str) { - if (/\s/.test(str)) { - throw new Error('class has illegal whitespace characters'); - } -} - -/** - * Produce a regular expression for matching a className within an elements className. - * - * @param {string} className - * The className to generate the RegExp for. - * - * @return {RegExp} - * The RegExp that will check for a specific `className` in an elements - * className. - */ -function classRegExp(className) { - return new RegExp('(^|\\s)' + className + '($|\\s)'); -} - -/** - * Whether the current DOM interface appears to be real. - * - * @return {Boolean} - */ -function isReal() { - return ( - - // Both document and window will never be undefined thanks to `global`. - document_1 === window_1.document && - - // In IE < 9, DOM methods return "object" as their type, so all we can - // confidently check is that it exists. - typeof document_1.createElement !== 'undefined' - ); -} - -/** - * Determines, via duck typing, whether or not a value is a DOM element. - * - * @param {Mixed} value - * The thing to check - * - * @return {boolean} - * - True if it is a DOM element - * - False otherwise - */ -function isEl(value) { - return isObject(value) && value.nodeType === 1; -} - -/** - * Determines if the current DOM is embedded in an iframe. - * - * @return {boolean} - * - */ -function isInFrame() { - - // We need a try/catch here because Safari will throw errors when attempting - // to get either `parent` or `self` - try { - return window_1.parent !== window_1.self; - } catch (x) { - return true; - } -} - -/** - * Creates functions to query the DOM using a given method. - * - * @param {string} method - * The method to create the query with. - * - * @return {Function} - * The query method - */ -function createQuerier(method) { - return function (selector, context) { - if (!isNonBlankString(selector)) { - return document_1[method](null); - } - if (isNonBlankString(context)) { - context = document_1.querySelector(context); - } - - var ctx = isEl(context) ? context : document_1; - - return ctx[method] && ctx[method](selector); - }; -} - -/** - * Creates an element and applies properties. - * - * @param {string} [tagName='div'] - * Name of tag to be created. - * - * @param {Object} [properties={}] - * Element properties to be applied. - * - * @param {Object} [attributes={}] - * Element attributes to be applied. - * - * @param {String|Element|TextNode|Array|Function} [content] - * Contents for the element (see: {@link dom:normalizeContent}) - * - * @return {Element} - * The element that was created. - */ -function createEl() { - var tagName = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 'div'; - var properties = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; - var attributes = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; - var content = arguments[3]; - - var el = document_1.createElement(tagName); - - Object.getOwnPropertyNames(properties).forEach(function (propName) { - var val = properties[propName]; - - // See #2176 - // We originally were accepting both properties and attributes in the - // same object, but that doesn't work so well. - if (propName.indexOf('aria-') !== -1 || propName === 'role' || propName === 'type') { - log$1.warn(tsml(_templateObject, propName, val)); - el.setAttribute(propName, val); - - // Handle textContent since it's not supported everywhere and we have a - // method for it. - } else if (propName === 'textContent') { - textContent(el, val); - } else { - el[propName] = val; - } - }); - - Object.getOwnPropertyNames(attributes).forEach(function (attrName) { - el.setAttribute(attrName, attributes[attrName]); - }); - - if (content) { - appendContent(el, content); - } - - return el; -} - -/** - * Injects text into an element, replacing any existing contents entirely. - * - * @param {Element} el - * The element to add text content into - * - * @param {string} text - * The text content to add. - * - * @return {Element} - * The element with added text content. - */ -function textContent(el, text) { - if (typeof el.textContent === 'undefined') { - el.innerText = text; - } else { - el.textContent = text; - } - return el; -} - -/** - * Insert an element as the first child node of another - * - * @param {Element} child - * Element to insert - * - * @param {Element} parent - * Element to insert child into - */ -function prependTo(child, parent) { - if (parent.firstChild) { - parent.insertBefore(child, parent.firstChild); - } else { - parent.appendChild(child); - } -} - -/** - * Check if an element has a CSS class - * - * @param {Element} element - * Element to check - * - * @param {string} classToCheck - * Class name to check for - * - * @return {boolean} - * - True if the element had the class - * - False otherwise. - * - * @throws {Error} - * Throws an error if `classToCheck` has white space. - */ -function hasClass(element, classToCheck) { - throwIfWhitespace(classToCheck); - if (element.classList) { - return element.classList.contains(classToCheck); - } - return classRegExp(classToCheck).test(element.className); -} - -/** - * Add a CSS class name to an element - * - * @param {Element} element - * Element to add class name to. - * - * @param {string} classToAdd - * Class name to add. - * - * @return {Element} - * The dom element with the added class name. - */ -function addClass(element, classToAdd) { - if (element.classList) { - element.classList.add(classToAdd); - - // Don't need to `throwIfWhitespace` here because `hasElClass` will do it - // in the case of classList not being supported. - } else if (!hasClass(element, classToAdd)) { - element.className = (element.className + ' ' + classToAdd).trim(); - } - - return element; -} - -/** - * Remove a CSS class name from an element - * - * @param {Element} element - * Element to remove a class name from. - * - * @param {string} classToRemove - * Class name to remove - * - * @return {Element} - * The dom element with class name removed. - */ -function removeClass(element, classToRemove) { - if (element.classList) { - element.classList.remove(classToRemove); - } else { - throwIfWhitespace(classToRemove); - element.className = element.className.split(/\s+/).filter(function (c) { - return c !== classToRemove; - }).join(' '); - } - - return element; -} - -/** - * The callback definition for toggleElClass. - * - * @callback Dom~PredicateCallback - * @param {Element} element - * The DOM element of the Component. - * - * @param {string} classToToggle - * The `className` that wants to be toggled - * - * @return {boolean|undefined} - * - If true the `classToToggle` will get added to `element`. - * - If false the `classToToggle` will get removed from `element`. - * - If undefined this callback will be ignored - */ - -/** - * Adds or removes a CSS class name on an element depending on an optional - * condition or the presence/absence of the class name. - * - * @param {Element} element - * The element to toggle a class name on. - * - * @param {string} classToToggle - * The class that should be toggled - * - * @param {boolean|PredicateCallback} [predicate] - * See the return value for {@link Dom~PredicateCallback} - * - * @return {Element} - * The element with a class that has been toggled. - */ -function toggleClass(element, classToToggle, predicate) { - - // This CANNOT use `classList` internally because IE does not support the - // second parameter to the `classList.toggle()` method! Which is fine because - // `classList` will be used by the add/remove functions. - var has = hasClass(element, classToToggle); - - if (typeof predicate === 'function') { - predicate = predicate(element, classToToggle); - } - - if (typeof predicate !== 'boolean') { - predicate = !has; - } - - // If the necessary class operation matches the current state of the - // element, no action is required. - if (predicate === has) { - return; - } - - if (predicate) { - addClass(element, classToToggle); - } else { - removeClass(element, classToToggle); - } - - return element; -} - -/** - * Apply attributes to an HTML element. - * - * @param {Element} el - * Element to add attributes to. - * - * @param {Object} [attributes] - * Attributes to be applied. - */ -function setAttributes(el, attributes) { - Object.getOwnPropertyNames(attributes).forEach(function (attrName) { - var attrValue = attributes[attrName]; - - if (attrValue === null || typeof attrValue === 'undefined' || attrValue === false) { - el.removeAttribute(attrName); - } else { - el.setAttribute(attrName, attrValue === true ? '' : attrValue); - } - }); -} - -/** - * Get an element's attribute values, as defined on the HTML tag - * Attributes are not the same as properties. They're defined on the tag - * or with setAttribute (which shouldn't be used with HTML) - * This will return true or false for boolean attributes. - * - * @param {Element} tag - * Element from which to get tag attributes. - * - * @return {Object} - * All attributes of the element. - */ -function getAttributes(tag) { - var obj = {}; - - // known boolean attributes - // we can check for matching boolean properties, but older browsers - // won't know about HTML5 boolean attributes that we still read from - var knownBooleans = ',' + 'autoplay,controls,playsinline,loop,muted,default,defaultMuted' + ','; - - if (tag && tag.attributes && tag.attributes.length > 0) { - var attrs = tag.attributes; - - for (var i = attrs.length - 1; i >= 0; i--) { - var attrName = attrs[i].name; - var attrVal = attrs[i].value; - - // check for known booleans - // the matching element property will return a value for typeof - if (typeof tag[attrName] === 'boolean' || knownBooleans.indexOf(',' + attrName + ',') !== -1) { - // the value of an included boolean attribute is typically an empty - // string ('') which would equal false if we just check for a false value. - // we also don't want support bad code like autoplay='false' - attrVal = attrVal !== null ? true : false; - } - - obj[attrName] = attrVal; - } - } - - return obj; -} - -/** - * Get the value of an element's attribute - * - * @param {Element} el - * A DOM element - * - * @param {string} attribute - * Attribute to get the value of - * - * @return {string} - * value of the attribute - */ -function getAttribute(el, attribute) { - return el.getAttribute(attribute); -} - -/** - * Set the value of an element's attribute - * - * @param {Element} el - * A DOM element - * - * @param {string} attribute - * Attribute to set - * - * @param {string} value - * Value to set the attribute to - */ -function setAttribute(el, attribute, value) { - el.setAttribute(attribute, value); -} - -/** - * Remove an element's attribute - * - * @param {Element} el - * A DOM element - * - * @param {string} attribute - * Attribute to remove - */ -function removeAttribute(el, attribute) { - el.removeAttribute(attribute); -} - -/** - * Attempt to block the ability to select text while dragging controls - */ -function blockTextSelection() { - document_1.body.focus(); - document_1.onselectstart = function () { - return false; - }; -} - -/** - * Turn off text selection blocking - */ -function unblockTextSelection() { - document_1.onselectstart = function () { - return true; - }; -} - -/** - * Identical to the native `getBoundingClientRect` function, but ensures that - * the method is supported at all (it is in all browsers we claim to support) - * and that the element is in the DOM before continuing. - * - * This wrapper function also shims properties which are not provided by some - * older browsers (namely, IE8). - * - * Additionally, some browsers do not support adding properties to a - * `ClientRect`/`DOMRect` object; so, we shallow-copy it with the standard - * properties (except `x` and `y` which are not widely supported). This helps - * avoid implementations where keys are non-enumerable. - * - * @param {Element} el - * Element whose `ClientRect` we want to calculate. - * - * @return {Object|undefined} - * Always returns a plain - */ -function getBoundingClientRect(el) { - if (el && el.getBoundingClientRect && el.parentNode) { - var rect = el.getBoundingClientRect(); - var result = {}; - - ['bottom', 'height', 'left', 'right', 'top', 'width'].forEach(function (k) { - if (rect[k] !== undefined) { - result[k] = rect[k]; - } - }); - - if (!result.height) { - result.height = parseFloat(computedStyle(el, 'height')); - } - - if (!result.width) { - result.width = parseFloat(computedStyle(el, 'width')); - } - - return result; - } -} - -/** - * The postion of a DOM element on the page. - * - * @typedef {Object} module:dom~Position - * - * @property {number} left - * Pixels to the left - * - * @property {number} top - * Pixels on top - */ - -/** - * Offset Left. - * getBoundingClientRect technique from - * John Resig - * - * @see http://ejohn.org/blog/getboundingclientrect-is-awesome/ - * - * @param {Element} el - * Element from which to get offset - * - * @return {module:dom~Position} - * The position of the element that was passed in. - */ -function findPosition(el) { - var box = void 0; - - if (el.getBoundingClientRect && el.parentNode) { - box = el.getBoundingClientRect(); - } - - if (!box) { - return { - left: 0, - top: 0 - }; - } - - var docEl = document_1.documentElement; - var body = document_1.body; - - var clientLeft = docEl.clientLeft || body.clientLeft || 0; - var scrollLeft = window_1.pageXOffset || body.scrollLeft; - var left = box.left + scrollLeft - clientLeft; - - var clientTop = docEl.clientTop || body.clientTop || 0; - var scrollTop = window_1.pageYOffset || body.scrollTop; - var top = box.top + scrollTop - clientTop; - - // Android sometimes returns slightly off decimal values, so need to round - return { - left: Math.round(left), - top: Math.round(top) - }; -} - -/** - * x and y coordinates for a dom element or mouse pointer - * - * @typedef {Object} Dom~Coordinates - * - * @property {number} x - * x coordinate in pixels - * - * @property {number} y - * y coordinate in pixels - */ - -/** - * Get pointer position in element - * Returns an object with x and y coordinates. - * The base on the coordinates are the bottom left of the element. - * - * @param {Element} el - * Element on which to get the pointer position on - * - * @param {EventTarget~Event} event - * Event object - * - * @return {Dom~Coordinates} - * A Coordinates object corresponding to the mouse position. - * - */ -function getPointerPosition(el, event) { - var position = {}; - var box = findPosition(el); - var boxW = el.offsetWidth; - var boxH = el.offsetHeight; - - var boxY = box.top; - var boxX = box.left; - var pageY = event.pageY; - var pageX = event.pageX; - - if (event.changedTouches) { - pageX = event.changedTouches[0].pageX; - pageY = event.changedTouches[0].pageY; - } - - position.y = Math.max(0, Math.min(1, (boxY - pageY + boxH) / boxH)); - position.x = Math.max(0, Math.min(1, (pageX - boxX) / boxW)); - - return position; -} - -/** - * Determines, via duck typing, whether or not a value is a text node. - * - * @param {Mixed} value - * Check if this value is a text node. - * - * @return {boolean} - * - True if it is a text node - * - False otherwise - */ -function isTextNode(value) { - return isObject(value) && value.nodeType === 3; -} - -/** - * Empties the contents of an element. - * - * @param {Element} el - * The element to empty children from - * - * @return {Element} - * The element with no children - */ -function emptyEl(el) { - while (el.firstChild) { - el.removeChild(el.firstChild); - } - return el; -} - -/** - * Normalizes content for eventual insertion into the DOM. - * - * This allows a wide range of content definition methods, but protects - * from falling into the trap of simply writing to `innerHTML`, which is - * an XSS concern. - * - * The content for an element can be passed in multiple types and - * combinations, whose behavior is as follows: - * - * @param {String|Element|TextNode|Array|Function} content - * - String: Normalized into a text node. - * - Element/TextNode: Passed through. - * - Array: A one-dimensional array of strings, elements, nodes, or functions - * (which return single strings, elements, or nodes). - * - Function: If the sole argument, is expected to produce a string, element, - * node, or array as defined above. - * - * @return {Array} - * All of the content that was passed in normalized. - */ -function normalizeContent(content) { - - // First, invoke content if it is a function. If it produces an array, - // that needs to happen before normalization. - if (typeof content === 'function') { - content = content(); - } - - // Next up, normalize to an array, so one or many items can be normalized, - // filtered, and returned. - return (Array.isArray(content) ? content : [content]).map(function (value) { - - // First, invoke value if it is a function to produce a new value, - // which will be subsequently normalized to a Node of some kind. - if (typeof value === 'function') { - value = value(); - } - - if (isEl(value) || isTextNode(value)) { - return value; - } - - if (typeof value === 'string' && /\S/.test(value)) { - return document_1.createTextNode(value); - } - }).filter(function (value) { - return value; - }); -} - -/** - * Normalizes and appends content to an element. - * - * @param {Element} el - * Element to append normalized content to. - * - * - * @param {String|Element|TextNode|Array|Function} content - * See the `content` argument of {@link dom:normalizeContent} - * - * @return {Element} - * The element with appended normalized content. - */ -function appendContent(el, content) { - normalizeContent(content).forEach(function (node) { - return el.appendChild(node); - }); - return el; -} - -/** - * Normalizes and inserts content into an element; this is identical to - * `appendContent()`, except it empties the element first. - * - * @param {Element} el - * Element to insert normalized content into. - * - * @param {String|Element|TextNode|Array|Function} content - * See the `content` argument of {@link dom:normalizeContent} - * - * @return {Element} - * The element with inserted normalized content. - * - */ -function insertContent(el, content) { - return appendContent(emptyEl(el), content); -} - -/** - * Check if event was a single left click - * - * @param {EventTarget~Event} event - * Event object - * - * @return {boolean} - * - True if a left click - * - False if not a left click - */ -function isSingleLeftClick(event) { - // Note: if you create something draggable, be sure to - // call it on both `mousedown` and `mousemove` event, - // otherwise `mousedown` should be enough for a button - - if (event.button === undefined && event.buttons === undefined) { - // Why do we need `buttons` ? - // Because, middle mouse sometimes have this: - // e.button === 0 and e.buttons === 4 - // Furthermore, we want to prevent combination click, something like - // HOLD middlemouse then left click, that would be - // e.button === 0, e.buttons === 5 - // just `button` is not gonna work - - // Alright, then what this block does ? - // this is for chrome `simulate mobile devices` - // I want to support this as well - - return true; - } - - if (event.button === 0 && event.buttons === undefined) { - // Touch screen, sometimes on some specific device, `buttons` - // doesn't have anything (safari on ios, blackberry...) - - return true; - } - - if (IE_VERSION === 9) { - // Ignore IE9 - - return true; - } - - if (event.button !== 0 || event.buttons !== 1) { - // This is the reason we have those if else block above - // if any special case we can catch and let it slide - // we do it above, when get to here, this definitely - // is-not-left-click - - return false; - } - - return true; -} - -/** - * Finds a single DOM element matching `selector` within the optional - * `context` of another DOM element (defaulting to `document`). - * - * @param {string} selector - * A valid CSS selector, which will be passed to `querySelector`. - * - * @param {Element|String} [context=document] - * A DOM element within which to query. Can also be a selector - * string in which case the first matching element will be used - * as context. If missing (or no element matches selector), falls - * back to `document`. - * - * @return {Element|null} - * The element that was found or null. - */ -var $ = createQuerier('querySelector'); - -/** - * Finds a all DOM elements matching `selector` within the optional - * `context` of another DOM element (defaulting to `document`). - * - * @param {string} selector - * A valid CSS selector, which will be passed to `querySelectorAll`. - * - * @param {Element|String} [context=document] - * A DOM element within which to query. Can also be a selector - * string in which case the first matching element will be used - * as context. If missing (or no element matches selector), falls - * back to `document`. - * - * @return {NodeList} - * A element list of elements that were found. Will be empty if none were found. - * - */ -var $$ = createQuerier('querySelectorAll'); - - - -var Dom = (Object.freeze || Object)({ - isReal: isReal, - isEl: isEl, - isInFrame: isInFrame, - createEl: createEl, - textContent: textContent, - prependTo: prependTo, - hasClass: hasClass, - addClass: addClass, - removeClass: removeClass, - toggleClass: toggleClass, - setAttributes: setAttributes, - getAttributes: getAttributes, - getAttribute: getAttribute, - setAttribute: setAttribute, - removeAttribute: removeAttribute, - blockTextSelection: blockTextSelection, - unblockTextSelection: unblockTextSelection, - getBoundingClientRect: getBoundingClientRect, - findPosition: findPosition, - getPointerPosition: getPointerPosition, - isTextNode: isTextNode, - emptyEl: emptyEl, - normalizeContent: normalizeContent, - appendContent: appendContent, - insertContent: insertContent, - isSingleLeftClick: isSingleLeftClick, - $: $, - $$: $$ -}); - -/** - * @file guid.js - * @module guid - */ - -/** - * Unique ID for an element or function - * @type {Number} - */ -var _guid = 1; - -/** - * Get a unique auto-incrementing ID by number that has not been returned before. - * - * @return {number} - * A new unique ID. - */ -function newGUID() { - return _guid++; -} - -/** - * @file dom-data.js - * @module dom-data - */ -/** - * Element Data Store. - * - * Allows for binding data to an element without putting it directly on the - * element. Ex. Event listeners are stored here. - * (also from jsninja.com, slightly modified and updated for closure compiler) - * - * @type {Object} - * @private - */ -var elData = {}; - -/* - * Unique attribute name to store an element's guid in - * - * @type {String} - * @constant - * @private - */ -var elIdAttr = 'vdata' + new Date().getTime(); - -/** - * Returns the cache object where data for an element is stored - * - * @param {Element} el - * Element to store data for. - * - * @return {Object} - * The cache object for that el that was passed in. - */ -function getData(el) { - var id = el[elIdAttr]; - - if (!id) { - id = el[elIdAttr] = newGUID(); - } - - if (!elData[id]) { - elData[id] = {}; - } - - return elData[id]; -} - -/** - * Returns whether or not an element has cached data - * - * @param {Element} el - * Check if this element has cached data. - * - * @return {boolean} - * - True if the DOM element has cached data. - * - False otherwise. - */ -function hasData(el) { - var id = el[elIdAttr]; - - if (!id) { - return false; - } - - return !!Object.getOwnPropertyNames(elData[id]).length; -} - -/** - * Delete data for the element from the cache and the guid attr from getElementById - * - * @param {Element} el - * Remove cached data for this element. - */ -function removeData(el) { - var id = el[elIdAttr]; - - if (!id) { - return; - } - - // Remove all stored data - delete elData[id]; - - // Remove the elIdAttr property from the DOM node - try { - delete el[elIdAttr]; - } catch (e) { - if (el.removeAttribute) { - el.removeAttribute(elIdAttr); - } else { - // IE doesn't appear to support removeAttribute on the document element - el[elIdAttr] = null; - } - } -} - -/** - * @file events.js. An Event System (John Resig - Secrets of a JS Ninja http://jsninja.com/) - * (Original book version wasn't completely usable, so fixed some things and made Closure Compiler compatible) - * This should work very similarly to jQuery's events, however it's based off the book version which isn't as - * robust as jquery's, so there's probably some differences. - * - * @module events - */ - -/** - * Clean up the listener cache and dispatchers - * - * @param {Element|Object} elem - * Element to clean up - * - * @param {string} type - * Type of event to clean up - */ -function _cleanUpEvents(elem, type) { - var data = getData(elem); - - // Remove the events of a particular type if there are none left - if (data.handlers[type].length === 0) { - delete data.handlers[type]; - // data.handlers[type] = null; - // Setting to null was causing an error with data.handlers - - // Remove the meta-handler from the element - if (elem.removeEventListener) { - elem.removeEventListener(type, data.dispatcher, false); - } else if (elem.detachEvent) { - elem.detachEvent('on' + type, data.dispatcher); - } - } - - // Remove the events object if there are no types left - if (Object.getOwnPropertyNames(data.handlers).length <= 0) { - delete data.handlers; - delete data.dispatcher; - delete data.disabled; - } - - // Finally remove the element data if there is no data left - if (Object.getOwnPropertyNames(data).length === 0) { - removeData(elem); - } -} - -/** - * Loops through an array of event types and calls the requested method for each type. - * - * @param {Function} fn - * The event method we want to use. - * - * @param {Element|Object} elem - * Element or object to bind listeners to - * - * @param {string} type - * Type of event to bind to. - * - * @param {EventTarget~EventListener} callback - * Event listener. - */ -function _handleMultipleEvents(fn, elem, types, callback) { - types.forEach(function (type) { - // Call the event method for each one of the types - fn(elem, type, callback); - }); -} - -/** - * Fix a native event to have standard property values - * - * @param {Object} event - * Event object to fix. - * - * @return {Object} - * Fixed event object. - */ -function fixEvent(event) { - - function returnTrue() { - return true; - } - - function returnFalse() { - return false; - } - - // Test if fixing up is needed - // Used to check if !event.stopPropagation instead of isPropagationStopped - // But native events return true for stopPropagation, but don't have - // other expected methods like isPropagationStopped. Seems to be a problem - // with the Javascript Ninja code. So we're just overriding all events now. - if (!event || !event.isPropagationStopped) { - var old = event || window_1.event; - - event = {}; - // Clone the old object so that we can modify the values event = {}; - // IE8 Doesn't like when you mess with native event properties - // Firefox returns false for event.hasOwnProperty('type') and other props - // which makes copying more difficult. - // TODO: Probably best to create a whitelist of event props - for (var key in old) { - // Safari 6.0.3 warns you if you try to copy deprecated layerX/Y - // Chrome warns you if you try to copy deprecated keyboardEvent.keyLocation - // and webkitMovementX/Y - if (key !== 'layerX' && key !== 'layerY' && key !== 'keyLocation' && key !== 'webkitMovementX' && key !== 'webkitMovementY') { - // Chrome 32+ warns if you try to copy deprecated returnValue, but - // we still want to if preventDefault isn't supported (IE8). - if (!(key === 'returnValue' && old.preventDefault)) { - event[key] = old[key]; - } - } - } - - // The event occurred on this element - if (!event.target) { - event.target = event.srcElement || document_1; - } - - // Handle which other element the event is related to - if (!event.relatedTarget) { - event.relatedTarget = event.fromElement === event.target ? event.toElement : event.fromElement; - } - - // Stop the default browser action - event.preventDefault = function () { - if (old.preventDefault) { - old.preventDefault(); - } - event.returnValue = false; - old.returnValue = false; - event.defaultPrevented = true; - }; - - event.defaultPrevented = false; - - // Stop the event from bubbling - event.stopPropagation = function () { - if (old.stopPropagation) { - old.stopPropagation(); - } - event.cancelBubble = true; - old.cancelBubble = true; - event.isPropagationStopped = returnTrue; - }; - - event.isPropagationStopped = returnFalse; - - // Stop the event from bubbling and executing other handlers - event.stopImmediatePropagation = function () { - if (old.stopImmediatePropagation) { - old.stopImmediatePropagation(); - } - event.isImmediatePropagationStopped = returnTrue; - event.stopPropagation(); - }; - - event.isImmediatePropagationStopped = returnFalse; - - // Handle mouse position - if (event.clientX !== null && event.clientX !== undefined) { - var doc = document_1.documentElement; - var body = document_1.body; - - event.pageX = event.clientX + (doc && doc.scrollLeft || body && body.scrollLeft || 0) - (doc && doc.clientLeft || body && body.clientLeft || 0); - event.pageY = event.clientY + (doc && doc.scrollTop || body && body.scrollTop || 0) - (doc && doc.clientTop || body && body.clientTop || 0); - } - - // Handle key presses - event.which = event.charCode || event.keyCode; - - // Fix button for mouse clicks: - // 0 == left; 1 == middle; 2 == right - if (event.button !== null && event.button !== undefined) { - - // The following is disabled because it does not pass videojs-standard - // and... yikes. - /* eslint-disable */ - event.button = event.button & 1 ? 0 : event.button & 4 ? 1 : event.button & 2 ? 2 : 0; - /* eslint-enable */ - } - } - - // Returns fixed-up instance - return event; -} - -/** - * Whether passive event listeners are supported - */ -var _supportsPassive = false; - -(function () { - try { - var opts = Object.defineProperty({}, 'passive', { - get: function get() { - _supportsPassive = true; - } - }); - - window_1.addEventListener('test', null, opts); - window_1.removeEventListener('test', null, opts); - } catch (e) { - // disregard - } -})(); - -/** - * Touch events Chrome expects to be passive - */ -var passiveEvents = ['touchstart', 'touchmove']; - -/** - * Add an event listener to element - * It stores the handler function in a separate cache object - * and adds a generic handler to the element's event, - * along with a unique id (guid) to the element. - * - * @param {Element|Object} elem - * Element or object to bind listeners to - * - * @param {string|string[]} type - * Type of event to bind to. - * - * @param {EventTarget~EventListener} fn - * Event listener. - */ -function on(elem, type, fn) { - if (Array.isArray(type)) { - return _handleMultipleEvents(on, elem, type, fn); - } - - var data = getData(elem); - - // We need a place to store all our handler data - if (!data.handlers) { - data.handlers = {}; - } - - if (!data.handlers[type]) { - data.handlers[type] = []; - } - - if (!fn.guid) { - fn.guid = newGUID(); - } - - data.handlers[type].push(fn); - - if (!data.dispatcher) { - data.disabled = false; - - data.dispatcher = function (event, hash) { - - if (data.disabled) { - return; - } - - event = fixEvent(event); - - var handlers = data.handlers[event.type]; - - if (handlers) { - // Copy handlers so if handlers are added/removed during the process it doesn't throw everything off. - var handlersCopy = handlers.slice(0); - - for (var m = 0, n = handlersCopy.length; m < n; m++) { - if (event.isImmediatePropagationStopped()) { - break; - } else { - try { - handlersCopy[m].call(elem, event, hash); - } catch (e) { - log$1.error(e); - } - } - } - } - }; - } - - if (data.handlers[type].length === 1) { - if (elem.addEventListener) { - var options = false; - - if (_supportsPassive && passiveEvents.indexOf(type) > -1) { - options = { passive: true }; - } - elem.addEventListener(type, data.dispatcher, options); - } else if (elem.attachEvent) { - elem.attachEvent('on' + type, data.dispatcher); - } - } -} - -/** - * Removes event listeners from an element - * - * @param {Element|Object} elem - * Object to remove listeners from. - * - * @param {string|string[]} [type] - * Type of listener to remove. Don't include to remove all events from element. - * - * @param {EventTarget~EventListener} [fn] - * Specific listener to remove. Don't include to remove listeners for an event - * type. - */ -function off(elem, type, fn) { - // Don't want to add a cache object through getElData if not needed - if (!hasData(elem)) { - return; - } - - var data = getData(elem); - - // If no events exist, nothing to unbind - if (!data.handlers) { - return; - } - - if (Array.isArray(type)) { - return _handleMultipleEvents(off, elem, type, fn); - } - - // Utility function - var removeType = function removeType(el, t) { - data.handlers[t] = []; - _cleanUpEvents(el, t); - }; - - // Are we removing all bound events? - if (type === undefined) { - for (var t in data.handlers) { - if (Object.prototype.hasOwnProperty.call(data.handlers || {}, t)) { - removeType(elem, t); - } - } - return; - } - - var handlers = data.handlers[type]; - - // If no handlers exist, nothing to unbind - if (!handlers) { - return; - } - - // If no listener was provided, remove all listeners for type - if (!fn) { - removeType(elem, type); - return; - } - - // We're only removing a single handler - if (fn.guid) { - for (var n = 0; n < handlers.length; n++) { - if (handlers[n].guid === fn.guid) { - handlers.splice(n--, 1); - } - } - } - - _cleanUpEvents(elem, type); -} - -/** - * Trigger an event for an element - * - * @param {Element|Object} elem - * Element to trigger an event on - * - * @param {EventTarget~Event|string} event - * A string (the type) or an event object with a type attribute - * - * @param {Object} [hash] - * data hash to pass along with the event - * - * @return {boolean|undefined} - * - Returns the opposite of `defaultPrevented` if default was prevented - * - Otherwise returns undefined - */ -function trigger(elem, event, hash) { - // Fetches element data and a reference to the parent (for bubbling). - // Don't want to add a data object to cache for every parent, - // so checking hasElData first. - var elemData = hasData(elem) ? getData(elem) : {}; - var parent = elem.parentNode || elem.ownerDocument; - // type = event.type || event, - // handler; - - // If an event name was passed as a string, creates an event out of it - if (typeof event === 'string') { - event = { type: event, target: elem }; - } else if (!event.target) { - event.target = elem; - } - - // Normalizes the event properties. - event = fixEvent(event); - - // If the passed element has a dispatcher, executes the established handlers. - if (elemData.dispatcher) { - elemData.dispatcher.call(elem, event, hash); - } - - // Unless explicitly stopped or the event does not bubble (e.g. media events) - // recursively calls this function to bubble the event up the DOM. - if (parent && !event.isPropagationStopped() && event.bubbles === true) { - trigger.call(null, parent, event, hash); - - // If at the top of the DOM, triggers the default action unless disabled. - } else if (!parent && !event.defaultPrevented) { - var targetData = getData(event.target); - - // Checks if the target has a default action for this event. - if (event.target[event.type]) { - // Temporarily disables event dispatching on the target as we have already executed the handler. - targetData.disabled = true; - // Executes the default action. - if (typeof event.target[event.type] === 'function') { - event.target[event.type](); - } - // Re-enables event dispatching. - targetData.disabled = false; - } - } - - // Inform the triggerer if the default was prevented by returning false - return !event.defaultPrevented; -} - -/** - * Trigger a listener only once for an event - * - * @param {Element|Object} elem - * Element or object to bind to. - * - * @param {string|string[]} type - * Name/type of event - * - * @param {Event~EventListener} fn - * Event Listener function - */ -function one(elem, type, fn) { - if (Array.isArray(type)) { - return _handleMultipleEvents(one, elem, type, fn); - } - var func = function func() { - off(elem, type, func); - fn.apply(this, arguments); - }; - - // copy the guid to the new function so it can removed using the original function's ID - func.guid = fn.guid = fn.guid || newGUID(); - on(elem, type, func); -} - -var Events = (Object.freeze || Object)({ - fixEvent: fixEvent, - on: on, - off: off, - trigger: trigger, - one: one -}); - -/** - * @file setup.js - Functions for setting up a player without - * user interaction based on the data-setup `attribute` of the video tag. - * - * @module setup - */ -var _windowLoaded = false; -var videojs$2 = void 0; - -/** - * Set up any tags that have a data-setup `attribute` when the player is started. - */ -var autoSetup = function autoSetup() { - - // Protect against breakage in non-browser environments. - if (!isReal()) { - return; - } - - // One day, when we stop supporting IE8, go back to this, but in the meantime...*hack hack hack* - // var vids = Array.prototype.slice.call(document.getElementsByTagName('video')); - // var audios = Array.prototype.slice.call(document.getElementsByTagName('audio')); - // var mediaEls = vids.concat(audios); - - // Because IE8 doesn't support calling slice on a node list, we need to loop - // through each list of elements to build up a new, combined list of elements. - var vids = document_1.getElementsByTagName('video'); - var audios = document_1.getElementsByTagName('audio'); - var divs = document_1.getElementsByTagName('video-js'); - var mediaEls = []; - - if (vids && vids.length > 0) { - for (var i = 0, e = vids.length; i < e; i++) { - mediaEls.push(vids[i]); - } - } - - if (audios && audios.length > 0) { - for (var _i = 0, _e = audios.length; _i < _e; _i++) { - mediaEls.push(audios[_i]); - } - } - - if (divs && divs.length > 0) { - for (var _i2 = 0, _e2 = divs.length; _i2 < _e2; _i2++) { - mediaEls.push(divs[_i2]); - } - } - - // Check if any media elements exist - if (mediaEls && mediaEls.length > 0) { - - for (var _i3 = 0, _e3 = mediaEls.length; _i3 < _e3; _i3++) { - var mediaEl = mediaEls[_i3]; - - // Check if element exists, has getAttribute func. - // IE seems to consider typeof el.getAttribute == 'object' instead of - // 'function' like expected, at least when loading the player immediately. - if (mediaEl && mediaEl.getAttribute) { - - // Make sure this player hasn't already been set up. - if (mediaEl.player === undefined) { - var options = mediaEl.getAttribute('data-setup'); - - // Check if data-setup attr exists. - // We only auto-setup if they've added the data-setup attr. - if (options !== null) { - // Create new video.js instance. - videojs$2(mediaEl); - } - } - - // If getAttribute isn't defined, we need to wait for the DOM. - } else { - autoSetupTimeout(1); - break; - } - } - - // No videos were found, so keep looping unless page is finished loading. - } else if (!_windowLoaded) { - autoSetupTimeout(1); - } -}; - -/** - * Wait until the page is loaded before running autoSetup. This will be called in - * autoSetup if `hasLoaded` returns false. - * - * @param {number} wait - * How long to wait in ms - * - * @param {module:videojs} [vjs] - * The videojs library function - */ -function autoSetupTimeout(wait, vjs) { - if (vjs) { - videojs$2 = vjs; - } - - window_1.setTimeout(autoSetup, wait); -} - -if (isReal() && document_1.readyState === 'complete') { - _windowLoaded = true; -} else { - /** - * Listen for the load event on window, and set _windowLoaded to true. - * - * @listens load - */ - one(window_1, 'load', function () { - _windowLoaded = true; - }); -} - -/** - * @file stylesheet.js - * @module stylesheet - */ -/** - * Create a DOM syle element given a className for it. - * - * @param {string} className - * The className to add to the created style element. - * - * @return {Element} - * The element that was created. - */ -var createStyleElement = function createStyleElement(className) { - var style = document_1.createElement('style'); - - style.className = className; - - return style; -}; - -/** - * Add text to a DOM element. - * - * @param {Element} el - * The Element to add text content to. - * - * @param {string} content - * The text to add to the element. - */ -var setTextContent = function setTextContent(el, content) { - if (el.styleSheet) { - el.styleSheet.cssText = content; - } else { - el.textContent = content; - } -}; - -/** - * @file fn.js - * @module fn - */ -/** - * Bind (a.k.a proxy or Context). A simple method for changing the context of a function - * It also stores a unique id on the function so it can be easily removed from events. - * - * @param {Mixed} context - * The object to bind as scope. - * - * @param {Function} fn - * The function to be bound to a scope. - * - * @param {number} [uid] - * An optional unique ID for the function to be set - * - * @return {Function} - * The new function that will be bound into the context given - */ -var bind = function bind(context, fn, uid) { - // Make sure the function has a unique ID - if (!fn.guid) { - fn.guid = newGUID(); - } - - // Create the new function that changes the context - var bound = function bound() { - return fn.apply(context, arguments); - }; - - // Allow for the ability to individualize this function - // Needed in the case where multiple objects might share the same prototype - // IF both items add an event listener with the same function, then you try to remove just one - // it will remove both because they both have the same guid. - // when using this, you need to use the bind method when you remove the listener as well. - // currently used in text tracks - bound.guid = uid ? uid + '_' + fn.guid : fn.guid; - - return bound; -}; - -/** - * Wraps the given function, `fn`, with a new function that only invokes `fn` - * at most once per every `wait` milliseconds. - * - * @param {Function} fn - * The function to be throttled. - * - * @param {Number} wait - * The number of milliseconds by which to throttle. - * - * @return {Function} - */ -var throttle = function throttle(fn, wait) { - var last = Date.now(); - - var throttled = function throttled() { - var now = Date.now(); - - if (now - last >= wait) { - fn.apply(undefined, arguments); - last = now; - } - }; - - return throttled; -}; - -/** - * Creates a debounced function that delays invoking `func` until after `wait` - * milliseconds have elapsed since the last time the debounced function was - * invoked. - * - * Inspired by lodash and underscore implementations. - * - * @param {Function} func - * The function to wrap with debounce behavior. - * - * @param {number} wait - * The number of milliseconds to wait after the last invocation. - * - * @param {boolean} [immediate] - * Whether or not to invoke the function immediately upon creation. - * - * @param {Object} [context=window] - * The "context" in which the debounced function should debounce. For - * example, if this function should be tied to a Video.js player, - * the player can be passed here. Alternatively, defaults to the - * global `window` object. - * - * @return {Function} - * A debounced function. - */ -var debounce = function debounce(func, wait, immediate) { - var context = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : window_1; - - var timeout = void 0; - - /* eslint-disable consistent-this */ - return function () { - var self = this; - var args = arguments; - - var _later = function later() { - timeout = null; - _later = null; - if (!immediate) { - func.apply(self, args); - } - }; - - if (!timeout && immediate) { - func.apply(self, args); - } - - context.clearTimeout(timeout); - timeout = context.setTimeout(_later, wait); - }; - /* eslint-enable consistent-this */ -}; - -/** - * @file src/js/event-target.js - */ -/** - * `EventTarget` is a class that can have the same API as the DOM `EventTarget`. It - * adds shorthand functions that wrap around lengthy functions. For example: - * the `on` function is a wrapper around `addEventListener`. - * - * @see [EventTarget Spec]{@link https://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-EventTarget} - * @class EventTarget - */ -var EventTarget = function EventTarget() {}; - -/** - * A Custom DOM event. - * - * @typedef {Object} EventTarget~Event - * @see [Properties]{@link https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent} - */ - -/** - * All event listeners should follow the following format. - * - * @callback EventTarget~EventListener - * @this {EventTarget} - * - * @param {EventTarget~Event} event - * the event that triggered this function - * - * @param {Object} [hash] - * hash of data sent during the event - */ - -/** - * An object containing event names as keys and booleans as values. - * - * > NOTE: If an event name is set to a true value here {@link EventTarget#trigger} - * will have extra functionality. See that function for more information. - * - * @property EventTarget.prototype.allowedEvents_ - * @private - */ -EventTarget.prototype.allowedEvents_ = {}; - -/** - * Adds an `event listener` to an instance of an `EventTarget`. An `event listener` is a - * function that will get called when an event with a certain name gets triggered. - * - * @param {string|string[]} type - * An event name or an array of event names. - * - * @param {EventTarget~EventListener} fn - * The function to call with `EventTarget`s - */ -EventTarget.prototype.on = function (type, fn) { - // Remove the addEventListener alias before calling Events.on - // so we don't get into an infinite type loop - var ael = this.addEventListener; - - this.addEventListener = function () {}; - on(this, type, fn); - this.addEventListener = ael; -}; - -/** - * An alias of {@link EventTarget#on}. Allows `EventTarget` to mimic - * the standard DOM API. - * - * @function - * @see {@link EventTarget#on} - */ -EventTarget.prototype.addEventListener = EventTarget.prototype.on; - -/** - * Removes an `event listener` for a specific event from an instance of `EventTarget`. - * This makes it so that the `event listener` will no longer get called when the - * named event happens. - * - * @param {string|string[]} type - * An event name or an array of event names. - * - * @param {EventTarget~EventListener} fn - * The function to remove. - */ -EventTarget.prototype.off = function (type, fn) { - off(this, type, fn); -}; - -/** - * An alias of {@link EventTarget#off}. Allows `EventTarget` to mimic - * the standard DOM API. - * - * @function - * @see {@link EventTarget#off} - */ -EventTarget.prototype.removeEventListener = EventTarget.prototype.off; - -/** - * This function will add an `event listener` that gets triggered only once. After the - * first trigger it will get removed. This is like adding an `event listener` - * with {@link EventTarget#on} that calls {@link EventTarget#off} on itself. - * - * @param {string|string[]} type - * An event name or an array of event names. - * - * @param {EventTarget~EventListener} fn - * The function to be called once for each event name. - */ -EventTarget.prototype.one = function (type, fn) { - // Remove the addEventListener alialing Events.on - // so we don't get into an infinite type loop - var ael = this.addEventListener; - - this.addEventListener = function () {}; - one(this, type, fn); - this.addEventListener = ael; -}; - -/** - * This function causes an event to happen. This will then cause any `event listeners` - * that are waiting for that event, to get called. If there are no `event listeners` - * for an event then nothing will happen. - * - * If the name of the `Event` that is being triggered is in `EventTarget.allowedEvents_`. - * Trigger will also call the `on` + `uppercaseEventName` function. - * - * Example: - * 'click' is in `EventTarget.allowedEvents_`, so, trigger will attempt to call - * `onClick` if it exists. - * - * @param {string|EventTarget~Event|Object} event - * The name of the event, an `Event`, or an object with a key of type set to - * an event name. - */ -EventTarget.prototype.trigger = function (event) { - var type = event.type || event; - - if (typeof event === 'string') { - event = { type: type }; - } - event = fixEvent(event); - - if (this.allowedEvents_[type] && this['on' + type]) { - this['on' + type](event); - } - - trigger(this, event); -}; - -/** - * An alias of {@link EventTarget#trigger}. Allows `EventTarget` to mimic - * the standard DOM API. - * - * @function - * @see {@link EventTarget#trigger} - */ -EventTarget.prototype.dispatchEvent = EventTarget.prototype.trigger; - -/** - * @file mixins/evented.js - * @module evented - */ -/** - * Returns whether or not an object has had the evented mixin applied. - * - * @param {Object} object - * An object to test. - * - * @return {boolean} - * Whether or not the object appears to be evented. - */ -var isEvented = function isEvented(object) { - return object instanceof EventTarget || !!object.eventBusEl_ && ['on', 'one', 'off', 'trigger'].every(function (k) { - return typeof object[k] === 'function'; - }); -}; - -/** - * Whether a value is a valid event type - non-empty string or array. - * - * @private - * @param {string|Array} type - * The type value to test. - * - * @return {boolean} - * Whether or not the type is a valid event type. - */ -var isValidEventType = function isValidEventType(type) { - return ( - // The regex here verifies that the `type` contains at least one non- - // whitespace character. - typeof type === 'string' && /\S/.test(type) || Array.isArray(type) && !!type.length - ); -}; - -/** - * Validates a value to determine if it is a valid event target. Throws if not. - * - * @private - * @throws {Error} - * If the target does not appear to be a valid event target. - * - * @param {Object} target - * The object to test. - */ -var validateTarget = function validateTarget(target) { - if (!target.nodeName && !isEvented(target)) { - throw new Error('Invalid target; must be a DOM node or evented object.'); - } -}; - -/** - * Validates a value to determine if it is a valid event target. Throws if not. - * - * @private - * @throws {Error} - * If the type does not appear to be a valid event type. - * - * @param {string|Array} type - * The type to test. - */ -var validateEventType = function validateEventType(type) { - if (!isValidEventType(type)) { - throw new Error('Invalid event type; must be a non-empty string or array.'); - } -}; - -/** - * Validates a value to determine if it is a valid listener. Throws if not. - * - * @private - * @throws {Error} - * If the listener is not a function. - * - * @param {Function} listener - * The listener to test. - */ -var validateListener = function validateListener(listener) { - if (typeof listener !== 'function') { - throw new Error('Invalid listener; must be a function.'); - } -}; - -/** - * Takes an array of arguments given to `on()` or `one()`, validates them, and - * normalizes them into an object. - * - * @private - * @param {Object} self - * The evented object on which `on()` or `one()` was called. This - * object will be bound as the `this` value for the listener. - * - * @param {Array} args - * An array of arguments passed to `on()` or `one()`. - * - * @return {Object} - * An object containing useful values for `on()` or `one()` calls. - */ -var normalizeListenArgs = function normalizeListenArgs(self, args) { - - // If the number of arguments is less than 3, the target is always the - // evented object itself. - var isTargetingSelf = args.length < 3 || args[0] === self || args[0] === self.eventBusEl_; - var target = void 0; - var type = void 0; - var listener = void 0; - - if (isTargetingSelf) { - target = self.eventBusEl_; - - // Deal with cases where we got 3 arguments, but we are still listening to - // the evented object itself. - if (args.length >= 3) { - args.shift(); - } - - type = args[0]; - listener = args[1]; - } else { - target = args[0]; - type = args[1]; - listener = args[2]; - } - - validateTarget(target); - validateEventType(type); - validateListener(listener); - - listener = bind(self, listener); - - return { isTargetingSelf: isTargetingSelf, target: target, type: type, listener: listener }; -}; - -/** - * Adds the listener to the event type(s) on the target, normalizing for - * the type of target. - * - * @private - * @param {Element|Object} target - * A DOM node or evented object. - * - * @param {string} method - * The event binding method to use ("on" or "one"). - * - * @param {string|Array} type - * One or more event type(s). - * - * @param {Function} listener - * A listener function. - */ -var listen = function listen(target, method, type, listener) { - validateTarget(target); - - if (target.nodeName) { - Events[method](target, type, listener); - } else { - target[method](type, listener); - } -}; - -/** - * Contains methods that provide event capabilites to an object which is passed - * to {@link module:evented|evented}. - * - * @mixin EventedMixin - */ -var EventedMixin = { - - /** - * Add a listener to an event (or events) on this object or another evented - * object. - * - * @param {string|Array|Element|Object} targetOrType - * If this is a string or array, it represents the event type(s) - * that will trigger the listener. - * - * Another evented object can be passed here instead, which will - * cause the listener to listen for events on _that_ object. - * - * In either case, the listener's `this` value will be bound to - * this object. - * - * @param {string|Array|Function} typeOrListener - * If the first argument was a string or array, this should be the - * listener function. Otherwise, this is a string or array of event - * type(s). - * - * @param {Function} [listener] - * If the first argument was another evented object, this will be - * the listener function. - */ - on: function on$$1() { - var _this = this; - - for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { - args[_key] = arguments[_key]; - } - - var _normalizeListenArgs = normalizeListenArgs(this, args), - isTargetingSelf = _normalizeListenArgs.isTargetingSelf, - target = _normalizeListenArgs.target, - type = _normalizeListenArgs.type, - listener = _normalizeListenArgs.listener; - - listen(target, 'on', type, listener); - - // If this object is listening to another evented object. - if (!isTargetingSelf) { - - // If this object is disposed, remove the listener. - var removeListenerOnDispose = function removeListenerOnDispose() { - return _this.off(target, type, listener); - }; - - // Use the same function ID as the listener so we can remove it later it - // using the ID of the original listener. - removeListenerOnDispose.guid = listener.guid; - - // Add a listener to the target's dispose event as well. This ensures - // that if the target is disposed BEFORE this object, we remove the - // removal listener that was just added. Otherwise, we create a memory leak. - var removeRemoverOnTargetDispose = function removeRemoverOnTargetDispose() { - return _this.off('dispose', removeListenerOnDispose); - }; - - // Use the same function ID as the listener so we can remove it later - // it using the ID of the original listener. - removeRemoverOnTargetDispose.guid = listener.guid; - - listen(this, 'on', 'dispose', removeListenerOnDispose); - listen(target, 'on', 'dispose', removeRemoverOnTargetDispose); - } - }, - - - /** - * Add a listener to an event (or events) on this object or another evented - * object. The listener will only be called once and then removed. - * - * @param {string|Array|Element|Object} targetOrType - * If this is a string or array, it represents the event type(s) - * that will trigger the listener. - * - * Another evented object can be passed here instead, which will - * cause the listener to listen for events on _that_ object. - * - * In either case, the listener's `this` value will be bound to - * this object. - * - * @param {string|Array|Function} typeOrListener - * If the first argument was a string or array, this should be the - * listener function. Otherwise, this is a string or array of event - * type(s). - * - * @param {Function} [listener] - * If the first argument was another evented object, this will be - * the listener function. - */ - one: function one$$1() { - var _this2 = this; - - for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { - args[_key2] = arguments[_key2]; - } - - var _normalizeListenArgs2 = normalizeListenArgs(this, args), - isTargetingSelf = _normalizeListenArgs2.isTargetingSelf, - target = _normalizeListenArgs2.target, - type = _normalizeListenArgs2.type, - listener = _normalizeListenArgs2.listener; - - // Targeting this evented object. - - - if (isTargetingSelf) { - listen(target, 'one', type, listener); - - // Targeting another evented object. - } else { - var wrapper = function wrapper() { - for (var _len3 = arguments.length, largs = Array(_len3), _key3 = 0; _key3 < _len3; _key3++) { - largs[_key3] = arguments[_key3]; - } - - _this2.off(target, type, wrapper); - listener.apply(null, largs); - }; - - // Use the same function ID as the listener so we can remove it later - // it using the ID of the original listener. - wrapper.guid = listener.guid; - listen(target, 'one', type, wrapper); - } - }, - - - /** - * Removes listener(s) from event(s) on an evented object. - * - * @param {string|Array|Element|Object} [targetOrType] - * If this is a string or array, it represents the event type(s). - * - * Another evented object can be passed here instead, in which case - * ALL 3 arguments are _required_. - * - * @param {string|Array|Function} [typeOrListener] - * If the first argument was a string or array, this may be the - * listener function. Otherwise, this is a string or array of event - * type(s). - * - * @param {Function} [listener] - * If the first argument was another evented object, this will be - * the listener function; otherwise, _all_ listeners bound to the - * event type(s) will be removed. - */ - off: function off$$1(targetOrType, typeOrListener, listener) { - - // Targeting this evented object. - if (!targetOrType || isValidEventType(targetOrType)) { - off(this.eventBusEl_, targetOrType, typeOrListener); - - // Targeting another evented object. - } else { - var target = targetOrType; - var type = typeOrListener; - - // Fail fast and in a meaningful way! - validateTarget(target); - validateEventType(type); - validateListener(listener); - - // Ensure there's at least a guid, even if the function hasn't been used - listener = bind(this, listener); - - // Remove the dispose listener on this evented object, which was given - // the same guid as the event listener in on(). - this.off('dispose', listener); - - if (target.nodeName) { - off(target, type, listener); - off(target, 'dispose', listener); - } else if (isEvented(target)) { - target.off(type, listener); - target.off('dispose', listener); - } - } - }, - - - /** - * Fire an event on this evented object, causing its listeners to be called. - * - * @param {string|Object} event - * An event type or an object with a type property. - * - * @param {Object} [hash] - * An additional object to pass along to listeners. - * - * @returns {boolean} - * Whether or not the default behavior was prevented. - */ - trigger: function trigger$$1(event, hash) { - return trigger(this.eventBusEl_, event, hash); - } -}; - -/** - * Applies {@link module:evented~EventedMixin|EventedMixin} to a target object. - * - * @param {Object} target - * The object to which to add event methods. - * - * @param {Object} [options={}] - * Options for customizing the mixin behavior. - * - * @param {String} [options.eventBusKey] - * By default, adds a `eventBusEl_` DOM element to the target object, - * which is used as an event bus. If the target object already has a - * DOM element that should be used, pass its key here. - * - * @return {Object} - * The target object. - */ -function evented(target) { - var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; - var eventBusKey = options.eventBusKey; - - // Set or create the eventBusEl_. - - if (eventBusKey) { - if (!target[eventBusKey].nodeName) { - throw new Error('The eventBusKey "' + eventBusKey + '" does not refer to an element.'); - } - target.eventBusEl_ = target[eventBusKey]; - } else { - target.eventBusEl_ = createEl('span', { className: 'vjs-event-bus' }); - } - - assign(target, EventedMixin); - - // When any evented object is disposed, it removes all its listeners. - target.on('dispose', function () { - target.off(); - window_1.setTimeout(function () { - target.eventBusEl_ = null; - }, 0); - }); - - return target; -} - -/** - * @file mixins/stateful.js - * @module stateful - */ -/** - * Contains methods that provide statefulness to an object which is passed - * to {@link module:stateful}. - * - * @mixin StatefulMixin - */ -var StatefulMixin = { - - /** - * A hash containing arbitrary keys and values representing the state of - * the object. - * - * @type {Object} - */ - state: {}, - - /** - * Set the state of an object by mutating its - * {@link module:stateful~StatefulMixin.state|state} object in place. - * - * @fires module:stateful~StatefulMixin#statechanged - * @param {Object|Function} stateUpdates - * A new set of properties to shallow-merge into the plugin state. - * Can be a plain object or a function returning a plain object. - * - * @returns {Object|undefined} - * An object containing changes that occurred. If no changes - * occurred, returns `undefined`. - */ - setState: function setState(stateUpdates) { - var _this = this; - - // Support providing the `stateUpdates` state as a function. - if (typeof stateUpdates === 'function') { - stateUpdates = stateUpdates(); - } - - var changes = void 0; - - each(stateUpdates, function (value, key) { - - // Record the change if the value is different from what's in the - // current state. - if (_this.state[key] !== value) { - changes = changes || {}; - changes[key] = { - from: _this.state[key], - to: value - }; - } - - _this.state[key] = value; - }); - - // Only trigger "statechange" if there were changes AND we have a trigger - // function. This allows us to not require that the target object be an - // evented object. - if (changes && isEvented(this)) { - - /** - * An event triggered on an object that is both - * {@link module:stateful|stateful} and {@link module:evented|evented} - * indicating that its state has changed. - * - * @event module:stateful~StatefulMixin#statechanged - * @type {Object} - * @property {Object} changes - * A hash containing the properties that were changed and - * the values they were changed `from` and `to`. - */ - this.trigger({ - changes: changes, - type: 'statechanged' - }); - } - - return changes; - } -}; - -/** - * Applies {@link module:stateful~StatefulMixin|StatefulMixin} to a target - * object. - * - * If the target object is {@link module:evented|evented} and has a - * `handleStateChanged` method, that method will be automatically bound to the - * `statechanged` event on itself. - * - * @param {Object} target - * The object to be made stateful. - * - * @param {Object} [defaultState] - * A default set of properties to populate the newly-stateful object's - * `state` property. - * - * @returns {Object} - * Returns the `target`. - */ -function stateful(target, defaultState) { - assign(target, StatefulMixin); - - // This happens after the mixing-in because we need to replace the `state` - // added in that step. - target.state = assign({}, target.state, defaultState); - - // Auto-bind the `handleStateChanged` method of the target object if it exists. - if (typeof target.handleStateChanged === 'function' && isEvented(target)) { - target.on('statechanged', target.handleStateChanged); - } - - return target; -} - -/** - * @file to-title-case.js - * @module to-title-case - */ - -/** - * Uppercase the first letter of a string. - * - * @param {string} string - * String to be uppercased - * - * @return {string} - * The string with an uppercased first letter - */ -function toTitleCase(string) { - if (typeof string !== 'string') { - return string; - } - - return string.charAt(0).toUpperCase() + string.slice(1); -} - -/** - * Compares the TitleCase versions of the two strings for equality. - * - * @param {string} str1 - * The first string to compare - * - * @param {string} str2 - * The second string to compare - * - * @return {boolean} - * Whether the TitleCase versions of the strings are equal - */ -function titleCaseEquals(str1, str2) { - return toTitleCase(str1) === toTitleCase(str2); -} - -/** - * @file merge-options.js - * @module merge-options - */ -/** - * Deep-merge one or more options objects, recursively merging **only** plain - * object properties. - * - * @param {Object[]} sources - * One or more objects to merge into a new object. - * - * @returns {Object} - * A new object that is the merged result of all sources. - */ -function mergeOptions() { - var result = {}; - - for (var _len = arguments.length, sources = Array(_len), _key = 0; _key < _len; _key++) { - sources[_key] = arguments[_key]; - } - - sources.forEach(function (source) { - if (!source) { - return; - } - - each(source, function (value, key) { - if (!isPlain(value)) { - result[key] = value; - return; - } - - if (!isPlain(result[key])) { - result[key] = {}; - } - - result[key] = mergeOptions(result[key], value); - }); - }); - - return result; -} - -/** - * Player Component - Base class for all UI objects - * - * @file component.js - */ -/** - * Base class for all UI Components. - * Components are UI objects which represent both a javascript object and an element - * in the DOM. They can be children of other components, and can have - * children themselves. - * - * Components can also use methods from {@link EventTarget} - */ - -var Component = function () { - - /** - * A callback that is called when a component is ready. Does not have any - * paramters and any callback value will be ignored. - * - * @callback Component~ReadyCallback - * @this Component - */ - - /** - * Creates an instance of this class. - * - * @param {Player} player - * The `Player` that this class should be attached to. - * - * @param {Object} [options] - * The key/value store of player options. - * - * @param {Object[]} [options.children] - * An array of children objects to intialize this component with. Children objects have - * a name property that will be used if more than one component of the same type needs to be - * added. - * - * @param {Component~ReadyCallback} [ready] - * Function that gets called when the `Component` is ready. - */ - function Component(player, options, ready) { - classCallCheck(this, Component); - - - // The component might be the player itself and we can't pass `this` to super - if (!player && this.play) { - this.player_ = player = this; // eslint-disable-line - } else { - this.player_ = player; - } - - // Make a copy of prototype.options_ to protect against overriding defaults - this.options_ = mergeOptions({}, this.options_); - - // Updated options with supplied options - options = this.options_ = mergeOptions(this.options_, options); - - // Get ID from options or options element if one is supplied - this.id_ = options.id || options.el && options.el.id; - - // If there was no ID from the options, generate one - if (!this.id_) { - // Don't require the player ID function in the case of mock players - var id = player && player.id && player.id() || 'no_player'; - - this.id_ = id + '_component_' + newGUID(); - } - - this.name_ = options.name || null; - - // Create element if one wasn't provided in options - if (options.el) { - this.el_ = options.el; - } else if (options.createEl !== false) { - this.el_ = this.createEl(); - } - - // if evented is anything except false, we want to mixin in evented - if (options.evented !== false) { - // Make this an evented object and use `el_`, if available, as its event bus - evented(this, { eventBusKey: this.el_ ? 'el_' : null }); - } - stateful(this, this.constructor.defaultState); - - this.children_ = []; - this.childIndex_ = {}; - this.childNameIndex_ = {}; - - // Add any child components in options - if (options.initChildren !== false) { - this.initChildren(); - } - - this.ready(ready); - // Don't want to trigger ready here or it will before init is actually - // finished for all children that run this constructor - - if (options.reportTouchActivity !== false) { - this.enableTouchActivity(); - } - } - - /** - * Dispose of the `Component` and all child components. - * - * @fires Component#dispose - */ - - - Component.prototype.dispose = function dispose() { - - /** - * Triggered when a `Component` is disposed. - * - * @event Component#dispose - * @type {EventTarget~Event} - * - * @property {boolean} [bubbles=false] - * set to false so that the close event does not - * bubble up - */ - this.trigger({ type: 'dispose', bubbles: false }); - - // Dispose all children. - if (this.children_) { - for (var i = this.children_.length - 1; i >= 0; i--) { - if (this.children_[i].dispose) { - this.children_[i].dispose(); - } - } - } - - // Delete child references - this.children_ = null; - this.childIndex_ = null; - this.childNameIndex_ = null; - - if (this.el_) { - // Remove element from DOM - if (this.el_.parentNode) { - this.el_.parentNode.removeChild(this.el_); - } - - removeData(this.el_); - this.el_ = null; - } - - // remove reference to the player after disposing of the element - this.player_ = null; - }; - - /** - * Return the {@link Player} that the `Component` has attached to. - * - * @return {Player} - * The player that this `Component` has attached to. - */ - - - Component.prototype.player = function player() { - return this.player_; - }; - - /** - * Deep merge of options objects with new options. - * > Note: When both `obj` and `options` contain properties whose values are objects. - * The two properties get merged using {@link module:mergeOptions} - * - * @param {Object} obj - * The object that contains new options. - * - * @return {Object} - * A new object of `this.options_` and `obj` merged together. - * - * @deprecated since version 5 - */ - - - Component.prototype.options = function options(obj) { - log$1.warn('this.options() has been deprecated and will be moved to the constructor in 6.0'); - - if (!obj) { - return this.options_; - } - - this.options_ = mergeOptions(this.options_, obj); - return this.options_; - }; - - /** - * Get the `Component`s DOM element - * - * @return {Element} - * The DOM element for this `Component`. - */ - - - Component.prototype.el = function el() { - return this.el_; - }; - - /** - * Create the `Component`s DOM element. - * - * @param {string} [tagName] - * Element's DOM node type. e.g. 'div' - * - * @param {Object} [properties] - * An object of properties that should be set. - * - * @param {Object} [attributes] - * An object of attributes that should be set. - * - * @return {Element} - * The element that gets created. - */ - - - Component.prototype.createEl = function createEl$$1(tagName, properties, attributes) { - return createEl(tagName, properties, attributes); - }; - - /** - * Localize a string given the string in english. - * - * If tokens are provided, it'll try and run a simple token replacement on the provided string. - * The tokens it looks for look like `{1}` with the index being 1-indexed into the tokens array. - * - * If a `defaultValue` is provided, it'll use that over `string`, - * if a value isn't found in provided language files. - * This is useful if you want to have a descriptive key for token replacement - * but have a succinct localized string and not require `en.json` to be included. - * - * Currently, it is used for the progress bar timing. - * ```js - * { - * "progress bar timing: currentTime={1} duration={2}": "{1} of {2}" - * } - * ``` - * It is then used like so: - * ```js - * this.localize('progress bar timing: currentTime={1} duration{2}', - * [this.player_.currentTime(), this.player_.duration()], - * '{1} of {2}'); - * ``` - * - * Which outputs something like: `01:23 of 24:56`. - * - * - * @param {string} string - * The string to localize and the key to lookup in the language files. - * @param {string[]} [tokens] - * If the current item has token replacements, provide the tokens here. - * @param {string} [defaultValue] - * Defaults to `string`. Can be a default value to use for token replacement - * if the lookup key is needed to be separate. - * - * @return {string} - * The localized string or if no localization exists the english string. - */ - - - Component.prototype.localize = function localize(string, tokens) { - var defaultValue = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : string; - - var code = this.player_.language && this.player_.language(); - var languages = this.player_.languages && this.player_.languages(); - var language = languages && languages[code]; - var primaryCode = code && code.split('-')[0]; - var primaryLang = languages && languages[primaryCode]; - - var localizedString = defaultValue; - - if (language && language[string]) { - localizedString = language[string]; - } else if (primaryLang && primaryLang[string]) { - localizedString = primaryLang[string]; - } - - if (tokens) { - localizedString = localizedString.replace(/\{(\d+)\}/g, function (match, index) { - var value = tokens[index - 1]; - var ret = value; - - if (typeof value === 'undefined') { - ret = match; - } - - return ret; - }); - } - - return localizedString; - }; - - /** - * Return the `Component`s DOM element. This is where children get inserted. - * This will usually be the the same as the element returned in {@link Component#el}. - * - * @return {Element} - * The content element for this `Component`. - */ - - - Component.prototype.contentEl = function contentEl() { - return this.contentEl_ || this.el_; - }; - - /** - * Get this `Component`s ID - * - * @return {string} - * The id of this `Component` - */ - - - Component.prototype.id = function id() { - return this.id_; - }; - - /** - * Get the `Component`s name. The name gets used to reference the `Component` - * and is set during registration. - * - * @return {string} - * The name of this `Component`. - */ - - - Component.prototype.name = function name() { - return this.name_; - }; - - /** - * Get an array of all child components - * - * @return {Array} - * The children - */ - - - Component.prototype.children = function children() { - return this.children_; - }; - - /** - * Returns the child `Component` with the given `id`. - * - * @param {string} id - * The id of the child `Component` to get. - * - * @return {Component|undefined} - * The child `Component` with the given `id` or undefined. - */ - - - Component.prototype.getChildById = function getChildById(id) { - return this.childIndex_[id]; - }; - - /** - * Returns the child `Component` with the given `name`. - * - * @param {string} name - * The name of the child `Component` to get. - * - * @return {Component|undefined} - * The child `Component` with the given `name` or undefined. - */ - - - Component.prototype.getChild = function getChild(name) { - if (!name) { - return; - } - - name = toTitleCase(name); - - return this.childNameIndex_[name]; - }; - - /** - * Add a child `Component` inside the current `Component`. - * - * - * @param {string|Component} child - * The name or instance of a child to add. - * - * @param {Object} [options={}] - * The key/value store of options that will get passed to children of - * the child. - * - * @param {number} [index=this.children_.length] - * The index to attempt to add a child into. - * - * @return {Component} - * The `Component` that gets added as a child. When using a string the - * `Component` will get created by this process. - */ - - - Component.prototype.addChild = function addChild(child) { - var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; - var index = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : this.children_.length; - - var component = void 0; - var componentName = void 0; - - // If child is a string, create component with options - if (typeof child === 'string') { - componentName = toTitleCase(child); - - var componentClassName = options.componentClass || componentName; - - // Set name through options - options.name = componentName; - - // Create a new object & element for this controls set - // If there's no .player_, this is a player - var ComponentClass = Component.getComponent(componentClassName); - - if (!ComponentClass) { - throw new Error('Component ' + componentClassName + ' does not exist'); - } - - // data stored directly on the videojs object may be - // misidentified as a component to retain - // backwards-compatibility with 4.x. check to make sure the - // component class can be instantiated. - if (typeof ComponentClass !== 'function') { - return null; - } - - component = new ComponentClass(this.player_ || this, options); - - // child is a component instance - } else { - component = child; - } - - this.children_.splice(index, 0, component); - - if (typeof component.id === 'function') { - this.childIndex_[component.id()] = component; - } - - // If a name wasn't used to create the component, check if we can use the - // name function of the component - componentName = componentName || component.name && toTitleCase(component.name()); - - if (componentName) { - this.childNameIndex_[componentName] = component; - } - - // Add the UI object's element to the container div (box) - // Having an element is not required - if (typeof component.el === 'function' && component.el()) { - var childNodes = this.contentEl().children; - var refNode = childNodes[index] || null; - - this.contentEl().insertBefore(component.el(), refNode); - } - - // Return so it can stored on parent object if desired. - return component; - }; - - /** - * Remove a child `Component` from this `Component`s list of children. Also removes - * the child `Component`s element from this `Component`s element. - * - * @param {Component} component - * The child `Component` to remove. - */ - - - Component.prototype.removeChild = function removeChild(component) { - if (typeof component === 'string') { - component = this.getChild(component); - } - - if (!component || !this.children_) { - return; - } - - var childFound = false; - - for (var i = this.children_.length - 1; i >= 0; i--) { - if (this.children_[i] === component) { - childFound = true; - this.children_.splice(i, 1); - break; - } - } - - if (!childFound) { - return; - } - - this.childIndex_[component.id()] = null; - this.childNameIndex_[component.name()] = null; - - var compEl = component.el(); - - if (compEl && compEl.parentNode === this.contentEl()) { - this.contentEl().removeChild(component.el()); - } - }; - - /** - * Add and initialize default child `Component`s based upon options. - */ - - - Component.prototype.initChildren = function initChildren() { - var _this = this; - - var children = this.options_.children; - - if (children) { - // `this` is `parent` - var parentOptions = this.options_; - - var handleAdd = function handleAdd(child) { - var name = child.name; - var opts = child.opts; - - // Allow options for children to be set at the parent options - // e.g. videojs(id, { controlBar: false }); - // instead of videojs(id, { children: { controlBar: false }); - if (parentOptions[name] !== undefined) { - opts = parentOptions[name]; - } - - // Allow for disabling default components - // e.g. options['children']['posterImage'] = false - if (opts === false) { - return; - } - - // Allow options to be passed as a simple boolean if no configuration - // is necessary. - if (opts === true) { - opts = {}; - } - - // We also want to pass the original player options - // to each component as well so they don't need to - // reach back into the player for options later. - opts.playerOptions = _this.options_.playerOptions; - - // Create and add the child component. - // Add a direct reference to the child by name on the parent instance. - // If two of the same component are used, different names should be supplied - // for each - var newChild = _this.addChild(name, opts); - - if (newChild) { - _this[name] = newChild; - } - }; - - // Allow for an array of children details to passed in the options - var workingChildren = void 0; - var Tech = Component.getComponent('Tech'); - - if (Array.isArray(children)) { - workingChildren = children; - } else { - workingChildren = Object.keys(children); - } - - workingChildren - // children that are in this.options_ but also in workingChildren would - // give us extra children we do not want. So, we want to filter them out. - .concat(Object.keys(this.options_).filter(function (child) { - return !workingChildren.some(function (wchild) { - if (typeof wchild === 'string') { - return child === wchild; - } - return child === wchild.name; - }); - })).map(function (child) { - var name = void 0; - var opts = void 0; - - if (typeof child === 'string') { - name = child; - opts = children[name] || _this.options_[name] || {}; - } else { - name = child.name; - opts = child; - } - - return { name: name, opts: opts }; - }).filter(function (child) { - // we have to make sure that child.name isn't in the techOrder since - // techs are registerd as Components but can't aren't compatible - // See https://github.com/videojs/video.js/issues/2772 - var c = Component.getComponent(child.opts.componentClass || toTitleCase(child.name)); - - return c && !Tech.isTech(c); - }).forEach(handleAdd); - } - }; - - /** - * Builds the default DOM class name. Should be overriden by sub-components. - * - * @return {string} - * The DOM class name for this object. - * - * @abstract - */ - - - Component.prototype.buildCSSClass = function buildCSSClass() { - // Child classes can include a function that does: - // return 'CLASS NAME' + this._super(); - return ''; - }; - - /** - * Bind a listener to the component's ready state. - * Different from event listeners in that if the ready event has already happened - * it will trigger the function immediately. - * - * @return {Component} - * Returns itself; method can be chained. - */ - - - Component.prototype.ready = function ready(fn) { - var sync = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; - - if (!fn) { - return; - } - - if (!this.isReady_) { - this.readyQueue_ = this.readyQueue_ || []; - this.readyQueue_.push(fn); - return; - } - - if (sync) { - fn.call(this); - } else { - // Call the function asynchronously by default for consistency - this.setTimeout(fn, 1); - } - }; - - /** - * Trigger all the ready listeners for this `Component`. - * - * @fires Component#ready - */ - - - Component.prototype.triggerReady = function triggerReady() { - this.isReady_ = true; - - // Ensure ready is triggered asynchronously - this.setTimeout(function () { - var readyQueue = this.readyQueue_; - - // Reset Ready Queue - this.readyQueue_ = []; - - if (readyQueue && readyQueue.length > 0) { - readyQueue.forEach(function (fn) { - fn.call(this); - }, this); - } - - // Allow for using event listeners also - /** - * Triggered when a `Component` is ready. - * - * @event Component#ready - * @type {EventTarget~Event} - */ - this.trigger('ready'); - }, 1); - }; - - /** - * Find a single DOM element matching a `selector`. This can be within the `Component`s - * `contentEl()` or another custom context. - * - * @param {string} selector - * A valid CSS selector, which will be passed to `querySelector`. - * - * @param {Element|string} [context=this.contentEl()] - * A DOM element within which to query. Can also be a selector string in - * which case the first matching element will get used as context. If - * missing `this.contentEl()` gets used. If `this.contentEl()` returns - * nothing it falls back to `document`. - * - * @return {Element|null} - * the dom element that was found, or null - * - * @see [Information on CSS Selectors](https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Getting_Started/Selectors) - */ - - - Component.prototype.$ = function $$$1(selector, context) { - return $(selector, context || this.contentEl()); - }; - - /** - * Finds all DOM element matching a `selector`. This can be within the `Component`s - * `contentEl()` or another custom context. - * - * @param {string} selector - * A valid CSS selector, which will be passed to `querySelectorAll`. - * - * @param {Element|string} [context=this.contentEl()] - * A DOM element within which to query. Can also be a selector string in - * which case the first matching element will get used as context. If - * missing `this.contentEl()` gets used. If `this.contentEl()` returns - * nothing it falls back to `document`. - * - * @return {NodeList} - * a list of dom elements that were found - * - * @see [Information on CSS Selectors](https://developer.mozilla.org/en-US/docs/Web/Guide/CSS/Getting_Started/Selectors) - */ - - - Component.prototype.$$ = function $$$$1(selector, context) { - return $$(selector, context || this.contentEl()); - }; - - /** - * Check if a component's element has a CSS class name. - * - * @param {string} classToCheck - * CSS class name to check. - * - * @return {boolean} - * - True if the `Component` has the class. - * - False if the `Component` does not have the class` - */ - - - Component.prototype.hasClass = function hasClass$$1(classToCheck) { - return hasClass(this.el_, classToCheck); - }; - - /** - * Add a CSS class name to the `Component`s element. - * - * @param {string} classToAdd - * CSS class name to add - */ - - - Component.prototype.addClass = function addClass$$1(classToAdd) { - addClass(this.el_, classToAdd); - }; - - /** - * Remove a CSS class name from the `Component`s element. - * - * @param {string} classToRemove - * CSS class name to remove - */ - - - Component.prototype.removeClass = function removeClass$$1(classToRemove) { - removeClass(this.el_, classToRemove); - }; - - /** - * Add or remove a CSS class name from the component's element. - * - `classToToggle` gets added when {@link Component#hasClass} would return false. - * - `classToToggle` gets removed when {@link Component#hasClass} would return true. - * - * @param {string} classToToggle - * The class to add or remove based on (@link Component#hasClass} - * - * @param {boolean|Dom~predicate} [predicate] - * An {@link Dom~predicate} function or a boolean - */ - - - Component.prototype.toggleClass = function toggleClass$$1(classToToggle, predicate) { - toggleClass(this.el_, classToToggle, predicate); - }; - - /** - * Show the `Component`s element if it is hidden by removing the - * 'vjs-hidden' class name from it. - */ - - - Component.prototype.show = function show() { - this.removeClass('vjs-hidden'); - }; - - /** - * Hide the `Component`s element if it is currently showing by adding the - * 'vjs-hidden` class name to it. - */ - - - Component.prototype.hide = function hide() { - this.addClass('vjs-hidden'); - }; - - /** - * Lock a `Component`s element in its visible state by adding the 'vjs-lock-showing' - * class name to it. Used during fadeIn/fadeOut. - * - * @private - */ - - - Component.prototype.lockShowing = function lockShowing() { - this.addClass('vjs-lock-showing'); - }; - - /** - * Unlock a `Component`s element from its visible state by removing the 'vjs-lock-showing' - * class name from it. Used during fadeIn/fadeOut. - * - * @private - */ - - - Component.prototype.unlockShowing = function unlockShowing() { - this.removeClass('vjs-lock-showing'); - }; - - /** - * Get the value of an attribute on the `Component`s element. - * - * @param {string} attribute - * Name of the attribute to get the value from. - * - * @return {string|null} - * - The value of the attribute that was asked for. - * - Can be an empty string on some browsers if the attribute does not exist - * or has no value - * - Most browsers will return null if the attibute does not exist or has - * no value. - * - * @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/getAttribute} - */ - - - Component.prototype.getAttribute = function getAttribute$$1(attribute) { - return getAttribute(this.el_, attribute); - }; - - /** - * Set the value of an attribute on the `Component`'s element - * - * @param {string} attribute - * Name of the attribute to set. - * - * @param {string} value - * Value to set the attribute to. - * - * @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/setAttribute} - */ - - - Component.prototype.setAttribute = function setAttribute$$1(attribute, value) { - setAttribute(this.el_, attribute, value); - }; - - /** - * Remove an attribute from the `Component`s element. - * - * @param {string} attribute - * Name of the attribute to remove. - * - * @see [DOM API]{@link https://developer.mozilla.org/en-US/docs/Web/API/Element/removeAttribute} - */ - - - Component.prototype.removeAttribute = function removeAttribute$$1(attribute) { - removeAttribute(this.el_, attribute); - }; - - /** - * Get or set the width of the component based upon the CSS styles. - * See {@link Component#dimension} for more detailed information. - * - * @param {number|string} [num] - * The width that you want to set postfixed with '%', 'px' or nothing. - * - * @param {boolean} [skipListeners] - * Skip the componentresize event trigger - * - * @return {number|string} - * The width when getting, zero if there is no width. Can be a string - * postpixed with '%' or 'px'. - */ - - - Component.prototype.width = function width(num, skipListeners) { - return this.dimension('width', num, skipListeners); - }; - - /** - * Get or set the height of the component based upon the CSS styles. - * See {@link Component#dimension} for more detailed information. - * - * @param {number|string} [num] - * The height that you want to set postfixed with '%', 'px' or nothing. - * - * @param {boolean} [skipListeners] - * Skip the componentresize event trigger - * - * @return {number|string} - * The width when getting, zero if there is no width. Can be a string - * postpixed with '%' or 'px'. - */ - - - Component.prototype.height = function height(num, skipListeners) { - return this.dimension('height', num, skipListeners); - }; - - /** - * Set both the width and height of the `Component` element at the same time. - * - * @param {number|string} width - * Width to set the `Component`s element to. - * - * @param {number|string} height - * Height to set the `Component`s element to. - */ - - - Component.prototype.dimensions = function dimensions(width, height) { - // Skip componentresize listeners on width for optimization - this.width(width, true); - this.height(height); - }; - - /** - * Get or set width or height of the `Component` element. This is the shared code - * for the {@link Component#width} and {@link Component#height}. - * - * Things to know: - * - If the width or height in an number this will return the number postfixed with 'px'. - * - If the width/height is a percent this will return the percent postfixed with '%' - * - Hidden elements have a width of 0 with `window.getComputedStyle`. This function - * defaults to the `Component`s `style.width` and falls back to `window.getComputedStyle`. - * See [this]{@link http://www.foliotek.com/devblog/getting-the-width-of-a-hidden-element-with-jquery-using-width/} - * for more information - * - If you want the computed style of the component, use {@link Component#currentWidth} - * and {@link {Component#currentHeight} - * - * @fires Component#componentresize - * - * @param {string} widthOrHeight - 8 'width' or 'height' - * - * @param {number|string} [num] - 8 New dimension - * - * @param {boolean} [skipListeners] - * Skip componentresize event trigger - * - * @return {number} - * The dimension when getting or 0 if unset - */ - - - Component.prototype.dimension = function dimension(widthOrHeight, num, skipListeners) { - if (num !== undefined) { - // Set to zero if null or literally NaN (NaN !== NaN) - if (num === null || num !== num) { - num = 0; - } - - // Check if using css width/height (% or px) and adjust - if (('' + num).indexOf('%') !== -1 || ('' + num).indexOf('px') !== -1) { - this.el_.style[widthOrHeight] = num; - } else if (num === 'auto') { - this.el_.style[widthOrHeight] = ''; - } else { - this.el_.style[widthOrHeight] = num + 'px'; - } - - // skipListeners allows us to avoid triggering the resize event when setting both width and height - if (!skipListeners) { - /** - * Triggered when a component is resized. - * - * @event Component#componentresize - * @type {EventTarget~Event} - */ - this.trigger('componentresize'); - } - - return; - } - - // Not setting a value, so getting it - // Make sure element exists - if (!this.el_) { - return 0; - } - - // Get dimension value from style - var val = this.el_.style[widthOrHeight]; - var pxIndex = val.indexOf('px'); - - if (pxIndex !== -1) { - // Return the pixel value with no 'px' - return parseInt(val.slice(0, pxIndex), 10); - } - - // No px so using % or no style was set, so falling back to offsetWidth/height - // If component has display:none, offset will return 0 - // TODO: handle display:none and no dimension style using px - return parseInt(this.el_['offset' + toTitleCase(widthOrHeight)], 10); - }; - - /** - * Get the width or the height of the `Component` elements computed style. Uses - * `window.getComputedStyle`. - * - * @param {string} widthOrHeight - * A string containing 'width' or 'height'. Whichever one you want to get. - * - * @return {number} - * The dimension that gets asked for or 0 if nothing was set - * for that dimension. - */ - - - Component.prototype.currentDimension = function currentDimension(widthOrHeight) { - var computedWidthOrHeight = 0; - - if (widthOrHeight !== 'width' && widthOrHeight !== 'height') { - throw new Error('currentDimension only accepts width or height value'); - } - - if (typeof window_1.getComputedStyle === 'function') { - var computedStyle = window_1.getComputedStyle(this.el_); - - computedWidthOrHeight = computedStyle.getPropertyValue(widthOrHeight) || computedStyle[widthOrHeight]; - } - - // remove 'px' from variable and parse as integer - computedWidthOrHeight = parseFloat(computedWidthOrHeight); - - // if the computed value is still 0, it's possible that the browser is lying - // and we want to check the offset values. - // This code also runs on IE8 and wherever getComputedStyle doesn't exist. - if (computedWidthOrHeight === 0) { - var rule = 'offset' + toTitleCase(widthOrHeight); - - computedWidthOrHeight = this.el_[rule]; - } - - return computedWidthOrHeight; - }; - - /** - * An object that contains width and height values of the `Component`s - * computed style. Uses `window.getComputedStyle`. - * - * @typedef {Object} Component~DimensionObject - * - * @property {number} width - * The width of the `Component`s computed style. - * - * @property {number} height - * The height of the `Component`s computed style. - */ - - /** - * Get an object that contains width and height values of the `Component`s - * computed style. - * - * @return {Component~DimensionObject} - * The dimensions of the components element - */ - - - Component.prototype.currentDimensions = function currentDimensions() { - return { - width: this.currentDimension('width'), - height: this.currentDimension('height') - }; - }; - - /** - * Get the width of the `Component`s computed style. Uses `window.getComputedStyle`. - * - * @return {number} width - * The width of the `Component`s computed style. - */ - - - Component.prototype.currentWidth = function currentWidth() { - return this.currentDimension('width'); - }; - - /** - * Get the height of the `Component`s computed style. Uses `window.getComputedStyle`. - * - * @return {number} height - * The height of the `Component`s computed style. - */ - - - Component.prototype.currentHeight = function currentHeight() { - return this.currentDimension('height'); - }; - - /** - * Set the focus to this component - */ - - - Component.prototype.focus = function focus() { - this.el_.focus(); - }; - - /** - * Remove the focus from this component - */ - - - Component.prototype.blur = function blur() { - this.el_.blur(); - }; - - /** - * Emit a 'tap' events when touch event support gets detected. This gets used to - * support toggling the controls through a tap on the video. They get enabled - * because every sub-component would have extra overhead otherwise. - * - * @private - * @fires Component#tap - * @listens Component#touchstart - * @listens Component#touchmove - * @listens Component#touchleave - * @listens Component#touchcancel - * @listens Component#touchend - */ - - - Component.prototype.emitTapEvents = function emitTapEvents() { - // Track the start time so we can determine how long the touch lasted - var touchStart = 0; - var firstTouch = null; - - // Maximum movement allowed during a touch event to still be considered a tap - // Other popular libs use anywhere from 2 (hammer.js) to 15, - // so 10 seems like a nice, round number. - var tapMovementThreshold = 10; - - // The maximum length a touch can be while still being considered a tap - var touchTimeThreshold = 200; - - var couldBeTap = void 0; - - this.on('touchstart', function (event) { - // If more than one finger, don't consider treating this as a click - if (event.touches.length === 1) { - // Copy pageX/pageY from the object - firstTouch = { - pageX: event.touches[0].pageX, - pageY: event.touches[0].pageY - }; - // Record start time so we can detect a tap vs. "touch and hold" - touchStart = new Date().getTime(); - // Reset couldBeTap tracking - couldBeTap = true; - } - }); - - this.on('touchmove', function (event) { - // If more than one finger, don't consider treating this as a click - if (event.touches.length > 1) { - couldBeTap = false; - } else if (firstTouch) { - // Some devices will throw touchmoves for all but the slightest of taps. - // So, if we moved only a small distance, this could still be a tap - var xdiff = event.touches[0].pageX - firstTouch.pageX; - var ydiff = event.touches[0].pageY - firstTouch.pageY; - var touchDistance = Math.sqrt(xdiff * xdiff + ydiff * ydiff); - - if (touchDistance > tapMovementThreshold) { - couldBeTap = false; - } - } - }); - - var noTap = function noTap() { - couldBeTap = false; - }; - - // TODO: Listen to the original target. http://youtu.be/DujfpXOKUp8?t=13m8s - this.on('touchleave', noTap); - this.on('touchcancel', noTap); - - // When the touch ends, measure how long it took and trigger the appropriate - // event - this.on('touchend', function (event) { - firstTouch = null; - // Proceed only if the touchmove/leave/cancel event didn't happen - if (couldBeTap === true) { - // Measure how long the touch lasted - var touchTime = new Date().getTime() - touchStart; - - // Make sure the touch was less than the threshold to be considered a tap - if (touchTime < touchTimeThreshold) { - // Don't let browser turn this into a click - event.preventDefault(); - /** - * Triggered when a `Component` is tapped. - * - * @event Component#tap - * @type {EventTarget~Event} - */ - this.trigger('tap'); - // It may be good to copy the touchend event object and change the - // type to tap, if the other event properties aren't exact after - // Events.fixEvent runs (e.g. event.target) - } - } - }); - }; - - /** - * This function reports user activity whenever touch events happen. This can get - * turned off by any sub-components that wants touch events to act another way. - * - * Report user touch activity when touch events occur. User activity gets used to - * determine when controls should show/hide. It is simple when it comes to mouse - * events, because any mouse event should show the controls. So we capture mouse - * events that bubble up to the player and report activity when that happens. - * With touch events it isn't as easy as `touchstart` and `touchend` toggle player - * controls. So touch events can't help us at the player level either. - * - * User activity gets checked asynchronously. So what could happen is a tap event - * on the video turns the controls off. Then the `touchend` event bubbles up to - * the player. Which, if it reported user activity, would turn the controls right - * back on. We also don't want to completely block touch events from bubbling up. - * Furthermore a `touchmove` event and anything other than a tap, should not turn - * controls back on. - * - * @listens Component#touchstart - * @listens Component#touchmove - * @listens Component#touchend - * @listens Component#touchcancel - */ - - - Component.prototype.enableTouchActivity = function enableTouchActivity() { - // Don't continue if the root player doesn't support reporting user activity - if (!this.player() || !this.player().reportUserActivity) { - return; - } - - // listener for reporting that the user is active - var report = bind(this.player(), this.player().reportUserActivity); - - var touchHolding = void 0; - - this.on('touchstart', function () { - report(); - // For as long as the they are touching the device or have their mouse down, - // we consider them active even if they're not moving their finger or mouse. - // So we want to continue to update that they are active - this.clearInterval(touchHolding); - // report at the same interval as activityCheck - touchHolding = this.setInterval(report, 250); - }); - - var touchEnd = function touchEnd(event) { - report(); - // stop the interval that maintains activity if the touch is holding - this.clearInterval(touchHolding); - }; - - this.on('touchmove', report); - this.on('touchend', touchEnd); - this.on('touchcancel', touchEnd); - }; - - /** - * A callback that has no parameters and is bound into `Component`s context. - * - * @callback Component~GenericCallback - * @this Component - */ - - /** - * Creates a function that runs after an `x` millisecond timeout. This function is a - * wrapper around `window.setTimeout`. There are a few reasons to use this one - * instead though: - * 1. It gets cleared via {@link Component#clearTimeout} when - * {@link Component#dispose} gets called. - * 2. The function callback will gets turned into a {@link Component~GenericCallback} - * - * > Note: You can use `window.clearTimeout` on the id returned by this function. This - * will cause its dispose listener not to get cleaned up! Please use - * {@link Component#clearTimeout} or {@link Component#dispose}. - * - * @param {Component~GenericCallback} fn - * The function that will be run after `timeout`. - * - * @param {number} timeout - * Timeout in milliseconds to delay before executing the specified function. - * - * @return {number} - * Returns a timeout ID that gets used to identify the timeout. It can also - * get used in {@link Component#clearTimeout} to clear the timeout that - * was set. - * - * @listens Component#dispose - * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setTimeout} - */ - - - Component.prototype.setTimeout = function setTimeout(fn, timeout) { - var _this2 = this; - - fn = bind(this, fn); - - var timeoutId = window_1.setTimeout(fn, timeout); - var disposeFn = function disposeFn() { - return _this2.clearTimeout(timeoutId); - }; - - disposeFn.guid = 'vjs-timeout-' + timeoutId; - - this.on('dispose', disposeFn); - - return timeoutId; - }; - - /** - * Clears a timeout that gets created via `window.setTimeout` or - * {@link Component#setTimeout}. If you set a timeout via {@link Component#setTimeout} - * use this function instead of `window.clearTimout`. If you don't your dispose - * listener will not get cleaned up until {@link Component#dispose}! - * - * @param {number} timeoutId - * The id of the timeout to clear. The return value of - * {@link Component#setTimeout} or `window.setTimeout`. - * - * @return {number} - * Returns the timeout id that was cleared. - * - * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/clearTimeout} - */ - - - Component.prototype.clearTimeout = function clearTimeout(timeoutId) { - window_1.clearTimeout(timeoutId); - - var disposeFn = function disposeFn() {}; - - disposeFn.guid = 'vjs-timeout-' + timeoutId; - - this.off('dispose', disposeFn); - - return timeoutId; - }; - - /** - * Creates a function that gets run every `x` milliseconds. This function is a wrapper - * around `window.setInterval`. There are a few reasons to use this one instead though. - * 1. It gets cleared via {@link Component#clearInterval} when - * {@link Component#dispose} gets called. - * 2. The function callback will be a {@link Component~GenericCallback} - * - * @param {Component~GenericCallback} fn - * The function to run every `x` seconds. - * - * @param {number} interval - * Execute the specified function every `x` milliseconds. - * - * @return {number} - * Returns an id that can be used to identify the interval. It can also be be used in - * {@link Component#clearInterval} to clear the interval. - * - * @listens Component#dispose - * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/setInterval} - */ - - - Component.prototype.setInterval = function setInterval(fn, interval) { - var _this3 = this; - - fn = bind(this, fn); - - var intervalId = window_1.setInterval(fn, interval); - - var disposeFn = function disposeFn() { - return _this3.clearInterval(intervalId); - }; - - disposeFn.guid = 'vjs-interval-' + intervalId; - - this.on('dispose', disposeFn); - - return intervalId; - }; - - /** - * Clears an interval that gets created via `window.setInterval` or - * {@link Component#setInterval}. If you set an inteval via {@link Component#setInterval} - * use this function instead of `window.clearInterval`. If you don't your dispose - * listener will not get cleaned up until {@link Component#dispose}! - * - * @param {number} intervalId - * The id of the interval to clear. The return value of - * {@link Component#setInterval} or `window.setInterval`. - * - * @return {number} - * Returns the interval id that was cleared. - * - * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowTimers/clearInterval} - */ - - - Component.prototype.clearInterval = function clearInterval(intervalId) { - window_1.clearInterval(intervalId); - - var disposeFn = function disposeFn() {}; - - disposeFn.guid = 'vjs-interval-' + intervalId; - - this.off('dispose', disposeFn); - - return intervalId; - }; - - /** - * Queues up a callback to be passed to requestAnimationFrame (rAF), but - * with a few extra bonuses: - * - * - Supports browsers that do not support rAF by falling back to - * {@link Component#setTimeout}. - * - * - The callback is turned into a {@link Component~GenericCallback} (i.e. - * bound to the component). - * - * - Automatic cancellation of the rAF callback is handled if the component - * is disposed before it is called. - * - * @param {Component~GenericCallback} fn - * A function that will be bound to this component and executed just - * before the browser's next repaint. - * - * @return {number} - * Returns an rAF ID that gets used to identify the timeout. It can - * also be used in {@link Component#cancelAnimationFrame} to cancel - * the animation frame callback. - * - * @listens Component#dispose - * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame} - */ - - - Component.prototype.requestAnimationFrame = function requestAnimationFrame(fn) { - var _this4 = this; - - if (this.supportsRaf_) { - fn = bind(this, fn); - - var id = window_1.requestAnimationFrame(fn); - var disposeFn = function disposeFn() { - return _this4.cancelAnimationFrame(id); - }; - - disposeFn.guid = 'vjs-raf-' + id; - this.on('dispose', disposeFn); - - return id; - } - - // Fall back to using a timer. - return this.setTimeout(fn, 1000 / 60); - }; - - /** - * Cancels a queued callback passed to {@link Component#requestAnimationFrame} - * (rAF). - * - * If you queue an rAF callback via {@link Component#requestAnimationFrame}, - * use this function instead of `window.cancelAnimationFrame`. If you don't, - * your dispose listener will not get cleaned up until {@link Component#dispose}! - * - * @param {number} id - * The rAF ID to clear. The return value of {@link Component#requestAnimationFrame}. - * - * @return {number} - * Returns the rAF ID that was cleared. - * - * @see [Similar to]{@link https://developer.mozilla.org/en-US/docs/Web/API/window/cancelAnimationFrame} - */ - - - Component.prototype.cancelAnimationFrame = function cancelAnimationFrame(id) { - if (this.supportsRaf_) { - window_1.cancelAnimationFrame(id); - - var disposeFn = function disposeFn() {}; - - disposeFn.guid = 'vjs-raf-' + id; - - this.off('dispose', disposeFn); - - return id; - } - - // Fall back to using a timer. - return this.clearTimeout(id); - }; - - /** - * Register a `Component` with `videojs` given the name and the component. - * - * > NOTE: {@link Tech}s should not be registered as a `Component`. {@link Tech}s - * should be registered using {@link Tech.registerTech} or - * {@link videojs:videojs.registerTech}. - * - * > NOTE: This function can also be seen on videojs as - * {@link videojs:videojs.registerComponent}. - * - * @param {string} name - * The name of the `Component` to register. - * - * @param {Component} ComponentToRegister - * The `Component` class to register. - * - * @return {Component} - * The `Component` that was registered. - */ - - - Component.registerComponent = function registerComponent(name, ComponentToRegister) { - if (typeof name !== 'string' || !name) { - throw new Error('Illegal component name, "' + name + '"; must be a non-empty string.'); - } - - var Tech = Component.getComponent('Tech'); - - // We need to make sure this check is only done if Tech has been registered. - var isTech = Tech && Tech.isTech(ComponentToRegister); - var isComp = Component === ComponentToRegister || Component.prototype.isPrototypeOf(ComponentToRegister.prototype); - - if (isTech || !isComp) { - var reason = void 0; - - if (isTech) { - reason = 'techs must be registered using Tech.registerTech()'; - } else { - reason = 'must be a Component subclass'; - } - - throw new Error('Illegal component, "' + name + '"; ' + reason + '.'); - } - - name = toTitleCase(name); - - if (!Component.components_) { - Component.components_ = {}; - } - - var Player = Component.getComponent('Player'); - - if (name === 'Player' && Player && Player.players) { - var players = Player.players; - var playerNames = Object.keys(players); - - // If we have players that were disposed, then their name will still be - // in Players.players. So, we must loop through and verify that the value - // for each item is not null. This allows registration of the Player component - // after all players have been disposed or before any were created. - if (players && playerNames.length > 0 && playerNames.map(function (pname) { - return players[pname]; - }).every(Boolean)) { - throw new Error('Can not register Player component after player has been created.'); - } - } - - Component.components_[name] = ComponentToRegister; - - return ComponentToRegister; - }; - - /** - * Get a `Component` based on the name it was registered with. - * - * @param {string} name - * The Name of the component to get. - * - * @return {Component} - * The `Component` that got registered under the given name. - * - * @deprecated In `videojs` 6 this will not return `Component`s that were not - * registered using {@link Component.registerComponent}. Currently we - * check the global `videojs` object for a `Component` name and - * return that if it exists. - */ - - - Component.getComponent = function getComponent(name) { - if (!name) { - return; - } - - name = toTitleCase(name); - - if (Component.components_ && Component.components_[name]) { - return Component.components_[name]; - } - }; - - return Component; -}(); - -/** - * Whether or not this component supports `requestAnimationFrame`. - * - * This is exposed primarily for testing purposes. - * - * @private - * @type {Boolean} - */ - - -Component.prototype.supportsRaf_ = typeof window_1.requestAnimationFrame === 'function' && typeof window_1.cancelAnimationFrame === 'function'; - -Component.registerComponent('Component', Component); - -/** - * @file time-ranges.js - * @module time-ranges - */ - -/** - * Returns the time for the specified index at the start or end - * of a TimeRange object. - * - * @function time-ranges:indexFunction - * - * @param {number} [index=0] - * The range number to return the time for. - * - * @return {number} - * The time that offset at the specified index. - * - * @depricated index must be set to a value, in the future this will throw an error. - */ - -/** - * An object that contains ranges of time for various reasons. - * - * @typedef {Object} TimeRange - * - * @property {number} length - * The number of time ranges represented by this Object - * - * @property {time-ranges:indexFunction} start - * Returns the time offset at which a specified time range begins. - * - * @property {time-ranges:indexFunction} end - * Returns the time offset at which a specified time range ends. - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/TimeRanges - */ - -/** - * Check if any of the time ranges are over the maximum index. - * - * @param {string} fnName - * The function name to use for logging - * - * @param {number} index - * The index to check - * - * @param {number} maxIndex - * The maximum possible index - * - * @throws {Error} if the timeRanges provided are over the maxIndex - */ -function rangeCheck(fnName, index, maxIndex) { - if (typeof index !== 'number' || index < 0 || index > maxIndex) { - throw new Error('Failed to execute \'' + fnName + '\' on \'TimeRanges\': The index provided (' + index + ') is non-numeric or out of bounds (0-' + maxIndex + ').'); - } -} - -/** - * Get the time for the specified index at the start or end - * of a TimeRange object. - * - * @param {string} fnName - * The function name to use for logging - * - * @param {string} valueIndex - * The proprety that should be used to get the time. should be 'start' or 'end' - * - * @param {Array} ranges - * An array of time ranges - * - * @param {Array} [rangeIndex=0] - * The index to start the search at - * - * @return {number} - * The time that offset at the specified index. - * - * - * @depricated rangeIndex must be set to a value, in the future this will throw an error. - * @throws {Error} if rangeIndex is more than the length of ranges - */ -function getRange(fnName, valueIndex, ranges, rangeIndex) { - rangeCheck(fnName, rangeIndex, ranges.length - 1); - return ranges[rangeIndex][valueIndex]; -} - -/** - * Create a time range object given ranges of time. - * - * @param {Array} [ranges] - * An array of time ranges. - */ -function createTimeRangesObj(ranges) { - if (ranges === undefined || ranges.length === 0) { - return { - length: 0, - start: function start() { - throw new Error('This TimeRanges object is empty'); - }, - end: function end() { - throw new Error('This TimeRanges object is empty'); - } - }; - } - return { - length: ranges.length, - start: getRange.bind(null, 'start', 0, ranges), - end: getRange.bind(null, 'end', 1, ranges) - }; -} - -/** - * Should create a fake `TimeRange` object which mimics an HTML5 time range instance. - * - * @param {number|Array} start - * The start of a single range or an array of ranges - * - * @param {number} end - * The end of a single range. - * - * @private - */ -function createTimeRanges(start, end) { - if (Array.isArray(start)) { - return createTimeRangesObj(start); - } else if (start === undefined || end === undefined) { - return createTimeRangesObj(); - } - return createTimeRangesObj([[start, end]]); -} - -/** - * @file buffer.js - * @module buffer - */ -/** - * Compute the percentage of the media that has been buffered. - * - * @param {TimeRange} buffered - * The current `TimeRange` object representing buffered time ranges - * - * @param {number} duration - * Total duration of the media - * - * @return {number} - * Percent buffered of the total duration in decimal form. - */ -function bufferedPercent(buffered, duration) { - var bufferedDuration = 0; - var start = void 0; - var end = void 0; - - if (!duration) { - return 0; - } - - if (!buffered || !buffered.length) { - buffered = createTimeRanges(0, 0); - } - - for (var i = 0; i < buffered.length; i++) { - start = buffered.start(i); - end = buffered.end(i); - - // buffered end can be bigger than duration by a very small fraction - if (end > duration) { - end = duration; - } - - bufferedDuration += end - start; - } - - return bufferedDuration / duration; -} - -/** - * @file fullscreen-api.js - * @module fullscreen-api - * @private - */ -/** - * Store the browser-specific methods for the fullscreen API. - * - * @type {Object} - * @see [Specification]{@link https://fullscreen.spec.whatwg.org} - * @see [Map Approach From Screenfull.js]{@link https://github.com/sindresorhus/screenfull.js} - */ -var FullscreenApi = {}; - -// browser API methods -var apiMap = [['requestFullscreen', 'exitFullscreen', 'fullscreenElement', 'fullscreenEnabled', 'fullscreenchange', 'fullscreenerror'], -// WebKit -['webkitRequestFullscreen', 'webkitExitFullscreen', 'webkitFullscreenElement', 'webkitFullscreenEnabled', 'webkitfullscreenchange', 'webkitfullscreenerror'], -// Old WebKit (Safari 5.1) -['webkitRequestFullScreen', 'webkitCancelFullScreen', 'webkitCurrentFullScreenElement', 'webkitCancelFullScreen', 'webkitfullscreenchange', 'webkitfullscreenerror'], -// Mozilla -['mozRequestFullScreen', 'mozCancelFullScreen', 'mozFullScreenElement', 'mozFullScreenEnabled', 'mozfullscreenchange', 'mozfullscreenerror'], -// Microsoft -['msRequestFullscreen', 'msExitFullscreen', 'msFullscreenElement', 'msFullscreenEnabled', 'MSFullscreenChange', 'MSFullscreenError']]; - -var specApi = apiMap[0]; -var browserApi = void 0; - -// determine the supported set of functions -for (var i = 0; i < apiMap.length; i++) { - // check for exitFullscreen function - if (apiMap[i][1] in document_1) { - browserApi = apiMap[i]; - break; - } -} - -// map the browser API names to the spec API names -if (browserApi) { - for (var _i = 0; _i < browserApi.length; _i++) { - FullscreenApi[specApi[_i]] = browserApi[_i]; - } -} - -/** - * @file media-error.js - */ -/** - * A Custom `MediaError` class which mimics the standard HTML5 `MediaError` class. - * - * @param {number|string|Object|MediaError} value - * This can be of multiple types: - * - number: should be a standard error code - * - string: an error message (the code will be 0) - * - Object: arbitrary properties - * - `MediaError` (native): used to populate a video.js `MediaError` object - * - `MediaError` (video.js): will return itself if it's already a - * video.js `MediaError` object. - * - * @see [MediaError Spec]{@link https://dev.w3.org/html5/spec-author-view/video.html#mediaerror} - * @see [Encrypted MediaError Spec]{@link https://www.w3.org/TR/2013/WD-encrypted-media-20130510/#error-codes} - * - * @class MediaError - */ -function MediaError(value) { - - // Allow redundant calls to this constructor to avoid having `instanceof` - // checks peppered around the code. - if (value instanceof MediaError) { - return value; - } - - if (typeof value === 'number') { - this.code = value; - } else if (typeof value === 'string') { - // default code is zero, so this is a custom error - this.message = value; - } else if (isObject(value)) { - - // We assign the `code` property manually because native `MediaError` objects - // do not expose it as an own/enumerable property of the object. - if (typeof value.code === 'number') { - this.code = value.code; - } - - assign(this, value); - } - - if (!this.message) { - this.message = MediaError.defaultMessages[this.code] || ''; - } -} - -/** - * The error code that refers two one of the defined `MediaError` types - * - * @type {Number} - */ -MediaError.prototype.code = 0; - -/** - * An optional message that to show with the error. Message is not part of the HTML5 - * video spec but allows for more informative custom errors. - * - * @type {String} - */ -MediaError.prototype.message = ''; - -/** - * An optional status code that can be set by plugins to allow even more detail about - * the error. For example a plugin might provide a specific HTTP status code and an - * error message for that code. Then when the plugin gets that error this class will - * know how to display an error message for it. This allows a custom message to show - * up on the `Player` error overlay. - * - * @type {Array} - */ -MediaError.prototype.status = null; - -/** - * Errors indexed by the W3C standard. The order **CANNOT CHANGE**! See the - * specification listed under {@link MediaError} for more information. - * - * @enum {array} - * @readonly - * @property {string} 0 - MEDIA_ERR_CUSTOM - * @property {string} 1 - MEDIA_ERR_CUSTOM - * @property {string} 2 - MEDIA_ERR_ABORTED - * @property {string} 3 - MEDIA_ERR_NETWORK - * @property {string} 4 - MEDIA_ERR_SRC_NOT_SUPPORTED - * @property {string} 5 - MEDIA_ERR_ENCRYPTED - */ -MediaError.errorTypes = ['MEDIA_ERR_CUSTOM', 'MEDIA_ERR_ABORTED', 'MEDIA_ERR_NETWORK', 'MEDIA_ERR_DECODE', 'MEDIA_ERR_SRC_NOT_SUPPORTED', 'MEDIA_ERR_ENCRYPTED']; - -/** - * The default `MediaError` messages based on the {@link MediaError.errorTypes}. - * - * @type {Array} - * @constant - */ -MediaError.defaultMessages = { - 1: 'You aborted the media playback', - 2: 'A network error caused the media download to fail part-way.', - 3: 'The media playback was aborted due to a corruption problem or because the media used features your browser did not support.', - 4: 'The media could not be loaded, either because the server or network failed or because the format is not supported.', - 5: 'The media is encrypted and we do not have the keys to decrypt it.' -}; - -// Add types as properties on MediaError -// e.g. MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED = 4; -for (var errNum = 0; errNum < MediaError.errorTypes.length; errNum++) { - MediaError[MediaError.errorTypes[errNum]] = errNum; - // values should be accessible on both the class and instance - MediaError.prototype[MediaError.errorTypes[errNum]] = errNum; -} - -var tuple = SafeParseTuple; - -function SafeParseTuple(obj, reviver) { - var json; - var error = null; - - try { - json = JSON.parse(obj, reviver); - } catch (err) { - error = err; - } - - return [error, json] -} - -/** - * Returns whether an object is `Promise`-like (i.e. has a `then` method). - * - * @param {Object} value - * An object that may or may not be `Promise`-like. - * - * @return {Boolean} - * Whether or not the object is `Promise`-like. - */ -function isPromise(value) { - return value !== undefined && value !== null && typeof value.then === 'function'; -} - -/** - * Silence a Promise-like object. - * - * This is useful for avoiding non-harmful, but potentially confusing "uncaught - * play promise" rejection error messages. - * - * @param {Object} value - * An object that may or may not be `Promise`-like. - */ -function silencePromise(value) { - if (isPromise(value)) { - value.then(null, function (e) {}); - } -} - -/** - * @file text-track-list-converter.js Utilities for capturing text track state and - * re-creating tracks based on a capture. - * - * @module text-track-list-converter - */ - -/** - * Examine a single {@link TextTrack} and return a JSON-compatible javascript object that - * represents the {@link TextTrack}'s state. - * - * @param {TextTrack} track - * The text track to query. - * - * @return {Object} - * A serializable javascript representation of the TextTrack. - * @private - */ -var trackToJson_ = function trackToJson_(track) { - var ret = ['kind', 'label', 'language', 'id', 'inBandMetadataTrackDispatchType', 'mode', 'src'].reduce(function (acc, prop, i) { - - if (track[prop]) { - acc[prop] = track[prop]; - } - - return acc; - }, { - cues: track.cues && Array.prototype.map.call(track.cues, function (cue) { - return { - startTime: cue.startTime, - endTime: cue.endTime, - text: cue.text, - id: cue.id - }; - }) - }); - - return ret; -}; - -/** - * Examine a {@link Tech} and return a JSON-compatible javascript array that represents the - * state of all {@link TextTrack}s currently configured. The return array is compatible with - * {@link text-track-list-converter:jsonToTextTracks}. - * - * @param {Tech} tech - * The tech object to query - * - * @return {Array} - * A serializable javascript representation of the {@link Tech}s - * {@link TextTrackList}. - */ -var textTracksToJson = function textTracksToJson(tech) { - - var trackEls = tech.$$('track'); - - var trackObjs = Array.prototype.map.call(trackEls, function (t) { - return t.track; - }); - var tracks = Array.prototype.map.call(trackEls, function (trackEl) { - var json = trackToJson_(trackEl.track); - - if (trackEl.src) { - json.src = trackEl.src; - } - return json; - }); - - return tracks.concat(Array.prototype.filter.call(tech.textTracks(), function (track) { - return trackObjs.indexOf(track) === -1; - }).map(trackToJson_)); -}; - -/** - * Create a set of remote {@link TextTrack}s on a {@link Tech} based on an array of javascript - * object {@link TextTrack} representations. - * - * @param {Array} json - * An array of `TextTrack` representation objects, like those that would be - * produced by `textTracksToJson`. - * - * @param {Tech} tech - * The `Tech` to create the `TextTrack`s on. - */ -var jsonToTextTracks = function jsonToTextTracks(json, tech) { - json.forEach(function (track) { - var addedTrack = tech.addRemoteTextTrack(track).track; - - if (!track.src && track.cues) { - track.cues.forEach(function (cue) { - return addedTrack.addCue(cue); - }); - } - }); - - return tech.textTracks(); -}; - -var textTrackConverter = { textTracksToJson: textTracksToJson, jsonToTextTracks: jsonToTextTracks, trackToJson_: trackToJson_ }; - -/** - * @file modal-dialog.js - */ -var MODAL_CLASS_NAME = 'vjs-modal-dialog'; -var ESC = 27; - -/** - * The `ModalDialog` displays over the video and its controls, which blocks - * interaction with the player until it is closed. - * - * Modal dialogs include a "Close" button and will close when that button - * is activated - or when ESC is pressed anywhere. - * - * @extends Component - */ - -var ModalDialog = function (_Component) { - inherits(ModalDialog, _Component); - - /** - * Create an instance of this class. - * - * @param {Player} player - * The `Player` that this class should be attached to. - * - * @param {Object} [options] - * The key/value store of player options. - * - * @param {Mixed} [options.content=undefined] - * Provide customized content for this modal. - * - * @param {string} [options.description] - * A text description for the modal, primarily for accessibility. - * - * @param {boolean} [options.fillAlways=false] - * Normally, modals are automatically filled only the first time - * they open. This tells the modal to refresh its content - * every time it opens. - * - * @param {string} [options.label] - * A text label for the modal, primarily for accessibility. - * - * @param {boolean} [options.temporary=true] - * If `true`, the modal can only be opened once; it will be - * disposed as soon as it's closed. - * - * @param {boolean} [options.uncloseable=false] - * If `true`, the user will not be able to close the modal - * through the UI in the normal ways. Programmatic closing is - * still possible. - */ - function ModalDialog(player, options) { - classCallCheck(this, ModalDialog); - - var _this = possibleConstructorReturn(this, _Component.call(this, player, options)); - - _this.opened_ = _this.hasBeenOpened_ = _this.hasBeenFilled_ = false; - - _this.closeable(!_this.options_.uncloseable); - _this.content(_this.options_.content); - - // Make sure the contentEl is defined AFTER any children are initialized - // because we only want the contents of the modal in the contentEl - // (not the UI elements like the close button). - _this.contentEl_ = createEl('div', { - className: MODAL_CLASS_NAME + '-content' - }, { - role: 'document' - }); - - _this.descEl_ = createEl('p', { - className: MODAL_CLASS_NAME + '-description vjs-control-text', - id: _this.el().getAttribute('aria-describedby') - }); - - textContent(_this.descEl_, _this.description()); - _this.el_.appendChild(_this.descEl_); - _this.el_.appendChild(_this.contentEl_); - return _this; - } - - /** - * Create the `ModalDialog`'s DOM element - * - * @return {Element} - * The DOM element that gets created. - */ - - - ModalDialog.prototype.createEl = function createEl$$1() { - return _Component.prototype.createEl.call(this, 'div', { - className: this.buildCSSClass(), - tabIndex: -1 - }, { - 'aria-describedby': this.id() + '_description', - 'aria-hidden': 'true', - 'aria-label': this.label(), - 'role': 'dialog' - }); - }; - - ModalDialog.prototype.dispose = function dispose() { - this.contentEl_ = null; - this.descEl_ = null; - this.previouslyActiveEl_ = null; - - _Component.prototype.dispose.call(this); - }; - - /** - * Builds the default DOM `className`. - * - * @return {string} - * The DOM `className` for this object. - */ - - - ModalDialog.prototype.buildCSSClass = function buildCSSClass() { - return MODAL_CLASS_NAME + ' vjs-hidden ' + _Component.prototype.buildCSSClass.call(this); - }; - - /** - * Handles `keydown` events on the document, looking for ESC, which closes - * the modal. - * - * @param {EventTarget~Event} e - * The keypress that triggered this event. - * - * @listens keydown - */ - - - ModalDialog.prototype.handleKeyPress = function handleKeyPress(e) { - if (e.which === ESC && this.closeable()) { - this.close(); - } - }; - - /** - * Returns the label string for this modal. Primarily used for accessibility. - * - * @return {string} - * the localized or raw label of this modal. - */ - - - ModalDialog.prototype.label = function label() { - return this.localize(this.options_.label || 'Modal Window'); - }; - - /** - * Returns the description string for this modal. Primarily used for - * accessibility. - * - * @return {string} - * The localized or raw description of this modal. - */ - - - ModalDialog.prototype.description = function description() { - var desc = this.options_.description || this.localize('This is a modal window.'); - - // Append a universal closeability message if the modal is closeable. - if (this.closeable()) { - desc += ' ' + this.localize('This modal can be closed by pressing the Escape key or activating the close button.'); - } - - return desc; - }; - - /** - * Opens the modal. - * - * @fires ModalDialog#beforemodalopen - * @fires ModalDialog#modalopen - */ - - - ModalDialog.prototype.open = function open() { - if (!this.opened_) { - var player = this.player(); - - /** - * Fired just before a `ModalDialog` is opened. - * - * @event ModalDialog#beforemodalopen - * @type {EventTarget~Event} - */ - this.trigger('beforemodalopen'); - this.opened_ = true; - - // Fill content if the modal has never opened before and - // never been filled. - if (this.options_.fillAlways || !this.hasBeenOpened_ && !this.hasBeenFilled_) { - this.fill(); - } - - // If the player was playing, pause it and take note of its previously - // playing state. - this.wasPlaying_ = !player.paused(); - - if (this.options_.pauseOnOpen && this.wasPlaying_) { - player.pause(); - } - - if (this.closeable()) { - this.on(this.el_.ownerDocument, 'keydown', bind(this, this.handleKeyPress)); - } - - // Hide controls and note if they were enabled. - this.hadControls_ = player.controls(); - player.controls(false); - - this.show(); - this.conditionalFocus_(); - this.el().setAttribute('aria-hidden', 'false'); - - /** - * Fired just after a `ModalDialog` is opened. - * - * @event ModalDialog#modalopen - * @type {EventTarget~Event} - */ - this.trigger('modalopen'); - this.hasBeenOpened_ = true; - } - }; - - /** - * If the `ModalDialog` is currently open or closed. - * - * @param {boolean} [value] - * If given, it will open (`true`) or close (`false`) the modal. - * - * @return {boolean} - * the current open state of the modaldialog - */ - - - ModalDialog.prototype.opened = function opened(value) { - if (typeof value === 'boolean') { - this[value ? 'open' : 'close'](); - } - return this.opened_; - }; - - /** - * Closes the modal, does nothing if the `ModalDialog` is - * not open. - * - * @fires ModalDialog#beforemodalclose - * @fires ModalDialog#modalclose - */ - - - ModalDialog.prototype.close = function close() { - if (!this.opened_) { - return; - } - var player = this.player(); - - /** - * Fired just before a `ModalDialog` is closed. - * - * @event ModalDialog#beforemodalclose - * @type {EventTarget~Event} - */ - this.trigger('beforemodalclose'); - this.opened_ = false; - - if (this.wasPlaying_ && this.options_.pauseOnOpen) { - player.play(); - } - - if (this.closeable()) { - this.off(this.el_.ownerDocument, 'keydown', bind(this, this.handleKeyPress)); - } - - if (this.hadControls_) { - player.controls(true); - } - - this.hide(); - this.el().setAttribute('aria-hidden', 'true'); - - /** - * Fired just after a `ModalDialog` is closed. - * - * @event ModalDialog#modalclose - * @type {EventTarget~Event} - */ - this.trigger('modalclose'); - this.conditionalBlur_(); - - if (this.options_.temporary) { - this.dispose(); - } - }; - - /** - * Check to see if the `ModalDialog` is closeable via the UI. - * - * @param {boolean} [value] - * If given as a boolean, it will set the `closeable` option. - * - * @return {boolean} - * Returns the final value of the closable option. - */ - - - ModalDialog.prototype.closeable = function closeable(value) { - if (typeof value === 'boolean') { - var closeable = this.closeable_ = !!value; - var close = this.getChild('closeButton'); - - // If this is being made closeable and has no close button, add one. - if (closeable && !close) { - - // The close button should be a child of the modal - not its - // content element, so temporarily change the content element. - var temp = this.contentEl_; - - this.contentEl_ = this.el_; - close = this.addChild('closeButton', { controlText: 'Close Modal Dialog' }); - this.contentEl_ = temp; - this.on(close, 'close', this.close); - } - - // If this is being made uncloseable and has a close button, remove it. - if (!closeable && close) { - this.off(close, 'close', this.close); - this.removeChild(close); - close.dispose(); - } - } - return this.closeable_; - }; - - /** - * Fill the modal's content element with the modal's "content" option. - * The content element will be emptied before this change takes place. - */ - - - ModalDialog.prototype.fill = function fill() { - this.fillWith(this.content()); - }; - - /** - * Fill the modal's content element with arbitrary content. - * The content element will be emptied before this change takes place. - * - * @fires ModalDialog#beforemodalfill - * @fires ModalDialog#modalfill - * - * @param {Mixed} [content] - * The same rules apply to this as apply to the `content` option. - */ - - - ModalDialog.prototype.fillWith = function fillWith(content) { - var contentEl = this.contentEl(); - var parentEl = contentEl.parentNode; - var nextSiblingEl = contentEl.nextSibling; - - /** - * Fired just before a `ModalDialog` is filled with content. - * - * @event ModalDialog#beforemodalfill - * @type {EventTarget~Event} - */ - this.trigger('beforemodalfill'); - this.hasBeenFilled_ = true; - - // Detach the content element from the DOM before performing - // manipulation to avoid modifying the live DOM multiple times. - parentEl.removeChild(contentEl); - this.empty(); - insertContent(contentEl, content); - /** - * Fired just after a `ModalDialog` is filled with content. - * - * @event ModalDialog#modalfill - * @type {EventTarget~Event} - */ - this.trigger('modalfill'); - - // Re-inject the re-filled content element. - if (nextSiblingEl) { - parentEl.insertBefore(contentEl, nextSiblingEl); - } else { - parentEl.appendChild(contentEl); - } - - // make sure that the close button is last in the dialog DOM - var closeButton = this.getChild('closeButton'); - - if (closeButton) { - parentEl.appendChild(closeButton.el_); - } - }; - - /** - * Empties the content element. This happens anytime the modal is filled. - * - * @fires ModalDialog#beforemodalempty - * @fires ModalDialog#modalempty - */ - - - ModalDialog.prototype.empty = function empty() { - /** - * Fired just before a `ModalDialog` is emptied. - * - * @event ModalDialog#beforemodalempty - * @type {EventTarget~Event} - */ - this.trigger('beforemodalempty'); - emptyEl(this.contentEl()); - - /** - * Fired just after a `ModalDialog` is emptied. - * - * @event ModalDialog#modalempty - * @type {EventTarget~Event} - */ - this.trigger('modalempty'); - }; - - /** - * Gets or sets the modal content, which gets normalized before being - * rendered into the DOM. - * - * This does not update the DOM or fill the modal, but it is called during - * that process. - * - * @param {Mixed} [value] - * If defined, sets the internal content value to be used on the - * next call(s) to `fill`. This value is normalized before being - * inserted. To "clear" the internal content value, pass `null`. - * - * @return {Mixed} - * The current content of the modal dialog - */ - - - ModalDialog.prototype.content = function content(value) { - if (typeof value !== 'undefined') { - this.content_ = value; - } - return this.content_; - }; - - /** - * conditionally focus the modal dialog if focus was previously on the player. - * - * @private - */ - - - ModalDialog.prototype.conditionalFocus_ = function conditionalFocus_() { - var activeEl = document_1.activeElement; - var playerEl = this.player_.el_; - - this.previouslyActiveEl_ = null; - - if (playerEl.contains(activeEl) || playerEl === activeEl) { - this.previouslyActiveEl_ = activeEl; - - this.focus(); - - this.on(document_1, 'keydown', this.handleKeyDown); - } - }; - - /** - * conditionally blur the element and refocus the last focused element - * - * @private - */ - - - ModalDialog.prototype.conditionalBlur_ = function conditionalBlur_() { - if (this.previouslyActiveEl_) { - this.previouslyActiveEl_.focus(); - this.previouslyActiveEl_ = null; - } - - this.off(document_1, 'keydown', this.handleKeyDown); - }; - - /** - * Keydown handler. Attached when modal is focused. - * - * @listens keydown - */ - - - ModalDialog.prototype.handleKeyDown = function handleKeyDown(event) { - // exit early if it isn't a tab key - if (event.which !== 9) { - return; - } - - var focusableEls = this.focusableEls_(); - var activeEl = this.el_.querySelector(':focus'); - var focusIndex = void 0; - - for (var i = 0; i < focusableEls.length; i++) { - if (activeEl === focusableEls[i]) { - focusIndex = i; - break; - } - } - - if (document_1.activeElement === this.el_) { - focusIndex = 0; - } - - if (event.shiftKey && focusIndex === 0) { - focusableEls[focusableEls.length - 1].focus(); - event.preventDefault(); - } else if (!event.shiftKey && focusIndex === focusableEls.length - 1) { - focusableEls[0].focus(); - event.preventDefault(); - } - }; - - /** - * get all focusable elements - * - * @private - */ - - - ModalDialog.prototype.focusableEls_ = function focusableEls_() { - var allChildren = this.el_.querySelectorAll('*'); - - return Array.prototype.filter.call(allChildren, function (child) { - return (child instanceof window_1.HTMLAnchorElement || child instanceof window_1.HTMLAreaElement) && child.hasAttribute('href') || (child instanceof window_1.HTMLInputElement || child instanceof window_1.HTMLSelectElement || child instanceof window_1.HTMLTextAreaElement || child instanceof window_1.HTMLButtonElement) && !child.hasAttribute('disabled') || child instanceof window_1.HTMLIFrameElement || child instanceof window_1.HTMLObjectElement || child instanceof window_1.HTMLEmbedElement || child.hasAttribute('tabindex') && child.getAttribute('tabindex') !== -1 || child.hasAttribute('contenteditable'); - }); - }; - - return ModalDialog; -}(Component); - -/** - * Default options for `ModalDialog` default options. - * - * @type {Object} - * @private - */ - - -ModalDialog.prototype.options_ = { - pauseOnOpen: true, - temporary: true -}; - -Component.registerComponent('ModalDialog', ModalDialog); - -/** - * @file track-list.js - */ -/** - * Common functionaliy between {@link TextTrackList}, {@link AudioTrackList}, and - * {@link VideoTrackList} - * - * @extends EventTarget - */ - -var TrackList = function (_EventTarget) { - inherits(TrackList, _EventTarget); - - /** - * Create an instance of this class - * - * @param {Track[]} tracks - * A list of tracks to initialize the list with. - * - * @param {Object} [list] - * The child object with inheritance done manually for ie8. - * - * @abstract - */ - function TrackList() { - var tracks = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; - - var _ret; - - var list = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; - classCallCheck(this, TrackList); - - var _this = possibleConstructorReturn(this, _EventTarget.call(this)); - - if (!list) { - list = _this; // eslint-disable-line - if (IS_IE8) { - list = document_1.createElement('custom'); - for (var prop in TrackList.prototype) { - if (prop !== 'constructor') { - list[prop] = TrackList.prototype[prop]; - } - } - } - } - - list.tracks_ = []; - - /** - * @memberof TrackList - * @member {number} length - * The current number of `Track`s in the this Trackist. - * @instance - */ - Object.defineProperty(list, 'length', { - get: function get$$1() { - return this.tracks_.length; - } - }); - - for (var i = 0; i < tracks.length; i++) { - list.addTrack(tracks[i]); - } - - // must return the object, as for ie8 it will not be this - // but a reference to a document object - return _ret = list, possibleConstructorReturn(_this, _ret); - } - - /** - * Add a {@link Track} to the `TrackList` - * - * @param {Track} track - * The audio, video, or text track to add to the list. - * - * @fires TrackList#addtrack - */ - - - TrackList.prototype.addTrack = function addTrack(track) { - var index = this.tracks_.length; - - if (!('' + index in this)) { - Object.defineProperty(this, index, { - get: function get$$1() { - return this.tracks_[index]; - } - }); - } - - // Do not add duplicate tracks - if (this.tracks_.indexOf(track) === -1) { - this.tracks_.push(track); - /** - * Triggered when a track is added to a track list. - * - * @event TrackList#addtrack - * @type {EventTarget~Event} - * @property {Track} track - * A reference to track that was added. - */ - this.trigger({ - track: track, - type: 'addtrack' - }); - } - }; - - /** - * Remove a {@link Track} from the `TrackList` - * - * @param {Track} rtrack - * The audio, video, or text track to remove from the list. - * - * @fires TrackList#removetrack - */ - - - TrackList.prototype.removeTrack = function removeTrack(rtrack) { - var track = void 0; - - for (var i = 0, l = this.length; i < l; i++) { - if (this[i] === rtrack) { - track = this[i]; - if (track.off) { - track.off(); - } - - this.tracks_.splice(i, 1); - - break; - } - } - - if (!track) { - return; - } - - /** - * Triggered when a track is removed from track list. - * - * @event TrackList#removetrack - * @type {EventTarget~Event} - * @property {Track} track - * A reference to track that was removed. - */ - this.trigger({ - track: track, - type: 'removetrack' - }); - }; - - /** - * Get a Track from the TrackList by a tracks id - * - * @param {String} id - the id of the track to get - * @method getTrackById - * @return {Track} - * @private - */ - - - TrackList.prototype.getTrackById = function getTrackById(id) { - var result = null; - - for (var i = 0, l = this.length; i < l; i++) { - var track = this[i]; - - if (track.id === id) { - result = track; - break; - } - } - - return result; - }; - - return TrackList; -}(EventTarget); - -/** - * Triggered when a different track is selected/enabled. - * - * @event TrackList#change - * @type {EventTarget~Event} - */ - -/** - * Events that can be called with on + eventName. See {@link EventHandler}. - * - * @property {Object} TrackList#allowedEvents_ - * @private - */ - - -TrackList.prototype.allowedEvents_ = { - change: 'change', - addtrack: 'addtrack', - removetrack: 'removetrack' -}; - -// emulate attribute EventHandler support to allow for feature detection -for (var event in TrackList.prototype.allowedEvents_) { - TrackList.prototype['on' + event] = null; -} - -/** - * @file audio-track-list.js - */ -/** - * Anywhere we call this function we diverge from the spec - * as we only support one enabled audiotrack at a time - * - * @param {AudioTrackList} list - * list to work on - * - * @param {AudioTrack} track - * The track to skip - * - * @private - */ -var disableOthers = function disableOthers(list, track) { - for (var i = 0; i < list.length; i++) { - if (!Object.keys(list[i]).length || track.id === list[i].id) { - continue; - } - // another audio track is enabled, disable it - list[i].enabled = false; - } -}; - -/** - * The current list of {@link AudioTrack} for a media file. - * - * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#audiotracklist} - * @extends TrackList - */ - -var AudioTrackList = function (_TrackList) { - inherits(AudioTrackList, _TrackList); - - /** - * Create an instance of this class. - * - * @param {AudioTrack[]} [tracks=[]] - * A list of `AudioTrack` to instantiate the list with. - */ - function AudioTrackList() { - var _this, _ret; - - var tracks = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; - classCallCheck(this, AudioTrackList); - - var list = void 0; - - // make sure only 1 track is enabled - // sorted from last index to first index - for (var i = tracks.length - 1; i >= 0; i--) { - if (tracks[i].enabled) { - disableOthers(tracks, tracks[i]); - break; - } - } - - // IE8 forces us to implement inheritance ourselves - // as it does not support Object.defineProperty properly - if (IS_IE8) { - list = document_1.createElement('custom'); - for (var prop in TrackList.prototype) { - if (prop !== 'constructor') { - list[prop] = TrackList.prototype[prop]; - } - } - for (var _prop in AudioTrackList.prototype) { - if (_prop !== 'constructor') { - list[_prop] = AudioTrackList.prototype[_prop]; - } - } - } - - list = (_this = possibleConstructorReturn(this, _TrackList.call(this, tracks, list)), _this); - list.changing_ = false; - - return _ret = list, possibleConstructorReturn(_this, _ret); - } - - /** - * Add an {@link AudioTrack} to the `AudioTrackList`. - * - * @param {AudioTrack} track - * The AudioTrack to add to the list - * - * @fires TrackList#addtrack - */ - - - AudioTrackList.prototype.addTrack = function addTrack(track) { - var _this2 = this; - - if (track.enabled) { - disableOthers(this, track); - } - - _TrackList.prototype.addTrack.call(this, track); - // native tracks don't have this - if (!track.addEventListener) { - return; - } - - /** - * @listens AudioTrack#enabledchange - * @fires TrackList#change - */ - track.addEventListener('enabledchange', function () { - // when we are disabling other tracks (since we don't support - // more than one track at a time) we will set changing_ - // to true so that we don't trigger additional change events - if (_this2.changing_) { - return; - } - _this2.changing_ = true; - disableOthers(_this2, track); - _this2.changing_ = false; - _this2.trigger('change'); - }); - }; - - return AudioTrackList; -}(TrackList); - -/** - * @file video-track-list.js - */ -/** - * Un-select all other {@link VideoTrack}s that are selected. - * - * @param {VideoTrackList} list - * list to work on - * - * @param {VideoTrack} track - * The track to skip - * - * @private - */ -var disableOthers$1 = function disableOthers(list, track) { - for (var i = 0; i < list.length; i++) { - if (!Object.keys(list[i]).length || track.id === list[i].id) { - continue; - } - // another video track is enabled, disable it - list[i].selected = false; - } -}; - -/** - * The current list of {@link VideoTrack} for a video. - * - * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#videotracklist} - * @extends TrackList - */ - -var VideoTrackList = function (_TrackList) { - inherits(VideoTrackList, _TrackList); - - /** - * Create an instance of this class. - * - * @param {VideoTrack[]} [tracks=[]] - * A list of `VideoTrack` to instantiate the list with. - */ - function VideoTrackList() { - var _this, _ret; - - var tracks = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; - classCallCheck(this, VideoTrackList); - - var list = void 0; - - // make sure only 1 track is enabled - // sorted from last index to first index - for (var i = tracks.length - 1; i >= 0; i--) { - if (tracks[i].selected) { - disableOthers$1(tracks, tracks[i]); - break; - } - } - - // IE8 forces us to implement inheritance ourselves - // as it does not support Object.defineProperty properly - if (IS_IE8) { - list = document_1.createElement('custom'); - for (var prop in TrackList.prototype) { - if (prop !== 'constructor') { - list[prop] = TrackList.prototype[prop]; - } - } - for (var _prop in VideoTrackList.prototype) { - if (_prop !== 'constructor') { - list[_prop] = VideoTrackList.prototype[_prop]; - } - } - } - - list = (_this = possibleConstructorReturn(this, _TrackList.call(this, tracks, list)), _this); - list.changing_ = false; - - /** - * @member {number} VideoTrackList#selectedIndex - * The current index of the selected {@link VideoTrack`}. - */ - Object.defineProperty(list, 'selectedIndex', { - get: function get$$1() { - for (var _i = 0; _i < this.length; _i++) { - if (this[_i].selected) { - return _i; - } - } - return -1; - }, - set: function set$$1() {} - }); - - return _ret = list, possibleConstructorReturn(_this, _ret); - } - - /** - * Add a {@link VideoTrack} to the `VideoTrackList`. - * - * @param {VideoTrack} track - * The VideoTrack to add to the list - * - * @fires TrackList#addtrack - */ - - - VideoTrackList.prototype.addTrack = function addTrack(track) { - var _this2 = this; - - if (track.selected) { - disableOthers$1(this, track); - } - - _TrackList.prototype.addTrack.call(this, track); - // native tracks don't have this - if (!track.addEventListener) { - return; - } - - /** - * @listens VideoTrack#selectedchange - * @fires TrackList#change - */ - track.addEventListener('selectedchange', function () { - if (_this2.changing_) { - return; - } - _this2.changing_ = true; - disableOthers$1(_this2, track); - _this2.changing_ = false; - _this2.trigger('change'); - }); - }; - - return VideoTrackList; -}(TrackList); - -/** - * @file text-track-list.js - */ -/** - * The current list of {@link TextTrack} for a media file. - * - * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttracklist} - * @extends TrackList - */ - -var TextTrackList = function (_TrackList) { - inherits(TextTrackList, _TrackList); - - /** - * Create an instance of this class. - * - * @param {TextTrack[]} [tracks=[]] - * A list of `TextTrack` to instantiate the list with. - */ - function TextTrackList() { - var _this, _ret; - - var tracks = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; - classCallCheck(this, TextTrackList); - - var list = void 0; - - // IE8 forces us to implement inheritance ourselves - // as it does not support Object.defineProperty properly - if (IS_IE8) { - list = document_1.createElement('custom'); - for (var prop in TrackList.prototype) { - if (prop !== 'constructor') { - list[prop] = TrackList.prototype[prop]; - } - } - for (var _prop in TextTrackList.prototype) { - if (_prop !== 'constructor') { - list[_prop] = TextTrackList.prototype[_prop]; - } - } - } - - list = (_this = possibleConstructorReturn(this, _TrackList.call(this, tracks, list)), _this); - return _ret = list, possibleConstructorReturn(_this, _ret); - } - - /** - * Add a {@link TextTrack} to the `TextTrackList` - * - * @param {TextTrack} track - * The text track to add to the list. - * - * @fires TrackList#addtrack - */ - - - TextTrackList.prototype.addTrack = function addTrack(track) { - _TrackList.prototype.addTrack.call(this, track); - - /** - * @listens TextTrack#modechange - * @fires TrackList#change - */ - track.addEventListener('modechange', bind(this, function () { - this.trigger('change'); - })); - - var nonLanguageTextTrackKind = ['metadata', 'chapters']; - - if (nonLanguageTextTrackKind.indexOf(track.kind) === -1) { - track.addEventListener('modechange', bind(this, function () { - this.trigger('selectedlanguagechange'); - })); - } - }; - - return TextTrackList; -}(TrackList); - -/** - * @file html-track-element-list.js - */ - -/** - * The current list of {@link HtmlTrackElement}s. - */ - -var HtmlTrackElementList = function () { - - /** - * Create an instance of this class. - * - * @param {HtmlTrackElement[]} [tracks=[]] - * A list of `HtmlTrackElement` to instantiate the list with. - */ - function HtmlTrackElementList() { - var trackElements = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : []; - classCallCheck(this, HtmlTrackElementList); - - var list = this; // eslint-disable-line - - if (IS_IE8) { - list = document_1.createElement('custom'); - - for (var prop in HtmlTrackElementList.prototype) { - if (prop !== 'constructor') { - list[prop] = HtmlTrackElementList.prototype[prop]; - } - } - } - - list.trackElements_ = []; - - /** - * @memberof HtmlTrackElementList - * @member {number} length - * The current number of `Track`s in the this Trackist. - * @instance - */ - Object.defineProperty(list, 'length', { - get: function get$$1() { - return this.trackElements_.length; - } - }); - - for (var i = 0, length = trackElements.length; i < length; i++) { - list.addTrackElement_(trackElements[i]); - } - - if (IS_IE8) { - return list; - } - } - - /** - * Add an {@link HtmlTrackElement} to the `HtmlTrackElementList` - * - * @param {HtmlTrackElement} trackElement - * The track element to add to the list. - * - * @private - */ - - - HtmlTrackElementList.prototype.addTrackElement_ = function addTrackElement_(trackElement) { - var index = this.trackElements_.length; - - if (!('' + index in this)) { - Object.defineProperty(this, index, { - get: function get$$1() { - return this.trackElements_[index]; - } - }); - } - - // Do not add duplicate elements - if (this.trackElements_.indexOf(trackElement) === -1) { - this.trackElements_.push(trackElement); - } - }; - - /** - * Get an {@link HtmlTrackElement} from the `HtmlTrackElementList` given an - * {@link TextTrack}. - * - * @param {TextTrack} track - * The track associated with a track element. - * - * @return {HtmlTrackElement|undefined} - * The track element that was found or undefined. - * - * @private - */ - - - HtmlTrackElementList.prototype.getTrackElementByTrack_ = function getTrackElementByTrack_(track) { - var trackElement_ = void 0; - - for (var i = 0, length = this.trackElements_.length; i < length; i++) { - if (track === this.trackElements_[i].track) { - trackElement_ = this.trackElements_[i]; - - break; - } - } - - return trackElement_; - }; - - /** - * Remove a {@link HtmlTrackElement} from the `HtmlTrackElementList` - * - * @param {HtmlTrackElement} trackElement - * The track element to remove from the list. - * - * @private - */ - - - HtmlTrackElementList.prototype.removeTrackElement_ = function removeTrackElement_(trackElement) { - for (var i = 0, length = this.trackElements_.length; i < length; i++) { - if (trackElement === this.trackElements_[i]) { - this.trackElements_.splice(i, 1); - - break; - } - } - }; - - return HtmlTrackElementList; -}(); - -/** - * @file text-track-cue-list.js - */ -/** - * @typedef {Object} TextTrackCueList~TextTrackCue - * - * @property {string} id - * The unique id for this text track cue - * - * @property {number} startTime - * The start time for this text track cue - * - * @property {number} endTime - * The end time for this text track cue - * - * @property {boolean} pauseOnExit - * Pause when the end time is reached if true. - * - * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttrackcue} - */ - -/** - * A List of TextTrackCues. - * - * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttrackcuelist} - */ - -var TextTrackCueList = function () { - - /** - * Create an instance of this class.. - * - * @param {Array} cues - * A list of cues to be initialized with - */ - function TextTrackCueList(cues) { - classCallCheck(this, TextTrackCueList); - - var list = this; // eslint-disable-line - - if (IS_IE8) { - list = document_1.createElement('custom'); - - for (var prop in TextTrackCueList.prototype) { - if (prop !== 'constructor') { - list[prop] = TextTrackCueList.prototype[prop]; - } - } - } - - TextTrackCueList.prototype.setCues_.call(list, cues); - - /** - * @memberof TextTrackCueList - * @member {number} length - * The current number of `TextTrackCue`s in the TextTrackCueList. - * @instance - */ - Object.defineProperty(list, 'length', { - get: function get$$1() { - return this.length_; - } - }); - - if (IS_IE8) { - return list; - } - } - - /** - * A setter for cues in this list. Creates getters - * an an index for the cues. - * - * @param {Array} cues - * An array of cues to set - * - * @private - */ - - - TextTrackCueList.prototype.setCues_ = function setCues_(cues) { - var oldLength = this.length || 0; - var i = 0; - var l = cues.length; - - this.cues_ = cues; - this.length_ = cues.length; - - var defineProp = function defineProp(index) { - if (!('' + index in this)) { - Object.defineProperty(this, '' + index, { - get: function get$$1() { - return this.cues_[index]; - } - }); - } - }; - - if (oldLength < l) { - i = oldLength; - - for (; i < l; i++) { - defineProp.call(this, i); - } - } - }; - - /** - * Get a `TextTrackCue` that is currently in the `TextTrackCueList` by id. - * - * @param {string} id - * The id of the cue that should be searched for. - * - * @return {TextTrackCueList~TextTrackCue|null} - * A single cue or null if none was found. - */ - - - TextTrackCueList.prototype.getCueById = function getCueById(id) { - var result = null; - - for (var i = 0, l = this.length; i < l; i++) { - var cue = this[i]; - - if (cue.id === id) { - result = cue; - break; - } - } - - return result; - }; - - return TextTrackCueList; -}(); - -/** - * @file track-kinds.js - */ - -/** - * All possible `VideoTrackKind`s - * - * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-videotrack-kind - * @typedef VideoTrack~Kind - * @enum - */ -var VideoTrackKind = { - alternative: 'alternative', - captions: 'captions', - main: 'main', - sign: 'sign', - subtitles: 'subtitles', - commentary: 'commentary' -}; - -/** - * All possible `AudioTrackKind`s - * - * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-audiotrack-kind - * @typedef AudioTrack~Kind - * @enum - */ -var AudioTrackKind = { - 'alternative': 'alternative', - 'descriptions': 'descriptions', - 'main': 'main', - 'main-desc': 'main-desc', - 'translation': 'translation', - 'commentary': 'commentary' -}; - -/** - * All possible `TextTrackKind`s - * - * @see https://html.spec.whatwg.org/multipage/embedded-content.html#dom-texttrack-kind - * @typedef TextTrack~Kind - * @enum - */ -var TextTrackKind = { - subtitles: 'subtitles', - captions: 'captions', - descriptions: 'descriptions', - chapters: 'chapters', - metadata: 'metadata' -}; - -/** - * All possible `TextTrackMode`s - * - * @see https://html.spec.whatwg.org/multipage/embedded-content.html#texttrackmode - * @typedef TextTrack~Mode - * @enum - */ -var TextTrackMode = { - disabled: 'disabled', - hidden: 'hidden', - showing: 'showing' -}; - -/** - * @file track.js - */ -/** - * A Track class that contains all of the common functionality for {@link AudioTrack}, - * {@link VideoTrack}, and {@link TextTrack}. - * - * > Note: This class should not be used directly - * - * @see {@link https://html.spec.whatwg.org/multipage/embedded-content.html} - * @extends EventTarget - * @abstract - */ - -var Track = function (_EventTarget) { - inherits(Track, _EventTarget); - - /** - * Create an instance of this class. - * - * @param {Object} [options={}] - * Object of option names and values - * - * @param {string} [options.kind=''] - * A valid kind for the track type you are creating. - * - * @param {string} [options.id='vjs_track_' + Guid.newGUID()] - * A unique id for this AudioTrack. - * - * @param {string} [options.label=''] - * The menu label for this track. - * - * @param {string} [options.language=''] - * A valid two character language code. - * - * @abstract - */ - function Track() { - var _ret; - - var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - classCallCheck(this, Track); - - var _this = possibleConstructorReturn(this, _EventTarget.call(this)); - - var track = _this; // eslint-disable-line - - if (IS_IE8) { - track = document_1.createElement('custom'); - for (var prop in Track.prototype) { - if (prop !== 'constructor') { - track[prop] = Track.prototype[prop]; - } - } - } - - var trackProps = { - id: options.id || 'vjs_track_' + newGUID(), - kind: options.kind || '', - label: options.label || '', - language: options.language || '' - }; - - /** - * @memberof Track - * @member {string} id - * The id of this track. Cannot be changed after creation. - * @instance - * - * @readonly - */ - - /** - * @memberof Track - * @member {string} kind - * The kind of track that this is. Cannot be changed after creation. - * @instance - * - * @readonly - */ - - /** - * @memberof Track - * @member {string} label - * The label of this track. Cannot be changed after creation. - * @instance - * - * @readonly - */ - - /** - * @memberof Track - * @member {string} language - * The two letter language code for this track. Cannot be changed after - * creation. - * @instance - * - * @readonly - */ - - var _loop = function _loop(key) { - Object.defineProperty(track, key, { - get: function get$$1() { - return trackProps[key]; - }, - set: function set$$1() {} - }); - }; - - for (var key in trackProps) { - _loop(key); - } - - return _ret = track, possibleConstructorReturn(_this, _ret); - } - - return Track; -}(EventTarget); - -/** - * @file url.js - * @module url - */ -/** - * @typedef {Object} url:URLObject - * - * @property {string} protocol - * The protocol of the url that was parsed. - * - * @property {string} hostname - * The hostname of the url that was parsed. - * - * @property {string} port - * The port of the url that was parsed. - * - * @property {string} pathname - * The pathname of the url that was parsed. - * - * @property {string} search - * The search query of the url that was parsed. - * - * @property {string} hash - * The hash of the url that was parsed. - * - * @property {string} host - * The host of the url that was parsed. - */ - -/** - * Resolve and parse the elements of a URL. - * - * @param {String} url - * The url to parse - * - * @return {url:URLObject} - * An object of url details - */ -var parseUrl = function parseUrl(url) { - var props = ['protocol', 'hostname', 'port', 'pathname', 'search', 'hash', 'host']; - - // add the url to an anchor and let the browser parse the URL - var a = document_1.createElement('a'); - - a.href = url; - - // IE8 (and 9?) Fix - // ie8 doesn't parse the URL correctly until the anchor is actually - // added to the body, and an innerHTML is needed to trigger the parsing - var addToBody = a.host === '' && a.protocol !== 'file:'; - var div = void 0; - - if (addToBody) { - div = document_1.createElement('div'); - div.innerHTML = ''; - a = div.firstChild; - // prevent the div from affecting layout - div.setAttribute('style', 'display:none; position:absolute;'); - document_1.body.appendChild(div); - } - - // Copy the specific URL properties to a new object - // This is also needed for IE8 because the anchor loses its - // properties when it's removed from the dom - var details = {}; - - for (var i = 0; i < props.length; i++) { - details[props[i]] = a[props[i]]; - } - - // IE9 adds the port to the host property unlike everyone else. If - // a port identifier is added for standard ports, strip it. - if (details.protocol === 'http:') { - details.host = details.host.replace(/:80$/, ''); - } - - if (details.protocol === 'https:') { - details.host = details.host.replace(/:443$/, ''); - } - - if (!details.protocol) { - details.protocol = window_1.location.protocol; - } - - if (addToBody) { - document_1.body.removeChild(div); - } - - return details; -}; - -/** - * Get absolute version of relative URL. Used to tell flash correct URL. - * - * - * @param {string} url - * URL to make absolute - * - * @return {string} - * Absolute URL - * - * @see http://stackoverflow.com/questions/470832/getting-an-absolute-url-from-a-relative-one-ie6-issue - */ -var getAbsoluteURL = function getAbsoluteURL(url) { - // Check if absolute URL - if (!url.match(/^https?:\/\//)) { - // Convert to absolute URL. Flash hosted off-site needs an absolute URL. - var div = document_1.createElement('div'); - - div.innerHTML = 'x'; - url = div.firstChild.href; - } - - return url; -}; - -/** - * Returns the extension of the passed file name. It will return an empty string - * if passed an invalid path. - * - * @param {string} path - * The fileName path like '/path/to/file.mp4' - * - * @returns {string} - * The extension in lower case or an empty string if no - * extension could be found. - */ -var getFileExtension = function getFileExtension(path) { - if (typeof path === 'string') { - var splitPathRe = /^(\/?)([\s\S]*?)((?:\.{1,2}|[^\/]+?)(\.([^\.\/\?]+)))(?:[\/]*|[\?].*)$/i; - var pathParts = splitPathRe.exec(path); - - if (pathParts) { - return pathParts.pop().toLowerCase(); - } - } - - return ''; -}; - -/** - * Returns whether the url passed is a cross domain request or not. - * - * @param {string} url - * The url to check. - * - * @return {boolean} - * Whether it is a cross domain request or not. - */ -var isCrossOrigin = function isCrossOrigin(url) { - var winLoc = window_1.location; - var urlInfo = parseUrl(url); - - // IE8 protocol relative urls will return ':' for protocol - var srcProtocol = urlInfo.protocol === ':' ? winLoc.protocol : urlInfo.protocol; - - // Check if url is for another domain/origin - // IE8 doesn't know location.origin, so we won't rely on it here - var crossOrigin = srcProtocol + urlInfo.host !== winLoc.protocol + winLoc.host; - - return crossOrigin; -}; - -var Url = (Object.freeze || Object)({ - parseUrl: parseUrl, - getAbsoluteURL: getAbsoluteURL, - getFileExtension: getFileExtension, - isCrossOrigin: isCrossOrigin -}); - -var isFunction_1 = isFunction; - -var toString$1 = Object.prototype.toString; - -function isFunction (fn) { - var string = toString$1.call(fn); - return string === '[object Function]' || - (typeof fn === 'function' && string !== '[object RegExp]') || - (typeof window !== 'undefined' && - // IE8 and below - (fn === window.setTimeout || - fn === window.alert || - fn === window.confirm || - fn === window.prompt)) -} - -var trim_1 = createCommonjsModule(function (module, exports) { -exports = module.exports = trim; - -function trim(str){ - return str.replace(/^\s*|\s*$/g, ''); -} - -exports.left = function(str){ - return str.replace(/^\s*/, ''); -}; - -exports.right = function(str){ - return str.replace(/\s*$/, ''); -}; -}); - -var forEach_1 = forEach; - -var toString$2 = Object.prototype.toString; -var hasOwnProperty = Object.prototype.hasOwnProperty; - -function forEach(list, iterator, context) { - if (!isFunction_1(iterator)) { - throw new TypeError('iterator must be a function') - } - - if (arguments.length < 3) { - context = this; - } - - if (toString$2.call(list) === '[object Array]') - forEachArray$1(list, iterator, context); - else if (typeof list === 'string') - forEachString(list, iterator, context); - else - forEachObject(list, iterator, context); -} - -function forEachArray$1(array, iterator, context) { - for (var i = 0, len = array.length; i < len; i++) { - if (hasOwnProperty.call(array, i)) { - iterator.call(context, array[i], i, array); - } - } -} - -function forEachString(string, iterator, context) { - for (var i = 0, len = string.length; i < len; i++) { - // no such thing as a sparse string. - iterator.call(context, string.charAt(i), i, string); - } -} - -function forEachObject(object, iterator, context) { - for (var k in object) { - if (hasOwnProperty.call(object, k)) { - iterator.call(context, object[k], k, object); - } - } -} - -var isArray = function(arg) { - return Object.prototype.toString.call(arg) === '[object Array]'; - }; - -var parseHeaders = function (headers) { - if (!headers) - return {} - - var result = {}; - - forEach_1( - trim_1(headers).split('\n') - , function (row) { - var index = row.indexOf(':') - , key = trim_1(row.slice(0, index)).toLowerCase() - , value = trim_1(row.slice(index + 1)); - - if (typeof(result[key]) === 'undefined') { - result[key] = value; - } else if (isArray(result[key])) { - result[key].push(value); - } else { - result[key] = [ result[key], value ]; - } - } - ); - - return result -}; - -var immutable = extend; - -var hasOwnProperty$1 = Object.prototype.hasOwnProperty; - -function extend() { - var target = {}; - - for (var i = 0; i < arguments.length; i++) { - var source = arguments[i]; - - for (var key in source) { - if (hasOwnProperty$1.call(source, key)) { - target[key] = source[key]; - } - } - } - - return target -} - -var xhr = createXHR; -createXHR.XMLHttpRequest = window_1.XMLHttpRequest || noop; -createXHR.XDomainRequest = "withCredentials" in (new createXHR.XMLHttpRequest()) ? createXHR.XMLHttpRequest : window_1.XDomainRequest; - -forEachArray(["get", "put", "post", "patch", "head", "delete"], function(method) { - createXHR[method === "delete" ? "del" : method] = function(uri, options, callback) { - options = initParams(uri, options, callback); - options.method = method.toUpperCase(); - return _createXHR(options) - }; -}); - -function forEachArray(array, iterator) { - for (var i = 0; i < array.length; i++) { - iterator(array[i]); - } -} - -function isEmpty(obj){ - for(var i in obj){ - if(obj.hasOwnProperty(i)) return false - } - return true -} - -function initParams(uri, options, callback) { - var params = uri; - - if (isFunction_1(options)) { - callback = options; - if (typeof uri === "string") { - params = {uri:uri}; - } - } else { - params = immutable(options, {uri: uri}); - } - - params.callback = callback; - return params -} - -function createXHR(uri, options, callback) { - options = initParams(uri, options, callback); - return _createXHR(options) -} - -function _createXHR(options) { - if(typeof options.callback === "undefined"){ - throw new Error("callback argument missing") - } - - var called = false; - var callback = function cbOnce(err, response, body){ - if(!called){ - called = true; - options.callback(err, response, body); - } - }; - - function readystatechange() { - if (xhr.readyState === 4) { - setTimeout(loadFunc, 0); - } - } - - function getBody() { - // Chrome with requestType=blob throws errors arround when even testing access to responseText - var body = undefined; - - if (xhr.response) { - body = xhr.response; - } else { - body = xhr.responseText || getXml(xhr); - } - - if (isJson) { - try { - body = JSON.parse(body); - } catch (e) {} - } - - return body - } - - function errorFunc(evt) { - clearTimeout(timeoutTimer); - if(!(evt instanceof Error)){ - evt = new Error("" + (evt || "Unknown XMLHttpRequest Error") ); - } - evt.statusCode = 0; - return callback(evt, failureResponse) - } - - // will load the data & process the response in a special response object - function loadFunc() { - if (aborted) return - var status; - clearTimeout(timeoutTimer); - if(options.useXDR && xhr.status===undefined) { - //IE8 CORS GET successful response doesn't have a status field, but body is fine - status = 200; - } else { - status = (xhr.status === 1223 ? 204 : xhr.status); - } - var response = failureResponse; - var err = null; - - if (status !== 0){ - response = { - body: getBody(), - statusCode: status, - method: method, - headers: {}, - url: uri, - rawRequest: xhr - }; - if(xhr.getAllResponseHeaders){ //remember xhr can in fact be XDR for CORS in IE - response.headers = parseHeaders(xhr.getAllResponseHeaders()); - } - } else { - err = new Error("Internal XMLHttpRequest Error"); - } - return callback(err, response, response.body) - } - - var xhr = options.xhr || null; - - if (!xhr) { - if (options.cors || options.useXDR) { - xhr = new createXHR.XDomainRequest(); - }else{ - xhr = new createXHR.XMLHttpRequest(); - } - } - - var key; - var aborted; - var uri = xhr.url = options.uri || options.url; - var method = xhr.method = options.method || "GET"; - var body = options.body || options.data; - var headers = xhr.headers = options.headers || {}; - var sync = !!options.sync; - var isJson = false; - var timeoutTimer; - var failureResponse = { - body: undefined, - headers: {}, - statusCode: 0, - method: method, - url: uri, - rawRequest: xhr - }; - - if ("json" in options && options.json !== false) { - isJson = true; - headers["accept"] || headers["Accept"] || (headers["Accept"] = "application/json"); //Don't override existing accept header declared by user - if (method !== "GET" && method !== "HEAD") { - headers["content-type"] || headers["Content-Type"] || (headers["Content-Type"] = "application/json"); //Don't override existing accept header declared by user - body = JSON.stringify(options.json === true ? body : options.json); - } - } - - xhr.onreadystatechange = readystatechange; - xhr.onload = loadFunc; - xhr.onerror = errorFunc; - // IE9 must have onprogress be set to a unique function. - xhr.onprogress = function () { - // IE must die - }; - xhr.onabort = function(){ - aborted = true; - }; - xhr.ontimeout = errorFunc; - xhr.open(method, uri, !sync, options.username, options.password); - //has to be after open - if(!sync) { - xhr.withCredentials = !!options.withCredentials; - } - // Cannot set timeout with sync request - // not setting timeout on the xhr object, because of old webkits etc. not handling that correctly - // both npm's request and jquery 1.x use this kind of timeout, so this is being consistent - if (!sync && options.timeout > 0 ) { - timeoutTimer = setTimeout(function(){ - if (aborted) return - aborted = true;//IE9 may still call readystatechange - xhr.abort("timeout"); - var e = new Error("XMLHttpRequest timeout"); - e.code = "ETIMEDOUT"; - errorFunc(e); - }, options.timeout ); - } - - if (xhr.setRequestHeader) { - for(key in headers){ - if(headers.hasOwnProperty(key)){ - xhr.setRequestHeader(key, headers[key]); - } - } - } else if (options.headers && !isEmpty(options.headers)) { - throw new Error("Headers cannot be set on an XDomainRequest object") - } - - if ("responseType" in options) { - xhr.responseType = options.responseType; - } - - if ("beforeSend" in options && - typeof options.beforeSend === "function" - ) { - options.beforeSend(xhr); - } - - // Microsoft Edge browser sends "undefined" when send is called with undefined value. - // XMLHttpRequest spec says to pass null as body to indicate no body - // See https://github.com/naugtur/xhr/issues/100. - xhr.send(body || null); - - return xhr - - -} - -function getXml(xhr) { - if (xhr.responseType === "document") { - return xhr.responseXML - } - var firefoxBugTakenEffect = xhr.responseXML && xhr.responseXML.documentElement.nodeName === "parsererror"; - if (xhr.responseType === "" && !firefoxBugTakenEffect) { - return xhr.responseXML - } - - return null -} - -function noop() {} - -/** - * @file text-track.js - */ -/** - * Takes a webvtt file contents and parses it into cues - * - * @param {string} srcContent - * webVTT file contents - * - * @param {TextTrack} track - * TextTrack to add cues to. Cues come from the srcContent. - * - * @private - */ -var parseCues = function parseCues(srcContent, track) { - var parser = new window_1.WebVTT.Parser(window_1, window_1.vttjs, window_1.WebVTT.StringDecoder()); - var errors = []; - - parser.oncue = function (cue) { - track.addCue(cue); - }; - - parser.onparsingerror = function (error) { - errors.push(error); - }; - - parser.onflush = function () { - track.trigger({ - type: 'loadeddata', - target: track - }); - }; - - parser.parse(srcContent); - if (errors.length > 0) { - if (window_1.console && window_1.console.groupCollapsed) { - window_1.console.groupCollapsed('Text Track parsing errors for ' + track.src); - } - errors.forEach(function (error) { - return log$1.error(error); - }); - if (window_1.console && window_1.console.groupEnd) { - window_1.console.groupEnd(); - } - } - - parser.flush(); -}; - -/** - * Load a `TextTrack` from a specifed url. - * - * @param {string} src - * Url to load track from. - * - * @param {TextTrack} track - * Track to add cues to. Comes from the content at the end of `url`. - * - * @private - */ -var loadTrack = function loadTrack(src, track) { - var opts = { - uri: src - }; - var crossOrigin = isCrossOrigin(src); - - if (crossOrigin) { - opts.cors = crossOrigin; - } - - xhr(opts, bind(this, function (err, response, responseBody) { - if (err) { - return log$1.error(err, response); - } - - track.loaded_ = true; - - // Make sure that vttjs has loaded, otherwise, wait till it finished loading - // NOTE: this is only used for the alt/video.novtt.js build - if (typeof window_1.WebVTT !== 'function') { - if (track.tech_) { - var loadHandler = function loadHandler() { - return parseCues(responseBody, track); - }; - - track.tech_.on('vttjsloaded', loadHandler); - track.tech_.on('vttjserror', function () { - log$1.error('vttjs failed to load, stopping trying to process ' + track.src); - track.tech_.off('vttjsloaded', loadHandler); - }); - } - } else { - parseCues(responseBody, track); - } - })); -}; - -/** - * A representation of a single `TextTrack`. - * - * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#texttrack} - * @extends Track - */ - -var TextTrack = function (_Track) { - inherits(TextTrack, _Track); - - /** - * Create an instance of this class. - * - * @param {Object} options={} - * Object of option names and values - * - * @param {Tech} options.tech - * A reference to the tech that owns this TextTrack. - * - * @param {TextTrack~Kind} [options.kind='subtitles'] - * A valid text track kind. - * - * @param {TextTrack~Mode} [options.mode='disabled'] - * A valid text track mode. - * - * @param {string} [options.id='vjs_track_' + Guid.newGUID()] - * A unique id for this TextTrack. - * - * @param {string} [options.label=''] - * The menu label for this track. - * - * @param {string} [options.language=''] - * A valid two character language code. - * - * @param {string} [options.srclang=''] - * A valid two character language code. An alternative, but deprioritized - * vesion of `options.language` - * - * @param {string} [options.src] - * A url to TextTrack cues. - * - * @param {boolean} [options.default] - * If this track should default to on or off. - */ - function TextTrack() { - var _this, _ret; - - var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - classCallCheck(this, TextTrack); - - if (!options.tech) { - throw new Error('A tech was not provided.'); - } - - var settings = mergeOptions(options, { - kind: TextTrackKind[options.kind] || 'subtitles', - language: options.language || options.srclang || '' - }); - var mode = TextTrackMode[settings.mode] || 'disabled'; - var default_ = settings['default']; - - if (settings.kind === 'metadata' || settings.kind === 'chapters') { - mode = 'hidden'; - } - // on IE8 this will be a document element - // for every other browser this will be a normal object - var tt = (_this = possibleConstructorReturn(this, _Track.call(this, settings)), _this); - - tt.tech_ = settings.tech; - - if (IS_IE8) { - for (var prop in TextTrack.prototype) { - if (prop !== 'constructor') { - tt[prop] = TextTrack.prototype[prop]; - } - } - } - - tt.cues_ = []; - tt.activeCues_ = []; - - var cues = new TextTrackCueList(tt.cues_); - var activeCues = new TextTrackCueList(tt.activeCues_); - var changed = false; - var timeupdateHandler = bind(tt, function () { - - // Accessing this.activeCues for the side-effects of updating itself - // due to it's nature as a getter function. Do not remove or cues will - // stop updating! - /* eslint-disable no-unused-expressions */ - this.activeCues; - /* eslint-enable no-unused-expressions */ - if (changed) { - this.trigger('cuechange'); - changed = false; - } - }); - - if (mode !== 'disabled') { - tt.tech_.ready(function () { - tt.tech_.on('timeupdate', timeupdateHandler); - }, true); - } - - /** - * @memberof TextTrack - * @member {boolean} default - * If this track was set to be on or off by default. Cannot be changed after - * creation. - * @instance - * - * @readonly - */ - Object.defineProperty(tt, 'default', { - get: function get$$1() { - return default_; - }, - set: function set$$1() {} - }); - - /** - * @memberof TextTrack - * @member {string} mode - * Set the mode of this TextTrack to a valid {@link TextTrack~Mode}. Will - * not be set if setting to an invalid mode. - * @instance - * - * @fires TextTrack#modechange - */ - Object.defineProperty(tt, 'mode', { - get: function get$$1() { - return mode; - }, - set: function set$$1(newMode) { - var _this2 = this; - - if (!TextTrackMode[newMode]) { - return; - } - mode = newMode; - if (mode === 'showing') { - - this.tech_.ready(function () { - _this2.tech_.on('timeupdate', timeupdateHandler); - }, true); - } - /** - * An event that fires when mode changes on this track. This allows - * the TextTrackList that holds this track to act accordingly. - * - * > Note: This is not part of the spec! - * - * @event TextTrack#modechange - * @type {EventTarget~Event} - */ - this.trigger('modechange'); - } - }); - - /** - * @memberof TextTrack - * @member {TextTrackCueList} cues - * The text track cue list for this TextTrack. - * @instance - */ - Object.defineProperty(tt, 'cues', { - get: function get$$1() { - if (!this.loaded_) { - return null; - } - - return cues; - }, - set: function set$$1() {} - }); - - /** - * @memberof TextTrack - * @member {TextTrackCueList} activeCues - * The list text track cues that are currently active for this TextTrack. - * @instance - */ - Object.defineProperty(tt, 'activeCues', { - get: function get$$1() { - if (!this.loaded_) { - return null; - } - - // nothing to do - if (this.cues.length === 0) { - return activeCues; - } - - var ct = this.tech_.currentTime(); - var active = []; - - for (var i = 0, l = this.cues.length; i < l; i++) { - var cue = this.cues[i]; - - if (cue.startTime <= ct && cue.endTime >= ct) { - active.push(cue); - } else if (cue.startTime === cue.endTime && cue.startTime <= ct && cue.startTime + 0.5 >= ct) { - active.push(cue); - } - } - - changed = false; - - if (active.length !== this.activeCues_.length) { - changed = true; - } else { - for (var _i = 0; _i < active.length; _i++) { - if (this.activeCues_.indexOf(active[_i]) === -1) { - changed = true; - } - } - } - - this.activeCues_ = active; - activeCues.setCues_(this.activeCues_); - - return activeCues; - }, - set: function set$$1() {} - }); - - if (settings.src) { - tt.src = settings.src; - loadTrack(settings.src, tt); - } else { - tt.loaded_ = true; - } - - return _ret = tt, possibleConstructorReturn(_this, _ret); - } - - /** - * Add a cue to the internal list of cues. - * - * @param {TextTrack~Cue} cue - * The cue to add to our internal list - */ - - - TextTrack.prototype.addCue = function addCue(originalCue) { - var cue = originalCue; - - if (window_1.vttjs && !(originalCue instanceof window_1.vttjs.VTTCue)) { - cue = new window_1.vttjs.VTTCue(originalCue.startTime, originalCue.endTime, originalCue.text); - - for (var prop in originalCue) { - if (!(prop in cue)) { - cue[prop] = originalCue[prop]; - } - } - - // make sure that `id` is copied over - cue.id = originalCue.id; - cue.originalCue_ = originalCue; - } - - var tracks = this.tech_.textTracks(); - - for (var i = 0; i < tracks.length; i++) { - if (tracks[i] !== this) { - tracks[i].removeCue(cue); - } - } - - this.cues_.push(cue); - this.cues.setCues_(this.cues_); - }; - - /** - * Remove a cue from our internal list - * - * @param {TextTrack~Cue} removeCue - * The cue to remove from our internal list - */ - - - TextTrack.prototype.removeCue = function removeCue(_removeCue) { - var i = this.cues_.length; - - while (i--) { - var cue = this.cues_[i]; - - if (cue === _removeCue || cue.originalCue_ && cue.originalCue_ === _removeCue) { - this.cues_.splice(i, 1); - this.cues.setCues_(this.cues_); - break; - } - } - }; - - return TextTrack; -}(Track); - -/** - * cuechange - One or more cues in the track have become active or stopped being active. - */ - - -TextTrack.prototype.allowedEvents_ = { - cuechange: 'cuechange' -}; - -/** - * A representation of a single `AudioTrack`. If it is part of an {@link AudioTrackList} - * only one `AudioTrack` in the list will be enabled at a time. - * - * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#audiotrack} - * @extends Track - */ - -var AudioTrack = function (_Track) { - inherits(AudioTrack, _Track); - - /** - * Create an instance of this class. - * - * @param {Object} [options={}] - * Object of option names and values - * - * @param {AudioTrack~Kind} [options.kind=''] - * A valid audio track kind - * - * @param {string} [options.id='vjs_track_' + Guid.newGUID()] - * A unique id for this AudioTrack. - * - * @param {string} [options.label=''] - * The menu label for this track. - * - * @param {string} [options.language=''] - * A valid two character language code. - * - * @param {boolean} [options.enabled] - * If this track is the one that is currently playing. If this track is part of - * an {@link AudioTrackList}, only one {@link AudioTrack} will be enabled. - */ - function AudioTrack() { - var _this, _ret; - - var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - classCallCheck(this, AudioTrack); - - var settings = mergeOptions(options, { - kind: AudioTrackKind[options.kind] || '' - }); - // on IE8 this will be a document element - // for every other browser this will be a normal object - var track = (_this = possibleConstructorReturn(this, _Track.call(this, settings)), _this); - var enabled = false; - - if (IS_IE8) { - for (var prop in AudioTrack.prototype) { - if (prop !== 'constructor') { - track[prop] = AudioTrack.prototype[prop]; - } - } - } - /** - * @memberof AudioTrack - * @member {boolean} enabled - * If this `AudioTrack` is enabled or not. When setting this will - * fire {@link AudioTrack#enabledchange} if the state of enabled is changed. - * @instance - * - * @fires VideoTrack#selectedchange - */ - Object.defineProperty(track, 'enabled', { - get: function get$$1() { - return enabled; - }, - set: function set$$1(newEnabled) { - // an invalid or unchanged value - if (typeof newEnabled !== 'boolean' || newEnabled === enabled) { - return; - } - enabled = newEnabled; - - /** - * An event that fires when enabled changes on this track. This allows - * the AudioTrackList that holds this track to act accordingly. - * - * > Note: This is not part of the spec! Native tracks will do - * this internally without an event. - * - * @event AudioTrack#enabledchange - * @type {EventTarget~Event} - */ - this.trigger('enabledchange'); - } - }); - - // if the user sets this track to selected then - // set selected to that true value otherwise - // we keep it false - if (settings.enabled) { - track.enabled = settings.enabled; - } - track.loaded_ = true; - - return _ret = track, possibleConstructorReturn(_this, _ret); - } - - return AudioTrack; -}(Track); - -/** - * A representation of a single `VideoTrack`. - * - * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#videotrack} - * @extends Track - */ - -var VideoTrack = function (_Track) { - inherits(VideoTrack, _Track); - - /** - * Create an instance of this class. - * - * @param {Object} [options={}] - * Object of option names and values - * - * @param {string} [options.kind=''] - * A valid {@link VideoTrack~Kind} - * - * @param {string} [options.id='vjs_track_' + Guid.newGUID()] - * A unique id for this AudioTrack. - * - * @param {string} [options.label=''] - * The menu label for this track. - * - * @param {string} [options.language=''] - * A valid two character language code. - * - * @param {boolean} [options.selected] - * If this track is the one that is currently playing. - */ - function VideoTrack() { - var _this, _ret; - - var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - classCallCheck(this, VideoTrack); - - var settings = mergeOptions(options, { - kind: VideoTrackKind[options.kind] || '' - }); - - // on IE8 this will be a document element - // for every other browser this will be a normal object - var track = (_this = possibleConstructorReturn(this, _Track.call(this, settings)), _this); - var selected = false; - - if (IS_IE8) { - for (var prop in VideoTrack.prototype) { - if (prop !== 'constructor') { - track[prop] = VideoTrack.prototype[prop]; - } - } - } - - /** - * @memberof VideoTrack - * @member {boolean} selected - * If this `VideoTrack` is selected or not. When setting this will - * fire {@link VideoTrack#selectedchange} if the state of selected changed. - * @instance - * - * @fires VideoTrack#selectedchange - */ - Object.defineProperty(track, 'selected', { - get: function get$$1() { - return selected; - }, - set: function set$$1(newSelected) { - // an invalid or unchanged value - if (typeof newSelected !== 'boolean' || newSelected === selected) { - return; - } - selected = newSelected; - - /** - * An event that fires when selected changes on this track. This allows - * the VideoTrackList that holds this track to act accordingly. - * - * > Note: This is not part of the spec! Native tracks will do - * this internally without an event. - * - * @event VideoTrack#selectedchange - * @type {EventTarget~Event} - */ - this.trigger('selectedchange'); - } - }); - - // if the user sets this track to selected then - // set selected to that true value otherwise - // we keep it false - if (settings.selected) { - track.selected = settings.selected; - } - - return _ret = track, possibleConstructorReturn(_this, _ret); - } - - return VideoTrack; -}(Track); - -/** - * @file html-track-element.js - */ - -/** - * @memberof HTMLTrackElement - * @typedef {HTMLTrackElement~ReadyState} - * @enum {number} - */ -var NONE = 0; -var LOADING = 1; -var LOADED = 2; -var ERROR = 3; - -/** - * A single track represented in the DOM. - * - * @see [Spec]{@link https://html.spec.whatwg.org/multipage/embedded-content.html#htmltrackelement} - * @extends EventTarget - */ - -var HTMLTrackElement = function (_EventTarget) { - inherits(HTMLTrackElement, _EventTarget); - - /** - * Create an instance of this class. - * - * @param {Object} options={} - * Object of option names and values - * - * @param {Tech} options.tech - * A reference to the tech that owns this HTMLTrackElement. - * - * @param {TextTrack~Kind} [options.kind='subtitles'] - * A valid text track kind. - * - * @param {TextTrack~Mode} [options.mode='disabled'] - * A valid text track mode. - * - * @param {string} [options.id='vjs_track_' + Guid.newGUID()] - * A unique id for this TextTrack. - * - * @param {string} [options.label=''] - * The menu label for this track. - * - * @param {string} [options.language=''] - * A valid two character language code. - * - * @param {string} [options.srclang=''] - * A valid two character language code. An alternative, but deprioritized - * vesion of `options.language` - * - * @param {string} [options.src] - * A url to TextTrack cues. - * - * @param {boolean} [options.default] - * If this track should default to on or off. - */ - function HTMLTrackElement() { - var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - classCallCheck(this, HTMLTrackElement); - - var _this = possibleConstructorReturn(this, _EventTarget.call(this)); - - var readyState = void 0; - var trackElement = _this; // eslint-disable-line - - if (IS_IE8) { - trackElement = document_1.createElement('custom'); - - for (var prop in HTMLTrackElement.prototype) { - if (prop !== 'constructor') { - trackElement[prop] = HTMLTrackElement.prototype[prop]; - } - } - } - - var track = new TextTrack(options); - - trackElement.kind = track.kind; - trackElement.src = track.src; - trackElement.srclang = track.language; - trackElement.label = track.label; - trackElement['default'] = track['default']; - - /** - * @memberof HTMLTrackElement - * @member {HTMLTrackElement~ReadyState} readyState - * The current ready state of the track element. - * @instance - */ - Object.defineProperty(trackElement, 'readyState', { - get: function get$$1() { - return readyState; - } - }); - - /** - * @memberof HTMLTrackElement - * @member {TextTrack} track - * The underlying TextTrack object. - * @instance - * - */ - Object.defineProperty(trackElement, 'track', { - get: function get$$1() { - return track; - } - }); - - readyState = NONE; - - /** - * @listens TextTrack#loadeddata - * @fires HTMLTrackElement#load - */ - track.addEventListener('loadeddata', function () { - readyState = LOADED; - - trackElement.trigger({ - type: 'load', - target: trackElement - }); - }); - - if (IS_IE8) { - var _ret; - - return _ret = trackElement, possibleConstructorReturn(_this, _ret); - } - return _this; - } - - return HTMLTrackElement; -}(EventTarget); - -HTMLTrackElement.prototype.allowedEvents_ = { - load: 'load' -}; - -HTMLTrackElement.NONE = NONE; -HTMLTrackElement.LOADING = LOADING; -HTMLTrackElement.LOADED = LOADED; -HTMLTrackElement.ERROR = ERROR; - -/* - * This file contains all track properties that are used in - * player.js, tech.js, html5.js and possibly other techs in the future. - */ - -var NORMAL = { - audio: { - ListClass: AudioTrackList, - TrackClass: AudioTrack, - capitalName: 'Audio' - }, - video: { - ListClass: VideoTrackList, - TrackClass: VideoTrack, - capitalName: 'Video' - }, - text: { - ListClass: TextTrackList, - TrackClass: TextTrack, - capitalName: 'Text' - } -}; - -Object.keys(NORMAL).forEach(function (type) { - NORMAL[type].getterName = type + 'Tracks'; - NORMAL[type].privateName = type + 'Tracks_'; -}); - -var REMOTE = { - remoteText: { - ListClass: TextTrackList, - TrackClass: TextTrack, - capitalName: 'RemoteText', - getterName: 'remoteTextTracks', - privateName: 'remoteTextTracks_' - }, - remoteTextEl: { - ListClass: HtmlTrackElementList, - TrackClass: HTMLTrackElement, - capitalName: 'RemoteTextTrackEls', - getterName: 'remoteTextTrackEls', - privateName: 'remoteTextTrackEls_' - } -}; - -var ALL = mergeOptions(NORMAL, REMOTE); - -REMOTE.names = Object.keys(REMOTE); -NORMAL.names = Object.keys(NORMAL); -ALL.names = [].concat(REMOTE.names).concat(NORMAL.names); - -var vtt = {}; - -/** - * @file tech.js - */ - -/** - * An Object containing a structure like: `{src: 'url', type: 'mimetype'}` or string - * that just contains the src url alone. - * * `var SourceObject = {src: 'http://ex.com/video.mp4', type: 'video/mp4'};` - * `var SourceString = 'http://example.com/some-video.mp4';` - * - * @typedef {Object|string} Tech~SourceObject - * - * @property {string} src - * The url to the source - * - * @property {string} type - * The mime type of the source - */ - -/** - * A function used by {@link Tech} to create a new {@link TextTrack}. - * - * @private - * - * @param {Tech} self - * An instance of the Tech class. - * - * @param {string} kind - * `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata) - * - * @param {string} [label] - * Label to identify the text track - * - * @param {string} [language] - * Two letter language abbreviation - * - * @param {Object} [options={}] - * An object with additional text track options - * - * @return {TextTrack} - * The text track that was created. - */ -function createTrackHelper(self, kind, label, language) { - var options = arguments.length > 4 && arguments[4] !== undefined ? arguments[4] : {}; - - var tracks = self.textTracks(); - - options.kind = kind; - - if (label) { - options.label = label; - } - if (language) { - options.language = language; - } - options.tech = self; - - var track = new ALL.text.TrackClass(options); - - tracks.addTrack(track); - - return track; -} - -/** - * This is the base class for media playback technology controllers, such as - * {@link Flash} and {@link HTML5} - * - * @extends Component - */ - -var Tech = function (_Component) { - inherits(Tech, _Component); - - /** - * Create an instance of this Tech. - * - * @param {Object} [options] - * The key/value store of player options. - * - * @param {Component~ReadyCallback} ready - * Callback function to call when the `HTML5` Tech is ready. - */ - function Tech() { - var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - var ready = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : function () {}; - classCallCheck(this, Tech); - - // we don't want the tech to report user activity automatically. - // This is done manually in addControlsListeners - options.reportTouchActivity = false; - - // keep track of whether the current source has played at all to - // implement a very limited played() - var _this = possibleConstructorReturn(this, _Component.call(this, null, options, ready)); - - _this.hasStarted_ = false; - _this.on('playing', function () { - this.hasStarted_ = true; - }); - _this.on('loadstart', function () { - this.hasStarted_ = false; - }); - - ALL.names.forEach(function (name) { - var props = ALL[name]; - - if (options && options[props.getterName]) { - _this[props.privateName] = options[props.getterName]; - } - }); - - // Manually track progress in cases where the browser/flash player doesn't report it. - if (!_this.featuresProgressEvents) { - _this.manualProgressOn(); - } - - // Manually track timeupdates in cases where the browser/flash player doesn't report it. - if (!_this.featuresTimeupdateEvents) { - _this.manualTimeUpdatesOn(); - } - - ['Text', 'Audio', 'Video'].forEach(function (track) { - if (options['native' + track + 'Tracks'] === false) { - _this['featuresNative' + track + 'Tracks'] = false; - } - }); - - if (options.nativeCaptions === false || options.nativeTextTracks === false) { - _this.featuresNativeTextTracks = false; - } else if (options.nativeCaptions === true || options.nativeTextTracks === true) { - _this.featuresNativeTextTracks = true; - } - - if (!_this.featuresNativeTextTracks) { - _this.emulateTextTracks(); - } - - _this.autoRemoteTextTracks_ = new ALL.text.ListClass(); - - _this.initTrackListeners(); - - // Turn on component tap events only if not using native controls - if (!options.nativeControlsForTouch) { - _this.emitTapEvents(); - } - - if (_this.constructor) { - _this.name_ = _this.constructor.name || 'Unknown Tech'; - } - return _this; - } - - /** - * A special function to trigger source set in a way that will allow player - * to re-trigger if the player or tech are not ready yet. - * - * @fires Tech#sourceset - * @param {string} src The source string at the time of the source changing. - */ - - - Tech.prototype.triggerSourceset = function triggerSourceset(src) { - var _this2 = this; - - if (!this.isReady_) { - // on initial ready we have to trigger source set - // 1ms after ready so that player can watch for it. - this.one('ready', function () { - return _this2.setTimeout(function () { - return _this2.triggerSourceset(src); - }, 1); - }); - } - - /** - * Fired when the source is set on the tech causing the media element - * to reload. - * - * @see {@link Player#event:sourceset} - * @event Tech#sourceset - * @type {EventTarget~Event} - */ - this.trigger({ - src: src, - type: 'sourceset' - }); - }; - - /* Fallbacks for unsupported event types - ================================================================================ */ - - /** - * Polyfill the `progress` event for browsers that don't support it natively. - * - * @see {@link Tech#trackProgress} - */ - - - Tech.prototype.manualProgressOn = function manualProgressOn() { - this.on('durationchange', this.onDurationChange); - - this.manualProgress = true; - - // Trigger progress watching when a source begins loading - this.one('ready', this.trackProgress); - }; - - /** - * Turn off the polyfill for `progress` events that was created in - * {@link Tech#manualProgressOn} - */ - - - Tech.prototype.manualProgressOff = function manualProgressOff() { - this.manualProgress = false; - this.stopTrackingProgress(); - - this.off('durationchange', this.onDurationChange); - }; - - /** - * This is used to trigger a `progress` event when the buffered percent changes. It - * sets an interval function that will be called every 500 milliseconds to check if the - * buffer end percent has changed. - * - * > This function is called by {@link Tech#manualProgressOn} - * - * @param {EventTarget~Event} event - * The `ready` event that caused this to run. - * - * @listens Tech#ready - * @fires Tech#progress - */ - - - Tech.prototype.trackProgress = function trackProgress(event) { - this.stopTrackingProgress(); - this.progressInterval = this.setInterval(bind(this, function () { - // Don't trigger unless buffered amount is greater than last time - - var numBufferedPercent = this.bufferedPercent(); - - if (this.bufferedPercent_ !== numBufferedPercent) { - /** - * See {@link Player#progress} - * - * @event Tech#progress - * @type {EventTarget~Event} - */ - this.trigger('progress'); - } - - this.bufferedPercent_ = numBufferedPercent; - - if (numBufferedPercent === 1) { - this.stopTrackingProgress(); - } - }), 500); - }; - - /** - * Update our internal duration on a `durationchange` event by calling - * {@link Tech#duration}. - * - * @param {EventTarget~Event} event - * The `durationchange` event that caused this to run. - * - * @listens Tech#durationchange - */ - - - Tech.prototype.onDurationChange = function onDurationChange(event) { - this.duration_ = this.duration(); - }; - - /** - * Get and create a `TimeRange` object for buffering. - * - * @return {TimeRange} - * The time range object that was created. - */ - - - Tech.prototype.buffered = function buffered() { - return createTimeRanges(0, 0); - }; - - /** - * Get the percentage of the current video that is currently buffered. - * - * @return {number} - * A number from 0 to 1 that represents the decimal percentage of the - * video that is buffered. - * - */ - - - Tech.prototype.bufferedPercent = function bufferedPercent$$1() { - return bufferedPercent(this.buffered(), this.duration_); - }; - - /** - * Turn off the polyfill for `progress` events that was created in - * {@link Tech#manualProgressOn} - * Stop manually tracking progress events by clearing the interval that was set in - * {@link Tech#trackProgress}. - */ - - - Tech.prototype.stopTrackingProgress = function stopTrackingProgress() { - this.clearInterval(this.progressInterval); - }; - - /** - * Polyfill the `timeupdate` event for browsers that don't support it. - * - * @see {@link Tech#trackCurrentTime} - */ - - - Tech.prototype.manualTimeUpdatesOn = function manualTimeUpdatesOn() { - this.manualTimeUpdates = true; - - this.on('play', this.trackCurrentTime); - this.on('pause', this.stopTrackingCurrentTime); - }; - - /** - * Turn off the polyfill for `timeupdate` events that was created in - * {@link Tech#manualTimeUpdatesOn} - */ - - - Tech.prototype.manualTimeUpdatesOff = function manualTimeUpdatesOff() { - this.manualTimeUpdates = false; - this.stopTrackingCurrentTime(); - this.off('play', this.trackCurrentTime); - this.off('pause', this.stopTrackingCurrentTime); - }; - - /** - * Sets up an interval function to track current time and trigger `timeupdate` every - * 250 milliseconds. - * - * @listens Tech#play - * @triggers Tech#timeupdate - */ - - - Tech.prototype.trackCurrentTime = function trackCurrentTime() { - if (this.currentTimeInterval) { - this.stopTrackingCurrentTime(); - } - this.currentTimeInterval = this.setInterval(function () { - /** - * Triggered at an interval of 250ms to indicated that time is passing in the video. - * - * @event Tech#timeupdate - * @type {EventTarget~Event} - */ - this.trigger({ type: 'timeupdate', target: this, manuallyTriggered: true }); - - // 42 = 24 fps // 250 is what Webkit uses // FF uses 15 - }, 250); - }; - - /** - * Stop the interval function created in {@link Tech#trackCurrentTime} so that the - * `timeupdate` event is no longer triggered. - * - * @listens {Tech#pause} - */ - - - Tech.prototype.stopTrackingCurrentTime = function stopTrackingCurrentTime() { - this.clearInterval(this.currentTimeInterval); - - // #1002 - if the video ends right before the next timeupdate would happen, - // the progress bar won't make it all the way to the end - this.trigger({ type: 'timeupdate', target: this, manuallyTriggered: true }); - }; - - /** - * Turn off all event polyfills, clear the `Tech`s {@link AudioTrackList}, - * {@link VideoTrackList}, and {@link TextTrackList}, and dispose of this Tech. - * - * @fires Component#dispose - */ - - - Tech.prototype.dispose = function dispose() { - - // clear out all tracks because we can't reuse them between techs - this.clearTracks(NORMAL.names); - - // Turn off any manual progress or timeupdate tracking - if (this.manualProgress) { - this.manualProgressOff(); - } - - if (this.manualTimeUpdates) { - this.manualTimeUpdatesOff(); - } - - _Component.prototype.dispose.call(this); - }; - - /** - * Clear out a single `TrackList` or an array of `TrackLists` given their names. - * - * > Note: Techs without source handlers should call this between sources for `video` - * & `audio` tracks. You don't want to use them between tracks! - * - * @param {string[]|string} types - * TrackList names to clear, valid names are `video`, `audio`, and - * `text`. - */ - - - Tech.prototype.clearTracks = function clearTracks(types) { - var _this3 = this; - - types = [].concat(types); - // clear out all tracks because we can't reuse them between techs - types.forEach(function (type) { - var list = _this3[type + 'Tracks']() || []; - var i = list.length; - - while (i--) { - var track = list[i]; - - if (type === 'text') { - _this3.removeRemoteTextTrack(track); - } - list.removeTrack(track); - } - }); - }; - - /** - * Remove any TextTracks added via addRemoteTextTrack that are - * flagged for automatic garbage collection - */ - - - Tech.prototype.cleanupAutoTextTracks = function cleanupAutoTextTracks() { - var list = this.autoRemoteTextTracks_ || []; - var i = list.length; - - while (i--) { - var track = list[i]; - - this.removeRemoteTextTrack(track); - } - }; - - /** - * Reset the tech, which will removes all sources and reset the internal readyState. - * - * @abstract - */ - - - Tech.prototype.reset = function reset() {}; - - /** - * Get or set an error on the Tech. - * - * @param {MediaError} [err] - * Error to set on the Tech - * - * @return {MediaError|null} - * The current error object on the tech, or null if there isn't one. - */ - - - Tech.prototype.error = function error(err) { - if (err !== undefined) { - this.error_ = new MediaError(err); - this.trigger('error'); - } - return this.error_; - }; - - /** - * Returns the `TimeRange`s that have been played through for the current source. - * - * > NOTE: This implementation is incomplete. It does not track the played `TimeRange`. - * It only checks wether the source has played at all or not. - * - * @return {TimeRange} - * - A single time range if this video has played - * - An empty set of ranges if not. - */ - - - Tech.prototype.played = function played() { - if (this.hasStarted_) { - return createTimeRanges(0, 0); - } - return createTimeRanges(); - }; - - /** - * Causes a manual time update to occur if {@link Tech#manualTimeUpdatesOn} was - * previously called. - * - * @fires Tech#timeupdate - */ - - - Tech.prototype.setCurrentTime = function setCurrentTime() { - // improve the accuracy of manual timeupdates - if (this.manualTimeUpdates) { - /** - * A manual `timeupdate` event. - * - * @event Tech#timeupdate - * @type {EventTarget~Event} - */ - this.trigger({ type: 'timeupdate', target: this, manuallyTriggered: true }); - } - }; - - /** - * Turn on listeners for {@link VideoTrackList}, {@link {AudioTrackList}, and - * {@link TextTrackList} events. - * - * This adds {@link EventTarget~EventListeners} for `addtrack`, and `removetrack`. - * - * @fires Tech#audiotrackchange - * @fires Tech#videotrackchange - * @fires Tech#texttrackchange - */ - - - Tech.prototype.initTrackListeners = function initTrackListeners() { - var _this4 = this; - - /** - * Triggered when tracks are added or removed on the Tech {@link AudioTrackList} - * - * @event Tech#audiotrackchange - * @type {EventTarget~Event} - */ - - /** - * Triggered when tracks are added or removed on the Tech {@link VideoTrackList} - * - * @event Tech#videotrackchange - * @type {EventTarget~Event} - */ - - /** - * Triggered when tracks are added or removed on the Tech {@link TextTrackList} - * - * @event Tech#texttrackchange - * @type {EventTarget~Event} - */ - NORMAL.names.forEach(function (name) { - var props = NORMAL[name]; - var trackListChanges = function trackListChanges() { - _this4.trigger(name + 'trackchange'); - }; - - var tracks = _this4[props.getterName](); - - tracks.addEventListener('removetrack', trackListChanges); - tracks.addEventListener('addtrack', trackListChanges); - - _this4.on('dispose', function () { - tracks.removeEventListener('removetrack', trackListChanges); - tracks.removeEventListener('addtrack', trackListChanges); - }); - }); - }; - - /** - * Emulate TextTracks using vtt.js if necessary - * - * @fires Tech#vttjsloaded - * @fires Tech#vttjserror - */ - - - Tech.prototype.addWebVttScript_ = function addWebVttScript_() { - var _this5 = this; - - if (window_1.WebVTT) { - return; - } - - // Initially, Tech.el_ is a child of a dummy-div wait until the Component system - // signals that the Tech is ready at which point Tech.el_ is part of the DOM - // before inserting the WebVTT script - if (document_1.body.contains(this.el())) { - - // load via require if available and vtt.js script location was not passed in - // as an option. novtt builds will turn the above require call into an empty object - // which will cause this if check to always fail. - if (!this.options_['vtt.js'] && isPlain(vtt) && Object.keys(vtt).length > 0) { - this.trigger('vttjsloaded'); - return; - } - - // load vtt.js via the script location option or the cdn of no location was - // passed in - var script = document_1.createElement('script'); - - script.src = this.options_['vtt.js'] || 'https://vjs.zencdn.net/vttjs/0.12.4/vtt.min.js'; - script.onload = function () { - /** - * Fired when vtt.js is loaded. - * - * @event Tech#vttjsloaded - * @type {EventTarget~Event} - */ - _this5.trigger('vttjsloaded'); - }; - script.onerror = function () { - /** - * Fired when vtt.js was not loaded due to an error - * - * @event Tech#vttjsloaded - * @type {EventTarget~Event} - */ - _this5.trigger('vttjserror'); - }; - this.on('dispose', function () { - script.onload = null; - script.onerror = null; - }); - // but have not loaded yet and we set it to true before the inject so that - // we don't overwrite the injected window.WebVTT if it loads right away - window_1.WebVTT = true; - this.el().parentNode.appendChild(script); - } else { - this.ready(this.addWebVttScript_); - } - }; - - /** - * Emulate texttracks - * - */ - - - Tech.prototype.emulateTextTracks = function emulateTextTracks() { - var _this6 = this; - - var tracks = this.textTracks(); - var remoteTracks = this.remoteTextTracks(); - var handleAddTrack = function handleAddTrack(e) { - return tracks.addTrack(e.track); - }; - var handleRemoveTrack = function handleRemoveTrack(e) { - return tracks.removeTrack(e.track); - }; - - remoteTracks.on('addtrack', handleAddTrack); - remoteTracks.on('removetrack', handleRemoveTrack); - - this.addWebVttScript_(); - - var updateDisplay = function updateDisplay() { - return _this6.trigger('texttrackchange'); - }; - - var textTracksChanges = function textTracksChanges() { - updateDisplay(); - - for (var i = 0; i < tracks.length; i++) { - var track = tracks[i]; - - track.removeEventListener('cuechange', updateDisplay); - if (track.mode === 'showing') { - track.addEventListener('cuechange', updateDisplay); - } - } - }; - - textTracksChanges(); - tracks.addEventListener('change', textTracksChanges); - tracks.addEventListener('addtrack', textTracksChanges); - tracks.addEventListener('removetrack', textTracksChanges); - - this.on('dispose', function () { - remoteTracks.off('addtrack', handleAddTrack); - remoteTracks.off('removetrack', handleRemoveTrack); - tracks.removeEventListener('change', textTracksChanges); - tracks.removeEventListener('addtrack', textTracksChanges); - tracks.removeEventListener('removetrack', textTracksChanges); - - for (var i = 0; i < tracks.length; i++) { - var track = tracks[i]; - - track.removeEventListener('cuechange', updateDisplay); - } - }); - }; - - /** - * Create and returns a remote {@link TextTrack} object. - * - * @param {string} kind - * `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata) - * - * @param {string} [label] - * Label to identify the text track - * - * @param {string} [language] - * Two letter language abbreviation - * - * @return {TextTrack} - * The TextTrack that gets created. - */ - - - Tech.prototype.addTextTrack = function addTextTrack(kind, label, language) { - if (!kind) { - throw new Error('TextTrack kind is required but was not provided'); - } - - return createTrackHelper(this, kind, label, language); - }; - - /** - * Create an emulated TextTrack for use by addRemoteTextTrack - * - * This is intended to be overridden by classes that inherit from - * Tech in order to create native or custom TextTracks. - * - * @param {Object} options - * The object should contain the options to initialize the TextTrack with. - * - * @param {string} [options.kind] - * `TextTrack` kind (subtitles, captions, descriptions, chapters, or metadata). - * - * @param {string} [options.label]. - * Label to identify the text track - * - * @param {string} [options.language] - * Two letter language abbreviation. - * - * @return {HTMLTrackElement} - * The track element that gets created. - */ - - - Tech.prototype.createRemoteTextTrack = function createRemoteTextTrack(options) { - var track = mergeOptions(options, { - tech: this - }); - - return new REMOTE.remoteTextEl.TrackClass(track); - }; - - /** - * Creates a remote text track object and returns an html track element. - * - * > Note: This can be an emulated {@link HTMLTrackElement} or a native one. - * - * @param {Object} options - * See {@link Tech#createRemoteTextTrack} for more detailed properties. - * - * @param {boolean} [manualCleanup=true] - * - When false: the TextTrack will be automatically removed from the video - * element whenever the source changes - * - When True: The TextTrack will have to be cleaned up manually - * - * @return {HTMLTrackElement} - * An Html Track Element. - * - * @deprecated The default functionality for this function will be equivalent - * to "manualCleanup=false" in the future. The manualCleanup parameter will - * also be removed. - */ - - - Tech.prototype.addRemoteTextTrack = function addRemoteTextTrack() { - var _this7 = this; - - var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}; - var manualCleanup = arguments[1]; - - var htmlTrackElement = this.createRemoteTextTrack(options); - - if (manualCleanup !== true && manualCleanup !== false) { - // deprecation warning - log$1.warn('Calling addRemoteTextTrack without explicitly setting the "manualCleanup" parameter to `true` is deprecated and default to `false` in future version of video.js'); - manualCleanup = true; - } - - // store HTMLTrackElement and TextTrack to remote list - this.remoteTextTrackEls().addTrackElement_(htmlTrackElement); - this.remoteTextTracks().addTrack(htmlTrackElement.track); - - if (manualCleanup !== true) { - // create the TextTrackList if it doesn't exist - this.ready(function () { - return _this7.autoRemoteTextTracks_.addTrack(htmlTrackElement.track); - }); - } - - return htmlTrackElement; - }; - - /** - * Remove a remote text track from the remote `TextTrackList`. - * - * @param {TextTrack} track - * `TextTrack` to remove from the `TextTrackList` - */ - - - Tech.prototype.removeRemoteTextTrack = function removeRemoteTextTrack(track) { - var trackElement = this.remoteTextTrackEls().getTrackElementByTrack_(track); - - // remove HTMLTrackElement and TextTrack from remote list - this.remoteTextTrackEls().removeTrackElement_(trackElement); - this.remoteTextTracks().removeTrack(track); - this.autoRemoteTextTracks_.removeTrack(track); - }; - - /** - * Gets available media playback quality metrics as specified by the W3C's Media - * Playback Quality API. - * - * @see [Spec]{@link https://wicg.github.io/media-playback-quality} - * - * @return {Object} - * An object with supported media playback quality metrics - * - * @abstract - */ - - - Tech.prototype.getVideoPlaybackQuality = function getVideoPlaybackQuality() { - return {}; - }; - - /** - * A method to set a poster from a `Tech`. - * - * @abstract - */ - - - Tech.prototype.setPoster = function setPoster() {}; - - /** - * A method to check for the presence of the 'playsinine'
-
+
+
+
{% endif %}
diff --git a/pod/video/views.py b/pod/video/views.py index 405461cd21..756d6820cf 100644 --- a/pod/video/views.py +++ b/pod/video/views.py @@ -1499,14 +1499,16 @@ def get_all_views_count(v_id, date_filter=date.today()): return all_views -# Retourne une ou plusieurs videos et le titre de -# (theme, ou video ou channel ou videos pour toutes) -# selon la réference du slug donnée -# (video ou channel ou theme ou videos pour toutes les videos) def get_videos(p_slug, target, p_slug_t=None): + """Retourne une ou plusieurs videos selon le slug donné. + Renvoi vidéo/s et titre de + (theme, ou video ou channel ou videos pour toutes) + selon la réference du slug donnée + (video ou channel ou theme ou videos pour toutes les videos) + """ videos = [] - title = _("Pod video view statistics") + title = _("Pod video viewing statistics") if target.lower() == "video": video_founded = Video.objects.filter(slug=p_slug).first() From ea150d74431f8c977722a8cb965ad321f5cfd0df Mon Sep 17 00:00:00 2001 From: Olivier Bado Date: Fri, 29 Jan 2021 16:23:50 +0100 Subject: [PATCH 119/145] Update test with new strings --- pod/video/tests/test_stats_view.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pod/video/tests/test_stats_view.py b/pod/video/tests/test_stats_view.py index cef1bb97c5..7b07e79db1 100644 --- a/pod/video/tests/test_stats_view.py +++ b/pod/video/tests/test_stats_view.py @@ -135,7 +135,7 @@ def test_stats_view_GET_request_videos(self): response = self.client.get(stat_url_videos, {"from": "videos"}) self.assertContains( response, - b"Pod video view statistics", + b"Pod video viewing statistics", status_code=200) @skipUnless(USE_STATS_VIEW, "Require acitvate URL video_stats_view") From 2f9a8af50c6e16a0c53bd5a4d57ff80c2e4085fa Mon Sep 17 00:00:00 2001 From: Olivier Bado Date: Fri, 29 Jan 2021 17:20:38 +0100 Subject: [PATCH 120/145] change video itemprop format to comply with schema.org (@see https://schema.org/Date) --- pod/video/templates/videos/video.html | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/pod/video/templates/videos/video.html b/pod/video/templates/videos/video.html index a484ea0b10..5ede401f46 100644 --- a/pod/video/templates/videos/video.html +++ b/pod/video/templates/videos/video.html @@ -84,14 +84,15 @@
- - - - - - - - + + {# uploadDate doit être au format 'c' - ISO 8601 (yyyy-mm-dd) #} + + + + + + + {%if channel %}

{{channel.title}}{% if request.user in channel.owners.all %} {% trans "Edit the channel"%}{%endif%}

From 93be6ca371a5740450d9455975dbfaf1aec2a714 Mon Sep 17 00:00:00 2001 From: Eliam LOTONGA Date: Tue, 2 Feb 2021 11:15:40 +0100 Subject: [PATCH 121/145] using window api scrollTo instead of element api scrollIntoView --- pod/video/static/js/comment-script.js | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/pod/video/static/js/comment-script.js b/pod/video/static/js/comment-script.js index c5015a0690..d6f9ca31d9 100644 --- a/pod/video/static/js/comment-script.js +++ b/pod/video/static/js/comment-script.js @@ -491,13 +491,31 @@ function delete_comment_child_DOM(comment, comment_top_parent_id, is_child) { } } +/*********** Get Html element his position ************ + * ****************************************************/ +function getElementPosition(element) { + let xPosition = 0; + let yPosition = 0; + // get body padding top if exists that will be + let bodyPaddingTop = getComputedStyle(document.body)['padding-top']; + + while(element) { + xPosition += (element.offsetLeft - element.scrollLeft + element.clientLeft); + yPosition += (element.offsetTop - element.scrollTop + element.clientTop); + element = element.offsetParent; + } + // subtract the padding top from the body to the yPosition to get closer to the exact value + return { x: xPosition, y: (yPosition - bodyPaddingTop) }; +} + /*********** Scroll to a specific comment ************* * ****************************************************/ function scrollToComment(targetComment) { - targetComment.scrollIntoView({ - behavior: "smooth", - block: "center", - inline: "nearest" + let currentElementPosition = getElementPosition(targetComment); + window.scrollTo({ + top: currentElementPosition.y, + left: currentElementPosition.x, + behavior: "smooth" }); let htmlTarget = targetComment.querySelector(".comment_content"); if (htmlTarget.classList.contains("scroll_to")) @@ -511,6 +529,7 @@ function scrollToComment(targetComment) { } + /****** Add the owner of parent comment as a tag ****** ******************************************************/ function add_user_tag(comment_value, parent_comment) { From ba577c973d439412e12089f3beff1428b9806ef0 Mon Sep 17 00:00:00 2001 From: Eliam LOTONGA Date: Tue, 2 Feb 2021 11:38:16 +0100 Subject: [PATCH 122/145] Prevent undefined value --- pod/video/static/js/comment-script.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pod/video/static/js/comment-script.js b/pod/video/static/js/comment-script.js index d6f9ca31d9..6dd59f6f89 100644 --- a/pod/video/static/js/comment-script.js +++ b/pod/video/static/js/comment-script.js @@ -497,7 +497,9 @@ function getElementPosition(element) { let xPosition = 0; let yPosition = 0; // get body padding top if exists that will be - let bodyPaddingTop = getComputedStyle(document.body)['padding-top']; + let bodyPaddingTop = Number.parseInt( + getComputedStyle(document.body)['padding-top']??0 + ); while(element) { xPosition += (element.offsetLeft - element.scrollLeft + element.clientLeft); From 3cdb1ff5c7c86e7ea77fdb6996079f01cff10d2f Mon Sep 17 00:00:00 2001 From: gcondess Date: Thu, 4 Feb 2021 15:26:04 +0100 Subject: [PATCH 123/145] Restore user notification for obsolete videos --- pod/video/management/commands/check_obsolete_videos.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pod/video/management/commands/check_obsolete_videos.py b/pod/video/management/commands/check_obsolete_videos.py index 4d7e4a1ab2..16d2a19420 100644 --- a/pod/video/management/commands/check_obsolete_videos.py +++ b/pod/video/management/commands/check_obsolete_videos.py @@ -94,7 +94,7 @@ def get_video_treatment_and_notify_user(self): sites=get_current_site( settings.SITE_ID)) for video in videos: - # self.notify_user(video, step_day) + self.notify_user(video, step_day) if ( USE_ESTABLISHMENT and MANAGERS and @@ -370,7 +370,7 @@ def get_manager_emails(self, video): video_estab = video.owner.owner.establishment.lower() return dict(MANAGERS)[video_estab] else: - return CONTACT_US_EMAIL + return dict(CONTACT_US_EMAIL) def write_in_csv(self, vid, type): file = '%s/%s.csv' % (settings.LOG_DIRECTORY, type) From 2ec8b750a15c1ab3923b80c7b9dd251430353af3 Mon Sep 17 00:00:00 2001 From: gcondess Date: Thu, 4 Feb 2021 16:24:13 +0100 Subject: [PATCH 124/145] Active chapters on enriched video version --- .../enrichment/video_enrichment-iframe.html | 23 +++++++++++++++--- .../enrichment/video_enrichment.html | 24 +++++++++++++++---- 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/pod/enrichment/templates/enrichment/video_enrichment-iframe.html b/pod/enrichment/templates/enrichment/video_enrichment-iframe.html index b11bc31458..452fd2b749 100644 --- a/pod/enrichment/templates/enrichment/video_enrichment-iframe.html +++ b/pod/enrichment/templates/enrichment/video_enrichment-iframe.html @@ -22,14 +22,31 @@

- {% for track in video.track_set.all%} - - {%endfor%} + {% for track in video.track_set.all%} + + {%endfor%}
{% csrf_token %}
+ + {% if video.chapter_set.all %} + +
    + {% for chapter in video.chapter_set.all %} +
  • + {{chapter.title}} +
  • + {% endfor %} +
+ {% endif %} + {% endif %} {% endblock video-element %} diff --git a/pod/enrichment/templates/enrichment/video_enrichment.html b/pod/enrichment/templates/enrichment/video_enrichment.html index 0eb5d322c4..8bf373cf4e 100644 --- a/pod/enrichment/templates/enrichment/video_enrichment.html +++ b/pod/enrichment/templates/enrichment/video_enrichment.html @@ -8,7 +8,6 @@ {% endblock page_extra_head %} - {% block breadcrumbs %}{{ block.super }} {% endblock %} @@ -29,15 +28,32 @@

- {% for track in video.track_set.all%} - - {%endfor%} + + {% for track in video.track_set.all %} + + {% endfor %}
{% csrf_token %}
+ {% if video.chapter_set.all %} + +
    + {% for chapter in video.chapter_set.all %} +
  • + {{chapter.title}} +
  • + {% endfor %} +
+ {% endif %} +

{% if video.licence %}{% endif %} {{video.title|title}} {% if video.date_evt %}[{{ video.date_evt }}]{% endif %} From b364051802bbabbe33e4e3faac64b3273f21c743 Mon Sep 17 00:00:00 2001 From: gcondess Date: Thu, 4 Feb 2021 19:10:33 +0100 Subject: [PATCH 125/145] Fix check_obsolete_videos user notification --- pod/video/management/commands/check_obsolete_videos.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pod/video/management/commands/check_obsolete_videos.py b/pod/video/management/commands/check_obsolete_videos.py index 16d2a19420..58e1c7d5ad 100644 --- a/pod/video/management/commands/check_obsolete_videos.py +++ b/pod/video/management/commands/check_obsolete_videos.py @@ -370,7 +370,7 @@ def get_manager_emails(self, video): video_estab = video.owner.owner.establishment.lower() return dict(MANAGERS)[video_estab] else: - return dict(CONTACT_US_EMAIL) + return CONTACT_US_EMAIL def write_in_csv(self, vid, type): file = '%s/%s.csv' % (settings.LOG_DIRECTORY, type) From d8295002bfb2d4bb9ba2dd241070ee0055f1124a Mon Sep 17 00:00:00 2001 From: Eliam LOTONGA Date: Sat, 6 Feb 2021 16:27:18 +0100 Subject: [PATCH 126/145] Fix bug click on thumbnail, Indent code properly --- .../templates/podfile/list_folder_files.html | 534 ++++++++++++------ 1 file changed, 366 insertions(+), 168 deletions(-) diff --git a/pod/podfile/templates/podfile/list_folder_files.html b/pod/podfile/templates/podfile/list_folder_files.html index 453280f0c0..db9ddc4b46 100644 --- a/pod/podfile/templates/podfile/list_folder_files.html +++ b/pod/podfile/templates/podfile/list_folder_files.html @@ -1,188 +1,386 @@ {% load i18n static podfile_filters %} - - - - -
- {% for file in folder.get_all_files %} -
+ +
+ {% for file in folder.get_all_files %} +
-
- {% if request.user == file.created_by or request.user.is_superuser or perms.podfile.change_customfilemodel or perms.podfile.change_customimagemodel or request.user in folder.users.all %} - +
+ +
+ + {% endif %} +
+ {% if file.class_name.lower == "customimagemodel" and file.file_exist %} + {% if type.lower == "image" and file.class_name.lower == "customimagemodel" %} + + Card image + + {% else %} + Card image {% endif %} -
- {% if file.class_name == "CustomImageModel" and file.file_exist %} - {% if type == "image" and file.class_name == "CustomImageModel" %} - Card image - {% else %} - Card image - {% endif %} {% else %} {% with file.file.name|icon_exists as icon %} - {% if type == "file" and file.class_name == "CustomFileModel"%} - Card image - {% else %} - Card image - {% endif%} + {% if type.lower == "file" and file.class_name.lower == "customfilemodel" or type.lower == "image" and file.class_name.lower == "customimagemodel" %} + + Card image + + {% else %} + Card image + {% endif%} {%endwith%} {% endif %} -
-

- {% if type %} +

+

+ {% if type %} {% if type == "image" and file.class_name == "CustomImageModel" %} - - {{file.name}} - - {% elif type == "file" and file.class_name == "CustomFileModel"%} - - {{file.name}} - - {% else %} - {{file.name}} - {% endif %} + + {{file.name}} + + {% elif type == "file" and file.class_name == "CustomFileModel"%} + + {{file.name}} + + {% else %} + {{file.name}} + {% endif %} {% else %} - - {{file.name}} - - {% endif %} -

-
- + + {{file.name}} + + {% endif %} +

+
- -
- {% endfor %} -
- +
+ {% endfor %} +

- - - - - - - - - From 6eb125ea9f96c16fb5d5ee430ad6b548689fec0d Mon Sep 17 00:00:00 2001 From: Eliam LOTONGA Date: Sat, 6 Feb 2021 16:29:00 +0100 Subject: [PATCH 127/145] unnecessary code --- pod/podfile/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pod/podfile/views.py b/pod/podfile/views.py index e1dbb00d08..66fdd63d8b 100644 --- a/pod/podfile/views.py +++ b/pod/podfile/views.py @@ -687,7 +687,7 @@ def user_folders(request): if not request.user.is_superuser: user_folder = user_folder.filter(owner=request.user) - user_folder = user_folder.values(*list(VALUES_LIST)) + user_folder = user_folder.values(*VALUES_LIST) search = request.GET.get('search', "") current_fold = json.loads( From 550a63a42dfed8d29d1b38e55d3e376c883d6c05 Mon Sep 17 00:00:00 2001 From: Eliam LOTONGA Date: Sat, 6 Feb 2021 16:32:16 +0100 Subject: [PATCH 128/145] Add 'folder_name' css class, fix and indent html --- pod/podfile/templates/podfile/userfolder.html | 272 ++++++++++++++---- 1 file changed, 209 insertions(+), 63 deletions(-) diff --git a/pod/podfile/templates/podfile/userfolder.html b/pod/podfile/templates/podfile/userfolder.html index 52cd5d6b0e..3181305a6a 100644 --- a/pod/podfile/templates/podfile/userfolder.html +++ b/pod/podfile/templates/podfile/userfolder.html @@ -1,74 +1,220 @@ {% load i18n static %} -
-
- - - - {% trans "My folders"%} - - -
-
- -
+
+
+ + + + + {% trans "My folders"%} + + +
+
+
- - {% if user.is_superuser %}{%trans "SuperUser mode : the folders of all users are listed (the owner is noted in brackets)" %}X{%endif%} -
-
- {% if share_folder.count > 0 or share_folder_user.count > 0 %} -