├── .gitattributes ├── .gitignore ├── app ├── audio │ └── ding.mp3 ├── fonts │ ├── fontello.eot │ ├── fontello.ttf │ ├── fontello.woff │ └── fontello.svg ├── img │ ├── background.jpg │ ├── notification-96x96-work.png │ ├── notification-96x96-break.png │ └── notification-96x96-longbreak.png ├── icons │ ├── favicon-32x32.png │ ├── favicon-96x96.png │ ├── favicon-break.ico │ ├── favicon-work.ico │ ├── mstile-144x144.png │ ├── mstile-150x150.png │ ├── mstile-310x150.png │ ├── mstile-310x310.png │ ├── mstile-70x70.png │ ├── apple-touch-icon.png │ ├── favicon-160x160.png │ ├── favicon-192x192.png │ ├── favicon-16x16-break.png │ ├── favicon-16x16-work.png │ ├── favicon-longbreak.ico │ ├── apple-touch-icon-114x114.png │ ├── apple-touch-icon-120x120.png │ ├── apple-touch-icon-144x144.png │ ├── apple-touch-icon-152x152.png │ ├── apple-touch-icon-180x180.png │ ├── apple-touch-icon-57x57.png │ ├── apple-touch-icon-60x60.png │ ├── apple-touch-icon-72x72.png │ ├── apple-touch-icon-76x76.png │ ├── favicon-16x16-longbreak.png │ ├── apple-touch-icon-precomposed.png │ └── browserconfig.xml ├── js │ ├── namespace.js │ ├── app.js │ ├── sidebar.js │ ├── views │ │ ├── timer.js │ │ ├── sidebar.js │ │ ├── controls.js │ │ ├── progress.js │ │ └── settings.js │ ├── services │ │ ├── storage.js │ │ ├── title.js │ │ ├── audio.js │ │ ├── taskbarFlash.js │ │ ├── browserDetection.js │ │ ├── notification.js │ │ └── favicon.js │ ├── config.js │ ├── hotkeys.js │ ├── settings.js │ └── timer.js ├── css │ ├── reset.css │ ├── shared.css │ ├── media.css │ ├── animations.css │ ├── main.css │ ├── fontello.css │ └── sidebar.css └── index.html ├── resources ├── tomato │ ├── tomato.png │ ├── tomato-work.png │ └── tomato.svg └── background │ ├── imgbg7.jpg │ ├── slide_04.jpg │ ├── ambientpink.jpg │ ├── ambientturquoise.jpg │ └── gaussian_blur_desktop_1440x900_hd-wallpaper-910291.png ├── .editorconfig ├── LICENSE ├── package.json ├── README.md ├── gulpfile.js └── .eslintrc /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | app/components 4 | -------------------------------------------------------------------------------- /app/audio/ding.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hurtak/Tomatotim/HEAD/app/audio/ding.mp3 -------------------------------------------------------------------------------- /app/fonts/fontello.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hurtak/Tomatotim/HEAD/app/fonts/fontello.eot -------------------------------------------------------------------------------- /app/fonts/fontello.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hurtak/Tomatotim/HEAD/app/fonts/fontello.ttf -------------------------------------------------------------------------------- /app/fonts/fontello.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hurtak/Tomatotim/HEAD/app/fonts/fontello.woff -------------------------------------------------------------------------------- /app/img/background.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hurtak/Tomatotim/HEAD/app/img/background.jpg -------------------------------------------------------------------------------- /app/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hurtak/Tomatotim/HEAD/app/icons/favicon-32x32.png -------------------------------------------------------------------------------- /app/icons/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hurtak/Tomatotim/HEAD/app/icons/favicon-96x96.png -------------------------------------------------------------------------------- /app/icons/favicon-break.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hurtak/Tomatotim/HEAD/app/icons/favicon-break.ico -------------------------------------------------------------------------------- /app/icons/favicon-work.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hurtak/Tomatotim/HEAD/app/icons/favicon-work.ico -------------------------------------------------------------------------------- /app/icons/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hurtak/Tomatotim/HEAD/app/icons/mstile-144x144.png -------------------------------------------------------------------------------- /app/icons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hurtak/Tomatotim/HEAD/app/icons/mstile-150x150.png -------------------------------------------------------------------------------- /app/icons/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hurtak/Tomatotim/HEAD/app/icons/mstile-310x150.png -------------------------------------------------------------------------------- /app/icons/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hurtak/Tomatotim/HEAD/app/icons/mstile-310x310.png -------------------------------------------------------------------------------- /app/icons/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hurtak/Tomatotim/HEAD/app/icons/mstile-70x70.png -------------------------------------------------------------------------------- /resources/tomato/tomato.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hurtak/Tomatotim/HEAD/resources/tomato/tomato.png -------------------------------------------------------------------------------- /app/icons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hurtak/Tomatotim/HEAD/app/icons/apple-touch-icon.png -------------------------------------------------------------------------------- /app/icons/favicon-160x160.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hurtak/Tomatotim/HEAD/app/icons/favicon-160x160.png -------------------------------------------------------------------------------- /app/icons/favicon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hurtak/Tomatotim/HEAD/app/icons/favicon-192x192.png -------------------------------------------------------------------------------- /app/icons/favicon-16x16-break.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hurtak/Tomatotim/HEAD/app/icons/favicon-16x16-break.png -------------------------------------------------------------------------------- /app/icons/favicon-16x16-work.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hurtak/Tomatotim/HEAD/app/icons/favicon-16x16-work.png -------------------------------------------------------------------------------- /app/icons/favicon-longbreak.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hurtak/Tomatotim/HEAD/app/icons/favicon-longbreak.ico -------------------------------------------------------------------------------- /resources/background/imgbg7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hurtak/Tomatotim/HEAD/resources/background/imgbg7.jpg -------------------------------------------------------------------------------- /resources/background/slide_04.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hurtak/Tomatotim/HEAD/resources/background/slide_04.jpg -------------------------------------------------------------------------------- /resources/tomato/tomato-work.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hurtak/Tomatotim/HEAD/resources/tomato/tomato-work.png -------------------------------------------------------------------------------- /app/img/notification-96x96-work.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hurtak/Tomatotim/HEAD/app/img/notification-96x96-work.png -------------------------------------------------------------------------------- /app/icons/apple-touch-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hurtak/Tomatotim/HEAD/app/icons/apple-touch-icon-114x114.png -------------------------------------------------------------------------------- /app/icons/apple-touch-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hurtak/Tomatotim/HEAD/app/icons/apple-touch-icon-120x120.png -------------------------------------------------------------------------------- /app/icons/apple-touch-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hurtak/Tomatotim/HEAD/app/icons/apple-touch-icon-144x144.png -------------------------------------------------------------------------------- /app/icons/apple-touch-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hurtak/Tomatotim/HEAD/app/icons/apple-touch-icon-152x152.png -------------------------------------------------------------------------------- /app/icons/apple-touch-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hurtak/Tomatotim/HEAD/app/icons/apple-touch-icon-180x180.png -------------------------------------------------------------------------------- /app/icons/apple-touch-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hurtak/Tomatotim/HEAD/app/icons/apple-touch-icon-57x57.png -------------------------------------------------------------------------------- /app/icons/apple-touch-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hurtak/Tomatotim/HEAD/app/icons/apple-touch-icon-60x60.png -------------------------------------------------------------------------------- /app/icons/apple-touch-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hurtak/Tomatotim/HEAD/app/icons/apple-touch-icon-72x72.png -------------------------------------------------------------------------------- /app/icons/apple-touch-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hurtak/Tomatotim/HEAD/app/icons/apple-touch-icon-76x76.png -------------------------------------------------------------------------------- /app/icons/favicon-16x16-longbreak.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hurtak/Tomatotim/HEAD/app/icons/favicon-16x16-longbreak.png -------------------------------------------------------------------------------- /app/img/notification-96x96-break.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hurtak/Tomatotim/HEAD/app/img/notification-96x96-break.png -------------------------------------------------------------------------------- /resources/background/ambientpink.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hurtak/Tomatotim/HEAD/resources/background/ambientpink.jpg -------------------------------------------------------------------------------- /app/img/notification-96x96-longbreak.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hurtak/Tomatotim/HEAD/app/img/notification-96x96-longbreak.png -------------------------------------------------------------------------------- /app/icons/apple-touch-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hurtak/Tomatotim/HEAD/app/icons/apple-touch-icon-precomposed.png -------------------------------------------------------------------------------- /resources/background/ambientturquoise.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hurtak/Tomatotim/HEAD/resources/background/ambientturquoise.jpg -------------------------------------------------------------------------------- /app/js/namespace.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | // TT stands for TomatoTim 5 | window.TT = { 6 | Services: {}, 7 | Views: {} 8 | }; 9 | })(); 10 | -------------------------------------------------------------------------------- /resources/background/gaussian_blur_desktop_1440x900_hd-wallpaper-910291.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hurtak/Tomatotim/HEAD/resources/background/gaussian_blur_desktop_1440x900_hd-wallpaper-910291.png -------------------------------------------------------------------------------- /app/js/app.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | TT.Services.Favicon.init(); 5 | TT.Services.Audio.init(); 6 | 7 | TT.Settings.init(); 8 | TT.Sidebar.init(); 9 | TT.Timer.init(); 10 | TT.Hotkeys.init(); 11 | })(); 12 | -------------------------------------------------------------------------------- /app/css/reset.css: -------------------------------------------------------------------------------- 1 | html { 2 | box-sizing: border-box; 3 | } 4 | 5 | *, 6 | *:before, 7 | *:after { 8 | box-sizing: inherit; 9 | } 10 | 11 | html, 12 | body { 13 | width: 100%; 14 | height: 100%; 15 | margin: 0; 16 | } 17 | 18 | button, 19 | input { 20 | outline: 0; 21 | } 22 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [{package.json,*.yml}] 11 | indent_style = space 12 | indent_size = 2 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /app/js/sidebar.js: -------------------------------------------------------------------------------- 1 | TT.Sidebar = (function () { 2 | 'use strict'; 3 | 4 | function init() { 5 | TT.Views.Sidebar.getSidebarButton().addEventListener('click', TT.Views.Sidebar.toogleSidebar); 6 | TT.Views.Sidebar.getSidebarOverlay().addEventListener('click', TT.Views.Sidebar.closeSidebar); 7 | } 8 | 9 | return { 10 | init: init 11 | }; 12 | })(); 13 | -------------------------------------------------------------------------------- /app/js/views/timer.js: -------------------------------------------------------------------------------- 1 | TT.Views.Timer = (function () { 2 | 'use strict'; 3 | 4 | var timerDiv = document.getElementById('clock'); 5 | 6 | function getTime() { 7 | return timerDiv.innerHTML.trim(); 8 | } 9 | 10 | function setTime(time) { 11 | timerDiv.innerHTML = time; 12 | } 13 | 14 | return { 15 | getTime: getTime, 16 | setTime: setTime 17 | }; 18 | })(); 19 | -------------------------------------------------------------------------------- /app/icons/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | #424242 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/js/services/storage.js: -------------------------------------------------------------------------------- 1 | TT.Services.Storage = (function () { 2 | 'use strict'; 3 | 4 | function get(storageName) { 5 | return JSON.parse(localStorage.getItem(storageName)); 6 | } 7 | 8 | function set(storageName, value) { 9 | localStorage.setItem(storageName, JSON.stringify(value)); 10 | } 11 | 12 | function clear() { 13 | localStorage.clear(); 14 | } 15 | 16 | return { 17 | set: set, 18 | get: get, 19 | clear: clear 20 | }; 21 | })(); 22 | -------------------------------------------------------------------------------- /app/js/services/title.js: -------------------------------------------------------------------------------- 1 | TT.Services.Title = (function () { 2 | 'use strict'; 3 | 4 | function setTitle(title, paused) { 5 | var titlePrefix = ''; 6 | if (!paused) { 7 | // black square character 8 | titlePrefix = '\u25A0 '; 9 | } 10 | 11 | document.title = titlePrefix + title + ' – ' + TT.Config.get('appName'); 12 | } 13 | 14 | function resetTitle() { 15 | document.title = TT.Config.get('appName'); 16 | } 17 | 18 | return { 19 | setTitle: setTitle, 20 | resetTitle: resetTitle 21 | }; 22 | })(); 23 | -------------------------------------------------------------------------------- /app/css/shared.css: -------------------------------------------------------------------------------- 1 | .no-select { 2 | -webkit-touch-callout: none; 3 | -webkit-user-select: none; 4 | -khtml-user-select: none; 5 | -moz-user-select: none; 6 | -ms-user-select: none; 7 | user-select: none; 8 | } 9 | 10 | .justify { 11 | text-align: justify; 12 | } 13 | 14 | /* colors */ 15 | .color-work { 16 | color: #e94a4a; 17 | } 18 | 19 | .color-break { 20 | color: #65e645; 21 | } 22 | 23 | .color-longbreak { 24 | color: #4e64f4; 25 | } 26 | 27 | .color-unfinished { 28 | color: #202020; 29 | } 30 | 31 | .color-finished { 32 | color: #ffffff; 33 | } 34 | -------------------------------------------------------------------------------- /app/js/services/audio.js: -------------------------------------------------------------------------------- 1 | TT.Services.Audio = (function () { 2 | 'use strict'; 3 | 4 | var audio; 5 | 6 | function init() { 7 | audio = document.createElement('audio'); 8 | // @source: http://soundbible.com/1619-Music-Box.html 9 | audio.src = 'audio/ding.mp3'; 10 | } 11 | 12 | function play() { 13 | audio.play(); 14 | } 15 | 16 | function setVolume(volume) { 17 | if (!(volume <= 1 && volume >= 0)) { 18 | volume = 1; 19 | } 20 | 21 | audio.volume = volume; 22 | } 23 | 24 | return { 25 | init: init, 26 | play: play, 27 | setVolume: setVolume 28 | }; 29 | })(); 30 | -------------------------------------------------------------------------------- /app/js/services/taskbarFlash.js: -------------------------------------------------------------------------------- 1 | TT.Services.TaskbarFlash = (function () { 2 | 'use strict'; 3 | 4 | // TODO: fix, doesent work for android 5 | function isAvaliable() { 6 | return typeof window.external.msIsSiteMode !== 'undefined' && 7 | // is webpage pinned to taskbar 8 | window.external.msIsSiteMode() && 9 | typeof window.external.msSiteModeActivate !== 'undefined'; 10 | } 11 | 12 | function flash() { 13 | if (isAvaliable()) { 14 | window.external.msSiteModeActivate(); 15 | } 16 | } 17 | 18 | return { 19 | isAvaliable: isAvaliable, 20 | flash: flash 21 | }; 22 | })(); 23 | -------------------------------------------------------------------------------- /app/js/services/browserDetection.js: -------------------------------------------------------------------------------- 1 | TT.Services.BrowserDetection = (function () { 2 | 'use strict'; 3 | 4 | // @source: browser detection from http://stackoverflow.com/a/9851769/2955574 5 | 6 | /* eslint-disable */ 7 | var isOpera = !!window.opera || navigator.userAgent.indexOf(' OPR/') >= 0; 8 | var isFirefox = typeof InstallTrigger !== 'undefined'; 9 | var isSafari = Object.prototype.toString.call(window.HTMLElement).indexOf('Constructor') > 0; 10 | var isChrome = !!window.chrome && !isOpera; 11 | var isIE = /*@cc_on!@*/false || !!document.documentMode; 12 | /* eslint-enable */ 13 | 14 | return { 15 | isOpera: isOpera, 16 | isFirefox: isFirefox, 17 | isSafari: isSafari, 18 | isChrome: isChrome, 19 | isIE: isIE 20 | }; 21 | })(); 22 | -------------------------------------------------------------------------------- /app/css/media.css: -------------------------------------------------------------------------------- 1 | @media only screen and (max-width: 620px) { 2 | .description { 3 | height: 40px; 4 | font-size: 40px; 5 | } 6 | 7 | .clock { 8 | height: 140px; 9 | font-size: 120px; 10 | } 11 | 12 | .progress { 13 | height: 52px; 14 | } 15 | 16 | .progress i { 17 | font-size: 45px; 18 | } 19 | 20 | .controls { 21 | margin-top: 25px; 22 | } 23 | 24 | .controls button { 25 | display: block; 26 | width: 65%; 27 | margin: 10px auto; 28 | padding: 10px; 29 | border: 2px solid #fff; 30 | font-size: 20px; 31 | } 32 | 33 | .controls button:first-child { 34 | padding: 20px; 35 | } 36 | 37 | .sidebar { 38 | right: -100%; 39 | width: 100%; 40 | } 41 | 42 | body[data-sidebar-open] .sidebar { 43 | transform: translateX(-100%); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/js/config.js: -------------------------------------------------------------------------------- 1 | TT.Config = (function () { 2 | 'use strict'; 3 | 4 | var config = {}; 5 | 6 | config.debug = false; 7 | if (window.location.search.indexOf('debug') > -1) { 8 | // '?debug' at the end of URL activates debug mode 9 | config.debug = true; 10 | } 11 | 12 | config.appName = 'Tomatotim'; 13 | 14 | config.audio = false; 15 | config.notifications = false; 16 | 17 | // seconds 18 | config.workInterval = 25 * 60; 19 | config.breakInterval = 5 * 60; 20 | config.longbreakInterval = 20 * 60; 21 | 22 | config.repeat = 4; 23 | 24 | if (config.debug) { 25 | config.appName = 'DEBUG'; 26 | 27 | config.workInterval = 7; 28 | config.breakInterval = 5; 29 | config.longbreakInterval = 6; 30 | } 31 | 32 | var get = function (name) { 33 | return config[name]; 34 | }; 35 | 36 | var set = function (name, value) { 37 | config[name] = value; 38 | }; 39 | 40 | return { 41 | get: get, 42 | set: set 43 | }; 44 | })(); 45 | -------------------------------------------------------------------------------- /app/js/hotkeys.js: -------------------------------------------------------------------------------- 1 | TT.Hotkeys = (function () { 2 | 'use strict'; 3 | 4 | var keys = { 5 | space: 32, 6 | enter: 13, 7 | esc: 27, 8 | tab: 9, 9 | r: 82, 10 | s: 83, 11 | h: 72 12 | }; 13 | 14 | function init() { 15 | document.addEventListener('keydown', keyDown); 16 | } 17 | 18 | function keyDown(e) { 19 | switch (e.keyCode) { 20 | case keys.space: 21 | e.preventDefault(); 22 | TT.Timer.startTimer(); 23 | break; 24 | case keys.esc: 25 | TT.Views.Sidebar.closeSidebar(); 26 | break; 27 | case keys.r: 28 | TT.Timer.resetTimer(); 29 | break; 30 | case keys.s: 31 | TT.Timer.skipInterval(); 32 | break; 33 | case keys.h: 34 | TT.Views.Sidebar.toogleSidebar(); 35 | break; 36 | case keys.tab: 37 | if (!TT.Views.Sidebar.isSidebarOpen()) { 38 | e.preventDefault(); 39 | } 40 | break; 41 | case keys.enter: 42 | e.preventDefault(); 43 | break; 44 | default: 45 | break; 46 | } 47 | } 48 | 49 | return { 50 | init: init 51 | }; 52 | })(); 53 | -------------------------------------------------------------------------------- /app/js/services/notification.js: -------------------------------------------------------------------------------- 1 | TT.Services.Notification = (function () { 2 | 'use strict'; 3 | 4 | // seconds 5 | var notificationTimeout = 5; 6 | 7 | function isAvaliable() { 8 | return typeof window.Notification === 'function'; 9 | } 10 | 11 | function requestPermission() { 12 | if (!isAvaliable()) { 13 | return false; 14 | } 15 | 16 | if (Notification.permission !== 'granted') { 17 | Notification.requestPermission(); 18 | return false; 19 | } 20 | 21 | return true; 22 | } 23 | 24 | function newNotification(message, iconType) { 25 | if (!requestPermission()) { 26 | return; 27 | } 28 | 29 | var notification = new Notification(TT.Config.get('appName'), { 30 | icon: 'img/notification-96x96-' + iconType + '.png', 31 | body: message 32 | }); 33 | 34 | notification.onshow = function () { 35 | setTimeout(function () { 36 | notification.close(); 37 | }, notificationTimeout * 1000); 38 | }; 39 | } 40 | 41 | return { 42 | isAvaliable: isAvaliable, 43 | requestPermission: requestPermission, 44 | newNotification: newNotification 45 | }; 46 | })(); 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Petr Huřťák 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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Tomatotim", 3 | "version": "1.1.0", 4 | "description": "Customizable application for time management inspired by Pomodoro technique", 5 | "main": "gulpfile.js", 6 | "dependencies": {}, 7 | "devDependencies": { 8 | "browser-sync": "2.9.11", 9 | "del": "2.0.2", 10 | "gulp": "3.9.0", 11 | "gulp-autoprefixer": "3.1.0", 12 | "gulp-csso": "1.0.1", 13 | "gulp-flatten": "0.2.0", 14 | "gulp-htmlmin": "1.2.0", 15 | "gulp-if": "2.0.0", 16 | "gulp-imagemin": "2.3.0", 17 | "gulp-load-plugins": "1.0.0", 18 | "gulp-rev": "6.0.1", 19 | "gulp-rev-replace": "0.4.2", 20 | "gulp-uglify": "1.4.2", 21 | "gulp-useref": "1.3.0", 22 | "gulp-xo": "0.4.0", 23 | "normalize.css": "3.0.3" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/Hurtak/Tomatotim" 28 | }, 29 | "xo": { 30 | "envs": [ 31 | "browser" 32 | ], 33 | "globals": [ 34 | "TT" 35 | ] 36 | }, 37 | "author": "Petr Huřťák", 38 | "license": "MIT", 39 | "bugs": { 40 | "url": "https://github.com/Hurtak/Tomatotim/issues" 41 | }, 42 | "homepage": "http://tomatotim.com" 43 | } 44 | -------------------------------------------------------------------------------- /resources/tomato/tomato.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/js/views/sidebar.js: -------------------------------------------------------------------------------- 1 | TT.Views.Sidebar = (function () { 2 | 'use strict'; 3 | 4 | var sidebarOpen = false; 5 | 6 | var sidebarButton = document.getElementById('sidebar-button'); 7 | var sidebarOverlay = document.getElementById('sidebar-overlay'); 8 | 9 | function getSidebarOverlay() { 10 | return sidebarOverlay; 11 | } 12 | 13 | function getSidebarButton() { 14 | return sidebarButton; 15 | } 16 | 17 | function openSidebar() { 18 | if (!sidebarOpen) { 19 | document.body.setAttribute('data-sidebar-open', ''); 20 | sidebarOpen = true; 21 | } 22 | } 23 | 24 | function closeSidebar() { 25 | if (sidebarOpen) { 26 | document.body.removeAttribute('data-sidebar-open'); 27 | sidebarOpen = false; 28 | } 29 | } 30 | 31 | function toogleSidebar() { 32 | if (sidebarOpen) { 33 | closeSidebar(); 34 | } else { 35 | openSidebar(); 36 | } 37 | } 38 | 39 | function isSidebarOpen() { 40 | return sidebarOpen; 41 | } 42 | 43 | return { 44 | getSidebarOverlay: getSidebarOverlay, 45 | getSidebarButton: getSidebarButton, 46 | isSidebarOpen: isSidebarOpen, 47 | closeSidebar: closeSidebar, 48 | openSidebar: openSidebar, 49 | toogleSidebar: toogleSidebar 50 | }; 51 | })(); 52 | -------------------------------------------------------------------------------- /app/js/views/controls.js: -------------------------------------------------------------------------------- 1 | TT.Views.Controls = (function () { 2 | 'use strict'; 3 | 4 | // true == start state, false == pause state 5 | var startButtonState = true; 6 | 7 | var startButton = document.getElementById('start'); 8 | var resetButton = document.getElementById('reset'); 9 | var skipButton = document.getElementById('skip'); 10 | 11 | var startButtonIcon = document.getElementById('start-icon'); 12 | var startButtonCaption = document.getElementById('start-caption'); 13 | 14 | function toogleStartButtonCaption() { 15 | if (startButtonState) { 16 | startButtonCaption.innerHTML = 'pause'; 17 | startButtonIcon.className = 'icon-pause-1'; 18 | } else { 19 | startButtonCaption.innerHTML = 'start'; 20 | startButtonIcon.className = 'icon-play-1'; 21 | } 22 | 23 | startButtonState = !startButtonState; 24 | } 25 | 26 | function resetStartButton() { 27 | if (!startButtonState) { 28 | toogleStartButtonCaption(); 29 | } 30 | } 31 | 32 | function getStartButton() { 33 | return startButton; 34 | } 35 | 36 | function getResetButton() { 37 | return resetButton; 38 | } 39 | 40 | function getSkipButton() { 41 | return skipButton; 42 | } 43 | 44 | return { 45 | getStartButton: getStartButton, 46 | getResetButton: getResetButton, 47 | getSkipButton: getSkipButton, 48 | resetStartButton: resetStartButton, 49 | toogleStartButtonCaption: toogleStartButtonCaption 50 | }; 51 | })(); 52 | -------------------------------------------------------------------------------- /app/js/views/progress.js: -------------------------------------------------------------------------------- 1 | TT.Views.Progress = (function () { 2 | 'use strict'; 3 | 4 | var imagesWrapper = document.getElementById('progress'); 5 | var images = imagesWrapper.getElementsByTagName('i'); 6 | 7 | var description = document.getElementById('description'); 8 | 9 | var imagesTitle = { 10 | unfinished: 'Unfinished interval', 11 | work: 'Work interval', 12 | break: 'Break interval', 13 | longbreak: 'Long break interval', 14 | finished: 'Finished interval' 15 | }; 16 | 17 | function setImageType(type, index) { 18 | images[index].className = 'icon-tomato color-' + type; 19 | images[index].title = imagesTitle[type]; 20 | } 21 | 22 | function resetProgress() { 23 | setDescription(''); 24 | 25 | for (var index = 0; index < images.length; index++) { 26 | setImageType('unfinished', index); 27 | } 28 | } 29 | 30 | function createImage(type) { 31 | var i = document.createElement('i'); 32 | i.className = 'icon-tomato color-' + type; 33 | i.title = imagesTitle[type]; 34 | imagesWrapper.appendChild(i); 35 | } 36 | 37 | function removeImages() { 38 | while (imagesWrapper.firstChild) { 39 | imagesWrapper.removeChild(imagesWrapper.firstChild); 40 | } 41 | } 42 | 43 | function setDescription(text) { 44 | description.innerHTML = text; 45 | } 46 | 47 | return { 48 | setDescription: setDescription, 49 | setImageType: setImageType, 50 | resetProgress: resetProgress, 51 | createImage: createImage, 52 | removeImages: removeImages 53 | }; 54 | })(); 55 | -------------------------------------------------------------------------------- /app/css/animations.css: -------------------------------------------------------------------------------- 1 | body[data-sidebar-open] .sidebar { 2 | transform: translateX(-450px); 3 | } 4 | 5 | body[data-sidebar-open] .sidebar-overlay { 6 | opacity: 1; 7 | transform: translateX(-100%); 8 | } 9 | 10 | body[data-sidebar-open] .sidebar-button { 11 | transform: translateX(-20px); 12 | } 13 | 14 | body[data-sidebar-open] .sidebar-button i { 15 | transform: rotate(-180deg); 16 | } 17 | 18 | body[data-sidebar-open] .sidebar-button .cog { 19 | opacity: 0; 20 | } 21 | 22 | body[data-sidebar-open] .sidebar-button .cancel, 23 | body[data-sidebar-open] .sidebar-button .background { 24 | opacity: 1; 25 | } 26 | 27 | /* transitions */ 28 | 29 | .clock, 30 | .controls button { 31 | transition: font-size .4s cubic-bezier(0.68, -0.55, 0.265, 1.55); 32 | } 33 | 34 | .progress i { 35 | transition: font-size .4s cubic-bezier(0.68, -0.55, 0.265, 1.55), 36 | color .6s; 37 | } 38 | 39 | body[data-sidebar-open] .sidebar, 40 | body[data-sidebar-open] .sidebar-button, 41 | body[data-sidebar-open] .sidebar-button i, 42 | .sidebar, 43 | .sidebar-button, 44 | .sidebar-button i { 45 | transition: transform .6s cubic-bezier(0.23, 1, 0.32, 1); 46 | } 47 | 48 | body[data-sidebar-open] .sidebar-overlay { 49 | transition: opacity .7s ease; 50 | } 51 | 52 | .sidebar-overlay { 53 | transition: transform .3s step-end, 54 | opacity .3s ease-out; 55 | } 56 | 57 | .checkbox + .checkbox-button, 58 | .checkbox + .checkbox-button:after, 59 | .checkbox + .checkbox-button:before { 60 | transition: background .2s ease, 61 | transform .2s ease; 62 | } 63 | -------------------------------------------------------------------------------- /app/css/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | overflow: hidden; 3 | background-color: #424242; 4 | background-image: url('/img/background.jpg'); 5 | background-position: center center; 6 | background-size: cover; 7 | background-repeat: no-repeat; 8 | } 9 | 10 | /* Wrapper */ 11 | 12 | .wrapper { 13 | height: 100%; 14 | text-align: center; 15 | } 16 | 17 | .wrapper:before { 18 | display: inline-block; 19 | height: 100%; 20 | margin-right: -0.25em; 21 | content: ''; 22 | vertical-align: middle; 23 | } 24 | 25 | .wrapper .centered { 26 | display: inline-block; 27 | vertical-align: middle; 28 | } 29 | 30 | /* Description */ 31 | 32 | .description { 33 | height: 60px; 34 | color: #fff; 35 | font-size: 60px; 36 | } 37 | 38 | /* Clock */ 39 | 40 | .clock { 41 | height: 170px; 42 | color: #fff; 43 | font-size: 150px; 44 | } 45 | 46 | /* Progress */ 47 | 48 | .progress { 49 | height: 70px; 50 | } 51 | 52 | .progress i { 53 | font-size: 60px; 54 | } 55 | 56 | /* Controls */ 57 | 58 | .controls { 59 | margin-top: 35px; 60 | } 61 | 62 | .controls button { 63 | position: relative; 64 | width: 180px; 65 | margin: 0 10px; 66 | padding: 20px; 67 | color: #fff; 68 | background-color: transparent; 69 | border: 3px solid #fff; 70 | border-radius: 10px; 71 | font-size: 25px; 72 | } 73 | 74 | .controls button:focus { 75 | outline: 0; 76 | } 77 | 78 | .controls button:active { 79 | position: relative; 80 | top: 2px; 81 | } 82 | 83 | .controls button i { 84 | position: absolute; 85 | left: 25px; 86 | } 87 | 88 | .controls button span { 89 | padding-left: 32px; 90 | } 91 | -------------------------------------------------------------------------------- /app/js/services/favicon.js: -------------------------------------------------------------------------------- 1 | TT.Services.Favicon = (function () { 2 | 'use strict'; 3 | 4 | var icon = document.getElementById('favicon-ico'); 5 | 6 | function init() { 7 | // When we change .ico favicon, IE switches to otherwise unused .png icons, 8 | // instead of using the changed one. If we remove these icons, dynamic 9 | // favicon change works. 10 | if (TT.Services.BrowserDetection.isIE) { 11 | var favicons = document.querySelectorAll('[data-favicon-explorer]'); 12 | 13 | for (var index = 0; index < favicons.length; index++) { 14 | document.head.removeChild(favicons[index]); 15 | } 16 | } 17 | } 18 | 19 | // type: 'work', 'break', 'longbreak' 20 | function setFavicon(type) { 21 | // Firefox: only uses .ico, changing href changes the icon 22 | // Chrome: we need to delete icon and create new one 23 | 24 | if (TT.Services.BrowserDetection.isFirefox || TT.Services.BrowserDetection.isIE) { 25 | icon.rel = 'shortcut icon'; 26 | icon.href = 'icons/favicon-' + type + '.ico'; 27 | icon.id = 'favicon-ico'; 28 | } else { 29 | // chrome, opera 30 | // TODO: test with Safari 31 | icon = document.createElement('link'); 32 | 33 | icon.rel = 'icon'; 34 | // TODO: maybe remove these? 35 | icon.setAttribute('type', 'image/png'); 36 | icon.href = 'icons/favicon-16x16-' + type + '.png'; 37 | // TODO: maybe remove these? 38 | icon.setAttribute('sizes', '16x16'); 39 | icon.id = 'favicon-png'; 40 | 41 | var oldIcon = document.getElementById('favicon-png'); 42 | if (oldIcon) { 43 | document.head.removeChild(oldIcon); 44 | } 45 | 46 | document.head.appendChild(icon); 47 | } 48 | } 49 | 50 | return { 51 | init: init, 52 | setFavicon: setFavicon 53 | }; 54 | })(); 55 | -------------------------------------------------------------------------------- /app/js/views/settings.js: -------------------------------------------------------------------------------- 1 | TT.Views.Settings = (function () { 2 | 'use strict'; 3 | 4 | var audio = document.getElementById('audio'); 5 | var audioTest = document.getElementById('audio-test'); 6 | var notifications = document.getElementById('notifications'); 7 | var notificationsTest = document.getElementById('notifications-test'); 8 | var taskbarFlash = document.getElementById('taskbar-flash'); 9 | var taskbarFlashTest = document.getElementById('taskbar-flash-test'); 10 | var timerAutoPause = document.getElementById('timer-auto-pause'); 11 | 12 | var workInterval = document.getElementById('work-interval'); 13 | var breakInterval = document.getElementById('break-interval'); 14 | var longbreakInterval = document.getElementById('longbreak-interval'); 15 | 16 | var repeat = document.getElementById('repeat'); 17 | 18 | var resetSettings = document.getElementById('reset-settings'); 19 | 20 | function getNumberInputs() { 21 | return document.querySelectorAll('.settings input[type=number]'); 22 | } 23 | 24 | function getPlusMinusButtons() { 25 | return document.querySelectorAll('.settings [data-increment]'); 26 | } 27 | 28 | function hide(el) { 29 | el.parentNode.parentNode.style.display = 'none'; 30 | } 31 | 32 | return { 33 | audio: audio, 34 | audioTest: audioTest, 35 | notifications: notifications, 36 | notificationsTest: notificationsTest, 37 | taskbarFlash: taskbarFlash, 38 | taskbarFlashTest: taskbarFlashTest, 39 | timerAutoPause: timerAutoPause, 40 | workInterval: workInterval, 41 | breakInterval: breakInterval, 42 | longbreakInterval: longbreakInterval, 43 | repeat: repeat, 44 | resetSettings: resetSettings, 45 | getNumberInputs: getNumberInputs, 46 | getPlusMinusButtons: getPlusMinusButtons, 47 | hide: hide 48 | }; 49 | })(); 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Tomatotim 2 | ========== 3 | 4 | ### Description 5 | 6 | Application for time management inspired by technique called 7 | [Pomodoro](https://en.wikipedia.org/wiki/Pomodoro_Technique). 8 | The technique uses a timer to break down your working time into 9 | 25 minutes long work periods separated by short breaks. The 10 | method is based on the idea that frequent breaks can improve 11 | mental agility. 12 | 13 | The most common implementation is as follows: 14 | 15 |
    16 |
  1. 25 minutes of work
  2. 17 |
  3. 5 minute break
  4. 18 |
  5. Repeat four times, after which you have a 20–30 minute break
  6. 19 |
  7. Start again
  8. 20 |
