diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..811736d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,94 @@
+# Files and directories created by pub
+.dart_tool/
+.packages
+# Remove the following pattern if you wish to check in your lock file
+pubspec.lock
+
+# Conventional directory for build outputs
+build/
+
+# Directory created by dartdoc
+doc/api/
+
+### JetBrains template
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+.idea/**/usage.statistics.xml
+.idea/**/dictionaries
+.idea/**/shelf
+
+# Generated files
+.idea/**/contentModel.xml
+
+# Sensitive or high-churn files
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+.idea/**/dbnavigator.xml
+
+# Gradle
+.idea/**/gradle.xml
+.idea/**/libraries
+
+# Gradle and Maven with auto-import
+# When using Gradle or Maven with auto-import, you should exclude module files,
+# since they will be recreated, and may cause churn. Uncomment if using
+# auto-import.
+# .idea/artifacts
+# .idea/compiler.xml
+# .idea/modules.xml
+# .idea/*.iml
+# .idea/modules
+# *.iml
+# *.ipr
+
+# CMake
+cmake-build-*/
+
+# Mongo Explorer plugin
+.idea/**/mongoSettings.xml
+
+# File-based project format
+*.iws
+
+# IntelliJ
+out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Cursive Clojure plugin
+.idea/replstate.xml
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+
+# Editor-based Rest Client
+.idea/httpRequests
+
+# Android studio 3.1+ serialized cache file
+.idea/caches/build_file_checksums.ser
+
+### VisualStudioCode template
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+*.code-workspace
+
+## Test config
+test-unsplash-credentials.json
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..5c98b42
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,2 @@
+# Default ignored files
+/workspace.xml
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..28a804d
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..412b793
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/unsplash_client.iml b/.idea/unsplash_client.iml
new file mode 100644
index 0000000..ae9af97
--- /dev/null
+++ b/.idea/unsplash_client.iml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..78cface
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..124e6ad
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,3 @@
+## 0.1.0
+
+- TODO
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..3d7c5a8
--- /dev/null
+++ b/README.md
@@ -0,0 +1,45 @@
+An unofficial client to for the [unsplash](https://unsplash.com) api.
+
+This is a work in progress. At the moment only **Photos.list** and **Photos.random** are
+implemented.
+
+## Usage
+
+A simple usage example:
+
+```dart
+import 'package:unsplash_client/client.dart';
+
+void main() async {
+ // Create a client.
+ final client = UnsplashClient(
+ settings: Settings(
+ // Use the credentials from the developer portal.
+ credentials: AppCredentials(
+ accessKey: '...',
+ secretKey: '...',
+ )
+ ),
+ );
+
+ // Fetch 5 random photos.
+ final response = await client.photos.random(count: 5).go();
+
+ // Check that the request was successful.
+ if (!response.isOk) {
+ throw 'Something is wrong: $response';
+ }
+
+ // Do something with the photos.
+ final photos = response.data;
+
+ // Create a dynamically resized url.
+ final resizedUrl = photos.first.urls.raw.resize(
+ width: 400,
+ height: 400,
+ fit: ResizeFitMode.cover,
+ format: ImageFormat.webp,
+ );
+
+}
+```
diff --git a/analysis_options.yaml b/analysis_options.yaml
new file mode 100644
index 0000000..9dbe1cb
--- /dev/null
+++ b/analysis_options.yaml
@@ -0,0 +1,10 @@
+include: package:effective_dart/analysis_options.1.2.0.yaml
+
+linter:
+ rules:
+ - prefer_relative_imports
+
+analyzer:
+ strong-mode:
+ implicit-casts: false
+ implicit-dynamic: false
diff --git a/lib/client.dart b/lib/client.dart
new file mode 100644
index 0000000..8d056f0
--- /dev/null
+++ b/lib/client.dart
@@ -0,0 +1,5 @@
+export 'src/app_credentials.dart';
+export 'src/client.dart';
+export 'src/model/model.dart';
+export 'src/photos.dart';
+export 'src/dynamic_resize.dart';
diff --git a/lib/src/app_credentials.dart b/lib/src/app_credentials.dart
new file mode 100644
index 0000000..33df1c7
--- /dev/null
+++ b/lib/src/app_credentials.dart
@@ -0,0 +1,63 @@
+import 'package:meta/meta.dart';
+
+/// Credentials for accessing the unsplash api, as a registered app.
+///
+/// You need to [register](https://unsplash.com/developers) as a developer
+/// and create an usnplash app to get credentials.
+@immutable
+class AppCredentials {
+ /// Creates [AppCredentials] from the given arguments.
+ const AppCredentials({
+ @required this.accessKey,
+ this.secretKey,
+ }) : assert(accessKey != null);
+
+ /// The access key of the app these [AppCredentials] belong to.
+ final String accessKey;
+
+ /// The secret key of the app these [AppCredentials] belong to.
+ final String secretKey;
+
+ /// Creates a copy of this instance, replacing non `null` fields.
+ AppCredentials copyWith({
+ String accessKey,
+ String secretKey,
+ }) {
+ return AppCredentials(
+ accessKey: accessKey ?? this.accessKey,
+ secretKey: secretKey ?? this.secretKey,
+ );
+ }
+
+ /// Serializes this instance to json.
+ Map toJson() {
+ return {
+ 'accessKey': accessKey,
+ 'secretKey': secretKey,
+ };
+ }
+
+ /// Deserializes [AppCredentials] from [json].
+ factory AppCredentials.fromJson(Map json) {
+ return AppCredentials(
+ accessKey: json['accessKey'] as String,
+ secretKey: json['secretKey'] as String,
+ );
+ }
+
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ other is AppCredentials &&
+ runtimeType == other.runtimeType &&
+ accessKey == other.accessKey &&
+ secretKey == other.secretKey;
+
+ @override
+ int get hashCode => accessKey.hashCode ^ secretKey.hashCode;
+
+ @override
+ String toString() {
+ return 'Credentials{accessKey: $accessKey, secretKey: $secretKey}';
+ }
+}
diff --git a/lib/src/client.dart b/lib/src/client.dart
new file mode 100644
index 0000000..e84488c
--- /dev/null
+++ b/lib/src/client.dart
@@ -0,0 +1,162 @@
+import 'dart:convert';
+
+import 'package:http/http.dart' as http;
+import 'package:meta/meta.dart';
+
+import '../client.dart';
+import 'app_credentials.dart';
+import 'exception.dart';
+import 'photos.dart';
+
+class ClientSettings {
+ const ClientSettings({
+ @required this.credentials,
+ });
+
+ final AppCredentials credentials;
+ final int maxPageSize = 30;
+}
+
+class UnsplashClient {
+ UnsplashClient({@required this.settings}) : _http = http.Client();
+
+ final baseUrl = Uri.parse('https://api.unsplash.com/');
+
+ final http.Client _http;
+
+ final ClientSettings settings;
+
+ Photos get photos => Photos(this);
+}
+
+typedef BodyDeserializer = T Function(dynamic body);
+
+class Request {
+ const Request({
+ @required this.client,
+ @required this.request,
+ this.json,
+ @required this.isPublicAction,
+ this.bodyDeserializer,
+ });
+
+ final UnsplashClient client;
+ final http.Request request;
+ final dynamic json;
+ final bool isPublicAction;
+ final BodyDeserializer bodyDeserializer;
+
+ Future> go() async {
+ final request = _prepareRequest();
+ http.StreamedResponse response;
+ String body;
+ dynamic json;
+ T data;
+ dynamic error;
+
+ try {
+ response = await client._http.send(request);
+
+ body = await response.stream.bytesToString();
+
+ json = jsonDecode(body);
+
+ if (bodyDeserializer != null) {
+ data = bodyDeserializer(json);
+ }
+ } catch (e, stackTrace) {
+ // ignore: avoid_catches_without_on_clauses
+ error = UnsplashClientException(
+ message: 'Failure while making request: $e',
+ cause: e,
+ stackTrace: stackTrace,
+ );
+ }
+
+ return Response(
+ request: this,
+ response: response,
+ error: error,
+ body: body,
+ json: json,
+ data: data,
+ );
+ }
+
+ http.Request _prepareRequest() {
+ final request = _copyRequest();
+
+ if (json != null) {
+ request.body = jsonEncode(json);
+ }
+
+ request.headers.addAll(_createHeaders());
+
+ return request;
+ }
+
+ http.Request _copyRequest() {
+ final request = http.Request(this.request.method, this.request.url);
+ request.headers.addAll(this.request.headers);
+ request.maxRedirects = this.request.maxRedirects;
+ request.followRedirects = this.request.followRedirects;
+ request.persistentConnection = this.request.persistentConnection;
+ request.encoding = this.request.encoding;
+ request.bodyBytes = this.request.bodyBytes;
+ return request;
+ }
+
+ Map _createHeaders() {
+ final headers = {};
+
+ // API Version
+ headers.addAll({'Accept-Version': 'v1'});
+
+ // Auth
+ assert(isPublicAction);
+ headers.addAll(_publicActionAuthHeader(client.settings.credentials));
+
+ return headers;
+ }
+}
+
+class Response {
+ Response({
+ this.request,
+ this.response,
+ this.error,
+ this.body,
+ this.json,
+ this.data,
+ });
+
+ final Request request;
+
+ final dynamic error;
+
+ final http.BaseResponse response;
+
+ final String body;
+
+ final dynamic json;
+
+ final T data;
+
+ bool get isOk => error == null && response.statusCode < 400;
+
+ bool get hasData => isOk && data != null;
+
+ @override
+ String toString() {
+ return 'Response{isOk: $isOk, error: $error, '
+ 'status: ${response?.statusCode}';
+ }
+}
+
+Map _publicActionAuthHeader(AppCredentials credentials) {
+ return {'Authorization': 'Client-ID ${credentials.accessKey}'};
+}
+
+Map _publicActionQueryParam(AppCredentials credentials) {
+ return {'client_id': credentials.accessKey};
+}
diff --git a/lib/src/dynamic_resize.dart b/lib/src/dynamic_resize.dart
new file mode 100644
index 0000000..5d7628c
--- /dev/null
+++ b/lib/src/dynamic_resize.dart
@@ -0,0 +1,133 @@
+import 'utils.dart';
+
+// ignore_for_file: public_member_api_docs
+
+/// The output format to convert the image to.
+///
+/// See: [Imgix docs](https://docs.imgix.com/apis/url/format/fm)
+enum ImageFormat {
+ gif,
+ jp2,
+ jpg,
+ json,
+ jxr,
+ pjpg,
+ mp4,
+ png,
+ png8,
+ png32,
+ webm,
+ webp,
+}
+
+/// Controls how the output image is fit to its target dimensions after
+/// resizing, and how any background areas will be filled.
+///
+/// See: [Imgix docs](https://docs.imgix.com/apis/url/size/fit)
+enum ResizeFitMode {
+ clamp,
+ clip,
+ crop,
+ facearea,
+ fill,
+ fillmax,
+ max,
+ min,
+ scale,
+}
+
+/// Returns a new [Uri], Based on [photoUrl], under which a dynamically
+/// resized version of the original photo can be accessed.
+///
+/// {@macro unsplash.client.photo.resize}
+///
+// TODO: auto=format: for automatically choosing the optimal image format
+// depending on user browser
+Uri resizePhotoUrl(
+ Uri photoUrl, {
+ int quality,
+ int width,
+ int height,
+ int devicePixelRatio,
+ ImageFormat format,
+ ResizeFitMode fit,
+ Map imgixParams,
+}) {
+ assert(quality.isNull || quality >= 0 && quality <= 100);
+ assert(width.isNull || width >= 0);
+ assert(height.isNull || height >= 0);
+ assert(devicePixelRatio.isNull ||
+ devicePixelRatio >= 0 && devicePixelRatio <= 8);
+
+ // The officially supported params.
+ final params = {
+ 'q': quality?.toString(),
+ 'w': width?.toString(),
+ 'h': height?.toString(),
+ 'dpi': devicePixelRatio?.toString(),
+ 'fit': fit?.let(enumName),
+ 'fm': format?.let(enumName),
+ };
+
+ if (imgixParams != null) {
+ params.addAll(imgixParams);
+ }
+
+ // Make sure the original query parameters are included for view tracking,
+ // as required by unsplash.
+ params.addAll(photoUrl.queryParameters);
+
+ // Remove params whose value is null.
+ params.removeWhereValue(isNull);
+
+ return photoUrl.replace(queryParameters: params);
+}
+
+/// Creates photo urls which dynamically resize the original image.
+extension DynamicResizeUrl on Uri {
+ /// Returns a new [Uri], Based on this photo url, under which a dynamically
+ /// resized version of the original photo can be accessed.
+ ///
+ /// {@template unsplash.client.photo.resize}
+ /// Unsplash supports dynamic resizing of photos. The transformations applied
+ /// to the original photo can be configured through a set of query parameters
+ /// in the requested url.
+ ///
+ /// The officially supported parameters are:
+ ///
+ /// - [width], [height] : for adjusting the width and height of a photo
+ /// - [crop] : for applying cropping to the photo
+ /// - [format] : for converting image format
+ /// - [quality] : for changing the compression quality when using lossy file
+ /// formats
+ /// - [fit] : for changing the fit of the image within the specified
+ /// dimensions
+ /// - [devicePixelRatio] : for adjusting the device pixel ratio of the image
+ ///
+ /// Under the hood unsplash uses [imgix](https://www.imgix.com/). The
+ /// [other parameters](https://docs.imgix.com/apis/url) offered by Imgix can be
+ /// used through [imgixParams], but unsplash dose not officially support them
+ /// and may remove support for them at any time in the future.
+ ///
+ /// See: [Unsplash docs](https://unsplash.com/documentation#supported-parameters)
+ /// {@endtemplate}
+ Uri resizePhoto({
+ int quality,
+ int width,
+ int height,
+ int devicePixelRatio,
+ ImageFormat format,
+ ResizeFitMode fit,
+ Map imgixParams,
+ }) =>
+ resizePhotoUrl(
+ this,
+ quality: quality,
+ width: width,
+ height: height,
+ devicePixelRatio: devicePixelRatio,
+ format: format,
+ fit: fit,
+ imgixParams: imgixParams,
+ );
+}
diff --git a/lib/src/exception.dart b/lib/src/exception.dart
new file mode 100644
index 0000000..6479323
--- /dev/null
+++ b/lib/src/exception.dart
@@ -0,0 +1,24 @@
+import 'package:meta/meta.dart';
+import 'client.dart';
+
+/// An exception which is thrown when an error happens in the [UnsplashClient].
+class UnsplashClientException implements Exception {
+ const UnsplashClientException({
+ @required this.message,
+ @required this.cause,
+ @required this.stackTrace,
+ });
+
+ final String message;
+ final dynamic cause;
+ final StackTrace stackTrace;
+
+ @override
+ String toString() {
+ return '''
+$runtimeType: $message
+$cause
+$stackTrace
+''';
+ }
+}
diff --git a/lib/src/model/base_model.dart b/lib/src/model/base_model.dart
new file mode 100644
index 0000000..ddad189
--- /dev/null
+++ b/lib/src/model/base_model.dart
@@ -0,0 +1,40 @@
+import 'dart:convert';
+
+import 'package:collection/collection.dart';
+import 'package:meta/meta.dart';
+
+// TODO document models
+
+@immutable
+/// Base class for all models.
+///
+/// [==] and [hashCode] is implemented based on the json representation,
+/// returned by [toJson].
+///
+/// [toString] returns a pretty json representation based on [toJson].
+abstract class BaseModel {
+ /// Equality used to implement json representation of all models.
+ static const jsonEquality = DeepCollectionEquality();
+
+ /// Const constructor to allow sub classes to have const constructors.
+ const BaseModel();
+
+ /// Returns the json representation of this model in the unsplash api.
+ Map toJson();
+
+ @override
+ bool operator ==(Object other) {
+ return identical(this, other) ||
+ other is BaseModel &&
+ runtimeType == other.runtimeType &&
+ jsonEquality.equals(toJson(), other.toJson());
+ }
+
+ @override
+ int get hashCode => jsonEquality.hash(toJson());
+
+ @override
+ String toString() {
+ return JsonEncoder.withIndent(' ').convert(toJson());
+ }
+}
diff --git a/lib/src/model/collection.dart b/lib/src/model/collection.dart
new file mode 100644
index 0000000..c99f3bb
--- /dev/null
+++ b/lib/src/model/collection.dart
@@ -0,0 +1,105 @@
+import 'package:meta/meta.dart';
+
+import '../utils.dart';
+import 'base_model.dart';
+import 'photo.dart';
+import 'user.dart';
+
+// ignore_for_file: public_member_api_docs
+
+class Collection extends BaseModel {
+ final int id;
+ final String title;
+ final String description;
+ final DateTime publishedAt;
+ final DateTime updatedAt;
+ final Photo coverPhoto;
+ final User user;
+ final bool featured;
+ final int totalPhotos;
+ final bool private;
+ final String shareKey;
+ final CollectionLinks links;
+
+ const Collection({
+ @required this.id,
+ @required this.title,
+ @required this.description,
+ @required this.publishedAt,
+ @required this.updatedAt,
+ @required this.coverPhoto,
+ @required this.user,
+ @required this.featured,
+ @required this.totalPhotos,
+ @required this.private,
+ @required this.shareKey,
+ @required this.links,
+ });
+
+ @override
+ Map toJson() {
+ return {
+ 'id': id,
+ 'title': title,
+ 'description': description,
+ 'published_at': publishedAt?.toIso8601String(),
+ 'updated_at': updatedAt?.toIso8601String(),
+ 'cover_photo': coverPhoto?.toJson(),
+ 'user': user?.toJson(),
+ 'featured': featured,
+ 'total_photos': totalPhotos,
+ 'private': private,
+ 'share_key': shareKey,
+ 'links': links?.toJson(),
+ };
+ }
+
+ factory Collection.fromJson(Map json) {
+ return Collection(
+ id: json['id'] as int,
+ title: json['title'] as String,
+ description: json['description'] as String,
+ publishedAt: (json['published_at'] as String)?.let(DateTime.parse),
+ updatedAt: (json['updated_at'] as String)?.let(DateTime.parse),
+ coverPhoto: (json['cover_photo'] as Map)
+ ?.let((it) => Photo.fromJson(it)),
+ user: (json['user'] as Map)
+ ?.let((it) => User.fromJson(it)),
+ featured: json['featured'] as bool,
+ totalPhotos: json['total_photos'] as int,
+ private: json['private'] as bool,
+ shareKey: json['shared_key'] as String,
+ links: (json['links'] as Map)
+ ?.let((it) => CollectionLinks.fromJson(it)),
+ );
+ }
+}
+
+class CollectionLinks extends BaseModel {
+ final Uri self;
+ final Uri html;
+ final Uri photos;
+
+ const CollectionLinks({
+ @required this.self,
+ @required this.html,
+ @required this.photos,
+ });
+
+ @override
+ Map toJson() {
+ return {
+ 'self': self?.toString(),
+ 'html': html?.toString(),
+ 'photos': photos?.toString(),
+ };
+ }
+
+ factory CollectionLinks.fromJson(Map json) {
+ return CollectionLinks(
+ self: (json['self'] as String)?.let(Uri.parse),
+ html: (json['html'] as String)?.let(Uri.parse),
+ photos: (json['photos'] as String)?.let(Uri.parse),
+ );
+ }
+}
diff --git a/lib/src/model/location.dart b/lib/src/model/location.dart
new file mode 100644
index 0000000..9fdc37b
--- /dev/null
+++ b/lib/src/model/location.dart
@@ -0,0 +1,67 @@
+import 'package:meta/meta.dart';
+
+import '../utils.dart';
+import 'base_model.dart';
+
+// ignore_for_file: public_member_api_docs
+
+/// A geographical location.
+class GeoLocation extends BaseModel {
+ const GeoLocation({
+ @required this.name,
+ @required this.city,
+ @required this.country,
+ @required this.position,
+ });
+
+ final String name;
+ final String city;
+ final String country;
+ final GeoPosition position;
+
+ @override
+ Map toJson() {
+ return {
+ 'name': name,
+ 'city': city,
+ 'country': country,
+ 'position': position,
+ };
+ }
+
+ factory GeoLocation.fromJson(Map json) {
+ return GeoLocation(
+ name: json['name'] as String,
+ city: json['city'] as String,
+ country: json['country'] as String,
+ position: (json['position'] as Map)
+ ?.let((it) => GeoPosition.fromJson(it)),
+ );
+ }
+}
+
+/// A precise geographical position on earth, in [latitude] and [longitude].
+class GeoPosition extends BaseModel {
+ const GeoPosition({
+ @required this.latitude,
+ @required this.longitude,
+ });
+
+ final double latitude;
+ final double longitude;
+
+ @override
+ Map toJson() {
+ return {
+ 'latitude': latitude,
+ 'longitude': longitude,
+ };
+ }
+
+ factory GeoPosition.fromJson(Map json) {
+ return GeoPosition(
+ latitude: json['latitude'] as double,
+ longitude: json['longitude'] as double,
+ );
+ }
+}
diff --git a/lib/src/model/model.dart b/lib/src/model/model.dart
new file mode 100644
index 0000000..01a264d
--- /dev/null
+++ b/lib/src/model/model.dart
@@ -0,0 +1,5 @@
+export 'base_model.dart';
+export 'collection.dart';
+export 'location.dart';
+export 'photo.dart';
+export 'user.dart';
diff --git a/lib/src/model/photo.dart b/lib/src/model/photo.dart
new file mode 100644
index 0000000..26887c8
--- /dev/null
+++ b/lib/src/model/photo.dart
@@ -0,0 +1,240 @@
+import 'package:meta/meta.dart';
+
+import '../utils.dart';
+import 'base_model.dart';
+import 'collection.dart';
+import 'location.dart';
+import 'user.dart';
+
+// ignore_for_file: public_member_api_docs
+
+class Photo extends BaseModel {
+ const Photo({
+ @required this.id,
+ @required this.createdAt,
+ @required this.updatedAt,
+ @required this.urls,
+ @required this.width,
+ @required this.height,
+ @required this.color,
+ @required this.downloads,
+ @required this.likes,
+ @required this.likedByUser,
+ @required this.description,
+ @required this.exif,
+ @required this.location,
+ @required this.user,
+ @required this.currentUserCollections,
+ @required this.links,
+ @required this.tags,
+ });
+
+ final String id;
+ final DateTime createdAt;
+ final DateTime updatedAt;
+ final Urls urls;
+ final int width;
+ final int height;
+ final String color;
+ final int downloads;
+ final int likes;
+ final bool likedByUser;
+ final String description;
+ final Exif exif;
+ final GeoLocation location;
+ final User user;
+ final List currentUserCollections;
+ final PhotoLinks links;
+ final List tags;
+
+ double get ratio => width / height;
+
+ @override
+ Map toJson() {
+ return {
+ 'id': id,
+ 'created_at': createdAt?.toIso8601String(),
+ 'updated_at': updatedAt?.toIso8601String(),
+ 'urls': urls?.toJson(),
+ 'width': width,
+ 'height': height,
+ 'color': color,
+ 'downloads': downloads,
+ 'likes': likes,
+ 'liked_by_user': likedByUser,
+ 'description': description,
+ 'exif': exif?.toJson(),
+ 'location': location?.toJson(),
+ 'user': user?.toJson(),
+ 'current_user_collections':
+ currentUserCollections?.map((it) => it.toJson())?.toList(),
+ 'links': links?.toJson(),
+ 'tags': tags?.map((tag) => tag.toJson())?.toList(),
+ };
+ }
+
+ factory Photo.fromJson(Map json) {
+ return Photo(
+ id: json['id'] as String,
+ createdAt: DateTime.parse(json['created_at'] as String),
+ updatedAt: DateTime.parse(json['updated_at'] as String),
+ urls: (json['urls'] as Map)
+ ?.let((it) => Urls.fromJson(it)),
+ width: json['width'] as int,
+ height: json['height'] as int,
+ color: json['color'] as String,
+ downloads: json['downloads'] as int,
+ likes: json['likes'] as int,
+ likedByUser: json['liked_by_user'] as bool,
+ description: json['description'] as String,
+ exif: (json['exif'] as Map)
+ ?.let((it) => Exif.fromJson(it)),
+ location: (json['location'] as Map)
+ ?.let((it) => GeoLocation.fromJson(it)),
+ user: User.fromJson(json['user'] as Map),
+ currentUserCollections:
+ (json['current_user_collections'] as List)
+ ?.cast