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.
This commit is contained in:
OverSu 2026-09-30 23:13:03 +02:00
commit b38dea0597
25901 changed files with 9982925 additions and 369 deletions

View file

@ -0,0 +1,54 @@
'use strict';
/* global utils, BookmarkEditor, BookmarkRemover */
var ActivityHandler = {
'save-bookmark': function ah_save(activity) {
BookmarkEditor.init({
data: activity.source.data,
onsaved: function onsaved(saved) {
window.addEventListener('status-hidden', function hidden() {
window.removeEventListener('status-hidden', hidden);
activity.postResult(saved ? 'saved' : 'updated');
});
var msg = saved ?
'added-to-home-screen-message' : 'updated-pinned-site';
navigator.mozL10n.formatValue(msg)
.then(msg => utils.status.show(msg));
},
oncancelled: function oncancelled() {
activity.postError('cancelled');
}
});
},
'remove-bookmark': function ah_remove(activity) {
BookmarkRemover.init({
id: activity.source.data.url,
onremoved: function onremoved() {
activity.postResult('removed');
},
oncancelled: function oncancelled(e) {
activity.postError(e);
}
});
}
};
navigator.mozSetMessageHandler('activity', function onActivity(activity) {
var name = activity.source.name;
switch (name) {
case 'save-bookmark':
case 'remove-bookmark':
if (activity.source.data.type === 'url') {
ActivityHandler[name](activity);
} else {
activity.postError('type not supported');
}
break;
default:
activity.postError('name not supported');
}
});

View file

