diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000000..a043666029 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,41 @@ +module.exports = { + "env": { + "browser": true, + "commonjs": true, + "es2021": true, + "node": true + }, + "extends": "eslint:recommended", + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "script" + }, + "rules": { + "indent": [ + "error", + 2, + {"SwitchCase": 1} + ], + "linebreak-style": [ + "error", + "unix" + ], + "semi": [ + "error", + "always", + ], + /*"camelcase": ["warn", {"ignoreGlobals": true}],*/ + /* First capital letter is reserved for "new" Objects. */ + "new-cap": ["warn", { "capIsNew": true }] + }, + /* functions and Objects that will not trigger a "not defined" error. */ + "globals": { + "Cookies": "readonly", + "gettext": "readonly", + "interpolate": "readonly", + "bootstrap": "readonly", + "videojs": "readonly", + "send_form_data": "writable", + "showalert": "writable", + } +}; diff --git a/.github/workflows/code_formatting.yml b/.github/workflows/code_formatting.yml index a1ee4ea2d3..bf7a7f4447 100644 --- a/.github/workflows/code_formatting.yml +++ b/.github/workflows/code_formatting.yml @@ -17,9 +17,11 @@ jobs: git config user.email github-actions@github.com git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }} - # Prettify js code with prettier + # Prettify js+css code with prettier - name: Running prettier - run: npx prettier --write pod/*/static/**/*.js + run: | + npx prettier --write pod/*/static/**/*.js + npx prettier --write pod/*/static/**/*.css - name: Check for modified files id: prettier-git-check diff --git a/.pa11yci b/.pa11yci index 2ff160e506..8bb2b412b0 100644 --- a/.pa11yci +++ b/.pa11yci @@ -16,7 +16,7 @@ "http://localhost:9090/video/0001-video-test/", "http://localhost:9090/live/events/" ], - "todo":[ + "todo": [ "These urls are not A11y compliant yet, and need more work from 3rd party", "http://localhost:9090/video/stats_view/videos/" ] diff --git a/.stylelintrc.json b/.stylelintrc.json new file mode 100644 index 0000000000..40db42c668 --- /dev/null +++ b/.stylelintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "stylelint-config-standard" +} diff --git a/CONFIGURATION_FR.md b/CONFIGURATION_FR.md index ceedd88995..0afa7f1d62 100644 --- a/CONFIGURATION_FR.md +++ b/CONFIGURATION_FR.md @@ -6,7 +6,7 @@ La plateforme Esup-Pod se base sur le framework Django écrit en Python.
-Elle supporte les versions 3.7, 3.8 et 3.9 de Python.
+Elle est compatible avec les versions 3.8, 3.9 et 3.10 de Python.
**Django Version : 3.2 LTS**
@@ -72,6 +72,30 @@ Voici les configurations des applications tierces utilisées par Esup-Pod.
>> Système d’authentification OpenID Connect
>> [https://mozilla-django-oidc.readthedocs.io/en/stable/installation.html]()
+ - `pwa` + + > valeur par défaut : `1.1.0` + + >> Mise en place du mode PWA grâce à l'application Django-pwa
+ >> Voici la configuration par défaut pour Pod, vous pouvez surcharger chaque variable dans votre fichier de configuration.
+ >> PWA_APP_NAME = "Pod"
+ >> PWA_APP_DESCRIPTION = _(
+ >> "Pod is aimed at users of our institutions, by allowing the publication of "
+ >> "videos in the fields of research (promotion of platforms, etc.), training "
+ >> "(tutorials, distance training, student reports, etc.), institutional life (video "
+ >> "of events), offering several days of content."
+ >> )
+ >> PWA_APP_THEME_COLOR = "#0A0302"
+ >> PWA_APP_BACKGROUND_COLOR = "#ffffff"
+ >> PWA_APP_DISPLAY = "standalone"
+ >> PWA_APP_SCOPE = "/"
+ >> PWA_APP_ORIENTATION = "any"
+ >> PWA_APP_START_URL = "/"
+ >> PWA_APP_STATUS_BAR_COLOR = "default"
+ >> PWA_APP_DIR = "ltr"
+ >> PWA_APP_LANG = "fr-FR"
+ >> Pour en savoir plus : [https://github.com/silviolleite/django-pwa]()
+ - `rest_framework` > valeur par défaut : `3.14.0` @@ -711,17 +735,17 @@ Vous pouvez tout à fait rajouter des langues comme vous le souhaitez. Il faudra >> Si True, permet de cacher l’onglet chaine dans la barre de menu du haut.
- - `HIDE_DISCIPLINES` + - `HIDE_CURSUS` > valeur par défaut : `False` - >> Si True, permet de ne pas afficher les disciplines dans la colonne de droite
+ >> Si True, permet de ne pas afficher les cursus dans la colonne de droite
- - `HIDE_CURSUS` + - `HIDE_DISCIPLINES` > valeur par défaut : `False` - >> Si True, permet de ne pas afficher les cursus dans la colonne de droite
+ >> Si True, permet de ne pas afficher les disciplines dans la colonne de droite
- `HIDE_LANGUAGE_SELECTOR` @@ -954,7 +978,7 @@ Vous pouvez tout à fait rajouter des langues comme vous le souhaitez. Il faudra - `TRANSCRIPTION_STT_SENTENCE_MAX_LENGTH` - > valeur par défaut : `3` + > valeur par défaut : `2` >> Temps en secondes maximum pour une phrase lors de la transcription avec l’outil STT.
@@ -1401,12 +1425,6 @@ Vous pouvez tout à fait rajouter des langues comme vous le souhaitez. Il faudra Application Import_video permettant d'importer des vidéos externes dans Pod.
Mettre `USE_IMPORT_VIDEO` à True pour activer cette application.
- - `RESTRICT_EDIT_IMPORT_VIDEO_ACCESS_TO_STAFF_ONLY` - - > valeur par défaut : `True` - - >> Seuls les utilisateurs "staff" pourront importer des vidéos
- - `MAX_UPLOAD_SIZE_ON_IMPORT` > valeur par défaut : `4` @@ -1414,6 +1432,12 @@ Mettre `USE_IMPORT_VIDEO` à True pour activer cette application.
>> Taille maximum en Go des fichiers vidéos qui peuvent être importés sur la plateforme
>> via l'application import_video (0 = pas de taille maximum).
+ - `RESTRICT_EDIT_IMPORT_VIDEO_ACCESS_TO_STAFF_ONLY` + + > valeur par défaut : `True` + + >> Seuls les utilisateurs "staff" pourront importer des vidéos
+ - `USE_IMPORT_VIDEO` > valeur par défaut : `True` @@ -1475,18 +1499,18 @@ Mettre `USE_IMPORT_VIDEO` à True pour activer cette application.
>> Permet de lancer automatiquement l’enregistrement sur l’interface utilisée (wowza, ) sur le broadcaster et spécifié par `BROADCASTER_PILOTING_SOFTWARE`
- - `EVENT_GROUP_ADMIN` - - > valeur par défaut : `event admin` - - >> Permet de préciser le nom du groupe dans lequel les utilisateurs peuvent planifier un évènement sur plusieurs jours.
- - `EVENT_CHECK_MAX_ATTEMPT` > valeur par défaut : `10` >> Nombre de tentatives maximum pour vérifier la présence / taille d'un fichier sur le filesystem
+ - `EVENT_GROUP_ADMIN` + + > valeur par défaut : `event admin` + + >> Permet de préciser le nom du groupe dans lequel les utilisateurs peuvent planifier un évènement sur plusieurs jours.
+ - `HEARTBEAT_DELAY` > valeur par défaut : `45` @@ -1784,13 +1808,6 @@ Mettre `USE_MEETING` à True pour activer cette application.
Application Playlist pour la gestion des playlists.
Mettre `USE_PLAYLIST` à True pour activer cette application.
- - `DEFAULT_PLAYLIST_THUMBNAIL` - - > valeur par défaut : `/static/playlist/img/default-playlist.svg` - - >> Image par défaut affichée comme poster ou vignette, utilisée pour présenter la playlist.
- >> Cette image doit se situer dans le répertoire `static`.
- - `COUNTDOWN_PLAYLIST_PLAYER` > valeur par défaut : `0` @@ -1798,6 +1815,13 @@ Mettre `USE_PLAYLIST` à True pour activer cette application.
>> Compte à rebours utilisé entre chaque vidéo lors de la lecture d'une playlist en lecture automatique.
>> Le compte à rebours n'est pas présent s'il est à 0.
+ - `DEFAULT_PLAYLIST_THUMBNAIL` + + > valeur par défaut : `/static/playlist/img/default-playlist.svg` + + >> Image par défaut affichée comme poster ou vignette, utilisée pour présenter la playlist.
+ >> Cette image doit se situer dans le répertoire `static`.
+ - `USE_FAVORITES` > valeur par défaut : `True` @@ -1840,6 +1864,12 @@ Mettre `USE_PLAYLIST` à True pour activer cette application.
### Configuration application progressive_web_app + - `USE_NOTIFICATIONS` + + > valeur par défaut : `True` + + >> Activation des notifications, attention, elles sont actives par défaut.
+ - `WEBPUSH_SETTINGS` > valeur par défaut : @@ -1998,6 +2028,13 @@ Mettre `USE_PLAYLIST` à True pour activer cette application.
>> Activer les commentaires au niveau de la plateforme
+ - `CACHE_VIDEO_DEFAULT_TIMEOUT` + + > valeur par défaut : `600` + + + >> Temps en seconde de conservation des données de l'application video
+ - `CHANNEL_FORM_FIELDS_HELP_TEXT` > valeur par défaut : `` @@ -2569,6 +2606,18 @@ Attention, il faut configurer Celery pour l’envoi des instructions pour l'enco >> Si True, un courriel est envoyé aux managers et à l’auteur (si DEBUG est à False) à la fin de la transcription
+ - `ENCODE_STUDIO` + + > valeur par défaut : `start_encode_studio` + + >> Fonction appelée pour lancer l’encodage du studio (merge and cut).
+ + - `ENCODE_VIDEO` + + > valeur par défaut : `start_encode` + + >> Fonction appelée pour lancer l’encodage des vidéos direct par thread ou distant par celery
+ - `ENCODING_CHOICES` > valeur par défaut : `()` @@ -2587,6 +2636,14 @@ Attention, il faut configurer Celery pour l’envoi des instructions pour l'enco >> >> ``` + - `ENCODING_TRANSCODING_CELERY_BROKER_URL` + + > valeur par défaut : `False` + + + >> Il faut renseigner l'url du redis sur lequel Celery va chercher les ordres d'encodage et de transcription
+ >> par exemple : "redis://redis:6379/7"
+ - `FORMAT_CHOICES` > valeur par défaut : `()` @@ -2605,17 +2662,12 @@ Attention, il faut configurer Celery pour l’envoi des instructions pour l'enco >> >> ``` - - `ENCODE_STUDIO` - - > valeur par défaut : `start_encode_studio` - - >> Fonction appelée pour lancer l’encodage du studio (merge and cut).
+ - `USE_DISTANT_ENCODING_TRANSCODING` - - `ENCODE_VIDEO` + > valeur par défaut : `False` - > valeur par défaut : `start_encode` - >> Fonction appelée pour lancer l’encodage des vidéos direct par thread ou distant par celery
+ >> Si True, active l'encodage et la transcription sur un environnement distant via redis+celery
- `VIDEO_RENDITIONS` @@ -2658,21 +2710,6 @@ Attention, il faut configurer Celery pour l’envoi des instructions pour l'enco >> >> ``` - - `ENCODING_TRANSCODING_CELERY_BROKER_URL` - - > valeur par défaut : `False` - - - >> Il faut renseigner l'url du redis sur lequel Celery va chercher les ordres d'encodage et de transcription
- >> par exemple : "redis://redis:6379/7"
- - - `USE_DISTANT_ENCODING_TRANSCODING` - - > valeur par défaut : `False` - - - >> Si True, active l'encodage et la transcription sur un environnement distant via redis+celery
- ### Configuration application search - `ES_INDEX` diff --git a/dockerfile-dev-with-volumes/pod-back/Dockerfile b/dockerfile-dev-with-volumes/pod-back/Dockerfile index e0d9013d7d..6f509e568c 100755 --- a/dockerfile-dev-with-volumes/pod-back/Dockerfile +++ b/dockerfile-dev-with-volumes/pod-back/Dockerfile @@ -19,7 +19,7 @@ FROM $PYTHON_VERSION # TODO #FROM harbor.urba.univ-lille.fr/store/python:3.7-buster -RUN apt-get clean && apt-get update && apt-get install -y netcat +RUN apt-get clean && apt-get update && apt-get install -y netcat && apt-get install -y gettext WORKDIR /usr/src/app diff --git a/dockerfile-dev-with-volumes/pod/Dockerfile b/dockerfile-dev-with-volumes/pod/Dockerfile index 68f8d08065..a716a6ff92 100755 --- a/dockerfile-dev-with-volumes/pod/Dockerfile +++ b/dockerfile-dev-with-volumes/pod/Dockerfile @@ -23,7 +23,8 @@ RUN apt-get clean && apt-get update \ && apt-get install -y netcat \ ffmpeg \ ffmpegthumbnailer \ - imagemagick + imagemagick \ + gettext WORKDIR /usr/src/app diff --git a/pod/authentication/backends.py b/pod/authentication/backends.py index 1b2ee527a7..57bda9b203 100644 --- a/pod/authentication/backends.py +++ b/pod/authentication/backends.py @@ -20,8 +20,9 @@ def is_staff_affiliation(affiliation): class ShibbBackend(ShibbolethRemoteUserBackend): def authenticate(self, request, remote_user, shib_meta): """ - The username passed as ``remote_user`` is considered trusted. Use the - username to get or create the user. + Username passed as `remote_user` is considered trusted. + + Use the username to get or create the user. """ if not remote_user: return diff --git a/pod/bbb/static/css/bbb.css b/pod/bbb/static/css/bbb.css index 521a6cc6e7..cd388c7916 100644 --- a/pod/bbb/static/css/bbb.css +++ b/pod/bbb/static/css/bbb.css @@ -1,15 +1,22 @@ +/** + * Esup-Pod BBB styles + */ + #bbb_meetings_list .card-header { background: var(--color-black-alpha); - padding: 0 .25rem; + padding: 0 0.25rem; z-index: 9; } -#bbb_meetings_list .card-header .text-muted{ - color: var(--color-white) !important; + +#bbb_meetings_list .card-header .text-muted { + color: var(--color-white) !important; } + /* To always see publish button */ -#bbb_meetings_list .infinite-item .card-body{ -height: auto !important; +#bbb_meetings_list .infinite-item .card-body { + height: auto !important; +} + +#bbb_meetings_list .infinite-item .card-body footer a { + width: 100% !important; } -#bbb_meetings_list .infinite-item .card-body footer a{ -width: 100% !important; -} \ No newline at end of file diff --git a/pod/chapter/static/css/chapters.css b/pod/chapter/static/css/chapters.css index 0530205422..b5d17425b6 100644 --- a/pod/chapter/static/css/chapters.css +++ b/pod/chapter/static/css/chapters.css @@ -1,90 +1,107 @@ +/** + * Esup-Pod Chapter styles + */ /*** Table scroll ***/ table.scroll { - width: 100%; - border-collapse: collapse; - border-spacing: 0; + width: 100%; + border-collapse: collapse; + border-spacing: 0; } + table.scroll .btn-sm, table.scroll .btn-group-sm > .btn { - border-radius: 3px; - font-size: 12px; - line-height: 0.5; - padding: 5px 10px; + border-radius: 3px; + font-size: 12px; + line-height: 0.5; + padding: 5px 10px; } + table.scroll tbody, table.scroll thead { - display: block; + display: block; } + table.scroll thead tr th { - height: 27px; - line-height: 27px; - text-align: left; + height: 27px; + line-height: 27px; + text-align: left; } + table.scroll tbody { - max-height: 200px; - overflow-y: auto; - overflow-x: hidden; + max-height: 200px; + overflow-y: auto; + overflow-x: hidden; } + table.scroll tbody td, table.scroll thead th { - width: 10%; - height: 20px; - white-space: nowrap; - line-height: 20px; + width: 10%; + height: 20px; + white-space: nowrap; + line-height: 20px; } + table.scroll thead th.chapter_title, table.scroll thead th.chapter_time_start, table.scroll thead th.chapter_time_end { - white-space: nowrap; + white-space: nowrap; } + #list_chapter .panel-heading, #form_chapter .panel-heading { - border-bottom: 1px solid transparent; - border-top-left-radius: 3px; - border-top-right-radius: 3px; - padding: 4px 15px; + border-bottom: 1px solid transparent; + border-top-left-radius: 3px; + border-top-right-radius: 3px; + padding: 4px 15px; } + #form_new { - padding-left: 12px; - padding-bottom: 10px; + padding-left: 12px; + padding-bottom: 10px; } + #form_chapter, #form_chapter_import, #form_vtt { - padding: 10px; + padding: 10px; } + form#form_chapter, form#form_chapter_import { - background-color: rgba(0, 0, 0, .05); + background-color: rgba(0 0 0 0.05); } + .form-group.row p { - padding: 2px; + padding: 2px; } /** Filepicker override **/ div.file-picker-overlay, div.file-picker, ul.css-tabs a { - box-sizing: content-box; + box-sizing: content-box; } + textarea#id_description { - line-height: normal; + line-height: normal; } + div.file-picker-overlay input[type="text"], -div.file-picker-overlay textarea, +div.file-picker-overlay textarea, div.file-picker-overlay select { - border: 1px solid #ccc; - border-radius: 4px; - padding: 5px 6px; - margin-top: 0; + border: 1px solid #ccc; + border-radius: 4px; + padding: 5px 6px; + margin-top: 0; } + .file-list a:hover { - cursor: pointer; - color: #007bff !important; + cursor: pointer; + color: #007bff !important; } /** Bootstrap override **/ .card-title { - margin: .45rem; -} \ No newline at end of file + margin: 0.45rem; +} diff --git a/pod/chapter/static/css/videojs-chapters.css b/pod/chapter/static/css/videojs-chapters.css index f8d2d1c38e..e196bdd172 100644 --- a/pod/chapter/static/css/videojs-chapters.css +++ b/pod/chapter/static/css/videojs-chapters.css @@ -1,3 +1,7 @@ +/** + * Esup-Pod video-js chapter styles + */ + .chapters-list.inactive, .chapters-list.active { position: absolute; @@ -5,12 +9,10 @@ top: 0; height: 100%; width: 20%; - background-color: rgba(43, 51, 63, .7); + background-color: rgba(43 51 63 0.7); overflow-y: auto; - /*border: 2px solid black;*/ cursor: default; z-index: 2; - } .chapters-list.inactive { @@ -45,9 +47,17 @@ display: list-item; list-style-type: none; text-align: -webkit-match-parent; - /*background-color: rgba(0, 0, 0, 0.8);*/ margin: 0 3px; - border-top: 1px solid rgba(0, 0, 0, .8); + border-top: 1px solid rgba(0 0 0 0.8); +} + +.chapters-list ol li a { + display: block; + padding: 0.7rem 1rem; + transition: 0.3s; + color: #fff; + text-align: left; + font-size: 1.3em; } .chapters-list ol li a:hover, @@ -58,15 +68,6 @@ cursor: pointer; } -.chapters-list ol li a { - display: block; - padding: .7rem 1rem; - transition: .3s; - color: #fff; - text-align: left; - font-size: 1.3em; -} - #chapters { display: none; } diff --git a/pod/completion/static/css/caption_maker.css b/pod/completion/static/css/caption_maker.css index a599853de7..4b7271ef65 100644 --- a/pod/completion/static/css/caption_maker.css +++ b/pod/completion/static/css/caption_maker.css @@ -1,356 +1,245 @@ +/** + * Esup-Pod caption_maker styles + */ + #videoElm { - display: block; - border: solid 1px #999; - width: 100%; - min-height: 297px; + display: block; + border: solid 1px #999; + width: 100%; + min-height: 297px; } + .videoError { - display: block; - background-color: #999; - width: 100%; - min-height: 297px; - color: white; - text-align: center; - padding: 12px; + display: block; + background-color: #999; + width: 100%; + min-height: 297px; + color: white; + text-align: center; + padding: 12px; } #videoError { - display: none; + display: none; } + #captionTitle { - padding-top: 4px; - padding-bottom: 4px; - min-height: 32px; + padding-top: 4px; + padding-bottom: 4px; + min-height: 32px; } #textCaptionEntry { - /* - width: 100%; - height: auto; - line-height: 19px; - border: 1px solid #999; - padding: 3px 8px 5px; - min-height: 50px; - overflow: hidden; - */ - text-align: center; + text-align: center; } #textCaptionEntry.playing { - background-color: var(--pod-background-dark); - color: white; + background-color: var(--pod-background-dark); + color: white; } -/*textarea { - line-height: 16px; - font-size: 14px; - resize: none; - border: 1px solid #ccc; - border-radius: 4px; -}*/ - .shortcutKey { - display: inline-block; - border: 2px solid #222; - border-radius: 6px; - padding: 3px 8px; - font-weight: 600; - margin-right: 8px; - margin-left: 8px; - min-width: 35px; - text-align: center; + display: inline-block; + border: 2px solid #222; + border-radius: 6px; + padding: 3px 8px; + font-weight: 600; + margin-right: 8px; + margin-left: 8px; + min-width: 35px; + text-align: center; } .shortcutKey svg { - transform: translateY(-2px); + transform: translateY(-2px); } /* the list of captions */ -.gray_no_video{ - display: flex; - flex-wrap: wrap; - border-top: 1px solid #E1E1E1; +.gray_no_video { + display: flex; + flex-wrap: wrap; + border-top: 1px solid #e1e1e1; } .videoSection { - float: left; - min-height: 425px; - width: 49%; - padding: 24px 0px; - padding-right: 24px; + float: left; + min-height: 425px; + width: 49%; + padding: 24px 0; + padding-right: 24px; } .caption_content { - border-left: 1px solid #E1E1E1; - display: inline-block; - width: 49%; - /* overflow-y: auto; */ -} - -.caption_content .caption_title{ - display: flex; - position: relative; -} -.caption_content svg{ - width: 20px; - color: var(--secondary); - margin-left: 4px; -} -.caption_content .caption_title svg:hover + .help_text{ - display: block; -} -.caption_content .help_text{ - display: none; - position: absolute; - background: var(--pod-primary); - color: #fff; - width: calc(100% - 110px); - right: 0; - padding: .4em .8em; - border-radius: 4px; - box-shadow: 4px 4px 10px #ccc; - font-size: 15px; - line-height: 1.4; + border-left: 1px solid #e1e1e1; + display: inline-block; + width: 49%; } -.captions_editor { - height: 470px; +.caption_content .caption_title { + display: flex; + position: relative; +} - overflow-y: auto; - /*resize: none; - width: 100%; - padding: 0;*/ +.caption_content svg { + width: 20px; + color: var(--secondary); + margin-left: 4px; +} +.caption_content .caption_title svg:hover + .help_text { + display: block; } -#newCaptionsEditor { - margin-bottom: 6px; +.caption_content .help_text { + display: none; + position: absolute; + background: var(--pod-primary); + color: #fff; + width: calc(100% - 110px); + right: 0; + padding: 0.4em 0.8em; + border-radius: 4px; + box-shadow: 4px 4px 10px #ccc; + font-size: 15px; + line-height: 1.4; +} +.captions_editor { + height: 470px; + overflow-y: auto; +} + +#newCaptionsEditor { + margin-bottom: 6px; } .newEditorBlock { - border-bottom: 1px solid #E1E1E1; - padding: 8px; - transition: 0.2s; + border-bottom: 1px solid #e1e1e1; + padding: 8px; + transition: 0.2s; } .newEditorBlock:hover { - background-color: rgba(223, 230, 246, 0.4); + background-color: rgba(223 230 246 0.4); } .newEditorBlock > textarea { - width: 77%; - /* - height: 80px; - border-style: hidden; - background-color: transparent; - padding: 8px; - transition: .4s; - */ + width: 77%; } .captionBeingEdited { - border-left: 3px solid var(--pod-primary); - background-color: rgba(223, 230, 246, 0.4); + border-left: 3px solid var(--pod-primary); + background-color: rgba(223 230 246 0.4); } -/*.captionBeingEdited > textarea { - border: 1px solid var(--pod-primary); - border-radius: 4px; - background-color: #FFF5FB; -}*/ - .captionTimestamps { - /*float: right;*/ - line-height: 32px; + line-height: 32px; } .newEditorBlock a { - display: block; - /* - margin: 6px 0; - max-width: 78px; - color: #3EA6FF; - font-weight: 400; - */ + display: block; } .newEditorBlock input { - margin: 6px 0; - max-width: 78px; - display: inline-block; - padding: 4px; + margin: 6px 0; + max-width: 78px; + display: inline-block; + padding: 4px; } -/*.captionButtons { - float: left; -}*/ - .captionButtons button { - /*margin: 9px 2px 2px 2px; - left: 16px;*/ - display: block; - width: 28px; - height: 28px; - padding: 0; - border: 0; - border-radius: 50%; - /*background: #fff; - box-shadow: 0 0 4px rgba(0,0,0,.15); - transition: 0.2s;*/ + display: block; + width: 28px; + height: 28px; + padding: 0; + border: 0; + border-radius: 50%; } .captionButtons button:hover { - background: var(--pod-primary-lighten); + background: var(--pod-primary-lighten); } -/* -.captionButtons button:hover svg { - color: #fff; -} - -.captionButtons svg{ - color: #707070; - margin: 0; -}*/ - #addSubtitle svg { - color: var(--pod-link-color); + color: var(--pod-link-color); } .makerNavbarRight { - float: right; + float: right; } @media only screen and (max-width: 1000px) { - .makerNavbarRight { - float: none; - } -} - -/* -#captionFilename { - width: 280px; - margin-left: 32px; - transform: translateY(-4px); - border-top-style: hidden; - border-right-style: hidden; - border-left-style: hidden; - border-bottom-style: groove; - background-color: transparent; -} - -#switchOldEditMode { - margin-bottom: 8px; + .makerNavbarRight { + float: none; + } } -#captionLanguageSelect { - margin-right: 10px; - height: 31px; - width: 120px; - background: none; - border: none; - color: var(--pod-primary); - font-weight: 600; - font-size: 20px; -} - -#captionLanguageSelect:hover { - cursor: pointer; -} - -#captionLanguageSelect option { - color: black; - text-align: left; - font-size: 16px; +#captionmakerModal.save > .modal-dialog { + max-width: 1000px; } - -#captionKindSelect { - margin-right: 10px; - height: 31px; - width: 120px; - background: none; - border: none; - color: var(--pod-primary); - font-weight: 600; - font-size: 20px; +#editorTipsAndGoBack { + width: 100%; + clear: both; + padding-top: 24px; + padding-bottom: 24px; + display: block; + border-top: 1px solid #e1e1e1; } -#captionKindSelect:hover { - cursor: pointer; -} +@media only screen and (max-width: 800px) { + .gray_no_video { + display: flex; + flex-direction: column; + } -#captionKindSelect option { - color: black; - text-align: left; - font-size: 16px; -}*/ + .gray_no_video > div { + width: 100% !important; + } -#captionmakerModal.save > .modal-dialog { - max-width: 1000px; -} + .gray_no_video div:first-child { + overflow: hidden; + margin: 0; + margin-bottom: 1em; + min-height: unset !important; + } -#editorTipsAndGoBack { - width: 100%; - clear: both; - padding-top: 24px; - padding-bottom: 24px; - display: block; - border-top: 1px solid #E1E1E1; -} - -@media only screen and (max-width: 800px){ - .gray_no_video{ - display: flex; - flex-direction: column; - } - .gray_no_video > div{ - width: 100% !important; - } - .gray_no_video div:first-child{ - overflow: hidden; - margin: 0; - margin-bottom: 1em; - min-height: unset !important; - } - - .newEditorBlock > textarea { - width: 60%; - } + .newEditorBlock > textarea { + width: 60%; + } } .keyframe { - position: absolute; - z-index: 2; - height: 11px; + position: absolute; + z-index: 2; + height: 11px; } .keyframe-left { - transform: translate(-6px, -11px); + transform: translate(-6px, -11px); } .keyframe-right { - transform: translate(0, -11px); + transform: translate(0, -11px); } .regionHighligh { - position: absolute; - z-index: 1; - background-color: rgba(173, 50, 122, .5); - height: 3px; + position: absolute; + z-index: 1; + background-color: rgba(173 50 122 0.5); + height: 3px; } ::-webkit-scrollbar { - padding: 2px; - width: 4px; + padding: 2px; + width: 4px; } ::-webkit-scrollbar-track { - background: none; + background: none; } ::-webkit-scrollbar-thumb { - background: var(--pod-primary); - border-radius: 2px; + background: var(--pod-primary); + border-radius: 2px; } diff --git a/pod/completion/static/css/completion.css b/pod/completion/static/css/completion.css index d50aee21a8..b228cbff4f 100644 --- a/pod/completion/static/css/completion.css +++ b/pod/completion/static/css/completion.css @@ -1,137 +1,184 @@ - -/** Table scroll **/ -table.scroll { - width: 100%; - border-collapse: collapse; - border-spacing: 0; -} -table.scroll .btn-sm, -table.scroll .btn-group-sm > .btn { - border-radius: 4px; - font-size: 14px; - line-height: 1.4; - padding: .2em .4em; -} -table.scroll tbody { - max-height: 100px; - overflow-y: auto; - overflow-x: hidden; -} -table.scroll tbody td, -table.scroll thead th { - width: 10%; - height: 20px; - line-height: 20px; - border-bottom: 0; -} -table.scroll thead tr th { - height: 27px; - line-height: 27px; - text-align: left; -} - -/** Table contributor **/ -table#table_list_contributors tbody td.contributor_name, -table#table_list_contributors thead th.contributor_name { - width: 20%; -} - -/** Accordeon for lists **/ -#accordeon li, -#accordeon div { - list-style: none; -} - -/** Lists style **/ -#list_contributor .panel-heading, -#list_track .panel-heading, -#list_document .panel-heading, -#list_overlay .panel-heading { - border-bottom: 1px solid transparent; - border-top-left-radius: 3px; - border-top-right-radius: 3px; - padding: 4px 15px; -} - -#id_background { - margin-left: 0.25rem; - position: inherit; -} - -.contenuTitre{ - display: flex; - flex-direction: column; -} -.contenuTitre #list_track{ - order: 1; -} -.contenuTitre .breadcrumb{ - padding: 0; -} -.contenuTitre .btn{ - margin-bottom: 1rem; - margin-right: 1rem; -} - -/***** Override track list *****/ - -.grid-list-track .division{ grid-column: 1 / span 4; border-bottom: 1px solid #ccc;} -.grid-list-track .track_kind.options .btn{ font-size: 14px;} -.grid-list-track .track_kind.options a.btn{ overflow: unset; align-self: self-start;} -.grid-list-track .track_kind.options .dropdown #dropdownMenuButton{ - background-color: var(--pod-primary); - color: #fff; - border-color: var(--pod-primary); - border-radius: 4px; -} -.grid-list-track .track_kind.options .dropdown .dropdown-item{ background-color: #fff; } -.grid-list-track .track_kind.options .dropdown .btn{ width: 100%; } -.grid-list-track .track_kind.options .dropdown{ display: none; background-color: #fff;} -.grid-list-track .thead_title{ - margin: 0; - padding-right: 20px; - color: var(--pod-primary); - word-wrap: break-word; - font-weight: 600; -} -.grid-list-track p{margin: 0;} -.grid-list-track{ - display: grid; - grid-template-columns: max-content min-content 1fr min-content; - grid-gap: 20px; - margin-bottom: 1em; - margin-top: 1em; -} -.grid-list-track .track_kind.file{word-break: break-word;} -.track_kind.options{ - display: grid; - grid-template-columns: repeat(3, 1fr); - grid-column-gap: 10px; -} -.track_kind.options .btn { - margin: 0; - padding: .2em .4em; - font-size: 1rem; - box-sizing: border-box; -} -@media only screen and (max-width: 840px) -{ -.grid-list-track .track_kind.options .dropdown{ display: inline-block;} -.grid-list-track .track_kind.options > form, -.grid-list-track .track_kind.options > #modifCapSubFile{ display: none;} -} - -/** Filepicker override **/ -div.file-picker-overlay, -div.file-picker, -ul.css-tabs a { - box-sizing: content-box; -} -textarea#id_description { - line-height: normal; -} - -/** Bootstrap override **/ -.card-title { - margin: .45rem; -} +/** + * Esup-Pod completion styles + */ + +/** Table scroll **/ +table.scroll { + width: 100%; + border-collapse: collapse; + border-spacing: 0; +} + +table.scroll .btn-sm, +table.scroll .btn-group-sm > .btn { + border-radius: 4px; + font-size: 14px; + line-height: 1.4; + padding: 0.2em 0.4em; +} + +table.scroll tbody { + max-height: 100px; + overflow-y: auto; + overflow-x: hidden; +} + +table.scroll tbody td, +table.scroll thead th { + width: 10%; + height: 20px; + line-height: 20px; + border-bottom: 0; +} + +table.scroll thead tr th { + height: 27px; + line-height: 27px; + text-align: left; +} + +/** Table contributor **/ +table#table_list_contributors tbody td.contributor_name, +table#table_list_contributors thead th.contributor_name { + width: 20%; +} + +/** Accordeon for lists **/ +#accordeon li, +#accordeon div { + list-style: none; +} + +/** Lists style **/ +#list_contributor .panel-heading, +#list_track .panel-heading, +#list_document .panel-heading, +#list_overlay .panel-heading { + border-bottom: 1px solid transparent; + border-top-left-radius: 3px; + border-top-right-radius: 3px; + padding: 4px 15px; +} + +#id_background { + margin-left: 0.25rem; + position: inherit; +} + +.contenuTitre { + display: flex; + flex-direction: column; +} + +.contenuTitre #list_track { + order: 1; +} + +.contenuTitre .breadcrumb { + padding: 0; +} + +.contenuTitre .btn { + margin-bottom: 1rem; + margin-right: 1rem; +} + +/***** Override track list *****/ + +.grid-list-track .division { + grid-column: 1 / span 4; + border-bottom: 1px solid #ccc; +} + +.grid-list-track .track_kind.options .btn { + font-size: 14px; +} + +.grid-list-track .track_kind.options a.btn { + overflow: unset; + align-self: self-start; +} + +.grid-list-track .track_kind.options .dropdown #dropdownMenuButton { + background-color: var(--pod-primary); + color: #fff; + border-color: var(--pod-primary); + border-radius: 4px; +} + +.grid-list-track .track_kind.options .dropdown .dropdown-item { + background-color: #fff; +} + +.grid-list-track .track_kind.options .dropdown .btn { + width: 100%; +} + +.grid-list-track .track_kind.options .dropdown { + display: none; + background-color: #fff; +} + +.grid-list-track .thead_title { + margin: 0; + padding-right: 20px; + color: var(--pod-primary); + word-wrap: break-word; + font-weight: 600; +} + +.grid-list-track p { + margin: 0; +} + +.grid-list-track { + display: grid; + grid-template-columns: max-content min-content 1fr min-content; + grid-gap: 20px; + margin-bottom: 1em; + margin-top: 1em; +} + +.grid-list-track .track_kind.file { + word-break: break-word; +} + +.track_kind.options { + display: grid; + grid-template-columns: repeat(3, 1fr); + grid-column-gap: 10px; +} + +.track_kind.options .btn { + margin: 0; + padding: 0.2em 0.4em; + font-size: 1rem; + box-sizing: border-box; +} + +@media only screen and (max-width: 840px) { + .grid-list-track .track_kind.options .dropdown { + display: inline-block; + } + + .grid-list-track .track_kind.options > form, + .grid-list-track .track_kind.options > #modifCapSubFile { + display: none; + } +} + +/** Filepicker override **/ +div.file-picker-overlay, +div.file-picker, +ul.css-tabs a { + box-sizing: content-box; +} + +textarea#id_description { + line-height: normal; +} + +/** Bootstrap override **/ +.card-title { + margin: 0.45rem; +} diff --git a/pod/completion/static/js/caption_maker.js b/pod/completion/static/js/caption_maker.js index 11c0a3704e..68082fc62a 100644 --- a/pod/completion/static/js/caption_maker.js +++ b/pod/completion/static/js/caption_maker.js @@ -1,8 +1,21 @@ -const caption_memories = { +// Esup-Pod Caption maker Tool + +// Read-only globals defined in video_caption_maker.html +/* + global current_folder +*/ + +// Global vars +var fileLoaded = false; +var fileLoadedId = undefined; +var captionsArray = []; +var autoPauseAtTime = -1; + +const captionMemories = { start_time: "00:00.000", }; const file_prefix = window.location.pathname - .match(/[\d\w\-]+\/$/)[0] + .match(/[\d\w-]+\/$/)[0] .replace("/", ""); document.addEventListener("click", (e) => { @@ -17,7 +30,7 @@ document.addEventListener("click", (e) => { let form = document.getElementById("captionmaker_form"); let data_form = new FormData(form); - send_form_data(url, data_form, "ProcessProxyVttResponse"); + send_form_data(url, data_form, "processProxyVttResponse"); }); // Charge caption/subtitle file if exists @@ -30,7 +43,7 @@ document.addEventListener("DOMContentLoaded", function () { csrfmiddlewaretoken: Cookies.get("csrftoken"), }; - send_form_data(url, data, "ProcessProxyVttResponse"); + send_form_data(url, data, "processProxyVttResponse"); } else { document.getElementById( "captionFilename", @@ -42,7 +55,7 @@ document.addEventListener("DOMContentLoaded", function () { ); let captionContent = document.getElementById("captionContent"); captionContent.setAttribute("placeholder", placeholder); - captionContent.addEventListener("mouseup", function (e) { + captionContent.addEventListener("mouseup", function () { let selectedText = this.value.substring( this.selectionStart, this.selectionEnd, @@ -57,10 +70,9 @@ document.addEventListener("DOMContentLoaded", function () { elt.remove(); }); if (this.value.match(/^WEBVTT/)) { - ParseAndLoadWebVTT(this.value); + parseAndLoadWebVTT(this.value); } else { alert(gettext("Unrecognized caption file format.")); - t; } }); }); @@ -69,13 +81,21 @@ document.addEventListener("submit", (e) => { if (e.target.id != "form_save_captions") return; e.preventDefault(); let caption_content = document.getElementById("captionContent"); - if (!oldModeSelected) caption_content.value = GenerateWEBVTT(); + if (!oldModeSelected) caption_content.value = generateWEBVTT(); + + if (caption_content.value === "false") { + showalert( + gettext("There are errors in your captions/subtitles. Please review."), + "alert-warning", + ); + return; + } if (caption_content.value.trim() === "") { - showalert(gettext("There is no captions to save."), "alert-danger"); + showalert(gettext("There is no caption/subtitle to save."), "alert-danger"); return; } - if (typeof file_loaded != "undefined" && file_loaded) { + if (typeof fileLoaded != "undefined" && fileLoaded) { let saveModalId = document.getElementById("saveCaptionsModal"); let saveModal = bootstrap.Modal.getOrCreateInstance(saveModalId); saveModal.show(); @@ -85,25 +105,25 @@ document.addEventListener("submit", (e) => { } }); -document.addEventListener("click", (elt) => { - if (elt.target.id != "modal-btn-new" && elt.target.id != "modal-btn-override") +document.addEventListener("click", (evt) => { + if (evt.target.id != "modal-btn-new" && evt.target.id != "modal-btn-override") return; let caption_content = document.getElementById("captionContent"); - if (!oldModeSelected) caption_content.value = GenerateWEBVTT(); + if (!oldModeSelected) caption_content.value = generateWEBVTT(); let saveModalId = document.getElementById("saveCaptionsModal"); let saveModal = bootstrap.Modal.getOrCreateInstance(saveModalId); saveModal.hide(); let form_save_captions = document.getElementById("form_save_captions"); - if (elt.target.id == "modal-btn-override") { + if (evt.target.id == "modal-btn-override") { document .getElementById("form_save_captions") - .querySelector('input[name="file_id"]').value = file_loaded_id; + .querySelector('input[name="file_id"]').value = fileLoadedId; //form_save_captions.querySelector('input[name="enrich_ready"]').value = ""; updateCaptionsArray(caption_content.value); send_form_save_captions(); - } else if (elt.target.id == "modal-btn-new") { + } else if (evt.target.id == "modal-btn-new") { form_save_captions.querySelector('input[name="file_id"]').value = ""; //form_save_captions.querySelector('input[name="enrich_ready"]').value=""; @@ -111,15 +131,18 @@ document.addEventListener("click", (elt) => { } }); +/** + * Send the captions form to be saved + */ const send_form_save_captions = function () { let fileName = document.getElementById("captionFilename").value; if (fileName.length == 0) { fileName = `${file_prefix}_captions_${Date.now()}`; } - rxSignatureLine = /^WEBVTT(?:\s.*)?$/; - vttContent = document.getElementById("captionContent").value.trim(); - vttLines = vttContent.split(/\r\n|\r|\n/); + let rxSignatureLine = /^WEBVTT(?:\s.*)?$/; + let vttContent = document.getElementById("captionContent").value.trim(); + let vttLines = vttContent.split(/\r\n|\r|\n/); if (!rxSignatureLine.test(vttLines[0])) { alert(gettext("Not a valid time track file.")); return; @@ -130,7 +153,9 @@ const send_form_save_captions = function () { data_form.append("folder", current_folder); data_form.append("file", f); - url = document.getElementById("form_save_captions").getAttribute("action"); + let url = document + .getElementById("form_save_captions") + .getAttribute("action"); fetch(url, { method: "POST", @@ -194,7 +219,6 @@ document video_error.textContent = gettext( "The video could not be loaded, either because the server or network failed or because the format is not supported.", ); - break; default: video_error.textContent = gettext("An unknown error occurred."); @@ -207,7 +231,7 @@ document let shortcutsDisplayed = false; document .getElementById("showShortcutTips") - .addEventListener("click", function (e) { + .addEventListener("click", function () { let shortcuts = document.getElementById("shortcutsBlock"); if (shortcutsDisplayed) { shortcuts.style.display = "none"; @@ -218,12 +242,12 @@ document shortcutsDisplayed = !shortcutsDisplayed; }); -document.getElementById("addSubtitle").addEventListener("click", function (e) { +document.getElementById("addSubtitle").addEventListener("click", function () { const pod = document.getElementById("podvideoplayer"); const podPlayer = pod.player; var playTime = podPlayer.currentTime(); var captionsEndTime = existingCaptionsEndTime(); - AddCaption( + addCaption( captionsEndTime, playTime > captionsEndTime ? playTime : parseInt(captionsEndTime) + 2, "", @@ -251,24 +275,31 @@ let oldModeSelected = false; document .getElementById("switchOldEditMode") - .addEventListener("click", function (e) { - oldModeSelected = !oldModeSelected; - - if (oldModeSelected) { - document.getElementById("captionContent").value = GenerateWEBVTT(); - document.getElementById("rawCaptionsEditor").style.display = "block"; - document.getElementById("newCaptionsEditor").style.display = "none"; + .addEventListener("click", function () { + if (!oldModeSelected) { + let vtt = generateWEBVTT(); + if (vtt) { + document.getElementById("captionContent").value = vtt; + document.getElementById("rawCaptionsEditor").style.display = "block"; + document.getElementById("newCaptionsEditor").style.display = "none"; + oldModeSelected = !oldModeSelected; + } } else { + oldModeSelected = !oldModeSelected; document.getElementById("rawCaptionsEditor").style.display = "none"; document.getElementById("newCaptionsEditor").style.display = "block"; } }); -// index into captionsArray of the caption being displayed. -1 if none. +// index into captionsArray of the caption being displayed. -1 if none. var captionBeingDisplayed = -1; -function DisplayExistingCaption(seconds) { - var ci = FindCaptionIndex(seconds); +/** + * Display existing caption + * @param {[type]} seconds [description] + */ +function displayExistingCaption(seconds) { + var ci = findCaptionIndex(seconds); captionBeingDisplayed = ci; if (ci != -1) { var theCaption = captionsArray[ci]; @@ -276,8 +307,8 @@ function DisplayExistingCaption(seconds) { divs[divs.length - 1].innerText = theCaption.caption; var message = gettext("Caption for segment from %s to %s:"); document.getElementById("captionTitle").textContent = interpolate(message, [ - FormatTime(theCaption.start), - FormatTime(theCaption.end), + formatTime(theCaption.start), + formatTime(theCaption.end), ]); document.getElementById("textCaptionEntry").value = theCaption.caption; @@ -289,12 +320,20 @@ function DisplayExistingCaption(seconds) { } } +/** + * Get last existing captions end time. + * @return {int} end time + */ function existingCaptionsEndTime() { return captionsArray.length > 0 ? captionsArray[captionsArray.length - 1].end : 0; } +/** + * Update captions array. + * @param {[type]} vtt [description] + */ let updateCaptionsArray = (vtt) => { let arr = vtt.split("\n\n"); captionsArray = []; @@ -306,19 +345,22 @@ let updateCaptionsArray = (vtt) => { let data = text.split("\n"); let times = data[0].split("-->"); let newCaption = { - start: ParseTime(times[0]), - end: ParseTime(times[1]), + start: parseTime(times[0]), + end: parseTime(times[1]), caption: data[1], }; captionsArray.push(newCaption); - CreateCaptionBlock(newCaption); + createCaptionBlock(newCaption); } }); }; +/** + * Video play event handler + */ function videoPlayEventHandler() { captionBeingDisplayed = -1; - // give Opera a beat before doing this + // give Opera a beat before doing this window.setTimeout(function () { let textCaption = document.getElementById("textCaptionEntry"); textCaption.value = ""; @@ -333,6 +375,9 @@ function videoPlayEventHandler() { }, 16); } +/** + * Video pause event handler + */ function videoPauseEventHandler() { document .querySelectorAll("#playButton, #justSaveCaption, #saveCaptionAndPlay") @@ -351,14 +396,14 @@ function videoPauseEventHandler() { var captionsEndTime = existingCaptionsEndTime(); var message = ""; if (playTime - 1 < captionsEndTime) { - var ci = FindCaptionIndex(playTime - 1); + var ci = findCaptionIndex(playTime - 1); if (ci != -1) { var theCaption = captionsArray[ci]; message = gettext("Edit caption for segment from %s to %s:"); document.getElementById("captionTitle").textContent = interpolate( message, - [FormatTime(theCaption.start), FormatTime(theCaption.end)], + [formatTime(theCaption.start), formatTime(theCaption.end)], ); textCaption.value = theCaption.caption; @@ -373,8 +418,8 @@ function videoPauseEventHandler() { } else { message = gettext("Enter caption for segment from %s to %s:"); document.getElementById("captionTitle").textContent = interpolate(message, [ - FormatTime(existingCaptionsEndTime()), - FormatTime(playTime), + formatTime(existingCaptionsEndTime()), + formatTime(playTime), ]); document.getElementById("textCaptionEntry").value = ""; @@ -384,6 +429,9 @@ function videoPauseEventHandler() { //$("#textCaptionEntry").focus().get(0).setSelectionRange(1000, 1000); // set focus and selection point to end } +/** + * Video time update event handler. + */ function videoTimeUpdateEventHandler() { const pod = document.getElementById("podvideoplayer"); const podPlayer = pod.player; @@ -396,12 +444,12 @@ function videoTimeUpdateEventHandler() { var captionsEndTime = existingCaptionsEndTime(); if (playTime < captionsEndTime) { - DisplayExistingCaption(playTime); + displayExistingCaption(playTime); } else { var message = gettext("Pause to enter caption for segment from %s to %s."); document.getElementById("captionTitle").textContent = interpolate(message, [ - FormatTime(captionsEndTime), - FormatTime(playTime), + formatTime(captionsEndTime), + formatTime(playTime), ]); let divs = document.querySelectorAll(".vjs-text-track-display div"); @@ -414,8 +462,10 @@ function videoTimeUpdateEventHandler() { } } -// this enables the demo after a successful video load -function EnableDemoAfterLoadVideo() { +/** + * Enables the demo after a successful video load + */ +function enableDemoAfterLoadVideo() { document .querySelectorAll(".grayNoVideo a, .grayNoVideo") .forEach(function (e) { @@ -443,8 +493,8 @@ const pod = document.getElementById("podvideoplayer"); pod.addEventListener("play", videoPlayEventHandler); pod.addEventListener("timeupdate", videoTimeUpdateEventHandler); pod.addEventListener("pause", videoPauseEventHandler); -pod.addEventListener("canplay", EnableDemoAfterLoadVideo); -pod.addEventListener("loadeddata", EnableDemoAfterLoadVideo); +pod.addEventListener("canplay", enableDemoAfterLoadVideo); +pod.addEventListener("loadeddata", enableDemoAfterLoadVideo); document.getElementById("playButton").addEventListener("click", function () { const pod = document.getElementById("podvideoplayer"); @@ -458,49 +508,96 @@ document.getElementById("pauseButton").addEventListener("click", function () { podPlayer.pause(); }); -function GenerateWEBVTT() { +/** + * Generate a WEBVTT file from all the captionTextInput. + * @return {string|false} The generated WEBVTT string + */ +function generateWEBVTT() { let vtt = ""; - document - .querySelectorAll("#newCaptionsEditor > .newEditorBlock") - .forEach((e) => { - let captionText = e.querySelector(".captionTextInput").value; - let startTime = e.querySelector(".startTimeBtn").text; - let endTime = e.querySelector(".endTimeBtn").text; - vtt += `\n\n${startTime} --> ${endTime}\n${captionText}`; - }); + let captionBlocks = document.querySelectorAll( + "#newCaptionsEditor > .newEditorBlock", + ); + + // If form has invalid fields, do not continue. + if (!validateForms(captionBlocks)) { + return false; + } + captionBlocks.forEach((e) => { + /* We use FormData to get a formatted version of captionText + * including auto "\n" generated by cols='y' rows='x' wrap='hard' + */ + let captionText = new FormData(e).get("captionTextInput"); + let startTime = e.querySelector(".startTimeBtn").text; + let endTime = e.querySelector(".endTimeBtn").text; + + vtt += `\n\n${startTime} --> ${endTime}\n${captionText}`; + }); if (vtt.length > 0) vtt = "WEBVTT" + vtt; return vtt; } -function SaveCurrentCaption() { +/** + * Check validity of every form and fires an invalid event on invalid elements + * @return {bool} true if everything's fine + */ +function validateForms(forms) { + let validity = true; + forms.forEach((e) => { + e.classList.remove("was-validated"); + + // After Browser checks, we add some custom ones + let captionInput = e.querySelector(".captionTextInput"); + if (captionInput.value.length > 80) { + captionInput.setCustomValidity( + gettext("A caption cannot has more than 80 characters.") + + "[" + + captionInput.value.length + + "]", + ); + } else { + captionInput.setCustomValidity(""); + } + + if (!e.reportValidity()) { + e.classList.add("was-validated"); + validity = false; + } + }); + return validity; +} + +/** + * Save current caption. + */ +function saveCurrentCaption() { const pod = document.getElementById("podvideoplayer"); const podPlayer = pod.player; var playTime = podPlayer.currentTime(); var captionsEndTime = existingCaptionsEndTime(); let new_entry = document.getElementById("textCaptionEntry").value; if (playTime - 1 < captionsEndTime) { - var ci = FindCaptionIndex(playTime - 1); + var ci = findCaptionIndex(playTime - 1); if (ci != -1) { - UpdateCaption(ci, new_entry); + updateCaption(ci, new_entry); } } else { - AddCaption(captionsEndTime, playTime, new_entry); + addCaption(captionsEndTime, playTime, new_entry); } } document .getElementById("justSaveCaption") .addEventListener("click", function () { - SaveCurrentCaption(); + saveCurrentCaption(); }); document .getElementById("saveCaptionAndPlay") .addEventListener("click", function () { - SaveCurrentCaption(); + saveCurrentCaption(); const pod = document.getElementById("podvideoplayer"); const podPlayer = pod.player; @@ -518,12 +615,12 @@ document }); /** - * Updat caption html content + * Update caption html content. */ let updateCaptionHtmlContent = () => { let vtt = "WEBVTT\n\n"; captionsArray.forEach((cap, i) => { - vtt += `${FormatTime(cap.start)} --> ${FormatTime(cap.end)}\n${ + vtt += `${formatTime(cap.start)} --> ${formatTime(cap.end)}\n${ cap.caption }`; if (i !== captionsArray.length - 1) vtt += "\n\n"; @@ -531,22 +628,36 @@ let updateCaptionHtmlContent = () => { document.getElementById("captionContent").value = vtt; }; -function UpdateCaption(ci, captionText) { +/** + * Update caption. + * @param {[type]} ci caption index + * @param {[type]} captionText caption text + */ +function updateCaption(ci, captionText) { captionsArray[ci].caption = captionText; updateCaptionHtmlContent(); } let lastEditedBlock = null; -function CreateCaptionBlock(newCaption, spawnFunction) { +/** + * Create a caption block object. + * @param {Object} newCaption Simple object representing the caption block + * @param {Function} spawnFunction Function to call after block init + */ +function createCaptionBlock(newCaption, spawnFunction) { let captionText = newCaption.caption; - let start = FormatTime(newCaption.start); - let end = FormatTime(newCaption.end); - - let block = { + let start = formatTime(newCaption.start); + let end = formatTime(newCaption.end); + + /** + * Caption Block Object + * @type {Object} + */ + let Block = { // parent div: new DOMParser().parseFromString( - `
`, + `
`, "text/html", ).body.firstChild, @@ -557,15 +668,19 @@ function CreateCaptionBlock(newCaption, spawnFunction) { ).body.firstChild, insertBtn: new DOMParser().parseFromString( - ``, + )}">`, "text/html", ).body.firstChild, deleteBtn: new DOMParser().parseFromString( - ``, + )}">`, "text/html", ).body.firstChild, // textarea @@ -573,13 +688,13 @@ function CreateCaptionBlock(newCaption, spawnFunction) { "
", "text/html", ).body.firstChild, - //captionTextInput: $(``), + captionTextLabel: new DOMParser().parseFromString( - ``, + ``, "text/html", ).body.firstChild, captionTextInput: new DOMParser().parseFromString( - ``, + ``, "text/html", ).body.firstChild, // time editable @@ -592,7 +707,7 @@ function CreateCaptionBlock(newCaption, spawnFunction) { "text/html", ).body.firstChild, startTimeInput: new DOMParser().parseFromString( - ``, + ``, "text/html", ).body.firstChild, endTimeLabel: new DOMParser().parseFromString( @@ -600,7 +715,7 @@ function CreateCaptionBlock(newCaption, spawnFunction) { "text/html", ).body.firstChild, endTimeInput: new DOMParser().parseFromString( - ``, + ``, "text/html", ).body.firstChild, @@ -623,6 +738,9 @@ function CreateCaptionBlock(newCaption, spawnFunction) { isEditEnabled: false, // methods + /** + * Enable Block edition mode + */ enableEdit: function () { if (!this.isEditEnabled) { if (lastEditedBlock) { @@ -630,9 +748,7 @@ function CreateCaptionBlock(newCaption, spawnFunction) { } this.startTimeInput.value = this.startTimeBtn.textContent; - this.endTimeInput.value = this.endTimeBtn.textContent; - this.timeBlockEditable.style.display = ""; this.timeBlock.style.display = "none"; this.div.classList.add("captionBeingEdited"); @@ -640,19 +756,23 @@ function CreateCaptionBlock(newCaption, spawnFunction) { lastEditedBlock = this; this.isEditEnabled = true; + seekVideoTo(newCaption.start); } }, + /** + * Disable Block edition mode + */ disableEdit: function () { if (this.isEditEnabled) { - let newStartTime = ParseTime(this.startTimeInput.value); - let newEndTime = ParseTime(this.endTimeInput.value); + let newStartTime = parseTime(this.startTimeInput.value); + let newEndTime = parseTime(this.endTimeInput.value); newCaption.start = newStartTime; newCaption.end = newEndTime; - this.startTimeBtn.textContent = FormatTime(newStartTime); - this.endTimeBtn.textContent = FormatTime(newEndTime); + this.startTimeBtn.textContent = formatTime(newStartTime); + this.endTimeBtn.textContent = formatTime(newEndTime); this.timeBlockEditable.style.display = "none"; this.timeBlock.style.display = ""; @@ -663,6 +783,9 @@ function CreateCaptionBlock(newCaption, spawnFunction) { } }, + /** + * Place Block In Order + */ placeInOrder: function () { for (let i in captionsArray) { let cap = captionsArray[i]; @@ -696,10 +819,19 @@ function CreateCaptionBlock(newCaption, spawnFunction) { } }, - spawnNew: function () { + /** + * Spawn New Block + * @param {Event} e Triggered Event + */ + spawnNew: function (e) { + e.preventDefault(); const pod = document.getElementById("podvideoplayer"); const podPlayer = pod.player; let playTime = podPlayer.currentTime(); + /** + * Caption object + * @type {Object} + */ let captionObj = { start: newCaption.end, end: @@ -709,18 +841,26 @@ function CreateCaptionBlock(newCaption, spawnFunction) { let index = Array.from(this.div.parentNode.children).indexOf(this.div); captionsArray.splice(index + 1, 0, captionObj); - CreateCaptionBlock(captionObj, (newDiv) => + createCaptionBlock(captionObj, (newDiv) => this.div.parentNode.insertBefore(newDiv, this.div.nextSibling), ); }, - delete: function () { + /** + * Delete Block + * @param {Event} e Triggered Event + */ + delete: function (e) { + e.preventDefault(); let index = Array.from(this.div.parentNode.children).indexOf(this.div); captionsArray.splice(index, 1); this.div.remove(); }, + /** + * Init Block + */ init: function () { var uniq = "c" + Math.floor(Math.random() * 100000000); this.div.captionBlockObject = this; @@ -730,8 +870,8 @@ function CreateCaptionBlock(newCaption, spawnFunction) { this.captionTextInput.setAttribute("id", uniq); this.captionTextLabel.setAttribute("for", uniq); - this.insertBtn.addEventListener("click", () => this.spawnNew()); - this.deleteBtn.addEventListener("click", () => this.delete()); + this.insertBtn.addEventListener("click", (e) => this.spawnNew(e)); + this.deleteBtn.addEventListener("click", (e) => this.delete(e)); this.startTimeBtn.addEventListener("click", () => seekVideoTo(newCaption.start), ); @@ -784,21 +924,21 @@ function CreateCaptionBlock(newCaption, spawnFunction) { }, }; - block.init(); - newCaption.blockObject = block; + Block.init(); + newCaption.blockObject = Block; if (spawnFunction) { - spawnFunction(block.div); + spawnFunction(Block.div); } else { let addSubtitle = document.getElementById("addSubtitle"); - addSubtitle.parentNode.insertBefore(block.div, addSubtitle); + addSubtitle.parentNode.insertBefore(Block.div, addSubtitle); } - block.captionTextInput.addEventListener("input propertychange", function () { - captionsArray[block.div.index()].caption = this.value; + Block.captionTextInput.addEventListener("input propertychange", function () { + captionsArray[Block.div.index()].caption = this.value; }); - block.div.addEventListener( + Block.div.addEventListener( "hover", function () { highlightVideoRegion(newCaption.start, newCaption.end); @@ -809,17 +949,21 @@ function CreateCaptionBlock(newCaption, spawnFunction) { ); document.getElementById("noCaptionsText")?.remove(); - return block; + return Block; } +/** + * Assign some keyboard shortcuts to editor functions + * @type {Object} + */ let editorShortcuts = { Delete: function (e) { if (e.altKey && lastEditedBlock) { - lastEditedBlock.delete(); + lastEditedBlock.delete(e); return false; } }, - PageUp: function (e) { + PageUp: function () { if (lastEditedBlock) { let prev = lastEditedBlock.div.previousElementSibling; if (prev) { @@ -829,7 +973,7 @@ let editorShortcuts = { } } }, - PageDown: function (e) { + PageDown: function () { if (lastEditedBlock) { let next = lastEditedBlock.div.nextElementSibling; if (next) { @@ -839,19 +983,19 @@ let editorShortcuts = { } } }, - ArrowLeft: function (e) { + ArrowLeft: function () { if (this.notFocused()) { seekVideo(-10); return false; } }, - ArrowRight: function (e) { + ArrowRight: function () { if (this.notFocused()) { seekVideo(10); return false; } }, - " ": function (e) { + " ": function () { // space if (this.notFocused()) { const pod = document.getElementById("podvideoplayer"); @@ -863,7 +1007,7 @@ let editorShortcuts = { return false; } }, - m: function (e) { + m: function () { if (this.notFocused()) { let player = podPlayer; @@ -873,13 +1017,13 @@ let editorShortcuts = { return false; } }, - "?": function (e) { + "?": function () { if (this.notFocused()) { document.getElementById("showShortcutTips").click(); return false; } }, - Insert: function (e) { + Insert: function () { if (lastEditedBlock) { lastEditedBlock.spawnNew(); } else { @@ -894,11 +1038,15 @@ let editorShortcuts = { return false; } }, - End: function (e) { + End: function () { document.getElementById("saveCaptionAndPlay").click(); return false; }, + /** + * Check if there is no element on document that is focused + * @return {bool} true if not focused + */ notFocused: function () { var focused = document.activeElement; return focused.length == 0; @@ -916,15 +1064,20 @@ let editorShortcuts = { editorShortcuts.init(); -function AddCaptionListRow(ci, newCaption) { +/** + * Add caption list row + * @param {[type]} ci [description] + * @param {[type]} newCaption [description] + */ +function addCaptionListRow(ci, newCaption) { let vtt = document.getElementById("captionContent"); let vtt_entry = document.getElementById("textCaptionEntry").value.trim(); - let start = caption_memories.start_time; + let start = captionMemories.start_time; const pod = document.getElementById("podvideoplayer"); const podPlayer = pod.player; - var end = FormatTime(podPlayer.currentTime()); - var captionsEndTime = existingCaptionsEndTime(); + var end = formatTime(podPlayer.currentTime()); + // var captionsEndTime = existingCaptionsEndTime(); let caption_text = `${start} --> ${end}\n${vtt_entry}`; if (vtt_entry !== "") { if (vtt.value.trim() === "") { @@ -934,11 +1087,17 @@ function AddCaptionListRow(ci, newCaption) { } } - CreateCaptionBlock(newCaption); - caption_memories.start_time = end; + createCaptionBlock(newCaption); + captionMemories.start_time = end; } -function AddCaption(captionStart, captionEnd, captionText) { +/** + * Add caption + * @param {[type]} captionStart [description] + * @param {[type]} captionEnd [description] + * @param {[type]} captionText [description] + */ +function addCaption(captionStart, captionEnd, captionText) { const pod = document.getElementById("podvideoplayer"); const podPlayer = pod.player; let videoDuration = podPlayer.duration(); @@ -952,9 +1111,14 @@ function AddCaption(captionStart, captionEnd, captionText) { }; captionsArray.push(newCaption); - AddCaptionListRow(captionsArray.length - 1, newCaption); + addCaptionListRow(captionsArray.length - 1, newCaption); } +/** + * Convert HMS time format to seconds only + * @param {string} str hms + * @return {number} corresponding seconds + */ function hmsToSecondsOnly(str) { let p = str.split(":"), s = 0, @@ -966,8 +1130,11 @@ function hmsToSecondsOnly(str) { return s; } -// parses webvtt time string format into floating point seconds -function ParseTime(sTime) { +/** + * Parses webvtt time string format into floating point seconds + * @param {[type]} sTime [description] + */ +function parseTime(sTime) { let seconds = hmsToSecondsOnly(sTime); return parseFloat(seconds + "." + (sTime.split(".")[1] || 0)); /*// parse time formatted as hours:mm:ss.sss where hours are optional @@ -985,8 +1152,11 @@ function ParseTime(sTime) { return 0;*/ } -// formats floating point seconds into the webvtt time string format -function FormatTime(seconds) { +/** + * formats floating point seconds into the webvtt time string format + * @param {[type]} seconds [description] + */ +function formatTime(seconds) { var hh = Math.floor(seconds / (60 * 60)); var mm = Math.floor(seconds / 60) % 60; var ss = seconds % 60; @@ -1000,7 +1170,11 @@ function FormatTime(seconds) { ); } -function FindCaptionIndex(seconds) { +/** + * Find caption index + * @param {[type]} seconds [description] + */ +function findCaptionIndex(seconds) { var below = -1; var above = captionsArray.length; var i = Math.floor((below + above) / 2); @@ -1018,11 +1192,15 @@ function FindCaptionIndex(seconds) { return -1; } +/** + * Play selected caption + * @param {[type]} timeline [description] + */ function playSelectedCaption(timeline) { if (timeline.includes("-->")) { - let times = timeline.trim().split(/\s?\-\->\s?/); - let start = times[0].match(/[\d:\.]/) ? ParseTime(times[0]) : null; - let end = times[1].match(/[\d:\.]/) ? ParseTime(times[1]) : null; + let times = timeline.trim().split(/\s?-->\s?/); + let start = times[0].match(/[\d:.]/) ? parseTime(times[0]) : null; + let end = times[1].match(/[\d:.]/) ? parseTime(times[1]) : null; if (!isNaN(start) && !isNaN(end)) { const pod = document.getElementById("podvideoplayer"); const podPlayer = pod.player; @@ -1036,10 +1214,11 @@ function playSelectedCaption(timeline) { /** * Escape Html entities + * @param {string} s String to be escaped */ function XMLEncode(s) { return s - .replace(/\&/g, "&") + .replace(/&/g, "&") .replace(/“/g, """) .replace(/”/g, """) .replace(/"/g, """) @@ -1047,6 +1226,10 @@ function XMLEncode(s) { .replace(/>/g, ">"); } +/** + * Decode Html entities + * @param {String} s String to be decoded + */ function XMLDecode(s) { return s .replace(/</g, "<") @@ -1056,12 +1239,17 @@ function XMLDecode(s) { .replace(/&/g, "&"); } -function LoadCaptionFile(fileObject) { +/** + * Load caption file + * @param {[type]} fileObject [description] + */ +/* +function loadCaptionFile(fileObject) { if (window.FileReader) { var reader = new window.FileReader(); reader.addEventListener("load", function () { - ProcessProxyVttResponse({ status: "success", response: reader.result }); + processProxyVttResponse({ status: "success", response: reader.result }); }); reader.addEventListener("onerror", function (evt) { @@ -1078,18 +1266,21 @@ function LoadCaptionFile(fileObject) { } else { alert(gettext("Your browser does not support FileReader.")); } -} +}*/ -// invoked by script insertion of proxyvtt.ashx -function ProcessProxyVttResponse(obj) { +/** + * Invoked by script insertion of proxyvtt.ashx + * @param {[type]} obj [description] + */ +function processProxyVttResponse(obj) { obj = JSON.parse(obj); if (obj.status == "error") alert(gettext("Error loading caption file: ") + obj.message); else if (obj.status == "success") { // delete any captions we've got captionsArray.length = 0; - file_loaded = true; - file_loaded_id = obj.id_file; + fileLoaded = true; + fileLoadedId = obj.id_file; current_folder = obj.id_folder; document.querySelectorAll(".newEditorBlock").forEach((elt) => { elt.remove(); @@ -1102,18 +1293,18 @@ function ProcessProxyVttResponse(obj) { ); if (obj.response.match(/^WEBVTT/)) { - ParseAndLoadWebVTT(obj.response); + parseAndLoadWebVTT(obj.response); } else { alert(gettext("Unrecognized caption file format.")); } } } -//----------------------------------------------------------------------------------------------------------------------------------------- -// Partial parser for WebVTT files based on the spec at http://dev.w3.org/html5/webvtt/ -//----------------------------------------------------------------------------------------------------------------------------------------- - -function ParseAndLoadWebVTT(vtt) { +/** + * Partial parser for WebVTT files based on the spec at http://dev.w3.org/html5/webvtt/ + * @param {[type]} vtt [description] + */ +function parseAndLoadWebVTT(vtt) { var vttLines = vtt.split(/\r\n|\r|\n/); // create an array of lines from our file if (vttLines[0].trim().toLowerCase() != "webvtt") { @@ -1126,7 +1317,7 @@ function ParseAndLoadWebVTT(vtt) { elt.remove(); }); - var rxTimeLine = /^([\d\.:]+)\s+-->\s+([\d\.:]+)(?:\s.*)?$/; + var rxTimeLine = /^([\d.:]+)\s+-->\s+([\d.:]+)(?:\s.*)?$/; var rxCaptionLine = /^(?:]+)>)?([^\r\n]+)$/; var rxBlankLine = /^\s*$/; var rxMarkup = /<[^>]>/g; @@ -1135,6 +1326,9 @@ function ParseAndLoadWebVTT(vtt) { cueEnd = null, cueText = null; + /** + * Append current caption + */ function appendCurrentCaption() { if (cueStart && cueEnd && cueText) { let newCaption = { @@ -1143,7 +1337,7 @@ function ParseAndLoadWebVTT(vtt) { caption: cueText.trim(), }; captionsArray.push(newCaption); - CreateCaptionBlock(newCaption); + createCaptionBlock(newCaption); } cueStart = cueEnd = cueText = null; } @@ -1155,22 +1349,22 @@ function ParseAndLoadWebVTT(vtt) { } if (!cueStart && !cueEnd && !cueText && vttLines[i].indexOf("-->") == -1) { - // this is a cue identifier we're ignoring + // this is a cue identifier we're ignoring continue; } var timeMatch = rxTimeLine.exec(vttLines[i]); if (timeMatch) { appendCurrentCaption(); - cueStart = ParseTime(timeMatch[1]); + cueStart = parseTime(timeMatch[1]); if (cueStart == 0) cueStart = "0.0"; - cueEnd = ParseTime(timeMatch[2]); + cueEnd = parseTime(timeMatch[2]); continue; } var captionMatch = rxCaptionLine.exec(vttLines[i]); if (captionMatch && cueStart && cueEnd) { - // captionMatch[1] is the optional voice (speaker) we're ignoring + // captionMatch[1] is the optional voice (speaker) we're ignoring var capLine = captionMatch[2].replace(rxMarkup, ""); if (cueText) cueText += " " + capLine; else { @@ -1189,11 +1383,19 @@ var clearVideoRegion; const registerPlugin = videojs.registerPlugin || videojs.plugin; +/** + * On player ready Event + * @param {[type]} player [description] + * @param {[type]} options [description] + */ const onPlayerReady = function (player, options) { let startKeyframe; let endKeyframe; let regionHighlight; + /** + * Clear video region + */ const clearVideoRegion = () => { startKeyframe?.remove(); @@ -1204,6 +1406,11 @@ const onPlayerReady = function (player, options) { player.userActive(false); }; + /** + * Highlight video region + * @param {[type]} startTime [description] + * @param {[type]} endTime [description] + */ highlightVideoRegion = function (startTime, endTime) { clearVideoRegion(); player.userActive(true); @@ -1223,8 +1430,8 @@ const onPlayerReady = function (player, options) { element.parentNode.insertBefore(startKeyframe, element); regionHighlight = "
"; - regionHighlight.style = "left: " + `${startPercent}%`; - regionHighlight.style = "width:" + `${endPercent - startPercent}%`; + regionHighlight.style.left = `${startPercent}%`; + regionHighlight.style.width = `${endPercent - startPercent}%`; startKeyframe.after(regionHighlight); @@ -1235,22 +1442,34 @@ const onPlayerReady = function (player, options) { regionHighlight.after(endKeyframe); }; + /** + * Seek video player to absolute `time`. + * @param {[type]} time [description] + */ seekVideoTo = function (time) { player.userActive(true); player.currentTime(time); }; + /** + * Seek video player to relative `time`. + * @param {[type]} time [description] + */ seekVideo = function (time) { player.userActive(true); player.currentTime(player.currentTime() + time); }; }; -const timelineRegions = function (options) { +/** + * Timeline regions + * @param {[type]} options [description] + */ +function timelineRegions(options) { this.ready(function () { onPlayerReady(this, options); }); -}; +} registerPlugin("timelineRegions", timelineRegions); diff --git a/pod/completion/templates/video_caption_maker.html b/pod/completion/templates/video_caption_maker.html index c86f707410..0b44717dc3 100644 --- a/pod/completion/templates/video_caption_maker.html +++ b/pod/completion/templates/video_caption_maker.html @@ -84,7 +84,7 @@
- +
-

{% trans "No captions" %}

-
@@ -218,11 +218,6 @@
+ - {% include "notification_toast.html" %} - + {% if USE_NOTIFICATIONS %} + {% include "notification_toast.html" %} + {% endif %} + {% endblock content %} {% if not request.GET.is_iframe %}