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

383
gaia/apps/ftu/js/tutorial.js Executable file
View file

@ -0,0 +1,383 @@
/* global ScreenLayout, Promise,
Utils, FinishScreen, LazyLoader, TutorialUtils */
/* exported Tutorial */
(function(exports) {
'use strict';
// default layout
var currentLayout = 'tiny';
// Most used DOM elements
var dom = {};
/**
* Manages and controls the configuration, content and state of the tutorial
* @module Tutorial
*/
var elementIDs = [
'tutorial',
'tutorial-step-title',
'tutorial-step-media',
'tutorial-step-image',
'tutorial-step-video',
'forward-tutorial',
'back-tutorial'
];
var Tutorial = {
// A configuration object.
config: null,
// Config for the current tutorial steps
_stepsConfig: {},
// Which tutorial steps to use
_stepsKey: '',
// Keeps track of the current step
_currentStep: 1,
// Track initialized state of tutorial
_initialized: false,
/**
* Initialize the tutorial. This is async as a config file must be loaded.
* A Sequence (array of sync or async functions) is used to manage the init
* tasks.
* When complete, the tutorial is ready to be shown via the 'start' method
*
* @param {String} stepsKey a key into the tutorial config object, i
* i.e. a version or version delta like 1.3..2.0
* @param {Function} onLoaded optional callback for when init is complete
* @memberof Tutorial
*/
init: function(stepsKey, onLoaded) {
// init is async
// need to load config, then load the first step and its assets.
if (this._initialized || this._initialization) {
// init already underway
return;
}
var initTasks = this._initialization = new TutorialUtils.Sequence(
// config should load or already be loaded.
// failure should abort
this.loadConfig.bind(this),
this._initWithConfig.bind(this, stepsKey)
);
initTasks.onabort = this._onAbortInitialization.bind(this);
initTasks.oncomplete =
this._onCompleteInitialization.bind(this, onLoaded);
// first time here: cache DOM elements
elementIDs.forEach(function(name) {
dom[Utils.camelCase(name)] = document.getElementById(name);
if (!dom[Utils.camelCase(name)]) {
console.error('Cache DOM elements: couldnt cache: ' + name);
}
}, this);
initTasks.next();
// watch a ton of video events as context for the tutorial
// media loading/playing
var mediaEvents = this._mediaEvents = ['abort',
'canplay',
'canplaythrough',
'durationchange',
'emptied',
'ended',
'error',
'interruptbegin',
'interruptend',
'loadeddata',
'loadedmetadata',
'loadstart',
'mozaudioavailable',
'pause',
'play',
'playing',
'stalled',
'suspend',
'waiting'];
this._debugEventHandler = {
handleEvent: function(evt) {
var errCodes = {
'1': 'MEDIA_ERR_ABORTED',
'2': 'MEDIA_ERR_NETWORK',
'3': 'MEDIA_ERR_DECODE',
'4': 'MEDIA_ERR_SRC_NOT_SUPPORTED'
};
console.log('TUTORIAL: media-event: '+ evt.type);
if (evt.type == 'error') {
var errtype = errCodes[evt.target.error.code] || 'unknown';
console.log('TUTORIAL: media-event, ' + errtype + ' error');
}
}
};
mediaEvents.forEach(name => {
dom.tutorialStepVideo.addEventListener(name, this._debugEventHandler);
});
},
/**
* Show the tutorial and play the first step
* May be called during or after the init process
* We defer rasing the 'tutorialinitialized' event until this point
* as it signals the tutorial step content is loaded and displayed
*
* @param {Function} onReady optional callback for when start is complete
* @memberof Tutorial
*/
start: function(onReady) {
var sequence;
var initInProgress = false;
if (this._initialization) {
// init still underway, tack steps onto existing sequence
sequence = this._initialization;
initInProgress = true;
} else {
// init already complete, create new sequence
this._initialization = sequence = new TutorialUtils.Sequence();
sequence.onabort = this._onAbortInitialization.bind(this);
sequence.oncomplete =
this._onCompleteInitialization.bind(this);
}
sequence.push(function setInitialStep() {
// setStep should return promise given by _loadMedia
console.log('TUTORIAL: setInitialStep');
return this._setStep();
}.bind(this));
sequence.push(function showTutorialAndFinishInit() {
// Show the panel
dom.tutorial.classList.add('show');
// Custom event that can be used to apply (screen reader) visibility
// changes.
console.log('TUTORIAL: dispatching tutorialinitialized');
window.dispatchEvent(new CustomEvent('tutorialinitialized'));
});
if(typeof onReady === 'function') {
sequence.push(onReady);
}
if (!initInProgress) {
// init done, starting start sequence
sequence.next();
}
},
/**
* Continue initialization once config data is loaded
* Called as a part of the init sequence
*
* @param {String} stepsKey a key into the tutorial config object, i
* i.e. a version or version delta like 1.3..2.0
* @memberof Tutorial
*/
_initWithConfig: function(stepsKey) {
stepsKey = stepsKey || 'default';
this._stepsKey = stepsKey;
this._stepsConfig = this.config[stepsKey] || this.config['default'];
// Add event listeners
dom.forwardTutorial.addEventListener('click', this);
dom.backTutorial.addEventListener('click', this);
// Set the first step
this._currentStep = 1;
},
_onAbortInitialization: function() {
this._initialization = null;
},
_onCompleteInitialization: function(onReady) {
this._initialization = null;
this._initialized = true;
if(typeof onReady === 'function') {
onReady();
}
},
/**
* Advance the tutorial to the given step (first step if no value given)
*
* @param {Number} value number of the step to show (1-based)
* @memberof Tutorial
*/
_setStep: function (value) {
// If value is bigger than the max, show finish screen
value = (typeof value === 'number') ? value : this._currentStep;
var stepIndex = value - 1;
if (stepIndex >= this._stepsConfig.steps.length) {
return Promise.resolve().then(function() {
Tutorial.done();
});
}
var stepData = this._stepsConfig.steps[stepIndex];
if (!stepData) {
return Promise.reject('No data for step: ' + value);
}
// Set the step
dom.tutorial.dataset.step = this._currentStep;
// Internationalize
document.l10n.setAttributes(
dom.tutorialStepTitle,
stepData.l10nKey
);
// Update the image/video
var imgElement = dom.tutorialStepImage,
videoElement = dom.tutorialStepVideo;
var stepPromise;
if (stepData.video) {
stepPromise = TutorialUtils.getBestAssetForDirection(stepData.video)
.then((bestSrc) => {
return TutorialUtils.loadAndPlayMedia(videoElement, bestSrc);
});
videoElement.hidden = false;
imgElement.hidden = true;
} else {
imgElement.hidden = false;
stepPromise = TutorialUtils.loadMedia(imgElement, stepData.image);
imgElement.hidden = false;
videoElement.hidden = true;
}
return stepPromise;
},
/**
* DOM Event handler
*
* @param {DOMEvent} evt Event object
* @memberof Tutorial
*/
handleEvent: function(evt) {
if (evt.type === 'click') {
switch(evt.target) {
case dom.forwardTutorial:
this.next(evt);
break;
case dom.backTutorial:
this.back(evt);
break;
}
}
},
/**
* Advance to the next step in the tutorial
* @memberof Tutorial
*/
next: function() {
return this._setStep(++this._currentStep);
},
/**
* Go back to the previous step in the tutorial
* @memberof Tutorial
*/
back: function() {
return this._setStep(--this._currentStep);
},
/**
* Tutorial complete
* @memberof Tutorial
*/
done: function() {
var isUpgrade = this._stepsKey && this._stepsKey !== 'default';
FinishScreen.init(isUpgrade);
dom.tutorial.classList.remove('show');
dom.tutorialStepVideo.removeAttribute('src');
dom.tutorialStepImage.removeAttribute('src');
},
/**
* Load the config with steps and associated resources for the tutorial
* We have different config files for each screen category:
* phone is 'tiny', tablet is 'large'
* and corresponding tiny.json, large.json
*
* @returns {Promise}
* @memberof Tutorial
*/
loadConfig: function() {
if (!this._configPromise) {
// Update the value of the layout if needed
// 'ScreenLayout' give us 4 different values
// tiny: '(max-width: 767px)',
// small: '(min-width: 768px) and (max-width: 991px)',
// medium: '(min-width: 992px) and (max-width: 1200px)',
// large: '(min-width: 1201px)',
//
// Currently we are taking into account only 'tiny', and we are
// going to consider 'tablet' as 'large'. If we want to add more
// or specific features for 'small' & 'medium', we should add more
// logic here.
currentLayout = ScreenLayout.getCurrentLayout() === 'tiny' ?
'tiny' : 'large';
var configUrl = '/config/' + currentLayout + '.json';
this._configPromise = LazyLoader.getJSON(configUrl)
.then(function(json) {
Tutorial.config = json;
return Tutorial.config;
}, function() {
return new Error('Tutorial config failed to load from: ' + configUrl);
});
}
return this._configPromise;
},
/**
* Test helper to reset the tutorial to its pre-initialized state
* to allow init to be called again
* @memberof Tutorial
*/
reset: function() {
var resetPromise = Promise.resolve();
if (dom.tutorialStepVideo) {
this._mediaEvents.forEach(name => {
dom.tutorialStepVideo.removeEventListener(
name,
this._debugEventHandler
);
});
dom.tutorialStepVideo.hidden = true;
if (dom.tutorialStepVideo.src) {
resetPromise = new Promise((resolve, reject) => {
dom.tutorialStepVideo.addEventListener('emptied', () => {
resolve();
});
dom.tutorialStepVideo.removeAttribute('src');
dom.tutorialStepVideo.load();
});
}
}
if (this._initialization) {
this._initialization.abort();
this._initialization = null;
}
this._configPromise = null;
this._currentStep = 1;
this._stepsConfig = this.config = null;
if (this._initialized) {
dom.tutorial.classList.remove('show');
this._initialized = false;
}
document.getElementById('tutorial').classList.remove('show');
return resetPromise;
}
};
exports.Tutorial = Tutorial;
})(this);