diff --git a/.eslintrc.js b/.eslintrc.js index 451528d..39139e2 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -48,5 +48,25 @@ module.exports = { 'code': 100, 'ignorePattern': '\\(resolve, reject\\) =>', }], + 'new-cap': 'off', + 'object-curly-newline': ['error', { consistent: true }], + 'nonblock-statement-body-position': ['off', 'any'], + 'no-plusplus': 'off', + 'import/order': 'off', + 'arrow-parens': ['error', 'as-needed'], + 'no-restricted-syntax': 'off', + 'implicit-arrow-linebreak': 'off', + 'function-paren-newline': ['error', 'consistent'], + 'no-restricted-globals': 'off', + 'prefer-destructuring': ['error', { + VariableDeclarator: { + array: false, + object: true, + }, + AssignmentExpression: { + array: false, + object: false, + }, + }], } }; diff --git a/.gitignore b/.gitignore index e8eeabc..ae68a2b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /node_modules +/build npm-debug.log .DS_Store @@ -6,4 +7,7 @@ npm-debug.log /server/backend/storage/**/*.db/ /server/backend/storage/**/*.files/ -/Console\ Lite-*-*/ +/Console\ Lite*/ +/Console-Lite-* + +/VERSION diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..3d700cb --- /dev/null +++ b/.travis.yml @@ -0,0 +1,26 @@ +sudo: false +os: +- linux +- osx +addons: + apt: + packages: + - clang +install: +- export CXX="clang++" +- npm install yarn --global +- yarn install --frozen-lockfile +language: node_js +node_js: +- '10' +script: +- yarn run rebuildNative +- yarn test +deploy: +- provider: script + skip_cleanup: true + script: node bin/deploy-travis.js + on: + repo: CircuitCoder/Console-Lite + all_branches: true + node: '10' diff --git a/README.md b/README.md index 53a86a4..53517c9 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,32 @@ # Console Lite -可以,这很现代化 +[![Travis](https://img.shields.io/travis/CircuitCoder/Console-Lite.svg?style=flat-square)](https://travis-ci.org/CircuitCoder/Console-Lite) +[![AppVeyor](https://img.shields.io/appveyor/ci/CircuitCoder/console-lite.svg?style=flat-square)](https://ci.appveyor.com/project/CircuitCoder/console-lite) +[![David](https://img.shields.io/david/CircuitCoder/Console-Lite.svg?style=flat-square)](https://david-dm.org/CircuitCoder/Console-Lite) +[![David Dev](https://img.shields.io/david/dev/CircuitCoder/Console-Lite.svg?style=flat-square)](https://david-dm.org/CircuitCoder/Console-Lite) -## 开发 -在 Clone 项目过后,请使用以下指令安装依赖: +## Develop ```bash -npm install -npm run rebuildNative # Rebuild native modules for Electron -``` +# Install dependencies +yarn install --frozen-lockfile +yarn rebuildNative # Rebuild native modules for Electron -启动应用: +# Start from source: +yarn start -```bash -npm start +# Start only the server: +yarn server ``` -启动服务器: +To build the executable package, please run the following commands. ```bash -npm run server +yarn pack ``` -生成可执行文件: - -```bash -npm prune --producation # 删除开发依赖 -npm install electron-packager # 重新安装打包器 -npm install electron # 重新安装Electron -npm run pack -npm install # 重新安装开发依赖 -``` +If you want to run from source again, you need to reinstall the dev dependencies -## 许可证 +## License -本项目所有代码在 MIT 协议下发布,详细信息请参考 LICENSE 文件 +All source code within this repository are released under the MIT License. For a detailed license file, please refer to LICENSE. diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 0000000..f08e095 --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,28 @@ +platform: +- x86 +- x64 + +matrix: + fast_finish: true + +init: +- git config --global core.autocrlf true + +- npm install yarn --global + +shallow_clone: true + +install: +- ps: Install-Product node 10 $env:platform +- node -e "console.log(process.platform + '-' + process.arch)" +- node -e "console.log(require('os').platform() + '-' + require('os').arch())" +- yarn install --frozen-lockfile + +build_script: +- yarn run rebuildNative + +test_script: +- yarn test + +deploy_script: +- node bin/deploy-appveyor.js diff --git a/bin/deploy-appveyor.js b/bin/deploy-appveyor.js new file mode 100644 index 0000000..55d0e1e --- /dev/null +++ b/bin/deploy-appveyor.js @@ -0,0 +1,130 @@ +/* eslint-disable camelcase */ + +const pack = require('./pack'); +const { upload, trim, runTasks, Ping, getAppDir } = require('./deploy-util'); + +const assert = require('assert'); +const child_process = require('child_process'); +const fs = require('fs'); +const listr = require('listr'); +const os = require('os'); +const path = require('path'); +const process = require('process'); +const rimraf = require('rimraf'); + +const { Observable } = require('rxjs'); + +const basedir = path.dirname(__dirname); +const targetdir = path.join(basedir, 'Console Lite'); + +const runListr = process.env.RUN_LISTR === 'true'; + +let tag; + +if(process.env.APPVEYOR_REPO_TAG && /^v\d+\.\d+\.\d+/.test(process.env.APPVEYOR_REPO_TAG_NAME)) + tag = process.env.APPVEYOR_REPO_TAG_NAME; +else if(process.env.TEST_UPLOAD) + tag = `COMMIT_${process.env.APPVEYOR_REPO_COMMIT}`; +else + process.exit(0); + +// Context +let paths; +const artifacts = []; + +const mainTasks = [ + { + title: 'Packaging', + task: () => new Promise((resolve, reject) => pack((err, _paths) => { + if(err) return reject(err); + + paths = _paths; + return resolve(); + }, true)), + skip: () => process.env.SKIP_PACKAGING === 'true', + }, { + title: 'Zipping', + task: () => { + const tasks = [ + { + title: 'Creating archive with fonts', + task: () => new Observable(ob => { + assert(paths.length === 1); + const p = paths[0]; + + rimraf.sync(targetdir); + fs.renameSync(p, targetdir); + + const fname = `Console-Lite-${tag}-${os.platform()}-${os.arch()}.7z`; + fs.writeFileSync(path.join(getAppDir(targetdir), 'VERSION'), fname); + + ob.next(`Writing to: ${fname}`); + + child_process + .spawnSync('7z', ['a', '-t7z', '-m0=lzma', '-mx=9', + path.join(basedir, fname), targetdir]); + + artifacts.push([ + fname, + path.join(basedir, fname), + { 'Content-Type': 'application/7z' }, + ]); + + ob.complete(); + }), + }, { + title: 'Trimming fonts', + task: () => trim(targetdir, artifacts), + }, { + title: 'Creating archive without fonts', + task: () => new Observable(ob => { + const fname = `Console-Lite-${tag}-${os.platform()}-${os.arch()}-nofont.7z`; + fs.writeFileSync(path.join(getAppDir(targetdir), 'VERSION'), fname); + + ob.next(`Writing to: ${fname}`); + + child_process + .spawnSync('7z', ['a', '-t7z', '-m0=lzma', '-mx=9', + path.join(basedir, fname), targetdir]); + + artifacts.push([ + fname, + path.join(basedir, fname), + { 'Content-Type': 'application/7z' }, + ]); + + ob.complete(); + }), + }, + ]; + + if(runListr) return new listr(tasks); + else return runTasks(tasks); + }, + skip: () => process.env.SKIP_ZIPPING === 'true', + }, { + title: 'Uploading', + task: () => { + const tasks = upload(artifacts).map((p, i) => ({ + title: artifacts[i][0], + task: () => p, + })); + + if(runListr) return new listr(tasks, { concurrent: true }); + else return runTasks(tasks, true); + }, + }, +]; + +if(runListr) + new listr(mainTasks).run().catch(e => { + console.error(e.stack); + process.exit(1); + }); +else { + const p = new Ping(); + runTasks(mainTasks).catch(e => { + console.error(e.stack); + process.exit(1); + }).then(() => p.stop()); +} diff --git a/bin/deploy-travis.js b/bin/deploy-travis.js new file mode 100644 index 0000000..56923ab --- /dev/null +++ b/bin/deploy-travis.js @@ -0,0 +1,139 @@ +const pack = require('./pack'); +const { upload, trim, runTasks, Ping, getAppDir } = require('./deploy-util'); + +const assert = require('assert'); +const fs = require('fs'); +const listr = require('listr'); +const os = require('os'); +const path = require('path'); +const process = require('process'); +const rimraf = require('rimraf'); +const tar = require('tar'); +const zlib = require('zlib'); + +const { Observable } = require('rxjs'); + +let tag; + +if(process.env.TRAVIS_TAG && /^v\d+\.\d+\.\d+/.test(process.env.TRAVIS_TAG)) + tag = process.env.TRAVIS_TAG; +else if(process.env.TEST_UPLOAD) + tag = `COMMIT_${process.env.TRAVIS_COMMIT}`; +else + process.exit(0); + +// Context +let paths = [path.join(__dirname, '..', 'Console Lite')]; +const artifacts = []; + +const gzipOpt = { + memLevel: 9, + level: 9, +}; + +const basedir = path.dirname(__dirname); +const targetdir = path.join(basedir, 'Console Lite'); + +const runListr = process.env.RUN_LISTR === 'true'; + +const mainTasks = [ + { + title: 'Packaging', + task: () => new Promise((resolve, reject) => pack((err, _paths) => { + if(err) return reject(err); + + paths = _paths; + return resolve(); + }, true)), + skip: () => process.env.SKIP_PACKAGING === 'true', + }, { + title: 'Zipping', + task: () => { + const tasks = [ + { + title: 'Creating archive with fonts', + task: () => new Observable(ob => { + assert(paths.length === 1); + const p = paths[0]; + + if(p !== targetdir) { + rimraf.sync(targetdir); + fs.renameSync(p, targetdir); + } + + const fname = `Console-Lite-${tag}-${os.platform()}-${os.arch()}.tar.gz`; + fs.writeFileSync(path.join(getAppDir(targetdir), 'VERSION'), fname); + + ob.next(`Writing to: ${fname}`); + + tar.c({}, [targetdir]) + .pipe(zlib.createGzip(gzipOpt)) + .pipe(fs.createWriteStream(path.join(basedir, fname))) + .on('finish', () => { + artifacts.push([ + fname, + path.join(basedir, fname), + { 'Content-Type': 'application/tar+gzip' }, + ]); + + ob.complete(); + }) + .on('error', err => ob.error(err)); + }), + }, { + title: 'Trimming fonts', + task: () => trim(targetdir, artifacts), + }, { + title: 'Creating archive without fonts', + task: () => new Observable(ob => { + const fname = `Console-Lite-${tag}-${os.platform()}-${os.arch()}-nofont.tar.gz`; + fs.writeFileSync(path.join(getAppDir(targetdir), 'VERSION'), fname); + + ob.next(`Writing to: ${fname}`); + + tar.c({}, [targetdir]) + .pipe(zlib.createGzip(gzipOpt)) + .pipe(fs.createWriteStream(path.join(basedir, fname))) + .on('finish', () => { + artifacts.push([ + fname, + path.join(basedir, fname), + { 'Content-Type': 'application/tar+gzip' }, + ]); + ob.complete(); + }) + .on('error', err => ob.error(err)); + }), + }, + ]; + + if(runListr) return new listr(tasks); + else return runTasks(tasks); + }, + skip: () => process.env.SKIP_ZIPPING === 'true', + }, { + title: 'Uploading', + task: () => { + const tasks = upload(artifacts).map((p, i) => ({ + title: artifacts[i][0], + task: () => p, + })); + + if(runListr) return new listr(tasks, { concurrent: true }); + else return runTasks(tasks, true); + }, + }, +]; + +if(runListr) + new listr(mainTasks).run().catch(e => { + console.error(e.stack); + process.exit(1); + }); +else { + const p = new Ping(); + runTasks(mainTasks).catch(e => { + console.error(e.stack); + process.exit(1); + }).then(() => p.stop()); +} diff --git a/bin/deploy-util.js b/bin/deploy-util.js new file mode 100644 index 0000000..2ae660b --- /dev/null +++ b/bin/deploy-util.js @@ -0,0 +1,90 @@ +const minio = require('minio'); +const os = require('os'); +const path = require('path'); +const process = require('process'); +const rimraf = require('rimraf'); + +const mc = new minio.Client({ + endPoint: 'store.easymun.com', + useSSL: true, + accessKey: process.env.MINIO_ACCESS_KEY, + secretKey: process.env.MINIO_SECRET_KEY, +}); + +function upload(artifacts) { + return artifacts.map(([name, dir, meta]) => new Promise((resolve, reject) => { + console.log(name); + console.log(dir); + console.log(meta); + mc.fPutObject('console-lite', name, dir, meta, + (err, etag) => err ? reject(err) : resolve([name, dir, etag])); + })); +} + +function getAppDir(targetdir) { + if(os.platform() === 'darwin') + return path.join(targetdir, 'Console Lite.app', 'Contents', 'Resources', 'app'); + else + return path.join(targetdir, 'resources', 'app'); +} + +function trim(targetdir) { + const fontbase = path.join(getAppDir(targetdir), 'fonts'); + + rimraf.sync(path.join(fontbase, 'NotoSansCJKsc-*')); + rimraf.sync(path.join(fontbase, 'Roboto-*')); +} + +function _taskToPromise(task) { + const inst = task(); + if(!inst) return Promise.resolve(); + else if(inst.subscribe) return new Promise((resolve, reject) => { + inst.subscribe({ + next: data => console.log(data), + error: reject, + complete: resolve, + }); + }); + else if(inst.then) return inst; + else return Promise.resolve(); // Sync method +} + +function runTasks(tasks, concurrent) { + if(concurrent) { + console.log('Running concurrently:'); + for(const { title } of tasks) console.log(` - ${title}`); + + // TODO: support skip + return Promise.all(tasks.map(({ title, task }) => _taskToPromise(task).then(() => { + console.log(`Completed: ${title}`); + }))); + } else + return tasks.reduce((prev, { title, task, skip }) => prev.then(() => { + if(skip && skip()) return console.log(`Skipped: ${title}`); + + console.log(`Running: ${title}`); + return _taskToPromise(task).then(() => { + console.log(`Completed: ${title}`); + }); + }), Promise.resolve()); +} + +class Ping { + constructor() { + this.intervalId = setInterval(() => { + console.log('PING'); + }, 1000 * 60); + } + + stop() { + clearInterval(this.intervalId); + } +} + +module.exports = { + upload, + trim, + runTasks, + Ping, + getAppDir, +}; diff --git a/bin/pack.js b/bin/pack.js index a7dae83..910113a 100644 --- a/bin/pack.js +++ b/bin/pack.js @@ -2,29 +2,63 @@ const packager = require('electron-packager'); const process = require('process'); const path = require('path'); -console.log(`Building package for ${process.platform} - ${process.arch}.`); -console.log('Please ensure that native dependecies are built using correct ABI version'); - -const opt = { - arch: process.arch, - platform: process.platform, - dir: path.join(__dirname, '..'), - ignore: [ - /^\/server\/.*\.db($|\/)/, - /^\/server\/.*\.files($|\/)/, - ], // Ignores databases and files - tmpdir: false, - icon: path.join(__dirname, '../images/icon'), -}; - -if(process.env.ELECTRON_MIRROR) - opt.download = { - mirror: process.env.ELECTRON_MIRROR, +function pack(cb, silent) { + if(!silent) { + console.log(`Building package for ${process.platform} - ${process.arch}.`); + console.log('Please ensure that native dependecies are built using correct ABI version'); + } + + const opt = { + arch: process.arch, + platform: process.platform, + out: path.dirname(__dirname), + dir: path.dirname(__dirname), + prune: true, + ignore: [ + /^\/server\/.*\.db($|\/)/, + /^\/server\/.*\.files($|\/)/, + /^\/Console Lite/, + /^\/Console-Lite-/, + ], // Ignores databases, files and artifacts + icon: path.join(__dirname, '../images/icon'), }; -packager(opt, (err, paths) => { - if(err) { - console.error('Packager failed:'); - console.error(err.stack); - } else console.log(`Package outputted to: ${paths}`); -}); + if(process.env.ELECTRON_MIRROR) + opt.download = { + mirror: process.env.ELECTRON_MIRROR, + }; + + packager(opt) + .then(paths => { + console.log(paths); + if(!silent) + console.log(`Package outputted to: ${paths}`); + if(cb) cb(null, paths); + }) + .catch(err => { + if(!silent) { + console.error('Packager failed:'); + console.error(err.stack); + } + if(cb) cb(err); + }); +} + +/* eslint-disable global-require */ + +if(require.main === module) { + const ora = require('ora'); + const ind = ora('Packaging').start(); + + pack((err, paths) => { + if(err) { + ind.fail(); + console.error(err.stack); + } else { + ind.text = `Package outputted to: ${paths}`; + ind.succeed(); + } + }, true); +} + +module.exports = pack; diff --git a/bin/rebuild.js b/bin/rebuild.js index 94355dc..1a6f62d 100644 --- a/bin/rebuild.js +++ b/bin/rebuild.js @@ -1,32 +1,48 @@ -const { - installNodeHeaders, - rebuildNativeModules, - shouldRebuildNativeModules } = require('electron-rebuild'); -const pathToElectron = require('electron'); -const childProcess = require('child_process'); +const { rebuild } = require('electron-rebuild'); -console.log('Rebuilding native modules...'); +const process = require('process'); +const fs = require('fs'); +const path = require('path'); -shouldRebuildNativeModules(pathToElectron) - .then(shouldRebuild => { - if(!shouldRebuild) { - console.log('Native modules ready.'); - return; +function locateElectronBase() { + let electronPath = path.join(__dirname, '..', '..', 'electron'); + if(!fs.existsSync(electronPath)) + electronPath = path.join(__dirname, '..', '..', 'electron-prebuilt'); + if(!fs.existsSync(electronPath)) + electronPath = path.join(__dirname, '..', '..', 'electron-prebuilt-compile'); + if(!fs.existsSync(electronPath)) + try { + electronPath = path.join(require.resolve('electron'), '..'); + } catch(e) { + // Module not found, do nothing + } + if(!fs.existsSync(electronPath)) + try { + electronPath = path.join(require.resolve('electron-prebuilt'), '..'); + } catch(e) { + // Module not found, do nothing } + if(!fs.existsSync(electronPath)) + electronPath = null; + return electronPath; +} + +console.log('Rebuilding native modules...'); + +const electronBase = locateElectronBase(); +const electronPkg = JSON.parse(fs.readFileSync(path.join(electronBase, 'package.json'))); +const electronVer = electronPkg.version; + +console.log(`Electron version: ${electronVer}`); - const electronVersion = childProcess.execSync(`${pathToElectron} --version`, { - encoding: 'utf8', - }).match(/v(\d+\.\d+\.\d+)/)[1]; +const headerURL = process.env.ELECTRON_HEADER; - return void installNodeHeaders(electronVersion, 'https://atom.io/download/atom-shell') - .then(() => rebuildNativeModules(electronVersion, - `${__dirname}/../node_modules`, - '--build-from-source')); - }) - .then(() => { - console.log('Rebuilding finished.'); - }) +rebuild({ + buildPath: path.resolve(__dirname, '..'), + electronVersion: electronVer, + headerURL, +}) + .then(() => console.info('Rebuild successful')) .catch(e => { - console.error('Rebuilding failed:'); - console.error(e.stack); + console.error(e); }); diff --git a/controller/action.js b/controller/action.js index 109d5a0..ca0de5a 100644 --- a/controller/action.js +++ b/controller/action.js @@ -1,12 +1,10 @@ -const Vue = require('vue'); -const VueAnimatedList = require('vue-animated-list'); -Vue.use(VueAnimatedList); +const Vue = require('vue/dist/vue.common.js'); -const io = require('socket.io-client/socket.io.js'); +const io = require('socket.io-client'); const Push = require('push.js'); const polo = require('polo'); const path = require('path'); -const { ipcRenderer } = require('electron'); +const { ipcRenderer, shell } = require('electron'); const { clipboard } = require('electron').remote; const GlobalConnection = require('./connection/global'); @@ -14,8 +12,8 @@ const ConferenceConnection = require('./connection/conference'); const util = require('../shared/util.js'); -require('../shared/components/timer'); -require('../shared/components/timer-input'); +const Timer = require('../shared/components/timer'); +const TimerInput = require('../shared/components/timer-input'); let globalConn; let confConn; @@ -25,13 +23,13 @@ let serverConfig; let connectedConf; const desc = { - el: 'body', data: { started: false, ready: false, loading: false, picker: false, frame: false, + server: false, projOn: false, proj: { @@ -45,6 +43,11 @@ const desc = { services: [], showServerFlag: false, + advancedBackendFlag: false, + backendPort: 4928, + backendHint: null, + backendPasskeySetting: null, + connectBackendFlag: false, backendIDKey: '', backendUrl: '', @@ -94,13 +97,21 @@ const desc = { file: require('./views/file/file'), vote: require('./views/vote/vote'), list: require('./views/list/list'), + + Timer, + TimerInput, }, /* eslint-enable global-require */ + mounted() { + this.init(); + }, + methods: { init() { this.started = true; + this.server = ipcRenderer.sendSync('isServerRunning'); this.projOn = ipcRenderer.sendSync('getProjector') !== null; this.sendToProjector({ type: 'reset' }); @@ -117,18 +128,28 @@ const desc = { poloRepo.on('up', (name, service) => { if(name.indexOf('console-lite') !== 0) return; + const ident = name.substring(13); + const matched = ident.match(/^([0-9A-F]+):(.*)$/); + let hint = null; + let idkey = ident; + if(matched) { + idkey = matched[1]; + hint = matched[2]; + } + this.services.push({ name, - idkey: name.substring(13), + hint, + idkey, host: service.host, port: service.port, }); }); - poloRepo.on('down', (name) => { - for(const s of this.services) - if(s.name === name) { - this.services.$remove(s); + poloRepo.on('down', name => { + for(let id = 0; id < this.services.length; ++id) + if(this.services[id].name === name) { + this.services.splice(id, 1); break; } }); @@ -153,7 +174,7 @@ const desc = { this.authorized = authorized; if(!this.authorized) - if(!confirm('密码错误,是否在只读模式连接?')) { + if(serverConfig.passkey && !confirm('密码错误,是否在只读模式连接?')) { this.confs = null; this.authorized = false; this.connectBackendFlag = true; @@ -172,8 +193,10 @@ const desc = { applyService(service) { this.backendUrl = `http://${service.host}:${service.port}`; - this.$els.connectOverlap.scrollTop = - this.$els.connectOverlap.scrollHeight - this.$els.connectOverlap.offsetHeight; + + /* eslint-disable-next-line */ // Overflows + this.$refs.connectOverlap.scrollTop = + this.$refs.connectOverlap.scrollHeight - this.$refs.connectOverlap.offsetHeight; }, connectBackend() { @@ -183,7 +206,7 @@ const desc = { }, performBackendConnection() { - if(this.backendUrl === '' || this.backendPasskey === '') return; + if(this.backendUrl === '') return; serverConfig = { url: this.backendUrl, passkey: this.backendPasskey, @@ -212,11 +235,27 @@ const desc = { this._createGlobalConn(); }); - ipcRenderer.send('startServer'); + const port = parseInt(this.backendPort, 10); + if(Number.isNaN(port)) return; + const hint = this.backendHint || null; + const passkey = this.backendPasskeySetting || null; + + this.advancedBackendFlag = false; + ipcRenderer.send('startServer', { port, hint, passkey }); + + this.loading = true; + }, + advancedBackend() { + this.advancedBackendFlag = true; this.loading = true; }, + discardAdvancedBackend() { + this.advancedBackendFlag = false; + this.loading = false; + }, + connectConf(confId, confName) { if(confConn && confConn.connected) confConn.disconnect(); @@ -224,9 +263,7 @@ const desc = { console.log(`Connecting to: ${serverConfig.url}/${confId}`); const socket = io(`${serverConfig.url}/${confId}`, { - extraHeaders: { - 'Console-Passkey': serverConfig.passkey, - }, + query: `console-passkey=${serverConfig.passkey}`, }); confConn = new ConferenceConnection(socket, ({ error, data }) => { @@ -282,7 +319,7 @@ const desc = { confConn.addListener({ /* Seats */ - seatsUpdated: (seats) => { + seatsUpdated: seats => { this.seats = seats; this.recalcCount(); this.sendSeatCount(); @@ -343,7 +380,7 @@ const desc = { } }, - timerStopped: (id /* , value */) => { + timerStopped: id => { this.executeOnTimer(id, timer => { timer.active = false; }); @@ -405,7 +442,7 @@ const desc = { }); }, - fileEdited: (id) => { + fileEdited: id => { this.fileCache[id] = null; for(const f of this.files) @@ -446,7 +483,7 @@ const desc = { v.matrix[index].vote = vote; if(v === this.vote && !v.status.running) - this.$broadcast('vote-rearrange'); + this.$refs.internal.$emit('vote-rearrange'); if(this.proj.mode === 'vote' && v === this.proj.target) this.sendToProjector({ @@ -464,7 +501,7 @@ const desc = { v.status = status; if(v === this.vote && !status.running) - this.$broadcast('vote-rearrange'); + this.$refs.internal.$emit('vote-rearrange'); if(this.proj.mode === 'vote' && v === this.proj.target) this.sendToProjector({ @@ -487,8 +524,15 @@ const desc = { if(timer.type === 'list-total') timerTotal = timer; else if(timer.type === 'list-current') timerCurrent = timer; - if(timerTotal) this.timerWaitingList.$remove(timerTotal); - if(timerCurrent) this.timerWaitingList.$remove(timerCurrent); + if(timerTotal) { + const index = this.timerWaitingList.indexOf(timerTotal); + if(index >= 0) this.timerWaitingList.splice(index, 1); + } + + if(timerCurrent) { + const index = this.timerWaitingList.indexOf(timerCurrent); + if(index >= 0) this.timerWaitingList.splice(index, 1); + } this.lists.unshift({ id, name, seats, timerTotal, timerCurrent, ptr: 0, @@ -522,12 +566,12 @@ const desc = { createConf() { this.confName = ''; this.createConfFlag = true; - setTimeout(() => this.$els.confNameInput.focus(), 0); + setTimeout(() => this.$refs.confNameInput.focus(), 0); }, performConfCreation() { if(this.confName === '') return; - globalConn.createConf(this.confName, (data) => { + globalConn.createConf(this.confName, data => { if(!data.ok) { console.error(data.error); alert('创建失败'); @@ -582,7 +626,7 @@ const desc = { /* Timers */ addTimer(name, sec) { - confConn.addTimer(name, 'standalone', sec, (err /* , id */) => { + confConn.addTimer(name, 'standalone', sec, err => { if(err) { console.error(err); alert('添加失败!'); @@ -591,7 +635,7 @@ const desc = { }, manipulateTimer(action, id) { - confConn.manipulateTimer(action, id, (err) => { + confConn.manipulateTimer(action, id, err => { if(err) { console.error(err); alert('操作失败!'); @@ -600,7 +644,7 @@ const desc = { }, updateTimer(id, value) { - confConn.updateTimer(id, value, (err) => { + confConn.updateTimer(id, value, err => { if(err) { console.error(err); alert('修改失败!'); @@ -671,14 +715,14 @@ const desc = { return this.sendToProjector({ type: 'layer', target: 'file', - data: { meta: file, content: new Uint8Array(content) }, + data: { meta: file, content }, }); }); }, /* Vote */ addVote(name, target, rounds, seats) { - confConn.addVote(name, target, rounds, seats, (err /* , id */) => { + confConn.addVote(name, target, rounds, seats, err => { if(err) { console.error(err); alert('添加失败!'); @@ -697,7 +741,7 @@ const desc = { }, updateVote(id, index, vote) { - confConn.updateVote(id, index, vote, (err /* , id */) => { + confConn.updateVote(id, index, vote, err => { if(err) { console.error(err); alert('更新失败!'); @@ -706,7 +750,7 @@ const desc = { }, iterateVote(id, status) { - confConn.iterateVote(id, status, (err /* , id */) => { + confConn.iterateVote(id, status, err => { if(err) { console.error(err); alert('更新失败!'); @@ -724,7 +768,7 @@ const desc = { addList(name, seats, totTime, eachTime) { new Promise((resolve, reject) => confConn.addList(name, seats, (err, id) => err ? reject(err) : resolve(id))) .then(id => Promise.all([ - new Promise((resolve, reject) => totTime === 0 ? resolve() : confConn.addTimer(id, 'list-total', totTime, err => err ? reject(err) : resolve())), + new Promise((resolve, reject) => confConn.addTimer(id, 'list-total', totTime, err => err ? reject(err) : resolve())), new Promise((resolve, reject) => confConn.addTimer(id, 'list-current', eachTime, err => err ? reject(err) : resolve())), ])).catch(e => { console.error(e); @@ -870,15 +914,27 @@ const desc = { // eslint-disable-next-line no-unused-vars function setup() { const instance = new Vue(desc); - instance.init(); + instance.$mount('#app'); - document.addEventListener('drop', (e) => { + document.addEventListener('drop', e => { e.preventDefault(); e.stopPropagation(); }); - document.addEventListener('dragover', (e) => { + document.addEventListener('dragover', e => { e.preventDefault(); e.stopPropagation(); }); + + ipcRenderer.once('updateAvailable', (event, { detail, version }) => { + Push.create(`软件更新: ${version}`, { + body: '点击开始下载', + timeout: 10000, + onClick: () => { + shell.openExternal(`https://store.bjmun.org/console-lite/${detail.name}`); + }, + }); + }); + + ipcRenderer.send('checkForUpdate'); } diff --git a/controller/connection/conference.js b/controller/connection/conference.js index e6b07a7..dae3a8a 100644 --- a/controller/connection/conference.js +++ b/controller/connection/conference.js @@ -29,7 +29,7 @@ class ConferenceConnection { this.pushSocketListener('voteUpdated', ['id', 'index', 'vote']); this.pushSocketListener('voteIterated', ['id', 'status']); - /* Lists*/ + /* Lists */ this.pushSocketListener('listAdded', ['id', 'name', 'seats']); this.pushSocketListener('listUpdated', ['id', 'seats']); @@ -37,7 +37,7 @@ class ConferenceConnection { } pushSocketListener(name, fields) { - this.socket.on(name, (data) => { + this.socket.on(name, data => { for(const l of this.listeners) if(name in l) l[name](...fields.map(e => data[e])); }); @@ -51,7 +51,7 @@ class ConferenceConnection { /* Seats */ updateSeats(seats, cb) { - this.socket.once('updateSeats', (data) => { + this.socket.once('updateSeats', data => { if(data.ok) cb(null); else cb(data.error); }); @@ -61,7 +61,7 @@ class ConferenceConnection { /* Timers */ addTimer(name, type, value, cb) { - this.socket.once('addTimer', (data) => { + this.socket.once('addTimer', data => { if(data.ok) cb(null, data.id); else cb(data.error); }); @@ -74,7 +74,7 @@ class ConferenceConnection { */ manipulateTimer(action, id, cb) { const token = `${action}Timer`; - this.socket.once(token, (data) => { + this.socket.once(token, data => { if(data.ok) cb(null); else cb(data.error); }); @@ -83,7 +83,7 @@ class ConferenceConnection { } updateTimer(id, value, cb) { - this.socket.once('updateTimer', (data) => { + this.socket.once('updateTimer', data => { if(data.ok) cb(null); else cb(data.error); }); @@ -94,7 +94,7 @@ class ConferenceConnection { /* Files */ addFile(name, type, content, cb) { - this.socket.once('addFile', (data) => { + this.socket.once('addFile', data => { if(data.ok) cb(null, data.id); else cb(data.error); }); @@ -103,7 +103,7 @@ class ConferenceConnection { } editFile(id, content, cb) { - this.socket.once('editFile', (data) => { + this.socket.once('editFile', data => { if(data.ok) cb(null); else cb(data.error); }); @@ -114,7 +114,7 @@ class ConferenceConnection { getFile(id, cb) { const respToken = `getFile:${id}`; - this.socket.once(respToken, (data) => { + this.socket.once(respToken, data => { if(data.ok) cb(null, data.content); else cb(data.error); }); @@ -124,7 +124,7 @@ class ConferenceConnection { /* Votes */ addVote(name, target, rounds, seats, cb) { - this.socket.once('addVote', (data) => { + this.socket.once('addVote', data => { if(data.ok) cb(null, data.id); else cb(data.error); }); @@ -133,7 +133,7 @@ class ConferenceConnection { } updateVote(id, index, vote, cb) { - this.socket.once('updateVote', (data) => { + this.socket.once('updateVote', data => { if(data.ok) cb(null); else cb(data.error); }); @@ -142,7 +142,7 @@ class ConferenceConnection { } iterateVote(id, status, cb) { - this.socket.once('iterateVote', (data) => { + this.socket.once('iterateVote', data => { if(data.ok) cb(null); else cb(data.error); }); @@ -152,7 +152,7 @@ class ConferenceConnection { /* Lists */ addList(name, seats, cb) { - this.socket.once('addList', (data) => { + this.socket.once('addList', data => { if(data.ok) cb(null, data.id); else cb(data.error); }); @@ -161,7 +161,7 @@ class ConferenceConnection { } updateList(id, seats, cb) { - this.socket.once('updateList', (data) => { + this.socket.once('updateList', data => { if(data.ok) cb(null); else cb(data.error); }); @@ -170,7 +170,7 @@ class ConferenceConnection { } iterateList(id, ptr, cb) { - this.socket.once('iterateList', (data) => { + this.socket.once('iterateList', data => { if(data.ok) cb(null); else cb(data.error); }); diff --git a/controller/index.html b/controller/index.html index 927d21b..79770a0 100644 --- a/controller/index.html +++ b/controller/index.html @@ -5,221 +5,275 @@ - - -
- - - - - - -
-
- person -
-
{{ presentCount }}
-
/
-
{{ seatCount }}
-
-
-
简单多数
-
{{ simpleHalfCount }}
-
-
-
三分之二多数
-
{{ twoThirdCount }}
+ +
+
+ + + + + + + + +
+
+ person +
+
{{ presentCount }}
+
/
+
{{ seatCount }}
+
+
+
简单多数
+
{{ simpleHalfCount }}
+
+
+
三分之二多数
+
{{ twoThirdCount }}
+
+
+
20%数
+
{{ twentyPercentCount }}
+
-
-
20%多数
-
{{ twentyPercentCount }}
+
+
+ home +
+
+ event_seat +
+
+ record_voice_over +
+
+ timer +
+
+ folder +
+
+ thumbs_up_down +
+
+ layers_clear +
+
+ cast_connected + cast +
-
-
- home -
-
- event_seat -
-
- record_voice_over -
-
- timer -
-
- folder -
-
- thumbs_up_down -
-
- layers_clear -
-
- cast_connected - cast +
+ + +
+
+
请选择委员会
+
+
+ {{ conf.name }} +
+
+ add创建一个新委员会 +
+
-
-
+ -
-
-
请选择委员会
-
-
- {{ conf.name }} + +
+
+ Console Lite
-
- add创建一个新委员会 +
+ +
+ 喵喵喵... +
+
+
+ wifi_tethering +
在本地创建一个新会话
+ more_vert +
+
+ home连接到本地已启动的会话 +
+
+ settings_ethernet连接到会话... +
-
-
+ -
-
- Console Lite -
-
-
- 喵喵喵... -
-
- settings_ethernet连接到会话... -
-
- wifi_tethering在本地创建一个新会话... + +
+
+ + +
+
+
+ 创建委员会 +
+
+
+
委员会名
+ +
+
+ +
+
+
-
-
+ -
+ +
+
+
+ 连接到... +
+
+
+
局域网内的服务
+
+ router{{ service.idkey }} {{ service.hint }} +
+
-
-
-
- 创建委员会 -
-
-
-
委员会名
- -
+
+
服务器URL
+ +
+
+ +
+
密码 ( 留空只读登陆 )
+ +
+
+ + +
+
- -
-
-
+ -
-
-
- 连接到... -
-
-
-
局域网内的服务
-
- router{{ service.idkey }} + +
+
+
+ 本地会话设置
-
+
+
+
注释
+ +
+
-
-
服务器URL
- -
-
+
+
端口
+ +
+
-
-
密码
- -
-
+
+
密码 ( 留空以自动生成 )
+ +
+
- -
+ +
+
+
-
-
+ -
-
-
- 服务器信息: -
-
-
- URL: {{ backendUrl }} -
-
- ID: {{ backendIDKey }} 已复制 -
-
- 密码: {{ backendPasskey }} 已复制 + +
+
+
+ 服务器信息: +
+
+
+ URL: {{ backendUrl }} +
+
+ ID: {{ backendIDKey }} 已复制 +
+
+ 密码: {{ backendPasskey }} 已复制 +
+
-
+
diff --git a/controller/views/file/file.css b/controller/views/file/file.css index 3ec5a73..ca2b4ca 100644 --- a/controller/views/file/file.css +++ b/controller/views/file/file.css @@ -1,6 +1,6 @@ .file-container { display: flex; - padding: 100px 0; + padding: 40px 0; align-items: center; justify-content: center; @@ -43,6 +43,18 @@ max-width: 100%; } +.file-markdown { + margin: auto; + width: 100%; + padding: 0 40px; +} + +.file-markdown .file-markdown-rendered { + box-shadow: rgba(0,0,0,.12) 0 2px 3px; + background: white; + padding: 20px 40px; +} + .dnd-mask.dnd-edit { position: absolute; top: 104px; diff --git a/controller/views/file/file.html b/controller/views/file/file.html index 45c8c42..4ac7bf6 100644 --- a/controller/views/file/file.html +++ b/controller/views/file/file.html @@ -20,22 +20,28 @@ @drop.stop.prevent="drop" @scroll="scroll"> -
+
+
+
+
+
-
- edit -
拖放以更新
-
+ +
+ edit +
拖放以更新
+
+
diff --git a/controller/views/file/file.js b/controller/views/file/file.js index 08935a8..46d8ec6 100644 --- a/controller/views/file/file.js +++ b/controller/views/file/file.js @@ -1,6 +1,7 @@ -const Vue = require('vue'); +const Vue = require('vue/dist/vue.common.js'); const fs = require('fs'); -const { dialog, shell } = require('electron').remote; +const { dialog } = require('electron').remote; +const { shell } = require('electron'); const util = require('../../../shared/util.js'); @@ -17,52 +18,46 @@ const FileView = Vue.extend({ rendered: '', }), - activate(done) { - this.$dispatch('get-file', this.file.id, (err, cont) => { - if(err) return alert('加载失败!'); + mounted() { + this.$emit('get-file', this.file.id, (err, cont) => { + if(err) return void alert('加载失败!'); else { this.type = util.getFileType(this.file.type); this.fileCont = cont; if(this.type === 'pdf') { this.clearPDF(); - return this.renderPDF(1).then(done); - } else if(this.type === 'image') - return done(); - else - // Display download link - return done(); + this.renderPDF(1); + } + + /* Images, markdown files and other types are handled automatically */ } }); }, methods: { clearPDF() { - while(this.$els.pages.firstChild) - this.$els.pages.removeChild(this.$els.pages.firstChild); + while(this.$refs.pages.firstChild) + this.$refs.pages.removeChild(this.$refs.pages.firstChild); }, renderPDF(scale) { - return util.renderPDF(this.fileCont, scale, this.$els.pages); + return util.renderPDF(new Uint8Array(this.fileCont), scale, this.$refs.pages); }, project() { - this.$dispatch('project-file', this.file); + this.$emit('project-file', this.file); }, save() { dialog.showSaveDialog({ title: '保存文件', defaultPath: this.file.name, - }, (filename) => { + }, filename => { if(!filename) return; - const buf = new Buffer(this.fileCont.byteLength); - const view = new Uint8Array(this.fileCont); - - for(let i = 0; i < buf.length; ++i) - buf[i] = view[i]; + const buf = Buffer.from(this.fileCont); - fs.writeFile(filename, buf, (err) => { + fs.writeFile(filename, buf, err => { if(err) dialog.showErrorBox('保存失败', err.stack); else dialog.showMessageBox({ type: 'info', @@ -71,7 +66,7 @@ const FileView = Vue.extend({ cancelId: 1, message: '保存成功!', detail: `保存到 ${filename}`, - }, (btn) => { + }, btn => { if(btn === 0) shell.openItem(filename); }); @@ -100,7 +95,7 @@ const FileView = Vue.extend({ return; } - const type = dt.files[0].type; + const { type } = dt.files[0]; if(type !== this.file.type) { this.dragging = false; alert(`请上传同样类型的文件: ${type}`); @@ -108,7 +103,7 @@ const FileView = Vue.extend({ } fs.readFile(dt.files[0].path, (err, data) => { - this.$dispatch('edit-file', this.file.id, data); + this.$emit('edit-file', this.file.id, data); }); this.dragging = false; @@ -125,8 +120,13 @@ const FileView = Vue.extend({ }, imgRendered() { - const b64str = btoa(String.fromCharCode(...new Uint8Array(this.fileCont))); - return `data:${this.file.type};base64,${b64str}`; + const blob = new Blob([this.fileCont], { type: this.file.type }); + return URL.createObjectURL(blob); + }, + + mdRendered() { + const str = new TextDecoder('utf-8').decode(this.fileCont); + return util.renderMD(str); }, }, }); diff --git a/controller/views/files/files.html b/controller/views/files/files.html index 0aff4c4..431a128 100644 --- a/controller/views/files/files.html +++ b/controller/views/files/files.html @@ -15,13 +15,15 @@
-
- close -
+ +
+ close +
+
-
+
@@ -39,8 +41,10 @@
无搜索结果
-
- file_upload -
拖放以上传
-
+ +
+ file_upload +
拖放以上传
+
+
diff --git a/controller/views/files/files.js b/controller/views/files/files.js index c87a24f..240db08 100644 --- a/controller/views/files/files.js +++ b/controller/views/files/files.js @@ -1,16 +1,13 @@ -const Vue = require('vue'); +const Vue = require('vue/dist/vue.common.js'); const fs = require('fs'); const FilesView = Vue.extend({ template: fs.readFileSync(`${__dirname}/files.html`).toString('utf-8'), - props: [ - 'files', - 'authorized', - { - name: 'searchInput', - default: '', - }, - ], + props: { + files: {}, + authorized: {}, + searchInput: { default: '' }, + }, data: () => ({ dragging: false, @@ -38,18 +35,25 @@ const FilesView = Vue.extend({ return; } - const name = dt.files[0].name; - const type = dt.files[0].type; + const { name, type } = dt.files[0]; fs.readFile(dt.files[0].path, (err, data) => { - this.$dispatch('add-file', name, type, data); + this.$emit('add-file', name, type, data); }); this.dragging = false; }, viewFile(file) { - this.$dispatch('view-file', file); + this.$emit('view-file', file); + }, + }, + + computed: { + filteredFiles() { + if(this.searchInput) return this.files.filter(e => + e.name.indexOf(this.searchInput) !== -1 || e.type.indexOf(this.searchInput) !== -1); + return this.files; }, }, }); diff --git a/controller/views/home/home.html b/controller/views/home/home.html index 119df5f..62cf6ce 100644 --- a/controller/views/home/home.html +++ b/controller/views/home/home.html @@ -3,7 +3,7 @@
活跃发言名单
@@ -44,7 +44,7 @@
活跃计时器
-
+
@@ -62,7 +62,7 @@
活跃投票
@@ -73,7 +73,7 @@
{{ vote.status.iteration }} - 未启动 + 未启动 - +
-
+
{{ voter.name }}
-
+ -
-
-
{{ targetVoter.name }}
-
-
-
-
-
{{ row.text }}
+ +
+
+
{{ targetVoter.name }}
+
+
+
+
+
{{ row.text }}
+
+ +
- -
-
+
diff --git a/controller/views/vote/vote.js b/controller/views/vote/vote.js index 3759737..529e433 100644 --- a/controller/views/vote/vote.js +++ b/controller/views/vote/vote.js @@ -1,4 +1,4 @@ -const Vue = require('vue'); +const Vue = require('vue/dist/vue.common.js'); const fs = require('fs'); const util = require('../../../shared/util.js'); @@ -30,7 +30,19 @@ const VoteView = Vue.extend({ ], }), - activate(done) { + transitions: { + item: { + enter(el, done) { + done(); + }, + + leave(el, done) { + done(); + }, + }, + }, + + created() { // Generate id for voters this.mat = [...this.vote.matrix]; @@ -47,8 +59,6 @@ const VoteView = Vue.extend({ this.rearrange(); }, 100); }); - - done(); }, methods: { @@ -66,7 +76,7 @@ const VoteView = Vue.extend({ if(this.emptyCount === 0) if(!confirm('现在所有席位都已经完成投票,是否开始下一轮?')) return false; - this.$dispatch('iterate-vote', this.vote.id, { + this.$emit('iterate-vote', this.vote.id, { iteration: this.vote.status.iteration + 1, running: true, }); @@ -110,7 +120,7 @@ const VoteView = Vue.extend({ if(this.vote.matrix.some(e => e.vote === 0)) if(!confirm('这是最后一轮投票了,还有投票为过的席位,是否结束这轮投票?')) return false; - this.$dispatch('iterate-vote', this.vote.id, { + this.$emit('iterate-vote', this.vote.id, { iteration: this.vote.status.iteration, running: false, }); @@ -145,7 +155,7 @@ const VoteView = Vue.extend({ else this._overrideSetVote = true; if(this.targetVote !== this.targetVoter.vote) // Changed - this.$dispatch('update-vote', this.vote.id, this.targetVoter.originalId, this.targetVote); + this.$emit('update-vote', this.vote.id, this.targetVoter.originalId, this.targetVote); if(this.autoMode) this.autoManipulate(this.autoIndex + 1); @@ -154,7 +164,7 @@ const VoteView = Vue.extend({ }, project() { - this.$dispatch('project-vote', this.vote); + this.$emit('project-vote', this.vote); }, }, diff --git a/controller/views/votes/votes.html b/controller/views/votes/votes.html index 61e43d5..e5493b9 100644 --- a/controller/views/votes/votes.html +++ b/controller/views/votes/votes.html @@ -12,15 +12,17 @@
-
- close -
+ +
+ close +
+
@@ -31,7 +33,7 @@
{{ vote.status.iteration }} - 未启动 + 未启动