├── .gitignore ├── LICENSE ├── README.md ├── app ├── components │ ├── Md.js │ └── removeCommonIndent.js ├── data │ ├── actions │ │ ├── board.js │ │ └── index.js │ ├── middleware │ │ ├── boardNavigation.js │ │ ├── index.js │ │ └── persist.js │ ├── reducers │ │ ├── dummyReducer.js │ │ ├── index.js │ │ └── tdo │ │ │ ├── boards │ │ │ └── index.js │ │ │ ├── cleanup.js │ │ │ ├── getDefaultData.js │ │ │ ├── index.js │ │ │ ├── lists │ │ │ └── index.js │ │ │ ├── migrate.js │ │ │ ├── tasks │ │ │ └── index.js │ │ │ └── tutorial.js │ └── services │ │ ├── GHAuth.js │ │ └── Gists.js ├── extension.js ├── index.html ├── index.js ├── index.scss ├── routes │ ├── Controller.js │ ├── default │ │ ├── BoardEditor.js │ │ ├── Controller.js │ │ ├── ListEditor.js │ │ ├── Task.js │ │ ├── index.js │ │ ├── index.scss │ │ └── styling.js │ ├── help │ │ ├── index.js │ │ └── index.scss │ ├── index.js │ ├── index.scss │ └── settings │ │ ├── Controller.js │ │ ├── index.js │ │ └── index.scss └── themes │ ├── dark │ └── index.scss │ ├── extension.scss │ └── index.scss ├── assets ├── favicon.png └── screenshot.png ├── circle.yml ├── config ├── babel.config.js ├── deploy-ghpages.sh ├── webpack.config.js ├── webpack.dev.js ├── webpack.extension.js └── webpack.prod.js ├── extension ├── assets │ ├── icon_128.png │ ├── icon_16.png │ └── icon_48.png ├── background.js └── manifest_chrome.json ├── package.json ├── test └── README.md └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | obj 3 | dist 4 | dist_extension 5 | node_modules 6 | .vs 7 | .idea 8 | *.user 9 | *.db 10 | *.pem 11 | packages -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Marko Stijak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tdo 2 | 3 | > This project is not maintained anymore. The new version of tdo based on Firebase is available at https://github.com/codaxy/tdo. 4 | 5 | A flexible TODO list with the following features: 6 | 7 | - Markdown support 8 | - Keyboard navigation 9 | - Use of hashtags for categories, priorities, etc 10 | - Auto remove completed items after period of time 11 | - CSS hackability: write functions to assign CSS styles andclasses to tasks and lists 12 | - Gist storage 13 | - Ability to work with a large number of tasks 14 | 15 |  16 | 17 | [Open](https://mstijak.github.io/tdo/) 18 | 19 | Also available as a [Chrome extension](https://chrome.google.com/webstore/detail/tdo/aaaabkbhklmpmlnjnbicdahijpkgnkfk). 20 | 21 | ## Install & Run 22 | 23 | Install: 24 | ``` 25 | yarn 26 | ``` 27 | Run: 28 | ``` 29 | yarn start 30 | ``` 31 | 32 | If you don't use yarn yet, npm will do too. 33 | 34 | ### Thanks 35 | 36 | Thanks to CircleCI the app is auto deployed to GH Pages. 37 | 38 | Thanks to https://github.com/Villanuevand/deployment-circleci-gh-pages for 39 | providing instructions for setting up CircleCI. 40 | 41 | -------------------------------------------------------------------------------- /app/components/Md.js: -------------------------------------------------------------------------------- 1 | import { HtmlElement } from 'cx/widgets'; 2 | import marked from 'marked'; 3 | import {removeCommonIndent} from './removeCommonIndent'; 4 | 5 | 6 | export class Md extends HtmlElement { 7 | 8 | declareData() { 9 | super.declareData(...arguments, { 10 | source: undefined 11 | }) 12 | } 13 | 14 | prepareData(context, instance) { 15 | var {data} = instance; 16 | 17 | if (data.source || this.content) { 18 | data.source = removeCommonIndent(data.source || this.content); 19 | data.innerHtml = marked(data.source); 20 | } 21 | 22 | super.prepareData(context, instance) 23 | } 24 | 25 | isValidHtmlAttribute(attrName) { 26 | if (attrName == 'source') 27 | return false; 28 | return super.isValidHtmlAttribute(attrName); 29 | } 30 | 31 | add(x) { 32 | 33 | if (Array.isArray(x)) 34 | x.forEach(t=>this.add(t)); 35 | 36 | if (typeof x != 'string') 37 | return; 38 | 39 | this.content += x; 40 | } 41 | } 42 | 43 | Md.prototype.trimWhitespace = false; 44 | Md.prototype.content = ''; 45 | 46 | -------------------------------------------------------------------------------- /app/components/removeCommonIndent.js: -------------------------------------------------------------------------------- 1 | export function removeCommonIndent(text) { 2 | var lines = text.split('\n'); 3 | var indent = 100000; 4 | var firstNonEmpty = 100000, lastNonEmpty; 5 | lines.forEach((l, index)=> { 6 | for (var i = 0; i < l.length; i++) { 7 | if (l[i] != ' ') { 8 | if (i < indent) 9 | indent = i; 10 | lastNonEmpty = index; 11 | if (index < firstNonEmpty) 12 | firstNonEmpty = index; 13 | break; 14 | } 15 | } 16 | }); 17 | 18 | if (indent == 0) 19 | return text; 20 | 21 | return lines.filter((v, i)=> (i >= firstNonEmpty && i <= lastNonEmpty)) 22 | .map(l=>(l.length > indent ? l.substring(indent) : '')) 23 | .join('\n'); 24 | } 25 | -------------------------------------------------------------------------------- /app/data/actions/board.js: -------------------------------------------------------------------------------- 1 | import uid from 'uid'; 2 | 3 | export const ADD_BOARD = 'ADD_BOARD'; 4 | export const REMOVE_BOARD = 'REMOVE_BOARD'; 5 | 6 | export const GOTO_BOARD = 'GOTO_BOARD'; 7 | export const GOTO_ANY_BOARD = 'SELECT_FIRST_BOARD'; 8 | 9 | export const gotoBoard = id => ({ 10 | type: GOTO_BOARD, 11 | id: id 12 | }); 13 | 14 | export const gotoAnyBoard = (forced) => ({ 15 | type: GOTO_ANY_BOARD, 16 | forced: forced 17 | }); 18 | 19 | export const addBoard = (board = {}) => dispatch => { 20 | if (!board) 21 | board.id = uid(); 22 | 23 | board.createdDate = new Date().toISOString(); 24 | 25 | dispatch({ 26 | type: ADD_BOARD, 27 | data: board 28 | }); 29 | }; 30 | 31 | export const removeBoard = id => ({ 32 | type: REMOVE_BOARD, 33 | id: id 34 | }); 35 | -------------------------------------------------------------------------------- /app/data/actions/index.js: -------------------------------------------------------------------------------- 1 | export const REQUEST_TDO = 'REQUEST_TDO'; 2 | export const RECEIVE_TDO = 'RECEIVE_TDO'; 3 | export const LOAD_DEFAULT_TDO = 'LOAD_DEFAULT_TDO'; 4 | 5 | export const loadDefault = () => ({ 6 | type: LOAD_DEFAULT_TDO 7 | }); 8 | 9 | import { Gists } from '../services/Gists'; 10 | import { GHAuth } from '../services/GHAuth'; 11 | 12 | import { gotoAnyBoard } from './board'; 13 | 14 | export const loadData = () => dispatch => { 15 | var storage = GHAuth.get(); 16 | if (storage && storage.token && storage.gistId) { 17 | dispatch({ 18 | type: REQUEST_TDO 19 | }); 20 | let gists = new Gists(storage); 21 | return gists.load() 22 | .then(x=> { 23 | dispatch({ 24 | type: RECEIVE_TDO, 25 | data: x 26 | }); 27 | dispatch(gotoAnyBoard()) 28 | }); 29 | } 30 | else { 31 | let data = localStorage.tdo && JSON.parse(localStorage.tdo); 32 | if (data) { 33 | dispatch({ 34 | type: RECEIVE_TDO, 35 | data: data 36 | }); 37 | } 38 | else { 39 | dispatch(loadDefault()); 40 | } 41 | dispatch(gotoAnyBoard()); 42 | } 43 | }; 44 | 45 | export * from './board'; -------------------------------------------------------------------------------- /app/data/middleware/boardNavigation.js: -------------------------------------------------------------------------------- 1 | import { GOTO_BOARD, GOTO_ANY_BOARD } from '../actions'; 2 | 3 | export default store => next => action => { 4 | switch (action.type) { 5 | 6 | case GOTO_BOARD: 7 | window.location.hash = '#' + action.id; 8 | return; 9 | 10 | case GOTO_ANY_BOARD: 11 | if (action.forced || (window.location.hash || '#') == '#') { 12 | var {tdo} = store.getState(); 13 | if (tdo.boards.length > 0) 14 | window.location.hash = '#' + tdo.boards[0].id; 15 | } 16 | return; 17 | 18 | default: 19 | return next(action); 20 | } 21 | }; -------------------------------------------------------------------------------- /app/data/middleware/index.js: -------------------------------------------------------------------------------- 1 | import boardNav from './boardNavigation'; 2 | import persist from './persist'; 3 | import thunk from 'redux-thunk'; 4 | 5 | export default [boardNav, thunk, persist]; 6 | -------------------------------------------------------------------------------- /app/data/middleware/persist.js: -------------------------------------------------------------------------------- 1 | import {Gists} from '../services/Gists'; 2 | import {GHAuth} from '../services/GHAuth'; 3 | 4 | var saved, saveTaskId; 5 | 6 | function save(tdo) { 7 | if (tdo != saved && tdo.lists && tdo.lists.length > 0) { 8 | saved = tdo; 9 | var gh = GHAuth.get(); 10 | if (gh && gh.token && gh.gistId) { 11 | if (saveTaskId) 12 | clearTimeout(saveTaskId); 13 | saveTaskId = setTimeout(()=> { 14 | saveTaskId = null; 15 | let gists = new Gists(gh); 16 | gists.update(tdo); 17 | }, 5000); 18 | } 19 | else { 20 | localStorage.tdo = JSON.stringify(tdo); 21 | } 22 | } 23 | } 24 | 25 | export default store => next => action => { 26 | var result = next(action); 27 | var {tdo} = store.getState(); 28 | save(tdo); 29 | return result; 30 | }; 31 | -------------------------------------------------------------------------------- /app/data/reducers/dummyReducer.js: -------------------------------------------------------------------------------- 1 | export default defaultState => (state = defaultState, action) => state; 2 | -------------------------------------------------------------------------------- /app/data/reducers/index.js: -------------------------------------------------------------------------------- 1 | import {combineReducers} from 'redux'; 2 | import tdo from './tdo'; 3 | 4 | import dummy from './dummyReducer'; 5 | 6 | export default combineReducers({ 7 | tdo, 8 | hash: dummy('#'), 9 | search: dummy({}), 10 | pages: dummy({}), 11 | layout: dummy({}) 12 | }); 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/data/reducers/tdo/boards/index.js: -------------------------------------------------------------------------------- 1 | import { append, updateArray } from 'cx/data'; 2 | import { ADD_BOARD, REMOVE_BOARD } from '../../../actions'; 3 | 4 | export default function(state = [], action) { 5 | switch (action.type) { 6 | case 'ADD_BOARD': 7 | return append(state, action.data); 8 | 9 | case 'REMOVE_BOARD': 10 | return updateArray(state, 11 | b => ({ 12 | ...b, 13 | deleted: true, 14 | deletedDate: new Date().toISOString() 15 | }), 16 | b=>b.id == action.id); 17 | 18 | default: 19 | return state; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/data/reducers/tdo/cleanup.js: -------------------------------------------------------------------------------- 1 | import { dateDiff } from 'cx/util'; 2 | 3 | function defaultValue(value, def) { 4 | if (typeof value == "number") 5 | return value; 6 | return def; 7 | } 8 | 9 | export function cleanup(data) { 10 | 11 | var now = new Date().getTime(); 12 | var oneDay = 24 * 60 * 60 * 1000; //ms 13 | var retention = defaultValue(data.settings.purgeDeletedObjectsAfterDays, 7) * oneDay; 14 | var completedTaskRetention = defaultValue(data.settings.deleteCompletedTasksAfterDays, 7) * oneDay; 15 | 16 | var boardKeys = {}; 17 | for (var board of data.boards) { 18 | if (!board.deleted || (board.deletedDate && Date.parse(board.deletedDate) + retention > now)) 19 | boardKeys[board.id] = true; 20 | } 21 | data.boards = data.boards.filter(b=>boardKeys[b.id]); 22 | 23 | 24 | var listKeys = {}; 25 | for (var list of data.lists) { 26 | if (boardKeys[list.boardId] && !list.deleted || (list.deletedDate && Date.parse(list.deletedDate) + retention > now)) 27 | listKeys[list.id] = true; 28 | } 29 | data.lists = data.lists.filter(b=>listKeys[b.id]); 30 | 31 | var taskKeys = {}; 32 | for (var task of data.tasks) { 33 | if (listKeys[task.listId] && !task.deleted || (task.deletedDate && Date.parse(task.deletedDate) + retention < now)) 34 | if (!task.completed || !data.settings.deleteCompletedTasks || (task.completedDate && (Date.parse(task.completedDate) + completedTaskRetention > now))) 35 | taskKeys[task.id] = true; 36 | } 37 | data.tasks = data.tasks.filter(b=>taskKeys[b.id]); 38 | } 39 | -------------------------------------------------------------------------------- /app/data/reducers/tdo/getDefaultData.js: -------------------------------------------------------------------------------- 1 | import uid from "uid"; 2 | 3 | export function getDefaultData() { 4 | var board = { 5 | id: uid(), 6 | name: 'Tasks', 7 | createdDate: new Date().toISOString() 8 | }; 9 | 10 | var list = { 11 | id: uid(), 12 | boardId: board.id, 13 | name: 'List 1', 14 | createdDate: new Date().toISOString() 15 | }; 16 | 17 | return { 18 | boards: [board], 19 | lists: [list], 20 | tasks: [] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/data/reducers/tdo/index.js: -------------------------------------------------------------------------------- 1 | import { migrate } from './migrate'; 2 | import { getDefaultData } from './getDefaultData'; 3 | import { combineReducers } from 'redux'; 4 | 5 | import dummy from '../dummyReducer'; 6 | import boards from './boards'; 7 | import lists from './lists'; 8 | import tasks from './tasks'; 9 | 10 | import { 11 | REQUEST_TDO, RECEIVE_TDO, 12 | LOAD_DEFAULT_TDO 13 | } 14 | from '../../actions'; 15 | 16 | const tdo = combineReducers({ 17 | version: dummy(0), 18 | boards, 19 | lists, 20 | tasks, 21 | settings: dummy({}) 22 | }); 23 | 24 | export default function(state = {}, action) { 25 | switch (action.type) { 26 | case REQUEST_TDO: 27 | return { 28 | ...state, 29 | status: 'loading' 30 | }; 31 | 32 | case RECEIVE_TDO: 33 | migrate(action.data); 34 | return action.data; 35 | 36 | case LOAD_DEFAULT_TDO: 37 | var data = getDefaultData(); 38 | migrate(data); 39 | return data; 40 | 41 | default: 42 | return tdo(state, action); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/data/reducers/tdo/lists/index.js: -------------------------------------------------------------------------------- 1 | export default function(state = [], action) { 2 | switch (action.type) { 3 | 4 | 5 | default: 6 | return state; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /app/data/reducers/tdo/migrate.js: -------------------------------------------------------------------------------- 1 | import uid from 'uid'; 2 | import {cleanup} from './cleanup'; 3 | import {getTutorial} from './tutorial'; 4 | 5 | var migrations = []; 6 | 7 | //zero 8 | migrations.push(function (data) {}); 9 | 10 | //add boards 11 | migrations.push(function (data) { 12 | 13 | if (!Array.isArray(data.boards)) 14 | data.boards = []; 15 | 16 | if (!Array.isArray(data.lists)) 17 | data.lists = []; 18 | 19 | if (!Array.isArray(data.tasks)) 20 | data.tasks = []; 21 | 22 | if (data.boards.length == 0) { 23 | data.boards = [{ 24 | id: uid(), 25 | name: 'Tasks' 26 | }]; 27 | } 28 | 29 | data.lists.forEach(l=> { 30 | if (!l.boardId) 31 | l.boardId = data.boards[0].id; 32 | }); 33 | }); 34 | 35 | //settings 36 | migrations.push(function (data) { 37 | if (!data.settings) 38 | data.settings = { 39 | completedTasksRetentionDays: 1, 40 | deleteCompletedTasks: true, 41 | deleteCompletedTasksAfterDays: 7, 42 | purgeDeletedObjectsAfterDays: 3 43 | }; 44 | }); 45 | 46 | //created => createdDate 47 | migrations.push(function (data) { 48 | var {boards, lists, tasks} = data; 49 | 50 | for (var array of [boards, lists, tasks]) { 51 | for (var b of array) { 52 | if (b.created) { 53 | b.createdDate = b.created; 54 | delete b.created; 55 | } 56 | if (b.isDeleted) { 57 | b.deleted = b.isDeleted; 58 | delete b.isDeleted; 59 | } 60 | } 61 | } 62 | }); 63 | 64 | //taskStyles 65 | migrations.push(function (data) { 66 | var {settings} = data; 67 | 68 | if (!Array.isArray(settings.taskStyles)) 69 | settings.taskStyles = [{ 70 | regex: '!important', 71 | style: 'color: orange' 72 | }, { 73 | regex: '#idea', 74 | style: 'color: yellow' 75 | }]; 76 | }); 77 | 78 | //add tutorial 79 | migrations.push(function (data) { 80 | var tutorial = getTutorial(); 81 | data.boards = [...data.boards, ...tutorial.boards]; 82 | data.lists = [...data.lists, ...tutorial.lists]; 83 | data.tasks = [...data.tasks, ...tutorial.tasks]; 84 | }); 85 | 86 | 87 | export function migrate(data) { 88 | var version = data.version || 0; 89 | 90 | for (var v = version + 1; v < migrations.length; v++) { 91 | migrations[v](data); 92 | data.version = v; 93 | } 94 | 95 | cleanup(data); 96 | } 97 | -------------------------------------------------------------------------------- /app/data/reducers/tdo/tasks/index.js: -------------------------------------------------------------------------------- 1 | export default function(state = [], action) { 2 | switch (action.type) { 3 | 4 | 5 | default: 6 | return state; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /app/data/reducers/tdo/tutorial.js: -------------------------------------------------------------------------------- 1 | import uid from "uid"; 2 | 3 | export function getTutorial() { 4 | 5 | const createdDate = new Date().toISOString(); 6 | 7 | var board = { 8 | id: uid(), 9 | name: 'Tutorial', 10 | createdDate 11 | }; 12 | 13 | var formatting = { 14 | id: uid(), 15 | boardId: board.id, 16 | name: 'Formatting', 17 | headerStyle: 'color: lightgreen', 18 | createdDate 19 | }; 20 | 21 | var keyboard = { 22 | id: uid(), 23 | boardId: board.id, 24 | name: 'Keyboard', 25 | headerStyle: 'color: lightblue', 26 | createdDate 27 | }; 28 | 29 | var general = { 30 | id: uid(), 31 | boardId: board.id, 32 | name: 'General', 33 | headerStyle: 'color: white', 34 | createdDate 35 | }; 36 | 37 | var tasks = [{ 38 | id: uid(), 39 | listId: formatting.id, 40 | name: `Use Markdown to make some text **bold** or *italic*`, 41 | createdDate 42 | }, { 43 | id: uid(), 44 | listId: formatting.id, 45 | name: `You can also use other Markdown features such as [links](https://github.com/mstijak/tdo) or lists. 46 | 47 | * Item 1 48 | * Item 2 49 | * Item 3 50 | `, 51 | createdDate 52 | }, { 53 | id: uid(), 54 | listId: formatting.id, 55 | name: `You can customize task highlighting in Settings. #idea`, 56 | createdDate 57 | }, { 58 | id: uid(), 59 | listId: formatting.id, 60 | name: `Add some custom CSS and override background or text color of the whole application. #idea 61 | 62 | Hint: \`body \{ color: lightblue \}\``, 63 | createdDate 64 | }, { 65 | id: uid(), 66 | listId: keyboard.id, 67 | name: 'Press ? to get Help. !important', 68 | createdDate 69 | }, { 70 | id: uid(), 71 | listId: keyboard.id, 72 | name: 'Use arrow keys to navigate lists and tasks', 73 | createdDate 74 | }, { 75 | id: uid(), 76 | listId: keyboard.id, 77 | name: 'Hit `Enter` to edit the selected task', 78 | createdDate 79 | }, { 80 | id: uid(), 81 | listId: keyboard.id, 82 | name: 'Hit `Space` to mark the task deleted', 83 | createdDate 84 | }, { 85 | id: uid(), 86 | listId: keyboard.id, 87 | name: 'Press the `Insert` key to insert a new task at cursor position', 88 | createdDate 89 | }, { 90 | id: uid(), 91 | listId: keyboard.id, 92 | name: 'Press the `Delete` key to delete the selected task', 93 | createdDate 94 | }, { 95 | id: uid(), 96 | listId: keyboard.id, 97 | name: 'Use `Ctrl` + `Up/Down/Left/Right` arrow keys to move the selected task around', 98 | createdDate 99 | }, { 100 | id: uid(), 101 | listId: general.id, 102 | name: 'Double click on the list header to edit the list', 103 | createdDate 104 | }, { 105 | id: uid(), 106 | listId: general.id, 107 | name: 'Double click on a task to edit it', 108 | createdDate 109 | }, { 110 | id: uid(), 111 | listId: general.id, 112 | name: 'Hit Add Board (on the top) and enjoy **`tdo`**!', 113 | createdDate 114 | }, ]; 115 | 116 | 117 | return { 118 | boards: [board], 119 | lists: [keyboard, formatting, general], 120 | tasks: tasks 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /app/data/services/GHAuth.js: -------------------------------------------------------------------------------- 1 | export class GHAuth { 2 | static get() { 3 | return localStorage.gh && JSON.parse(localStorage.gh); 4 | } 5 | 6 | static set(gh) { 7 | if (gh) 8 | localStorage.gh = JSON.stringify(gh); 9 | else 10 | delete localStorage.gh; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/data/services/Gists.js: -------------------------------------------------------------------------------- 1 | export class Gists { 2 | 3 | constructor(gh) { 4 | this.gh = gh; 5 | } 6 | 7 | checkOk(response) { 8 | if (!response.ok) 9 | throw new Error(`Invalid response received. ${response.statusText}`); 10 | return response; 11 | } 12 | 13 | get() { 14 | let options = { 15 | headers: { 16 | 'Authorization': `token ${this.getToken()}` 17 | } 18 | }; 19 | 20 | return fetch(`https://api.github.com/gists/${this.getGistId()}`, options) 21 | .then(::this.checkOk) 22 | .then(x=>x.json()); 23 | } 24 | 25 | load() { 26 | return this.get() 27 | .then(x=> { 28 | var tdo = x.files['tdo.json']; 29 | var data = {}; 30 | if (tdo && tdo.content) 31 | data = JSON.parse(tdo.content); 32 | return data; 33 | }) 34 | } 35 | 36 | create(data) { 37 | let options = { 38 | method: 'POST', 39 | headers: { 40 | 'Authorization': `token ${this.getToken()}`, 41 | 'Content-Type': 'application/json' 42 | }, 43 | body: JSON.stringify({ 44 | description: 'tdo tasks', 45 | files: this.convertFiles(data) 46 | }) 47 | }; 48 | return fetch(`https://api.github.com/gists`, options) 49 | .then(::this.checkOk) 50 | .then(x=>x.json()) 51 | } 52 | 53 | update(data) { 54 | let options = { 55 | method: 'PATCH', 56 | headers: { 57 | 'Authorization': `token ${this.getToken()}`, 58 | 'Content-Type': 'application/json' 59 | }, 60 | body: JSON.stringify({ 61 | files: this.convertFiles(data) 62 | }) 63 | }; 64 | return fetch(`https://api.github.com/gists/${this.getGistId()}`, options) 65 | .then(::this.checkOk) 66 | .then(x=>x.json()) 67 | } 68 | 69 | convertFiles(data) { 70 | return { 71 | "tdo.json": { 72 | content: JSON.stringify(data, null, 2) 73 | } 74 | } 75 | } 76 | 77 | getGistId() { 78 | if (!this.gh || !this.gh.gistId) 79 | throw new Error('GistId not set. Please visit the Settings page.'); 80 | return this.gh.gistId; 81 | } 82 | 83 | getToken() { 84 | if (!this.gh || !this.gh.token) 85 | throw new Error('GitHub personal access token not set. Please visit the Settings page.'); 86 | return this.gh.token; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /app/extension.js: -------------------------------------------------------------------------------- 1 | import "./themes/extension.scss"; 2 | import "./index"; 3 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 |'; 70 | 71 | var styles = getStyles(data.task.name, data.styles); 72 | var className = widget.CSS.element('checkbox', "input", { 73 | checked: !!data.task.completed, 74 | }); 75 | 76 | return [ 77 |