Skip to content

Instantly share code, notes, and snippets.

@humbletim
Last active October 15, 2016 13:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save humbletim/4efa277651f47698197da4bb324ef8db to your computer and use it in GitHub Desktop.
Save humbletim/4efa277651f47698197da4bb324ef8db to your computer and use it in GitHub Desktop.
localStorage.js for Client scripts

localStorage.js + localStorage.html

These two files work together to provide a subset of the HTML5 localStorage API to Client scripts:

(note: links above go to Mozilla Developer Network (MDN) docs)

Compared to HiFi's built-in Settings API, localStorage data is saved separately from Interface.ini -- so is generally able to survive a full cache/settings reset...

See also:

Example:

Script.include('localStorage.js');

var SCRIPT_KEY = 'my-script-key';
var config;

// most reliable way to access is with .$ready(callback)
localStorage.$ready(function() {
  config = JSON.parse(localStorage.getItem() || '{}');
  
  // ... do stuff with config...
  
  // could save config back at script exit
  Script.scriptEnding.connect(function() {
    localStorage.setItem(SCRIPT_KEY, JSON.stringify(config));
  });
});

// in some designs it might be cleaner to assume initialization will have completed
// (which can be verified by calling $ready() without any arguments)
function saveClicked() {
    if (!localStorage.$ready())
        throw new Error('save called before localStorage was ready!');
    localStorage.setItem(SCRIPT_KEY, JSON.stringify(config));
}
<!doctype html>
<html>
<!--
// localStorage.html (see also localStorage.js)
//
// HTML5-like persistence layer for Client scripts (using a wrapped QML WebEngineView)
//
// note: this web page is normally never seen because it's only used from a hidden OverlayWebWindow.
//
// - humbletim @ 2016.10.14
//
-->
<head>
<title>localStorage connector</title>
<script src="qrc:///qtwebchannel/qwebchannel.js"></script>
<script>
function log() {
var out = [].slice.call(arguments).join(' ');
debug.value = debug.value.split('\n').slice(-10).concat(out).join('\n');
}
new QWebChannel(qt.webChannelTransport, function(channel) {
EventBridge = channel.objects.eventBridgeWrapper.eventBridge;
EventBridge.scriptEventReceived.connect(function(raw){
log('scriptEventReceived', raw);
try {
var msg = JSON.parse(raw);
if (msg.id === 'confirm')
return log('...sync confirmed');
if (msg.setItem)
localStorage.setItem(msg.setItem, msg.value);
if (msg.removeItem)
localStorage.removeItem(msg.removeItem);
if (msg.id) {
if (msg.getItem || msg.setItem || msg.removeItem)
EventBridge.emitWebEvent(JSON.stringify({
id: msg.id,
result: localStorage.getItem(msg.getItem || msg.setItem || msg.removeItem)
}));
else if (msg.sync)
EventBridge.emitWebEvent(JSON.stringify({ id: msg.id, localStorage: localStorage }));
}
} catch(e) {
log('error:' ,e);
}
});
setTimeout(function() {
log('initial localStorage sync...');
EventBridge.emitWebEvent(JSON.stringify({ id: 'confirm', localStorage: localStorage }));
}, 100);
});
window.addEventListener('DOMContentLoaded', window.onresize = function() {
debug.style.width = (innerWidth)+'px';
debug.style.height = (innerHeight - debug.offsetTop)+'px';
});
</script>
<style>
body { overflow: hidden; width: 640px; margin:0; padding:0 }
textarea { margin: 0; white-space: nowrap; }
</style>
</head>
<body>
<textarea id=debug readonly></textarea>
</body>
</html>
// localStorage.js (see also localStorage.html)
//
// HTML5-like persistence layer for Client scripts (using a wrapped QML WebEngineView)
//
// standard:
// localStorage.getItem(key)
// localStorage.setItem(key, value)
// localStorage.removeItem(key)
//
// non-standard:
// localStorage.$ready(handler) // allows for async initialization
//
//
// -- humbletim @ 2016.10.14
var version = '0.0.0';
// inline test and usage example
localStorage_js_example = function localStorage_js_example() {
//Script.include('localStorage.js');
// wait until localStorage is $ready (before storing or retrieving any values)
localStorage.$ready(function handler() {
var key = 'test';
print('localStorage keys', Object.keys(localStorage));
// writing values via direct property is NOT supported:
// localStorage[key] = 'string'; // won't work!
// instead, use the standard `setItem` method:
localStorage.setItem(key, 'string');
// reading values via direct properties DOES work (or you can use `getItem`)
var val = localStorage[key];
var val2 = localStorage.getItem(key);
print('localStorage.getItem('+key+')', val);
print('localStorage["'+key+'"]', val2);
// removing values via direct property is NOT supported:
// delete localStorage[key]; // won't work!
// instead, use the standard `removeItem` method:
localStorage.removeItem(key);
print('//localStorage keys', Object.keys(localStorage));
});
}
function log() { print(log.prefix + [].slice.call(arguments).join(' ')); }
// this derives current script filename (including any hash fragment)
try {
throw new Error('stacktrace');
} catch(e) {
var filename = e.fileName;
}
var basename = filename.split(/[?#]/)[0].split('/').pop();
log.prefix = '['+basename+'] ';
log(version);
// sibling localStorage.html page
var HTML_URL = Script.resolvePath('').replace('.js', '.html');
// check for html= override to a different .html location
// eg: Script.include('localStorage.js#html=http://yourdomain/localStorage.html');
// note: only pages on the same domain can see the same localStorage values...
filename.replace(/\bhtml=([^&=]+[.]html[^&=]*)/, function(_, url) {
HTML_URL = url;
});
log('HTML_URL', HTML_URL);
// inline .bind polyfill
Function.prototype.bind = Function.prototype.bind||function(){var fn=this,s=[].slice,a=s.call(arguments),o=a.shift();return function boundFunc(){return fn.apply(o,a.concat(s.call(arguments)))}};
// NOTE: coincidentally WebEngineView stores localStorage data as a simple SQLite3 database
// You can find it under QtWebEngine/ (a sibling of the Logs/ folder)
// On Linux that would be:
// ~/.local/share/High Fidelity/Interface/Logs/ <-- reference point
// ~/.local/share/High Fidelity/Interface/QtWebEngine/qmlWebEngine/Local Storage/*.localstorage
function _Storage(url) {
var synced = false;
var window = new OverlayWebWindow({
title: 'localStorage', source: 'about:blank',
width: 0, height: 0, visible: false
});
// defined as non-enumerable properties
// (so they don't show up in Object.keys or JSON.stringify(localStorage)
Object.defineProperties(this, {
$url: { value: url },
$callbacks: { value: { ready: [ function(s){ log('ready!', s); }] } },
$synced: { get: function() { return synced }, set: function(nv) { synced=nv; } },
$window: { value: window }
});
this.$window.webEventReceived.connect(this, 'onWebEventReceived');
this.$window.setURL(url);
};
_Storage.prototype = {
toString: function() { return '[localStorage '+Object.keys(this)+(this.$synced?' (synced)':'')+']'; },
// enable Client scripts to be notified when localStorage becomes available
$ready: function(callback) {
if (callback) {
if (this.$synced)
this.$try(callback);
else {
log('$ready -- !synced, queued handler');
this.$callbacks.ready.push(callback);
}
}
return this.$synced;
},
// https://developer.mozilla.org/en-US/docs/Web/API/Storage/getItem
getItem: function(key) { return this[key]; },
// https://developer.mozilla.org/en-US/docs/Web/API/Storage/setItem
setItem: function(key, value) {
this.$withCallback({
setItem: key,
value: this[key] = value,
}, function(ret) {
log('stored...', key, ret);
});
},
// https://developer.mozilla.org/en-US/docs/Web/API/Storage/removeItem
removeItem: function(key) {
delete this[key]// = undefined;
this.$withCallback({
removeItem: key
}, function(ret) {
log('removed...', key, ret);
//delete this[key];
});
},
$try: function tryit(callback) {
//log('$try', callback);
try { callback.call(this, this); }
catch(e) { log('$ready error:', e); }
},
$withCallback: function(msg, callback) {
msg.id = msg.id || new Date().getTime().toString(36);
this.$callbacks[msg.id] = function(ret) {
try { callback.call(this, ret); } finally {
delete this.$callbacks[msg.id];
}
}.bind(this);
this.$window.emitScriptEvent(JSON.stringify(msg));
},
// mini RPC service of EventBridge
onWebEventReceived: function(str) {
log('webEventReceived', (str+'').substr(0,60)+'...');
try {
var msg = JSON.parse(str);
var result;
if (msg.localStorage) {
log('sync...', Object.keys(msg.localStorage));
// clear out the previous values
Object.keys(this).forEach(function(k) { delete this[k]; }.bind(this));
// setup the new values
Object.keys(msg.localStorage).forEach(function(k) { this[k] = msg.localStorage[k]; }.bind(this));
result = true;
}
if (msg.id) {
if (this.$callbacks[msg.id]) {
log('$callbacks['+msg.id+']...', str);
this.$callbacks[msg.id].call(this, msg);
} else
this.$window.emitScriptEvent(JSON.stringify({ id: msg.id, result: result }));
}
} catch(e) {
log('webEvent error', msg, e);
}
if (!this.$synced) {
//log('$synced!', this, this.$callbacks.ready && this.$callbacks.ready.length);
this.$synced = true;
if (this.$callbacks.ready)
this.$callbacks.ready.forEach(this.$try.bind(this));
delete this.$callbacks.ready;
}
},
};
// export as a global for Client script access
localStorage = new _Storage(HTML_URL);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment