├── .github └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .nvmrc ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── app ├── jest.config.js ├── package.json ├── public │ ├── favicon.ico │ └── index.html ├── src │ ├── components │ │ ├── AddButton │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ │ ├── AppDrag │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ │ ├── AppMenu │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ │ ├── BackButton │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ │ ├── Button │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ │ ├── ExternalLink │ │ │ └── index.tsx │ │ ├── Input │ │ │ ├── Textarea.tsx │ │ │ ├── index.tsx │ │ │ ├── styles.module.css │ │ │ └── utils.tsx │ │ ├── Section │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ │ ├── Sortable │ │ │ ├── SortableHOC │ │ │ │ ├── AutoScroller.ts │ │ │ │ ├── Container.tsx │ │ │ │ ├── Element.tsx │ │ │ │ ├── Handle.tsx │ │ │ │ ├── Manager.ts │ │ │ │ ├── README.md │ │ │ │ ├── Sorter.ts │ │ │ │ ├── context.ts │ │ │ │ ├── index.ts │ │ │ │ ├── reorderArray.ts │ │ │ │ ├── types.ts │ │ │ │ └── utils.ts │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ │ ├── Stack │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ │ ├── Table │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ │ └── Title │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ ├── hooks │ │ ├── useDispatch.tsx │ │ ├── useDragAndDropFiles.tsx │ │ ├── useEventListener.tsx │ │ ├── useSelector.tsx │ │ ├── useShortcuts.tsx │ │ └── useTheme.tsx │ ├── icons │ │ ├── close.svg │ │ ├── drag.svg │ │ ├── logo.png │ │ └── preferences.svg │ ├── index.css │ ├── index.tsx │ ├── modules │ │ ├── actions.ts │ │ ├── index.tsx │ │ ├── preferences │ │ │ └── index.ts │ │ ├── selectedScreen │ │ │ └── index.tsx │ │ ├── selectors.ts │ │ ├── task │ │ │ ├── index.tsx │ │ │ ├── types.tsx │ │ │ └── utils.tsx │ │ └── tasks │ │ │ └── index.ts │ ├── react-app-env.d.ts │ ├── screens │ │ ├── about │ │ │ ├── AutoUpdateStatus.tsx │ │ │ ├── TextButton.tsx │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ │ ├── changelog │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ │ ├── index.tsx │ │ ├── preferences │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ │ ├── screens.tsx │ │ ├── shortcuts │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ │ └── task │ │ │ ├── Bookmarks │ │ │ ├── OpenLink │ │ │ │ ├── index.tsx │ │ │ │ └── styles.module.css │ │ │ └── index.tsx │ │ │ ├── DragFileMessage │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ │ │ ├── Note │ │ │ └── index.tsx │ │ │ ├── Title │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ │ │ ├── Todos │ │ │ ├── Checkbox │ │ │ │ ├── index.tsx │ │ │ │ └── styles.module.css │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ │ │ └── index.tsx │ ├── setupTests.tsx │ ├── types │ │ ├── dom.d.ts │ │ └── raw.micro.d.ts │ └── utils │ │ ├── bookmarks.test.ts │ │ ├── bookmarks.ts │ │ ├── electron │ │ ├── index.tsx │ │ └── shim.tsx │ │ ├── focusOn.tsx │ │ ├── generateId.ts │ │ ├── isURI.test.ts │ │ ├── isURI.tsx │ │ ├── keyCodes.tsx │ │ ├── stateRestore.tsx │ │ └── storage.tsx ├── tests │ ├── setup.js │ └── styleMock.js ├── tsconfig.json └── yarn.lock ├── assets ├── Icon.icns ├── icon.png └── screenshot.png ├── bin ├── bootstrap ├── electron-package.js ├── electron-start ├── react-build ├── react-start └── test ├── credentials.json.example ├── entitlements.plist ├── package.json ├── releases.json ├── shell ├── assets │ ├── MenuBarIconTemplate.png │ └── MenuBarIconTemplate@2x.png ├── main.js ├── package.json ├── utils │ ├── autoupdate.js │ ├── settings.js │ └── switchTask.js └── yarn.lock ├── tsconfig.json ├── updater ├── Procfile ├── README.md ├── docker-compose.yml ├── package.json ├── src │ ├── config.ts │ ├── db │ │ ├── index.ts │ │ └── migrate.ts │ ├── index.ts │ └── releases.ts ├── tsconfig.json └── yarn.lock └── yarn.lock /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Motivation 2 | 3 | `[Why was this implemented]` 4 | 5 | ## Solution 6 | 7 | `[How was this implemented]` 8 | 9 | - [ ] `[Sub task]` 10 | 11 | ### Screenshots 12 | 13 | `[TODO: Image on mobile and desktop]` 14 | 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # misc 10 | .DS_Store 11 | .env.local 12 | .env.development.local 13 | .env.test.local 14 | .env.production.local 15 | 16 | yarn-debug.log* 17 | yarn-error.log* 18 | 19 | /docs 20 | /shell/build 21 | /dist 22 | credentials.json 23 | 24 | app/.eslintcache -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 10.20.0 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | 4 | ## Version 0.5.0 5 | 6 | * __bug fix__ Fix multiline paste on Todo and Bookmark (@rstankov) 7 | * __feature__ Dark / Light mode with preferences page (@tjhdev) 8 | * __feature__ Go back - ESC shortcut key when in sub menus (@tjhdev) 9 | * __feature__ Shortcut page shortcut (cmd + h) (@tjhdev) 10 | * __feature__ Focus task title (cmd + e) (@tjhdev) 11 | * __bug fix__ Fix crash when dragging empty todo in final position (@tjhdev) 12 | * __bug fix__ Pressing ESC in notes removes focus on textarea (@tjhdev) 13 | * __bug fix__ Pressing ESC in task title removes focus (@rstankov) 14 | 15 | ## Version 0.4.1 16 | 17 | * __bug fix__ Fix file paths are bookmarks (@rstankov) 18 | 19 | ## Version 0.4.0 20 | 21 | * __feature__ Shortcut Cmd + ` to switch between tasks (@rstankov) 22 | 23 | ## Version 0.3.1 24 | 25 | * __bug fix__ Support multiple spaces (@rstankov) 26 | 27 | ## Version 0.3.0 28 | 29 | * __feature__ Allow titles for links (@rstankov) 30 | * __bug fix__ Show drag handle when you hover over it (@rstankov) 31 | * __feature__ Shorten longer links (@rstankov) 32 | * __feature__ Support x-callback urls like bear://x-callback-url/note/123 (@rstankov) 33 | 34 | ## Version 0.2.1 35 | 36 | * __feature__ Initial markdown support - bold text (@rstankov) 37 | * __bug fix__ Show numbers for all bookmarks, even when number is better than 10 (@rstankov) 38 | 39 | ## Version 0.2.0 40 | 41 | * __bug fix__ ESC on input doesn't close the app (@rstankov) 42 | * __bug fix__ UI fixes (@rstankov) 43 | * __feature__ Ask for task name when creating it (@rstankov) 44 | 45 | ## Version 0.1.3 46 | 47 | * __feature__ Polish UI (@rstankov, @vestimir) 48 | * __feature__ Enable tab to focus todos, bookmarks and notes (@rstankov) 49 | * __bug fix__ Fix undo/redo (@rstankov) 50 | 51 | ## Version 0.1.2 52 | 53 | * __feature__ Auto update via About screen (@rstankov) 54 | * __feature__ Click on note header focuses on note (@rstankov) 55 | 56 | ## Version 0.1.1 57 | 58 | * __feature__ Implement autoUpdate (@rstankov) 59 | 60 | ## Version 0.1.0 61 | 62 | * __feature__ MacOS code sign and notarize (@rstankov) 63 | * __feature__ Added about screen (@rstankov) 64 | * __feature__ Shortcut Cmd + Shift + t to focus on first incomplete task (@rstankov) 65 | * __feature__ Shortcut Cmd + backspace to remove todos and bookmarks (@rstankov) 66 | * __feature__ Shortcut Cmd + click on todo to toggle it (@rstankov) 67 | * __feature__ Allow multiple tasks (@rstankov) 68 | * __bug fix__ Don't allow broke state when loading a task (@rstankov) 69 | * __feature__ Update app icon (@rstankov) 70 | * __feature__ Drag & Drop file to bookmark (@rstankov) 71 | * __feature__ Support for Dark Appearance (@rstankov) 72 | * __feature__ Support for undo/redo (@rstankov) 73 | * __bug fix__ Allow localhost to be used for bookmarks (@rstankov) 74 | * __feature__ Drag & drop sorting of todos and bookmarks (@rstankov) 75 | * __feature__ Allow global shortcut to be configured (@rstankov) 76 | * __feature__ Remember window bounds (@rstankov) 77 | * __feature__ Show whole todo text even when longer than one line (@rstankov) 78 | * __change__ Replace Toggle todo from Cmd + c to Cmd + Shift + c (@rstankov) 79 | * __feature__ Changelog screen (@rstankov) 80 | * __feature__ Create new tasks via app menu (@rstankov) 81 | * __feature__ Shortcut Cmd + click on bookmarks to opens them (@rstankov) 82 | 83 | ## Version 0.0.0 84 | 85 | * Initial release (@rstankov) 86 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Radoslav Stankov 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Focused Task 2 | 3 | Focused Task at Product Hunt 4 | 5 | MacOS menu bar app that helps you focus on a single task. 6 | By splitting it into smaller todos and keeping all related information about it in one place. 7 | The app is designed to get out of your way. Everything can be done via a shortcut. 8 | 9 | It is built with Electron and uses React and Redux. 10 | 11 | 12 | 13 | 🍿 See it in action 🎥 14 | 15 | 16 | ### Features 17 | 18 | * Focus on a single task - *multitasking is dangerous* 🎯 19 | * Shortcut for everything - *you don't need mouse* 🛑 🖱 20 | * Menu-bar app - *it gets out of your way* 😇 21 | * Organize your tasks with - todos, bookmark links, and free-form text 📋 22 | * Drag & Drop file as bookmarks 🔖 23 | * Open-sourced 💻 24 | 25 | ### Download 26 | 27 | 👉 Download latest version 👈 28 | 29 | ## Development 30 | 31 | **Installation** 32 | 33 | Have `yarn` installed 34 | 35 | ``` 36 | ./bin/bootstrap 37 | ``` 38 | 39 | **Running** 40 | 41 | ``` 42 | yarn dev 43 | ``` 44 | -------------------------------------------------------------------------------- /app/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { '^.+\\.tsx?$': 'ts-jest' }, 3 | moduleNameMapper: { 4 | '\\.css$': '/tests/styleMock.js', 5 | '^(utils|modules)/(.*)': '/src/$1/$2', 6 | }, 7 | setupFiles: ['/tests/setup.js'], 8 | }; 9 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "license": "MIT", 3 | "homepage": ".", 4 | "dependencies": { 5 | "@reduxjs/toolkit": "1.5.1", 6 | "autosize": "^4.0.2", 7 | "classnames": "^2.2.6", 8 | "electron-is-accelerator": "^0.2.0", 9 | "lodash": "4.17.21", 10 | "raw.macro": "0.4.2", 11 | "react": "17.0.2", 12 | "react-dom": "17.0.2", 13 | "react-markdown": "5.0.3", 14 | "react-redux": "7.2.3", 15 | "react-scripts": "3.4.4", 16 | "utility-types": "^3.10.0" 17 | }, 18 | "devDependencies": { 19 | "@testing-library/jest-dom": "5.11.10", 20 | "@testing-library/react": "11.2.5", 21 | "@testing-library/user-event": "13.0.16", 22 | "@types/autosize": "^3.0.7", 23 | "@types/classnames": "2.2.11", 24 | "@types/lodash": "4.14.168", 25 | "@types/react": "17.0.3", 26 | "@types/react-dom": "17.0.3", 27 | "@types/react-redux": "7.1.16", 28 | "ts-jest": "26.5.4", 29 | "typescript": "4.2.3" 30 | }, 31 | "scripts": { 32 | "start": "react-scripts start", 33 | "build": "react-scripts build", 34 | "test": "react-scripts test", 35 | "eject": "react-scripts eject", 36 | "lint:types": "tsc --noEmit" 37 | }, 38 | "eslintConfig": { 39 | "extends": "react-app" 40 | }, 41 | "browserslist": { 42 | "production": [ 43 | ">0.2%", 44 | "not dead", 45 | "not op_mini all" 46 | ], 47 | "development": [ 48 | "last 1 chrome version", 49 | "last 1 firefox version", 50 | "last 1 safari version" 51 | ] 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RStankov/FocusedTask/2532371d6b6a3d097cd8e23588c7786390b4e268/app/public/favicon.ico -------------------------------------------------------------------------------- /app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Focused Task 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/components/AddButton/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import styles from './styles.module.css'; 3 | 4 | interface IProps { 5 | onClick: () => void; 6 | subject: string; 7 | } 8 | 9 | export default function AddButton({ onClick, subject }: IProps) { 10 | return ( 11 |
12 | Click to add {subject} 13 |
14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /app/src/components/AddButton/styles.module.css: -------------------------------------------------------------------------------- 1 | .button { 2 | user-select: none; 3 | cursor: pointer; 4 | color: var(--silent); 5 | } 6 | 7 | .button:hover { 8 | color: var(--hover); 9 | } 10 | -------------------------------------------------------------------------------- /app/src/components/AppDrag/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import styles from './styles.module.css'; 3 | 4 | export default function AppDrag() { 5 | return
; 6 | } 7 | -------------------------------------------------------------------------------- /app/src/components/AppDrag/styles.module.css: -------------------------------------------------------------------------------- 1 | .drag { 2 | -webkit-app-region: drag; 3 | height: var(--box-size); 4 | position: absolute; 5 | top: 0; 6 | left: 0; 7 | right: 0; 8 | } 9 | -------------------------------------------------------------------------------- /app/src/components/AppMenu/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import store from 'modules'; 3 | import { ReactComponent as PreferencesIcon } from 'icons/preferences.svg'; 4 | import styles from './styles.module.css'; 5 | import { removeCompletedTodos } from 'modules/task'; 6 | import { newTask, deleteTask } from 'modules/actions'; 7 | import { getSelectedTask, getAllTasks } from 'modules/selectors'; 8 | import { selectTask, importTask } from 'modules/actions'; 9 | 10 | import { 11 | openShortcuts, 12 | openChangelog, 13 | openPreferences, 14 | openAbout, 15 | } from 'modules/selectedScreen'; 16 | 17 | import { 18 | isElectron, 19 | openMenu, 20 | closeApp, 21 | readTaskFromFile, 22 | writeTaskToFile, 23 | confirm, 24 | IMenuItem, 25 | } from 'utils/electron'; 26 | 27 | export default function AppClose() { 28 | if (!isElectron) { 29 | return null; 30 | } 31 | 32 | return ( 33 | 34 | 35 | 36 | ); 37 | } 38 | 39 | function openAppMenu() { 40 | const allTask = getAllTasks(store.getState()); 41 | const selectedTask = getSelectedTask(store.getState()); 42 | 43 | openMenu([ 44 | { 45 | label: 'New', 46 | click: () => store.dispatch(newTask()), 47 | }, 48 | { 49 | label: 'Open', 50 | submenu: (allTask.map(task => ({ 51 | label: task.title, 52 | type: 'checkbox', 53 | checked: task.id === selectedTask.id, 54 | click: () => store.dispatch(selectTask(task)), 55 | })) as IMenuItem[]).concat([ 56 | { 57 | type: 'separator', 58 | }, 59 | { 60 | label: 'New', 61 | click: () => store.dispatch(newTask()), 62 | }, 63 | ]), 64 | }, 65 | { 66 | label: 'Delete', 67 | click: () => 68 | confirm({ 69 | message: 'Are you sure?', 70 | detail: 'Your current task data will be erased.', 71 | fn: () => store.dispatch(deleteTask(selectedTask)), 72 | }), 73 | }, 74 | { 75 | label: 'Save As...', 76 | click: () => writeTaskToFile(store.getState()), 77 | }, 78 | { 79 | label: 'Import...', 80 | click: async () => { 81 | const task = await readTaskFromFile(); 82 | if (task) { 83 | store.dispatch(importTask(task)); 84 | } 85 | }, 86 | }, 87 | { 88 | type: 'separator', 89 | }, 90 | { 91 | label: 'Clear Completed Todos', 92 | click: () => store.dispatch(removeCompletedTodos()), 93 | }, 94 | { 95 | type: 'separator', 96 | }, 97 | { 98 | label: 'About', 99 | click: () => store.dispatch(openAbout()), 100 | }, 101 | { 102 | label: 'Changelog', 103 | click: () => store.dispatch(openChangelog()), 104 | }, 105 | { 106 | label: 'Preferences', 107 | click: () => store.dispatch(openPreferences()), 108 | }, 109 | { 110 | label: 'Shortcuts', 111 | click: () => store.dispatch(openShortcuts()), 112 | }, 113 | { 114 | type: 'separator', 115 | }, 116 | { 117 | label: 'Quit', 118 | click: closeApp, 119 | }, 120 | ]); 121 | } 122 | -------------------------------------------------------------------------------- /app/src/components/AppMenu/styles.module.css: -------------------------------------------------------------------------------- 1 | .button { 2 | cursor: pointer; 3 | user-select: none; 4 | position: absolute; 5 | top: 10px; 6 | right: 10px; 7 | } 8 | 9 | .button svg { 10 | width: var(--box-size); 11 | height: var(--box-size); 12 | } 13 | 14 | .button path { 15 | stroke: var(--line); 16 | } 17 | 18 | .button:hover path { 19 | stroke: var(--hover); 20 | } 21 | -------------------------------------------------------------------------------- /app/src/components/BackButton/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import useDispatch from 'hooks/useDispatch'; 3 | import styles from './styles.module.css'; 4 | import { ReactComponent as CloseIcon } from 'icons/close.svg'; 5 | import { openTask } from 'modules/selectedScreen'; 6 | 7 | export default function BackButton() { 8 | const dispatch = useDispatch(); 9 | 10 | return ( 11 | dispatch(openTask())} /> 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /app/src/components/BackButton/styles.module.css: -------------------------------------------------------------------------------- 1 | .button { 2 | cursor: pointer; 3 | user-select: none; 4 | position: absolute; 5 | top: var(--l); 6 | right: var(--l); 7 | width: var(--box-size); 8 | height: var(--box-size); 9 | background: var(--line); 10 | border-radius: 50%; 11 | } 12 | 13 | .button path { 14 | fill: var(--background); 15 | } 16 | 17 | .button:hover { 18 | background: var(--hover); 19 | } 20 | -------------------------------------------------------------------------------- /app/src/components/Button/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import styles from './styles.module.css'; 3 | 4 | interface IProps { 5 | onClick: () => void; 6 | title?: string; 7 | children: React.ReactNode; 8 | } 9 | 10 | export default function Button(props: IProps) { 11 | return