-
Notifications
You must be signed in to change notification settings - Fork 20
Take screenshots on the web beta, and of cloud strategies anywhere #229
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
414af22
Take screenshots on the web beta, and of cloud strategies anywhere
SunkenInTime f1cfe1d
Copy the page and decode its images before a screenshot waits
SunkenInTime 625637e
Refresh a screenshot's image URLs through the reader's share link
SunkenInTime 971b6aa
Keep a capture's images live until it is done, and bound their downloads
SunkenInTime d316f08
Let go of a capture image only once
SunkenInTime 3cc0350
Keep a capture from signing the editor out of cloud sync
SunkenInTime 3d1dc7f
Download a capture's images together, and take one screenshot at a time
SunkenInTime 40edb70
Abort a capture's other downloads when it stops early
SunkenInTime File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,153 @@ | ||
| import 'dart:async'; | ||
| import 'dart:typed_data'; | ||
|
|
||
| import 'package:flutter/painting.dart'; | ||
| import 'package:http/http.dart' as http; | ||
| import 'package:icarus/providers/strategy_image_source.dart'; | ||
|
|
||
| /// An image a capture would paint is not ready: its cloud copy is still | ||
| /// loading, or its bytes could not be fetched or decoded. The capture stops | ||
| /// rather than save a picture with the image missing. | ||
| class CaptureImagesUnavailable implements Exception { | ||
| const CaptureImagesUnavailable.stillLoading() : cause = null; | ||
| const CaptureImagesUnavailable.fetchFailed(this.cause); | ||
|
|
||
| /// Why the image could not be loaded; null while it is still loading. | ||
| final Object? cause; | ||
|
|
||
| String get userMessage => cause == null | ||
| ? 'Images on this page are still loading. Try again in a moment.' | ||
| : "Couldn't load the images on this page. Check your connection and " | ||
| 'try again.'; | ||
|
|
||
| @override | ||
| String toString() => cause == null | ||
| ? 'CaptureImagesUnavailable: an image is still loading' | ||
| : 'CaptureImagesUnavailable: $cause'; | ||
| } | ||
|
|
||
| /// Fetches the bytes behind a cloud image's [url]. | ||
| typedef CaptureImageFetcher = Future<Uint8List> Function( | ||
| String imageId, | ||
| String url, | ||
| ); | ||
|
|
||
| /// What each image in a capture paints, by image id, decoded and held in | ||
| /// the image cache so the capture's first frame already has every picture. | ||
| /// Call [release] once the capture is done. | ||
| class CaptureImages { | ||
| CaptureImages._(this.sources, this._holds); | ||
|
|
||
| /// Feeds [captureImageSourcesProvider] in the capture's container. | ||
| final Map<String, StrategyImageSource> sources; | ||
| final List<_HeldImage> _holds; | ||
|
|
||
| void release() { | ||
| for (final hold in _holds) { | ||
| hold.release(); | ||
| } | ||
| _holds.clear(); | ||
| } | ||
| } | ||
|
|
||
| /// Turns where each image's bytes come from, by image id, into what an | ||
| /// offscreen capture can paint. | ||
| /// | ||
| /// A capture has no network reads of its own, so cloud URLs are fetched here | ||
| /// and painted from memory. Files and bytes already in memory paint as they | ||
| /// are, and an image the editor shows as unavailable is captured that way | ||
| /// too. Every image that paints is decoded before this returns. Throws | ||
| /// [CaptureImagesUnavailable] while an image is still loading, or when a | ||
| /// fetch or a decode fails. [checkpoint] runs before each image; whatever it | ||
| /// throws stops the work and releases what was held. | ||
| Future<CaptureImages> resolveCaptureImages( | ||
| Map<String, StrategyImageSource> sources, { | ||
| required CaptureImageFetcher fetch, | ||
| void Function()? checkpoint, | ||
| }) async { | ||
| if (sources.values.any((source) => source is ImageLoading)) { | ||
| throw const CaptureImagesUnavailable.stillLoading(); | ||
| } | ||
| // Downloads run together, so a page's wait is its slowest image rather | ||
| // than the sum of them. They share one client, closed when this returns, | ||
| // so a capture that stops early aborts the downloads it no longer needs; | ||
| // a download nobody awaits must not surface as an unhandled error. | ||
| final client = http.Client(); | ||
| final downloads = { | ||
| for (final MapEntry(key: imageId, value: source) in sources.entries) | ||
| if (source case RemoteImageUrl(:final url)) | ||
| imageId: http.runWithClient( | ||
| () => _guard(() => fetch(imageId, url)), | ||
| () => client, | ||
| )..ignore(), | ||
| }; | ||
|
greptile-apps[bot] marked this conversation as resolved.
|
||
| final resolved = <String, StrategyImageSource>{}; | ||
| final holds = <_HeldImage>[]; | ||
| try { | ||
| for (final MapEntry(key: imageId, value: source) in sources.entries) { | ||
| checkpoint?.call(); | ||
| final download = downloads[imageId]; | ||
| final paintable = download == null ? source : ImageBytes(await download); | ||
| final image = paintable.imageProvider; | ||
| if (image != null) { | ||
| holds.add(await _guard(() => _HeldImage.decode(image))); | ||
|
greptile-apps[bot] marked this conversation as resolved.
|
||
| } | ||
| resolved[imageId] = paintable; | ||
| } | ||
| checkpoint?.call(); | ||
| } catch (_) { | ||
| for (final hold in holds) { | ||
| hold.release(); | ||
| } | ||
| rethrow; | ||
| } finally { | ||
| client.close(); | ||
| } | ||
| return CaptureImages._(resolved, holds); | ||
| } | ||
|
|
||
| Future<T> _guard<T>(Future<T> Function() load) async { | ||
| try { | ||
| return await load(); | ||
| } catch (error) { | ||
| throw CaptureImagesUnavailable.fetchFailed(error); | ||
| } | ||
| } | ||
|
|
||
| /// A decoded image kept listened to, which keeps it among the image cache's | ||
| /// live images: a widget asking for the same image finds it decoded, however | ||
| /// full the cache gets, until [release]. | ||
| class _HeldImage { | ||
| _HeldImage._(this._stream); | ||
|
|
||
| final ImageStream _stream; | ||
| late final ImageStreamListener _listener; | ||
| bool _attached = false; | ||
|
|
||
| /// Completes once [image]'s first frame is decoded. | ||
| static Future<_HeldImage> decode(ImageProvider image) { | ||
| final hold = _HeldImage._(image.resolve(ImageConfiguration.empty)); | ||
| final decoded = Completer<_HeldImage>(); | ||
| hold._listener = ImageStreamListener( | ||
| (info, _) { | ||
| info.dispose(); | ||
| if (!decoded.isCompleted) decoded.complete(hold); | ||
| }, | ||
| onError: (Object error, StackTrace? stackTrace) { | ||
| hold.release(); | ||
| if (!decoded.isCompleted) decoded.completeError(error, stackTrace); | ||
| }, | ||
| ); | ||
| hold._attached = true; | ||
| hold._stream.addListener(hold._listener); | ||
| return decoded.future; | ||
| } | ||
|
|
||
| /// Lets go of the image. Safe to call again: a later frame that fails to | ||
| /// decode lets go first, and the stream may be gone by the second call. | ||
| void release() { | ||
| if (!_attached) return; | ||
| _attached = false; | ||
| _stream.removeListener(_listener); | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a cloud image’s signed URL has expired, the downloader requests a fresh URL. The first
http.getcloses the client shared here, so the retry fails even when the fresh URL is valid. The screenshot cannot be saved; video export in PR #230 uses the same image-resolution path. Keep the capture-scoped client open until all requests finish.Artifacts
Command output from the check
Command output from the check
Evidence from the check
Evidence from the check