diff --git a/index.html b/index.html
index f07fc62..e8610dd 100644
--- a/index.html
+++ b/index.html
@@ -1,5 +1,5 @@
-
+
New Tab
@@ -8,7 +8,7 @@
-
+
diff --git a/lib.js b/lib.js
new file mode 100644
index 0000000..c318f7d
--- /dev/null
+++ b/lib.js
@@ -0,0 +1,117 @@
+const apiURL = 'https://api.artic.edu/api/v1/search';
+const noDepartmentTerm = 'None (No Department Association)';
+
+function getJson(body, callback, forceNew) {
+ let request = new XMLHttpRequest();
+ request.open('POST', apiURL, true);
+ request.setRequestHeader('Content-Type', 'application/json');
+ request.onreadystatechange = function () {
+ if (this.readyState === 4 && this.status === 200) {
+ callback(JSON.parse(this.responseText), forceNew);
+ }
+ };
+ request.send(JSON.stringify(body));
+}
+
+function getJsonData(body) {
+ return new Promise((resolve, reject) => {
+ setTimeout(() => {
+ reject('request timed out');
+ }, 30 * 1000);
+ getJson(body, (data) => {
+ resolve(data);
+ });
+ });
+}
+
+function escape(s) {
+ const bad1 = /&/g;
+ const good1 = '&';
+
+ const bad2 = //g;
+ const good3 = '>';
+
+ const bad4 = /"/g;
+ const good4 = '"';
+
+ const bad5 = /'/g;
+ const good5 = ''';
+
+ // prettier-ignore
+ return s
+ .replace(bad1, good1)
+ .replace(bad2, good2)
+ .replace(bad3, good3)
+ .replace(bad4, good4)
+ .replace(bad5, good5);
+}
+
+function merge(target, source) {
+ for (const key in source) {
+ if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
+ if (!target[key] || typeof target[key] !== 'object') {
+ target[key] = {};
+ }
+ merge(target[key], source[key]);
+ } else {
+ target[key] = source[key];
+ }
+ }
+}
+
+// initialize settings with defaults
+const settings = {
+ dailyMode: false,
+ departmentOptions: {
+ options: [],
+ lastFetched: null,
+ selected: [],
+ },
+};
+
+// load settings if they exist
+const extensionSettingsKey = 'extensionSettings';
+merge(settings, JSON.parse(localStorage.getItem(extensionSettingsKey)));
+saveSettings(); // save in case there are new default settings or settings are not persisted yet
+
+function getSettings() {
+ // make sure we are always reading from localStorage so we don't load stale data
+ return new Proxy(settings, {
+ get(target, property) {
+ const s = JSON.parse(localStorage.getItem(extensionSettingsKey));
+ return s ? s[property] : null;
+ },
+ });
+}
+
+function saveSettings() {
+ localStorage.setItem(extensionSettingsKey, JSON.stringify(settings));
+}
+
+const filterFields = {
+ department: 'department_title.keyword',
+};
+
+const artworkCacheKeys = {
+ // LocalStorage keys for reference
+ savedResponseKey: 'response',
+ preloadedImagesKey: 'preloaded',
+ preloadingImagesKey: 'preloading',
+ lastLoadedDateKey: 'lastLoadedDate',
+};
+
+// prettier-ignore
+export {
+ artworkCacheKeys,
+ escape,
+ filterFields,
+ getJson,
+ getJsonData,
+ getSettings,
+ merge,
+ noDepartmentTerm,
+ saveSettings,
+};
diff --git a/options.css b/options.css
new file mode 100644
index 0000000..ac4b275
--- /dev/null
+++ b/options.css
@@ -0,0 +1,32 @@
+body {
+ background: #21212c;
+ color: lightgrey;
+ font-family: sans-serif;
+ font-size: 16px;
+ padding: 0 32px;
+}
+
+label,
+legend,
+select
+{
+ font-size: 24px;
+}
+
+select {
+ margin: 0 24px;
+}
+
+.setting {
+ margin: 24px 0;
+ padding: 24px 12px;
+}
+
+.checkbox {
+ display: block;
+ margin: 12px;
+
+ input {
+ margin-right: 12px;
+ }
+}
diff --git a/options.html b/options.html
index ee060dc..cd6b358 100644
--- a/options.html
+++ b/options.html
@@ -1,39 +1,22 @@
-
+
Art Institute of Chicago: Art Tab - Options
-
+
Art Institute of Chicago: Art Tab - Options
-
-
-
+
-
-
-
+
+
+
+ Reload artwork image once per day instead of on every new tab
+
+
+
diff --git a/options.js b/options.js
index 0fd646c..644fb60 100644
--- a/options.js
+++ b/options.js
@@ -1,10 +1,89 @@
-const extensionSettingsKey = 'extensionSettings';
-const settings = JSON.parse(localStorage.getItem(extensionSettingsKey)) || {};
+// prettier-ignore
+import {
+ artworkCacheKeys,
+ escape,
+ filterFields,
+ getJsonData,
+ getSettings,
+ noDepartmentTerm,
+ saveSettings,
+} from './lib.js';
+
+const contemporaryArt = 'Contemporary Art';
+
+const baseQuery = {
+ resources: 'artworks',
+ size: 0,
+ aggregations: {},
+};
+
+const departmentQuery = Object.assign({}, baseQuery);
+departmentQuery.aggregations = {
+ departments: {
+ terms: {
+ field: filterFields.department,
+ },
+ },
+};
+
+const settings = getSettings();
const selectDaily = document.querySelector('#daily');
selectDaily.value = settings.dailyMode;
-selectDaily.addEventListener('change', e => {
+selectDaily.addEventListener('change', (e) => {
settings.dailyMode = e.target.value === 'true';
- localStorage.setItem(extensionSettingsKey, JSON.stringify(settings));
-})
+ save();
+});
+
+const oneWeekMs = 7 * 24 * 60 * 60 * 1000;
+const lastFetchedMoreThanAWeekAgo = (Date.now() - settings.departmentOptions.lastFetched) > oneWeekMs;
+
+if (settings.departmentOptions.options.length === 0 || lastFetchedMoreThanAWeekAgo) {
+ const departmentData = await getJsonData(departmentQuery);
+ // prettier-ignore
+ const departmentOptions = departmentData.aggregations.departments.buckets
+ .map((b) => b.key)
+ .filter(o => o !== contemporaryArt) // for some reason, filtering on contemporary art yields zero results, despite there being many artworks with that department
+ .sort();
+ departmentOptions.push(noDepartmentTerm);
+ settings.departmentOptions.options = departmentOptions;
+ settings.departmentOptions.lastFetched = Date.now();
+ save();
+}
+
+const divDepartments = document.getElementById('departments');
+
+for (const o of settings.departmentOptions.options) {
+ const sanitized = escape(o);
+ const template = document.createElement('div');
+ template.innerHTML = ``;
+ divDepartments.append(...template.children);
+}
+
+function updateDepartment(e) {
+ settings.departmentOptions.selected = Array.from(divDepartments.querySelectorAll('input:checked')).map(
+ (i) => i.value
+ );
+ save();
+ // clear cached artwork data so that preferences are respected immediately
+ Object.values(artworkCacheKeys).forEach(k => localStorage.removeItem(k));
+}
+
+if (settings.departmentOptions.selected.length === 0) {
+ divDepartments.querySelectorAll('input').forEach((i) => (i.checked = true));
+} else {
+ settings.departmentOptions.selected.forEach((o) => {
+ const option = divDepartments.querySelector(`[value="${o}"]`);
+ // guard against options disappearing or being renamed
+ if(option) {
+ option.checked = true;
+ }
+ });
+}
+
+divDepartments.querySelectorAll('input').forEach((i) => i.addEventListener('change', updateDepartment));
+
+function save() {
+ saveSettings();
+}
diff --git a/script.js b/script.js
index d680901..78cf1c5 100644
--- a/script.js
+++ b/script.js
@@ -1,12 +1,23 @@
+// prettier-ignore
+import {
+ artworkCacheKeys,
+ filterFields,
+ getJson,
+ getSettings,
+ noDepartmentTerm
+} from './lib.js';
+
(function () {
- // LocalStorage keys for reference
- const savedResponseKey = 'response';
- const preloadedImagesKey = 'preloaded';
- const preloadingImagesKey = 'preloading';
- const lastLoadedDateKey = 'lastLoadedDate'
- const extensionSettingsKey = 'extensionSettings';
- const settings = JSON.parse(localStorage.getItem(extensionSettingsKey)) || {};
+ // prettier-ignore
+ const {
+ savedResponseKey,
+ preloadedImagesKey,
+ preloadingImagesKey,
+ lastLoadedDateKey
+ } = artworkCacheKeys;
+
+ const settings = getSettings();
// Settings for cache aggressiveness
const artworksToPrefetch = 50;
@@ -64,12 +75,12 @@
function handleReload(e) {
// handle keyboard interaction
if (e.type === 'click' || (e.type === 'keypress' && (e.key === 'Enter' || e.key === ' '))) {
- e.preventDefault();
- loadNewArtwork(true);
+ e.preventDefault();
+ loadNewArtwork(true);
}
- }
+ }
- function loadNewArtwork(forceNew){
+ function loadNewArtwork(forceNew) {
// https://developer.mozilla.org/en-US/docs/Web/API/Storage/getItem
// ...returns `null` if not found. JSON.parsing `null` also returns `null`
let savedResponse = JSON.parse(localStorage.getItem(savedResponseKey));
@@ -80,19 +91,7 @@
}
}
- getJson('https://api.artic.edu/api/v1/search', getQuery(), processResponse, forceNew);
- }
-
- function getJson(url, body, callback, forceNew) {
- let request = new XMLHttpRequest();
- request.open('POST', url, true);
- request.setRequestHeader('Content-Type', 'application/json');
- request.onreadystatechange = function () {
- if (this.readyState === 4 && this.status === 200) {
- callback(JSON.parse(this.responseText), forceNew);
- }
- };
- request.send(JSON.stringify(body));
+ getJson(getQuery(), processResponse, forceNew);
}
/**
@@ -101,16 +100,15 @@
function processResponse(response, forceNew) {
let artwork = response.data[0];
- const dateNow = (new Date()).toLocaleDateString();
+ const dateNow = new Date().toLocaleDateString();
const lastLoaded = localStorage.getItem(lastLoadedDateKey);
- if(!settings.dailyMode || forceNew || lastLoaded !== dateNow) {
- localStorage.setItem(lastLoadedDateKey, (new Date()).toLocaleDateString());
+ if (!settings.dailyMode || forceNew || lastLoaded !== dateNow) {
+ localStorage.setItem(lastLoadedDateKey, new Date().toLocaleDateString());
response.data = response.data.slice(1);
artwork = response.data[0];
- }
- else {
- // artwork was loaded on today's date, don't load a new one
+ } else {
+ // artwork was loaded on today's date, don't load a new one
}
localStorage.setItem(savedResponseKey, JSON.stringify(response));
@@ -188,8 +186,7 @@
// Save this so we can add it to our preload log
let currentImageId = artwork.image_id;
- if(!isPreload)
- {
+ if (!isPreload) {
// clear out any previous
viewer.world.removeAll();
}
@@ -268,7 +265,7 @@
}
function getQuery() {
- return {
+ const query = {
resources: 'artworks',
// prettier-ignore
fields: [
@@ -278,6 +275,7 @@
'image_id',
'date_display',
'thumbnail',
+ 'department_title',
],
boost: false,
limit: artworksToPrefetch,
@@ -317,6 +315,24 @@
},
},
};
+
+ if (settings.departmentOptions.selected.length > 0) {
+ const filter = { bool: { should: [] } };
+ settings.departmentOptions.selected.forEach((o) => {
+ if (o === noDepartmentTerm) {
+ filter.bool.should.push({
+ bool: { must_not: [{ exists: { field: 'department_title.keyword' } }] },
+ });
+ } else {
+ const term = { term: {} };
+ term.term[filterFields.department] = o;
+ filter.bool.should.push(term);
+ }
+ });
+ query.query.function_score.query.bool.filter.push(filter);
+ }
+
+ return query;
}
/**