/** * '||''''| . . * || . .. ... .... ... .... .... .||. .... .||. .... * ||''| || || '|. | '' .|| ||. ' || '' .|| || ||. ' * || || || '|.| .|' || . '|.. || .|' || || . '|.. * .||.....| .||. ||. '| '|..'|' |'..|' '|.' '|..'|' '|.' |'..|' * * Copyright (c) 2012 Display:inline * @mail contact@display-inline.fr * * Permission is hereby granted, free of charge, to any person obtaining a copy of this software * and associated documentation files (the "Software"), to deal in the Software without restriction, * including without limitation the rights to use, copy, modify, merge, publish, distribute, * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all copies or * substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING * BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. * * ------------------------------------------------------------------------------------------------- * * How does this thing work? * * This extension is basically built around an IndexedDB database which stores the sales statement. * * There are 2 main types of objects: * * - Publishers: those objects are meant to load and store specific data, and provide an API to * add callbacks which will be fired everytime the data is loaded or changes. There are 3 types * of publishers: * - options (stored using LocalStorage) * - ressources (data loaded via Envato API) * - requests (data loaded in the database) * Publishers can listen to others publishers, so they may update their internal data whenever * something changes (an option, for instance) * * - Widgets: those are the actual visible parts of the dashboard. They listen to one or several * publishers, and are refreshed everytime one of them changes. * * All objects are stored in the library object. * * The startup process: * * - The local database is opened, and the tables are created if they don't exist * - The latest statement files are loaded to refresh the database * - If an alternate currency is set, the current rate is loaded, and applyed to recents sales * in the database * - Then the main widgets screen is built and shown * * Content summary: * * - variables declaration * - init process * - core functions * - classes * - controls declaration * - options declaration * - request declaration * - widgets declaration * - currency functions * - utility functions * - Elychart templates * * Known issues: * - currencies that convert to high numbers spread out of their columns * * Suggested features: * - Time range selector, instead of buttons * - Full-screen mode */ ;(function ($, window, document, undefined) { /* * Global vars */ // Database and local storage var db, storage, // Username and domain username = $.trim( $( '#user_username' ).text() ), domain = document.location.protocol + '//' + document.location.hostname, // Prefixed names storageName = 'envastats-' + username + '-', dbName = 'envastats_' + username.replace( /[^a-zA-Z0-9]+/, '' ), /* * Envato vars */ envato = { // API api: { version: 3, url: 'http://marketplace.envato.com/api/v3/' }, // Statements statement: { // Type codes, used to save storing space in DB types: { 'sale': 1, 'withdrawal': 2, 'referral_cut': 3, 'deposit': 4, 'purchase': 5, 'refund': 6, // Not sure of this one 'sale_reversal': 7, 'other': 0 // For unknown types } }, // Badges badges: { // Paws paws: [ { start: 0, name: 'Orange Paw' }, { start: 100, name: 'Brown Paw' }, { start: 1000, name: 'Red Paw' }, { start: 5000, name: 'Black Paw' }, { start: 10000, name: 'Silver Paw' }, { start: 50000, name: 'Gold Paw' }, { start: 100000, name: 'Blue Steel Paw' }, { start: 250000, name: 'Plutonium Paw' }, { start: 1000000, name: 'Power Elite Paw' } ], // Commission/elite levels elite: [ { start: 0, name: 'Rate 50%' }, { start: 3750, name: 'Rate 51%' }, { start: 7500, name: 'Rate 52%' }, { start: 11250, name: 'Rate 53%' }, { start: 15000, name: 'Rate 54%' }, { start: 18750, name: 'Rate 55%' }, { start: 22500, name: 'Rate 56%' }, { start: 26250, name: 'Rate 57%' }, { start: 30000, name: 'Rate 58%' }, { start: 33750, name: 'Rate 59%' }, { start: 37500, name: 'Rate 60%' }, { start: 41250, name: 'Rate 61%' }, { start: 45000, name: 'Rate 62%' }, { start: 48750, name: 'Rate 63%' }, { start: 52500, name: 'Rate 64%' }, { start: 56250, name: 'Rate 65%' }, { start: 60000, name: 'Rate 66%' }, { start: 63750, name: 'Rate 67%' }, { start: 67500, name: 'Rate 68%' }, { start: 71250, name: 'Rate 69%' }, { start: 75000, name: 'Elite level 1' }, { start: 125000, name: 'Elite level 2' }, { start: 250000, name: 'Elite level 3' }, { start: 500000, name: 'Elite level 4' }, { start: 750000, name: 'Elite level 5' }, { start: 1000000, name: 'Power Elite level 1' }, { start: 2500000, name: 'Power Elite level 2' }, { start: 5000000, name: 'Power Elite level 3' }, { start: 10000000, name: 'Power Elite level 4' } ] }, // Currency of statements currency: 'USD' }, /* * Currency rates using Open Exchange Rates API */ rates = { // API api: { // Current rates latest: 'http://openexchangerates.org/api/latest.json', // Historical rates historical: 'http://openexchangerates.org/api/historical/{{date}}.json' }, // Validity of latest rates (2H) latestExpiration: 2 * 60 * 60 * 1000, // List of awaiting callbacks callbacks: {}, // State if a database update of rates is going on updating: false, // If option changes while a previous update is still running, store new value here waiting: false, // Currencies currencies: { EUR: 'Euro', AED: 'United Arab Emirates Dirham', AFN: 'Afghan Afghani', ALL: 'Albanian Lek', AMD: 'Armenian Dram', ANG: 'Netherlands Antillean Guilder', AOA: 'Angolan Kwanza', ARS: 'Argentine Peso', AUD: 'Australian Dollar', AWG: 'Aruban Florin', AZN: 'Azerbaijani Manat', BAM: 'Bosnia-Herzegovina Convertible Mark', BBD: 'Barbadian Dollar', BDT: 'Bangladeshi Taka', BGN: 'Bulgarian Lev', BHD: 'Bahraini Dinar', BIF: 'Burundian Franc', BMD: 'Bermudan Dollar', BND: 'Brunei Dollar', BOB: 'Bolivian Boliviano', BRL: 'Brazilian Real', BSD: 'Bahamian Dollar', BTN: 'Bhutanese Ngultrum', BWP: 'Botswanan Pula', BYR: 'Belarusian Ruble', BZD: 'Belize Dollar', CAD: 'Canadian Dollar', CDF: 'Congolese Franc', CHF: 'Swiss Franc', CLF: 'Chilean Unit of Account (UF)', CLP: 'Chilean Peso', CNY: 'Chinese Yuan', COP: 'Colombian Peso', CRC: 'Costa Rican Colón', CUP: 'Cuban Peso', CVE: 'Cape Verdean Escudo', CZK: 'Czech Republic Koruna', DJF: 'Djiboutian Franc', DKK: 'Danish Krone', DOP: 'Dominican Peso', DZD: 'Algerian Dinar', EGP: 'Egyptian Pound', ETB: 'Ethiopian Birr', FJD: 'Fijian Dollar', FKP: 'Falkland Islands Pound', GBP: 'British Pound Sterling', GEL: 'Georgian Lari', GHS: 'Ghanaian Cedi', GIP: 'Gibraltar Pound', GMD: 'Gambian Dalasi', GNF: 'Guinean Franc', GTQ: 'Guatemalan Quetzal', GYD: 'Guyanaese Dollar', HKD: 'Hong Kong Dollar', HNL: 'Honduran Lempira', HRK: 'Croatian Kuna', HTG: 'Haitian Gourde', HUF: 'Hungarian Forint', IDR: 'Indonesian Rupiah', ILS: 'Israeli New Sheqel', INR: 'Indian Rupee', IQD: 'Iraqi Dinar', IRR: 'Iranian Rial', ISK: 'Icelandic Króna', JMD: 'Jamaican Dollar', JOD: 'Jordanian Dinar', JPY: 'Japanese Yen', KES: 'Kenyan Shilling', KGS: 'Kyrgystani Som', KHR: 'Cambodian Riel', KMF: 'Comorian Franc', KPW: 'North Korean Won', KRW: 'South Korean Won', KWD: 'Kuwaiti Dinar', KZT: 'Kazakhstani Tenge', LAK: 'Laotian Kip', LBP: 'Lebanese Pound', LKR: 'Sri Lankan Rupee', LRD: 'Liberian Dollar', LSL: 'Lesotho Loti', LTL: 'Lithuanian Litas', LVL: 'Latvian Lats', LYD: 'Libyan Dinar', MAD: 'Moroccan Dirham', MDL: 'Moldovan Leu', MGA: 'Malagasy Ariary', MKD: 'Macedonian Denar', MMK: 'Myanma Kyat', MNT: 'Mongolian Tugrik', MOP: 'Macanese Pataca', MRO: 'Mauritanian Ouguiya', MUR: 'Mauritian Rupee', MVR: 'Maldivian Rufiyaa', MWK: 'Malawian Kwacha', MXN: 'Mexican Peso', MYR: 'Malaysian Ringgit', MZN: 'Mozambican Metical', NAD: 'Namibian Dollar', NGN: 'Nigerian Naira', NIO: 'Nicaraguan Córdoba', NOK: 'Norwegian Krone', NPR: 'Nepalese Rupee', NZD: 'New Zealand Dollar', OMR: 'Omani Rial', PAB: 'Panamanian Balboa', PEN: 'Peruvian Nuevo Sol', PGK: 'Papua New Guinean Kina', PHP: 'Philippine Peso', PKR: 'Pakistani Rupee', PLN: 'Polish Zloty', PYG: 'Paraguayan Guarani', QAR: 'Qatari Rial', RON: 'Romanian Leu', RSD: 'Serbian Dinar', RUB: 'Russian Ruble', RWF: 'Rwandan Franc', SAR: 'Saudi Riyal', SBD: 'Solomon Islands Dollar', SCR: 'Seychellois Rupee', SDG: 'Sudanese Pound', SEK: 'Swedish Krona', SGD: 'Singapore Dollar', SHP: 'Saint Helena Pound', SLL: 'Sierra Leonean Leone', SOS: 'Somali Shilling', SRD: 'Surinamese Dollar', STD: 'São Tomé and Príncipe Dobra', SVC: 'Salvadoran Colón', SYP: 'Syrian Pound', SZL: 'Swazi Lilangeni', THB: 'Thai Baht', TJS: 'Tajikistani Somoni', TMT: 'Turkmenistani Manat', TND: 'Tunisian Dinar', TOP: 'Tongan Paʻanga', TRY: 'Turkish Lira', TTD: 'Trinidad and Tobago Dollar', TWD: 'New Taiwan Dollar', TZS: 'Tanzanian Shilling', UAH: 'Ukrainian Hryvnia', UGX: 'Ugandan Shilling', USD: 'United States Dollar', UYU: 'Uruguayan Peso', UZS: 'Uzbekistan Som', VEF: 'Venezuelan Bolívar', VND: 'Vietnamese Dong', VUV: 'Vanuatu Vatu', WST: 'Samoan Tala', XAF: 'CFA Franc BEAC', XCD: 'East Caribbean Dollar', XDR: 'Special Drawing Rights', XOF: 'CFA Franc BCEAO', XPF: 'CFP Franc', YER: 'Yemeni Rial', ZAR: 'South African Rand', ZMK: 'Zambian Kwacha', ZWL: 'Zimbabwean Dollar' }, // Symbols symbols: { AFN: '؋', ARS: '$', AWG: 'ƒ', AUD: '$', AZN: 'ман', BSD: '$', BBD: '$', BYR: 'p.', BZD: 'BZ$', BMD: '$', BOB: '$b', BAM: 'KM', BWP: 'P', BGN: 'лв', BRL: 'R$', BND: '$', KHR: '៛', CAD: '$', KYD: '$', CLP: '$', CNY: '¥', COP: '$', CRC: '₡', HRK: 'kn', CUP: '₱', CZK: 'Kč', DKK: 'kr', DOP: 'RD$', XCD: '$', EGP: '£', SVC: '$', EEK: 'kr', EUR: '€', FKP: '£', FJD: '$', GHC: '¢', GIP: '£', GTQ: 'Q', GGP: '£', GYD: '$', HNL: 'L', HKD: '$', HUF: 'Ft', ISK: 'kr', INR: '₹', IDR: 'Rp', IRR: '﷼', IMP: '£', ILS: '₪', JMD: 'J$', JPY: '¥', JEP: '£', KZT: 'лв', KPW: '₩', KRW: '₩', KGS: 'лв', LAK: '₭', LVL: 'Ls', LBP: '£', LRD: '$', LTL: 'Lt', MKD: 'ден', MYR: 'RM', MUR: '₨', MXN: '$', MNT: '₮', MZN: 'MT', NAD: '$', NPR: '₨', ANG: 'ƒ', NZD: '$', NIO: 'C$', NGN: '₦', NOK: 'kr', OMR: '﷼', PKR: '₨', PAB: 'B/.', PYG: 'Gs', PEN: 'S/.', PHP: '₱', PLN: 'zł', QAR: '﷼', RUB: 'руб', SHP: '£', SAR: '﷼', RSD: 'Дин.', SCR: '₨', SGD: '$', SBD: '$', SOS: 'S', ZAR: 'R', LKR: '₨', SEK: 'kr', SRD: '$', SYP: '£', TWD: 'NT$', THB: '฿', TTD: 'TT$', TRL: '₤', TVD: '$', UAH: '₴', GBP: '£', USD: '$', UYU: '$U', UZS: 'лв', VEF: 'Bs', VND: '₫', YER: '﷼', ZWD: 'Z$' } }, /* * Dates * * Working with dates is a bit tricky (for me) because of timezones offsets, so here's how I handle it: * - reference time is Envato's time, because statements files use it * - the date object 'now' handles the actual time at Envato * - other dates (for instance, 'today') are based on 'now's date and used only for the date functions, the time part is ignored * - widgets/requests that should be refreshed everytime the time changes just need to listen to the option now (updated every minute) * - widgets/requests that should be refreshed everytime the date changes just need to listen to the option today * - firstSale is the date of the first sale in the database, use only for the date part (time not handled) */ // Dates (local and at Envato's HQ, GMT +10) nowLocal = new Date(), timeOffset = ( nowLocal.getTimezoneOffset() * 60000 ) + ( 10 * 3600000 ), now = new Date( nowLocal.getTime() + timeOffset ), nowMonth = now.getMonth(), nowYear = now.getFullYear(), today = new Date( nowYear, nowMonth, now.getDate() ), firstSale = false, /* * l10n / i18n * Basic implementation */ l10n = { // Numeric formats thousands_sep: chrome.i18n.getMessage( 'thousands_sep' ), dec_point: chrome.i18n.getMessage( 'dec_point' ), currency_display: chrome.i18n.getMessage( 'currency_display' ), // Date format date_formats: { 'default': chrome.i18n.getMessage( 'dateFormat_default' ), shortDate: chrome.i18n.getMessage( 'dateFormat_shortDate' ), mediumDate: chrome.i18n.getMessage( 'dateFormat_mediumDate' ), longDate: chrome.i18n.getMessage( 'dateFormat_longDate' ), fullDate: chrome.i18n.getMessage( 'dateFormat_fullDate' ), shortTime: chrome.i18n.getMessage( 'dateFormat_shortTime' ), mediumTime: chrome.i18n.getMessage( 'dateFormat_mediumTime' ), longTime: chrome.i18n.getMessage( 'dateFormat_longTime' ), isoDate: chrome.i18n.getMessage( 'dateFormat_isoDate' ), isoTime: chrome.i18n.getMessage( 'dateFormat_isoTime' ), sqlDatetime: chrome.i18n.getMessage( 'dateFormat_sqlDatetime' ), week: chrome.i18n.getMessage( 'dateFormat_week' ), month: chrome.i18n.getMessage( 'dateFormat_month' ) } }, i18n = { // Months/days names months: [ chrome.i18n.getMessage( 'months_January' ), chrome.i18n.getMessage( 'months_February' ), chrome.i18n.getMessage( 'months_March' ), chrome.i18n.getMessage( 'months_April' ), chrome.i18n.getMessage( 'months_May' ), chrome.i18n.getMessage( 'months_June' ), chrome.i18n.getMessage( 'months_July' ), chrome.i18n.getMessage( 'months_August' ), chrome.i18n.getMessage( 'months_September' ), chrome.i18n.getMessage( 'months_October' ), chrome.i18n.getMessage( 'months_November' ), chrome.i18n.getMessage( 'months_December' ) ], monthsShort: [ chrome.i18n.getMessage( 'monthsShort_January' ), chrome.i18n.getMessage( 'monthsShort_February' ), chrome.i18n.getMessage( 'monthsShort_March' ), chrome.i18n.getMessage( 'monthsShort_April' ), chrome.i18n.getMessage( 'monthsShort_May' ), chrome.i18n.getMessage( 'monthsShort_June' ), chrome.i18n.getMessage( 'monthsShort_July' ), chrome.i18n.getMessage( 'monthsShort_August' ), chrome.i18n.getMessage( 'monthsShort_September' ), chrome.i18n.getMessage( 'monthsShort_October' ), chrome.i18n.getMessage( 'monthsShort_November' ), chrome.i18n.getMessage( 'monthsShort_December' ) ], days: [ chrome.i18n.getMessage( 'days_Sunday' ), chrome.i18n.getMessage( 'days_Monday' ), chrome.i18n.getMessage( 'days_Tuesday' ), chrome.i18n.getMessage( 'days_Wednesday' ), chrome.i18n.getMessage( 'days_Thursday' ), chrome.i18n.getMessage( 'days_Friday' ), chrome.i18n.getMessage( 'days_Saturday' ) ], daysShort: [ chrome.i18n.getMessage( 'daysShort_Sunday' ), chrome.i18n.getMessage( 'daysShort_Monday' ), chrome.i18n.getMessage( 'daysShort_Tuesday' ), chrome.i18n.getMessage( 'daysShort_Wednesday' ), chrome.i18n.getMessage( 'daysShort_Thursday' ), chrome.i18n.getMessage( 'daysShort_Friday' ), chrome.i18n.getMessage( 'daysShort_Saturday' ) ], daysLetter: [ chrome.i18n.getMessage( 'daysLetter_Sunday' ), chrome.i18n.getMessage( 'daysLetter_Monday' ), chrome.i18n.getMessage( 'daysLetter_Tuesday' ), chrome.i18n.getMessage( 'daysLetter_Wednesday' ), chrome.i18n.getMessage( 'daysLetter_Thursday' ), chrome.i18n.getMessage( 'daysLetter_Friday' ), chrome.i18n.getMessage( 'daysLetter_Saturday' ) ] }, /* * Blocks */ // Main content wrapper wrapper = $( '
' ).insertBefore( '.content-l:first' ), // Screens and blocks screens = { current: false }, /* * Storage */ // Library library = { controls: {}, options: {}, ressources: { items: {} }, requests: {}, widgets: {} }, // List of declared widgets screens widgets = {}; /** * Get l10n value * @param string name l10n value name * @return string|array the localized value */ function __l10n( name ) { return l10n[ name ] || ''; } /** * Get i18n value * @param string name i18n value name * @return string|array the internationalized value */ function __i18n( name ) { return i18n[ name ] || ''; } /*************************************************************************/ /* Init plugin */ /*************************************************************************/ // Check environment if ( !username || username.length === 0 ) { return; } // Add init screen screens.init = addScreen( 'init' ).append( '' + chrome.i18n.getMessage( 'loadingDatabase' ) + '
' ).appendTo( screens.init ); showScreen( screens.init ); // Setup tables db = openDatabase( dbName, '1.0', 'Envastats database for ' + username, 5 * 1024 * 1024 ); db.transaction(function (tx) { //tx.executeSql( 'DROP TABLE IF EXISTS `statements`'); tx.executeSql( 'CREATE TABLE IF NOT EXISTS `statements` (' + ' `date` DATETIME NOT NULL ,' + ' `type` TINYINT(1) NOT NULL ,' + ' `detail` VARCHAR(255) NULL ,' + ' `item` INT NOT NULL ,' + ' `amount` FLOAT(7,2) NOT NULL ,' + ' `rate` FLOAT(4,1) NULL ,' + ' `price` FLOAT(7,2) NULL,' + ' `amount_converted` FLOAT(7,2) NOT NULL )' ); }, function ( e ) { // Couldn't create table screens.initStatus.removeClass(' envastats-loading' ).text( chrome.i18n.getMessage( 'errorInitDatabase' ) ); console.log( 'Error while initializing database: ' + e.message ); }, function () { // Startup process refreshStatementsTable( screens.initStatus, function() { finalizeInitialStatementRefresh( buildWidgetsScreen ); }, false ); }); /*************************************************************************/ /* Core functions */ /*************************************************************************/ /** * Update dates 'now' and 'today' * @return void */ function updateCoreDates() { // Update now nowLocal = new Date(); now.setTime( nowLocal.getTime() + timeOffset ); library.options.now.change(); // Update today if ( today.getDate() !== now.getDate() ) { // Reset day of moth to prevent auto correction today.setDate( 1 ); // Copy from now today.setFullYear( now.getFullYear() ); today.setMonth( now.getMonth() ); today.setDate( now.getDate() ); library.options.today.change(); } // Next update setTimeout( updateCoreDates, 60000 - ( ( nowLocal.getSeconds() * 1000 ) + nowLocal.getMilliseconds() ) ); } setTimeout( updateCoreDates, 60000 - ( ( nowLocal.getSeconds() * 1000 ) + nowLocal.getMilliseconds() ) ); /** * Get the database code of a statement type * @param string type the type name * @return int the internal code for the type */ function getStatementTypeCode( type ) { return envato.statement.types[ type ] || 0; } /** * Finalize database initial update * @param function callback a function to call when complete * @return void */ function finalizeInitialStatementRefresh( callback ) { var currency = library.options.currency.get(), lastFinalizedMonth = library.options.lastFinalizedMonth.get(), month, year; // Message screens.initStatus.text( 'Retrieving first sale...' ); // Update first sale date db.transaction( function (tx) { tx.executeSql( 'SELECT strftime(\'%d\', `date`) AS `day`,' + ' strftime(\'%m\', `date`) AS `month`,' + ' strftime(\'%Y\', `date`) AS `year`' + ' FROM `statements` WHERE `type`=? ORDER BY `date` ASC LIMIT 1', [ getStatementTypeCode( 'sale' ) ], function ( tx, result ) { var row; // If found if ( result.rows.length > 0 ) { // Convert to date row = result.rows.item( 0 ); firstSale = new Date( parseInt( row.year, 10 ), parseInt( row.month, 10 ) - 1, parseInt( row.day, 10 ) ); // Log console.log( 'First sale date: ' + displayDate( firstSale, 'longDate' ) ); } else { // Log console.log( 'No sales yet' ); } // Callback if ( callback ) { callback(); } }, function ( tx, e ) { console.log( 'Error while retrieving first sale date: ' + e.message ); // Callback if ( callback ) { callback(); } } ); } ); } /** * Build the widgets screen * @return void */ function buildWidgetsScreen() { // Prevent refresh of the screen before it is built var ready = false; // Function to refresh wrapper size and controls block position refreshScreen = function() { if ( ready ) { // Refresh screen size updateScreenHeight( screens.widgets ); } }; // Add widgets block screens.widgets = addScreen( 'widgets' ); // Build widgets buildWidgets( $( '' ).appendTo( screens.widgets ), refreshScreen ); // Controls block screens.widgetsControls = $( '' ).appendTo( screens.widgets ); // Build options buildControls( screens.widgetsControls ); // Set as ready ready = true; showScreen( screens.widgets ); // First refresh refreshScreen(); } /** * Build all widgets * @param jQuery screenDiv the block in which to build the widgets * @param function onResize a callback to fire for each resize * @return void */ function buildWidgets( screenDiv, onResize ) { // Get configuration var userWidgets = library.options.widgets.get(); // Main widget screen widgets.main = new WidgetsScreen( screenDiv, onResize ); // Create rows $.each( userWidgets, function ( rowName, rowConfig ) { // Object var row = new WidgetRow( widgets.main.newChildDiv(), rowConfig.height || 100 ); // Register widgets.main.addRow( rowName, row ); // Create widgets $.each( rowConfig.widgets, function ( widgetName, widgetConfig ) { var div = row.newChildDiv(), column, widget; // Position if ( widgetConfig.position ) { div.css( widgetConfig.position ); } // Type if ( widgetConfig.type === 'column' ) { // Object column = new WidgetColumn( div ); // Register row.addWidget( widgetName, column ); // Inner widgets $.each( widgetConfig.widgets, function ( subWidgetName, subWidgetConfig ) { column.addWidget( subWidgetName, new Widget( column.newChildDiv(), library.widgets[ subWidgetConfig.controller ], subWidgetConfig.options ) ); } ); } else { // Object widget = new Widget( div, library.widgets[ widgetConfig.controller ], widgetConfig.options ); // Register row.addWidget( widgetName, widget ); } } ); } ); } /** * Build controls block * @param jQuery blockControls the block in which to build the widgets * @return void */ function buildControls( blockControls ) { $.each( library.controls, function( name, control ) { control.build( blockControls ); } ); } /** * Add a main screen * @param string className the screen's class name, which will be prefixed by 'envastats-' * @return jQuery the new block object */ function addScreen( className ) { return $( '' ).appendTo( wrapper ); } /** * Show the given screen: slide it in position, set the correct block height * @param jQuery screen the block to show * @return void */ function showScreen( screen ) { // Hide previous screen if ( screens.current && screens.current[ 0 ] !== screen[ 0 ] ) { screens.current.animate( { left: '-100%' }, function () { // Reset position $( this ).css( 'left', '' ); } ); } // Show screen.animate( { left: '0%' } ); // Set correct height wrapper.animate( { height: screen.outerHeight() + 'px' } ); // Store as current screens.current = screen; } /** * Update the main div height to fit the given screen, if it is the current one * @param jQuery screen the block whose height to use * @return void */ function updateScreenHeight( screen ) { // If active screen if ( screens.current && screens.current[ 0 ] === screen[ 0 ] ) { // Set correct height wrapper.stop( true ).animate( { height: screen.outerHeight() + 'px' } ); } } /** * Refresh statement table * @param jQuery status the element to show progress status * @param function callback a function to call when complete * @param boolean reload use true to empty database and reload it * @return boolean */ function refreshStatementsTable( status, callback, reload ) { var lastMonth, lastYear, params, nbStatements, currentStatement = 1, nextMonth; // If reloading if ( reload ) { // Empty database db.transaction( function (tx) { tx.executeSql( 'DELETE FROM `statements`', [], function ( tx, result ) { console.log( 'Statement table has been truncated' ); }, function ( tx, e ) { console.log( 'Error while truncating statements table: ' + e.message ); } ); } ); // Clear cache setStoredValue( 'last-month', false ); setStoredValue( 'last-year', false ); } // When shall we start loading lastMonth = getStoredValue( 'last-month', false ); lastYear = getStoredValue( 'last-year', false ); // If never refreshed (or reload is true), parse first available statement date if ( lastMonth === false || !lastYear ) { // Defaults lastMonth = now.getMonth() + 1; lastYear = now.getFullYear(); // Past statements links $( '.sidebar-s .feature-list a[href^="/statement/"]' ).first().each( function () { params = /([0-9]+)-([0-9]+)$/.exec( this.href ); if ( params ) { lastMonth = parseInt( params[ 1 ], 10 ); lastYear = parseInt( params[ 2 ], 10 ); } } ); } // Number or statements files to load, including current one (always refreshed) nbStatements = Math.max( 1, ( lastYear != now.getFullYear() ) ? ( 13 - lastMonth ) + ( ( now.getFullYear() - lastYear - 1 ) * 12 ) + now.getMonth() + 1 : now.getMonth() + 2 - lastMonth ); // Function to parse months one after the other nextMonth = function () { // Increment month ++lastMonth; if ( lastMonth > 12 ) { lastMonth = 1; ++lastYear; } ++currentStatement; // If all files have been loaded if ( ( lastYear === now.getFullYear() && lastMonth > now.getMonth() + 1 ) || lastYear > now.getFullYear() ) { // Cache setStoredValue( 'last-month', now.getMonth() + 1 ); setStoredValue( 'last-year', now.getFullYear() ); // Force refresh of loaded requests $.each( library.requests, function ( name, request ) { if ( request.isLoaded() ) { request.load(); } } ); // Refresh statements rate finalizeStatementRefresh( callback ); // Stop return; } // Display status if ( status ) { status.text( chrome.i18n.getMessage( 'loadingStatementArchiveStatus', [ currentStatement, nbStatements ] ) ); } // Load next refreshMonthStatement( lastMonth, lastYear, nextMonth ); }; // Display status if ( status ) { status.text( chrome.i18n.getMessage( 'loadingStatementArchiveStatus', [ currentStatement, nbStatements ] ) ); } // First call refreshMonthStatement( lastMonth, lastYear, nextMonth ); } /** * Load the statement CSV file for a month and inject into database * @param int month the month (1-12) * @param int year the year * @param function callback a function to call when complete * @return void */ function refreshMonthStatement( month, year, callback ) { var url = '/statement/' + year + '-' + month + '.csv'; // Log console.log( 'Refreshing ' + month + '/' + year ); console.log( '* Downloading statements: ' + url ); // Load content $.ajax( url, { dataType: 'text', success: function ( data ) { var headers, columns = {}, i; console.log( '* File downloaded' ); // Detect empty request if ( !data || typeof data !== 'string' ) { console.log( '* Empty response from server, abort refresh for ' + month + '/' + year ); // Callback if ( callback ) { callback.call(); } return; } // Detect HTML error message else if ( data.substr( 0, 1 ) === '<' ) { console.log( '* Invalid response from server (probably updating), abort refresh for ' + month + '/' + year ); // Callback if ( callback ) { callback.call(); } return; } // Parse data = $.csv()( data ); // Headers headers = data.shift(); // Index columns for ( i = 0; i < headers.length; ++i ) { columns[ headers[i].toLowerCase().replace( /[^a-zA-Z0-9]+/, '_' ) ] = i; } // Drop existing entries in database db.transaction( function ( tx ) { tx.executeSql( 'DELETE FROM `statements` WHERE strftime(\'%m-%Y\', `date`)=?', [ padDateValue( month ) + '-' + year ], function ( tx, result ) { console.log( '* Clear month statements: removed ' + result.rowsAffected + ' row(s)' ); // Insert rows db.transaction( function ( tx ) { $.each( data, function ( i ) { // Amount var amount = parseFloat( this[ columns.amount ] ) || 0; // Insert tx.executeSql( 'INSERT INTO `statements` (`date`, `type`, `detail`, `item`, `amount`, `rate`, `price`, `amount_converted`) VALUES (?, ?, ?, ?, ?, ?, ?, ?)', [ this[ columns.date ].substr( 0, 19 ), getStatementTypeCode( this[ columns.type ] ), ( this[ columns.type ] === 'sale' ) ? '' : this[ columns.detail ], this[ columns.item_id ], amount, parseFloat( this[ columns.rate ] ) || 0, parseFloat( this[ columns.price ] ) || 0, amount ], function( tx, result ) {}, function ( tx, e ) { console.log( '* Error while adding new statement: ' + e.message ); } ); } ); }, function( e ) { // Callback if ( callback ) { callback.call(); } }, function() { console.log( '* Done, added ' + data.length + ' statements for ' + month + '/' + year ); // Callback if ( callback ) { callback.call(); } } ); } ); }, function ( e ) { console.log( '* Error while clearing month statements: ' + e.message ); // Callback if ( callback ) { callback.call(); } } ); }, error: function () { console.log( '* Error while downloading file, aborting refresh for ' + month + '/' + year ); // Callback if ( callback ) { callback.call(); } } } ); } /** * Finalize statements refresh: apply currency rate to newly added sales, and refresh rate for recent ones * @param function callback a function to call when complete * @return void */ function finalizeStatementRefresh( callback ) { var currencyAlt = library.options.currencyAlt.get(), lastFinalizedMonth; // If using alternative currency if ( currencyAlt ) { // Message screens.initStatus.text( chrome.i18n.getMessage( 'refreshingCurrencyRates' ) ); console.log( 'Refreshing currency rates...' ); // If already partialy updated lastFinalizedMonth = library.options.lastFinalizedMonth.get(); if ( lastFinalizedMonth ) { // Refresh from last finalized month + 1 month = lastFinalizedMonth.month + 1; year = lastFinalizedMonth.year; if ( month > 12 ) { month = 1; ++year; } updateDatabaseConvertedAmounts( currencyAlt, year, month, callback ); } else { // Whole database refresh setNewAltCurrency( currencyAlt, callback ); } } else { // Callback if ( callback ) { callback(); } } } /** * Get a stored value, using an object to preserve types * @param string name the name of the value * @param mixed def the default value if not set * @return mixed the value, or def */ function getStoredValue( name, def ) { // Values storage object if ( !storage ) { storage = getStoredObject( 'storage', {} ); } return ( storage[ name ] === null || storage[ name ] === undefined ) ? def : storage[ name ]; } /** * Set a stored value, using an object to preserve types * @param string name the name of the value * @param mixed value the value * @return void */ function setStoredValue( name, value ) { // Values storage object if ( !storage ) { storage = getStoredObject( 'storage', {} ); } storage[ name ] = value; setStoredObject( 'storage', storage ); } /** * Get a stored object value * @param string name the name of the value * @param object def the default value if not set * @return object the value, or def */ function getStoredObject( name, def ) { var value = localStorage.getItem( storageName + name ); return ( value === null || value === undefined || value === 'undefined' ) ? def : JSON.parse( value ); } /** * Set a stored object value * @param string name the name of the value * @param object value the value * @return void */ function setStoredObject( name, value ) { localStorage.setItem( storageName + name, JSON.stringify( value ) ); } /** * Reset all stored values * @return void */ function resetStorage() { // Empty storage setStoredObject( 'options', {} ); setStoredObject( 'ressources', {} ); setStoredObject( 'rate', {} ); setStoredObject( 'stoage', {} ); // Reset options $.each( library.options, function( name, object ) { object.reset(); } ); } /** * Display an amount in the given currency, using corresponding l10n * @param string currency the currency to use * @param float amount the amount to display * @param int decimals number of decimals to display (default: 0) * @return string the formated amount */ function displayCurrencyAmount( currency, amount, decimals ) { var symbol = rates.symbols[ currency ] || currency; amount = number_format( amount, decimals || 0 ); return __l10n( 'currency_display' ).replace( '{currency}', currency ) .replace( '{symbol}', symbol ) .replace( '{amount}', amount ); } /** * Display an date, using corresponding l10n * @param Date date the date object * @param string format name of the date format (default: 'default') * @return string the formated date string */ function displayDate( date, format ) { // Format var formats = __l10n( 'date_formats' ), template = ( format && formats[ format ] ) ? formats[ format ] : formats[ 'default' ], // Date parts day = date.getDay(), dayofmonth = date.getDate(), month = date.getMonth(), hours = date.getHours(), // Final string output = '', // Work vars i, chr; // Parse template for ( i = 0; i < template.length; ++i ) { chr = template.substr( i, 1 ); switch (chr) { // Escaped char case '\\': output += template.substr( i + 1, 1 ); ++i; break; // Day case 'd': output += padDateValue( dayofmonth ); break; // Day of the month, 2 digits with leading zeros case 'D': output += __i18n( 'daysShort' )[ day ]; break; // A textual representation of a day, three letters case 'j': output += dayofmonth; break; // Day of the month without leading zeros case 'l': output += __i18n( 'days' )[ day ]; break; // A full textual representation of the day of the week case 'N': output += day || 7; break; // ISO-8601 numeric representation of the day of the week (added in PHP 5.1.0) case 'w': output += day; break; // Numeric representation of the day of the week case 'z': output += getDayOfYear( date ); break; // The day of the year (starting from 0) // Week case 'W': output += getWeekNumber( date ); break; // ISO-8601 week number of year, weeks starting on Monday (added in PHP 4.1.0) // Month case 'F': output += __i18n( 'months' )[ month ]; break; // A full textual representation of a month, such as January or March case 'm': output += padDateValue( month + 1 ); break; // Numeric representation of a month, with leading zeros case 'M': output += __i18n( 'monthsShort' )[ month ]; break; // A short textual representation of a month, three letters case 'n': output += ( month + 1 ); break; // Numeric representation of a month, without leading zeros // Year case 'Y': output += date.getFullYear(); break; // A full numeric representation of a year, 4 digits case 'y': output += date.getFullYear().substr( 2, 2 ); break; // A two digit representation of a year // Time case 'a': output += ( hours < 12 ) ? 'am' : 'pm'; break; // Lowercase Ante meridiem and Post meridiem case 'A': output += ( hours < 12 ) ? 'AM' : 'PM'; break; // Uppercase Ante meridiem and Post meridiem case 'g': output += ( hours < 12 ) ? hours : hours - 12; break; // 12-hour format of an hour without leading zeros case 'G': output += hours; break; // 24-hour format of an hour without leading zeros case 'h': output += padDateValue( ( hours < 12 ) ? hours : hours - 12 ); break; // 12-hour format of an hour with leading zeros case 'H': output += padDateValue( hours ); break; // 24-hour format of an hour with leading zeros case 'i': output += padDateValue( date.getMinutes() ); break; // Minutes with leading zeros case 's': output += padDateValue( date.getSeconds() ); break; // Seconds, with leading zeros default: output += chr; break; } } return output; } /*************************************************************************/ /* Control class */ /*************************************************************************/ /** * Constructor * @param string markup the control's markup * @param function init the function to init * @param object children list of sub-elements */ var Control = function ( markup, init, children ) { var cache, i; // Store this.markup = markup; this.init = init; this.children = children || {}; this.settings = { prependChildren: false }; // Init this.element = false; this.childrenWrapper = false; }; /** * Build the control * @param jQuery wrapper the element in which to build the control * @param boolean prepend true if the control must use prepend instead of append * @return void */ Control.prototype.build = function ( wrapper, prepend ) { var self = this; // Create element this.element = $( this.markup )[ prepend ? 'prependTo' : 'appendTo' ]( wrapper ); this.childrenWrapper = this.element; // Init this.init.call( this ); // Build chidren $.each( this.children, function( name, control ) { control.build( self.childrenWrapper, self.settings.prependChildren ); } ); }; /*************************************************************************/ /* Publisher pattern class */ /*************************************************************************/ var Publisher = function() { // Init this.subscribers = []; }; /** * Add a subscriber object * @param object object any object with the required callback methods * @return void */ Publisher.prototype.addSubscriber = function ( object ) { // Add this.subscribers.push( object ); }; /** * Remove a subscriber object * @param object object the object to remove * @return void */ Publisher.prototype.removeSubscriber = function ( object ) { var i; for ( i = 0; i < this.subscribers.length; ++i ) { if ( this.subscribers[ i ] === object ) { this.subscribers.splice( i, 1 ); --i; } } }; /*************************************************************************/ /* Option class */ /*************************************************************************/ /** * Constructor * Subscribers must provide one method: onOptionChange( name, value ) * @param string name the options's name * @param mixed def the option's default value * @param boolean disableCache use true to disable caching for this option */ var Option = function ( name, def, disableCache ) { var cache; // Store this.name = name; this.value = def; this.def = def; this.disableCache = disableCache; // Init this.subscribers = []; // Check cache if ( !this.disableCache ) { cache = this.checkCache(); if ( cache ) { this.value = cache.value; } } }; Option.prototype = new Publisher(); /** * Check if the option has already been defined * @return object the option's value (an object with one index: value), or false if not defined yet */ Option.prototype.checkCache = function () { // Retrieve cache var cache = getStoredObject( 'options', {} ), value = cache[ this.name ]; return value ? value : false; }; /** * Reset option to its default value * @return void */ Option.prototype.reset = function () { this.set( this.def ); }; /** * Get the option's value * @return mixed the option's value */ Option.prototype.get = function () { return this.value; }; /** * Set the option's value * @param mixed value the new value * @param boolean forceChange use true to trigger the change listeners even if the value did not change (default: false) * @return void */ Option.prototype.set = function ( value, forceChange ) { var cache; // If different if ( value !== this.value ) { // Store this.value = value; // Update cache if ( !this.disableCache ) { cache = getStoredObject( 'options', {} ); if ( this.value === this.def ) { delete cache[ this.name ]; } else { cache[ this.name ] = { value: value }; } setStoredObject( 'options', cache ); } // Notify subscribers this.change(); } else if ( forceChange ) { // Forced update this.change(); } }; /** * Trigger the change callback on all subscribers * @return void */ Option.prototype.change = function ( value ) { var option = this; // Notify subscribers $.each( this.subscribers, function ( i, object ) { object.onOptionChange( option.name, option.value ); } ); }; /*************************************************************************/ /* Ressource class */ /*************************************************************************/ /** * Constructor * Subscribers must provide two methods: onRessourceLoad( name ) and onRessourceFail( name ) * @param string name the ressource's name * @param string|function url the url to call to get the ressource (just the filename if in api), or a function that will return the url * @param int the expiration timeout of the cache for the ressource in milliseconds */ var Ressource = function ( name, url, expiration ) { var cache; // Store this.name = name; this.url = ( typeof url === 'string' && url.indexOf( '://' ) === -1 ) ? envato.api.url + url : url; this.expiration = expiration; // Init this.data = null; this.date = null; this.loading = false; this.failures = 0; this.failed = false; this.subscribers = []; // Check cache cache = this.checkCache(); if ( cache ) { this.data = cache.data; this.date = cache.date; } }; Ressource.prototype = new Publisher(); /** * Check if the ressource has been cached * @return object the cached ressource (an object with two indexes: date and data), or false if not cached or expired */ Ressource.prototype.checkCache = function () { // Retrieve cache var cache = getStoredObject( 'ressources', {} ), cached = cache[ this.name ], date; // If stored if ( cached ) { // Check expiration date date = new Date(); if ( cached.date + this.expiration > date.getTime() ) { return cached; } else { // Remove cache delete cache[ this.name ]; setStoredObject( 'ressources', cache ); } } // Not available return false; }; /** * Get the ressource data. If not ready, start loading. * @return object the ressource data, or false if not loaded yet or expired */ Ressource.prototype.get = function () { var date; // If set if ( this.data ) { // Check expiration date date = new Date(); if ( this.date + this.expiration > date.getTime() ) { return this.data; } else { // Clear this.data = null; this.date = null; } } // Start loading this.load(); // Not ready return false; }; /** * Check if the ressource has already been loaded * @return boolean true if loaded, else false */ Ressource.prototype.isLoaded = function () { return ( this.data !== null ); }; /** * Start loading the ressource * @return void */ Ressource.prototype.load = function () { var url; // If not already loading or failed if ( !this.loading && !this.failed ) { // Final url url = ( typeof this.url === 'function' ) ? this.url.call( this ) : this.url; // Start loading this.loading = true; console.log( '§ Loading ressource ' + url ); // Request $.ajax({ url: url, dataType: 'json', data: '', context: this, error: function ( jqXHR, textStatus, errorThrown ) { var ressource = this; // End loading this.loading = false; console.log( '§ Error while loading ressource ' + url ); // Count failures ++this.failures; // If under the maximum number of tries, start again if ( this.failures < 3 ) { this.load(); } else { // Mark as permanent fail this.failed = true; // Notify subscribers $.each( this.subscribers, function ( i, object ) { object.onRessourceFail( ressource.name ); } ); } }, success: function ( data, textStatus, jqXHR ) { var ressource = this, date = new Date(), cache = getStoredObject( 'ressources', {} ); // End loading this.loading = false; console.log( '§ Ressource loaded: ' + url ); // Store this.data = data; this.date = date.getTime(); // Update cache cache[ this.name ] = { data: this.data, date: this.date }; setStoredObject( 'ressources', cache ); // Reset failure count this.failed = false; this.failures = 0; // Notify subscribers $.each( this.subscribers, function ( i, object ) { object.onRessourceLoad( ressource.name ); } ); } }); } }; /** * Function to get the ressource corresponding to an item * @param int id the item id * @return Ressource the item's ressource */ function getItemRessource( id ) { // If not created yet if ( !library.ressources.items[ id ] ) { library.ressources.items[ id ] = new Ressource( 'Item ' + id, 'item:' + id + '.json', 7 * 86400000 ); } return library.ressources.items[ id ]; } /*************************************************************************/ /* Request class */ /*************************************************************************/ /** * Constructor * Subscribers must provide two methods: onRequestLoad( name ) and onRequestFail( name ) * @param string name the request's name * @param string|function request the request to call, or a function that will return the request * @param array params list of parameters for the request * @param array options list of options the request is bound to */ var Request = function ( name, request, params, options ) { var cache, i; // Store this.name = name; this.request = request; this.params = params || []; this.options = options || []; // Init this.data = null; this.failed = false; this.subscribers = []; this.timeout = false; // Registrer options for ( i = 0; i < this.options.length; ++i ) { this.options[ i ].addSubscriber( this ); } }; Request.prototype = new Publisher(); /** * List of awaiting requests * @var array */ Request.prototype.queue = []; /** * Callback when an option changes * @param string name of the option * @param mixed value the new value * @return void */ Request.prototype.onOptionChange = function ( name, value ) { // Clear data to force refresh this.data = null; this.failed = false; // Reload with delay (to account form ultiple options changes) this.delayLoad(); }; /** * Get the request data. If not ready, start loading. * @return object the ressource data, or false if not loaded yet or expired */ Request.prototype.get = function () { // If set if ( this.data ) { return this.data; } // Start loading this.load(); // Not ready return false; }; /** * Check if the request has already been loaded * @return boolean true if loaded, else false */ Request.prototype.isLoaded = function () { return ( this.data !== null ); }; /** * Delay load of request, to account for multiple changes * @return void */ Request.prototype.delayLoad = function () { var instance = this; // If not delayed yet if ( !this.timeout ) { this.timeout = setTimeout( function() { // Clear instance.timeout = false; // Load instance.load(); }, 20 ); } }; /** * Start loading the request * @return void */ Request.prototype.load = function () { // If not already loading or failed if ( !this.loading && !this.failed ) { // Queue Request.prototype.queue.push( this ); // Start loading this.loading = true; // If first request, start loading if ( Request.prototype.queue.length === 1 ) { this.loadNext(); } } }; /** * Start loading the next queued request * @return void */ Request.prototype.loadNext = function () { var instance, request, params, options = {}, i; // If there are no more requests in the queue if ( Request.prototype.queue.length === 0 ) { return; } // Get next instance = Request.prototype.queue[ 0 ]; // Options for functions if ( typeof instance.request === 'function' || typeof instance.params === 'function' ) { for ( i = 0; i < instance.options.length; ++i ) { options[ instance.options[ i ].name ] = instance.options[ i ].get(); } } // Final url and params request = ( typeof instance.request === 'function' ) ? instance.request.call( instance, options ) : instance.request; params = ( typeof instance.params === 'function' ) ? instance.params.call( instance, options ) : instance.params; // Request db.transaction( function ( tx ) { tx.executeSql( request, params, function ( tx, results ) { // End loading instance.loading = false; // Store instance.data = results; // Reset failure status instance.failed = false; // Notify subscribers $.each( instance.subscribers, function ( i, object ) { object.onRequestLoad( instance.name ); } ); // Clear queue Request.prototype.queue.shift(); // Next request instance.loadNext(); }, function ( tx, e ) { // End loading instance.loading = false; // Mark as permanent fail instance.failed = true; // Log console.log( 'Error while executing request ' + instance.name + ': ' + e.message ); // Notify subscribers $.each( instance.subscribers, function ( i, object ) { object.onRequestFail( instance.name ); } ); // Clear queue Request.prototype.queue.shift(); // Next request instance.loadNext(); } ); } ); }; /*************************************************************************/ /* Widgets screen */ /*************************************************************************/ /** * Constructor * @param jQuery div the target div of the screen * @param function onResize a callback to fire for each resize */ var WidgetsScreen = function ( div, onResize ) { var cache, i; // Store this.div = div.addClass( 'envastats-widgets-screen' ); this.onResize = onResize; // Init this.height = 0; this.rows = {}; }; /** * Get a new child div for a WidgetRow * @return jQuery the new div */ WidgetsScreen.prototype.newChildDiv = function() { return $( '' ).appendTo( this.div ); }; /** * Add a new WidgetRow * @param string name the name of the widget * @param WidgetRow row the new row * @return void */ WidgetsScreen.prototype.addRow = function( name, row ) { // Register this.rows[ name ] = row; // Refresh positions this.refreshPositions(); }; /** * Remove a WidgetRow * @param string name the name of the row to remove * @return void */ WidgetsScreen.prototype.removeRow = function( name ) { if ( this.rows[ name ] ) { // Remove delete this.rows[ name ]; // Refresh positions this.refreshPositions(); } }; /** * Update rows positions and overall size * @return void */ WidgetsScreen.prototype.refreshPositions = function() { var height = 0; // Position rows $.each( this.rows, function( name, row ) { // Margin if ( height > 0 ) { height += 30; } // Position row.setPosition( height ); // Total height height += row.height; } ); // Set screen size this.height = height; this.div.height( height ); // Trigger listener if ( this.onResize ) { this.onResize.call( this ); } }; /*************************************************************************/ /* Widgets Row */ /*************************************************************************/ /** * Constructor * @param jQuery div the target div of the row * @param int height height of the row */ var WidgetRow = function ( div, height ) { var cache, i; // Store this.height = height || 100; this.div = div.addClass( 'envastats-widgets-row' ).height( this.height ); // Init this.widgets = {}; }; /** * Get a new child div for a Widget * @return jQuery the new div */ WidgetRow.prototype.newChildDiv = function() { return $( '' ).appendTo( this.div ); }; /** * Set the row vertical position in the parent screen * @param int position the new position * @return void */ WidgetRow.prototype.setPosition = function( position ) { this.div.css( 'top', position + 'px' ); }; /** * Add a new Widget * @param string name the name of the widget * @param Widget widget the new widget * @return void */ WidgetRow.prototype.addWidget = function( name, widget ) { this.widgets[ name ] = widget; }; /** * Remove a Widget * @param string name the name of the row to remove * @return void */ WidgetRow.prototype.removeWidget = function( name ) { if ( this.widgets[ name ] ) { delete this.widgets[ name ]; } }; /*************************************************************************/ /* Widgets Column */ /*************************************************************************/ /** * Constructor * @param jQuery div the target div of the column */ var WidgetColumn = function ( div ) { var cache, i; // Store this.div = div.addClass( 'envastats-widgets-column' ); // Init this.widgets = {}; }; /** * Get a new child div for a Widget * @return jQuery the new div */ WidgetColumn.prototype.newChildDiv = function() { return $( '' ).appendTo( this.div ); }; /** * Add a new Widget * @param string name the name of the widget * @param Widget widget the new widget * @return void */ WidgetColumn.prototype.addWidget = function( name, widget ) { this.widgets[ name ] = widget; }; /** * Remove a Widget * @param string name the name of the row to remove * @return void */ WidgetColumn.prototype.removeWidget = function( name ) { if ( this.widgets[ name ] ) { delete this.widgets[ name ]; } }; /*************************************************************************/ /* Widget class */ /*************************************************************************/ /** * Constructor * @param jQuery div the target div * @param WidgetController controller the widget controller * @param object options the options */ var Widget = function ( div, controller, options ) { var i; // Store this.div = div.addClass( 'envastats-widget' ); this.controller = controller; this.options = options || {}; // Inner DOM elements this.rebuildContentDiv(); // Init this.vars = {}; this.loadingMessage = false; this.errorMessage = false; this.failed = false; this.failedRessource = false; this.failedRequest = false; this.timeout = false; // Controller init if ( this.controller.init ) { this.controller.init.call( this, this.options ); } // Gather ressources this.library = { options : ( typeof this.controller.options === 'function' ) ? this.controller.options( this.options ) : this.controller.options, ressources : ( typeof this.controller.ressources === 'function' ) ? this.controller.ressources( this.options ) : this.controller.ressources, requests : ( typeof this.controller.requests === 'function' ) ? this.controller.requests( this.options ) : this.controller.requests }; // Registrer options for ( i = 0; i < this.library.options.length; ++i ) { this.library.options[ i ].addSubscriber( this ); } // Registrer ressources for ( i = 0; i < this.library.ressources.length; ++i ) { this.library.ressources[ i ].addSubscriber( this ); } // Registrer requests for ( i = 0; i < this.library.requests.length; ++i ) { this.library.requests[ i ].addSubscriber( this ); } // Create loading status this.loadingMessage = $( '' ).appendTo( this.div ); // First try to build widget this.build(); }; /** * Delay build of widget, to account for multiple changes * @return void */ Widget.prototype.delayBuild = function () { var instance = this; // If not delayed yet if ( !this.timeout ) { this.timeout = setTimeout( function() { // Clear instance.timeout = false; // Build instance.build(); }, 20 ); } }; /** * Build the widget * @return void */ Widget.prototype.build = function () { var message, i, ressources = {}, requests = {}, options = {}, data; // If error if ( this.failed ) { // Remove loading message if set if ( this.loadingMessage ) { this.loadingMessage.remove(); this.loadingMessage = false; } // Error description message = chrome.i18n.getMessage( 'errorWhileLoadingType', [ this.failedRessource ? this.failedRessource : this.failedRequest ] ); // Show message if ( !this.errorMessage ) { this.errorMessage = $( '' + chrome.i18n.getMessage( 'instructionOERAccount', [ '', '' ] ) + '
' + ' ', { classes: [ 'large-padding' ], onShow: function() { var tooltip = $( this ), input = tooltip.find( 'input' ), save = tooltip.find( 'button' ); save.click( function() { var key = $.trim( input.val() ); if ( key.length > 0 ) { button.removeEnvastatsTooltip(); library.options.oerKey.set( key ); } else { alert( chrome.i18n.getMessage( 'pleaseEnterOERAPIKey' ) ); } } ); } } ); } }, // Options subscriber subscriber = { onOptionChange: function( name, value ) { build(); } }; // First build build(); // Watch for clicks $( document ).on( 'click', '#envastats-default-currency', function() { library.options.currency.set( envato.currency ); } ) .on( 'click', '#envastats-alt-currency', function() { library.options.currency.set( library.options.currencyAlt.get() ); } ) .on( 'click', '#envastats-change-currency', function() { build( true ); } ); // Watch for changes library.options.oerKey.addSubscriber( subscriber ); library.options.currency.addSubscriber( subscriber ); library.options.currencyAlt.addSubscriber( subscriber ); } ) } ), // Right controls block rightControls: new Control( '', function() { this.settings.prependChildren = true; }, { settings: new Control( '', function() { var element = this.element; // Menu element.envastatsMenuTooltip( '' + number_format( amount, 0 ) + '' + estimation + '
' ); } } ), totalAmount: new WidgetController( { 'options': function( options ) { // Mode if ( options.mode === 'week'|| options.mode === 'month' ) { return [ library.options.now ]; } else { return []; } }, 'ressources': [], 'requests': function( options ) { // Mode if ( options.mode === 'week' ) { return [ library.requests.weekStats ]; } else if ( options.mode === 'month' ) { return [ library.requests.monthStats ]; } else { return [ library.requests.globalStats ]; } }, 'init': function( options ) { this.div.addClass( 'envastats-count' ); if ( options.mode === 'week' || options.mode === 'month' ) { this.div.addClass( 'envastats-compact' ); } }, 'build': function ( ressources, requests, options ) { var title, request, proportion, estimation = ''; // Empty target this.emptyDiv(); // Text if ( options.mode === 'week' ) { title = chrome.i18n.getMessage( 'thisWeek' ); request = requests.weekStats; } else if ( options.mode === 'month' ) { title = chrome.i18n.getMessage( 'thisMonth' ); request = requests.monthStats; } else { title = chrome.i18n.getMessage( 'totalAmountSold' ); request = requests.globalStats; } // Final value amount = request.rows.item( 0 ).totalPrice; // Estimation if ( options.mode === 'week' ) { proportion = ( 7 * 86400000 ) / Math.max( 1, now.getTime() - getFirstDayOfWeek( now ).getTime() ); estimation = '' + displayCurrencyAmount( envato.currency, request.rows.item( 0 ).totalPrice ) + '' + estimation + '
' ); } } ), totalEarnings: new WidgetController( { 'options': function( options ) { // Mode if ( options.mode === 'week'|| options.mode === 'month' ) { return [ library.options.currency, library.options.now ]; } else { return [ library.options.currency ]; } }, 'ressources': [], 'requests': function( options ) { // Mode if ( options.mode === 'week' ) { return [ library.requests.weekStats ]; } else if ( options.mode === 'month' ) { return [ library.requests.monthStats ]; } else { return [ library.requests.globalStats ]; } }, 'init': function( options ) { this.div.addClass( 'envastats-count' ); if ( options.mode === 'week' || options.mode === 'month' ) { this.div.addClass( 'envastats-compact' ); } }, 'build': function ( ressources, requests, options ) { var row, amount, title, request, proportion, estimation = ''; // Empty target this.emptyDiv(); // Text if ( options.mode === 'week' ) { title = chrome.i18n.getMessage( 'thisWeek' ); request = requests.weekStats; } else if ( options.mode === 'month' ) { title = chrome.i18n.getMessage( 'thisMonth' ); request = requests.monthStats; } else { title = chrome.i18n.getMessage( 'totalEarnings' ); request = requests.globalStats; } // Final value row = request.rows.item( 0 ); amount = isCurrencyUSD() ? row.totalAmount : row.totalAmountConverted; // Estimation if ( options.mode === 'week' ) { proportion = ( 7 * 86400000 ) / Math.max( 1, now.getTime() - getFirstDayOfWeek( now ).getTime() ); estimation = '' + displayCurrencyAmount( options.currency, amount, 2 ) + '' + estimation + '
' ); } } ), totalRefCut: new WidgetController( { 'options': function( options ) { // Mode if ( options.mode === 'week'|| options.mode === 'month' ) { return [ library.options.currency, library.options.now ]; } else { return [ library.options.currency ]; } }, 'ressources': [], 'requests': function( options ) { // Mode if ( options.mode === 'week' ) { return [ library.requests.weekRefCut ]; } else if ( options.mode === 'month' ) { return [ library.requests.monthRefCut ]; } else { return [ library.requests.globalRefCut ]; } }, 'init': function( options ) { this.div.addClass( 'envastats-count' ); if ( options.mode === 'week' || options.mode === 'month' ) { this.div.addClass( 'envastats-compact' ); } }, 'build': function ( ressources, requests, options ) { var row, amount, request, proportion, estimation = ''; // Empty target this.emptyDiv(); // Text if ( options.mode === 'week' ) { request = requests.weekRefCut; } else if ( options.mode === 'month' ) { request = requests.monthRefCut; } else { request = requests.globalRefCut; } // Final value row = request.rows.item( 0 ); amount = isCurrencyUSD() ? row.totalAmount : row.totalAmountConverted; // Estimation if ( options.mode === 'week' ) { proportion = ( 7 * 86400000 ) / Math.max( 1, now.getTime() - getFirstDayOfWeek( now ).getTime() ); estimation = '' + displayCurrencyAmount( options.currency, amount, 2 ) + '' + estimation + '
' ); } } ), progressLevel: new WidgetController( { 'options': [], 'ressources': [], 'requests': [ library.requests.globalStats ], 'init': function( options ) { // Radial progress class this.div.addClass( 'envastats-progress-radial-widget' ); // Chart div this.vars.chart = $( '' ).appendTo( this.content ); // Text div this.vars.desc = $( '' ).appendTo( this.content ); }, 'build': function ( ressources, requests, options ) { var amount = requests.globalStats.rows.item( 0 ).totalPrice, levels = ( typeof options.levels === 'string' ) ? envato.badges[ options.levels ] : options.levels, level = false, toNext, previous = levels[ 0 ], percentage = 100, i, max = levels.length; // Paw name for ( i = 0; i < max; ++i ) { // If level not reached yet if ( amount < levels[ i ].start ) { // Process percentage = ( ( amount - previous.start ) / ( levels[ i ].start - previous.start ) ) * 100; level = previous.name; toNext = chrome.i18n.getMessage( 'amountToNext', [ displayCurrencyAmount( envato.currency, levels[ i ].start - amount ) ] ); break; } // Store for next level previous = levels[ i ]; } // If over the max level if ( !level ) { level = previous.name; toNext = chrome.i18n.getMessage( 'maxReached' ); } // Build chart this.vars.chart.chart( { template: 'envastats_radialProgress', values: { serie1: [ percentage, 100 - percentage ] } } ); // Text this.vars.desc.html( '' + level + '