mirror of
https://github.com/flutter/samples.git
synced 2025-11-10 14:58:34 +00:00
Use flutter_simple_treeview (#461)
This commit is contained in:
@@ -4,9 +4,10 @@
|
|||||||
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:file_chooser/file_chooser.dart' as file_chooser;
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:file_chooser/file_chooser.dart' as file_chooser;
|
import 'package:flutter_simple_treeview/flutter_simple_treeview.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:menubar/menubar.dart' as menubar;
|
import 'package:menubar/menubar.dart' as menubar;
|
||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
@@ -14,7 +15,6 @@ import 'package:provider/provider.dart';
|
|||||||
|
|
||||||
import 'src/model/photo_search_model.dart';
|
import 'src/model/photo_search_model.dart';
|
||||||
import 'src/unsplash/unsplash.dart';
|
import 'src/unsplash/unsplash.dart';
|
||||||
import 'src/widgets/data_tree.dart';
|
|
||||||
import 'src/widgets/photo_details.dart';
|
import 'src/widgets/photo_details.dart';
|
||||||
import 'src/widgets/photo_search_dialog.dart';
|
import 'src/widgets/photo_search_dialog.dart';
|
||||||
import 'src/widgets/split.dart';
|
import 'src/widgets/split.dart';
|
||||||
@@ -86,7 +86,16 @@ class UnsplashHomePage extends StatelessWidget {
|
|||||||
? Split(
|
? Split(
|
||||||
axis: Axis.horizontal,
|
axis: Axis.horizontal,
|
||||||
initialFirstFraction: 0.4,
|
initialFirstFraction: 0.4,
|
||||||
firstChild: DataTree(photoSearchModel.entries),
|
firstChild: Scrollbar(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
child: TreeView(
|
||||||
|
nodes: photoSearchModel.entries
|
||||||
|
.map(_buildSearchEntry)
|
||||||
|
.toList(),
|
||||||
|
indent: 0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
secondChild: Center(
|
secondChild: Center(
|
||||||
child: photoSearchModel.selectedPhoto != null
|
child: photoSearchModel.selectedPhoto != null
|
||||||
? PhotoDetails(
|
? PhotoDetails(
|
||||||
@@ -124,4 +133,31 @@ class UnsplashHomePage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TreeNode _buildSearchEntry(SearchEntry searchEntry) {
|
||||||
|
return TreeNode(
|
||||||
|
content: Expanded(
|
||||||
|
child: Text(searchEntry.query),
|
||||||
|
),
|
||||||
|
children: searchEntry.photos
|
||||||
|
.map<TreeNode>(
|
||||||
|
(photo) => TreeNode(
|
||||||
|
content: Expanded(
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () {
|
||||||
|
searchEntry.model.selectedPhoto = photo;
|
||||||
|
},
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(12.0),
|
||||||
|
child: Text(
|
||||||
|
'Photo by ${photo.user.name}',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,43 +9,24 @@ import 'package:meta/meta.dart';
|
|||||||
|
|
||||||
import '../unsplash/photo.dart';
|
import '../unsplash/photo.dart';
|
||||||
import '../unsplash/unsplash.dart';
|
import '../unsplash/unsplash.dart';
|
||||||
import '../widgets/data_tree.dart' show Entry;
|
|
||||||
import 'search.dart';
|
import 'search.dart';
|
||||||
|
|
||||||
class _PhotoEntry extends Entry {
|
class SearchEntry {
|
||||||
_PhotoEntry(this._photo, this._model) : super('Photo by ${_photo.user.name}');
|
const SearchEntry(this.query, this.photos, this.model);
|
||||||
|
final String query;
|
||||||
final Photo _photo;
|
final List<Photo> photos;
|
||||||
final PhotoSearchModel _model;
|
final PhotoSearchModel model;
|
||||||
|
|
||||||
@override
|
|
||||||
bool get isSelected => false;
|
|
||||||
|
|
||||||
@override
|
|
||||||
set isSelected(bool selected) {
|
|
||||||
_model._setSelectedPhoto(_photo);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _SearchEntry extends Entry {
|
|
||||||
_SearchEntry(String query, List<Photo> photos, PhotoSearchModel model)
|
|
||||||
: super(
|
|
||||||
query,
|
|
||||||
List<Entry>.unmodifiable(
|
|
||||||
photos.map<Entry>((photo) => _PhotoEntry(photo, model)),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class PhotoSearchModel extends ChangeNotifier {
|
class PhotoSearchModel extends ChangeNotifier {
|
||||||
PhotoSearchModel(this._client);
|
PhotoSearchModel(this._client);
|
||||||
final Unsplash _client;
|
final Unsplash _client;
|
||||||
|
|
||||||
List<Entry> get entries => List.unmodifiable(_entries);
|
List<SearchEntry> get entries => List.unmodifiable(_entries);
|
||||||
final List<Entry> _entries = <Entry>[];
|
final List<SearchEntry> _entries = [];
|
||||||
|
|
||||||
Photo get selectedPhoto => _selectedPhoto;
|
Photo get selectedPhoto => _selectedPhoto;
|
||||||
void _setSelectedPhoto(Photo photo) {
|
set selectedPhoto(Photo photo) {
|
||||||
_selectedPhoto = photo;
|
_selectedPhoto = photo;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
@@ -63,7 +44,7 @@ class PhotoSearchModel extends ChangeNotifier {
|
|||||||
..results.addAll(result.results);
|
..results.addAll(result.results);
|
||||||
});
|
});
|
||||||
|
|
||||||
_entries.add(_SearchEntry(query, search.results.toList(), this));
|
_entries.add(SearchEntry(query, search.results.toList(), this));
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
// Use of this source code is governed by a BSD-style license that can be
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:built_collection/built_collection.dart';
|
||||||
import 'package:built_value/serializer.dart';
|
import 'package:built_value/serializer.dart';
|
||||||
import 'package:built_value/standard_json_plugin.dart';
|
import 'package:built_value/standard_json_plugin.dart';
|
||||||
import 'package:built_collection/built_collection.dart';
|
|
||||||
|
|
||||||
import 'model/search.dart';
|
import 'model/search.dart';
|
||||||
import 'unsplash/api_error.dart';
|
import 'unsplash/api_error.dart';
|
||||||
|
|||||||
@@ -1,350 +0,0 @@
|
|||||||
// 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:flutter/material.dart';
|
|
||||||
import 'package:flutter/scheduler.dart';
|
|
||||||
|
|
||||||
class DataTreeRule extends StatelessWidget {
|
|
||||||
const DataTreeRule({Key key}) : super(key: key);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Container(
|
|
||||||
height: 2,
|
|
||||||
color: Theme.of(context).colorScheme.onBackground.withOpacity(0.1),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class DataTreeInkWell extends StatefulWidget {
|
|
||||||
const DataTreeInkWell({Key key, this.onTap, this.isSelected, this.child})
|
|
||||||
: super(key: key);
|
|
||||||
|
|
||||||
final VoidCallback onTap;
|
|
||||||
final bool isSelected;
|
|
||||||
final Widget child;
|
|
||||||
|
|
||||||
@override
|
|
||||||
_DataTreeInkWellState createState() => _DataTreeInkWellState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _DataTreeInkWellState extends State<DataTreeInkWell>
|
|
||||||
with SingleTickerProviderStateMixin {
|
|
||||||
AnimationController controller;
|
|
||||||
Animation selectionColor;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
controller = AnimationController(
|
|
||||||
duration: const Duration(milliseconds: 100), vsync: this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
controller.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void didUpdateWidget(DataTreeInkWell oldWidget) {
|
|
||||||
super.didUpdateWidget(oldWidget);
|
|
||||||
if (widget.isSelected) {
|
|
||||||
controller.forward();
|
|
||||||
} else {
|
|
||||||
controller.reverse();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final colorScheme = Theme.of(context).colorScheme;
|
|
||||||
|
|
||||||
final backgroundColor = controller
|
|
||||||
.drive(CurveTween(curve: Curves.fastOutSlowIn))
|
|
||||||
.drive(ColorTween(
|
|
||||||
begin: colorScheme.primary.withOpacity(0.0),
|
|
||||||
end: colorScheme.primary.withOpacity(0.08),
|
|
||||||
));
|
|
||||||
|
|
||||||
final iconColor = controller
|
|
||||||
.drive(CurveTween(curve: Curves.fastOutSlowIn))
|
|
||||||
.drive(ColorTween(
|
|
||||||
begin: colorScheme.onBackground.withOpacity(0.54),
|
|
||||||
end: colorScheme.onBackground.withOpacity(0.87),
|
|
||||||
));
|
|
||||||
|
|
||||||
return AnimatedBuilder(
|
|
||||||
animation: controller,
|
|
||||||
builder: (context, child) {
|
|
||||||
return IconTheme(
|
|
||||||
data: IconThemeData(color: iconColor.value),
|
|
||||||
child: Container(
|
|
||||||
color: backgroundColor.value,
|
|
||||||
child: child,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: InkWell(
|
|
||||||
onTap: widget.onTap,
|
|
||||||
splashFactory: InkRipple.splashFactory,
|
|
||||||
splashColor: colorScheme.primary.withOpacity(0.14),
|
|
||||||
highlightColor: Colors.transparent,
|
|
||||||
child: widget.child,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class DataTreeNode extends StatefulWidget {
|
|
||||||
const DataTreeNode({
|
|
||||||
Key key,
|
|
||||||
this.leading,
|
|
||||||
@required this.title,
|
|
||||||
this.backgroundColor,
|
|
||||||
this.onExpansionChanged,
|
|
||||||
this.onSelectionChanged,
|
|
||||||
this.children = const <Widget>[],
|
|
||||||
this.initiallyExpanded = false,
|
|
||||||
this.indent = 0,
|
|
||||||
this.height = 36,
|
|
||||||
}) : assert(initiallyExpanded != null),
|
|
||||||
assert(indent != null && indent >= 0),
|
|
||||||
super(key: key);
|
|
||||||
|
|
||||||
final Widget leading;
|
|
||||||
final Widget title;
|
|
||||||
final ValueChanged<bool> onExpansionChanged;
|
|
||||||
final ValueChanged<bool> onSelectionChanged;
|
|
||||||
final List<Widget> children;
|
|
||||||
final Color backgroundColor;
|
|
||||||
final bool initiallyExpanded;
|
|
||||||
final double indent;
|
|
||||||
final double height;
|
|
||||||
|
|
||||||
@override
|
|
||||||
_DataTreeNodeState createState() => _DataTreeNodeState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _DataTreeNodeState extends State<DataTreeNode>
|
|
||||||
with SingleTickerProviderStateMixin {
|
|
||||||
static final Animatable<double> _easeInTween =
|
|
||||||
CurveTween(curve: Curves.easeIn);
|
|
||||||
static final Animatable<double> _halfTween =
|
|
||||||
Tween<double>(begin: 0.0, end: 0.25);
|
|
||||||
|
|
||||||
AnimationController _controller;
|
|
||||||
Animation<double> _iconTurns;
|
|
||||||
Animation<double> _heightFactor;
|
|
||||||
|
|
||||||
bool _isExpanded = false;
|
|
||||||
bool _isSelected = false;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_controller = AnimationController(
|
|
||||||
duration: const Duration(milliseconds: 200), vsync: this);
|
|
||||||
_heightFactor = _controller.drive(_easeInTween);
|
|
||||||
_iconTurns = _controller.drive(_halfTween.chain(_easeInTween));
|
|
||||||
_isExpanded = (PageStorage.of(context)?.readState(context) ??
|
|
||||||
widget.initiallyExpanded) as bool;
|
|
||||||
if (_isExpanded) {
|
|
||||||
_controller.value = 1.0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_controller.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleNodeTap() {
|
|
||||||
setState(() {
|
|
||||||
_isExpanded = !_isExpanded;
|
|
||||||
if (_isExpanded) {
|
|
||||||
_controller.forward();
|
|
||||||
} else {
|
|
||||||
_controller.reverse().then<void>((value) {
|
|
||||||
if (!mounted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setState(() {
|
|
||||||
// Rebuild without widget.children.
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
PageStorage.of(context)?.writeState(context, _isExpanded);
|
|
||||||
});
|
|
||||||
if (widget.onExpansionChanged != null) {
|
|
||||||
widget.onExpansionChanged(_isExpanded);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void _handleLeafTap() {
|
|
||||||
_isSelected = !_isSelected;
|
|
||||||
if (widget.onSelectionChanged != null) {
|
|
||||||
widget.onSelectionChanged(_isSelected);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildChildren(BuildContext context, Widget child) {
|
|
||||||
return Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: <Widget>[
|
|
||||||
DataTreeInkWell(
|
|
||||||
onTap: _handleNodeTap,
|
|
||||||
isSelected: _isExpanded,
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
|
||||||
children: <Widget>[
|
|
||||||
SizedBox(width: widget.indent),
|
|
||||||
RotationTransition(
|
|
||||||
turns: _iconTurns,
|
|
||||||
child: Icon(
|
|
||||||
Icons.arrow_right,
|
|
||||||
size: 20,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Icon(
|
|
||||||
Icons.folder,
|
|
||||||
size: 20,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
widget.title,
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (child != null) // If child == null, then this DataNode is closed.
|
|
||||||
const DataTreeRule(),
|
|
||||||
if (child != null)
|
|
||||||
ClipRect(
|
|
||||||
child: Align(
|
|
||||||
alignment: Alignment.topLeft,
|
|
||||||
heightFactor: _heightFactor.value,
|
|
||||||
child: child,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final colorScheme = Theme.of(context).colorScheme;
|
|
||||||
final textColor = colorScheme.onBackground.withOpacity(0.87);
|
|
||||||
|
|
||||||
final closed = !_isExpanded && _controller.isDismissed;
|
|
||||||
|
|
||||||
return widget.children.isEmpty
|
|
||||||
// Leaf node.
|
|
||||||
? DataTreeInkWell(
|
|
||||||
onTap: _handleLeafTap,
|
|
||||||
isSelected: _isSelected,
|
|
||||||
child: Row(
|
|
||||||
children: <Widget>[
|
|
||||||
SizedBox(width: widget.indent),
|
|
||||||
Icon(
|
|
||||||
Icons.web_asset,
|
|
||||||
size: 20,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
DefaultTextStyle(
|
|
||||||
style: Theme.of(context).textTheme.bodyText2.copyWith(
|
|
||||||
color: textColor,
|
|
||||||
),
|
|
||||||
child: widget.title,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
// Not a leaf node.
|
|
||||||
: AnimatedBuilder(
|
|
||||||
animation: _controller.view,
|
|
||||||
builder: _buildChildren,
|
|
||||||
child: closed
|
|
||||||
? null
|
|
||||||
: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: <Widget>[
|
|
||||||
for (int index = 0;
|
|
||||||
index < (widget.children.length * 2) - 1;
|
|
||||||
index += 1)
|
|
||||||
(index % 2 == 1)
|
|
||||||
? const DataTreeRule()
|
|
||||||
: widget.children[index ~/ 2],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// One entry in the multilevel list displayed by this app.
|
|
||||||
class Entry {
|
|
||||||
Entry(this.title, [this.children = const <Entry>[]]);
|
|
||||||
final String title;
|
|
||||||
final List<Entry> children;
|
|
||||||
bool isSelected = false;
|
|
||||||
bool isEnabled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// A visualization of one Entry based on DataTreeNode.
|
|
||||||
class EntryItem extends StatefulWidget {
|
|
||||||
const EntryItem({Key key, this.entry}) : super(key: key);
|
|
||||||
|
|
||||||
final Entry entry;
|
|
||||||
|
|
||||||
@override
|
|
||||||
_EntryItemState createState() => _EntryItemState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _EntryItemState extends State<EntryItem> {
|
|
||||||
Widget _buildNodes(Entry root, double indent) {
|
|
||||||
return DataTreeNode(
|
|
||||||
key: PageStorageKey<Entry>(root),
|
|
||||||
onSelectionChanged: (isSelected) {
|
|
||||||
setState(() {
|
|
||||||
root.isSelected = isSelected;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
title: Container(
|
|
||||||
alignment: AlignmentDirectional.centerStart,
|
|
||||||
height: 36,
|
|
||||||
child: Text(root.title),
|
|
||||||
),
|
|
||||||
indent: indent,
|
|
||||||
children: root.children.map<Widget>((entry) {
|
|
||||||
return _buildNodes(entry, indent + 28);
|
|
||||||
}).toList(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return _buildNodes(widget.entry, 16);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class DataTree extends StatelessWidget {
|
|
||||||
DataTree(this.entries)
|
|
||||||
: assert(entries != null),
|
|
||||||
assert(entries.isNotEmpty);
|
|
||||||
final List<Entry> entries;
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) => ListView.builder(
|
|
||||||
itemCount: entries.length * 2 - 1,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
if (index % 2 == 1) {
|
|
||||||
return const DataTreeRule();
|
|
||||||
}
|
|
||||||
return EntryItem(
|
|
||||||
entry: entries[index ~/ 2],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -29,96 +29,101 @@ class PhotoDetails extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _PhotoDetailsState extends State<PhotoDetails>
|
class _PhotoDetailsState extends State<PhotoDetails>
|
||||||
with SingleTickerProviderStateMixin {
|
with TickerProviderStateMixin {
|
||||||
Widget _buildPhotoAttribution(BuildContext context) {
|
Widget _buildPhotoAttribution(BuildContext context) {
|
||||||
return RichText(
|
return Expanded(
|
||||||
text: TextSpan(
|
child: RichText(
|
||||||
style: Theme.of(context).textTheme.bodyText2,
|
overflow: TextOverflow.fade,
|
||||||
children: [
|
text: TextSpan(
|
||||||
const TextSpan(text: 'Photo by '),
|
style: Theme.of(context).textTheme.bodyText2,
|
||||||
TextSpan(
|
children: [
|
||||||
text: widget.photo.user.name,
|
const TextSpan(text: 'Photo by '),
|
||||||
style: const TextStyle(
|
TextSpan(
|
||||||
decoration: TextDecoration.underline,
|
text: widget.photo.user.name,
|
||||||
|
style: const TextStyle(
|
||||||
|
decoration: TextDecoration.underline,
|
||||||
|
),
|
||||||
|
recognizer: TapGestureRecognizer()
|
||||||
|
..onTap = () async {
|
||||||
|
final url = Uri.encodeFull(
|
||||||
|
'https://unsplash.com/@${widget.photo.user.username}?utm_source=$unsplashAppName&utm_medium=referral');
|
||||||
|
if (await url_launcher.canLaunch(url)) {
|
||||||
|
await url_launcher.launch(url);
|
||||||
|
}
|
||||||
|
},
|
||||||
),
|
),
|
||||||
recognizer: TapGestureRecognizer()
|
const TextSpan(text: ' on '),
|
||||||
..onTap = () async {
|
TextSpan(
|
||||||
final url = Uri.encodeFull(
|
text: 'Unsplash',
|
||||||
'https://unsplash.com/@${widget.photo.user.username}?utm_source=$unsplashAppName&utm_medium=referral');
|
style: const TextStyle(
|
||||||
if (await url_launcher.canLaunch(url)) {
|
decoration: TextDecoration.underline,
|
||||||
await url_launcher.launch(url);
|
),
|
||||||
}
|
recognizer: TapGestureRecognizer()
|
||||||
},
|
..onTap = () async {
|
||||||
),
|
if (await url_launcher.canLaunch(_unsplashHomepage)) {
|
||||||
const TextSpan(text: ' on '),
|
await url_launcher.launch(_unsplashHomepage);
|
||||||
TextSpan(
|
}
|
||||||
text: 'Unsplash',
|
},
|
||||||
style: const TextStyle(
|
|
||||||
decoration: TextDecoration.underline,
|
|
||||||
),
|
),
|
||||||
recognizer: TapGestureRecognizer()
|
],
|
||||||
..onTap = () async {
|
),
|
||||||
if (await url_launcher.canLaunch(_unsplashHomepage)) {
|
|
||||||
await url_launcher.launch(_unsplashHomepage);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SingleChildScrollView(
|
return Scrollbar(
|
||||||
child: Center(
|
child: SingleChildScrollView(
|
||||||
child: Column(
|
child: Center(
|
||||||
mainAxisSize: MainAxisSize.min,
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
const SizedBox(height: 16),
|
children: [
|
||||||
Card(
|
const SizedBox(height: 16),
|
||||||
shape: ContinuousRectangleBorder(
|
Card(
|
||||||
side: BorderSide(color: Colors.black12),
|
shape: ContinuousRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(4),
|
side: BorderSide(color: Colors.black12),
|
||||||
),
|
borderRadius: BorderRadius.circular(4),
|
||||||
child: AnimatedSize(
|
),
|
||||||
vsync: this,
|
child: AnimatedSize(
|
||||||
duration: Duration(milliseconds: 750),
|
vsync: this,
|
||||||
child: Padding(
|
duration: Duration(milliseconds: 750),
|
||||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 40),
|
child: Padding(
|
||||||
child: ConstrainedBox(
|
padding: const EdgeInsets.fromLTRB(16, 16, 16, 40),
|
||||||
constraints: BoxConstraints(
|
child: ConstrainedBox(
|
||||||
minWidth: 400,
|
constraints: BoxConstraints(
|
||||||
minHeight: 400,
|
minWidth: 400,
|
||||||
),
|
minHeight: 400,
|
||||||
child: FadeInImage.memoryNetwork(
|
),
|
||||||
placeholder: kTransparentImage,
|
child: FadeInImage.memoryNetwork(
|
||||||
image: widget.photo.urls.small,
|
placeholder: kTransparentImage,
|
||||||
|
image: widget.photo.urls.small,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 8),
|
||||||
const SizedBox(height: 8),
|
Padding(
|
||||||
Padding(
|
padding: const EdgeInsets.only(left: 4),
|
||||||
padding: const EdgeInsets.only(left: 4),
|
child: Row(
|
||||||
child: Row(
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisSize: MainAxisSize.min,
|
||||||
mainAxisSize: MainAxisSize.min,
|
children: [
|
||||||
children: [
|
_buildPhotoAttribution(context),
|
||||||
_buildPhotoAttribution(context),
|
const SizedBox(width: 8),
|
||||||
const SizedBox(width: 8),
|
IconButton(
|
||||||
IconButton(
|
visualDensity: VisualDensity.compact,
|
||||||
visualDensity: VisualDensity.compact,
|
icon: Icon(Icons.cloud_download),
|
||||||
icon: Icon(Icons.cloud_download),
|
onPressed: () => widget.onPhotoSave(widget.photo),
|
||||||
onPressed: () => widget.onPhotoSave(widget.photo),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 48),
|
||||||
const SizedBox(height: 48),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ SPEC CHECKSUMS:
|
|||||||
FlutterMacOS: 15bea8a44d2fa024068daa0140371c020b4b6ff9
|
FlutterMacOS: 15bea8a44d2fa024068daa0140371c020b4b6ff9
|
||||||
menubar: 4e3d461d62d775540277ce6639acafe2a111a231
|
menubar: 4e3d461d62d775540277ce6639acafe2a111a231
|
||||||
url_launcher: af78307ef9bafff91273b34f1c6c0c86a0004fd7
|
url_launcher: af78307ef9bafff91273b34f1c6c0c86a0004fd7
|
||||||
url_launcher_macos: 76867a28e24e0b6b98bfd65f157b64108e6d477a
|
url_launcher_macos: 45af3d61de06997666568a7149c1be98b41c95d4
|
||||||
|
|
||||||
PODFILE CHECKSUM: d8ba9b3e9e93c62c74a660b46c6fcb09f03991a7
|
PODFILE CHECKSUM: d8ba9b3e9e93c62c74a660b46c6fcb09f03991a7
|
||||||
|
|
||||||
COCOAPODS: 1.8.4
|
COCOAPODS: 1.9.1
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<Workspace
|
|
||||||
version = "1.0">
|
|
||||||
<FileRef
|
|
||||||
location = "self:/Users/stuartmorgan/src/embedder-opensource/flutter-desktop-embedding/example/macos/Runner.xcodeproj">
|
|
||||||
</FileRef>
|
|
||||||
</Workspace>
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Scheme
|
<Scheme
|
||||||
LastUpgradeVersion = "1120"
|
LastUpgradeVersion = "1150"
|
||||||
version = "1.3">
|
version = "1.3">
|
||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ packages:
|
|||||||
name: _fe_analyzer_shared
|
name: _fe_analyzer_shared
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.0"
|
version: "4.0.0"
|
||||||
analysis_server_lib:
|
analysis_server_lib:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -21,14 +21,7 @@ packages:
|
|||||||
name: analyzer
|
name: analyzer
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.39.8"
|
version: "0.39.10"
|
||||||
archive:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: archive
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.0.13"
|
|
||||||
args:
|
args:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -56,7 +49,7 @@ packages:
|
|||||||
name: build
|
name: build
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.2"
|
version: "1.3.0"
|
||||||
build_config:
|
build_config:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -77,21 +70,21 @@ packages:
|
|||||||
name: build_resolvers
|
name: build_resolvers
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.7"
|
version: "1.3.9"
|
||||||
build_runner:
|
build_runner:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description:
|
description:
|
||||||
name: build_runner
|
name: build_runner
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.0"
|
version: "1.10.0"
|
||||||
build_runner_core:
|
build_runner_core:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: build_runner_core
|
name: build_runner_core
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.1.0"
|
version: "5.2.0"
|
||||||
built_collection:
|
built_collection:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -134,13 +127,20 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.4"
|
version: "0.1.4"
|
||||||
|
clock:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: clock
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.1"
|
||||||
code_builder:
|
code_builder:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: code_builder
|
name: code_builder
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.2.1"
|
version: "3.3.0"
|
||||||
collection:
|
collection:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -161,14 +161,14 @@ packages:
|
|||||||
name: coverage
|
name: coverage
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.13.9"
|
version: "0.13.10"
|
||||||
crypto:
|
crypto:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: crypto
|
name: crypto
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.4"
|
version: "2.1.5"
|
||||||
csslib:
|
csslib:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -190,12 +190,19 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.6"
|
version: "1.3.6"
|
||||||
|
fake_async:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: fake_async
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.0"
|
||||||
file_chooser:
|
file_chooser:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: "plugins/file_chooser"
|
path: "plugins/file_chooser"
|
||||||
ref: HEAD
|
ref: HEAD
|
||||||
resolved-ref: "100bc306dd15532dbbb2b353170aea47e8173a13"
|
resolved-ref: b9c7e27ba16f86ff3390cfb0e460802e9980399a
|
||||||
url: "https://github.com/google/flutter-desktop-embedding.git"
|
url: "https://github.com/google/flutter-desktop-embedding.git"
|
||||||
source: git
|
source: git
|
||||||
version: "0.1.0"
|
version: "0.1.0"
|
||||||
@@ -211,6 +218,13 @@ packages:
|
|||||||
description: flutter
|
description: flutter
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
|
flutter_simple_treeview:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_simple_treeview
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.1"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
@@ -270,13 +284,6 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.4"
|
version: "3.1.4"
|
||||||
image:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: image
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "2.1.12"
|
|
||||||
intl:
|
intl:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -324,7 +331,7 @@ packages:
|
|||||||
description:
|
description:
|
||||||
path: "plugins/menubar"
|
path: "plugins/menubar"
|
||||||
ref: HEAD
|
ref: HEAD
|
||||||
resolved-ref: "100bc306dd15532dbbb2b353170aea47e8173a13"
|
resolved-ref: b9c7e27ba16f86ff3390cfb0e460802e9980399a
|
||||||
url: "https://github.com/google/flutter-desktop-embedding.git"
|
url: "https://github.com/google/flutter-desktop-embedding.git"
|
||||||
source: git
|
source: git
|
||||||
version: "0.0.1"
|
version: "0.0.1"
|
||||||
@@ -362,21 +369,21 @@ packages:
|
|||||||
name: node_interop
|
name: node_interop
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.3"
|
version: "1.1.1"
|
||||||
node_io:
|
node_io:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: node_io
|
name: node_io
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.0"
|
version: "1.1.1"
|
||||||
node_preamble:
|
node_preamble:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: node_preamble
|
name: node_preamble
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.8"
|
version: "1.4.10"
|
||||||
package_config:
|
package_config:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -390,7 +397,7 @@ packages:
|
|||||||
name: path
|
name: path
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.6.4"
|
version: "1.7.0"
|
||||||
pedantic:
|
pedantic:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -398,13 +405,13 @@ packages:
|
|||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.0"
|
version: "1.9.0"
|
||||||
petitparser:
|
platform_detect:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: petitparser
|
name: platform_detect
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.0"
|
version: "1.4.0"
|
||||||
plugin_platform_interface:
|
plugin_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -425,7 +432,7 @@ packages:
|
|||||||
name: provider
|
name: provider
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.1.0"
|
version: "4.1.2"
|
||||||
pub_semver:
|
pub_semver:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -549,21 +556,21 @@ packages:
|
|||||||
name: test
|
name: test
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.14.3"
|
version: "1.14.6"
|
||||||
test_api:
|
test_api:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.15"
|
version: "0.2.16"
|
||||||
test_core:
|
test_core:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_core
|
name: test_core
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.3.4"
|
version: "0.3.6"
|
||||||
timing:
|
timing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -598,28 +605,28 @@ packages:
|
|||||||
name: url_launcher
|
name: url_launcher
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.4.5"
|
version: "5.4.10"
|
||||||
url_launcher_macos:
|
url_launcher_macos:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_macos
|
name: url_launcher_macos
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.0.1+5"
|
version: "0.0.1+7"
|
||||||
url_launcher_platform_interface:
|
url_launcher_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_platform_interface
|
name: url_launcher_platform_interface
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.6"
|
version: "1.0.7"
|
||||||
url_launcher_web:
|
url_launcher_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_web
|
name: url_launcher_web
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.1+4"
|
version: "0.1.1+6"
|
||||||
uuid:
|
uuid:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -640,7 +647,7 @@ packages:
|
|||||||
name: vm_service
|
name: vm_service
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.0.2"
|
version: "4.0.4"
|
||||||
watcher:
|
watcher:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -661,14 +668,7 @@ packages:
|
|||||||
name: webkit_inspection_protocol
|
name: webkit_inspection_protocol
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.5.3"
|
version: "0.7.3"
|
||||||
xml:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: xml
|
|
||||||
url: "https://pub.dartlang.org"
|
|
||||||
source: hosted
|
|
||||||
version: "3.6.1"
|
|
||||||
yaml:
|
yaml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -677,5 +677,5 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.1"
|
version: "2.2.1"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=2.7.0 <3.0.0"
|
dart: ">=2.9.0-11.0.dev <3.0.0"
|
||||||
flutter: ">=1.17.0 <2.0.0"
|
flutter: ">=1.19.0-3.0.pre <2.0.0"
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ description: Search for Photos, using the Unsplash API.
|
|||||||
version: 1.0.0+1
|
version: 1.0.0+1
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^2.7.0-dev
|
sdk: ^2.9.0-11.0.dev
|
||||||
flutter: ^1.13.1-pre
|
flutter: ^1.19.0-3.0.pre
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
@@ -17,6 +17,7 @@ dependencies:
|
|||||||
url: https://github.com/google/flutter-desktop-embedding.git
|
url: https://github.com/google/flutter-desktop-embedding.git
|
||||||
path: plugins/file_chooser
|
path: plugins/file_chooser
|
||||||
logging: ^0.11.3+2
|
logging: ^0.11.3+2
|
||||||
|
flutter_simple_treeview: ^2.0.1
|
||||||
menubar:
|
menubar:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/google/flutter-desktop-embedding.git
|
url: https://github.com/google/flutter-desktop-embedding.git
|
||||||
|
|||||||
Reference in New Issue
Block a user