Using an HTML 5 database instead of localStorage so I can cache data and store preferences in a more organized and quicker manner.

This commit is contained in:
Kerrick Long
2011-06-16 18:33:34 -05:00
parent 0410785162
commit d7c66b5bca
5 changed files with 352 additions and 220 deletions
+7 -1
View File
@@ -1,3 +1,5 @@
## Mostly Harmless ##
This is an upcoming reddit extension for Google Chrome.
1. It looks and functions just like reddit, but is API driven (I'm not just iframing reddit or anything).
@@ -8,4 +10,8 @@ This is an upcoming reddit extension for Google Chrome.
4. Once infobars come out of the expiremental API, I'll be implementing a UI similar to Socialite as well.
5. I hope to open source this and get it officially endorsed by reddit in the same way Socialite and RedditAddict are.
5. I hope to open source this and get it officially endorsed by reddit in the same way Socialite and RedditAddict are.
## Known Issues ##
* After clicking a vote icon, the state doesn't *appear* to change unless you hover over it again. This is a [known bug in Google Chrome](http://code.google.com/p/chromium/issues/detail?id=77246).
+178 -70
View File
@@ -1,13 +1,35 @@
<script>
var reddit_session = null;
setBadgeDefaults();
window.localStorage.clear();
chrome.tabs.onUpdated.addListener(listenToTabs);
chrome.tabs.onRemoved.addListener(cleanCache);
chrome.cookies.get({url: 'http://reddit.com', name: 'reddit_session'}, function(cookie){
reddit_session = cookie.value;
console.log('cookie.value = ' + cookie.value);
});
var db = openDatabase('mhdb', '1.0', 'Mostly Harmless Database', 5 * 1024 * 1024);
var cacheTime;
var over18;
init();
function init() {
setBadgeDefaults();
chrome.tabs.onUpdated.addListener(listenToTabs);
if(window.localStorage.getItem('installed') !== 'true') {
installDefaults();
}
db.transaction(function(tx){
tx.executeSql('SELECT * FROM prefs WHERE pref=?', ['cacheTime'], function(tx, results) {
cacheTime = results.rows.item(0).choice;
});
tx.executeSql('SELECT * FROM prefs WHERE pref=?', ['over18'], function(tx, results) {
over18 = results.rows.item(0).choice;
});
});
}
function installDefaults(tx) {
window.localStorage.setItem('installed','true');
db.transaction(function(tx) {
tx.executeSql('CREATE TABLE IF NOT EXISTS cache (pageUrl unique, cacheTime, howManyPosts)');
tx.executeSql('CREATE TABLE IF NOT EXISTS posts (id unique, name, likes, domain, subreddit, author, score, over_18, hidden, thumbnail, downs, permalink, created_utc, url, title, num_comments, ups, modhash)')
tx.executeSql('CREATE TABLE IF NOT EXISTS prefs (pref unique, choice)');
tx.executeSql('INSERT INTO prefs (pref, choice) VALUES (?, ?)', ['cacheTime','1']);
tx.executeSql('INSERT INTO prefs (pref, choice) VALUES (?, ?)', ['over18','false']);
});
}
function setBadgeDefaults(tabId) {
chrome.browserAction.setBadgeBackgroundColor({
@@ -32,83 +54,169 @@
});
}
}
function cleanCache() {
// find old caches from localStorage and remove anything older than ___.
}
function listenToTabs(tabId,changeInfo,tab){
if(changeInfo.status === 'loading') {
grabData(tab.url,tabId);
}
}
function grabData(url,tabId) {
//check if the url has been cached recently
if(window.localStorage.getItem(url) === null || JSON.parse(window.localStorage.getItem(url)).cachetime - new Date < -60000) {
console.log('Loading from reddit api...');
var reqUrl = 'http://www.reddit.com/api/info.json?url=' + encodeURI(url);
var api = new XMLHttpRequest();
api.open('GET',reqUrl,false);
api.send(null);
if(api.status !== 200) {
console.error('Error loading API.\nURL: ' + reqUrl + '\nStatus: ' + api.status);
console.log(api);
setBadgeDefaults(tabId);
}
api.onload = prepareButton(JSON.parse(api.responseText),url,tabId);
} else {
console.log('Loading from cache...');
prepareButton(JSON.parse(window.localStorage.getItem(url)).response,url,tabId);
}
// If the URL hasn't been cached recently, fetch it from the API.
db.transaction(function(tx) {
tx.executeSql('SELECT * FROM cache WHERE pageUrl=?', [url], function(tx, results) {
var cache = results.rows;
if(cache.length === 0 || -(cache.item(0).cacheTime - epoch()) > 60 * cacheTime ) { // cacheTime in minutes
console.log('Loading from reddit api...');
var reqUrl = 'http://www.reddit.com/api/info.json?url=' + encodeURI(url);
var api = new XMLHttpRequest();
api.open('GET',reqUrl,false);
api.send(null);
if(api.status !== 200) {
console.error('Error loading API.\nURL: ' + reqUrl + '\nStatus: ' + api.status);
console.log(api);
setBadgeDefaults(tabId);
}
api.onload = cacheData(JSON.parse(api.responseText),url,tabId);
} else {
console.log('Loading from cache...');
preparePopup(url,tabId);
}
});
});
}
function prepareButton(response,pageUrl,tabId) {
function epoch() {
return Math.floor(new Date().getTime()/1000);
}
function cacheData(response,pageUrl,tabId) {
// add response to cache to reduce API calls
if(window.localStorage.getItem(pageUrl) === null || JSON.parse(window.localStorage.getItem(pageUrl)).cachetime - new Date < -60000) {
var toCache = new Object();
toCache.cachetime = new Date();
toCache.response = response;
window.localStorage.setItem(pageUrl,JSON.stringify(toCache));
delete toCache;
}
// counts submissions and prepare the browserAction button appropriately
var numberOfSubmissions = response.data.children.length.toString();
if(numberOfSubmissions > 0) {
chrome.browserAction.setTitle({
title: 'This page has been submitted to reddit ' + numberOfSubmissions + ' times.',
tabId: tabId
if(response.data.children.length === 0) {
db.transaction(function(tx) {
tx.executeSql('INSERT OR REPLACE INTO cache (pageUrl, cacheTime, howManyPosts) VALUES (?, ?, ?)',[pageUrl, epoch(), '0',]);
});
chrome.browserAction.setBadgeText({
text: numberOfSubmissions,
tabId: tabId
});
chrome.browserAction.setPopup({
popup: 'popup.html',
tabId: tabId
});
chrome.browserAction.setBadgeBackgroundColor({
color: [255,69,0,255], //r,g,b,a,
tabId: tabId
});
chrome.browserAction.onClicked.removeListener(submitToReddit);
} else {
chrome.browserAction.setTitle({
title: 'Submit this page to reddit',
tabId: tabId
db.transaction(function(tx) {
for(var i = 0; i < response.data.children.length; i++) {
var data = response.data.children[i].data;
tx.executeSql(
'INSERT OR REPLACE INTO posts' +
'(id, name, likes, domain, subreddit, author, score, over_18, hidden, thumbnail, downs, permalink, created_utc, url, title, num_comments, ups, modhash)' +
'VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
[data.id, data.name, data.likes, data.domain, data.subreddit, data.author, data.score, data.over_18, data.hidden, data.thumbnail, data.downs, data.permalink, data.created_utc, data.url, data.title, data.num_comments, data.ups, response.data.modhash],
function(tx) {
tx.executeSql('INSERT OR REPLACE INTO cache (pageUrl, cacheTime, howManyPosts) VALUES (?, ?, ?)',[pageUrl, epoch(), response.data.children.length]);
});
}
});
chrome.browserAction.setBadgeText({
text: '',
tabId: tabId
});
chrome.browserAction.setPopup({
popup: '',
tabId: tabId
});
chrome.browserAction.onClicked.removeListener(submitToReddit);
chrome.browserAction.onClicked.addListener(submitToReddit);
}
preparePopup(pageUrl,tabId);
}
function preparePopup(url,tabId) {
// counts submissions and prepare the browserAction button appropriately
var numberOfSubmissions;
db.transaction(function(tx){
tx.executeSql('SELECT * FROM cache WHERE pageUrl=?', [url], function(tx, results) {
numberOfSubmissions = results.rows.item(0).howManyPosts;
if(numberOfSubmissions > 0) {
chrome.browserAction.setTitle({
title: 'This page has been submitted to reddit ' + numberOfSubmissions + ' times.',
tabId: tabId
});
chrome.browserAction.setBadgeText({
text: numberOfSubmissions.toString(),
tabId: tabId
});
chrome.browserAction.setPopup({
popup: 'popup.html',
tabId: tabId
});
chrome.browserAction.setBadgeBackgroundColor({
color: [255,69,0,255], //r,g,b,a,
tabId: tabId
});
chrome.browserAction.onClicked.removeListener(submitToReddit);
} else {
chrome.browserAction.setTitle({
title: 'Submit this page to reddit',
tabId: tabId
});
chrome.browserAction.setBadgeText({
text: '',
tabId: tabId
});
chrome.browserAction.setPopup({
popup: '',
tabId: tabId
});
chrome.browserAction.onClicked.removeListener(submitToReddit);
chrome.browserAction.onClicked.addListener(submitToReddit);
}
});
});
}
function submitToReddit(tab){
chrome.tabs.create({
url: 'http://www.reddit.com/submit?url=' + encodeURI(tab.url)
});
}
/*
* JavaScript Pretty Date
* Thanks to Dean Landolt's comment on
* http://ejohn.org/blog/javascript-pretty-date/#postcomment
*/
// Takes an ISO time and returns a string representing how
// long ago the date represents.
function prettyDate(date_str){
var time = ('' + date_str).replace(/-/g,"/").replace(/[TZ]/g," ");
var seconds = (new Date - new Date(time)) / 1000;
var token = 'ago', list_choice = 1;
if (seconds < 0) {
seconds = Math.abs(seconds);
token = 'from now';
list_choice = 2;
}
var i = 0, format;
while (format = time_formats[i++]) if (seconds < format[0]) {
if (typeof format[2] == 'string')
return format[list_choice];
else
return Math.floor(seconds / format[2]) + ' ' + format[1] + ' ' + token;
}
return time;
};
var time_formats = [
[60, 'just now', 1], // 60
[120, '1 minute ago', '1 minute from now'], // 60*2
[3600, 'minutes', 60], // 60*60, 60
[7200, '1 hour ago', '1 hour from now'], // 60*60*2
[86400, 'hours', 3600], // 60*60*24, 60*60
[172800, 'yesterday', 'tomorrow'], // 60*60*24*2
[604800, 'days', 86400], // 60*60*24*7, 60*60*24
[1209600, 'last week', 'next week'], // 60*60*24*7*4*2
[2419200, 'weeks', 604800], // 60*60*24*7*4, 60*60*24*7
[4838400, 'last month', 'next month'], // 60*60*24*7*4*2
[29030400, 'months', 2419200], // 60*60*24*7*4*12, 60*60*24*7*4
[58060800, 'last year', 'next year'], // 60*60*24*7*4*12*2
[2903040000, 'years', 29030400], // 60*60*24*7*4*12*100, 60*60*24*7*4*12
[5806080000, 'last century', 'next century'], // 60*60*24*7*4*12*100*2
[58060800000, 'centuries', 2903040000] // 60*60*24*7*4*12*100*20, 60*60*24*7*4*12*100
];
/*
* ISO 8601 Formatted Dates
* Gotten from the Mozilla Developer Center
*/
function ISODateString(d){
function pad(n){return n<10 ? '0'+n : n}
return d.getUTCFullYear()+'-'
+ pad(d.getUTCMonth()+1)+'-'
+ pad(d.getUTCDate())+'T'
+ pad(d.getUTCHours())+':'
+ pad(d.getUTCMinutes())+':'
+ pad(d.getUTCSeconds())+'Z'}
</script>
+3 -3
View File
@@ -7,8 +7,8 @@
},
"permissions": [
"http://*.reddit.com/",
"tabs",
"cookies"
"tabs"
],
"background_page": "background.html"
"background_page": "background.html",
"options_page": "options.html"
}
+1
View File
@@ -0,0 +1 @@
Hello, world.
+163 -146
View File
@@ -1,7 +1,6 @@
<!doctype html>
<html>
<head>
<title>TESTING the title</title>
<meta charset="utf-8">
<style>
body {
@@ -161,165 +160,183 @@
}
</style>
<script>
chrome.windows.getCurrent(function(currWindow) {
chrome.tabs.getSelected(currWindow.id, function(currTab) {
processApi(JSON.parse(window.localStorage.getItem(currTab.url)).response);
});
});
function processApi(response) {
document.getElementById('posts').setAttribute('data-modhash',response.data.modhash);
document.getElementById('posts').setAttribute('data-url',response.data.children[0].data.url);
var children = response.data.children;
var now = new Date();
for(var i = 0; i < children.length; i++) {
var data = children[i].data;
var entry = document.createElement('li');
entry.id = data.name;
if (data.likes === true) entry.setAttribute('data-dir','1');
if (data.likes === null) entry.setAttribute('data-dir','0');
if (data.likes === false) entry.setAttribute('data-dir','-1');
var votes = document.createElement('div');
votes.className = 'votes';
var upmod = document.createElement('a');
upmod.className = 'upmod';
upmod.addEventListener('click',upmodPost);
votes.appendChild(upmod);
var count = document.createElement('span');
count.className = 'count';
count.id = 'count_' + data.name;
count.innerHTML = data.score;
count.title = data.ups + ' up votes, ' + data.downs + ' down votes';
votes.appendChild(count);
var downmod = document.createElement('a');
downmod.className = 'downmod';
downmod.id = 'down_' + data.name;
votes.appendChild(downmod);
entry.appendChild(votes);
var thumblink = document.createElement('a');
thumblink.className = 'thumblink';
thumblink.href = 'http://www.reddit.com' + data.permalink;
thumblink.target = '_blank';
thumblink.title = 'View this submission on reddit';
var thumb = document.createElement('img');
thumb.className = 'thumb';
data.thumbnail.indexOf('/') === 0 ? thumb.src = 'http://www.reddit.com' + data.thumbnail : thumb.src = data.thumbnail;
thumb.alt = data.title;
thumblink.appendChild(thumb);
entry.appendChild(thumblink);
var post = document.createElement('div');
post.className = 'post';
var link = document.createElement('a');
link.className = 'link';
link.href = 'http://www.reddit.com' + data.permalink;
link.target = '_blank';
link.innerHTML = data.title;
link.title = 'View this submission on reddit';
post.appendChild(link);
var space = document.createTextNode(' ');
post.appendChild(space);
var domain = document.createElement('a');
domain.className = 'domain';
domain.href = 'http://www.reddit.com/domain/' + data.domain + '/';
domain.target = '_href';
domain.innerHTML = '(' + data.domain + ')';
post.appendChild(domain);
var meta = document.createElement('div');
meta.className = 'meta';
var timestamp = document.createElement('span');
timestamp.className = 'timestamp';
timestamp.innerHTML = 'submitted ' + prettyDate(ISODateString(new Date(data.created_utc * 1000)));
meta.appendChild(timestamp);
var by = document.createTextNode(' by ');
meta.appendChild(by);
var submitter = document.createElement('a');
submitter.className = 'submitter';
submitter.href = 'http://www.reddit.com/user/' + data.author + '/';
submitter.target = '_blank';
submitter.innerHTML = data.author;
meta.appendChild(submitter);
var to = document.createTextNode(' to ');
meta.appendChild(to);
var subreddit = document.createElement('a');
subreddit.className = 'subreddit';
subreddit.href = 'http://www.reddit.com/r/' + data.subreddit + '/';
subreddit.target = '_blank';
subreddit.innerHTML = data.subreddit;
meta.appendChild(subreddit);
post.appendChild(meta);
var actions = document.createElement('div');
actions.className = 'actions';
var comments = document.createElement('a');
comments.className = 'comments';
comments.href = 'http://www.reddit.com' + data.permalink;
comments.target = '_blank';
comments.innerHTML = data.num_comments + ' comments';
actions.appendChild(comments);
post.appendChild(actions);
var share = document.createElement('a');
share.className = 'share';
share.innerHTML = 'share';
actions.appendChild(share);
var save = document.createElement('a');
save.className = 'save';
save.innerHTML = data.saved === true ? 'saved' : 'save';
actions.appendChild(save);
var hide = document.createElement('a');
hide.className = 'hide';
if(data.hidden === true) {
entry.className += ' hidden';
hide.innerHTML = 'unhide'
} else {
hide.innerHTML = 'hide';
}
actions.appendChild(hide);
var report = document.createElement('a');
report.className = 'report';
report.innerHTML = 'report';
actions.appendChild(report);
entry.appendChild(post);
document.getElementById('posts').appendChild(entry);
var db = openDatabase('mhdb', '1.0', 'Mostly Harmless Database', 5 * 1024 * 1024);
var cacheTime;
var over18;
init();
function init() {
if(window.localStorage.getItem('installed') !== 'true') {
installDefaults();
}
document.getElementById('submit').href = 'http://www.reddit.com/submit?url=' + encodeURI(document.getElementById('posts').getAttribute('data-url'));
db.transaction(function(tx){
tx.executeSql('SELECT * FROM prefs WHERE pref=?', ['cacheTime'], function(tx, results) {
cacheTime = results.rows.item(0).choice;
});
tx.executeSql('SELECT * FROM prefs WHERE pref=?', ['over18'], function(tx, results) {
over18 = results.rows.item(0).choice;
});
});
}
chrome.tabs.getSelected(undefined, function(currTab) {
buildPage(currTab.url);
});
function buildPage(pageUrl) {
db.transaction(function(tx){
console.log(pageUrl);
tx.executeSql('SELECT * FROM posts WHERE url=?', [pageUrl], function(tx, results) {
console.log(results.rows.item(0));
document.getElementById('posts').setAttribute('data-modhash',results.rows.item(0).modhash);
document.getElementById('posts').setAttribute('data-url',pageUrl);
var children = results.rows;
var now = new Date();
for(var i = 0; i < children.length; i++) {
var data = children.item(i);
var entry = document.createElement('li');
entry.id = data.name;
if (data.likes === 'true') entry.setAttribute('data-dir','1');
if (data.likes === null) entry.setAttribute('data-dir','0');
if (data.likes === 'false') entry.setAttribute('data-dir','-1');
var votes = document.createElement('div');
votes.className = 'votes';
var upmod = document.createElement('a');
upmod.className = 'upmod';
upmod.addEventListener('click',upmodPost);
votes.appendChild(upmod);
var count = document.createElement('span');
count.className = 'count';
count.id = 'count_' + data.name;
count.innerHTML = data.score;
count.title = data.ups + ' up votes, ' + data.downs + ' down votes';
votes.appendChild(count);
var downmod = document.createElement('a');
downmod.className = 'downmod';
downmod.id = 'down_' + data.name;
votes.appendChild(downmod);
entry.appendChild(votes);
var thumblink = document.createElement('a');
thumblink.className = 'thumblink';
thumblink.href = 'http://www.reddit.com' + data.permalink;
thumblink.target = '_blank';
thumblink.title = 'View this submission on reddit';
var thumb = document.createElement('img');
thumb.className = 'thumb';
data.thumbnail.indexOf('/') === 0 ? thumb.src = 'http://www.reddit.com' + data.thumbnail : thumb.src = data.thumbnail;
thumb.alt = data.title;
thumblink.appendChild(thumb);
entry.appendChild(thumblink);
var post = document.createElement('div');
post.className = 'post';
var link = document.createElement('a');
link.className = 'link';
link.href = 'http://www.reddit.com' + data.permalink;
link.target = '_blank';
link.innerHTML = data.title;
link.title = 'View this submission on reddit';
post.appendChild(link);
var space = document.createTextNode(' ');
post.appendChild(space);
var domain = document.createElement('a');
domain.className = 'domain';
domain.href = 'http://www.reddit.com/domain/' + data.domain + '/';
domain.target = '_href';
domain.innerHTML = '(' + data.domain + ')';
post.appendChild(domain);
var meta = document.createElement('div');
meta.className = 'meta';
var timestamp = document.createElement('span');
timestamp.className = 'timestamp';
timestamp.innerHTML = 'submitted ' + prettyDate(ISODateString(new Date(data.created_utc * 1000)));
meta.appendChild(timestamp);
var by = document.createTextNode(' by ');
meta.appendChild(by);
var submitter = document.createElement('a');
submitter.className = 'submitter';
submitter.href = 'http://www.reddit.com/user/' + data.author + '/';
submitter.target = '_blank';
submitter.innerHTML = data.author;
meta.appendChild(submitter);
var to = document.createTextNode(' to ');
meta.appendChild(to);
var subreddit = document.createElement('a');
subreddit.className = 'subreddit';
subreddit.href = 'http://www.reddit.com/r/' + data.subreddit + '/';
subreddit.target = '_blank';
subreddit.innerHTML = data.subreddit;
meta.appendChild(subreddit);
post.appendChild(meta);
var actions = document.createElement('div');
actions.className = 'actions';
var comments = document.createElement('a');
comments.className = 'comments';
comments.href = 'http://www.reddit.com' + data.permalink;
comments.target = '_blank';
comments.innerHTML = data.num_comments + ' comments';
actions.appendChild(comments);
post.appendChild(actions);
var share = document.createElement('a');
share.className = 'share';
share.innerHTML = 'share';
actions.appendChild(share);
var save = document.createElement('a');
save.className = 'save';
save.innerHTML = data.saved === true ? 'saved' : 'save';
actions.appendChild(save);
var hide = document.createElement('a');
hide.className = 'hide';
if(data.hidden === true) {
entry.className += ' hidden';
hide.innerHTML = 'unhide'
} else {
hide.innerHTML = 'hide';
}
actions.appendChild(hide);
var report = document.createElement('a');
report.className = 'report';
report.innerHTML = 'report';
actions.appendChild(report);
entry.appendChild(post);
document.getElementById('posts').appendChild(entry);
}
document.getElementById('submit').href = 'http://www.reddit.com/submit?url=' + encodeURI(document.getElementById('posts').getAttribute('data-url'));
});
});
}
function upmodPost() {
var formData = new FormData();
formData.append('id',this.parentNode.parentNode.id);
var listItem = this.parentNode.parentNode;
formData.append('id',listItem.id);
formData.append('uh',document.getElementById('posts').getAttribute('data-modhash'));
var voteWas = this.parentNode.parentNode.getAttribute('data-dir');
var voteCount = document.getElementById('count_' + this.parentNode.parentNode.id)
var voteCount = document.getElementById('count_' + listItem.id)
if(voteWas === '1') formData.append('dir','0');
if(voteWas === '0') formData.append('dir','1');
if(voteWas === '-1') formData.append('dir','1');
var api = new XMLHttpRequest();
api.open('POST','http://www.reddit.com/api/vote',false);
api.send(formData);
if (api.statusText === 'OK') {
if(voteWas === '1') {
this.parentNode.parentNode.setAttribute('data-dir','0');
voteCount.innerHTML --;
db.transaction(function(tx) {
if (api.statusText === 'OK') {
if(voteWas === '1') {
listItem.setAttribute('data-dir','0');
tx.executeSql('UPDATE posts SET likes=? WHERE name=?', [null, listItem.id]);
}
if(voteWas === '0') {
listItem.setAttribute('data-dir','1');
tx.executeSql('UPDATE posts SET likes=? WHERE name=?', ['true', listItem.id]);
}
if(voteWas === '-1') {
listItem.setAttribute('data-dir','1');
tx.executeSql('UPDATE posts SET likes=? WHERE name=?', ['true', listItem.id]);
}
voteCount.title = 'Voted!';
} else {
console.error('Error voting.\n' + api.statusText);
console.warn(api);
return false;
}
if(voteWas === '0') {
this.parentNode.parentNode.setAttribute('data-dir','1');
voteCount.innerHTML ++;
}
if(voteWas === '-1') {
this.parentNode.parentNode.setAttribute('data-dir','1');
voteCount.innerHTML ++;
voteCount.innerHTML ++;
}
voteCount.title = 'Voted!';
window.localStorage.removeItem(document.getElementById('posts').getAttribute('data-url'));
// TODO:
// Since I'm removing the data from the cache after voting, I need to add a
// message passing function so that if the url isn't in the cache when the
// popup is invoked, background.html will request the data and cache it so
// the popup can read it.
} else {
console.error('Error voting.\n' + api.statusText);
console.warn(api);
return false;
}
});
}