├── .gitignore ├── LICENSE ├── README.md ├── app ├── css │ ├── app-dialogs.css │ ├── app-dict.css │ ├── app-utility.css │ ├── app.css │ ├── awesome-bootstrap-checkbox.css │ ├── bootstrap.css │ ├── font-awesome.min.css │ ├── fontello.css │ ├── loading.css │ ├── normalize.css │ ├── select.css │ ├── select2-override.css │ ├── select2.css │ ├── select2.png │ ├── select2x2.png │ └── selectize.bootstrap3.css ├── fontello │ ├── LICENSE.txt │ ├── README.txt │ ├── config.json │ ├── css │ │ ├── animation.css │ │ ├── fontello-codes.css │ │ ├── fontello-embedded.css │ │ ├── fontello-ie7-codes.css │ │ ├── fontello-ie7.css │ │ └── fontello.css │ ├── demo.html │ └── font │ │ ├── fontello.eot │ │ ├── fontello.svg │ │ ├── fontello.ttf │ │ └── fontello.woff ├── fonts │ ├── FontAwesome.otf │ ├── fontawesome-webfont.eot │ ├── fontawesome-webfont.svg │ ├── fontawesome-webfont.ttf │ ├── fontawesome-webfont.woff │ ├── fontawesome-webfont.woff2 │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.svg │ ├── glyphicons-halflings-regular.ttf │ ├── glyphicons-halflings-regular.woff │ └── glyphicons-halflings-regular.woff2 ├── img │ ├── googletranslate.png │ ├── logo-sm.png │ └── logo.png ├── index.html ├── js │ ├── app.js │ └── node │ │ ├── database.js │ │ ├── downloader.js │ │ ├── encodings.js │ │ ├── parse.js │ │ ├── parsers │ │ ├── parserSUB.js │ │ ├── popcorn.parserSBV.js │ │ ├── popcorn.parserSRT.js │ │ ├── popcorn.parserSSA.js │ │ ├── popcorn.parserTTML.js │ │ ├── popcorn.parserTTXT.js │ │ └── popcorn.parserVTT.js │ │ ├── subsync.js │ │ ├── subtitle.js │ │ ├── tokenizer.js │ │ └── vl_util.js ├── libs │ ├── Snowball.js │ ├── angular-animate.js │ ├── angular.min.js │ ├── bootstrap.min.js │ ├── jquery.min.js │ ├── select.js │ ├── ui-bootstrap-tpls-0.13.4.min.js │ └── ui-bootstrap-tpls-0.14.2.min.js └── partials │ ├── aboutDialog.html │ ├── dictionary.html │ ├── fileDialog.html │ ├── helpDialog.html │ ├── popover-phrase.html │ ├── popover.html │ ├── shortcutsDialog.html │ ├── subsDialog.html │ └── wordsDialog.html ├── karma.conf.js ├── package.json └── tests ├── test_subtitle.srt └── unit ├── subtitle_test.js ├── test1.js └── tokenizer_test.js /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /node_modules 3 | 4 | # subtitles used for testing 5 | /test_material 6 | 7 | # vlc player 8 | /plugins 9 | /*.dll 10 | /*.dll.manifest -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Ognjen Apic 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 13 | all 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 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | Open source, cross-platform video player with language learning features. 4 | 5 | - Based on VLC player, offers dictionary lookup for subtitles, word pronunciation and word saving features 6 | - Works with 90 languages (8100 language combinations!), since it's using Google Translate 7 | 8 | To download Windows release go here: http://oaprograms.github.io/lingo-player 9 | 10 | **How it works:** 11 | 12 | Let's say you are learning Spanish: 13 | 14 | - Open a Spanish movie, with Spanish subtitles, and optionally 2nd subtitles in your language. 15 | - Hover over any word to see translations to your language, click on it hear it and to save it to dictionary. 16 | - You can mark words with 4 levels of familiarity (New - red, Recognized - orange, Familiar - yellow and Known - green) 17 | - You can look up list of saved words any time and read subtitles in subtitle listing mode. 18 | 19 | ## Used Technologies 20 | 21 | - NW.js (http://nwjs.io/) 22 | - AngularJS (https://angularjs.org/) 23 | - WebChimera.js (http://www.webchimera.org/) 24 | - VLC player (http://www.videolan.org/vlc/index.html) 25 | - NeDB (https://github.com/louischatriot/nedb) 26 | - Google Translate (https://translate.google.com/) 27 | 28 | ## Prerequisites 29 | 30 | - [WebChimera.js prerequisites](https://github.com/RSATom/WebChimera.js#build-prerequisites) 31 | 32 | ## Installation 33 | 34 | - ``npm install`` 35 | 36 | ## Contributing 37 | 38 | Anyone is very welcome to contribute to this project. In case you are interested, contact me at ognjen.apic at gmail.com, or start an issue. 39 | 40 | **Planned features in future (help is welcome):** 41 | 42 | - Make OSX version 43 | - Add more dictionary sources 44 | - Lemmatization / stemming? 45 | - Highlight frequent words? 46 | - Translate app UI into several languages 47 | 48 | ## Screenshots 49 | 50 | ![Main screen](http://oaprograms.github.io/lingo-player/images/screenshots/2.png) 51 | ![Words dialog](http://oaprograms.github.io/lingo-player/images/screenshots/1.png) 52 | ![File dialog](http://oaprograms.github.io/lingo-player/images/screenshots/3.png) 53 | ![Main screen](http://oaprograms.github.io/lingo-player/images/screenshots/5.png) 54 | 55 | ## Licence 56 | 57 | MIT -------------------------------------------------------------------------------- /app/css/app-dialogs.css: -------------------------------------------------------------------------------- 1 | 2 | .subs-dialog-left-div{ 3 | position: absolute; 4 | width: 38%; 5 | bottom: 0; 6 | top: 0; 7 | left: 0; 8 | overflow-y: auto; 9 | padding: 24px 16px 8px 12px; 10 | background-color: #eee; 11 | direction:rtl; 12 | } 13 | 14 | .words-table{ 15 | width: 80%; 16 | margin: 16px 16px 16px 0; 17 | } 18 | 19 | .words-table tr.word-row:hover, .subs-table tr:hover, .words-table tr.add-word{ 20 | background-color: #eeeeee; 21 | } 22 | 23 | .words-table .actions{ 24 | color: #b6b6b6; 25 | cursor: pointer; 26 | visibility: hidden; 27 | } 28 | 29 | .word-level .word-level-indicator{ visibility: visible;} 30 | .word-level .word-level-change{ visibility: hidden;} 31 | .word-level:hover .word-level-indicator{ visibility: hidden;} 32 | .word-level:hover .word-level-change{ visibility: visible;} 33 | 34 | .words-table tr:hover .no-actions{ 35 | visibility: hidden; 36 | } 37 | 38 | .words-table tr:hover .actions{ 39 | visibility: visible; 40 | } 41 | 42 | .words-table .actions:hover{ 43 | color: #2f2f2f; 44 | } 45 | .words-table td{ 46 | padding: 10px; 47 | } 48 | .words-table tr.word-row td{ 49 | border-bottom: 1px solid #eee; 50 | } 51 | 52 | .subs-dialog-right-div .checkbox-filters label{ 53 | font-weight: normal; 54 | font-size: 14px; 55 | color: #777777; 56 | } 57 | 58 | .file-table{ 59 | width: 100%; 60 | margin-top: 20px; 61 | margin-bottom: 23px; 62 | } 63 | 64 | .file-table td{ 65 | padding: 7px; 66 | } 67 | 68 | .file-table .file-text-input{ 69 | width: 100%; 70 | } 71 | 72 | .recent-files{ 73 | margin: 30px 15px 0 0; 74 | /*background-color: #ddd;*/ 75 | height: 250px; 76 | padding: 60px 15px 0 25px; 77 | border-right: 1px solid #cbcbcb; 78 | /*border-top-right-radius: 10px;*/ 79 | /*border-bottom-right-radius: 10px;*/ 80 | } 81 | 82 | .file-field-label{ 83 | margin: 2px 4px; 84 | } 85 | 86 | .about-table td:first-child{ 87 | text-align: right; 88 | font-weight: bold; 89 | } 90 | .about-table td{ 91 | padding: 6px; 92 | } 93 | 94 | .langsel{ 95 | width: 174px; 96 | display: inline-block; 97 | } 98 | .bordered{ 99 | border-style: solid !important; 100 | } 101 | 102 | .level-picker{ 103 | width: 16px; 104 | height: 16px; 105 | border: 2px none #7c7c7c; 106 | display: inline-block; 107 | position: relative; 108 | margin-left: 2px; 109 | top: 4px; 110 | } 111 | 112 | .subs-dialog-right-div{ 113 | position: absolute; 114 | top: 35px; 115 | width: 61%; 116 | bottom: 0; 117 | right: 0; 118 | overflow-y: scroll; 119 | font-size: 15px; 120 | padding: 10px; 121 | } 122 | 123 | table.subs-table td{ 124 | width: 50%; 125 | padding: 5px; 126 | } -------------------------------------------------------------------------------- /app/css/app-dict.css: -------------------------------------------------------------------------------- 1 | .def { 2 | clear: both; 3 | color: rgb(119,119,119); 4 | font-size: 15px; 5 | 6 | overflow-y: auto; 7 | overflow-x: hidden; 8 | padding: 10px 10px 10px 0; 9 | 10 | } 11 | .def table{ 12 | margin: 10px auto 0 auto; 13 | } 14 | .def tr{ 15 | border: none; 16 | vertical-align: top; 17 | } 18 | .def td{ 19 | border: none; 20 | height: 22px; 21 | padding: 0 5px; 22 | } 23 | .def td:last-child { 24 | width: auto !important; 25 | } 26 | .def .gt-baf-pos-head { 27 | font-style: italic; 28 | text-align: left; 29 | } 30 | .def .gt-baf-marker-container { 31 | max-width: 38px; 32 | text-align: right; 33 | } 34 | .def .gt-baf-word-clickable { 35 | color: #000; 36 | } 37 | .def .gt-baf-translations { 38 | padding: 0 12px 0 8px; 39 | } 40 | 41 | .def .gt-baf-cts { 42 | background: #aaa; 43 | display: inline-block; 44 | height: 7px; 45 | margin: 6px 2px 5px 0; 46 | } -------------------------------------------------------------------------------- /app/css/app-utility.css: -------------------------------------------------------------------------------- 1 | .visibility-hidden{ 2 | visibility: hidden; 3 | } 4 | 5 | .inline-block{ 6 | display: inline-block; 7 | } 8 | .pointer{ 9 | cursor: pointer; 10 | } 11 | 12 | .bigger{ 13 | font-size: 1.5em; 14 | } 15 | 16 | .hidden{ 17 | display:none; 18 | } 19 | 20 | .green{color:green;} .red{color:red;} 21 | 22 | .mt { margin-top: 4px; } .mmt { margin-top: 8px; } .mmmt { margin-top: 16px; } .mmmmt { margin-top: 32px; } 23 | .ml { margin-left: 4px; } .mml { margin-left: 8px; } .mmml { margin-left: 16px; } .mmmml { margin-left: 32px; } 24 | .mb { margin-bottom: 4px; } .mmb { margin-bottom: 8px; } .mmmb { margin-bottom: 16px; } .mmmmb { margin-bottom: 32px; } 25 | .mr { margin-right: 4px; } .mmr { margin-right: 8px; } .mmmr { margin-right: 16px; } .mmmmr { margin-right: 32px; } 26 | 27 | hr{ 28 | margin: 15px 0; 29 | } 30 | 31 | input{ 32 | border: 1px solid #ddd; 33 | padding: 4px 5px; 34 | } 35 | 36 | .dim-background{ 37 | position: absolute; 38 | width: 100%; 39 | height: 100%; 40 | background-color: rgba(90, 90, 90, 0.50); 41 | z-index: 100; 42 | } 43 | 44 | .ui-select-container{ 45 | font-size: 14px; 46 | cursor: pointer; 47 | } 48 | 49 | .loading{ 50 | font-size: 2em; 51 | text-align: center; 52 | color: #bbb; 53 | padding: 20%; 54 | font-weight: bold; 55 | } 56 | 57 | .no-select{ 58 | user-select: none; 59 | -o-user-select:none; 60 | -moz-user-select: none; 61 | -khtml-user-select: none; 62 | -webkit-user-select: none; 63 | } 64 | 65 | .subtle-button{ 66 | opacity: 0.125; 67 | } 68 | .subtle-button:hover{ 69 | opacity: 1; 70 | } 71 | 72 | .dim-button{ 73 | opacity: 0.5; 74 | } 75 | .dim-button:hover{ 76 | opacity: 1; 77 | } 78 | 79 | .no-events{ 80 | pointer-events: none !important; 81 | } 82 | 83 | .item-list{ 84 | white-space:nowrap; 85 | overflow:hidden; 86 | text-overflow: ellipsis; 87 | } 88 | 89 | [ng\:cloak], [ng-cloak], .ng-cloak { 90 | display: none !important; 91 | } -------------------------------------------------------------------------------- /app/css/app.css: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100%; 3 | overflow: hidden; 4 | font-family: Verdana, Helvetica, Arial, sans-serif; 5 | } 6 | 7 | body { 8 | height: 100%; 9 | margin: 0; 10 | padding: 0; 11 | } 12 | 13 | /*.wcp-center{*/ 14 | /*z-index: 0;*/ 15 | /*}*/ 16 | 17 | .vl-container{ 18 | /*padding: 0 370px 150px 0;*/ 19 | height: 100%; 20 | overflow: hidden; 21 | } 22 | #player { 23 | width: 100%; 24 | height: 100%; 25 | } 26 | #down { 27 | display: flex; 28 | flex-direction: column-reverse; 29 | position: absolute; 30 | /*height: 200px;*/ 31 | bottom: 38px; 32 | left: 0; 33 | right: 0; 34 | color: white; 35 | z-index: 5; 36 | pointer-events: none; 37 | } 38 | 39 | #down * { 40 | pointer-events: all; 41 | } 42 | 43 | .player-button{ 44 | position: absolute; 45 | left: 0; 46 | top: 15px; 47 | text-align: center; 48 | z-index: 5; 49 | font-size: 1.25em; 50 | cursor: pointer; 51 | color: black; 52 | background-color: rgba(145, 230, 134, 0.6); 53 | /*border-radius: 35px;*/ 54 | border-top-right-radius: 50px; 55 | border-bottom-right-radius: 50px; 56 | padding: 4px 18px; 57 | } 58 | .player-button i{ 59 | font-size: 16px; 60 | } 61 | .player-button:hover{ 62 | background-color: rgba(145, 230, 134, 1); 63 | } 64 | .center{ 65 | position: absolute; 66 | margin: auto; 67 | left: 0; right: 0; top: 0; bottom: 0; 68 | } 69 | 70 | 71 | #iframe, #iframe2{ 72 | visibility: hidden; 73 | /*z-index: 100;*/ 74 | position: absolute; 75 | width: 300px; 76 | height: 500px; 77 | } 78 | .popover, .popover * { 79 | pointer-events: none !important; 80 | max-width: 350px; 81 | } 82 | .popover{ 83 | color: black; 84 | } 85 | 86 | .btn-icon{ 87 | /*padding: 3px 7px;*/ 88 | font-size: 0.8em; 89 | margin-left: 6px; 90 | color: #00ff4a; 91 | cursor: pointer; 92 | } 93 | 94 | .bold {font-weight: bold; } 95 | .panel{ 96 | border: 1px solid #b3b3b3; 97 | } 98 | 99 | .dialog{ 100 | position: absolute; 101 | left: 0; 102 | right: 0; 103 | top: 0; 104 | bottom: 0; 105 | margin: auto; 106 | } 107 | 108 | .subtitle{ 109 | color: white; 110 | font-weight: bold; 111 | text-align: center; 112 | margin: 0 30px 0 30px; 113 | text-shadow: 2px 2px 10px rgba(0, 0, 0, 1), 114 | 3px 3px 35px rgba(0, 0, 0, 1), 115 | 3px 3px 18px rgba(0, 0, 0, 1), 116 | 1px 1px 0 rgba(0, 0, 0, 0.5), 117 | -1px -1px 0 rgba(0, 0, 0, 0.5), 118 | 1px -1px 0 rgba(0, 0, 0, 0.5), 119 | -1px 1px 0 rgba(0, 0, 0, 0.5); 120 | } 121 | 122 | @media (max-width: 599px) { 123 | .subtitle{font-size: 22px;} 124 | .subtitle2{font-size: 16px;} 125 | } 126 | @media (min-width: 600px) and (max-width: 899px){ 127 | .subtitle{font-size: 24px;} 128 | .subtitle2{font-size: 18px;} 129 | } 130 | @media (min-width: 900px) and (max-width: 1199px){ 131 | .subtitle{font-size: 26px;} 132 | .subtitle2{font-size: 19px;} 133 | } 134 | @media (min-width: 1200px) { 135 | .subtitle{font-size: 28px;} 136 | .subtitle2{font-size: 22px;} 137 | } 138 | 139 | .subtitle2{ 140 | color: #a3a3a3; 141 | padding-top: 15px; 142 | } 143 | 144 | .tooltip{ 145 | opacity: 1 !important; 146 | } 147 | .tooltip > .tooltip-inner { 148 | background-color: white; 149 | color: black; 150 | border: 1px solid #aaa; 151 | } 152 | .tooltip > .tooltip-arrow { 153 | border-right-color: #aaa !important; 154 | } 155 | 156 | .language-select{ 157 | min-width: 110px; 158 | float: right; 159 | margin-top: 6px; 160 | margin-right: 10px; 161 | } 162 | 163 | .level1{border-bottom: 3px solid #ff1000;} 164 | .level2{border-bottom: 3px solid #ff8a00;} 165 | .level3{border-bottom: 3px solid #ffff00;} 166 | .level4{border-bottom: 3px solid #00a006;} 167 | 168 | i{ 169 | font-size: 14px; 170 | } 171 | 172 | .play-btn{ 173 | background-color: #48ff40; 174 | color: black; 175 | padding: 6px 20px; 176 | } 177 | 178 | .play-btn:hover, .play-btn:focus{ 179 | background-color: #35b12f; 180 | } 181 | 182 | .level-selector{ 183 | color: #5d5d5d; 184 | height: 30px; 185 | float: left; 186 | border-bottom: 3px solid #858585; 187 | box-sizing: border-box; 188 | margin-right: 4px; 189 | } 190 | .level-selector.level-unchecked{ 191 | opacity: .4; 192 | border-bottom: 3px solid white; 193 | } 194 | .level-selector:hover{ 195 | opacity: .7; 196 | } 197 | .g-dictionary{ 198 | padding: 15px; 199 | margin-top: 10px; 200 | width: 100%; 201 | clear: both; 202 | } 203 | .sub-buttons{ 204 | text-align: center; 205 | margin: 0 30px 0 30px; 206 | padding: 5px 0 12px 0; 207 | color: rgba(255,255,255,0.3); 208 | opacity: 0; 209 | } 210 | .down-content:hover > .sub-buttons{ 211 | opacity: 1; 212 | } 213 | .down-content:hover{ 214 | background-color: rgba(0,0,0,0.4); 215 | } 216 | .down-content{ 217 | padding: 10px; 218 | } 219 | .sub-button{ 220 | font-size: 1.35em; 221 | display: inline-block; 222 | cursor: pointer; 223 | color: black; 224 | background-color: rgba(255,255,255,0.3); 225 | /*border-radius: 35px;*/ 226 | padding: 4px 18px; 227 | margin: 5px -1px; 228 | } 229 | .sub-buttons .sub-button:first-child{ 230 | padding: 4px 35px; 231 | border-top-left-radius: 50px; 232 | border-bottom-left-radius: 50px; 233 | } 234 | .sub-buttons .sub-button:last-child{ 235 | padding: 4px 35px; 236 | border-top-right-radius: 50px; 237 | border-bottom-right-radius: 50px; 238 | } 239 | 240 | .sub-buttons .sub-button.b-green{background-color: rgba(100,255,100,0.3);} 241 | .sub-buttons .sub-button.b-red{background-color: rgba(255, 200, 93, 0.3);} 242 | /*.sub-buttons .sub-button.b-blue{background-color: rgba(66, 187, 183, 0.3);}*/ 243 | .sub-buttons .sub-button.b-green:hover{background-color: rgba(100,255,100,0.7);} 244 | .sub-buttons .sub-button.b-red:hover{background-color: rgba(255, 200, 93, 0.7);} 245 | /*.sub-buttons .sub-button.b-blue:hover{background-color: rgba(66, 187, 183, 0.6);}*/ 246 | 247 | .sub-buttons .sub-button:hover{ 248 | background-color: rgba(255,255,255,0.7); 249 | color: rgb(0, 0, 0); 250 | } 251 | -------------------------------------------------------------------------------- /app/css/awesome-bootstrap-checkbox.css: -------------------------------------------------------------------------------- 1 | .checkbox { 2 | padding-left: 20px; 3 | } 4 | .checkbox label { 5 | display: inline-block; 6 | vertical-align: middle; 7 | position: relative; 8 | padding-left: 5px; 9 | } 10 | .checkbox label::before { 11 | content: ""; 12 | display: inline-block; 13 | position: absolute; 14 | width: 17px; 15 | height: 17px; 16 | left: 0; 17 | top: 2px; 18 | margin-left: -20px; 19 | border: 1px solid #cccccc; 20 | border-radius: 3px; 21 | background-color: #fff; 22 | -webkit-transition: border 0.15s ease-in-out, color 0.15s ease-in-out; 23 | -o-transition: border 0.15s ease-in-out, color 0.15s ease-in-out; 24 | transition: border 0.15s ease-in-out, color 0.15s ease-in-out; 25 | } 26 | .checkbox label::after { 27 | display: inline-block; 28 | position: absolute; 29 | width: 16px; 30 | height: 16px; 31 | left: 0; 32 | top: 2px; 33 | margin-left: -20px; 34 | padding-left: 3px; 35 | padding-top: 1px; 36 | font-size: 11px; 37 | color: #555555; 38 | } 39 | .checkbox input[type="checkbox"], 40 | .checkbox input[type="radio"] { 41 | opacity: 0; 42 | z-index: 1; 43 | } 44 | .checkbox input[type="checkbox"]:focus + label::before, 45 | .checkbox input[type="radio"]:focus + label::before { 46 | outline: thin dotted; 47 | outline: 5px auto -webkit-focus-ring-color; 48 | outline-offset: -2px; 49 | } 50 | .checkbox input[type="checkbox"]:checked + label::after, 51 | .checkbox input[type="radio"]:checked + label::after { 52 | font-family: "FontAwesome"; 53 | content: "\f00c"; 54 | } 55 | .checkbox input[type="checkbox"]:disabled + label, 56 | .checkbox input[type="radio"]:disabled + label { 57 | opacity: 0.65; 58 | } 59 | .checkbox input[type="checkbox"]:disabled + label::before, 60 | .checkbox input[type="radio"]:disabled + label::before { 61 | background-color: #eeeeee; 62 | cursor: not-allowed; 63 | } 64 | .checkbox.checkbox-circle label::before { 65 | border-radius: 50%; 66 | } 67 | .checkbox.checkbox-inline { 68 | margin-top: 0; 69 | } 70 | 71 | .checkbox-primary input[type="checkbox"]:checked + label::before, 72 | .checkbox-primary input[type="radio"]:checked + label::before { 73 | background-color: #337ab7; 74 | border-color: #337ab7; 75 | } 76 | .checkbox-primary input[type="checkbox"]:checked + label::after, 77 | .checkbox-primary input[type="radio"]:checked + label::after { 78 | color: #fff; 79 | } 80 | 81 | .checkbox-danger input[type="checkbox"]:checked + label::before, 82 | .checkbox-danger input[type="radio"]:checked + label::before { 83 | background-color: #d9534f; 84 | border-color: #d9534f; 85 | } 86 | .checkbox-danger input[type="checkbox"]:checked + label::after, 87 | .checkbox-danger input[type="radio"]:checked + label::after { 88 | color: #fff; 89 | } 90 | 91 | .checkbox-info input[type="checkbox"]:checked + label::before, 92 | .checkbox-info input[type="radio"]:checked + label::before { 93 | background-color: #5bc0de; 94 | border-color: #5bc0de; 95 | } 96 | .checkbox-info input[type="checkbox"]:checked + label::after, 97 | .checkbox-info input[type="radio"]:checked + label::after { 98 | color: #fff; 99 | } 100 | 101 | .checkbox-warning input[type="checkbox"]:checked + label::before, 102 | .checkbox-warning input[type="radio"]:checked + label::before { 103 | background-color: #f0ad4e; 104 | border-color: #f0ad4e; 105 | } 106 | .checkbox-warning input[type="checkbox"]:checked + label::after, 107 | .checkbox-warning input[type="radio"]:checked + label::after { 108 | color: #fff; 109 | } 110 | 111 | .checkbox-success input[type="checkbox"]:checked + label::before, 112 | .checkbox-success input[type="radio"]:checked + label::before { 113 | background-color: #5cb85c; 114 | border-color: #5cb85c; 115 | } 116 | .checkbox-success input[type="checkbox"]:checked + label::after, 117 | .checkbox-success input[type="radio"]:checked + label::after { 118 | color: #fff; 119 | } 120 | 121 | .radio { 122 | padding-left: 20px; 123 | } 124 | .radio label { 125 | display: inline-block; 126 | vertical-align: middle; 127 | position: relative; 128 | padding-left: 5px; 129 | } 130 | .radio label::before { 131 | content: ""; 132 | display: inline-block; 133 | position: absolute; 134 | width: 17px; 135 | height: 17px; 136 | left: 0; 137 | margin-left: -20px; 138 | border: 1px solid #cccccc; 139 | border-radius: 50%; 140 | background-color: #fff; 141 | -webkit-transition: border 0.15s ease-in-out; 142 | -o-transition: border 0.15s ease-in-out; 143 | transition: border 0.15s ease-in-out; 144 | } 145 | .radio label::after { 146 | display: inline-block; 147 | position: absolute; 148 | content: " "; 149 | width: 11px; 150 | height: 11px; 151 | left: 3px; 152 | top: 3px; 153 | margin-left: -20px; 154 | border-radius: 50%; 155 | background-color: #555555; 156 | -webkit-transform: scale(0, 0); 157 | -ms-transform: scale(0, 0); 158 | -o-transform: scale(0, 0); 159 | transform: scale(0, 0); 160 | -webkit-transition: -webkit-transform 0.1s cubic-bezier(0.8, -0.33, 0.2, 1.33); 161 | -moz-transition: -moz-transform 0.1s cubic-bezier(0.8, -0.33, 0.2, 1.33); 162 | -o-transition: -o-transform 0.1s cubic-bezier(0.8, -0.33, 0.2, 1.33); 163 | transition: transform 0.1s cubic-bezier(0.8, -0.33, 0.2, 1.33); 164 | } 165 | .radio input[type="radio"] { 166 | opacity: 0; 167 | z-index: 1; 168 | } 169 | .radio input[type="radio"]:focus + label::before { 170 | outline: thin dotted; 171 | outline: 5px auto -webkit-focus-ring-color; 172 | outline-offset: -2px; 173 | } 174 | .radio input[type="radio"]:checked + label::after { 175 | -webkit-transform: scale(1, 1); 176 | -ms-transform: scale(1, 1); 177 | -o-transform: scale(1, 1); 178 | transform: scale(1, 1); 179 | } 180 | .radio input[type="radio"]:disabled + label { 181 | opacity: 0.65; 182 | } 183 | .radio input[type="radio"]:disabled + label::before { 184 | cursor: not-allowed; 185 | } 186 | .radio.radio-inline { 187 | margin-top: 0; 188 | } 189 | 190 | .radio-primary input[type="radio"] + label::after { 191 | background-color: #337ab7; 192 | } 193 | .radio-primary input[type="radio"]:checked + label::before { 194 | border-color: #337ab7; 195 | } 196 | .radio-primary input[type="radio"]:checked + label::after { 197 | background-color: #337ab7; 198 | } 199 | 200 | .radio-danger input[type="radio"] + label::after { 201 | background-color: #d9534f; 202 | } 203 | .radio-danger input[type="radio"]:checked + label::before { 204 | border-color: #d9534f; 205 | } 206 | .radio-danger input[type="radio"]:checked + label::after { 207 | background-color: #d9534f; 208 | } 209 | 210 | .radio-info input[type="radio"] + label::after { 211 | background-color: #5bc0de; 212 | } 213 | .radio-info input[type="radio"]:checked + label::before { 214 | border-color: #5bc0de; 215 | } 216 | .radio-info input[type="radio"]:checked + label::after { 217 | background-color: #5bc0de; 218 | } 219 | 220 | .radio-warning input[type="radio"] + label::after { 221 | background-color: #f0ad4e; 222 | } 223 | .radio-warning input[type="radio"]:checked + label::before { 224 | border-color: #f0ad4e; 225 | } 226 | .radio-warning input[type="radio"]:checked + label::after { 227 | background-color: #f0ad4e; 228 | } 229 | 230 | .radio-success input[type="radio"] + label::after { 231 | background-color: #5cb85c; 232 | } 233 | .radio-success input[type="radio"]:checked + label::before { 234 | border-color: #5cb85c; 235 | } 236 | .radio-success input[type="radio"]:checked + label::after { 237 | background-color: #5cb85c; 238 | } 239 | 240 | input[type="checkbox"].styled:checked + label:after, 241 | input[type="radio"].styled:checked + label:after { 242 | font-family: 'FontAwesome'; 243 | content: "\f00c"; 244 | } 245 | input[type="checkbox"] .styled:checked + label::before, 246 | input[type="radio"] .styled:checked + label::before { 247 | color: #fff; 248 | } 249 | input[type="checkbox"] .styled:checked + label::after, 250 | input[type="radio"] .styled:checked + label::after { 251 | color: #fff; 252 | } 253 | -------------------------------------------------------------------------------- /app/css/fontello.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'fontello'; 3 | src: url('../font/fontello.eot?78498164'); 4 | src: url('../font/fontello.eot?78498164#iefix') format('embedded-opentype'), 5 | url('../font/fontello.woff?78498164') format('woff'), 6 | url('../font/fontello.ttf?78498164') format('truetype'), 7 | url('../font/fontello.svg?78498164#fontello') format('svg'); 8 | font-weight: normal; 9 | font-style: normal; 10 | } 11 | /* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */ 12 | /* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */ 13 | /* 14 | @media screen and (-webkit-min-device-pixel-ratio:0) { 15 | @font-face { 16 | font-family: 'fontello'; 17 | src: url('../font/fontello.svg?78498164#fontello') format('svg'); 18 | } 19 | } 20 | */ 21 | 22 | [class^="icon-"]:before, [class*=" icon-"]:before { 23 | font-family: "fontello"; 24 | font-style: normal; 25 | font-weight: normal; 26 | speak: none; 27 | 28 | display: inline-block; 29 | text-decoration: inherit; 30 | width: 1em; 31 | margin-right: .2em; 32 | text-align: center; 33 | /* opacity: .8; */ 34 | 35 | /* For safety - reset parent styles, that can break glyph codes*/ 36 | font-variant: normal; 37 | text-transform: none; 38 | 39 | /* fix buttons height, for twitter bootstrap */ 40 | line-height: 1em; 41 | 42 | /* Animation center compensation - margins should be symmetric */ 43 | /* remove if not needed */ 44 | margin-left: .2em; 45 | 46 | /* you can be more comfortable with increased icons size */ 47 | /* font-size: 120%; */ 48 | 49 | /* Font smoothing. That was taken from TWBS */ 50 | -webkit-font-smoothing: antialiased; 51 | -moz-osx-font-smoothing: grayscale; 52 | 53 | /* Uncomment for 3D effect */ 54 | /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */ 55 | } 56 | 57 | .icon-folder-open:before { content: '\e800'; } /* '' */ 58 | .icon-play:before { content: '\e801'; } /* '' */ 59 | .icon-stop:before { content: '\e802'; } /* '' */ 60 | .icon-pause:before { content: '\e803'; } /* '' */ 61 | .icon-fast-fw:before { content: '\e804'; } /* '' */ 62 | .icon-fast-bw:before { content: '\e805'; } /* '' */ 63 | .icon-ok:before { content: '\e806'; } /* '' */ 64 | .icon-down-dir:before { content: '\e807'; } /* '' */ 65 | .icon-up-dir:before { content: '\e808'; } /* '' */ 66 | .icon-left-dir:before { content: '\e809'; } /* '' */ 67 | .icon-right-dir:before { content: '\e80a'; } /* '' */ 68 | .icon-down-open:before { content: '\e80b'; } /* '' */ 69 | .icon-left-open:before { content: '\e80c'; } /* '' */ 70 | .icon-right-open:before { content: '\e80d'; } /* '' */ 71 | .icon-up-open:before { content: '\e80e'; } /* '' */ 72 | .icon-fontsize:before { content: '\e80f'; } /* '' */ 73 | .icon-font:before { content: '\e810'; } /* '' */ 74 | .icon-cw:before { content: '\e811'; } /* '' */ 75 | .icon-ccw:before { content: '\e812'; } /* '' */ -------------------------------------------------------------------------------- /app/css/loading.css: -------------------------------------------------------------------------------- 1 | .sk-cube-grid { 2 | width: 40px; 3 | height: 40px; 4 | margin: 100px auto; 5 | } 6 | 7 | .sk-cube-grid .sk-cube { 8 | width: 33%; 9 | height: 33%; 10 | background-color: #333; 11 | float: left; 12 | -webkit-animation: sk-cubeGridScaleDelay 1.3s infinite ease-in-out; 13 | animation: sk-cubeGridScaleDelay 1.3s infinite ease-in-out; 14 | } 15 | .sk-cube-grid .sk-cube1 { 16 | -webkit-animation-delay: 0.2s; 17 | animation-delay: 0.2s; } 18 | .sk-cube-grid .sk-cube2 { 19 | -webkit-animation-delay: 0.3s; 20 | animation-delay: 0.3s; } 21 | .sk-cube-grid .sk-cube3 { 22 | -webkit-animation-delay: 0.4s; 23 | animation-delay: 0.4s; } 24 | .sk-cube-grid .sk-cube4 { 25 | -webkit-animation-delay: 0.1s; 26 | animation-delay: 0.1s; } 27 | .sk-cube-grid .sk-cube5 { 28 | -webkit-animation-delay: 0.2s; 29 | animation-delay: 0.2s; } 30 | .sk-cube-grid .sk-cube6 { 31 | -webkit-animation-delay: 0.3s; 32 | animation-delay: 0.3s; } 33 | .sk-cube-grid .sk-cube7 { 34 | -webkit-animation-delay: 0s; 35 | animation-delay: 0s; } 36 | .sk-cube-grid .sk-cube8 { 37 | -webkit-animation-delay: 0.1s; 38 | animation-delay: 0.1s; } 39 | .sk-cube-grid .sk-cube9 { 40 | -webkit-animation-delay: 0.2s; 41 | animation-delay: 0.2s; } 42 | 43 | @-webkit-keyframes sk-cubeGridScaleDelay { 44 | 0%, 70%, 100% { 45 | -webkit-transform: scale3D(1, 1, 1); 46 | transform: scale3D(1, 1, 1); 47 | } 35% { 48 | -webkit-transform: scale3D(0, 0, 1); 49 | transform: scale3D(0, 0, 1); 50 | } 51 | } 52 | 53 | @keyframes sk-cubeGridScaleDelay { 54 | 0%, 70%, 100% { 55 | -webkit-transform: scale3D(1, 1, 1); 56 | transform: scale3D(1, 1, 1); 57 | } 35% { 58 | -webkit-transform: scale3D(0, 0, 1); 59 | transform: scale3D(0, 0, 1); 60 | } 61 | } -------------------------------------------------------------------------------- /app/css/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v3.0.2 | MIT License | git.io/normalize */ 2 | 3 | /** 4 | * 1. Set default font family to sans-serif. 5 | * 2. Prevent iOS text size adjust after orientation change, without disabling 6 | * user zoom. 7 | */ 8 | 9 | html { 10 | font-family: sans-serif; /* 1 */ 11 | -ms-text-size-adjust: 100%; /* 2 */ 12 | -webkit-text-size-adjust: 100%; /* 2 */ 13 | } 14 | 15 | /** 16 | * Remove default margin. 17 | */ 18 | 19 | body { 20 | margin: 0; 21 | } 22 | 23 | /* HTML5 display definitions 24 | ========================================================================== */ 25 | 26 | /** 27 | * Correct `block` display not defined for any HTML5 element in IE 8/9. 28 | * Correct `block` display not defined for `details` or `summary` in IE 10/11 29 | * and Firefox. 30 | * Correct `block` display not defined for `main` in IE 11. 31 | */ 32 | 33 | article, 34 | aside, 35 | details, 36 | figcaption, 37 | figure, 38 | footer, 39 | header, 40 | hgroup, 41 | main, 42 | menu, 43 | nav, 44 | section, 45 | summary { 46 | display: block; 47 | } 48 | 49 | /** 50 | * 1. Correct `inline-block` display not defined in IE 8/9. 51 | * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. 52 | */ 53 | 54 | audio, 55 | canvas, 56 | progress, 57 | video { 58 | display: inline-block; /* 1 */ 59 | vertical-align: baseline; /* 2 */ 60 | } 61 | 62 | /** 63 | * Prevent modern browsers from displaying `audio` without controls. 64 | * Remove excess height in iOS 5 devices. 65 | */ 66 | 67 | audio:not([controls]) { 68 | display: none; 69 | height: 0; 70 | } 71 | 72 | /** 73 | * Address `[hidden]` styling not present in IE 8/9/10. 74 | * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22. 75 | */ 76 | 77 | [hidden], 78 | template { 79 | display: none; 80 | } 81 | 82 | /* Links 83 | ========================================================================== */ 84 | 85 | /** 86 | * Remove the gray background color from active links in IE 10. 87 | */ 88 | 89 | a { 90 | background-color: transparent; 91 | } 92 | 93 | /** 94 | * Improve readability when focused and also mouse hovered in all browsers. 95 | */ 96 | 97 | a:active, 98 | a:hover { 99 | outline: 0; 100 | } 101 | 102 | /* Text-level semantics 103 | ========================================================================== */ 104 | 105 | /** 106 | * Address styling not present in IE 8/9/10/11, Safari, and Chrome. 107 | */ 108 | 109 | abbr[title] { 110 | border-bottom: 1px dotted; 111 | } 112 | 113 | /** 114 | * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. 115 | */ 116 | 117 | b, 118 | strong { 119 | font-weight: bold; 120 | } 121 | 122 | /** 123 | * Address styling not present in Safari and Chrome. 124 | */ 125 | 126 | dfn { 127 | font-style: italic; 128 | } 129 | 130 | /** 131 | * Address variable `h1` font-size and margin within `section` and `article` 132 | * contexts in Firefox 4+, Safari, and Chrome. 133 | */ 134 | 135 | h1 { 136 | font-size: 2em; 137 | margin: 0.67em 0; 138 | } 139 | 140 | /** 141 | * Address styling not present in IE 8/9. 142 | */ 143 | 144 | mark { 145 | background: #ff0; 146 | color: #000; 147 | } 148 | 149 | /** 150 | * Address inconsistent and variable font size in all browsers. 151 | */ 152 | 153 | small { 154 | font-size: 80%; 155 | } 156 | 157 | /** 158 | * Prevent `sub` and `sup` affecting `line-height` in all browsers. 159 | */ 160 | 161 | sub, 162 | sup { 163 | font-size: 75%; 164 | line-height: 0; 165 | position: relative; 166 | vertical-align: baseline; 167 | } 168 | 169 | sup { 170 | top: -0.5em; 171 | } 172 | 173 | sub { 174 | bottom: -0.25em; 175 | } 176 | 177 | /* Embedded content 178 | ========================================================================== */ 179 | 180 | /** 181 | * Remove border when inside `a` element in IE 8/9/10. 182 | */ 183 | 184 | img { 185 | border: 0; 186 | } 187 | 188 | /** 189 | * Correct overflow not hidden in IE 9/10/11. 190 | */ 191 | 192 | svg:not(:root) { 193 | overflow: hidden; 194 | } 195 | 196 | /* Grouping content 197 | ========================================================================== */ 198 | 199 | /** 200 | * Address margin not present in IE 8/9 and Safari. 201 | */ 202 | 203 | figure { 204 | margin: 1em 40px; 205 | } 206 | 207 | /** 208 | * Address differences between Firefox and other browsers. 209 | */ 210 | 211 | hr { 212 | -moz-box-sizing: content-box; 213 | box-sizing: content-box; 214 | height: 0; 215 | } 216 | 217 | /** 218 | * Contain overflow in all browsers. 219 | */ 220 | 221 | pre { 222 | overflow: auto; 223 | } 224 | 225 | /** 226 | * Address odd `em`-unit font size rendering in all browsers. 227 | */ 228 | 229 | code, 230 | kbd, 231 | pre, 232 | samp { 233 | font-family: monospace, monospace; 234 | font-size: 1em; 235 | } 236 | 237 | /* Forms 238 | ========================================================================== */ 239 | 240 | /** 241 | * Known limitation: by default, Chrome and Safari on OS X allow very limited 242 | * styling of `select`, unless a `border` property is set. 243 | */ 244 | 245 | /** 246 | * 1. Correct color not being inherited. 247 | * Known issue: affects color of disabled elements. 248 | * 2. Correct font properties not being inherited. 249 | * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. 250 | */ 251 | 252 | button, 253 | input, 254 | optgroup, 255 | select, 256 | textarea { 257 | color: inherit; /* 1 */ 258 | font: inherit; /* 2 */ 259 | margin: 0; /* 3 */ 260 | } 261 | 262 | /** 263 | * Address `overflow` set to `hidden` in IE 8/9/10/11. 264 | */ 265 | 266 | button { 267 | overflow: visible; 268 | } 269 | 270 | /** 271 | * Address inconsistent `text-transform` inheritance for `button` and `select`. 272 | * All other form control elements do not inherit `text-transform` values. 273 | * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. 274 | * Correct `select` style inheritance in Firefox. 275 | */ 276 | 277 | button, 278 | select { 279 | text-transform: none; 280 | } 281 | 282 | /** 283 | * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` 284 | * and `video` controls. 285 | * 2. Correct inability to style clickable `input` types in iOS. 286 | * 3. Improve usability and consistency of cursor style between image-type 287 | * `input` and others. 288 | */ 289 | 290 | button, 291 | html input[type="button"], /* 1 */ 292 | input[type="reset"], 293 | input[type="submit"] { 294 | -webkit-appearance: button; /* 2 */ 295 | cursor: pointer; /* 3 */ 296 | } 297 | 298 | /** 299 | * Re-set default cursor for disabled elements. 300 | */ 301 | 302 | button[disabled], 303 | html input[disabled] { 304 | cursor: default; 305 | } 306 | 307 | /** 308 | * Remove inner padding and border in Firefox 4+. 309 | */ 310 | 311 | button::-moz-focus-inner, 312 | input::-moz-focus-inner { 313 | border: 0; 314 | padding: 0; 315 | } 316 | 317 | /** 318 | * Address Firefox 4+ setting `line-height` on `input` using `!important` in 319 | * the UA stylesheet. 320 | */ 321 | 322 | input { 323 | line-height: normal; 324 | } 325 | 326 | /** 327 | * It's recommended that you don't attempt to style these elements. 328 | * Firefox's implementation doesn't respect box-sizing, padding, or width. 329 | * 330 | * 1. Address box sizing set to `content-box` in IE 8/9/10. 331 | * 2. Remove excess padding in IE 8/9/10. 332 | */ 333 | 334 | input[type="checkbox"], 335 | input[type="radio"] { 336 | box-sizing: border-box; /* 1 */ 337 | padding: 0; /* 2 */ 338 | } 339 | 340 | /** 341 | * Fix the cursor style for Chrome's increment/decrement buttons. For certain 342 | * `font-size` values of the `input`, it causes the cursor style of the 343 | * decrement button to change from `default` to `text`. 344 | */ 345 | 346 | input[type="number"]::-webkit-inner-spin-button, 347 | input[type="number"]::-webkit-outer-spin-button { 348 | height: auto; 349 | } 350 | 351 | /** 352 | * 1. Address `appearance` set to `searchfield` in Safari and Chrome. 353 | * 2. Address `box-sizing` set to `border-box` in Safari and Chrome 354 | * (include `-moz` to future-proof). 355 | */ 356 | 357 | input[type="search"] { 358 | -webkit-appearance: textfield; /* 1 */ 359 | -moz-box-sizing: content-box; 360 | -webkit-box-sizing: content-box; /* 2 */ 361 | box-sizing: content-box; 362 | } 363 | 364 | /** 365 | * Remove inner padding and search cancel button in Safari and Chrome on OS X. 366 | * Safari (but not Chrome) clips the cancel button when the search input has 367 | * padding (and `textfield` appearance). 368 | */ 369 | 370 | input[type="search"]::-webkit-search-cancel-button, 371 | input[type="search"]::-webkit-search-decoration { 372 | -webkit-appearance: none; 373 | } 374 | 375 | /** 376 | * Define consistent border, margin, and padding. 377 | */ 378 | 379 | fieldset { 380 | border: 1px solid #c0c0c0; 381 | margin: 0 2px; 382 | padding: 0.35em 0.625em 0.75em; 383 | } 384 | 385 | /** 386 | * 1. Correct `color` not being inherited in IE 8/9/10/11. 387 | * 2. Remove padding so people aren't caught out if they zero out fieldsets. 388 | */ 389 | 390 | legend { 391 | border: 0; /* 1 */ 392 | padding: 0; /* 2 */ 393 | } 394 | 395 | /** 396 | * Remove default vertical scrollbar in IE 8/9/10/11. 397 | */ 398 | 399 | textarea { 400 | overflow: auto; 401 | } 402 | 403 | /** 404 | * Don't inherit the `font-weight` (applied by a rule above). 405 | * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. 406 | */ 407 | 408 | optgroup { 409 | font-weight: bold; 410 | } 411 | 412 | /* Tables 413 | ========================================================================== */ 414 | 415 | /** 416 | * Remove most spacing between table cells. 417 | */ 418 | 419 | table { 420 | border-collapse: collapse; 421 | border-spacing: 0; 422 | } 423 | 424 | td, 425 | th { 426 | padding: 0; 427 | } 428 | -------------------------------------------------------------------------------- /app/css/select.css: -------------------------------------------------------------------------------- 1 | /*! 2 | * ui-select 3 | * http://github.com/angular-ui/ui-select 4 | * Version: 0.12.0 - 2015-05-28T07:44:11.364Z 5 | * License: MIT 6 | */ 7 | 8 | 9 | /* Style when highlighting a search. */ 10 | .ui-select-highlight { 11 | font-weight: bold; 12 | } 13 | 14 | .ui-select-offscreen { 15 | clip: rect(0 0 0 0) !important; 16 | width: 1px !important; 17 | height: 1px !important; 18 | border: 0 !important; 19 | margin: 0 !important; 20 | padding: 0 !important; 21 | overflow: hidden !important; 22 | position: absolute !important; 23 | outline: 0 !important; 24 | left: 0px !important; 25 | top: 0px !important; 26 | } 27 | 28 | /* Select2 theme */ 29 | 30 | /* Mark invalid Select2 */ 31 | .ng-dirty.ng-invalid > a.select2-choice { 32 | border-color: #D44950; 33 | } 34 | 35 | .select2-result-single { 36 | padding-left: 0; 37 | } 38 | 39 | .select2-locked > .select2-search-choice-close{ 40 | display:none; 41 | } 42 | 43 | .select-locked > .ui-select-match-close{ 44 | display:none; 45 | } 46 | 47 | body > .select2-container.open { 48 | z-index: 9999; /* The z-index Select2 applies to the select2-drop */ 49 | } 50 | 51 | /* Handle up direction Select2 */ 52 | .ui-select-container[theme="select2"].direction-up .ui-select-match { 53 | border-radius: 4px; /* FIXME hardcoded value :-/ */ 54 | border-top-left-radius: 0; 55 | border-top-right-radius: 0; 56 | } 57 | .ui-select-container[theme="select2"].direction-up .ui-select-dropdown { 58 | border-radius: 4px; /* FIXME hardcoded value :-/ */ 59 | border-bottom-left-radius: 0; 60 | border-bottom-right-radius: 0; 61 | 62 | border-top-width: 1px; /* FIXME hardcoded value :-/ */ 63 | border-top-style: solid; 64 | 65 | box-shadow: 0 -4px 8px rgba(0, 0, 0, 0.25); 66 | 67 | margin-top: -4px; /* FIXME hardcoded value :-/ */ 68 | } 69 | .ui-select-container[theme="select2"].direction-up .ui-select-dropdown .select2-search { 70 | margin-top: 4px; /* FIXME hardcoded value :-/ */ 71 | } 72 | .ui-select-container[theme="select2"].direction-up.select2-dropdown-open .ui-select-match { 73 | border-bottom-color: #5897fb; 74 | } 75 | 76 | /* Selectize theme */ 77 | 78 | /* Helper class to show styles when focus */ 79 | .selectize-input.selectize-focus{ 80 | border-color: #007FBB !important; 81 | } 82 | 83 | /* Fix input width for Selectize theme */ 84 | .selectize-control > .selectize-input > input { 85 | width: 100%; 86 | } 87 | 88 | /* Fix dropdown width for Selectize theme */ 89 | .selectize-control > .selectize-dropdown { 90 | width: 100%; 91 | } 92 | 93 | /* Mark invalid Selectize */ 94 | .ng-dirty.ng-invalid > div.selectize-input { 95 | border-color: #D44950; 96 | } 97 | 98 | /* Handle up direction Selectize */ 99 | .ui-select-container[theme="selectize"].direction-up .ui-select-dropdown { 100 | box-shadow: 0 -4px 8px rgba(0, 0, 0, 0.25); 101 | 102 | margin-top: -2px; /* FIXME hardcoded value :-/ */ 103 | } 104 | 105 | /* Bootstrap theme */ 106 | 107 | /* Helper class to show styles when focus */ 108 | .btn-default-focus { 109 | color: #333; 110 | background-color: #EBEBEB; 111 | border-color: #ADADAD; 112 | text-decoration: none; 113 | outline: 5px auto -webkit-focus-ring-color; 114 | outline-offset: -2px; 115 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6); 116 | } 117 | 118 | .ui-select-bootstrap .ui-select-toggle { 119 | position: relative; 120 | } 121 | 122 | .ui-select-bootstrap .ui-select-toggle > .caret { 123 | position: absolute; 124 | height: 10px; 125 | top: 50%; 126 | right: 10px; 127 | margin-top: -2px; 128 | } 129 | 130 | /* Fix Bootstrap dropdown position when inside a input-group */ 131 | .input-group > .ui-select-bootstrap.dropdown { 132 | /* Instead of relative */ 133 | position: static; 134 | } 135 | 136 | .input-group > .ui-select-bootstrap > input.ui-select-search.form-control { 137 | border-radius: 4px; /* FIXME hardcoded value :-/ */ 138 | border-top-right-radius: 0; 139 | border-bottom-right-radius: 0; 140 | } 141 | .input-group > .ui-select-bootstrap > input.ui-select-search.form-control.direction-up { 142 | border-radius: 4px !important; /* FIXME hardcoded value :-/ */ 143 | border-top-right-radius: 0 !important; 144 | border-bottom-right-radius: 0 !important; 145 | } 146 | 147 | .ui-select-bootstrap > .ui-select-match > .btn{ 148 | /* Instead of center because of .btn */ 149 | text-align: left !important; 150 | } 151 | 152 | .ui-select-bootstrap > .ui-select-match > .caret { 153 | position: absolute; 154 | top: 45%; 155 | right: 15px; 156 | } 157 | 158 | /* See Scrollable Menu with Bootstrap 3 http://stackoverflow.com/questions/19227496 */ 159 | .ui-select-bootstrap > .ui-select-choices { 160 | width: 100%; 161 | height: auto; 162 | max-height: 200px; 163 | overflow-x: hidden; 164 | margin-top: -1px; 165 | } 166 | 167 | body > .ui-select-bootstrap.open { 168 | z-index: 1000; /* Standard Bootstrap dropdown z-index */ 169 | } 170 | 171 | .ui-select-multiple.ui-select-bootstrap { 172 | height: auto; 173 | padding: 3px 3px 0 3px; 174 | } 175 | 176 | .ui-select-multiple.ui-select-bootstrap input.ui-select-search { 177 | background-color: transparent !important; /* To prevent double background when disabled */ 178 | border: none; 179 | outline: none; 180 | height: 1.666666em; 181 | margin-bottom: 3px; 182 | } 183 | 184 | .ui-select-multiple.ui-select-bootstrap .ui-select-match .close { 185 | font-size: 1.6em; 186 | line-height: 0.75; 187 | } 188 | 189 | .ui-select-multiple.ui-select-bootstrap .ui-select-match-item { 190 | outline: 0; 191 | margin: 0 3px 3px 0; 192 | } 193 | 194 | .ui-select-multiple .ui-select-match-item { 195 | position: relative; 196 | } 197 | 198 | .ui-select-multiple .ui-select-match-item.dropping-before:before { 199 | content: ""; 200 | position: absolute; 201 | top: 0; 202 | right: 100%; 203 | height: 100%; 204 | margin-right: 2px; 205 | border-left: 1px solid #428bca; 206 | } 207 | 208 | .ui-select-multiple .ui-select-match-item.dropping-after:after { 209 | content: ""; 210 | position: absolute; 211 | top: 0; 212 | left: 100%; 213 | height: 100%; 214 | margin-left: 2px; 215 | border-right: 1px solid #428bca; 216 | } 217 | 218 | .ui-select-bootstrap .ui-select-choices-row>a { 219 | display: block; 220 | padding: 3px 20px; 221 | clear: both; 222 | font-weight: 400; 223 | line-height: 1.42857143; 224 | color: #333; 225 | white-space: nowrap; 226 | } 227 | 228 | .ui-select-bootstrap .ui-select-choices-row>a:hover, .ui-select-bootstrap .ui-select-choices-row>a:focus { 229 | text-decoration: none; 230 | color: #262626; 231 | background-color: #f5f5f5; 232 | } 233 | 234 | .ui-select-bootstrap .ui-select-choices-row.active>a { 235 | color: #fff; 236 | text-decoration: none; 237 | outline: 0; 238 | background-color: #428bca; 239 | } 240 | 241 | .ui-select-bootstrap .ui-select-choices-row.disabled>a, 242 | .ui-select-bootstrap .ui-select-choices-row.active.disabled>a { 243 | color: #777; 244 | cursor: not-allowed; 245 | background-color: #fff; 246 | } 247 | 248 | /* fix hide/show angular animation */ 249 | .ui-select-match.ng-hide-add, 250 | .ui-select-search.ng-hide-add { 251 | display: none !important; 252 | } 253 | 254 | /* Mark invalid Bootstrap */ 255 | .ui-select-bootstrap.ng-dirty.ng-invalid > button.btn.ui-select-match { 256 | border-color: #D44950; 257 | } 258 | 259 | /* Handle up direction Bootstrap */ 260 | .ui-select-container[theme="bootstrap"].direction-up .ui-select-dropdown { 261 | box-shadow: 0 -4px 8px rgba(0, 0, 0, 0.25); 262 | } 263 | -------------------------------------------------------------------------------- /app/css/select2-override.css: -------------------------------------------------------------------------------- 1 | .select2-container .select2-choice { 2 | height: 34px; 3 | -webkit-box-shadow: none; 4 | -moz-box-shadow: none; 5 | box-shadow: none; 6 | background-color: #fff; 7 | background-image: none; 8 | background: #fff; 9 | } 10 | 11 | .select2-container .select2-choice .select2-chosen { margin-top: 4px } 12 | .select2-container .select2-choice abbr { top: 13px } 13 | .select2-container .select2-choice, 14 | .select2-container .select2-choice .select2-arrow { 15 | -webkit-border-radius: 0; 16 | border-radius: 0; 17 | border-color: #ccc; 18 | } 19 | 20 | .select2-container .select2-choice .select2-arrow { 21 | background-color: #fafafa; 22 | background-image: none; 23 | background: #fafafa; 24 | width: 22px; 25 | } 26 | 27 | .select2-container .select2-choice .select2-arrow b>span { margin: 4px 0 0 6px } 28 | .select2-container.select2-container-active .select2-choice { border-color: #91acce } 29 | .select2-container.select2-dropdown-open .select2-choice { border-bottom-color: #ccc } 30 | .select2-drop { 31 | -webkit-border-radius: 0 !important; 32 | border-radius: 0 !important; 33 | } 34 | 35 | .select2-drop:not(.select2-drop-above) { margin-top: -3px } 36 | .select2-drop .select2-results { max-height: 300px } 37 | .select2-drop .select2-results li { 38 | line-height: inherit; 39 | margin: 0; 40 | padding: 0; 41 | } 42 | 43 | .select2-search { margin: 4px 0 } 44 | .select2-search input, 45 | .select2-search input:focus { 46 | background-color: #fff; 47 | background-image: none; 48 | background: #fff; 49 | -webkit-box-shadow: none; 50 | -moz-box-shadow: none; 51 | box-shadow: none; 52 | border: none; 53 | } 54 | 55 | .select2-container.form-control { 56 | border: none; 57 | -webkit-box-shadow: none; 58 | -moz-box-shadow: none; 59 | box-shadow: none; 60 | padding: 0; 61 | } 62 | 63 | .select2-container .select2-input, .select2-container .select2-input:focus{ 64 | border: 1px solid #d2d2d2; 65 | } 66 | 67 | .select2-container .select2-choice .select2-arrow{ 68 | padding: 2px; 69 | } -------------------------------------------------------------------------------- /app/css/select2.css: -------------------------------------------------------------------------------- 1 | /* 2 | Version: 3.4.5 Timestamp: Mon Nov 4 08:22:42 PST 2013 3 | */ 4 | .select2-container { 5 | margin: 0; 6 | position: relative; 7 | display: inline-block; 8 | /* inline-block for ie7 */ 9 | zoom: 1; 10 | *display: inline; 11 | vertical-align: middle; 12 | } 13 | 14 | .select2-container, 15 | .select2-drop, 16 | .select2-search, 17 | .select2-search input { 18 | /* 19 | Force border-box so that % widths fit the parent 20 | container without overlap because of margin/padding. 21 | 22 | More Info : http://www.quirksmode.org/css/box.html 23 | */ 24 | -webkit-box-sizing: border-box; /* webkit */ 25 | -moz-box-sizing: border-box; /* firefox */ 26 | box-sizing: border-box; /* css3 */ 27 | } 28 | 29 | .select2-container .select2-choice { 30 | display: block; 31 | height: 26px; 32 | padding: 0 0 0 8px; 33 | overflow: hidden; 34 | position: relative; 35 | 36 | border: 1px solid #aaa; 37 | white-space: nowrap; 38 | line-height: 26px; 39 | color: #444; 40 | text-decoration: none; 41 | 42 | border-radius: 4px; 43 | 44 | background-clip: padding-box; 45 | 46 | -webkit-touch-callout: none; 47 | -webkit-user-select: none; 48 | -moz-user-select: none; 49 | -ms-user-select: none; 50 | user-select: none; 51 | 52 | background-color: #fff; 53 | background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #eee), color-stop(0.5, #fff)); 54 | background-image: -webkit-linear-gradient(center bottom, #eee 0%, #fff 50%); 55 | background-image: -moz-linear-gradient(center bottom, #eee 0%, #fff 50%); 56 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr = '#ffffff', endColorstr = '#eeeeee', GradientType = 0); 57 | background-image: linear-gradient(top, #fff 0%, #eee 50%); 58 | } 59 | 60 | .select2-container.select2-drop-above .select2-choice { 61 | border-bottom-color: #aaa; 62 | 63 | border-radius: 0 0 4px 4px; 64 | 65 | background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #eee), color-stop(0.9, #fff)); 66 | background-image: -webkit-linear-gradient(center bottom, #eee 0%, #fff 90%); 67 | background-image: -moz-linear-gradient(center bottom, #eee 0%, #fff 90%); 68 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#eeeeee', GradientType=0); 69 | background-image: linear-gradient(top, #eee 0%, #fff 90%); 70 | } 71 | 72 | .select2-container.select2-allowclear .select2-choice .select2-chosen { 73 | margin-right: 42px; 74 | } 75 | 76 | .select2-container .select2-choice > .select2-chosen { 77 | margin-right: 26px; 78 | display: block; 79 | overflow: hidden; 80 | 81 | white-space: nowrap; 82 | 83 | text-overflow: ellipsis; 84 | } 85 | 86 | .select2-container .select2-choice abbr { 87 | display: none; 88 | width: 12px; 89 | height: 12px; 90 | position: absolute; 91 | right: 24px; 92 | top: 8px; 93 | 94 | font-size: 1px; 95 | text-decoration: none; 96 | 97 | border: 0; 98 | background: url('select2.png') right top no-repeat; 99 | cursor: pointer; 100 | outline: 0; 101 | } 102 | 103 | .select2-container.select2-allowclear .select2-choice abbr { 104 | display: inline-block; 105 | } 106 | 107 | .select2-container .select2-choice abbr:hover { 108 | background-position: right -11px; 109 | cursor: pointer; 110 | } 111 | 112 | .select2-drop-mask { 113 | border: 0; 114 | margin: 0; 115 | padding: 0; 116 | position: fixed; 117 | left: 0; 118 | top: 0; 119 | min-height: 100%; 120 | min-width: 100%; 121 | height: auto; 122 | width: auto; 123 | opacity: 0; 124 | z-index: 9998; 125 | /* styles required for IE to work */ 126 | background-color: #fff; 127 | filter: alpha(opacity=0); 128 | } 129 | 130 | .select2-drop { 131 | width: 100%; 132 | margin-top: -1px; 133 | position: absolute; 134 | z-index: 9999; 135 | top: 100%; 136 | 137 | background: #fff; 138 | color: #000; 139 | border: 1px solid #aaa; 140 | border-top: 0; 141 | 142 | border-radius: 0 0 4px 4px; 143 | 144 | -webkit-box-shadow: 0 4px 5px rgba(0, 0, 0, .15); 145 | box-shadow: 0 4px 5px rgba(0, 0, 0, .15); 146 | } 147 | 148 | .select2-drop-auto-width { 149 | border-top: 1px solid #aaa; 150 | width: auto; 151 | } 152 | 153 | .select2-drop-auto-width .select2-search { 154 | padding-top: 4px; 155 | } 156 | 157 | .select2-drop.select2-drop-above { 158 | margin-top: 1px; 159 | border-top: 1px solid #aaa; 160 | border-bottom: 0; 161 | 162 | border-radius: 4px 4px 0 0; 163 | 164 | -webkit-box-shadow: 0 -4px 5px rgba(0, 0, 0, .15); 165 | box-shadow: 0 -4px 5px rgba(0, 0, 0, .15); 166 | } 167 | 168 | .select2-drop-active { 169 | border: 1px solid #5897fb; 170 | border-top: none; 171 | } 172 | 173 | .select2-drop.select2-drop-above.select2-drop-active { 174 | border-top: 1px solid #5897fb; 175 | } 176 | 177 | .select2-container .select2-choice .select2-arrow { 178 | display: inline-block; 179 | width: 18px; 180 | height: 100%; 181 | position: absolute; 182 | right: 0; 183 | top: 0; 184 | 185 | border-left: 1px solid #aaa; 186 | border-radius: 0 4px 4px 0; 187 | 188 | background-clip: padding-box; 189 | 190 | background: #ccc; 191 | background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #ccc), color-stop(0.6, #eee)); 192 | background-image: -webkit-linear-gradient(center bottom, #ccc 0%, #eee 60%); 193 | background-image: -moz-linear-gradient(center bottom, #ccc 0%, #eee 60%); 194 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr = '#eeeeee', endColorstr = '#cccccc', GradientType = 0); 195 | background-image: linear-gradient(top, #ccc 0%, #eee 60%); 196 | } 197 | 198 | .select2-container .select2-choice .select2-arrow b { 199 | display: block; 200 | width: 100%; 201 | height: 100%; 202 | background: url('select2.png') no-repeat 0 1px; 203 | } 204 | 205 | .select2-search { 206 | display: inline-block; 207 | width: 100%; 208 | min-height: 26px; 209 | margin: 0; 210 | padding-left: 4px; 211 | padding-right: 4px; 212 | 213 | position: relative; 214 | z-index: 10000; 215 | 216 | white-space: nowrap; 217 | } 218 | 219 | .select2-search input { 220 | width: 100%; 221 | height: auto !important; 222 | min-height: 26px; 223 | padding: 4px 20px 4px 5px; 224 | margin: 0; 225 | 226 | outline: 0; 227 | font-family: sans-serif; 228 | font-size: 1em; 229 | 230 | border: 1px solid #aaa; 231 | border-radius: 0; 232 | 233 | -webkit-box-shadow: none; 234 | box-shadow: none; 235 | 236 | background: #fff url('select2.png') no-repeat 100% -22px; 237 | background: url('select2.png') no-repeat 100% -22px, -webkit-gradient(linear, left bottom, left top, color-stop(0.85, #fff), color-stop(0.99, #eee)); 238 | background: url('select2.png') no-repeat 100% -22px, -webkit-linear-gradient(center bottom, #fff 85%, #eee 99%); 239 | background: url('select2.png') no-repeat 100% -22px, -moz-linear-gradient(center bottom, #fff 85%, #eee 99%); 240 | background: url('select2.png') no-repeat 100% -22px, linear-gradient(top, #fff 85%, #eee 99%); 241 | } 242 | 243 | .select2-drop.select2-drop-above .select2-search input { 244 | margin-top: 4px; 245 | } 246 | 247 | .select2-search input.select2-active { 248 | background: #fff url('select2-spinner.gif') no-repeat 100%; 249 | background: url('select2-spinner.gif') no-repeat 100%, -webkit-gradient(linear, left bottom, left top, color-stop(0.85, #fff), color-stop(0.99, #eee)); 250 | background: url('select2-spinner.gif') no-repeat 100%, -webkit-linear-gradient(center bottom, #fff 85%, #eee 99%); 251 | background: url('select2-spinner.gif') no-repeat 100%, -moz-linear-gradient(center bottom, #fff 85%, #eee 99%); 252 | background: url('select2-spinner.gif') no-repeat 100%, linear-gradient(top, #fff 85%, #eee 99%); 253 | } 254 | 255 | .select2-container-active .select2-choice, 256 | .select2-container-active .select2-choices { 257 | border: 1px solid #5897fb; 258 | outline: none; 259 | 260 | -webkit-box-shadow: 0 0 5px rgba(0, 0, 0, .3); 261 | box-shadow: 0 0 5px rgba(0, 0, 0, .3); 262 | } 263 | 264 | .select2-dropdown-open .select2-choice { 265 | border-bottom-color: transparent; 266 | -webkit-box-shadow: 0 1px 0 #fff inset; 267 | box-shadow: 0 1px 0 #fff inset; 268 | 269 | border-bottom-left-radius: 0; 270 | border-bottom-right-radius: 0; 271 | 272 | background-color: #eee; 273 | background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0, #fff), color-stop(0.5, #eee)); 274 | background-image: -webkit-linear-gradient(center bottom, #fff 0%, #eee 50%); 275 | background-image: -moz-linear-gradient(center bottom, #fff 0%, #eee 50%); 276 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#eeeeee', endColorstr='#ffffff', GradientType=0); 277 | background-image: linear-gradient(top, #fff 0%, #eee 50%); 278 | } 279 | 280 | .select2-dropdown-open.select2-drop-above .select2-choice, 281 | .select2-dropdown-open.select2-drop-above .select2-choices { 282 | border: 1px solid #5897fb; 283 | border-top-color: transparent; 284 | 285 | background-image: -webkit-gradient(linear, left top, left bottom, color-stop(0, #fff), color-stop(0.5, #eee)); 286 | background-image: -webkit-linear-gradient(center top, #fff 0%, #eee 50%); 287 | background-image: -moz-linear-gradient(center top, #fff 0%, #eee 50%); 288 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#eeeeee', endColorstr='#ffffff', GradientType=0); 289 | background-image: linear-gradient(bottom, #fff 0%, #eee 50%); 290 | } 291 | 292 | .select2-dropdown-open .select2-choice .select2-arrow { 293 | background: transparent; 294 | border-left: none; 295 | filter: none; 296 | } 297 | .select2-dropdown-open .select2-choice .select2-arrow b { 298 | background-position: -18px 1px; 299 | } 300 | 301 | /* results */ 302 | .select2-results { 303 | max-height: 200px; 304 | padding: 0 0 0 4px; 305 | margin: 4px 4px 4px 0; 306 | position: relative; 307 | overflow-x: hidden; 308 | overflow-y: auto; 309 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 310 | } 311 | 312 | .select2-results ul.select2-result-sub { 313 | margin: 0; 314 | padding-left: 0; 315 | } 316 | 317 | .select2-results ul.select2-result-sub > li .select2-result-label { padding-left: 20px } 318 | .select2-results ul.select2-result-sub ul.select2-result-sub > li .select2-result-label { padding-left: 40px } 319 | .select2-results ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub > li .select2-result-label { padding-left: 60px } 320 | .select2-results ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub > li .select2-result-label { padding-left: 80px } 321 | .select2-results ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub > li .select2-result-label { padding-left: 100px } 322 | .select2-results ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub > li .select2-result-label { padding-left: 110px } 323 | .select2-results ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub ul.select2-result-sub > li .select2-result-label { padding-left: 120px } 324 | 325 | .select2-results li { 326 | list-style: none; 327 | display: list-item; 328 | background-image: none; 329 | } 330 | 331 | .select2-results li.select2-result-with-children > .select2-result-label { 332 | font-weight: bold; 333 | } 334 | 335 | .select2-results .select2-result-label { 336 | padding: 3px 7px 4px; 337 | margin: 0; 338 | cursor: pointer; 339 | 340 | min-height: 1em; 341 | 342 | -webkit-touch-callout: none; 343 | -webkit-user-select: none; 344 | -moz-user-select: none; 345 | -ms-user-select: none; 346 | user-select: none; 347 | } 348 | 349 | .select2-results .select2-highlighted { 350 | background: #3875d7; 351 | color: #fff; 352 | } 353 | 354 | .select2-results li em { 355 | background: #feffde; 356 | font-style: normal; 357 | } 358 | 359 | .select2-results .select2-highlighted em { 360 | background: transparent; 361 | } 362 | 363 | .select2-results .select2-highlighted ul { 364 | background: #fff; 365 | color: #000; 366 | } 367 | 368 | 369 | .select2-results .select2-no-results, 370 | .select2-results .select2-searching, 371 | .select2-results .select2-selection-limit { 372 | background: #f4f4f4; 373 | display: list-item; 374 | } 375 | 376 | /* 377 | disabled look for disabled choices in the results dropdown 378 | */ 379 | .select2-results .select2-disabled.select2-highlighted { 380 | color: #666; 381 | background: #f4f4f4; 382 | display: list-item; 383 | cursor: default; 384 | } 385 | .select2-results .select2-disabled { 386 | background: #f4f4f4; 387 | display: list-item; 388 | cursor: default; 389 | } 390 | 391 | .select2-results .select2-selected { 392 | display: none; 393 | } 394 | 395 | .select2-more-results.select2-active { 396 | background: #f4f4f4 url('select2-spinner.gif') no-repeat 100%; 397 | } 398 | 399 | .select2-more-results { 400 | background: #f4f4f4; 401 | display: list-item; 402 | } 403 | 404 | /* disabled styles */ 405 | 406 | .select2-container.select2-container-disabled .select2-choice { 407 | background-color: #f4f4f4; 408 | background-image: none; 409 | border: 1px solid #ddd; 410 | cursor: default; 411 | } 412 | 413 | .select2-container.select2-container-disabled .select2-choice .select2-arrow { 414 | background-color: #f4f4f4; 415 | background-image: none; 416 | border-left: 0; 417 | } 418 | 419 | .select2-container.select2-container-disabled .select2-choice abbr { 420 | display: none; 421 | } 422 | 423 | 424 | /* multiselect */ 425 | 426 | .select2-container-multi .select2-choices { 427 | height: auto !important; 428 | height: 1%; 429 | margin: 0; 430 | padding: 0; 431 | position: relative; 432 | 433 | border: 1px solid #aaa; 434 | cursor: text; 435 | overflow: hidden; 436 | 437 | background-color: #fff; 438 | background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(1%, #eee), color-stop(15%, #fff)); 439 | background-image: -webkit-linear-gradient(top, #eee 1%, #fff 15%); 440 | background-image: -moz-linear-gradient(top, #eee 1%, #fff 15%); 441 | background-image: linear-gradient(top, #eee 1%, #fff 15%); 442 | } 443 | 444 | .select2-locked { 445 | padding: 3px 5px 3px 5px !important; 446 | } 447 | 448 | .select2-container-multi .select2-choices { 449 | min-height: 26px; 450 | } 451 | 452 | .select2-container-multi.select2-container-active .select2-choices { 453 | border: 1px solid #5897fb; 454 | outline: none; 455 | 456 | -webkit-box-shadow: 0 0 5px rgba(0, 0, 0, .3); 457 | box-shadow: 0 0 5px rgba(0, 0, 0, .3); 458 | } 459 | .select2-container-multi .select2-choices li { 460 | float: left; 461 | list-style: none; 462 | } 463 | .select2-container-multi .select2-choices .select2-search-field { 464 | margin: 0; 465 | padding: 0; 466 | white-space: nowrap; 467 | } 468 | 469 | .select2-container-multi .select2-choices .select2-search-field input { 470 | padding: 5px; 471 | margin: 1px 0; 472 | 473 | font-family: sans-serif; 474 | font-size: 100%; 475 | color: #666; 476 | outline: 0; 477 | border: 0; 478 | -webkit-box-shadow: none; 479 | box-shadow: none; 480 | background: transparent !important; 481 | } 482 | 483 | .select2-container-multi .select2-choices .select2-search-field input.select2-active { 484 | background: #fff url('select2-spinner.gif') no-repeat 100% !important; 485 | } 486 | 487 | .select2-default { 488 | color: #999 !important; 489 | } 490 | 491 | .select2-container-multi .select2-choices .select2-search-choice { 492 | padding: 3px 5px 3px 18px; 493 | margin: 3px 0 3px 5px; 494 | position: relative; 495 | 496 | line-height: 13px; 497 | color: #333; 498 | cursor: default; 499 | border: 1px solid #aaaaaa; 500 | 501 | border-radius: 3px; 502 | 503 | -webkit-box-shadow: 0 0 2px #fff inset, 0 1px 0 rgba(0, 0, 0, 0.05); 504 | box-shadow: 0 0 2px #fff inset, 0 1px 0 rgba(0, 0, 0, 0.05); 505 | 506 | background-clip: padding-box; 507 | 508 | -webkit-touch-callout: none; 509 | -webkit-user-select: none; 510 | -moz-user-select: none; 511 | -ms-user-select: none; 512 | user-select: none; 513 | 514 | background-color: #e4e4e4; 515 | filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#eeeeee', endColorstr='#f4f4f4', GradientType=0); 516 | background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, color-stop(20%, #f4f4f4), color-stop(50%, #f0f0f0), color-stop(52%, #e8e8e8), color-stop(100%, #eee)); 517 | background-image: -webkit-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eee 100%); 518 | background-image: -moz-linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eee 100%); 519 | background-image: linear-gradient(top, #f4f4f4 20%, #f0f0f0 50%, #e8e8e8 52%, #eee 100%); 520 | } 521 | .select2-container-multi .select2-choices .select2-search-choice .select2-chosen { 522 | cursor: default; 523 | } 524 | .select2-container-multi .select2-choices .select2-search-choice-focus { 525 | background: #d4d4d4; 526 | } 527 | 528 | .select2-search-choice-close { 529 | display: block; 530 | width: 12px; 531 | height: 13px; 532 | position: absolute; 533 | right: 3px; 534 | top: 4px; 535 | 536 | font-size: 1px; 537 | outline: none; 538 | background: url('select2.png') right top no-repeat; 539 | } 540 | 541 | .select2-container-multi .select2-search-choice-close { 542 | left: 3px; 543 | } 544 | 545 | .select2-container-multi .select2-choices .select2-search-choice .select2-search-choice-close:hover { 546 | background-position: right -11px; 547 | } 548 | .select2-container-multi .select2-choices .select2-search-choice-focus .select2-search-choice-close { 549 | background-position: right -11px; 550 | } 551 | 552 | /* disabled styles */ 553 | .select2-container-multi.select2-container-disabled .select2-choices { 554 | background-color: #f4f4f4; 555 | background-image: none; 556 | border: 1px solid #ddd; 557 | cursor: default; 558 | } 559 | 560 | .select2-container-multi.select2-container-disabled .select2-choices .select2-search-choice { 561 | padding: 3px 5px 3px 5px; 562 | border: 1px solid #ddd; 563 | background-image: none; 564 | background-color: #f4f4f4; 565 | } 566 | 567 | .select2-container-multi.select2-container-disabled .select2-choices .select2-search-choice .select2-search-choice-close { display: none; 568 | background: none; 569 | } 570 | /* end multiselect */ 571 | 572 | 573 | .select2-result-selectable .select2-match, 574 | .select2-result-unselectable .select2-match { 575 | text-decoration: underline; 576 | } 577 | 578 | .select2-offscreen, .select2-offscreen:focus { 579 | clip: rect(0 0 0 0) !important; 580 | width: 1px !important; 581 | height: 1px !important; 582 | border: 0 !important; 583 | margin: 0 !important; 584 | padding: 0 !important; 585 | overflow: hidden !important; 586 | position: absolute !important; 587 | outline: 0 !important; 588 | left: 0px !important; 589 | top: 0px !important; 590 | } 591 | 592 | .select2-display-none { 593 | display: none; 594 | } 595 | 596 | .select2-measure-scrollbar { 597 | position: absolute; 598 | top: -10000px; 599 | left: -10000px; 600 | width: 100px; 601 | height: 100px; 602 | overflow: scroll; 603 | } 604 | /* Retina-ize icons */ 605 | 606 | @media only screen and (-webkit-min-device-pixel-ratio: 1.5), only screen and (min-resolution: 144dpi) { 607 | .select2-search input, .select2-search-choice-close, .select2-container .select2-choice abbr, .select2-container .select2-choice .select2-arrow b { 608 | background-image: url('select2x2.png') !important; 609 | background-repeat: no-repeat !important; 610 | background-size: 60px 40px !important; 611 | } 612 | .select2-search input { 613 | background-position: 100% -21px !important; 614 | } 615 | } 616 | -------------------------------------------------------------------------------- /app/css/select2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oaprograms/lingo-player/37732ef949ced3d575cf2f079fd5d4461b671fd5/app/css/select2.png -------------------------------------------------------------------------------- /app/css/select2x2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oaprograms/lingo-player/37732ef949ced3d575cf2f079fd5d4461b671fd5/app/css/select2x2.png -------------------------------------------------------------------------------- /app/css/selectize.bootstrap3.css: -------------------------------------------------------------------------------- 1 | /** 2 | * selectize.bootstrap3.css (v0.8.5) - Bootstrap 3 Theme 3 | * Copyright (c) 2013 Brian Reavis & contributors 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this 6 | * file except in compliance with the License. You may obtain a copy of the License at: 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under 10 | * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF 11 | * ANY KIND, either express or implied. See the License for the specific language 12 | * governing permissions and limitations under the License. 13 | * 14 | * @author Brian Reavis 15 | */ 16 | 17 | .selectize-control.plugin-drag_drop.multi > .selectize-input > div.ui-sortable-placeholder { 18 | background: #f2f2f2 !important; 19 | background: rgba(0, 0, 0, 0.06) !important; 20 | border: 0 none !important; 21 | visibility: visible !important; 22 | -webkit-box-shadow: inset 0 0 12px 4px #ffffff; 23 | box-shadow: inset 0 0 12px 4px #ffffff; 24 | } 25 | 26 | .selectize-control.plugin-drag_drop .ui-sortable-placeholder::after { 27 | content: '!'; 28 | visibility: hidden; 29 | } 30 | 31 | .selectize-control.plugin-drag_drop .ui-sortable-helper { 32 | -webkit-box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); 33 | box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); 34 | } 35 | 36 | .selectize-dropdown-header { 37 | position: relative; 38 | padding: 3px 12px; 39 | background: #f8f8f8; 40 | border-bottom: 1px solid #d0d0d0; 41 | -webkit-border-radius: 4px 4px 0 0; 42 | -moz-border-radius: 4px 4px 0 0; 43 | border-radius: 4px 4px 0 0; 44 | } 45 | 46 | .selectize-dropdown-header-close { 47 | position: absolute; 48 | top: 50%; 49 | right: 12px; 50 | margin-top: -12px; 51 | font-size: 20px !important; 52 | line-height: 20px; 53 | color: #333333; 54 | opacity: 0.4; 55 | } 56 | 57 | .selectize-dropdown-header-close:hover { 58 | color: #000000; 59 | } 60 | 61 | .selectize-dropdown.plugin-optgroup_columns .optgroup { 62 | float: left; 63 | border-top: 0 none; 64 | border-right: 1px solid #f2f2f2; 65 | -webkit-box-sizing: border-box; 66 | -moz-box-sizing: border-box; 67 | box-sizing: border-box; 68 | } 69 | 70 | .selectize-dropdown.plugin-optgroup_columns .optgroup:last-child { 71 | border-right: 0 none; 72 | } 73 | 74 | .selectize-dropdown.plugin-optgroup_columns .optgroup:before { 75 | display: none; 76 | } 77 | 78 | .selectize-dropdown.plugin-optgroup_columns .optgroup-header { 79 | border-top: 0 none; 80 | } 81 | 82 | .selectize-control.plugin-remove_button [data-value] { 83 | position: relative; 84 | padding-right: 24px !important; 85 | } 86 | 87 | .selectize-control.plugin-remove_button [data-value] .remove { 88 | position: absolute; 89 | top: 0; 90 | right: 0; 91 | bottom: 0; 92 | display: inline-block; 93 | width: 17px; 94 | padding: 1px 0 0 0; 95 | font-size: 12px; 96 | font-weight: bold; 97 | color: inherit; 98 | text-align: center; 99 | text-decoration: none; 100 | vertical-align: middle; 101 | border-left: 1px solid rgba(0, 0, 0, 0); 102 | -webkit-border-radius: 0 2px 2px 0; 103 | -moz-border-radius: 0 2px 2px 0; 104 | border-radius: 0 2px 2px 0; 105 | -webkit-box-sizing: border-box; 106 | -moz-box-sizing: border-box; 107 | box-sizing: border-box; 108 | } 109 | 110 | .selectize-control.plugin-remove_button [data-value] .remove:hover { 111 | background: rgba(0, 0, 0, 0.05); 112 | } 113 | 114 | .selectize-control.plugin-remove_button [data-value].active .remove { 115 | border-left-color: rgba(0, 0, 0, 0); 116 | } 117 | 118 | .selectize-control.plugin-remove_button .disabled [data-value] .remove:hover { 119 | background: none; 120 | } 121 | 122 | .selectize-control.plugin-remove_button .disabled [data-value] .remove { 123 | border-left-color: rgba(77, 77, 77, 0); 124 | } 125 | 126 | .selectize-control { 127 | position: relative; 128 | } 129 | 130 | .selectize-dropdown, 131 | .selectize-input, 132 | .selectize-input input { 133 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 134 | font-size: 14px; 135 | -webkit-font-smoothing: inherit; 136 | line-height: 20px; 137 | color: #333333; 138 | } 139 | 140 | .selectize-input, 141 | .selectize-control.single .selectize-input.input-active { 142 | display: inline-block; 143 | cursor: text; 144 | background: #ffffff; 145 | } 146 | 147 | .selectize-input { 148 | position: relative; 149 | z-index: 1; 150 | display: inline-block; 151 | width: 100%; 152 | padding: 6px 12px; 153 | overflow: hidden; 154 | border: 1px solid #cccccc; 155 | -webkit-border-radius: 4px; 156 | -moz-border-radius: 4px; 157 | border-radius: 4px; 158 | -webkit-box-shadow: none; 159 | box-shadow: none; 160 | -webkit-box-sizing: border-box; 161 | -moz-box-sizing: border-box; 162 | box-sizing: border-box; 163 | } 164 | 165 | .selectize-control.multi .selectize-input.has-items { 166 | padding: 5px 12px 2px; 167 | } 168 | 169 | .selectize-input.full { 170 | background-color: #ffffff; 171 | } 172 | 173 | .selectize-input.disabled, 174 | .selectize-input.disabled * { 175 | cursor: default !important; 176 | } 177 | 178 | .selectize-input.focus { 179 | -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.15); 180 | box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.15); 181 | } 182 | 183 | .selectize-input.dropdown-active { 184 | -webkit-border-radius: 4px 4px 0 0; 185 | -moz-border-radius: 4px 4px 0 0; 186 | border-radius: 4px 4px 0 0; 187 | } 188 | 189 | .selectize-input > * { 190 | display: -moz-inline-stack; 191 | display: inline-block; 192 | *display: inline; 193 | vertical-align: baseline; 194 | zoom: 1; 195 | } 196 | 197 | .selectize-control.multi .selectize-input > div { 198 | padding: 1px 3px; 199 | margin: 0 3px 3px 0; 200 | color: #333333; 201 | cursor: pointer; 202 | background: #efefef; 203 | border: 0 solid rgba(0, 0, 0, 0); 204 | } 205 | 206 | .selectize-control.multi .selectize-input > div.active { 207 | color: #ffffff; 208 | background: #428bca; 209 | border: 0 solid rgba(0, 0, 0, 0); 210 | } 211 | 212 | .selectize-control.multi .selectize-input.disabled > div, 213 | .selectize-control.multi .selectize-input.disabled > div.active { 214 | color: #808080; 215 | background: #ffffff; 216 | border: 0 solid rgba(77, 77, 77, 0); 217 | } 218 | 219 | .selectize-input > input { 220 | max-width: 100% !important; 221 | max-height: none !important; 222 | min-height: 0 !important; 223 | padding: 0 !important; 224 | margin: 0 !important; 225 | line-height: inherit !important; 226 | text-indent: 0 !important; 227 | background: none !important; 228 | border: 0 none !important; 229 | -webkit-box-shadow: none !important; 230 | box-shadow: none !important; 231 | -webkit-user-select: auto !important; 232 | } 233 | 234 | .selectize-input > input:focus { 235 | outline: none !important; 236 | } 237 | 238 | .selectize-input::after { 239 | display: block; 240 | clear: left; 241 | content: ' '; 242 | } 243 | 244 | .selectize-input.dropdown-active::before { 245 | position: absolute; 246 | right: 0; 247 | bottom: 0; 248 | left: 0; 249 | display: block; 250 | height: 1px; 251 | background: #ffffff; 252 | content: ' '; 253 | } 254 | 255 | .selectize-dropdown { 256 | position: absolute; 257 | z-index: 10; 258 | margin: -1px 0 0 0; 259 | background: #ffffff; 260 | border: 1px solid #cccccc; 261 | border-top: 0 none; 262 | -webkit-border-radius: 0 0 4px 4px; 263 | -moz-border-radius: 0 0 4px 4px; 264 | border-radius: 0 0 4px 4px; 265 | -webkit-box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 266 | box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); 267 | -webkit-box-sizing: border-box; 268 | -moz-box-sizing: border-box; 269 | box-sizing: border-box; 270 | } 271 | 272 | .selectize-dropdown [data-selectable] { 273 | overflow: hidden; 274 | cursor: pointer; 275 | } 276 | 277 | .selectize-dropdown [data-selectable] .highlight { 278 | background: rgba(255, 237, 40, 0.4); 279 | -webkit-border-radius: 1px; 280 | -moz-border-radius: 1px; 281 | border-radius: 1px; 282 | } 283 | 284 | .selectize-dropdown [data-selectable], 285 | .selectize-dropdown .optgroup-header { 286 | padding: 3px 12px; 287 | } 288 | 289 | .selectize-dropdown .optgroup:first-child .optgroup-header { 290 | border-top: 0 none; 291 | } 292 | 293 | .selectize-dropdown .optgroup-header { 294 | color: #999999; 295 | cursor: default; 296 | background: #ffffff; 297 | } 298 | 299 | .selectize-dropdown .active { 300 | color: #ffffff; 301 | background-color: #428bca; 302 | } 303 | 304 | .selectize-dropdown .active.create { 305 | color: #ffffff; 306 | } 307 | 308 | .selectize-dropdown .create { 309 | color: rgba(51, 51, 51, 0.5); 310 | } 311 | 312 | .selectize-dropdown-content { 313 | max-height: 200px; 314 | overflow-x: hidden; 315 | overflow-y: auto; 316 | } 317 | 318 | .selectize-control.single .selectize-input, 319 | .selectize-control.single .selectize-input input { 320 | cursor: pointer; 321 | } 322 | 323 | .selectize-control.single .selectize-input.input-active, 324 | .selectize-control.single .selectize-input.input-active input { 325 | cursor: text; 326 | } 327 | 328 | .selectize-control.single .selectize-input:after { 329 | position: absolute; 330 | top: 50%; 331 | right: 17px; 332 | display: block; 333 | width: 0; 334 | height: 0; 335 | margin-top: -3px; 336 | border-color: #000000 transparent transparent transparent; 337 | border-style: solid; 338 | border-width: 5px 5px 0 5px; 339 | content: ' '; 340 | } 341 | 342 | .selectize-control.single .selectize-input.dropdown-active:after { 343 | margin-top: -4px; 344 | border-color: transparent transparent #000000 transparent; 345 | border-width: 0 5px 5px 5px; 346 | } 347 | 348 | .selectize-control.rtl.single .selectize-input:after { 349 | right: auto; 350 | left: 17px; 351 | } 352 | 353 | .selectize-control.rtl .selectize-input > input { 354 | margin: 0 4px 0 -2px !important; 355 | } 356 | 357 | .selectize-control .selectize-input.disabled { 358 | background-color: #ffffff; 359 | opacity: 0.5; 360 | } 361 | 362 | .selectize-dropdown, 363 | .selectize-dropdown.form-control { 364 | z-index: 1000; 365 | height: auto; 366 | padding: 0; 367 | margin: 2px 0 0 0; 368 | background: #ffffff; 369 | border: 1px solid #cccccc; 370 | border: 1px solid rgba(0, 0, 0, 0.15); 371 | -webkit-border-radius: 4px; 372 | -moz-border-radius: 4px; 373 | border-radius: 4px; 374 | -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); 375 | box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175); 376 | } 377 | 378 | .selectize-dropdown .optgroup-header { 379 | font-size: 12px; 380 | line-height: 1.428571429; 381 | } 382 | 383 | .selectize-dropdown .optgroup:first-child:before { 384 | display: none; 385 | } 386 | 387 | .selectize-dropdown .optgroup:before { 388 | display: block; 389 | height: 1px; 390 | margin: 9px 0; 391 | margin-right: -12px; 392 | margin-left: -12px; 393 | overflow: hidden; 394 | background-color: #e5e5e5; 395 | content: ' '; 396 | } 397 | 398 | .selectize-dropdown-content { 399 | padding: 5px 0; 400 | } 401 | 402 | .selectize-dropdown-header { 403 | padding: 6px 12px; 404 | } 405 | 406 | .selectize-input { 407 | min-height: 34px; 408 | } 409 | 410 | .selectize-input.dropdown-active { 411 | -webkit-border-radius: 4px; 412 | -moz-border-radius: 4px; 413 | border-radius: 4px; 414 | } 415 | 416 | .selectize-input.dropdown-active::before { 417 | display: none; 418 | } 419 | 420 | .selectize-input.focus { 421 | border-color: #66afe9; 422 | outline: 0; 423 | -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6); 424 | box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 8px rgba(102, 175, 233, 0.6); 425 | } 426 | 427 | .selectize-control.multi .selectize-input.has-items { 428 | padding-right: 9px; 429 | padding-left: 9px; 430 | } 431 | 432 | .selectize-control.multi .selectize-input > div { 433 | -webkit-border-radius: 3px; 434 | -moz-border-radius: 3px; 435 | border-radius: 3px; 436 | } 437 | 438 | .form-control.selectize-control { 439 | height: auto; 440 | padding: 0; 441 | background: none; 442 | border: none; 443 | -webkit-border-radius: 0; 444 | -moz-border-radius: 0; 445 | border-radius: 0; 446 | -webkit-box-shadow: none; 447 | box-shadow: none; 448 | } -------------------------------------------------------------------------------- /app/fontello/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Font license info 2 | 3 | 4 | ## Elusive 5 | 6 | Copyright (C) 2013 by Aristeides Stathopoulos 7 | 8 | Author: Aristeides Stathopoulos 9 | License: SIL (http://scripts.sil.org/OFL) 10 | Homepage: http://aristeides.com/ 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/fontello/README.txt: -------------------------------------------------------------------------------- 1 | This webfont is generated by http://fontello.com open source project. 2 | 3 | 4 | ================================================================================ 5 | Please, note, that you should obey original font licences, used to make this 6 | webfont pack. Details available in LICENSE.txt file. 7 | 8 | - Usually, it's enough to publish content of LICENSE.txt file somewhere on your 9 | site in "About" section. 10 | 11 | - If your project is open-source, usually, it will be ok to make LICENSE.txt 12 | file publically available in your repository. 13 | 14 | - Fonts, used in Fontello, don't require a clickable link on your site. 15 | But any kind of additional authors crediting is welcome. 16 | ================================================================================ 17 | 18 | 19 | Comments on archive content 20 | --------------------------- 21 | 22 | - /font/* - fonts in different formats 23 | 24 | - /css/* - different kinds of css, for all situations. Should be ok with 25 | twitter bootstrap. Also, you can skip style and assign icon classes 26 | directly to text elements, if you don't mind about IE7. 27 | 28 | - demo.html - demo file, to show your webfont content 29 | 30 | - LICENSE.txt - license info about source fonts, used to build your one. 31 | 32 | - config.json - keeps your settings. You can import it back into fontello 33 | anytime, to continue your work 34 | 35 | 36 | Why so many CSS files ? 37 | ----------------------- 38 | 39 | Because we like to fit all your needs :) 40 | 41 | - basic file, .css - is usually enough, it contains @font-face 42 | and character code definitions 43 | 44 | - *-ie7.css - if you need IE7 support, but still don't wish to put char codes 45 | directly into html 46 | 47 | - *-codes.css and *-ie7-codes.css - if you like to use your own @font-face 48 | rules, but still wish to benefit from css generation. That can be very 49 | convenient for automated asset build systems. When you need to update font - 50 | no need to manually edit files, just override old version with archive 51 | content. See fontello source code for examples. 52 | 53 | - *-embedded.css - basic css file, but with embedded WOFF font, to avoid 54 | CORS issues in Firefox and IE9+, when fonts are hosted on the separate domain. 55 | We strongly recommend to resolve this issue by `Access-Control-Allow-Origin` 56 | server headers. But if you ok with dirty hack - this file is for you. Note, 57 | that data url moved to separate @font-face to avoid problems with 2 | 3 | 4 | 278 | 279 | 291 | 292 | 293 |
294 |

