Source: sources/photo_source.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
 */
(function() {
  'use strict';
  window.app = window.app || {};

  new ExceptionHandler();

  /**
   * A photo from a {@link app.PhotoSource}
   * This is the photo information that is persisted.
   *
   * @typedef {{}} app.PhotoSource.Photo
   * @property {string} url - The url to the photo
   * @property {string} author - The photographer
   * @property {number} asp - The aspect ratio of the photo
   * @property {Object} [ex] - Additional information about the photo
   * @property {string} [point] - geolocation 'lat lon'
   */

  /**
   * The photos for a {@link app.PhotoSource}
   *
   * @typedef {{}} app.PhotoSource.Photos
   * @property {string} type - type of {@link app.PhotoSource}
   * @property {app.PhotoSource.Photo[]} photos - The photos
   */

  /**
   * A potential source of photos for the screen saver
   * @alias app.PhotoSource
   */
  app.PhotoSource = class PhotoSource {

    /**
     * Create a new photo source
     * @param {string} useKey - The key for if the source is selected
     * @param {string} photosKey - The key for the collection of photos
     * @param {string} type - A descriptor of the photo source
     * @param {string} desc - A human readable description of the source
     * @param {boolean} isDaily - Should the source be updated daily
     * @param {boolean} isArray - Is the source an Array of photo Arrays
     * @param {?Object} [loadArg=null] - optional arg for load function
     * @constructor
     */
    constructor(useKey, photosKey, type, desc, isDaily, isArray,
                loadArg = null) {
      this._useKey = useKey;
      this._photosKey = photosKey;
      this._type = type;
      this._desc = desc;
      this._isDaily = isDaily;
      this._isArray = isArray;
      this._loadArg = loadArg;

      // set the user facing description
    }

    /**
     * Factory Method to create a new {@link app.PhotoSource}
     * @param {app.PhotoSources.UseKey} useKey - photo source useKey
     * @returns {?app.PhotoSource} a new PhotoSource or subclass
     * @static
     */
    static createSource(useKey) {
      switch (useKey) {
        case app.PhotoSources.UseKey.ALBUMS_GOOGLE:
          return new app.GoogleSource(useKey, 'albumSelections', 'Google User',
              Chrome.Locale.localize('google_title_photos'),
              true, true, true);
        case app.PhotoSources.UseKey.PHOTOS_GOOGLE:
          // not implemented yet
          return new app.GoogleSource(useKey, 'googleImages', 'Google User',
              'NOT IMPLEMENTED',
              true, false, false);
        case app.PhotoSources.UseKey.CHROMECAST:
          return new app.CCSource(useKey, 'ccImages', 'Google',
              Chrome.Locale.localize('setting_chromecast'),
              false, false, null);
        case app.PhotoSources.UseKey.ED_500:
          return new app.Px500Source(useKey, 'editors500pxImages', '500',
              Chrome.Locale.localize('setting_500editors'),
              true, false, 'editors');
        case app.PhotoSources.UseKey.POP_500:
          return new app.Px500Source(useKey, 'popular500pxImages', '500',
              Chrome.Locale.localize('setting_500popular'),
              true, false, 'popular');
        case app.PhotoSources.UseKey.YEST_500:
          return new app.Px500Source(useKey, 'yesterday500pxImages', '500',
              Chrome.Locale.localize('setting_500yest'),
              true, false, 'fresh_yesterday');
        case app.PhotoSources.UseKey.INT_FLICKR:
          return new app.FlickrSource(useKey, 'flickrInterestingImages',
              'flickr',
              Chrome.Locale.localize('setting_flickr_int'),
              true, false, false);
        case app.PhotoSources.UseKey.AUTHOR:
          return new app.FlickrSource(useKey, 'authorImages', 'flickr',
              Chrome.Locale.localize('setting_mine'),
              false, false, true);
        case app.PhotoSources.UseKey.SPACE_RED:
          return new app.RedditSource(useKey, 'spaceRedditImages', 'reddit',
              Chrome.Locale.localize('setting_reddit_space'),
              true, false, 'r/spaceporn/');
        case app.PhotoSources.UseKey.EARTH_RED:
          return new app.RedditSource(useKey, 'earthRedditImages', 'reddit',
              Chrome.Locale.localize('setting_reddit_earth'),
              true, false, 'r/EarthPorn/');
        case app.PhotoSources.UseKey.ANIMAL_RED:
          return new app.RedditSource(useKey, 'animalRedditImages', 'reddit',
              Chrome.Locale.localize('setting_reddit_animal'),
              true, false, 'r/animalporn/');
        default:
          // TODO title
          Chrome.Log.error(`Bad PhotoSource type: ${useKey}`,
              'SSView.createView');
          return null;
      }
    }

    /**
     * Add a {@link app.PhotoSource.Photo} to an existing Array
     * @param {Array} photos - {@link app.PhotoSource.Photo} Array
     * @param {string} url - The url to the photo
     * @param {string} author - The photographer
     * @param {number} asp - The aspect ratio of the photo
     * @param {Object} [ex] - Additional information about the photo
     * @param {string} [point] - 'lat lon'
     */
    static addPhoto(photos, url, author, asp, ex, point) {
      /** @type {app.PhotoSource.Photo} */
      const photo = {
        url: url,
        author: author,
        asp: asp.toPrecision(3),
      };
      if (ex) {
        photo.ex = ex;
      }
      if (point) {
        photo.point = point;
      }
      photos.push(photo);
    }

    /**
     * Create a geo point string from a latitude and longitude
     * @param {number} lat - latitude
     * @param {number} lon - longitude
     * @returns {string} 'lat lon'
     * @memberOf app.Geo
     */
    static createPoint(lat, lon) {
      if ((typeof lat === 'number') && (typeof lon === 'number')) {
        return `${lat.toFixed(6)} ${lon.toFixed(6)}`;
      } else {
        return `${lat} ${lon}`;
      }
    }

    /**
     * Fetch the photos for this source - override
     * @abstract
     * @returns {Promise<app.PhotoSource.Photo[]>} Array of photos
     */
    fetchPhotos() {
    }

    /**
     * Get if we should update daily
     * @returns {boolean} if true, update daily
     */
    isDaily() {
      return this._isDaily;
    }

    /**
     * Get the photos from local storage
     * @returns {app.PhotoSource.Photos} the photos
     */
    getPhotos() {
      let ret = {
        type: this._type,
        photos: [],
      };
      if (this.use()) {
        let photos = [];
        if (this._isArray) {
          let items = Chrome.Storage.get(this._photosKey);
          // could be that items have not been retrieved yet
          items = items || [];
          for (const item of items) {
            photos = photos.concat(item.photos);
          }
        } else {
          photos = Chrome.Storage.get(this._photosKey);
          // could be that items have not been retrieved yet
          photos = photos || [];
        }
        ret.photos = photos;
      }
      return ret;
    }

    /**
     * Determine if this source has been selected for display
     * @returns {boolean} true if selected
     */
    use() {
      return Chrome.Storage.getBool(this._useKey);
    }

    /**
     * Process the photo source.
     * @returns {Promise<void>} void
     */
    process() {
      if (this.use()) {
        return this.fetchPhotos().then((photos) => {
          const errMess = this._savePhotos(photos);
          if (!Chrome.Utils.isWhiteSpace(errMess)) {
            return Promise.reject(new Error(errMess));
          }
          return Promise.resolve();
        }).catch((err) => {
          let title = Chrome.Locale.localize('err_photo_source_title');
          title += `: ${this._desc}`;
          Chrome.Log.error(err.message, 'PhotoSource.process', title);
          return Promise.reject(err);
        });
      } else {
        localStorage.removeItem(this._photosKey);
        return Promise.resolve();
      }
    }

    /**
     * Save the photos to localStorage in a safe manner
     * @param {app.PhotoSource.Photo[]} photos
     * - {@link app.PhotoSource.Photo} Array
     * @returns {?string} non-null on error
     * @private
     */
    _savePhotos(photos) {
      let ret = null;
      const keyBool = this._useKey;
      if (photos && photos.length) {
        const set = Chrome.Storage.safeSet(this._photosKey, photos, keyBool);
        if (!set) {
          ret = 'Exceeded storage capacity.';
        }
      }
      return ret;
    }
  };
})();