Skip to content

Commit

Permalink
Updates:
Browse files Browse the repository at this point in the history
* Built pluggable content provider (Google photos versus Unsplash)
* Fixed memory leak bug where we weren't unregistering image stream listener,
  thus keeping the image alive.
* Prefix dart:ui imports with 'ui' everywhere
* Remove unused methods
* Add speed up / slow down handling
  • Loading branch information
Todd Volkert committed Jul 23, 2024
1 parent 2fb5342 commit c462720
Show file tree
Hide file tree
Showing 28 changed files with 695 additions and 426 deletions.
12 changes: 6 additions & 6 deletions .metadata
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# This file should be version controlled and should not be manually edited.

version:
revision: "aa5f4a28e9a8f121a2719c28d0c9a9c33dd798c4"
revision: "c63733310f2751959df940005e9decc68b39da6e"
channel: "main"

project_type: app
Expand All @@ -13,11 +13,11 @@ project_type: app
migration:
platforms:
- platform: root
create_revision: aa5f4a28e9a8f121a2719c28d0c9a9c33dd798c4
base_revision: aa5f4a28e9a8f121a2719c28d0c9a9c33dd798c4
- platform: web
create_revision: aa5f4a28e9a8f121a2719c28d0c9a9c33dd798c4
base_revision: aa5f4a28e9a8f121a2719c28d0c9a9c33dd798c4
create_revision: c63733310f2751959df940005e9decc68b39da6e
base_revision: c63733310f2751959df940005e9decc68b39da6e
- platform: macos
create_revision: c63733310f2751959df940005e9decc68b39da6e
base_revision: c63733310f2751959df940005e9decc68b39da6e

# User provided section

Expand Down
1 change: 1 addition & 0 deletions assets/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
unsplash_client_id
3 changes: 2 additions & 1 deletion lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:photos/src/model/content_provider.dart';