295 | fontello 296 | font demo 297 |

298 | 301 |
302 |
303 |
304 |
icon-play0xe800
305 |
icon-volume0xe801
306 |
307 |
308 | 309 | 310 | -------------------------------------------------------------------------------- /app/fontello/font/fontello.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oaprograms/lingo-player/37732ef949ced3d575cf2f079fd5d4461b671fd5/app/fontello/font/fontello.eot -------------------------------------------------------------------------------- /app/fontello/font/fontello.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Copyright (C) 2015 by original authors @ fontello.com 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/fontello/font/fontello.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oaprograms/lingo-player/37732ef949ced3d575cf2f079fd5d4461b671fd5/app/fontello/font/fontello.ttf -------------------------------------------------------------------------------- /app/fontello/font/fontello.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oaprograms/lingo-player/37732ef949ced3d575cf2f079fd5d4461b671fd5/app/fontello/font/fontello.woff -------------------------------------------------------------------------------- /app/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oaprograms/lingo-player/37732ef949ced3d575cf2f079fd5d4461b671fd5/app/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /app/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oaprograms/lingo-player/37732ef949ced3d575cf2f079fd5d4461b671fd5/app/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /app/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oaprograms/lingo-player/37732ef949ced3d575cf2f079fd5d4461b671fd5/app/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /app/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oaprograms/lingo-player/37732ef949ced3d575cf2f079fd5d4461b671fd5/app/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /app/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oaprograms/lingo-player/37732ef949ced3d575cf2f079fd5d4461b671fd5/app/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /app/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oaprograms/lingo-player/37732ef949ced3d575cf2f079fd5d4461b671fd5/app/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /app/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oaprograms/lingo-player/37732ef949ced3d575cf2f079fd5d4461b671fd5/app/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /app/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oaprograms/lingo-player/37732ef949ced3d575cf2f079fd5d4461b671fd5/app/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /app/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oaprograms/lingo-player/37732ef949ced3d575cf2f079fd5d4461b671fd5/app/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /app/img/googletranslate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oaprograms/lingo-player/37732ef949ced3d575cf2f079fd5d4461b671fd5/app/img/googletranslate.png -------------------------------------------------------------------------------- /app/img/logo-sm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oaprograms/lingo-player/37732ef949ced3d575cf2f079fd5d4461b671fd5/app/img/logo-sm.png -------------------------------------------------------------------------------- /app/img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oaprograms/lingo-player/37732ef949ced3d575cf2f079fd5d4461b671fd5/app/img/logo.png -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Lingo Player 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
Loading...
35 |
36 | 37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | 45 | 46 | 47 |
48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 |
57 | Saved Words 58 |
59 | 60 | 61 |
62 | 63 |
64 | 65 | 66 | 67 | 68 | 69 | 70 |
71 | 72 | 73 |
74 |
75 | 76 |
77 | 78 | 80 | 83 | 84 | 85 | 86 | 87 | 88 |
89 | 90 | 91 |
92 | {{ word.text }}{{ word.text }} 98 |
99 | 100 |
{{ word.text }}{{ word.text }} 106 |
107 |
108 |
109 | 110 |
111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | -------------------------------------------------------------------------------- /app/js/node/database.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Ognjen on 6/27/15. 3 | */ 4 | 5 | exports.openDatabase = function (gui) { 6 | var path = require('path'); 7 | var Datastore = require('nedb'); 8 | var db = new Datastore({ 9 | filename:path.join(gui.App.dataPath, 'lingo_player_words.nedb'), 10 | 11 | autoload: true 12 | }); 13 | db.ensureIndex({ fieldName: 'text'}); 14 | db.ensureIndex({ fieldName: 'lang1'}); 15 | db.ensureIndex({ fieldName: 'lang2'}); 16 | db.ensureIndex({ fieldName: 'level'}); 17 | db.ensureIndex({ fieldName: 'frequency'}); 18 | return db; 19 | }; 20 | 21 | exports.saveWord = function (db, word, lang1, lang2, translation, level, frequency, callback) { 22 | db.update({ 23 | text: word, 24 | lang1: lang1, 25 | lang2: lang2 26 | }, { 27 | text: word, 28 | lang1: lang1, 29 | lang2: lang2, 30 | translation: translation, 31 | level: level, 32 | frequency:frequency, 33 | date: Date.now() 34 | }, { upsert: true }, 35 | function (err, numReplaced, upsert) { 36 | callback(err, numReplaced, upsert); 37 | }); 38 | }; 39 | 40 | exports.removeWord = function (db, word, lang1, lang2, callback) { 41 | db.remove({ 42 | text: word, 43 | lang1: lang1, 44 | lang2: lang2 45 | }, function (err) { 46 | callback(err); 47 | }); 48 | }; 49 | 50 | exports.getWords = function (db, lang1, lang2, limit, sortBy, levels, callback) { 51 | var sortOptions = { 52 | 'Frequency': {frequency: -1}, 53 | 'Recent': {date: -1}, 54 | 'Alphabetical': {text: 1} 55 | }; 56 | var levelsList = []; 57 | for (var level in levels){ 58 | if(levels.hasOwnProperty(level)){ 59 | if(levels[level]) levelsList.push(parseInt(level)); 60 | } 61 | } 62 | 63 | var findBy = {lang1: lang1, lang2: lang2, level: {$in: levelsList}}; 64 | 65 | db.find(findBy).sort(sortOptions[sortBy]).limit(limit).exec(function (err, docs) { 66 | callback(docs); 67 | }); 68 | 69 | }; 70 | 71 | exports.getCount = function (db, lang1, lang2, level, callback) { 72 | 73 | db.count({level: level, lang1: lang1, lang2: lang2}).exec(function (err, cnt) { 74 | callback(cnt); 75 | }); 76 | }; 77 | 78 | exports.getWord = function (db, word, lang1, lang2, callback) { 79 | db.find({text: word, lang1: lang1, lang2: lang2}).exec(function (err, docs) { 80 | if (docs.length){ 81 | callback(docs[0]); 82 | } else { 83 | callback(null); 84 | } 85 | }); 86 | }; 87 | 88 | exports.getAll = function (db, callback) { 89 | db.find({}).sort({date: -1}).exec(function (err, docs) { 90 | callback(docs); 91 | }); 92 | }; 93 | 94 | exports.uniqueLanguagePairs = function(db, callback){ 95 | // todo: optimize 96 | var langPairs = {}; 97 | exports.getAll(db, function(docs){ 98 | for (var i in docs){ 99 | if(docs.hasOwnProperty(i)){ 100 | langPairs[docs[i].lang1 + '|' + docs[i].lang2] = { 101 | lang1 : docs[i].lang1, 102 | lang2: docs[i].lang2 103 | }; 104 | } 105 | } 106 | var ret = []; 107 | for (var i in langPairs){ 108 | ret.push(langPairs[i]); 109 | } 110 | callback(ret); 111 | }); 112 | }; -------------------------------------------------------------------------------- /app/js/node/downloader.js: -------------------------------------------------------------------------------- 1 | var http = require('http'); 2 | var fs = require('fs'); 3 | 4 | exports.download = function(url, dest, cb) { 5 | // check if file exists 6 | fs.stat(dest, function(err, stat) { 7 | if(err == null) { 8 | // file exists already, just return 9 | cb(); 10 | } else { 11 | var file = fs.createWriteStream(dest); 12 | var request = http.get(url, function(response) { 13 | response.pipe(file); 14 | file.on('finish', function() { 15 | file.close(cb); // close() is async, call cb after close completes. 16 | }); 17 | }).on('error', function(err) { // Handle errors 18 | fs.unlink(dest); // Delete the file async. (But we don't check the result) 19 | if (cb) cb(err.message); 20 | }); 21 | } 22 | }); 23 | }; 24 | 25 | var compactSubtitleUrls = function(urls, limit){ 26 | var final_urls = []; 27 | for (var u in urls) { 28 | if (final_urls.length >= limit) break; 29 | var url = urls[u]; 30 | if (final_urls.indexOf(url) == -1) { 31 | final_urls.push(url); 32 | } 33 | } 34 | return final_urls; 35 | }; 36 | 37 | exports.searchSubsExtendByImdbID = function(language, moviePath, extensions, limit, cb){ // todo: more error handling 38 | var OS = require('opensubtitles-api'); 39 | var os = new OS('Lingo Player'); 40 | os.api.LogIn('','','en','Lingo Player').then(function(result){ 41 | var token = result.token; 42 | // calculate movie hash 43 | os.extractInfo(moviePath).then(function(info){ 44 | var moviebytesize = info.moviebytesize; 45 | var moviehash = info.moviehash; 46 | // get movie imdb id (for more results) 47 | os.api.CheckMovieHash(token, [moviehash]).then(function(info2){ 48 | var imdbid; 49 | if(info2.data && info2.data[moviehash] && info2.data[moviehash].MovieImdbID) { 50 | imdbid = info2.data[moviehash].MovieImdbID; 51 | } 52 | // search by both hash and imdbid 53 | os.api.SearchSubtitles(token, [{ 54 | moviehash: moviehash, 55 | moviebytesize: moviebytesize, 56 | sublanguageid: language 57 | },{ 58 | sublanguageid: language, 59 | imdbid: imdbid 60 | }], {limit: 20}).then(function(results){ 61 | if(results.data && results.data.length){ 62 | var ret = []; 63 | for (var i in results.data){ 64 | var sub = results.data[i]; 65 | // if the format is supported 66 | if(extensions.indexOf(sub.SubFormat.toLowerCase()) > -1){ 67 | // add the link 68 | ret.push(sub.SubDownloadLink.replace('.gz', '.'+sub.SubFormat)); 69 | } 70 | } 71 | cb(compactSubtitleUrls(ret, limit)); 72 | } else { 73 | cb([]); 74 | } 75 | }); 76 | }); 77 | }); 78 | }); 79 | }; -------------------------------------------------------------------------------- /app/js/node/encodings.js: -------------------------------------------------------------------------------- 1 | var encodings = { 2 | 'af': ['windows-1252','iso-8859-1'], 3 | 'sq': ['windows-1252','iso-8859-1'], 4 | 'ar': ['iso-8859-6'], 5 | 'eu': ['windows-1252','iso-8859-1'], 6 | 'bg': ['iso-8859-5'], 7 | 'be': ['iso-8859-5'], 8 | 'ca': ['windows-1252','iso-8859-1'], 9 | 'hr': ['windows-1250','iso-8859-2'], 10 | 'cs': ['iso-8859-2'], 11 | 'da': ['windows-1252','iso-8859-1'], 12 | 'nl': ['windows-1252','iso-8859-1'], 13 | 'en': ['windows-1252','iso-8859-1'], 14 | 'eo': ['iso-8859-3'], 15 | 'et': ['iso-8859-15'], 16 | 'fo': ['windows-1252','iso-8859-1'], 17 | 'fi': ['windows-1252','iso-8859-1'], 18 | 'fr': ['windows-1252','iso-8859-1'], 19 | 'gl': ['windows-1252','iso-8859-1'], 20 | 'de': ['windows-1252','iso-8859-1'], 21 | 'el': ['iso-8859-7'], 22 | 'iw': ['iso-8859-8'], 23 | 'hu': ['iso-8859-2'], 24 | 'is': ['windows-1252','iso-8859-1'], 25 | 'ga': ['windows-1252','iso-8859-1'], 26 | 'it': ['windows-1252','iso-8859-1'], 27 | 'ja': ['euc-jp','iso-2022-jp','shift_jis'], 28 | 'ko': ['euc-kr'], 29 | 'lv': ['windows-1257','iso-8859-13'], 30 | 'lt': ['windows-1257','iso-8859-13'], 31 | 'mk': ['windows-1251','iso-8859-5'], 32 | 'mt': ['iso-8859-3'], 33 | 'no': ['windows-1252','iso-8859-1'], 34 | 'pl': ['iso-8859-2'], 35 | 'pt': ['windows-1252','iso-8859-1'], 36 | 'ro': ['iso-8859-2'], 37 | 'ru': ['iso-8859-5','koi8-r'], 38 | 'gd': ['windows-1252','iso-8859-1'], 39 | 'sr': ['windows-1250','windows-1251','iso-8859-2','iso-8859-5'], 40 | 'sk': ['iso-8859-2'], 41 | 'sl': ['windows-1250','iso-8859-2'], 42 | 'es': ['windows-1252','iso-8859-1'], 43 | 'sv': ['windows-1252','iso-8859-1'], 44 | 'tr': ['windows-1254','iso-8859-9'], 45 | 'uk': ['iso-8859-5'] 46 | }; 47 | 48 | exports.getEncodings = function(langCode){ 49 | return (encodings[langCode] || []).concat(['utf-8']); 50 | }; -------------------------------------------------------------------------------- /app/js/node/parse.js: -------------------------------------------------------------------------------- 1 | srt = require('./parsers/popcorn.parserSRT.js'); 2 | sub = require('./parsers/parserSUB.js'); 3 | exports.parseSubtitles = function(data, extension){ 4 | if (extension == 'srt'){ 5 | return srt.parseSRT(data); 6 | } else if (extension == 'sub' || extension == 'txt'){ 7 | return sub.parseSUB(data); 8 | } else { 9 | return {}; 10 | } 11 | 12 | }; -------------------------------------------------------------------------------- /app/js/node/parsers/parserSUB.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Ognjen on 10/19/15. 3 | */ 4 | 5 | exports.parseSUB = function( data, frameRate ) { 6 | if(! frameRate) frameRate = 23.976; 7 | var ret = { 8 | data: [] 9 | }; 10 | var srt = data.replace(/\r\n|\r|\n/g, '\n'); 11 | srt = srt.replace(/^\s+|\s+$/g,""); 12 | var srty = srt.split('\n'); 13 | 14 | for (var s = 0; s < srty.length; s++) { 15 | var st = srty[s].split('}{'); 16 | if (st.length >=2) { 17 | try { 18 | var start = Math.round(st[0].substr(1)) / frameRate; 19 | var end = Math.round(st[1].split('}')[0]) / frameRate; 20 | var text = st[1].split('}')[1].replace('|', '\n'); 21 | ret.data.push({ 22 | subtitle:{ 23 | start: start, 24 | end: end, 25 | text: text 26 | } 27 | }); 28 | } catch (e){ 29 | console.log('invalid sub row: ', srty[s]) 30 | } 31 | } 32 | } 33 | return ret; 34 | }; -------------------------------------------------------------------------------- /app/js/node/parsers/popcorn.parserSBV.js: -------------------------------------------------------------------------------- 1 | // PARSER: 0.1 SBV 2 | 3 | (function (Popcorn) { 4 | 5 | /** 6 | * SBV popcorn parser plug-in 7 | * Parses subtitle files in the SBV format. 8 | * Times are expected in H:MM:SS.MIL format, with hours optional 9 | * Subtitles which don't match expected format are ignored 10 | * Data parameter is given by Popcorn, will need a text. 11 | * Text is the file contents to be parsed 12 | * 13 | * @param {Object} data 14 | * 15 | * Example: 16 | 0:00:02.400,0:00:07.200 17 | Senator, we're making our final approach into Coruscant. 18 | */ 19 | Popcorn.parser( "parseSBV", function( data ) { 20 | 21 | // declare needed variables 22 | var retObj = { 23 | title: "", 24 | remote: "", 25 | data: [] 26 | }, 27 | subs = [], 28 | lines, 29 | i = 0, 30 | len = 0, 31 | idx = 0; 32 | 33 | // [H:]MM:SS.MIL string to SS.MIL 34 | // Will thrown exception on bad time format 35 | var toSeconds = function( t_in ) { 36 | var t = t_in.split( ":" ), 37 | l = t.length-1, 38 | time; 39 | 40 | try { 41 | time = parseInt( t[l-1], 10 )*60 + parseFloat( t[l], 10 ); 42 | 43 | // Hours optionally given 44 | if ( l === 2 ) { 45 | time += parseInt( t[0], 10 )*3600; 46 | } 47 | } catch ( e ) { 48 | throw "Bad cue"; 49 | } 50 | 51 | return time; 52 | }; 53 | 54 | var createTrack = function( name, attributes ) { 55 | var track = {}; 56 | track[name] = attributes; 57 | return track; 58 | }; 59 | 60 | // Here is where the magic happens 61 | // Split on line breaks 62 | lines = data.text.split( /(?:\r\n|\r|\n)/gm ); 63 | len = lines.length; 64 | 65 | while ( i < len ) { 66 | var sub = {}, 67 | text = [], 68 | time = lines[i++].split( "," ); 69 | 70 | try { 71 | sub.start = toSeconds( time[0] ); 72 | sub.end = toSeconds( time[1] ); 73 | 74 | // Gather all lines of text 75 | while ( i < len && lines[i] ) { 76 | text.push( lines[i++] ); 77 | } 78 | 79 | // Join line breaks in text 80 | sub.text = text.join( "
" ); 81 | subs.push( createTrack( "subtitle", sub ) ); 82 | } catch ( e ) { 83 | // Bad cue, advance to end of cue 84 | while ( i < len && lines[i] ) { 85 | i++; 86 | } 87 | } 88 | 89 | // Consume empty whitespace 90 | while ( i < len && !lines[i] ) { 91 | i++; 92 | } 93 | } 94 | 95 | retObj.data = subs; 96 | 97 | return retObj; 98 | }); 99 | 100 | })( Popcorn ); 101 | -------------------------------------------------------------------------------- /app/js/node/parsers/popcorn.parserSRT.js: -------------------------------------------------------------------------------- 1 | // PARSER: 0.3 SRT 2 | /** 3 | * SRT popcorn parser plug-in 4 | * Parses subtitle files in the SRT format. 5 | * Times are expected in HH:MM:SS,MIL format, though HH:MM:SS.MIL also supported 6 | * Ignore styling, which may occur after the end time or in-text 7 | * While not part of the "official" spec, majority of players support HTML and SSA styling tags 8 | * SSA-style tags are stripped, HTML style tags are left for the browser to handle: 9 | * HTML: , , , , 10 | * SSA: \N or \n, {\cmdArg1}, {\cmd(arg1, arg2, ...)} 11 | 12 | * Data parameter is given by Popcorn, will need a text. 13 | * Text is the file contents to be parsed 14 | * 15 | * @param {Object} data 16 | * 17 | * Example: 18 | 1 19 | 00:00:25,712 --> 00:00:30.399 20 | This text is RED and has not been {\pos(142,120)} positioned. 21 | This takes \Nup three \nentire lines. 22 | This contains nested bold, italic, underline and strike-through HTML tags 23 | Unclosed but supported tags are left in 24 | Unsupported HTML tags are left in, even if not closed. 25 | SSA tags with {\i1} would open and close italicize {\i0}, but are stripped 26 | Multiple {\pos(142,120)\b1}SSA tags are stripped 27 | */ 28 | exports.parseSRT = function( data, options ) { 29 | 30 | // declare needed variables 31 | var retObj = { 32 | title: "", 33 | remote: "", 34 | data: [] 35 | }, 36 | subs = [], 37 | i = 0, 38 | idx = 0, 39 | lines, 40 | time, 41 | text, 42 | endIdx, 43 | sub; 44 | 45 | // Here is where the magic happens 46 | // Split on line breaks 47 | lines = data.split( /(?:\r\n|\r|\n)/gm ); 48 | endIdx = lastNonEmptyLine( lines ) + 1; 49 | 50 | for( i=0; i < endIdx; i++ ) { 51 | try { 52 | sub = {}; 53 | text = []; 54 | 55 | i = nextNonEmptyLine(lines, i); 56 | sub.id = parseInt(lines[i++], 10); 57 | 58 | // Split on '-->' delimiter, trimming spaces as well 59 | time = lines[i++].split(/[\t ]*-->[\t ]*/); 60 | 61 | sub.start = toSeconds(time[0]); 62 | 63 | // So as to trim positioning information from end 64 | idx = time[1].indexOf(" "); 65 | if (idx !== -1) { 66 | time[1] = time[1].substr(0, idx); 67 | } 68 | sub.end = toSeconds(time[1]); 69 | 70 | // Build single line of text from multi-line subtitle in file 71 | while (i < endIdx && lines[i]) { 72 | text.push(lines[i++]); 73 | } 74 | 75 | // Join into 1 line, SSA-style linebreaks 76 | // Strip out other SSA-style tags 77 | sub.text = text.join("\\N").replace(/\{(\\[\w]+\(?([\w\d]+,?)+\)?)+\}/gi, ""); 78 | 79 | // Escape HTML entities 80 | sub.text = sub.text.replace(//g, ">"); 81 | 82 | // Unescape great than and less than when it makes a valid html tag of a supported style (font, b, u, s, i) 83 | // Modified version of regex from Phil Haack's blog: http://haacked.com/archive/2004/10/25/usingregularexpressionstomatchhtml.aspx 84 | // Later modified by kev: http://kevin.deldycke.com/2007/03/ultimate-regular-expression-for-html-tag-parsing-with-php/ 85 | //sub.text = sub.text.replace( /<(\/?(font|b|u|i|s))((\s+(\w|\w[\w\-]*\w)(\s*=\s*(?:\".*?\"|'.*?'|[^'\">\s]+))?)+\s*|\s*)(\/?)>/gi, "<$1$3$7>" ); 86 | //sub.text = sub.text.replace( /\\N/gi, "
" ); 87 | 88 | 89 | // actually, strip all html tags (ognjen's mod) 90 | sub.text = sub.text.replace(/<(\/?(font|b|u|i|s))((\s+(\w|\w[\w\-]*\w)(\s*=\s*(?:\".*?\"|'.*?'|[^'\">\s]+))?)+\s*|\s*)(\/?)>/gi, ""); 91 | sub.text = sub.text.replace(/\\N/gi, " "); 92 | if (options && options["target"]) { 93 | sub.target = options["target"]; 94 | } 95 | 96 | subs.push(createTrack("subtitle", sub)); 97 | } catch (e){ 98 | console.log('invalid sub row: ', i) 99 | } 100 | } 101 | 102 | retObj.data = subs; 103 | return retObj; 104 | 105 | function createTrack( name, attributes ) { 106 | var track = {}; 107 | track[name] = attributes; 108 | return track; 109 | } 110 | 111 | // Simple function to convert HH:MM:SS,MMM or HH:MM:SS.MMM to SS.MMM 112 | // Assume valid, returns 0 on error 113 | function toSeconds( t_in ) { 114 | var t = t_in.split( ':' ); 115 | 116 | try { 117 | var s = t[2].split( ',' ); 118 | 119 | // Just in case a . is decimal seperator 120 | if ( s.length === 1 ) { 121 | s = t[2].split( '.' ); 122 | } 123 | 124 | return parseFloat( t[0], 10 ) * 3600 + parseFloat( t[1], 10 ) * 60 + parseFloat( s[0], 10 ) + parseFloat( s[1], 10 ) / 1000; 125 | } catch ( e ) { 126 | return 0; 127 | } 128 | } 129 | 130 | function nextNonEmptyLine( linesArray, position ) { 131 | var idx = position; 132 | while ( !linesArray[idx] ) { 133 | idx++; 134 | } 135 | return idx; 136 | } 137 | 138 | function lastNonEmptyLine( linesArray ) { 139 | var idx = linesArray.length - 1; 140 | 141 | while ( idx >= 0 && !linesArray[idx] ) { 142 | idx--; 143 | } 144 | 145 | return idx; 146 | } 147 | }; 148 | 149 | 150 | -------------------------------------------------------------------------------- /app/js/node/parsers/popcorn.parserSSA.js: -------------------------------------------------------------------------------- 1 | // PARSER: 0.3 SSA/ASS 2 | 3 | (function ( Popcorn ) { 4 | /** 5 | * SSA/ASS popcorn parser plug-in 6 | * Parses subtitle files in the identical SSA and ASS formats. 7 | * Style information is ignored, and may be found in these 8 | * formats: (\N \n {\pos(400,570)} {\kf89}) 9 | * Out of the [Script Info], [V4 Styles], [Events], [Pictures], 10 | * and [Fonts] sections, only [Events] is processed. 11 | * Data parameter is given by Popcorn, will need a text. 12 | * Text is the file contents to be parsed 13 | * 14 | * @param {Object} data 15 | * 16 | * Example: 17 | [Script Info] 18 | Title: Testing subtitles for the SSA Format 19 | [V4 Styles] 20 | Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, TertiaryColour, BackColour, Bold, Italic, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, AlphaLevel, Encoding 21 | Style: Default,Arial,20,65535,65535,65535,-2147483640,-1,0,1,3,0,2,30,30,30,0,0 22 | [Events] 23 | Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text 24 | Dialogue: 0,0:00:02.40,0:00:07.20,Default,,0000,0000,0000,,Senator, {\kf89}we're \Nmaking our final \napproach into Coruscant. 25 | Dialogue: 0,0:00:09.71,0:00:13.39,Default,,0000,0000,0000,,{\pos(400,570)}Very good, Lieutenant. 26 | Dialogue: 0,0:00:15.04,0:00:18.04,Default,,0000,0000,0000,,It's \Na \ntrap! 27 | * 28 | */ 29 | 30 | // Register for SSA extensions 31 | Popcorn.parser( "parseSSA", function( data ) { 32 | // declare needed variables 33 | var retObj = { 34 | title: "", 35 | remote: "", 36 | data: [ ] 37 | }, 38 | rNewLineFile = /(?:\r\n|\r|\n)/gm, 39 | subs = [ ], 40 | lines, 41 | headers, 42 | i = 0, 43 | len; 44 | 45 | // Here is where the magic happens 46 | // Split on line breaks 47 | lines = data.text.split( rNewLineFile ); 48 | len = lines.length; 49 | 50 | // Ignore non-textual info 51 | while ( i < len && lines[ i ] !== "[Events]" ) { 52 | i++; 53 | } 54 | 55 | headers = parseFieldHeaders( lines[ ++i ] ); 56 | 57 | while ( ++i < len && lines[ i ] && lines[ i ][ 0 ] !== "[" ) { 58 | try { 59 | subs.push( createTrack( "subtitle", parseSub( lines[ i ], headers ) ) ); 60 | } catch ( e ) {} 61 | } 62 | 63 | retObj.data = subs; 64 | return retObj; 65 | }); 66 | 67 | function parseSub( line, headers ) { 68 | // Trim beginning 'Dialogue: ' and split on delim 69 | var fields = line.substr( 10 ).split( "," ), 70 | rAdvancedStyles = /\{(\\[\w]+\(?([\w\d]+,?)+\)?)+\}/gi, 71 | rNewLineSSA = /\\N/gi, 72 | sub; 73 | 74 | sub = { 75 | start: toSeconds( fields[ headers.start ] ), 76 | end: toSeconds( fields[ headers.end ] ) 77 | }; 78 | 79 | // Invalid time, skip 80 | if ( sub.start === -1 || sub.end === -1 ) { 81 | throw "Invalid time"; 82 | } 83 | 84 | // Eliminate advanced styles and convert forced line breaks 85 | sub.text = getTextFromFields( fields, headers.text ).replace( rAdvancedStyles, "" ).replace( rNewLineSSA, "
" ); 86 | 87 | return sub; 88 | } 89 | 90 | // h:mm:ss.cc (centisec) string to SS.mmm 91 | // Returns -1 if invalid 92 | function toSeconds( t_in ) { 93 | var t = t_in.split( ":" ); 94 | 95 | // Not all there 96 | if ( t_in.length !== 10 || t.length < 3 ) { 97 | return -1; 98 | } 99 | 100 | return parseInt( t[ 0 ], 10 ) * 3600 + parseInt( t[ 1 ], 10 ) * 60 + parseFloat( t[ 2 ], 10 ); 101 | } 102 | 103 | function getTextFromFields( fields, startIdx ) { 104 | var fieldLen = fields.length, 105 | text = [ ], 106 | i = startIdx; 107 | 108 | // There may be commas in the text which were split, append back together into one line 109 | for( ; i < fieldLen; i++ ) { 110 | text.push( fields[ i ] ); 111 | } 112 | 113 | return text.join( "," ); 114 | } 115 | 116 | function createTrack( name, attributes ) { 117 | var track = {}; 118 | track[ name ] = attributes; 119 | return track; 120 | } 121 | 122 | function parseFieldHeaders( line ) { 123 | // Trim 'Format: ' off front, split on delim 124 | var fields = line.substr( 8 ).split( ", " ), 125 | result = {}, 126 | len, 127 | i; 128 | 129 | //Find where in Dialogue string the start, end and text info is 130 | for ( i = 0, len = fields.length; i < len; i++ ) { 131 | if ( fields[ i ] === "Start" ) { 132 | result.start = i; 133 | } else if ( fields[ i ] === "End" ) { 134 | result.end = i; 135 | } else if ( fields[ i ] === "Text" ) { 136 | result.text = i; 137 | } 138 | } 139 | 140 | return result; 141 | } 142 | })( Popcorn ); 143 | -------------------------------------------------------------------------------- /app/js/node/parsers/popcorn.parserTTML.js: -------------------------------------------------------------------------------- 1 | // PARSER: 1.0 TTML 2 | (function ( Popcorn ) { 3 | /** 4 | * TTML popcorn parser plug-in 5 | * Parses subtitle files in the TTML format. 6 | * Times may be absolute to the timeline or relative 7 | * Absolute times are ISO 8601 format (hh:mm:ss[.mmm]) 8 | * Relative times are a fraction followed by a unit metric (d.ddu) 9 | * Relative times are relative to the time given on the parent node 10 | * Styling information is ignored 11 | * Data parameter is given by Popcorn, will need an xml. 12 | * Xml is the file contents to be processed 13 | * 14 | * @param {Object} data 15 | * 16 | * Example: 17 | 18 | 19 |
20 |

21 | It seems a paradox, does it not, 22 |

23 |
24 | 25 |
26 | */ 27 | 28 | var rWhitespace = /^[\s]+|[\s]+$/gm, 29 | rLineBreak = /(?:\r\n|\r|\n)/gm; 30 | 31 | Popcorn.parser( "parseTTML", function( data ) { 32 | var returnData = { 33 | title: "", 34 | remote: "", 35 | data: [] 36 | }, 37 | node; 38 | 39 | // Null checks 40 | if ( !data.xml || !data.xml.documentElement ) { 41 | return returnData; 42 | } 43 | 44 | node = data.xml.documentElement.firstChild; 45 | 46 | if ( !node ) { 47 | return returnData; 48 | } 49 | 50 | // Find body tag 51 | while ( node.nodeName !== "body" ) { 52 | node = node.nextSibling; 53 | } 54 | 55 | if ( node ) { 56 | returnData.data = parseChildren( node, 0 ); 57 | } 58 | 59 | return returnData; 60 | }); 61 | 62 | // Parse the children of the given node 63 | function parseChildren( node, timeOffset, region ) { 64 | var currNode = node.firstChild, 65 | currRegion = getNodeRegion( node, region ), 66 | retVal = [], 67 | newOffset; 68 | 69 | while ( currNode ) { 70 | if ( currNode.nodeType === 1 ) { 71 | if ( currNode.nodeName === "p" ) { 72 | // p is a textual node, process contents as subtitle 73 | retVal.push( parseNode( currNode, timeOffset, currRegion ) ); 74 | } else if ( currNode.nodeName === "div" ) { 75 | // div is container for subtitles, recurse 76 | newOffset = toSeconds( currNode.getAttribute( "begin" ) ); 77 | 78 | if (newOffset < 0 ) { 79 | newOffset = timeOffset; 80 | } 81 | 82 | retVal.push.apply( retVal, parseChildren( currNode, newOffset, currRegion ) ); 83 | } 84 | } 85 | 86 | currNode = currNode.nextSibling; 87 | } 88 | 89 | return retVal; 90 | } 91 | 92 | // Get the "region" attribute of a node, to know where to put the subtitles 93 | function getNodeRegion( node, defaultTo ) { 94 | var region = node.getAttribute( "region" ); 95 | 96 | if ( region !== null ) { 97 | return region; 98 | } else { 99 | return defaultTo || ""; 100 | } 101 | } 102 | 103 | // Parse a node for text content 104 | function parseNode( node, timeOffset, region ) { 105 | var sub = {}; 106 | 107 | // Trim left and right whitespace from text and convert non-explicit line breaks 108 | sub.text = ( node.textContent || node.text ).replace( rWhitespace, "" ).replace( rLineBreak, "
" ); 109 | sub.id = node.getAttribute( "xml:id" ) || node.getAttribute( "id" ); 110 | sub.start = toSeconds ( node.getAttribute( "begin" ), timeOffset ); 111 | sub.end = toSeconds( node.getAttribute( "end" ), timeOffset ); 112 | sub.target = getNodeRegion( node, region ); 113 | 114 | if ( sub.end < 0 ) { 115 | // No end given, infer duration if possible 116 | // Otherwise, give end as MAX_VALUE 117 | sub.end = toSeconds( node.getAttribute( "duration" ), 0 ); 118 | 119 | if ( sub.end >= 0 ) { 120 | sub.end += sub.start; 121 | } else { 122 | sub.end = Number.MAX_VALUE; 123 | } 124 | } 125 | 126 | return { subtitle : sub }; 127 | } 128 | 129 | // Convert time expression to SS.mmm 130 | // Expression may be absolute to timeline (hh:mm:ss.ms) 131 | // or relative ( decimal followed by metric ) ex: 3.4s, 5.7m 132 | // Returns -1 if invalid 133 | function toSeconds( t_in, offset ) { 134 | var i; 135 | 136 | if ( !t_in ) { 137 | return -1; 138 | } 139 | 140 | try { 141 | return Popcorn.util.toSeconds( t_in ); 142 | } catch ( e ) { 143 | i = getMetricIndex( t_in ); 144 | return parseFloat( t_in.substring( 0, i ) ) * getMultipler( t_in.substring( i ) ) + ( offset || 0 ); 145 | } 146 | } 147 | 148 | // In a time string such as 3.4ms, get the index of the first character (m) of the time metric (ms) 149 | function getMetricIndex( t_in ) { 150 | var i = t_in.length - 1; 151 | 152 | while ( i >= 0 && t_in[ i ] <= "9" && t_in[ i ] >= "0" ) { 153 | i--; 154 | } 155 | 156 | return i; 157 | } 158 | 159 | // Determine multiplier for metric relative to seconds 160 | function getMultipler( metric ) { 161 | return { 162 | "h" : 3600, 163 | "m" : 60, 164 | "s" : 1, 165 | "ms" : 0.001 166 | }[ metric ] || -1; 167 | } 168 | })( Popcorn ); 169 | -------------------------------------------------------------------------------- /app/js/node/parsers/popcorn.parserTTXT.js: -------------------------------------------------------------------------------- 1 | // PARSER: 0.1 TTXT 2 | 3 | (function (Popcorn) { 4 | 5 | /** 6 | * TTXT popcorn parser plug-in 7 | * Parses subtitle files in the TTXT format. 8 | * Style information is ignored. 9 | * Data parameter is given by Popcorn, will need an xml. 10 | * Xml is the file contents to be parsed as a DOM tree 11 | * 12 | * @param {Object} data 13 | * 14 | * Example: 15 | 16 | */ 17 | Popcorn.parser( "parseTTXT", function( data ) { 18 | 19 | // declare needed variables 20 | var returnData = { 21 | title: "", 22 | remote: "", 23 | data: [] 24 | }; 25 | 26 | // Simple function to convert HH:MM:SS.MMM to SS.MMM 27 | // Assume valid, returns 0 on error 28 | var toSeconds = function(t_in) { 29 | var t = t_in.split(":"); 30 | var time = 0; 31 | 32 | try { 33 | return parseFloat(t[0], 10)*60*60 + parseFloat(t[1], 10)*60 + parseFloat(t[2], 10); 34 | } catch (e) { time = 0; } 35 | 36 | return time; 37 | }; 38 | 39 | // creates an object of all atrributes keyed by name 40 | var createTrack = function( name, attributes ) { 41 | var track = {}; 42 | track[name] = attributes; 43 | return track; 44 | }; 45 | 46 | // this is where things actually start 47 | var node = data.xml.lastChild.lastChild; // Last Child of TextStreamHeader 48 | var lastStart = Number.MAX_VALUE; 49 | var cmds = []; 50 | 51 | // Work backwards through DOM, processing TextSample nodes 52 | while (node) { 53 | if ( node.nodeType === 1 && node.nodeName === "TextSample") { 54 | var sub = {}; 55 | sub.start = toSeconds(node.getAttribute('sampleTime')); 56 | sub.text = node.getAttribute('text'); 57 | 58 | if (sub.text) { // Only process if text to display 59 | // Infer end time from prior element, ms accuracy 60 | sub.end = lastStart - 0.001; 61 | cmds.push( createTrack("subtitle", sub) ); 62 | } 63 | lastStart = sub.start; 64 | } 65 | node = node.previousSibling; 66 | } 67 | 68 | returnData.data = cmds.reverse(); 69 | 70 | return returnData; 71 | }); 72 | 73 | })( Popcorn ); 74 | -------------------------------------------------------------------------------- /app/js/node/parsers/popcorn.parserVTT.js: -------------------------------------------------------------------------------- 1 | // PARSER: 0.3 WebSRT/VTT 2 | 3 | (function ( Popcorn ) { 4 | /** 5 | * WebVTT popcorn parser plug-in 6 | * Parses subtitle files in the WebVTT format. 7 | * Specification here: http://www.whatwg.org/specs/web-apps/current-work/webvtt.html 8 | * Styles which appear after timing information are presently ignored. 9 | * Inline styling tags follow HTML conventions and are left in for the browser to handle (or ignore if VTT-specific) 10 | * Data parameter is given by Popcorn, text property holds file contents. 11 | * Text is the file contents to be parsed 12 | * 13 | * @param {Object} data 14 | * 15 | * Example: 16 | 00:32.500 --> 00:00:33.500 A:start S:50% D:vertical L:98% 17 | Laughs 18 | */ 19 | Popcorn.parser( "parseVTT", function( data ) { 20 | 21 | // declare needed variables 22 | var retObj = { 23 | title: "", 24 | remote: "", 25 | data: [] 26 | }, 27 | subs = [], 28 | i = 0, 29 | len = 0, 30 | lines, 31 | text, 32 | sub, 33 | rNewLine = /(?:\r\n|\r|\n)/gm; 34 | 35 | // Here is where the magic happens 36 | // Split on line breaks 37 | lines = data.text.split( rNewLine ); 38 | len = lines.length; 39 | 40 | // Check for BOF token 41 | if ( len === 0 || lines[ 0 ] !== "WEBVTT" ) { 42 | return retObj; 43 | } 44 | 45 | i++; 46 | 47 | while ( i < len ) { 48 | text = []; 49 | 50 | try { 51 | i = skipWhitespace( lines, len, i ); 52 | sub = parseCueHeader( lines[ i++ ] ); 53 | 54 | // Build single line of text from multi-line subtitle in file 55 | while ( i < len && lines[ i ] ) { 56 | text.push( lines[ i++ ] ); 57 | } 58 | 59 | // Join lines together to one and build subtitle text 60 | sub.text = text.join( "
" ); 61 | subs.push( createTrack( "subtitle", sub ) ); 62 | } catch ( e ) { 63 | i = skipNonWhitespace( lines, len, i ); 64 | } 65 | } 66 | 67 | retObj.data = subs; 68 | return retObj; 69 | }); 70 | 71 | // [HH:]MM:SS.mmm string to SS.mmm float 72 | // Throws exception if invalid 73 | function toSeconds ( t_in ) { 74 | var t = t_in.split( ":" ), 75 | l = t_in.length, 76 | time; 77 | 78 | // Invalid time string provided 79 | if ( l !== 12 && l !== 9 ) { 80 | throw "Bad cue"; 81 | } 82 | 83 | l = t.length - 1; 84 | 85 | try { 86 | time = parseInt( t[ l-1 ], 10 ) * 60 + parseFloat( t[ l ], 10 ); 87 | 88 | // Hours were given 89 | if ( l === 2 ) { 90 | time += parseInt( t[ 0 ], 10 ) * 3600; 91 | } 92 | } catch ( e ) { 93 | throw "Bad cue"; 94 | } 95 | 96 | return time; 97 | } 98 | 99 | function createTrack( name, attributes ) { 100 | var track = {}; 101 | track[ name ] = attributes; 102 | return track; 103 | } 104 | 105 | function parseCueHeader ( line ) { 106 | var lineSegments, 107 | args, 108 | sub = {}, 109 | rToken = /-->/, 110 | rWhitespace = /[\t ]+/; 111 | 112 | if ( !line || line.indexOf( "-->" ) === -1 ) { 113 | throw "Bad cue"; 114 | } 115 | 116 | lineSegments = line.replace( rToken, " --> " ).split( rWhitespace ); 117 | 118 | if ( lineSegments.length < 2 ) { 119 | throw "Bad cue"; 120 | } 121 | 122 | sub.id = line; 123 | sub.start = toSeconds( lineSegments[ 0 ] ); 124 | sub.end = toSeconds( lineSegments[ 2 ] ); 125 | 126 | return sub; 127 | } 128 | 129 | function skipWhitespace ( lines, len, i ) { 130 | while ( i < len && !lines[ i ] ) { 131 | i++; 132 | } 133 | 134 | return i; 135 | } 136 | 137 | function skipNonWhitespace ( lines, len, i ) { 138 | while ( i < len && lines[ i ] ) { 139 | i++; 140 | } 141 | 142 | return i; 143 | } 144 | })( Popcorn ); 145 | -------------------------------------------------------------------------------- /app/js/node/subsync.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Ognjen on 10/19/15. 3 | */ 4 | 5 | // counts differences between subtitles (text lengths over intervals) 6 | function getDiff(subs1, subs2, k, n, searchIntervals, searchNumSubs){ 7 | var diffs = {}; 8 | var subLen, subTime, key; 9 | for (var i in searchIntervals){ 10 | var interval = searchIntervals[i]; 11 | for(var i1 in subs1){ 12 | subLen = subs1[i1].subtitle.text.length; 13 | subTime = Math.round(subs1[i1].subtitle.start / interval); 14 | key = interval + '|' + subTime; 15 | diffs[key] = (diffs[key] || 0) + subLen; 16 | if(i1 > searchNumSubs) break; 17 | } 18 | for(var i2 in subs2){ 19 | subLen = subs2[i2].subtitle.text.length; 20 | subTime = Math.round((subs2[i2].subtitle.start * k + n) / interval); 21 | key = interval + '|' + subTime; 22 | diffs[key] = (diffs[key] || 0) - subLen; 23 | if(i2 > searchNumSubs) break; 24 | } 25 | } 26 | // just sum absolute values of all diff values 27 | var diff = 0; 28 | for (key in diffs) { 29 | diff += Math.abs(diffs[key]); 30 | } 31 | return diff; 32 | } 33 | 34 | 35 | // returns best k, n (subtitle timing t is transformed like: t*k+n) 36 | exports.getBestTransform = function(subs1, subs2){ 37 | var params = { 38 | rates: [1, 23.976 / 25.0, 25.0 / 23.976, 30.0 / 23.976, 23.976 / 30.0], // // 24.0, 30.0 39 | range1: 30.0, delta1: 1, 40 | range2: 3.0, delta2: 0.2, 41 | searchIntervals: [2, 20], 42 | searchNumSubs: 200 43 | }; 44 | var start = new Date().getTime(); 45 | var best = {diff: null, k: 1, n:0}; 46 | var diff; 47 | for (var ki in params.rates){ 48 | var k = params.rates[ki]; 49 | for(var n = - params.range1; n <= params.range1; n+=params.delta1){ 50 | diff = getDiff(subs1, subs2, k, n, params.searchIntervals, params.searchNumSubs); 51 | if (best.diff === null || diff < best.diff){ 52 | best = {diff:diff, k:k, n:n}; 53 | } 54 | } 55 | for(var n2 = best.n - params.range2; n2 <= best.n + params.range2; n2+=params.delta2){ 56 | diff = getDiff(subs1, subs2, k, n2, params.searchIntervals, params.searchNumSubs); 57 | if (best.diff === null || diff < best.diff){ 58 | best = {diff:diff, k:k, n:n2}; 59 | } 60 | } 61 | } 62 | best.duration = new Date().getTime() - start; 63 | return best; 64 | 65 | }; -------------------------------------------------------------------------------- /app/js/node/subtitle.js: -------------------------------------------------------------------------------- 1 | var _ = require('underscore'); 2 | 3 | /** 4 | * Finds caption to be shown at a given time 5 | * @param subs - subtitles 6 | * @param time - time in seconds 7 | * @returns the caption 8 | */ 9 | exports.findSub = function(subs, time){ // todo: add searchStartIndex argument 10 | var index = _.sortedIndex(subs.data, {subtitle: {start: time}}, function(sub){return sub.subtitle.start}); 11 | if (index > subs.data.length){ 12 | index = subs.data.length 13 | } else if (index < 1){ 14 | index = 1; 15 | } 16 | var ret = subs.data[index - 1]; 17 | 18 | return ret; 19 | }; 20 | 21 | 22 | /** 23 | * Counts words in subtitle file 24 | * @param subs - subtitles 25 | * @returns dictionary {word: count} for every word in subtitle (case-insensitive) 26 | */ 27 | exports.wordFrequencies = function(subs){ 28 | return {}; // todo 29 | }; 30 | 31 | /** 32 | * Aligns a pair of subtitles recursively 33 | * @param sub1 - 1st subtitle (subtitle's data property) 34 | * @param sub2 - 2nd subtitle 35 | * @param _i1 - index in sub1 to look from 36 | * @param _i2 - index in sub2 to look from 37 | * @param numAligned - num. of aligned captions so far (for stats) 38 | * @param numUnaligned - num. of unaligned captions so far 39 | * @param pairs - output pairs 40 | */ 41 | function tryAndAlign(sub1, sub2, _i1, _i2, numAligned, numUnaligned, pairs){ 42 | var i1 = _i1, i2 = _i2; 43 | 44 | // make sure we're not out of range 45 | if(i1 < sub1.length && i2 < sub2.length){ 46 | s1 = sub1[i1].subtitle.start; 47 | s2 = sub2[i2].subtitle.start; 48 | 49 | // check if timings are near enough to be merged 50 | var diff = Math.abs(s1 - s2); 51 | if (diff <= 1.5){ // 1 second precision 52 | pairs.push([JSON.parse(JSON.stringify(sub1[i1])), JSON.parse(JSON.stringify(sub2[i2]))]); 53 | // merge them 54 | if (s1 > s2){ 55 | sub1[i1].subtitle.start = s2; 56 | } else { 57 | sub2[i2].subtitle.start = s1; 58 | } 59 | // continue 60 | return tryAndAlign(sub1, sub2, i1 + 1, i2 + 1, numAligned + 2 - diff, numUnaligned, pairs); 61 | } else { 62 | // continue with next possible pair 63 | if (s1 < s2){ 64 | pairs.push([JSON.parse(JSON.stringify(sub1[i1])), null]); 65 | // merge subs if possible 66 | if(i1-1 >= 0 && sub1[i1-1].subtitle.text.length + sub1[i1].subtitle.text.length < 180 && 67 | Math.abs(sub1[i1].subtitle.start - sub1[i1-1].subtitle.start) < 12){ 68 | sub1[i1-1].subtitle.text += ' ' + sub1[i1].subtitle.text; 69 | sub1.splice(i1, 1); 70 | i1 -= 1; 71 | } 72 | return tryAndAlign(sub1, sub2, i1 + 1, i2, numAligned, numUnaligned + 1, pairs); 73 | } else { 74 | pairs.push([null, JSON.parse(JSON.stringify(sub2[i2]))]); 75 | // merge subs if possible 76 | if(i2-1 >= 0 && sub2[i2-1].subtitle.text.length + sub2[i2].subtitle.text.length < 180 && 77 | Math.abs(sub2[i2].subtitle.start - sub2[i2-1].subtitle.start) < 12){ 78 | sub2[i2-1].subtitle.text += ' ' + sub2[i2].subtitle.text; 79 | sub2.splice(i2, 1); 80 | i2 -= 1; 81 | } 82 | return tryAndAlign(sub1, sub2, i1, i2 + 1, numAligned, numUnaligned + 1, pairs) 83 | } 84 | } 85 | } else { 86 | // return stats (% match) 87 | return { 88 | match: numAligned / (numAligned + numUnaligned) 89 | }; 90 | } 91 | } 92 | 93 | /** 94 | * Aligns a pair of subtitles 95 | * @param sub1 - 1st subs 96 | * @param sub2 - 2nd subs 97 | * @param sync - k & n for 2nd sub 98 | * @returns {{sub1: first sub aligned, sub2: second sub aligned}} 99 | */ 100 | exports.alignSubs = function(sub1, sub2, sync){ 101 | // make deep copy of subs 102 | var sub1Copy = JSON.parse(JSON.stringify(sub1)); 103 | var sub2Copy = JSON.parse(JSON.stringify(sub2)); 104 | var pairs = []; 105 | // align subs 106 | for(var ii in sub2Copy.data){ 107 | sub2Copy.data[ii].subtitle.start *= sync.k; 108 | sub2Copy.data[ii].subtitle.start += sync.n; 109 | } 110 | if(sub1.data && sub2.data){ 111 | var stats = tryAndAlign(sub1Copy.data, sub2Copy.data, 0, 0, 0, 0, pairs); 112 | } 113 | 114 | // update ids 115 | if(sub1.data){ 116 | for(var i = 0; i < sub1Copy.data.length; i++){ 117 | sub1Copy.data[i].subtitle.id = i + 1; 118 | } 119 | } 120 | if(sub2.data){ 121 | for(i = 0; i < sub2Copy.data.length; i++){ 122 | sub2Copy.data[i].subtitle.id = i + 1; 123 | } 124 | } 125 | return { 126 | sub1: sub1Copy, 127 | sub2: sub2Copy, 128 | match: stats.match, 129 | pairs: pairs 130 | } 131 | }; -------------------------------------------------------------------------------- /app/js/node/tokenizer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Ognjen on 6/24/15. 3 | */ 4 | 'use strict'; 5 | /** 6 | * Checks if character is a letter 7 | * @param c character 8 | * @returns {boolean} 9 | */ 10 | exports.is_letter = function(c){ 11 | return [' ', '.', ',', ';', '!', ':', '?', '@', '%', '^', '&', '=', 12 | '\t', '-', '/', '\\', '|', '"', '”', '”', '<', '>', '\n', '(', ')'].indexOf(c) < 0; 13 | }; 14 | //var a = require('./tokenizer.js'); 15 | // console.log(a.splitWords('ovo je ptoba')); 16 | exports.splitWords = function(str){ 17 | var s = str.replace('
', ' ').replace('
', ' '); 18 | s = s.replace('', ' ').replace('', ' '); 19 | var ret = [{'text': ''}]; 20 | var nw = false; 21 | for (var i = 0, len = s.length; i < len; i++) { 22 | var c = s[i]; 23 | if ((! exports.is_letter(c)) != nw){ 24 | if (ret[ret.length - 1]['text'] != ''){ 25 | if (! nw) ret[ret.length - 1]['isWord'] = true; 26 | ret.push({'text':''}); 27 | } 28 | } 29 | nw = ! exports.is_letter(c); 30 | ret[ret.length - 1]['text'] = ret[ret.length - 1]['text'] + c 31 | } 32 | if (! nw) ret[ret.length - 1]['isWord'] = true; 33 | 34 | return ret; 35 | 36 | }; -------------------------------------------------------------------------------- /app/js/node/vl_util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Ognjen on 4/18/15. 3 | */ 4 | var fs = require("fs"); 5 | var jschardet = require("jschardet"); 6 | var iconv = require('iconv-lite'); 7 | var parse = require('./parse.js'); 8 | var subtitle = require('./subtitle.js'); 9 | var encodings = require('./encodings.js'); 10 | var tokenizer = require('./tokenizer.js'); 11 | 12 | var loadFile = function(path, langCode, explicit_encoding){ 13 | var encoding = explicit_encoding; 14 | var content = fs.readFileSync(path); 15 | if (! encoding) { 16 | encoding = jschardet.detect(content).encoding.toLowerCase(); 17 | if (!encoding.startsWith('utf-') && langCode) { 18 | encoding = encodings.getEncodings(langCode)[0]; 19 | } 20 | } 21 | return iconv.decode(content, encoding); 22 | }; 23 | 24 | exports.detectFormat = function(path){ 25 | var pathArr = path.toLowerCase().split('.'); 26 | var extension = pathArr.pop(); 27 | if (extension == 'srt' || extension == 'sub' || extension == 'txt'){ 28 | return { 29 | type: 'subtitle', 30 | extension: extension 31 | }; 32 | } else { 33 | return { 34 | type: 'video', 35 | extension: extension 36 | }; 37 | } 38 | }; 39 | 40 | exports.loadSubtitle = function(path, langCode, encoding){ 41 | // load file to string, then auto-detect format and language 42 | var format = exports.detectFormat(path); 43 | if (format.type == 'subtitle'){ 44 | var content = loadFile(path, langCode, encoding); 45 | return parse.parseSubtitles(content, format.extension); 46 | // .data[i].subtitle.start, end, id, text 47 | }else { 48 | return {}; 49 | } 50 | }; 51 | 52 | exports.addTokenizations = function(sub){ 53 | if(sub && sub.data){ 54 | for(var i = 0; i < sub.data.length; i++){ 55 | sub.data[i].subtitle.words = tokenizer.splitWords(sub.data[i].subtitle.text) 56 | } 57 | } 58 | return sub; 59 | }; 60 | 61 | 62 | var addInc = function(obj, val){ 63 | if (val in obj){ 64 | obj[val] ++; 65 | } else { 66 | obj[val] = 1; 67 | } 68 | }; 69 | var dictTop = function(obj, n){ 70 | //sort objects by value, return top n 71 | var sortable = []; 72 | for (var p in obj){ 73 | sortable.push({ 74 | p: p, 75 | v: obj[p] 76 | } 77 | ); 78 | } 79 | sortable.sort(function(a, b) {return b.v - a.v}); 80 | ret = []; 81 | for (var i = 0; i < Math.min(sortable.length, n); i++){ 82 | ret.push(sortable[i].p); 83 | } 84 | return ret; 85 | }; 86 | 87 | exports.countWordFrequencies = function(sub){ 88 | // get word counts 89 | var words = {}; 90 | var total = 0; 91 | var prevWord = null; 92 | if(sub && sub.data){ 93 | sub.data.forEach(function(s){ 94 | s.subtitle.words.forEach(function(word){ 95 | if(word.isWord){ 96 | var lower = word.text.toLowerCase(); 97 | // add word count 98 | if (lower in words){ 99 | words[lower].count ++; 100 | } else { 101 | words[lower] = { 102 | count: 1, 103 | prev: {}, 104 | next: {} 105 | }; 106 | } 107 | // add prev/next words 108 | if (prevWord){ 109 | addInc(words[lower].prev, prevWord); 110 | addInc(words[prevWord].next, lower); 111 | } 112 | total ++; 113 | 114 | prevWord = lower; 115 | } 116 | }); 117 | }); 118 | } 119 | 120 | var ret = {}; 121 | for (var w in words){ 122 | var word = words[w]; 123 | ret[w] = { 124 | count: word.count, 125 | percentCount: word.count * 100 / total, 126 | topPrevWords: dictTop(word.prev, 3), 127 | topNextWords: dictTop(word.next, 3) 128 | } 129 | } 130 | return ret; 131 | }; -------------------------------------------------------------------------------- /app/partials/aboutDialog.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | About 4 | 5 | Close 6 | 7 |
8 |
9 |
10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 24 | 25 | 26 | 27 | 31 | 32 |
Author: Ognjen Apic
Website: 21 | 22 | http://oaprograms.github.io/lingo-player 23 |
Source on GitHub: 28 | 29 | https://github.com/oaprograms/lingo-player 30 |
33 |
34 |
35 | -------------------------------------------------------------------------------- /app/partials/dictionary.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 |
{{ data.wordDefinition.original }}
5 | 6 |
7 |
{{ data.wordDefinition.translation }}
8 |
Dictionary
9 |
10 | 11 |
12 |
13 |
-------------------------------------------------------------------------------- /app/partials/fileDialog.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | Choose File 4 | 5 | Close 6 | 7 |
8 |
9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |
21 | Recent files: 22 | Clear 23 | None 24 |
25 | 26 | 32 |
33 | 34 |
35 |
36 |
37 |
38 | 41 | {{$select.selected.name}} 42 | 43 |
44 |
45 |
46 |
47 |
48 |
49 | 52 | {{$select.selected.name}} 53 | 54 |
55 |
56 |
57 |
58 |
59 | 60 | 61 | 75 | 76 | 77 | 101 | 102 | 103 | 104 | 128 | 129 | 130 |
62 |
Video file:
63 |
64 | 66 | 67 | 68 | 72 | 73 |
74 |
78 |
79 | {{ data.languages[data.lang1] }} subtitles: 80 | 81 | Encoding 82 | 83 | Clear 84 |
85 |
86 | 88 | 89 | 94 | 98 | 99 |
100 |
105 |
106 | {{ data.languages[data.lang2] }} subtitles (optional): 107 | 108 | Encoding 109 | 110 | Clear 111 |
112 |
113 | 115 | 116 | 121 | 125 | 126 |
127 |
131 |
132 |
133 | 134 |
135 | 137 | Subtitles match: {{ data.subtitles.match | percentage:0 }} 138 | (good) 139 | (bad) 140 | 141 | 144 |
145 |
146 |
-------------------------------------------------------------------------------- /app/partials/helpDialog.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/partials/popover-phrase.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | Auto-translated 4 |
{{ data.phraseTranslation }}
5 | 6 |
-------------------------------------------------------------------------------- /app/partials/popover.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 | 5 | 6 |
{{ data.wordDefinition.original }}
7 | 8 |
9 |
{{ data.wordDefinition.translation }}
10 |
No connection
11 |
12 | 13 |
14 |
15 |
16 | 17 |
-------------------------------------------------------------------------------- /app/partials/shortcutsDialog.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | Keyboard Shortcuts 4 | 5 | Close 6 | 7 |
8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
Left / right arrow: previous / next
R key: replay current subtitle
Space: play / pause
Up / down arrow: volume up / down
28 | 29 |
30 |
-------------------------------------------------------------------------------- /app/partials/subsDialog.html: -------------------------------------------------------------------------------- 1 |
2 |
Loading...
3 |
4 | 9 |
10 |
11 |
12 |
13 |
14 | 15 | 16 | 17 | 30 | 31 | 44 | 45 | 46 |
18 |
19 | {{ word.text }}{{ word.text }} 23 | 24 |
25 | 26 |
27 | {{ s[0].subtitle.text }} 28 |
29 |
32 |
33 | {{ word.text }}{{ word.text }} 38 |
39 | 40 |
41 | {{ s[1].subtitle.text }} 42 |
43 |
47 |
48 |
49 | 50 | 51 | 52 | 67 | 68 | 69 |
53 |
54 | {{ word.text }}{{ word.text }} 60 | 61 |
62 | 63 |
64 | {{ s.subtitle.text }} 65 |
66 |
70 |
71 |
72 |
73 |
-------------------------------------------------------------------------------- /app/partials/wordsDialog.html: -------------------------------------------------------------------------------- 1 |
2 | 7 |
8 |
9 |
10 |
11 |
12 | Language: 13 | 15 | {{data.languages[$select.selected.lang1] + ' - ' + data.languages[$select.selected.lang2]}} 16 | 17 |
18 |
19 |
20 |
21 |
22 | Sort: 23 | 25 | {{ $select.selected }} 26 | 27 |
28 |
29 |
30 |
31 |
32 |
33 |
37 | 38 | {{ level.label }} ({{data.levelCounts[level.value] }}) 39 |
40 |
41 |
42 | 43 | 44 | 45 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 83 | 84 |
46 | 47 |
48 |
49 |
50 |
54 |
55 |
56 | 57 | {{word.text}} 58 | 59 | 60 | 62 | remove 63 | 64 | 65 |
73 | 78 | 82 |
85 |
86 | 87 |
-------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function (config) { 2 | config.set({ 3 | // base path that will be used to resolve all patterns (eg. files, exclude) 4 | basePath: '', 5 | // frameworks to use 6 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 7 | frameworks: ['jasmine'], 8 | 9 | // list of files / patterns to load in the browser 10 | files: [ 11 | // a lot of application files first 12 | 'tests/unit/*.js' 13 | ], 14 | // list of files to exclude 15 | exclude: [], 16 | // preprocess matching files before serving them to the browser 17 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 18 | preprocessors: { 19 | }, 20 | // test results reporter to use 21 | // possible values: 'dots', 'progress' 22 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 23 | reporters: ['progress'], 24 | // web server port 25 | port: 9876, 26 | // enable / disable colors in the output (reporters and logs) 27 | colors: true, 28 | // level of logging 29 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 30 | // logLevel: config.LOG_INFO, 31 | logLevel: config.LOG_DEBUG, 32 | // enable / disable watching file and executing tests whenever any file changes 33 | autoWatch: true, 34 | // start these browsers 35 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 36 | browsers: ['NodeWebkitWithCustomPath'], 37 | customLaunchers: { 38 | 'NodeWebkitWithCustomPath': { 39 | base: 'NodeWebkit', 40 | // Remember to include 'node_modules' if you have some modules there 41 | paths: ['app/js/node'] //, 'node_modules'] 42 | } 43 | }, 44 | // Continuous Integration mode 45 | // if true, Karma captures browsers, runs the tests and exits 46 | singleRun: false 47 | }); 48 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "app://host/app/index.html", 3 | "name": "lingo-player", 4 | "version": "0.0.1", 5 | "license" : "MIT", 6 | "author": "Ognjen Apic ", 7 | "repository": { 8 | "type" : "git", 9 | "url" : "https://github.com/oaprograms/lingo-player" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/oaprograms/lingo-player/issues" 13 | }, 14 | "chromium-args": "--d3d9 --disable-d3d11 --gpu-no-context-lost", 15 | "single-instance": false, 16 | "window": { 17 | "width": 900, 18 | "height": 600, 19 | "min_width": 300, 20 | "min_height": 260, 21 | "icon": "app/img/logo-sm.png", 22 | "toolbar": false 23 | }, 24 | "dependencies": { 25 | "file-url": "^1.0.0", 26 | "iconv-lite": "^0.4.8", 27 | "jschardet": "^1.1.1", 28 | "nedb": "^1.1.2", 29 | "opensubtitles-api": "^1.3.1", 30 | "underscore": "^1.8.3", 31 | "wcjs-player": "*" 32 | }, 33 | "devDependencies": { 34 | "jasmine-core": "^2.3.4", 35 | "karma": "^0.12.36", 36 | "karma-jasmine": "^0.3.5", 37 | "karma-nodewebkit-launcher": "0.0.12" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/test_subtitle.srt: -------------------------------------------------------------------------------- 1 | 1 2 | 00:01:51,306 --> 00:01:54,026 3 | Jo� trinaest metara. 4 | Trebali biste ga vidjeti. 5 | 6 | 2 7 | 00:02:06,146 --> 00:02:08,464 8 | Pomakni je gore na prednji dio broda. 9 | 10 | 3 11 | 00:02:09,904 --> 00:02:13,104 12 | Mir-2, idemo preko 13 | pramca. Ostanite uz nas. 14 | 15 | 4 16 | 00:02:45,982 --> 00:02:47,983 17 | Tiho. Snimamo. 18 | 19 | 5 20 | 00:02:48,622 --> 00:02:51,982 21 | Kad ga vidim kako izlazi iz 22 | tame poput broda duhova... 23 | 24 | 6 25 | 00:02:52,062 --> 00:02:54,062 26 | svaki me put zapanji. 27 | 28 | 7 29 | 00:02:55,102 --> 00:02:58,343 30 | Gledati tu�ne ostatke 31 | velikog broda, nepomi�nog... 32 | 33 | 8 34 | 00:02:59,142 --> 00:03:03,503 35 | gdje je potonuo u 2:30, 36 | 15. travnja 1912... 37 | 38 | 9 39 | 00:03:03,982 --> 00:03:05,982 40 | nakon dugog pada... 41 | 42 | 10 43 | 00:03:06,623 --> 00:03:08,623 44 | s gornjeg svijeta. 45 | -------------------------------------------------------------------------------- /tests/unit/subtitle_test.js: -------------------------------------------------------------------------------- 1 | describe('subtitle test', function () { 2 | var subtitle = require('subtitle'); 3 | 4 | beforeEach(function () { 5 | 6 | }); 7 | 8 | it('tests that correct subs are shown for given time', function () { 9 | var t1 = {data: [ 10 | {subtitle: {start: 0, text: 'a'}}, 11 | {subtitle: {start: 3, text: 'b'}}, 12 | {subtitle: {start: 9.1, text: 'c'}} 13 | ]}; 14 | 15 | var sub1 = subtitle.findSub(t1, 1); 16 | expect(sub1).toBe(t1.data[0]); 17 | 18 | var sub2 = subtitle.findSub(t1, 3.1); 19 | expect(sub2).toBe(t1.data[1]); 20 | 21 | var sub3 = subtitle.findSub(t1, -1); 22 | expect(sub3).toBe(t1.data[0]); 23 | 24 | var sub4 = subtitle.findSub(t1, 50); 25 | expect(sub4).toBe(t1.data[2]); 26 | 27 | }); 28 | 29 | it('tests that subtitles are aligned properly (alignSubs)', function () { 30 | var t1 = {data: [ 31 | {subtitle: {start: 0.5, text: 'a'}}, 32 | {subtitle: {start: 3, text: 'b'}}, 33 | {subtitle: {start: 9, text: 'c'}} 34 | ]}; 35 | var t2 = {data: [ 36 | {subtitle: {start: 0, text: '1'}}, 37 | {subtitle: {start: 3.2, text: '2'}}, 38 | {subtitle: {start: 9, text: '3'}} 39 | ]}; 40 | var aligned = subtitle.alignSubs(t1, t2); 41 | var expected1 = {data: [ 42 | {subtitle:{start: 0, text: 'a', id: 1}}, 43 | {subtitle:{start: 3, text: 'b', id: 2}}, 44 | {subtitle:{start: 9, text: 'c', id: 3}} 45 | ]}; 46 | var expected2 = {data: [ 47 | {subtitle:{start: 0, text: '1', id: 1}}, 48 | {subtitle:{start: 3, text: '2', id: 2}}, 49 | {subtitle:{start: 9, text: '3', id: 3}} 50 | ]}; 51 | expect(aligned.sub1).toEqual(expected1); 52 | expect(aligned.sub2).toEqual(expected2); 53 | 54 | //------------------------------------------------- 55 | t1 = {data: [ 56 | {subtitle: {start: 0.5, text: 'a'}}, 57 | {subtitle: {start: 3, text: 'b'}}, 58 | {subtitle: {start: 9, text: 'c'}} 59 | ]}; 60 | t2 = {data: [ 61 | {subtitle: {start: 3.2, text: '2'}}, 62 | {subtitle: {start: 9, text: '3'}} 63 | ]}; 64 | aligned = subtitle.alignSubs(t1, t2); 65 | expected1 = {data: [ 66 | {subtitle:{start: 0.5, text: 'a', id: 1}}, 67 | {subtitle:{start: 3, text: 'b', id: 2}}, 68 | {subtitle:{start: 9, text: 'c', id: 3}} 69 | ]}; 70 | expected2 = {data: [ 71 | {subtitle:{start: 3, text: '2', id: 1}}, 72 | {subtitle:{start: 9, text: '3', id: 2}} 73 | ]}; 74 | expect(aligned.sub1).toEqual(expected1); 75 | expect(aligned.sub2).toEqual(expected2); 76 | 77 | //------------------------------------------------- merging 78 | t1 = {data: [ 79 | {subtitle: {start: 0.5, text: 'a'}}, 80 | {subtitle: {start: 3, text: 'b'}}, 81 | {subtitle: {start: 9, text: 'c'}} 82 | ]}; 83 | t2 = {data: [ 84 | {subtitle: {start: 0, text: '2'}}, 85 | {subtitle: {start: 9, text: '3'}} 86 | ]}; 87 | aligned = subtitle.alignSubs(t1, t2); 88 | expected1 = {data: [ 89 | {subtitle:{start: 0, text: 'a b', id: 1}}, 90 | {subtitle:{start: 9, text: 'c', id: 2}} 91 | ]}; 92 | expected2 = {data: [ 93 | {subtitle:{start: 0, text: '2', id: 1}}, 94 | {subtitle:{start: 9, text: '3', id: 2}} 95 | ]}; 96 | expect(aligned.sub1).toEqual(expected1); 97 | expect(aligned.sub2).toEqual(expected2); 98 | 99 | //------------------------------------------------- 100 | t1 = {data: [ 101 | {subtitle: {start: 0.5, text: 'a'}}, 102 | {subtitle: {start: 3, text: 'b'}}, 103 | {subtitle: {start: 9, text: 'c'}} 104 | ]}; 105 | t2 = {data: [ 106 | {subtitle: {start: 0, text: '1'}}, 107 | {subtitle: {start: 0.5, text: '2'}}, 108 | {subtitle: {start: 1.5, text: '3'}}, 109 | {subtitle: {start: 2.5, text: '4'}} 110 | ]}; 111 | aligned = subtitle.alignSubs(t1, t2); 112 | expected1 = {data: [ 113 | {subtitle:{start: 0, text: 'a', id: 1}}, 114 | {subtitle:{start: 2.5, text: 'b', id: 2}}, 115 | {subtitle:{start: 9, text: 'c', id: 3}} 116 | ]}; 117 | expected2 = {data: [ 118 | {subtitle:{start: 0, text: '1 2 3', id: 1}}, 119 | {subtitle:{start: 2.5, text: '4', id: 2}} 120 | ]}; 121 | expect(aligned.sub1).toEqual(expected1); 122 | expect(aligned.sub2).toEqual(expected2); 123 | 124 | //------------------------------------------------- 125 | t1 = {data: [ 126 | {subtitle: {start: 0, text: 'a'}}, 127 | {subtitle: {start: 4, text: 'b'}}, 128 | {subtitle: {start: 8, text: 'c'}} 129 | ]}; 130 | t2 = {data: [ 131 | {subtitle: {start: 2, text: '1'}}, 132 | {subtitle: {start: 6, text: '2'}}, 133 | {subtitle: {start: 10, text: '3'}} 134 | ]}; 135 | aligned = subtitle.alignSubs(t1, t2); 136 | expected1 = {data: [ 137 | {subtitle:{start: 0, text: 'a b c', id: 1}} 138 | ]}; 139 | expected2 = {data: [ 140 | {subtitle:{start: 2, text: '1 2', id: 1}}, 141 | {subtitle:{start: 10, text: '3', id: 2}} 142 | ]}; 143 | expect(aligned.sub1).toEqual(expected1); 144 | expect(aligned.sub2).toEqual(expected2); 145 | }); 146 | }); -------------------------------------------------------------------------------- /tests/unit/test1.js: -------------------------------------------------------------------------------- 1 | 2 | describe('initial test', function () { 3 | //var _ = require('underscore'); 4 | 5 | beforeEach(function () { 6 | 7 | }); 8 | 9 | it('makes sure tests are working', function () { 10 | //expect(_.clone({'test': 1})).toEqual({'test': 1}); 11 | }); 12 | }); -------------------------------------------------------------------------------- /tests/unit/tokenizer_test.js: -------------------------------------------------------------------------------- 1 | 2 | describe('tokenizer test', function () { 3 | 4 | var tokenizer = require('tokenizer'); 5 | beforeEach(function () { 6 | 7 | }); 8 | 9 | it('tests is_letter', function () { 10 | expect(tokenizer.is_letter('a')).toEqual(true); 11 | expect(tokenizer.is_letter('!')).toEqual(false); 12 | expect(tokenizer.is_letter('\t')).toEqual(false); 13 | }); 14 | 15 | it('tests splitWords', function () { 16 | expect(tokenizer.splitWords('test')).toEqual([ 17 | {isWord: true, text: 'test'} 18 | ]); 19 | 20 | expect(tokenizer.splitWords('Hello, world!')).toEqual([ 21 | {text: 'Hello', isWord: true}, 22 | {text: ', '}, 23 | {text: 'world', isWord: true}, 24 | {text: '!'} 25 | ]); 26 | 27 | expect(tokenizer.splitWords("!, .,?ћирилица\nOgnjen's test ")).toEqual([ 28 | {text: '!, .,?'}, 29 | {text: 'ћирилица', isWord: true}, 30 | {text: '\n'}, 31 | {text: "Ognjen's", isWord: true}, 32 | {text: ' '}, 33 | {text: 'test', isWord: true}, 34 | {text: ' '} 35 | ]); 36 | }); 37 | }); --------------------------------------------------------------------------------