21 | 22 | For more detailed information see the very helpful 23 | [Pomodoro get started guide](http://pomodorotechnique.com/get-started/). 24 | 25 | ### Demo 26 | 27 | * http://tomatotim.com 28 | 29 | ### Features 30 | 31 | Dynamic favicon and title 32 | 33 | * Dynamic favicon and title 34 | 35 | Dynamic favicon and title 36 | 37 | * Audio and web browser notifications 38 | 39 | Audio and web browser notifications 40 | 41 | * Customization options 42 | 43 | Customization options 44 | 45 | * Keyboard hotkeys 46 | 47 | Keyboard hotkeys 48 | 49 | ### Build 50 | 51 | ##### Prerequisites 52 | 53 | [Node.js](http://nodejs.org) is required. 54 | ``` 55 | npm install -g gulp 56 | ``` 57 | 58 | ##### Create App 59 | 60 | ``` 61 | git clone https://github.com/Hurtak/Tomatotim.git 62 | cd Tomatotim 63 | npm install 64 | ``` 65 | 66 | ##### Usage 67 | 68 | build app into dist folder and run it from there 69 | 70 | ``` 71 | gulp 72 | ``` 73 | 74 | run app from app folder 75 | 76 | ``` 77 | gulp dev 78 | ``` 79 | -------------------------------------------------------------------------------- /app/css/fontello.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'fontello'; 3 | src: url('../fonts/fontello.eot?96155523'); 4 | src: url('../fonts/fontello.eot?96155523#iefix') format('embedded-opentype'), 5 | url('../fonts/fontello.woff?96155523') format('woff'), 6 | url('../fonts/fontello.ttf?96155523') format('truetype'), 7 | url('../fonts/fontello.svg?96155523#fontello') format('svg'); 8 | font-weight: normal; 9 | font-style: normal; 10 | } 11 | /* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */ 12 | /* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */ 13 | /* 14 | @media screen and (-webkit-min-device-pixel-ratio:0) { 15 | @font-face { 16 | font-family: 'fontello'; 17 | src: url('../fonts/fontello.svg?96155523#fontello') format('svg'); 18 | } 19 | } 20 | */ 21 | 22 | [class^="icon-"]:before, [class*=" icon-"]:before { 23 | font-family: "fontello"; 24 | font-style: normal; 25 | font-weight: normal; 26 | speak: none; 27 | 28 | display: inline-block; 29 | text-decoration: inherit; 30 | width: 1em; 31 | margin-right: .2em; 32 | text-align: center; 33 | /* opacity: .8; */ 34 | 35 | /* For safety - reset parent styles, that can break glyph codes*/ 36 | font-variant: normal; 37 | text-transform: none; 38 | 39 | /* fix buttons height, for twitter bootstrap */ 40 | line-height: 1em; 41 | 42 | /* Animation center compensation - margins should be symmetric */ 43 | /* remove if not needed */ 44 | margin-left: .2em; 45 | 46 | /* you can be more comfortable with increased icons size */ 47 | /* font-size: 120%; */ 48 | 49 | /* Uncomment for 3D effect */ 50 | /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */ 51 | } 52 | 53 | .icon-play-1:before { content: '\e800'; } /* '' */ 54 | .icon-fast-forward:before { content: '\e801'; } /* '' */ 55 | .icon-cw-1:before { content: '\e802'; } /* '' */ 56 | .icon-cog-circled:before { content: '\e803'; } /* '' */ 57 | .icon-tomato:before { content: '\e804'; } /* '' */ 58 | .icon-cancel-circle-2:before { content: '\e805'; } /* '' */ 59 | .icon-info-circled-1:before { content: '\e806'; } /* '' */ 60 | .icon-wrench-4:before { content: '\e807'; } /* '' */ 61 | .icon-keyboard:before { content: '\e808'; } /* '' */ 62 | .icon-github-circled:before { content: '\e809'; } /* '' */ 63 | .icon-bitcoin:before { content: '\e80a'; } /* '' */ 64 | .icon-wallet:before { content: '\e80b'; } /* '' */ 65 | .icon-qrcode-1:before { content: '\e80c'; } /* '' */ 66 | .icon-search-5:before { content: '\e80d'; } /* '' */ 67 | .icon-file-code:before { content: '\e80e'; } /* '' */ 68 | .icon-link-ext:before { content: '\e80f'; } /* '' */ 69 | .icon-twitter-1:before { content: '\e810'; } /* '' */ 70 | .icon-flashlight:before { content: '\e811'; } /* '' */ 71 | .icon-pause-1:before { content: '\e812'; } /* '' */ 72 | .icon-bell-1:before { content: '\e815'; } /* '' */ 73 | .icon-volume-down:before { content: '\e816'; } /* '' */ 74 | .icon-plus-3:before { content: '\e817'; } /* '' */ 75 | .icon-minus-3:before { content: '\e818'; } /* '' */ 76 | .icon-mail-4:before { content: '\e81b'; } /* '' */ 77 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | 5 | var $ = require('gulp-load-plugins')(); 6 | var browserSync = require('browser-sync'); 7 | var del = require('del'); 8 | 9 | // Paths 10 | 11 | var appPath = 'app'; 12 | var distPath = 'dist'; 13 | 14 | var paths = { 15 | app: { 16 | html: appPath + '/**/*.{html,htm}', 17 | js: appPath + '/js/**/*.js', 18 | css: appPath + '/css/**/*.css', 19 | img: appPath + '/img/**/*', 20 | fonts: appPath + '/fonts/*.{eot,svg,ttf,woff}', 21 | icons: appPath + '/icons/*', 22 | audio: appPath + '/audio/*' 23 | }, 24 | dist: { 25 | fonts: distPath + '/fonts', 26 | icons: distPath + '/icons', 27 | audio: distPath + '/audio', 28 | img: distPath + '/img' 29 | } 30 | }; 31 | 32 | // Options 33 | 34 | var options = { 35 | autoprefixer: { 36 | browsers: [ 37 | '> 2%', 38 | 'last 2 versions', 39 | 'Firefox ESR', 40 | 'ie >= 9' 41 | ], 42 | cascade: false 43 | }, 44 | htmlmin: { 45 | removeComments: true, 46 | collapseWhitespace: true 47 | }, 48 | imagemin: { 49 | progressive: true, 50 | svgoPlugins: [ 51 | {removeViewBox: false} 52 | ], 53 | use: [] 54 | } 55 | }; 56 | 57 | // linters 58 | 59 | gulp.task('lint', function () { 60 | return gulp.src([paths.app.js, 'gulpfile.js']) 61 | .pipe($.xo()); 62 | }); 63 | 64 | // clean 65 | 66 | gulp.task('clean', function () { 67 | del([ 68 | distPath + '/*' 69 | ]); 70 | }); 71 | 72 | // compile 73 | 74 | gulp.task('compile', function () { 75 | var assets = $.useref.assets(); 76 | 77 | return gulp.src(paths.app.html) 78 | .pipe(assets) 79 | .pipe($.if('*.js', $.uglify())) 80 | .pipe($.if('*.css', $.autoprefixer(options.autoprefixer))) 81 | .pipe($.if('*.css', $.csso())) 82 | .pipe($.rev()) 83 | .pipe(assets.restore()) 84 | .pipe($.useref()) 85 | .pipe($.revReplace()) 86 | .pipe($.if('*.html', $.htmlmin(options.htmlmin))) 87 | .pipe(gulp.dest(distPath)); 88 | }); 89 | 90 | gulp.task('img', function () { 91 | return gulp.src(paths.app.img) 92 | .pipe($.imagemin(options.imagemin)) 93 | .pipe(gulp.dest(paths.dist.img)); 94 | }); 95 | 96 | gulp.task('fonts', function () { 97 | return gulp.src(paths.app.fonts) 98 | .pipe($.flatten()) 99 | .pipe(gulp.dest(paths.dist.fonts)); 100 | }); 101 | 102 | gulp.task('icons', function () { 103 | return gulp.src(paths.app.icons) 104 | .pipe(gulp.dest(paths.dist.icons)); 105 | }); 106 | 107 | gulp.task('audio', function () { 108 | return gulp.src(paths.app.audio) 109 | .pipe(gulp.dest(paths.dist.audio)); 110 | }); 111 | 112 | // Browser sync 113 | 114 | gulp.task('browser-sync', function () { 115 | return browserSync({ 116 | server: { 117 | baseDir: distPath, 118 | index: 'index.html', 119 | routes: { 120 | '/node_modules': 'node_modules' 121 | } 122 | } 123 | }); 124 | }); 125 | 126 | gulp.task('browser-sync-dev', function () { 127 | return browserSync({ 128 | server: { 129 | baseDir: appPath, 130 | index: 'index.html', 131 | routes: { 132 | '/node_modules': 'node_modules' 133 | } 134 | } 135 | }); 136 | }); 137 | 138 | // Main gulp tasks 139 | 140 | // builds all files and runs from dist directory 141 | gulp.task('default', ['lint', 'compile', 'img', 'fonts', 'icons', 'audio', 'browser-sync']); 142 | 143 | // skips building phase and runs from dist directory 144 | gulp.task('run', ['browser-sync']); 145 | 146 | // runs from app directory 147 | gulp.task('dev', ['browser-sync-dev'], function () { 148 | // watch for JS changes 149 | gulp.watch(paths.app.js, ['lint', browserSync.reload]); 150 | 151 | // watch for CSS changes 152 | gulp.watch(paths.app.css, browserSync.reload); 153 | 154 | // watch for HTML changes 155 | gulp.watch(paths.app.html, browserSync.reload); 156 | }); 157 | -------------------------------------------------------------------------------- /app/css/sidebar.css: -------------------------------------------------------------------------------- 1 | .sidebar { 2 | position: absolute; 3 | top: 0; 4 | right: -450px; 5 | width: 450px; 6 | height: 100%; 7 | padding: 20px; 8 | overflow-y: auto; 9 | color: #fff; 10 | background-color: #213a57; 11 | } 12 | 13 | .sidebar h2 { 14 | margin-top: 40px; 15 | font-family: Verdana; 16 | font-weight: 400; 17 | } 18 | 19 | .sidebar h2:first-child { 20 | margin-top: 10px; 21 | } 22 | 23 | .sidebar p, 24 | .sidebar li { 25 | line-height: 140%; 26 | } 27 | 28 | .sidebar a { 29 | color: #fff; 30 | } 31 | 32 | .sidebar .button { 33 | display: inline-block; 34 | line-height: 30px; 35 | padding: 0 10px 0 7px; 36 | color: #fff; 37 | background-color: transparent; 38 | border: 1px solid #fff; 39 | border-radius: 5px; 40 | text-decoration: none; 41 | } 42 | 43 | .sidebar .button:active { 44 | transform: translateY(2px); 45 | } 46 | 47 | .sidebar .donation .button { 48 | margin-right: 14px; 49 | } 50 | 51 | .sidebar .donation .button:last-child { 52 | margin-right: 0; 53 | } 54 | 55 | .sidebar .highlight { 56 | padding: 3px 8px 4px; 57 | color: #fff; 58 | background-color: #41576f; 59 | border-radius: 5px; 60 | font-family: 'Consolas', 'Courier New'; 61 | } 62 | 63 | .sidebar .hotkeys .highlight { 64 | margin-right: 6px; 65 | } 66 | 67 | .sidebar .bitcoin-address { 68 | word-wrap: break-word; 69 | } 70 | 71 | .sidebar .links i { 72 | font-size: 120%; 73 | } 74 | 75 | .sidebar .links .email { 76 | position: relative; 77 | top: 1px; 78 | } 79 | 80 | .sidebar .links .code { 81 | font-size: 100%; 82 | } 83 | 84 | /* Settings */ 85 | 86 | .settings td { 87 | padding: 5px 0; 88 | padding-right: 10px; 89 | } 90 | 91 | .settings td:last-child { 92 | padding-right: 0; 93 | } 94 | 95 | .checkbox { 96 | display: none; 97 | } 98 | 99 | .checkbox + .checkbox-button { 100 | position: relative; 101 | display: block; 102 | width: 60px; 103 | height: 25px; 104 | overflow: hidden; 105 | border: 1px solid #fff; 106 | border-radius: 5px; 107 | cursor: pointer; 108 | } 109 | 110 | .checkbox + .checkbox-button:after, 111 | .checkbox + .checkbox-button:before { 112 | position: absolute; 113 | width: 100%; 114 | line-height: 25px; 115 | color: #fff; 116 | text-shadow: 0 1px 0 rgba(0, 0, 0, .4); 117 | text-align: center; 118 | font-family: sans-serif; 119 | font-weight: bold; 120 | } 121 | 122 | .checkbox:checked + .checkbox-button { 123 | background: rgba(101, 230, 69, .6); 124 | } 125 | 126 | .checkbox + .checkbox-button:after { 127 | left: 100%; 128 | content: attr(data-caption-on); 129 | } 130 | 131 | .checkbox + .checkbox-button:before { 132 | left: 0; 133 | content: attr(data-caption-off); 134 | } 135 | 136 | .checkbox + .checkbox-button:active:before { 137 | transform: translateX(-10%); 138 | } 139 | 140 | .checkbox:checked + .checkbox-button:active:after { 141 | transform: translateX(-90%); 142 | } 143 | 144 | .checkbox:checked + .checkbox-button:before { 145 | transform: translateX(-100%); 146 | } 147 | 148 | .checkbox:checked + .checkbox-button:after { 149 | transform: translateX(-100%); 150 | } 151 | 152 | .settings input[type=number] { 153 | width: 60px; 154 | height: 25px; 155 | padding-right: 1em; 156 | background-color: transparent; 157 | border: 1px solid #fff; 158 | border-radius: 5px; 159 | text-align: right; 160 | font-family: 'Consolas', 'Courier New'; 161 | } 162 | 163 | .settings input[type=number] { 164 | /* disable browser's +- buttons inside input */ 165 | -moz-appearance: textfield; 166 | } 167 | 168 | .settings input::-webkit-outer-spin-button, 169 | .settings input::-webkit-inner-spin-button { 170 | /* disable browser's +- buttons inside input */ 171 | -webkit-appearance: none; 172 | } 173 | 174 | .settings .test, 175 | .settings .plus, 176 | .settings .minus { 177 | height: 25px; 178 | line-height: 23px; 179 | padding: 0; 180 | background-color: transparent; 181 | border: 1px solid #fff; 182 | border-radius: 5px; 183 | } 184 | 185 | .settings .test:active, 186 | .settings .plus:active, 187 | .settings .minus:active { 188 | transform: translateY(1px); 189 | } 190 | 191 | .settings .plus, 192 | .settings .minus { 193 | float: left; 194 | width: 30px; 195 | text-align: center; 196 | } 197 | 198 | .settings .plus i, 199 | .settings .minus i { 200 | position: relative; 201 | top: -2px; 202 | font-size: 70%; 203 | } 204 | 205 | .settings .plus { 206 | border-left: 0; 207 | border-radius: 0 5px 5px 0; 208 | } 209 | 210 | .settings .minus { 211 | border-radius: 5px 0 0 5px; 212 | } 213 | 214 | .settings .test { 215 | width: 60px; 216 | } 217 | 218 | .settings .test i { 219 | position: relative; 220 | left: -1px; 221 | } 222 | 223 | .settings .test span { 224 | position: relative; 225 | left: -4px; 226 | } 227 | 228 | /* Share buttons */ 229 | 230 | .share-buttons > div, 231 | .share-buttons > iframe { 232 | float: left; 233 | margin-right: 10px; 234 | } 235 | 236 | .share-buttons > div:last-child { 237 | margin-right: 0; 238 | } 239 | 240 | /* Sidebar button */ 241 | 242 | .sidebar-button { 243 | position: absolute; 244 | top: 10px; 245 | right: 10px; 246 | width: 55px; 247 | height: 55px; 248 | z-index: 10; 249 | color: #fff; 250 | background-color: transparent; 251 | border: 0; 252 | outline: 0; 253 | cursor: pointer; 254 | font-size: 40px; 255 | } 256 | 257 | .sidebar-button i { 258 | position: absolute; 259 | top: 5px; 260 | left: 0; 261 | z-index: 15; 262 | } 263 | 264 | .sidebar-button .cancel, 265 | .sidebar-button .background { 266 | opacity: 0; 267 | } 268 | 269 | .sidebar-button .background { 270 | position: relative; 271 | top: 5px; 272 | left: 5px; 273 | width: 46px; 274 | height: 46px; 275 | 276 | background-color: #213a57; 277 | border-radius: 50%; 278 | } 279 | 280 | /* Sidebar overlay */ 281 | 282 | .sidebar-overlay { 283 | position: fixed; 284 | top: 0; 285 | left: 100%; 286 | width: 100%; 287 | height: 100%; 288 | opacity: 0; 289 | background-color: rgba(0, 0, 0, .5); 290 | } 291 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | // http://eslint.org/docs/rules 2 | { 3 | "rules": { 4 | // Possible Errors 5 | "no-extra-parens": 1, // disallow unnecessary parentheses (off by default) 6 | "valid-jsdoc": [1, {"requireReturn": false}], // Ensure JSDoc comments are valid (off by default) 7 | 8 | // Best Practices 9 | "block-scoped-var": 0, // treat var statements as if they were block scoped (off by default) 10 | "guard-for-in": 1, // make sure for-in loops have an if statement (off by default) 11 | "no-else-return": 1, // disallow else after a return in an if (off by default) 12 | "no-eq-null": 2, // disallow comparisons to null without a type-checking operator (off by default) 13 | "no-floating-decimal": 1, // disallow the use of leading or trailing decimal points in numeric literals (off by default) 14 | "no-self-compare": 2, // disallow comparisons where both sides are exactly the same (off by default) 15 | "no-throw-literal": 1, // restrict what can be thrown as an exception (off by default) 16 | "no-void": 1, // disallow use of void operator (off by default) 17 | "wrap-iife": [2, "inside"], // require immediate function invocation to be wrapped in parentheses (off by default) 18 | 19 | // Strict Mode 20 | "strict": [2, "function"], // controls location of Use Strict Directives 21 | 22 | // Variables 23 | "no-undef": 0, // disallow use of undeclared variables unless mentioned in a /*global */ block 24 | "no-undefined": 1, // disallow use of undefined variable (off by default) 25 | "no-unused-vars": 0, // disallow usage of expressions in statement position 26 | "no-use-before-define": 0, // disallow use of variables before they are defined 27 | 28 | // Node.js 29 | 30 | // Stylistic Issues 31 | "indent": [1, 2], // this option sets a specific tab width for your code (off by default) 32 | "brace-style": [1, "1tbs", {"allowSingleLine": false}], // enforce one true brace style (off by default) 33 | "comma-style": [1, "last"], // enforce one true comma style (off by default) 34 | "consistent-this": [1, "_this"], // enforces consistent naming when capturing the current execution context (off by default) 35 | "comma-spacing": [2, {"before": false, "after": true}], // enforce spacing before and after comma 36 | "func-style": [1, "epression"], // enforces use of function declarations or expressions (off by default) 37 | "max-nested-callbacks": [1, 2], // specify the maximum depth callbacks can be nested (off by default) 38 | "no-lonely-if": [1, "tab"], // disallow if as the only statement in an else block (off by default) 39 | "no-multiple-empty-lines": [1, {"max": 2}], // disallow multiple empty lines (off by default) 40 | "no-nested-ternary": 1, // disallow nested ternary expressions (off by default) 41 | "one-var": [1, "never"], // allow just one var statement per function (off by default) 42 | "quote-props": [1, "as-needed"], // require quotes around object literal property names (off by default) 43 | "quotes": [1, "single"], // specify whether double or single quotes should be used 44 | "space-after-keywords": [1, "always"], // require a space after certain keywords (off by default) 45 | "space-before-blocks": [1, "always"], // require or disallow space before blocks (off by default) 46 | "space-before-function-paren": [1, "never"], // require or disallow space before function opening parenthesis (off by default) 47 | "space-in-brackets": [1, "never"], // require or disallow spaces inside brackets (off by default) 48 | "space-in-parens": [1, "never"], // require or disallow spaces inside parentheses (off by default) 49 | "spaced-line-comment": [1, "always"], // require or disallow a space immediately following the // in a line comment (off by default) 50 | 51 | // ECMAScript 6 52 | "generator-star-spacing": [2, "after"], // enforce the spacing around the * in generator functions (off by default) 53 | "no-var": 0, // require let or const instead of var (off by default) 54 | 55 | // Legacy 56 | "no-bitwise": 1 // disallow use of bitwise operators (off by default) 57 | }, 58 | "ecmaFeatures": { 59 | // Enable support for ECMAScript 6 features 60 | "arrowFunctions": false, // enable arrow functions 61 | "binaryLiterals": false, // enable binary literals 62 | "blockBindings": false, // enable let and const (aka block bindings) 63 | "classes": false, // enable classes 64 | "defaultParams": false, // enable default function parameters 65 | "destructuring": false, // enable destructuring 66 | "forOf": false, // enable for-of loops 67 | "generators": false, // enable generators 68 | "modules": false, // enable modules and global strict mode 69 | "objectLiteralComputedProperties": false, // enable computed object literal property names 70 | "objectLiteralDuplicateProperties": false, // enable duplicate object literal properties in strict mode 71 | "objectLiteralShorthandMethods": false, // enable object literal shorthand methods 72 | "objectLiteralShorthandProperties": false, // enable object literal shorthand properties 73 | "octalLiterals": false, // enable octal literals 74 | "regexUFlag": false, // enable the regular expression u flag 75 | "regexYFlag": false, // enable the regular expression y flag 76 | "spread": false, // enable the spread operator 77 | "superInFunctions": false, // enable super references inside of functions 78 | "templateStrings": false, // enable template strings 79 | "unicodeCodePointEscapes": false, // enable code point escapes 80 | "globalReturn": false, // allow return statements in the global scope 81 | "jsx": false, // enable JSX 82 | }, 83 | "env": { 84 | "browser": false, // browser global variables 85 | "es6": false, // enable all ECMAScript 6 features except for modules 86 | "node": false, // Node.js global variables and Node.js-specific rules 87 | "amd": false, // defines require() and define() as global variables as per the amd spec 88 | "mocha": false, // adds all of the Mocha testing global variables 89 | "jasmine": false, // adds all of the Jasmine testing global variables for version 1.3 and 2.0 90 | "phantomjs": false, // phantomjs global variables 91 | "jquery": false // jquery global variables 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /app/js/settings.js: -------------------------------------------------------------------------------- 1 | TT.Settings = (function () { 2 | 'use strict'; 3 | 4 | function init() { 5 | // update config defaults with saved settings (if available) 6 | TT.Config.set('audio', Boolean(TT.Services.Storage.get('audio'))); 7 | TT.Config.set('notifications', Boolean(TT.Services.Storage.get('notifications'))); 8 | TT.Config.set('taskbarFlash', Boolean(TT.Services.Storage.get('taskbarFlash'))); 9 | TT.Config.set('timerAutoPause', Boolean(TT.Services.Storage.get('timerAutoPause'))); 10 | 11 | TT.Config.set('workInterval', TT.Services.Storage.get('workInterval') || TT.Config.get('workInterval')); 12 | TT.Config.set('breakInterval', TT.Services.Storage.get('breakInterval') || TT.Config.get('breakInterval')); 13 | TT.Config.set('longbreakInterval', TT.Services.Storage.get('longbreakInterval') || TT.Config.get('longbreakInterval')); 14 | 15 | TT.Config.set('repeat', TT.Services.Storage.get('repeat') || TT.Config.get('repeat')); 16 | 17 | // update settings view 18 | TT.Views.Settings.audio.checked = TT.Config.get('audio'); 19 | TT.Views.Settings.notifications.checked = TT.Config.get('notifications'); 20 | TT.Views.Settings.taskbarFlash.checked = TT.Config.get('taskbarFlash'); 21 | TT.Views.Settings.timerAutoPause.checked = TT.Config.get('timerAutoPause'); 22 | 23 | TT.Views.Settings.workInterval.value = TT.Config.get('workInterval') / 60; 24 | TT.Views.Settings.breakInterval.value = TT.Config.get('breakInterval') / 60; 25 | TT.Views.Settings.longbreakInterval.value = TT.Config.get('longbreakInterval') / 60; 26 | 27 | TT.Views.Settings.repeat.value = TT.Config.get('repeat'); 28 | 29 | // checkboxes 30 | TT.Views.Settings.audio.addEventListener('click', function () { 31 | TT.Config.set('audio', this.checked); 32 | TT.Services.Storage.set('audio', TT.Config.get('audio')); 33 | }); 34 | 35 | if (TT.Services.Notification.isAvaliable()) { 36 | TT.Views.Settings.notifications.addEventListener('click', function () { 37 | TT.Config.set('notifications', this.checked); 38 | 39 | if (TT.Config.get('notifications') === true) { 40 | TT.Services.Notification.requestPermission(); 41 | } 42 | 43 | TT.Services.Storage.set('notifications', TT.Config.get('notifications')); 44 | }); 45 | } else { 46 | TT.Views.Settings.hide(TT.Views.Settings.notifications); 47 | } 48 | 49 | if (TT.Services.TaskbarFlash.isAvaliable()) { 50 | TT.Views.Settings.taskbarFlash.addEventListener('click', function () { 51 | TT.Config.set('taskbarFlash', this.checked); 52 | TT.Services.Storage.set('taskbarFlash', this.checked); 53 | }); 54 | } else { 55 | TT.Views.Settings.hide(TT.Views.Settings.taskbarFlash); 56 | } 57 | 58 | TT.Views.Settings.timerAutoPause.addEventListener('click', function () { 59 | TT.Config.set('timerAutoPause', this.checked); 60 | TT.Services.Storage.set('timerAutoPause', this.checked); 61 | }); 62 | 63 | // test buttons 64 | TT.Views.Settings.audioTest.addEventListener('click', function () { 65 | TT.Services.Audio.play(); 66 | }); 67 | 68 | TT.Views.Settings.notificationsTest.addEventListener('click', function () { 69 | TT.Services.Notification.newNotification('Web notification test', 'work'); 70 | }); 71 | 72 | TT.Views.Settings.taskbarFlashTest.addEventListener('click', function () { 73 | // flashing only works when browser doesn't have focus 74 | for (var count = 0; count < 20; count++) { 75 | setTimeout(TT.Services.TaskbarFlash.flash, 500 * count); 76 | } 77 | }); 78 | 79 | // inputs type number and +- buttons 80 | var intervalNames = ['workInterval', 'breakInterval', 'longbreakInterval', 'repeat']; 81 | 82 | var numberInputs = TT.Views.Settings.getNumberInputs(); 83 | var plusMinusButtons = TT.Views.Settings.getPlusMinusButtons(); 84 | 85 | for (var i = 0; i < intervalNames.length; i++) { 86 | // interval settings inputs 87 | numberInputs[i].addEventListener('blur', makeClickHandlerForInput(numberInputs[i], intervalNames[i])); 88 | 89 | // plus minus buttons 90 | for (var j = 0; j < 2; j++) { 91 | plusMinusButtons[i * 2 + j].addEventListener('click', makeClickHandlerForControls(plusMinusButtons[i * 2 + j], intervalNames[i])); 92 | } 93 | } 94 | 95 | // reset settings 96 | TT.Views.Settings.resetSettings.addEventListener('click', function () { 97 | var confim = confirm('Are you sure?'); // eslint-disable-line no-alert 98 | if (confim) { 99 | TT.Services.Storage.clear(); 100 | location.reload(false); 101 | } 102 | }); 103 | 104 | // request permission in case we have notifications enabled in saved settings 105 | if (TT.Config.get('notifications') === true) { 106 | TT.Services.Notification.requestPermission(); 107 | } 108 | } 109 | 110 | function validateInput(value, min, max, defaultValue) { 111 | // non-number values converted to NaN 112 | value = Math.floor(value); 113 | 114 | if (!value) { 115 | value = defaultValue; 116 | } else if (value < Number(min)) { 117 | value = min; 118 | } else if (value > Number(max)) { 119 | value = max; 120 | } 121 | 122 | return Number(value); 123 | } 124 | 125 | function intervalInput(that, intervalType) { 126 | // conversion between seconds and minutes 127 | var multiplier = 60; 128 | if (intervalType === 'repeat') { 129 | multiplier = 1; 130 | } 131 | 132 | that.value = validateInput(that.value, that.min, that.max, TT.Config.get(intervalType) / multiplier); 133 | TT.Config.set(intervalType, that.value * multiplier); 134 | 135 | if (intervalType === 'repeat') { 136 | TT.Views.Progress.removeImages(); 137 | TT.Timer.init(); 138 | } else { 139 | TT.Timer.updateIntervals(); 140 | } 141 | 142 | TT.Services.Storage.set(intervalType, TT.Config.get(intervalType)); 143 | } 144 | 145 | // click handlers for number inputs in settings 146 | function makeClickHandlerForInput(that, intervalName) { 147 | return function () { 148 | intervalInput(that, intervalName); 149 | }; 150 | } 151 | 152 | // click handler for +- buttons next to settings inputs 153 | function makeClickHandlerForControls(that, intervalName) { 154 | return function () { 155 | // TODO: move to views 156 | var target = that.getAttribute('data-target'); 157 | target = document.getElementById(target); 158 | 159 | target.value = Number(target.value) + Number(that.getAttribute('data-increment')); 160 | 161 | intervalInput(target, intervalName); 162 | }; 163 | } 164 | 165 | return { 166 | init: init 167 | }; 168 | })(); 169 | -------------------------------------------------------------------------------- /app/js/timer.js: -------------------------------------------------------------------------------- 1 | TT.Timer = (function () { 2 | 'use strict'; 3 | 4 | var intervalIndex; 5 | var timerInterval; 6 | 7 | var intervals = []; 8 | 9 | var timer; 10 | // how often are we running precise timer to check if second of real time elapsed in ms 11 | var timerPrecision = 100; 12 | 13 | function init() { 14 | // initialize intervals array 15 | updateIntervals(); 16 | 17 | // load saved progress 18 | intervalIndex = TT.Services.Storage.get('intervalIndex') || 0; 19 | timerInterval = TT.Services.Storage.get('timerInterval') || TT.Config.get('workInterval'); 20 | 21 | // when user changes number of intervals in settings 22 | if (intervalIndex > intervals.length - 1) { 23 | if (intervalIndex % 2 === 0) { 24 | intervalIndex = intervals.length - 2; 25 | } else { 26 | intervalIndex = intervals.length - 1; 27 | } 28 | } 29 | 30 | // initialize progress images 31 | for (var i = 0; i < TT.Config.get('repeat'); i++) { 32 | TT.Views.Progress.createImage('unfinished'); 33 | } 34 | 35 | for (var index = 0; index <= intervalIndex; index++) { 36 | updateTimerViews(index, true); 37 | } 38 | 39 | if (intervalIndex === 0 && timerInterval < TT.Config.get('workInterval')) { 40 | TT.Views.Progress.setImageType('work', 0); 41 | TT.Views.Progress.setDescription('work'); 42 | } 43 | 44 | if (intervalIndex === 0 && timerInterval === TT.Config.get('workInterval')) { 45 | TT.Services.Title.resetTitle(); 46 | } else { 47 | TT.Services.Title.setTitle(secondsToTime(timerInterval)); 48 | } 49 | 50 | var firstVisit = !TT.Services.Storage.get('recurringVisit'); 51 | if (firstVisit) { 52 | TT.Views.Sidebar.openSidebar(); 53 | TT.Services.Storage.set('recurringVisit', true); 54 | } 55 | 56 | // binding 57 | TT.Views.Controls.getStartButton().addEventListener('click', startTimer); 58 | TT.Views.Controls.getSkipButton().addEventListener('click', skipInterval); 59 | TT.Views.Controls.getResetButton().addEventListener('click', resetTimer); 60 | } 61 | 62 | function updateIntervals() { 63 | intervals = []; 64 | 65 | for (var i = 0; i < TT.Config.get('repeat'); i++) { 66 | intervals.push(TT.Config.get('workInterval')); 67 | intervals.push(TT.Config.get('breakInterval')); 68 | } 69 | 70 | // replace last break with long break 71 | intervals.pop(); 72 | intervals.push(TT.Config.get('longbreakInterval')); 73 | } 74 | 75 | function secondsToTime(seconds) { 76 | function addLeadingZero(number) { 77 | if (number < 10) { 78 | number = '0' + number; 79 | } 80 | return number; 81 | } 82 | 83 | var minutes = Math.floor(seconds / 60); 84 | seconds %= 60; 85 | 86 | return addLeadingZero(minutes) + ':' + addLeadingZero(seconds); 87 | } 88 | 89 | function timerTick() { 90 | timerInterval--; 91 | 92 | if (timerInterval <= 0) { 93 | nextInterval(); 94 | } 95 | 96 | var time = secondsToTime(timerInterval); 97 | 98 | TT.Views.Timer.setTime(time); 99 | 100 | TT.Services.Title.setTitle(time, timer); 101 | TT.Services.Storage.set('timerInterval', timerInterval); 102 | } 103 | 104 | function skipInterval() { 105 | nextInterval(true); 106 | } 107 | 108 | function nextInterval(skipped) { 109 | skipped = skipped || false; 110 | 111 | intervalIndex++; 112 | if (intervalIndex > intervals.length - 1) { 113 | intervalIndex = 0; 114 | resetTimer(); 115 | } 116 | 117 | timerInterval = intervals[intervalIndex]; 118 | 119 | updateTimerViews(intervalIndex, skipped); 120 | 121 | TT.Services.Storage.set('intervalIndex', intervalIndex); 122 | 123 | if (skipped) { 124 | if (timer) { 125 | // resets timeout countdown 126 | pauseTimer(); 127 | runTimer(); 128 | } 129 | 130 | TT.Services.Title.setTitle(secondsToTime(timerInterval), timer); 131 | TT.Services.Storage.set('timerInterval', timerInterval); 132 | } 133 | 134 | if (TT.Config.get('timerAutoPause')) { 135 | pauseTimer(); 136 | // TODO: refactor this into pauseTimer() 137 | TT.Views.Controls.resetStartButton(); 138 | TT.Services.Title.setTitle(secondsToTime(timerInterval), timer); 139 | } 140 | } 141 | 142 | function updateTimerViews(index, skipped) { 143 | var imageIndex = Math.floor(index / 2); 144 | 145 | TT.Views.Timer.setTime(secondsToTime(timerInterval)); 146 | 147 | // intervals[ work, break, work, break, ... , long break ] 148 | if (index === intervals.length - 1) { 149 | // last interval 150 | 151 | TT.Services.Favicon.setFavicon('longbreak'); 152 | if (!skipped && TT.Config.get('notifications')) { 153 | TT.Services.Notification.newNotification(TT.Config.get('longbreakInterval') / 60 + ' minute long break', 'longbreak'); 154 | } 155 | 156 | TT.Views.Progress.setDescription('long break'); 157 | TT.Views.Progress.setImageType('longbreak', imageIndex); 158 | } else if (index === 0) { 159 | // first interval: 0 160 | TT.Services.Favicon.setFavicon('work'); 161 | if (!skipped && TT.Config.get('notifications')) { 162 | // TODO: think of better notification text 163 | TT.Services.Notification.newNotification('Done', 'work'); 164 | } 165 | } else if (index % 2 === 1) { 166 | // odd interval: 1, 3, 5.. 167 | TT.Services.Favicon.setFavicon('break'); 168 | if (!skipped && TT.Config.get('notifications')) { 169 | TT.Services.Notification.newNotification(TT.Config.get('breakInterval') / 60 + ' minute break', 'break'); 170 | } 171 | 172 | TT.Views.Progress.setDescription('break'); 173 | TT.Views.Progress.setImageType('break', imageIndex); 174 | } else if (index % 2 === 0) { 175 | // even interval: 2, 4, 6.. 176 | TT.Services.Favicon.setFavicon('work'); 177 | if (!skipped && TT.Config.get('notifications')) { 178 | TT.Services.Notification.newNotification(TT.Config.get('workInterval') / 60 + ' minute work', 'work'); 179 | } 180 | 181 | TT.Views.Progress.setDescription('work'); 182 | TT.Views.Progress.setImageType('work', imageIndex); 183 | TT.Views.Progress.setImageType('finished', imageIndex - 1); 184 | } 185 | 186 | if (!skipped) { 187 | if (TT.Config.get('audio')) { 188 | TT.Services.Audio.play(); 189 | } 190 | if (TT.Config.get('taskbarFlash')) { 191 | TT.Services.TaskbarFlash.flash(); 192 | } 193 | } 194 | } 195 | 196 | function startTimer() { 197 | if (!timer) { 198 | runTimer(); 199 | } else { 200 | pauseTimer(); 201 | } 202 | 203 | TT.Services.Title.setTitle(secondsToTime(timerInterval), timer); 204 | TT.Views.Controls.toogleStartButtonCaption(); 205 | 206 | if (intervalIndex === 0) { 207 | TT.Views.Progress.setImageType('work', 0); 208 | TT.Views.Progress.setDescription('work'); 209 | } 210 | } 211 | 212 | function runTimer() { 213 | var elapsedTime = 0; 214 | var before = new Date(); 215 | 216 | timer = setInterval(function () { 217 | elapsedTime += new Date().getTime() - before.getTime(); 218 | 219 | if (elapsedTime >= 1000) { 220 | timerTick(); 221 | elapsedTime -= 1000; 222 | } 223 | 224 | before = new Date(); 225 | }, timerPrecision); 226 | } 227 | 228 | function pauseTimer() { 229 | timer = clearInterval(timer); 230 | } 231 | 232 | function resetTimer() { 233 | pauseTimer(); 234 | 235 | intervalIndex = 0; 236 | timerInterval = TT.Config.get('workInterval'); 237 | 238 | var time = secondsToTime(timerInterval); 239 | 240 | TT.Views.Timer.setTime(time); 241 | TT.Views.Controls.resetStartButton(); 242 | 243 | TT.Services.Title.resetTitle(); 244 | TT.Services.Favicon.setFavicon('work'); 245 | TT.Services.Storage.set('intervalIndex', intervalIndex); 246 | TT.Services.Storage.set('timerInterval', timerInterval); 247 | 248 | TT.Views.Progress.resetProgress(); 249 | } 250 | 251 | return { 252 | init: init, 253 | updateIntervals: updateIntervals, 254 | startTimer: startTimer, 255 | pauseTimer: pauseTimer, 256 | skipInterval: skipInterval, 257 | resetTimer: resetTimer 258 | }; 259 | })(); 260 | -------------------------------------------------------------------------------- /app/fonts/fontello.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Copyright (C) 2015 by original authors @ fontello.com 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 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Tomatotim 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 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 |
76 |
77 |
78 |
79 | 80 |
81 |
82 | 83 |
84 |
85 | 86 |
87 | 91 | 95 | 99 |
100 |
101 |
102 | 103 | 104 | 109 | 110 | 111 | 113 | 114 | 115 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 384 | 385 | 386 | 389 | 390 | 391 |
392 | 401 | 402 | 403 | 410 | 411 | 412 | 413 | --------------------------------------------------------------------------------