@ -0,0 +1,175 @@
'use strict';
/* global UrlHelper, BookmarksDatabase, Icon */
/* exported BookmarkEditor */
var BookmarkEditor = {
BOOKMARK_ICON_SIZE: 60,
APP_ICON_SIZE: 60,
init: function bookmarkEditor_show(options) {
this.data = options.data;
this.onsaved = options.onsaved;
this.oncancelled = options.oncancelled;
var mode = 'add';
BookmarksDatabase.get(this.data.url).then((function got(bookmark) {
if (bookmark) {
this.data = bookmark;
mode = 'put';
}
this._init(mode);
}).bind(this), this._init.bind(this, mode));
},
_init: function bookmarkEditor_init(mode) {
this.mode = document.body.dataset.mode = mode;
this.bookmarkTitle = document.getElementById('bookmark-title');
this.bookmarkURL = document.getElementById('bookmark-url');
this.bookmarkIcon = document.getElementById('bookmark-icon');
this.header = document.getElementById('header');
this.saveButton = document.getElementById('done-button');
this.appInstallationSection = document.getElementById('app-installation');
this.appIcon = document.getElementById('app-icon');
this.appIconPlaceholder = document.getElementById('app-icon-placeholder');
this.appNameText = document.getElementById('app-name');
this.installAppButton = document.getElementById('install-app-button');
this.header.addEventListener('action', this.close.bind(this));
this.saveListener = this.save.bind(this);
this.saveButton.addEventListener('click', this.saveListener);
this.bookmarkTitle.value = this.data.name || '';
this.bookmarkURL.textContent = this.data.url;
this._renderIcon();
if (this.data.manifestURL) {
this.manifestURL = this.data.manifestURL;
this._fetchManifest(this.manifestURL);
}
this._checkDoneButton();
this.form = document.getElementById('bookmark-form');
this.form.addEventListener('input', this._checkDoneButton.bind(this));
this.form.addEventListener('submit', this._submit.bind(this));
var touchstart = 'ontouchstart' in window ? 'touchstart' : 'mousedown';
this.clearButton = document.getElementById('bookmark-title-clear');
this.clearButton.addEventListener(touchstart, this._clearTitle.bind(this));
if (mode === 'put') {
this._onEditMode();
this.saveButton.setAttribute('data-l10n-id', 'done-action');
} else {
this.saveButton.setAttribute('data-l10n-id', 'add-action');
}
// We're appending new elements to DOM so to make sure headers are
// properly resized and centered, we emmit a lazyload event.
// This will be removed when the gaia-header web component lands.
window.dispatchEvent(new CustomEvent('lazyload', {
detail: document.body
}));
},
_renderIcon: function renderIcon() {
var icon = new Icon(this.bookmarkIcon, this.data.icon);
icon.render({'size': this.BOOKMARK_ICON_SIZE});
},
_renderAppIcon: function renderAppIcon(manifest, size) {
// Parse icon URL from app manifest
var iconURL = window.IconsHelper.getBestIconFromWebManifest(manifest, size);
if (!iconURL) {
return;
}
// Switch out placeholder for real icon when it loads
this.appIconListener = this._handleAppIconLoad.bind(this);
this.appIcon.addEventListener('load', this.appIconListener);
this.appIcon.setAttribute('src', iconURL);
},
_handleAppIconLoad: function _handleAppIconLoad() {
this.appIconPlaceholder.classList.add('hidden');
this.appIcon.classList.remove('hidden');
this.appIcon.removeEventListener('load', this.appIconListener);
},
_fetchManifest: function bookmarkEditor_fetchManifest(manifestURL) {
var manifestPromise = window.WebManifestHelper.getManifest(manifestURL);
manifestPromise.then((function(manifestData) {
if (manifestData) {
this.installAppButtonListener = this._installApp.bind(this);
this.installAppButton.addEventListener('click',
this.installAppButtonListener);
this.appNameText.textContent = manifestData.short_name ||
manifestData.name;
this.appInstallationSection.classList.remove('hidden');
this._renderAppIcon(manifestData, manifestURL, this.APP_ICON_SIZE);
}
}).bind(this)).catch(function(error) {
console.error('Unable to get web manifest: ' + error);
});
return manifestPromise;
},
_onEditMode: function bookmarkEditor_onEditMode() {
// Done button will be disabled on edit mode once it is displayed
this.saveButton.disabled = true;
},
close: function bookmarkEditor_close() {
this.oncancelled();
},
/**
* Handles the submit case for the form when the user presses the enter key.
* @param {Event} event The form submit event.
*/
_submit: function(event) {
event.preventDefault();
if (!this.saveButton.disabled) {
this.save();
}
},
_clearTitle: function bookmarkEditor_clearTitle(event) {
event.preventDefault();
this.bookmarkTitle.value = '';
this._checkDoneButton();
},
_checkDoneButton: function bookmarkEditor_checkDoneButton() {
// If one of the fields is blank, the “Done” button should be dimmed and
// inactive
var title = this.bookmarkTitle.value.trim();
this.saveButton.disabled = title === '';
},
_installApp: function bookmarkEditor_installApp() {
window.navigator.mozApps.install(this.manifestURL);
},
save: function bookmarkEditor_save(evt) {
this.saveButton.removeEventListener('click', this.saveListener);
if (this.installAppButtonListener) {
this.installAppButton.removeEventListener('click',
this.installAppButtonListener);
}
// Only allow urls to be bookmarked.
// This is defensive check - callers should filter out non-URLs.
var url = this.data.url.trim();
if (UrlHelper.isNotURL(url)) {
this.oncancelled();
return;
}
this.data.name = this.bookmarkTitle.value;
this.data.url = url;
BookmarksDatabase[this.mode](this.data).then(this.onsaved.bind(this),
this.close.bind(this));
}
};

View file

@ -0,0 +1,39 @@
'use strict';
/* global BookmarksDatabase */
/* exported BookmarkRemover */
var BookmarkRemover = {
init: function bookmarkRemover_init(data) {
var id = data.id;
BookmarksDatabase.get(id).then(function got(bookmark) {
if (!bookmark) {
data.oncancelled('bookmark does not exist');
return;
}
// Title and message
var ttl = document.getElementById('title');
var msg = document.getElementById('message');
ttl.setAttribute('data-l10n-id', 'remove-title');
ttl.setAttribute('data-l10n-args',
JSON.stringify({ name: bookmark.name }));
msg.setAttribute('data-l10n-id', 'remove-message');
msg.setAttribute('data-l10n-args',
JSON.stringify({ name: bookmark.name }));
// Action handlers
document.getElementById('cancel-action').addEventListener('click',
function onCancel() {
data.oncancelled('cancelled');
}
);
document.getElementById('remove-action').addEventListener('click',
function onConfirm(evt) {
evt.target.removeEventListener('click', onConfirm);
BookmarksDatabase.remove(id).then(data.onremoved, data.oncancelled);
}
);
}, data.oncancelled);
}
};

