Version 1.0: Working with TimeStamper API and GTDServlet.
This commit is contained in:
252
src/www/css/personal-display.scss
Normal file
252
src/www/css/personal-display.scss
Normal file
@ -0,0 +1,252 @@
|
||||
/* Personal Status Display
|
||||
Author: Jonathan Bernard <jdbernard@gmail.com> */
|
||||
|
||||
/* Theme: [She Sells Seashells](http://www.colourlovers.com/palette/57650/She_Sells_Seashells)
|
||||
$color1: #ffefd3;
|
||||
$color2: #fffee4;
|
||||
$color3: #d0ecea;
|
||||
$color4: #49aca5;
|
||||
$color5: #6d5f49;
|
||||
|
||||
$fgColor: $color5;
|
||||
$mutedFgColor: lighten($fgColor, 15%);
|
||||
$bgColor: $color2;
|
||||
$bgColor2: $color1;
|
||||
$accent1: $color3;
|
||||
$accent2: $color4;
|
||||
*/
|
||||
|
||||
/* Theme: [Between The Clouds](http://www.colourlovers.com/palette/1083480/Between_The_Clouds)
|
||||
$color1: #73c8a9;
|
||||
$color2: #dee1b6;
|
||||
$color3: #e1b866;
|
||||
$color4: #bd5532;
|
||||
$color5: #373b44;
|
||||
|
||||
$fgColor: $color5;
|
||||
$mutedFgColor: lighten($fgColor, 15%);
|
||||
$bgColor: lighten($color2, 10%);
|
||||
$bgColor2: $color3;
|
||||
$accent1: $color1;
|
||||
$accent2: $color4;
|
||||
*/
|
||||
|
||||
/* Theme: [Less Squinting!](http://www.colourlovers.com/palette/1066563/Less_Squinting!)
|
||||
$color1: #B2CBA3;
|
||||
$color2: #E0DF9F;
|
||||
$color3: #E7A83E;
|
||||
$color4: #9A736E;
|
||||
$color5: #EA525F;
|
||||
|
||||
$fgColor: darken($color4, 10%);
|
||||
$mutedFgColor: lighten($fgColor, 15%);
|
||||
$bgColor: lighten($color2, 15%);
|
||||
$bgColor2: $color1;
|
||||
$accent1: $color3;
|
||||
$accent2: $color5;
|
||||
*/
|
||||
|
||||
/* Theme: [with you](http://www.colourlovers.com/palette/908111/with_you)
|
||||
$color1: #785D56;
|
||||
$color2: #BE4C54;
|
||||
$color3: #C6B299;
|
||||
$color4: #E6D5C1;
|
||||
$color5: #FFF4E3;
|
||||
|
||||
$fgColor: $color1;
|
||||
$mutedFgColor: lighten($fgColor, 15%);
|
||||
$bgColor: lighten($color5, 5%);
|
||||
$bgColor2: $color4;
|
||||
$accent1: $color3;
|
||||
$accent2: $color2;
|
||||
*/
|
||||
|
||||
/* Theme: [Autumn Oak](http://www.colourlovers.com/palette/29411/Autumn_Oak)
|
||||
*/
|
||||
$color1: #111111;
|
||||
$color2: #FCF7D1;
|
||||
$color3: #A9A17A;
|
||||
$color4: #B52C00;
|
||||
$color5: #8C0005;
|
||||
|
||||
/* Normal
|
||||
$fgColor: $color1;
|
||||
$mutedFgColor: lighten($fgColor, 15%);
|
||||
$bgColor: $color2;
|
||||
$bgColor2: $color3;
|
||||
$accent1: $color5;
|
||||
$accent2: $color4;
|
||||
*/
|
||||
|
||||
/* Inverted */
|
||||
$mutedFgColor: $color2;
|
||||
$fgColor: lighten($mutedFgColor, 15%);
|
||||
$bgColor: $color1;
|
||||
$bgColor2: $color3;
|
||||
$accent1: $color5;
|
||||
$accent2: $color4;
|
||||
|
||||
/// Global Rules
|
||||
* {
|
||||
-moz-box-sizing: border-box;
|
||||
-webkit-box-sizing: border-box;
|
||||
box-sizing: border-box; }
|
||||
|
||||
html {
|
||||
background-color: $bgColor;
|
||||
color: $fgColor;
|
||||
font-size: 150%; }
|
||||
|
||||
/* HTML5 elements */
|
||||
article,aside,details,figcaption,figure,
|
||||
footer,header,hgroup,menu,nav,section {
|
||||
display:block; }
|
||||
|
||||
p, ul, li { margin: 0.5em 0; }
|
||||
|
||||
a { color: $accent2; }
|
||||
|
||||
body {
|
||||
font-family: Advent Pro, Rosario, Jura, Average Sans, Cantarell;
|
||||
margin: 0.5rem 1rem; }
|
||||
|
||||
body > section {
|
||||
padding: 0.2rem;
|
||||
|
||||
h3 {
|
||||
border-bottom: solid 2px $accent1;
|
||||
color: $accent2;
|
||||
font-family: Play, Jura, Exo, Rationale, Quicksand, Average Sans, sans-serif;
|
||||
font-size: larger;
|
||||
margin: 0.2rem 0.2rem 0.2rem -0.2rem;
|
||||
padding-left: 0.2rem; }
|
||||
|
||||
.task-notes {
|
||||
color: $mutedFgColor;
|
||||
font-size: 75%;
|
||||
max-height: 10em;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.next-action {
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
span { display: none; }
|
||||
|
||||
.action { display: inline-block; }
|
||||
|
||||
.date, .project {
|
||||
background: $bgColor2;
|
||||
border-radius: 5px;
|
||||
color: $mutedFgColor;
|
||||
display: inline-block;
|
||||
font-size: 66%;
|
||||
margin: 0 0.5rem;
|
||||
max-width: 33%;
|
||||
padding: 0 0.5rem;
|
||||
white-space: nowrap; }
|
||||
|
||||
.details {
|
||||
color: $mutedFgColor;
|
||||
display: block;
|
||||
font-size: 75%; }
|
||||
|
||||
.project:before {
|
||||
content: 'Project: ';
|
||||
display: inline-block;
|
||||
font-family: Play;
|
||||
font-variant: small-caps; }
|
||||
|
||||
.date:before {
|
||||
content: 'Due: ';
|
||||
display: inline-block;
|
||||
font-family: Play;
|
||||
font-variant: small-caps; }
|
||||
}
|
||||
}
|
||||
|
||||
#config-dialog {
|
||||
display: none;
|
||||
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
|
||||
form {
|
||||
border: solid thin $accent1;
|
||||
background: $bgColor;
|
||||
border-radius: 10px;
|
||||
margin-left: 10%;
|
||||
margin-right: 10%;
|
||||
margin-top: 1em;
|
||||
padding: 0 0.5em;
|
||||
position: relative;
|
||||
width: 80%;
|
||||
max-width: 32em;
|
||||
|
||||
.validate-tips { display: block; }
|
||||
|
||||
.wait-overlay {
|
||||
display: none;
|
||||
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
padding: 1em 0.5em;
|
||||
position: absolute;
|
||||
text-align: center;
|
||||
|
||||
top: 0;
|
||||
bottom:0;
|
||||
left: 0;
|
||||
right: 0; }
|
||||
|
||||
.button-panel {
|
||||
padding: 0.5em;
|
||||
text-align: right;
|
||||
width: 100%;
|
||||
|
||||
.global-config { float: left; }
|
||||
|
||||
.save-button {
|
||||
border: $accent2 solid thin;
|
||||
border-radius: 5px;
|
||||
display: inline-block;
|
||||
padding: 0.1em 0.3em; } } }
|
||||
|
||||
.config-section-header { color: $accent2; }
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
width: 6rem; }
|
||||
|
||||
input, select { width: 8rem; }
|
||||
|
||||
.timestamper-config, .gtd-config {
|
||||
vertical-align: top;
|
||||
display: inline-block;
|
||||
width: 15em; }
|
||||
|
||||
.category-name {
|
||||
display: inline-block;
|
||||
margin-left: 0.2rem;
|
||||
width: 8rem; }
|
||||
|
||||
.remove-button {
|
||||
color: $mutedFgColor;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
margin-left: 2rem;
|
||||
width: 4rem; }
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
li {
|
||||
display-style: block;
|
||||
margin: 0;
|
||||
padding: 0 } }
|
||||
}
|
73
src/www/index.html
Normal file
73
src/www/index.html
Normal file
@ -0,0 +1,73 @@
|
||||
<!DOCTYPE HTML>
|
||||
<html>
|
||||
<head>
|
||||
<title>What I am Doing</title>
|
||||
<link rel="stylesheet" href="css/personal-display.css" type="text/css">
|
||||
<link href='//fonts.googleapis.com/css?family=Play|Advent+Pro' rel='stylesheet' type='text/css'>
|
||||
|
||||
<!-- PROD Libraries
|
||||
-->
|
||||
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/jquery/1.10.0/jquery.min.js" defer></script>
|
||||
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.4.4/underscore-min.js" defer></script>
|
||||
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/backbone.js/1.0.0/backbone-min.js" defer></script>
|
||||
|
||||
<!-- DEV Libraries
|
||||
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/jquery/1.10.0/jquery.js" defer></script>
|
||||
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/underscore.js/1.4.4/underscore.js" defer></script>
|
||||
<script type="text/javascript" src="//cdnjs.cloudflare.com/ajax/libs/backbone.js/1.0.0/backbone.js" defer></script>
|
||||
-->
|
||||
|
||||
<script src="js/personal-display.js" type="text/javascript" defer></script>
|
||||
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no">
|
||||
</head>
|
||||
<body>
|
||||
<section id=current-task>
|
||||
<h3>Current Activity</h3>
|
||||
<span class=task>Loading...</span>
|
||||
<div class=task-notes></div>
|
||||
</section>
|
||||
<section id=priorities>
|
||||
<h3>Next Actions (unsorted)</h3>
|
||||
</section>
|
||||
<section id=config-dialog>
|
||||
<form>
|
||||
<h3>Configuration</h3>
|
||||
<span class=validate-tips></span>
|
||||
<section class=timestamper-config>
|
||||
<span class=config-section-header>TimeStamper</span>
|
||||
<div><label>Server Name: </label>
|
||||
<input type=text class=host></div>
|
||||
<div><label>Username: </label>
|
||||
<input type=text class=username></div>
|
||||
<div><label>Password: </label>
|
||||
<input type=password class=password></div>
|
||||
<div><label>Timeline: </label>
|
||||
<select class=timeline>
|
||||
<option value="none">None</option>
|
||||
</select></div>
|
||||
</section>
|
||||
<section class=gtd-config>
|
||||
<span class=config-section-header>Getting Things Done</span>
|
||||
<div><label>Server Name: </label>
|
||||
<input type=text class=host></div>
|
||||
<div><label>Username: </label>
|
||||
<input type=text class=username></div>
|
||||
<div><label>Password: </label>
|
||||
<input type=password class=password></div>
|
||||
<ul> <li><label>Context: </label>
|
||||
<select class=category>
|
||||
<option class=default-option value="none">Add a category...</option>
|
||||
</select></li> </ul>
|
||||
</section>
|
||||
<div class=button-panel>
|
||||
<div class=global-config>
|
||||
<label>Refresh (sec): </label>
|
||||
<input type=text class=refresh></div>
|
||||
<div class=save-button><a href="#">Save and Close</a></div>
|
||||
</div>
|
||||
<div class=wait-overlay><img src="img/loading-spinner.gif"><br><span></span></div>
|
||||
</form>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
499
src/www/js/personal-display.js
Normal file
499
src/www/js/personal-display.js
Normal file
@ -0,0 +1,499 @@
|
||||
(function() {
|
||||
var root = this;
|
||||
|
||||
var PD = root.PersonalDisplay = {};
|
||||
|
||||
PD.hasHTML5LocalStorage = function() {
|
||||
try {
|
||||
return 'localStorage' in window && window['localStorage'] !== null; }
|
||||
catch (e) { return false; } }
|
||||
|
||||
/// ## Models
|
||||
|
||||
PD.TimelineMarkModel = Backbone.Model.extend({
|
||||
initialize: function() { _.bindAll(this, "equals"); },
|
||||
equals: function(that) { return this.id == that.id; }
|
||||
});
|
||||
|
||||
PD.GTDEntryModel = Backbone.Model.extend({
|
||||
initialize: function() { _.bindAll(this, "equals"); },
|
||||
equals: function(that) { return this.id == that.id; }
|
||||
});
|
||||
|
||||
// ## Views
|
||||
|
||||
PD.CurrentActivityView = Backbone.View.extend({
|
||||
el: $("#current-task"),
|
||||
|
||||
initialize: function() {
|
||||
_.bindAll(this, "render");
|
||||
|
||||
this.model.on('change', this.render, this); },
|
||||
|
||||
render: function() {
|
||||
this.$el.find(".task").text(this.model.get("mark"));
|
||||
this.$el.find(".task-notes").text(this.model.get("notes"));
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
PD.GTDNextActionView = Backbone.View.extend({
|
||||
className: "next-action",
|
||||
tagName: "div",
|
||||
ignoredProperties: ["action", "details", "id"],
|
||||
|
||||
initialize: function() {
|
||||
_.bindAll(this, "render");
|
||||
$("#priorities").append(this.el);
|
||||
|
||||
this.model.on('change', this.render, this); },
|
||||
|
||||
render: function() {
|
||||
var elements = [];
|
||||
|
||||
// Look for the "action" property first
|
||||
var actionEl = $(document.createElement("span"))
|
||||
.addClass("action")
|
||||
actionEl.text(this.model.get("action").toString());
|
||||
elements.push(actionEl);
|
||||
|
||||
// Add span elements for each of the other attributes.
|
||||
_.each(this.model.attributes, function(val, key) {
|
||||
if (!_.contains(this.ignoredProperties, key)) {
|
||||
var el = $(document.createElement("span"))
|
||||
.addClass(key.toString());
|
||||
el.text(val);
|
||||
elements.push(el); } }, this);
|
||||
|
||||
// Finally, look for the "details" property
|
||||
if (this.model.get("details")) {
|
||||
var detailEl = $(document.createElement("span"))
|
||||
.addClass("details");
|
||||
detailEl.text(this.model.get("details").toString());
|
||||
elements.push(detailEl); }
|
||||
|
||||
// Clear the old data and add our new elements in order
|
||||
this.$el.empty();
|
||||
_.each(elements, function(el) { this.$el.append(el); }, this);
|
||||
|
||||
return this;
|
||||
}
|
||||
});
|
||||
|
||||
PD.GTDNextActionCollection = Backbone.Collection.extend({
|
||||
model: PD.GTDEntryModel,
|
||||
|
||||
initialize: function() {
|
||||
_.bindAll(this, "addNextActionView", "removeNextActionView");
|
||||
this.views = {};
|
||||
this.on('add', this.addNextActionView);
|
||||
this.on('remove', this.removeNextActionView); },
|
||||
|
||||
addNextActionView: function(entryModel) {
|
||||
var view = new PD.GTDNextActionView({model: entryModel});
|
||||
view.render();
|
||||
this.views[entryModel.get("id")] = view; },
|
||||
|
||||
removeNextActionView: function(entryModel) {
|
||||
var view = this.views[entryModel.get("id")];
|
||||
view.$el.remove(); }
|
||||
});
|
||||
|
||||
PD.ConfigDialog = Backbone.View.extend({
|
||||
el: $("#config-dialog"),
|
||||
|
||||
events: {
|
||||
"blur .timestamper-config .password" : "tsLogin",
|
||||
"blur .gtd-config .password" : "gtdLogin",
|
||||
|
||||
"change .gtd-config .category" : "addCategory",
|
||||
"click .remove-button" : "removeCategory",
|
||||
|
||||
"click .save-button" : "saveAndClose" },
|
||||
|
||||
initialize: function() {
|
||||
_.bindAll(this, "show", "hide", "tsLogin", "gtdLogin",
|
||||
"loadTsData", "loadGtdData", "addCategory", "makeCategoryItem",
|
||||
"removeCategory", "saveAndClose"); },
|
||||
|
||||
show: function() {
|
||||
var $tsSection = this.$el.find(".timestamper-config");
|
||||
var $gtdSection = this.$el.find(".gtd-config");
|
||||
|
||||
// Load TimeStamper configuration values.
|
||||
if (PD.tsCfg) {
|
||||
$tsSection.find(".username").val(PD.tsCfg.username);
|
||||
$tsSection.find(".password").val(PD.tsCfg.password);
|
||||
$tsSection.find(".host").val(PD.tsCfg.host);
|
||||
|
||||
if (PD.tsAuth) {
|
||||
this.loadTsData(PD.tsCfg.host, PD.tsCfg.username); }
|
||||
|
||||
this.$el.find('.timeline').val(PD.tsCfg.timelineId); }
|
||||
|
||||
// Or suggest a default server.
|
||||
else { $tsSection.find(".host").val("timestamper.jdb-labs.com"); }
|
||||
|
||||
// Load GTD configuration values.
|
||||
if (PD.gtdCfg) {
|
||||
$gtdSection.find(".username").val(PD.gtdCfg.username);
|
||||
$gtdSection.find(".password").val(PD.gtdCfg.password);
|
||||
$gtdSection.find(".host").val(PD.gtdCfg.host);
|
||||
|
||||
if (PD.gtdAuth) { this.loadGtdData(PD.gtdCfg.host); }
|
||||
|
||||
// Create the items for the selected categories
|
||||
$(".category-name").parent().remove();
|
||||
_.forEach(PD.gtdCfg.categories, this.makeCategoryItem); }
|
||||
|
||||
this.$el.find('.refresh').val(
|
||||
PD.refreshPeriod ? PD.refreshPeriod / 1000 : 15);
|
||||
|
||||
this.$el.fadeIn(); },
|
||||
|
||||
hide: function() { this.$el.fadeOut(); },
|
||||
|
||||
tsLogin: function() {
|
||||
var username = this.$el.find(".timestamper-config .username").val();
|
||||
var password = this.$el.find(".timestamper-config .password").val();
|
||||
var host = this.$el.find(".timestamper-config .host").val();
|
||||
|
||||
if (!PD.tsCfg) { PD.tsCfg = {}; }
|
||||
|
||||
// Hide the configuration dialog.
|
||||
this.$el.find(".wait-overlay span").text("Connecting to " + host);
|
||||
this.$el.find(".wait-overlay").fadeIn();
|
||||
|
||||
// Try to log in to the TimeStamper service.
|
||||
$.ajax({
|
||||
url: "https://" + host + "/ts_api/login",
|
||||
xhrFields: { withCredentials: true },
|
||||
processData: false,
|
||||
type: 'POST',
|
||||
async: false,
|
||||
|
||||
data: JSON.stringify(
|
||||
{"username": username, "password": password}),
|
||||
|
||||
error: function(jqXHR, textStatus, error) {
|
||||
if (jqXHR.status == 401) { $(".validate-tips")
|
||||
.text("Invalid username/password combination for " +
|
||||
"the TimeStamper service."); }
|
||||
else { $(".validate-tips").text("There was an error " +
|
||||
"trying to log into the TimeStamper service: " +
|
||||
error); }
|
||||
PD.tsAuth = false; },
|
||||
|
||||
success: function(data, textStatus, jqXHR) {
|
||||
PD.tsAuth = true;
|
||||
|
||||
$(".validate-tips").text("");
|
||||
|
||||
// Load the user's timelines.
|
||||
PD.configDialog.loadTsData(host, username);
|
||||
}
|
||||
});
|
||||
|
||||
// Success or failure we hide the wait overlay.
|
||||
this.$el.find(".wait-overlay").fadeOut();
|
||||
|
||||
},
|
||||
|
||||
gtdLogin: function() {
|
||||
var username = this.$el.find(".gtd-config .username").val();
|
||||
var password = this.$el.find(".gtd-config .password").val();
|
||||
var host = this.$el.find(".gtd-config .host").val();
|
||||
|
||||
if (!PD.gtdCfg) { PD.gtdCfg = {}; }
|
||||
|
||||
// Hide the configuration dialog.
|
||||
this.$el.find(".wait-overlay span").text("Connecting to " + host);
|
||||
this.$el.find(".wait-overlay").fadeIn();
|
||||
|
||||
// Try to log in to the GTD service.
|
||||
$.ajax({
|
||||
url: "http://" + host + "/gtd/login",
|
||||
xhrFields: { withCredentials: true },
|
||||
processData: false,
|
||||
type: 'POST',
|
||||
async: false,
|
||||
|
||||
data: JSON.stringify(
|
||||
{"username": username, "password": password}),
|
||||
|
||||
error: function(jqXHR, textStatus, error) {
|
||||
if (jqXHR.status == 401) { $(".validate-tips")
|
||||
.text("Invalid username/password combination for " +
|
||||
"the Getting Things Done service."); }
|
||||
else { $(".validate-tips").text("There was an error " +
|
||||
"trying to log into the Getting Things Done service: " +
|
||||
error); }
|
||||
PD.gtdAuth = false; },
|
||||
|
||||
success: function(data, textStatus, jqXHR) {
|
||||
PD.gtdAuth = true;
|
||||
|
||||
$(".validate-tips").text("");
|
||||
|
||||
PD.configDialog.loadGtdData(host); }
|
||||
});
|
||||
|
||||
this.$el.find(".wait-overlay").fadeOut();
|
||||
},
|
||||
|
||||
loadTsData: function(host, username) {
|
||||
// (Re)load the user's timelines.
|
||||
PD.tsCfg.timelines = JSON.parse($.ajax({
|
||||
url: 'https://' + host + '/ts_api/timelines/' + username,
|
||||
xhrFields: { withCredentials: true },
|
||||
async: false}).responseText);
|
||||
|
||||
// Populate the available timelines list.
|
||||
var $timelineSelectEl = this.$el.find(".timestamper-config .timeline");
|
||||
$timelineSelectEl.empty();
|
||||
_.forEach(PD.tsCfg.timelines, function(timeline) {
|
||||
var $optionEl = $(document.createElement("option"));
|
||||
$optionEl.attr("value", timeline.id);
|
||||
$optionEl.text(timeline.description);
|
||||
$timelineSelectEl.append($optionEl); }); },
|
||||
|
||||
loadGtdData: function(host) {
|
||||
// Load the user's contexts
|
||||
PD.gtdCfg.contexts = JSON.parse($.ajax({
|
||||
url: 'http://' + host + '/gtd/contexts',
|
||||
xhrFields: { withCredentials: true },
|
||||
async: false }).responseText);
|
||||
|
||||
// Load the user's projects
|
||||
PD.gtdCfg.projects = JSON.parse($.ajax({
|
||||
url: 'http://' + host + '/gtd/projects',
|
||||
xhrFields: { withCredentials: true },
|
||||
async: false }).responseText);
|
||||
|
||||
// Populate the available contexts and projects drop-down.
|
||||
var $categorySelectEl = $(".gtd-config .category")
|
||||
$categorySelectEl.empty();
|
||||
$categorySelectEl.append(
|
||||
"<option class=default-option value='none'>" +
|
||||
"Add a category...</option>");
|
||||
_.forEach(PD.gtdCfg.contexts.concat(PD.gtdCfg.projects),
|
||||
function(category) {
|
||||
var $optionEl = $(document.createElement("option"));
|
||||
$optionEl.attr("value", category.id);
|
||||
$optionEl.text(category.id);
|
||||
$categorySelectEl.append($optionEl); });
|
||||
$categorySelectEl[0].selectedIndex = 0; },
|
||||
|
||||
makeCategoryItem: function(catName) {
|
||||
var $liEl = $(
|
||||
"<li class><span class=remove-button>remove</span>" +
|
||||
"<span class=category-name></span></li>");
|
||||
$liEl.find('.category-name').text(catName);
|
||||
this.$el.find(".gtd-config ul").append($liEl); },
|
||||
|
||||
addCategory: function(source) {
|
||||
var selectEl = source.target;
|
||||
var $selectEl = $(selectEl);
|
||||
if (selectEl.selectedIndex == 0) { return; }
|
||||
this.makeCategoryItem($selectEl.val());
|
||||
selectEl.selectedIndex = 0; },
|
||||
|
||||
removeCategory: function(source) {
|
||||
$(source.target).parent().remove(); },
|
||||
|
||||
saveAndClose: function() {
|
||||
if (!PD.tsCfg) { PD.tsCfg = {}; }
|
||||
if (!PD.gtdCfg) { PD.gtdCfg = {}; }
|
||||
|
||||
// Save TimeStamper configuration.
|
||||
var $tsEl = this.$el.find(".timestamper-config");
|
||||
PD.tsCfg.host = $tsEl.find(".host").val();
|
||||
PD.tsCfg.username = $tsEl.find(".username").val();
|
||||
PD.tsCfg.password = $tsEl.find(".password").val();
|
||||
PD.tsCfg.timelineId = $tsEl.find(".timeline").val();
|
||||
|
||||
// Save Getting Things Done configuration.
|
||||
var $gtdEl = this.$el.find(".gtd-config");
|
||||
PD.gtdCfg.host = $gtdEl.find(".host").val();
|
||||
PD.gtdCfg.username = $gtdEl.find(".username").val();
|
||||
PD.gtdCfg.password = $gtdEl.find(".password").val();
|
||||
PD.gtdCfg.categories = _.map(
|
||||
this.$el.find(".category-name"),
|
||||
function(span) { return $(span).text(); });
|
||||
|
||||
// Save global data
|
||||
PD.refreshPeriod = parseInt(this.$el.find(".refresh").val()) * 1000;
|
||||
|
||||
if (PD.hasHTML5LocalStorage()) {
|
||||
localStorage.setItem("tsCfg", JSON.stringify(PD.tsCfg));
|
||||
localStorage.setItem("gtdCfg", JSON.stringify(PD.gtdCfg));
|
||||
localStorage.setItem("refreshPeriod",
|
||||
JSON.stringify(PD.refreshPeriod)); }
|
||||
|
||||
this.hide();
|
||||
}
|
||||
});
|
||||
|
||||
PD.Main = Backbone.View.extend({
|
||||
el: $("body"),
|
||||
|
||||
initialize: function() {
|
||||
|
||||
_.bindAll(this, "refresh");
|
||||
|
||||
// Create our config dialog view.
|
||||
PD.configDialog = new PD.ConfigDialog();
|
||||
|
||||
// Create our initial models and views.
|
||||
PD.currentActivityModel = new PD.TimelineMarkModel({});
|
||||
PD.currentActivityView = new PD.CurrentActivityView(
|
||||
{model: PD.currentActivityModel})
|
||||
|
||||
// Test for localStorage support
|
||||
if (!PD.hasHTML5LocalStorage()) {
|
||||
alert("Your browser does not support HTML5 localStorage." +
|
||||
"Without this I cannot store your preferences.");
|
||||
PD.configDialog.show(); }
|
||||
else {
|
||||
PD.tsCfg = JSON.parse(localStorage.getItem('tsCfg'));
|
||||
PD.gtdCfg = JSON.parse(localStorage.getItem('gtdCfg'));
|
||||
PD.refreshPeriod = JSON.parse(
|
||||
localStorage.getItem('refreshPeriod')); }
|
||||
|
||||
PD.gtdNextActionCollection = new PD.GTDNextActionCollection();
|
||||
|
||||
// Perform the initial refresh.
|
||||
this.refresh();
|
||||
|
||||
// Schedule future refreshes.
|
||||
setInterval(this.refresh, PD.refreshPeriod ? PD.refreshPeriod : 15000);
|
||||
},
|
||||
|
||||
refresh: function() {
|
||||
// If the dialog is still open we skip this sync to give the user
|
||||
// a chance to finish configuration.
|
||||
if ($("#config-dialog").is(":visible")) { return; }
|
||||
|
||||
// Otherwise, if we do not have configuration information, open the
|
||||
// dialog so the user can enter it.
|
||||
if (!(PD.tsCfg && PD.gtdCfg)) { PD.configDialog.show(); return; }
|
||||
|
||||
// Check that we are authenticated to the services we need. Try to
|
||||
// authenticate if we are not.
|
||||
if (!PD.tsAuth) {
|
||||
$.ajax({
|
||||
url: "https://" + PD.tsCfg.host + "/ts_api/login",
|
||||
xhrFields: { withCredentials: true },
|
||||
processData: false,
|
||||
type: "POST",
|
||||
async: false,
|
||||
|
||||
data: JSON.stringify(
|
||||
{ "username": PD.tsCfg.username,
|
||||
"password": PD.tsCfg.password }),
|
||||
|
||||
error: function(jqXHR, textStatus, error) {
|
||||
// TODO: Handle error.
|
||||
PD.tsAuth=false;
|
||||
alert("Unable to authenticate to the TimeStamper " +
|
||||
"service: " + error);
|
||||
PD.configDialog.show(); },
|
||||
|
||||
success: function(data, textStatus, jqXHR) {
|
||||
PD.tsAuth = true; }}); }
|
||||
|
||||
if (!PD.gtdAuth) {
|
||||
$.ajax({
|
||||
url: "http://" + PD.gtdCfg.host + "/gtd/login",
|
||||
xhrFields: { withCredentials: true },
|
||||
processData: false,
|
||||
type: "POST",
|
||||
async: false,
|
||||
|
||||
data: JSON.stringify(
|
||||
{ "username": PD.gtdCfg.username,
|
||||
"password": PD.gtdCfg.password }),
|
||||
|
||||
error: function(jqXHR, textStatus, error) {
|
||||
// TODO: Handle error.
|
||||
PD.gtdAuth=false;
|
||||
alert("Unable to authenticate to the GTD service: " +
|
||||
error);
|
||||
PD.configDialog.show(); },
|
||||
|
||||
success: function(data, textStatus, jqXHR) {
|
||||
PD.gtdAuth = true; }}); }
|
||||
|
||||
|
||||
// Check that we have successfully authenticated to both services.
|
||||
// If we are not, we will skip this refresh.
|
||||
if (!(PD.tsAuth && PD.gtdAuth)) { return; }
|
||||
|
||||
// Get the latest timestamp from the TimeStamper service.
|
||||
$.ajax({
|
||||
url: "https://" + PD.tsCfg.host + "/ts_api/entries/" +
|
||||
PD.tsCfg.username + "/" + PD.tsCfg.timelineId,
|
||||
xhrFields: { withCredentials: true },
|
||||
data: {"order": "asc" },
|
||||
dataType: 'json',
|
||||
type: 'GET',
|
||||
async: true,
|
||||
|
||||
error: function(jqXHR, textStatus, errorText) {
|
||||
if (jqXHR.status == 401) { PD.tsAuth = false; }
|
||||
else {
|
||||
alert("Unable to retrieve current timestamp: " + errorText);
|
||||
PD.configDialog.show(); }
|
||||
},
|
||||
|
||||
success: function(data, textStatus, jqXHR) {
|
||||
PD.currentActivityModel.set(data[0]); }
|
||||
});
|
||||
|
||||
// Get the list of GTD entries for each of our categories.
|
||||
var categories = _.reduce(
|
||||
PD.gtdCfg.categories,
|
||||
function(acc, cat) { return acc ? acc + "," + cat : cat; }, "");
|
||||
|
||||
$.ajax({
|
||||
url: "http://" + PD.gtdCfg.host + "/gtd/next-actions/" +
|
||||
categories,
|
||||
xhrFields: { withCredentials: true },
|
||||
dataType: 'json',
|
||||
type: 'GET',
|
||||
async: true,
|
||||
|
||||
error: function(jqXHR, textStatus, errorText) {
|
||||
if (jqXHR.status == 401) { PD.gtdAtuh = false; }
|
||||
else if (jqXHR.status == 500) { return; }
|
||||
else {
|
||||
alert("Unable to retrieve next actions: " + errorText);
|
||||
PD.configDialog.show(); }
|
||||
},
|
||||
|
||||
success: function(data, textStatus, jqXHR) {
|
||||
var collection = PD.gtdNextActionCollection;
|
||||
|
||||
// Add all the retrieved items to the collection.
|
||||
_.forEach(data, function(actionAttr) {
|
||||
|
||||
// Try to find this entry in out collection.
|
||||
var model = collection.get(actionAttr.id);
|
||||
// Update it if found
|
||||
if (model) { model.set(actionAttr); }
|
||||
// Insert a new model if not found.
|
||||
else { collection.add(
|
||||
new PD.GTDEntryModel(actionAttr)); }});
|
||||
|
||||
// Look through our collection for entries that are no
|
||||
// longer in our retrieved data and remove them.
|
||||
collection.forEach(function(model) {
|
||||
if (!_.any(data, model.equals)) {
|
||||
collection.remove(model); }});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
PD.main = new PD.Main();
|
||||
}).call(this);
|
Reference in New Issue
Block a user