-
Notifications
You must be signed in to change notification settings - Fork 309
lightbox: Prototype lightbox #29
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
Conversation
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.
Thanks! Looking forward to having this.
lib/widgets/lightbox.dart
Outdated
); | ||
} | ||
|
||
class _Page extends StatefulWidget { |
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.
nit: I think I'd prefer the class names be able to stand alone, even when private to the file: so _LightboxPage
.
lib/widgets/lightbox.dart
Outdated
required super.child | ||
}) : super( |
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.
nit (here and other places in the file): trailing comma when a list item doesn't have the closing punctuation on the same line
lib/widgets/lightbox.dart
Outdated
// message. Maybe keep `src`, so that on exit the lightbox image doesn't | ||
// fly to an image preview with a different URL, following a message edit | ||
// while the lightbox was open. | ||
tag: '${message.id.toString()} $src' |
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.
nit: pretty sure the toString
is redundant when doing string interpolation
tag: '${message.id.toString()} $src' | |
tag: '${message.id} $src' |
lib/widgets/lightbox.dart
Outdated
// TODO: Add index of the image preview in the message, to not break if | ||
// there are multiple image previews with the same URL in the same | ||
// message. Maybe keep `src`, so that on exit the lightbox image doesn't | ||
// fly to an image preview with a different URL, following a message edit | ||
// while the lightbox was open. | ||
tag: '${message.id.toString()} $src' |
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.
Instead of a string, this can be an instance of a small ad-hoc class that just contains these two fields and overrides ==
. That's what Flutter upstream would definitely do; string concatenation is expensive by comparison.
In the near future we'll be able to simplify that to a record. But the class is quick to write.
lib/widgets/lightbox.dart
Outdated
void _handleRouteEntranceAnimationStatusChange(AnimationStatus status) { | ||
if (status == AnimationStatus.completed) { | ||
setState(() { | ||
_headerFooterVisible = true; |
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.
Can the animation status ever go backward — turn from completed
to something else?
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.
I think so?
PageRouteBuilder.pageBuilder
(where we consume the relevant Animation<double>
)
https://github.com/flutter/flutter/blob/2ad6cd72c0/packages/flutter/lib/src/widgets/pages.dart#L94
/// See [ModalRoute.buildPage] for complete definition of the parameters.
ModalRoute.buildPage
https://github.com/flutter/flutter/blob/2ad6cd72c0/packages/flutter/lib/src/widgets/routes.dart#LL1062-L1064C41
/// * [animation]: The animation for this route's transition. When entering,
/// the animation runs forward from 0.0 to 1.0. When exiting, this animation
/// runs backwards from 1.0 to 0.0.
ModalRoute.buildPage
https://github.com/flutter/flutter/blob/2ad6cd72c0/packages/flutter/lib/src/widgets/routes.dart#L1069-L1071
/// This method is only called when the route is first built, and rarely
/// thereafter. In particular, it is not automatically called again when the
/// route's state changes unless it uses [ModalRoute.of]. […]
As long as ModalRoute.buildPage
is not called synchronously with the beginning of the route exit, I think our listener will observe a transition from completed
to something else (AnimationStatus.reverse
?) at that time. (I say "as long as" because I think a fresh call to ModalRoute.buildPage
would tear down our current listener and we'd make a new listener, and in theory the state change could happen synchronously in between, when we have no listener.)
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.
So maybe we add a case for AnimationStatus.reverse
that sets _headerFooterVisible
to false.
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.
So maybe we add a case for
AnimationStatus.reverse
that sets_headerFooterVisible
to false.
Sure, except let's make it comprehensive: if the status is completed, we want true, and if not completed, we want false. Right?
lib/widgets/lightbox.dart
Outdated
body: MediaQuery( | ||
// Declare the vertical insets unconsumed by `appBar` and | ||
// `bottomNavigationBar`, so we can position the image to not overlap | ||
// any system intrusions, at least until the user zooms in. The top | ||
// and bottom app bars overlay the pan-zoom layer in the Z direction; | ||
// they don't affect anything in the pan-zoom layer's Y direction. | ||
// | ||
// Done by clobbering the ambient MediaQueryData (prepared by Scaffold | ||
// for the `body`) with one taken from a BuildContext above the | ||
// Scaffold. | ||
data: MediaQuery.of(context), |
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.
Hmm. We do need this, but this explanation of why doesn't seem right.
As the doc on [Scaffold.extendBody] says, when there's a bottom nav bar, the body will be wrapped in a MediaQuery with bottom padding corresponding to the bottom nav bar. Effectively the bottom nav bar itself is being treated as a system intrusion. This would mean that when we then properly use a SafeArea, we'd shrink the image to avoid the bottom nav bar.
The doc on [Scaffold.extendBodyBehindAppBar] doesn't mention this, but it seems to have much the same behavior, just as one would hope for them to be similar to each other.
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.
I'll give another attempt at this in my next revision.
lib/widgets/lightbox.dart
Outdated
|
||
// With Material 3, later, try removing these "transparent" overrides | ||
shadowColor: Colors.transparent, | ||
surfaceTintColor: Colors.transparent, |
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.
Does this have a distinct effect from null
, the default? I'm not seeing a difference.
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.
Ah yeah, surfaceTintColor
does nothing in Material 2, so equivalently we could not pass surfaceTintColor
.
In fact, here, I think passing a transparent shadowColor
does nothing either. (Using a color picker, I haven't detected a difference from not passing it.) In Material 2, the default shadow color is black (transparent in M3), and the shadow is drawn on the Scaffold's background, which we've set to black.
I'll remove these lines and the comment, here and on the BottomAppBar
.
lib/widgets/lightbox.dart
Outdated
ScaffoldMessenger.of(context).showSnackBar( | ||
const SnackBar( | ||
behavior: SnackBarBehavior.floating, | ||
content: Text('Link copied'))); |
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.
On modern Android, this ends up looking a bit ridiculous (and potentially frustrating) because the system has its own "something was just copied to the clipboard" popup… and it appears at the bottom left, and completely obscures the words "Link copied". So the user just sees what appears to be a blank snackbar :-P
Looking around for an example to follow, if I go to system settings and long-press on something like the IMEI or the IP address, I get an option to copy it; when I take that option, it shows a toast. The toast is really quite a lot like a floating snackbar, but:
- it's rounded into a stadium shape — whatever;
- it has a bounded width, roughly 160px. (Longer text wraps onto two lines and then gets ellipsized.)
This feels fine, and it solves the problem — no overlap with the system's own popup.
So perhaps here:
ScaffoldMessenger.of(context).showSnackBar( | |
const SnackBar( | |
behavior: SnackBarBehavior.floating, | |
content: Text('Link copied'))); | |
ScaffoldMessenger.of(context).showSnackBar( | |
const SnackBar( | |
behavior: SnackBarBehavior.floating, | |
width: 160, | |
content: Text('Link copied'))); |
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.
the system has its own "something was just copied to the clipboard" popup…
Ah, looks like the Android docs have guidelines for handling this:
https://developer.android.com/develop/ui/views/touch-and-input/copy-paste#duplicate-notifications
In Android 12L (API level 32) and lower, we suggest alerting users that they’ve successfully copied by issuing a visual pop-up in-app widget (like Toasts or Snackbars) after copying.
To avoid duplicate displays of information, we strongly recommend removing any pop-up widget shown after an in-app copy for Android 13 and higher.
How about we follow that? It seems easy.
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.
Sure, sounds good to me.
lib/widgets/lightbox.dart
Outdated
IconButton( | ||
tooltip: 'Copy link', |
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.
Can this IconButton
be pulled out as its own widget? So that this would look like:
children: [
_CopyLinkButton(url: widget.src),
// TODO …
]
I think that would help keep this build method easier to read, especially when more buttons are added in the future.
lib/widgets/lightbox.dart
Outdated
icon: const Icon(Icons.copy), | ||
onPressed: () async { | ||
await Clipboard.setData(ClipboardData(text: widget.src)); | ||
if (!context.mounted) return; // ignore: use_build_context_synchronously |
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.
Hmm interesting. What's the reasoning for the ignore?
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.
An earlier version of this code was "BAD" according to this lint rule: https://dart-lang.github.io/linter/lints/use_build_context_synchronously.html
This revision is "GOOD", but the rule doesn't recognize that.
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.
Huh, weird.
Let's have a comment explaining any lint ignores we use.
This one seems like a bug in the lint rule. Is there an issue for it in the upstream tracker? If so, a link to that would suffice as an explanation.
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.
It looks like this is https://github.com/dart-lang/linter/issues/4007 . It comes with a workaround that's good enough — you rewrite the code as:
if (context.mounted) {
// … use context …
}
Thanks for the review! Revision pushed. One interesting thing is that, after dart-lang/sdk#57178, the I've added a |
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.
Thanks! This is getting close.
lib/widgets/content.dart
Outdated
final messageContentWidget = context.findAncestorWidgetOfExactType<MessageContent>(); | ||
assert(messageContentWidget != null, 'No MessageContent ancestor'); | ||
final message = messageContentWidget!.message; |
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.
Let's encapsulate this as a static method on MessageContent
, so that we can say something like:
final message = MessageContent.of(context);
That method is expensive because of the "findAncestor…" call. So it should have a note about that in dartdoc — can copy from and/or refer to the doc of "findAncestor…" itself — and also a TODO comment to use InheritedWidget to fix that. (ISTR having a chat thread about that part; I forget what all our conclusions were.)
lib/widgets/content.dart
Outdated
alignment: Alignment.center, | ||
color: const Color.fromRGBO(0, 0, 0, 0.03), | ||
child: LightboxHero( | ||
accountId: PerAccountStoreWidget.accountIdOf(context), |
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.
Let's pull this sort of call out as a local before the main widget expression.
lib/widgets/lightbox.dart
Outdated
// RealmContentNetworkImage needs an ancestor PerAccountStoreWidget, and | ||
// during the hero's "flight", we wouldn't otherwise get one. | ||
child: PerAccountStoreWidget(accountId: accountId, |
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.
Hmm interesting. I guess it lives directly under the navigator's Stack, and not under either page:
Heroes and the Navigator's Overlay Stack must be axis-aligned for all this to work. The top left and bottom right coordinates of each animated Hero will be converted to global coordinates and then from there converted to that Stack's coordinate space, and the entire Hero subtree will, for the duration of the animation, be lifted out of its original place, and positioned on that stack.
https://api.flutter.dev/flutter/widgets/Hero-class.html
This solution feels a bit kludgy, though. I think a major part of why it feels that way to me is that it's inserting a PerAccountStoreWidget even while we're still within each route, when there's already one as an ancestor.
I think this API's preferred solution for this sort of problem is to use [Hero.flightShuttleBuilder]. See here in the Hero doc:
Additionally, if the Hero subtree changes appearance based on an InheritedWidget (such as MediaQuery or Theme), then the hero animation may have discontinuity at the start or the end of the animation because route A and route B provides different such InheritedWidgets. Consider providing a custom flightShuttleBuilder to ensure smooth transitions. The default flightShuttleBuilder interpolates MediaQuery's paddings. If your Hero widget uses custom InheritedWidgets and displays a discontinuity in the animation, try to provide custom in-flight transition using flightShuttleBuilder.
And here's the type for that builder:
https://api.flutter.dev/flutter/widgets/HeroFlightShuttleBuilder.html
So it gets plenty of contexts, which it can use to find this information and provide its own PerAccountStoreWidget.
lib/widgets/clipboard.dart
Outdated
void copyWithPopup({ | ||
required BuildContext context, | ||
required ClipboardData data, | ||
required Text successText // TODO(i18n) |
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.
required Text successText // TODO(i18n) | |
required Widget successContent, |
In general, APIs that take a widget should avoid caring about what type of widget it is; the type should be just Widget, unless the code is actually going to make use of features of a more specific type.
(And then this todo-i18n comment can go at the call site.)
lib/widgets/clipboard.dart
Outdated
final shouldShowSnackbar = | ||
(deviceInfo is IosDeviceInfo) | ||
|
||
// Android 13+ shows its own popup on copying to the clipboard, | ||
// so we suppress ours, following the advice at: | ||
// https://developer.android.com/develop/ui/views/touch-and-input/copy-paste#duplicate-notifications | ||
// TODO(android-sdk-32) Remove and simplify dartdoc | ||
|| (deviceInfo is AndroidDeviceInfo && deviceInfo.version.sdkInt <= 32); |
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.
This is a bit hard to read because the conditions are separated so widely from each other by the comment, it looks after the "IosDeviceInfo" line as if that's the whole condition.
One solution could be to move the comment above the whole expression.
Or, here's a version using fancy Dart 3 pattern-matching:
final shouldShowSnackbar = | |
(deviceInfo is IosDeviceInfo) | |
// Android 13+ shows its own popup on copying to the clipboard, | |
// so we suppress ours, following the advice at: | |
// https://developer.android.com/develop/ui/views/touch-and-input/copy-paste#duplicate-notifications | |
// TODO(android-sdk-32) Remove and simplify dartdoc | |
|| (deviceInfo is AndroidDeviceInfo && deviceInfo.version.sdkInt <= 32); | |
final bool shouldShowSnackbar; | |
switch (deviceInfo) { | |
case AndroidDeviceInfo(:var version): | |
// Android 13+ shows its own popup on copying to the clipboard, | |
// so we suppress ours, following the advice at: | |
// https://developer.android.com/develop/ui/views/touch-and-input/copy-paste#duplicate-notifications | |
// TODO(android-sdk-33): Simplify this and dartdoc | |
shouldShowSnackbar = version.sdkInt <= 32; | |
default: | |
shouldShowSnackbar = true; | |
}; |
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.
Also reflected in that version: if SDK 32 is the last version with the old behavior, then SDK 33 is the one where this TODO becomes actionable when we bump the minimum that far, which means that's the one we want to mention in the comment.
lib/widgets/lightbox.dart
Outdated
switch (status) { | ||
case AnimationStatus.completed: | ||
// Route entrance transition complete | ||
setState(() { | ||
_headerFooterVisible = true; | ||
}); | ||
break; | ||
default: | ||
setState(() { | ||
_headerFooterVisible = false; | ||
}); | ||
break; | ||
} |
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.
can simplify:
switch (status) { | |
case AnimationStatus.completed: | |
// Route entrance transition complete | |
setState(() { | |
_headerFooterVisible = true; | |
}); | |
break; | |
default: | |
setState(() { | |
_headerFooterVisible = false; | |
}); | |
break; | |
} | |
final entranceAnimationComplete = status == AnimationStatus.completed; | |
setState(() { | |
_headerFooterVisible = entranceAnimationComplete; | |
}); |
(The switch already isn't adding any kind of robustness compared to that, because it has a default case.)
final appBarBackgroundColor = Colors.grey.shade900.withOpacity(0.87); | ||
const appBarForegroundColor = Colors.white; | ||
|
||
PreferredSizeWidget? appBar; |
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.
similarly, just Widget?
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.
Scaffold
asks for a PreferredSizeWidget?
for its appBar
param: https://api.flutter.dev/flutter/material/Scaffold/appBar.html
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.
Huh, I see! 👍, then.
lib/widgets/lightbox.dart
Outdated
centerTitle: false, | ||
foregroundColor: appBarForegroundColor, | ||
backgroundColor: appBarBackgroundColor, |
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.
nit: put these small arguments before the long, "child"-style, argument
lib/widgets/content.dart
Outdated
src: resolvedSrc)))), | ||
); |
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.
nit:
src: resolvedSrc)))), | |
); | |
src: resolvedSrc))))); |
lib/widgets/lightbox.dart
Outdated
bool operator ==(Object other) { | ||
return other is _LightboxHeroTag && | ||
other.messageId == messageId && | ||
other.src == other.src; |
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.
other.src == other.src; | |
other.src == src; |
🙂
We'll use this to conditionalize on the Android SDK version integer.
A lot of different widgets, some pretty deeply nested, go into rendering a message's content. This way each of those widgets can get data from the relevant Message object as needed, without the need for passing it all the way down, from widget params to widget params.
Thanks for the review! Revision pushed. |
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.
Thanks! Mostly small things remaining.
lib/widgets/content.dart
Outdated
/// Provides access to [message]. | ||
class InheritedMessage extends InheritedWidget { |
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.
I think this is a case of https://github.com/flutter/flutter/wiki/Style-guide-for-Flutter-repo#avoid-useless-documentation 🙂
Not everything in our tree has docs, and that's fine — unlike the Flutter framework, this isn't an API being provided to thousands of developers to consume, so I don't anticipate we'd ever want to turn on a lint that requires all public members to have docs.
src: resolvedSrc, | ||
child: RealmContentNetworkImage( | ||
resolvedSrc, |
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.
Hmm, this feels kind of redundant. Can the LightboxHero use src
to construct this RealmContentNetworkImage widget for itself?
lib/widgets/lightbox.dart
Outdated
child: RealmContentNetworkImage(src, filterQuality: FilterQuality.medium)); | ||
}, | ||
child: child, |
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.
And in fact this feels like it's encoding an assumption that the child
field is basically the same as this RealmContentNetworkImage widget. So it may as well construct it itself in all cases. Like this, say:
Widget build(BuildContext context) {
final child = RealmContentNetworkImage(src, filterQuality: FilterQuality.medium);
return Hero(
// …
child: child);
},
child: child);
lib/widgets/content.dart
Outdated
static InheritedMessage of(BuildContext context) { | ||
final widget = context.dependOnInheritedWidgetOfExactType<InheritedMessage>(); | ||
assert(widget != null, 'No InheritedMessage ancestor'); | ||
return widget!; |
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.
return widget!; | |
return widget!.message; |
Much like Theme.of(context)
and MediaQuery.of(context)
and friends.
lib/widgets/content.dart
Outdated
@@ -22,7 +23,24 @@ class MessageContent extends StatelessWidget { | |||
|
|||
@override | |||
Widget build(BuildContext context) { | |||
return BlockContentList(nodes: content.nodes); | |||
return InheritedMessage(message: message, child: BlockContentList(nodes: content.nodes)); |
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.
nit:
return InheritedMessage(message: message, child: BlockContentList(nodes: content.nodes)); | |
return InheritedMessage(message: message, | |
child: BlockContentList(nodes: content.nodes)); |
That way the main payload (the BlockContentList) is kept prominently visible.
lib/widgets/clipboard.dart
Outdated
// Early return on !mounted would be better, but: | ||
// https://github.com/dart-lang/linter/issues/4007 | ||
if (context.mounted) { |
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.
Here's a fun way to express this, which I developed the other day in my draft for #11 :
if (context.mounted) {} // https://github.com/dart-lang/linter/issues/4007
else {
return;
}
void _handleRouteEntranceAnimationStatusChange(AnimationStatus status) { | ||
final entranceAnimationComplete = status == AnimationStatus.completed; | ||
setState(() { | ||
_headerFooterVisible = entranceAnimationComplete; | ||
}); | ||
} | ||
|
||
void _handleTap() { |
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.
nit: I think the order reads a bit more cleanly if these come after initState/dispose, so just above build.
lib/widgets/lightbox.dart
Outdated
style: themeData.textTheme.titleLarge!.copyWith(color: appBarForegroundColor), | ||
), |
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.
nit:
style: themeData.textTheme.titleLarge!.copyWith(color: appBarForegroundColor), | |
), | |
style: themeData.textTheme.titleLarge!.copyWith(color: appBarForegroundColor)), |
lib/widgets/lightbox.dart
Outdated
iconTheme: themeData.iconTheme.copyWith(color: appBarForegroundColor), | ||
), |
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.
nit:
iconTheme: themeData.iconTheme.copyWith(color: appBarForegroundColor), | |
), | |
iconTheme: themeData.iconTheme.copyWith(color: appBarForegroundColor)), |
Thanks for the review! Revision pushed. |
LGTM! Merging. |
Great, thanks for all the reviews! I have this planned followup which I hope to do soon (quoting from PR description):
|
SGTM |
Done: 1381f5e |
It was great to see how easily we could implement the "hero" animation and pan-zoom!
This is stacked on top of dart-lang/sdk#57174, for the copy-link feature.(That's been merged! 🎉)If this is merged, I plan to file issues for the following:
then push a commit to
main
that just updates the correspondingTODO
s in the code to point to those new issues.