├── src ├── assets │ └── css │ │ ├── common │ │ ├── _typography.scss │ │ └── _themes.scss │ │ ├── abstracts │ │ ├── _mixins.scss │ │ └── _variables.scss │ │ ├── styles.scss │ │ ├── components │ │ ├── _toolbar.scss │ │ └── _editor.scss │ │ ├── fields │ │ └── tracksbuilder.scss │ │ └── sections │ │ └── _subtitler.scss ├── components │ ├── icons │ │ ├── play.vue │ │ ├── edit.vue │ │ ├── pause.vue │ │ ├── arrow.vue │ │ └── audio-file.vue │ ├── tracksbuilder.vue │ └── subtitler.vue └── index.js ├── .gitignore ├── .editorconfig ├── lib ├── languages │ ├── en.php │ ├── fr.php │ └── de.php ├── fields.php ├── routes.php ├── fieldMethods.php ├── sections.php └── subtitler.php ├── composer.json ├── index.php ├── package.json ├── LICENSE ├── README.md ├── index.css └── index.js /src/assets/css/common/_typography.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | node_modules 3 | package-lock.json 4 | -------------------------------------------------------------------------------- /src/assets/css/abstracts/_mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin text-truncate { 2 | overflow: hidden; 3 | text-overflow: ellipsis; 4 | white-space: nowrap; 5 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true -------------------------------------------------------------------------------- /src/assets/css/styles.scss: -------------------------------------------------------------------------------- 1 | @import 2 | 'abstracts/variables', 3 | 'abstracts/mixins'; 4 | 5 | @import 6 | 'common/themes', 7 | 'common/typography'; 8 | 9 | @import 10 | 'components/editor', 11 | 'components/toolbar'; 12 | 13 | @import 14 | 'sections/subtitler'; -------------------------------------------------------------------------------- /src/components/icons/play.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/components/icons/edit.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import Subtitler from './components/subtitler.vue' 2 | import tracksBuilder from './components/tracksbuilder.vue' 3 | 4 | panel.plugin('sylvainjule/subtitler', { 5 | sections: { 6 | subtitler: Subtitler 7 | }, 8 | fields: { 9 | tracksbuilder: tracksBuilder 10 | } 11 | }); 12 | -------------------------------------------------------------------------------- /src/components/icons/pause.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/components/icons/arrow.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /lib/languages/en.php: -------------------------------------------------------------------------------- 1 | 'Update .vtt files', 5 | 'subtitler.vttbuilder.updated' => 'Files updated!', 6 | 'subtitler.vttbuilder.create' => 'Create .vtt files', 7 | 'subtitler.vttbuilder.created' => 'Files created!', 8 | 'subtitler.vttbuilder.error' => 'An error occured!', 9 | 'subtitler.vttbuilder.processing' => 'Processing…', 10 | ); -------------------------------------------------------------------------------- /lib/languages/fr.php: -------------------------------------------------------------------------------- 1 | 'Mettre à jour les .vtt', 5 | 'subtitler.vttbuilder.updated' => 'Fichiers mis à jour !', 6 | 'subtitler.vttbuilder.create' => 'Générer les .vtt', 7 | 'subtitler.vttbuilder.created' => 'Fichiers créés !', 8 | 'subtitler.vttbuilder.error' => 'Une erreur s\'est produite !', 9 | 'subtitler.vttbuilder.processing' => 'Traitement en cours…', 10 | ); -------------------------------------------------------------------------------- /lib/languages/de.php: -------------------------------------------------------------------------------- 1 | 'Aktualisieren von .vtt-Dateien', 5 | 'subtitler.vttbuilder.updated' => 'Dateien aktualisiert!', 6 | 'subtitler.vttbuilder.create' => 'Erstellen von .vtt-Dateien', 7 | 'subtitler.vttbuilder.created' => 'Dateien erstellt!', 8 | 'subtitler.vttbuilder.error' => 'Es ist ein Fehler aufgetreten!', 9 | 'subtitler.vttbuilder.processing' => 'Verarbeiten…', 10 | ); -------------------------------------------------------------------------------- /lib/fields.php: -------------------------------------------------------------------------------- 1 | array( 5 | 'props' => array( 6 | 'src' => function($src = false) { 7 | return $src; 8 | } 9 | ), 10 | 'computed' => array( 11 | 'vttLength' => function() { 12 | return $this->model()->files()->filterBy('extension', '==', 'vtt')->count(); 13 | }, 14 | 'uri' => function() { 15 | return $this->model()->uri(); 16 | } 17 | ) 18 | ), 19 | ); -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sylvainjule/subtitler", 3 | "description": "Synchronize content with audio/video files within Kirby", 4 | "type": "kirby-plugin", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Sylvain Julé", 9 | "email": "contact@sylvain-jule.fr" 10 | } 11 | ], 12 | "require": { 13 | "getkirby/composer-installer": "^1.1" 14 | }, 15 | "extra": { 16 | "installer-name": "subtitler" 17 | }, 18 | "minimum-stability": "beta" 19 | } 20 | -------------------------------------------------------------------------------- /src/components/icons/audio-file.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /index.php: -------------------------------------------------------------------------------- 1 | require_once __DIR__ . '/lib/sections.php', 7 | 'fields' => require_once __DIR__ . '/lib/fields.php', 8 | 'fieldMethods' => require_once __DIR__ . '/lib/fieldMethods.php', 9 | 'translations' => array( 10 | 'en' => require_once __DIR__ . '/lib/languages/en.php', 11 | 'de' => require_once __DIR__ . '/lib/languages/de.php', 12 | 'fr' => require_once __DIR__ . '/lib/languages/fr.php', 13 | ), 14 | 'api' => array( 15 | 'routes' => require_once __DIR__ . '/lib/routes.php', 16 | ), 17 | )); -------------------------------------------------------------------------------- /lib/routes.php: -------------------------------------------------------------------------------- 1 | 'subtitler/generate-vtt-files', 6 | 'method' => 'POST', 7 | 'action' => function() { 8 | $uri = get('uri'); 9 | $fieldname = get('src'); 10 | 11 | try { 12 | $page = kirby()->page($uri); 13 | Subtitler::generateVttFiles($page, $fieldname); 14 | 15 | $response = array( 16 | 'status' => 'success', 17 | ); 18 | return $response; 19 | } 20 | catch (Exception $e) { 21 | $response = array( 22 | 'status' => 'error', 23 | 'message' => $e->getMessage() 24 | ); 25 | return $response; 26 | } 27 | } 28 | ) 29 | ); 30 | -------------------------------------------------------------------------------- /src/assets/css/components/_toolbar.scss: -------------------------------------------------------------------------------- 1 | .subtitler-toolbar { 2 | position: absolute; 3 | width: 100%; 4 | top: 0; 5 | z-index: 2; 6 | display: flex; 7 | justify-content: flex-end; 8 | &-debug { 9 | height: 2rem; 10 | margin-left: 0rem; 11 | box-shadow: 0 2px 5px rgba(22,23,26,.05); 12 | display: flex; 13 | align-items: center; 14 | font-family: monospace; 15 | font-size: 11px; 16 | .coord { 17 | padding: 0 1rem; 18 | &.prop { 19 | width: 100px; 20 | &:before { 21 | content: 'x: '; 22 | } 23 | } 24 | &.time { 25 | width: 100px; 26 | padding-left: 0; 27 | @include text-truncate; 28 | &:before { 29 | content: 'time: '; 30 | } 31 | } 32 | &.text { 33 | width: 100px; 34 | padding-left: 0; 35 | &:before { 36 | content: 'text: '; 37 | } 38 | } 39 | } 40 | } 41 | } -------------------------------------------------------------------------------- /src/assets/css/abstracts/_variables.scss: -------------------------------------------------------------------------------- 1 | /* Colors 2 | ---------------------*/ 3 | 4 | $color-dark: #16171a; 5 | $color-light: #efefef; 6 | 7 | $purple: #7a73a5; 8 | $purple-light: #ece7ff; 9 | $orange: #e19d45; 10 | $orange-light: #fff1e8; 11 | $blue: #7682bb; 12 | $blue-light: #e1ecff; 13 | $green: #7aba4b; 14 | $green-light: #f1faed; 15 | $red: #f93048; 16 | $red-light: #feeaee; 17 | 18 | $vibrant-blue: #2288e8; 19 | $grey: darken(#dddddd, 45%); 20 | 21 | 22 | /* Patterns 23 | ---------------------*/ 24 | 25 | $pattern: "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXR0ZXJuIGlkPSJhIiB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHBhdHRlcm5Vbml0cz0idXNlclNwYWNlT25Vc2UiPjxwYXRoIGZpbGw9InJnYmEoMCwgMCwgMCwgMC4yKSIgZD0iTTAgMGgxMHYxMEgwem0xMCAxMGgxMHYxMEgxMHoiLz48L3BhdHRlcm4+PHJlY3QgZmlsbD0idXJsKCNhKSIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIvPjwvc3ZnPg=="; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "kirby-subtitler", 3 | "version": "0.0.1", 4 | "description": "Synchronize content with audio/video files within Kirby", 5 | "main": "index.js", 6 | "author": "Sylvain Julé", 7 | "license": "MIT", 8 | "scripts": { 9 | "dev": "parcel watch src/index.js --no-source-maps -d ./", 10 | "build": "parcel build src/index.js --no-source-maps --experimental-scope-hoisting -d ./" 11 | }, 12 | "posthtml": { 13 | "recognizeSelfClosing": true 14 | }, 15 | "browserslist": [ 16 | "> 1%", 17 | "last 2 versions", 18 | "not ie <= 11" 19 | ], 20 | "devDependencies": { 21 | "@vue/component-compiler-utils": "^3.0.0", 22 | "cssnano": "^4.1.10", 23 | "sass": "^1.21.0", 24 | "vue-template-compiler": "^2.6.10" 25 | }, 26 | "dependencies": { 27 | "vue": "^2.6.10", 28 | "vue-hot-reload-api": "^2.3.3" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/fieldMethods.php: -------------------------------------------------------------------------------- 1 | function($field, $timeline) { 5 | $subtitles = $field->toStructure()->filter(function($child) use($timeline) { 6 | return $child->timeline() == $timeline; 7 | }); 8 | $field = $subtitles; 9 | return $field; 10 | }, 11 | 'toPercent' => function($field) { 12 | return floatval($field->value) * 100; 13 | }, 14 | 'toPercentString' => function($field) { 15 | return floatval($field->value) * 100 . '%'; 16 | }, 17 | 'toVttTime' => function($field) { 18 | return Subtitler::toVttTime($field->value); 19 | }, 20 | 'vtt' => function($field, $timeline, $lang = false) { 21 | if(kirby()->multilang() && $lang) { 22 | $filename = $field->key() . '-' . $timeline . '-' . $lang . '.vtt'; 23 | } 24 | else { 25 | $filename = $field->key() . '-' . $timeline . '.vtt'; 26 | } 27 | return $field->parent()->file($filename); 28 | } 29 | ); -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Sylvain Julé 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/sections.php: -------------------------------------------------------------------------------- 1 | array( 5 | 'props' => array( 6 | 'debug' => function($debug = false) { 7 | return $debug; 8 | }, 9 | 'theme' => function($theme = 'light') { 10 | return $theme; 11 | }, 12 | 'timelines' => function($timelines = []) { 13 | $orderedTimelines = array(); 14 | foreach($timelines as $timeline) { 15 | $data = array( 16 | 'id' => $timeline['id'], 17 | 'label' => $timeline['label'], 18 | 'color' => $timeline['color'], 19 | ); 20 | array_push($orderedTimelines, $data); 21 | } 22 | return $orderedTimelines; 23 | }, 24 | 'storage' => function($storage = []) { 25 | return $storage; 26 | }, 27 | ), 28 | 'computed' => array( 29 | 'file' => function() { 30 | if (in_array($this->model()->type(), ['audio', 'video'])) { 31 | return array( 32 | 'url' => $this->model()->url(), 33 | 'type' => $this->model()->type(), 34 | ); 35 | } 36 | else { 37 | return false; 38 | } 39 | }, 40 | 'lang' => function() { 41 | return kirby()->languageCode() ?? false; 42 | } 43 | ), 44 | ), 45 | ); -------------------------------------------------------------------------------- /src/assets/css/fields/tracksbuilder.scss: -------------------------------------------------------------------------------- 1 | .tracksbuilder-button { 2 | position: relative; 3 | display: flex; 4 | align-items: center; 5 | background: white; 6 | border-radius: 1px; 7 | margin-bottom: 2px; 8 | box-shadow: 0 2px 5px rgba(22,23,26,.05); 9 | cursor: pointer; 10 | .icon { 11 | width: 38px; 12 | height: 38px; 13 | background: #f5f5f5; 14 | display: flex; 15 | align-items: center; 16 | justify-content: center; 17 | svg { 18 | fill: #16171a; 19 | width: 16px; 20 | height: 16px; 21 | transform: scaleX(-1); 22 | } 23 | } 24 | .text { 25 | white-space: nowrap; 26 | overflow: hidden; 27 | text-overflow: ellipsis; 28 | align-items: baseline; 29 | width: calc(100% - 38px); 30 | line-height: 1.25rem; 31 | padding: .5rem .75rem; 32 | em { 33 | font-style: normal; 34 | margin-right: 1rem; 35 | flex-grow: 1; 36 | font-size: .875rem; 37 | color: #16171a; 38 | } 39 | } 40 | &.error { 41 | .icon { 42 | background: #d52a20; 43 | svg { 44 | fill: white; 45 | } 46 | } 47 | } 48 | &.success { 49 | .icon { 50 | svg { 51 | fill: #a7bd69; 52 | } 53 | } 54 | } 55 | &.processing { 56 | .icon { 57 | svg { 58 | animation: rotate-icon 1.5s linear infinite; 59 | } 60 | } 61 | } 62 | 63 | @keyframes rotate-icon { 64 | from { 65 | transform: scaleX(-1) rotate(0deg); 66 | } 67 | 68 | to { 69 | transform: scaleX(-1) rotate(-360deg); 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /src/assets/css/sections/_subtitler.scss: -------------------------------------------------------------------------------- 1 | .subtitler { 2 | position: relative; 3 | width: 100%; 4 | padding-bottom: 0; 5 | margin-bottom: 3rem; 6 | user-select: none; 7 | 8 | /* Reset form elements */ 9 | button { 10 | outline: none; 11 | } 12 | 13 | &-background { 14 | position: absolute; 15 | width: 100%; 16 | height: 100%; 17 | } 18 | &-placeholder { 19 | position: relative; 20 | padding: 2.5rem; 21 | &-ctn { 22 | position: relative; 23 | z-index: 1; 24 | display: flex; 25 | justify-content: center; 26 | padding: 2.5rem; 27 | } 28 | } 29 | &-file { 30 | position: relative; 31 | width: 100%; 32 | height: 100%; 33 | z-index: 1; 34 | padding: 1.5rem; 35 | padding-bottom: 2.5rem; 36 | &.debug { 37 | padding-top: 2.5rem; 38 | } 39 | .subtitler-video { 40 | position: relative; 41 | width: 100%; 42 | display: flex; 43 | justify-content: center; 44 | align-items: center; 45 | &-ctn { 46 | position: relative; 47 | display: inline-block; 48 | max-width: 100%; 49 | z-index: 2; 50 | video { 51 | display: block; 52 | max-width: 100%; 53 | max-height: calc(75vh - 2.5rem - 9.5rem); 54 | } 55 | } 56 | } 57 | .subtitler-audio { 58 | .audio-icon { 59 | display: block; 60 | width: 75px; 61 | margin: 0 auto; 62 | margin-top: 1rem; 63 | } 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /src/components/tracksbuilder.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {{text}} 10 | 11 | 12 | 13 | 14 | 75 | 76 | 79 | -------------------------------------------------------------------------------- /lib/subtitler.php: -------------------------------------------------------------------------------- 1 | 3600) { 11 | $hours = floor($seconds / 3600); 12 | } 13 | $seconds = $seconds % 3600; 14 | 15 | return str_pad($hours, 2, '0', STR_PAD_LEFT) 16 | . gmdate(':i:s', $seconds) 17 | . ($milliseconds ? '.' . $milliseconds : ''); 18 | } 19 | 20 | // .vtt files builder 21 | public static function generateVttFiles($page, $fieldname) { 22 | $root = $page->root(); 23 | 24 | // if the site is multilang 25 | if(kirby()->multilang()) { 26 | // for each language 27 | foreach(kirby()->languages() as $language) { 28 | $lang = $language->code(); 29 | $subtitles = $page->content($lang)->get($fieldname); 30 | $timelines = array_unique(array_column($subtitles->yaml(), 'timeline')); 31 | 32 | // generate a .vtt file for eahc timeline in the language 33 | foreach($timelines as $timeline) { 34 | self::generateVttFile($subtitles, $root, $fieldname, $timeline, $lang); 35 | } 36 | } 37 | } 38 | else { 39 | $subtitles = $page->content()->get($fieldname); 40 | $timelines = array_unique(array_column($subtitles->yaml(), 'timeline')); 41 | 42 | // generate a .vtt file for each timeline 43 | foreach($timelines as $timeline) { 44 | self::generateVttFile($subtitles, $root, $fieldname, $timeline); 45 | } 46 | } 47 | } 48 | 49 | // create / update a .vtt file 50 | public static function generateVttFile($subtitles, $root, $fieldname, $timeline, $lang = false) { 51 | $timelineSubs = $subtitles->getTimeline($timeline); 52 | 53 | $filepath = $root . '/' . $fieldname . '-' . $timeline; 54 | $filepath .= $lang ? '-' . $lang : ''; 55 | $filepath .= '.vtt'; 56 | 57 | $content = 'WEBVTT' . PHP_EOL . PHP_EOL; 58 | $i = 0; 59 | 60 | // loop through each subtitle 61 | foreach($timelineSubs as $subtitle) { 62 | $i++; 63 | 64 | $start = $subtitle->start()->toVttTime(); 65 | $end = $subtitle->end()->toVttTime(); 66 | $time = $start . ' --> ' . $end; 67 | $content .= $i . PHP_EOL . $time . PHP_EOL; 68 | $content .= $subtitle->content()->subtitle(); 69 | 70 | if($i < $timelineSubs->count()) { 71 | $content .= PHP_EOL . PHP_EOL; 72 | } 73 | } 74 | 75 | // write the file 76 | f::write($filepath, $content); 77 | } 78 | 79 | } -------------------------------------------------------------------------------- /src/assets/css/common/_themes.scss: -------------------------------------------------------------------------------- 1 | /* Light or dark 2 | --------------------*/ 3 | 4 | .subtitler[data-theme="light"] { 5 | .subtitler-background { 6 | background: $color-light url($pattern); 7 | opacity: 0.45; 8 | } 9 | .subtitler-placeholder { 10 | &-ctn { 11 | background: lighten(#efefef, 5%); 12 | } 13 | } 14 | .subtitler-toolbar-debug { 15 | background: white; 16 | } 17 | .subtitler-editor { 18 | &-firstline { 19 | .left { 20 | border-right: 1px solid #e5e5e5; 21 | background: #f8f8f8; 22 | .play { 23 | border-right: 1px solid #e5e5e5; 24 | svg { 25 | fill: lighten(#848484, 10%); 26 | } 27 | &:hover { 28 | svg { 29 | fill: black; 30 | } 31 | } 32 | } 33 | .range-buttons { 34 | .range-button { 35 | background: darken(#f2f2f2, 4%); 36 | color: darken(#919191, 5%); 37 | } 38 | } 39 | } 40 | .right { 41 | background: white; 42 | color: lighten(#919191, 10%); 43 | } 44 | } 45 | &-timelines { 46 | .timeline { 47 | border-top: 1px solid #e5e5e5; 48 | .left { 49 | border-right: 1px solid #e5e5e5; 50 | .tick-wrapper { 51 | .tick svg { 52 | fill: white; 53 | } 54 | } 55 | .count-wrapper { 56 | .count { 57 | background: #f2f2f2; 58 | color: #919191; 59 | } 60 | } 61 | } 62 | .right { 63 | background: #f8f8f8; 64 | } 65 | 66 | &.active { 67 | .right { 68 | background: repeating-linear-gradient( 69 | -45deg, 70 | #ffffff, 71 | #ffffff 5px, 72 | darken(#fafafa, 3%) 5px, 73 | darken(#fafafa, 3%) 6px 74 | ); 75 | } 76 | } 77 | &:not(.active) { 78 | .tick { 79 | border: 1px solid #b2b2b2; 80 | &:hover { 81 | border: 1px solid #2d2f36; 82 | } 83 | } 84 | } 85 | 86 | } 87 | } 88 | } 89 | } 90 | 91 | .subtitler[data-theme="dark"] { 92 | .subtitler-background { 93 | background: lighten($color-dark, 10%) url($pattern); 94 | } 95 | .subtitler-placeholder { 96 | &-ctn { 97 | background: $color-dark; 98 | color: #c5c5c6; 99 | } 100 | } 101 | .subtitler-toolbar-debug { 102 | background: $color-dark; 103 | color: white; 104 | } 105 | .subtitler-file { 106 | .audio-icon { 107 | svg path { 108 | &:first-child { 109 | fill: lighten($color-dark, 30%); 110 | } 111 | &:last-child { 112 | fill: $color-dark; 113 | } 114 | } 115 | } 116 | } 117 | .subtitler-editor { 118 | &-firstline { 119 | .left { 120 | border-right: 1px solid lighten($color-dark, 25%); 121 | background: lighten($color-dark, 10%); 122 | .play { 123 | border-right: 1px solid lighten($color-dark, 25%); 124 | svg { 125 | fill: lighten($color-dark, 50%); 126 | } 127 | &:hover { 128 | svg { 129 | fill: white; 130 | } 131 | } 132 | } 133 | .range-buttons { 134 | .arrow { 135 | fill: darken($color-dark, 10%); 136 | opacity: 1; 137 | } 138 | .range-button { 139 | background: $color-dark; 140 | color: darken(#919191, 5%); 141 | } 142 | } 143 | } 144 | .right { 145 | background: $color-dark; 146 | color: lighten($color-dark, 50%); 147 | } 148 | } 149 | &-timelines { 150 | .timeline { 151 | border-top: 1px solid lighten($color-dark, 20%); 152 | .left { 153 | border-right: 1px solid lighten($color-dark, 25%); 154 | background: $color-dark; 155 | color: lighten($color-dark, 50%); 156 | .tick-wrapper { 157 | .tick svg { 158 | fill: $color-dark; 159 | } 160 | } 161 | .count-wrapper { 162 | .count { 163 | background: lighten($color-dark, 10%); 164 | color: #919191; 165 | } 166 | } 167 | } 168 | .right { 169 | background: lighten($color-dark, 10%); 170 | } 171 | 172 | &.active { 173 | .left { 174 | .tick-wrapper .tick svg { 175 | fill: white; 176 | } 177 | } 178 | .right { 179 | background: repeating-linear-gradient( 180 | -45deg, 181 | $color-dark, 182 | $color-dark 5px, 183 | lighten($color-dark, 3%) 5px, 184 | lighten($color-dark, 3%) 6px 185 | ); 186 | } 187 | } 188 | &:not(.active) { 189 | .tick { 190 | border: 1px solid lighten($color-dark, 30%); 191 | &:hover { 192 | border: 1px solid #b2b2b2; 193 | } 194 | } 195 | } 196 | 197 | } 198 | } 199 | } 200 | } 201 | 202 | 203 | /* Timeline colors 204 | ---------------------*/ 205 | 206 | .timeline[data-theme="purple"] { 207 | &.active { 208 | .tick { 209 | border: 1px solid $purple; 210 | background: $purple; 211 | } 212 | } 213 | .sub { 214 | border: 1px solid $purple; 215 | background: $purple-light; 216 | color: $purple; 217 | } 218 | } 219 | .timeline[data-theme="orange"] { 220 | &.active { 221 | .tick { 222 | border: 1px solid $orange; 223 | background: $orange; 224 | } 225 | } 226 | .sub { 227 | border: 1px solid $orange; 228 | background: $orange-light; 229 | color: $orange; 230 | } 231 | } 232 | .timeline[data-theme="blue"] { 233 | &.active { 234 | .tick { 235 | border: 1px solid $blue; 236 | background: $blue; 237 | } 238 | } 239 | .sub { 240 | border: 1px solid $blue; 241 | background: $blue-light; 242 | color: $blue; 243 | } 244 | } 245 | .timeline[data-theme="green"] { 246 | &.active { 247 | .tick { 248 | border: 1px solid $green; 249 | background: $green; 250 | } 251 | } 252 | .sub { 253 | border: 1px solid $green; 254 | background: $green-light; 255 | color: $green; 256 | } 257 | } 258 | .timeline[data-theme="red"] { 259 | &.active { 260 | .tick { 261 | border: 1px solid $red; 262 | background: $red; 263 | } 264 | } 265 | .sub { 266 | border: 1px solid $red; 267 | background: $red-light; 268 | color: $red; 269 | } 270 | } 271 | 272 | 273 | /* Ranges colors 274 | ---------------------*/ 275 | 276 | .subtitler[data-color="purple"] { 277 | .subtitler-editor-range { 278 | .start, .end { 279 | border-right: 1px dashed $purple; 280 | &-time { 281 | background: $purple; 282 | &:after { 283 | border-bottom-color: $purple; 284 | } 285 | } 286 | } 287 | } 288 | .range-button.active { 289 | &:after { 290 | background: $purple; 291 | } 292 | } 293 | } 294 | .subtitler[data-color="orange"] { 295 | .subtitler-editor-range { 296 | .start, .end { 297 | border-right: 1px dashed $orange; 298 | &-time { 299 | background: $orange; 300 | &:after { 301 | border-bottom-color: $orange; 302 | } 303 | } 304 | } 305 | } 306 | .range-button.active { 307 | &:after { 308 | background: $orange; 309 | } 310 | } 311 | } 312 | .subtitler[data-color="blue"] { 313 | .subtitler-editor-range { 314 | .start, .end { 315 | border-right: 1px dashed $blue; 316 | &-time { 317 | background: $blue; 318 | &:after { 319 | border-bottom-color: $blue; 320 | } 321 | } 322 | } 323 | } 324 | .range-button.active { 325 | &:after { 326 | background: $blue; 327 | } 328 | } 329 | } 330 | .subtitler[data-color="green"] { 331 | .subtitler-editor-range { 332 | .start, .end { 333 | border-right: 1px dashed $green; 334 | &-time { 335 | background: $green; 336 | &:after { 337 | border-bottom-color: $green; 338 | } 339 | } 340 | } 341 | } 342 | .range-button.active { 343 | &:after { 344 | background: $green; 345 | } 346 | } 347 | } 348 | .subtitler[data-color="red"] { 349 | .subtitler-editor-range { 350 | .start, .end { 351 | border-right: 1px dashed $red; 352 | &-time { 353 | background: $red; 354 | &:after { 355 | border-bottom-color: $red; 356 | } 357 | } 358 | } 359 | } 360 | .range-button.active { 361 | &:after { 362 | background: $red; 363 | } 364 | } 365 | } -------------------------------------------------------------------------------- /src/assets/css/components/_editor.scss: -------------------------------------------------------------------------------- 1 | .subtitler-editor { 2 | position: relative; 3 | width: 100%; 4 | &-firstline { 5 | width: 100%; 6 | height: 38px; 7 | display: flex; 8 | align-items: flex-end; 9 | .left { 10 | display: flex; 11 | align-items: center; 12 | width: 200px; 13 | padding-right: 10px; 14 | font-size: 0.75rem; 15 | .play { 16 | width: 38px; 17 | height: 38px; 18 | display: flex; 19 | justify-content: center; 20 | align-items: center; 21 | cursor: pointer; 22 | svg { 23 | width: 12px; 24 | transition: fill 0.2s ease-out; 25 | opacity: 1; 26 | } 27 | } 28 | .range-buttons { 29 | display: flex; 30 | align-items: center; 31 | margin-left: 12px; 32 | .arrow { 33 | display: block; 34 | width: 10px; 35 | height: 10px; 36 | opacity: 0.15; 37 | } 38 | .range-button { 39 | position: relative; 40 | display: block; 41 | padding: 5px 8px 7px 8px; 42 | border-radius: 4px; 43 | font-weight: 600; 44 | cursor: pointer; 45 | &:first-child { 46 | margin-right: 8px; 47 | } 48 | &:nth-child(3) { 49 | margin-left: 8px; 50 | } 51 | &:hover { 52 | color: black; 53 | } 54 | &.disabled { 55 | opacity: 0.25; 56 | pointer-events: none; 57 | } 58 | &.active { 59 | padding-right: 15px; 60 | &:after { 61 | position: absolute; 62 | content: ''; 63 | right: 4px; 64 | bottom: 4px; 65 | width: 5px; 66 | height: 5px; 67 | border-radius: 50%; 68 | } 69 | } 70 | } 71 | } 72 | } 73 | .right { 74 | width: calc(100% - 200px); 75 | height: calc(100% - 3px); 76 | display: flex; 77 | align-items: center; 78 | justify-content: flex-end; 79 | padding-top: 3px; 80 | padding-right: 12px; 81 | font-size: 0.75rem; 82 | } 83 | } 84 | &-timelines { 85 | .timeline { 86 | width: 100%; 87 | height: 44px; 88 | background: white; 89 | display: flex; 90 | .left { 91 | display: flex; 92 | align-items: center; 93 | width: 200px; 94 | font-size: 0.75rem; 95 | .tick-wrapper { 96 | width: 40px; 97 | height: 40px; 98 | display: flex; 99 | align-items: center; 100 | justify-content: center; 101 | .tick { 102 | width: 14px; 103 | height: 14px; 104 | border-radius: 3px; 105 | display: flex; 106 | justify-content: center; 107 | align-items: center; 108 | cursor: pointer; 109 | svg { 110 | width: 8px; 111 | height: 8px; 112 | } 113 | } 114 | } 115 | .name { 116 | width: calc(100% - 80px); 117 | } 118 | .count-wrapper { 119 | width: 40px; 120 | height: 40px; 121 | display: flex; 122 | align-items: center; 123 | justify-content: center; 124 | .count { 125 | padding: 2px 5px; 126 | border-radius: 4px; 127 | } 128 | } 129 | } 130 | .right { 131 | width: calc(100% - 200px); 132 | display: flex; 133 | align-items: center; 134 | .subs { 135 | position: relative; 136 | width: 100%; 137 | height: 30px; 138 | .sub { 139 | position: absolute; 140 | top: 0; 141 | height: 100%; 142 | border-radius: 4px; 143 | font-size: 0.75rem; 144 | &-index { 145 | position: absolute; 146 | top: 3px; 147 | left: 5px; 148 | } 149 | &-handle { 150 | position: absolute; 151 | top: 0; 152 | left: 0; 153 | width: 2px; 154 | height: 100%; 155 | pointer-events: none; 156 | &-left { 157 | left: -1px; 158 | } 159 | &-right { 160 | left: calc(100% - 1px); 161 | } 162 | } 163 | } 164 | } 165 | } 166 | 167 | .left, .right { 168 | > * { 169 | transition: opacity 0.2s ease-out; 170 | } 171 | } 172 | 173 | &.active { 174 | .right { 175 | .subs { 176 | .sub-handle { 177 | cursor: ew-resize; 178 | pointer-events: auto; 179 | } 180 | } 181 | } 182 | } 183 | 184 | &.inactive { 185 | .left, .right { 186 | > * { 187 | opacity: 0.2; 188 | } 189 | } 190 | &:hover { 191 | .left, .right { 192 | > * { 193 | opacity: 1; 194 | } 195 | } 196 | } 197 | } 198 | &:not(.active) { 199 | .tick { 200 | transition: border 0.15s ease-out; 201 | } 202 | } 203 | } 204 | } 205 | &-progress { 206 | position: absolute; 207 | top: 0; 208 | left: 200px; 209 | width: calc(100% - 200px); 210 | height: 100%; 211 | pointer-events: none; 212 | z-index: 3; 213 | .bar { 214 | position: relative; 215 | width: 0%; 216 | height: 100%; 217 | border-top: 4px solid $vibrant-blue; 218 | border-right: 1px dashed $vibrant-blue; 219 | .handle { 220 | position: absolute; 221 | width: 14px; 222 | height: 14px; 223 | background: $vibrant-blue; 224 | right: 0; 225 | top: -2px; 226 | border-radius: 50%; 227 | transform: translate(50%, -50%); 228 | pointer-events: auto; 229 | cursor: ew-resize; 230 | &:hover { 231 | transform: translate(50%, -50%) scale(1.1); 232 | } 233 | } 234 | .current-time { 235 | position: absolute; 236 | right: 0; 237 | transform: translateX(50%); 238 | top: 8px; 239 | background: $vibrant-blue; 240 | padding: 2px 5px; 241 | font-size: 0.75rem; 242 | color: white; 243 | border-radius: 2px; 244 | min-width: 35px; 245 | text-align: center; 246 | &:after { 247 | bottom: 100%; 248 | left: 50%; 249 | border: solid transparent; 250 | content: " "; 251 | height: 0; 252 | width: 0; 253 | position: absolute; 254 | pointer-events: none; 255 | border-color: rgba(0, 0, 0, 0); 256 | border-bottom-color: $vibrant-blue; 257 | border-width: 3px; 258 | margin-left: -3px; 259 | } 260 | } 261 | } 262 | } 263 | &-range { 264 | position: absolute; 265 | top: 0; 266 | left: 200px; 267 | width: calc(100% - 200px); 268 | height: 100%; 269 | pointer-events: none; 270 | z-index: 2; 271 | .start, .end { 272 | position: absolute; 273 | top: 0; 274 | left: 0; 275 | width: 0; 276 | height: 100%; 277 | &-time { 278 | position: absolute; 279 | right: 0; 280 | transform: translateX(50%); 281 | top: 12px; 282 | padding: 2px 5px; 283 | font-size: 0.75rem; 284 | color: white; 285 | border-radius: 2px; 286 | min-width: 35px; 287 | text-align: center; 288 | &:after { 289 | bottom: 100%; 290 | left: 50%; 291 | border: solid transparent; 292 | content: " "; 293 | height: 0; 294 | width: 0; 295 | position: absolute; 296 | pointer-events: none; 297 | border-color: rgba(0, 0, 0, 0); 298 | border-width: 3px; 299 | margin-left: -3px; 300 | } 301 | } 302 | } 303 | } 304 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > ⚠️ This plugin won't be maintained for Kirby 4+. 2 | 3 | # Kirby 3 – Subtitler 4 | 5 | This plugin allows you to sync any content with audio or video files. 6 | 7 |  8 | 9 | 10 | 11 | ## Overview 12 | 13 | > This plugin is completely free and published under the MIT license. However, if you are using it in a commercial project and want to help me keep up with maintenance, please consider [making a donation of your choice](https://www.paypal.me/sylvainjl) or purchasing your license(s) through [my affiliate link](https://a.paddle.com/v2/click/1129/36369?link=1170). 14 | 15 | - [1. Installation](#1-installation) 16 | - [2. Blueprint usage](#2-blueprint-usage) 17 | - [3. Options](#3-options) 18 | * [3.1. Timelines](#31-timelines) 19 | * [3.2. Display options](#32-display-options) 20 | * [3.3. Storage options](#33-storage-options) 21 | - [4. Template usage](#4-template-usage) 22 | * [4.1. How are the informations stored?](#41-how-are-the-informations-stored) 23 | * [4.2. Create a subtitles collections](#42-create-a-subtitles-collection) 24 | * [4.3. Generate and use a track file](#43-generate-and-use-a-track-file) 25 | - [5. License](#5-license) 26 | - [6. Credits](#6-credits) 27 | 28 | 29 | #### TLDR – Just get me started 👀 30 | 31 | - [Blueprint example](#2-blueprint-usage) 32 | - [Template example](#single-language-setup) 33 | 34 | 35 | 36 | ## 1. Installation 37 | 38 | > If you are looking to use this field with Kirby 2, please switch to the `kirby-2` branch. 39 | 40 | Download and copy this repository to ```/site/plugins/subtitler``` 41 | 42 | Alternatively, you can install it with composer: ```composer require sylvainjule/subtitler``` 43 | 44 | 45 | 46 | ## 2. Blueprint usage 47 | 48 | The subtitler is a section which doesn't store any information itself. 49 | Instead, it provides an interface to manipulate content from other fields. Here's a basic setup of the plugin within your blueprint: 50 | 51 | #### 2.1. Example 52 | 53 | ```yaml 54 | columns: 55 | - width: 2/3 56 | sections: 57 | subtitler: 58 | type: subtitler 59 | timelines: 60 | mytimeline: 61 | id: mytimeline 62 | label: My timeline 63 | color: purple 64 | storage: 65 | src: src 66 | subs: subs 67 | 68 | - width: 1/3 69 | sections: 70 | myfields: 71 | type: fields 72 | fields: 73 | src: 74 | type: files 75 | max: 1 76 | subs: 77 | type: structure 78 | sortable: false 79 | fields: 80 | note: 81 | label: Note 82 | type: text 83 | ``` 84 | 85 | Notice the `sortable: false` for the structure field, else the section will easily be lost with the incrementation. 86 | 87 | You should also ensure that the structure field will only contain subtitles. To do so, you can hide the `Add +` button of the field. This way, there will be no alternative to populate it other than the subtitler section. 88 | 89 | This should be set in a custom `panel.css`: 90 | 91 | ```css 92 | .k-field-subs .k-field-header button { 93 | display: none; 94 | } 95 | ``` 96 | 97 | #### 2.2. Usage within a file page 98 | 99 | You can use this plugin within a file page by setting it like stated above, but skipping the `src` option within the `storage` settings. The plugin will automatically detect the image of the given page. 100 | 101 | 102 | 103 | ## 3. Options 104 | 105 | ### 3.1. Timelines 106 | 107 | This new version handles multiple timelines for a given video / audio file, instead of having to duplicate the field. 108 | Timelines have to be specified within the `timelines` option, an given 3 attributes : 109 | 110 | - An `id` (will be used to divide the structure entries) 111 | - A `label` (will be displayed within the editor) 112 | - A `color` (will be displayed within the editor, and have to be either `blue`, `green`, `red`, `orange`or `purple`) 113 | 114 | Here's an example with multiple timelines: 115 | 116 | ```yaml 117 | timelines: 118 | chapters: 119 | id: chapters 120 | label: Chapters 121 | color: purple 122 | links: 123 | id: links 124 | label: Links 125 | color: orange 126 | images: 127 | id: images 128 | label: Images 129 | color: blue 130 | ``` 131 | 132 | Labels also support multi-language syntax: 133 | 134 | ```yaml 135 | timelines: 136 | chapters: 137 | id: chapters 138 | label: 139 | en: Chapters 140 | fr: Chapitres 141 | color: purple 142 | ``` 143 | 144 | ### 3.2. Display options 145 | 146 | ##### • Theme 147 | 148 | > type: `string`, default: `light` 149 | 150 | You have two themes available, a dark and a light one. (doesn't work for now, but you'll have them pretty soon) 151 | 152 |  153 | 154 | ##### • Debug 155 | 156 | > type: `boolean`, default: `false` 157 | 158 | When set to `true`, timecodes and coordinates based on cursor position will be shown in real-time in the toolbar. Not needed unless you're trying to extend some functionality. 159 | 160 |  161 | 162 | 163 | ### 3.3. Storage options 164 | 165 | ##### • Audio / video file 166 | 167 | The section needs to be synced with a field returning a file object to work with. 168 | Using a ```files``` field is required. Not only does it look nicer than a select field, but most importantly it returns both an absolute url and an id of the file: 169 | 170 | ```yaml 171 | # subtitler section 172 | storage: 173 | src: src 174 | 175 | # fields section 176 | src: 177 | type: files 178 | max: 1 179 | ``` 180 | 181 | > Note: You don’t need to explicitly set a ```max``` value, though it may look clearer. When confronted to a files field containing multiple files, the plugin will always use the first one. 182 | 183 | ##### • Subtitles structure 184 | 185 | The plugin needs an associated structure field to store the subtitles informations. It has 5 reserved fields that shouldn't be used for any other purpose: `timeline`, `start`, `startprop`,`end` and `endprop`. Those will be automatically set and don't need to be explicitely specified unless you want to show them within the panel: 186 | 187 |  188 | 189 | ```yaml 190 | # subtitler section 191 | storage: 192 | subs: subs 193 | 194 | # fields section 195 | subs: 196 | type: structure 197 | fields: 198 | timeline: 199 | label: 'Timeline' 200 | type: text 201 | start: 202 | label: 'Start' 203 | type: text 204 | startprop: 205 | label: 'Start (%)' 206 | type: text 207 | end: 208 | label: 'End' 209 | type: text 210 | endprop: 211 | label: 'End (%)' 212 | type: text 213 | ``` 214 | 215 | Otherwise, you can directly start adding fields you'd like to sync content with: 216 | 217 |  218 | 219 | 220 | ```yaml 221 | # subtitler section 222 | storage: 223 | subs: subs 224 | 225 | # fields section 226 | subs: 227 | type: structure 228 | fields: 229 | mynote: 230 | label: 'Note' 231 | type: text 232 | ``` 233 | 234 | 235 | 236 | ## 4. Template usage 237 | 238 | There are different ways to use the subtitles, depending on your use case. You could, for example, send them as `json` to a Javascript app. Or, you could make use of some simple helpers shipped with the plugin to use them directly in your templates: 239 | 240 | ### 4.1. How are the informations stored? 241 | 242 | Each entry will have a `start`, `startprop`, `end` and `endprop` value, formatted like this: 243 | 244 | ```yaml 245 | start / end : 364.58745 # (number of seconds) 246 | startprop / endprop : 0.35 # (the progress relative to the file's duration, between 0 and 1) 247 | # + all your custom field 248 | ``` 249 | 250 | ### 4.2. Create a subtitles collections 251 | 252 | Subtitles are stored in a structure field. 253 | If you have a single timeline, you can create a collection with the ```toStructure()``` method to access them: 254 | 255 | ```php 256 | foreach($page->mysubtitles()->toStructure() as $subtitle) { 257 | // do something 258 | } 259 | ``` 260 | 261 | When dealing with multiple timelines, you either need to filter them manually or call the `getTimeline` method, which is a `toStructure` wrapper providing filtering by timeline. Use it this way: 262 | 263 | ```php 264 | foreach($page->mysubtitles()->getTimeline('timelinekey') as $subtitle) { 265 | // do something 266 | } 267 | ``` 268 | 269 | ### 4.3. Generate and use a track file 270 | 271 | #### • Creating the tracks 272 | 273 | The plugin provides a simple way to generate [WebVTT files](https://developer.mozilla.org/en-US/docs/Web/API/WebVTT_API), which can later be added in your html `` tags. 274 | 275 | First, make sure that the field used to get the subtitles' content is called `subtitle`, else the plugin won't know where to look for the text: 276 | 277 | ``` 278 | subs: 279 | label: Subtitles 280 | type: structure 281 | fields: 282 | subtitle: 283 | label: Content 284 | type: text 285 | ``` 286 | 287 | Then, include the `tracksbuilder` field on your page, which is basically a button generating the `.vtt` files on request (stored in the page's folder). 288 | 289 | ```yaml 290 | mytracks: 291 | label: Generate tracks 292 | type: tracksbuilder 293 | src: subs # (key of the structure field containing the subtitles) 294 | ``` 295 | 296 | The output will look like: 297 | 298 | ```yaml 299 | # if single language 300 | ${fieldname}-${timeline}.vtt # (content/my/page/subs-notes.vtt) 301 | 302 | # if multi-language 303 | ${fieldname}-${timeline}-${languageCode}.vtt # (content/my/page/subs-notes-en.vtt) 304 | ``` 305 | 306 | You are now able to get them easily by calling `->vtt('timeline')` on the subtitles structure field (example below). 307 | 308 | 309 | #### • Single language setup 310 | 311 | Once your `.vtt` files have been created, you can [include them](https://developer.mozilla.org/en-US/docs/Web/Apps/Fundamentals/Audio_and_video_delivery/Adding_captions_and_subtitles_to_HTML5_video) in your `video` tag. 312 | Here is a basic example, using a timeline called `notes`: 313 | 314 | ```php 315 | src()->toFile()): ?> 317 | 318 | 319 | 320 | subs()->vtt('notes')): ?> 322 | 323 | 324 | 325 | 326 | 327 | ``` 328 | 329 | #### • Multi-language setup 330 | 331 | Multi-language setup is pretty much the same, except we need to put the `track` tag within a loop and set the desired languages explicitely: 332 | 333 | ```php 334 | 335 | src()->toFile()): ?> 337 | 338 | 339 | 340 | languages() as $language): ?> 342 | subs()->vtt('notes', $language->code())): ?> 344 | 345 | 346 | 347 | 348 | 349 | 350 | ``` 351 | 352 | 353 | 354 | ## 5. License 355 | 356 | MIT 357 | 358 | 359 | 360 | ## 6. Credits 361 | 362 | - The fields synchronization has been taken from [@rasteiner](https://github.com/rasteiner/kn-map-section)'s map section. 🙏 363 | -------------------------------------------------------------------------------- /index.css: -------------------------------------------------------------------------------- 1 | .subtitler[data-theme=light] .subtitler-background{background:#efefef url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXR0ZXJuIGlkPSJhIiB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHBhdHRlcm5Vbml0cz0idXNlclNwYWNlT25Vc2UiPjxwYXRoIGZpbGw9InJnYmEoMCwgMCwgMCwgMC4yKSIgZD0iTTAgMGgxMHYxMEgwem0xMCAxMGgxMHYxMEgxMHoiLz48L3BhdHRlcm4+PHJlY3QgZmlsbD0idXJsKCNhKSIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIvPjwvc3ZnPg==");opacity:.45}.subtitler[data-theme=light] .subtitler-placeholder-ctn{background:#fcfcfc}.subtitler[data-theme=light] .subtitler-toolbar-debug{background:#fff}.subtitler[data-theme=light] .subtitler-editor-firstline .left{border-right:1px solid #e5e5e5;background:#f8f8f8}.subtitler[data-theme=light] .subtitler-editor-firstline .left .play{border-right:1px solid #e5e5e5}.subtitler[data-theme=light] .subtitler-editor-firstline .left .play svg{fill:#9e9e9e}.subtitler[data-theme=light] .subtitler-editor-firstline .left .play:hover svg{fill:#000}.subtitler[data-theme=light] .subtitler-editor-firstline .left .range-buttons .range-button{background:#e8e8e8;color:#848484}.subtitler[data-theme=light] .subtitler-editor-firstline .right{background:#fff;color:#ababab}.subtitler[data-theme=light] .subtitler-editor-timelines .timeline{border-top:1px solid #e5e5e5}.subtitler[data-theme=light] .subtitler-editor-timelines .timeline .left{border-right:1px solid #e5e5e5}.subtitler[data-theme=light] .subtitler-editor-timelines .timeline .left .tick-wrapper .tick svg{fill:#fff}.subtitler[data-theme=light] .subtitler-editor-timelines .timeline .left .count-wrapper .count{background:#f2f2f2;color:#919191}.subtitler[data-theme=light] .subtitler-editor-timelines .timeline .right{background:#f8f8f8}.subtitler[data-theme=light] .subtitler-editor-timelines .timeline.active .right{background:repeating-linear-gradient(-45deg,#fff,#fff 5px,#f2f2f2 0,#f2f2f2 6px)}.subtitler[data-theme=light] .subtitler-editor-timelines .timeline:not(.active) .tick{border:1px solid #b2b2b2}.subtitler[data-theme=light] .subtitler-editor-timelines .timeline:not(.active) .tick:hover{border:1px solid #2d2f36}.subtitler[data-theme=dark] .subtitler-background{background:#2d2f36 url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxwYXR0ZXJuIGlkPSJhIiB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHBhdHRlcm5Vbml0cz0idXNlclNwYWNlT25Vc2UiPjxwYXRoIGZpbGw9InJnYmEoMCwgMCwgMCwgMC4yKSIgZD0iTTAgMGgxMHYxMEgwem0xMCAxMGgxMHYxMEgxMHoiLz48L3BhdHRlcm4+PHJlY3QgZmlsbD0idXJsKCNhKSIgd2lkdGg9IjEwMCUiIGhlaWdodD0iMTAwJSIvPjwvc3ZnPg==")}.subtitler[data-theme=dark] .subtitler-placeholder-ctn{background:#16171a;color:#c5c5c6}.subtitler[data-theme=dark] .subtitler-toolbar-debug{background:#16171a;color:#fff}.subtitler[data-theme=dark] .subtitler-file .audio-icon svg path:first-child{fill:#5c606d}.subtitler[data-theme=dark] .subtitler-file .audio-icon svg path:last-child{fill:#16171a}.subtitler[data-theme=dark] .subtitler-editor-firstline .left{border-right:1px solid #50545f;background:#2d2f36}.subtitler[data-theme=dark] .subtitler-editor-firstline .left .play{border-right:1px solid #50545f}.subtitler[data-theme=dark] .subtitler-editor-firstline .left .play svg{fill:#8f93a0}.subtitler[data-theme=dark] .subtitler-editor-firstline .left .play:hover svg{fill:#fff}.subtitler[data-theme=dark] .subtitler-editor-firstline .left .range-buttons .arrow{fill:#000;opacity:1}.subtitler[data-theme=dark] .subtitler-editor-firstline .left .range-buttons .range-button{background:#16171a;color:#848484}.subtitler[data-theme=dark] .subtitler-editor-firstline .right{background:#16171a;color:#8f93a0}.subtitler[data-theme=dark] .subtitler-editor-timelines .timeline{border-top:1px solid #454851}.subtitler[data-theme=dark] .subtitler-editor-timelines .timeline .left{border-right:1px solid #50545f;background:#16171a;color:#8f93a0}.subtitler[data-theme=dark] .subtitler-editor-timelines .timeline .left .tick-wrapper .tick svg{fill:#16171a}.subtitler[data-theme=dark] .subtitler-editor-timelines .timeline .left .count-wrapper .count{background:#2d2f36;color:#919191}.subtitler[data-theme=dark] .subtitler-editor-timelines .timeline .right{background:#2d2f36}.subtitler[data-theme=dark] .subtitler-editor-timelines .timeline.active .left .tick-wrapper .tick svg{fill:#fff}.subtitler[data-theme=dark] .subtitler-editor-timelines .timeline.active .right{background:repeating-linear-gradient(-45deg,#16171a,#16171a 5px,#1d1e22 0,#1d1e22 6px)}.subtitler[data-theme=dark] .subtitler-editor-timelines .timeline:not(.active) .tick{border:1px solid #5c606d}.subtitler[data-theme=dark] .subtitler-editor-timelines .timeline:not(.active) .tick:hover{border:1px solid #b2b2b2}.timeline[data-theme=purple].active .tick{border:1px solid #7a73a5;background:#7a73a5}.timeline[data-theme=purple] .sub{border:1px solid #7a73a5;background:#ece7ff;color:#7a73a5}.timeline[data-theme=orange].active .tick{border:1px solid #e19d45;background:#e19d45}.timeline[data-theme=orange] .sub{border:1px solid #e19d45;background:#fff1e8;color:#e19d45}.timeline[data-theme=blue].active .tick{border:1px solid #7682bb;background:#7682bb}.timeline[data-theme=blue] .sub{border:1px solid #7682bb;background:#e1ecff;color:#7682bb}.timeline[data-theme=green].active .tick{border:1px solid #7aba4b;background:#7aba4b}.timeline[data-theme=green] .sub{border:1px solid #7aba4b;background:#f1faed;color:#7aba4b}.timeline[data-theme=red].active .tick{border:1px solid #f93048;background:#f93048}.timeline[data-theme=red] .sub{border:1px solid #f93048;background:#feeaee;color:#f93048}.subtitler[data-color=purple] .subtitler-editor-range .end,.subtitler[data-color=purple] .subtitler-editor-range .start{border-right:1px dashed #7a73a5}.subtitler[data-color=purple] .subtitler-editor-range .end-time,.subtitler[data-color=purple] .subtitler-editor-range .start-time{background:#7a73a5}.subtitler[data-color=purple] .subtitler-editor-range .end-time:after,.subtitler[data-color=purple] .subtitler-editor-range .start-time:after{border-bottom-color:#7a73a5}.subtitler[data-color=purple] .range-button.active:after{background:#7a73a5}.subtitler[data-color=orange] .subtitler-editor-range .end,.subtitler[data-color=orange] .subtitler-editor-range .start{border-right:1px dashed #e19d45}.subtitler[data-color=orange] .subtitler-editor-range .end-time,.subtitler[data-color=orange] .subtitler-editor-range .start-time{background:#e19d45}.subtitler[data-color=orange] .subtitler-editor-range .end-time:after,.subtitler[data-color=orange] .subtitler-editor-range .start-time:after{border-bottom-color:#e19d45}.subtitler[data-color=orange] .range-button.active:after{background:#e19d45}.subtitler[data-color=blue] .subtitler-editor-range .end,.subtitler[data-color=blue] .subtitler-editor-range .start{border-right:1px dashed #7682bb}.subtitler[data-color=blue] .subtitler-editor-range .end-time,.subtitler[data-color=blue] .subtitler-editor-range .start-time{background:#7682bb}.subtitler[data-color=blue] .subtitler-editor-range .end-time:after,.subtitler[data-color=blue] .subtitler-editor-range .start-time:after{border-bottom-color:#7682bb}.subtitler[data-color=blue] .range-button.active:after{background:#7682bb}.subtitler[data-color=green] .subtitler-editor-range .end,.subtitler[data-color=green] .subtitler-editor-range .start{border-right:1px dashed #7aba4b}.subtitler[data-color=green] .subtitler-editor-range .end-time,.subtitler[data-color=green] .subtitler-editor-range .start-time{background:#7aba4b}.subtitler[data-color=green] .subtitler-editor-range .end-time:after,.subtitler[data-color=green] .subtitler-editor-range .start-time:after{border-bottom-color:#7aba4b}.subtitler[data-color=green] .range-button.active:after{background:#7aba4b}.subtitler[data-color=red] .subtitler-editor-range .end,.subtitler[data-color=red] .subtitler-editor-range .start{border-right:1px dashed #f93048}.subtitler[data-color=red] .subtitler-editor-range .end-time,.subtitler[data-color=red] .subtitler-editor-range .start-time{background:#f93048}.subtitler[data-color=red] .subtitler-editor-range .end-time:after,.subtitler[data-color=red] .subtitler-editor-range .start-time:after{border-bottom-color:#f93048}.subtitler[data-color=red] .range-button.active:after{background:#f93048}.subtitler-editor{position:relative;width:100%}.subtitler-editor-firstline{width:100%;height:38px;display:flex;align-items:flex-end}.subtitler-editor-firstline .left{display:flex;align-items:center;width:200px;padding-right:10px;font-size:.75rem}.subtitler-editor-firstline .left .play{width:38px;height:38px;display:flex;justify-content:center;align-items:center;cursor:pointer}.subtitler-editor-firstline .left .play svg{width:12px;transition:fill .2s ease-out;opacity:1}.subtitler-editor-firstline .left .range-buttons{display:flex;align-items:center;margin-left:12px}.subtitler-editor-firstline .left .range-buttons .arrow{display:block;width:10px;height:10px;opacity:.15}.subtitler-editor-firstline .left .range-buttons .range-button{position:relative;display:block;padding:5px 8px 7px;border-radius:4px;font-weight:600;cursor:pointer}.subtitler-editor-firstline .left .range-buttons .range-button:first-child{margin-right:8px}.subtitler-editor-firstline .left .range-buttons .range-button:nth-child(3){margin-left:8px}.subtitler-editor-firstline .left .range-buttons .range-button:hover{color:#000}.subtitler-editor-firstline .left .range-buttons .range-button.disabled{opacity:.25;pointer-events:none}.subtitler-editor-firstline .left .range-buttons .range-button.active{padding-right:15px}.subtitler-editor-firstline .left .range-buttons .range-button.active:after{position:absolute;content:"";right:4px;bottom:4px;width:5px;height:5px;border-radius:50%}.subtitler-editor-firstline .right{width:calc(100% - 200px);height:calc(100% - 3px);display:flex;align-items:center;justify-content:flex-end;padding-top:3px;padding-right:12px;font-size:.75rem}.subtitler-editor-timelines .timeline{width:100%;height:44px;background:#fff;display:flex}.subtitler-editor-timelines .timeline .left{display:flex;align-items:center;width:200px;font-size:.75rem}.subtitler-editor-timelines .timeline .left .tick-wrapper{width:40px;height:40px;display:flex;align-items:center;justify-content:center}.subtitler-editor-timelines .timeline .left .tick-wrapper .tick{width:14px;height:14px;border-radius:3px;display:flex;justify-content:center;align-items:center;cursor:pointer}.subtitler-editor-timelines .timeline .left .tick-wrapper .tick svg{width:8px;height:8px}.subtitler-editor-timelines .timeline .left .name{width:calc(100% - 80px)}.subtitler-editor-timelines .timeline .left .count-wrapper{width:40px;height:40px;display:flex;align-items:center;justify-content:center}.subtitler-editor-timelines .timeline .left .count-wrapper .count{padding:2px 5px;border-radius:4px}.subtitler-editor-timelines .timeline .right{width:calc(100% - 200px);display:flex;align-items:center}.subtitler-editor-timelines .timeline .right .subs{position:relative;width:100%;height:30px}.subtitler-editor-timelines .timeline .right .subs .sub{position:absolute;top:0;height:100%;border-radius:4px;font-size:.75rem}.subtitler-editor-timelines .timeline .right .subs .sub-index{position:absolute;top:3px;left:5px}.subtitler-editor-timelines .timeline .right .subs .sub-handle{position:absolute;top:0;left:0;width:2px;height:100%;pointer-events:none}.subtitler-editor-timelines .timeline .right .subs .sub-handle-left{left:-1px}.subtitler-editor-timelines .timeline .right .subs .sub-handle-right{left:calc(100% - 1px)}.subtitler-editor-timelines .timeline .left>*,.subtitler-editor-timelines .timeline .right>*{transition:opacity .2s ease-out}.subtitler-editor-timelines .timeline.active .right .subs .sub-handle{cursor:ew-resize;pointer-events:auto}.subtitler-editor-timelines .timeline.inactive .left>*,.subtitler-editor-timelines .timeline.inactive .right>*{opacity:.2}.subtitler-editor-timelines .timeline.inactive:hover .left>*,.subtitler-editor-timelines .timeline.inactive:hover .right>*{opacity:1}.subtitler-editor-timelines .timeline:not(.active) .tick{transition:border .15s ease-out}.subtitler-editor-progress{position:absolute;top:0;left:200px;width:calc(100% - 200px);height:100%;pointer-events:none;z-index:3}.subtitler-editor-progress .bar{position:relative;width:0;height:100%;border-top:4px solid #2288e8;border-right:1px dashed #2288e8}.subtitler-editor-progress .bar .handle{position:absolute;width:14px;height:14px;background:#2288e8;right:0;top:-2px;border-radius:50%;transform:translate(50%,-50%);pointer-events:auto;cursor:ew-resize}.subtitler-editor-progress .bar .handle:hover{transform:translate(50%,-50%) scale(1.1)}.subtitler-editor-progress .bar .current-time{position:absolute;right:0;transform:translateX(50%);top:8px;background:#2288e8;padding:2px 5px;font-size:.75rem;color:#fff;border-radius:2px;min-width:35px;text-align:center}.subtitler-editor-progress .bar .current-time:after{bottom:100%;left:50%;content:" ";height:0;width:0;position:absolute;pointer-events:none;border:3px solid transparent;border-bottom-color:#2288e8;margin-left:-3px}.subtitler-editor-range{position:absolute;top:0;left:200px;width:calc(100% - 200px);height:100%;pointer-events:none;z-index:2}.subtitler-editor-range .end,.subtitler-editor-range .start{position:absolute;top:0;left:0;width:0;height:100%}.subtitler-editor-range .end-time,.subtitler-editor-range .start-time{position:absolute;right:0;transform:translateX(50%);top:12px;padding:2px 5px;font-size:.75rem;color:#fff;border-radius:2px;min-width:35px;text-align:center}.subtitler-editor-range .end-time:after,.subtitler-editor-range .start-time:after{bottom:100%;left:50%;content:" ";height:0;width:0;position:absolute;pointer-events:none;border:3px solid transparent;margin-left:-3px}.subtitler-toolbar{position:absolute;width:100%;top:0;z-index:2;display:flex;justify-content:flex-end}.subtitler-toolbar-debug{height:2rem;margin-left:0;box-shadow:0 2px 5px rgba(22,23,26,.05);display:flex;align-items:center;font-family:monospace;font-size:11px}.subtitler-toolbar-debug .coord{padding:0 1rem}.subtitler-toolbar-debug .coord.prop{width:100px}.subtitler-toolbar-debug .coord.prop:before{content:"x: "}.subtitler-toolbar-debug .coord.time{width:100px;padding-left:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.subtitler-toolbar-debug .coord.time:before{content:"time: "}.subtitler-toolbar-debug .coord.text{width:100px;padding-left:0}.subtitler-toolbar-debug .coord.text:before{content:"text: "}.subtitler{position:relative;width:100%;padding-bottom:0;margin-bottom:3rem;user-select:none}.subtitler button{outline:none}.subtitler-background{position:absolute;width:100%;height:100%}.subtitler-placeholder,.subtitler-placeholder-ctn{position:relative;padding:2.5rem}.subtitler-placeholder-ctn{z-index:1;display:flex;justify-content:center}.subtitler-file{position:relative;width:100%;height:100%;z-index:1;padding:1.5rem 1.5rem 2.5rem}.subtitler-file.debug{padding-top:2.5rem}.subtitler-file .subtitler-video{position:relative;width:100%;display:flex;justify-content:center;align-items:center}.subtitler-file .subtitler-video-ctn{position:relative;display:inline-block;max-width:100%;z-index:2}.subtitler-file .subtitler-video-ctn video{display:block;max-width:100%;max-height:calc(75vh - 12rem)}.subtitler-file .subtitler-audio .audio-icon{display:block;width:75px;margin:1rem auto 0}.tracksbuilder-button{position:relative;display:flex;align-items:center;background:#fff;border-radius:1px;margin-bottom:2px;box-shadow:0 2px 5px rgba(22,23,26,.05);cursor:pointer}.tracksbuilder-button .icon{width:38px;height:38px;background:#f5f5f5;display:flex;align-items:center;justify-content:center}.tracksbuilder-button .icon svg{fill:#16171a;width:16px;height:16px;transform:scaleX(-1)}.tracksbuilder-button .text{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;align-items:baseline;width:calc(100% - 38px);line-height:1.25rem;padding:.5rem .75rem}.tracksbuilder-button .text em{font-style:normal;margin-right:1rem;flex-grow:1;font-size:.875rem;color:#16171a}.tracksbuilder-button.error .icon{background:#d52a20}.tracksbuilder-button.error .icon svg{fill:#fff}.tracksbuilder-button.success .icon svg{fill:#a7bd69}.tracksbuilder-button.processing .icon svg{animation:rotate-icon 1.5s linear infinite}@keyframes rotate-icon{0%{transform:scaleX(-1) rotate(0deg)}to{transform:scaleX(-1) rotate(-1turn)}} -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | (function () {var a={};if(typeof a==="function"){a=a.options}Object.assign(a,function(){var render=function(){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c("svg",{attrs:{"xmlns":"http://www.w3.org/2000/svg","viewBox":"0 0 100 100"}},[_c("path",{attrs:{"fill":"#f8f8f8","d":"M88.76 25.43v66.91a7.66 7.66 0 0 1-7.67 7.66H18.91a7.66 7.66 0 0 1-7.66-7.66V7.66A7.66 7.66 0 0 1 18.91 0h45z"}}),_c("path",{attrs:{"fill":"#c1c1c1","d":"M88.76 25.43H71.58a7.66 7.66 0 0 1-7.66-7.66V0zM68.63 62.29v.26a7.13 7.13 0 1 1-3.7-6.25V40.21l-22.71 4.08V76a7.13 7.13 0 1 1-3.7-6.51V42.75A1.85 1.85 0 0 1 40 40.93l26.41-4.75h.33A1.85 1.85 0 0 1 68.63 38z"}})])};var staticRenderFns=[];return{render:render,staticRenderFns:staticRenderFns,_compiled:true,_scopeId:null,functional:undefined}}());var b={};if(typeof b==="function"){b=b.options}Object.assign(b,function(){var render=function(){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c("svg",{attrs:{"xmlns":"http://www.w3.org/2000/svg","viewBox":"0 0 100 100"}},[_c("path",{attrs:{"d":"M87.73 42L21.46 1.42A9 9 0 0 0 7.83 9.48v81.15c0 7.31 7.66 11.72 13.63 8.05l66.27-40.56a9.53 9.53 0 0 0 0-16.11"}})])};var staticRenderFns=[];return{render:render,staticRenderFns:staticRenderFns,_compiled:true,_scopeId:null,functional:undefined}}());var c={};if(typeof c==="function"){c=c.options}Object.assign(c,function(){var render=function(){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c("svg",{attrs:{"xmlns":"http://www.w3.org/2000/svg","viewBox":"0 0 100 100"}},[_c("path",{attrs:{"d":"M67.17 6a6 6 0 0 1 6-6H87a6 6 0 0 1 6 6v88a6 6 0 0 1-6 6H73.17a6 6 0 0 1-6-6zM7 94a6 6 0 0 0 6 6h13.83a6 6 0 0 0 6-6V6a6 6 0 0 0-6-6H13a6 6 0 0 0-6 6z"}})])};var staticRenderFns=[];return{render:render,staticRenderFns:staticRenderFns,_compiled:true,_scopeId:null,functional:undefined}}());var d={};if(typeof d==="function"){d=d.options}Object.assign(d,function(){var render=function(){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c("svg",{attrs:{"xmlns":"http://www.w3.org/2000/svg","viewBox":"0 0 100 100"}},[_c("path",{attrs:{"d":"M98.35 22.5a5.5 5.5 0 0 0 0-7.86l-13-13a5.5 5.5 0 0 0-7.86 0L66.64 12.57l20.79 20.79zM0 79.21V100h20.79L80 40.83 59.17 20z"}})])};var staticRenderFns=[];return{render:render,staticRenderFns:staticRenderFns,_compiled:true,_scopeId:null,functional:undefined}}());var e={};if(typeof e==="function"){e=e.options}Object.assign(e,function(){var render=function(){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c("svg",{staticClass:"arrow",attrs:{"xmlns":"http://www.w3.org/2000/svg","viewBox":"0 0 100 100"}},[_c("path",{attrs:{"d":"M99.53 52.45a6.26 6.26 0 0 0-1.35-6.8L54.49 2a6.24 6.24 0 0 0-8.83 8.83l33 33H6.39a6.24 6.24 0 1 0 0 12.48H78.7l-33 33a6.24 6.24 0 1 0 8.83 8.83l43.65-43.65a6.23 6.23 0 0 0 1.35-2.04z"}})])};var staticRenderFns=[];return{render:render,staticRenderFns:staticRenderFns,_compiled:true,_scopeId:null,functional:undefined}}());var f={components:{AudioFile:a,PlayIcon:b,PauseIcon:c,EditIcon:d,ArrowIcon:e},data:function(){return{src:{id:"",type:"",url:""},resize:{isResizing:!1,handle:String,index:Number,max:Number,min:Number},loaded:!1,isPlaying:!1,active:null,subs:[],coords:{time:0,prop:0,text:""},currentTime:{time:0,prop:0,text:""},duration:{time:0,text:""},start:{time:0,prop:0,text:""},end:{time:0,prop:0,text:""}}},props:{storage:Object,theme:String,file:String,debug:Boolean,timelines:Object},computed:{isAudio:function(){return"audio"==this.src.type},isVideo:function(){return"video"==this.src.type},currentPlayer:function(){return this.isAudio?this.$refs.audioPlayer:this.$refs.videoPlayer},activeTimeline:function(){return null!=this.active},activeColor:function(){return!!this.activeTimeline&&this.active.color},fileExists:function(){return this.src.url&&this.src.type},startExists:function(){return this.start.time>0},endExists:function(){return this.end.time>0},disableStart:function(){return!this.startExists&&this.isOverlapping},disableEnd:function(){return this.isBeforeStart||this.isAfterNextSub||this.startIsOverlapping},isOverlapping:function(){var t=!1,e=!0,i=!1,r=void 0;try{for(var s,n=this.timelineSubs(this.active)[Symbol.iterator]();!(e=(s=n.next()).done);e=!0){var o=s.value;if(this.currentTime.prop>=o.startprop&&this.currentTime.prop<=o.endprop){t=!0;break}}}catch(a){i=!0,r=a}finally{try{e||null==n.return||n.return()}finally{if(i)throw r}}return t},startIsOverlapping:function(){var t=!1,e=!0,i=!1,r=void 0;try{for(var s,n=this.timelineSubs(this.active)[Symbol.iterator]();!(e=(s=n.next()).done);e=!0){var o=s.value;if(this.start.prop>=o.startprop&&this.start.prop<=o.endprop){t=!0;break}}}catch(a){i=!0,r=a}finally{try{e||null==n.return||n.return()}finally{if(i)throw r}}return t},isBeforeStart:function(){return this.currentTime.time<=this.start.time},isAfterNextSub:function(){var t=this.getNextSub(this.start.prop);return!!t&&this.currentTime.prop>=t.startprop},id:function(){return this.$store.state.content.current},pageValues:function(){return this.$store.getters["content/values"](this.id)}},watch:{pageValues:{immediate:!0,handler:function(){this.updateValues()}}},created:function(){var t=this;this.load().then(function(e){t.theme=e.theme,t.debug=e.debug,t.storage=e.storage,t.timelines=e.timelines,t.storage=e.storage,t.lang=e.lang,e.file&&(t.src.url=e.file.url,t.src.type=e.file.type)}),document.addEventListener("mouseup",this.stopResizing)},destroyed:function(){document.removeEventListener("mouseup",this.stopResizing)},methods:{init:function(){this.currentPlayer.addEventListener("timeupdate",this.timeUpdate),this.currentPlayer.addEventListener("loadeddata",this.loadedData),this.loaded=!0},loadedData:function(){this.currentPlayer.readyState>=2&&(this.setDuration(),this.syncCurrentTime(0))},timeUpdate:function(t){var e=this.currentPlayer.currentTime;!this.loaded&&e>0&&(this.loaded=!0),this.syncCurrentTime(e),e==this.duration.time&&this.isPlaying&&this.playPause()},playPause:function(){var t=this.currentPlayer;0==this.duration.time&&this.setDuration(),this.isPlaying?(t.pause(),this.isPlaying=!1):(t.play(),this.isPlaying=!0)},updateCoords:function(t){var e=this.$refs.timeline;if(!e)return!1;var i=e.getBoundingClientRect(),r=e.clientWidth,s=(t.clientX-i.left)/r;s=Math.max(0,Math.min(1,s.toFixed(4))),this.coords.prop=s,this.coords.time=s*this.duration.time,this.coords.text=this.formatTime(this.coords.time),this.resize.isResizing&&this.resizeSub(t)},initResize:function(t,e){if(!this.active)return!1;if(this.resize.index=this.subs.indexOf(t),this.resize.isResizing=!0,this.resize.handle=e,"left"==e){var i=this.getPrevSub(parseFloat(t.startprop));this.resize.min=i?parseFloat(i.endprop)+1e-4:0,this.resize.max=parseFloat(t.endprop)-1e-4}else if("right"==e){var r=this.getNextSub(parseFloat(t.startprop));this.resize.min=parseFloat(t.startprop)+1e-4,this.resize.max=r?parseFloat(r.startprop)-1e-4:1}},resizeSub:function(t){var e=this.subs[this.resize.index];if("left"==this.resize.handle){var i=Math.min(this.resize.max,Math.max(this.resize.min,this.coords.prop));e.startprop=i,e.start=i*this.duration.time}if("right"==this.resize.handle){var r=Math.min(this.resize.max,Math.max(this.resize.min,this.coords.prop));e.endprop=r,e.end=r*this.duration.time}},stopResizing:function(t){if(!this.resize.isResizing)return!1;this.updateStructure(),this.resize.isResizing=!1,this.resize.handle=String,this.resize.index=Number,this.resize.max=Number,this.resize.min=Number},onHandleMouseDown:function(t){document.addEventListener("mousemove",this.onDocumentMouseMove),document.addEventListener("mouseup",this.onDocumentMouseUp)},onDocumentMouseUp:function(t){document.removeEventListener("mouseup",this.onDocumentMouseUp),document.removeEventListener("mousemove",this.onDocumentMouseMove),this.syncProgress(t)},onDocumentMouseMove:function(t){this.syncProgress(t)},syncProgress:function(t){this.currentTime.prop=this.coords.prop.toFixed(4),this.currentTime.time=this.coords.prop*this.duration.time,this.currentTime.text=this.formatTime(this.coords.time),this.currentPlayer.currentTime=this.currentTime.time},setStart:function(){this.startExists?(this.resetStart(),this.resetEnd()):(this.start.prop=this.currentTime.prop,this.start.text=this.currentTime.text,this.start.time=this.currentTime.time)},setEnd:function(){this.end.prop=this.currentTime.prop,this.end.text=this.currentTime.text,this.end.time=this.currentTime.time;var t={start:this.start.time,startprop:this.start.prop,end:this.end.time,endprop:this.end.prop,timeline:this.active.id},e=this.getNewIndex();this.subs.splice(e,0,t),this.updateStructure(),this.resetStart(),this.resetEnd()},getNextSub:function(t){return this.timelineSubs(this.active).find(function(e){return e.startprop>t})},getPrevSub:function(t){return this.timelineSubs(this.active).splice(0).reverse().find(function(e){return t>e.endprop})},getNewIndex:function(){var t=this;if(!this.subs.length)return 0;if(this.timelineSubs(this.active).length){var e=this.getNextSub(this.start.prop);if(e)return this.subs.indexOf(e);var i=this.getPrevSub(this.start.prop);return this.subs.indexOf(i)+1}if(0==this.timelines.indexOf(this.active))return 0;var r=this.timelines.slice(this.timelines.indexOf(this.active)+1);if(r){var s=r.filter(function(e){return t.timelineSubs(e).length});if(s.length){var n=s[0],o=this.timelineSubs(n)[0];return this.subs.indexOf(o)}return this.subs.length}return this.subs.length},resetStart:function(){this.start.prop=0,this.start.text=0,this.start.time=0},resetEnd:function(){this.end.prop=0,this.end.text=0,this.end.time=0},setDuration:function(){var t=this.currentPlayer;this.duration.time=t.duration,this.duration.text=this.formatTime(t.duration)},syncCurrentTime:function(t){this.currentTime.time=t,this.currentTime.prop=(t/this.duration.time).toFixed(4),this.currentTime.text=this.formatTime(t)},setActive:function(t){this.active==t?(this.active=null,this.resetStart(),this.resetEnd()):this.active=t},timelineSubs:function(t){return this.subs.filter(function(e){return e.timeline==t.id})},formatTime:function(t){var e=Math.floor(t);return(e-(e%=60))/60+(9 2 | 3 | 4 | 5 | 6 | 7 | Please select a file 8 | 9 | 10 | 11 | 12 | 13 | {{coords.prop}} 14 | {{coords.time}} 15 | {{coords.text}} 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | { 46 | 47 | } 48 | 49 | 50 | 51 | {{duration.text}} 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | {{label(timeline)}} 61 | 62 | {{timelineSubs(timeline).length}} 63 | 64 | 65 | 66 | 67 | 68 | 69 | {{subs.indexOf(sub) + 1}} 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | {{currentTime.text}} 80 | 81 | 82 | 83 | 84 | {{start.text}} 85 | 86 | 87 | {{end.text}} 88 | 89 | 90 | 91 | 92 | 93 | 94 | 574 | 575 | 578 | --------------------------------------------------------------------------------