Source: background/data.js

/*
 *  Copyright (c) 2015-2017, Michael A. Updike All rights reserved.
 *  Licensed under the BSD-3-Clause
 *  https://opensource.org/licenses/BSD-3-Clause
 *  https://github.com/opus1269/photo-screen-saver/blob/master/LICENSE.md
 */
window.app = window.app || {};

/**
 * Manage the extensions data
 * @namespace
 */
app.Data = (function() {
  'use strict';

  new ExceptionHandler();

  const chromep = new ChromePromise();

  /**
   * Version of localStorage - update when items are added, removed, changed
   * @type {int}
   * @default
   * @const
   * @private
   * @memberOf app.Data
   */
  const _DATA_VERSION = 17;

  /**
   * A number and associated units
   * @typedef {Object} app.Data.UnitValue
   * @property {number} base - value in base unit
   * @property {number} display - value in display unit
   * @property {int} unit - display unit
   */

  /**
   * Values for items in localStorage
   * @typedef {Object} app.Data.LocalStorage
   * @property {int} version - version of data
   * @property {boolean} enabled - is screensaver enabled
   * @property {string} permPicasa - optional permission for Picasa
   * @property {string} permBackground - optional permission to run in bg
   * @property {boolean} allowBackground - run Chrome in background
   * @property {app.Data.UnitValue} idleTime - idle time to display screensaver
   * @property {app.Data.UnitValue} transitionTime - time between photos
   * @property {boolean} skip - ignore extreme aspect ratio photos
   * @property {boolean} shuffle - randomize photo order
   * @property {int} photoSizing - photo display type
   * @property {int} photoTransition - transition animation
   * @property {boolean} interactive - vcr controls for screensaver
   * @property {int} showTime - time display format
   * @property {boolean} largeTime - display larger time label
   * @property {boolean} showPhotog - display name on own photos
   * @property {boolean} showLocation - display photo location
   * @property {string} background - background image
   * @property {boolean} keepAwake - manage computer poser settings
   * @property {boolean} chromeFullscreen - don't display over fullscreen
   * @property {boolean} allDisplays - show on all displays
   * @property {string} activeStart - Keep Wake start time '00:00' 24 hr
   * @property {string} activeStop - Keep Wake stop time '00:00' 24 hr
   * @property {boolean} allowSuspend - let computer sleep
   * @property {boolean} allowPhotoClicks - show photo source on left click
   * @property {boolean} useSpaceReddit - use this photo source
   * @property {boolean} useEarthReddit - use this photo source
   * @property {boolean} useAnimalReddit - use this photo source
   * @property {boolean} useEditors500px - use this photo source
   * @property {boolean} usePopular500px - use this photo source
   * @property {boolean} useYesterday500px - use this photo source
   * @property {boolean} useInterestingFlickr - use this photo source
   * @property {boolean} useChromecast - use this photo source
   * @property {boolean} useAuthors - use this photo source
   * @property {boolean} useSpaceReddit - use this photo source
   * @property {boolean} fullResGoogle - true for actual size Google photos
   * @property {boolean} isAlbumMode - true if Google Photos album mode
   * @property {boolean} useGoogle - use this photo source
   * @property {boolean} useGoogleAlbums - use this photo source
   * @property {Array} albumSelections - user's selected Google Photos albums
   * @property {boolean} useGooglePhotos - use this photo source
   */

  /**
   * Default values in localStorage
   * @type {app.Data.LocalStorage}
   * @const
   * @private
   * @memberOf app.Data
   */
  const _DEF_VALUES = {
    'version': _DATA_VERSION,
    'enabled': true,
    'isAlbumMode': true,
    'permPicasa': 'notSet', // enum: notSet allowed denied
    'permBackground': 'notSet', // enum: notSet allowed denied
    'allowBackground': false,
    'idleTime': {'base': 5, 'display': 5, 'unit': 0}, // minutes
    'transitionTime': {'base': 30, 'display': 30, 'unit': 0}, // seconds
    'skip': true,
    'shuffle': true,
    'photoSizing': 0,
    'photoTransition': 1,
    'interactive': false,
    'showTime': 2, // 24 hr format
    'largeTime': false,
    'fullResGoogle': false,
    'showPhotog': true,
    'showLocation': true,
    'background': 'background:linear-gradient(to bottom, #3a3a3a, #b5bdc8)',
    'keepAwake': false,
    'chromeFullscreen': true,
    'allDisplays': false,
    'activeStart': '00:00', // 24 hr time
    'activeStop': '00:00', // 24 hr time
    'allowSuspend': false,
    'allowPhotoClicks': true,
    'useSpaceReddit': false,
    'useEarthReddit': false,
    'useAnimalReddit': false,
    'useEditors500px': false,
    'usePopular500px': false,
    'useYesterday500px': false,
    'useInterestingFlickr': false,
    'useChromecast': true,
    'useAuthors': false,
    'useGoogle': true,
    'useGoogleAlbums': true,
    'albumSelections': [],
    'useGooglePhotos': false,
  };

  /**
   * Set state based on screensaver enabled flag
   * Note: this does not effect the keep awake settings so you could
   * use the extension as a display keep awake scheduler without
   * using the screensaver
   * @private
   * @memberOf app.Data
   */
  function _processEnabled() {
    // update context menu text
    const label = Chrome.Storage.getBool('enabled') ? Chrome.Locale.localize(
        'disable') : Chrome.Locale.localize('enable');
    app.Alarm.updateBadgeText();
    chromep.contextMenus.update('ENABLE_MENU', {
      title: label,
    }).catch(() => {});
  }

  /**
   * Set power scheduling features
   * @private
   * @memberOf app.Data
   */
  function _processKeepAwake() {
    Chrome.Storage.getBool('keepAwake') ? chrome.power.requestKeepAwake(
        'display') : chrome.power.releaseKeepAwake();
    app.Alarm.updateRepeatingAlarms();
    app.Alarm.updateBadgeText();
  }

  /**
   * Set wait time for screen saver display after machine is idle
   * @private
   * @memberOf app.Data
   */
  function _processIdleTime() {
    chrome.idle.setDetectionInterval(app.Data.getIdleSeconds());
  }

  /**
   * Get default time format based on locale
   * @returns {int} 12 or 24
   * @private
   * @memberOf app.Data
   */
  function _getTimeFormat() {
    let ret = 2; // 24 hr
    const format = Chrome.Locale.localize('time_format');
    if (format && (format === '12')) {
      ret = 1;
    }
    return ret;
  }

  /**
   * Set the 'os' value
   * @returns {Promise} err on failure
   * @private
   * @memberOf app.Data
   */
  function _setOS() {
    return chromep.runtime.getPlatformInfo().then((info) => {
      Chrome.Storage.set('os', info.os);
      return Promise.resolve();
    });
  }

  /**
   * Save the [_DEF_VALUES]{@link app.Data._DEF_VALUES} items, if they
   * do not already exist
   * @private
   * @memberOf app.Data
   */
  function _addDefaults() {
    Object.keys(_DEF_VALUES).forEach(function(key) {
      if (Chrome.Storage.get(key) === null) {
        Chrome.Storage.set(key, _DEF_VALUES[key]);
      }
    });
  }

  /**
   * Convert a setting-slider value due to addition of units
   * @param {!string} key - localStorage key
   * @private
   * @memberOf app.Data
   */
  function _convertSliderValue(key) {
    const value = Chrome.Storage.get(key);
    if (value) {
      const newValue = {
        base: value,
        display: value,
        unit: 0,
      };
      Chrome.Storage.set(key, newValue);
    }
  }

  return {
    /**
     * Initialize the data saved in localStorage
     * @memberOf app.Data
     */
    initialize: function() {
      _addDefaults();

      // set operating system
      _setOS().catch(() => {});

      // and the last error
      Chrome.Storage.clearLastError().catch((err) => {
        Chrome.GA.error(err.message, 'Data.initialize');
      });
      
      // set time format based on locale
      Chrome.Storage.set('showTime', _getTimeFormat());

      // update state
      app.Data.processState();
    },

    /**
     * Update the data saved in localStorage
     * @memberOf app.Data
     */
    update: function() {
      // New items, changes, and removal of unused items can take place
      // here when the version changes
      const oldVersion = Chrome.Storage.getInt('version');

      if (Number.isNaN(oldVersion) || (_DATA_VERSION > oldVersion)) {
        // update version number
        Chrome.Storage.set('version', _DATA_VERSION);
      }

      if (!Number.isNaN(oldVersion)) {
        if (oldVersion < 14) {
          // background used to be a required permission
          // installed extensions before the change will keep
          // this permission on update.
          // https://stackoverflow.com/a/38278824/4468645
          Chrome.Storage.set('permBackground', 'allowed');
          Chrome.Storage.set('allowBackground', true);
        }

        if (oldVersion < 12) {
          // picasa used to be a required permission
          // installed extensions before the change will keep
          // this permission on update.
          // https://stackoverflow.com/a/38278824/4468645
          Chrome.Storage.set('permPicasa', 'allowed');
        }

        if (oldVersion < 10) {
          // was setting this without quotes before
          const oldOS = localStorage.getItem('os');
          if (oldOS) {
            Chrome.Storage.set('os', oldOS);
          }
        }

        if (oldVersion < 8) {
          // change setting-slider values due to adding units
          _convertSliderValue('transitionTime');
          _convertSliderValue('idleTime');
        }
      }

      _addDefaults();

      // update state
      app.Data.processState();
    },

    /**
     * Restore default values for data saved in localStorage
     * @memberOf app.Data
     */
    restoreDefaults: function() {
      Object.keys(_DEF_VALUES).forEach(function(key) {
        if (!key.includes('useGoogle') &&
            (key !== 'googlePhotosSelections') &&
            (key !== 'albumSelections')) {
          // skip Google photos settings
          Chrome.Storage.set(key, _DEF_VALUES[key]);
        }
      });

      // restore default time format based on locale
      Chrome.Storage.set('showTime', _getTimeFormat());

      // update state
      app.Data.processState();
    },

    /**
     * Process changes to localStorage items
     * @param {string} [key='all'] - the item that changed
     * @memberOf app.Data
     */
    processState: function(key = 'all') {
      // Map processing functions to localStorage values
      const STATE_MAP = {
        'enabled': _processEnabled,
        'keepAwake': _processKeepAwake,
        'activeStart': _processKeepAwake,
        'activeStop': _processKeepAwake,
        'allowSuspend': _processKeepAwake,
        'idleTime': _processIdleTime,
      };
      if (key === 'all') {
        // process everything
        Object.keys(STATE_MAP).forEach(function(ky) {
          const fn = STATE_MAP[ky];
          fn();
        });
        // process photo SOURCES
        app.PhotoSources.processAll();
        // set os, if not already
        if (!Chrome.Storage.get('os')) {
          _setOS().catch(() => {});
        }
      } else {
        // individual change
        if (app.PhotoSources.isUseKey(key) || (key === 'fullResGoogle')) {
          // photo source change
          const useKey = (key === 'fullResGoogle') ? 'useGoogleAlbums' : key; 
          app.PhotoSources.process(useKey).catch((err) => {
            // send message on processing error
            const msg = app.Msg.PHOTO_SOURCE_FAILED;
            msg.key = useKey;
            msg.error = err.message;
            return Chrome.Msg.send(msg);
          }).catch(() => {});
        } else {
          const fn = STATE_MAP[key];
          if (typeof(fn) !== 'undefined') {
            fn();
          }
        }
      }
    },

    /**
     * Get the idle time in seconds
     * @returns {int} idle time in seconds
     * @memberOf app.Utils
     */
    getIdleSeconds: function() {
      const idle = Chrome.Storage.get('idleTime');
      return idle.base * 60;
    },

  };
})();