├── schemas
├── gschemas.compiled
└── org.gnome.shell.extensions.todoist.gschema.xml
├── assets
└── todoist-gnome-shell-extension.png
├── metadata.json
├── stylesheet.css
├── utils.js
├── LICENSE
├── polyfill.js
├── README.md
├── prefs.js
├── convenience.js
└── extension.js
/schemas/gschemas.compiled:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ubuntudroid/todoist-gnome-shell-extension/HEAD/schemas/gschemas.compiled
--------------------------------------------------------------------------------
/assets/todoist-gnome-shell-extension.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ubuntudroid/todoist-gnome-shell-extension/HEAD/assets/todoist-gnome-shell-extension.png
--------------------------------------------------------------------------------
/metadata.json:
--------------------------------------------------------------------------------
1 | {"name": "Todoist", "description": "Lists tasks from Todoist task manager", "shell-version": ["3.20.4"], "uuid": "todoist@ubuntudroid.gmail.com", "settings-schema": "org.gnome.shell.extensions.todoist"}
2 |
--------------------------------------------------------------------------------
/stylesheet.css:
--------------------------------------------------------------------------------
1 |
2 | .helloworld-label {
3 | font-size: 36px;
4 | font-weight: bold;
5 | color: #ffffff;
6 | background-color: rgba(10,10,10,0.7);
7 | border-radius: 5px;
8 | padding: .5em;
9 | }
10 |
--------------------------------------------------------------------------------
/utils.js:
--------------------------------------------------------------------------------
1 | function isDueDateInPast(item) {
2 | if (item.due === null) return false;
3 |
4 | let dueDate = new Date(item.due.date);
5 | dueDate.setHours(0,0,0,0);
6 | let today = new Date;
7 | today.setHours(0,0,0,0);
8 |
9 | return dueDate <= today;
10 | }
--------------------------------------------------------------------------------
/schemas/org.gnome.shell.extensions.todoist.gschema.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | 'token'
5 | Todoist API token
6 | Personal Todoist API token as found at the bottom of the integrations settings page
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Sven Bendel
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 |
--------------------------------------------------------------------------------
/polyfill.js:
--------------------------------------------------------------------------------
1 | // https://tc39.github.io/ecma262/#sec-array.prototype.findIndex
2 | if (!Array.prototype.findIndex) {
3 | Object.defineProperty(Array.prototype, 'findIndex', {
4 | value: function(predicate) {
5 | // 1. Let O be ? ToObject(this value).
6 | if (this == null) {
7 | throw new TypeError('"this" is null or not defined');
8 | }
9 |
10 | var o = Object(this);
11 |
12 | // 2. Let len be ? ToLength(? Get(O, "length")).
13 | var len = o.length >>> 0;
14 |
15 | // 3. If IsCallable(predicate) is false, throw a TypeError exception.
16 | if (typeof predicate !== 'function') {
17 | throw new TypeError('predicate must be a function');
18 | }
19 |
20 | // 4. If thisArg was supplied, let T be thisArg; else let T be undefined.
21 | var thisArg = arguments[1];
22 |
23 | // 5. Let k be 0.
24 | var k = 0;
25 |
26 | // 6. Repeat, while k < len
27 | while (k < len) {
28 | // a. Let Pk be ! ToString(k).
29 | // b. Let kValue be ? Get(O, Pk).
30 | // c. Let testResult be ToBoolean(? Call(predicate, T, « kValue, k, O »)).
31 | // d. If testResult is true, return k.
32 | var kValue = o[k];
33 | if (predicate.call(thisArg, kValue, k, o)) {
34 | return k;
35 | }
36 | // e. Increase k by 1.
37 | k++;
38 | }
39 |
40 | // 7. Return -1.
41 | return -1;
42 | }
43 | });
44 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | **Unmaintained: Please check out [todoist-indicator](https://github.com/tarelda/todoist-indicator) which started as a fork of this project and greatly expand on its feature set.**
2 |
3 | # Unofficial Todoist Gnome Shell extension
4 |
5 | This is an unofficial Gnome Shell extension which displays the number of currently open tasks in the top right corner of your Gnome Shell.
6 |
7 | It has not yet been submitted to the Gnome Shell extension directory and might never be as this is more a playground for experimenting with Gnome Shell. However that might change in the future.
8 |
9 | Being an absolute beginner when it comes to programming for Gnome the extension's code has been heavily influenced by [this](http://smasue.github.io/gnome-shell-tw) and [this](http://www.mibus.org/2013/02/15/making-gnome-shell-plugins-save-their-config/) blogpost. Kudos to the authors! :)
10 |
11 | 
12 |
13 | # Setup
14 |
15 | Clone the repository to `~/.local/share/gnome-shell/extensions/` into a folder named `todoist@ubuntudroid.gmail.com` using the following command:
16 |
17 | git clone https://github.com/ubuntudroid/todoist-gnome-shell-extension.git todoist@ubuntudroid.gmail.com
18 |
19 | The name of the directory is important because Gnome Shell won't recognize the extension otherwise.
20 |
21 | Then restart Gnome Shell (ALT-F2 and then 'r') and navigate to https://extensions.gnome.org/local/. You can enable the extension and specify your Todoist API token there.
22 |
23 | Currently the extension syncs every 60 seconds, but I'll probably also make this configurable via the settings later. That means it could take up to one minute before the task count appears after setting the API token in the settings.
24 |
--------------------------------------------------------------------------------
/prefs.js:
--------------------------------------------------------------------------------
1 | const GLib = imports.gi.GLib;
2 | const GObject = imports.gi.GObject;
3 | const Gio = imports.gi.Gio;
4 | const Gtk = imports.gi.Gtk;
5 |
6 | const Gettext = imports.gettext.domain('gnome-shell-extensions');
7 | const _ = Gettext.gettext;
8 |
9 | const ExtensionUtils = imports.misc.extensionUtils;
10 | const Me = ExtensionUtils.getCurrentExtension();
11 | const Convenience = Me.imports.convenience;
12 |
13 | function init() {
14 | }
15 |
16 | const TodoistPrefsWidget = new GObject.Class({
17 | Name: 'Todoist.Prefs.Widget',
18 | GTypeName: 'TodoistPrefsWidget',
19 | Extends: Gtk.Grid,
20 |
21 | _init: function(params) {
22 | this.parent(params);
23 | this.margin = 12;
24 | this.row_spacing = this.column_spacing = 6;
25 | this.set_orientation(Gtk.Orientation.VERTICAL);
26 |
27 | this.add(new Gtk.Label({ label: '' + _("Todoist API token") + '',
28 | use_markup: true,
29 | halign: Gtk.Align.START }));
30 |
31 | let entry = new Gtk.Entry({ hexpand: true,
32 | margin_bottom: 12 });
33 | this.add(entry);
34 |
35 | this._settings = Convenience.getSettings();
36 | this._settings.bind('api-token', entry, 'text', Gio.SettingsBindFlags.DEFAULT);
37 |
38 | let primaryText = _("You need to declare a valid API token to allow this extension to communicate with the \
39 | Todoist API on your behalf.\n\
40 | You can find your personal API token on Todoist's integration settings page at the very bottom.");
41 |
42 | this.add(new Gtk.Label({ label: primaryText,
43 | wrap: true, xalign: 0 }));
44 | }
45 | });
46 |
47 | function buildPrefsWidget() {
48 | let widget = new TodoistPrefsWidget();
49 | widget.show_all();
50 |
51 | return widget;
52 | }
53 |
54 |
--------------------------------------------------------------------------------
/convenience.js:
--------------------------------------------------------------------------------
1 | /* -*- mode: js; js-basic-offset: 4; indent-tabs-mode: nil -*- */
2 | /*
3 | Copyright (c) 2011-2012, Giovanni Campagna
4 |
5 | Redistribution and use in source and binary forms, with or without
6 | modification, are permitted provided that the following conditions are met:
7 | * Redistributions of source code must retain the above copyright
8 | notice, this list of conditions and the following disclaimer.
9 | * Redistributions in binary form must reproduce the above copyright
10 | notice, this list of conditions and the following disclaimer in the
11 | documentation and/or other materials provided with the distribution.
12 | * Neither the name of the GNOME nor the
13 | names of its contributors may be used to endorse or promote products
14 | derived from this software without specific prior written permission.
15 |
16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY
20 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
23 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 | */
27 |
28 | const Gettext = imports.gettext;
29 | const Gio = imports.gi.Gio;
30 |
31 | const Config = imports.misc.config;
32 | const ExtensionUtils = imports.misc.extensionUtils;
33 |
34 | /**
35 | * initTranslations:
36 | * @domain: (optional): the gettext domain to use
37 | *
38 | * Initialize Gettext to load translations from extensionsdir/locale.
39 | * If @domain is not provided, it will be taken from metadata['gettext-domain']
40 | */
41 | function initTranslations(domain) {
42 | let extension = ExtensionUtils.getCurrentExtension();
43 |
44 | domain = domain || extension.metadata['gettext-domain'];
45 |
46 | // check if this extension was built with "make zip-file", and thus
47 | // has the locale files in a subfolder
48 | // otherwise assume that extension has been installed in the
49 | // same prefix as gnome-shell
50 | let localeDir = extension.dir.get_child('locale');
51 | if (localeDir.query_exists(null))
52 | Gettext.bindtextdomain(domain, localeDir.get_path());
53 | else
54 | Gettext.bindtextdomain(domain, Config.LOCALEDIR);
55 | }
56 |
57 | /**
58 | * getSettings:
59 | * @schema: (optional): the GSettings schema id
60 | *
61 | * Builds and return a GSettings schema for @schema, using schema files
62 | * in extensionsdir/schemas. If @schema is not provided, it is taken from
63 | * metadata['settings-schema'].
64 | */
65 | function getSettings(schema) {
66 | let extension = ExtensionUtils.getCurrentExtension();
67 |
68 | schema = schema || extension.metadata['settings-schema'];
69 |
70 | const GioSSS = Gio.SettingsSchemaSource;
71 |
72 | // check if this extension was built with "make zip-file", and thus
73 | // has the schema files in a subfolder
74 | // otherwise assume that extension has been installed in the
75 | // same prefix as gnome-shell (and therefore schemas are available
76 | // in the standard folders)
77 | let schemaDir = extension.dir.get_child('schemas');
78 | let schemaSource;
79 | if (schemaDir.query_exists(null))
80 | schemaSource = GioSSS.new_from_directory(schemaDir.get_path(),
81 | GioSSS.get_default(),
82 | false);
83 | else
84 | schemaSource = GioSSS.get_default();
85 |
86 | let schemaObj = schemaSource.lookup(schema, true);
87 | if (!schemaObj)
88 | throw new Error('Schema ' + schema + ' could not be found for extension '
89 | + extension.metadata.uuid + '. Please check your installation.');
90 |
91 | return new Gio.Settings({ settings_schema: schemaObj });
92 | }
93 |
94 |
--------------------------------------------------------------------------------
/extension.js:
--------------------------------------------------------------------------------
1 | const St = imports.gi.St;
2 | const Main = imports.ui.main;
3 | const Soup = imports.gi.Soup;
4 | const Lang = imports.lang;
5 | const Mainloop = imports.mainloop;
6 | const Clutter = imports.gi.Clutter;
7 | const PanelMenu = imports.ui.panelMenu;
8 | const ExtensionUtils = imports.misc.extensionUtils;
9 | const Util = imports.misc.util;
10 | const Me = ExtensionUtils.getCurrentExtension();
11 | const Convenience = Me.imports.convenience;
12 | const Polyfill = Me.imports.polyfill;
13 | const Utils = Me.imports.utils;
14 |
15 | const URL = 'https://todoist.com/API/v8/sync';
16 |
17 |
18 | let _httpSession;
19 | let _syncToken;
20 | let _openItems;
21 | let _schema;
22 |
23 | const TodoistIndicator = new Lang.Class({
24 | Name: 'Todoist.Indicator',
25 | Extends: PanelMenu.Button,
26 |
27 | _init: function () {
28 | this.parent(0.0, "Todoist Indicator", false);
29 | this.buttonText = new St.Label({
30 | text: _("Loading..."),
31 | y_align: Clutter.ActorAlign.CENTER
32 | });
33 | this.actor.add_actor(this.buttonText);
34 | this.actor.connect('button-press-event', _openWebpage);
35 | this._refresh();
36 | },
37 |
38 | _refresh: function () {
39 | this._loadData(this._refreshUI);
40 | this._removeTimeout();
41 | this._timeout = Mainloop.timeout_add_seconds(60, Lang.bind(this, this._refresh));
42 | return true;
43 | },
44 |
45 | _loadData: function () {
46 | let token = _schema.get_string('api-token');
47 | let params = {
48 | token: token,
49 | sync_token: _syncToken,
50 | resource_types: '["items"]'
51 | }
52 | _httpSession = new Soup.Session();
53 | let message = Soup.form_request_new_from_hash('POST', URL, params);
54 | _httpSession.queue_message(message, Lang.bind(this, function (_httpSession, message) {
55 | if (message.status_code !== 200)
56 | return;
57 | let json = JSON.parse(message.response_body.data);
58 | this._refreshUI(json);
59 | }
60 | )
61 | );
62 | },
63 |
64 | _isDoneOrDeletedOrArchived: function (item) {
65 | return item.checked === 1 || item.is_deleted === 1 || item.in_history === 1;
66 | },
67 |
68 | _isNotDone: function (item) {
69 | return item.checked === 0;
70 | },
71 |
72 | _extractId: function (item) {
73 | return item.id;
74 | },
75 |
76 | _removeIfInList: function (item) {
77 | let index = _openItems.findIndex(openItem => openItem.id === item.id);
78 | if (index > -1)
79 | _openItems.splice(index, 1);
80 | },
81 |
82 | _addOrUpdate: function (item) {
83 | let index = _openItems.findIndex(openItem => openItem.id === item.id);
84 | if (index === -1)
85 | _openItems.splice(_openItems.length, 0, item);
86 | else
87 | _openItems[index] = item
88 | },
89 |
90 | _getTextForTaskCount: function (count) {
91 | switch (count) {
92 | case 0: return "no due tasks";
93 | case 1: return "one due task";
94 | default: return count + " due tasks";
95 | }
96 | },
97 |
98 | _parseJson: function (data) {
99 | _syncToken = data.sync_token;
100 |
101 | let undoneItems = data.items.filter(this._isNotDone);
102 | let doneItems = data.items.filter(this._isDoneOrDeletedOrArchived);
103 | undoneItems.forEach(this._addOrUpdate);
104 | doneItems.forEach(this._removeIfInList);
105 | },
106 |
107 | _refreshUI: function (data) {
108 | this._parseJson(data);
109 |
110 | let count = _openItems.filter(Utils.isDueDateInPast).length;
111 | this.buttonText.set_text(this._getTextForTaskCount(count));
112 | },
113 |
114 | _removeTimeout: function () {
115 | if (this._timeout) {
116 | Mainloop.source_remove(this._timeout);
117 | this._timeout = null;
118 | }
119 | },
120 |
121 | stop: function () {
122 | if (_httpSession !== undefined)
123 | _httpSession.abort();
124 | _httpSession = undefined;
125 |
126 | if (this._timeout)
127 | Mainloop.source_remove(this._timeout);
128 | this._timeout = undefined;
129 |
130 | this.menu.removeAll();
131 | }
132 | }
133 | );
134 |
135 | function _openWebpage() {
136 | Util.spawn(['xdg-open', 'https://todoist.com/app#agenda%2Foverdue%2C%20today'])
137 | }
138 |
139 | let todoistMenu;
140 |
141 | function init() {
142 | _syncToken = '*';
143 | _openItems = [];
144 | _schema = Convenience.getSettings();
145 | }
146 |
147 | function enable() {
148 | todoistMenu = new TodoistIndicator;
149 | Main.panel.addToStatusArea('todoist-indicator', todoistMenu);
150 | }
151 |
152 | function disable() {
153 | todoistMenu.stop();
154 | todoistMenu.destroy();
155 | }
156 |
--------------------------------------------------------------------------------