View file

@ -0,0 +1,32 @@
section[role="status"] {
animation-name: hide;
animation-duration: .3s;
animation-fill-mode: forwards;
}
section[role="status"].onviewport {
animation-name: show;
}
section[role="status"].hidden {
display: none;
}
@keyframes show {
from {
transform: translateY(100%)
}
to {
transform: translateY(0)
}
}
@keyframes hide {
from {
transform: translateY(0)
}
to {
transform: translateY(100%)
}
}

View file

@ -0,0 +1,177 @@
'use strict';
window.utils = window.utils || {};
window.utils.status = (function() {
// How many milliseconds is displayed the status component by default
var DISPLAYED_TIME = 1500;
// References to the DOMElement(s) that renders the status UI component
var section, content;
// The numerical ID of the timeout in order to hide UI component
var timeoutID;
/*
* Clears the callback in charge of hiding the component after timeout
*/
function clearHideTimeout() {
if (timeoutID === null) {
return;
}
window.clearTimeout(timeoutID);
timeoutID = null;
}
/*
* Shows the status component
*
* @param{Object} Message. It could be a string or a DOMFragment that
* represents the normal and strong strings
*
* @param{int} It defines the time that the status is displayed in ms. This
* parameter is optional
*
*/
function show(message, duration) {
clearHideTimeout();
content.innerHTML = '';
if (typeof message === 'string') {
content.textContent = message;
} else {
try {
// Here we should have a DOMFragment
content.appendChild(message);
} catch (ex) {
console.error('DOMException: ' + ex.message);
}
}
section.classList.remove('hidden');
section.classList.add('onviewport');
timeoutID = window.setTimeout(hide, duration || DISPLAYED_TIME);
}
/*
* This function is invoked when some animation is ended
*/
function animationEnd(evt) {
var eventName = 'status-showed';
if (evt.animationName === 'hide') {
clearHideTimeout();
section.classList.add('hidden');
eventName = 'status-hidden';
}
window.dispatchEvent(new CustomEvent(eventName));
}
/*
* Hides the status component
*/
function hide() {
section.classList.remove('onviewport');
}
/*
* Releases memory
*/
function destroy() {
section.removeEventListener('animationend', animationEnd);
document.body.removeChild(section);
clearHideTimeout();
section = content = null;
}
function getPath() {
return '/js/components/';
}
/*
* Initializes the library. Basically it creates the markup:
*
* <section role="status">
* <p>xxx</p>
* </section>
*/
function initialize() {
if (section) {
return;
}
section = document.createElement('section');
var link = document.createElement('link');
link.type = 'text/css';
link.rel = 'stylesheet';
link.href = getPath() + 'status-behavior.css';
document.head.appendChild(link);
section.setAttribute('role', 'status');
section.classList.add('hidden');
content = document.createElement('p');
section.appendChild(content);
section.addEventListener('animationend', animationEnd);
// to prevent flickering
setTimeout(function append() {
document.body.appendChild(section);
});
}
// Initializing the library
if (document.readyState === 'complete') {
initialize();
} else {
document.addEventListener('DOMContentLoaded', function loaded() {
document.removeEventListener('DOMContentLoaded', loaded);
initialize();
});
}
return {
/*
* The library is auto-initialized but it is for unit testing purposes
*/
init: initialize,
/*
* Shows the status component
*
* @param{Object} Message. It could be a string or a DOMFragment that
* represents the normal and strong strings
*
* @param{int} It defines the time that the status is displayed in ms
*
*/
show: show,
/*
* Hides the status component
*/
hide: hide,
/*
* Releases memory
*/
destroy: destroy,
/*
* Sets up the duration in milliseconds that a status is displayed
*
* @param{int} The time in milliseconds
*
*/
setDuration: function setDuration(time) {
DISPLAYED_TIME = time || DISPLAYED_TIME;
}
};
})();