├── src ├── assets │ ├── tomato.png │ ├── defaultIco.svg │ ├── addTODO.svg │ ├── textareaIco.svg │ ├── information.svg │ ├── staticsIco.svg │ ├── rmTODO.svg │ ├── onGoingTomato.svg │ ├── tomato.svg │ ├── focusing.svg │ ├── edit.svg │ ├── add.svg │ ├── abandonTomatoOnGoing.svg │ └── abandonTomato.svg ├── colors.js ├── manifest.json ├── util │ ├── generateClockAnimations.js │ ├── minuteAnimation.js │ ├── showSidebar.js │ ├── generateCalender.js │ └── showTodoListAndTomatoes.js ├── styles │ ├── switch.scss │ ├── statics.scss │ ├── modal.scss │ ├── listsMenu.scss │ ├── todo.scss │ └── index.scss ├── background.js ├── index.js ├── index.html ├── scripts │ └── create-demo-tomatoes.js └── store.js ├── .editorconfig ├── LICENSE ├── .eslintrc ├── .github └── FUNDING.yml ├── blog ├── categories-feature-cn.md ├── stay_focus_when_doing_tomato-cn.md ├── categories-feature.md ├── stay_focus_when_doing_tomato.md └── README.md ├── .scss-lint.yml ├── .gitignore ├── package.json └── README.md /src/assets/tomato.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/skyme5/tomato-pie/master/src/assets/tomato.png -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.{js,scss}] 4 | indent_style = space 5 | indent_size = 2 6 | max_line_length = 120 7 | quote_type = single -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) T9t Software Inc 2 | 3 | Tomato Pie is an Open Source project licensed under the terms of 4 | the GPLv3 license. Please see 5 | for license text. -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["airbnb-base"], 3 | "env": { 4 | "browser": true, 5 | "webextensions": true 6 | }, 7 | "rules": { 8 | "no-console": "off", 9 | "no-return-assign": "warn", 10 | "no-param-reassign": "warn" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/colors.js: -------------------------------------------------------------------------------- 1 | const colors = ['#e6194b', '#3cb44b', '#ffe119', '#4363d8', '#f58231', '#911eb4', '#46f0f0', '#f032e6', '#bcf60c', '#fabebe', '#008080', '#e6beff', '#9a6324', '#fffac8', '#800000', '#aaffc3', '#808000', '#ffd8b1', '#000075', '#808080', '#ffffff', '#000000']; 2 | 3 | export default colors; 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: timqian # Replace with a single Patreon username 5 | open_collective: t9tio # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | custom: # Replace with a single custom sponsorship URL 9 | -------------------------------------------------------------------------------- /src/assets/defaultIco.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/assets/addTODO.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /src/assets/textareaIco.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /blog/categories-feature-cn.md: -------------------------------------------------------------------------------- 1 | > [English](./categories-feature.md) 2 | 3 | ## 关于 Tomato Pie 4 | 5 | Tomato Pie 的两个主要部分是 todo list 和 “tomato-pie”(周围围绕着番茄的时钟)。用户可以从 todo 启动番茄,然后番茄的历史的围绕在时钟周围,提供了对过去12小时工作状态粗略但直观的估计。 6 | 7 | ![](https://raw.githubusercontent.com/timqian/images/master/about-tomato-pie.gif) 8 | 9 | ## 关于 categories 10 | 11 | 当你的 todo list 越来越长,对他们分类的需求自然就出现了 12 | 13 | 但是保持产品的简洁性也是很重要的。考虑到这一点,对于没有分类需求的用户,可以自然得忽略此功能,好像只有一个待办事项列表一样使用。当更多的待办事项进入并且您发现要对待办事项进行分组时,您可以开始向待办事项添加类别并轻松管理它们。 14 | 15 | ![](https://raw.githubusercontent.com/timqian/images/master/tomato-pie-categories.png) 16 | 17 | ## 其他更新 18 | 19 | - 用户可以选择关闭或打开 [focusing mode](./stay_focus_when_doing_tomato-cn.md) 20 | - 用户设置也移至了侧边栏 -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "author": "timqian", 4 | "homepage_url": "https://github.com/t9tio/tomato-pie", 5 | "name": "Tomato Pie", 6 | "description": "A new UI for Pomodoro Technique. Peek into your working status with ease", 7 | "version": "2.8", 8 | "icons": { 9 | "128": "assets/tomato.png" 10 | }, 11 | "browser_action": { 12 | "default_icon": "assets/tomato.png" 13 | }, 14 | "permissions": [ 15 | "tabs", 16 | "storage", 17 | "notifications", 18 | "background" 19 | ], 20 | "background": { 21 | "scripts": [ 22 | "background.js" 23 | ] 24 | }, 25 | "chrome_url_overrides": { 26 | "newtab": "index.html" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.scss-lint.yml: -------------------------------------------------------------------------------- 1 | severity: error 2 | 3 | linters: 4 | 5 | BorderZero: 6 | enabled: true 7 | convention: zero 8 | 9 | BemDepth: 10 | enabled: true 11 | 12 | DeclarationOrder: 13 | enabled: false 14 | 15 | ExtendDirective: 16 | enabled: true 17 | 18 | LeadingZero: 19 | enabled: false 20 | 21 | NameFormat: 22 | enabled: true 23 | 24 | PrivateNamingConvention: 25 | enabled: true 26 | prefix: _ 27 | 28 | PropertySortOrder: 29 | enabled: false 30 | 31 | QualifyingElement: 32 | enabled: false 33 | 34 | SelectorFormat: 35 | enabled: true 36 | convention: hyphenated_BEM 37 | class_convention: ^(?!js-).* 38 | class_convention_explanation: should not be written in the form js-* 39 | 40 | SingleLinePerProperty: 41 | enabled: true 42 | allow_single_line_rule_sets: false 43 | 44 | StringQuotes: 45 | enabled: true 46 | style: double_quotes -------------------------------------------------------------------------------- /src/util/generateClockAnimations.js: -------------------------------------------------------------------------------- 1 | // generate clock animations by using keyframes and animation 2 | // TODO: move to index.html? 3 | function generateClockAnimations() { 4 | const now = new Date(); 5 | const hourDeg = now.getHours() / 12 * 360 + now.getMinutes() / 60 * 30; 6 | const minuteDeg = now.getMinutes() / 60 * 360 + now.getSeconds() / 60 * 6; 7 | const secondDeg = now.getSeconds() / 60 * 360; 8 | const stylesDeg = [ 9 | `@keyframes rotate-hour{from{transform:rotate(${hourDeg}deg);}to{transform:rotate(${hourDeg + 360}deg);}}`, 10 | `@keyframes rotate-minute{from{transform:rotate(${minuteDeg}deg);}to{transform:rotate(${minuteDeg + 360}deg);}}`, 11 | `@keyframes rotate-second{from{transform:rotate(${secondDeg}deg);}to{transform:rotate(${secondDeg + 360}deg);}}`, 12 | ].join(''); 13 | 14 | document.getElementById('clock-animations-style').innerHTML = stylesDeg; 15 | } 16 | 17 | generateClockAnimations(); 18 | -------------------------------------------------------------------------------- /src/assets/information.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /blog/stay_focus_when_doing_tomato-cn.md: -------------------------------------------------------------------------------- 1 | > [English](./stay_focus_when_doing_tomato.md) 2 | 3 | ### 关于 tomato-pie 的前序介绍 4 | 5 | - https://www.v2ex.com/t/538331#reply24 6 | - https://www.v2ex.com/t/547816#reply8 7 | 8 | ### 缘起 9 | 10 | 最近在阅读 [Getting Things Done 11 | ](https://en.wikipedia.org/wiki/Getting_Things_Done) by [David Allen](https://en.wikipedia.org/wiki/David_Allen_(author))。里面提到的一个点很触动到我 -- 关注 "next action" 而不是同时思考着很多事情。整理好自己的 todo list 们,排好优先级之后,只关注接下来你要做的事情并且专注于此。这样的工作方式下自己会更专注,感受到的压力更小,从而更有效率。 12 | 13 | ### Tomato pie 的 "next action" 14 | 15 | 于是顺手给 tomato-pie 加了一个功能: 当你在为某个 todo 做番茄时,将其他 todo 模糊掉: 16 | 17 | ![](https://raw.githubusercontent.com/timqian/images/master/focusing_mode.gif) 18 | 19 | 20 | ### 欢迎安装试用 21 | 22 | - [从 chrome web store 安装试用]( https://chrome.google.com/webstore/detail/gffgechdocgfajkbpinmjjjlkjfjampi) 23 | - [Github 上的源代码](https://github.com/t9tio/tomato-pie) 24 | - [加入 t9t.io spectrum 群](https://spectrum.chat/t9tio) 25 | - [加入 t9t.io 微信群](https://user-images.githubusercontent.com/5512552/40399903-53d1ebde-5e72-11e8-98d8-615fc40c09f1.jpeg) 26 | -------------------------------------------------------------------------------- /src/assets/staticsIco.svg: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /blog/categories-feature.md: -------------------------------------------------------------------------------- 1 | > [中文](./categories-feature-cn.md) 2 | 3 | ## About Tomato Pie 4 | 5 | The two major part of tomato-pie is the todo list and the "tomato-pie"(the clock with tomatoes around). User can start a tomato of a todo and the tomato history is shown directly around the clock. Which provide a rough measurement of your working status of last 12 hours. 6 | 7 | ![](https://raw.githubusercontent.com/timqian/images/master/about-tomato-pie.gif) 8 | 9 | ## Introducing categories 10 | 11 | As your todo list grows longer, the demand to categorize them become obvious. 12 | 13 | But also, keep the simplicity of Tomato Pie is also important. 14 | 15 | For simple use case, you can just ignore this feature and use tomato-pie as if there is only one todo list. When more todos come in and you find you want to group the todos, you can start adding categories to todos and managing them with ease. 16 | 17 | ![](https://raw.githubusercontent.com/timqian/images/master/tomato-pie-categories.png) 18 | 19 | ## Other updates 20 | 21 | - User is able to turn on/off [focusing mode](./stay_focus_when_doing_tomato.md) now. 22 | - The setting is moved to sidebar 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .cache 2 | *.crx 3 | dist 4 | .DS_Store 5 | oldAssets 6 | marketing.md 7 | dist.zip 8 | 9 | # Logs 10 | logs 11 | *.log 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | *.pid.lock 21 | 22 | # Directory for instrumented libs generated by jscoverage/JSCover 23 | lib-cov 24 | 25 | # Coverage directory used by tools like istanbul 26 | coverage 27 | 28 | # nyc test coverage 29 | .nyc_output 30 | 31 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 32 | .grunt 33 | 34 | # Bower dependency directory (https://bower.io/) 35 | bower_components 36 | 37 | # node-waf configuration 38 | .lock-wscript 39 | 40 | # Compiled binary addons (https://nodejs.org/api/addons.html) 41 | build/Release 42 | 43 | # Dependency directories 44 | node_modules/ 45 | jspm_packages/ 46 | 47 | # TypeScript v1 declaration files 48 | typings/ 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Optional REPL history 57 | .node_repl_history 58 | 59 | # Output of 'npm pack' 60 | *.tgz 61 | 62 | # Yarn Integrity file 63 | .yarn-integrity 64 | 65 | # dotenv environment variables file 66 | .env 67 | 68 | # next.js build output 69 | .next 70 | -------------------------------------------------------------------------------- /src/styles/switch.scss: -------------------------------------------------------------------------------- 1 | // ref: https://codepen.io/mallendeo/pen/eLIiG 2 | .tgl { 3 | display: none; 4 | 5 | // add default box-sizing for this scope 6 | &, 7 | &:after, 8 | &:before, 9 | & *, 10 | & *:after, 11 | & *:before, 12 | & + .tgl-btn { 13 | box-sizing: border-box; 14 | &::selection { 15 | background: none; 16 | } 17 | } 18 | 19 | + .tgl-btn { 20 | outline: 0; 21 | display: block; 22 | width: 2em; 23 | height: 1em; 24 | position: relative; 25 | cursor: pointer; 26 | user-select: none; 27 | &:after, 28 | &:before { 29 | position: relative; 30 | display: block; 31 | content: ''; 32 | width: 50%; 33 | height: 100%; 34 | } 35 | 36 | &:after { 37 | left: 0; 38 | } 39 | 40 | &:before { 41 | display: none; 42 | } 43 | } 44 | 45 | &:checked + .tgl-btn:after { 46 | left: 50%; 47 | } 48 | } 49 | 50 | // themes 51 | .tgl-light { 52 | + .tgl-btn { 53 | background: #f0f0f0; 54 | border-radius: 1em; 55 | padding: 2px; 56 | transition: all 0.4s ease; 57 | &:after { 58 | border-radius: 50%; 59 | background: #fff; 60 | transition: all 0.2s ease; 61 | } 62 | } 63 | 64 | &:checked + .tgl-btn { 65 | background: #7bc96f; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/assets/rmTODO.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/onGoingTomato.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 10 | 11 | 14 | 15 | 17 | 18 | -------------------------------------------------------------------------------- /src/assets/tomato.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 10 | 11 | 14 | 15 | 17 | 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tomato-pie", 3 | "version": "1.0.0", 4 | "description": "Schedule your time with a clock", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "cp src/background.js dist/background.js && cp src/manifest.json dist/manifest.json && cp -r src/assets dist && parcel watch src/background.js src/index.html --public-url '.' --no-cache", 8 | "build": "rm -r dist && rm dist.zip && parcel build src/background.js src/index.html --public-url '.' --no-source-maps && cp src/manifest.json dist/manifest.json && cp -r src/assets dist && zip -r dist.zip dist", 9 | "packDisk": "crx pack dist -o tomato-pie.crx", 10 | "test": "mocha --require babel-polyfill --require babel-register -u qunit './**/*.spec.js'", 11 | "lint": "eslint ./src" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/getPie/Pie.git" 16 | }, 17 | "author": "timqian", 18 | "bugs": { 19 | "url": "https://github.com/getPie/Pie/issues" 20 | }, 21 | "homepage": "https://github.com/getPie/Pie#readme", 22 | "dependencies": { 23 | "calendar-graph": "^1.0.0", 24 | "sortablejs": "^1.7.0" 25 | }, 26 | "devDependencies": { 27 | "crx": "^3.2.1", 28 | "eslint": "^5.14.1", 29 | "eslint-config-airbnb-base": "^13.1.0", 30 | "eslint-plugin-import": "^2.14.0", 31 | "mocha": "^5.2.0", 32 | "node-sass": "^4.11.0", 33 | "parcel": "^1.12.3", 34 | "should": "^13.2.3" 35 | }, 36 | "browserslist": [ 37 | "since 2017-06" 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /src/assets/focusing.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/assets/edit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/util/minuteAnimation.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Show tomato animation 3 | * @param startTime {Date} when the tomato begin 4 | */ 5 | function show(startTime) { 6 | if (typeof startTime !== 'number') throw new Error('startTime should be number'); 7 | document.getElementById('minite-animition-div').classList.remove('invisible'); 8 | const startDate = new Date(startTime); 9 | const currentDate = new Date(); 10 | 11 | // - 45 because the edge start from there 12 | const workHalfDeg = startDate.getMinutes() / 60 * 360 + startDate.getSeconds() / 60 * 6 - 45; 13 | const invisibleHalfDeg = workHalfDeg + 180; 14 | const restHalfDeg = workHalfDeg + ((25 / 60) * 360); 15 | const doneHalfDeg = currentDate.getMinutes() / 60 * 360 16 | + currentDate.getSeconds() / 60 * 6 + 180 - 45; 17 | 18 | document.querySelector('.work-half').style.transform = `rotate(${workHalfDeg}deg)`; 19 | document.querySelectorAll('.invisible-half').forEach(el => el.style.transform = `rotate(${invisibleHalfDeg}deg)`); 20 | document.querySelector('.rest-half').style.transform = `rotate(${restHalfDeg}deg)`; 21 | document.querySelector('.done-half').style.transform = `rotate(${doneHalfDeg}deg)`; 22 | 23 | document.getElementById('clock-animations-style').innerHTML = `${document.getElementById('clock-animations-style').innerHTML} 24 | @keyframes rotate-done-minute { 25 | from { 26 | transform: rotate(${doneHalfDeg}deg); 27 | } 28 | to { 29 | transform: rotate(${doneHalfDeg + 360}deg); 30 | } 31 | }; 32 | `; 33 | } 34 | 35 | /** 36 | * Hide tomato 30 min animation 37 | */ 38 | function hide() { 39 | document.getElementById('minite-animition-div').classList.add('invisible'); 40 | } 41 | 42 | export default { 43 | show, 44 | hide, 45 | }; 46 | -------------------------------------------------------------------------------- /src/styles/statics.scss: -------------------------------------------------------------------------------- 1 | $distance-to-edge: 15px; 2 | 3 | hr { 4 | display: block; 5 | height: 1px; 6 | border: 0; 7 | border-top: 1px solid #eee; 8 | margin: 1em 0; 9 | padding: 0; 10 | } 11 | 12 | .calendar { 13 | width: 805px; 14 | position: absolute; 15 | margin: 0 auto; 16 | bottom: $distance-to-edge; 17 | right: 0; 18 | left: 155px; 19 | padding: 6px; 20 | background-color: #fff; 21 | border: solid 1px #eee; 22 | border-radius: 3px; 23 | transition: 0.3s; 24 | 25 | &.calendar-pushed { 26 | left: 310px; // 505 - (250 / 2) 27 | } 28 | } 29 | 30 | .cg-day { 31 | &:hover { 32 | cursor: pointer; 33 | }; 34 | } 35 | 36 | // for calender hover 37 | #tooltip { 38 | display: none; 39 | position: fixed; 40 | left: 0; 41 | top: 0; 42 | height: 23px; 43 | background-color: rgba(0, 0, 0, 0.8); 44 | color: #fff; 45 | padding: 4px 10px; 46 | border-radius: 3px; 47 | z-index: 1; 48 | } 49 | 50 | #tooltip:after { 51 | display: block; 52 | position: absolute; 53 | content: ''; 54 | bottom: -6px; 55 | left: 50%; 56 | margin-left: -6px; 57 | width: 0; 58 | height: 0; 59 | border-left: 6px solid transparent; 60 | border-right: 6px solid transparent; 61 | border-top: 6px solid rgba(0, 0, 0, 0.8); 62 | } 63 | 64 | 65 | .textarea { 66 | resize: none; 67 | background: transparent; 68 | font-size: 15px; 69 | position: fixed; 70 | top: 50px; 71 | right: $distance-to-edge; 72 | width: 280px; 73 | height: 70%; 74 | border: 0px solid #eee; 75 | border-radius: 3px; 76 | outline: 0; 77 | // padding: 5px; 78 | color: #555; 79 | background-attachment:local; 80 | // ref; https://css-tricks.com/fun-line-height/ 81 | line-height: 30px; // 注意行高要和背景图高度一致resize: none; 82 | background: repeating-linear-gradient(transparent, transparent 1px, transparent 29px, rgba(221, 221, 221, 0.8) 30px) repeat; 83 | 84 | &::placeholder { 85 | color: rgba(221, 221, 221, 1); 86 | text-align: left; 87 | } 88 | } 89 | 90 | .setting-ico { 91 | display: inline; 92 | width: 1em; 93 | height: 1em; 94 | } 95 | -------------------------------------------------------------------------------- /blog/stay_focus_when_doing_tomato.md: -------------------------------------------------------------------------------- 1 | > [中文](./stay_focus_when_doing_tomato-cn.md) 2 | 3 | # Introducing focusing mode (v2.2 release) 4 | 5 | ## The idea 6 | 7 | Recently I was reading the book [Getting Things Done 8 | ](https://en.wikipedia.org/wiki/Getting_Things_Done) by [David Allen](https://en.wikipedia.org/wiki/David_Allen_(author)) in order to be more efficient on my work. One impressive point this book addressed out is to focusing on "next action" instead of thinking about many stuff at the same time. After you have collected all the tasks you need to do and ordered them in your todo list. The next thing you should do is to focusing one "next action" instead of worrying other tasks. In this way, you become more focused; more efficient; and feel less stress. 9 | 10 | ## The major feature 11 | 12 | I add a new feature to tomato-pie: to blur other todos when you are doing a tomato about certain todo. 13 | 14 | In most circumstance, After you start a tomato of a todo. You should close the tomato-pie tab and focusing on your work. But some times, when you return back to check what you are doing, you might got disrupted by other todos listed around it. By blur other todos, it helps user to be more focused on current task. 15 | 16 | ![](https://raw.githubusercontent.com/timqian/images/master/focusing_mode.gif) 17 | 18 | ## Other updates 19 | 20 | - add a new feedback channel (https://spectrum.chat/t9tio) 21 | - fix a bug: after tomato is done, tomato color and clock style is not right if user do not refresh the page 22 | 23 | ## "Next action" of tomato-pie 24 | 25 | The next feature I want to implement is to support more than one todo list, stay tuned! 26 | 27 | ![](https://raw.githubusercontent.com/timqian/images/master/reminder_multiple_list.gif) 28 | 29 | ## Interested? 30 | 31 | - [Install tomato-pie from chrome web store](https://chrome.google.com/webstore/detail/gffgechdocgfajkbpinmjjjlkjfjampi) 32 | - [Join t9t.io's community in spectrum](https://spectrum.chat/t9tio) 33 | - [Join t9t.io's wechat group](https://user-images.githubusercontent.com/5512552/40399903-53d1ebde-5e72-11e8-98d8-615fc40c09f1.jpeg) 34 | -------------------------------------------------------------------------------- /src/styles/modal.scss: -------------------------------------------------------------------------------- 1 | /* The Modal (background) */ 2 | .modal { 3 | display: none; /* Hidden by default */ 4 | position: fixed; /* Stay in place */ 5 | z-index: 100; /* Sit on top */ 6 | left: 0; 7 | top: 0; 8 | width: 100%; /* Full width */ 9 | height: 100%; /* Full height */ 10 | overflow: auto; /* Enable scroll if needed */ 11 | background-color: rgb(0,0,0); /* Fallback color */ 12 | background-color: rgba(0,0,0,0.85); /* Black w/ opacity */ 13 | a { 14 | color: #7bc96f; 15 | text-decoration: none; 16 | } 17 | } 18 | 19 | /* Modal Content/Box */ 20 | .modal-content { 21 | background-color: #fefefe; 22 | margin: 15% auto; /* 15% from the top and centered */ 23 | padding: 20px; 24 | // border: 1px solid #888; 25 | border-radius: 5px; 26 | width: 50%; /* Could be more or less, depending on screen size */ 27 | } 28 | 29 | .modal-heading { 30 | margin: 0; 31 | padding-bottom: 15px; 32 | border-bottom: solid 1px #ddd; 33 | text-align: center; 34 | } 35 | 36 | .modal-li { 37 | // line-height: 30px; 38 | position: relative; 39 | font-size: 15px; 40 | list-style: none; 41 | padding: 0; 42 | margin: 0; 43 | border: 0; 44 | // margin-left: 20%; 45 | // text-align: center; 46 | 47 | .modal-tomato { 48 | width: 15px; 49 | display: inline-block; 50 | } 51 | 52 | .checkbox { 53 | font-size: 13px; 54 | width: 16px; 55 | height: 16px; 56 | background-color: white; 57 | border-radius: 50%; 58 | vertical-align: middle; 59 | border: 1px solid #ddd; 60 | -webkit-appearance: none; 61 | outline: none; 62 | cursor: default; 63 | vertical-align: top; 64 | margin-top: 12px; 65 | &:checked { 66 | background-image:url("data:image/svg+xml;utf8,"); 67 | } 68 | } 69 | .modal-todo { 70 | display: inline-block; 71 | width: 270px; 72 | border-bottom: solid 1px #ddd; 73 | padding-top: 10px; 74 | padding-bottom: 10px; 75 | text-align: left; 76 | } 77 | } 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /blog/README.md: -------------------------------------------------------------------------------- 1 | ## Updates 2 | 3 | ### 2019-10-22: fix update link 4 | 5 | ### 2019-10-22: hyperlinks parsing 6 | 7 | - clickable hyperlinks in todo 8 | 9 | category color 10 | - bug fix: display bug for deleted todos on calendar 11 | - make donation message less eye-catching 12 | 13 | ### 2019-08-30: Assign colors to categories 14 | 15 | Each category has a color now! This might help you recogonize which category the todo belongs to when you are browsering all todos. 16 | 17 | category color 18 | 19 | ### What feature would you like to see first? 20 | 21 | [![](https://api.gh-polls.com/poll/01DKGPHBTAMR67XXDKPTPFB8PG/see%20finished%20todos%20by%20clicking%20todo%20list%20title)](https://api.gh-polls.com/poll/01DKGPHBTAMR67XXDKPTPFB8PG/see%20finished%20todos%20by%20clicking%20todo%20list%20title/vote) 22 | [![](https://api.gh-polls.com/poll/01DKGPHBTAMR67XXDKPTPFB8PG/todos%20today%20tab%20to%20plan%20for%20today's%20work)](https://api.gh-polls.com/poll/01DKGPHBTAMR67XXDKPTPFB8PG/todos%20today%20tab%20to%20plan%20for%20today's%20work/vote) 23 | [![](https://api.gh-polls.com/poll/01DKGPHBTAMR67XXDKPTPFB8PG/more%20detailed%20working%20status%20analysis)](https://api.gh-polls.com/poll/01DKGPHBTAMR67XXDKPTPFB8PG/more%20detailed%20working%20status%20analysis/vote) 24 | [![](https://api.gh-polls.com/poll/01DKGPHBTAMR67XXDKPTPFB8PG/reorder%20categories)](https://api.gh-polls.com/poll/01DKGPHBTAMR67XXDKPTPFB8PG/reorder%20categories/vote) 25 | [![](https://api.gh-polls.com/poll/01DKGPHBTAMR67XXDKPTPFB8PG/%20sync%20to%20cloud%28patron%20only%29)](https://api.gh-polls.com/poll/01DKGPHBTAMR67XXDKPTPFB8PG/%20sync%20to%20cloud%28patron%20only%29/vote) 26 | [![](https://api.gh-polls.com/poll/01DKGPHBTAMR67XXDKPTPFB8PG/customize%20tomato%20time)](https://api.gh-polls.com/poll/01DKGPHBTAMR67XXDKPTPFB8PG/customize%20tomato%20time/vote) 27 | 28 | ### 2019-04-12 29 | 30 | [Introducing categories](./categories-feature.md). [[中文]](./categories-feature-cn.md) 31 | 32 | ### 2019-04-03 33 | 34 | [Introducing focusing mode](./stay_focus_when_doing_tomato.md). [[中文]](./stay_focus_when_doing_tomato-cn.md) 35 | -------------------------------------------------------------------------------- /src/assets/add.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Layer 1 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/assets/abandonTomatoOnGoing.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/assets/abandonTomato.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

3 | 4 |

5 | 6 | [![Join us](https://badgen.net/badge/Join%20the%20community%20of%20t9t.io/Get%20in%20touch/green)](https://t9t.io/#contact) 7 | 8 | # Tomato Pie 9 | 10 | A new UI for [Pomodoro Technique](https://en.wikipedia.org/wiki/Pomodoro_Technique). Peek into your working status with ease. 11 | 12 | ## Install 13 | 14 | From [Chrome Web Store](https://chrome.google.com/webstore/detail/gffgechdocgfajkbpinmjjjlkjfjampi) or [manually](https://github.com/t9tio/tomato-pie/releases) 15 | 16 | ## [Recent updates](./blog) 17 | 18 | ## Features 19 | 20 | - start new pomodoro from TODO list 21 | - red part: 25 minutes for one tomato 22 | - green part: 5 minutes for rest 23 | - show tomatoes of last 12 hours on clock 24 | - view tomato history on calendar 25 | - override default page for new tab 26 | - indicate time on the extension icon 27 | - get notifications when a tomato is finished 28 | 29 | ## Screenshots 30 | 31 | ### Editing Todo list 32 | 33 | ![](https://raw.githubusercontent.com/timqian/images/master/tomatopie-intro1.gif) 34 | 35 | ### Doing Pomodoro 36 | 37 | ![](https://raw.githubusercontent.com/timqian/images/master/tomatopie-intro2.gif) 38 | 39 | ## Develop 40 | 41 | 1. `npm i` 42 | 1. `npm start` 43 | 1. Navigate to `chrome://extensions/` 44 | 1. Click the `load unpacked` button and load `dist` folder 45 | 46 | ## Architecture 47 | 48 | store ==> render function ==> view =update=> store 49 | 50 | ## Build and publish 51 | 52 | ```bash 53 | # update verision in src/manifest.json 54 | npm run build 55 | # zip dist file and upload to chrome webstore 56 | ``` 57 | 58 | ## Refs 59 | 60 | - Clock styles: https://codepen.io/collection/moAia/2/ 61 | 62 | #### Similar tools 63 | 64 | - https://chrome.google.com/webstore/detail/marinara-pomodoro%C2%AE-assist/lojgmehidjdhhbmpjfamhpkpodfcodef 65 | - https://chrome.google.com/webstore/detail/task-timer/aomfjmibjhhfdenfkpaodhnlhkolngif 66 | - https://chrome.google.com/webstore/detail/timer/hepmlgghomccjinhcnkkikjpgkjibglj 67 | - https://chrome.google.com/webstore/detail/timer-25-the-minimalist-t/gmdbcklinofignhfmibchnmgjcocccbh 68 | - https://chrome.google.com/webstore/detail/timecamp-timer/ohbkdjmhoegleofcohdjagmcnkimfdaa 69 | - https://chrome.google.com/webstore/detail/timer-25-the-minimalist-t/gmdbcklinofignhfmibchnmgjcocccbh 70 | 71 | ## Tech notes 72 | 73 | - Make background.js always running [ref](https://stackoverflow.com/questions/17119266/how-do-i-keep-my-app-from-going-inactive): 74 | 1. in`manifest.json`, add `background` in `permission` key 75 | 2. in `manifest.json`, don't add `persistence: false` in `background` key 76 | - clock animation: https://codepen.io/Alca/pen/ZeKjmB 77 | 78 | ## Thanks 79 | 80 | - Layla and [Joshua](https://github.com/joshua7v) for meaningful discussions 81 | -------------------------------------------------------------------------------- /src/styles/listsMenu.scss: -------------------------------------------------------------------------------- 1 | $sidebar-width: 150px; 2 | 3 | body { 4 | transition: margin-left 0.3s; 5 | 6 | &.body-pushed { 7 | margin-left: $sidebar-width; //- 27px; 8 | } 9 | } 10 | 11 | .sidebar { 12 | height: 100%; 13 | width: 0; // 0 width - change this with JavaScript 14 | position: fixed; // Stay in place 15 | color: #555; 16 | top: 0; 17 | left: 0; 18 | background-color: rgb(245, 245, 245); 19 | overflow-x: hidden; // Disable horizontal scroll 20 | padding-top: 48px; 21 | transition: 0.3s; 22 | 23 | &.sidebar-pushed { 24 | width: $sidebar-width; 25 | border-right: 1px; 26 | border-color: rgba(216, 216, 216, 0.068); 27 | } 28 | 29 | hr { 30 | margin: 6px 10px; 31 | border-top: 1px solid #ddd; 32 | } 33 | 34 | .add-tag-input { 35 | white-space: nowrap; 36 | margin: 5px 8px 5px 20px; 37 | width: 110px; 38 | } 39 | } 40 | 41 | .open-sidebar-btn { 42 | z-index: 100; 43 | font-size: 20px; 44 | cursor: pointer; 45 | border: 0; 46 | position: fixed; 47 | left: 17px; 48 | top: 16px; 49 | display: inline-block; 50 | // height: 1.3rem; 51 | // width: 1.3rem; 52 | // background-color: rgb(221, 217, 217); 53 | transition: 0.3s; 54 | 55 | &.sidebar-opened { 56 | left: 10px; 57 | } 58 | } 59 | 60 | .lists-div { 61 | // padding-left: 10px; 62 | // border-top: solid 3px #ddd; 63 | h2 { 64 | // font-size: 14px; 65 | margin: 0; 66 | padding: 10px 10px 5px 10px; 67 | } 68 | 69 | .all-todo-a { 70 | display: block; 71 | white-space: nowrap; 72 | padding: 5px 8px 5px 20px; 73 | cursor: pointer; 74 | 75 | &:hover { 76 | background-color: #d3d3d336; 77 | } 78 | 79 | &.selected { 80 | background-color: #d3d3d396; 81 | } 82 | } 83 | 84 | .lists-list { 85 | a { 86 | position: relative; 87 | display: block; 88 | white-space: nowrap; 89 | padding: 5px 8px 5px 20px; 90 | cursor: pointer; 91 | &:hover { 92 | background-color: #d3d3d336; 93 | } 94 | 95 | &.selected { 96 | background-color: #d3d3d396; 97 | } 98 | } 99 | 100 | .edit-tag-btn { 101 | position: absolute; 102 | right: 3px; 103 | top: 5px; 104 | width: 15px; 105 | } 106 | 107 | .remove-tag-btn { 108 | position: absolute; 109 | right: 20px; 110 | top: 5px; 111 | width: 15px; 112 | } 113 | } 114 | 115 | .add-tag-div { 116 | display: block; 117 | white-space: nowrap; 118 | padding: 15px 8px 5px 20px; 119 | cursor: pointer; 120 | } 121 | 122 | .add-btn { 123 | width: 9px; 124 | } 125 | } 126 | 127 | .settings-div { 128 | width: 100%; 129 | position: absolute; 130 | display: block; 131 | white-space: nowrap; 132 | left: 0; 133 | bottom: 5px; 134 | 135 | h2 { 136 | // font-size: .9em; 137 | margin: 0; 138 | padding: 10px 10px 5px 10px; 139 | } 140 | } 141 | 142 | .info-div { 143 | line-height: 1.6; 144 | color: #888; 145 | width: 100%; 146 | 147 | ul { 148 | margin: 0; 149 | padding-left: 10px; 150 | list-style-type: none; 151 | 152 | li { 153 | padding-top: 5px; 154 | } 155 | } 156 | 157 | a { 158 | color: #7bc96f; 159 | text-decoration: none; 160 | } 161 | 162 | .links { 163 | margin: 0; 164 | padding-left: 10px; 165 | // text-align: right; 166 | list-style-type: none; 167 | } 168 | } 169 | 170 | -------------------------------------------------------------------------------- /src/background.js: -------------------------------------------------------------------------------- 1 | import store from './store'; 2 | 3 | // adjust time speed 4 | const oneMinute = 60 * 1000; 5 | 6 | // update message 7 | chrome.runtime.onInstalled.addListener(() => { 8 | chrome.notifications.create('noti_id_update', { 9 | type: 'basic', 10 | iconUrl: 'assets/tomato.png', 11 | title: 'Tomato-pie updated', 12 | message: 'click to see update details', 13 | requireInteraction: true, // do not close until click 14 | }); 15 | }); 16 | 17 | // open new tab when click icon; ref: https://stackoverflow.com/a/14682627/4674834 18 | chrome.browserAction.onClicked.addListener(() => { 19 | window.focus(); 20 | chrome.tabs.create({ url: chrome.extension.getURL('index.html#from_action') }); 21 | }); 22 | 23 | // open new tab when click notification 24 | chrome.notifications.onClicked.addListener((notificationId) => { 25 | if (notificationId === 'noti_id_update') { 26 | window.focus(); 27 | chrome.tabs.create({ url: 'https://github.com/t9tio/tomato-pie/tree/master/blog#updates' }); 28 | chrome.notifications.clear(notificationId); 29 | } else { 30 | window.focus(); 31 | chrome.tabs.create({ url: chrome.extension.getURL('index.html#from_action') }); 32 | chrome.notifications.clear(notificationId); 33 | } 34 | }); 35 | 36 | let current = 25; 37 | let rest = 5; 38 | let currentTimeout; 39 | let restTimeout; 40 | 41 | async function updateRest() { 42 | if (rest === 0) { 43 | chrome.browserAction.setBadgeText({ text: '' }); 44 | chrome.notifications.create({ 45 | type: 'basic', 46 | iconUrl: 'assets/tomato.png', 47 | title: 'Rest done', 48 | message: (new Date()).toLocaleTimeString(), 49 | requireInteraction: true, // do not close until click 50 | buttons: [{ 51 | title: 'Start another tomato for current todo directly', 52 | }, { 53 | title: 'Abandon this tomato', 54 | }], 55 | }); 56 | await store.CurrentStartAt.remove(); 57 | } else { 58 | chrome.browserAction.setBadgeText({ text: `${rest.toString()}m` }); 59 | chrome.browserAction.setBadgeBackgroundColor({ color: 'green' }); 60 | rest -= 1; 61 | restTimeout = setTimeout(updateRest, oneMinute); 62 | } 63 | } 64 | 65 | async function updateCurrent() { 66 | if (current === 0) { 67 | rest = 5; 68 | updateRest(); 69 | chrome.notifications.create({ 70 | type: 'basic', 71 | iconUrl: 'assets/tomato.png', 72 | title: 'Tomato got, take a break~', 73 | message: (new Date()).toLocaleTimeString(), 74 | requireInteraction: true, // do not close until click 75 | // eventTime: Date.now() + 1000 * 10, 76 | }); 77 | } else { 78 | chrome.browserAction.setBadgeText({ text: `${current.toString()}m` }); 79 | chrome.browserAction.setBadgeBackgroundColor({ color: 'red' }); 80 | current -= 1; 81 | currentTimeout = setTimeout(updateCurrent, oneMinute); 82 | } 83 | } 84 | 85 | function startTimer() { 86 | current = 25; 87 | rest = 5; 88 | 89 | // when calling startTimer multiple times, previous timeout should be cleared 90 | clearTimeout(currentTimeout); 91 | clearTimeout(restTimeout); 92 | updateCurrent(); 93 | } 94 | 95 | chrome.notifications.onButtonClicked.addListener(async (notificationId, buttonIndex) => { 96 | const today = new Date(); 97 | const tomatoes = await store.Tomato.getAll(); 98 | const lastTomato = tomatoes[tomatoes.length - 1]; 99 | 100 | // start another tomato for current todo directly 101 | if (buttonIndex === 0) { 102 | const startTime = today.getTime(); 103 | const newTomato = { 104 | startAt: startTime, 105 | todoId: lastTomato.todoId, 106 | }; 107 | await store.Tomato.push(newTomato); 108 | await store.CurrentStartAt.put(startTime); 109 | chrome.browserAction.setBadgeText({ text: '25m' }); 110 | chrome.browserAction.setBadgeBackgroundColor({ color: 'red' }); 111 | startTimer(); 112 | 113 | // abandon current tomato 114 | } else if (buttonIndex === 1) { 115 | await store.Tomato.pop(); 116 | } 117 | }); 118 | 119 | 120 | // export function from background page 121 | // (load in other place by `chrome.extension.getBackgroundPage();`) 122 | window.startTimer = startTimer; 123 | -------------------------------------------------------------------------------- /src/styles/todo.scss: -------------------------------------------------------------------------------- 1 | $distance-to-edge: 15px; 2 | 3 | #todo-div { 4 | width: 450px; 5 | height: 100vh; 6 | overflow-y: scroll; 7 | scrollbar-width: none; 8 | &::-webkit-scrollbar { 9 | width: 0; 10 | } 11 | overflow-x: hidden; 12 | // position: fixed; 13 | margin-left: $distance-to-edge; 14 | padding-top: $distance-to-edge; 15 | // margin: $distance-to-edge; 16 | // transform: scale(1.1, 1.1); 17 | // transform-origin: left; 18 | // TODO: find another way if you want it bigger 19 | #todo-title { 20 | margin: 0; 21 | padding-bottom: 6px; 22 | padding-left: 27px; 23 | border-bottom: solid 1px #ddd; 24 | width: 297px; 25 | } 26 | 27 | #list { 28 | padding: 0; 29 | margin: 0; 30 | border: 0; 31 | li { 32 | // text-align: left; 33 | position: relative; 34 | font-size: 15px; 35 | list-style: none; 36 | width: 450px; 37 | margin: 0; 38 | border: 0; 39 | 40 | // blur ref: https://codepen.io/juanbrujo/pen/HIexB 41 | &.is-blurred { 42 | color: transparent; 43 | text-shadow: #aaa 0 0 3px; 44 | transition: 0.4s; 45 | } 46 | &.is-highlighted { 47 | font-weight: bold; 48 | } 49 | 50 | &:hover { 51 | background-color: rgba(238, 238, 238, 0.473); 52 | cursor: move; 53 | color: #000; 54 | text-shadow: none; 55 | } 56 | &.chosen { 57 | background-color: white; 58 | } 59 | &.ghost { 60 | // background-color: white; 61 | opacity: 0; 62 | } 63 | .checkbox { 64 | font-size: 13px; 65 | width: 16px; 66 | height: 16px; 67 | background-color: white; 68 | border-radius: 50%; 69 | vertical-align: middle; 70 | border: 1px solid #ddd; 71 | -webkit-appearance: none; 72 | outline: none; 73 | cursor: pointer; 74 | vertical-align: top; 75 | margin-top: 12px; 76 | &:hover { 77 | cursor: pointer; 78 | } 79 | &:checked { 80 | background-color: gray; 81 | } 82 | } 83 | .content-div { 84 | display: inline-block; 85 | vertical-align: top; 86 | width: 270px; 87 | height: auto; 88 | // word-wrap: break-word; 89 | // word-break: break-all; 90 | // overflow: hidden; 91 | text-align: left; 92 | // padding-left: 1px; 93 | outline: 0; 94 | border-bottom: solid 1px #ddd; 95 | // border-top: solid 1px #ddd; 96 | padding-top: 10px; 97 | padding-bottom: 10px; 98 | &:hover { 99 | cursor: text; 100 | } 101 | &.done { 102 | // text-decoration: line-through; 103 | color: #999; 104 | } 105 | } 106 | 107 | .todo-tomato-div { 108 | display: inline-block; 109 | width: 140px; 110 | .todo-tomato { 111 | display: inline-block; 112 | padding-top: 12px; 113 | width: 15px; 114 | } 115 | } 116 | .add-tomato-btn { 117 | // padding-top: 6px; 118 | // outline: 0; 119 | display: none; 120 | width: 20px; 121 | position: absolute; 122 | right: 150px; 123 | top: 8px; 124 | opacity: 0.8; 125 | cursor: pointer; 126 | } 127 | 128 | .rm-todo-btn { 129 | // padding-top: 6px; 130 | // outline: 0; 131 | display: none; 132 | width: 20px; 133 | position: absolute; 134 | right: 172px; 135 | top: 8px; 136 | opacity: 0.8; 137 | cursor: pointer; 138 | } 139 | } 140 | } 141 | 142 | #input-div { 143 | width: 270px; 144 | margin-top: 10px; 145 | margin-bottom: 50px; 146 | margin-left: 28px; 147 | border-bottom: solid 1px #ddd; 148 | position: relative; 149 | #input { 150 | display: inline-block; 151 | font-size: 15px; 152 | border: 0; 153 | width: 270px; 154 | padding-bottom: 10px; 155 | padding-top: 0; 156 | background: transparent; 157 | &:focus { 158 | outline: none; 159 | } 160 | } 161 | #add-todo { 162 | // padding-top: 6px; 163 | // outline: 0; 164 | display: none; 165 | width: 15px; 166 | position: absolute; 167 | right: 2px; 168 | top: 2px; 169 | opacity: 0.8; 170 | } 171 | } 172 | } 173 | 174 | .links-in-todo { 175 | color: #07c; 176 | text-decoration: none; 177 | } 178 | 179 | .links-in-todo:hover { 180 | text-decoration: underline; 181 | } -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import './styles/index.scss'; 2 | import './styles/todo.scss'; 3 | import './styles/switch.scss'; 4 | import './styles/statics.scss'; 5 | import './styles/modal.scss'; 6 | import './styles/listsMenu.scss'; 7 | import './util/generateClockAnimations'; 8 | import './util/showSidebar'; 9 | 10 | import minuteAnimation from './util/minuteAnimation'; 11 | import generateCalender from './util/generateCalender'; 12 | import store from './store'; 13 | import showTodoListAndTomatoes from './util/showTodoListAndTomatoes'; 14 | 15 | // decide if show tomato-pie or default page 16 | const isDefaultNewTab = store.DefaultNewTab.get(); 17 | 18 | if (!window.location.hash && isDefaultNewTab === false) { 19 | chrome.tabs.update({ url: 'chrome-search://local-ntp/local-ntp.html' }); 20 | } 21 | 22 | // show minuteAnimation and set extension badge if there is tomato running 23 | async function showMinuteAnimationAndExtensionBadge() { 24 | const currentStartAt = await store.CurrentStartAt.get(); 25 | if (currentStartAt && new Date().getTime() - currentStartAt < 1000 * 60 * 30) { 26 | minuteAnimation.show(currentStartAt); 27 | } else { 28 | await store.CurrentStartAt.remove(); 29 | chrome.browserAction.setBadgeText({ text: '' }); 30 | } 31 | } 32 | showMinuteAnimationAndExtensionBadge(); 33 | 34 | generateCalender(); 35 | 36 | // show todoList and tomatoes 37 | async function showTodosAndTomatoes() { 38 | await showTodoListAndTomatoes(); 39 | } 40 | showTodosAndTomatoes(); 41 | 42 | document.querySelector('#input-div').addEventListener('mouseover', () => { 43 | document.querySelector('#add-todo').style.display = 'inline-block'; 44 | }); 45 | 46 | document.querySelector('#input-div').addEventListener('mouseleave', () => { 47 | document.querySelector('#add-todo').style.display = 'none'; 48 | }); 49 | 50 | 51 | // add todo 52 | async function addTodoFromInput() { 53 | const content = document.querySelector('#input').value; 54 | if (!content) { 55 | alert('Please type in content before adding todo'); 56 | return; 57 | } 58 | await store.Todo.push({ 59 | createdAt: new Date().getTime(), 60 | content, 61 | tag: store.SelectedTag.get(), 62 | }); 63 | await showTodoListAndTomatoes(); 64 | 65 | document.getElementById('input').value = ''; 66 | } 67 | 68 | document.querySelector('#add-todo').addEventListener('click', async () => { 69 | await addTodoFromInput(); 70 | }); 71 | 72 | document.querySelector('#input').addEventListener('keyup', async (e) => { 73 | if (e.keyCode === 13) { 74 | await addTodoFromInput(); 75 | } 76 | }); 77 | 78 | // settings 79 | const calenderDiv = document.querySelector('.calendar'); 80 | const textareaDiv = document.querySelector('.textarea-div'); 81 | const calToggle = document.querySelector('#cb1'); 82 | const texToggle = document.querySelector('#cb2'); 83 | const defToggle = document.querySelector('#cb3'); 84 | const focusToggle = document.querySelector('#cb4'); 85 | 86 | function renderCalTexAccordingToStore() { 87 | const isShowCal = store.ShowStatics.get(); 88 | const isShowTex = store.ShowTextarea.get(); 89 | const isDefaultTab = store.DefaultNewTab.get(); 90 | const isFocusingMode = store.FocusingMode.get(); 91 | 92 | if (isShowCal) { 93 | calenderDiv.classList.remove('invisible'); 94 | calToggle.checked = true; 95 | } else { 96 | calenderDiv.classList.add('invisible'); 97 | } 98 | 99 | if (isShowTex) { 100 | textareaDiv.classList.remove('invisible'); 101 | texToggle.checked = true; 102 | } else { 103 | textareaDiv.classList.add('invisible'); 104 | } 105 | 106 | defToggle.checked = isDefaultTab; 107 | focusToggle.checked = isFocusingMode; 108 | } 109 | 110 | renderCalTexAccordingToStore(); 111 | 112 | calToggle.addEventListener('click', () => { 113 | store.ShowStatics.set(calToggle.checked); 114 | renderCalTexAccordingToStore(); 115 | }); 116 | 117 | texToggle.addEventListener('click', () => { 118 | store.ShowTextarea.set(texToggle.checked); 119 | renderCalTexAccordingToStore(); 120 | }); 121 | 122 | defToggle.addEventListener('click', () => { 123 | store.DefaultNewTab.set(defToggle.checked); 124 | renderCalTexAccordingToStore(); 125 | }); 126 | 127 | focusToggle.addEventListener('click', () => { 128 | store.FocusingMode.set(focusToggle.checked); 129 | renderCalTexAccordingToStore(); 130 | showTodoListAndTomatoes(); 131 | }); 132 | 133 | // show info 134 | const infoDiv = document.querySelector('.info-div'); 135 | 136 | infoDiv.addEventListener('click', (event) => { 137 | event.stopPropagation(); 138 | }); 139 | 140 | const textarea = document.querySelector('.textarea'); 141 | 142 | textarea.value = store.Textarea.get(); 143 | 144 | textarea.addEventListener('input', () => store.Textarea.set(textarea.value)); 145 | 146 | // check if current tomato is done 147 | let lastCurrentStartAt = store.CurrentStartAt.get(); 148 | setInterval(() => { 149 | const currentStartAt = store.CurrentStartAt.get(); 150 | if (lastCurrentStartAt && !currentStartAt) { 151 | window.location.reload(); 152 | } 153 | lastCurrentStartAt = currentStartAt; 154 | }, 2000); 155 | -------------------------------------------------------------------------------- /src/styles/index.scss: -------------------------------------------------------------------------------- 1 | // adjust time speed 2 | $rotate-minute: 3600s; 3 | $clock-size: scale(1.4); 4 | $red: #ff3737; 5 | $orange: #f5fc97c9; 6 | $green: #9de16f; 7 | $clock-base-color: #eee; 8 | $clock-hand-color: #555; 9 | $clock-hand-shadow-color: #555; 10 | $clock-center-color: #ddd; 11 | $click-indicator-color: #aaa; 12 | 13 | // do math on padding/border/margin is not interesting: http://zh.learnlayout.com/box-sizing.html 14 | * { 15 | box-sizing: border-box; 16 | } 17 | 18 | html, 19 | body { 20 | height: 100vh; 21 | overflow: hidden; 22 | font-family: system-ui; 23 | margin: 0; 24 | padding: 0; 25 | position: relative; 26 | } 27 | 28 | 29 | .tomato-around-clock { 30 | position: absolute; 31 | z-index: 6; 32 | margin: 113px 114px; 33 | display: block; 34 | 35 | &:hover { 36 | cursor: pointer; 37 | } 38 | } 39 | 40 | .invisible { 41 | display: none; 42 | } 43 | 44 | .displayNone { 45 | display: none; 46 | } 47 | 48 | .highlighted-tomato { 49 | opacity: 0.35; 50 | } 51 | 52 | .clock-wrapper { 53 | z-index: 1; 54 | position: absolute; 55 | top: 35%; 56 | bottom: 50%; 57 | right: 0; 58 | left: 155px; // 355 - (250 / 2) 59 | width: 260px; 60 | height: 260px; 61 | margin: auto; 62 | padding: 5px; 63 | border-radius: 50%; 64 | transform: $clock-size; 65 | transform-origin: center; 66 | transition: 0.3s; 67 | 68 | &.clock-pushed { 69 | left: 310px; // 505 - (250 / 2) 70 | } 71 | 72 | .clock-base { 73 | width: 250px; 74 | height: 250px; 75 | background-color: $clock-base-color; 76 | border-radius: 50%; 77 | 78 | .click-indicator { 79 | position: absolute; 80 | z-index: 1; 81 | top: 15px; 82 | left: 15px; 83 | width: 230px; 84 | height: 230px; 85 | 86 | div { 87 | position: absolute; 88 | width: 2px; 89 | height: 7px; 90 | margin: 113px 114px; 91 | background-color: $click-indicator-color; 92 | } 93 | 94 | @for $i from 1 through 12 { 95 | div:nth-child(#{$i}) { 96 | transform: rotate(30deg * $i) translateY(-110px); 97 | } 98 | } 99 | } 100 | 101 | .clock-hour { 102 | position: absolute; 103 | z-index: 2; 104 | top: 80px; 105 | left: 128px; 106 | width: 4px; 107 | height: 65px; 108 | background-color: $clock-hand-color; 109 | border-radius: 2px; 110 | box-shadow: 0 0 2px $clock-hand-shadow-color; 111 | transform-origin: 2px 50px; 112 | transition: 0.3s; 113 | animation: rotate-hour 43200s linear infinite; 114 | } 115 | 116 | .clock-minute { 117 | position: absolute; 118 | z-index: 3; 119 | top: 60px; 120 | left: 128px; 121 | width: 4px; 122 | height: 85px; 123 | background-color: $clock-hand-color; 124 | border-radius: 2px; 125 | box-shadow: 0 0 2px $clock-hand-shadow-color; 126 | transform-origin: 2px 70px; 127 | transition: 0.3s; 128 | animation: rotate-minute $rotate-minute linear infinite; 129 | } 130 | 131 | .clock-minute:after { 132 | content: ''; 133 | display: block; 134 | position: absolute; 135 | left: -4px; 136 | bottom: 9px; 137 | width: 8px; 138 | height: 8px; 139 | background-color: $clock-hand-color; 140 | border: solid 2px $clock-hand-color; 141 | border-radius: 50%; 142 | box-shadow: 0 0 3px $clock-hand-shadow-color; 143 | } 144 | 145 | .clock-center { 146 | position: absolute; 147 | z-index: 1; 148 | width: 150px; 149 | height: 150px; 150 | top: 55px; 151 | left: 55px; 152 | border-radius: 50%; 153 | } 154 | 155 | .clock-center:after { 156 | content: ""; 157 | display: block; 158 | width: 20px; 159 | height: 20px; 160 | margin: 65px; 161 | background-color: $clock-center-color; 162 | border-radius: 50%; 163 | } 164 | 165 | .work-half { 166 | position: absolute; 167 | z-index: 1; 168 | border: 7px solid $red; 169 | border-radius: 50%; 170 | height: 250; 171 | width: 250; 172 | border-top-color: transparent; 173 | border-left-color: transparent; 174 | } 175 | 176 | .invisible-half { 177 | position: absolute; 178 | z-index: 4; 179 | border: 7px solid $clock-base-color; 180 | border-radius: 50%; 181 | height: 250; 182 | width: 250; 183 | border-top-color: transparent; 184 | border-left-color: transparent; 185 | } 186 | 187 | .rest-half { 188 | position: absolute; 189 | z-index: 2; 190 | border: 7px solid $green; 191 | border-radius: 50%; 192 | height: 250; 193 | width: 250; 194 | border-top-color: transparent; 195 | border-left-color: transparent; 196 | } 197 | 198 | .done-half { 199 | position: absolute; 200 | z-index: 3; 201 | border: 7px solid $orange; 202 | border-radius: 50%; 203 | height: 250; 204 | width: 250; 205 | border-top-color: transparent; 206 | border-left-color: transparent; 207 | transition: 0.3s; 208 | animation: rotate-done-minute $rotate-minute linear infinite; 209 | } 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Tomato Pie 4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | 30 |
31 |
32 |
33 |
34 | 35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | 58 |
59 |
60 |
61 | 62 | 148 | 149 |
150 | 151 |
152 |

Todo List

153 | 154 |
155 | 156 | 157 |
158 |
159 | 160 | 161 | 162 |
tool tip of calendar
163 | 164 | 171 | 172 | 173 | 179 | 180 | 181 | 182 | -------------------------------------------------------------------------------- /src/util/showSidebar.js: -------------------------------------------------------------------------------- 1 | import store from '../store'; 2 | import showTodoListAndTomatoes from './showTodoListAndTomatoes'; 3 | import colors from '../colors'; 4 | 5 | // show sidebar or not 6 | function addSidebar() { 7 | document.querySelector('.sidebar').classList.add('sidebar-pushed'); 8 | document.querySelector('body').classList.add('body-pushed'); 9 | document.querySelector('.clock-wrapper').classList.add('clock-pushed'); 10 | document.querySelector('.calendar').classList.add('calendar-pushed'); 11 | document.querySelector('.open-sidebar-btn').classList.add('sidebar-opened'); 12 | } 13 | 14 | function removeSidebar() { 15 | document.querySelector('.sidebar').classList.remove('sidebar-pushed'); 16 | document.querySelector('body').classList.remove('body-pushed'); 17 | document.querySelector('.clock-wrapper').classList.remove('clock-pushed'); 18 | document.querySelector('.calendar').classList.remove('calendar-pushed'); 19 | document.querySelector('.open-sidebar-btn').classList.remove('sidebar-opened'); 20 | } 21 | 22 | const initIsShowSidebar = store.ShowSidebar.get(); 23 | 24 | if (initIsShowSidebar) { 25 | addSidebar(); 26 | } 27 | 28 | document.querySelector('.open-sidebar-btn').addEventListener('click', () => { 29 | const isShowSidebar = store.ShowSidebar.get(); 30 | 31 | if (isShowSidebar) { 32 | store.ShowSidebar.set(false); 33 | removeSidebar(); 34 | } else { 35 | store.ShowSidebar.set(true); 36 | addSidebar(); 37 | } 38 | }); 39 | 40 | // sidebar functionality 41 | function renderSidebar() { 42 | const tags = store.Tag.getAll(); 43 | let selectedTag = store.SelectedTag.get(); 44 | console.log(tags, selectedTag, 1); 45 | if (!tags.includes(selectedTag)) { 46 | selectedTag = ''; 47 | store.SelectedTag.set(selectedTag); 48 | // showTodoListAndTomatoes(); 49 | document.querySelector('#todo-title').textContent = 'Todo List'; 50 | } 51 | if (selectedTag) document.querySelector('#todo-title').textContent = selectedTag; 52 | const allTodoA = document.querySelector('.all-todo-a'); 53 | if (!selectedTag) { 54 | allTodoA.classList.add('selected'); 55 | } else { 56 | allTodoA.classList.remove('selected'); 57 | } 58 | 59 | allTodoA.addEventListener('click', () => { 60 | store.SelectedTag.set(''); 61 | renderSidebar(); 62 | showTodoListAndTomatoes(); 63 | // document.querySelector('#todo-title').textContent = 'Todo List'; 64 | }); 65 | 66 | const tagColors = store.TagColor.getAll(); 67 | const tagsHTMLs = tags.map((tag, i) => ` 68 | 69 | 72 | ${tag} 73 | 74 | 75 | 76 | `); 77 | 78 | document.querySelector('.lists-list').innerHTML = tagsHTMLs.join(''); 79 | 80 | // event listener 81 | tags.forEach((tag, i) => { 82 | const tagEl = document.querySelector(`.tag-${i}`); 83 | const tagElColor = document.querySelector(`.tag-color-${i}`); 84 | const rmTagEl = document.querySelector(`.tag-${i} .remove-tag-btn`); 85 | const editTagEl = document.querySelector(`.tag-${i} .edit-tag-btn`); 86 | 87 | tagEl.addEventListener('click', () => { 88 | store.SelectedTag.set(tag); 89 | renderSidebar(); 90 | showTodoListAndTomatoes(); 91 | }); 92 | tagEl.addEventListener('mouseover', () => { 93 | rmTagEl.classList.remove('invisible'); 94 | editTagEl.classList.remove('invisible'); 95 | }); 96 | tagEl.addEventListener('mouseleave', () => { 97 | rmTagEl.classList.add('invisible'); 98 | editTagEl.classList.add('invisible'); 99 | }); 100 | tagElColor.addEventListener('input', (event) => { 101 | store.TagColor.update(i, event.target.value); 102 | renderSidebar(); 103 | showTodoListAndTomatoes(); 104 | }); 105 | rmTagEl.addEventListener('click', () => { 106 | const isConfimed = window.confirm('Do you want to remove this category? \nNote: todos with this category will not be removed'); 107 | if (isConfimed) { 108 | store.Tag.remove(i); 109 | renderSidebar(); 110 | showTodoListAndTomatoes(); 111 | } 112 | }); 113 | editTagEl.addEventListener('click', (e) => { 114 | e.stopPropagation(); // important: prevent the el click event been fired! 115 | const newName = prompt("What's the new category name?", tag).trim(); 116 | if (!newName) { 117 | alert('Please specify a category name'); 118 | return; 119 | } 120 | if (tags.includes(newName)) { 121 | alert('Same category already exist, please change the category name'); 122 | return; 123 | } 124 | store.SelectedTag.set(newName); 125 | store.Tag.update(i, newName); 126 | 127 | const allTodos = store.Todo.getAll(); 128 | const newAllTodos = allTodos.map((todo) => { 129 | if (todo.tag === tag) { 130 | return { 131 | createdAt: todo.createdAt, 132 | content: todo.content, 133 | tag: newName, 134 | }; 135 | } 136 | return todo; 137 | }); 138 | store.Todo.setAll(newAllTodos); 139 | 140 | renderSidebar(); 141 | showTodoListAndTomatoes(); 142 | }); 143 | }); 144 | } 145 | 146 | renderSidebar(); 147 | 148 | const listInput = document.querySelector('.add-tag-input'); 149 | 150 | listInput.addEventListener('keydown', (e) => { 151 | const tagValue = listInput.value.trim(); 152 | if (e.keyCode === 13) { 153 | if (!tagValue) { 154 | alert('Please specify a category name'); 155 | return; 156 | } 157 | try { 158 | store.Tag.push(listInput.value.trim()); 159 | listInput.classList.add('invisible'); 160 | store.SelectedTag.set(listInput.value); 161 | renderSidebar(); 162 | showTodoListAndTomatoes(); 163 | } catch (error) { 164 | alert('Same category already exist, please change the category name'); 165 | console.log(error); 166 | } 167 | } 168 | }); 169 | 170 | listInput.addEventListener('blur', () => { 171 | listInput.classList.add('invisible'); 172 | }); 173 | 174 | document.querySelector('.add-tag-div').addEventListener('click', () => { 175 | listInput.classList.remove('invisible'); 176 | listInput.value = 'New List'; 177 | listInput.focus(); 178 | listInput.select(); 179 | }); 180 | -------------------------------------------------------------------------------- /src/util/generateCalender.js: -------------------------------------------------------------------------------- 1 | import { SVGGraph } from 'calendar-graph'; 2 | import store from '../store'; 3 | import colors from '../colors'; 4 | 5 | /** 6 | * group object array by certain key 7 | * @ref https://stackoverflow.com/a/34890276/4674834 8 | * @param {Array} xs group of objects 9 | * @param {String} key object key 10 | * @example 11 | * // {3: ["one", "two"], 5: ["three"]} 12 | * groupBy(['one', 'two', 'three'], 'length'); 13 | */ 14 | function groupBy(xs, key) { 15 | return xs.reduce((rv, x) => { 16 | (rv[x[key]] = rv[x[key]] || []).push(x); 17 | return rv; 18 | }, {}); 19 | } 20 | 21 | function generateCalender() { 22 | const tomatoes = store.Tomato.getAll(); 23 | 24 | const groupedTomatoes = tomatoes 25 | .map((tomato) => { 26 | const date = new Date(tomato.startAt); 27 | const dateStr = `${date.getFullYear()}-${date.getMonth() + 1 <= 9 ? `0${date.getMonth() + 1}` : date.getMonth() + 1}-${date.getDate() <= 9 ? `0${date.getDate()}` : date.getDate()}`; 28 | return { 29 | date: dateStr, 30 | tomatoes: [tomato], 31 | }; 32 | }) 33 | .reduce((pre, cur) => { 34 | if (pre.length <= 0) return [cur]; 35 | if (pre[pre.length - 1].date === cur.date) { 36 | pre[pre.length - 1].tomatoes.push(cur.tomatoes[0]); 37 | return pre; 38 | } 39 | return pre.concat(cur); 40 | }, []) 41 | .map((tomatoGroup) => { 42 | tomatoGroup.count = tomatoGroup.tomatoes.length; 43 | return { 44 | date: tomatoGroup.date, 45 | count: tomatoGroup.count, 46 | tomatoes: tomatoGroup.tomatoes, 47 | }; 48 | }); 49 | 50 | const calender = new SVGGraph('.calendar', groupedTomatoes, { 51 | // startDate: new Date(new Date().getTime() - 1000 * 60 * 60 * 24 * 365), 52 | // endDate: new Date(), 53 | // styleOptions: { 54 | // textColor: '#959494', 55 | // fontSize: '12px', 56 | // fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif', 57 | // }, 58 | colorFun: (v) => { 59 | if (v.count > 0 && v.count <= 4) return '#c6e48b'; 60 | if (v.count > 4 && v.count <= 8) return '#7bc96f'; 61 | if (v.count > 8 && v.count <= 12) return '#239a3b'; 62 | if (v.count > 12) return '#196127'; 63 | return '#eee'; 64 | }, 65 | onClick: (v) => { 66 | console.log(v); 67 | }, 68 | }); 69 | 70 | function tooltipInit() { 71 | const tip = document.getElementById('tooltip'); 72 | const elems = document.getElementsByClassName('cg-day'); 73 | const modal = document.getElementById('myModal'); 74 | const modalContent = document.getElementById('myModalContent'); 75 | 76 | // show tip 77 | const mouseOver = (e) => { 78 | e = e || window.event; 79 | const elem = e.target || e.srcElement; 80 | const rect = elem.getBoundingClientRect(); 81 | const count = elem.getAttribute('data-count'); 82 | const date = elem.getAttribute('data-date'); 83 | tip.style.display = 'block'; 84 | tip.textContent = `${count} tomatoes on ${date}`; 85 | const w = tip.getBoundingClientRect().width; 86 | tip.style.left = `${rect.left - (w / 2) + 6}px`; 87 | tip.style.top = `${rect.top - 30}px`; 88 | }; 89 | 90 | // hide tip 91 | const mouseOut = () => { 92 | tip.style.display = 'none'; 93 | }; 94 | 95 | // show statics 96 | const mouseClick = (e) => { 97 | e = e || window.event; 98 | const elem = e.target || e.srcElement; 99 | const date = elem.getAttribute('data-date'); 100 | const tomatoGroup = groupedTomatoes.filter(group => group.date === date)[0]; 101 | 102 | // only show modal when there are tomatoes in that day? 103 | if (tomatoGroup) { 104 | const todosObj = groupBy(tomatoGroup.tomatoes, 'todoId'); 105 | // [{"id":"1552873989450","todo":{"createdAt":1552873989450,"content":"\n \n seeking new idea"},"tomatos":[]}] 106 | const todosArr = Object.keys(todosObj).map(todoId => ({ 107 | id: todoId, 108 | todo: store.Todo.get(todoId) || store.Done.get(todoId), 109 | tomatos: todosObj[todoId], 110 | isDone: !!store.Done.get(todoId), 111 | })); 112 | const todoHTMLs = todosArr.map((todo) => { 113 | const todoTomatoHTMLs = todo.tomatos 114 | .map(() => ''); 115 | 116 | const tagsList = store.Tag.getAll(); 117 | 118 | let todoColor; 119 | let todoContent; 120 | if (todo.todo) { 121 | const tagIndex = tagsList.indexOf(todo.todo.tag); 122 | todoColor = store.TagColor.getAll()[tagIndex] || colors[tagIndex]; 123 | todoContent = todo.todo.content; 124 | } 125 | 126 | return ` 127 | 136 | `; 137 | }); 138 | modal.style.display = 'block'; 139 | modalContent.innerHTML = ` 140 |

${date}

141 |
142 | ${todoHTMLs.join('')} 143 |
144 |
145 | 152 | `; 153 | } else { 154 | modal.style.display = 'block'; 155 | modalContent.innerHTML = '

No tomato today

'; 156 | } 157 | }; 158 | 159 | // hide modal 160 | window.onclick = (event) => { 161 | if (event.target === modal) { 162 | modal.style.display = 'none'; 163 | } 164 | }; 165 | 166 | Array.from(elems).forEach((elem) => { 167 | elem.addEventListener('mouseover', mouseOver); 168 | elem.addEventListener('mouseout', mouseOut); 169 | elem.addEventListener('click', mouseClick); 170 | }); 171 | } 172 | 173 | tooltipInit(); 174 | } 175 | 176 | export default generateCalender; 177 | -------------------------------------------------------------------------------- /src/scripts/create-demo-tomatoes.js: -------------------------------------------------------------------------------- 1 | const now = new Date().getTime(); 2 | const arr = [ 3 | { 4 | startAt: now - 1000 * 60 * 60 * 4, 5 | 6 | }, 7 | { 8 | startAt: now - 1000 * 60 * 60 * 2.5, 9 | 10 | }, 11 | { 12 | startAt: now - 1000 * 60 * 60 * 3, 13 | 14 | }, 15 | { 16 | startAt: now - 1000 * 60 * 60 * 3.5, 17 | 18 | }, 19 | { 20 | startAt: now - 1000 * 60 * 60 * 0.5, 21 | 22 | }, 23 | { 24 | startAt: now - 1000 * 60 * 60 * 1, 25 | 26 | }, 27 | { 28 | startAt: now - 1000 * 60 * 60 * 1.5, 29 | 30 | }, 31 | { 32 | startAt: now - 1000 * 60 * 60 * 24 * 18, 33 | 34 | }, 35 | { 36 | startAt: now - 1000 * 60 * 60 * 24 * 2, 37 | 38 | }, 39 | { 40 | startAt: now - 1000 * 60 * 60 * 24 * 2, 41 | 42 | }, 43 | { 44 | startAt: now - 1000 * 60 * 60 * 24 * 2, 45 | 46 | }, 47 | { 48 | startAt: now - 1000 * 60 * 60 * 24 * 2, 49 | 50 | }, 51 | { 52 | startAt: now - 1000 * 60 * 60 * 24 * 2, 53 | 54 | }, 55 | { 56 | startAt: now - 1000 * 60 * 60 * 24 * 2, 57 | 58 | }, 59 | { 60 | startAt: now - 1000 * 60 * 60 * 24 * 2, 61 | 62 | }, 63 | { 64 | startAt: now - 1000 * 60 * 60 * 24 * 2, 65 | 66 | }, 67 | { 68 | startAt: now - 1000 * 60 * 60 * 24 * 2, 69 | 70 | }, 71 | { 72 | startAt: now - 1000 * 60 * 60 * 24 * 2, 73 | 74 | }, 75 | { 76 | startAt: now - 1000 * 60 * 60 * 24 * 2, 77 | 78 | }, 79 | { 80 | startAt: now - 1000 * 60 * 60 * 24 * 2, 81 | 82 | }, 83 | { 84 | startAt: now - 1000 * 60 * 60 * 24 * 3, 85 | 86 | }, 87 | { 88 | startAt: now - 1000 * 60 * 60 * 24 * 6, 89 | 90 | }, 91 | { 92 | startAt: now - 1000 * 60 * 60 * 24 * 7, 93 | 94 | }, 95 | { 96 | startAt: now - 1000 * 60 * 60 * 24 * 28, 97 | 98 | }, 99 | { 100 | startAt: now - 1000 * 60 * 60 * 24 * 28, 101 | 102 | }, 103 | { 104 | startAt: now - 1000 * 60 * 60 * 24 * 28, 105 | 106 | }, 107 | { 108 | startAt: now - 1000 * 60 * 60 * 24 * 28, 109 | 110 | }, 111 | { 112 | startAt: now - 1000 * 60 * 60 * 24 * 28, 113 | 114 | }, 115 | { 116 | startAt: now - 1000 * 60 * 60 * 24 * 28, 117 | 118 | }, 119 | { 120 | startAt: now - 1000 * 60 * 60 * 24 * 28, 121 | 122 | }, 123 | { 124 | startAt: now - 1000 * 60 * 60 * 24 * 28, 125 | 126 | }, 127 | { 128 | startAt: now - 1000 * 60 * 60 * 24 * 28, 129 | 130 | }, 131 | { 132 | startAt: now - 1000 * 60 * 60 * 24 * 28, 133 | 134 | }, 135 | { 136 | startAt: now - 1000 * 60 * 60 * 24 * 28, 137 | 138 | }, 139 | { 140 | startAt: now - 1000 * 60 * 60 * 24 * 28, 141 | 142 | }, 143 | { 144 | startAt: now - 1000 * 60 * 60 * 24 * 28, 145 | 146 | }, 147 | { 148 | startAt: now - 1000 * 60 * 60 * 24 * 28, 149 | 150 | }, 151 | { 152 | startAt: now - 1000 * 60 * 60 * 24 * 28, 153 | 154 | }, 155 | { 156 | startAt: now - 1000 * 60 * 60 * 24 * 28, 157 | 158 | }, 159 | 160 | { 161 | startAt: now - 1000 * 60 * 60 * 24 * 30, 162 | 163 | }, 164 | { 165 | startAt: now - 1000 * 60 * 60 * 24 * 89, 166 | content: 'aac', 167 | }, 168 | { 169 | startAt: now - 1000 * 60 * 60 * 24 * 18, 170 | 171 | }, 172 | { 173 | startAt: now - 1000 * 60 * 60 * 24 * 2, 174 | 175 | }, 176 | { 177 | startAt: now - 1000 * 60 * 60 * 24 * 77, 178 | 179 | }, 180 | { 181 | startAt: now - 1000 * 60 * 60 * 24 * 88, 182 | 183 | }, 184 | { 185 | startAt: now - 1000 * 60 * 60 * 24 * 100, 186 | 187 | }, 188 | { 189 | startAt: now - 1000 * 60 * 60 * 24 * 130, 190 | 191 | }, 192 | { 193 | startAt: now - 1000 * 60 * 60 * 24 * 12, 194 | 195 | }, 196 | { 197 | startAt: now - 1000 * 60 * 60 * 24 * 2, 198 | 199 | }, 200 | { 201 | startAt: now - 1000 * 60 * 60 * 24 * 2, 202 | 203 | }, 204 | 205 | { 206 | startAt: now - 1000 * 60 * 60 * 24 * 3, 207 | 208 | }, 209 | { 210 | startAt: now - 1000 * 60 * 60 * 24 * 6, 211 | 212 | }, 213 | { 214 | startAt: now - 1000 * 60 * 60 * 24 * 7, 215 | 216 | }, 217 | { 218 | startAt: now - 1000 * 60 * 60 * 24 * 28, 219 | 220 | }, 221 | { 222 | startAt: now - 1000 * 60 * 60 * 24 * 30, 223 | 224 | }, 225 | { 226 | startAt: now - 1000 * 60 * 60 * 24 * 89, 227 | }, 228 | { 229 | startAt: now - 1000 * 60 * 60 * 24 * 9, 230 | }, 231 | { 232 | startAt: now - 1000 * 60 * 60 * 24 * 89, 233 | }, 234 | { 235 | startAt: now - 1000 * 60 * 60 * 24 * 8, 236 | }, 237 | { 238 | startAt: now - 1000 * 60 * 60 * 24 * 89, 239 | }, 240 | { 241 | startAt: now - 1000 * 60 * 60 * 24 * 89, 242 | }, 243 | { 244 | startAt: now - 1000 * 60 * 60 * 24 * 48, 245 | }, 246 | { 247 | startAt: now - 1000 * 60 * 60 * 24 * 48, 248 | }, 249 | { 250 | startAt: now - 1000 * 60 * 60 * 24 * 48, 251 | }, 252 | { 253 | startAt: now - 1000 * 60 * 60 * 24 * 48, 254 | }, { 255 | startAt: now - 1000 * 60 * 60 * 24 * 48, 256 | }, 257 | { 258 | startAt: now - 1000 * 60 * 60 * 24 * 18, 259 | }, { 260 | startAt: now - 1000 * 60 * 60 * 24 * 28, 261 | }, 262 | { 263 | startAt: now - 1000 * 60 * 60 * 24 * 38, 264 | }, { 265 | startAt: now - 1000 * 60 * 60 * 24 * 47, 266 | }, 267 | { 268 | startAt: now - 1000 * 60 * 60 * 24 * 48, 269 | }, { 270 | startAt: now - 1000 * 60 * 60 * 24 * 42, 271 | }, 272 | { 273 | startAt: now - 1000 * 60 * 60 * 24 * 40, 274 | }, 275 | { 276 | startAt: now - 1000 * 60 * 60 * 24 * 43, 277 | }, 278 | { 279 | startAt: now - 1000 * 60 * 60 * 24 * 45, 280 | }, { 281 | startAt: now - 1000 * 60 * 60 * 24 * 28, 282 | }, 283 | { 284 | startAt: now - 1000 * 60 * 60 * 24 * 38, 285 | }, { 286 | startAt: now - 1000 * 60 * 60 * 24 * 47, 287 | }, 288 | { 289 | startAt: now - 1000 * 60 * 60 * 24 * 48, 290 | }, { 291 | startAt: now - 1000 * 60 * 60 * 24 * 42, 292 | }, 293 | { 294 | startAt: now - 1000 * 60 * 60 * 24 * 40, 295 | }, 296 | { 297 | startAt: now - 1000 * 60 * 60 * 24 * 43, 298 | }, 299 | { 300 | startAt: now - 1000 * 60 * 60 * 24 * 45, 301 | }, { 302 | startAt: now - 1000 * 60 * 60 * 24 * 28, 303 | }, 304 | { 305 | startAt: now - 1000 * 60 * 60 * 24 * 38, 306 | }, { 307 | startAt: now - 1000 * 60 * 60 * 24 * 47, 308 | }, 309 | { 310 | startAt: now - 1000 * 60 * 60 * 24 * 48, 311 | }, { 312 | startAt: now - 1000 * 60 * 60 * 24 * 42, 313 | }, 314 | { 315 | startAt: now - 1000 * 60 * 60 * 24 * 70, 316 | }, 317 | { 318 | startAt: now - 1000 * 60 * 60 * 24 * 4, 319 | }, 320 | { 321 | startAt: now - 1000 * 60 * 60 * 24 * 15, 322 | }, { 323 | startAt: now - 1000 * 60 * 60 * 24 * 18, 324 | }, 325 | { 326 | startAt: now - 1000 * 60 * 60 * 24 * 38, 327 | }, { 328 | startAt: now - 1000 * 60 * 60 * 24 * 47, 329 | }, 330 | { 331 | startAt: now - 1000 * 60 * 60 * 24 * 8, 332 | }, { 333 | startAt: now - 1000 * 60 * 60 * 24 * 42, 334 | }, 335 | { 336 | startAt: now - 1000 * 60 * 60 * 24 * 10, 337 | }, 338 | { 339 | startAt: now - 1000 * 60 * 60 * 24 * 23, 340 | }, 341 | { 342 | startAt: now - 1000 * 60 * 60 * 24 * 65, 343 | }, 344 | { 345 | startAt: now - 1000 * 60 * 60 * 24 * 98, 346 | 347 | }, 348 | { 349 | startAt: now - 1000 * 60 * 60 * 24 * 99, 350 | 351 | }, 352 | { 353 | startAt: now - 1000 * 60 * 60 * 24 * 89, 354 | 355 | }, 356 | { 357 | startAt: now - 1000 * 60 * 60 * 24 * 79, 358 | 359 | }, 360 | { 361 | startAt: now - 1000 * 60 * 60 * 24 * 69, 362 | 363 | }, 364 | { 365 | startAt: now - 1000 * 60 * 60 * 24 * 59, 366 | 367 | }, 368 | { 369 | startAt: now - 1000 * 60 * 60 * 24 * 90, 370 | 371 | }, 372 | { 373 | startAt: now - 1000 * 60 * 60 * 24 * 109, 374 | 375 | }, 376 | 377 | { 378 | startAt: now - 1000 * 60 * 60 * 24 * 69, 379 | 380 | }, 381 | { 382 | startAt: now - 1000 * 60 * 60 * 24 * 59, 383 | 384 | }, 385 | { 386 | startAt: now - 1000 * 60 * 60 * 24 * 90, 387 | 388 | }, 389 | { 390 | startAt: now - 1000 * 60 * 60 * 24 * 109, 391 | 392 | }, 393 | { 394 | startAt: now - 1000 * 60 * 60 * 24 * 69, 395 | 396 | }, 397 | { 398 | startAt: now - 1000 * 60 * 60 * 24 * 49, 399 | 400 | }, 401 | { 402 | startAt: now - 1000 * 60 * 60 * 24 * 90, 403 | 404 | }, 405 | { 406 | startAt: now - 1000 * 60 * 60 * 24 * 129, 407 | 408 | }, 409 | { 410 | startAt: now - 1000 * 60 * 60 * 24 * 79, 411 | 412 | }, 413 | { 414 | startAt: now - 1000 * 60 * 60 * 24 * 79, 415 | 416 | }, 417 | { 418 | startAt: now - 1000 * 60 * 60 * 24 * 70, 419 | 420 | }, 421 | { 422 | startAt: now - 1000 * 60 * 60 * 24 * 139, 423 | 424 | }, 425 | ]; 426 | 427 | arr.sort((a, b) => a.startAt - b.startAt); 428 | 429 | console.log(arr); 430 | localStorage.setItem('tomatoList', JSON.stringify(arr)); 431 | -------------------------------------------------------------------------------- /src/util/showTodoListAndTomatoes.js: -------------------------------------------------------------------------------- 1 | import Sortable from 'sortablejs'; 2 | import store from '../store'; 3 | import minuteAnimation from './minuteAnimation'; 4 | import colors from '../colors'; 5 | 6 | // communicate with background page: https://stackoverflow.com/a/11967860/4674834 7 | const backgroundPage = chrome.extension.getBackgroundPage(); 8 | 9 | async function showTodoListAndTomatoes() { 10 | const tomatoesLast12H = await store.Tomato.get12h(); 11 | const todoList = await store.Todo.getAll(); 12 | 13 | const currentStartAt = await store.CurrentStartAt.get(); 14 | 15 | // calculate tomato angle and display them 16 | const tomatoContainer = document.getElementById('tomato-container'); 17 | const tomatoHTMLs = tomatoesLast12H 18 | .map((tomato) => { 19 | const now = new Date(); 20 | if (tomato.startAt === currentStartAt 21 | && (now.getTime() - currentStartAt < 25 * 60 * 1000)) { 22 | return ``; 23 | } 24 | return ``; 25 | }); 26 | tomatoContainer.innerHTML = tomatoHTMLs.join(''); 27 | 28 | tomatoesLast12H.forEach((tomato) => { 29 | const { startAt } = tomato; 30 | const date = new Date(startAt); 31 | const angle = date.getHours() / 12 * 360 + date.getMinutes() / 60 * 30 + 30 / 4; 32 | const tomatoEl = document.querySelector(`#tomato-${tomato.startAt}`); 33 | tomatoEl.style.transform = `rotate(${angle}deg) translateY(-140px)`; 34 | 35 | tomatoEl.addEventListener('click', async () => { 36 | const isConfimed = window.confirm('Do you want to abandon this tomato?'); 37 | if (isConfimed) { 38 | store.Tomato.remove(tomato.startAt); 39 | 40 | // if the abandoned tomato is current one; clear currentStartAt and badge on action 41 | if (currentStartAt === tomato.startAt) { 42 | store.CurrentStartAt.remove(); 43 | chrome.browserAction.setBadgeText({ text: '' }); 44 | minuteAnimation.hide(); 45 | } 46 | 47 | await showTodoListAndTomatoes(); 48 | } 49 | }); 50 | 51 | tomatoEl.addEventListener('mouseover', () => { 52 | const now = new Date(); 53 | if (tomato.startAt === currentStartAt 54 | && (now.getTime() - currentStartAt < 25 * 60 * 1000)) { 55 | tomatoEl.src = './assets/abandonTomatoOnGoing.svg'; 56 | } else { 57 | tomatoEl.src = './assets/abandonTomato.svg'; 58 | } 59 | }); 60 | 61 | tomatoEl.addEventListener('mouseout', () => { 62 | const now = new Date(); 63 | if (tomato.startAt === currentStartAt 64 | && (now.getTime() - currentStartAt < 25 * 60 * 1000)) { 65 | tomatoEl.src = './assets/onGoingTomato.svg'; 66 | } else { 67 | tomatoEl.src = './assets/tomato.svg'; 68 | } 69 | }); 70 | }); 71 | 72 | // show todo list 73 | const ul = document.getElementById('list'); 74 | 75 | // TODO: hide todo without tag 76 | const todoHTMLs = todoList 77 | .map((todo) => { 78 | let isDoingThisTomato = false; 79 | 80 | const todoTomatoHTMLs = tomatoesLast12H 81 | .filter(tomato => tomato.todoId === todo.createdAt) 82 | .map((tomato) => { 83 | const now = new Date(); 84 | if (tomato.startAt === currentStartAt 85 | && (now.getTime() - currentStartAt < 25 * 60 * 1000)) { 86 | isDoingThisTomato = true; 87 | return ''; 88 | } 89 | return ''; 90 | }); 91 | 92 | let liClass = ''; 93 | const now = new Date(); 94 | if (now.getTime() - currentStartAt < 25 * 60 * 1000) { 95 | if (isDoingThisTomato) { 96 | liClass = 'is-highlighted'; 97 | } else { 98 | liClass = store.FocusingMode.get() ? 'is-blurred' : ''; 99 | } 100 | } 101 | 102 | let isVisible = true; 103 | const selectedTag = store.SelectedTag.get(); 104 | if (selectedTag && (selectedTag !== todo.tag)) isVisible = false; 105 | 106 | const tagsList = store.Tag.getAll(); 107 | const tagIndex = tagsList.indexOf(todo.tag); 108 | const todoColor = store.TagColor.getAll()[tagIndex] || colors[tagIndex]; 109 | 110 | return ` 111 |
  • 112 | 113 |
    ${todo.content.replace(/(https?:\/\/[^\s]+)/g, "$1")} 
    114 |
    115 | ${todoTomatoHTMLs.join('')} 116 |
    117 | 118 | 119 |
  • 120 | `; 121 | }); 122 | ul.innerHTML = todoHTMLs.join(''); 123 | 124 | // sortable li; 125 | Sortable.create(ul, { 126 | animation: 50, 127 | // scroll: true, 128 | chosenClass: 'chosen', 129 | ghostClass: 'ghost', 130 | onEnd: async (e) => { 131 | const { oldIndex, newIndex } = e; 132 | await store.Todo.move(oldIndex, newIndex); 133 | await showTodoListAndTomatoes(); 134 | }, 135 | }); 136 | 137 | // eventListeners 138 | // TODO: refactoring: separate event handle with view? ref: https://stackoverflow.com/a/27373951/4674834 139 | todoList.forEach((todo) => { 140 | const li = document.querySelector(`#todo-${todo.createdAt}`); 141 | 142 | // hover on li show add-tomato-btn and rm-todo-btn 143 | const addTomatoBtn = document.querySelector(`#todo-${todo.createdAt} .add-tomato-btn`); 144 | const rmTomatoBtn = document.querySelector(`#todo-${todo.createdAt} .rm-todo-btn`); 145 | li.addEventListener('mouseover', () => { 146 | rmTomatoBtn.style.display = 'inline-block'; 147 | addTomatoBtn.style.display = 'inline-block'; 148 | const liTomatoEls = document.querySelectorAll(`#todo-${todo.createdAt} .todo-tomato`); 149 | liTomatoEls.forEach(tomatoEl => tomatoEl.classList.add('highlighted-tomato')); 150 | tomatoesLast12H 151 | .filter(tomato => tomato.todoId === todo.createdAt) 152 | .forEach((tomato) => { 153 | const clockTomatoEl = document.querySelector(`#tomato-${tomato.startAt}`); 154 | clockTomatoEl.classList.add('highlighted-tomato'); 155 | }); 156 | }); 157 | li.addEventListener('mouseleave', () => { 158 | addTomatoBtn.style.display = 'none'; 159 | rmTomatoBtn.style.display = 'none'; 160 | const liTomatoEls = document.querySelectorAll(`#todo-${todo.createdAt} .todo-tomato`); 161 | liTomatoEls.forEach(tomato => tomato.classList.remove('highlighted-tomato')); 162 | tomatoesLast12H 163 | .filter(tomato => tomato.todoId === todo.createdAt) 164 | .forEach((tomato) => { 165 | const clockTomatoEl = document.querySelector(`#tomato-${tomato.startAt}`); 166 | clockTomatoEl.classList.remove('highlighted-tomato'); 167 | }); 168 | }); 169 | 170 | // event listener for add-tomato btn 171 | addTomatoBtn.addEventListener('click', async () => { 172 | const lastCurrentStartAt = await store.CurrentStartAt.get(); 173 | const startTime = new Date().getTime(); 174 | 175 | async function startNewTomato() { 176 | await store.Tomato.push({ 177 | startAt: startTime, 178 | todoId: todo.createdAt, 179 | }); 180 | 181 | await store.CurrentStartAt.put(startTime); 182 | await showTodoListAndTomatoes(); 183 | 184 | chrome.browserAction.setBadgeText({ text: '25m' }); 185 | chrome.browserAction.setBadgeBackgroundColor({ color: 'red' }); 186 | minuteAnimation.show(startTime); 187 | backgroundPage.startTimer(); 188 | } 189 | 190 | if (lastCurrentStartAt && lastCurrentStartAt > startTime - 1000 * 60 * 25) { 191 | // ask user if he want to abandon last tomato 192 | const isConfimed = window.confirm('You are doing a tomato now, if you start a new one, the current one will be abandoned.'); 193 | if (isConfimed) { 194 | await store.Tomato.pop(); 195 | await startNewTomato(); 196 | } 197 | } else { 198 | await startNewTomato(); 199 | } 200 | }); 201 | 202 | // rm todo 203 | // Question: rm corresponding tomato? 204 | rmTomatoBtn.addEventListener('click', async () => { 205 | await store.Todo.remove(todo.createdAt); 206 | await showTodoListAndTomatoes(); 207 | }); 208 | 209 | // event listener for checkbox 210 | // mv todo from todoList to doneList 211 | const checkbox = document.querySelector(`#todo-${todo.createdAt} > input`); 212 | checkbox.addEventListener('click', async (e) => { 213 | console.log('checked'); 214 | e.preventDefault(); 215 | await store.Todo.remove(todo.createdAt); 216 | await store.Done.push(todo); 217 | await showTodoListAndTomatoes(); 218 | }); 219 | 220 | // content-div editable updating todo content 221 | const contentDiv = document.querySelector(`#todo-${todo.createdAt} .content-div`); 222 | contentDiv.addEventListener('input', async (e) => { 223 | // hit enter 224 | if (e.inputType === 'insertParagraph' || (e.inputType === 'insertText' && e.data === null)) { 225 | // rerender the list to make sure user loose mouse focus 226 | await showTodoListAndTomatoes(); 227 | } else { 228 | const newContent = contentDiv.textContent; 229 | await store.Todo.update(todo.createdAt, newContent); 230 | } 231 | }); 232 | }); 233 | } 234 | 235 | export default showTodoListAndTomatoes; 236 | -------------------------------------------------------------------------------- /src/store.js: -------------------------------------------------------------------------------- 1 | /** 2 | * store data structure 3 | * 4 | * currentStartAt: 132413241234, 5 | * 6 | * isDefaultNewTab: boolean, 7 | * 8 | * isFocusingMode: boolean, 9 | * 10 | * isShowStatics: boolean, 11 | * 12 | * isShowTextarea: boolean, 13 | * 14 | * isShowSidebar: boolean, 15 | * 16 | * textareData: string, 17 | * 18 | * tomatoList: [{ 19 | * startAt: 1533653542468, // also used as id 20 | * todoId: 1533653542468, 21 | * }], 22 | * 23 | * todoList: [{ 24 | * createdAt: 1533653542468, // also used as id 25 | * content: "master css position", 26 | * tag: "Tomato Pie" 27 | * }] 28 | * 29 | * doneList: [{ 30 | * createdAt: 1533653542468, // also used as id 31 | * content: "master css position", 32 | * tag: "", 33 | * }] 34 | * 35 | * tagList: [ 36 | * "Todo List", 37 | * "Tomato Pie" 38 | * ] 39 | * 40 | * tagColorList: [ 41 | * ] 42 | * 43 | * selectedTag: "Todo List" 44 | */ 45 | 46 | // null; true; false; 47 | const DefaultNewTab = { 48 | // TODO: write function to remove code duplication 49 | get: () => { 50 | const item = localStorage.getItem('isDefaultNewTab'); 51 | if (item === 'true') return true; 52 | if (item === 'false') return false; 53 | if (item === null) return true; // default true 54 | throw new Error('error getting boolean'); 55 | }, 56 | set: (boolean) => { 57 | if (boolean === true) { 58 | localStorage.setItem('isDefaultNewTab', 'true'); 59 | } else if (boolean === false) { 60 | localStorage.setItem('isDefaultNewTab', 'false'); 61 | } else { 62 | throw new Error('error setting boolean, only allow true or false'); 63 | } 64 | }, 65 | }; 66 | 67 | // null; true; false; 68 | const FocusingMode = { 69 | get: () => { 70 | const item = localStorage.getItem('isFocusingMode'); 71 | if (item === 'true') return true; 72 | if (item === 'false') return false; 73 | if (item === null) return true; // default true 74 | throw new Error('error getting boolean'); 75 | }, 76 | set: (boolean) => { 77 | if (boolean === true) { 78 | localStorage.setItem('isFocusingMode', 'true'); 79 | } else if (boolean === false) { 80 | localStorage.setItem('isFocusingMode', 'false'); 81 | } else { 82 | throw new Error('error setting boolean, only allow true or false'); 83 | } 84 | }, 85 | }; 86 | 87 | const ShowSidebar = { 88 | get: () => { 89 | const item = localStorage.getItem('isShowSidebar'); 90 | if (item === 'true') return true; 91 | if (item === 'false') return false; 92 | if (item === null) return null; 93 | throw new Error('error getting boolean'); 94 | }, 95 | set: (boolean) => { 96 | if (boolean === true) { 97 | localStorage.setItem('isShowSidebar', 'true'); 98 | } else if (boolean === false) { 99 | localStorage.setItem('isShowSidebar', 'false'); 100 | } else { 101 | throw new Error('error setting boolean, only allow true or false'); 102 | } 103 | }, 104 | }; 105 | 106 | const ShowStatics = { 107 | get: () => { 108 | const item = localStorage.getItem('isShowStatics'); 109 | if (item === 'true') return true; 110 | if (item === 'false') return false; 111 | if (item === null) return null; 112 | throw new Error('error getting boolean'); 113 | }, 114 | set: (boolean) => { 115 | if (boolean === true) { 116 | localStorage.setItem('isShowStatics', 'true'); 117 | } else if (boolean === false) { 118 | localStorage.setItem('isShowStatics', 'false'); 119 | } else { 120 | throw new Error('error setting boolean, only allow true or false'); 121 | } 122 | }, 123 | }; 124 | 125 | const ShowTextarea = { 126 | get: () => { 127 | const item = localStorage.getItem('isShowTextarea'); 128 | if (item === 'true') return true; 129 | if (item === 'false') return false; 130 | if (item === null) return null; 131 | throw new Error('error getting boolean'); 132 | }, 133 | set: (boolean) => { 134 | if (boolean === true) { 135 | localStorage.setItem('isShowTextarea', 'true'); 136 | } else if (boolean === false) { 137 | localStorage.setItem('isShowTextarea', 'false'); 138 | } else { 139 | throw new Error('error setting boolean, only allow true or false'); 140 | } 141 | }, 142 | }; 143 | 144 | const Textarea = { 145 | get: () => localStorage.getItem('textareaData'), 146 | set: content => localStorage.setItem('textareaData', content), 147 | }; 148 | 149 | const CurrentStartAt = { 150 | get: () => Number(localStorage.getItem('currentStartAt')), 151 | put: currentStartAt => localStorage.setItem('currentStartAt', currentStartAt), 152 | remove: () => localStorage.removeItem('currentStartAt'), 153 | }; 154 | 155 | const Tomato = { 156 | getAll: () => { 157 | const all = JSON.parse(localStorage.getItem('tomatoList')); 158 | if (!all) return []; 159 | return all; 160 | }, 161 | // get tomatoes of last 12 hours 162 | get12h: () => { 163 | const all = JSON.parse(localStorage.getItem('tomatoList')); 164 | if (!all) return []; 165 | return all.filter(tomato => tomato.startAt > new Date().getTime() - 1000 * 60 * 60 * 12); 166 | }, 167 | push: (tomato) => { 168 | const cur = JSON.parse(localStorage.getItem('tomatoList')); 169 | let next; 170 | if (!cur) { 171 | next = [tomato]; 172 | } else { 173 | next = cur.concat(tomato); 174 | } 175 | localStorage.setItem('tomatoList', JSON.stringify(next)); 176 | return next; 177 | }, 178 | pop: () => { 179 | const cur = JSON.parse(localStorage.getItem('tomatoList')); 180 | const next = cur.slice(0, -1); 181 | localStorage.setItem('tomatoList', JSON.stringify(next)); 182 | return next; 183 | }, 184 | remove: (startAt) => { 185 | const cur = JSON.parse(localStorage.getItem('tomatoList')); 186 | const next = cur.filter(tomato => tomato.startAt !== startAt); 187 | localStorage.setItem('tomatoList', JSON.stringify(next)); 188 | return next; 189 | }, 190 | }; 191 | 192 | const Todo = { 193 | get: (id) => { 194 | const all = JSON.parse(localStorage.getItem('todoList')); 195 | if (!all) return null; 196 | const theOne = all.filter(todo => Number(todo.createdAt) === Number(id))[0]; 197 | if (!theOne) return null; 198 | return theOne; 199 | }, 200 | getAll: () => { 201 | const all = JSON.parse(localStorage.getItem('todoList')); 202 | if (!all) return []; 203 | return all; 204 | }, 205 | setAll: (todos) => { 206 | localStorage.setItem('todoList', JSON.stringify(todos)); 207 | return todos; 208 | }, 209 | remove: (createdAt) => { 210 | const cur = JSON.parse(localStorage.getItem('todoList')); 211 | const next = cur.filter(todo => todo.createdAt !== createdAt); 212 | localStorage.setItem('todoList', JSON.stringify(next)); 213 | return next; 214 | }, 215 | push: (todo) => { 216 | const cur = JSON.parse(localStorage.getItem('todoList')); 217 | let next; 218 | if (!cur) { 219 | next = [todo]; 220 | } else { 221 | next = cur.concat(todo); 222 | } 223 | localStorage.setItem('todoList', JSON.stringify(next)); 224 | return next; 225 | }, 226 | update: (createdAt, content) => { 227 | const cur = JSON.parse(localStorage.getItem('todoList')); 228 | const next = cur.map((todo) => { 229 | const newTodo = todo; 230 | if (newTodo.createdAt === createdAt) newTodo.content = content; 231 | return newTodo; 232 | }); 233 | localStorage.setItem('todoList', JSON.stringify(next)); 234 | return next; 235 | }, 236 | move: (oldIndex, newIndex) => { 237 | const cur = JSON.parse(localStorage.getItem('todoList')); 238 | const [oldTodo] = cur.splice(oldIndex, 1); 239 | cur.splice(newIndex, 0, oldTodo); 240 | const next = cur; 241 | localStorage.setItem('todoList', JSON.stringify(next)); 242 | return next; 243 | }, 244 | }; 245 | 246 | const Done = { 247 | get: (id) => { 248 | const all = JSON.parse(localStorage.getItem('doneList')); 249 | if (!all) return null; 250 | const theOne = all.filter(todo => Number(todo.createdAt) === Number(id))[0]; 251 | if (!theOne) return null; 252 | return theOne; 253 | }, 254 | getAll: () => JSON.parse(localStorage.getItem('doneList')), 255 | remove: (createdAt) => { 256 | const cur = JSON.parse(localStorage.getItem('doneList')); 257 | const next = cur.filter(todo => todo.createdAt !== createdAt); 258 | localStorage.setItem('doneList', JSON.stringify(next)); 259 | return next; 260 | }, 261 | push: (todo) => { 262 | const cur = JSON.parse(localStorage.getItem('doneList')); 263 | let next; 264 | if (!cur) { 265 | next = [todo]; 266 | } else { 267 | next = cur.concat(todo); 268 | } 269 | localStorage.setItem('doneList', JSON.stringify(next)); 270 | return next; 271 | }, 272 | }; 273 | 274 | const Tag = { 275 | getAll: () => { 276 | const all = JSON.parse(localStorage.getItem('tagList')); 277 | if (!all) return []; 278 | return all; 279 | }, 280 | remove: (i) => { 281 | const cur = JSON.parse(localStorage.getItem('tagList')); 282 | cur.splice(i, 1); 283 | localStorage.setItem('tagList', JSON.stringify(cur)); 284 | return cur; 285 | }, 286 | push: (name) => { 287 | const cur = JSON.parse(localStorage.getItem('tagList')); 288 | let next; 289 | if (!cur) { 290 | next = [name]; 291 | } else { 292 | // check duplication 293 | if (cur.includes(name)) { 294 | throw new Error('same tag name not allowed'); 295 | } 296 | next = cur.concat(name); 297 | } 298 | localStorage.setItem('tagList', JSON.stringify(next)); 299 | return next; 300 | }, 301 | update: (i, newName) => { 302 | const cur = JSON.parse(localStorage.getItem('tagList')); 303 | cur[i] = newName; 304 | localStorage.setItem('tagList', JSON.stringify(cur)); 305 | return cur; 306 | }, 307 | move: (oldIndex, newIndex) => { 308 | const cur = JSON.parse(localStorage.getItem('tagList')); 309 | const [oldTodo] = cur.splice(oldIndex, 1); 310 | cur.splice(newIndex, 0, oldTodo); 311 | const next = cur; 312 | localStorage.setItem('tagList', JSON.stringify(next)); 313 | return next; 314 | }, 315 | }; 316 | 317 | const SelectedTag = { 318 | get: () => { 319 | const cur = localStorage.getItem('selectedTag'); 320 | return cur; 321 | }, 322 | set: (tag) => { 323 | localStorage.setItem('selectedTag', tag); 324 | return tag; 325 | }, 326 | }; 327 | 328 | const TagColor = { 329 | getAll: () => { 330 | const all = JSON.parse(localStorage.getItem('tagColorList')); 331 | if (!all) return []; 332 | return all; 333 | }, 334 | update: (i, newName) => { 335 | let cur = JSON.parse(localStorage.getItem('tagColorList')); 336 | if (!cur) cur = []; 337 | cur[i] = newName; 338 | localStorage.setItem('tagColorList', JSON.stringify(cur)); 339 | return cur; 340 | }, 341 | }; 342 | 343 | // TODO: todo tags?? 344 | export default { 345 | DefaultNewTab, 346 | FocusingMode, 347 | ShowSidebar, 348 | ShowStatics, 349 | ShowTextarea, 350 | Textarea, 351 | CurrentStartAt, 352 | Tomato, 353 | Todo, 354 | Done, 355 | Tag, 356 | SelectedTag, 357 | TagColor, 358 | }; 359 | --------------------------------------------------------------------------------