Source: app.js

/**
 * @module stb/app
 * @author Stanislav Kalashnik <sk@infomir.eu>
 * @license GNU GENERAL PUBLIC LICENSE Version 3
 */

'use strict';

var Model    = require('./model'),
	router   = require('./router'),
	keys     = require('./keys'),
	keyCodes = {},
	app, key;


require('./shims');


/**
 * @instance
 * @type {Model}
 */
app = new Model({
	/**
	 * Enable logging and debugging flag set by debug module at runtime.
	 *
	 * @type {boolean}
	 */
	debug: false,

	/**
	 * True if executed on the STB device, set by debug module at runtime.
	 *
	 * @type {boolean}
	 */
	host: true,

	/**
	 * Screen geometry and margins.
	 *
	 * @type {Object}
	 * @property {number} height Total screen height
	 * @property {number} width Total screen width
	 * @property {number} availTop top safe zone margin
	 * @property {number} availRight right safe zone margin
	 * @property {number} availBottom bottom safe zone margin
	 * @property {number} availLeft left safe zone margin
	 * @property {number} availHeight safe zone height
	 * @property {number} availWidth safe zone width
	 */
	screen: null,

	/**
	 * Timestamps data.
	 *
	 * @type {Object}
	 * @property {number} init application initialization time (right now)
	 * @property {number} load document onload event
	 * @property {number} done onload event sent and processed
	 */
	time: {
		init: +new Date(),
		load: 0,
		done: 0
	}
});


/**
 * Set crops, total, content size and link the corresponding CSS file.
 *
 * @param {Object} metrics screen params specific to resolution
 *
 * @return {boolean} operation status
 */
app.setScreen = function ( metrics ) {
	var linkCSS;

	if ( DEBUG ) {
		if ( arguments.length !== 1 ) { throw 'wrong arguments number'; }
	}

	if ( metrics ) {
		if ( DEBUG ) {
			if ( typeof metrics !== 'object' ) { throw 'wrong metrics type'; }
		}

		// calculate and extend
		metrics.availHeight = metrics.height - (metrics.availTop + metrics.availBottom);
		metrics.availWidth  = metrics.width - (metrics.availLeft + metrics.availRight);

		// set max browser window size
		window.moveTo(0, 0);
		window.resizeTo(metrics.width, metrics.height);

		// get the link tag
		linkCSS = document.querySelector('link[rel=stylesheet]');

		// already was initialized
		if ( linkCSS && linkCSS instanceof HTMLLinkElement ) {
			// remove all current CSS styles
			document.head.removeChild(linkCSS);
		}

		// load CSS file base on resolution
		linkCSS = document.createElement('link');
		linkCSS.rel  = 'stylesheet';
		linkCSS.href = 'css/' + (DEBUG ? 'develop.' : 'release.') + metrics.height + '.css';
		document.head.appendChild(linkCSS);

		// provide global access
		this.data.screen = metrics;

		return true;
	}

	// nothing has applied
	return false;
};

// define events constants

/**
 * The player reached the end of the media content or detected a discontinuity of the stream
 *
 * @const {number} EVENT_END_OF_FILE
 * @default 1
 */
app.EVENT_END_OF_FILE = 1;

/**
 * Information on audio and video tracks of the media content is received. It's now possible to call gSTB.GetAudioPIDs etc.
 *
 * @const {number} EVENT_GET_MEDIA_INFO
 * @default 2
 */
app.EVENT_GET_MEDIA_INFO = 2;

/**
 * Video and/or audio playback has begun
 *
 * @const {number} EVENT_PLAYBACK_BEGIN
 * @default 4
 */
app.EVENT_PLAYBACK_BEGIN = 4;

/**
 * Error when opening the content: content not found on the server or connection with the server was rejected
 *
 * @const {number} EVENT_CONTENT_ERROR
 * @default 5
 */
app.EVENT_CONTENT_ERROR = 5;

/**
 * Detected DualMono AC-3 sound
 *
 * @const {number} EVENT_DUAL_MONO_DETECT
 * @default 6
 */
app.EVENT_DUAL_MONO_DETECT = 6;

/**
 * The decoder has received info about the content and started to play. It's now possible to call gSTB.GetVideoInfo
 *
 * @const {number} EVENT_INFO_GET
 * @default 7
 */
app.EVENT_INFO_GET = 7;

/**
 * Error occurred while loading external subtitles
 *
 * @const {number} EVENT_SUBTITLE_LOAD_ERROR
 * @default 8
 */
app.EVENT_SUBTITLE_LOAD_ERROR = 8;

/**
 * Found new teletext subtitles in stream
 *
 * @const {number} EVENT_SUBTITLE_FIND
 * @default 9
 */
app.EVENT_SUBTITLE_FIND = 9;

/**
 * HDMI device has been connected
 *
 * @const {number} EVENT_HDMI_CONNECT
 * @default 32
 */
app.EVENT_HDMI_CONNECT = 32;

