├── .bebelrc ├── .eslintrc.js ├── .github └── workflows │ └── nodejs.yml ├── .gitignore ├── .tern-project ├── Dockerfile ├── README.md ├── app.js ├── app ├── main.js └── subtitle.js ├── config.json ├── manual ├── live.gif ├── obs setup.png ├── sample.png ├── screen.gif ├── screen.png └── subtitle.gif ├── package-lock.json ├── package.json ├── production.sh ├── public ├── example.json ├── favicon.ico ├── stylesheets │ ├── bootstrap.css │ ├── if_microphone-slash.png │ ├── if_microphone.png │ ├── images │ │ └── ui-icons_444444_256x240.png │ ├── jquery-ui.css │ ├── micswitch.css │ ├── style.css │ └── switch.css ├── upload │ └── upload.txt └── vTaiwanLogo.png ├── routes └── api.js ├── views ├── admin.ejs ├── history.ejs ├── include.ejs ├── site.ejs └── subtitle.ejs ├── webpack.common.js ├── webpack.dev.js └── webpack.production.js /.bebelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015", 4 | "react", 5 | ], 6 | "plugins": [] 7 | } 8 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "extends": "google", 3 | "parserOptions": { 4 | "ecmaVersion": 6, 5 | "sourceType": "module", 6 | "ecmaFeatures": { 7 | "jsx": true 8 | } 9 | }, 10 | "rules": { 11 | "semi": 2, 12 | "comma-dangle": ["error", "never"] 13 | }, 14 | "env": { 15 | "node": true, 16 | "mongo": true, 17 | "es6": true 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [8.x, 10.x, 12.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - run: npm install 21 | - run: npm run build --if-present 22 | - run: npm test 23 | env: 24 | CI: true 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # .gitignore_global 2 | #################################### 3 | ######## OS generated files ######## 4 | #################################### 5 | .DS_Store 6 | .DS_Store? 7 | *.swp 8 | ._* 9 | .Spotlight-V100 10 | .Trashes 11 | Icon? 12 | ehthumbs.db 13 | Thumbs.db 14 | #################################### 15 | ############# Packages ############# 16 | #################################### 17 | *.7z 18 | *.dmg 19 | *.gz 20 | *.iso 21 | *.jar 22 | *.rar 23 | *.tar 24 | *.zip 25 | node_modules 26 | .idea/ 27 | .xcuserstate 28 | xcuserdata/ 29 | list.json 30 | .vscode/launch.json 31 | upload 32 | position.json 33 | dist 34 | bundle.js 35 | bundle.js.map 36 | -------------------------------------------------------------------------------- /.tern-project: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": { 3 | "node": {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:latest 2 | RUN apk add --no-cache git nodejs nodejs-npm 3 | RUN git clone https://github.com/peterlee0127/RealTimeSubtitle 4 | WORKDIR RealTimeSubtitle 5 | RUN npm install 6 | RUN npm run build 7 | EXPOSE 8080 8 | CMD npm start 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RealTimeSubtitle 2 | 3 | 即時字幕器 4 | 5 | A free plug-in for Broadcast software(support Browser Source). We can show live guest speaking during the live event. Without any commercial software needed, we can use it with any OS (Mac/Windows/Linux......). 6 | 7 | Just embeded the display page to your input. 8 | 9 | ### Tech 10 | 11 | 1. Node.js with socket.io. 12 | 13 | ### Sample. 14 | 15 | Different color block means different source. The bottom orange block shows our subtitle systems. 16 | 17 | Sample Video 18 | 19 | Demo Video 20 | 21 | 22 | 23 | 24 | #### Setup Browser Source at OBS 25 | 26 | fill URL with your own server. 27 | 28 | 29 | 30 |
31 | 32 | 33 | 搭配OBS直撥與支援Browser Source的直撥軟體,在直撥時,能夠顯示即時註解與文字. 34 | 不再只能在主控台控制,能夠讓其他人協助控制直播. 35 | 36 |

Feature

37 | 38 | - [x] Change display subtitle by guest. 39 | - [x] Change subtitle logo. 40 | - [x] Input JSON file to generate list. 41 | - [x] drap and drop to change the list. 42 |
43 | 44 | ## Install 45 | 46 | ### Install node_nodules 47 | 48 | ``` 49 | $ npm install 50 | ``` 51 | 52 | ### Build production script 53 | 54 | ``` 55 | $ npm run build 56 | ``` 57 | 58 | ### Start Server 59 | 60 | ``` 61 | $ node app.js 62 | 63 | ``` 64 | 65 | ### Default Account/Password 66 | 67 | #### Change account and password in config.json. 68 | ``` 69 | #config.json 70 | { 71 | "account":"pdis", 72 | "password":"pdis" 73 | } 74 | ``` 75 | 76 | ## Admin Page / 77 | 78 | ## Subtitle Page /site 79 | 80 |
81 | 82 | Display Sample 83 | 84 | 85 | 86 | 87 | 88 |
89 | 架構 90 | 91 | +------------------+ +---------------------+ 92 | | | | | 93 | | | | | 94 | | | | | 95 | | Admin Client(Web)| | Display Client(Web) | 96 | | | | | 97 | | | | | 98 | +-----------+-+----+ +---------------------+ 99 | | ^ 100 | +---+ | 101 | v +--+ 102 | +----------------+ 103 | | | 104 | | | 105 | | Server(Node.js)| 106 | | | 107 | | | 108 | +----------------+ 109 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | let express = require('express'); 2 | let app = express(); 3 | let helmet = require('helmet'); 4 | let path = require('path'); 5 | let bodyParser = require('body-parser'); 6 | let port = process.env.PORT || 8080; 7 | let server = require('http').createServer(app); 8 | let io = require('socket.io')(server); 9 | 10 | 11 | 12 | app.use(express.static(path.join(__dirname, 'public'))); 13 | app.set('view engine', 'ejs'); 14 | app.use(helmet()); 15 | app.use(bodyParser.urlencoded({ extended: false })) 16 | app.use(bodyParser.json()); 17 | 18 | let api = require('./routes/api')(io); 19 | app.use('/', api); 20 | 21 | server.listen(port, function() { 22 | console.log('Server listening at port %d', port); 23 | }); 24 | -------------------------------------------------------------------------------- /app/main.js: -------------------------------------------------------------------------------- 1 | 2 | // import io from 'socket.io'; 3 | var io = require("socket.io-client"); 4 | require("jquery-ui/ui/widgets/draggable"); 5 | require("jquery-ui/ui/widgets/resizable"); 6 | require('jquery-ui-touch-punch'); 7 | 8 | let socket = io(); 9 | window.socket = socket; 10 | let nowText = $('#title').text; 11 | let showStatus = $('#displaySwitch').is(':checked'); 12 | let list = ''; 13 | let editPosition = false; 14 | 15 | // 接收訊息並顯示在前端畫面上 16 | socket.on('new title', function (json) { 17 | 18 | if (nowText != json.title) { 19 | 20 | 21 | 22 | var depart = json.title.split('/')[0]; 23 | var name = json.title.split('/')[1]; 24 | var title = json.title.split('/')[2]; 25 | var buttonText = ''; 26 | if (depart != null && depart != '') { 27 | buttonText += depart; 28 | } 29 | if (name != null && name != '') { 30 | buttonText += ' ' + name; 31 | } 32 | if (title != null && title != '') { 33 | buttonText += '/' + title; 34 | } 35 | console.log(title) 36 | 37 | buttonText = $.trim(buttonText); 38 | 39 | 40 | 41 | if (!showStatus) { 42 | nowText = buttonText; 43 | $('#title').text(nowText); 44 | return; 45 | } 46 | $('#textbg').fadeOut('fast', function () { 47 | // Animation complete. 48 | }); 49 | $('#title').animate({ opacity: 0 }, 200, function () { 50 | $('#title').text(buttonText).animate({ opacity: 1 }, 200); 51 | }); 52 | nowText = buttonText; 53 | $('#textbg').fadeIn('fast', function () { 54 | // Animation complete. 55 | }); 56 | } 57 | }); 58 | 59 | socket.on('new status', function (json) { 60 | // 接收顯示狀態是否改變 61 | showStatus = json.status; 62 | $('#displaySwitch').attr('checked', showStatus); 63 | 64 | if (showStatus == false) { 65 | $('#textbg').fadeOut('slow', function () { 66 | // Animation complete. 67 | }); 68 | } else { 69 | $('#textbg').fadeIn('slow', function () { 70 | // Animation complete. 71 | }); 72 | } 73 | }); 74 | 75 | // 送出訊息(訊息,顯示狀態) 76 | function sendNewTitle(text) { 77 | 78 | $('#button-array :button').attr('class', 'btn btn-primary btn-sm'); 79 | socket.emit('title', { title: text }); 80 | } 81 | 82 | // 更改顯示狀態 83 | function changeShowStatus() { 84 | if (nowText == '') { 85 | $('#displaySwitch').attr('checked', false); 86 | return; 87 | } 88 | socket.emit('status', { 89 | status: $('#displaySwitch').is(':checked') 90 | }); 91 | } 92 | window.changeShowStatus = changeShowStatus; 93 | 94 | // 輸入新訊息 95 | function newTitle() { 96 | var data = $('#inputField').val(); 97 | if (data == '') { 98 | return 99 | } 100 | sendNewTitle(data); 101 | $('#inputField').val(''); 102 | } 103 | 104 | // 點選匯入的名單 105 | function clickTitle(title_text, btn_Id) { 106 | 107 | if (title_text != '') { 108 | $('#inputField').val(title_text); 109 | newTitle(); 110 | } 111 | $('.listbutton').css('background-color', '#fff'); 112 | $('.listbutton').css('color', '#000'); 113 | $('#' + btn_Id).css('background-color', '#000'); 114 | $('#' + btn_Id).css('color', '#fff'); 115 | 116 | }; 117 | window.clickTitle = clickTitle; 118 | 119 | var sPositions = ''; 120 | var positions = ''; 121 | 122 | $.getJSON("api/position", function (json) { 123 | sPositions = json || "{}", 124 | positions = JSON.parse(sPositions); 125 | SetPosition(); 126 | }); 127 | 128 | //將匯入名單轉成按鈕,供直接點選 129 | $.getJSON('api/list', function (json) { 130 | list = JSON.parse(json).list; 131 | 132 | BindListData(); 133 | 134 | $('.draggable').draggable('disable'); 135 | }); 136 | 137 | //劃出List 138 | function BindListData() { 139 | 140 | var list_array = ''; 141 | 142 | var departStatus = $("#DepartdisplaySwitch").is(':checked'); 143 | var nameStatus = $("#NamedisplaySwitch").is(':checked'); 144 | var titleStatus = $("#JobdisplaySwitch").is(':checked'); 145 | 146 | var col_num = 12 / list.length; 147 | 148 | var maxheigh = 0; 149 | 150 | for (var i = 0; i < list.length; i++) { 151 | 152 | var buttonSrc = `
`; 153 | var indexheigh = -1; 154 | 155 | list[i].forEach(function (element, index) { 156 | 157 | 158 | var depart = element.split('/')[0]; 159 | var name = element.split('/')[1]; 160 | var title = element.split('/')[2]; 161 | var buttonText = ''; 162 | if (departStatus && depart != null && depart != '') { 163 | buttonText += depart; 164 | } 165 | if (nameStatus && name != null && name != '') { 166 | buttonText += ' ' + name; 167 | } 168 | if (titleStatus && title != null && title != '') { 169 | buttonText += '/' + title; 170 | } 171 | 172 | 173 | 174 | 175 | 176 | //判斷是否已拉入框內 177 | if (!positions.hasOwnProperty('drag_' + (i + 1) + '-' + (index + 1))) { 178 | indexheigh = indexheigh + 1; 179 | } 180 | 181 | //名單高度計算 182 | if (indexheigh * 50 > maxheigh) { 183 | maxheigh = indexheigh * 50; 184 | } 185 | $("#FilePanel").css('padding-top', (maxheigh + 100) + 'px'); 186 | 187 | const idName = `${i + 1}-${index + 1}`; 188 | const divStyle = `top:${indexheigh * 50}px;` 189 | 190 | buttonSrc += ` 191 |
196 | 197 |
${buttonText}
198 |
`; 199 | 200 | 201 | }, this); 202 | buttonSrc += "
"; 203 | 204 | 205 | list_array += buttonSrc; 206 | 207 | } 208 | $("#button-array").html(list_array); 209 | 210 | init_draggble(); 211 | SetPosition(); 212 | 213 | }; 214 | window.BindListData = BindListData; 215 | 216 | 217 | 218 | 219 | //設定各位置 220 | function SetPosition() { 221 | 222 | $.each(positions, function (id, pos) { 223 | 224 | $("#" + id).css(pos) 225 | $("#" + id).css('position', 'absolute') 226 | 227 | }) 228 | 229 | } 230 | 231 | //建立draggble事件連結 232 | function init_draggble() { 233 | $(".draggable").draggable({ 234 | containment: "#container-draw", 235 | scroll: true, 236 | stop: function (event, ui) { 237 | 238 | positions[this.id] = ui.position; 239 | localStorage.positions = JSON.stringify(positions); 240 | BindListData(); 241 | 242 | 243 | 244 | } 245 | }); 246 | 247 | 248 | $("#display_Switch").draggable({ 249 | }); 250 | 251 | 252 | 253 | $("#container-draw").resizable({ 254 | 255 | stop: function (event, ui) { 256 | 257 | } 258 | }) 259 | 260 | 261 | 262 | 263 | } 264 | window.init_draggble = init_draggble; 265 | 266 | 267 | //切換編輯按鈕 儲存 268 | function draggableDisplay() { 269 | if (!editPosition) { 270 | 271 | $('.draggable').draggable('enable'); 272 | $("#button_EditPosition").attr('class', 'btn btn-danger '); 273 | $("#button_EditPosition").html('Edit Finish'); 274 | 275 | } 276 | else { 277 | $('.draggable').draggable('disable'); 278 | $("#button_EditPosition").attr('class', 'btn btn-primary'); 279 | $("#button_EditPosition").html('Edit Position'); 280 | 281 | 282 | $.ajax 283 | ({ 284 | type: "post", 285 | dataType: 'json', 286 | async: true, 287 | url: '/api/upload/position', 288 | data: { json: JSON.stringify(positions) }, 289 | success: function () { 290 | console.log('OK'); 291 | }, 292 | failure: function () { 293 | console.log('err'); 294 | } 295 | }); 296 | 297 | 298 | } 299 | editPosition = !editPosition; 300 | 301 | $.each(positions, function (id, pos) { 302 | $("#" + id).css(pos) 303 | 304 | }) 305 | }; 306 | window.draggableDisplay = draggableDisplay; 307 | 308 | function ondragging(element_id) { 309 | $('#' + element_id).css('border', '3px solid #d9534f'); 310 | } 311 | window.ondragging = ondragging; 312 | 313 | //hotkey 314 | $(document).keypress(function (e) { 315 | if (e.which == 13) { 316 | // enter pressed 317 | newTitle(); 318 | } 319 | }); 320 | // ctrl+~ = dislpay click 321 | $(document).keydown(function (e) { 322 | if (e.keyCode == 192 && e.ctrlKey) { 323 | $('#displaySwitch').click(); 324 | changeShowStatus(); 325 | } 326 | }); 327 | 328 | module.exports = { BindListData } 329 | -------------------------------------------------------------------------------- /app/subtitle.js: -------------------------------------------------------------------------------- 1 | var micStatus = false; 2 | var context; 3 | 4 | function micswitch() { 5 | if (micStatus) {//關 6 | $('#micswitchbuttom').attr("class", "btn btn-secondary micswitch") 7 | $('#micicon').attr("class", "fa-microphone-slash"); 8 | console.log("mic close"); 9 | context.close(); 10 | } 11 | else { 12 | initAudio(); 13 | $('#micswitchbuttom').attr("class", "btn btn-lg btn-danger micswitch") 14 | $('#micicon').attr("class", "fa-microphone"); 15 | 16 | } 17 | micStatus = !micStatus; 18 | } 19 | 20 | window.micswitch = micswitch; 21 | 22 | function initAudio() { 23 | let websocket = new WebSocket("wss://developer.ailabs.tw/asr/api/"); 24 | websocket.onopen = function (event) { 25 | 26 | var initStr = { action: "open_processor", session_id: "asr", meta_out: "reflect" } 27 | websocket.send(JSON.stringify(initStr)); 28 | }; 29 | 30 | websocket.onmessage = function (e) { 31 | 32 | var data = JSON.parse(e.data); 33 | var message = ""; 34 | console.log(data); 35 | if (data.asr_sentence != undefined) { 36 | message = data.asr_sentence; 37 | if (data.final == true)//句尾才傳 38 | socket.emit('subtitle', { subtitle: message }); 39 | } else { 40 | socket.emit('subtitle', { subtitle: "" }); 41 | } 42 | 43 | 44 | } 45 | 46 | try { 47 | window.AudioContext = window.AudioContext || window.webkitAudioContext; 48 | context = new AudioContext(); 49 | } 50 | catch (e) { 51 | alert('Web Audio API is not supported in this browser'); 52 | } 53 | 54 | navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia; 55 | 56 | navigator.getUserMedia({ audio: true }, function (stream) { 57 | var microphone = context.createMediaStreamSource(stream); 58 | var jsNode = context.createScriptProcessor(4096, 1, 1); 59 | 60 | // microphone -> filter -> destination. 61 | microphone.connect(jsNode); 62 | jsNode.connect(context.destination); 63 | 64 | jsNode.onaudioprocess = function (event) { 65 | var audio_data = event.inputBuffer.getChannelData(0);// || new Float32Array(2048); 66 | var sampleRate = event.inputBuffer.sampleRate; 67 | audio_data = downSampling(audio_data, sampleRate); 68 | websocket.send(audio_data); 69 | // send audio_data to server 70 | } 71 | }, function (error) { 72 | console.log(error); 73 | }); 74 | } 75 | 76 | function downSampling(buffer, sampleRate) { 77 | var e = buffer; 78 | var n = sampleRate; 79 | for (var t = n / 16e3, o = Math.round(e.length / t), r = new Int16Array(o), i = 0, a = 0; i < r.length;) { 80 | for (var c = Math.round((i + 1) * t), u = 0, s = 0, l = a; l < c && l < e.length; l++) u += e[l], s++; 81 | var f = u / s, 82 | d = Math.max(-1, Math.min(1, f)); 83 | r[i] = d < 0 ? 32768 * d : 32767 * d, i++ , a = c 84 | } 85 | return r; 86 | } 87 | 88 | let startInt=1; 89 | let startTime=""; 90 | let lastEndTimestr="00:00:00.000"; 91 | 92 | socket.on('new subtitle', function (json) { 93 | 94 | const subtitle = json.subtitle; 95 | $('#subtitle').text(subtitle); 96 | if(startTime=="") 97 | {//init FirstDatetime 98 | startTime=new Date(); 99 | } 100 | 101 | if (subtitle != "") 102 | { 103 | let nowTime=new Date(); 104 | let nowEnd=("0"+(new Date( Math.abs(nowTime - startTime)).getUTCHours())).slice(-2)+":"+("0"+(new Date( Math.abs(nowTime - startTime)).getUTCMinutes())).slice(-2)+":"+("0"+(new Date( Math.abs(nowTime - startTime)).getUTCSeconds())).slice(-2)+"."+("00"+(new Date( Math.abs(nowTime - startTime)).getUTCMilliseconds())).slice(-3); 105 | 106 | $('#his_subtitle').html( subtitle ); 107 | $('#history').append(startInt+"
"+lastEndTimestr+" --> "+nowEnd+"
- "+ subtitle + "

"); 108 | lastEndTimestr=nowEnd; 109 | startInt++; 110 | } 111 | 112 | }); 113 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "account":"pdis", 3 | "password":"pdis" 4 | } 5 | -------------------------------------------------------------------------------- /manual/live.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterlee0127/RealTimeSubtitle/843b455314238001d22d40ca9cee184899cf0af6/manual/live.gif -------------------------------------------------------------------------------- /manual/obs setup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterlee0127/RealTimeSubtitle/843b455314238001d22d40ca9cee184899cf0af6/manual/obs setup.png -------------------------------------------------------------------------------- /manual/sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterlee0127/RealTimeSubtitle/843b455314238001d22d40ca9cee184899cf0af6/manual/sample.png -------------------------------------------------------------------------------- /manual/screen.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterlee0127/RealTimeSubtitle/843b455314238001d22d40ca9cee184899cf0af6/manual/screen.gif -------------------------------------------------------------------------------- /manual/screen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterlee0127/RealTimeSubtitle/843b455314238001d22d40ca9cee184899cf0af6/manual/screen.png -------------------------------------------------------------------------------- /manual/subtitle.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterlee0127/RealTimeSubtitle/843b455314238001d22d40ca9cee184899cf0af6/manual/subtitle.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "RealTimeSubtitle", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "dependencies": { 7 | "basic-auth": "^2.0.0", 8 | "body-parser": "^1.18.2", 9 | "ejs": "^2.5.7", 10 | "express": "^4.15.4", 11 | "express-fileupload": "^0.2.0", 12 | "helmet": "^3.8.2", 13 | "jquery": "^3.4.0", 14 | "jquery-ui": "^1.12.1", 15 | "jquery-ui-touch-punch": "^0.2.3", 16 | "multer": "^1.3.0", 17 | "socket.io": "^2.0.3", 18 | "socket.io-client": "^2.0.3" 19 | }, 20 | "devDependencies": { 21 | "babel-cli": "^6.26.0", 22 | "babel-core": "^6.26.0", 23 | "babel-loader": "^7.1.2", 24 | "babel-plugin-flow-react-proptypes": "^6.1.0", 25 | "babel-plugin-transform-class-properties": "^6.24.1", 26 | "babel-plugin-transform-object-assign": "^6.22.0", 27 | "babel-preset-env": "^1.6.0", 28 | "babel-preset-es2017": "^6.24.1", 29 | "eslint-config-google": "^0.9.1", 30 | "uglifyjs-webpack-plugin": "^1.0.1", 31 | "webpack": "", 32 | "webpack-cli": "^2.0.14", 33 | "webpack-merge": "^4.1.1" 34 | }, 35 | "scripts": { 36 | "test": "echo \"Error: no test specified\" && exit 1", 37 | "start": "node app.js", 38 | "dev": "webpack --config webpack.dev.js --mode development --watch --debug", 39 | "build": "webpack --config webpack.production.js" 40 | }, 41 | "author": "peterlee0127 shbhsu", 42 | "license": "MIT" 43 | } 44 | -------------------------------------------------------------------------------- /production.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | export NODE_ENV=production 3 | export NODE_CLUSTER_SCHED_POLICY=rr 4 | pm2 start --merge-logs --log-date-format="YYYY-MM-DD HH:mm Z" -n realtime-subtitle app.js 5 | -------------------------------------------------------------------------------- /public/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "title":"", 3 | "list": 4 | [ 5 | [ 6 | "depart/Name/job", 7 | "depart/Name/job", 8 | "depart/Name/job" 9 | ], 10 | 11 | [ 12 | "depart/Name/job", 13 | "depart/Name/job" 14 | ], 15 | 16 | [ 17 | "depart/Name/job", 18 | "depart/Name/job" 19 | ] 20 | ] 21 | 22 | 23 | } -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterlee0127/RealTimeSubtitle/843b455314238001d22d40ca9cee184899cf0af6/public/favicon.ico -------------------------------------------------------------------------------- /public/stylesheets/if_microphone-slash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterlee0127/RealTimeSubtitle/843b455314238001d22d40ca9cee184899cf0af6/public/stylesheets/if_microphone-slash.png -------------------------------------------------------------------------------- /public/stylesheets/if_microphone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterlee0127/RealTimeSubtitle/843b455314238001d22d40ca9cee184899cf0af6/public/stylesheets/if_microphone.png -------------------------------------------------------------------------------- /public/stylesheets/images/ui-icons_444444_256x240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterlee0127/RealTimeSubtitle/843b455314238001d22d40ca9cee184899cf0af6/public/stylesheets/images/ui-icons_444444_256x240.png -------------------------------------------------------------------------------- /public/stylesheets/jquery-ui.css: -------------------------------------------------------------------------------- 1 | /*! jQuery UI - v1.12.1 - 2016-09-14 2 | * http://jqueryui.com 3 | * Includes: core.css, accordion.css, autocomplete.css, menu.css, button.css, controlgroup.css, checkboxradio.css, datepicker.css, dialog.css, draggable.css, resizable.css, progressbar.css, selectable.css, selectmenu.css, slider.css, sortable.css, spinner.css, tabs.css, tooltip.css, theme.css 4 | * To view and modify this theme, visit http://jqueryui.com/themeroller/?ffDefault=Arial%2CHelvetica%2Csans-serif&fsDefault=1em&fwDefault=normal&cornerRadius=3px&bgColorHeader=e9e9e9&bgTextureHeader=flat&borderColorHeader=dddddd&fcHeader=333333&iconColorHeader=444444&bgColorContent=ffffff&bgTextureContent=flat&borderColorContent=dddddd&fcContent=333333&iconColorContent=444444&bgColorDefault=f6f6f6&bgTextureDefault=flat&borderColorDefault=c5c5c5&fcDefault=454545&iconColorDefault=777777&bgColorHover=ededed&bgTextureHover=flat&borderColorHover=cccccc&fcHover=2b2b2b&iconColorHover=555555&bgColorActive=007fff&bgTextureActive=flat&borderColorActive=003eff&fcActive=ffffff&iconColorActive=ffffff&bgColorHighlight=fffa90&bgTextureHighlight=flat&borderColorHighlight=dad55e&fcHighlight=777620&iconColorHighlight=777620&bgColorError=fddfdf&bgTextureError=flat&borderColorError=f1a899&fcError=5f3f3f&iconColorError=cc0000&bgColorOverlay=aaaaaa&bgTextureOverlay=flat&bgImgOpacityOverlay=0&opacityOverlay=30&bgColorShadow=666666&bgTextureShadow=flat&bgImgOpacityShadow=0&opacityShadow=30&thicknessShadow=5px&offsetTopShadow=0px&offsetLeftShadow=0px&cornerRadiusShadow=8px 5 | * Copyright jQuery Foundation and other contributors; Licensed MIT */ 6 | 7 | /* Layout helpers 8 | ----------------------------------*/ 9 | .ui-helper-hidden { 10 | display: none; 11 | } 12 | .ui-helper-hidden-accessible { 13 | border: 0; 14 | clip: rect(0 0 0 0); 15 | height: 1px; 16 | margin: -1px; 17 | overflow: hidden; 18 | padding: 0; 19 | position: absolute; 20 | width: 1px; 21 | } 22 | .ui-helper-reset { 23 | margin: 0; 24 | padding: 0; 25 | border: 0; 26 | outline: 0; 27 | line-height: 1.3; 28 | text-decoration: none; 29 | font-size: 100%; 30 | list-style: none; 31 | } 32 | .ui-helper-clearfix:before, 33 | .ui-helper-clearfix:after { 34 | content: ""; 35 | display: table; 36 | border-collapse: collapse; 37 | } 38 | .ui-helper-clearfix:after { 39 | clear: both; 40 | } 41 | .ui-helper-zfix { 42 | width: 100%; 43 | height: 100%; 44 | top: 0; 45 | left: 0; 46 | position: absolute; 47 | opacity: 0; 48 | filter:Alpha(Opacity=0); /* support: IE8 */ 49 | } 50 | 51 | .ui-front { 52 | z-index: 100; 53 | } 54 | 55 | 56 | /* Interaction Cues 57 | ----------------------------------*/ 58 | .ui-state-disabled { 59 | cursor: default !important; 60 | pointer-events: none; 61 | } 62 | 63 | 64 | /* Icons 65 | ----------------------------------*/ 66 | .ui-icon { 67 | display: inline-block; 68 | vertical-align: middle; 69 | margin-top: -.25em; 70 | position: relative; 71 | text-indent: -99999px; 72 | overflow: hidden; 73 | background-repeat: no-repeat; 74 | } 75 | 76 | .ui-widget-icon-block { 77 | left: 50%; 78 | margin-left: -8px; 79 | display: block; 80 | } 81 | 82 | /* Misc visuals 83 | ----------------------------------*/ 84 | 85 | /* Overlays */ 86 | .ui-widget-overlay { 87 | position: fixed; 88 | top: 0; 89 | left: 0; 90 | width: 100%; 91 | height: 100%; 92 | } 93 | .ui-accordion .ui-accordion-header { 94 | display: block; 95 | cursor: pointer; 96 | position: relative; 97 | margin: 2px 0 0 0; 98 | padding: .5em .5em .5em .7em; 99 | font-size: 100%; 100 | } 101 | .ui-accordion .ui-accordion-content { 102 | padding: 1em 2.2em; 103 | border-top: 0; 104 | overflow: auto; 105 | } 106 | .ui-autocomplete { 107 | position: absolute; 108 | top: 0; 109 | left: 0; 110 | cursor: default; 111 | } 112 | .ui-menu { 113 | list-style: none; 114 | padding: 0; 115 | margin: 0; 116 | display: block; 117 | outline: 0; 118 | } 119 | .ui-menu .ui-menu { 120 | position: absolute; 121 | } 122 | .ui-menu .ui-menu-item { 123 | margin: 0; 124 | cursor: pointer; 125 | /* support: IE10, see #8844 */ 126 | list-style-image: url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"); 127 | } 128 | .ui-menu .ui-menu-item-wrapper { 129 | position: relative; 130 | padding: 3px 1em 3px .4em; 131 | } 132 | .ui-menu .ui-menu-divider { 133 | margin: 5px 0; 134 | height: 0; 135 | font-size: 0; 136 | line-height: 0; 137 | border-width: 1px 0 0 0; 138 | } 139 | .ui-menu .ui-state-focus, 140 | .ui-menu .ui-state-active { 141 | margin: -1px; 142 | } 143 | 144 | /* icon support */ 145 | .ui-menu-icons { 146 | position: relative; 147 | } 148 | .ui-menu-icons .ui-menu-item-wrapper { 149 | padding-left: 2em; 150 | } 151 | 152 | /* left-aligned */ 153 | .ui-menu .ui-icon { 154 | position: absolute; 155 | top: 0; 156 | bottom: 0; 157 | left: .2em; 158 | margin: auto 0; 159 | } 160 | 161 | /* right-aligned */ 162 | .ui-menu .ui-menu-icon { 163 | left: auto; 164 | right: 0; 165 | } 166 | .ui-button { 167 | padding: .4em 1em; 168 | display: inline-block; 169 | position: relative; 170 | line-height: normal; 171 | margin-right: .1em; 172 | cursor: pointer; 173 | vertical-align: middle; 174 | text-align: center; 175 | -webkit-user-select: none; 176 | -moz-user-select: none; 177 | -ms-user-select: none; 178 | user-select: none; 179 | 180 | /* Support: IE <= 11 */ 181 | overflow: visible; 182 | } 183 | 184 | .ui-button, 185 | .ui-button:link, 186 | .ui-button:visited, 187 | .ui-button:hover, 188 | .ui-button:active { 189 | text-decoration: none; 190 | } 191 | 192 | /* to make room for the icon, a width needs to be set here */ 193 | .ui-button-icon-only { 194 | width: 2em; 195 | box-sizing: border-box; 196 | text-indent: -9999px; 197 | white-space: nowrap; 198 | } 199 | 200 | /* no icon support for input elements */ 201 | input.ui-button.ui-button-icon-only { 202 | text-indent: 0; 203 | } 204 | 205 | /* button icon element(s) */ 206 | .ui-button-icon-only .ui-icon { 207 | position: absolute; 208 | top: 50%; 209 | left: 50%; 210 | margin-top: -8px; 211 | margin-left: -8px; 212 | } 213 | 214 | .ui-button.ui-icon-notext .ui-icon { 215 | padding: 0; 216 | width: 2.1em; 217 | height: 2.1em; 218 | text-indent: -9999px; 219 | white-space: nowrap; 220 | 221 | } 222 | 223 | input.ui-button.ui-icon-notext .ui-icon { 224 | width: auto; 225 | height: auto; 226 | text-indent: 0; 227 | white-space: normal; 228 | padding: .4em 1em; 229 | } 230 | 231 | /* workarounds */ 232 | /* Support: Firefox 5 - 40 */ 233 | input.ui-button::-moz-focus-inner, 234 | button.ui-button::-moz-focus-inner { 235 | border: 0; 236 | padding: 0; 237 | } 238 | .ui-controlgroup { 239 | vertical-align: middle; 240 | display: inline-block; 241 | } 242 | .ui-controlgroup > .ui-controlgroup-item { 243 | float: left; 244 | margin-left: 0; 245 | margin-right: 0; 246 | } 247 | .ui-controlgroup > .ui-controlgroup-item:focus, 248 | .ui-controlgroup > .ui-controlgroup-item.ui-visual-focus { 249 | z-index: 9999; 250 | } 251 | .ui-controlgroup-vertical > .ui-controlgroup-item { 252 | display: block; 253 | float: none; 254 | width: 100%; 255 | margin-top: 0; 256 | margin-bottom: 0; 257 | text-align: left; 258 | } 259 | .ui-controlgroup-vertical .ui-controlgroup-item { 260 | box-sizing: border-box; 261 | } 262 | .ui-controlgroup .ui-controlgroup-label { 263 | padding: .4em 1em; 264 | } 265 | .ui-controlgroup .ui-controlgroup-label span { 266 | font-size: 80%; 267 | } 268 | .ui-controlgroup-horizontal .ui-controlgroup-label + .ui-controlgroup-item { 269 | border-left: none; 270 | } 271 | .ui-controlgroup-vertical .ui-controlgroup-label + .ui-controlgroup-item { 272 | border-top: none; 273 | } 274 | .ui-controlgroup-horizontal .ui-controlgroup-label.ui-widget-content { 275 | border-right: none; 276 | } 277 | .ui-controlgroup-vertical .ui-controlgroup-label.ui-widget-content { 278 | border-bottom: none; 279 | } 280 | 281 | /* Spinner specific style fixes */ 282 | .ui-controlgroup-vertical .ui-spinner-input { 283 | 284 | /* Support: IE8 only, Android < 4.4 only */ 285 | width: 75%; 286 | width: calc( 100% - 2.4em ); 287 | } 288 | .ui-controlgroup-vertical .ui-spinner .ui-spinner-up { 289 | border-top-style: solid; 290 | } 291 | 292 | .ui-checkboxradio-label .ui-icon-background { 293 | box-shadow: inset 1px 1px 1px #ccc; 294 | border-radius: .12em; 295 | border: none; 296 | } 297 | .ui-checkboxradio-radio-label .ui-icon-background { 298 | width: 16px; 299 | height: 16px; 300 | border-radius: 1em; 301 | overflow: visible; 302 | border: none; 303 | } 304 | .ui-checkboxradio-radio-label.ui-checkboxradio-checked .ui-icon, 305 | .ui-checkboxradio-radio-label.ui-checkboxradio-checked:hover .ui-icon { 306 | background-image: none; 307 | width: 8px; 308 | height: 8px; 309 | border-width: 4px; 310 | border-style: solid; 311 | } 312 | .ui-checkboxradio-disabled { 313 | pointer-events: none; 314 | } 315 | .ui-datepicker { 316 | width: 17em; 317 | padding: .2em .2em 0; 318 | display: none; 319 | } 320 | .ui-datepicker .ui-datepicker-header { 321 | position: relative; 322 | padding: .2em 0; 323 | } 324 | .ui-datepicker .ui-datepicker-prev, 325 | .ui-datepicker .ui-datepicker-next { 326 | position: absolute; 327 | top: 2px; 328 | width: 1.8em; 329 | height: 1.8em; 330 | } 331 | .ui-datepicker .ui-datepicker-prev-hover, 332 | .ui-datepicker .ui-datepicker-next-hover { 333 | top: 1px; 334 | } 335 | .ui-datepicker .ui-datepicker-prev { 336 | left: 2px; 337 | } 338 | .ui-datepicker .ui-datepicker-next { 339 | right: 2px; 340 | } 341 | .ui-datepicker .ui-datepicker-prev-hover { 342 | left: 1px; 343 | } 344 | .ui-datepicker .ui-datepicker-next-hover { 345 | right: 1px; 346 | } 347 | .ui-datepicker .ui-datepicker-prev span, 348 | .ui-datepicker .ui-datepicker-next span { 349 | display: block; 350 | position: absolute; 351 | left: 50%; 352 | margin-left: -8px; 353 | top: 50%; 354 | margin-top: -8px; 355 | } 356 | .ui-datepicker .ui-datepicker-title { 357 | margin: 0 2.3em; 358 | line-height: 1.8em; 359 | text-align: center; 360 | } 361 | .ui-datepicker .ui-datepicker-title select { 362 | font-size: 1em; 363 | margin: 1px 0; 364 | } 365 | .ui-datepicker select.ui-datepicker-month, 366 | .ui-datepicker select.ui-datepicker-year { 367 | width: 45%; 368 | } 369 | .ui-datepicker table { 370 | width: 100%; 371 | font-size: .9em; 372 | border-collapse: collapse; 373 | margin: 0 0 .4em; 374 | } 375 | .ui-datepicker th { 376 | padding: .7em .3em; 377 | text-align: center; 378 | font-weight: bold; 379 | border: 0; 380 | } 381 | .ui-datepicker td { 382 | border: 0; 383 | padding: 1px; 384 | } 385 | .ui-datepicker td span, 386 | .ui-datepicker td a { 387 | display: block; 388 | padding: .2em; 389 | text-align: right; 390 | text-decoration: none; 391 | } 392 | .ui-datepicker .ui-datepicker-buttonpane { 393 | background-image: none; 394 | margin: .7em 0 0 0; 395 | padding: 0 .2em; 396 | border-left: 0; 397 | border-right: 0; 398 | border-bottom: 0; 399 | } 400 | .ui-datepicker .ui-datepicker-buttonpane button { 401 | float: right; 402 | margin: .5em .2em .4em; 403 | cursor: pointer; 404 | padding: .2em .6em .3em .6em; 405 | width: auto; 406 | overflow: visible; 407 | } 408 | .ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current { 409 | float: left; 410 | } 411 | 412 | /* with multiple calendars */ 413 | .ui-datepicker.ui-datepicker-multi { 414 | width: auto; 415 | } 416 | .ui-datepicker-multi .ui-datepicker-group { 417 | float: left; 418 | } 419 | .ui-datepicker-multi .ui-datepicker-group table { 420 | width: 95%; 421 | margin: 0 auto .4em; 422 | } 423 | .ui-datepicker-multi-2 .ui-datepicker-group { 424 | width: 50%; 425 | } 426 | .ui-datepicker-multi-3 .ui-datepicker-group { 427 | width: 33.3%; 428 | } 429 | .ui-datepicker-multi-4 .ui-datepicker-group { 430 | width: 25%; 431 | } 432 | .ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header, 433 | .ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header { 434 | border-left-width: 0; 435 | } 436 | .ui-datepicker-multi .ui-datepicker-buttonpane { 437 | clear: left; 438 | } 439 | .ui-datepicker-row-break { 440 | clear: both; 441 | width: 100%; 442 | font-size: 0; 443 | } 444 | 445 | /* RTL support */ 446 | .ui-datepicker-rtl { 447 | direction: rtl; 448 | } 449 | .ui-datepicker-rtl .ui-datepicker-prev { 450 | right: 2px; 451 | left: auto; 452 | } 453 | .ui-datepicker-rtl .ui-datepicker-next { 454 | left: 2px; 455 | right: auto; 456 | } 457 | .ui-datepicker-rtl .ui-datepicker-prev:hover { 458 | right: 1px; 459 | left: auto; 460 | } 461 | .ui-datepicker-rtl .ui-datepicker-next:hover { 462 | left: 1px; 463 | right: auto; 464 | } 465 | .ui-datepicker-rtl .ui-datepicker-buttonpane { 466 | clear: right; 467 | } 468 | .ui-datepicker-rtl .ui-datepicker-buttonpane button { 469 | float: left; 470 | } 471 | .ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current, 472 | .ui-datepicker-rtl .ui-datepicker-group { 473 | float: right; 474 | } 475 | .ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header, 476 | .ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header { 477 | border-right-width: 0; 478 | border-left-width: 1px; 479 | } 480 | 481 | /* Icons */ 482 | .ui-datepicker .ui-icon { 483 | display: block; 484 | text-indent: -99999px; 485 | overflow: hidden; 486 | background-repeat: no-repeat; 487 | left: .5em; 488 | top: .3em; 489 | } 490 | .ui-dialog { 491 | position: absolute; 492 | top: 0; 493 | left: 0; 494 | padding: .2em; 495 | outline: 0; 496 | } 497 | .ui-dialog .ui-dialog-titlebar { 498 | padding: .4em 1em; 499 | position: relative; 500 | } 501 | .ui-dialog .ui-dialog-title { 502 | float: left; 503 | margin: .1em 0; 504 | white-space: nowrap; 505 | width: 90%; 506 | overflow: hidden; 507 | text-overflow: ellipsis; 508 | } 509 | .ui-dialog .ui-dialog-titlebar-close { 510 | position: absolute; 511 | right: .3em; 512 | top: 50%; 513 | width: 20px; 514 | margin: -10px 0 0 0; 515 | padding: 1px; 516 | height: 20px; 517 | } 518 | .ui-dialog .ui-dialog-content { 519 | position: relative; 520 | border: 0; 521 | padding: .5em 1em; 522 | background: none; 523 | overflow: auto; 524 | } 525 | .ui-dialog .ui-dialog-buttonpane { 526 | text-align: left; 527 | border-width: 1px 0 0 0; 528 | background-image: none; 529 | margin-top: .5em; 530 | padding: .3em 1em .5em .4em; 531 | } 532 | .ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset { 533 | float: right; 534 | } 535 | .ui-dialog .ui-dialog-buttonpane button { 536 | margin: .5em .4em .5em 0; 537 | cursor: pointer; 538 | } 539 | .ui-dialog .ui-resizable-n { 540 | height: 2px; 541 | top: 0; 542 | } 543 | .ui-dialog .ui-resizable-e { 544 | width: 2px; 545 | right: 0; 546 | } 547 | .ui-dialog .ui-resizable-s { 548 | height: 2px; 549 | bottom: 0; 550 | } 551 | .ui-dialog .ui-resizable-w { 552 | width: 2px; 553 | left: 0; 554 | } 555 | .ui-dialog .ui-resizable-se, 556 | .ui-dialog .ui-resizable-sw, 557 | .ui-dialog .ui-resizable-ne, 558 | .ui-dialog .ui-resizable-nw { 559 | width: 7px; 560 | height: 7px; 561 | } 562 | .ui-dialog .ui-resizable-se { 563 | right: 0; 564 | bottom: 0; 565 | } 566 | .ui-dialog .ui-resizable-sw { 567 | left: 0; 568 | bottom: 0; 569 | } 570 | .ui-dialog .ui-resizable-ne { 571 | right: 0; 572 | top: 0; 573 | } 574 | .ui-dialog .ui-resizable-nw { 575 | left: 0; 576 | top: 0; 577 | } 578 | .ui-draggable .ui-dialog-titlebar { 579 | cursor: move; 580 | } 581 | .ui-draggable-handle { 582 | -ms-touch-action: none; 583 | touch-action: none; 584 | } 585 | .ui-resizable { 586 | position: relative; 587 | } 588 | .ui-resizable-handle { 589 | position: absolute; 590 | font-size: 0.1px; 591 | display: block; 592 | -ms-touch-action: none; 593 | touch-action: none; 594 | } 595 | .ui-resizable-disabled .ui-resizable-handle, 596 | .ui-resizable-autohide .ui-resizable-handle { 597 | display: none; 598 | } 599 | .ui-resizable-n { 600 | cursor: n-resize; 601 | height: 7px; 602 | width: 100%; 603 | top: -5px; 604 | left: 0; 605 | } 606 | .ui-resizable-s { 607 | cursor: s-resize; 608 | height: 7px; 609 | width: 100%; 610 | bottom: -5px; 611 | left: 0; 612 | } 613 | .ui-resizable-e { 614 | cursor: e-resize; 615 | width: 7px; 616 | right: -5px; 617 | top: 0; 618 | height: 100%; 619 | } 620 | .ui-resizable-w { 621 | cursor: w-resize; 622 | width: 7px; 623 | left: -5px; 624 | top: 0; 625 | height: 100%; 626 | } 627 | .ui-resizable-se { 628 | cursor: se-resize; 629 | width: 12px; 630 | height: 12px; 631 | right: 1px; 632 | bottom: 1px; 633 | } 634 | .ui-resizable-sw { 635 | cursor: sw-resize; 636 | width: 9px; 637 | height: 9px; 638 | left: -5px; 639 | bottom: -5px; 640 | } 641 | .ui-resizable-nw { 642 | cursor: nw-resize; 643 | width: 9px; 644 | height: 9px; 645 | left: -5px; 646 | top: -5px; 647 | } 648 | .ui-resizable-ne { 649 | cursor: ne-resize; 650 | width: 9px; 651 | height: 9px; 652 | right: -5px; 653 | top: -5px; 654 | } 655 | .ui-progressbar { 656 | height: 2em; 657 | text-align: left; 658 | overflow: hidden; 659 | } 660 | .ui-progressbar .ui-progressbar-value { 661 | margin: -1px; 662 | height: 100%; 663 | } 664 | .ui-progressbar .ui-progressbar-overlay { 665 | background: url("data:image/gif;base64,R0lGODlhKAAoAIABAAAAAP///yH/C05FVFNDQVBFMi4wAwEAAAAh+QQJAQABACwAAAAAKAAoAAACkYwNqXrdC52DS06a7MFZI+4FHBCKoDeWKXqymPqGqxvJrXZbMx7Ttc+w9XgU2FB3lOyQRWET2IFGiU9m1frDVpxZZc6bfHwv4c1YXP6k1Vdy292Fb6UkuvFtXpvWSzA+HycXJHUXiGYIiMg2R6W459gnWGfHNdjIqDWVqemH2ekpObkpOlppWUqZiqr6edqqWQAAIfkECQEAAQAsAAAAACgAKAAAApSMgZnGfaqcg1E2uuzDmmHUBR8Qil95hiPKqWn3aqtLsS18y7G1SzNeowWBENtQd+T1JktP05nzPTdJZlR6vUxNWWjV+vUWhWNkWFwxl9VpZRedYcflIOLafaa28XdsH/ynlcc1uPVDZxQIR0K25+cICCmoqCe5mGhZOfeYSUh5yJcJyrkZWWpaR8doJ2o4NYq62lAAACH5BAkBAAEALAAAAAAoACgAAAKVDI4Yy22ZnINRNqosw0Bv7i1gyHUkFj7oSaWlu3ovC8GxNso5fluz3qLVhBVeT/Lz7ZTHyxL5dDalQWPVOsQWtRnuwXaFTj9jVVh8pma9JjZ4zYSj5ZOyma7uuolffh+IR5aW97cHuBUXKGKXlKjn+DiHWMcYJah4N0lYCMlJOXipGRr5qdgoSTrqWSq6WFl2ypoaUAAAIfkECQEAAQAsAAAAACgAKAAAApaEb6HLgd/iO7FNWtcFWe+ufODGjRfoiJ2akShbueb0wtI50zm02pbvwfWEMWBQ1zKGlLIhskiEPm9R6vRXxV4ZzWT2yHOGpWMyorblKlNp8HmHEb/lCXjcW7bmtXP8Xt229OVWR1fod2eWqNfHuMjXCPkIGNileOiImVmCOEmoSfn3yXlJWmoHGhqp6ilYuWYpmTqKUgAAIfkECQEAAQAsAAAAACgAKAAAApiEH6kb58biQ3FNWtMFWW3eNVcojuFGfqnZqSebuS06w5V80/X02pKe8zFwP6EFWOT1lDFk8rGERh1TTNOocQ61Hm4Xm2VexUHpzjymViHrFbiELsefVrn6XKfnt2Q9G/+Xdie499XHd2g4h7ioOGhXGJboGAnXSBnoBwKYyfioubZJ2Hn0RuRZaflZOil56Zp6iioKSXpUAAAh+QQJAQABACwAAAAAKAAoAAACkoQRqRvnxuI7kU1a1UU5bd5tnSeOZXhmn5lWK3qNTWvRdQxP8qvaC+/yaYQzXO7BMvaUEmJRd3TsiMAgswmNYrSgZdYrTX6tSHGZO73ezuAw2uxuQ+BbeZfMxsexY35+/Qe4J1inV0g4x3WHuMhIl2jXOKT2Q+VU5fgoSUI52VfZyfkJGkha6jmY+aaYdirq+lQAACH5BAkBAAEALAAAAAAoACgAAAKWBIKpYe0L3YNKToqswUlvznigd4wiR4KhZrKt9Upqip61i9E3vMvxRdHlbEFiEXfk9YARYxOZZD6VQ2pUunBmtRXo1Lf8hMVVcNl8JafV38aM2/Fu5V16Bn63r6xt97j09+MXSFi4BniGFae3hzbH9+hYBzkpuUh5aZmHuanZOZgIuvbGiNeomCnaxxap2upaCZsq+1kAACH5BAkBAAEALAAAAAAoACgAAAKXjI8By5zf4kOxTVrXNVlv1X0d8IGZGKLnNpYtm8Lr9cqVeuOSvfOW79D9aDHizNhDJidFZhNydEahOaDH6nomtJjp1tutKoNWkvA6JqfRVLHU/QUfau9l2x7G54d1fl995xcIGAdXqMfBNadoYrhH+Mg2KBlpVpbluCiXmMnZ2Sh4GBqJ+ckIOqqJ6LmKSllZmsoq6wpQAAAh+QQJAQABACwAAAAAKAAoAAAClYx/oLvoxuJDkU1a1YUZbJ59nSd2ZXhWqbRa2/gF8Gu2DY3iqs7yrq+xBYEkYvFSM8aSSObE+ZgRl1BHFZNr7pRCavZ5BW2142hY3AN/zWtsmf12p9XxxFl2lpLn1rseztfXZjdIWIf2s5dItwjYKBgo9yg5pHgzJXTEeGlZuenpyPmpGQoKOWkYmSpaSnqKileI2FAAACH5BAkBAAEALAAAAAAoACgAAAKVjB+gu+jG4kORTVrVhRlsnn2dJ3ZleFaptFrb+CXmO9OozeL5VfP99HvAWhpiUdcwkpBH3825AwYdU8xTqlLGhtCosArKMpvfa1mMRae9VvWZfeB2XfPkeLmm18lUcBj+p5dnN8jXZ3YIGEhYuOUn45aoCDkp16hl5IjYJvjWKcnoGQpqyPlpOhr3aElaqrq56Bq7VAAAOw=="); 666 | height: 100%; 667 | filter: alpha(opacity=25); /* support: IE8 */ 668 | opacity: 0.25; 669 | } 670 | .ui-progressbar-indeterminate .ui-progressbar-value { 671 | background-image: none; 672 | } 673 | .ui-selectable { 674 | -ms-touch-action: none; 675 | touch-action: none; 676 | } 677 | .ui-selectable-helper { 678 | position: absolute; 679 | z-index: 100; 680 | border: 1px dotted black; 681 | } 682 | .ui-selectmenu-menu { 683 | padding: 0; 684 | margin: 0; 685 | position: absolute; 686 | top: 0; 687 | left: 0; 688 | display: none; 689 | } 690 | .ui-selectmenu-menu .ui-menu { 691 | overflow: auto; 692 | overflow-x: hidden; 693 | padding-bottom: 1px; 694 | } 695 | .ui-selectmenu-menu .ui-menu .ui-selectmenu-optgroup { 696 | font-size: 1em; 697 | font-weight: bold; 698 | line-height: 1.5; 699 | padding: 2px 0.4em; 700 | margin: 0.5em 0 0 0; 701 | height: auto; 702 | border: 0; 703 | } 704 | .ui-selectmenu-open { 705 | display: block; 706 | } 707 | .ui-selectmenu-text { 708 | display: block; 709 | margin-right: 20px; 710 | overflow: hidden; 711 | text-overflow: ellipsis; 712 | } 713 | .ui-selectmenu-button.ui-button { 714 | text-align: left; 715 | white-space: nowrap; 716 | width: 14em; 717 | } 718 | .ui-selectmenu-icon.ui-icon { 719 | float: right; 720 | margin-top: 0; 721 | } 722 | .ui-slider { 723 | position: relative; 724 | text-align: left; 725 | } 726 | .ui-slider .ui-slider-handle { 727 | position: absolute; 728 | z-index: 2; 729 | width: 1.2em; 730 | height: 1.2em; 731 | cursor: default; 732 | -ms-touch-action: none; 733 | touch-action: none; 734 | } 735 | .ui-slider .ui-slider-range { 736 | position: absolute; 737 | z-index: 1; 738 | font-size: .7em; 739 | display: block; 740 | border: 0; 741 | background-position: 0 0; 742 | } 743 | 744 | /* support: IE8 - See #6727 */ 745 | .ui-slider.ui-state-disabled .ui-slider-handle, 746 | .ui-slider.ui-state-disabled .ui-slider-range { 747 | filter: inherit; 748 | } 749 | 750 | .ui-slider-horizontal { 751 | height: .8em; 752 | } 753 | .ui-slider-horizontal .ui-slider-handle { 754 | top: -.3em; 755 | margin-left: -.6em; 756 | } 757 | .ui-slider-horizontal .ui-slider-range { 758 | top: 0; 759 | height: 100%; 760 | } 761 | .ui-slider-horizontal .ui-slider-range-min { 762 | left: 0; 763 | } 764 | .ui-slider-horizontal .ui-slider-range-max { 765 | right: 0; 766 | } 767 | 768 | .ui-slider-vertical { 769 | width: .8em; 770 | height: 100px; 771 | } 772 | .ui-slider-vertical .ui-slider-handle { 773 | left: -.3em; 774 | margin-left: 0; 775 | margin-bottom: -.6em; 776 | } 777 | .ui-slider-vertical .ui-slider-range { 778 | left: 0; 779 | width: 100%; 780 | } 781 | .ui-slider-vertical .ui-slider-range-min { 782 | bottom: 0; 783 | } 784 | .ui-slider-vertical .ui-slider-range-max { 785 | top: 0; 786 | } 787 | .ui-sortable-handle { 788 | -ms-touch-action: none; 789 | touch-action: none; 790 | } 791 | .ui-spinner { 792 | position: relative; 793 | display: inline-block; 794 | overflow: hidden; 795 | padding: 0; 796 | vertical-align: middle; 797 | } 798 | .ui-spinner-input { 799 | border: none; 800 | background: none; 801 | color: inherit; 802 | padding: .222em 0; 803 | margin: .2em 0; 804 | vertical-align: middle; 805 | margin-left: .4em; 806 | margin-right: 2em; 807 | } 808 | .ui-spinner-button { 809 | width: 1.6em; 810 | height: 50%; 811 | font-size: .5em; 812 | padding: 0; 813 | margin: 0; 814 | text-align: center; 815 | position: absolute; 816 | cursor: default; 817 | display: block; 818 | overflow: hidden; 819 | right: 0; 820 | } 821 | /* more specificity required here to override default borders */ 822 | .ui-spinner a.ui-spinner-button { 823 | border-top-style: none; 824 | border-bottom-style: none; 825 | border-right-style: none; 826 | } 827 | .ui-spinner-up { 828 | top: 0; 829 | } 830 | .ui-spinner-down { 831 | bottom: 0; 832 | } 833 | .ui-tabs { 834 | position: relative;/* position: relative prevents IE scroll bug (element with position: relative inside container with overflow: auto appear as "fixed") */ 835 | padding: .2em; 836 | } 837 | .ui-tabs .ui-tabs-nav { 838 | margin: 0; 839 | padding: .2em .2em 0; 840 | } 841 | .ui-tabs .ui-tabs-nav li { 842 | list-style: none; 843 | float: left; 844 | position: relative; 845 | top: 0; 846 | margin: 1px .2em 0 0; 847 | border-bottom-width: 0; 848 | padding: 0; 849 | white-space: nowrap; 850 | } 851 | .ui-tabs .ui-tabs-nav .ui-tabs-anchor { 852 | float: left; 853 | padding: .5em 1em; 854 | text-decoration: none; 855 | } 856 | .ui-tabs .ui-tabs-nav li.ui-tabs-active { 857 | margin-bottom: -1px; 858 | padding-bottom: 1px; 859 | } 860 | .ui-tabs .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor, 861 | .ui-tabs .ui-tabs-nav li.ui-state-disabled .ui-tabs-anchor, 862 | .ui-tabs .ui-tabs-nav li.ui-tabs-loading .ui-tabs-anchor { 863 | cursor: text; 864 | } 865 | .ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor { 866 | cursor: pointer; 867 | } 868 | .ui-tabs .ui-tabs-panel { 869 | display: block; 870 | border-width: 0; 871 | padding: 1em 1.4em; 872 | background: none; 873 | } 874 | .ui-tooltip { 875 | padding: 8px; 876 | position: absolute; 877 | z-index: 9999; 878 | max-width: 300px; 879 | } 880 | body .ui-tooltip { 881 | border-width: 2px; 882 | } 883 | /* Component containers 884 | ----------------------------------*/ 885 | .ui-widget { 886 | font-family: Arial,Helvetica,sans-serif; 887 | font-size: 1em; 888 | } 889 | .ui-widget .ui-widget { 890 | font-size: 1em; 891 | } 892 | .ui-widget input, 893 | .ui-widget select, 894 | .ui-widget textarea, 895 | .ui-widget button { 896 | font-family: Arial,Helvetica,sans-serif; 897 | font-size: 1em; 898 | } 899 | .ui-widget.ui-widget-content { 900 | border: 1px solid #c5c5c5; 901 | } 902 | .ui-widget-content { 903 | border: 1px solid #dddddd; 904 | background: #ffffff; 905 | color: #333333; 906 | } 907 | .ui-widget-content a { 908 | color: #333333; 909 | } 910 | .ui-widget-header { 911 | border: 1px solid #dddddd; 912 | background: #e9e9e9; 913 | color: #333333; 914 | font-weight: bold; 915 | } 916 | .ui-widget-header a { 917 | color: #333333; 918 | } 919 | 920 | /* Interaction states 921 | ----------------------------------*/ 922 | .ui-state-default, 923 | .ui-widget-content .ui-state-default, 924 | .ui-widget-header .ui-state-default, 925 | .ui-button, 926 | 927 | /* We use html here because we need a greater specificity to make sure disabled 928 | works properly when clicked or hovered */ 929 | html .ui-button.ui-state-disabled:hover, 930 | html .ui-button.ui-state-disabled:active { 931 | border: 1px solid #c5c5c5; 932 | background: #f6f6f6; 933 | font-weight: normal; 934 | color: #454545; 935 | } 936 | .ui-state-default a, 937 | .ui-state-default a:link, 938 | .ui-state-default a:visited, 939 | a.ui-button, 940 | a:link.ui-button, 941 | a:visited.ui-button, 942 | .ui-button { 943 | color: #454545; 944 | text-decoration: none; 945 | } 946 | .ui-state-hover, 947 | .ui-widget-content .ui-state-hover, 948 | .ui-widget-header .ui-state-hover, 949 | .ui-state-focus, 950 | .ui-widget-content .ui-state-focus, 951 | .ui-widget-header .ui-state-focus, 952 | .ui-button:hover, 953 | .ui-button:focus { 954 | border: 1px solid #cccccc; 955 | background: #ededed; 956 | font-weight: normal; 957 | color: #2b2b2b; 958 | } 959 | .ui-state-hover a, 960 | .ui-state-hover a:hover, 961 | .ui-state-hover a:link, 962 | .ui-state-hover a:visited, 963 | .ui-state-focus a, 964 | .ui-state-focus a:hover, 965 | .ui-state-focus a:link, 966 | .ui-state-focus a:visited, 967 | a.ui-button:hover, 968 | a.ui-button:focus { 969 | color: #2b2b2b; 970 | text-decoration: none; 971 | } 972 | 973 | .ui-visual-focus { 974 | box-shadow: 0 0 3px 1px rgb(94, 158, 214); 975 | } 976 | .ui-state-active, 977 | .ui-widget-content .ui-state-active, 978 | .ui-widget-header .ui-state-active, 979 | a.ui-button:active, 980 | .ui-button:active, 981 | .ui-button.ui-state-active:hover { 982 | border: 1px solid #003eff; 983 | background: #007fff; 984 | font-weight: normal; 985 | color: #ffffff; 986 | } 987 | .ui-icon-background, 988 | .ui-state-active .ui-icon-background { 989 | border: #003eff; 990 | background-color: #ffffff; 991 | } 992 | .ui-state-active a, 993 | .ui-state-active a:link, 994 | .ui-state-active a:visited { 995 | color: #ffffff; 996 | text-decoration: none; 997 | } 998 | 999 | /* Interaction Cues 1000 | ----------------------------------*/ 1001 | .ui-state-highlight, 1002 | .ui-widget-content .ui-state-highlight, 1003 | .ui-widget-header .ui-state-highlight { 1004 | border: 1px solid #dad55e; 1005 | background: #fffa90; 1006 | color: #777620; 1007 | } 1008 | .ui-state-checked { 1009 | border: 1px solid #dad55e; 1010 | background: #fffa90; 1011 | } 1012 | .ui-state-highlight a, 1013 | .ui-widget-content .ui-state-highlight a, 1014 | .ui-widget-header .ui-state-highlight a { 1015 | color: #777620; 1016 | } 1017 | .ui-state-error, 1018 | .ui-widget-content .ui-state-error, 1019 | .ui-widget-header .ui-state-error { 1020 | border: 1px solid #f1a899; 1021 | background: #fddfdf; 1022 | color: #5f3f3f; 1023 | } 1024 | .ui-state-error a, 1025 | .ui-widget-content .ui-state-error a, 1026 | .ui-widget-header .ui-state-error a { 1027 | color: #5f3f3f; 1028 | } 1029 | .ui-state-error-text, 1030 | .ui-widget-content .ui-state-error-text, 1031 | .ui-widget-header .ui-state-error-text { 1032 | color: #5f3f3f; 1033 | } 1034 | .ui-priority-primary, 1035 | .ui-widget-content .ui-priority-primary, 1036 | .ui-widget-header .ui-priority-primary { 1037 | font-weight: bold; 1038 | } 1039 | .ui-priority-secondary, 1040 | .ui-widget-content .ui-priority-secondary, 1041 | .ui-widget-header .ui-priority-secondary { 1042 | opacity: .7; 1043 | filter:Alpha(Opacity=70); /* support: IE8 */ 1044 | font-weight: normal; 1045 | } 1046 | .ui-state-disabled, 1047 | .ui-widget-content .ui-state-disabled, 1048 | .ui-widget-header .ui-state-disabled { 1049 | opacity: .35; 1050 | filter:Alpha(Opacity=35); /* support: IE8 */ 1051 | background-image: none; 1052 | } 1053 | .ui-state-disabled .ui-icon { 1054 | filter:Alpha(Opacity=35); /* support: IE8 - See #6059 */ 1055 | } 1056 | 1057 | /* Icons 1058 | ----------------------------------*/ 1059 | 1060 | /* states and images */ 1061 | .ui-icon { 1062 | width: 16px; 1063 | height: 16px; 1064 | } 1065 | .ui-icon, 1066 | .ui-widget-content .ui-icon { 1067 | background-image: url("images/ui-icons_444444_256x240.png"); 1068 | } 1069 | .ui-widget-header .ui-icon { 1070 | background-image: url("images/ui-icons_444444_256x240.png"); 1071 | } 1072 | .ui-state-hover .ui-icon, 1073 | .ui-state-focus .ui-icon, 1074 | .ui-button:hover .ui-icon, 1075 | .ui-button:focus .ui-icon { 1076 | background-image: url("images/ui-icons_555555_256x240.png"); 1077 | } 1078 | .ui-state-active .ui-icon, 1079 | .ui-button:active .ui-icon { 1080 | background-image: url("images/ui-icons_ffffff_256x240.png"); 1081 | } 1082 | .ui-state-highlight .ui-icon, 1083 | .ui-button .ui-state-highlight.ui-icon { 1084 | background-image: url("images/ui-icons_777620_256x240.png"); 1085 | } 1086 | .ui-state-error .ui-icon, 1087 | .ui-state-error-text .ui-icon { 1088 | background-image: url("images/ui-icons_cc0000_256x240.png"); 1089 | } 1090 | .ui-button .ui-icon { 1091 | background-image: url("images/ui-icons_777777_256x240.png"); 1092 | } 1093 | 1094 | /* positioning */ 1095 | .ui-icon-blank { background-position: 16px 16px; } 1096 | .ui-icon-caret-1-n { background-position: 0 0; } 1097 | .ui-icon-caret-1-ne { background-position: -16px 0; } 1098 | .ui-icon-caret-1-e { background-position: -32px 0; } 1099 | .ui-icon-caret-1-se { background-position: -48px 0; } 1100 | .ui-icon-caret-1-s { background-position: -65px 0; } 1101 | .ui-icon-caret-1-sw { background-position: -80px 0; } 1102 | .ui-icon-caret-1-w { background-position: -96px 0; } 1103 | .ui-icon-caret-1-nw { background-position: -112px 0; } 1104 | .ui-icon-caret-2-n-s { background-position: -128px 0; } 1105 | .ui-icon-caret-2-e-w { background-position: -144px 0; } 1106 | .ui-icon-triangle-1-n { background-position: 0 -16px; } 1107 | .ui-icon-triangle-1-ne { background-position: -16px -16px; } 1108 | .ui-icon-triangle-1-e { background-position: -32px -16px; } 1109 | .ui-icon-triangle-1-se { background-position: -48px -16px; } 1110 | .ui-icon-triangle-1-s { background-position: -65px -16px; } 1111 | .ui-icon-triangle-1-sw { background-position: -80px -16px; } 1112 | .ui-icon-triangle-1-w { background-position: -96px -16px; } 1113 | .ui-icon-triangle-1-nw { background-position: -112px -16px; } 1114 | .ui-icon-triangle-2-n-s { background-position: -128px -16px; } 1115 | .ui-icon-triangle-2-e-w { background-position: -144px -16px; } 1116 | .ui-icon-arrow-1-n { background-position: 0 -32px; } 1117 | .ui-icon-arrow-1-ne { background-position: -16px -32px; } 1118 | .ui-icon-arrow-1-e { background-position: -32px -32px; } 1119 | .ui-icon-arrow-1-se { background-position: -48px -32px; } 1120 | .ui-icon-arrow-1-s { background-position: -65px -32px; } 1121 | .ui-icon-arrow-1-sw { background-position: -80px -32px; } 1122 | .ui-icon-arrow-1-w { background-position: -96px -32px; } 1123 | .ui-icon-arrow-1-nw { background-position: -112px -32px; } 1124 | .ui-icon-arrow-2-n-s { background-position: -128px -32px; } 1125 | .ui-icon-arrow-2-ne-sw { background-position: -144px -32px; } 1126 | .ui-icon-arrow-2-e-w { background-position: -160px -32px; } 1127 | .ui-icon-arrow-2-se-nw { background-position: -176px -32px; } 1128 | .ui-icon-arrowstop-1-n { background-position: -192px -32px; } 1129 | .ui-icon-arrowstop-1-e { background-position: -208px -32px; } 1130 | .ui-icon-arrowstop-1-s { background-position: -224px -32px; } 1131 | .ui-icon-arrowstop-1-w { background-position: -240px -32px; } 1132 | .ui-icon-arrowthick-1-n { background-position: 1px -48px; } 1133 | .ui-icon-arrowthick-1-ne { background-position: -16px -48px; } 1134 | .ui-icon-arrowthick-1-e { background-position: -32px -48px; } 1135 | .ui-icon-arrowthick-1-se { background-position: -48px -48px; } 1136 | .ui-icon-arrowthick-1-s { background-position: -64px -48px; } 1137 | .ui-icon-arrowthick-1-sw { background-position: -80px -48px; } 1138 | .ui-icon-arrowthick-1-w { background-position: -96px -48px; } 1139 | .ui-icon-arrowthick-1-nw { background-position: -112px -48px; } 1140 | .ui-icon-arrowthick-2-n-s { background-position: -128px -48px; } 1141 | .ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; } 1142 | .ui-icon-arrowthick-2-e-w { background-position: -160px -48px; } 1143 | .ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; } 1144 | .ui-icon-arrowthickstop-1-n { background-position: -192px -48px; } 1145 | .ui-icon-arrowthickstop-1-e { background-position: -208px -48px; } 1146 | .ui-icon-arrowthickstop-1-s { background-position: -224px -48px; } 1147 | .ui-icon-arrowthickstop-1-w { background-position: -240px -48px; } 1148 | .ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; } 1149 | .ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; } 1150 | .ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; } 1151 | .ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; } 1152 | .ui-icon-arrowreturn-1-w { background-position: -64px -64px; } 1153 | .ui-icon-arrowreturn-1-n { background-position: -80px -64px; } 1154 | .ui-icon-arrowreturn-1-e { background-position: -96px -64px; } 1155 | .ui-icon-arrowreturn-1-s { background-position: -112px -64px; } 1156 | .ui-icon-arrowrefresh-1-w { background-position: -128px -64px; } 1157 | .ui-icon-arrowrefresh-1-n { background-position: -144px -64px; } 1158 | .ui-icon-arrowrefresh-1-e { background-position: -160px -64px; } 1159 | .ui-icon-arrowrefresh-1-s { background-position: -176px -64px; } 1160 | .ui-icon-arrow-4 { background-position: 0 -80px; } 1161 | .ui-icon-arrow-4-diag { background-position: -16px -80px; } 1162 | .ui-icon-extlink { background-position: -32px -80px; } 1163 | .ui-icon-newwin { background-position: -48px -80px; } 1164 | .ui-icon-refresh { background-position: -64px -80px; } 1165 | .ui-icon-shuffle { background-position: -80px -80px; } 1166 | .ui-icon-transfer-e-w { background-position: -96px -80px; } 1167 | .ui-icon-transferthick-e-w { background-position: -112px -80px; } 1168 | .ui-icon-folder-collapsed { background-position: 0 -96px; } 1169 | .ui-icon-folder-open { background-position: -16px -96px; } 1170 | .ui-icon-document { background-position: -32px -96px; } 1171 | .ui-icon-document-b { background-position: -48px -96px; } 1172 | .ui-icon-note { background-position: -64px -96px; } 1173 | .ui-icon-mail-closed { background-position: -80px -96px; } 1174 | .ui-icon-mail-open { background-position: -96px -96px; } 1175 | .ui-icon-suitcase { background-position: -112px -96px; } 1176 | .ui-icon-comment { background-position: -128px -96px; } 1177 | .ui-icon-person { background-position: -144px -96px; } 1178 | .ui-icon-print { background-position: -160px -96px; } 1179 | .ui-icon-trash { background-position: -176px -96px; } 1180 | .ui-icon-locked { background-position: -192px -96px; } 1181 | .ui-icon-unlocked { background-position: -208px -96px; } 1182 | .ui-icon-bookmark { background-position: -224px -96px; } 1183 | .ui-icon-tag { background-position: -240px -96px; } 1184 | .ui-icon-home { background-position: 0 -112px; } 1185 | .ui-icon-flag { background-position: -16px -112px; } 1186 | .ui-icon-calendar { background-position: -32px -112px; } 1187 | .ui-icon-cart { background-position: -48px -112px; } 1188 | .ui-icon-pencil { background-position: -64px -112px; } 1189 | .ui-icon-clock { background-position: -80px -112px; } 1190 | .ui-icon-disk { background-position: -96px -112px; } 1191 | .ui-icon-calculator { background-position: -112px -112px; } 1192 | .ui-icon-zoomin { background-position: -128px -112px; } 1193 | .ui-icon-zoomout { background-position: -144px -112px; } 1194 | .ui-icon-search { background-position: -160px -112px; } 1195 | .ui-icon-wrench { background-position: -176px -112px; } 1196 | .ui-icon-gear { background-position: -192px -112px; } 1197 | .ui-icon-heart { background-position: -208px -112px; } 1198 | .ui-icon-star { background-position: -224px -112px; } 1199 | .ui-icon-link { background-position: -240px -112px; } 1200 | .ui-icon-cancel { background-position: 0 -128px; } 1201 | .ui-icon-plus { background-position: -16px -128px; } 1202 | .ui-icon-plusthick { background-position: -32px -128px; } 1203 | .ui-icon-minus { background-position: -48px -128px; } 1204 | .ui-icon-minusthick { background-position: -64px -128px; } 1205 | .ui-icon-close { background-position: -80px -128px; } 1206 | .ui-icon-closethick { background-position: -96px -128px; } 1207 | .ui-icon-key { background-position: -112px -128px; } 1208 | .ui-icon-lightbulb { background-position: -128px -128px; } 1209 | .ui-icon-scissors { background-position: -144px -128px; } 1210 | .ui-icon-clipboard { background-position: -160px -128px; } 1211 | .ui-icon-copy { background-position: -176px -128px; } 1212 | .ui-icon-contact { background-position: -192px -128px; } 1213 | .ui-icon-image { background-position: -208px -128px; } 1214 | .ui-icon-video { background-position: -224px -128px; } 1215 | .ui-icon-script { background-position: -240px -128px; } 1216 | .ui-icon-alert { background-position: 0 -144px; } 1217 | .ui-icon-info { background-position: -16px -144px; } 1218 | .ui-icon-notice { background-position: -32px -144px; } 1219 | .ui-icon-help { background-position: -48px -144px; } 1220 | .ui-icon-check { background-position: -64px -144px; } 1221 | .ui-icon-bullet { background-position: -80px -144px; } 1222 | .ui-icon-radio-on { background-position: -96px -144px; } 1223 | .ui-icon-radio-off { background-position: -112px -144px; } 1224 | .ui-icon-pin-w { background-position: -128px -144px; } 1225 | .ui-icon-pin-s { background-position: -144px -144px; } 1226 | .ui-icon-play { background-position: 0 -160px; } 1227 | .ui-icon-pause { background-position: -16px -160px; } 1228 | .ui-icon-seek-next { background-position: -32px -160px; } 1229 | .ui-icon-seek-prev { background-position: -48px -160px; } 1230 | .ui-icon-seek-end { background-position: -64px -160px; } 1231 | .ui-icon-seek-start { background-position: -80px -160px; } 1232 | /* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */ 1233 | .ui-icon-seek-first { background-position: -80px -160px; } 1234 | .ui-icon-stop { background-position: -96px -160px; } 1235 | .ui-icon-eject { background-position: -112px -160px; } 1236 | .ui-icon-volume-off { background-position: -128px -160px; } 1237 | .ui-icon-volume-on { background-position: -144px -160px; } 1238 | .ui-icon-power { background-position: 0 -176px; } 1239 | .ui-icon-signal-diag { background-position: -16px -176px; } 1240 | .ui-icon-signal { background-position: -32px -176px; } 1241 | .ui-icon-battery-0 { background-position: -48px -176px; } 1242 | .ui-icon-battery-1 { background-position: -64px -176px; } 1243 | .ui-icon-battery-2 { background-position: -80px -176px; } 1244 | .ui-icon-battery-3 { background-position: -96px -176px; } 1245 | .ui-icon-circle-plus { background-position: 0 -192px; } 1246 | .ui-icon-circle-minus { background-position: -16px -192px; } 1247 | .ui-icon-circle-close { background-position: -32px -192px; } 1248 | .ui-icon-circle-triangle-e { background-position: -48px -192px; } 1249 | .ui-icon-circle-triangle-s { background-position: -64px -192px; } 1250 | .ui-icon-circle-triangle-w { background-position: -80px -192px; } 1251 | .ui-icon-circle-triangle-n { background-position: -96px -192px; } 1252 | .ui-icon-circle-arrow-e { background-position: -112px -192px; } 1253 | .ui-icon-circle-arrow-s { background-position: -128px -192px; } 1254 | .ui-icon-circle-arrow-w { background-position: -144px -192px; } 1255 | .ui-icon-circle-arrow-n { background-position: -160px -192px; } 1256 | .ui-icon-circle-zoomin { background-position: -176px -192px; } 1257 | .ui-icon-circle-zoomout { background-position: -192px -192px; } 1258 | .ui-icon-circle-check { background-position: -208px -192px; } 1259 | .ui-icon-circlesmall-plus { background-position: 0 -208px; } 1260 | .ui-icon-circlesmall-minus { background-position: -16px -208px; } 1261 | .ui-icon-circlesmall-close { background-position: -32px -208px; } 1262 | .ui-icon-squaresmall-plus { background-position: -48px -208px; } 1263 | .ui-icon-squaresmall-minus { background-position: -64px -208px; } 1264 | .ui-icon-squaresmall-close { background-position: -80px -208px; } 1265 | .ui-icon-grip-dotted-vertical { background-position: 0 -224px; } 1266 | .ui-icon-grip-dotted-horizontal { background-position: -16px -224px; } 1267 | .ui-icon-grip-solid-vertical { background-position: -32px -224px; } 1268 | .ui-icon-grip-solid-horizontal { background-position: -48px -224px; } 1269 | .ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; } 1270 | .ui-icon-grip-diagonal-se { background-position: -80px -224px; } 1271 | 1272 | 1273 | /* Misc visuals 1274 | ----------------------------------*/ 1275 | 1276 | /* Corner radius */ 1277 | .ui-corner-all, 1278 | .ui-corner-top, 1279 | .ui-corner-left, 1280 | .ui-corner-tl { 1281 | border-top-left-radius: 3px; 1282 | } 1283 | .ui-corner-all, 1284 | .ui-corner-top, 1285 | .ui-corner-right, 1286 | .ui-corner-tr { 1287 | border-top-right-radius: 3px; 1288 | } 1289 | .ui-corner-all, 1290 | .ui-corner-bottom, 1291 | .ui-corner-left, 1292 | .ui-corner-bl { 1293 | border-bottom-left-radius: 3px; 1294 | } 1295 | .ui-corner-all, 1296 | .ui-corner-bottom, 1297 | .ui-corner-right, 1298 | .ui-corner-br { 1299 | border-bottom-right-radius: 3px; 1300 | } 1301 | 1302 | /* Overlays */ 1303 | .ui-widget-overlay { 1304 | background: #aaaaaa; 1305 | opacity: .3; 1306 | filter: Alpha(Opacity=30); /* support: IE8 */ 1307 | } 1308 | .ui-widget-shadow { 1309 | -webkit-box-shadow: 0px 0px 5px #666666; 1310 | box-shadow: 0px 0px 5px #666666; 1311 | } -------------------------------------------------------------------------------- /public/stylesheets/micswitch.css: -------------------------------------------------------------------------------- 1 | 2 | .micswitch { 3 | font-size: 36px; 4 | border-radius: 50%; 5 | width: 2em; 6 | height: 2em; 7 | padding: 0 8 | } 9 | 10 | .fa-microphone{ 11 | background: url(if_microphone.png) no-repeat center; 12 | background-size: 30px 30px; 13 | height: 50px; 14 | } 15 | 16 | .fa-microphone-slash{ 17 | 18 | background: url(if_microphone-slash.png) no-repeat center; 19 | background-size: 30px 30px; 20 | height: 50px; 21 | } 22 | -------------------------------------------------------------------------------- /public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | /* Fix user-agent */ 2 | 3 | * { 4 | box-sizing: border-box; 5 | } 6 | 7 | html { 8 | font-weight: 300; 9 | -webkit-font-smoothing: antialiased; 10 | } 11 | 12 | 13 | .admin { 14 | background-color:white; 15 | } 16 | 17 | 18 | html, input { 19 | font-family: 20 | "HelveticaNeue-Light", 21 | "Helvetica Neue Light", 22 | "Helvetica Neue", 23 | Helvetica, 24 | Arial, 25 | "Lucida Grande", 26 | sans-serif; 27 | } 28 | 29 | html, body { 30 | text-align: center; 31 | margin: 0; 32 | padding: 0; 33 | } 34 | 35 | ul { 36 | list-style: none; 37 | word-wrap: break-word; 38 | } 39 | 40 | 41 | 42 | .pages { 43 | 44 | background: url("back.png") no-repeat ; 45 | 46 | background-position:center; 47 | display:block; 48 | margin:0 auto; 49 | height: auto; 50 | padding: 0; 51 | width: 100%; 52 | 53 | } 54 | 55 | .messageLog { 56 | font-family: '標楷體'; 57 | font-size: 12px; 58 | margin:0 auto; 59 | color:black; 60 | } 61 | 62 | .message { 63 | font-family: '標楷體'; 64 | font-size: 30px; 65 | padding: 30px; 66 | margin:0 auto; 67 | color:black; 68 | } 69 | 70 | .inputMessage { 71 | font-size: 100%; 72 | } 73 | 74 | .messages { 75 | height: 100%; 76 | margin: 0; 77 | overflow-y: scroll; 78 | padding: 10px 20px 10px 20px; 79 | } 80 | .img_bg 81 | { 82 | box-shadow:3px 0px 16px rgba(250,250,250,0.7); 83 | } 84 | .textbox_bg { 85 | display: inline-block; 86 | height: auto; 87 | font-size: 35px; 88 | opacity: 0.9; 89 | border-radius: 50px 50px 50px 50px; 90 | color: rgb(245, 245, 245); 91 | background-color: rgb(35, 31, 32); 92 | box-shadow: 13px 13px 13px rgba(54,60,68,0.6); 93 | } 94 | 95 | .verticaltextbox_bg { 96 | -webkit-writing-mode: vertical-lr ; 97 | writing-mode: vertical-lr ; 98 | display: inline-block; 99 | opacity: 0.9; 100 | height: auto; 101 | font-size: 35px; 102 | color:#fff; 103 | 104 | background-color: rgb(0, 178, 158); 105 | box-shadow: 13px 13px 13px rgba(10%,20%,40%,0.8); 106 | 107 | 108 | 109 | } 110 | .inputMessage { 111 | border: 10px solid #000; 112 | bottom: 0px; 113 | height: 60px; 114 | left: 0; 115 | outline: none; 116 | padding-left: 10px; 117 | position: fixed; 118 | right: 0; 119 | width: 100%; 120 | } 121 | .listbutton{ 122 | padding: 3px; 123 | vertical-align: middle; 124 | display: table; 125 | width: 100%; 126 | } 127 | 128 | .container-draw { 129 | height: 500px; 130 | width: 1200px; 131 | border:1px solid #000; 132 | padding: 5px; 133 | position: relative; 134 | } 135 | 136 | .draggable { 137 | width: 100px; 138 | border:1px solid #000; 139 | color:#000; 140 | padding: 0.5em; 141 | margin: 3px; 142 | float: left; 143 | cursor: move; 144 | position: absolute; 145 | text-align: center; 146 | 147 | } 148 | 149 | .ui-selected{ 150 | background-color:orange !important; 151 | 152 | } 153 | 154 | 155 | 156 | .subtitle_bg { 157 | display: inline-block; 158 | height: 95px; 159 | font-size: 35px; 160 | 161 | 162 | } 163 | 164 | .history 165 | { 166 | text-align: left; 167 | font-size: 20px;padding: 25px; float: left; word-break: break-all;font-family:標楷體;color:#000;box-shadow:4px 4px rgba(20%,20%,40%,0.5); 168 | height:80vh;width:100%; box-shadow: 15px 12px 12px rgba(30%,40%,40%,0.2); 169 | overflow: auto; 170 | border: 1px solid silver; 171 | } -------------------------------------------------------------------------------- /public/stylesheets/switch.css: -------------------------------------------------------------------------------- 1 | .material-switch > input[type="checkbox"] { 2 | display: none; 3 | } 4 | 5 | .material-switch > label { 6 | cursor: pointer; 7 | height: 0px; 8 | position: relative; 9 | left: -20px;top:5px; 10 | } 11 | 12 | .material-switch > label::before { 13 | background: rgb(0, 0, 0); 14 | box-shadow: inset 0px 0px 10px rgba(0, 0, 0, 0.5); 15 | border-radius: 8px; 16 | content: ''; 17 | height: 20px; 18 | margin-top: -8px; 19 | position:absolute; 20 | opacity: 0.3; 21 | transition: all 0.4s ease-in-out; 22 | width: 40px; 23 | } 24 | .material-switch > label::after { 25 | background: rgb(255, 255, 255); 26 | border-radius: 16px; 27 | box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.3); 28 | content: ''; 29 | height: 30px; 30 | left: -4px; 31 | margin-top: -8px; 32 | position: absolute; 33 | top: -4px; 34 | transition: all 0.3s ease-in-out; 35 | width: 24px; 36 | } 37 | .material-switch > input[type="checkbox"]:checked + label::before { 38 | background: rgb(17, 89, 184); 39 | /* opacity: 0.5; */ 40 | } 41 | .material-switch > input[type="checkbox"]:checked + label::after { 42 | background:rgb(17, 89, 184); 43 | left: 20px; 44 | } 45 | #display_Switch 46 | { 47 | cursor: move; 48 | } -------------------------------------------------------------------------------- /public/upload/upload.txt: -------------------------------------------------------------------------------- 1 | upload in here. 2 | -------------------------------------------------------------------------------- /public/vTaiwanLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/peterlee0127/RealTimeSubtitle/843b455314238001d22d40ca9cee184899cf0af6/public/vTaiwanLogo.png -------------------------------------------------------------------------------- /routes/api.js: -------------------------------------------------------------------------------- 1 | module.exports = function (io) { 2 | 3 | let express = require('express'); 4 | let router = express.Router(); 5 | let fs = require('fs'); 6 | let fileUpload = require('express-fileupload'); 7 | let basicAuth = require('basic-auth'); 8 | 9 | let message = ''; 10 | let subtitlemessage = ''; 11 | let showStatus = false; 12 | let DepartdisplaySwitch = false; 13 | let NamedisplaySwitch = true; 14 | let JobdisplaySwitch = false; 15 | let titlealign=''; 16 | 17 | router.get('/site', (req, res) => { 18 | 19 | if(req.query.align=='vertical') 20 | { 21 | titlealign = req.query.align; 22 | } 23 | else 24 | { 25 | titlealign = ''; 26 | } 27 | 28 | res.render('site', { 29 | title: message, status: showStatus, 30 | DepartStatus: DepartdisplaySwitch, 31 | NameStatus: NamedisplaySwitch, 32 | JobStatus: JobdisplaySwitch, 33 | titlealign:titlealign 34 | }); 35 | }); 36 | 37 | router.get('/subtitle', (req, res) => { 38 | res.render('subtitle', { 39 | subtitle: subtitlemessage, status: showStatus, 40 | }); 41 | }); 42 | 43 | router.get('/history', (req ,res) =>{ 44 | res.render('history', { 45 | subtitle: subtitlemessage, status: showStatus, 46 | }); 47 | }); 48 | 49 | 50 | const auth = function (req, res, next) { 51 | function unauthorized(res) { 52 | res.set('WWW-Authenticate', 'Basic realm=Authorization Required'); 53 | return res.sendStatus(401); 54 | }; 55 | const user = basicAuth(req); 56 | if (!user || !user.name || !user.pass) { 57 | return unauthorized(res); 58 | }; 59 | let authConfig = fs.readFileSync('./config.json','utf8'); 60 | if(authConfig) { 61 | authConfig = JSON.parse(authConfig); 62 | if (user.name === authConfig.account && user.pass === authConfig.password) { 63 | return next(); 64 | } else { 65 | return unauthorized(res); 66 | }; 67 | }else { 68 | return next(); 69 | } 70 | }; 71 | 72 | router.get('/', auth, (req, res) => { 73 | res.render('admin', { 74 | title: message, status: showStatus, 75 | DepartStatus: DepartdisplaySwitch, 76 | NameStatus: NamedisplaySwitch, 77 | JobStatus: JobdisplaySwitch 78 | }); 79 | }); 80 | 81 | router.get('/api/list', (req, res) => { 82 | fs.readFile('./public/upload/list.json', 'utf8', (error, json) => { 83 | if (!error) { 84 | res.json(json); 85 | } else { 86 | res.json({}); 87 | } 88 | }); 89 | }); 90 | 91 | 92 | router.get('/api/position', (req, res) => { 93 | fs.readFile('./public/upload/position.json', 'utf8', (error, json)=> { 94 | if (!error) { 95 | res.json(json); 96 | } else { 97 | res.json({}); 98 | } 99 | }); 100 | }); 101 | 102 | 103 | router.use(fileUpload()); 104 | 105 | router.post('/upload/image', function (req, res) { 106 | if (!req.files.pic) { 107 | return res.status(400).send('No files were uploaded.'); 108 | } else { 109 | let file = req.files.pic; 110 | handlefile(file, res, './public/upload/back.png'); 111 | } 112 | }); 113 | 114 | router.post('/upload/json', function (req, res) { 115 | if (!req.files.json) { 116 | return res.status(400).send('No files were uploaded.'); 117 | } else { 118 | // fs.writeFile('./public/upload/position.json', JSON.stringify({}),'utf-8',function(){}); 119 | let file = req.files.json; 120 | handlefile(file, res, './public/upload/list.json'); 121 | } 122 | }); 123 | 124 | router.post('/upload/position', function (req, res) { 125 | fs.writeFile('./public/upload/position.json', JSON.stringify({}),'utf-8',function(){}); 126 | }); 127 | 128 | router.post('/api/upload/position', function (req, res) { 129 | res.setHeader('Content-Type', 'text/plain') 130 | console.log(req.body.json); 131 | var stream = fs.createWriteStream("./public/upload/position.json"); 132 | stream.once('open', function (fd) { 133 | stream.write(req.body.json); 134 | stream.end(); 135 | }); 136 | res.redirect('/'); 137 | }); 138 | 139 | router.post('/clear/position', function (req, res) { 140 | res.setHeader('Content-Type', 'text/plain') 141 | var stream = fs.createWriteStream("./public/upload/position.json"); 142 | stream.once('open', function (fd) { 143 | stream.write('{}'); 144 | stream.end(); 145 | }); 146 | res.redirect('/'); 147 | }); 148 | 149 | 150 | function handlefile(file, res, save_path) { 151 | file.mv(save_path, function (err) { 152 | if (err) { 153 | return res.status(500).send(err); 154 | } 155 | }); 156 | setTimeout(function () { 157 | 158 | res.redirect('/'); 159 | }, 500); 160 | } 161 | 162 | io.on('connection', function (socket) { 163 | socket.on('title', function (data) { 164 | message = data.title; 165 | io.emit('new title', { title: message }); 166 | }); 167 | socket.on('status', function (data) { 168 | showStatus = data.status; 169 | io.emit('new status', { status: showStatus }); 170 | }); 171 | socket.on('subtitle', function (data) { 172 | subtitlemessage = data.subtitle; 173 | console.log(subtitlemessage); 174 | io.emit('new subtitle', { subtitle: subtitlemessage }); 175 | }); 176 | }); 177 | return router; 178 | } 179 | -------------------------------------------------------------------------------- /views/admin.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | PDIS - Real Time Subtitle 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 35 | 36 |
37 | 38 |
39 |
40 |
41 |

42 | <%= title %> 43 |

44 |
45 |
46 | 47 |
48 | 49 |
50 | 51 |
52 |
53 |
Dept. Display
54 |
55 | /> 57 | 58 |
59 |
60 |
61 |
Name Display
62 |
63 | /> 65 | 66 |
67 |
68 |
69 |
Job Display
70 |
71 | /> 72 | 73 |
74 |
75 |
76 |
77 | 78 |
79 |
80 | 81 | 82 | 83 |
84 | 85 |
86 |
87 | 88 | 89 | 90 |
91 | 92 | 93 | 94 | 95 | 96 | 97 |
98 |
99 |
100 | 101 |
102 | 103 | 104 |
105 |
106 |
107 | 108 |
109 |
110 |
111 | 112 | 113 |
114 |
115 | 118 |
119 |
120 | 121 |
122 |
123 |
    124 |
  • 125 |
    126 |
    127 |
    Display
    128 | 129 | 130 |
    131 |
    132 | 133 |
    134 | /> 135 | 136 |
    137 |
    138 | 139 |
    140 | 141 | 142 |
  • 143 |
144 |
145 |
146 | 147 | 148 | 149 |
150 |
151 |
152 |

Logo upload

153 | vTaiwan Logo 154 |
155 | 156 | 157 |
158 |
159 |
160 |

JSON upload

161 | example 162 |
163 | 164 |
165 | 166 | 167 |
168 | 169 | 170 | 171 |
172 | 173 |
174 | 181 | 182 | 183 |
184 | <%- include include.ejs %> 185 | <%# %> 186 | 187 | 188 | 189 | -------------------------------------------------------------------------------- /views/history.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | PDIS - Real Time Subtitle 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 |
17 | 18 |
19 | 20 |
21 | 22 | <%= subtitle %> 23 | 24 |
25 | 26 |
27 | 28 | <%- include include.ejs %> 29 | 30 | 31 | -------------------------------------------------------------------------------- /views/include.ejs: -------------------------------------------------------------------------------- 1 | 2 | <%# %> 3 | <%# %> 4 | <%# %> 5 | <%# %> 6 | <%# %> 7 | 8 | -------------------------------------------------------------------------------- /views/site.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | PDIS - Real Time Subtitle 7 | 8 | 9 | 10 | 11 | 19 | 20 |
21 | 22 |
<%= status?"": 'style="display:none;"' %>> 23 | > 24 | 25 |
26 | <%= title %> 27 |
28 | 29 |
30 |
31 | <%- include include.ejs %> 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /views/subtitle.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | PDIS - Real Time Subtitle 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 |
16 | 17 |
18 | <%= subtitle %> 19 |
20 | 21 |
22 |
23 | <%- include include.ejs %> 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /webpack.common.js: -------------------------------------------------------------------------------- 1 | let path = require('path'); 2 | let webpack = require('webpack'); 3 | 4 | module.exports = { 5 | entry: { app: [ 6 | './app/main.js', 7 | './app/subtitle.js'] 8 | }, 9 | module: { 10 | rules: [ 11 | { 12 | test: /.jsx?$/, 13 | loader: 'babel-loader', 14 | exclude: /node_modules/, 15 | query: { 16 | //presets: ['es2017', 'react'] 17 | } 18 | }, 19 | { 20 | test: /(\.md|\.map)$/, 21 | loader: 'null-loader' 22 | }, 23 | { 24 | test: /\.json$/, 25 | loader: 'json-loader' 26 | }, 27 | { 28 | test: /\.(jpe?g|png|gif)$/i, 29 | loader:"file-loader", 30 | query:{ 31 | name:'[name].[ext]', 32 | outputPath:'images/' 33 | } 34 | }, 35 | { 36 | test: /\.css$/, 37 | loaders: ["style-loader","css-loader"] 38 | } 39 | ] 40 | }, 41 | node: { 42 | fs: 'empty', 43 | dgram: 'empty', 44 | fs: 'empty', 45 | net: 'empty', 46 | tls: 'empty', 47 | child_process: 'empty' 48 | }, 49 | plugins: [ 50 | new webpack.ProvidePlugin({ 51 | 'window.jQuery' : 'jquery', 52 | 'window.$' : 'jquery', 53 | 'jQuery' : 'jquery', 54 | '$' : 'jquery' 55 | }) 56 | ], 57 | output: { 58 | filename: 'bundle.js', 59 | path: path.resolve(__dirname, 'public') 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge'); 2 | const common = require('./webpack.common.js'); 3 | 4 | module.exports = merge(common, { 5 | devtool: 'source-map' 6 | }); 7 | -------------------------------------------------------------------------------- /webpack.production.js: -------------------------------------------------------------------------------- 1 | const merge = require('webpack-merge'); 2 | const UglifyJSPlugin = require('uglifyjs-webpack-plugin'); 3 | const common = require('./webpack.common.js'); 4 | 5 | module.exports = merge(common, { 6 | plugins: [ 7 | new UglifyJSPlugin() 8 | ] 9 | }); 10 | --------------------------------------------------------------------------------