Browse Source

Add better offline sync support

tags/v1.1.0
Skylar Ittner 11 months ago
parent
commit
022ff4d113
10 changed files with 339 additions and 117 deletions
  1. 2
    0
      www/index.html
  2. 96
    37
      www/js/NotePostNotes.class.js
  3. 58
    26
      www/js/Notes.class.js
  4. 83
    0
      www/js/delta.js
  5. 26
    27
      www/js/editnote.js
  6. 2
    2
      www/js/home.js
  7. 9
    1
      www/js/main.js
  8. 29
    4
      www/js/notes.js
  9. 11
    1
      www/pages/home.html
  10. 23
    19
      www/routes.js

+ 2
- 0
www/index.html View File

@@ -31,6 +31,8 @@
<script src="node_modules/easymde/dist/easymde.min.js"></script>
<script src="node_modules/shufflejs/dist/shuffle.min.js"></script>

<script src="js/delta.js"></script>

<script src="js/Notes.class.js"></script>
<script src="js/NotePostNotes.class.js"></script>
<script src="js/notes.js"></script>

+ 96
- 37
www/js/NotePostNotes.class.js View File

@@ -13,10 +13,10 @@ class NotePostNotes extends Notes {
this.password = password;
}

del(noteid, callback) {
del(noteid, success, error) {
super.del(noteid);
var self = this;
$.ajax({
return $.ajax({
url: this.server + "/api/deletenote",
dataType: "json",
cache: false,
@@ -30,45 +30,51 @@ class NotePostNotes extends Notes {
if (val.status == "OK") {
self.notes = val.notes;
}
if (typeof callback == 'function') {
callback();
if (typeof success == 'function') {
success();
}
}, error: function () {
if (typeof error == 'function') {
error();
}
}
});
}

add(note, callback) {
add(note, success, error) {
note.norealid = true;
super.add(note, callback);
this.saveNote(note, success, error);
}

fix(note) {
super.fix(note);
note.id = note.noteid;
this.set(note);
}

load(callback) {
var self = this;
$.ajax({
url: this.server + "/api/getnotes",
getNote(noteid, success, error) {
return $.ajax({
url: this.server + "/api/getnote",
dataType: "json",
cache: false,
method: "POST",
data: {
id: noteid
},
beforeSend: function (xhr) {
xhr.setRequestHeader("Authorization", "Basic " + btoa(self.username + ":" + self.password));
}, success: function (val) {
if (val.status == "OK") {
self.notes = val.notes;
if (typeof success == 'function') {
success(val.note);
}
} else {
if (typeof error == 'function') {
error(val.msg);
}
}
if (typeof callback == 'function') {
callback();
}, error: function () {
if (typeof error == 'function') {
error();
}
}
});
}

saveNote(note, callback) {
saveNote(note, success, error) {
var self = this;
var data = {
text: note.content,
@@ -78,35 +84,88 @@ class NotePostNotes extends Notes {
};
// Don't send ID if it's a locally-made note
if (note.norealid != true) {
data.id = note.id;
data.id = note.noteid;
}
$.ajax({

return $.ajax({
url: this.server + "/api/savenote",
dataType: "json",
method: "POST",
data: data,
beforeSend: function (xhr) {
xhr.setRequestHeader("Authorization", "Basic " + btoa(self.username + ":" + self.password));
}
}).always(function () {
if (typeof callback == 'function') {
callback();
}, success: function (val) {
if (val.status == "OK") {
if (typeof success == 'function') {
success(val.note);
}
} else {
if (typeof error == 'function') {
error();
}
}
}, error: function () {
if (typeof error == 'function') {
error();
}
}
});
}

save(callback) {
this.fixAll();
super.save();
/**
* Sync notes with the NotePost server, resolving conflicts in the process.
*
* @param {function} success(notes) called when everything's synced up.
* @param {function} error
* @returns {undefined}
*/
sync(success, error) {
super.sync();

var ajaxcalls = [];
for (var i = 0; i < this.notes.length; i++) {
ajaxcalls.push(this.saveNote(this.notes[i]));
}
var self = this;
$.ajax({
url: this.server + "/api/getnotes",
dataType: "json",
cache: false,
method: "POST",
beforeSend: function (xhr) {
xhr.setRequestHeader("Authorization", "Basic " + btoa(self.username + ":" + self.password));
}, success: function (val) {
if (val.status == "OK") {

$.when(ajaxcalls).always(function () {
if (typeof callback == 'function') {
callback();
console.log("Comparing notes...");
console.log("Local copy:", self.notes);
console.log("Remote copy:", val.notes);
var delta = getDelta(self.notes, val.notes);
console.log("Comparison: ", delta);

var notes = delta.noChange;
notes = notes.concat(delta.addedRemote);
notes = notes.concat(delta.changedRemote);

// Sync locally-created or modified notes
var notesToUpload = delta.addedLocal;
notesToUpload = notesToUpload.concat(delta.changedLocal);
var addedOrChangedLocallyAjax = [];
for (var i = 0; i < notesToUpload.length; i++) {
addedOrChangedLocallyAjax.push(self.saveNote(self.fix(notesToUpload[i]), function (n) {
notes.push(n);
}));
}
$.when(addedOrChangedLocallyAjax).then(function () {
self.notes = notes;
self.fixAll();
localStorage.setItem("notes", JSON.stringify(notes));
console.log(JSON.parse(localStorage.getItem("notes")));
if (typeof success == 'function') {
success(notes);
}
});
}
}, error: function () {
if (typeof error == 'function') {
error();
}
}
});
}

+ 58
- 26
www/js/Notes.class.js View File

@@ -10,9 +10,9 @@ class Notes {
this.notes = [];
}

get(id) {
get(noteid) {
for (var i = 0; i < this.notes.length; i++) {
if (this.notes[i].id == id) {
if (this.notes[i].noteid == noteid) {
return this.notes[i];
}
}
@@ -25,38 +25,51 @@ class Notes {

set(note) {
for (var i = 0; i < this.notes.length; i++) {
if (this.notes[i].id == note.id) {
if (this.notes[i].noteid == note.noteid) {
// Refresh HTML rendering
note.html = marked(note.content);

this.notes[i] = note;
this.save();
return;
}
}
this.notes.push(note);
this.save();
}

del(noteid, callback) {
del(noteid, success, error) {
var newnotearr = [];
for (var i = 0; i < this.notes.length; i++) {
if (this.notes[i].id != noteid) {
if (this.notes[i].noteid != noteid) {
newnotearr.push(this.notes[i]);
}
}
this.notes = newnotearr;
if (typeof callback == 'function') {
callback();
this.save();
if (typeof success == 'function') {
success();
}
}

add(note, callback) {
add(note, success, error) {
var noteid = null;
do {
noteid = Math.floor(Math.random() * (9999999999 - 1000000000) + 1000000000);
console.log("Generating random note ID: " + noteid);
} while (this.get(noteid) != null);
note["noteid"] = noteid;
this.notes.push(note);
if (typeof callback == 'function') {
callback();
this.save();
if (typeof success == 'function') {
success(note);
}
}

fix(note) {
if (typeof note.noteid !== 'number') {
note.noteid = null;
}
// Set background color
if (typeof note.color !== 'string') {
note.color = "FFF59D";
@@ -85,39 +98,58 @@ class Notes {
if (typeof note.title !== 'string') {
note.title = note.content.split('\n')[0].replace(/[#\-]+/gi, "").trim();
}
if (typeof note.modified !== 'string') {
note.modified = (new Date()).toISOString();
if (typeof note.modified !== 'number') {
note.modified = Math.round((new Date()).getTime() / 1000);
}
// Render Markdown to HTML
if (typeof note.html !== 'string') {
note.html = marked(note.content);
}
// Save
this.set(note);
return note;
}

fixAll() {
for (var i = 0; i < this.notes.length; i++) {
this.fix(this.notes[i]);
this.notes[i] = this.fix(this.notes[i]);
}
this.save();
}

load(callback) {
if (localStorage.getItem("notes") !== null) {
var data = JSON.parse(localStorage.getItem("notes"));
if (data.length > 0) {
this.notes = data;
}
/**
* Sync notes with the storage backend.
*
* @param {type} success
* @param {type} error
* @returns {undefined}
*/
sync(success, error) {
if (localStorage.getItem("notes") !== null) { // There is localstorage
var storage = JSON.parse(localStorage.getItem("notes"));

console.log("Memory copy:", this.notes);
console.log("Storage copy:", storage);
var delta = getDelta(this.notes, storage);
console.log("Comparison: ", delta);

var notes = delta.noChange;
notes = notes.concat(delta.addedRemote);
notes = notes.concat(delta.changedRemote);
notes = notes.concat(delta.addedLocal);
notes = notes.concat(delta.changedLocal);
// If localStorage is missing something, we still want it
notes = notes.concat(delta.deletedRemote);

this.notes = notes;
this.fixAll();
}
if (typeof callback == 'function') {
callback();
this.save();
if (typeof success == 'function') {
success(this.notes);
}
}

save(callback) {
save() {
localStorage.setItem("notes", JSON.stringify(this.notes));
if (typeof callback == 'function') {
callback();
}
}
}

+ 83
- 0
www/js/delta.js View File

@@ -0,0 +1,83 @@
/*
* The code in this file is by StackOverflow user Juan Mendes.
* License: Creative Commons Attribution-ShareAlike 3.0 Unported (CC BY-SA 3.0).
* Source: https://stackoverflow.com/a/14966749
*/


/**
* Creates a map out of an array be choosing what property to key by
* @param {object[]} array Array that will be converted into a map
* @param {string} prop Name of property to key by
* @return {object} The mapped array. Example:
* mapFromArray([{a:1,b:2}, {a:3,b:4}], 'a')
* returns {1: {a:1,b:2}, 3: {a:3,b:4}}
*/
function mapFromArray(array, prop) {
var map = {};
for (var i = 0; i < array.length; i++) {
map[ array[i][prop] ] = array[i];
}
return map;
}

/**
* @param {object[]} o old array of notes (local copy)
* @param {object[]} n new array of notes (remote copy)
* @param {object} An object with changes
*/
function getDelta(o, n) {
var delta = {
addedRemote: [],
addedLocal: [],
deletedRemote: [],
deletedLocal: [],
changedRemote: [],
changedLocal: [],
noChange: []
};
oSane = [];
for (var i = 0; i < o.length; i++) {
if (o[i].noteid == null) { // Note has no real `noteid`
delta.addedLocal.push(o[i]);
} else {
oSane.push(o[i]);
}
}
var local = mapFromArray(oSane, 'noteid');
var remote = mapFromArray(n, 'noteid');

for (var id in local) {
if (!remote.hasOwnProperty(id)) { // Notes that are only present locally
delta.addedLocal.push(local[id]);
// TODO: Figure out which notes were actually added locally and which were deleted on the server
/*if (local[id].norealid) { // Note hasn't been synced to the remote yet
delta.addedLocal.push(local[id]);
} else { // Note has been synced to remote but isn't there anymore
delta.deletedRemote.push(local[id]);
}*/
} else { // Notes that are present on both
if (local[id].modified > remote[id].modified) { // Local copy is newer
delta.changedLocal.push(local[id]);
} else if (local[id].modified < remote[id].modified) { // Remote copy is newer
delta.changedRemote.push(remote[id]);
} else { // Modified date is same, let's check content
if (local[id].content == remote[id].content) {
delta.noChange.push(local[id]);
} else if (local[id].content.length > remote[id].content.length) {
delta.changedLocal.push(local[id]);
} else {
delta.changedRemote.push(remote[id]);
}
}
}
}

// Add notes that are only on the remote
for (var id in remote) {
if (!local.hasOwnProperty(id)) {
delta.addedRemote.push(remote[id]);
}
}
return delta;
}

+ 26
- 27
www/js/editnote.js View File

@@ -4,38 +4,37 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/


function saveme(callback) {
function finishSave(note, callback) {
NOTES.fixAll();
NOTES.sync(function () {
app.toast.create({
text: 'Note saved.',
closeTimeout: 2000
}).open();
$("#orig_content").val(note.content);
});
if (typeof callback == "function") {
callback();
}
}

var noteid = $("#note_content").data("noteid");
if (noteid == "") {
// Make a very random ID number and check that it won't collide,
// in case the user is good at winning the lottery
do {
noteid = Math.floor(Math.random() * (9999999999 - 1000000000) + 1000000000);
console.log("Generating random note ID: " + noteid);
} while (notes.get(noteid) != null);

var note = {id: noteid};
note.content = $("#note_content").val();
note.modified = (new Date()).toISOString();
notes.add(note);
$("#note_content").data("noteid", noteid);
var note = {
content: $("#note_content").val(),
modified: Math.round((new Date()).getTime() / 1000)
};
NOTES.add(note, function (n) {
$("#note_content").data("noteid", n.noteid);
finishSave(n, callback);
});
} else {
var note = notes.get(noteid);
var note = NOTES.get(noteid);
note.content = $("#note_content").val();
note.modified = (new Date()).toISOString();
notes.set(note);
}
notes.fix(note);
notes.save(function () {
app.toast.create({
text: 'Note saved.',
closeTimeout: 2000
}).open();
$("#orig_content").val(note.content);
});
if (typeof callback == "function") {
callback();
note.modified = Math.round((new Date()).getTime() / 1000);
NOTES.set(note);
finishSave(note, callback);
}
}


+ 2
- 2
www/js/home.js View File

@@ -10,7 +10,7 @@ $(".view-main").on("ptr:refresh", ".ptr-content", function () {
});

function editNote(id) {
var note = notes.get(id);
var note = NOTES.get(id);
router.navigate("/editnote", {
context: {
noteid: id,
@@ -31,7 +31,7 @@ function makeList(id) {

function deleteNote(id) {
app.dialog.confirm('Are you sure?', 'Delete Note', function () {
notes.del(id, function () {
NOTES.del(id, function () {
app.ptr.refresh();
});
});

+ 9
- 1
www/js/main.js View File

@@ -19,6 +19,10 @@ var mainView = app.views.create('.view-main', {

var router = mainView.router;

var NOTES = null;

var OFFLINE = false;

/**
* Thanks to https://stackoverflow.com/a/13542669
* @param {type} color
@@ -54,9 +58,13 @@ router.on("pageInit", function (pagedata) {
// Run platform-specific setup code for Cordova or NW.js
initPlatform();


if (localStorage.getItem("configured") == null) {
// Open the setup page
router.navigate("/setup/0");
} else {
router.navigate("/home");
createNotesObject(function (n) {
NOTES = n;
router.navigate("/home");
});
}

+ 29
- 4
www/js/notes.js View File

@@ -4,8 +4,33 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

if (localStorage.getItem("serverurl") == null) {
var notes = new Notes();
} else {
var notes = new NotePostNotes(localStorage.getItem("serverurl"), localStorage.getItem("username"), localStorage.getItem("password"));
function createNotesObject(callback) {
if (localStorage.getItem("serverurl") == null) {
callback(new Notes());
} else {
var checkurl = localStorage.getItem("serverurl") + "/api/ping";
$.ajax({
url: checkurl,
dataType: "json",
cache: false,
method: "POST",
beforeSend: function (xhr) {
xhr.setRequestHeader("Authorization", "Basic " + btoa(localStorage.getItem("username") + ":" + localStorage.getItem("password")));
}, success: function (data) {
if (data.status == "OK") {
callback(new NotePostNotes(localStorage.getItem("serverurl"), localStorage.getItem("username"), localStorage.getItem("password")));
} else if (data.status == "ERROR") {
app.dialog.alert(data.msg, "Error");
OFFLINE = true;
callback(new Notes());
} else {
OFFLINE = true;
callback(new Notes());
}
}, error: function () {
OFFLINE = true;
callback(new Notes());
}
});
}
}

+ 11
- 1
www/pages/home.html View File

@@ -28,9 +28,19 @@
</div>

<div class="row notecards-row" id="notecards-bin">
{{#if offline}}
<div class="col-100 tablet-50 desktop-33 notecard-col">
<div class="card">
<div class="card-content card-content-padding text-align-center text-color-gray">
<div><i class="material-icons" style="font-size: 40pt;">cloud_off</i></div>
Offline
</div>
</div>
</div>
{{/if}}
{{#each notecards}}
<div class="col-100 tablet-50 desktop-33 notecard-col">
<div class="card notecard" id="notecard-{{id}}" data-id="{{id}}" data-bg="{{color}}" data-fg="{{textcolor}}" style="background-color: #{{color}}; color: #{{textcolor}};">
<div class="card notecard" id="notecard-{{noteid}}" data-id="{{noteid}}" data-bg="{{color}}" data-fg="{{textcolor}}" style="background-color: #{{color}}; color: #{{textcolor}};">
<div class="menubtn">
<i class="material-icons">more_vert</i>
</div>

+ 23
- 19
www/routes.js View File

@@ -10,21 +10,31 @@ var routes = [
path: '/home',
name: 'home',
async: function (routeTo, routeFrom, resolve, reject) {
notes.load(function () {
console.log("Loading");
notes.fixAll();
var notecards = notes.getAll();
var pageinfo = {
templateUrl: './pages/home.html',
reloadCurrent: (routeFrom.name == "home")
};
var context = {
showrefreshbtn: (platform_type != "cordova"),
offline: OFFLINE
};
NOTES.sync(function (notes) {
context["notecards"] = notes;
if (routeFrom.name == "home") {
app.ptr.done();
}
resolve({
templateUrl: './pages/home.html',
reloadCurrent: (routeFrom.name == "home")
}, {
context: {
notecards: notecards,
showrefreshbtn: (platform_type != "cordova")
}
resolve(pageinfo, {
context: context
});
}, function () {
NOTES.fixAll();
context["notecards"] = NOTES.getAll();
if (routeFrom.name == "home") {
app.ptr.done();
}
context["offline"] = true;
resolve(pageinfo, {
context: context
});
});
}
@@ -66,15 +76,9 @@ var routes = [
title: "Sign in to a different account",
onclick: "router.navigate('/setup/0')"
},
{
setting: "opensource",
title: "Open Source Information",
text: "",
onclick: "router.navigate('/credits')"
},
{
setting: "versions",
title: "NotePost app v0.1",
title: "NotePost app v1.0.0",
text: "Copyright &copy; 2018-2019 Netsyms Technologies. License: <span style=\"text-decoration: underline;\" onclick=\"openBrowser('https://source.netsyms.com/Apps/NotePostApp?pk_campaign=NotePostApp');\">Mozilla Public License 2.0</span>.",
onclick: ""
},

Loading…
Cancel
Save