VulpesOS/gaia/apps/search/js/providers/places.js
OverSu b38dea0597 Publish Vulpes Preview 15 sources for desktop, Android and Tundra
Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
2026-09-30 23:13:03 +02:00

400 lines
11 KiB
JavaScript
Executable file

/* globals Provider, SyncDataStore, Promise, IconsHelper */
/* globals Search, PlacesIdbStore */
/* globals DateHelper */
/* globals asyncStorage */
/* globals LazyLoader */
(function(exports) {
'use strict';
var _ = navigator.mozL10n.get;
// Maximum number of results to show show for a single query
var MAX_AWESOME_RESULTS = 4;
var MAX_HISTORY_RESULTS = 20;
var MAX_TOPSITES_RESULTS = 6;
// Name of the datastore we pick up places from
var STORE_NAME = 'places';
var topSitesWrapper = document.getElementById('top-sites');
var historyWrapper = document.getElementById('history');
// These elements are only included in the newtab page.
if (topSitesWrapper && historyWrapper) {
topSitesWrapper.addEventListener('click', itemClicked);
historyWrapper.addEventListener('click', itemClicked);
}
var cachedLink = document.createElement('a');
function parseUrl(url) {
cachedLink.href = url;
return cachedLink;
}
// Storage for locally fetched icons
var icons = {};
var iconUrls = {};
function getIcon(place) {
if (place.url in icons && icons[place.url]) {
return icons[place.url];
}
IconsHelper.getIcon(place.url, null, place).then(icon => {
saveIcon(place.url, icon);
});
return false;
}
function saveIcon(key, url) {
if (key in icons) {
return;
}
icons[key] = null;
iconUrls[key] = url;
fetchIcon(url, function(err, icon) {
if (!err) {
icons[key] = icon;
}
});
}
function fetchIcon(uri, callback) {
var xhr = new XMLHttpRequest({mozSystem: true});
xhr.open('GET', uri, true);
xhr.responseType = 'blob';
xhr.addEventListener('load', function() {
// Check icon was successfully downloded
// 0 is due to https://bugzilla.mozilla.org/show_bug.cgi?id=716491
if (!(xhr.status === 200 || xhr.status === 0)) {
return callback(new Error('error_downloading'));
}
var blob = xhr.response;
if (blob.size > this.MAX_ICON_SIZE) {
return callback(new Error('image_to_large'));
}
// Only save the icon if it can be loaded as an image bigger than 0px
var img = document.createElement('img');
img.src = window.URL.createObjectURL(blob);
img.onload = function() {
window.URL.revokeObjectURL(img.src);
if (img.naturalWidth <= 0) {
return callback(new Error('Cannot load image'));
}
callback(null, blob);
};
img.onerror = function() {
window.URL.revokeObjectURL(img.src);
return callback(new Error('Cannot load image'));
};
});
xhr.onerror = function(err) {
return callback(new Error('Cannot load uri'));
};
xhr.send();
}
function itemClicked(e) {
if (e.target.dataset.url) {
Search.navigate(e.target.dataset.url);
}
}
var listTemplate = createList();
function createList() {
var list = document.createElement('ul');
list.setAttribute('role', 'listbox');
return list;
}
function incrementHistoryThreshold(timestamp, currentThreshold, thresholds) {
var newThreshold = currentThreshold += 1;
if (timestamp < thresholds[newThreshold]) {
return incrementHistoryThreshold(timestamp, newThreshold, thresholds);
}
return newThreshold;
}
function drawHistoryHeading(parent, threshold, timestamp) {
var LABELS = [
'future',
'today',
'yesterday',
'last-7-days',
'this-month',
'last-6-months',
'older-than-6-months'
];
var text = '';
// Special case for month headings
if (threshold == 5 && timestamp) {
var date = new Date(timestamp);
var now = new Date();
text = _('month-' + date.getMonth());
if (date.getFullYear() != now.getFullYear()) {
text += ' ' + date.getFullYear();
}
} else {
text = _(LABELS[threshold]);
}
var h3 = document.createElement('h3');
var spanNode = document.createElement('span');
spanNode.textContent = text;
var ul = listTemplate.cloneNode(true);
h3.appendChild(spanNode);
parent.appendChild(h3);
parent.appendChild(ul);
}
function updateIcon(visit, iconWrapper) {
var iconDom = iconWrapper.querySelector('img');
IconsHelper.getIcon(visit.url, null, visit).then((icon) => {
if (icon && iconDom) {
iconDom.onload = function () {
iconWrapper.classList.remove('empty');
iconDom.style.display = 'block';
};
iconDom.src = icon;
}
});
}
function buildHistory(visits) {
var thresholds = [
Date.now(), // 0. Now
DateHelper.todayStarted(), // 1. Today
DateHelper.yesterdayStarted(), // 2. Yesterday
DateHelper.thisWeekStarted(), // 3. This week
DateHelper.thisMonthStarted(), // 4. This month
DateHelper.lastSixMonthsStarted(), // 5. Six months
0 // 6. Epoch!
];
var threshold = 0;
var month = null;
var year = null;
var fragment = document.createDocumentFragment();
visits.forEach(function(visit) {
// Draw new heading if new threshold reached
if (visit.date > 0 && visit.date < thresholds[threshold]) {
threshold = incrementHistoryThreshold(visit.date,
threshold, thresholds);
// Special case for month headings
if (threshold != 5) {
drawHistoryHeading(fragment, threshold);
}
}
if (threshold === 5) {
var timestampDate = new Date(visit.date);
if (timestampDate.getMonth() != month ||
timestampDate.getFullYear() != year) {
month = timestampDate.getMonth();
year = timestampDate.getFullYear();
drawHistoryHeading(fragment, threshold, visit.date);
}
}
visit.meta = visit.url;
visit.dataset = { url: visit.url };
var dom = exports.Places.buildResultsDom([visit]);
var iconDom = dom.querySelector('.icon');
fragment.appendChild(dom);
updateIcon(visit, iconDom);
});
return fragment;
}
function showStartPage() {
if (!topSitesWrapper || !historyWrapper) {
return;
}
var store = exports.Places.persistStore;
var urls = [];
store.readVisits(MAX_HISTORY_RESULTS, function(results) {
var docFragment = buildHistory(results);
historyWrapper.innerHTML = '';
historyWrapper.appendChild(docFragment);
}, function filter(visit) {
var isStored = visit.url in urls;
urls[visit.url] = true;
return !isStored;
});
store.read('frecency', MAX_TOPSITES_RESULTS, function(results) {
var docFragment = document.createDocumentFragment();
results.forEach(function(x) {
docFragment.appendChild(formatTopResult(x));
});
topSitesWrapper.innerHTML = '';
topSitesWrapper.appendChild(docFragment);
});
}
function formatTopResult(result) {
var div = document.createElement('div');
var span = document.createElement('span');
if (result.title) {
span.setAttribute('dir', 'auto');
span.textContent = result.title;
} else {
span.setAttribute('dir', 'ltr');
span.textContent = result.url;
}
div.dataset.url = result.url;
div.classList.add('top-site');
div.appendChild(span);
div.setAttribute('role', 'link');
if (result.screenshot || result.tile) {
var img = result.screenshot || result.tile;
var imgUrl = (typeof img === 'string') ? img : URL.createObjectURL(img);
div.style.backgroundImage = 'url(' + imgUrl + ')';
}
return div;
}
function matchesFilter(value, filter) {
return !filter || (value && value.match(new RegExp(filter, 'i')) !== null);
}
function formatPlace(placeObj, filter) {
var result = {
'title': placeObj.title,
'meta': placeObj.url,
'icon': getIcon(placeObj),
'dataset': {
'url': placeObj.url
},
'label': placeObj.title
};
return result;
}
function Places() {}
Places.prototype = {
__proto__: Provider.prototype,
name: 'Places',
click: itemClicked,
init: function() {
// If not on new tab page, set places div as container
if (!topSitesWrapper || !historyWrapper) {
this.header = document.getElementById(this.name.toLowerCase() +
'-header');
this.container = document.getElementById(this.name.toLowerCase());
this.container.addEventListener('click', this.click.bind(this));
}
this.persistStore = new PlacesIdbStore();
this.persistStore.init().then(() => {
this.syncStore =
new SyncDataStore(STORE_NAME, this.persistStore, 'url');
this.syncStore.filter = function(place) {
return place.url.startsWith('app://') ||
place.url === 'about:blank';
};
this.syncStore.onChange = function() {
showStartPage();
};
// Make init return a promise, so we know when
// we did the sync. Used right now for testing
// porpoises.
var rev = this.persistStore.latestRevision || 0;
return this.syncStore.sync(rev).then(() => {
return new Promise(resolve => {
function done() {
showStartPage();
resolve();
}
asyncStorage.getItem('have-preloaded-sites', (havePreloaded) => {
if (!havePreloaded) {
this.preloadTopSites().then(() => {
asyncStorage.setItem('have-preloaded-sites', true);
done();
});
} else {
done();
}
});
});
});
});
},
saveSites: function(sites) {
return Promise.all(sites.map(site => {
site.frecency = -2;
return this.persistStore.addPlace(site);
}));
},
preloadTopSites: function() {
// load default build top sites
return LazyLoader.getJSON('/js/inittopsites.json').then(sites => {
return this.saveSites(sites);
});
},
search: function(filter) {
return new Promise((resolve, reject) => {
var matchedOrigins = {};
this.persistStore.read('frecency', MAX_AWESOME_RESULTS, (results) => {
resolve(results.map(function(result) {
return formatPlace(result, filter);
}));
}, function filterFun(result) {
if (result.frecency <= 0) {
return false;
}
var url = parseUrl(result.url);
var matches = !(url.hostname in matchedOrigins) &&
(matchesFilter(result.title, filter) ||
matchesFilter(result.url, filter));
if (matches) {
matchedOrigins[url.hostname] = true;
}
return matches;
});
});
}
};
exports.Places = new Places();
Search.provider(exports.Places);
}(window));