=this._reconnectionAttempts)h("reconnect failed"),this.backoff.reset(),this.emitAll("reconnect_failed"),this.reconnecting=!1;else{var e=this.backoff.duration();h("will wait %dms before reconnect attempt",e),this.reconnecting=!0;var n=setTimeout(function(){t.skipReconnect||(h("attempting reconnect"),t.emitAll("reconnect_attempt",t.backoff.attempts),t.emitAll("reconnecting",t.backoff.attempts),t.skipReconnect||t.open(function(e){e?(h("reconnect attempt error"),t.reconnecting=!1,t.reconnect(),t.emitAll("reconnect_error",e.data)):(h("reconnect success"),t.onreconnect())}))},e);this.subs.push({destroy:function(){clearTimeout(n)}})}},r.prototype.onreconnect=function(){var t=this.backoff.attempts;this.reconnecting=!1,this.backoff.reset(),this.updateSocketIds(),this.emitAll("reconnect",t)}},function(t,e,n){t.exports=n(14),t.exports.parser=n(21)},function(t,e,n){function r(t,e){return this instanceof r?(e=e||{},t&&"object"==typeof t&&(e=t,t=null),t?(t=u(t),e.hostname=t.host,e.secure="https"===t.protocol||"wss"===t.protocol,e.port=t.port,t.query&&(e.query=t.query)):e.host&&(e.hostname=u(e.host).host),this.secure=null!=e.secure?e.secure:"undefined"!=typeof location&&"https:"===location.protocol,e.hostname&&!e.port&&(e.port=this.secure?"443":"80"),this.agent=e.agent||!1,this.hostname=e.hostname||("undefined"!=typeof location?location.hostname:"localhost"),this.port=e.port||("undefined"!=typeof location&&location.port?location.port:this.secure?443:80),this.query=e.query||{},"string"==typeof this.query&&(this.query=h.decode(this.query)),this.upgrade=!1!==e.upgrade,this.path=(e.path||"/engine.io").replace(/\/$/,"")+"/",this.forceJSONP=!!e.forceJSONP,this.jsonp=!1!==e.jsonp,this.forceBase64=!!e.forceBase64,this.enablesXDR=!!e.enablesXDR,this.timestampParam=e.timestampParam||"t",this.timestampRequests=e.timestampRequests,this.transports=e.transports||["polling","websocket"],this.transportOptions=e.transportOptions||{},this.readyState="",this.writeBuffer=[],this.prevBufferLen=0,this.policyPort=e.policyPort||843,this.rememberUpgrade=e.rememberUpgrade||!1,this.binaryType=null,this.onlyBinaryUpgrades=e.onlyBinaryUpgrades,this.perMessageDeflate=!1!==e.perMessageDeflate&&(e.perMessageDeflate||{}),!0===this.perMessageDeflate&&(this.perMessageDeflate={}),this.perMessageDeflate&&null==this.perMessageDeflate.threshold&&(this.perMessageDeflate.threshold=1024),this.pfx=e.pfx||null,this.key=e.key||null,this.passphrase=e.passphrase||null,this.cert=e.cert||null,this.ca=e.ca||null,this.ciphers=e.ciphers||null,this.rejectUnauthorized=void 0===e.rejectUnauthorized||e.rejectUnauthorized,this.forceNode=!!e.forceNode,this.isReactNative="undefined"!=typeof navigator&&"string"==typeof navigator.product&&"reactnative"===navigator.product.toLowerCase(),("undefined"==typeof self||this.isReactNative)&&(e.extraHeaders&&Object.keys(e.extraHeaders).length>0&&(this.extraHeaders=e.extraHeaders),e.localAddress&&(this.localAddress=e.localAddress)),this.id=null,this.upgrades=null,this.pingInterval=null,this.pingTimeout=null,this.pingIntervalTimer=null,this.pingTimeoutTimer=null,void this.open()):new r(t,e)}function o(t){var e={};for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n]);return e}var i=n(15),s=n(8),a=n(3)("engine.io-client:socket"),c=n(35),p=n(21),u=n(2),h=n(29);t.exports=r,r.priorWebsocketSuccess=!1,s(r.prototype),r.protocol=p.protocol,r.Socket=r,r.Transport=n(20),r.transports=n(15),r.parser=n(21),r.prototype.createTransport=function(t){a('creating transport "%s"',t);var e=o(this.query);e.EIO=p.protocol,e.transport=t;var n=this.transportOptions[t]||{};this.id&&(e.sid=this.id);var r=new i[t]({query:e,socket:this,agent:n.agent||this.agent,hostname:n.hostname||this.hostname,port:n.port||this.port,secure:n.secure||this.secure,path:n.path||this.path,forceJSONP:n.forceJSONP||this.forceJSONP,jsonp:n.jsonp||this.jsonp,forceBase64:n.forceBase64||this.forceBase64,enablesXDR:n.enablesXDR||this.enablesXDR,timestampRequests:n.timestampRequests||this.timestampRequests,timestampParam:n.timestampParam||this.timestampParam,policyPort:n.policyPort||this.policyPort,pfx:n.pfx||this.pfx,key:n.key||this.key,passphrase:n.passphrase||this.passphrase,cert:n.cert||this.cert,ca:n.ca||this.ca,ciphers:n.ciphers||this.ciphers,rejectUnauthorized:n.rejectUnauthorized||this.rejectUnauthorized,perMessageDeflate:n.perMessageDeflate||this.perMessageDeflate,extraHeaders:n.extraHeaders||this.extraHeaders,forceNode:n.forceNode||this.forceNode,localAddress:n.localAddress||this.localAddress,requestTimeout:n.requestTimeout||this.requestTimeout,protocols:n.protocols||void 0,isReactNative:this.isReactNative});return r},r.prototype.open=function(){var t;if(this.rememberUpgrade&&r.priorWebsocketSuccess&&this.transports.indexOf("websocket")!==-1)t="websocket";else{if(0===this.transports.length){var e=this;return void setTimeout(function(){e.emit("error","No transports available")},0)}t=this.transports[0]}this.readyState="opening";try{t=this.createTransport(t)}catch(n){return this.transports.shift(),void this.open()}t.open(),this.setTransport(t)},r.prototype.setTransport=function(t){a("setting transport %s",t.name);var e=this;this.transport&&(a("clearing existing transport %s",this.transport.name),this.transport.removeAllListeners()),this.transport=t,t.on("drain",function(){e.onDrain()}).on("packet",function(t){e.onPacket(t)}).on("error",function(t){e.onError(t)}).on("close",function(){e.onClose("transport close")})},r.prototype.probe=function(t){function e(){if(f.onlyBinaryUpgrades){var e=!this.supportsBinary&&f.transport.supportsBinary;h=h||e}h||(a('probe transport "%s" opened',t),u.send([{type:"ping",data:"probe"}]),u.once("packet",function(e){if(!h)if("pong"===e.type&&"probe"===e.data){if(a('probe transport "%s" pong',t),f.upgrading=!0,f.emit("upgrading",u),!u)return;r.priorWebsocketSuccess="websocket"===u.name,a('pausing current transport "%s"',f.transport.name),f.transport.pause(function(){h||"closed"!==f.readyState&&(a("changing transport and sending upgrade packet"),p(),f.setTransport(u),u.send([{type:"upgrade"}]),f.emit("upgrade",u),u=null,f.upgrading=!1,f.flush())})}else{a('probe transport "%s" failed',t);var n=new Error("probe error");n.transport=u.name,f.emit("upgradeError",n)}}))}function n(){h||(h=!0,p(),u.close(),u=null)}function o(e){var r=new Error("probe error: "+e);r.transport=u.name,n(),a('probe transport "%s" failed because of error: %s',t,e),f.emit("upgradeError",r)}function i(){o("transport closed")}function s(){o("socket closed")}function c(t){u&&t.name!==u.name&&(a('"%s" works - aborting "%s"',t.name,u.name),n())}function p(){u.removeListener("open",e),u.removeListener("error",o),u.removeListener("close",i),f.removeListener("close",s),f.removeListener("upgrading",c)}a('probing transport "%s"',t);var u=this.createTransport(t,{probe:1}),h=!1,f=this;r.priorWebsocketSuccess=!1,u.once("open",e),u.once("error",o),u.once("close",i),this.once("close",s),this.once("upgrading",c),u.open()},r.prototype.onOpen=function(){if(a("socket open"),this.readyState="open",r.priorWebsocketSuccess="websocket"===this.transport.name,this.emit("open"),this.flush(),"open"===this.readyState&&this.upgrade&&this.transport.pause){a("starting upgrade probes");for(var t=0,e=this.upgrades.length;t1?{type:b[o],data:t.substring(1)}:{type:b[o]}:w}var i=new Uint8Array(t),o=i[0],s=f(t,1);return k&&"blob"===n&&(s=new k([s])),{type:b[o],data:s}},e.decodeBase64Packet=function(t,e){var n=b[t.charAt(0)];if(!p)return{type:n,data:{base64:!0,data:t.substr(1)}};var r=p.decode(t.substr(1));return"blob"===e&&k&&(r=new k([r])),{type:n,data:r}},e.encodePayload=function(t,n,r){function o(t){return t.length+":"+t}function i(t,r){e.encodePacket(t,!!s&&n,!1,function(t){r(null,o(t))})}"function"==typeof n&&(r=n,n=null);var s=h(t);return n&&s?k&&!g?e.encodePayloadAsBlob(t,r):e.encodePayloadAsArrayBuffer(t,r):t.length?void c(t,i,function(t,e){return r(e.join(""))}):r("0:")},e.decodePayload=function(t,n,r){if("string"!=typeof t)return e.decodePayloadAsBinary(t,n,r);"function"==typeof n&&(r=n,n=null);var o;if(""===t)return r(w,0,1);for(var i,s,a="",c=0,p=t.length;c0;){for(var s=new Uint8Array(o),a=0===s[0],c="",p=1;255!==s[p];p++){if(c.length>310)return r(w,0,1);c+=s[p]}o=f(o,2+c.length),c=parseInt(c);var u=f(o,0,c);if(a)try{u=String.fromCharCode.apply(null,new Uint8Array(u))}catch(h){var l=new Uint8Array(u);u="";for(var p=0;pr&&(n=r),e>=r||e>=n||0===r)return new ArrayBuffer(0);for(var o=new Uint8Array(t),i=new Uint8Array(n-e),s=e,a=0;s=55296&&e<=56319&&o65535&&(e-=65536,o+=d(e>>>10&1023|55296),e=56320|1023&e),o+=d(e);return o}function o(t,e){if(t>=55296&&t<=57343){if(e)throw Error("Lone surrogate U+"+t.toString(16).toUpperCase()+" is not a scalar value");return!1}return!0}function i(t,e){return d(t>>e&63|128)}function s(t,e){if(0==(4294967168&t))return d(t);var n="";return 0==(4294965248&t)?n=d(t>>6&31|192):0==(4294901760&t)?(o(t,e)||(t=65533),n=d(t>>12&15|224),n+=i(t,6)):0==(4292870144&t)&&(n=d(t>>18&7|240),n+=i(t,12),n+=i(t,6)),n+=d(63&t|128)}function a(t,e){e=e||{};for(var r,o=!1!==e.strict,i=n(t),a=i.length,c=-1,p="";++c=f)throw Error("Invalid byte index");var t=255&h[l];if(l++,128==(192&t))return 63&t;throw Error("Invalid continuation byte")}function p(t){var e,n,r,i,s;if(l>f)throw Error("Invalid byte index");if(l==f)return!1;if(e=255&h[l],l++,0==(128&e))return e;if(192==(224&e)){if(n=c(),s=(31&e)<<6|n,s>=128)return s;throw Error("Invalid continuation byte")}if(224==(240&e)){if(n=c(),r=c(),s=(15&e)<<12|n<<6|r,s>=2048)return o(s,t)?s:65533;throw Error("Invalid continuation byte")}if(240==(248&e)&&(n=c(),r=c(),i=c(),s=(7&e)<<18|n<<12|r<<6|i,s>=65536&&s<=1114111))return s;throw Error("Invalid UTF-8 detected")}function u(t,e){e=e||{};var o=!1!==e.strict;h=n(t),f=h.length,l=0;for(var i,s=[];(i=p(o))!==!1;)s.push(i);return r(s)}/*! https://mths.be/utf8js v2.1.2 by @mathias */
8 | var h,f,l,d=String.fromCharCode;t.exports={version:"2.1.2",encode:a,decode:u}},function(t,e){!function(){"use strict";for(var t="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",n=new Uint8Array(256),r=0;r>2],i+=t[(3&r[n])<<4|r[n+1]>>4],i+=t[(15&r[n+1])<<2|r[n+2]>>6],i+=t[63&r[n+2]];return o%3===2?i=i.substring(0,i.length-1)+"=":o%3===1&&(i=i.substring(0,i.length-2)+"=="),i},e.decode=function(t){var e,r,o,i,s,a=.75*t.length,c=t.length,p=0;"="===t[t.length-1]&&(a--,"="===t[t.length-2]&&a--);var u=new ArrayBuffer(a),h=new Uint8Array(u);for(e=0;e>4,h[p++]=(15&o)<<4|i>>2,h[p++]=(3&i)<<6|63&s;return u}}()},function(t,e){function n(t){return t.map(function(t){if(t.buffer instanceof ArrayBuffer){var e=t.buffer;if(t.byteLength!==e.byteLength){var n=new Uint8Array(t.byteLength);n.set(new Uint8Array(e,t.byteOffset,t.byteLength)),e=n.buffer}return e}return t})}function r(t,e){e=e||{};var r=new i;return n(t).forEach(function(t){r.append(t)}),e.type?r.getBlob(e.type):r.getBlob()}function o(t,e){return new Blob(n(t),e||{})}var i="undefined"!=typeof i?i:"undefined"!=typeof WebKitBlobBuilder?WebKitBlobBuilder:"undefined"!=typeof MSBlobBuilder?MSBlobBuilder:"undefined"!=typeof MozBlobBuilder&&MozBlobBuilder,s=function(){try{var t=new Blob(["hi"]);return 2===t.size}catch(e){return!1}}(),a=s&&function(){try{var t=new Blob([new Uint8Array([1,2])]);return 2===t.size}catch(e){return!1}}(),c=i&&i.prototype.append&&i.prototype.getBlob;"undefined"!=typeof Blob&&(r.prototype=Blob.prototype,o.prototype=Blob.prototype),t.exports=function(){return s?a?Blob:o:c?r:void 0}()},function(t,e){e.encode=function(t){var e="";for(var n in t)t.hasOwnProperty(n)&&(e.length&&(e+="&"),e+=encodeURIComponent(n)+"="+encodeURIComponent(t[n]));return e},e.decode=function(t){for(var e={},n=t.split("&"),r=0,o=n.length;r0);return e}function r(t){var e=0;for(u=0;u';i=document.createElement(e)}catch(t){i=document.createElement("iframe"),i.name=o.iframeId,i.src="javascript:0"}i.id=o.iframeId,o.form.appendChild(i),o.iframe=i}var o=this;if(!this.form){var i,s=document.createElement("form"),a=document.createElement("textarea"),c=this.iframeId="eio_iframe_"+this.index;s.className="socketio",s.style.position="absolute",s.style.top="-1000px",s.style.left="-1000px",s.target=c,s.method="POST",s.setAttribute("accept-charset","utf-8"),a.name="d",s.appendChild(a),document.body.appendChild(s),this.form=s,this.area=a}this.form.action=this.uri(),r(),t=t.replace(u,"\\\n"),this.area.value=t.replace(p,"\\n");try{this.form.submit()}catch(h){}this.iframe.attachEvent?this.iframe.onreadystatechange=function(){"complete"===o.iframe.readyState&&n()}:this.iframe.onload=n}}).call(e,function(){return this}())},function(t,e,n){function r(t){var e=t&&t.forceBase64;e&&(this.supportsBinary=!1),this.perMessageDeflate=t.perMessageDeflate,this.usingBrowserWebSocket=o&&!t.forceNode,this.protocols=t.protocols,this.usingBrowserWebSocket||(l=i),s.call(this,t)}var o,i,s=n(20),a=n(21),c=n(29),p=n(30),u=n(31),h=n(3)("engine.io-client:websocket");if("undefined"==typeof self)try{i=n(34)}catch(f){}else o=self.WebSocket||self.MozWebSocket;var l=o||i;t.exports=r,p(r,s),r.prototype.name="websocket",r.prototype.supportsBinary=!0,r.prototype.doOpen=function(){if(this.check()){var t=this.uri(),e=this.protocols,n={agent:this.agent,perMessageDeflate:this.perMessageDeflate};n.pfx=this.pfx,n.key=this.key,n.passphrase=this.passphrase,n.cert=this.cert,n.ca=this.ca,n.ciphers=this.ciphers,n.rejectUnauthorized=this.rejectUnauthorized,this.extraHeaders&&(n.headers=this.extraHeaders),this.localAddress&&(n.localAddress=this.localAddress);try{this.ws=this.usingBrowserWebSocket&&!this.isReactNative?e?new l(t,e):new l(t):new l(t,e,n)}catch(r){return this.emit("error",r)}void 0===this.ws.binaryType&&(this.supportsBinary=!1),this.ws.supports&&this.ws.supports.binary?(this.supportsBinary=!0,this.ws.binaryType="nodebuffer"):this.ws.binaryType="arraybuffer",this.addEventListeners()}},r.prototype.addEventListeners=function(){var t=this;this.ws.onopen=function(){t.onOpen()},this.ws.onclose=function(){t.onClose()},this.ws.onmessage=function(e){t.onData(e.data)},this.ws.onerror=function(e){t.onError("websocket error",e)}},r.prototype.write=function(t){function e(){n.emit("flush"),setTimeout(function(){n.writable=!0,n.emit("drain")},0)}var n=this;this.writable=!1;for(var r=t.length,o=0,i=r;o0&&t.jitter<=1?t.jitter:0,this.attempts=0}t.exports=n,n.prototype.duration=function(){var t=this.ms*Math.pow(this.factor,this.attempts++);if(this.jitter){var e=Math.random(),n=Math.floor(e*this.jitter*t);t=0==(1&Math.floor(10*e))?t-n:t+n}return 0|Math.min(t,this.max)},n.prototype.reset=function(){this.attempts=0},n.prototype.setMin=function(t){this.ms=t},n.prototype.setMax=function(t){this.max=t},n.prototype.setJitter=function(t){this.jitter=t}}])});
--------------------------------------------------------------------------------
/public/server/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | {{ index | indexToLetter }}
19 |
20 |
21 | {{ result[index] }}
22 |
23 |
24 |
25 | {{ item.label }}
26 |
27 |
28 |
29 |
30 |
31 |
32 |
94 |
95 |
--------------------------------------------------------------------------------
/public/server/index.server.js:
--------------------------------------------------------------------------------
1 | const express = require('express');
2 | const app = express();
3 | const server = require('http').Server(app);
4 | const io = require('socket.io')(server);
5 | const fs = require('fs');
6 | const path = require('path');
7 |
8 | server.listen(9317);
9 |
10 | app.get('/', (req, res) => {
11 | res.sendFile(__dirname + '/index.html');
12 | });
13 |
14 | app.get('/options', (req, res) => {
15 | res.json(
16 | JSON.parse(
17 | fs.readFileSync(path.resolve(__dirname, '../../runtime/options.json')).toString()
18 | )
19 | );
20 | });
21 |
22 | app.use('/assets', express.static(path.join(__dirname, 'assets')));
23 |
24 | io.on('connection', function (socket) {
25 | socket.on('refresh-options', (data) => {
26 | socket.broadcast.emit('refresh-options', data);
27 | });
28 | socket.on('update-result', (data) => {
29 | socket.broadcast.emit('update-result', data);
30 | });
31 | socket.on('update-language', (data) => {
32 | socket.broadcast.emit('update-language', data);
33 | });
34 | });
35 |
36 | module.exports = {};
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
24 |
--------------------------------------------------------------------------------
/src/background.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | import { app, protocol, BrowserWindow } from 'electron'
4 | import {
5 | createProtocol,
6 | installVueDevtools
7 | } from 'vue-cli-plugin-electron-builder/lib'
8 | const path = require('path');
9 | const server = __non_webpack_require__(`${__static}/server/index.server.js`);
10 | const fs = require('fs');
11 | if (!fs.existsSync(path.resolve(__static, '../runtime'))) {
12 | fs.mkdirSync(path.resolve(__static, '../runtime'));
13 | }
14 | process
15 | .on('uncaughtException', err => {
16 | fs.writeFileSync(path.resolve(__static, '../runtime/log.txt'), err.stack);
17 | });
18 | const isDevelopment = process.env.NODE_ENV !== 'production'
19 |
20 | // Keep a global reference of the window object, if you don't, the window will
21 | // be closed automatically when the JavaScript object is garbage collected.
22 | let win
23 |
24 | // Standard scheme must be registered before the app is ready
25 | protocol.registerStandardSchemes(['app'], { secure: true })
26 | function createWindow() {
27 | // Create the browser window.
28 | win = new BrowserWindow({
29 | width: 1600,
30 | height: 900,
31 | webPreferences: {
32 | webSecurity: false
33 | }
34 | })
35 | if (process.env.WEBPACK_DEV_SERVER_URL) {
36 | // Load the url of the dev server if in development mode
37 | win.loadURL(process.env.WEBPACK_DEV_SERVER_URL)
38 | if (!process.env.IS_TEST) win.webContents.openDevTools()
39 | } else {
40 | createProtocol('app')
41 | // Load the index.html when not in development
42 | win.loadURL('app://./index.html')
43 | }
44 |
45 | win.on('closed', () => {
46 | win = null
47 | })
48 | }
49 | // Allow video plays without user interact
50 | app.commandLine.appendSwitch('autoplay-policy', 'no-user-gesture-required');
51 | // Quit when all windows are closed.
52 | app.on('window-all-closed', () => {
53 | // On macOS it is common for applications and their menu bar
54 | // to stay active until the user quits explicitly with Cmd + Q
55 | if (process.platform !== 'darwin') {
56 | app.quit()
57 | }
58 | })
59 |
60 | app.on('activate', () => {
61 | // On macOS it's common to re-create a window in the app when the
62 | // dock icon is clicked and there are no other windows open.
63 | if (win === null) {
64 | createWindow()
65 | }
66 | })
67 |
68 | // This method will be called when Electron has finished
69 | // initialization and is ready to create browser windows.
70 | // Some APIs can only be used after this event occurs.
71 | app.on('ready', async () => {
72 | if (isDevelopment && !process.env.IS_TEST) {
73 | // Install Vue Devtools
74 | try {
75 | await installVueDevtools()
76 | } catch (e) {
77 | console.error('Vue Devtools failed to install:', e.toString())
78 | }
79 | }
80 | createWindow()
81 | })
82 |
83 | // Exit cleanly on request from parent process in development mode.
84 | if (isDevelopment) {
85 | if (process.platform === 'win32') {
86 | process.on('message', data => {
87 | if (data === 'graceful-exit') {
88 | app.quit()
89 | }
90 | })
91 | } else {
92 | process.on('SIGTERM', () => {
93 | app.quit()
94 | })
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/src/components/Home/AddOption.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ $vuetify.t('$vuetify.index.addOption') }}
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | {{ $vuetify.t('$vuetify.index.confirm') }}
16 |
17 |
18 |
19 |
39 |
--------------------------------------------------------------------------------
/src/components/Home/PollOptionList.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ index | numberToLetter }} - {{ item.label }}
5 |
6 | close
7 |
8 |
9 |
10 |
11 |
17 |
18 |
33 |
--------------------------------------------------------------------------------
/src/components/Home/ResultGraph.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
61 |
--------------------------------------------------------------------------------
/src/components/Home/SetAPIKey.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ $vuetify.t('$vuetify.index.setAPIKey') }}
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | {{ $vuetify.t('$vuetify.index.howToGetAPIKey') }}
13 |
14 |
15 |
16 |
17 |
18 | {{ $vuetify.t('$vuetify.index.confirm') }}
19 |
20 |
21 |
22 |
45 |
--------------------------------------------------------------------------------
/src/components/Index.css:
--------------------------------------------------------------------------------
1 | body {
2 | padding: 0px;
3 | margin: 0px;
4 | overflow: hidden;
5 | }
6 | .index-cards {
7 | margin: 5px auto;
8 | }
9 | .lang-ja {
10 | font-family: "ヒラギノ角ゴ Pro", "Hiragino Kaku Gothic Pro", "メイリオ", Meiryo, Osaka, "MS Pゴシック", "MS PGothic", "MS Gothic", "MS ゴシック", "Helvetica Neue", Helvetica, Arial, sans-serif
11 | }
12 | .lang-zh {
13 | font-family: "Hiragino Sans GB", "华文细黑", "STHeiti", "微软雅黑", "Microsoft YaHei", SimHei, "Helvetica Neue", Helvetica, Arial, sans-serif;
14 | }
15 | .lang-en {
16 | font-family: Arial, Helvetica, sans-serif;
17 | }
18 | .control-panel-container {
19 | right: 5px;
20 | top: 5px;
21 | position: absolute;
22 | display: flex;
23 | flex-direction: column;
24 | }
25 | .control-panel-container i {
26 | cursor: pointer;
27 | }
--------------------------------------------------------------------------------
/src/components/Index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | settings
5 | refresh
6 |
7 |
8 |
9 |
10 | 设置
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | {{ error.message }}
25 | ×
26 |
27 |
28 | {{ notice.message }}
29 | ×
30 |
31 |
32 |
33 |
34 |
35 | {{ $vuetify.t('$vuetify.index.fillBasicInfo') }}
36 |
37 |
41 |
48 |
49 |
50 |
51 |
52 |
53 | {{ $vuetify.t('$vuetify.index.setPollOptions') }}
54 |
55 | {{ $vuetify.t('$vuetify.index.addOption') }}
59 | {{ $vuetify.t('$vuetify.index.saveOptions') }}
65 |
66 |
67 |
68 |
69 |
70 |
71 | {{ $vuetify.t('$vuetify.control.title') }}
72 | {{ $vuetify.t('$vuetify.control.start') }}
78 | {{ $vuetify.t('$vuetify.control.end') }}
83 |
84 |
85 |
86 |
87 | {{ $vuetify.t('$vuetify.result.title') }}
88 |
89 |
90 |
91 |
92 |
93 | {{ $vuetify.t('$vuetify.display.title') }}
94 |
95 | http://localhost:9317
96 |
97 |
98 | {{ $vuetify.t('$vuetify.display.instruction') }}
99 |
100 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
350 |
351 |
353 |
--------------------------------------------------------------------------------
/src/components/Settings/About.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
12 | Open-sourced under GPLv3. © 2019, Eridanus Sora, member of MeowSound Idols.
13 |
14 |
15 |
16 |
17 | DevTools
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/components/Settings/Credential.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {{ $vuetify.t('$vuetify.setting.save') }}
8 |
9 |
10 |
11 |
12 |
13 |
32 |
--------------------------------------------------------------------------------
/src/components/Settings/Index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ $vuetify.t('$vuetify.setting.credential') }}
4 |
5 |
6 |
7 | {{ $vuetify.t('$vuetify.setting.about') }}
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/locale/en.js:
--------------------------------------------------------------------------------
1 | export default {
2 | 'index': {
3 | 'setAPIKey': 'Please Set YouTube API Key',
4 | 'confirm': 'Confirm',
5 | 'pleaseInputAPIKey': 'Please set YouTube API Key',
6 | 'pleaseInputVideoUrl': 'Please input live URL',
7 | 'fillBasicInfo': 'Basic Information',
8 | 'setPollOptions': 'Poll Options',
9 | 'addOption': 'Add Option',
10 | 'addOptionLabel': 'Option Label',
11 | 'pleaseInputOptionLabel': 'Please input option label',
12 | 'saveOptions': 'Save',
13 | 'optionSaved': 'Options Saved',
14 | 'howToGetAPIKey': 'How to get an API Key',
15 | 'howToGetAPIKeyLink': 'https://elfsight.com/help/how-to-get-youtube-api-key/'
16 | },
17 | 'control': {
18 | 'title': 'Control',
19 | 'start': 'Start Polling',
20 | 'end': 'End Polling',
21 | 'noVideoUrl': 'Please input live URL',
22 | 'failToGetVideoId': 'Fail to parse video',
23 | 'noOptions': 'No poll options set'
24 | },
25 | 'result': {
26 | 'title': 'Result'
27 | },
28 | 'display': {
29 | 'title': 'Result Display',
30 | 'instruction': 'Please add an browser source in OBS Studio with the address above. Recommended browser width: 1600.',
31 | 'collectPollRealtime': 'Display poll results in realtime'
32 | },
33 | 'setting': {
34 | 'credential': 'Credentials',
35 | 'about': 'About',
36 | 'setAPIKey': 'Set YouTube API Key',
37 | 'save': 'Save'
38 | },
39 | 'noDataText': 'No Data'
40 | }
--------------------------------------------------------------------------------
/src/locale/ja.js:
--------------------------------------------------------------------------------
1 | export default {
2 | 'index': {
3 | 'setAPIKey': 'YouTube API Keyの設定',
4 | 'confirm': '確認する',
5 | 'pleaseInputAPIKey': 'YouTube API Keyを正しく入力してください',
6 | 'pleaseInputVideoUrl': 'ライブURLを入力してください',
7 | 'fillBasicInfo': '基本情報',
8 | 'setPollOptions': '選択肢設定',
9 | 'addOption': '選択肢追加',
10 | 'addOptionLabel': '選択肢内容',
11 | 'pleaseInputOptionLabel': '選択肢の内容を入力してください',
12 | 'saveOptions': '保存する',
13 | 'optionSaved': '保存しました',
14 | 'howToGetAPIKey': 'API Keyの取得方法',
15 | 'howToGetAPIKeyLink': 'https://www.sourcenext.com/support/qa/?faq=HP-08470/'
16 | },
17 | 'control': {
18 | 'title': 'コントロール',
19 | 'start': '投票を開始',
20 | 'end': '投票を終了',
21 | 'noVideoUrl': 'ライブURLを入力してください',
22 | 'failToGetVideoId': 'ライブURLを分析できません',
23 | 'noOptions': '投票オプションが設定されていません'
24 | },
25 | 'result': {
26 | 'title': '結果'
27 | },
28 | 'display': {
29 | 'title': '結果表示',
30 | 'instruction': 'OBS Studioソースでブラウザを追加して上記のアドレスを入力してください;推奨ブラウザ幅:1600',
31 | 'collectPollRealtime': 'リアルタイム結果表示'
32 | },
33 | 'setting': {
34 | 'credential': 'クレデンシャル',
35 | 'about': 'バージョン情報',
36 | 'setAPIKey': 'YouTube API Keyの設定',
37 | 'save': '保存する'
38 | },
39 | 'noDataText': 'データがありません'
40 | }
41 |
--------------------------------------------------------------------------------
/src/locale/zh.js:
--------------------------------------------------------------------------------
1 | export default {
2 | 'index': {
3 | 'setAPIKey': '请设定 YouTube API Key',
4 | 'confirm': '确定',
5 | 'pleaseInputAPIKey': '请正确输入 YouTube API Key',
6 | 'pleaseInputVideoUrl': '请输入直播地址',
7 | 'fillBasicInfo': '基本信息填写',
8 | 'setPollOptions': '设置投票选项',
9 | 'addOption': '添加选项',
10 | 'addOptionLabel': '选项内容',
11 | 'pleaseInputOptionLabel': '请输入选项内容',
12 | 'saveOptions': '保存',
13 | 'optionSaved': '选项已保存',
14 | 'howToGetAPIKey': '如何获得API Key',
15 | 'howToGetAPIKeyLink': 'https://elfsight.com/help/how-to-get-youtube-api-key/'
16 | },
17 | 'control': {
18 | 'title': '控制',
19 | 'start': '开始投票',
20 | 'end': '结束投票',
21 | 'noVideoUrl': '请填写直播地址',
22 | 'failToGetVideoId': '无法解析视频地址',
23 | 'noOptions': '未设置投票选项'
24 | },
25 | 'result': {
26 | 'title': '结果'
27 | },
28 | 'display': {
29 | 'title': '展示',
30 | 'instruction': '请在 OBS Studio 中添加浏览器源,输入以上地址;推荐浏览器宽度:1600',
31 | 'collectPollRealtime': '实时反映投票结果'
32 | },
33 | 'setting': {
34 | 'credential': '凭据',
35 | 'about': '关于',
36 | 'setAPIKey': '设定 YouTube API Key',
37 | 'save': '保存'
38 | },
39 | 'noDataText': '无可用数据'
40 | }
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue'
2 | import './plugins/vuetify'
3 | import App from './App.vue'
4 | import store from './store/index';
5 |
6 | Vue.config.productionTip = false
7 |
8 | new Vue({
9 | store,
10 | render: h => h(App),
11 | }).$mount('#app')
12 |
--------------------------------------------------------------------------------
/src/plugins/vuetify.js:
--------------------------------------------------------------------------------
1 | import 'material-design-icons-iconfont/dist/material-design-icons.css'
2 | import Vue from 'vue'
3 | import Vuetify from 'vuetify/lib'
4 | import 'vuetify/src/stylus/app.styl'
5 | // Load language files
6 | import zh from '../locale/zh';
7 | import ja from '../locale/ja';
8 | import en from '../locale/en';
9 |
10 | Vue.use(Vuetify, {
11 | iconfont: 'md',
12 | lang: {
13 | locales: { zh, ja, en },
14 | current: navigator.language.slice(0, 2)
15 | }
16 | })
17 |
--------------------------------------------------------------------------------
/src/services/clients/bilibili.js:
--------------------------------------------------------------------------------
1 | import { EventEmitter } from 'events';
2 | import axios from 'axios';
3 | const { DanmuProvider, DanmuAutoParseStream } = require('danmulive');
4 |
5 | export default class Bilibili extends EventEmitter {
6 | constructor(url) {
7 | super();
8 | this.url = url;
9 | }
10 |
11 | async init() {
12 | this.roomId = await Bilibili.getRoomId(this.url);
13 | this.danmuParser = new DanmuAutoParseStream();
14 | this.danmuProvider = new DanmuProvider(this.roomId, this.danmuParser);
15 | }
16 |
17 | async connect() {
18 | this.danmuProvider.connect();
19 | this.danmuParser.on("data", data => {
20 | if (data.type == "danmu") {
21 | if (data.value.cmd.startsWith("DANMU_MSG")) {
22 | let userId = data.value.info[2][1];
23 | let message = data.value.info[1];
24 | this.emit('comment', {
25 | message,
26 | userId
27 | });
28 | }
29 | }
30 | });
31 | }
32 |
33 | disconnect() {
34 | this.danmuProvider.disconnect();
35 | }
36 |
37 | static async getRoomId(url) {
38 | const roomId = new URL(url).pathname.slice(1);
39 | if (roomId.length < 4) {
40 | // 短号
41 | const roomInfo = (await axios.get(`https://api.live.bilibili.com/room/v1/Room/get_info?id=${roomId}`)).data;
42 | return roomInfo.data.room_id;
43 | }
44 | return parseInt(roomId);
45 | }
46 | }
--------------------------------------------------------------------------------
/src/services/clients/youtube.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import utils from '../../utils/common';
3 | import { EventEmitter } from 'events';
4 | export default class YouTube extends EventEmitter {
5 | constructor(url, apiKey) {
6 | super();
7 | this.url = url;
8 | this.apiKey = apiKey;
9 | }
10 | async init() {
11 | this.status = "idle";
12 | this.videoId = await YouTube.getVideoId(this.url);
13 | this.chatId = await YouTube.getChatId(this.videoId, this.apiKey);
14 | }
15 | async connect() {
16 | this.status = "polling";
17 | this.startedAt = new Date();
18 | // eslint-disable-next-line
19 | while (true) {
20 | if (this.status === "idle") {
21 | // exit when stop
22 | break;
23 | }
24 | try {
25 | const messages = await YouTube.getChatMessages(
26 | this.chatId,
27 | this.apiKey,
28 | this.nextPageToken
29 | );
30 | this.nextPageToken = messages.nextPageToken;
31 | for (const item of messages.items) {
32 | // empty message
33 | if (!item.snippet.displayMessage) {
34 | continue;
35 | }
36 | // send before start
37 | if (
38 | new Date(item.snippet.publishedAt).valueOf() <
39 | this.startedAt.valueOf()
40 | ) {
41 | continue;
42 | }
43 | const userChannelId = item.authorDetails.channelId;
44 | this.emit('comment', {
45 | message: item.snippet.displayMessage,
46 | userId: userChannelId
47 | });
48 | }
49 | // cooldown
50 | await utils.sleep(messages.pollingIntervalMillis);
51 | } catch (e) {
52 | this.emit('error', e);
53 | await utils.sleep(3000);
54 | }
55 | }
56 | }
57 | async disconnect() {
58 | this.status = 'idle';
59 | }
60 | static async getVideoId(url) {
61 | let videoId;
62 | if (url.includes('youtu.be/')) {
63 | videoId = url.match(/\/(.+)/)[1];
64 | } else if (url.includes('channel')) {
65 | // get later
66 | } else if (url.includes('user')) {
67 | // get later
68 | } else {
69 | return new URLSearchParams(new URL(url).search).get('v');
70 | }
71 | const page = await axios.get(url);
72 | if (url.endsWith('/live')) {
73 | videoId = page.data.match(/{\\"videoId\\":\\"(.+?)\\"/)[1];
74 | } else {
75 | throw new Error('Not Supported');
76 | }
77 | return videoId;
78 | }
79 | static async getChatId(videoId, apiKey) {
80 | const url = `https://www.googleapis.com/youtube/v3/videos?part=liveStreamingDetails&id=${videoId}&key=${apiKey}`;
81 | const result = (await axios.get(url)).data;
82 | return result.items[0].liveStreamingDetails.activeLiveChatId;
83 | }
84 | static async getChatMessages(chatId, apiKey, pageToken) {
85 | let url = `https://www.googleapis.com/youtube/v3/liveChat/messages?part=snippet,authorDetails&liveChatId=${chatId}&key=${apiKey}`;
86 | if (pageToken) {
87 | url += `&pageToken=${pageToken}`;
88 | }
89 | const result = (await axios.get(url)).data;
90 | return result;
91 | }
92 | }
--------------------------------------------------------------------------------
/src/services/comment.js:
--------------------------------------------------------------------------------
1 | import { EventEmitter } from 'events';
2 | import YouTube from './clients/youtube';
3 | import Bilibili from './clients/bilibili';
4 | class CommentListener extends EventEmitter {
5 | constructor({
6 | url,
7 | apiKey
8 | }) {
9 | super();
10 | this.url = url;
11 | this.apiKey = apiKey;
12 | }
13 |
14 | async init() {
15 | if (this.url.includes('youtu.be') || this.url.includes('youtube.com')) {
16 | this.client = new YouTube(this.url, this.apiKey);
17 | await this.client.init();
18 | } else if (this.url.includes('live.bilibili.com')) {
19 | this.client = new Bilibili(this.url);
20 | await this.client.init();
21 | }
22 | }
23 |
24 | connect() {
25 | this.client.on('comment', (comment) => {
26 | this.emit('comment', comment);
27 | });
28 | this.client.on('error', (e) => {
29 | this.emit('error', e);
30 | });
31 | this.client.connect();
32 | }
33 |
34 | disconnect() {
35 | this.client.disconnect();
36 | }
37 | }
38 |
39 | export default CommentListener;
--------------------------------------------------------------------------------
/src/services/version.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | const electron = require('electron');
3 | export default class Version {
4 | static async getLatestVersion() {
5 | return (await axios.get('https://api.github.com/repos/Last-Order/Monita/releases/latest')).data;
6 | }
7 | static getLocalVersion() {
8 | return electron.remote.app.getVersion();
9 | }
10 | }
--------------------------------------------------------------------------------
/src/store/index.js:
--------------------------------------------------------------------------------
1 | import Vuex from 'vuex';
2 | import Vue from 'vue';
3 | Vue.use(Vuex);
4 |
5 | const store = new Vuex.Store({
6 | state: {
7 |
8 | },
9 | mutations: {
10 |
11 | }
12 | });
13 |
14 | export default store;
--------------------------------------------------------------------------------
/src/utils/common.js:
--------------------------------------------------------------------------------
1 | export default {
2 | sleep: async delay => new Promise(resolve => setTimeout(resolve, delay))
3 | }
--------------------------------------------------------------------------------
/vue.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | devServer: {
3 | host: 'localhost',
4 | disableHostCheck: true
5 | },
6 | transpileDependencies: [
7 | 'vue-echarts',
8 | 'resize-detector'
9 | ],
10 | pluginOptions: {
11 | electronBuilder: {
12 | chainWebpackMainProcess: config => {
13 | // Chain webpack config for electron main process only
14 | config.mode('development');
15 | // config.externals(['uws']);
16 | },
17 | externals: ['socket.io'],
18 | builderOptions: {
19 | appId: 'moe.sound.sora.ylp',
20 | // asar: false,
21 | artifactName: "ylp-${os}-${version}.${ext}",
22 | productName: 'YouTube Live Poll',
23 | win: {
24 | target: ['portable', 'msi'],
25 | icon: 'build/icons/icon.ico',
26 | },
27 | nsis: {
28 | oneClick: false,
29 | allowToChangeInstallationDirectory: true
30 | },
31 | mac: {
32 | category: 'public.app-category.developer-tools',
33 | target: ['dmg']
34 | },
35 | linux: {
36 | target: ['deb', 'appImage']
37 | }
38 | }
39 | }
40 | }
41 | }
--------------------------------------------------------------------------------