2017-10-30 12:38:01 +03:00
|
|
|
import Service, {inject as service} from '@ember/service';
|
2017-08-02 10:05:59 +03:00
|
|
|
import fetch from 'fetch';
|
2017-12-12 20:22:59 +03:00
|
|
|
import {assign} from '@ember/polyfills';
|
2017-08-02 10:05:59 +03:00
|
|
|
import {isEmpty} from '@ember/utils';
|
|
|
|
import {or} from '@ember/object/computed';
|
|
|
|
import {reject, resolve} from 'rsvp';
|
|
|
|
import {task, taskGroup, timeout} from 'ember-concurrency';
|
|
|
|
|
|
|
|
const API_URL = 'https://api.unsplash.com';
|
|
|
|
const API_VERSION = 'v1';
|
|
|
|
const DEBOUNCE_MS = 600;
|
|
|
|
|
|
|
|
export default Service.extend({
|
2017-10-30 12:38:01 +03:00
|
|
|
config: service(),
|
|
|
|
settings: service(),
|
2017-08-02 10:05:59 +03:00
|
|
|
|
|
|
|
columnCount: 3,
|
|
|
|
columns: null,
|
|
|
|
error: '',
|
|
|
|
photos: null,
|
|
|
|
searchTerm: '',
|
|
|
|
|
|
|
|
_columnHeights: null,
|
|
|
|
_pagination: null,
|
|
|
|
|
2017-09-20 13:24:04 +03:00
|
|
|
applicationId: '8672af113b0a8573edae3aa3713886265d9bb741d707f6c01a486cde8c278980',
|
2017-08-02 10:05:59 +03:00
|
|
|
isLoading: or('_search.isRunning', '_loadingTasks.isRunning'),
|
|
|
|
|
|
|
|
init() {
|
|
|
|
this._super(...arguments);
|
|
|
|
this._reset();
|
|
|
|
},
|
|
|
|
|
|
|
|
loadNew() {
|
|
|
|
this._reset();
|
|
|
|
return this.get('_loadNew').perform();
|
|
|
|
},
|
|
|
|
|
|
|
|
loadNextPage() {
|
|
|
|
// protect against scroll trigger firing when the photos are reset
|
|
|
|
if (this.get('_search.isRunning')) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isEmpty(this.get('photos'))) {
|
|
|
|
return this.get('_loadNew').perform();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this._pagination.next) {
|
|
|
|
return this.get('_loadNextPage').perform();
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO: return error?
|
|
|
|
return reject();
|
|
|
|
},
|
|
|
|
|
2018-08-09 19:55:11 +03:00
|
|
|
updateSearch(term) {
|
|
|
|
if (term === this.get('searchTerm')) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.set('searchTerm', term);
|
|
|
|
this._reset();
|
|
|
|
|
|
|
|
if (term) {
|
|
|
|
return this.get('_search').perform(term);
|
|
|
|
} else {
|
|
|
|
return this.get('_loadNew').perform();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2017-09-28 06:00:04 +03:00
|
|
|
retryLastRequest() {
|
|
|
|
return this.get('_retryLastRequest').perform();
|
|
|
|
},
|
|
|
|
|
2017-08-02 10:05:59 +03:00
|
|
|
changeColumnCount(newColumnCount) {
|
|
|
|
if (newColumnCount !== this.get('columnCount')) {
|
|
|
|
this.set('columnCount', newColumnCount);
|
|
|
|
this._resetColumns();
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2017-12-12 20:22:59 +03:00
|
|
|
// let Unsplash know that the photo was inserted
|
|
|
|
// https://medium.com/unsplash/unsplash-api-guidelines-triggering-a-download-c39b24e99e02
|
|
|
|
triggerDownload(photo) {
|
|
|
|
if (photo.links.download_location) {
|
|
|
|
this._makeRequest(photo.links.download_location, {ignoreErrors: true});
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2017-08-02 10:05:59 +03:00
|
|
|
actions: {
|
|
|
|
updateSearch(term) {
|
2018-08-09 19:55:11 +03:00
|
|
|
return this.updateSearch(term);
|
2017-08-02 10:05:59 +03:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
_loadingTasks: taskGroup().drop(),
|
|
|
|
|
|
|
|
_loadNew: task(function* () {
|
|
|
|
let url = `${API_URL}/photos?per_page=30`;
|
|
|
|
yield this._makeRequest(url);
|
|
|
|
}).group('_loadingTasks'),
|
|
|
|
|
|
|
|
_loadNextPage: task(function* () {
|
|
|
|
yield this._makeRequest(this._pagination.next);
|
|
|
|
}).group('_loadingTasks'),
|
|
|
|
|
|
|
|
_retryLastRequest: task(function* () {
|
|
|
|
yield this._makeRequest(this._lastRequestUrl);
|
|
|
|
}).group('_loadingTasks'),
|
|
|
|
|
|
|
|
_search: task(function* (term) {
|
|
|
|
yield timeout(DEBOUNCE_MS);
|
|
|
|
|
|
|
|
let url = `${API_URL}/search/photos?query=${term}&per_page=30`;
|
|
|
|
yield this._makeRequest(url);
|
|
|
|
}).restartable(),
|
|
|
|
|
|
|
|
_addPhotosFromResponse(response) {
|
|
|
|
let photos = response.results || response;
|
|
|
|
|
2018-01-05 18:38:23 +03:00
|
|
|
photos.forEach(photo => this._addPhoto(photo));
|
2017-08-02 10:05:59 +03:00
|
|
|
},
|
|
|
|
|
|
|
|
_addPhoto(photo) {
|
|
|
|
// pre-calculate ratio for later use
|
|
|
|
photo.ratio = photo.height / photo.width;
|
|
|
|
|
|
|
|
// add to general photo list
|
|
|
|
this.get('photos').pushObject(photo);
|
|
|
|
|
|
|
|
// add to least populated column
|
|
|
|
this._addPhotoToColumns(photo);
|
|
|
|
},
|
|
|
|
|
|
|
|
_addPhotoToColumns(photo) {
|
|
|
|
let min = Math.min(...this._columnHeights);
|
|
|
|
let columnIndex = this._columnHeights.indexOf(min);
|
|
|
|
|
|
|
|
// use a fixed width when calculating height to compensate for different
|
|
|
|
// overall image sizes
|
|
|
|
this._columnHeights[columnIndex] += 300 * photo.ratio;
|
|
|
|
this.get('columns')[columnIndex].pushObject(photo);
|
|
|
|
},
|
|
|
|
|
|
|
|
_reset() {
|
|
|
|
this.set('photos', []);
|
|
|
|
this._pagination = {};
|
|
|
|
this._resetColumns();
|
|
|
|
},
|
|
|
|
|
|
|
|
_resetColumns() {
|
|
|
|
let columns = [];
|
|
|
|
let columnHeights = [];
|
|
|
|
|
|
|
|
// pre-fill column arrays based on columnCount
|
2018-01-05 18:38:23 +03:00
|
|
|
for (let i = 0; i < this.get('columnCount'); i += 1) {
|
2017-08-02 10:05:59 +03:00
|
|
|
columns[i] = [];
|
|
|
|
columnHeights[i] = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.set('columns', columns);
|
|
|
|
this._columnHeights = columnHeights;
|
|
|
|
|
|
|
|
if (!isEmpty(this.get('photos'))) {
|
|
|
|
this.get('photos').forEach((photo) => {
|
|
|
|
this._addPhotoToColumns(photo);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
2017-12-12 20:22:59 +03:00
|
|
|
_makeRequest(url, _options = {}) {
|
|
|
|
let defaultOptions = {ignoreErrors: false};
|
2017-08-02 10:05:59 +03:00
|
|
|
let headers = {};
|
2017-12-12 20:22:59 +03:00
|
|
|
let options = {};
|
|
|
|
|
|
|
|
assign(options, defaultOptions, _options);
|
2017-08-02 10:05:59 +03:00
|
|
|
|
|
|
|
// clear any previous error
|
|
|
|
this.set('error', '');
|
|
|
|
|
|
|
|
// store the url so it can be retried if needed
|
|
|
|
this._lastRequestUrl = url;
|
|
|
|
|
|
|
|
headers.Authorization = `Client-ID ${this.get('applicationId')}`;
|
|
|
|
headers['Accept-Version'] = API_VERSION;
|
2017-08-24 16:36:31 +03:00
|
|
|
headers['App-Pragma'] = 'no-cache';
|
2017-09-26 15:43:19 +03:00
|
|
|
headers['X-Unsplash-Cache'] = true;
|
2017-08-02 10:05:59 +03:00
|
|
|
|
|
|
|
return fetch(url, {headers})
|
2018-01-05 18:38:23 +03:00
|
|
|
.then(response => this._checkStatus(response))
|
|
|
|
.then(response => this._extractPagination(response))
|
|
|
|
.then(response => response.json())
|
|
|
|
.then(response => this._addPhotosFromResponse(response))
|
2017-08-02 10:05:59 +03:00
|
|
|
.catch(() => {
|
|
|
|
// if the error text isn't already set then we've get a connection error from `fetch`
|
2017-12-12 20:22:59 +03:00
|
|
|
if (!options.ignoreErrors && !this.get('error')) {
|
2017-08-02 10:05:59 +03:00
|
|
|
this.set('error', 'Uh-oh! Trouble reaching the Unsplash API, please check your connection');
|
|
|
|
}
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
_checkStatus(response) {
|
|
|
|
// successful request
|
|
|
|
if (response.status >= 200 && response.status < 300) {
|
|
|
|
return resolve(response);
|
|
|
|
}
|
|
|
|
|
|
|
|
let errorText = '';
|
|
|
|
let responseTextPromise = resolve();
|
|
|
|
|
|
|
|
if (response.headers.map['content-type'] === 'application/json') {
|
2018-01-05 18:38:23 +03:00
|
|
|
responseTextPromise = response.json().then(json => json.errors[0]);
|
2017-08-02 10:05:59 +03:00
|
|
|
} else if (response.headers.map['content-type'] === 'text/xml') {
|
|
|
|
responseTextPromise = response.text();
|
|
|
|
}
|
|
|
|
|
|
|
|
return responseTextPromise.then((responseText) => {
|
|
|
|
if (response.status === 403 && response.headers.map['x-ratelimit-remaining'] === '0') {
|
|
|
|
// we've hit the ratelimit on the API
|
|
|
|
errorText = 'Unsplash API rate limit reached, please try again later.';
|
|
|
|
}
|
|
|
|
|
|
|
|
errorText = errorText || responseText || `Error ${response.status}: Uh-oh! Trouble reaching the Unsplash API`;
|
|
|
|
|
|
|
|
// set error text for display in UI
|
|
|
|
this.set('error', errorText);
|
|
|
|
|
|
|
|
// throw error to prevent further processing
|
|
|
|
let error = new Error(errorText);
|
|
|
|
error.response = response;
|
|
|
|
throw error;
|
|
|
|
});
|
|
|
|
},
|
|
|
|
|
|
|
|
_extractPagination(response) {
|
|
|
|
let pagination = {};
|
|
|
|
let linkRegex = new RegExp('<(.*)>; rel="(.*)"');
|
|
|
|
let {link} = response.headers.map;
|
|
|
|
|
|
|
|
if (link) {
|
|
|
|
link.split(',').forEach((link) => {
|
|
|
|
let [, url, rel] = linkRegex.exec(link);
|
|
|
|
|
|
|
|
pagination[rel] = url;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
this._pagination = pagination;
|
|
|
|
|
|
|
|
return response;
|
|
|
|
}
|
|
|
|
});
|