-
Notifications
You must be signed in to change notification settings - Fork 7.8k
Desktop photo search #174
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
domesticmouse
merged 12 commits into
flutter:master
from
domesticmouse:desktop_photo_search
Dec 10, 2019
Merged
Desktop photo search #174
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
8573cfe
Initial commit
domesticmouse 4e55616
Run through Xcode 11.2.1 to fix up config
domesticmouse 3e50764
Add flutter aware clean
domesticmouse 447e183
Merge branch 'master' of https://github.com/flutter/samples into desk…
domesticmouse 48fd90f
Code review fixups
domesticmouse d41de70
Refactor photo attrib, and animate image size.
domesticmouse d72a0ca
Pubspec versions update
domesticmouse 92d28c5
url_launcher_macos
domesticmouse 071d118
Simplifying naming of tests.
domesticmouse ec5b732
Taking out shared preferences
domesticmouse dd8df6b
Fix test naming
domesticmouse c77ce72
Handle invalid JSON
domesticmouse File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| # Miscellaneous | ||
| *.class | ||
| *.log | ||
| *.pyc | ||
| *.swp | ||
| .DS_Store | ||
| .atom/ | ||
| .buildlog/ | ||
| .history | ||
| .svn/ | ||
|
|
||
| # IntelliJ related | ||
| *.iml | ||
| *.ipr | ||
| *.iws | ||
| .idea/ | ||
|
|
||
| # The .vscode folder contains launch configuration and tasks you configure in | ||
| # VS Code which you may wish to be included in version control, so this line | ||
| # is commented out by default. | ||
| #.vscode/ | ||
|
|
||
| # Flutter/Dart/Pub related | ||
| **/doc/api/ | ||
| .dart_tool/ | ||
| .flutter-plugins | ||
| .flutter-plugins-dependencies | ||
| .packages | ||
| .pub-cache/ | ||
| .pub/ | ||
| /build/ | ||
|
|
||
| # Web related | ||
| lib/generated_plugin_registrant.dart | ||
|
|
||
| # Exceptions to above rules. | ||
| !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| # This file tracks properties of this Flutter project. | ||
| # Used by Flutter tool to assess capabilities and perform upgrades etc. | ||
| # | ||
| # This file should be version controlled and should not be manually edited. | ||
|
|
||
| version: | ||
| revision: 7caef218b55acac803aa67e7393e97f75ec70c5c | ||
| channel: master | ||
|
|
||
| project_type: app |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| # Photo Search app | ||
|
|
||
| This macOS application enables you to search [Unsplash](https://unsplash.com/) | ||
| for photographs that interest you. To use it, you need to add an | ||
| **Access Key** from [Unsplash API](https://unsplash.com/developers) to | ||
| [unsplash_access_key.dart](lib/unsplash_access_key.dart). | ||
|
|
||
| ## A quick tour of the code | ||
|
|
||
| This Flutter project builds a desktop application. It utilises the following | ||
| desktop specific plugins: | ||
|
|
||
| - [file_choser] to enable the application user to select where to save a photo | ||
| from the Unsplash API. | ||
| - [menubar] for exposing Image Search functionality through the menu bar. | ||
| - [url_launcher_fde] and [url_launcher] plugin, which are used to open external links. | ||
|
|
||
| The Unsplash API client entry point is in the [Unsplash] class, and is built | ||
| atop [http], [built_value] and [built_collection] for JSON Rest API access. | ||
|
|
||
| The [DataTreeNode] and widget family, along with the [Split] widget capture | ||
| desktop file explorer master/detail view idioms. | ||
|
|
||
| ## macOS Network and File entitlements | ||
|
|
||
| To access the network, macOS requires applications enable the | ||
| [com.apple.security.network.client entitlement][macOS-client]. For this | ||
| sample, this entitlement is required to access the Unsplash API. | ||
|
|
||
| Likewise, to save a Photo to the local file system using the `file_choser` plugin requires the | ||
| [com.apple.security.files.user-selected.read-write entitlement][macOS-read-write]. | ||
|
|
||
| Please see [macOS Signing and Security][macOS-security] for more detail. | ||
|
|
||
| ## Flutter Desktop is not in Flutter Stable Release Channel | ||
|
|
||
| This sample is an initial preview, intended to enable developers to preview what is | ||
| under development. As such, it is currently only available for use on the `master` channel | ||
| of Flutter. Please see [Flutter build release channels][flutter_channels] for more detail, | ||
| and how to switch between Flutter release channels. | ||
|
|
||
| [DataTreeNode]: lib/src/widgets/data_tree.dart | ||
| [Split]: lib/src/widgets/split.dart | ||
| [Unsplash]: lib/src/unsplash/unsplash.dart | ||
|
|
||
| [built_collection]: https://pub.dev/packages/built_collection | ||
| [built_value]: https://pub.dev/packages/built_value | ||
| [file_choser]: https://github.com/google/flutter-desktop-embedding/tree/master/plugins/file_chooser | ||
| [flutter_channels]: https://github.com/flutter/flutter/wiki/Flutter-build-release-channels | ||
| [http]: https://pub.dev/packages/http | ||
| [macOS-client]: https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_network_client | ||
| [macOS-read-write]: https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_files_user-selected_read-write | ||
| [macOS-security]: https://github.com/google/flutter-desktop-embedding/blob/master/macOS-Security.md | ||
| [menubar]: https://github.com/google/flutter-desktop-embedding/tree/master/plugins/menubar | ||
| [url_launcher]: https://pub.dev/packages/url_launcher | ||
| [url_launcher_fde]: https://github.com/google/flutter-desktop-embedding/tree/master/plugins/flutter_plugins/url_launcher_fde |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| include: package:pedantic/analysis_options.1.8.0.yaml | ||
|
|
||
| analyzer: | ||
| strong-mode: | ||
| implicit-casts: false | ||
| implicit-dynamic: false | ||
|
|
||
| linter: | ||
| rules: | ||
| - avoid_types_on_closure_parameters | ||
| - avoid_void_async | ||
| - await_only_futures | ||
| - camel_case_types | ||
| - cancel_subscriptions | ||
| - close_sinks | ||
| - constant_identifier_names | ||
| - control_flow_in_finally | ||
| - empty_statements | ||
| - hash_and_equals | ||
| - implementation_imports | ||
| - non_constant_identifier_names | ||
| - package_api_docs | ||
| - package_names | ||
| - package_prefixed_library_names | ||
| - test_types_in_equals | ||
| - throw_in_finally | ||
| - unnecessary_brace_in_string_interps | ||
| - unnecessary_getters_setters | ||
| - unnecessary_new | ||
| - unnecessary_statements |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| targets: | ||
| $default: | ||
| builders: | ||
| built_value_generator|built_value: | ||
| enabled: true | ||
|
|
||
| builders: | ||
| built_value: | ||
| target: ":built_value_generator" | ||
| import: "package:built_value_generator/builder.dart" | ||
| builder_factories: ["builtValue"] | ||
| build_extensions: {".dart": [".built_value.g.part"]} | ||
| auto_apply: dependents | ||
| build_to: cache | ||
| applies_builders: ["source_gen|combining_builder"] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,122 @@ | ||
| // 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 'dart:io'; | ||
|
|
||
| import 'package:flutter/foundation.dart'; | ||
| import 'package:flutter/material.dart'; | ||
| import 'package:file_chooser/file_chooser.dart' as file_choser; | ||
| import 'package:logging/logging.dart'; | ||
| import 'package:menubar/menubar.dart' as menubar; | ||
| import 'package:meta/meta.dart'; | ||
| import 'package:provider/provider.dart'; | ||
|
|
||
| import 'src/model/photo_search_model.dart'; | ||
| import 'src/unsplash/unsplash.dart'; | ||
| import 'src/widgets/data_tree.dart'; | ||
| import 'src/widgets/photo_details.dart'; | ||
| import 'src/widgets/photo_search_dialog.dart'; | ||
| import 'src/widgets/split.dart'; | ||
| import 'unsplash_access_key.dart'; | ||
|
|
||
| void main() { | ||
| Logger.root.level = Level.ALL; | ||
| Logger.root.onRecord.listen((rec) { | ||
| // ignore: avoid_print | ||
| print('${rec.loggerName} ${rec.level.name}: ${rec.time}: ${rec.message}'); | ||
| }); | ||
|
|
||
| if (unsplashAccessKey.isEmpty) { | ||
| Logger('main').severe('Unsplash Access Key is required. ' | ||
| 'Please add to `lib/unsplash_access_key.dart`.'); | ||
| exit(1); | ||
| } | ||
|
|
||
| runApp( | ||
| ChangeNotifierProvider<PhotoSearchModel>( | ||
| create: (context) => PhotoSearchModel( | ||
| Unsplash(accessKey: unsplashAccessKey), | ||
| ), | ||
| child: UnsplashSearchApp(), | ||
| ), | ||
| ); | ||
| } | ||
|
|
||
| class UnsplashSearchApp extends StatelessWidget { | ||
| @override | ||
| Widget build(BuildContext context) { | ||
| return MaterialApp( | ||
| title: 'Photo Search', | ||
| theme: ThemeData( | ||
| primarySwatch: Colors.orange, | ||
| ), | ||
| home: const UnsplashHomePage(title: 'Photo Search'), | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| class UnsplashHomePage extends StatelessWidget { | ||
| const UnsplashHomePage({@required this.title}); | ||
| final String title; | ||
|
|
||
| @override | ||
| Widget build(BuildContext context) { | ||
| final photoSearchModel = Provider.of<PhotoSearchModel>(context); | ||
| menubar.setApplicationMenu([ | ||
| menubar.Submenu(label: 'Search', children: [ | ||
| menubar.MenuItem( | ||
| label: 'Search ...', | ||
| onClicked: () { | ||
| showDialog<void>( | ||
| context: context, | ||
| builder: (context) => | ||
| PhotoSearchDialog(photoSearchModel.addSearch), | ||
| ); | ||
| }, | ||
| ), | ||
| ]) | ||
| ]); | ||
|
|
||
| return Scaffold( | ||
| appBar: AppBar( | ||
| title: Text(title), | ||
| ), | ||
| body: photoSearchModel.entries.isNotEmpty | ||
redbrogdon marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ? Split( | ||
| axis: Axis.horizontal, | ||
| initialFirstFraction: 0.4, | ||
| firstChild: DataTree(photoSearchModel.entries), | ||
| secondChild: Center( | ||
| child: photoSearchModel.selectedPhoto != null | ||
| ? PhotoDetails( | ||
| photo: photoSearchModel.selectedPhoto, | ||
| onPhotoSave: (photo) async { | ||
| final result = await file_choser.showSavePanel( | ||
| suggestedFileName: '${photo.id}.jpg', | ||
| allowedFileTypes: ['jpg'], | ||
| ); | ||
| if (!result.canceled) { | ||
| final bytes = | ||
| await photoSearchModel.download(photo: photo); | ||
| await File(result.paths[0]).writeAsBytes(bytes); | ||
| } | ||
| }, | ||
| ) | ||
| : Container(), | ||
| ), | ||
| ) | ||
| : const Center( | ||
| child: Text('Search for Photos using the Fab button'), | ||
| ), | ||
| floatingActionButton: FloatingActionButton( | ||
| onPressed: () => showDialog<void>( | ||
| context: context, | ||
| builder: (context) => PhotoSearchDialog(photoSearchModel.addSearch), | ||
| ), | ||
| tooltip: 'Search for a photo', | ||
| child: Icon(Icons.search), | ||
| ), | ||
| ); | ||
| } | ||
| } | ||
72 changes: 72 additions & 0 deletions
72
experimental/desktop_photo_search/lib/src/model/photo_search_model.dart
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| // 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 'dart:typed_data'; | ||
|
|
||
| import 'package:flutter/foundation.dart'; | ||
| import 'package:meta/meta.dart'; | ||
|
|
||
| import '../unsplash/photo.dart'; | ||
| import '../unsplash/unsplash.dart'; | ||
| import '../widgets/data_tree.dart' show Entry; | ||
| import 'search.dart'; | ||
|
|
||
| class _PhotoEntry extends Entry { | ||
| _PhotoEntry(this._photo, this._model) : super('Photo by ${_photo.user.name}'); | ||
|
|
||
| final Photo _photo; | ||
| final PhotoSearchModel _model; | ||
|
|
||
| @override | ||
| bool get isSelected => false; | ||
|
|
||
| @override | ||
redbrogdon marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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 { | ||
| PhotoSearchModel(this._client); | ||
| final Unsplash _client; | ||
|
|
||
| List<Entry> get entries => List.unmodifiable(_entries); | ||
| final List<Entry> _entries = <Entry>[]; | ||
|
|
||
| Photo get selectedPhoto => _selectedPhoto; | ||
| void _setSelectedPhoto(Photo photo) { | ||
| _selectedPhoto = photo; | ||
| notifyListeners(); | ||
| } | ||
|
|
||
| Photo _selectedPhoto; | ||
|
|
||
| Future<void> addSearch(String query) async { | ||
| final result = await _client.searchPhotos( | ||
| query: query, | ||
| orientation: SearchPhotosOrientation.portrait, | ||
| ); | ||
| final search = Search((s) { | ||
| s | ||
| ..query = query | ||
| ..results.addAll(result.results); | ||
| }); | ||
|
|
||
| _entries.add(_SearchEntry(query, search.results.toList(), this)); | ||
| notifyListeners(); | ||
| } | ||
|
|
||
| Future<Uint8List> download({@required Photo photo}) => | ||
| _client.download(photo); | ||
| } | ||
36 changes: 36 additions & 0 deletions
36
experimental/desktop_photo_search/lib/src/model/search.dart
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| // 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 'dart:convert'; | ||
|
|
||
| import 'package:built_collection/built_collection.dart'; | ||
| import 'package:built_value/built_value.dart'; | ||
| import 'package:built_value/serializer.dart'; | ||
|
|
||
| import '../serializers.dart'; | ||
| import '../unsplash/photo.dart'; | ||
|
|
||
| part 'search.g.dart'; | ||
|
|
||
| abstract class Search implements Built<Search, SearchBuilder> { | ||
| factory Search([void Function(SearchBuilder) updates]) = _$Search; | ||
| Search._(); | ||
|
|
||
| @BuiltValueField(wireName: 'query') | ||
| String get query; | ||
|
|
||
| @BuiltValueField(wireName: 'results') | ||
| BuiltList<Photo> get results; | ||
|
|
||
| String toJson() { | ||
| return json.encode(serializers.serializeWith(Search.serializer, this)); | ||
| } | ||
|
|
||
| static Search fromJson(String jsonString) { | ||
| return serializers.deserializeWith( | ||
| Search.serializer, json.decode(jsonString)); | ||
| } | ||
|
|
||
| static Serializer<Search> get serializer => _$searchSerializer; | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this something that could/should be moved out of the build method? The fact that the widget's being built doesn't necessarily imply it's actively on the screen.
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 menu action has an implicit linkage to this window through the callback's construction of a dialog in the window. The only way I can see to make this work is for this code to be run during build, so that it has access to the build context for
showDialog. Happy to take guidance if my understanding of this constraint is incorrect.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.
You're right, the need for a context weirds things up. I was mostly operating on my "build methods should avoid side effects wherever possible" instincts.
This reminds me of a situation I went to Hans for advice on: I wanted a network refresh for some data to be kicked off whenever the app navigated to a widget (DetailsScreen, I think it was). I had the update call initiating in a build method, but IIRC Hans suggested having my own Route object to do it.
At the moment, I don't have any real advice to offer, so stick with what you've got. I'm curious now, though, and I may ask Hans about it next week. 😄
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 fun part is that the menu will change depending on which window is selected for input in the user's window stack, which is independent of the repaint cycle.