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

-
-
- +
+ Daily mode - Reload artwork image once per day instead of on every new tab -
-
- - + + + + Reload artwork image once per day instead of on every new tab + +
+ Department Filter +
+ 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; } /**