VulpesOS/gaia/apps/ftu/js/tutorial_utils.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

179 lines
6 KiB
JavaScript
Executable file

(function(exports) {
'use strict';
var TutorialUtils = {
/**
* Helper function to load images and video
* @param {DOMNode} mediaElement video or image to assign new src to
* @param {String} src URL for video/image resource
* @returns {Promise}
*/
loadMedia: function (mediaElement, src) {
console.log('loadMedia, got mediaElement: ', mediaElement);
var isVideo = (mediaElement.nodeName === 'VIDEO');
console.log('TutorialUtils.loadMedia, isVideo: ',
isVideo, mediaElement.nodeName);
return new Promise(function(resolve, reject) {
function onMediaLoadOrError(evt) {
console.log('TutorialUtils.loadMedia, onMediaLoadOrError for ' +
'evt.type: ' + (evt && evt.type));
evt.target.removeEventListener('error', onMediaLoadOrError);
if (isVideo) {
evt.target.removeEventListener('canplay', onMediaLoadOrError);
evt.target.removeEventListener('abort', onMediaLoadOrError);
} else {
evt.target.removeEventListener('load', onMediaLoadOrError);
}
// Dont block progress on failure to load media
if (evt.type === 'error') {
console.error('Failed to load tutorial media: ' + src);
} else if (evt.type === 'abort') {
console.error('Loading of tutorial media aborted: ' + src);
}
resolve(evt);
}
function onVideoUnloaded(evt) {
console.log('TutorialUtils.loadMedia, onVideoUnloaded for evt.type: ' +
(evt && evt.type));
mediaElement.removeEventListener('emptied', onVideoUnloaded);
mediaElement.addEventListener('canplay', onMediaLoadOrError);
mediaElement.addEventListener('abort', onMediaLoadOrError);
mediaElement.addEventListener('error', onMediaLoadOrError);
console.log('TutorialUtils.loadMedia, onVideoUnloaded, ' +
'assigning src: ' + src);
mediaElement.src = src;
mediaElement.load();
}
if (isVideo) {
// must unload video and force load before switching to new source
if (mediaElement.src) {
console.log('TutorialUtils.loadMedia, removing src attribute');
mediaElement.removeAttribute('src');
mediaElement.addEventListener('emptied', onVideoUnloaded, false);
console.log('TutorialUtils.loadMedia, calling load');
mediaElement.load();
} else {
console.log('TutorialUtils.loadMedia, no src, ' +
'just calling onVideoUnloaded');
onVideoUnloaded();
}
} else {
console.log('TutorialUtils.loadMedia, not a video, ' +
'listen for load/error only');
mediaElement.addEventListener('load', onMediaLoadOrError, false);
mediaElement.addEventListener('error', onMediaLoadOrError, false);
mediaElement.src = src;
}
});
},
/**
* Convenience to load and then play video
* @param {DOMNode} mediaElement video or image to assign new src to
* @param {String} src URL for video/image resource
* @returns {Promise}
*/
loadAndPlayMedia: function(mediaElement, src) {
return this.loadMedia(mediaElement, src).then((evt) => {
if (evt.type !== 'error' && mediaElement.nodeName === 'VIDEO') {
mediaElement.play();
}
});
},
/**
* Helper to check (via HEAD request) a given resource exists
* The returned promise resolves with a true|false value
*
* @param {String} src URL for video/image resource
* @returns {Promise}
*/
fileExists: function (src) {
return new Promise((resolve) => {
window.fetch(src, { method: 'HEAD' }).then((response) => {
resolve(response.ok);
}).catch(() => {
resolve(false);
});
});
},
/**
* Helper to get best-match resource url for the current document.dir
* @param {String} src URL for video/image resource
* @returns {Promise}
*/
getBestAssetForDirection: function(src) {
var dir = document.dir || 'ltr';
var re =
/(-rtl|ltr)?\.(png|gif|jpg|webm|mp4|m4v|ogg|ogv)$/;
var directionSrc = src.replace(re, '-'+dir+'.$2');
return this.fileExists(directionSrc).then((result) => {
return result ? directionSrc : src;
}).catch((e) => {
console.log('Exception from getBestAssetForDirection: ', e);
});
},
/**
* Private helper class to manage a series of sync or async functions
*
* The array may be manipulated using standard array methods while the
* sequence runs. The sequence completes when there are no more functions or
* an exception is raised.
* At the end of the sequence, any 'oncomplete' assigned will be called with
* the return value from the last function
* Functions may return a 'thenable' to indicate async return
* Exceptions will be passed into the oncomplete function
* A Sequence may be cleanly aborted by calling abort() - no callbacks will
* be fired
* @class Sequence
*/
Sequence: function() {
var sequence = Array.slice(arguments);
var aborted = false;
sequence.abort = function() {
aborted = true;
this.length = 0;
if (typeof this.onabort === 'function') {
this.onabort();
}
};
sequence.complete = function(result) {
if(!aborted && typeof this.oncomplete === 'function') {
this.oncomplete(result);
}
};
sequence.fail = function(reason) {
this.complete(reason);
};
sequence.next = function(previousTaskResult) {
var result, exception;
if (aborted) {
return;
}
var task = this.shift();
if (task) {
try {
result = task.apply(null, arguments);
} catch(e) {
exception = e;
}
if (exception) {
this.fail(exception);
} else if (result && typeof result.then === 'function') {
result.then(this.next.bind(this), this.fail.bind(this));
} else {
this.next(result);
}
} else {
this.complete(previousTaskResult);
}
};
return sequence;
}
};
exports.TutorialUtils = TutorialUtils;
})(window);