import 'src/model/app.dart';
import 'src/model/auth.dart';
Expand All @@ -26,7 +27,7 @@ void settingsMain() {
void dream() async {
_runWithErrorChecking(() async {
await PhotosAppBinding.ensureInitialized();
AuthBinding.instance.signInSilently();
await ContentProviderBinding.instance.init();
runApp(const PhotosApp());
});
}
Expand Down
6 changes: 3 additions & 3 deletions lib/src/extensions/matrix_4.dart
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import 'dart:typed_data';
import 'dart:ui';
import 'dart:ui' as ui;

import 'package:vector_math/vector_math_64.dart';

extension Matrix4Extensions on Matrix4 {
Size transformSize(Size size) {
ui.Size transformSize(ui.Size size) {
final Float64List storage = this.storage;
final double width = (storage[0] * size.width) +
(storage[4] * size.height) +
storage[12];
final double height = (storage[1] * size.width) +
(storage[5] * size.height) +
storage[13];
return Size(width, height);
return ui.Size(width, height);
}

Vector3 transform2(Vector3 arg) {
Expand Down
6 changes: 4 additions & 2 deletions lib/src/model/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@ import 'dart:developer' as developer;

import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:photos/src/model/dream.dart';

import 'auth.dart';
import 'content_provider.dart';
import 'dream.dart';
import 'files.dart';
import 'google_photos.dart';
import 'photos_api.dart';
import 'ui.dart';

class PhotosAppBinding extends AppBindingBase with ChangeNotifier, FilesBinding, DreamBinding, AuthBinding, UiBinding, PhotosApiBinding {
class PhotosAppBinding extends AppBindingBase with ChangeNotifier, FilesBinding, DreamBinding, AuthBinding, UiBinding, PhotosApiBinding, ContentProviderBinding, GooglePhotosContentProvider {
/// Creates and initializes the application binding if necessary.
///
/// Applications should call this method before calling [runApp].
Expand Down
27 changes: 27 additions & 0 deletions lib/src/model/content_provider.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import 'dart:ui' as ui;

import 'package:flutter/widgets.dart';
import 'package:photos/src/photos_library_api/media_item.dart';

import 'app.dart';

mixin ContentProviderBinding on AppBindingBase {
/// The singleton instance of this object.
static late ContentProviderBinding _instance;
static ContentProviderBinding get instance => _instance;

@override
@protected
@mustCallSuper
Future<void> initInstances() async {
await super.initInstances();
_instance = this;
}

Future<void> init();

Widget buildHome(BuildContext context);

/// Gets the URL to load this media item at the specified size.
String getMediaItemUrl(MediaItem item, ui.Size size);
}
152 changes: 152 additions & 0 deletions lib/src/model/google_photos.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import 'dart:async';
import 'dart:ui' as ui;

import 'package:flutter/widgets.dart';
import 'package:photos/src/photos_library_api/media_item.dart';
import 'package:photos/src/ui/common/notifications.dart';
import 'package:photos/src/ui/photos/app.dart';
import 'package:photos/src/ui/photos/photos_page.dart';

import 'auth.dart';
import 'content_provider.dart';
import 'files.dart';
import 'photo.dart';
import 'photo_producer.dart';
import 'photos_api.dart';

mixin GooglePhotosContentProvider on ContentProviderBinding {
@override
Future<void> init() async {
await AuthBinding.instance.signInSilently();
}

@override
Widget buildHome(BuildContext context) {
switch (PhotosApp.of(context).state) {
case PhotosLibraryApiState.pendingAuthentication:
// Show a blank screen while we try to non-interactively sign in.
return Container();
case PhotosLibraryApiState.unauthenticated:
return const MontageScaffold(
producer: AssetPhotoProducer(),
bottomBarNotification: NeedToLoginNotification(),
);
case PhotosLibraryApiState.authenticated:
case PhotosLibraryApiState.authenticationExpired:
return MontageContainer(producer: GooglePhotosPhotoProducer());
case PhotosLibraryApiState.rateLimited:
return const AssetPhotosMontageContainer();
}
}

@override
String getMediaItemUrl(MediaItem item, ui.Size size) =>'${item.baseUrl}=w${size.width.toInt()}-h${size.height.toInt()}';
}

/// A [PhotoProducer] that produces photos from Google Photos using
/// the specified [model] object.
///
/// If the Google Photos API fails for any reason, this photo producer will
/// fall back to producing photos that are pulled statically from assets that
/// are bundled with this app (or, as a last resort, Todd's profile pic).
class GooglePhotosPhotoProducer extends PhotoProducer {
GooglePhotosPhotoProducer();

final List<MediaItem> queue = <MediaItem>[];
Completer<void>? _queueCompleter;

/// How many photos to load from the Google Photos API in one request.
///
/// Batching saves the number of requests being made to the photos API, which
/// is not only more efficient, but it makes it less likely that the app will
/// be rate limited.
static const int _batchSize = 50;

/// Makes a batch request to the Google Photos API, and adds all the media
/// items that it loaded into the [queue].
///
/// This method depends on the entire set of photo IDs having already been
/// loaded so that we can choose random photos from the main set. See
/// [PhotosLibraryApiModel.populateDatabase] for more info.
///
/// If this method is called, and then it is called again while the first
/// call's future is still pending, then the existing future will be reused
/// and returned.
Future<void> _queueItems() {
if (_queueCompleter != null) {
return _queueCompleter!.future;
}
_queueCompleter = Completer<void>();
final Future<void> result = _doQueueItems();
_queueCompleter!.complete(result);
_queueCompleter = null;
return result;
}

/// Method that does the actual work of queueing media items.
///
/// Assuming the database files have been created, this method will always
/// make a request to the Google Photos API, even if other requests are still
/// pending. To ensure that we reuse existing pending requests, use
/// [_queueItems] instead.
Future<void> _doQueueItems() async {
final FilesBinding files = FilesBinding.instance;
if (!files.photosFile.existsSync()) {
// We haven't yet finished loading the set of photo IDs from which to
// choose the next batch of media items; nothing to queue.
return;
}

final PhotosApiBinding photosApi = PhotosApiBinding.instance;
final List<String> mediaItemIds = await photosApi.pickRandomMediaItems(_batchSize);
Iterable<MediaItem> items = await photosApi.getMediaItems(mediaItemIds);
items = items.where((MediaItem item) => item.size != null);
assert(() {
if (items.length != _batchSize) {
debugPrint('Expecting $_batchSize items, but retrieved ${items.length}');
}
return true;
}());
queue.insertAll(0, items);
}

@override
Future<Photo> produce({
required BuildContext context,
required Size sizeConstraints,
double scaleMultiplier = 1,
}) async {
final ui.FlutterView window = View.of(context);

if (queue.isEmpty) {
// The queue can still be empty after this call, e.g. if the database
// files haven't been created yet.
await _queueItems();
}

if (queue.isEmpty) {
// ignore: use_build_context_synchronously
return await const AssetPhotoProducer().produce(
context: context,
sizeConstraints: sizeConstraints,
scaleMultiplier: scaleMultiplier,
);
} else {
final double scale = window.devicePixelRatio * scaleMultiplier;
final MediaItem mediaItem = queue.removeLast();
final Size photoLogicalSize = applyBoxFit(
BoxFit.scaleDown,
mediaItem.size!,
sizeConstraints,
).destination;
return Photo(
id: mediaItem.id,
mediaItem: mediaItem,
size: photoLogicalSize,
scale: scale,
boundingConstraints: sizeConstraints,
image: NetworkImage(ContentProviderBinding.instance.getMediaItemUrl(mediaItem, photoLogicalSize * scale), scale: scale),
);
}
}
}
Loading

0 comments on commit c462720

Please sign in to comment.