info: metadata search
This commit is contained in:
parent
3a18f16d7c
commit
69349e2b2c
16 changed files with 431 additions and 196 deletions
|
@ -172,7 +172,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
List<Widget> _buildActions() {
|
List<Widget> _buildActions() {
|
||||||
return [
|
return [
|
||||||
if (collection.isBrowsing)
|
if (collection.isBrowsing)
|
||||||
SearchButton(
|
CollectionSearchButton(
|
||||||
source,
|
source,
|
||||||
parentCollection: collection,
|
parentCollection: collection,
|
||||||
),
|
),
|
||||||
|
@ -361,7 +361,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
SearchPageRoute(
|
SearchPageRoute(
|
||||||
delegate: ImageSearchDelegate(
|
delegate: CollectionSearchDelegate(
|
||||||
source: collection.source,
|
source: collection.source,
|
||||||
parentCollection: collection,
|
parentCollection: collection,
|
||||||
),
|
),
|
||||||
|
|
79
lib/widgets/common/basic/query_bar.dart
Normal file
79
lib/widgets/common/basic/query_bar.dart
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
import 'package:aves/theme/durations.dart';
|
||||||
|
import 'package:aves/theme/icons.dart';
|
||||||
|
import 'package:aves/utils/debouncer.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
class QueryBar extends StatefulWidget {
|
||||||
|
final ValueNotifier<String> filterNotifier;
|
||||||
|
|
||||||
|
const QueryBar({@required this.filterNotifier});
|
||||||
|
|
||||||
|
@override
|
||||||
|
_QueryBarState createState() => _QueryBarState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _QueryBarState extends State<QueryBar> {
|
||||||
|
final Debouncer _debouncer = Debouncer(delay: Durations.searchDebounceDelay);
|
||||||
|
TextEditingController _controller;
|
||||||
|
|
||||||
|
ValueNotifier<String> get filterNotifier => widget.filterNotifier;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = TextEditingController(text: filterNotifier.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final clearButton = IconButton(
|
||||||
|
icon: Icon(AIcons.clear),
|
||||||
|
onPressed: () {
|
||||||
|
_controller.clear();
|
||||||
|
filterNotifier.value = '';
|
||||||
|
},
|
||||||
|
tooltip: 'Clear',
|
||||||
|
);
|
||||||
|
|
||||||
|
return Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
controller: _controller,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
icon: Padding(
|
||||||
|
padding: EdgeInsetsDirectional.only(start: 16),
|
||||||
|
child: Icon(AIcons.search),
|
||||||
|
),
|
||||||
|
hintText: MaterialLocalizations.of(context).searchFieldLabel,
|
||||||
|
hintStyle: Theme.of(context).inputDecorationTheme.hintStyle,
|
||||||
|
),
|
||||||
|
textInputAction: TextInputAction.search,
|
||||||
|
onChanged: (s) => _debouncer(() => filterNotifier.value = s),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(minWidth: 16),
|
||||||
|
child: ValueListenableBuilder<TextEditingValue>(
|
||||||
|
valueListenable: _controller,
|
||||||
|
builder: (context, value, child) => AnimatedSwitcher(
|
||||||
|
duration: Durations.appBarActionChangeAnimation,
|
||||||
|
transitionBuilder: (child, animation) => FadeTransition(
|
||||||
|
opacity: animation,
|
||||||
|
child: SizeTransition(
|
||||||
|
axis: Axis.horizontal,
|
||||||
|
sizeFactor: animation,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: value.text.isNotEmpty ? clearButton : SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,13 +5,15 @@ import 'package:flutter/material.dart';
|
||||||
class AvesExpansionTile extends StatelessWidget {
|
class AvesExpansionTile extends StatelessWidget {
|
||||||
final String title;
|
final String title;
|
||||||
final Color color;
|
final Color color;
|
||||||
final List<Widget> children;
|
|
||||||
final ValueNotifier<String> expandedNotifier;
|
final ValueNotifier<String> expandedNotifier;
|
||||||
|
final bool initiallyExpanded;
|
||||||
|
final List<Widget> children;
|
||||||
|
|
||||||
const AvesExpansionTile({
|
const AvesExpansionTile({
|
||||||
@required this.title,
|
@required this.title,
|
||||||
this.color,
|
this.color,
|
||||||
this.expandedNotifier,
|
this.expandedNotifier,
|
||||||
|
this.initiallyExpanded = false,
|
||||||
@required this.children,
|
@required this.children,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -33,6 +35,9 @@ class AvesExpansionTile extends StatelessWidget {
|
||||||
enabled: enabled,
|
enabled: enabled,
|
||||||
),
|
),
|
||||||
expandable: enabled,
|
expandable: enabled,
|
||||||
|
initiallyExpanded: initiallyExpanded,
|
||||||
|
baseColor: Colors.grey[900],
|
||||||
|
expandedColor: Colors.grey[850],
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
@ -41,8 +46,6 @@ class AvesExpansionTile extends StatelessWidget {
|
||||||
if (enabled) ...children,
|
if (enabled) ...children,
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
baseColor: Colors.grey[900],
|
|
||||||
expandedColor: Colors.grey[850],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,10 +3,9 @@ import 'package:aves/model/filters/album.dart';
|
||||||
import 'package:aves/model/settings/settings.dart';
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/model/source/collection_source.dart';
|
import 'package:aves/model/source/collection_source.dart';
|
||||||
import 'package:aves/model/source/enums.dart';
|
import 'package:aves/model/source/enums.dart';
|
||||||
import 'package:aves/theme/durations.dart';
|
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
import 'package:aves/utils/debouncer.dart';
|
|
||||||
import 'package:aves/widgets/collection/empty.dart';
|
import 'package:aves/widgets/collection/empty.dart';
|
||||||
|
import 'package:aves/widgets/common/basic/query_bar.dart';
|
||||||
import 'package:aves/widgets/dialogs/create_album_dialog.dart';
|
import 'package:aves/widgets/dialogs/create_album_dialog.dart';
|
||||||
import 'package:aves/widgets/filter_grids/albums_page.dart';
|
import 'package:aves/widgets/filter_grids/albums_page.dart';
|
||||||
import 'package:aves/widgets/filter_grids/common/chip_set_action_delegate.dart';
|
import 'package:aves/widgets/filter_grids/common/chip_set_action_delegate.dart';
|
||||||
|
@ -116,84 +115,25 @@ class AlbumPickAppBar extends StatelessWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AlbumFilterBar extends StatefulWidget implements PreferredSizeWidget {
|
class AlbumFilterBar extends StatelessWidget implements PreferredSizeWidget {
|
||||||
final ValueNotifier<String> filterNotifier;
|
final ValueNotifier<String> filterNotifier;
|
||||||
|
|
||||||
static const preferredHeight = kToolbarHeight;
|
static const preferredHeight = kToolbarHeight;
|
||||||
|
|
||||||
const AlbumFilterBar({@required this.filterNotifier});
|
const AlbumFilterBar({
|
||||||
|
@required this.filterNotifier,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Size get preferredSize => Size.fromHeight(preferredHeight);
|
Size get preferredSize => Size.fromHeight(preferredHeight);
|
||||||
|
|
||||||
@override
|
|
||||||
_AlbumFilterBarState createState() => _AlbumFilterBarState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AlbumFilterBarState extends State<AlbumFilterBar> {
|
|
||||||
final Debouncer _debouncer = Debouncer(delay: Durations.searchDebounceDelay);
|
|
||||||
TextEditingController _controller;
|
|
||||||
|
|
||||||
ValueNotifier<String> get filterNotifier => widget.filterNotifier;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_controller = TextEditingController(text: filterNotifier.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final clearButton = IconButton(
|
|
||||||
icon: Icon(AIcons.clear),
|
|
||||||
onPressed: () {
|
|
||||||
_controller.clear();
|
|
||||||
filterNotifier.value = '';
|
|
||||||
},
|
|
||||||
tooltip: 'Clear',
|
|
||||||
);
|
|
||||||
return Container(
|
return Container(
|
||||||
height: AlbumFilterBar.preferredHeight,
|
height: AlbumFilterBar.preferredHeight,
|
||||||
alignment: Alignment.topCenter,
|
alignment: Alignment.topCenter,
|
||||||
child: Row(
|
child: QueryBar(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
filterNotifier: filterNotifier,
|
||||||
children: [
|
|
||||||
// Icon(AIcons.search),
|
|
||||||
Expanded(
|
|
||||||
child: TextField(
|
|
||||||
controller: _controller,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
icon: Padding(
|
|
||||||
padding: EdgeInsetsDirectional.only(start: 16),
|
|
||||||
child: Icon(AIcons.search),
|
|
||||||
),
|
|
||||||
// border: OutlineInputBorder(),
|
|
||||||
hintText: MaterialLocalizations.of(context).searchFieldLabel,
|
|
||||||
hintStyle: Theme.of(context).inputDecorationTheme.hintStyle,
|
|
||||||
),
|
|
||||||
textInputAction: TextInputAction.search,
|
|
||||||
onChanged: (s) => _debouncer(() => filterNotifier.value = s),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
ConstrainedBox(
|
|
||||||
constraints: BoxConstraints(minWidth: 16),
|
|
||||||
child: ValueListenableBuilder<TextEditingValue>(
|
|
||||||
valueListenable: _controller,
|
|
||||||
builder: (context, value, child) => AnimatedSwitcher(
|
|
||||||
duration: Durations.appBarActionChangeAnimation,
|
|
||||||
transitionBuilder: (child, animation) => FadeTransition(
|
|
||||||
opacity: animation,
|
|
||||||
child: SizeTransition(
|
|
||||||
axis: Axis.horizontal,
|
|
||||||
sizeFactor: animation,
|
|
||||||
child: child,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: value.text.isNotEmpty ? clearButton : SizedBox.shrink(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -104,7 +104,7 @@ class FilterNavigationPage<T extends CollectionFilter> extends StatelessWidget {
|
||||||
|
|
||||||
List<Widget> _buildActions(BuildContext context) {
|
List<Widget> _buildActions(BuildContext context) {
|
||||||
return [
|
return [
|
||||||
SearchButton(source),
|
CollectionSearchButton(source),
|
||||||
PopupMenuButton<ChipSetAction>(
|
PopupMenuButton<ChipSetAction>(
|
||||||
key: Key('appbar-menu-button'),
|
key: Key('appbar-menu-button'),
|
||||||
itemBuilder: (context) {
|
itemBuilder: (context) {
|
||||||
|
@ -137,7 +137,7 @@ class FilterNavigationPage<T extends CollectionFilter> extends StatelessWidget {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
SearchPageRoute(
|
SearchPageRoute(
|
||||||
delegate: ImageSearchDelegate(
|
delegate: CollectionSearchDelegate(
|
||||||
source: source,
|
source: source,
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
|
|
53
lib/widgets/fullscreen/info/info_app_bar.dart
Normal file
53
lib/widgets/fullscreen/info/info_app_bar.dart
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
import 'package:aves/model/image_entry.dart';
|
||||||
|
import 'package:aves/theme/icons.dart';
|
||||||
|
import 'package:aves/widgets/common/app_bar_title.dart';
|
||||||
|
import 'package:aves/widgets/fullscreen/info/info_search.dart';
|
||||||
|
import 'package:aves/widgets/fullscreen/info/metadata/metadata_section.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class InfoAppBar extends StatelessWidget {
|
||||||
|
final ImageEntry entry;
|
||||||
|
final ValueNotifier<Map<String, MetadataDirectory>> metadataNotifier;
|
||||||
|
final VoidCallback onBackPressed;
|
||||||
|
|
||||||
|
const InfoAppBar({
|
||||||
|
@required this.entry,
|
||||||
|
@required this.metadataNotifier,
|
||||||
|
@required this.onBackPressed,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SliverAppBar(
|
||||||
|
leading: IconButton(
|
||||||
|
key: Key('back-button'),
|
||||||
|
icon: Icon(AIcons.goUp),
|
||||||
|
onPressed: onBackPressed,
|
||||||
|
tooltip: 'Back to viewer',
|
||||||
|
),
|
||||||
|
title: TappableAppBarTitle(
|
||||||
|
onTap: () => _goToSearch(context),
|
||||||
|
child: Text('Info'),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(AIcons.search),
|
||||||
|
onPressed: () => _goToSearch(context),
|
||||||
|
tooltip: 'Search',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
titleSpacing: 0,
|
||||||
|
floating: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _goToSearch(BuildContext context) {
|
||||||
|
showSearch(
|
||||||
|
context: context,
|
||||||
|
delegate: InfoSearchDelegate(
|
||||||
|
entry: entry,
|
||||||
|
metadataNotifier: metadataNotifier,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,9 +2,9 @@ import 'package:aves/model/filters/filters.dart';
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/model/source/collection_lens.dart';
|
import 'package:aves/model/source/collection_lens.dart';
|
||||||
import 'package:aves/theme/durations.dart';
|
import 'package:aves/theme/durations.dart';
|
||||||
import 'package:aves/theme/icons.dart';
|
|
||||||
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
|
||||||
import 'package:aves/widgets/fullscreen/info/basic_section.dart';
|
import 'package:aves/widgets/fullscreen/info/basic_section.dart';
|
||||||
|
import 'package:aves/widgets/fullscreen/info/info_app_bar.dart';
|
||||||
import 'package:aves/widgets/fullscreen/info/location_section.dart';
|
import 'package:aves/widgets/fullscreen/info/location_section.dart';
|
||||||
import 'package:aves/widgets/fullscreen/info/metadata/metadata_section.dart';
|
import 'package:aves/widgets/fullscreen/info/metadata/metadata_section.dart';
|
||||||
import 'package:aves/widgets/fullscreen/info/notifications.dart';
|
import 'package:aves/widgets/fullscreen/info/notifications.dart';
|
||||||
|
@ -38,17 +38,6 @@ class InfoPageState extends State<InfoPage> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final appBar = SliverAppBar(
|
|
||||||
leading: IconButton(
|
|
||||||
key: Key('back-button'),
|
|
||||||
icon: Icon(AIcons.goUp),
|
|
||||||
onPressed: _goToImage,
|
|
||||||
tooltip: 'Back to viewer',
|
|
||||||
),
|
|
||||||
title: Text('Info'),
|
|
||||||
floating: true,
|
|
||||||
);
|
|
||||||
|
|
||||||
return MediaQueryDataProvider(
|
return MediaQueryDataProvider(
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
|
@ -68,9 +57,9 @@ class InfoPageState extends State<InfoPage> {
|
||||||
entry: entry,
|
entry: entry,
|
||||||
visibleNotifier: widget.visibleNotifier,
|
visibleNotifier: widget.visibleNotifier,
|
||||||
scrollController: _scrollController,
|
scrollController: _scrollController,
|
||||||
appBar: appBar,
|
|
||||||
split: mqWidth > 400,
|
split: mqWidth > 400,
|
||||||
mqViewInsetsBottom: mqViewInsetsBottom,
|
mqViewInsetsBottom: mqViewInsetsBottom,
|
||||||
|
goToViewer: _goToViewer,
|
||||||
)
|
)
|
||||||
: SizedBox.shrink();
|
: SizedBox.shrink();
|
||||||
},
|
},
|
||||||
|
@ -97,7 +86,7 @@ class InfoPageState extends State<InfoPage> {
|
||||||
_scrollStartFromTop = false;
|
_scrollStartFromTop = false;
|
||||||
} else if (notification is OverscrollNotification) {
|
} else if (notification is OverscrollNotification) {
|
||||||
if (notification.overscroll < 0) {
|
if (notification.overscroll < 0) {
|
||||||
_goToImage();
|
_goToViewer();
|
||||||
_scrollStartFromTop = false;
|
_scrollStartFromTop = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -106,7 +95,7 @@ class InfoPageState extends State<InfoPage> {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _goToImage() {
|
void _goToViewer() {
|
||||||
BackUpNotification().dispatch(context);
|
BackUpNotification().dispatch(context);
|
||||||
_scrollController.animateTo(
|
_scrollController.animateTo(
|
||||||
0,
|
0,
|
||||||
|
@ -121,9 +110,9 @@ class _InfoPageContent extends StatefulWidget {
|
||||||
final ImageEntry entry;
|
final ImageEntry entry;
|
||||||
final ValueNotifier<bool> visibleNotifier;
|
final ValueNotifier<bool> visibleNotifier;
|
||||||
final ScrollController scrollController;
|
final ScrollController scrollController;
|
||||||
final SliverAppBar appBar;
|
|
||||||
final bool split;
|
final bool split;
|
||||||
final double mqViewInsetsBottom;
|
final double mqViewInsetsBottom;
|
||||||
|
final VoidCallback goToViewer;
|
||||||
|
|
||||||
const _InfoPageContent({
|
const _InfoPageContent({
|
||||||
Key key,
|
Key key,
|
||||||
|
@ -131,9 +120,9 @@ class _InfoPageContent extends StatefulWidget {
|
||||||
@required this.entry,
|
@required this.entry,
|
||||||
@required this.visibleNotifier,
|
@required this.visibleNotifier,
|
||||||
@required this.scrollController,
|
@required this.scrollController,
|
||||||
@required this.appBar,
|
|
||||||
@required this.split,
|
@required this.split,
|
||||||
@required this.mqViewInsetsBottom,
|
@required this.mqViewInsetsBottom,
|
||||||
|
@required this.goToViewer,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -143,6 +132,8 @@ class _InfoPageContent extends StatefulWidget {
|
||||||
class _InfoPageContentState extends State<_InfoPageContent> {
|
class _InfoPageContentState extends State<_InfoPageContent> {
|
||||||
static const horizontalPadding = EdgeInsets.symmetric(horizontal: 8);
|
static const horizontalPadding = EdgeInsets.symmetric(horizontal: 8);
|
||||||
|
|
||||||
|
final ValueNotifier<Map<String, MetadataDirectory>> _metadataNotifier = ValueNotifier({});
|
||||||
|
|
||||||
CollectionLens get collection => widget.collection;
|
CollectionLens get collection => widget.collection;
|
||||||
|
|
||||||
ImageEntry get entry => widget.entry;
|
ImageEntry get entry => widget.entry;
|
||||||
|
@ -178,13 +169,18 @@ class _InfoPageContentState extends State<_InfoPageContent> {
|
||||||
);
|
);
|
||||||
final metadataSliver = MetadataSectionSliver(
|
final metadataSliver = MetadataSectionSliver(
|
||||||
entry: entry,
|
entry: entry,
|
||||||
|
metadataNotifier: _metadataNotifier,
|
||||||
visibleNotifier: widget.visibleNotifier,
|
visibleNotifier: widget.visibleNotifier,
|
||||||
);
|
);
|
||||||
|
|
||||||
return CustomScrollView(
|
return CustomScrollView(
|
||||||
controller: widget.scrollController,
|
controller: widget.scrollController,
|
||||||
slivers: [
|
slivers: [
|
||||||
widget.appBar,
|
InfoAppBar(
|
||||||
|
entry: entry,
|
||||||
|
metadataNotifier: _metadataNotifier,
|
||||||
|
onBackPressed: widget.goToViewer,
|
||||||
|
),
|
||||||
SliverPadding(
|
SliverPadding(
|
||||||
padding: horizontalPadding + EdgeInsets.only(top: 8),
|
padding: horizontalPadding + EdgeInsets.only(top: 8),
|
||||||
sliver: basicAndLocationSliver,
|
sliver: basicAndLocationSliver,
|
||||||
|
|
109
lib/widgets/fullscreen/info/info_search.dart
Normal file
109
lib/widgets/fullscreen/info/info_search.dart
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
import 'package:aves/model/image_entry.dart';
|
||||||
|
import 'package:aves/theme/icons.dart';
|
||||||
|
import 'package:aves/widgets/collection/empty.dart';
|
||||||
|
import 'package:aves/widgets/fullscreen/info/metadata/metadata_dir_tile.dart';
|
||||||
|
import 'package:aves/widgets/fullscreen/info/metadata/metadata_section.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class InfoSearchDelegate extends SearchDelegate {
|
||||||
|
final ImageEntry entry;
|
||||||
|
final ValueNotifier<Map<String, MetadataDirectory>> metadataNotifier;
|
||||||
|
|
||||||
|
Map<String, MetadataDirectory> get metadata => metadataNotifier.value;
|
||||||
|
|
||||||
|
static const suggestions = {
|
||||||
|
'Date & time': 'date or time or when',
|
||||||
|
'Description': 'abtract or description or comment',
|
||||||
|
'Dimensions': 'width or height or dimension or framesize',
|
||||||
|
'Resolution': 'resolution',
|
||||||
|
'Rights': 'rights or copyright or artist or creator or by-line or credit',
|
||||||
|
};
|
||||||
|
|
||||||
|
InfoSearchDelegate({
|
||||||
|
@required this.entry,
|
||||||
|
@required this.metadataNotifier,
|
||||||
|
}) : super(
|
||||||
|
searchFieldLabel: 'Search metadata',
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
ThemeData appBarTheme(BuildContext context) {
|
||||||
|
return Theme.of(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildLeading(BuildContext context) {
|
||||||
|
return IconButton(
|
||||||
|
icon: AnimatedIcon(
|
||||||
|
icon: AnimatedIcons.menu_arrow,
|
||||||
|
progress: transitionAnimation,
|
||||||
|
),
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
tooltip: MaterialLocalizations.of(context).backButtonTooltip,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Widget> buildActions(BuildContext context) {
|
||||||
|
return [
|
||||||
|
if (query.isNotEmpty)
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(AIcons.clear),
|
||||||
|
onPressed: () {
|
||||||
|
query = '';
|
||||||
|
showSuggestions(context);
|
||||||
|
},
|
||||||
|
tooltip: 'Clear',
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildSuggestions(BuildContext context) => ListView(
|
||||||
|
children: suggestions.entries
|
||||||
|
.map((kv) => ListTile(
|
||||||
|
title: Text(kv.key),
|
||||||
|
onTap: () {
|
||||||
|
query = kv.value;
|
||||||
|
showResults(context);
|
||||||
|
},
|
||||||
|
))
|
||||||
|
.toList(),
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildResults(BuildContext context) {
|
||||||
|
if (query.isEmpty) {
|
||||||
|
showSuggestions(context);
|
||||||
|
return SizedBox();
|
||||||
|
}
|
||||||
|
|
||||||
|
final effectiveQueries = query.toUpperCase().split(' OR ').map((query) => query.trim());
|
||||||
|
bool testKey(String key) => effectiveQueries.any(key.toUpperCase().contains);
|
||||||
|
final filteredMetadata = Map.fromEntries(metadata.entries.map((kv) {
|
||||||
|
final filteredDir = kv.value.filterKeys(testKey);
|
||||||
|
return MapEntry(kv.key, filteredDir);
|
||||||
|
}));
|
||||||
|
|
||||||
|
final tiles = filteredMetadata.entries
|
||||||
|
.where((kv) => kv.value.tags.isNotEmpty)
|
||||||
|
.map((kv) => MetadataDirTile(
|
||||||
|
entry: entry,
|
||||||
|
title: kv.key,
|
||||||
|
dir: kv.value,
|
||||||
|
initiallyExpanded: true,
|
||||||
|
showPrefixChildren: false,
|
||||||
|
))
|
||||||
|
.toList();
|
||||||
|
return tiles.isEmpty
|
||||||
|
? EmptyContent(
|
||||||
|
icon: AIcons.info,
|
||||||
|
text: 'No matching keys',
|
||||||
|
)
|
||||||
|
: ListView.builder(
|
||||||
|
padding: EdgeInsets.all(8),
|
||||||
|
itemBuilder: (context, index) => tiles[index],
|
||||||
|
itemCount: tiles.length,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
113
lib/widgets/fullscreen/info/metadata/metadata_dir_tile.dart
Normal file
113
lib/widgets/fullscreen/info/metadata/metadata_dir_tile.dart
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
import 'dart:collection';
|
||||||
|
|
||||||
|
import 'package:aves/model/image_entry.dart';
|
||||||
|
import 'package:aves/ref/brand_colors.dart';
|
||||||
|
import 'package:aves/services/svg_metadata_service.dart';
|
||||||
|
import 'package:aves/theme/icons.dart';
|
||||||
|
import 'package:aves/utils/color_utils.dart';
|
||||||
|
import 'package:aves/utils/constants.dart';
|
||||||
|
import 'package:aves/widgets/common/identity/aves_expansion_tile.dart';
|
||||||
|
import 'package:aves/widgets/fullscreen/info/common.dart';
|
||||||
|
import 'package:aves/widgets/fullscreen/info/metadata/metadata_section.dart';
|
||||||
|
import 'package:aves/widgets/fullscreen/info/metadata/metadata_thumbnail.dart';
|
||||||
|
import 'package:aves/widgets/fullscreen/info/metadata/xmp_tile.dart';
|
||||||
|
import 'package:aves/widgets/fullscreen/source_viewer_page.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class MetadataDirTile extends StatelessWidget {
|
||||||
|
final ImageEntry entry;
|
||||||
|
final String title;
|
||||||
|
final MetadataDirectory dir;
|
||||||
|
final ValueNotifier<String> expandedDirectoryNotifier;
|
||||||
|
final bool initiallyExpanded, showPrefixChildren;
|
||||||
|
|
||||||
|
const MetadataDirTile({
|
||||||
|
@required this.entry,
|
||||||
|
@required this.title,
|
||||||
|
@required this.dir,
|
||||||
|
this.expandedDirectoryNotifier,
|
||||||
|
this.initiallyExpanded = false,
|
||||||
|
this.showPrefixChildren = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final tags = dir.tags;
|
||||||
|
if (tags.isEmpty) return SizedBox.shrink();
|
||||||
|
|
||||||
|
final dirName = dir.name;
|
||||||
|
if (dirName == MetadataDirectory.xmpDirectory) {
|
||||||
|
return XmpDirTile(
|
||||||
|
entry: entry,
|
||||||
|
tags: tags,
|
||||||
|
expandedNotifier: expandedDirectoryNotifier,
|
||||||
|
initiallyExpanded: initiallyExpanded,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget thumbnail;
|
||||||
|
final prefixChildren = <Widget>[];
|
||||||
|
if (showPrefixChildren) {
|
||||||
|
switch (dirName) {
|
||||||
|
case MetadataDirectory.exifThumbnailDirectory:
|
||||||
|
thumbnail = MetadataThumbnails(source: MetadataThumbnailSource.exif, entry: entry);
|
||||||
|
break;
|
||||||
|
case MetadataDirectory.mediaDirectory:
|
||||||
|
thumbnail = MetadataThumbnails(source: MetadataThumbnailSource.embedded, entry: entry);
|
||||||
|
Widget builder(IconData data) => Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 4, horizontal: 8),
|
||||||
|
child: Icon(data),
|
||||||
|
);
|
||||||
|
if (tags['Has Video'] == 'yes') prefixChildren.add(builder(AIcons.video));
|
||||||
|
if (tags['Has Audio'] == 'yes') prefixChildren.add(builder(AIcons.audio));
|
||||||
|
if (tags['Has Image'] == 'yes') {
|
||||||
|
int count;
|
||||||
|
if (tags.containsKey('Image Count')) {
|
||||||
|
count = int.tryParse(tags['Image Count']);
|
||||||
|
}
|
||||||
|
prefixChildren.addAll(List.generate(count ?? 1, (i) => builder(AIcons.image)));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return AvesExpansionTile(
|
||||||
|
title: title,
|
||||||
|
color: BrandColors.get(dirName) ?? stringToColor(dirName),
|
||||||
|
expandedNotifier: expandedDirectoryNotifier,
|
||||||
|
initiallyExpanded: initiallyExpanded,
|
||||||
|
children: [
|
||||||
|
if (prefixChildren.isNotEmpty) Wrap(children: prefixChildren),
|
||||||
|
if (thumbnail != null) thumbnail,
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsets.only(left: 8, right: 8, bottom: 8),
|
||||||
|
child: InfoRowGroup(
|
||||||
|
tags,
|
||||||
|
maxValueLength: Constants.infoGroupMaxValueLength,
|
||||||
|
linkHandlers: dirName == SvgMetadataService.metadataDirectory ? getSvgLinkHandlers(tags) : null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, InfoLinkHandler> getSvgLinkHandlers(SplayTreeMap<String, String> tags) {
|
||||||
|
return {
|
||||||
|
'Metadata': InfoLinkHandler(
|
||||||
|
linkText: 'View XML',
|
||||||
|
onTap: (context) {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
settings: RouteSettings(name: SourceViewerPage.routeName),
|
||||||
|
builder: (context) => SourceViewerPage(
|
||||||
|
loader: () => SynchronousFuture(tags['Metadata']),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,18 +1,12 @@
|
||||||
import 'dart:collection';
|
import 'dart:collection';
|
||||||
|
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/ref/brand_colors.dart';
|
|
||||||
import 'package:aves/services/metadata_service.dart';
|
import 'package:aves/services/metadata_service.dart';
|
||||||
import 'package:aves/services/svg_metadata_service.dart';
|
import 'package:aves/services/svg_metadata_service.dart';
|
||||||
import 'package:aves/theme/durations.dart';
|
import 'package:aves/theme/durations.dart';
|
||||||
import 'package:aves/theme/icons.dart';
|
import 'package:aves/theme/icons.dart';
|
||||||
import 'package:aves/utils/color_utils.dart';
|
|
||||||
import 'package:aves/utils/constants.dart';
|
|
||||||
import 'package:aves/widgets/common/identity/aves_expansion_tile.dart';
|
|
||||||
import 'package:aves/widgets/fullscreen/info/common.dart';
|
import 'package:aves/widgets/fullscreen/info/common.dart';
|
||||||
import 'package:aves/widgets/fullscreen/info/metadata/metadata_thumbnail.dart';
|
import 'package:aves/widgets/fullscreen/info/metadata/metadata_dir_tile.dart';
|
||||||
import 'package:aves/widgets/fullscreen/info/metadata/xmp_tile.dart';
|
|
||||||
import 'package:aves/widgets/fullscreen/source_viewer_page.dart';
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
@ -21,10 +15,12 @@ import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
|
||||||
class MetadataSectionSliver extends StatefulWidget {
|
class MetadataSectionSliver extends StatefulWidget {
|
||||||
final ImageEntry entry;
|
final ImageEntry entry;
|
||||||
final ValueNotifier<bool> visibleNotifier;
|
final ValueNotifier<bool> visibleNotifier;
|
||||||
|
final ValueNotifier<Map<String, MetadataDirectory>> metadataNotifier;
|
||||||
|
|
||||||
const MetadataSectionSliver({
|
const MetadataSectionSliver({
|
||||||
@required this.entry,
|
@required this.entry,
|
||||||
@required this.visibleNotifier,
|
@required this.visibleNotifier,
|
||||||
|
@required this.metadataNotifier,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -32,7 +28,6 @@ class MetadataSectionSliver extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MetadataSectionSliverState extends State<MetadataSectionSliver> with AutomaticKeepAliveClientMixin {
|
class _MetadataSectionSliverState extends State<MetadataSectionSliver> with AutomaticKeepAliveClientMixin {
|
||||||
Map<String, _MetadataDirectory> _metadata = {};
|
|
||||||
final ValueNotifier<String> _loadedMetadataUri = ValueNotifier(null);
|
final ValueNotifier<String> _loadedMetadataUri = ValueNotifier(null);
|
||||||
final ValueNotifier<String> _expandedDirectoryNotifier = ValueNotifier(null);
|
final ValueNotifier<String> _expandedDirectoryNotifier = ValueNotifier(null);
|
||||||
|
|
||||||
|
@ -40,10 +35,9 @@ class _MetadataSectionSliverState extends State<MetadataSectionSliver> with Auto
|
||||||
|
|
||||||
bool get isVisible => widget.visibleNotifier.value;
|
bool get isVisible => widget.visibleNotifier.value;
|
||||||
|
|
||||||
// special directory names
|
ValueNotifier<Map<String, MetadataDirectory>> get metadataNotifier => widget.metadataNotifier;
|
||||||
static const exifThumbnailDirectory = 'Exif Thumbnail'; // from metadata-extractor
|
|
||||||
static const xmpDirectory = 'XMP'; // from metadata-extractor
|
Map<String, MetadataDirectory> get metadata => metadataNotifier.value;
|
||||||
static const mediaDirectory = 'Media'; // additional media (video/audio/images) directory
|
|
||||||
|
|
||||||
// directory names may contain the name of their parent directory
|
// directory names may contain the name of their parent directory
|
||||||
// if so, they are separated by this character
|
// if so, they are separated by this character
|
||||||
|
@ -53,6 +47,7 @@ class _MetadataSectionSliverState extends State<MetadataSectionSliver> with Auto
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_registerWidget(widget);
|
_registerWidget(widget);
|
||||||
|
metadataNotifier.value = {};
|
||||||
_getMetadata();
|
_getMetadata();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,7 +91,7 @@ class _MetadataSectionSliverState extends State<MetadataSectionSliver> with Auto
|
||||||
valueListenable: _loadedMetadataUri,
|
valueListenable: _loadedMetadataUri,
|
||||||
builder: (context, uri, child) {
|
builder: (context, uri, child) {
|
||||||
Widget content;
|
Widget content;
|
||||||
if (_metadata.isEmpty) {
|
if (metadata.isEmpty) {
|
||||||
content = SizedBox.shrink();
|
content = SizedBox.shrink();
|
||||||
} else {
|
} else {
|
||||||
content = Column(
|
content = Column(
|
||||||
|
@ -111,7 +106,12 @@ class _MetadataSectionSliverState extends State<MetadataSectionSliver> with Auto
|
||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
SectionRow(AIcons.info),
|
SectionRow(AIcons.info),
|
||||||
..._metadata.entries.map((kv) => _buildDirTile(kv.key, kv.value)),
|
...metadata.entries.map((kv) => MetadataDirTile(
|
||||||
|
entry: entry,
|
||||||
|
title: kv.key,
|
||||||
|
dir: kv.value,
|
||||||
|
expandedDirectoryNotifier: _expandedDirectoryNotifier,
|
||||||
|
)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -128,64 +128,9 @@ class _MetadataSectionSliverState extends State<MetadataSectionSliver> with Auto
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDirTile(String title, _MetadataDirectory dir) {
|
|
||||||
if (dir.tags.isEmpty) return SizedBox.shrink();
|
|
||||||
|
|
||||||
final dirName = dir.name;
|
|
||||||
if (dirName == xmpDirectory) {
|
|
||||||
return XmpDirTile(
|
|
||||||
entry: entry,
|
|
||||||
tags: dir.tags,
|
|
||||||
expandedNotifier: _expandedDirectoryNotifier,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget thumbnail;
|
|
||||||
final prefixChildren = <Widget>[];
|
|
||||||
switch (dirName) {
|
|
||||||
case exifThumbnailDirectory:
|
|
||||||
thumbnail = MetadataThumbnails(source: MetadataThumbnailSource.exif, entry: entry);
|
|
||||||
break;
|
|
||||||
case mediaDirectory:
|
|
||||||
thumbnail = MetadataThumbnails(source: MetadataThumbnailSource.embedded, entry: entry);
|
|
||||||
Widget builder(IconData data) => Padding(
|
|
||||||
padding: EdgeInsets.symmetric(vertical: 4, horizontal: 8),
|
|
||||||
child: Icon(data),
|
|
||||||
);
|
|
||||||
if (dir.tags['Has Video'] == 'yes') prefixChildren.add(builder(AIcons.video));
|
|
||||||
if (dir.tags['Has Audio'] == 'yes') prefixChildren.add(builder(AIcons.audio));
|
|
||||||
if (dir.tags['Has Image'] == 'yes') {
|
|
||||||
int count;
|
|
||||||
if (dir.tags.containsKey('Image Count')) {
|
|
||||||
count = int.tryParse(dir.tags['Image Count']);
|
|
||||||
}
|
|
||||||
prefixChildren.addAll(List.generate(count ?? 1, (i) => builder(AIcons.image)));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return AvesExpansionTile(
|
|
||||||
title: title,
|
|
||||||
color: BrandColors.get(dirName) ?? stringToColor(dirName),
|
|
||||||
expandedNotifier: _expandedDirectoryNotifier,
|
|
||||||
children: [
|
|
||||||
if (prefixChildren.isNotEmpty) Wrap(children: prefixChildren),
|
|
||||||
if (thumbnail != null) thumbnail,
|
|
||||||
Padding(
|
|
||||||
padding: EdgeInsets.only(left: 8, right: 8, bottom: 8),
|
|
||||||
child: InfoRowGroup(
|
|
||||||
dir.tags,
|
|
||||||
maxValueLength: Constants.infoGroupMaxValueLength,
|
|
||||||
linkHandlers: dirName == SvgMetadataService.metadataDirectory ? getSvgLinkHandlers(dir.tags) : null,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _onMetadataChanged() {
|
void _onMetadataChanged() {
|
||||||
_loadedMetadataUri.value = null;
|
_loadedMetadataUri.value = null;
|
||||||
_metadata = {};
|
metadataNotifier.value = {};
|
||||||
_getMetadata();
|
_getMetadata();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -211,7 +156,7 @@ class _MetadataSectionSliverState extends State<MetadataSectionSliver> with Auto
|
||||||
final tagName = tagKV.key as String ?? '';
|
final tagName = tagKV.key as String ?? '';
|
||||||
return MapEntry(tagName, value);
|
return MapEntry(tagName, value);
|
||||||
}).where((kv) => kv != null)));
|
}).where((kv) => kv != null)));
|
||||||
return _MetadataDirectory(directoryName, parent, tags);
|
return MetadataDirectory(directoryName, parent, tags);
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
final titledDirectories = directories.map((dir) {
|
final titledDirectories = directories.map((dir) {
|
||||||
|
@ -222,42 +167,36 @@ class _MetadataSectionSliverState extends State<MetadataSectionSliver> with Auto
|
||||||
return MapEntry(title, dir);
|
return MapEntry(title, dir);
|
||||||
}).toList()
|
}).toList()
|
||||||
..sort((a, b) => compareAsciiUpperCase(a.key, b.key));
|
..sort((a, b) => compareAsciiUpperCase(a.key, b.key));
|
||||||
_metadata = Map.fromEntries(titledDirectories);
|
metadataNotifier.value = Map.fromEntries(titledDirectories);
|
||||||
_loadedMetadataUri.value = entry.uri;
|
_loadedMetadataUri.value = entry.uri;
|
||||||
} else {
|
} else {
|
||||||
_metadata = {};
|
metadataNotifier.value = {};
|
||||||
_loadedMetadataUri.value = null;
|
_loadedMetadataUri.value = null;
|
||||||
}
|
}
|
||||||
_expandedDirectoryNotifier.value = null;
|
_expandedDirectoryNotifier.value = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Map<String, InfoLinkHandler> getSvgLinkHandlers(SplayTreeMap<String, String> tags) {
|
|
||||||
return {
|
|
||||||
'Metadata': InfoLinkHandler(
|
|
||||||
linkText: 'View XML',
|
|
||||||
onTap: (context) {
|
|
||||||
Navigator.push(
|
|
||||||
context,
|
|
||||||
MaterialPageRoute(
|
|
||||||
settings: RouteSettings(name: SourceViewerPage.routeName),
|
|
||||||
builder: (context) => SourceViewerPage(
|
|
||||||
loader: () => SynchronousFuture(tags['Metadata']),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool get wantKeepAlive => true;
|
bool get wantKeepAlive => true;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MetadataDirectory {
|
class MetadataDirectory {
|
||||||
final String name;
|
final String name;
|
||||||
final String parent;
|
final String parent;
|
||||||
|
final SplayTreeMap<String, String> allTags;
|
||||||
final SplayTreeMap<String, String> tags;
|
final SplayTreeMap<String, String> tags;
|
||||||
|
|
||||||
const _MetadataDirectory(this.name, this.parent, this.tags);
|
// special directory names
|
||||||
|
static const exifThumbnailDirectory = 'Exif Thumbnail'; // from metadata-extractor
|
||||||
|
static const xmpDirectory = 'XMP'; // from metadata-extractor
|
||||||
|
static const mediaDirectory = 'Media'; // additional media (video/audio/images) directory
|
||||||
|
|
||||||
|
const MetadataDirectory(this.name, this.parent, SplayTreeMap<String, String> allTags, {SplayTreeMap<String, String> tags})
|
||||||
|
: allTags = allTags,
|
||||||
|
tags = tags ?? allTags;
|
||||||
|
|
||||||
|
MetadataDirectory filterKeys(bool Function(String key) testKey) {
|
||||||
|
final filteredTags = SplayTreeMap.of(Map.fromEntries(allTags.entries.where((kv) => testKey(kv.key))));
|
||||||
|
return MetadataDirectory(name, parent, tags, tags: filteredTags);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,7 +89,7 @@ class _XmpStructArrayCardState extends State<XmpStructArrayCard> {
|
||||||
// without clipping the text
|
// without clipping the text
|
||||||
padding: EdgeInsets.only(left: 8, right: 8, bottom: 8),
|
padding: EdgeInsets.only(left: 8, right: 8, bottom: 8),
|
||||||
child: InfoRowGroup(
|
child: InfoRowGroup(
|
||||||
structs[_index],
|
structs[_index] ?? {},
|
||||||
maxValueLength: Constants.infoGroupMaxValueLength,
|
maxValueLength: Constants.infoGroupMaxValueLength,
|
||||||
linkHandlers: widget.linkifier?.call(_index + 1),
|
linkHandlers: widget.linkifier?.call(_index + 1),
|
||||||
),
|
),
|
||||||
|
|
|
@ -25,11 +25,13 @@ class XmpDirTile extends StatefulWidget {
|
||||||
final ImageEntry entry;
|
final ImageEntry entry;
|
||||||
final SplayTreeMap<String, String> tags;
|
final SplayTreeMap<String, String> tags;
|
||||||
final ValueNotifier<String> expandedNotifier;
|
final ValueNotifier<String> expandedNotifier;
|
||||||
|
final bool initiallyExpanded;
|
||||||
|
|
||||||
const XmpDirTile({
|
const XmpDirTile({
|
||||||
@required this.entry,
|
@required this.entry,
|
||||||
@required this.tags,
|
@required this.tags,
|
||||||
@required this.expandedNotifier,
|
@required this.expandedNotifier,
|
||||||
|
@required this.initiallyExpanded,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -76,6 +78,7 @@ class _XmpDirTileState extends State<XmpDirTile> with FeedbackMixin {
|
||||||
return AvesExpansionTile(
|
return AvesExpansionTile(
|
||||||
title: 'XMP',
|
title: 'XMP',
|
||||||
expandedNotifier: widget.expandedNotifier,
|
expandedNotifier: widget.expandedNotifier,
|
||||||
|
initiallyExpanded: widget.initiallyExpanded,
|
||||||
children: [
|
children: [
|
||||||
NotificationListener<OpenEmbeddedDataNotification>(
|
NotificationListener<OpenEmbeddedDataNotification>(
|
||||||
onNotification: (notification) {
|
onNotification: (notification) {
|
||||||
|
|
|
@ -142,7 +142,7 @@ class _HomePageState extends State<HomePage> {
|
||||||
);
|
);
|
||||||
case SearchPage.routeName:
|
case SearchPage.routeName:
|
||||||
return SearchPageRoute(
|
return SearchPageRoute(
|
||||||
delegate: ImageSearchDelegate(source: _mediaStore),
|
delegate: CollectionSearchDelegate(source: _mediaStore),
|
||||||
);
|
);
|
||||||
case CollectionPage.routeName:
|
case CollectionPage.routeName:
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -4,11 +4,11 @@ import 'package:aves/theme/icons.dart';
|
||||||
import 'package:aves/widgets/search/search_delegate.dart';
|
import 'package:aves/widgets/search/search_delegate.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class SearchButton extends StatelessWidget {
|
class CollectionSearchButton extends StatelessWidget {
|
||||||
final CollectionSource source;
|
final CollectionSource source;
|
||||||
final CollectionLens parentCollection;
|
final CollectionLens parentCollection;
|
||||||
|
|
||||||
const SearchButton(this.source, {this.parentCollection});
|
const CollectionSearchButton(this.source, {this.parentCollection});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
@ -24,7 +24,7 @@ class SearchButton extends StatelessWidget {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
SearchPageRoute(
|
SearchPageRoute(
|
||||||
delegate: ImageSearchDelegate(
|
delegate: CollectionSearchDelegate(
|
||||||
source: source,
|
source: source,
|
||||||
parentCollection: parentCollection,
|
parentCollection: parentCollection,
|
||||||
),
|
),
|
||||||
|
|
|
@ -20,14 +20,14 @@ import 'package:aves/widgets/search/search_page.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
class ImageSearchDelegate {
|
class CollectionSearchDelegate {
|
||||||
final CollectionSource source;
|
final CollectionSource source;
|
||||||
final CollectionLens parentCollection;
|
final CollectionLens parentCollection;
|
||||||
final ValueNotifier<String> expandedSectionNotifier = ValueNotifier(null);
|
final ValueNotifier<String> expandedSectionNotifier = ValueNotifier(null);
|
||||||
|
|
||||||
static const searchHistoryCount = 10;
|
static const searchHistoryCount = 10;
|
||||||
|
|
||||||
ImageSearchDelegate({@required this.source, this.parentCollection});
|
CollectionSearchDelegate({@required this.source, this.parentCollection});
|
||||||
|
|
||||||
ThemeData appBarTheme(BuildContext context) {
|
ThemeData appBarTheme(BuildContext context) {
|
||||||
return Theme.of(context);
|
return Theme.of(context);
|
||||||
|
@ -289,7 +289,7 @@ class SearchPageRoute<T> extends PageRoute<T> {
|
||||||
delegate.route = this;
|
delegate.route = this;
|
||||||
}
|
}
|
||||||
|
|
||||||
final ImageSearchDelegate delegate;
|
final CollectionSearchDelegate delegate;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Color get barrierColor => null;
|
Color get barrierColor => null;
|
||||||
|
|
|
@ -7,7 +7,7 @@ import 'package:flutter/material.dart';
|
||||||
class SearchPage extends StatefulWidget {
|
class SearchPage extends StatefulWidget {
|
||||||
static const routeName = '/search';
|
static const routeName = '/search';
|
||||||
|
|
||||||
final ImageSearchDelegate delegate;
|
final CollectionSearchDelegate delegate;
|
||||||
final Animation<double> animation;
|
final Animation<double> animation;
|
||||||
|
|
||||||
const SearchPage({
|
const SearchPage({
|
||||||
|
@ -115,7 +115,7 @@ class _SearchPageState extends State<SearchPage> {
|
||||||
onSubmitted: (_) => widget.delegate.showResults(context),
|
onSubmitted: (_) => widget.delegate.showResults(context),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
hintText: MaterialLocalizations.of(context).searchFieldLabel,
|
hintText: 'Search collection',
|
||||||
hintStyle: theme.inputDecorationTheme.hintStyle,
|
hintStyle: theme.inputDecorationTheme.hintStyle,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
Loading…
Reference in a new issue