├── .babelrc ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .stylelintrc ├── LICENSE ├── README.md ├── app ├── main.js ├── menu.js ├── package.json ├── updater.js └── yarn.lock ├── build ├── build.js ├── check-versions.js ├── dev-client.js ├── dev-server.js ├── resources │ ├── icon.icns │ ├── icon.ico │ ├── icon.png │ └── spinner.gif ├── utils.js ├── webpack.base.conf.js ├── webpack.dev.conf.js └── webpack.prod.conf.js ├── config ├── dev.env.js ├── index.js ├── prod.env.js └── test.env.js ├── index.html ├── package.json ├── src ├── App.vue ├── assets │ ├── ClearSans-Regular.ttf │ ├── bg-select.svg │ ├── icon-chevron-left.svg │ ├── icon-chevron-right.svg │ ├── icon-close.svg │ ├── icon-settings-active.svg │ └── icon-settings.svg ├── components │ ├── List.vue │ ├── Settings.vue │ ├── Settings │ │ ├── Key.vue │ │ └── Locale.vue │ ├── Task.vue │ ├── Task │ │ ├── Checkbox.vue │ │ └── Input.vue │ └── Topbar.vue ├── main.js ├── store │ ├── db.js │ ├── helpers.js │ ├── index.js │ ├── localstorage.js │ └── modules │ │ ├── caret.js │ │ ├── list.js │ │ ├── settings.js │ │ └── timeline.js └── variables.css ├── test └── components │ └── settings-key.test.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-2"], 3 | "plugins": ["transform-runtime"], 4 | "comments": false 5 | } 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/*.js 2 | config/*.js 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: 'babel-eslint', 4 | parserOptions: { 5 | sourceType: 'module' 6 | }, 7 | // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style 8 | extends: 'standard', 9 | // required to lint *.vue files 10 | plugins: [ 11 | 'html' 12 | ], 13 | // add your custom rules here 14 | 'rules': { 15 | // allow paren-less arrow functions 16 | 'arrow-parens': 0, 17 | // allow async-await 18 | 'generator-star-spacing': 0, 19 | // allow debugger during development 20 | 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log 5 | 6 | app/node_modules/ 7 | app/static/ 8 | app/index.html 9 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "processors": ["stylelint-processor-html"], 3 | "extends": [ 4 | "stylelint-config-standard", 5 | "stylelint-config-recess-order" 6 | ], 7 | "rules": { 8 | "no-empty-source": null 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Bartłomiej Kozal, Maciej Kozal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # coypu 2 | 3 | > Text-editor-like weekly planner 4 | 5 | ## Build Setup 6 | 7 | ```bash 8 | # install dependencies 9 | npm install 10 | 11 | # serve with hot reload at localhost:8080 12 | npm run dev 13 | 14 | # build for production with minification 15 | npm run build 16 | 17 | # pack into electron app 18 | npm run pack 19 | 20 | # release new version 21 | npm run dist 22 | 23 | # run tests 24 | npm test 25 | ``` 26 | -------------------------------------------------------------------------------- /app/main.js: -------------------------------------------------------------------------------- 1 | if (require('electron-squirrel-startup')) return; 2 | const { app, BrowserWindow, Menu } = require('electron') 3 | const windowStateKeeper = require('electron-window-state') 4 | const path = require('path') 5 | const url = require('url') 6 | 7 | require('electron-context-menu')({ 8 | showInspectElement: false 9 | }) 10 | const menu = require('./menu') 11 | const updater = require('./updater') 12 | 13 | let window 14 | 15 | function createWindow () { 16 | let windowState = windowStateKeeper({ 17 | defaultWidth: 420, 18 | defaultHeight: 640 19 | }) 20 | 21 | window = new BrowserWindow({ 22 | x: windowState.x, 23 | y: windowState.y, 24 | width: windowState.width, 25 | height: windowState.height, 26 | minWidth: 370, 27 | minHeight: 100, 28 | titleBarStyle: 'hidden-inset' 29 | }) 30 | 31 | windowState.manage(window) 32 | 33 | window.loadURL(url.format({ 34 | pathname: path.join(__dirname, 'index.html'), 35 | protocol: 'file:', 36 | slashes: true 37 | })) 38 | 39 | window.on('closed', () => { 40 | window = null 41 | }) 42 | 43 | Menu.setApplicationMenu(menu) 44 | } 45 | 46 | app.on('ready', () => { 47 | createWindow() 48 | if (process.platform === 'darwin' || process.platform === 'win32') { 49 | updater() 50 | } 51 | }) 52 | 53 | app.on('window-all-closed', () => { 54 | if (process.platform !== 'darwin') { 55 | app.quit() 56 | } 57 | }) 58 | 59 | app.on('activate', () => { 60 | if (window === null) { 61 | createWindow() 62 | } 63 | }) 64 | -------------------------------------------------------------------------------- /app/menu.js: -------------------------------------------------------------------------------- 1 | const { Menu } = require('electron') 2 | 3 | const template = [ 4 | { 5 | label: 'Edit', 6 | submenu: [ 7 | { 8 | role: 'undo' 9 | }, 10 | { 11 | role: 'redo' 12 | }, 13 | { 14 | type: 'separator' 15 | }, 16 | { 17 | role: 'cut' 18 | }, 19 | { 20 | role: 'copy' 21 | }, 22 | { 23 | role: 'paste' 24 | }, 25 | { 26 | role: 'pasteandmatchstyle' 27 | }, 28 | { 29 | role: 'delete' 30 | }, 31 | { 32 | role: 'selectall' 33 | } 34 | ] 35 | }, 36 | { 37 | label: 'View', 38 | submenu: [ 39 | { 40 | role: 'reload' 41 | }, 42 | { 43 | type: 'separator' 44 | }, 45 | { 46 | role: 'resetzoom' 47 | }, 48 | { 49 | role: 'zoomin' 50 | }, 51 | { 52 | role: 'zoomout' 53 | }, 54 | { 55 | type: 'separator' 56 | }, 57 | { 58 | role: 'togglefullscreen' 59 | } 60 | ] 61 | }, 62 | { 63 | role: 'window', 64 | submenu: [ 65 | { 66 | role: 'minimize' 67 | } 68 | ] 69 | } 70 | ] 71 | 72 | if (process.platform === 'darwin') { 73 | template.unshift({ 74 | label: 'Coypu', 75 | submenu: [ 76 | { 77 | label: 'About Coypu', 78 | role: 'about' 79 | }, 80 | { 81 | type: 'separator' 82 | }, 83 | { 84 | role: 'services', 85 | submenu: [] 86 | }, 87 | { 88 | type: 'separator' 89 | }, 90 | { 91 | label: 'Hide Coypu', 92 | role: 'hide' 93 | }, 94 | { 95 | role: 'hideothers' 96 | }, 97 | { 98 | role: 'unhide' 99 | }, 100 | { 101 | type: 'separator' 102 | }, 103 | { 104 | label: 'Quit Coypu', 105 | role: 'quit' 106 | } 107 | ] 108 | }) 109 | // Edit menu. 110 | template[1].submenu.push( 111 | { 112 | type: 'separator' 113 | }, 114 | { 115 | label: 'Speech', 116 | submenu: [ 117 | { 118 | role: 'startspeaking' 119 | }, 120 | { 121 | role: 'stopspeaking' 122 | } 123 | ] 124 | } 125 | ) 126 | // Window menu. 127 | template[3].submenu = [ 128 | { 129 | label: 'Close', 130 | accelerator: 'CmdOrCtrl+W', 131 | role: 'hide' 132 | }, 133 | { 134 | label: 'Minimize', 135 | accelerator: 'CmdOrCtrl+M', 136 | role: 'minimize' 137 | }, 138 | { 139 | label: 'Zoom', 140 | role: 'zoom' 141 | }, 142 | { 143 | type: 'separator' 144 | }, 145 | { 146 | label: 'Bring All to Front', 147 | role: 'front' 148 | } 149 | ] 150 | } 151 | 152 | module.exports = Menu.buildFromTemplate(template) 153 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "coypu", 3 | "version": "1.3.0", 4 | "homepage": "https://coypu.co", 5 | "license": "MIT", 6 | "description": "Text-editor-like weekly planner", 7 | "author": "Bart Kozal ", 8 | "main": "main.js", 9 | "dependencies": { 10 | "electron-context-menu": "0.8.0", 11 | "electron-squirrel-startup": "1.0.0", 12 | "electron-window-state": "4.0.2" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/updater.js: -------------------------------------------------------------------------------- 1 | const os = require('os') 2 | const { app, autoUpdater, dialog } = require('electron') 3 | 4 | const platform = os.platform() + '_' + os.arch() 5 | const version = app.getVersion() 6 | const feedURL = 'https://download.coypu.co/update/' + platform + '/' + version 7 | 8 | function updater () { 9 | autoUpdater.setFeedURL(feedURL) 10 | 11 | autoUpdater.on('update-downloaded', function(event, releaseNotes, releaseName, releaseDate, updateURL) { 12 | dialog.showMessageBox({ 13 | type: 'info', 14 | buttons: ['Restart', 'Cancel'], 15 | defaultId: 0, 16 | title: 'Update is available', 17 | message: 'New version of Coypu (v' + releaseName + ') is ready. Restart the app to install update.' 18 | }, function(button) { 19 | if (button === 0) { 20 | autoUpdater.quitAndInstall() 21 | } else { 22 | return 23 | } 24 | }) 25 | }) 26 | 27 | autoUpdater.checkForUpdates() 28 | } 29 | 30 | module.exports = updater 31 | -------------------------------------------------------------------------------- /app/yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | debug@^2.2.0: 6 | version "2.6.9" 7 | resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" 8 | dependencies: 9 | ms "2.0.0" 10 | 11 | deep-equal@^1.0.1: 12 | version "1.0.1" 13 | resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" 14 | 15 | electron-context-menu@0.8.0: 16 | version "0.8.0" 17 | resolved "https://registry.yarnpkg.com/electron-context-menu/-/electron-context-menu-0.8.0.tgz#60c4b5e29d5514007be8728a6ebdf3cb694c4e3e" 18 | dependencies: 19 | electron-dl "^1.2.0" 20 | electron-is-dev "^0.1.1" 21 | 22 | electron-dl@^1.2.0: 23 | version "1.11.0" 24 | resolved "https://registry.yarnpkg.com/electron-dl/-/electron-dl-1.11.0.tgz#112851f3857bb1a556b5c736af06040bd40df850" 25 | dependencies: 26 | ext-name "^5.0.0" 27 | pupa "^1.0.0" 28 | unused-filename "^1.0.0" 29 | 30 | electron-is-dev@^0.1.1: 31 | version "0.1.2" 32 | resolved "https://registry.yarnpkg.com/electron-is-dev/-/electron-is-dev-0.1.2.tgz#8a1043e32b3a1da1c3f553dce28ce764246167e3" 33 | 34 | electron-squirrel-startup@1.0.0: 35 | version "1.0.0" 36 | resolved "https://registry.yarnpkg.com/electron-squirrel-startup/-/electron-squirrel-startup-1.0.0.tgz#19b4e55933fa0ef8f556784b9c660f772546a0b8" 37 | dependencies: 38 | debug "^2.2.0" 39 | 40 | electron-window-state@4.0.2: 41 | version "4.0.2" 42 | resolved "https://registry.yarnpkg.com/electron-window-state/-/electron-window-state-4.0.2.tgz#f214ddb99c57d905af89bd2b314e1d4cda628963" 43 | dependencies: 44 | deep-equal "^1.0.1" 45 | jsonfile "^2.2.3" 46 | mkdirp "^0.5.1" 47 | 48 | ext-list@^2.0.0: 49 | version "2.2.2" 50 | resolved "https://registry.yarnpkg.com/ext-list/-/ext-list-2.2.2.tgz#0b98e64ed82f5acf0f2931babf69212ef52ddd37" 51 | dependencies: 52 | mime-db "^1.28.0" 53 | 54 | ext-name@^5.0.0: 55 | version "5.0.0" 56 | resolved "https://registry.yarnpkg.com/ext-name/-/ext-name-5.0.0.tgz#70781981d183ee15d13993c8822045c506c8f0a6" 57 | dependencies: 58 | ext-list "^2.0.0" 59 | sort-keys-length "^1.0.0" 60 | 61 | graceful-fs@^4.1.6: 62 | version "4.1.11" 63 | resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" 64 | 65 | is-plain-obj@^1.0.0: 66 | version "1.1.0" 67 | resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-1.1.0.tgz#71a50c8429dfca773c92a390a4a03b39fcd51d3e" 68 | 69 | jsonfile@^2.2.3: 70 | version "2.4.0" 71 | resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-2.4.0.tgz#3736a2b428b87bbda0cc83b53fa3d633a35c2ae8" 72 | optionalDependencies: 73 | graceful-fs "^4.1.6" 74 | 75 | mime-db@^1.28.0: 76 | version "1.33.0" 77 | resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db" 78 | 79 | minimist@0.0.8: 80 | version "0.0.8" 81 | resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" 82 | 83 | mkdirp@^0.5.1: 84 | version "0.5.1" 85 | resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" 86 | dependencies: 87 | minimist "0.0.8" 88 | 89 | modify-filename@^1.1.0: 90 | version "1.1.0" 91 | resolved "https://registry.yarnpkg.com/modify-filename/-/modify-filename-1.1.0.tgz#9a2dec83806fbb2d975f22beec859ca26b393aa1" 92 | 93 | ms@2.0.0: 94 | version "2.0.0" 95 | resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" 96 | 97 | path-exists@^3.0.0: 98 | version "3.0.0" 99 | resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" 100 | 101 | pupa@^1.0.0: 102 | version "1.0.0" 103 | resolved "https://registry.yarnpkg.com/pupa/-/pupa-1.0.0.tgz#9a9568a5af7e657b8462a6e9d5328743560ceff6" 104 | 105 | sort-keys-length@^1.0.0: 106 | version "1.0.1" 107 | resolved "https://registry.yarnpkg.com/sort-keys-length/-/sort-keys-length-1.0.1.tgz#9cb6f4f4e9e48155a6aa0671edd336ff1479a188" 108 | dependencies: 109 | sort-keys "^1.0.0" 110 | 111 | sort-keys@^1.0.0: 112 | version "1.1.2" 113 | resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad" 114 | dependencies: 115 | is-plain-obj "^1.0.0" 116 | 117 | unused-filename@^1.0.0: 118 | version "1.0.0" 119 | resolved "https://registry.yarnpkg.com/unused-filename/-/unused-filename-1.0.0.tgz#d340880f71ae2115ebaa1325bef05cc6684469c6" 120 | dependencies: 121 | modify-filename "^1.1.0" 122 | path-exists "^3.0.0" 123 | -------------------------------------------------------------------------------- /build/build.js: -------------------------------------------------------------------------------- 1 | require('./check-versions')() 2 | require('shelljs/global') 3 | env.NODE_ENV = 'production' 4 | 5 | var config = require('../config').build 6 | var path = require('path') 7 | var webpack = require('webpack') 8 | var webpackConfig = require('./webpack.prod.conf') 9 | 10 | rm('-r', path.join(config.assetsRoot, config.assetsSubDirectory)) 11 | 12 | webpack(webpackConfig, function (err, stats) { 13 | if (err) throw err 14 | process.stdout.write(stats.toString({ 15 | colors: true, 16 | modules: false, 17 | children: false, 18 | chunks: false, 19 | chunkModules: false 20 | }) + '\n') 21 | }) 22 | -------------------------------------------------------------------------------- /build/check-versions.js: -------------------------------------------------------------------------------- 1 | var semver = require('semver') 2 | var chalk = require('chalk') 3 | var packageConfig = require('../package.json') 4 | var exec = function (cmd) { 5 | return require('child_process') 6 | .execSync(cmd).toString().trim() 7 | } 8 | 9 | var versionRequirements = [ 10 | { 11 | name: 'node', 12 | currentVersion: semver.clean(process.version), 13 | versionRequirement: packageConfig.engines.node 14 | }, 15 | { 16 | name: 'npm', 17 | currentVersion: exec('npm --version'), 18 | versionRequirement: packageConfig.engines.npm 19 | } 20 | ] 21 | 22 | module.exports = function () { 23 | var warnings = [] 24 | for (var i = 0; i < versionRequirements.length; i++) { 25 | var mod = versionRequirements[i] 26 | if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { 27 | warnings.push(mod.name + ': ' + 28 | chalk.red(mod.currentVersion) + ' should be ' + 29 | chalk.green(mod.versionRequirement) 30 | ) 31 | } 32 | } 33 | 34 | if (warnings.length) { 35 | console.log('') 36 | console.log(chalk.yellow('To use this template, you must update following to modules:')) 37 | console.log() 38 | for (var i = 0; i < warnings.length; i++) { 39 | var warning = warnings[i] 40 | console.log(' ' + warning) 41 | } 42 | console.log() 43 | process.exit(1) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /build/dev-client.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | require('eventsource-polyfill') 3 | var hotClient = require('webpack-hot-middleware/client?noInfo=true&reload=true') 4 | 5 | hotClient.subscribe(function (event) { 6 | if (event.action === 'reload') { 7 | window.location.reload() 8 | } 9 | }) 10 | -------------------------------------------------------------------------------- /build/dev-server.js: -------------------------------------------------------------------------------- 1 | require('./check-versions')() 2 | var config = require('../config') 3 | if (!process.env.NODE_ENV) process.env.NODE_ENV = JSON.parse(config.dev.env.NODE_ENV) 4 | var path = require('path') 5 | var express = require('express') 6 | var webpack = require('webpack') 7 | var opn = require('opn') 8 | var proxyMiddleware = require('http-proxy-middleware') 9 | var webpackConfig = process.env.NODE_ENV === 'testing' 10 | ? require('./webpack.prod.conf') 11 | : require('./webpack.dev.conf') 12 | 13 | // default port where dev server listens for incoming traffic 14 | var port = process.env.PORT || config.dev.port 15 | // Define HTTP proxies to your custom API backend 16 | // https://github.com/chimurai/http-proxy-middleware 17 | var proxyTable = config.dev.proxyTable 18 | 19 | var app = express() 20 | var compiler = webpack(webpackConfig) 21 | 22 | var devMiddleware = require('webpack-dev-middleware')(compiler, { 23 | publicPath: webpackConfig.output.publicPath, 24 | stats: { 25 | colors: true, 26 | chunks: false 27 | } 28 | }) 29 | 30 | var hotMiddleware = require('webpack-hot-middleware')(compiler) 31 | // force page reload when html-webpack-plugin template changes 32 | compiler.plugin('compilation', function (compilation) { 33 | compilation.plugin('html-webpack-plugin-after-emit', function (data, cb) { 34 | hotMiddleware.publish({ action: 'reload' }) 35 | cb() 36 | }) 37 | }) 38 | 39 | // proxy api requests 40 | Object.keys(proxyTable).forEach(function (context) { 41 | var options = proxyTable[context] 42 | if (typeof options === 'string') { 43 | options = { target: options } 44 | } 45 | app.use(proxyMiddleware(context, options)) 46 | }) 47 | 48 | // handle fallback for HTML5 history API 49 | app.use(require('connect-history-api-fallback')()) 50 | 51 | // serve webpack bundle output 52 | app.use(devMiddleware) 53 | 54 | // enable hot-reload and state-preserving 55 | // compilation error display 56 | app.use(hotMiddleware) 57 | 58 | // serve pure static assets 59 | var staticPath = path.posix.join(config.dev.assetsPublicPath, config.dev.assetsSubDirectory) 60 | app.use(staticPath, express.static('./static')) 61 | 62 | module.exports = app.listen(port, function (err) { 63 | if (err) { 64 | console.log(err) 65 | return 66 | } 67 | var uri = 'http://localhost:' + port 68 | console.log('Listening at ' + uri + '\n') 69 | 70 | // when env is testing, don't need open it 71 | if (process.env.NODE_ENV !== 'testing') { 72 | opn(uri, { app: 'google-chrome' }) 73 | } 74 | }) 75 | -------------------------------------------------------------------------------- /build/resources/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bartkozal/coypu/cac223af532089a3cf0441e5f1d87ff1c94e4607/build/resources/icon.icns -------------------------------------------------------------------------------- /build/resources/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bartkozal/coypu/cac223af532089a3cf0441e5f1d87ff1c94e4607/build/resources/icon.ico -------------------------------------------------------------------------------- /build/resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bartkozal/coypu/cac223af532089a3cf0441e5f1d87ff1c94e4607/build/resources/icon.png -------------------------------------------------------------------------------- /build/resources/spinner.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bartkozal/coypu/cac223af532089a3cf0441e5f1d87ff1c94e4607/build/resources/spinner.gif -------------------------------------------------------------------------------- /build/utils.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var config = require('../config') 3 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 4 | 5 | exports.assetsPath = function (_path) { 6 | var assetsSubDirectory = process.env.NODE_ENV === 'production' 7 | ? config.build.assetsSubDirectory 8 | : config.dev.assetsSubDirectory 9 | return path.posix.join(assetsSubDirectory, _path) 10 | } 11 | 12 | exports.cssLoaders = function (options) { 13 | options = options || {} 14 | // generate loader string to be used with extract text plugin 15 | function generateLoaders (loaders) { 16 | var sourceLoader = loaders.map(function (loader) { 17 | var extraParamChar 18 | if (/\?/.test(loader)) { 19 | loader = loader.replace(/\?/, '-loader?') 20 | extraParamChar = '&' 21 | } else { 22 | loader = loader + '-loader' 23 | extraParamChar = '?' 24 | } 25 | return loader + (options.sourceMap ? extraParamChar + 'sourceMap' : '') 26 | }).join('!') 27 | 28 | // Extract CSS when that option is specified 29 | // (which is the case during production build) 30 | if (options.extract) { 31 | return ExtractTextPlugin.extract('vue-style-loader', sourceLoader) 32 | } else { 33 | return ['vue-style-loader', sourceLoader].join('!') 34 | } 35 | } 36 | 37 | // http://vuejs.github.io/vue-loader/en/configurations/extract-css.html 38 | return { 39 | css: generateLoaders(['css']), 40 | postcss: generateLoaders(['css']), 41 | less: generateLoaders(['css', 'less']), 42 | sass: generateLoaders(['css', 'sass?indentedSyntax']), 43 | scss: generateLoaders(['css', 'sass']), 44 | stylus: generateLoaders(['css', 'stylus']), 45 | styl: generateLoaders(['css', 'stylus']) 46 | } 47 | } 48 | 49 | // Generate loaders for standalone style files (outside of .vue) 50 | exports.styleLoaders = function (options) { 51 | var output = [] 52 | var loaders = exports.cssLoaders(options) 53 | for (var extension in loaders) { 54 | var loader = loaders[extension] 55 | output.push({ 56 | test: new RegExp('\\.' + extension + '$'), 57 | loader: loader 58 | }) 59 | } 60 | return output 61 | } 62 | -------------------------------------------------------------------------------- /build/webpack.base.conf.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var config = require('../config') 3 | var utils = require('./utils') 4 | var projectRoot = path.resolve(__dirname, '../') 5 | 6 | var env = process.env.NODE_ENV 7 | // check env & config/index.js to decide whether to enable CSS source maps for the 8 | // various preprocessor loaders added to vue-loader at the end of this file 9 | var cssSourceMapDev = (env === 'development' && config.dev.cssSourceMap) 10 | var cssSourceMapProd = (env === 'production' && config.build.productionSourceMap) 11 | var useCssSourceMap = cssSourceMapDev || cssSourceMapProd 12 | 13 | module.exports = { 14 | entry: { 15 | app: './src/main.js' 16 | }, 17 | output: { 18 | path: config.build.assetsRoot, 19 | publicPath: process.env.NODE_ENV === 'production' ? config.build.assetsPublicPath : config.dev.assetsPublicPath, 20 | filename: '[name].js' 21 | }, 22 | resolve: { 23 | extensions: ['', '.js', '.vue', '.json'], 24 | fallback: [path.join(__dirname, '../node_modules')], 25 | alias: { 26 | 'vue$': 'vue/dist/vue.common.js', 27 | 'src': path.resolve(__dirname, '../src'), 28 | 'assets': path.resolve(__dirname, '../src/assets'), 29 | 'components': path.resolve(__dirname, '../src/components') 30 | } 31 | }, 32 | resolveLoader: { 33 | fallback: [path.join(__dirname, '../node_modules')] 34 | }, 35 | module: { 36 | preLoaders: [ 37 | { 38 | test: /\.vue$/, 39 | loader: 'eslint', 40 | include: projectRoot, 41 | exclude: /node_modules/ 42 | }, 43 | { 44 | test: /\.js$/, 45 | loader: 'eslint', 46 | include: projectRoot, 47 | exclude: /node_modules/ 48 | } 49 | ], 50 | loaders: [ 51 | { 52 | test: /\.vue$/, 53 | loader: 'vue' 54 | }, 55 | { 56 | test: /\.js$/, 57 | loader: 'babel', 58 | include: projectRoot, 59 | exclude: /node_modules/ 60 | }, 61 | { 62 | test: /\.json$/, 63 | loader: 'json' 64 | }, 65 | { 66 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 67 | loader: 'url', 68 | query: { 69 | limit: 10000, 70 | name: utils.assetsPath('img/[name].[hash:7].[ext]'), 71 | publicPath: '../../' 72 | } 73 | }, 74 | { 75 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 76 | loader: 'url', 77 | query: { 78 | limit: 10000, 79 | name: utils.assetsPath('fonts/[name].[hash:7].[ext]'), 80 | publicPath: '../../' 81 | } 82 | } 83 | ] 84 | }, 85 | eslint: { 86 | formatter: require('eslint-friendly-formatter') 87 | }, 88 | vue: { 89 | loaders: utils.cssLoaders({ sourceMap: useCssSourceMap }), 90 | postcss: [ 91 | require('postcss-import')(), 92 | require('postcss-cssnext')() 93 | ] 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /build/webpack.dev.conf.js: -------------------------------------------------------------------------------- 1 | var config = require('../config') 2 | var webpack = require('webpack') 3 | var merge = require('webpack-merge') 4 | var utils = require('./utils') 5 | var baseWebpackConfig = require('./webpack.base.conf') 6 | var StylelintPlugin = require('stylelint-webpack-plugin') 7 | var HtmlWebpackPlugin = require('html-webpack-plugin') 8 | 9 | // add hot-reload related code to entry chunks 10 | Object.keys(baseWebpackConfig.entry).forEach(function (name) { 11 | baseWebpackConfig.entry[name] = ['./build/dev-client'].concat(baseWebpackConfig.entry[name]) 12 | }) 13 | 14 | module.exports = merge(baseWebpackConfig, { 15 | module: { 16 | loaders: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap }) 17 | }, 18 | // eval-source-map is faster for development 19 | devtool: '#eval-source-map', 20 | plugins: [ 21 | new webpack.DefinePlugin({ 22 | 'process.env': config.dev.env 23 | }), 24 | // https://github.com/glenjamin/webpack-hot-middleware#installation--usage 25 | new webpack.optimize.OccurrenceOrderPlugin(), 26 | new webpack.HotModuleReplacementPlugin(), 27 | new webpack.NoErrorsPlugin(), 28 | // https://github.com/ampedandwired/html-webpack-plugin 29 | new HtmlWebpackPlugin({ 30 | filename: 'index.html', 31 | template: 'index.html', 32 | inject: true 33 | }), 34 | new StylelintPlugin({ 35 | files: ['src/**/*.vue'] 36 | }) 37 | ] 38 | }) 39 | -------------------------------------------------------------------------------- /build/webpack.prod.conf.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var config = require('../config') 3 | var utils = require('./utils') 4 | var webpack = require('webpack') 5 | var merge = require('webpack-merge') 6 | var baseWebpackConfig = require('./webpack.base.conf') 7 | var ExtractTextPlugin = require('extract-text-webpack-plugin') 8 | var HtmlWebpackPlugin = require('html-webpack-plugin') 9 | var env = process.env.NODE_ENV === 'testing' 10 | ? require('../config/test.env') 11 | : config.build.env 12 | 13 | var webpackConfig = merge(baseWebpackConfig, { 14 | module: { 15 | loaders: utils.styleLoaders({ sourceMap: config.build.productionSourceMap, extract: true }) 16 | }, 17 | devtool: config.build.productionSourceMap ? '#source-map' : false, 18 | output: { 19 | path: config.build.assetsRoot, 20 | filename: utils.assetsPath('js/[name].[chunkhash].js'), 21 | chunkFilename: utils.assetsPath('js/[id].[chunkhash].js') 22 | }, 23 | vue: { 24 | loaders: utils.cssLoaders({ 25 | sourceMap: config.build.productionSourceMap, 26 | extract: true 27 | }) 28 | }, 29 | plugins: [ 30 | // http://vuejs.github.io/vue-loader/en/workflow/production.html 31 | new webpack.DefinePlugin({ 32 | 'process.env': env 33 | }), 34 | new webpack.optimize.UglifyJsPlugin({ 35 | compress: { 36 | warnings: false 37 | } 38 | }), 39 | new webpack.optimize.OccurrenceOrderPlugin(), 40 | // extract css into its own file 41 | new ExtractTextPlugin(utils.assetsPath('css/[name].[contenthash].css')), 42 | // generate dist index.html with correct asset hash for caching. 43 | // you can customize output by editing /index.html 44 | // see https://github.com/ampedandwired/html-webpack-plugin 45 | new HtmlWebpackPlugin({ 46 | filename: process.env.NODE_ENV === 'testing' 47 | ? 'index.html' 48 | : config.build.index, 49 | template: 'index.html', 50 | inject: true, 51 | minify: { 52 | removeComments: true, 53 | collapseWhitespace: true, 54 | removeAttributeQuotes: true 55 | // more options: 56 | // https://github.com/kangax/html-minifier#options-quick-reference 57 | }, 58 | // necessary to consistently work with multiple chunks via CommonsChunkPlugin 59 | chunksSortMode: 'dependency' 60 | }), 61 | // split vendor js into its own file 62 | new webpack.optimize.CommonsChunkPlugin({ 63 | name: 'vendor', 64 | minChunks: function (module, count) { 65 | // any required modules inside node_modules are extracted to vendor 66 | return ( 67 | module.resource && 68 | /\.js$/.test(module.resource) && 69 | module.resource.indexOf( 70 | path.join(__dirname, '../node_modules') 71 | ) === 0 72 | ) 73 | } 74 | }), 75 | // extract webpack runtime and module manifest to its own file in order to 76 | // prevent vendor hash from being updated whenever app bundle is updated 77 | new webpack.optimize.CommonsChunkPlugin({ 78 | name: 'manifest', 79 | chunks: ['vendor'] 80 | }) 81 | ] 82 | }) 83 | 84 | if (config.build.productionGzip) { 85 | var CompressionWebpackPlugin = require('compression-webpack-plugin') 86 | 87 | webpackConfig.plugins.push( 88 | new CompressionWebpackPlugin({ 89 | asset: '[path].gz[query]', 90 | algorithm: 'gzip', 91 | test: new RegExp( 92 | '\\.(' + 93 | config.build.productionGzipExtensions.join('|') + 94 | ')$' 95 | ), 96 | threshold: 10240, 97 | minRatio: 0.8 98 | }) 99 | ) 100 | } 101 | 102 | module.exports = webpackConfig 103 | -------------------------------------------------------------------------------- /config/dev.env.js: -------------------------------------------------------------------------------- 1 | var merge = require('webpack-merge') 2 | var prodEnv = require('./prod.env') 3 | 4 | module.exports = merge(prodEnv, { 5 | NODE_ENV: '"development"' 6 | }) 7 | -------------------------------------------------------------------------------- /config/index.js: -------------------------------------------------------------------------------- 1 | // see http://vuejs-templates.github.io/webpack for documentation. 2 | var path = require('path') 3 | 4 | module.exports = { 5 | build: { 6 | env: require('./prod.env'), 7 | index: path.resolve(__dirname, '../app/index.html'), 8 | assetsRoot: path.resolve(__dirname, '../app'), 9 | assetsSubDirectory: 'static', 10 | assetsPublicPath: '', 11 | productionSourceMap: false, 12 | // Gzip off by default as many popular static hosts such as 13 | // Surge or Netlify already gzip all static assets for you. 14 | // Before setting to `true`, make sure to: 15 | // npm install --save-dev compression-webpack-plugin 16 | productionGzip: false, 17 | productionGzipExtensions: ['js', 'css'] 18 | }, 19 | dev: { 20 | env: require('./dev.env'), 21 | port: 8080, 22 | assetsSubDirectory: 'static', 23 | assetsPublicPath: '/', 24 | proxyTable: {}, 25 | // CSS Sourcemaps off by default because relative paths are "buggy" 26 | // with this option, according to the CSS-Loader README 27 | // (https://github.com/webpack/css-loader#sourcemaps) 28 | // In our experience, they generally work as expected, 29 | // just be aware of this issue when enabling this option. 30 | cssSourceMap: false 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /config/prod.env.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | NODE_ENV: '"production"' 3 | } 4 | -------------------------------------------------------------------------------- /config/test.env.js: -------------------------------------------------------------------------------- 1 | var merge = require('webpack-merge') 2 | var devEnv = require('./dev.env') 3 | 4 | module.exports = merge(devEnv, { 5 | NODE_ENV: '"testing"' 6 | }) 7 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Coypu 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "coypu", 3 | "version": "1.3.0", 4 | "homepage": "https://coypu.co", 5 | "license": "MIT", 6 | "description": "Text-editor-like weekly planner", 7 | "author": "Bart Kozal ", 8 | "private": true, 9 | "scripts": { 10 | "start": "yarn dev", 11 | "postinstall": "install-app-deps", 12 | "dev": "node build/dev-server.js", 13 | "build": "node build/build.js", 14 | "app": "electron app", 15 | "pack": "yarn run build && build --dir", 16 | "dist": "yarn run build && build -mwl", 17 | "test": "jest", 18 | "lint": "eslint --ext .js,.vue src test" 19 | }, 20 | "build": { 21 | "appId": "bkzl.coypu", 22 | "copyright": "© 2017 Bart Kozal, Maciej Kozal", 23 | "productName": "Coypu", 24 | "asar": true, 25 | "forceCodeSigning": true, 26 | "publish": { 27 | "provider": "generic", 28 | "url": "https://download.coypu.co/" 29 | }, 30 | "dmg": { 31 | "title": "${productName}" 32 | }, 33 | "directories": { 34 | "buildResources": "build/resources", 35 | "output": "dist", 36 | "app": "app" 37 | }, 38 | "linux": { 39 | "category": "Office", 40 | "packageCategory": "misc", 41 | "target": "deb", 42 | "synopsis": "Text-editor-like weekly planner" 43 | }, 44 | "mac": { 45 | "category": "public.app-category.productivity", 46 | "target": ["dmg", "zip"] 47 | }, 48 | "squirrelWindows": { 49 | "iconUrl": "https://coypu.co/icon.ico", 50 | "loadingGif": "build/resources/spinner.gif" 51 | }, 52 | "win": { 53 | "target": "squirrel" 54 | } 55 | }, 56 | "jest": { 57 | "moduleNameMapper": { 58 | "(.*)$": "/src/$1" 59 | }, 60 | "moduleFileExtensions": ["js", "vue"], 61 | "transform": { 62 | ".*\\.js$": "babel-jest", 63 | ".*\\.vue$": "jest-vue-preprocessor" 64 | } 65 | }, 66 | "dependencies": { 67 | "autosize": "3.0.21", 68 | "lodash": "4.17.4", 69 | "moment": "2.19.2", 70 | "moment-range": "3.0.3", 71 | "normalize.css": "5.0.0", 72 | "perfect-scrollbar": "0.6.16", 73 | "platform": "1.3.4", 74 | "pouchdb": "6.3.4", 75 | "sensible.css": "1.1.0", 76 | "vue": "2.5.7", 77 | "vuex": "2.5.0" 78 | }, 79 | "devDependencies": { 80 | "babel-core": "6.0.0", 81 | "babel-eslint": "7.0.0", 82 | "babel-loader": "6.0.0", 83 | "babel-plugin-transform-runtime": "6.0.0", 84 | "babel-preset-es2015": "6.0.0", 85 | "babel-preset-stage-2": "6.0.0", 86 | "babel-register": "6.0.0", 87 | "chalk": "1.1.3", 88 | "connect-history-api-fallback": "1.1.0", 89 | "css-loader": "0.25.0", 90 | "electron": "1.7.12", 91 | "electron-builder": "11.4.4", 92 | "electron-builder-squirrel-windows": "11.4.0", 93 | "eslint": "3.7.1", 94 | "eslint-config-standard": "6.1.0", 95 | "eslint-friendly-formatter": "2.0.5", 96 | "eslint-loader": "1.5.0", 97 | "eslint-plugin-html": "1.3.0", 98 | "eslint-plugin-promise": "3.4.0", 99 | "eslint-plugin-standard": "2.0.1", 100 | "eventsource-polyfill": "0.9.6", 101 | "express": "4.13.3", 102 | "extract-text-webpack-plugin": "1.0.1", 103 | "file-loader": "0.9.0", 104 | "function-bind": "1.0.2", 105 | "html-webpack-plugin": "2.8.1", 106 | "http-proxy-middleware": "0.17.2", 107 | "isparta-loader": "2.0.0", 108 | "jest": "19.0.2", 109 | "jest-vue-preprocessor": "0.1.2", 110 | "json-loader": "0.5.4", 111 | "opn": "4.0.2", 112 | "postcss-cssnext": "2.9.0", 113 | "postcss-import": "9.0.0", 114 | "semver": "5.3.0", 115 | "shelljs": "0.7.4", 116 | "stylelint": "8.0.0", 117 | "stylelint-config-recess-order": "1.0.0", 118 | "stylelint-config-standard": "18.0.0", 119 | "stylelint-processor-html": "1.0.0", 120 | "stylelint-webpack-plugin": "0.9.0", 121 | "url-loader": "0.5.7", 122 | "vue-loader": "10.0.2", 123 | "vue-style-loader": "1.0.0", 124 | "vue-template-compiler": "2.1.0", 125 | "webpack": "1.13.2", 126 | "webpack-dev-middleware": "1.8.3", 127 | "webpack-hot-middleware": "2.12.2", 128 | "webpack-merge": "0.14.1" 129 | }, 130 | "engines": { 131 | "node": ">= 4.0.0", 132 | "npm": ">= 3.0.0" 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 51 | 52 | 53 | 54 | 55 | 114 | -------------------------------------------------------------------------------- /src/assets/ClearSans-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bartkozal/coypu/cac223af532089a3cf0441e5f1d87ff1c94e4607/src/assets/ClearSans-Regular.ttf -------------------------------------------------------------------------------- /src/assets/bg-select.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icon-chevron-left.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icon-chevron-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/icon-close.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/assets/icon-settings-active.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/assets/icon-settings.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/components/List.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 63 | 64 | 131 | -------------------------------------------------------------------------------- /src/components/Settings.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 43 | 44 | 101 | -------------------------------------------------------------------------------- /src/components/Settings/Key.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 36 | 37 | 80 | -------------------------------------------------------------------------------- /src/components/Settings/Locale.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 464 | 465 | 489 | -------------------------------------------------------------------------------- /src/components/Task.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 28 | 29 | 37 | -------------------------------------------------------------------------------- /src/components/Task/Checkbox.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 23 | 24 | 63 | -------------------------------------------------------------------------------- /src/components/Task/Input.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 173 | 174 | 205 | -------------------------------------------------------------------------------- /src/components/Topbar.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 60 | 61 | 86 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | // The Vue build version to load with the `import` command 2 | // (runtime-only or standalone) has been set in webpack.base.conf with an alias. 3 | import Vue from 'vue' 4 | import App from './App' 5 | import store from './store' 6 | 7 | /* eslint-disable no-new */ 8 | new Vue({ 9 | el: '#app', 10 | template: '', 11 | store, 12 | components: { App } 13 | }) 14 | -------------------------------------------------------------------------------- /src/store/db.js: -------------------------------------------------------------------------------- 1 | import PouchDB from 'pouchdb' 2 | import { groupBy, forEach } from 'lodash' 3 | 4 | export const db = new PouchDB('coypu-offline') 5 | const dbVersion = 1 6 | 7 | const schemaKey = 'schema' 8 | 9 | // TODO: works only for migration to 1.2.0 10 | export const migrate = () => { 11 | db.get(schemaKey).catch(error => { 12 | if (error.status === 404) { 13 | db.allDocs({ 14 | include_docs: true 15 | }).then(docs => { 16 | return Promise.all(docs.rows.map(row => { 17 | const grouped = groupBy(row.doc.tasks, 'date') 18 | const days = [] 19 | 20 | forEach(grouped, (tasks, date) => { 21 | days.push({ 22 | _id: date, 23 | tasks: tasks.map(task => { 24 | return { body: task.body, completion: task.completion } 25 | }) 26 | }) 27 | }) 28 | 29 | return db.bulkDocs(days).then(() => { 30 | db.remove(row.id, row.value.rev) 31 | }) 32 | })) 33 | }).then(() => { 34 | db.put({ 35 | _id: schemaKey, 36 | version: dbVersion 37 | }) 38 | }).then(() => { 39 | window.location.reload(true) 40 | }) 41 | } 42 | }) 43 | } 44 | -------------------------------------------------------------------------------- /src/store/helpers.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment' 2 | import { isUndefined, values, flatten } from 'lodash' 3 | import { extendMoment } from 'moment-range' 4 | 5 | export const previousTask = (state, cb) => { 6 | const tasks = flatten(values(state.list)) 7 | const index = tasks.indexOf(state.activeTask) 8 | 9 | if (!isUndefined(cb)) { cb(state) } 10 | 11 | return tasks[index - 1] 12 | } 13 | 14 | export const nextTask = (state, cb) => { 15 | const tasks = flatten(values(state.list)) 16 | const index = tasks.indexOf(state.activeTask) 17 | 18 | if (!isUndefined(cb)) { cb(state) } 19 | 20 | return tasks[index + 1] 21 | } 22 | 23 | export const weekDays = timeline => { 24 | const dayFormat = 'YYYY-MM-DD' 25 | const startOfWeek = timeline.startOf('week').format(dayFormat) 26 | const endOfWeek = timeline.endOf('week').format(dayFormat) 27 | const week = extendMoment(moment).range(startOfWeek, endOfWeek).by('days') 28 | 29 | return Array.from(week).map(day => day.format(dayFormat)) 30 | } 31 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | import list from './modules/list' 5 | import timeline from './modules/timeline' 6 | import caret from './modules/caret' 7 | import settings from './modules/settings' 8 | 9 | Vue.use(Vuex) 10 | 11 | export default new Vuex.Store({ 12 | modules: { 13 | list, 14 | timeline, 15 | caret, 16 | settings 17 | } 18 | }) 19 | -------------------------------------------------------------------------------- /src/store/localstorage.js: -------------------------------------------------------------------------------- 1 | export const getItem = (key) => { 2 | return window.localStorage.getItem(key) 3 | } 4 | 5 | export const setItem = (key, value) => { 6 | window.localStorage.setItem(key, value) 7 | } 8 | -------------------------------------------------------------------------------- /src/store/modules/caret.js: -------------------------------------------------------------------------------- 1 | export default { 2 | state: { 3 | caretOffset: 0 4 | }, 5 | getters: { 6 | caretOffset: state => { return state.caretOffset } 7 | }, 8 | mutations: { 9 | setCaretOffset (state, offset) { 10 | state.caretOffset = offset 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/store/modules/list.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import moment from 'moment' 3 | import { isUndefined } from 'lodash' 4 | import { previousTask, nextTask } from '../helpers' 5 | 6 | export default { 7 | state: { 8 | activeTask: null, 9 | activeDay: null, 10 | selectWithFocus: false, 11 | list: {} 12 | }, 13 | getters: { 14 | activeTask: state => { return state.activeTask }, 15 | list: state => { return state.list }, 16 | listKey: (_, __, rootState) => { 17 | return moment(rootState.timeline.timelineDate).format('YYYY-w') 18 | }, 19 | selectWithFocus: state => { return state.selectWithFocus } 20 | }, 21 | mutations: { 22 | setList (state, list) { 23 | state.list = list 24 | }, 25 | createTask (state, { day, body, insertBefore }) { 26 | const dayList = state.list[day] 27 | const newTask = { 28 | body: body, 29 | completion: false 30 | } 31 | 32 | if (state.activeTask) { 33 | if (insertBefore) { 34 | newTask.completion = state.activeTask.completion 35 | state.activeTask.completion = false 36 | } 37 | dayList.splice(dayList.indexOf(state.activeTask) + 1, 0, newTask) 38 | } else { 39 | dayList.push(newTask) 40 | } 41 | 42 | state.activeTask = dayList[dayList.indexOf(newTask)] 43 | }, 44 | updateTaskCompletion ({ activeTask }, completion) { 45 | Vue.set(activeTask, 'completion', completion) 46 | }, 47 | updateTaskBody ({ activeTask }, body) { 48 | Vue.set(activeTask, 'body', body) 49 | }, 50 | updateTaskNote ({ activeTask }, note) { 51 | Vue.set(activeTask, 'note', note) 52 | }, 53 | toggleTask (state, task) { 54 | task.completion = !task.completion 55 | }, 56 | selectTask (state, { day, task, withFocus }) { 57 | state.activeDay = day 58 | state.activeTask = task 59 | state.selectWithFocus = withFocus 60 | }, 61 | selectPreviousTask (state) { 62 | const task = previousTask(state) 63 | 64 | if (task) { 65 | state.activeTask = task 66 | state.selectWithFocus = true 67 | } 68 | }, 69 | selectNextTask (state) { 70 | const task = nextTask(state) 71 | 72 | if (task) { 73 | state.activeTask = task 74 | state.selectWithFocus = true 75 | } 76 | }, 77 | removeTask (state) { 78 | const task = previousTask(state, (state) => { 79 | const dayList = state.list[state.activeDay] 80 | const index = dayList.indexOf(state.activeTask) 81 | dayList.splice(index, 1) 82 | }) 83 | 84 | if (task) { state.activeTask = task } else { 85 | state.activeTask = nextTask(state) 86 | } 87 | }, 88 | joinTasks (state) { 89 | const task = previousTask(state) 90 | 91 | if (task) { 92 | task.body = task.body.concat(state.activeTask.body) 93 | } 94 | } 95 | }, 96 | actions: { 97 | joinTasks ({ commit, dispatch }, { caretOffset }) { 98 | commit('joinTasks') 99 | commit('removeTask') 100 | commit('setCaretOffset', caretOffset) 101 | dispatch('saveTimeline') 102 | }, 103 | updateTask ({ commit, dispatch }, { body, completion, note }) { 104 | if (!isUndefined(body)) { commit('updateTaskBody', body) } 105 | if (!isUndefined(completion)) { commit('updateTaskCompletion', completion) } 106 | if (!isUndefined(note)) { commit('updateTaskNote', note) } 107 | dispatch('saveTimeline') 108 | }, 109 | toggleTask ({ commit, dispatch }, task) { 110 | commit('toggleTask', task) 111 | dispatch('saveTimeline') 112 | }, 113 | createTask ({ commit, dispatch }, { day, body = '', caretOffset = 0, insertBefore = false }) { 114 | commit('createTask', { day, body, insertBefore }) 115 | commit('setCaretOffset', caretOffset) 116 | dispatch('saveTimeline') 117 | }, 118 | selectTask ({ commit }, { day, task, withFocus }) { 119 | commit('selectTask', { day, task, withFocus }) 120 | }, 121 | selectPreviousTask ({ commit }) { 122 | commit('selectPreviousTask') 123 | }, 124 | selectNextTask ({ commit }) { 125 | commit('selectNextTask') 126 | }, 127 | deselectTask ({ commit }) { 128 | commit('selectTask', { day: null, task: null, withFocus: false }) 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/store/modules/settings.js: -------------------------------------------------------------------------------- 1 | import { getItem, setItem } from '../localstorage' 2 | 3 | const calendarLocaleKey = 'calendar-locale' 4 | 5 | export default { 6 | state: { 7 | calendarLocale: getItem(calendarLocaleKey) || 'en-gb' 8 | }, 9 | getters: { 10 | calendarLocale: state => { return state.calendarLocale } 11 | }, 12 | mutations: { 13 | setCalendarLocale (state, locale) { 14 | state.calendarLocale = locale 15 | setItem(calendarLocaleKey, locale) 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/store/modules/timeline.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment' 2 | import { get, debounce } from 'lodash' 3 | import { db } from '../db' 4 | import { weekDays } from '../helpers' 5 | 6 | export default { 7 | state: { 8 | timelineDate: null, 9 | timelineTransition: null 10 | }, 11 | getters: { 12 | timeline: (state, _, rootState) => { 13 | return moment(state.timelineDate).locale(rootState.settings.calendarLocale) 14 | }, 15 | timelineDate: state => { return state.timelineDate }, 16 | timelineTransition: state => { return state.timelineTransition } 17 | }, 18 | mutations: { 19 | setDate (state, date) { 20 | state.timelineDate = date 21 | }, 22 | setTransition (state, name) { 23 | state.timelineTransition = name 24 | } 25 | }, 26 | actions: { 27 | setTimeline ({ state, commit, getters }, date = moment().format()) { 28 | const isAfter = moment(date).isAfter(state.timelineDate) 29 | 30 | commit('setTransition', isAfter ? 'next' : 'previous') 31 | commit('setDate', date) 32 | 33 | db.allDocs({ 34 | include_docs: true, 35 | keys: weekDays(getters.timeline) 36 | }).then(result => { 37 | return result.rows.reduce((list, row) => { 38 | list[row.key] = get(row, 'doc.tasks', []) 39 | return list 40 | }, {}) 41 | }).then(list => { 42 | commit('setList', list) 43 | }).catch(error => { 44 | throw error 45 | }) 46 | }, 47 | saveTimeline: debounce(({ getters, rootState }) => { 48 | db.allDocs({ 49 | include_docs: true, 50 | keys: weekDays(getters.timeline) 51 | }).then(result => { 52 | return result.rows.reduce((docs, row) => { 53 | const rev = get(row, 'value.rev') 54 | const doc = { 55 | _id: row.key, 56 | tasks: JSON.parse(JSON.stringify(rootState.list.list[row.key])) 57 | } 58 | 59 | if (rev) { doc['_rev'] = rev } 60 | docs.push(doc) 61 | 62 | return docs 63 | }, []) 64 | }).then(docs => { 65 | db.bulkDocs(docs) 66 | }).catch(error => { 67 | throw error 68 | }) 69 | }, 300) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/variables.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --font-stack: 'Clear Sans', sans-serif; 3 | --font-base: 1em; 4 | --font-ratio: 1.3333; 5 | --font-leading: 1.45; 6 | --font-small: calc(1em / var(--font-ratio)); 7 | --font-large: calc(1em * var(--font-ratio)); 8 | --spacing-unit: 1rem; 9 | --color-text: #111; 10 | --color-primary: #ea7362; 11 | --color-secondary: #b74242; 12 | --color-muted: #888; 13 | --transition-duration: 0.2s; 14 | --z-settings-view: 1; 15 | --z-settings-icon: 2; 16 | } 17 | -------------------------------------------------------------------------------- /test/components/settings-key.test.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import SettingsKey from 'components/Settings/Key.vue' 3 | 4 | const ctor = ({ keycap }) => { 5 | return new Vue({ 6 | extends: SettingsKey, 7 | propsData: { keycap } 8 | }) 9 | } 10 | 11 | describe('keys', () => { 12 | test('return single key for one word keycap', () => { 13 | const vm = ctor({ keycap: 'Enter' }) 14 | expect(vm.keys).toEqual(['Enter']) 15 | }) 16 | 17 | test('return multiple keys for keycap with "or"', () => { 18 | const vm = ctor({ keycap: 'Down or Up' }) 19 | expect(vm.keys).toEqual(['Down', 'Up']) 20 | }) 21 | 22 | test('return multiple keys for keycap with "and"', () => { 23 | const vm = ctor({ keycap: 'Shift and T' }) 24 | expect(vm.keys).toEqual(['Shift', 'T']) 25 | }) 26 | }) 27 | 28 | describe('atOnce', () => { 29 | test('return false for one word keycap', () => { 30 | const vm = ctor({ keycap: 'Enter' }) 31 | expect(vm.atOnce).toBe(false) 32 | }) 33 | 34 | test('return false for keycap with "or"', () => { 35 | const vm = ctor({ keycap: 'Down or Up' }) 36 | expect(vm.atOnce).toBe(false) 37 | }) 38 | 39 | test('return true for keycap with "and"', () => { 40 | const vm = ctor({ keycap: 'Shift and T' }) 41 | expect(vm.atOnce).toBe(true) 42 | }) 43 | }) 44 | 45 | test('keycap with "and" renders "+" connector', () => { 46 | const vm = ctor({ keycap: 'Shift and T'}) 47 | const el = vm.$mount().$el 48 | expect(el.innerHTML).toMatch('+') 49 | }) 50 | --------------------------------------------------------------------------------