/**
 * HDMI device has been disconnected
 *
 * @const {number} EVENT_HDMI_DISCONNECT
 * @default 33
 */
app.EVENT_HDMI_DISCONNECT = 33;

/**
 * Recording task has been finished successfully. See Appendix 13. JavaScript API for PVR subsystem
 *
 * @const {number} EVENT_RECORD_FINISH_SUCCESSFULL
 * @default 34
 */
app.EVENT_RECORD_FINISH_SUCCESSFULL = 34;

/**
 * Recording task has been finished with error. See Appendix 13. JavaScript API for PVR subsystem
 *
 * @const {number} EVENT_RECORD_FINISH_ERROR
 * @default 35
 */
app.EVENT_RECORD_FINISH_ERROR = 35;

/**
 * Scanning DVB Channel in progress
 *
 * @const {number} EVENT_DVB_SCANING
 * @default 40
 */
app.EVENT_DVB_SCANING = 40;

/**
 * Scanning DVB Channel found
 *
 * @const {number} EVENT_DVB_FOUND
 * @default 41
 */
app.EVENT_DVB_FOUND = 41;

/**
 * DVB Channel EPG update
 *
 * @const {number} EVENT_DVB_CHANELL_EPG_UPDATE
 * @default 42
 */
app.EVENT_DVB_CHANELL_EPG_UPDATE = 42;

/**
 * DVB antenna power off
 *
 * @const {number} EVENT_DVB_ANTENNA_OFF
 * @default 43
 */
app.EVENT_DVB_ANTENNA_OFF = 43;


// apply screen size, position and margins
app.setScreen(require('../../../config/metrics')[screen.height]);

// extract key codes
for ( key in keys ) {
	if ( key === 'volumeUp' || key === 'volumeDown' ) {
		continue;
	}
	// no need to save key names
	keyCodes[keys[key]] = true;
}

/**
 * The load event is fired when a resource and its dependent resources have finished loading.
 *
 * Control flow:
 *   1. Global handler.
 *   2. Each page handler.
 *   3. Application DONE event.
 *
 * @see https://developer.mozilla.org/en-US/docs/Web/Reference/Events/load
 *
 * @param {Event} event generated object with event data
 */
window.addEventListener('load', function globalEventListenerLoad ( event ) {
	var path;

	debug.event(event);

	// time mark
	app.data.time.load = event.timeStamp;

	// require device event listener for stb target
	//require('./targets/stb/events');

	// global handler
	// there are some listeners
	if ( app.events[event.type] !== undefined ) {
		// notify listeners
		app.emit(event.type, event);
	}

	// local handler on each page
	router.pages.forEach(function forEachPages ( page ) {
		debug.log('component ' + page.constructor.name + '.' + page.id + ' load', 'green');

		// there are some listeners
		if ( page.events[event.type] !== undefined ) {
			// notify listeners
			page.emit(event.type, event);
		}
	});

	// go to the given page if set
	if ( location.hash ) {
		path = router.parse(location.hash);
		router.navigate(path.name, path.data);
	}

	// time mark
	app.data.time.done = +new Date();

	// everything is ready
	// and there are some listeners
	if ( app.events['done'] !== undefined ) {
		// notify listeners
		app.emit('done', event);
	}
});


/**
 * The unload event is fired when the document or a child resource is being unloaded.
 *
 * Control flow:
 *   1. Each page handler.
 *   2. Global handler.
 *
 * @see https://developer.mozilla.org/en-US/docs/Web/Reference/Events/unload
 *
 * @param {Event} event generated object with event data
 */
window.addEventListener('unload', function globalEventListenerUnload ( event ) {
	debug.event(event);

	// global handler
	// there are some listeners
	if ( app.events[event.type] !== undefined ) {
		// notify listeners
		app.emit(event.type, event);
	}

	// local handler on each page
	router.pages.forEach(function forEachPages ( page ) {
		// there are some listeners
		if ( page.events[event.type] !== undefined ) {
			// notify listeners
			page.emit(event.type, event);
		}
	});
});


///**
// * The hashchange event is fired when the fragment identifier of the URL has changed (the part of the URL that follows the # symbol, including the # symbol).
// * @see https://developer.mozilla.org/en-US/docs/Web/Reference/Events/hashchange
// */
//window.addEventListener('hashchange', function globalEventListenerHashChange ( event ) {
//	//var page, data;
//
//	console.log(event);
//	router.emit('change');
//
//	//debug.event(event);
//	//debug.inspect(event);
//    //
//	//app.emit(event.type, event);
//
//	//app.parseHash();
//
////	data = document.location.hash.split('/');
////
////	// the page is given
////	if ( data.length > 0 && (page = decodeURIComponent(data.shift().slice(1))) ) {
////		// the page params are given
////		if ( data.length > 0 ) {
////			data = data.map(decodeURIComponent);
////		}
////
////		app.emit(event.type, {page: page, data: data});
////	}
//});


