blob: 075ec68b770ec78cac47e80bd8ff6e9570b32b87 [file] [log] [blame]
// Copyright 2019 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:url_launcher/url_launcher.dart';
import '../model/commit.pb.dart';
import 'commit_author_avatar.dart';
// TODO(ianh): Factor out the logic in task_overlay.dart and use it here as well,
// so that all our popups have the same look and feel and we don't duplicate code.
/// Displays Git commit information.
///
/// On click, it will open an [OverlayEntry] with [CommitOverlayContents]
/// to show the information provided. Otherwise, it just shows the avatar
/// for the author of this commit. Clicking outside of the [OverlayEntry]
/// will close it.
class CommitBox extends StatefulWidget {
const CommitBox({
super.key,
required this.commit,
});
/// The commit being shown
final Commit commit;
@override
CommitBoxState createState() => CommitBoxState();
}
class CommitBoxState extends State<CommitBox> {
OverlayEntry? _commitOverlay;
@override
Widget build(BuildContext context) {
return InkWell(
onTap: _handleTap,
child: Padding(
padding: const EdgeInsets.all(4.0),
child: CommitAuthorAvatar(commit: widget.commit),
),
);
}
void _handleTap() {
_commitOverlay = OverlayEntry(
builder: (_) => CommitOverlayContents(
parentContext: context,
commit: widget.commit,
closeCallback: _closeOverlay,
),
);
Overlay.of(context).insert(_commitOverlay!);
}
void _closeOverlay() => _commitOverlay?.remove();
}
/// Displays the information from a Git commit.
///
/// This is intended to be inserted in an [OverlayEntry] as it requires
/// [closeCallback] that will remove the widget from the tree.
class CommitOverlayContents extends StatelessWidget {
const CommitOverlayContents({
super.key,
required this.parentContext,
required this.commit,
required this.closeCallback,
});
/// The parent context that has the size of the whole screen
final BuildContext parentContext;
/// The commit data to display in the overlay
final Commit commit;
/// This callback removes the parent overlay from the widget tree.
///
/// On a click that is outside the area of the overlay (the rest of the screen),
/// this callback is called closing the overlay.
final void Function() closeCallback;
@override
Widget build(BuildContext context) {
final ThemeData theme = Theme.of(context);
final RenderBox renderBox = parentContext.findRenderObject() as RenderBox;
final Offset offsetLeft = renderBox.localToGlobal(Offset.zero);
return Stack(
children: <Widget>[
// This is the area a user can click (the rest of the screen) to close the overlay.
GestureDetector(
onTap: closeCallback,
child: Container(
width: MediaQuery.of(parentContext).size.width,
height: MediaQuery.of(parentContext).size.height,
// Color must be defined otherwise the container can't be clicked on
color: Colors.transparent,
),
),
Positioned(
width: 300,
// Move this overlay to be where the parent is
top: offsetLeft.dy + (renderBox.size.height / 2),
left: offsetLeft.dx + (renderBox.size.width / 2),
child: Card(
child: SafeArea(
minimum: const EdgeInsets.all(16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
CommitAuthorAvatar(commit: commit),
Expanded(
child: Padding(
padding: const EdgeInsetsDirectional.only(start: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Align(
alignment: Alignment.centerLeft,
child: AnimatedDefaultTextStyle(
style: theme.textTheme.titleMedium!,
duration: kThemeChangeDuration,
child: Row(
children: <Widget>[
Hyperlink(
text: commit.sha.substring(0, 7),
onPressed: _openGithub,
),
IconButton(
icon: const Icon(Icons.copy),
onPressed: () => unawaited(
Clipboard.setData(
ClipboardData(text: commit.sha),
),
),
),
],
),
),
),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.only(bottom: 4),
child: AnimatedDefaultTextStyle(
style: theme.textTheme.bodyMedium!.copyWith(
color: theme.textTheme.bodySmall!.color,
),
duration: kThemeChangeDuration,
child: SelectableText(commit.message.split('\n').first),
),
),
SelectableText(commit.author),
],
),
),
),
],
),
),
),
),
],
);
}
Future<void> _openGithub() async {
final String githubUrl = 'https://github.com/${commit.repository}/commit/${commit.sha}';
await launchUrl(Uri.parse(githubUrl));
}
}
class Hyperlink extends StatefulWidget {
const Hyperlink({
super.key,
required this.text,
this.onPressed,
});
final String text;
final VoidCallback? onPressed;
@override
HyperlinkState createState() => HyperlinkState();
}
class HyperlinkState extends State<Hyperlink> {
bool hover = false;
@override
Widget build(BuildContext context) {
final TextStyle defaultStyle = DefaultTextStyle.of(context).style;
return MouseRegion(
onEnter: (PointerEnterEvent _) => setState(() => hover = true),
onExit: (PointerExitEvent _) => setState(() => hover = false),
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: widget.onPressed,
child: Text(
widget.text,
style: defaultStyle.copyWith(
color: const Color(0xff1377c0),
decoration: hover ? TextDecoration.underline : TextDecoration.none,
),
),
),
);
}
}