mirror of
https://github.com/flutter/samples.git
synced 2026-04-04 18:51:05 +00:00
Migrate desktop_photo_search to top level (#1002)
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
// Copyright 2019 The Flutter team. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart' hide FluentIcons;
|
||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
||||
import 'package:flutter/material.dart' show Card, BorderSide;
|
||||
import 'package:transparent_image/transparent_image.dart';
|
||||
import 'package:url_launcher/link.dart';
|
||||
|
||||
import '../../unsplash_access_key.dart';
|
||||
import '../unsplash/photo.dart';
|
||||
|
||||
final _unsplashHomepage = Uri.parse(
|
||||
'https://unsplash.com/?utm_source=$unsplashAppName&utm_medium=referral');
|
||||
|
||||
typedef PhotoDetailsPhotoSaveCallback = void Function(Photo);
|
||||
|
||||
class PhotoDetails extends StatefulWidget {
|
||||
const PhotoDetails({
|
||||
required this.photo,
|
||||
required this.onPhotoSave,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
final Photo photo;
|
||||
final PhotoDetailsPhotoSaveCallback onPhotoSave;
|
||||
|
||||
@override
|
||||
_PhotoDetailsState createState() => _PhotoDetailsState();
|
||||
}
|
||||
|
||||
class _PhotoDetailsState extends State<PhotoDetails> {
|
||||
Widget _buildPhotoAttribution(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
const Text('Photo by'),
|
||||
Link(
|
||||
uri: Uri.parse(
|
||||
'https://unsplash.com/@${widget.photo.user!.username}?utm_source=$unsplashAppName&utm_medium=referral'),
|
||||
builder: (context, followLink) => TextButton(
|
||||
onPressed: followLink,
|
||||
child: Text(widget.photo.user!.name),
|
||||
),
|
||||
),
|
||||
const Text('on'),
|
||||
Link(
|
||||
uri: _unsplashHomepage,
|
||||
builder: (context, followLink) => TextButton(
|
||||
onPressed: followLink,
|
||||
child: const Text('Unsplash'),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scrollbar(
|
||||
child: SingleChildScrollView(
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
Card(
|
||||
shape: ContinuousRectangleBorder(
|
||||
side: const BorderSide(color: Colors.black),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: AnimatedSize(
|
||||
duration: const Duration(milliseconds: 750),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 40),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 400,
|
||||
minHeight: 400,
|
||||
),
|
||||
child: FadeInImage.memoryNetwork(
|
||||
placeholder: kTransparentImage,
|
||||
imageSemanticLabel: widget.photo.description,
|
||||
image: widget.photo.urls!.small!,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 4),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildPhotoAttribution(context),
|
||||
const SizedBox(width: 8),
|
||||
IconButton(
|
||||
icon: const Icon(
|
||||
FluentIcons.arrow_download_20_regular,
|
||||
size: 20,
|
||||
),
|
||||
onPressed: () => widget.onPhotoSave(widget.photo),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 48),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
// Copyright 2019 The Flutter team. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
|
||||
typedef PhotoSearchDialogCallback = void Function(String searchQuery);
|
||||
|
||||
class PhotoSearchDialog extends StatefulWidget {
|
||||
const PhotoSearchDialog({required this.callback, Key? key}) : super(key: key);
|
||||
final PhotoSearchDialogCallback callback;
|
||||
@override
|
||||
State<PhotoSearchDialog> createState() => _PhotoSearchDialogState();
|
||||
}
|
||||
|
||||
class _PhotoSearchDialogState extends State<PhotoSearchDialog> {
|
||||
final _controller = TextEditingController();
|
||||
bool _searchEnabled = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller.addListener(() {
|
||||
setState(() {
|
||||
_searchEnabled = _controller.text.isNotEmpty;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => ContentDialog(
|
||||
title: const Text('Photo Search'),
|
||||
content: TextBox(
|
||||
autofocus: true,
|
||||
controller: _controller,
|
||||
onSubmitted: (content) {
|
||||
if (content.isNotEmpty) {
|
||||
widget.callback(content);
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
FilledButton(
|
||||
onPressed: _searchEnabled
|
||||
? () {
|
||||
widget.callback(_controller.text);
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
: null,
|
||||
child: const Text('Search'),
|
||||
),
|
||||
Button(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
// Copyright 2019 The Flutter team. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:url_launcher/url_launcher.dart' as url_launcher;
|
||||
|
||||
class PolicyDialog extends StatelessWidget {
|
||||
const PolicyDialog({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ContentDialog(
|
||||
title: const Text('Terms & Conditions'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
RichText(
|
||||
textAlign: TextAlign.left,
|
||||
text: TextSpan(
|
||||
text: '• ',
|
||||
style: const TextStyle(color: Colors.black, fontSize: 18),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: 'https://policies.google.com/terms',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold, color: Colors.blue.normal),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () async {
|
||||
const url = 'https://policies.google.com/terms';
|
||||
if (await url_launcher.canLaunch(url)) {
|
||||
await url_launcher.launch(url);
|
||||
}
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
RichText(
|
||||
textAlign: TextAlign.left,
|
||||
text: TextSpan(
|
||||
text: '• ',
|
||||
style: const TextStyle(color: Colors.black, fontSize: 18),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: 'https://unsplash.com/terms',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold, color: Colors.blue.normal),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () async {
|
||||
const url = 'https://unsplash.com/terms';
|
||||
if (await url_launcher.canLaunch(url)) {
|
||||
await url_launcher.launch(url);
|
||||
}
|
||||
},
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
FilledButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: const Text('Accept'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
190
desktop_photo_search/fluent_ui/lib/src/widgets/split.dart
Normal file
190
desktop_photo_search/fluent_ui/lib/src/widgets/split.dart
Normal file
@@ -0,0 +1,190 @@
|
||||
// Copyright 2019 The Chromium 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:math';
|
||||
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// A widget that takes two children, lays them out along [axis], and allows
|
||||
/// the user to resize them.
|
||||
///
|
||||
/// The user can customize the amount of space allocated to each child by
|
||||
/// dragging a divider between them.
|
||||
///
|
||||
/// [initialFirstFraction] defines how much space to give the [firstChild]
|
||||
/// when first building this widget. [secondChild] will take the remaining
|
||||
/// space.
|
||||
///
|
||||
/// The user can drag the widget with key [dividerKey] to change
|
||||
/// the space allocated between [firstChild] and [secondChild].
|
||||
// TODO(djshuckerow): introduce support for a minimum fraction a child
|
||||
// is allowed.
|
||||
class Split extends StatefulWidget {
|
||||
/// Builds a split oriented along [axis].
|
||||
const Split({
|
||||
Key? key,
|
||||
required this.axis,
|
||||
required this.firstChild,
|
||||
required this.secondChild,
|
||||
double? initialFirstFraction,
|
||||
}) : initialFirstFraction = initialFirstFraction ?? 0.5,
|
||||
super(key: key);
|
||||
|
||||
/// The main axis the children will lay out on.
|
||||
///
|
||||
/// If [Axis.horizontal], the children will be placed in a [Row]
|
||||
/// and they will be horizontally resizable.
|
||||
///
|
||||
/// If [Axis.vertical], the children will be placed in a [Column]
|
||||
/// and they will be vertically resizable.
|
||||
///
|
||||
/// Cannot be null.
|
||||
final Axis axis;
|
||||
|
||||
/// The child that will be laid out first along [axis].
|
||||
final Widget firstChild;
|
||||
|
||||
/// The child that will be laid out last along [axis].
|
||||
final Widget secondChild;
|
||||
|
||||
/// The fraction of the layout to allocate to [firstChild].
|
||||
///
|
||||
/// [secondChild] will receive a fraction of `1 - initialFirstFraction`.
|
||||
final double initialFirstFraction;
|
||||
|
||||
/// The key passed to the divider between [firstChild] and [secondChild].
|
||||
///
|
||||
/// Visible to grab it in tests.
|
||||
@visibleForTesting
|
||||
Key get dividerKey => Key('$this dividerKey');
|
||||
|
||||
/// The size of the divider between [firstChild] and [secondChild] in
|
||||
/// logical pixels (dp, not px).
|
||||
static const double dividerMainAxisSize = 10;
|
||||
|
||||
static Axis axisFor(BuildContext context, double horizontalAspectRatio) {
|
||||
final screenSize = MediaQuery.of(context).size;
|
||||
final aspectRatio = screenSize.width / screenSize.height;
|
||||
if (aspectRatio >= horizontalAspectRatio) {
|
||||
return Axis.horizontal;
|
||||
}
|
||||
return Axis.vertical;
|
||||
}
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() => _SplitState();
|
||||
}
|
||||
|
||||
class _SplitState extends State<Split> {
|
||||
late double firstFraction;
|
||||
|
||||
double get secondFraction => 1 - firstFraction;
|
||||
|
||||
bool get isHorizontal => widget.axis == Axis.horizontal;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
firstFraction = widget.initialFirstFraction;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return LayoutBuilder(builder: _buildLayout);
|
||||
}
|
||||
|
||||
Widget _buildLayout(BuildContext context, BoxConstraints constraints) {
|
||||
final width = constraints.maxWidth;
|
||||
final height = constraints.maxHeight;
|
||||
final axisSize = isHorizontal ? width : height;
|
||||
final crossAxisSize = isHorizontal ? height : width;
|
||||
const halfDivider = Split.dividerMainAxisSize / 2.0;
|
||||
|
||||
// Determine what fraction to give each child, including enough space to
|
||||
// display the divider.
|
||||
var firstSize = axisSize * firstFraction;
|
||||
var secondSize = axisSize * secondFraction;
|
||||
|
||||
// Clamp the sizes to be sure there is enough space for the dividers.
|
||||
firstSize = firstSize.clamp(halfDivider, axisSize - halfDivider);
|
||||
secondSize = secondSize.clamp(halfDivider, axisSize - halfDivider);
|
||||
|
||||
// Remove space from each child to place the divider in the middle.
|
||||
firstSize = firstSize - halfDivider;
|
||||
secondSize = secondSize - halfDivider;
|
||||
|
||||
void updateSpacing(DragUpdateDetails dragDetails) {
|
||||
final delta = isHorizontal ? dragDetails.delta.dx : dragDetails.delta.dy;
|
||||
final fractionalDelta = delta / axisSize;
|
||||
setState(() {
|
||||
// Update the fraction of space consumed by the children,
|
||||
// being sure not to allocate any negative space.
|
||||
firstFraction += fractionalDelta;
|
||||
firstFraction = firstFraction.clamp(0.0, 1.0);
|
||||
});
|
||||
}
|
||||
|
||||
// TODO(https://github.com/flutter/flutter/issues/43747): use an icon.
|
||||
// The material icon for a drag handle is not currently available.
|
||||
// For now, draw an indicator that is 3 lines running in the direction
|
||||
// of the main axis, like a hamburger menu.
|
||||
// TODO(https://github.com/flutter/devtools/issues/1265): update mouse
|
||||
// to indicate that this is resizable.
|
||||
final dragIndicator = Flex(
|
||||
direction: isHorizontal ? Axis.vertical : Axis.horizontal,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
for (var i = 0; i < min(crossAxisSize / 6.0, 3).floor(); i++)
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
vertical: isHorizontal ? 2.0 : 0.0,
|
||||
horizontal: isHorizontal ? 0.0 : 2.0,
|
||||
),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).dividerColor,
|
||||
borderRadius: BorderRadius.circular(Split.dividerMainAxisSize),
|
||||
),
|
||||
child: SizedBox(
|
||||
height: isHorizontal ? 2.0 : Split.dividerMainAxisSize - 2.0,
|
||||
width: isHorizontal ? Split.dividerMainAxisSize - 2.0 : 2.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
final children = [
|
||||
SizedBox(
|
||||
width: isHorizontal ? firstSize : width,
|
||||
height: isHorizontal ? height : firstSize,
|
||||
child: widget.firstChild,
|
||||
),
|
||||
GestureDetector(
|
||||
key: widget.dividerKey,
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onHorizontalDragUpdate: isHorizontal ? updateSpacing : null,
|
||||
onVerticalDragUpdate: isHorizontal ? null : updateSpacing,
|
||||
// DartStartBehavior.down is needed to keep the mouse pointer stuck to
|
||||
// the drag bar. There still appears to be a few frame lag before the
|
||||
// drag action triggers which is't ideal but isn't a launch blocker.
|
||||
dragStartBehavior: DragStartBehavior.down,
|
||||
child: SizedBox(
|
||||
width: isHorizontal ? Split.dividerMainAxisSize : width,
|
||||
height: isHorizontal ? height : Split.dividerMainAxisSize,
|
||||
child: Center(
|
||||
child: dragIndicator,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: isHorizontal ? secondSize : width,
|
||||
height: isHorizontal ? height : secondSize,
|
||||
child: widget.secondChild,
|
||||
),
|
||||
];
|
||||
return Flex(direction: widget.axis, children: children);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
// Copyright 2022 The Flutter team. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../../unsplash_access_key.dart';
|
||||
|
||||
final _unsplashHomepage =
|
||||
'https://unsplash.com/?utm_source=${Uri.encodeFull(unsplashAppName)}&utm_medium=referral';
|
||||
final _unsplashPrivacyPolicy =
|
||||
'https://unsplash.com/privacy?utm_source=${Uri.encodeFull(unsplashAppName)}&utm_medium=referral';
|
||||
|
||||
class UnsplashNotice extends StatefulWidget {
|
||||
const UnsplashNotice({Key? key, required this.child}) : super(key: key);
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
State<UnsplashNotice> createState() => _UnsplashNoticeState();
|
||||
}
|
||||
|
||||
class _UnsplashNoticeState extends State<UnsplashNotice> {
|
||||
bool noticeAccepted = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance!.addPostFrameCallback((timeStamp) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return _UnsplashDialog(accepted: () {
|
||||
setState(() {
|
||||
noticeAccepted = true;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return widget.child;
|
||||
}
|
||||
}
|
||||
|
||||
class _UnsplashDialog extends StatelessWidget {
|
||||
const _UnsplashDialog({Key? key, required this.accepted}) : super(key: key);
|
||||
final Function accepted;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ContentDialog(
|
||||
title: const Text('Unsplash Notice'),
|
||||
content: RichText(
|
||||
text: TextSpan(
|
||||
text: 'This is a sample desktop application provided by Google'
|
||||
' that enables you to search ',
|
||||
style: const TextStyle(color: Colors.grey),
|
||||
children: [
|
||||
TextSpan(
|
||||
text: 'Unsplash',
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () async {
|
||||
if (!await launch(_unsplashHomepage)) {
|
||||
throw 'Could not launch $_unsplashHomepage';
|
||||
}
|
||||
},
|
||||
style: TextStyle(color: Colors.blue),
|
||||
),
|
||||
const TextSpan(
|
||||
text: ' for photographs that interest you. When you search'
|
||||
' for and interact with photos, Unsplash will collect'
|
||||
' information about you and your use of the Unsplash'
|
||||
' services. Learn more about ',
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
TextSpan(
|
||||
text: 'how Unsplash collects and uses data',
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () async {
|
||||
if (!await launch(_unsplashPrivacyPolicy)) {
|
||||
throw 'Could not launch $_unsplashPrivacyPolicy';
|
||||
}
|
||||
},
|
||||
style: TextStyle(color: Colors.blue),
|
||||
),
|
||||
const TextSpan(
|
||||
text: '.',
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
]),
|
||||
),
|
||||
actions: [
|
||||
Button(
|
||||
child: const Text('Got it'),
|
||||
onPressed: () {
|
||||
accepted();
|
||||
Navigator.pop(context);
|
||||
})
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
// Copyright 2022 The Flutter team. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:file_selector/file_selector.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../model/photo_search_model.dart';
|
||||
import '../unsplash/photo.dart';
|
||||
import '../widgets/photo_details.dart';
|
||||
import '../widgets/split.dart';
|
||||
|
||||
class UnsplashSearchContent extends StatefulWidget {
|
||||
const UnsplashSearchContent({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<UnsplashSearchContent> createState() => _UnsplashSearchContentState();
|
||||
}
|
||||
|
||||
class _UnsplashSearchContentState extends State<UnsplashSearchContent> {
|
||||
final _treeViewScrollController = ScrollController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final photoSearchModel = Provider.of<PhotoSearchModel>(context);
|
||||
|
||||
return Split(
|
||||
axis: Axis.horizontal,
|
||||
initialFirstFraction: 0.4,
|
||||
firstChild: Scrollbar(
|
||||
controller: _treeViewScrollController,
|
||||
child: SingleChildScrollView(
|
||||
controller: _treeViewScrollController,
|
||||
child: TreeView(
|
||||
items: photoSearchModel.entries.map(_buildSearchEntry).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
secondChild: Center(
|
||||
child: photoSearchModel.selectedPhoto != null
|
||||
? PhotoDetails(
|
||||
photo: photoSearchModel.selectedPhoto!,
|
||||
onPhotoSave: (photo) async {
|
||||
final path = await getSavePath(
|
||||
suggestedName: '${photo.id}.jpg',
|
||||
acceptedTypeGroups: [
|
||||
XTypeGroup(
|
||||
label: 'JPG',
|
||||
extensions: ['jpg'],
|
||||
mimeTypes: ['image/jpeg'],
|
||||
),
|
||||
],
|
||||
);
|
||||
if (path != null) {
|
||||
final fileData =
|
||||
await photoSearchModel.download(photo: photo);
|
||||
final photoFile =
|
||||
XFile.fromData(fileData, mimeType: 'image/jpeg');
|
||||
await photoFile.saveTo(path);
|
||||
}
|
||||
},
|
||||
)
|
||||
: Container(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
TreeViewItem _buildSearchEntry(SearchEntry searchEntry) {
|
||||
void selectPhoto(Photo photo) {
|
||||
searchEntry.model.selectedPhoto = photo;
|
||||
}
|
||||
|
||||
String labelForPhoto(Photo photo) => 'Photo by ${photo.user!.name}';
|
||||
|
||||
return TreeViewItem(
|
||||
content: Text(searchEntry.query),
|
||||
children: searchEntry.photos
|
||||
.map<TreeViewItem>(
|
||||
(photo) => TreeViewItem(
|
||||
content: Semantics(
|
||||
button: true,
|
||||
onTap: () => selectPhoto(photo),
|
||||
label: labelForPhoto(photo),
|
||||
excludeSemantics: true,
|
||||
child: GestureDetector(
|
||||
onTap: () => selectPhoto(photo),
|
||||
child: Text(
|
||||
labelForPhoto(photo),
|
||||
style: const TextStyle(color: Colors.black),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user