/**
 * The error event is fired when a resource failed to load.
 *
 * @see https://developer.mozilla.org/en-US/docs/Web/Reference/Events/error
 *
 * @param {Event} event generated object with event data
 */
window.addEventListener('error', function globalEventListenerError ( event ) {
	debug.event(event);
});


function globalEventListenerKeydown ( event ) {
	var page = router.current;

	if ( DEBUG ) {
		if ( page === null || page === undefined ) { throw 'app should have at least one page'; }
	}

	// filter phantoms
	if ( event.keyCode === 0 ) { return; }

	// combined key code
	event.code = event.keyCode;

	// apply key modifiers
	if ( event.shiftKey ) { event.code += 1000; }
	if ( event.altKey )   { event.code += 2000; }

	debug.event(event);

	// current component handler
	if ( page.activeComponent && page.activeComponent !== page ) {
		// component is available and not page itself
		if ( page.activeComponent.events[event.type] !== undefined ) {
			// there are some listeners
			page.activeComponent.emit(event.type, event);
		}
	}

	// page handler
	if ( !event.stop ) {
		// not prevented
		if ( page.events[event.type] !== undefined ) {
			// there are some listeners
			page.emit(event.type, event);
		}
	}

	// global app handler
	if ( !event.stop ) {
		// not prevented
		if ( app.events[event.type] !== undefined ) {
			// there are some listeners
			app.emit(event.type, event);
		}
	}

	// suppress non-printable keys in stb device (not in your browser)
	if ( app.data.host && keyCodes[event.code] ) {
		event.preventDefault();
	}
}


/**
 * The keydown event is fired when a key is pressed down.
 * Set event.stop to true in order to prevent bubbling.
 *
 * Control flow:
 *   1. Current active component on the active page.
 *   2. Current active page itself.
 *   3. Application.
 *
 * @see https://developer.mozilla.org/en-US/docs/Web/Reference/Events/keydown
 *
 * @param {Event} event generated object with event data
 */
window.addEventListener('keydown', globalEventListenerKeydown);


/**
 * The keypress event is fired when press a printable character.
 * Delivers the event only to activeComponent at active page.
 *
 * @see https://developer.mozilla.org/en-US/docs/Web/Reference/Events/keypress
 *
 * @param {Event} event generated object with event data
 * @param {string} event.char entered character
 */
window.addEventListener('keypress', function ( event ) {
	var page = router.current;

	if ( DEBUG ) {
		if ( page === null || page === undefined ) { throw 'app should have at least one page'; }
	}

	//debug.event(event);

	// current component handler
	if ( page.activeComponent && page.activeComponent !== page ) {
		// component is available and not page itself
		if ( page.activeComponent.events[event.type] !== undefined ) {
			// there are some listeners
			page.activeComponent.emit(event.type, event);
		}
	}
});


/**
 * The click event is fired when a pointing device button (usually a mouse button) is pressed and released on a single element.
 *
 * @see https://developer.mozilla.org/en-US/docs/Web/Reference/Events/click
 *
 * @param {Event} event generated object with event data
 */
window.addEventListener('click', function globalEventListenerClick ( event ) {
	debug.event(event);
});


/**
 * The contextmenu event is fired when the right button of the mouse is clicked (before the context menu is displayed),
 * or when the context menu key is pressed (in which case the context menu is displayed at the bottom left of the focused
 * element, unless the element is a tree, in which case the context menu is displayed at the bottom left of the current row).
 *
 * @see https://developer.mozilla.org/en-US/docs/Web/Reference/Events/contextmenu
 *
 * @param {Event} event generated object with event data
 */
window.addEventListener('contextmenu', function globalEventListenerContextmenu ( event ) {
	//var kbEvent = {}; //Object.create(document.createEvent('KeyboardEvent'));

	debug.event(event);

	//kbEvent.type    = 'keydown';
	//kbEvent.keyCode = 8;

	//debug.log(kbEvent.type);

	//globalEventListenerKeydown(kbEvent);
	//var event = document.createEvent('KeyboardEvent');
	//event.initEvent('keydown', true, true);

	//document.dispatchEvent(kbEvent);

	if ( !DEBUG ) {
		// disable right click in release mode
		event.preventDefault();
	}
});


///**
// * The wheel event is fired when a wheel button of a pointing device (usually a mouse) is rotated.
// * @see https://developer.mozilla.org/en-US/docs/Web/Reference/Events/wheel
// */
//window.addEventListener('wheel', function globalEventListenerWheel ( event ) {
//	var page = router.current;
//
//	debug.event(event);
//
//	event.preventDefault();
//	event.stopPropagation();
//
//	// local handler
//	if ( page ) {
//		if ( page.activeComponent && page.activeComponent !== page ) {
//			page.activeComponent.emit(event.type, event);
//		}
//
//		if ( !event.stop ) {
//			// not prevented
//			page.emit(event.type, event);
//		}
//	}
//});


// public
module.exports = app;