Skip to content

Instantly share code, notes, and snippets.

@Milly
Last active September 2, 2018 13:03
Show Gist options
  • Save Milly/87c9b0bfbcf89ab98d77 to your computer and use it in GitHub Desktop.
Save Milly/87c9b0bfbcf89ab98d77 to your computer and use it in GitHub Desktop.
Twipla printlist enhance. for Greasemonky user script
// ==UserScript==
// @name Twipla printlist enhance
// @namespace milly.ca
// @include https://twipla.jp/events/printlist/*
// @version 2.4.0
// @require https://ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/store.js/1.3.17/store.min.js
// @require https://raw.githubusercontent.com/eligrey/FileSaver.js/1.3.3/FileSaver.min.js
// @require https://evidenceprime.github.io/html-docx-js/build/html-docx.js
// @grant GM_xmlhttpRequest
// @source https://gist.github.com/Milly/87c9b0bfbcf89ab98d77
// @updateURL https://gist.github.com/Milly/87c9b0bfbcf89ab98d77/raw/Twipla_printlist_enhance.user.js
// ==/UserScript==
/* eslint new-cap:0 */
/* global GM_xmlhttpRequest */
(function main() {
'use strict';
var CHANGE_ICON_TO_LARGER = false;
var CREATE_ICON_LINK = true;
var CREATE_TWITTER_ID_LINK = true;
var SEPARATE_ID_AND_NAME_COLUMN = true;
var ENABLE_INITIAL_DISTINCT = true;
var CREATE_SERIAL_NUMBER = true;
var CREATE_ICON_FILENAME = true;
var CREATE_RESERVE_CHECKBOX = true;
var CREATE_U19_CHECKBOX = true;
var ENABLE_NAMEPLATE = true;
var ENABLE_NAMEEDIT = true;
var ENABLE_ORPHAN_SEARCH = true;
var NAMEPLATE_SAVE_AS_DOCX = false;
var RESERVE_INITIAL = '\xff';
var STORAGE_PREFIX = location.pathname + ':';
var $ = jQuery;
var $store = store;
var TITLE = $('body>.container>h1:nth(1)').text();
$('<style type="text/css"></style>').text([
'#entries { width: auto !important; }',
'#entries th { white-space: nowrap !important; background-color: #e0e0e0; }',
'#entries tr:nth-child(odd) { background-color: #e0e8ff; }',
'#entries th, #entries td { border: 2px solid #000000; }',
'.head-initial { width: 3.4em !important; }',
'.head-serialno { width: 4.4em !important; }',
'.head-icon { width: 50px !important; }',
'.head-filename { width: 8em !important; }',
'.head-id { width: 8em !important; }',
'.head-name { width: 20em !important; }',
'.head-profile { width: auto !important; }',
'.head-checkbox { width: 2.4em !important; }',
'.initial { vertical-align: top; background-color: #ffffff; text-align: center; font-weight: bold; }',
'.serialno { text-align: right; }',
'.reserve,.u19 { text-align: center; }',
'#nameplate-edit { margin-top: 1em; }',
'#nameplate-style, #nameplate-body { margin-bottom: 1ex; width: 100%; height: 20em; }',
'#u19label { width: 5em; }',
'#nameplate-sample { margin: 1em 0 2em 0; width: 100%; height: 300pt; }',
'#control button, #nameplate-edit-control button { margin-left: 0.5ex; height: 1.5em; vertical-align: middle; }',
'#nameplate-edit-control input { vertical-align: middle; }',
'#orphan-search-base { position: fixed; width: 100%; height: 100%; top: 0; left: 0;',
' z-index: 99999; background-color: rgba(0, 0, 0, 0.8); }',
'#orphan-search-progress { display: block; position: absolute; width: 50%; height: 2em;',
' margin: auto; top: 0; bottom: 0; left: 0; right: 0; }',
].join('')).appendTo(document.head);
var LABELS = {
initial : '頭文字',
serialno : '整理番号',
icon : 'Icon',
filename : 'ファイル名',
id : 'ID',
name : '名前',
profile : 'プロフィール',
reserve : '補欠',
u19 : 'U19',
};
var NAMEPLATE_DOCX_NAME = 'nameplates.docx';
var NAMEPLATE_HTML_NAME = 'nameplates.html';
var NAMEPLATE_STYLE = '';
var NAMEPLATE_TEMPLATE = [
'<table border="0" style="table-layout:fixed; width:285pt; margin: 0 -1px -1px 0; border:solid black 1px; padding:0; float:left; overflow:hidden;">',
' <tr>',
' <td style="height:40pt; width:31%; border:none; text-align:center; vertical-align:middle;">',
' <div style="font-size:10pt;">整理番号:@{serialno}</div>',
' </td>',
' <td style="height:136pt; border:none; text-align:center; vertical-align:middle;" colspan="2" rowspan="2">',
' <div style="font-size:24pt; color:red;">@{u19}</div>',
' <div style="font-size:24pt;">@{name}</div>',
' <div style="font-size:24pt;">@{id}</div>',
' </td>',
' </tr>',
' <tr>',
' <td style="height:96pt; border:none; text-align:center; vertical-align:middle; overflow:hidden;">',
' <img src="@{icon}" style="height:80pt;">',
' </td>',
' </tr>',
' <tr>',
' <td style="height:52pt; width:69%; border:none; text-align:left; vertical-align:bottom;" colspan="2">',
' <ul style="padding-left:15pt; font-size:9pt;">',
' <li style="margin-bottom:4pt;">この名札はお帰りの際にご返却下さい。</li>',
' <li>この名札を必ず着用してください。</li>',
' </ul>',
' </td>',
' <td style="width:31%; border:none; text-align:center; vertical-align:middle; " rowspan="2">',
' <img src="http://tsunagarumirai.com/images/tsunagarumirai2017_mv_day1.jpg" style="height:110pt;">',
' </td>',
' </tr>',
' <tr>',
' <td style="height:74pt; border:none; text-align:center; vertical-align:middle;" colspan="2">',
' <img src="http://tsunagarumirai.com/images/tsunagarumirai2017_logo_w1500px.png" style="width:90%;">',
' </td>',
' </tr>',
'</table>',
].join('\n');
var U19_LABEL = 'U19';
var NAMEPLATE_DOCUMENT_OPTIONS = {
orientation: 'portrait',
margins: {
top: 181,
bottom: 181,
left: 357,
right: 357,
},
};
var table = $('table').prop('id', 'entries');
var control = $('<span id="control"></span>').insertBefore(table);
function toNormalizedId(str) {
return str
.replace(new RegExp('^'+RESERVE_INITIAL+'?@'), '')
.toUpperCase()
.replace(/[^0-9A-Z]/g, '_');
}
function toSortString(str) {
var reserve = (str[0] == RESERVE_INITIAL) ? RESERVE_INITIAL : '';
return reserve + toNormalizedId(str).replace(/_/g, '!');
}
function toInitial(str) {
var c = toSortString(str)[0];
if (c == RESERVE_INITIAL) return '補欠';
if (c.match(/^[0-9]/)) return '数字';
if (c.match(/^[^0-9A-Z]/)) return '記号';
return c;
}
function compareRowById(a, b) {
a = toSortString($(a).getRowId());
b = toSortString($(b).getRowId());
return (a > b) ? 1 : (a < b) ? -1 : 0;
}
function loadState() {
if (CREATE_RESERVE_CHECKBOX) loadCheckboxes('reserve');
if (CREATE_U19_CHECKBOX) loadCheckboxes('u19');
if (ENABLE_NAMEEDIT) loadTexts('name');
if (ENABLE_ORPHAN_SEARCH) {
loadTexts('id', /* @this HTMLElement */ function(id) {
var url = 'https://twitter.com/' + id.replace(/^@/, '');
$(this).find('a').attr('href', url).text(id);
});
loadTexts('icon', /* @this HTMLElement */ function(url) {
$(this).find('a').attr('href', url)
.find('img').attr('src', url);
});
}
if (ENABLE_INITIAL_DISTINCT) createOrUpdateInitialColumn();
updateSerialNumbers();
}
function saveCheckboxes(type) {
var sets = table.find('tr:has(td.'+type+' :checked)')
.map(/* @this HTMLElement */ function() { return $(this).data('entry-id'); }).toArray();
if (sets.length) {
$store.set(STORAGE_PREFIX + type + 's', sets);
} else {
$store.remove(STORAGE_PREFIX + type + 's');
}
}
function loadCheckboxes(type) {
var sets = $store.get(STORAGE_PREFIX + type + 's') || [];
table.find('>tbody>tr')
.find('td.'+type+'>input').prop('checked', false).end()
.filter(/* @this HTMLElement */ function() { return 0 <= $.inArray($(this).data('entry-id'), sets); })
.find('td.'+type+'>input').prop('checked', true);
}
function saveTexts(type, getValue) {
getValue = getValue || /* @this HTMLElement */ function() { return $(this).text(); };
var texts = {};
var changed = table.find('td.'+type)
.filter(/* @this HTMLElement */ function() { return $(this).data('changed') || false; })
.each(/* @this HTMLElement */ function() { texts[$(this).closest('tr').data('entry-id')] = getValue.call(this); });
if (changed.length) {
$store.set(STORAGE_PREFIX + type + 's', texts);
} else {
$store.remove(STORAGE_PREFIX + type + 's');
}
}
function loadTexts(type, setValue) {
setValue = setValue || /* @this HTMLElement */ function(text) { $(this).text(text); };
var texts = $store.get(STORAGE_PREFIX + type + 's') || {};
table.find('td.'+type).each(/* @this HTMLElement */ function() {
var id = $(this).closest('tr').data('entry-id');
if (texts.hasOwnProperty(id)) {
$(this).data('changed', true);
setValue.call(this, texts[id]);
}
});
}
$.fn.extend({
addButton: function(label, id, proc) {
return $('<button>').prop({id: 'button-'+id}).text(label).appendTo(this).click(proc);
},
addToggleButton: function(label, id) {
return $(this).addButton(label, id, function(ev) { table.find('.head-'+id+',.'+id).toggle(); });
},
getRowId: function() {
var id = $(this).find('td.id').text();
if (id === '') return '';
var reserve = $(this).find('td.reserve>input').prop('checked');
return reserve ? RESERVE_INITIAL + id : id;
},
findRowId: function(id) {
return $(this).find('#entry-'.toNormalizedId(id));
},
getRowData: function() {
var row = $(this);
return {
'serialno': row.find('.serialno').text(),
'icon': row.find('.icon a').prop('href'),
'id': row.find('.id').text(),
'name': row.find('.name').text(),
'u19': row.find('.u19 input').is(':checked') ? $('#u19label').val() : '',
};
},
});
// create thead
{
var ids = ['icon', 'id', 'profile'];
var head_row = table.find('>tbody>tr:has(th)');
head_row.find('th').removeAttr('width');
$('<thead></thead>').append(head_row).prependTo(table);
$.each(ids, function(idx, id) {
table.find('>thead>tr>th:nth-child('+(idx+1)+')').addClass('head-' + id);
table.find('>tbody>tr>td:nth-child('+(idx+1)+')').addClass(id);
});
table.find('th.head-icon').text('Icon');
}
// change icon to larger
if (CHANGE_ICON_TO_LARGER) {
table.find('img').prop('src', function(_, value) {
return value.replace(/_normal\b/, '');
});
}
// create icon link
if (CREATE_ICON_LINK) {
table.find('img').wrap(/* @this HTMLElement */ function() {
return $('<a>').prop({
href: this.src.replace(/_normal\b/, ''),
title: this.parentNode.nextSibling.firstChild.textContent,
});
});
}
// create icon filename
if (CREATE_ICON_FILENAME) {
table.find('th.head-icon').after('<th class="head-filename">ファイル名</th>');
table.find('td.icon').after('<td class="filename"></td>');
table.find('td.filename').text(/* @this HTMLElement */ function() { return $(this).prev('td.icon').find('img').prop('src').replace(/.*\//, ''); });
}
// separate ID and Name column
if (SEPARATE_ID_AND_NAME_COLUMN) {
table.find('th.head-id').replaceWith('<th class="head-id">ID</th><th class="head-name">名前</th>');
table.find('td.id').replaceWith(/* @this HTMLElement */ function() {
var id = this.firstChild.textContent;
var name = this.lastChild.textContent;
$(this).closest('tr').prop('id', 'entry-'+toNormalizedId(id)).data('entry-id', id);
var twitter_url = null;
if (CREATE_TWITTER_ID_LINK && id[0] == '@') {
twitter_url = 'https://twitter.com/' + id.substr(1);
}
var col_id = (twitter_url) ?
$('<td class="id"><a></a></td>').children('a').prop({href: twitter_url}).text(id).end():
$('<td class="id"></td>').text(id);
var col_name = $('<td class="name"></td>').text(name);
return col_id.add(col_name);
});
}
// initial distinct
if (ENABLE_INITIAL_DISTINCT) createOrUpdateInitialColumn();
function createOrUpdateInitialColumn() {
table.find('.head-initial,.initial').remove();
var rows = table.find('>tbody>tr').sort(compareRowById);
var prevInitial = null;
var startIndex = null;
for (var idx = 0; idx <= rows.length; ++idx) {
var initial = rows[idx] ? toInitial($(rows[idx]).getRowId()) : null;
if (initial !== prevInitial) {
if (startIndex !== null) {
$('<td class="initial"></td>').prop('rowspan', idx - startIndex).text(prevInitial)
.prependTo(rows[startIndex]);
}
startIndex = idx;
prevInitial = initial;
}
}
table.find('>thead>tr').prepend('<th class="head-initial">頭文字</th>');
table.find('>tbody').append(rows);
}
// create serial number
if (CREATE_SERIAL_NUMBER) createSerialNumberColumn();
function createSerialNumberColumn() {
table.find('th.head-icon').before('<th class="head-serialno">整理番号</th>');
table.find('td.icon').before('<td class="serialno"></td>');
updateSerialNumbers();
}
function updateSerialNumbers() {
table.find('td.serialno').text(function(idx) { return idx + 1; });
}
// create reserve checkbox
if (CREATE_RESERVE_CHECKBOX) {
table.find('>thead>tr').append('<th class="head-reserve head-checkbox">補欠</th>');
table.find('>tbody>tr').append('<td class="reserve"><input type="checkbox"></td>')
.find('>td>input').prop('name', /* @this HTMLElement */ function() { return $(this).closest('tr').prop('entry-id') + '-reserve'; });
table.on('click', 'td.reserve>input', function(ev) {
createOrUpdateInitialColumn();
updateSerialNumbers();
saveCheckboxes('reserve');
});
}
// create U19 checkbox
if (CREATE_U19_CHECKBOX) {
table.find('>thead>tr').append('<th class="head-u19 head-checkbox">補欠</th>');
table.find('>tbody>tr').append('<td class="u19"><input type="checkbox"></td>')
.find('>td>input').prop('name', /* @this HTMLElement */ function() { return $(this).closest('tr').data('entry-id') + '-u19'; });
table.on('click', 'td.u19>input', function(ev) {
saveCheckboxes('u19');
});
}
// add controls
control.addButton('リスト', 'entries', function(ev) { table.toggle(); });
table.find('>thead>tr>th').each(/* @this HTMLElement */ function() {
var id = this.className.replace(/ .*$/, '').replace(/head-/, '');
var label = LABELS[id];
$(this).text(label);
control.addToggleButton(label, id);
});
table.find('.head-profile,.profile').toggle();
if (ENABLE_NAMEPLATE) createNameplateEditor();
function createNameplateEditor() {
$('<div id="nameplate-edit">'
+ '<textarea id="nameplate-style"></textarea>'
+ '<textarea id="nameplate-body"></textarea>'
+ '<div id="nameplate-edit-control">'
+ '<label for="u19label">U19表示文字列</label>'
+ '<input id="u19label">'
+ '<button id="nameplate-save">テンプレートをブラウザにセーブ</button>'
+ '<button id="nameplate-test">別ウィンドウでプレビューを開く</button>'
+ '<button id="nameplate-download">HTMLファイルをダウンロード</button>'
+ '</div>'
+ '<iframe id="nameplate-sample"></iframe>'
+ '</div>')
.insertAfter(control).hide();
control.addButton('名札編集', 'nameplate-edit', function(ev) {
if ($('#nameplate-edit').toggle().is(':visible')) {
updateNameplateSample();
}
});
var nameplateUpdateTimer = null;
$('#nameplate-style,#nameplate-body').on('keyup', /* @this HTMLElement */ function(ev) {
var self = this;
clearTimeout(nameplateUpdateTimer);
nameplateUpdateTimer = setTimeout(nameplateUpdateTimeout, 500);
function nameplateUpdateTimeout() {
var newText = $(self).val();
var lastText = $(self).data('lastText');
if (lastText != newText) {
$(self).data('lastText', newText);
updateNameplateSample();
}
}
});
var testWindow = null;
var testBlobUrl = null;
$('#nameplate-test').click(function(ev) {
ev.preventDefault();
testWindow = window.open('', 'twipla-nameplate-test');
updateNameplateSample();
var contentHtml = wrapBody(getNameplateHtml());
var blob = new Blob([contentHtml], {type: 'text/html;charset=utf-8'});
if (testBlobUrl) {
URL.revokeObjectURL(testBlobUrl);
}
testBlobUrl = URL.createObjectURL(blob);
testWindow.location = testBlobUrl;
});
$('#nameplate-save').click(function(ev) {
ev.preventDefault();
updateNameplateSample();
var baseContentStyle = $('#nameplate-style').val();
var baseContentHtml = $('#nameplate-body').val();
var u19label = $('u19label').val();
$store.set(STORAGE_PREFIX + 'nameplate-style', baseContentStyle);
$store.set(STORAGE_PREFIX + 'nameplate', baseContentHtml);
$store.set(STORAGE_PREFIX + 'u19label', u19label);
});
$('#nameplate-download').click(function(ev) {
ev.preventDefault();
updateNameplateSample();
if (NAMEPLATE_SAVE_AS_DOCX) {
downloadNameplateDocx();
} else {
downloadNameplateHtml();
}
});
var sampleBlobUrl = null;
function updateNameplateSample() {
var contentHtml = wrapBody(getNameplateHtml(0, 2));
var blob = new Blob([contentHtml], {type: 'text/html;charset=utf-8'});
if (sampleBlobUrl) {
URL.revokeObjectURL(sampleBlobUrl);
}
sampleBlobUrl = URL.createObjectURL(blob);
$('#nameplate-sample').prop('src', sampleBlobUrl);
}
function getNameplateHtml(start, count) {
var baseContentStyle = $('#nameplate-style').val();
var baseContentHtml = $('#nameplate-body').val();
var rows = table.find('tbody tr');
if (count) {
rows = rows.slice(start, count);
}
var style = '<style>' + baseContentStyle + '</style>';
var body = rows.map(/* @this HTMLElement */ function() {
var data = $(this).getRowData();
var html = baseContentHtml;
$.each(data, function(key, value) {
html = html.replace(new RegExp('@\{'+key+'\}', 'g'), value);
});
return html;
}).toArray().join('\n');
return style + body;
}
function wrapBody(html) {
return '<!DOCTYPE html><html>'
+ '<head>'
+ '<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />'
+ '<title>Twipla 名札 - ' + TITLE + '</title>'
+ '</head>'
+ '<body>' + html + '</body></html>';
}
function downloadNameplateDocx() {
var contentHtml = getNameplateHtml();
var contentDocument = $('<body>').html(contentHtml);
var imageCount = 0;
contentDocument.find('img').each(/* @this HTMLElement */ function() {
++imageCount;
var img = this;
GM_xmlhttpRequest({
method: 'GET',
url: img.src,
responseType: 'blob',
onload: function(res) {
var reader = new FileReader();
reader.onload = function() {
img.src = reader.result;
--imageCount;
};
reader.readAsDataURL(res.response);
}
});
});
(function onloadAllImages() {
if (imageCount) {
return setTimeout(onloadAllImages, 500);
}
var content = wrapBody(contentDocument.html());
var blob = htmlDocx.asBlob(content, NAMEPLATE_DOCUMENT_OPTIONS);
saveAs(blob, NAMEPLATE_DOCX_NAME);
})();
}
function downloadNameplateHtml() {
var contentHtml = wrapBody(getNameplateHtml());
var blob = new Blob([contentHtml], {type: 'text/html;charset=utf-8'});
saveAs(blob, NAMEPLATE_HTML_NAME);
}
$('#nameplate-style').val($store.get(STORAGE_PREFIX + 'nameplate-style') || NAMEPLATE_STYLE);
$('#nameplate-body').val($store.get(STORAGE_PREFIX + 'nameplate') || NAMEPLATE_TEMPLATE);
$('#u19label').val($store.get(STORAGE_PREFIX + 'u19label') || U19_LABEL);
}
if (ENABLE_NAMEEDIT) createNameEditor();
function createNameEditor() {
table.find('td.name').each(/* @this HTMLElement */ function() {
var cell = $(this);
cell.data('origVal', cell.text());
});
$(' <span>\ud83d\udcac</span>').appendTo('.head-name')
.attr('title', 'ダブルクリック: 編集開始\nEnter / フォーカス移動: 確定\nEsc: 初期値に戻す');
table.on('dblclick', 'td.name', /* @this HTMLElement */ function(ev) {
ev.preventDefault();
var cell = $(this);
var oldValue = cell.text();
var origValue = cell.data('origVal');
var input = $('<input>').css('width', '100%').val(oldValue);
cell.empty().append(input);
input.on('keypress blur', function(ev) {
if (ev.type === 'keypress' && ev.which === 13 || ev.type === 'blur') {
ev.preventDefault();
var newValue = input.val();
cell.empty().text(newValue);
if (newValue !== oldValue) {
cell.data('changed', (newValue !== origValue));
saveTexts('name');
}
}
});
input.on('keyup', function(ev) {
if (ev.which === 27) {
ev.preventDefault();
cell.empty().text(origValue).data('changed', false);
}
});
input.focus();
});
}
if (ENABLE_ORPHAN_SEARCH) createOrphanSearcher();
function createOrphanSearcher() {
var base = $('<div id="orphan-search-base"><progress id="orphan-search-progress">')
.prependTo(document.body).hide();
control.addButton('行方不明検索', 'orphan-search', function(ev) {
var orphans = table.find('.icon img')
.filter(/* @this HTMLElement */ function() { return $(this).prop('naturalWidth') === 0; })
.closest('tr');
var count = 0, errorCount = 0, max = orphans.length;
if (max === 0) {
alert('行方不明はありません。');
return;
}
var progress = $('#orphan-search-progress').prop({max: max}).val(0);
progress.hide();
base.show();
if (!confirm('この処理は時間がかかる場合があります。\n開始しますか?')) {
base.hide();
return;
}
progress.show();
function updateProgress() {
var current = count + errorCount;
progress.val(current);
if (current >= max) {
// progress done
saveTexts('id');
saveTexts('icon', /* @this HTMLElement */ function getIconUrl() {
return $(this).find('a').prop('href');
});
base.hide();
}
}
orphans.find('.id a').each(/* @this HTMLElement */ function() {
var idlink = $(this);
var icon = idlink.closest('tr').find('.icon img');
searchUserOnTwitter(this.href, true);
function searchUserOnTwitter(url, fallbackGoogle) {
GM_xmlhttpRequest({
method: 'GET',
url: url,
onload: function(res) {
if (res.status === 200) {
var regIcon = /<a (?=[^>]*ProfileCardMini-avatar)[^>]*\bdata-url="([^"]+)"/m;
var matchIcon = regIcon.exec(res.responseText);
if (matchIcon) {
icon.attr('src', matchIcon[1])
.closest('a').attr('href', matchIcon[1])
.closest('td').data('changed', true);
}
var regId = /<link (?=[^>]*\brel="canonical")[^>]*\bhref="(https:\/\/twitter\.com\/([^\/"]+))\/[^"]*"/m;
var matchId = regId.exec(res.responseText);
if (matchId) {
idlink.attr('href', matchId[1]).text('@' + matchId[2])
.closest('td').data('changed', true);
}
++count;
} else if (fallbackGoogle) {
searchStatusOnGoogle();
} else {
++errorCount;
}
updateProgress();
},
});
}
function searchStatusOnGoogle() {
var id = idlink.text().replace(/^@/, '');
var statusUrl = 'https://twitter.com/' + id + '/status/';
var searchUrl = 'https://www.google.co.jp/search?q=' + encodeURIComponent(statusUrl);
GM_xmlhttpRequest({
method: 'GET',
url: searchUrl,
onload: function(res) {
var regStatus = new RegExp('<a href="('+statusUrl+'[0-9]+)"', 'm');
var matchStatus = regStatus.exec(res.responseText);
if (matchStatus) {
searchUserOnTwitter(matchStatus[1], false);
}
}
});
}
});
});
}
loadState();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment