use Provider/Selector for MediaQueryData

This commit is contained in:
Thibault Deckers 2019-12-23 18:13:09 +09:00
parent 65b51c7e83
commit 4761e16208
23 changed files with 569 additions and 434 deletions

View file

@ -8,6 +8,7 @@ import 'package:aves/widgets/album/all_collection_drawer.dart';
import 'package:aves/widgets/album/all_collection_page.dart'; import 'package:aves/widgets/album/all_collection_page.dart';
import 'package:aves/widgets/common/fake_app_bar.dart'; import 'package:aves/widgets/common/fake_app_bar.dart';
import 'package:aves/widgets/common/icons.dart'; import 'package:aves/widgets/common/icons.dart';
import 'package:aves/widgets/common/media_query_data_provider.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_native_timezone/flutter_native_timezone.dart'; import 'package:flutter_native_timezone/flutter_native_timezone.dart';
@ -73,7 +74,9 @@ class _HomePageState extends State<HomePage> {
Future<void> setup() async { Future<void> setup() async {
debugPrint('$runtimeType setup start, elapsed=${stopwatch.elapsed}'); debugPrint('$runtimeType setup start, elapsed=${stopwatch.elapsed}');
// TODO reduce permission check time // TODO reduce permission check time
final permissions = await PermissionHandler().requestPermissions([PermissionGroup.storage]); // 350ms final permissions = await PermissionHandler().requestPermissions([
PermissionGroup.storage
]); // 350ms
if (permissions[PermissionGroup.storage] != PermissionStatus.granted) { if (permissions[PermissionGroup.storage] != PermissionStatus.granted) {
unawaited(SystemNavigator.pop()); unawaited(SystemNavigator.pop());
return; return;
@ -123,12 +126,14 @@ class _HomePageState extends State<HomePage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return MediaQueryDataProvider(
// fake app bar so that content is safe from status bar, even though we use a SliverAppBar child: Scaffold(
appBar: FakeAppBar(), // fake app bar so that content is safe from status bar, even though we use a SliverAppBar
body: AllCollectionPage(collection: localMediaCollection), appBar: FakeAppBar(),
drawer: AllCollectionDrawer(collection: localMediaCollection), body: AllCollectionPage(collection: localMediaCollection),
resizeToAvoidBottomInset: false, drawer: AllCollectionDrawer(collection: localMediaCollection),
resizeToAvoidBottomInset: false,
),
); );
} }
} }

View file

@ -59,7 +59,9 @@ class ImageCollection with ChangeNotifier {
} }
break; break;
case SortFactor.size: case SortFactor.size:
sections = Map.fromEntries([MapEntry('All', _rawEntries)]); sections = Map.fromEntries([
MapEntry('All', _rawEntries)
]);
break; break;
} }
notifyListeners(); notifyListeners();

View file

@ -181,7 +181,11 @@ class ImageEntry {
// admin area examples: Seoul, Geneva, null // admin area examples: Seoul, Geneva, null
// locality examples: Mapo-gu, Geneva, Annecy // locality examples: Mapo-gu, Geneva, Annecy
return LinkedHashSet.of( return LinkedHashSet.of(
[addressDetails.countryName, addressDetails.adminArea, addressDetails.locality], [
addressDetails.countryName,
addressDetails.adminArea,
addressDetails.locality
],
).where((part) => part != null && part.isNotEmpty).join(', '); ).where((part) => part != null && part.isNotEmpty).join(', ');
} }

View file

@ -1,5 +1,3 @@
import 'dart:ui';
import 'package:aves/model/image_collection.dart'; import 'package:aves/model/image_collection.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
@ -22,28 +20,8 @@ class Settings {
static const infoMapZoomKey = 'info_map_zoom'; static const infoMapZoomKey = 'info_map_zoom';
static const catalogTimeZoneKey = 'catalog_time_zone'; static const catalogTimeZoneKey = 'catalog_time_zone';
// state
static const windowMetricsKey = 'window_metrics';
Future<void> init() async { Future<void> init() async {
prefs = await SharedPreferences.getInstance(); prefs = await SharedPreferences.getInstance();
// TODO TLAD try this as an alternative to MediaQuery, in order to rebuild only on specific property change
// window.onMetricsChanged = onMetricsChanged;
}
WindowMetrics _metrics;
void onMetricsChanged() {
final newValue = WindowMetrics(
devicePixelRatio: window.devicePixelRatio,
physicalSize: window.physicalSize,
viewInsets: window.viewInsets,
viewPadding: window.viewPadding,
systemGestureInsets: window.systemGestureInsets,
padding: window.padding,
);
notifyListeners(windowMetricsKey, _metrics, newValue);
_metrics = newValue;
} }
void addListener(SettingsCallback listener) => _listeners.add(listener); void addListener(SettingsCallback listener) => _listeners.add(listener);
@ -118,21 +96,3 @@ class Settings {
} }
} }
} }
class WindowMetrics {
final double devicePixelRatio;
final Size physicalSize;
final WindowPadding viewInsets;
final WindowPadding viewPadding;
final WindowPadding systemGestureInsets;
final WindowPadding padding;
const WindowMetrics({
this.devicePixelRatio,
this.physicalSize,
this.viewInsets,
this.viewPadding,
this.systemGestureInsets,
this.padding,
});
}

View file

@ -4,4 +4,4 @@ class Constants {
// as of Flutter v1.11.0, overflowing `Text` miscalculates height and some text (e.g. 'Å') is clipped // as of Flutter v1.11.0, overflowing `Text` miscalculates height and some text (e.g. 'Å') is clipped
// so we give it a `strutStyle` with a slightly larger height // so we give it a `strutStyle` with a slightly larger height
static const overflowStrutStyle = StrutStyle(height: 1.3); static const overflowStrutStyle = StrutStyle(height: 1.3);
} }

View file

@ -11,7 +11,10 @@ String _decimal2sexagesimal(final double dec) {
// NumberFormat is necessary to create digit after comma if the value // NumberFormat is necessary to create digit after comma if the value
// has no decimal point (only necessary for browser) // has no decimal point (only necessary for browser)
final List<String> tmp = NumberFormat('0.0#####').format(_round(value, decimals: 10)).split('.'); final List<String> tmp = NumberFormat('0.0#####').format(_round(value, decimals: 10)).split('.');
return <int>[int.parse(tmp[0]).abs(), int.parse(tmp[1])]; return <int>[
int.parse(tmp[0]).abs(),
int.parse(tmp[1])
];
} }
final List<int> parts = _split(dec); final List<int> parts = _split(dec);
@ -34,5 +37,8 @@ List<String> toDMS(Tuple2<double, double> latLng) {
if (latLng == null) return []; if (latLng == null) return [];
final lat = latLng.item1; final lat = latLng.item1;
final lng = latLng.item2; final lng = latLng.item2;
return ['${_decimal2sexagesimal(lat)} ${lat < 0 ? 'S' : 'N'}', '${_decimal2sexagesimal(lng)} ${lng < 0 ? 'W' : 'E'}']; return [
'${_decimal2sexagesimal(lat)} ${lat < 0 ? 'S' : 'N'}',
'${_decimal2sexagesimal(lng)} ${lng < 0 ? 'W' : 'E'}'
];
} }

View file

@ -52,10 +52,26 @@ class AllCollectionDrawer extends StatelessWidget {
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Row(children: [Icon(Icons.photo_library), SizedBox(width: 4), Text('${collection.imageCount}')]), Row(children: [
Row(children: [Icon(Icons.video_library), SizedBox(width: 4), Text('${collection.videoCount}')]), Icon(Icons.photo_library),
Row(children: [Icon(Icons.photo_album), SizedBox(width: 4), Text('${collection.albumCount}')]), SizedBox(width: 4),
Row(children: [Icon(Icons.label), SizedBox(width: 4), Text('${collection.tagCount}')]), Text('${collection.imageCount}')
]),
Row(children: [
Icon(Icons.video_library),
SizedBox(width: 4),
Text('${collection.videoCount}')
]),
Row(children: [
Icon(Icons.photo_album),
SizedBox(width: 4),
Text('${collection.albumCount}')
]),
Row(children: [
Icon(Icons.label),
SizedBox(width: 4),
Text('${collection.tagCount}')
]),
], ],
), ),
], ],

View file

@ -1,6 +1,7 @@
import 'package:aves/model/image_collection.dart'; import 'package:aves/model/image_collection.dart';
import 'package:aves/model/image_entry.dart'; import 'package:aves/model/image_entry.dart';
import 'package:aves/widgets/album/thumbnail_collection.dart'; import 'package:aves/widgets/album/thumbnail_collection.dart';
import 'package:aves/widgets/common/media_query_data_provider.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class FilteredCollectionPage extends StatelessWidget { class FilteredCollectionPage extends StatelessWidget {
@ -14,15 +15,17 @@ class FilteredCollectionPage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return MediaQueryDataProvider(
body: ThumbnailCollection( child: Scaffold(
collection: collection, body: ThumbnailCollection(
appBar: SliverAppBar( collection: collection,
title: Text(title), appBar: SliverAppBar(
floating: true, title: Text(title),
floating: true,
),
), ),
resizeToAvoidBottomInset: false,
), ),
resizeToAvoidBottomInset: false,
); );
} }
} }

View file

@ -8,13 +8,11 @@ import 'package:flutter/material.dart';
class Thumbnail extends StatelessWidget { class Thumbnail extends StatelessWidget {
final ImageEntry entry; final ImageEntry entry;
final double extent; final double extent;
final double devicePixelRatio;
const Thumbnail({ const Thumbnail({
Key key, Key key,
@required this.entry, @required this.entry,
@required this.extent, @required this.extent,
@required this.devicePixelRatio,
}) : super(key: key); }) : super(key: key);
@override @override
@ -23,7 +21,6 @@ class Thumbnail extends StatelessWidget {
entry: entry, entry: entry,
width: extent, width: extent,
height: extent, height: extent,
devicePixelRatio: devicePixelRatio,
builder: (bytes) { builder: (bytes) {
return Hero( return Hero(
tag: entry.uri, tag: entry.uri,

View file

@ -1,5 +1,3 @@
import 'dart:ui';
import 'package:aves/model/image_collection.dart'; import 'package:aves/model/image_collection.dart';
import 'package:aves/model/image_entry.dart'; import 'package:aves/model/image_entry.dart';
import 'package:aves/widgets/album/sections.dart'; import 'package:aves/widgets/album/sections.dart';
@ -9,6 +7,7 @@ import 'package:aves/widgets/fullscreen/fullscreen_page.dart';
import 'package:draggable_scrollbar/draggable_scrollbar.dart'; import 'package:draggable_scrollbar/draggable_scrollbar.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_sticky_header/flutter_sticky_header.dart'; import 'package:flutter_sticky_header/flutter_sticky_header.dart';
import 'package:provider/provider.dart';
class ThumbnailCollection extends AnimatedWidget { class ThumbnailCollection extends AnimatedWidget {
final ImageCollection collection; final ImageCollection collection;
@ -22,10 +21,13 @@ class ThumbnailCollection extends AnimatedWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ThumbnailCollectionContent( return Selector<MediaQueryData, double>(
collection: collection, selector: (c, mq) => mq.size.width,
appBar: appBar, builder: (c, mqWidth, child) => ThumbnailCollectionContent(
screenWidth: MediaQuery.of(context).size.width, collection: collection,
appBar: appBar,
screenWidth: mqWidth,
),
); );
} }
} }
@ -48,7 +50,6 @@ class ThumbnailCollectionContent extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final bottomInsets = MediaQuery.of(context).viewInsets.bottom;
final sectionKeys = _sections.keys.toList(); final sectionKeys = _sections.keys.toList();
double topPadding = 0; double topPadding = 0;
if (appBar != null) { if (appBar != null) {
@ -61,35 +62,39 @@ class ThumbnailCollectionContent extends StatelessWidget {
} }
return SafeArea( return SafeArea(
child: DraggableScrollbar.arrows( child: Selector<MediaQueryData, double>(
child: CustomScrollView( selector: (c, mq) => mq.viewInsets.bottom,
controller: _scrollController, builder: (c, mqViewInsetsBottom, child) => DraggableScrollbar.arrows(
slivers: [ child: CustomScrollView(
if (appBar != null) appBar, controller: _scrollController,
...sectionKeys.map((sectionKey) { slivers: [
Widget sliver = SectionSliver( if (appBar != null) appBar,
// need key to prevent section header mismatch ...sectionKeys.map((sectionKey) {
// but it should not be unique key, otherwise sections are rebuilt when changing page Widget sliver = SectionSliver(
key: ValueKey(sectionKey), // need key to prevent section header mismatch
collection: collection, // but it should not be unique key, otherwise sections are rebuilt when changing page
sections: _sections, key: ValueKey(sectionKey),
sectionKey: sectionKey, collection: collection,
screenWidth: screenWidth, sections: _sections,
); sectionKey: sectionKey,
if (sectionKey == sectionKeys.last) { screenWidth: screenWidth,
sliver = SliverPadding(
padding: EdgeInsets.only(bottom: bottomInsets),
sliver: sliver,
); );
} if (sectionKey == sectionKeys.last) {
return sliver; sliver = SliverPadding(
}), padding: EdgeInsets.only(bottom: mqViewInsetsBottom),
], sliver: sliver,
), );
controller: _scrollController, }
padding: EdgeInsets.only( return sliver;
top: topPadding, }),
bottom: bottomInsets, ],
),
controller: _scrollController,
padding: EdgeInsets.only(
// top padding to adjust scroll thumb
top: topPadding,
bottom: mqViewInsetsBottom,
),
), ),
), ),
); );
@ -131,7 +136,6 @@ class SectionSliver extends StatelessWidget {
child: Thumbnail( child: Thumbnail(
entry: entry, entry: entry,
extent: screenWidth / columnCount, extent: screenWidth / columnCount,
devicePixelRatio: window.devicePixelRatio,
), ),
); );
}, },

View file

@ -1,13 +1,15 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:after_init/after_init.dart';
import 'package:aves/model/image_entry.dart'; import 'package:aves/model/image_entry.dart';
import 'package:aves/model/image_file_service.dart'; import 'package:aves/model/image_file_service.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:transparent_image/transparent_image.dart'; import 'package:transparent_image/transparent_image.dart';
class ImagePreview extends StatefulWidget { class ImagePreview extends StatefulWidget {
final ImageEntry entry; final ImageEntry entry;
final double width, height, devicePixelRatio; final double width, height;
final Widget Function(Uint8List bytes) builder; final Widget Function(Uint8List bytes) builder;
const ImagePreview({ const ImagePreview({
@ -15,7 +17,6 @@ class ImagePreview extends StatefulWidget {
@required this.entry, @required this.entry,
@required this.width, @required this.width,
@required this.height, @required this.height,
@required this.devicePixelRatio,
@required this.builder, @required this.builder,
}) : super(key: key); }) : super(key: key);
@ -23,9 +24,10 @@ class ImagePreview extends StatefulWidget {
State<StatefulWidget> createState() => ImagePreviewState(); State<StatefulWidget> createState() => ImagePreviewState();
} }
class ImagePreviewState extends State<ImagePreview> { class ImagePreviewState extends State<ImagePreview> with AfterInitMixin {
Future<Uint8List> _byteLoader; Future<Uint8List> _byteLoader;
Listenable _entryChangeNotifier; Listenable _entryChangeNotifier;
double _devicePixelRatio;
ImageEntry get entry => widget.entry; ImageEntry get entry => widget.entry;
@ -34,8 +36,16 @@ class ImagePreviewState extends State<ImagePreview> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_entryChangeNotifier = Listenable.merge([entry.imageChangeNotifier, entry.metadataChangeNotifier]); _entryChangeNotifier = Listenable.merge([
entry.imageChangeNotifier,
entry.metadataChangeNotifier
]);
_entryChangeNotifier.addListener(onEntryChange); _entryChangeNotifier.addListener(onEntryChange);
}
@override
void didInitState() {
_devicePixelRatio = Provider.of<MediaQueryData>(context, listen: false).devicePixelRatio;
initByteLoader(); initByteLoader();
} }
@ -47,8 +57,8 @@ class ImagePreviewState extends State<ImagePreview> {
} }
initByteLoader() { initByteLoader() {
final width = (widget.width * widget.devicePixelRatio).round(); final width = (widget.width * _devicePixelRatio).round();
final height = (widget.height * widget.devicePixelRatio).round(); final height = (widget.height * _devicePixelRatio).round();
_byteLoader = ImageFileService.getImageBytes(widget.entry, width, height); _byteLoader = ImageFileService.getImageBytes(widget.entry, width, height);
} }

View file

@ -0,0 +1,16 @@
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';
class MediaQueryDataProvider extends StatelessWidget {
final Widget child;
const MediaQueryDataProvider({@required this.child});
@override
Widget build(BuildContext context) {
return Provider<MediaQueryData>.value(
value: MediaQuery.of(context),
child: child,
);
}
}

View file

@ -3,6 +3,7 @@ import 'package:aves/model/image_metadata.dart';
import 'package:aves/model/metadata_db.dart'; import 'package:aves/model/metadata_db.dart';
import 'package:aves/model/settings.dart'; import 'package:aves/model/settings.dart';
import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/android_file_utils.dart';
import 'package:aves/widgets/common/media_query_data_provider.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
@ -35,59 +36,61 @@ class DebugPageState extends State<DebugPage> {
final catalogued = entries.where((entry) => entry.isCatalogued); final catalogued = entries.where((entry) => entry.isCatalogued);
final withGps = catalogued.where((entry) => entry.hasGps); final withGps = catalogued.where((entry) => entry.hasGps);
final located = withGps.where((entry) => entry.isLocated); final located = withGps.where((entry) => entry.isLocated);
return Scaffold( return MediaQueryDataProvider(
appBar: AppBar( child: Scaffold(
title: Text('Info'), appBar: AppBar(
), title: Text('Info'),
body: Column( ),
crossAxisAlignment: CrossAxisAlignment.start, body: Column(
children: [ crossAxisAlignment: CrossAxisAlignment.start,
Text('Paths'), children: [
Text('DCIM path: ${androidFileUtils.dcimPath}'), Text('Paths'),
Text('pictures path: ${androidFileUtils.picturesPath}'), Text('DCIM path: ${androidFileUtils.dcimPath}'),
Divider(), Text('pictures path: ${androidFileUtils.picturesPath}'),
Text('Settings'), Divider(),
Text('collectionGroupFactor: ${settings.collectionGroupFactor}'), Text('Settings'),
Text('collectionSortFactor: ${settings.collectionSortFactor}'), Text('collectionGroupFactor: ${settings.collectionGroupFactor}'),
Text('infoMapZoom: ${settings.infoMapZoom}'), Text('collectionSortFactor: ${settings.collectionSortFactor}'),
Divider(), Text('infoMapZoom: ${settings.infoMapZoom}'),
Text('Entries: ${entries.length}'), Divider(),
...byMimeTypes.keys.map((mimeType) => Text('- $mimeType: ${byMimeTypes[mimeType].length}')), Text('Entries: ${entries.length}'),
Text('Catalogued: ${catalogued.length}'), ...byMimeTypes.keys.map((mimeType) => Text('- $mimeType: ${byMimeTypes[mimeType].length}')),
Text('With GPS: ${withGps.length}'), Text('Catalogued: ${catalogued.length}'),
Text('With address: ${located.length}'), Text('With GPS: ${withGps.length}'),
Divider(), Text('With address: ${located.length}'),
RaisedButton( Divider(),
onPressed: () => metadataDb.reset(), RaisedButton(
child: Text('Reset DB'), onPressed: () => metadataDb.reset(),
), child: Text('Reset DB'),
FutureBuilder( ),
future: _dbMetadataLoader, FutureBuilder(
builder: (futureContext, AsyncSnapshot<List<CatalogMetadata>> snapshot) { future: _dbMetadataLoader,
if (snapshot.hasError) return Text(snapshot.error); builder: (futureContext, AsyncSnapshot<List<CatalogMetadata>> snapshot) {
if (snapshot.connectionState != ConnectionState.done) return SizedBox.shrink(); if (snapshot.hasError) return Text(snapshot.error);
return Text('DB metadata rows: ${snapshot.data.length}'); if (snapshot.connectionState != ConnectionState.done) return SizedBox.shrink();
}, return Text('DB metadata rows: ${snapshot.data.length}');
), },
FutureBuilder( ),
future: _dbAddressLoader, FutureBuilder(
builder: (futureContext, AsyncSnapshot<List<AddressDetails>> snapshot) { future: _dbAddressLoader,
if (snapshot.hasError) return Text(snapshot.error); builder: (futureContext, AsyncSnapshot<List<AddressDetails>> snapshot) {
if (snapshot.connectionState != ConnectionState.done) return SizedBox.shrink(); if (snapshot.hasError) return Text(snapshot.error);
return Text('DB address rows: ${snapshot.data.length}'); if (snapshot.connectionState != ConnectionState.done) return SizedBox.shrink();
}, return Text('DB address rows: ${snapshot.data.length}');
), },
Divider(), ),
Text('Time dilation'), Divider(),
Slider( Text('Time dilation'),
value: timeDilation, Slider(
onChanged: (v) => setState(() => timeDilation = v), value: timeDilation,
min: 1.0, onChanged: (v) => setState(() => timeDilation = v),
max: 10.0, min: 1.0,
divisions: 9, max: 10.0,
label: '$timeDilation', divisions: 9,
), label: '$timeDilation',
], ),
],
),
), ),
); );
} }

View file

@ -3,6 +3,7 @@ import 'dart:math';
import 'package:aves/model/image_collection.dart'; import 'package:aves/model/image_collection.dart';
import 'package:aves/model/image_entry.dart'; import 'package:aves/model/image_entry.dart';
import 'package:aves/widgets/common/media_query_data_provider.dart';
import 'package:aves/widgets/fullscreen/fullscreen_action_delegate.dart'; import 'package:aves/widgets/fullscreen/fullscreen_action_delegate.dart';
import 'package:aves/widgets/fullscreen/image_page.dart'; import 'package:aves/widgets/fullscreen/image_page.dart';
import 'package:aves/widgets/fullscreen/info/info_page.dart'; import 'package:aves/widgets/fullscreen/info/info_page.dart';
@ -13,6 +14,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:photo_view/photo_view.dart'; import 'package:photo_view/photo_view.dart';
import 'package:provider/provider.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
import 'package:video_player/video_player.dart'; import 'package:video_player/video_player.dart';
@ -28,13 +30,15 @@ class FullscreenPage extends AnimatedWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return MediaQueryDataProvider(
body: FullscreenBody( child: Scaffold(
collection: collection, body: FullscreenBody(
initialUri: initialUri, collection: collection,
initialUri: initialUri,
),
backgroundColor: Colors.black,
resizeToAvoidBottomInset: false,
), ),
backgroundColor: Colors.black,
resizeToAvoidBottomInset: false,
); );
} }
} }
@ -92,25 +96,25 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
parent: _overlayAnimationController, parent: _overlayAnimationController,
curve: Curves.easeOutQuart, curve: Curves.easeOutQuart,
)); ));
_overlayVisible.addListener(onOverlayVisibleChange); _overlayVisible.addListener(_onOverlayVisibleChange);
_actionDelegate = FullscreenActionDelegate( _actionDelegate = FullscreenActionDelegate(
collection: collection, collection: collection,
showInfo: () => goToVerticalPage(infoPage), showInfo: () => _goToVerticalPage(infoPage),
); );
initVideoController(); _initVideoController();
initOverlay(); _initOverlay();
} }
initOverlay() async { _initOverlay() async {
// wait for MaterialPageRoute.transitionDuration // wait for MaterialPageRoute.transitionDuration
// to show overlay after hero animation is complete // to show overlay after hero animation is complete
await Future.delayed(Duration(milliseconds: (300 * timeDilation).toInt())); await Future.delayed(Duration(milliseconds: (300 * timeDilation).toInt()));
onOverlayVisibleChange(); await _onOverlayVisibleChange();
} }
@override @override
void dispose() { void dispose() {
_overlayVisible.removeListener(onOverlayVisibleChange); _overlayVisible.removeListener(_onOverlayVisibleChange);
_videoControllers.forEach((kv) => kv.item2.dispose()); _videoControllers.forEach((kv) => kv.item2.dispose());
super.dispose(); super.dispose();
} }
@ -121,7 +125,7 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
return WillPopScope( return WillPopScope(
onWillPop: () { onWillPop: () {
if (_currentVerticalPage == infoPage) { if (_currentVerticalPage == infoPage) {
goToVerticalPage(imagePage); _goToVerticalPage(imagePage);
return Future.value(false); return Future.value(false);
} }
_onLeave(); _onLeave();
@ -142,14 +146,14 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
collection: collection, collection: collection,
pageController: _horizontalPager, pageController: _horizontalPager,
onTap: () => _overlayVisible.value = !_overlayVisible.value, onTap: () => _overlayVisible.value = !_overlayVisible.value,
onPageChanged: onHorizontalPageChanged, onPageChanged: _onHorizontalPageChanged,
onScaleChanged: (state) => setState(() => _isInitialScale = state == PhotoViewScaleState.initial), onScaleChanged: (state) => setState(() => _isInitialScale = state == PhotoViewScaleState.initial),
videoControllers: _videoControllers, videoControllers: _videoControllers,
), ),
), ),
NotificationListener( NotificationListener(
onNotification: (notification) { onNotification: (notification) {
if (notification is BackUpNotification) goToVerticalPage(imagePage); if (notification is BackUpNotification) _goToVerticalPage(imagePage);
return false; return false;
}, },
child: InfoPage(collection: collection, entry: entry), child: InfoPage(collection: collection, entry: entry),
@ -201,7 +205,7 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
]; ];
} }
goToVerticalPage(int page) { Future<void> _goToVerticalPage(int page) {
return _verticalPager.animateToPage( return _verticalPager.animateToPage(
page, page,
duration: Duration(milliseconds: 350), duration: Duration(milliseconds: 350),
@ -209,7 +213,7 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
); );
} }
_onVerticalPageChanged(page) { void _onVerticalPageChanged(page) {
setState(() => _currentVerticalPage = page); setState(() => _currentVerticalPage = page);
if (_currentVerticalPage == transitionPage) { if (_currentVerticalPage == transitionPage) {
_onLeave(); _onLeave();
@ -217,20 +221,22 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
} }
} }
_onLeave() => _showSystemUI(); void _onLeave() => _showSystemUI();
_showSystemUI() => SystemChrome.setEnabledSystemUIOverlays(SystemUiOverlay.values); void _showSystemUI() => SystemChrome.setEnabledSystemUIOverlays(SystemUiOverlay.values);
_hideSystemUI() => SystemChrome.setEnabledSystemUIOverlays([]); void _hideSystemUI() => SystemChrome.setEnabledSystemUIOverlays([]);
onOverlayVisibleChange() async { Future<void> _onOverlayVisibleChange() async {
if (_overlayVisible.value) { if (_overlayVisible.value) {
_showSystemUI(); _showSystemUI();
_overlayAnimationController.forward(); _overlayAnimationController.forward();
} else { } else {
final mediaQuery = MediaQuery.of(context); final mediaQuery = Provider.of<MediaQueryData>(context, listen: false);
_frozenViewInsets = mediaQuery.viewInsets; setState(() {
_frozenViewPadding = mediaQuery.viewPadding; _frozenViewInsets = mediaQuery.viewInsets;
_frozenViewPadding = mediaQuery.viewPadding;
});
_hideSystemUI(); _hideSystemUI();
await _overlayAnimationController.reverse(); await _overlayAnimationController.reverse();
_frozenViewInsets = null; _frozenViewInsets = null;
@ -238,16 +244,16 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
} }
} }
onHorizontalPageChanged(int page) { void _onHorizontalPageChanged(int page) {
_currentHorizontalPage = page; _currentHorizontalPage = page;
pauseVideoControllers(); _pauseVideoControllers();
initVideoController(); _initVideoController();
setState(() {}); setState(() {});
} }
pauseVideoControllers() => _videoControllers.forEach((e) => e.item2.pause()); void _pauseVideoControllers() => _videoControllers.forEach((e) => e.item2.pause());
initVideoController() { void _initVideoController() {
final entry = _currentHorizontalPage != null && _currentHorizontalPage < entries.length ? entries[_currentHorizontalPage] : null; final entry = _currentHorizontalPage != null && _currentHorizontalPage < entries.length ? entries[_currentHorizontalPage] : null;
if (entry == null || !entry.isVideo) return; if (entry == null || !entry.isVideo) return;

View file

@ -6,6 +6,7 @@ import 'package:aves/widgets/fullscreen/video.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:photo_view/photo_view.dart'; import 'package:photo_view/photo_view.dart';
import 'package:photo_view/photo_view_gallery.dart'; import 'package:photo_view/photo_view_gallery.dart';
import 'package:provider/provider.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
import 'package:video_player/video_player.dart'; import 'package:video_player/video_player.dart';
@ -36,45 +37,48 @@ class ImagePageState extends State<ImagePage> with AutomaticKeepAliveClientMixin
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context); super.build(context);
return PhotoViewGallery.builder( return Selector<MediaQueryData, Size>(
itemCount: entries.length, selector: (c, mq) => mq.size,
builder: (galleryContext, index) { builder: (c, mqSize, child) => PhotoViewGallery.builder(
final entry = entries[index]; itemCount: entries.length,
if (entry.isVideo) { builder: (galleryContext, index) {
final videoController = widget.videoControllers.firstWhere((kv) => kv.item1 == entry.path, orElse: () => null)?.item2; final entry = entries[index];
return PhotoViewGalleryPageOptions.customChild( if (entry.isVideo) {
child: videoController != null final videoController = widget.videoControllers.firstWhere((kv) => kv.item1 == entry.path, orElse: () => null)?.item2;
? AvesVideo( return PhotoViewGalleryPageOptions.customChild(
entry: entry, child: videoController != null
controller: videoController, ? AvesVideo(
) entry: entry,
: SizedBox(), controller: videoController,
childSize: MediaQuery.of(galleryContext).size, )
// no hero as most videos fullscreen image is different from its thumbnail : SizedBox(),
childSize: mqSize,
// no hero as most videos fullscreen image is different from its thumbnail
minScale: PhotoViewComputedScale.contained,
initialScale: PhotoViewComputedScale.contained,
onTapUp: (tapContext, details, value) => widget.onTap?.call(),
);
}
return PhotoViewGalleryPageOptions(
imageProvider: FileImage(File(entry.path)),
heroAttributes: PhotoViewHeroAttributes(
tag: entry.uri,
transitionOnUserGestures: true,
),
minScale: PhotoViewComputedScale.contained, minScale: PhotoViewComputedScale.contained,
initialScale: PhotoViewComputedScale.contained, initialScale: PhotoViewComputedScale.contained,
onTapUp: (tapContext, details, value) => widget.onTap?.call(), onTapUp: (tapContext, details, value) => widget.onTap?.call(),
); );
} },
return PhotoViewGalleryPageOptions( loadingChild: Center(
imageProvider: FileImage(File(entry.path)), child: CircularProgressIndicator(),
heroAttributes: PhotoViewHeroAttributes( ),
tag: entry.uri, backgroundDecoration: BoxDecoration(color: Colors.transparent),
transitionOnUserGestures: true, pageController: widget.pageController,
), onPageChanged: widget.onPageChanged,
minScale: PhotoViewComputedScale.contained, scaleStateChangedCallback: widget.onScaleChanged,
initialScale: PhotoViewComputedScale.contained, scrollPhysics: BouncingScrollPhysics(),
onTapUp: (tapContext, details, value) => widget.onTap?.call(),
);
},
loadingChild: Center(
child: CircularProgressIndicator(),
), ),
backgroundDecoration: BoxDecoration(color: Colors.transparent),
pageController: widget.pageController,
onPageChanged: widget.onPageChanged,
scaleStateChangedCallback: widget.onScaleChanged,
scrollPhysics: BouncingScrollPhysics(),
); );
} }

View file

@ -35,6 +35,9 @@ class BasicSection extends StatelessWidget {
List<Widget> _buildVideoRows() { List<Widget> _buildVideoRows() {
final rotation = entry.catalogMetadata?.videoRotation; final rotation = entry.catalogMetadata?.videoRotation;
if (rotation != null) InfoRow('Rotation', '$rotation°'); if (rotation != null) InfoRow('Rotation', '$rotation°');
return [InfoRow('Duration', entry.durationText), if (rotation != null) InfoRow('Rotation', '$rotation°')]; return [
InfoRow('Duration', entry.durationText),
if (rotation != null) InfoRow('Rotation', '$rotation°')
];
} }
} }

View file

@ -1,10 +1,13 @@
import 'package:aves/model/image_collection.dart'; import 'package:aves/model/image_collection.dart';
import 'package:aves/model/image_entry.dart'; import 'package:aves/model/image_entry.dart';
import 'package:aves/widgets/common/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/location_section.dart'; import 'package:aves/widgets/fullscreen/info/location_section.dart';
import 'package:aves/widgets/fullscreen/info/metadata_section.dart'; import 'package:aves/widgets/fullscreen/info/metadata_section.dart';
import 'package:aves/widgets/fullscreen/info/xmp_section.dart'; import 'package:aves/widgets/fullscreen/info/xmp_section.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:tuple/tuple.dart';
class InfoPage extends StatefulWidget { class InfoPage extends StatefulWidget {
final ImageCollection collection; final ImageCollection collection;
@ -33,44 +36,51 @@ class InfoPageState extends State<InfoPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// use MediaQuery instead of unreliable OrientationBuilder return MediaQueryDataProvider(
final orientation = MediaQuery.of(context).orientation; child: Scaffold(
final bottomInsets = MediaQuery.of(context).viewInsets.bottom; appBar: AppBar(
return Scaffold( leading: IconButton(
appBar: AppBar( icon: const Icon(Icons.arrow_upward),
leading: IconButton( onPressed: () => BackUpNotification().dispatch(context),
icon: Icon(Icons.arrow_upward), tooltip: 'Back to image',
onPressed: () => BackUpNotification().dispatch(context), ),
tooltip: 'Back to image', title: const Text('Info'),
), ),
title: Text('Info'), body: SafeArea(
), child: NotificationListener(
body: SafeArea( onNotification: _handleTopScroll,
child: NotificationListener( child: Selector<MediaQueryData, Tuple2<Orientation, double>>(
onNotification: _handleTopScroll, selector: (c, mq) => Tuple2(mq.orientation, mq.viewInsets.bottom),
child: ListView( builder: (c, mq, child) {
padding: EdgeInsets.all(8.0) + EdgeInsets.only(bottom: bottomInsets), final mqOrientation = mq.item1;
children: [ final mqViewInsetsBottom = mq.item2;
if (orientation == Orientation.landscape && entry.hasGps)
Row( return ListView(
crossAxisAlignment: CrossAxisAlignment.start, padding: const EdgeInsets.all(8.0) + EdgeInsets.only(bottom: mqViewInsetsBottom),
children: [ children: [
Expanded(child: BasicSection(entry: entry)), if (mqOrientation == Orientation.landscape && entry.hasGps)
SizedBox(width: 8), Row(
Expanded(child: LocationSection(entry: entry, showTitle: false)), crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(child: BasicSection(entry: entry)),
const SizedBox(width: 8),
Expanded(child: LocationSection(entry: entry, showTitle: false)),
],
)
else ...[
BasicSection(entry: entry),
LocationSection(entry: entry, showTitle: true),
],
XmpTagSection(collection: widget.collection, entry: entry),
MetadataSection(entry: entry),
], ],
) );
else ...[ },
BasicSection(entry: entry), ),
LocationSection(entry: entry, showTitle: true),
],
XmpTagSection(collection: widget.collection, entry: entry),
MetadataSection(entry: entry),
],
), ),
), ),
resizeToAvoidBottomInset: false,
), ),
resizeToAvoidBottomInset: false,
); );
} }
@ -104,9 +114,9 @@ class SectionRow extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Row( return Row(
children: [ children: [
Expanded(child: Divider(color: Colors.white70)), const Expanded(child: Divider(color: Colors.white70)),
Padding( Padding(
padding: EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Text( child: Text(
title, title,
style: TextStyle( style: TextStyle(
@ -115,7 +125,7 @@ class SectionRow extends StatelessWidget {
), ),
), ),
), ),
Expanded(child: Divider(color: Colors.white70)), const Expanded(child: Divider(color: Colors.white70)),
], ],
); );
} }
@ -129,7 +139,7 @@ class InfoRow extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( return Padding(
padding: EdgeInsets.symmetric(vertical: 4.0), padding: const EdgeInsets.symmetric(vertical: 4.0),
child: RichText( child: RichText(
text: TextSpan( text: TextSpan(
style: TextStyle(fontFamily: 'Concourse'), style: TextStyle(fontFamily: 'Concourse'),

View file

@ -13,7 +13,12 @@ class LocationSection extends AnimatedWidget {
Key key, Key key,
@required this.entry, @required this.entry,
@required this.showTitle, @required this.showTitle,
}) : super(key: key, listenable: Listenable.merge([entry.metadataChangeNotifier, entry.addressChangeNotifier])); }) : super(
key: key,
listenable: Listenable.merge([
entry.metadataChangeNotifier,
entry.addressChangeNotifier
]));
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {

View file

@ -4,6 +4,7 @@ import 'package:aves/model/image_entry.dart';
import 'package:aves/model/metadata_service.dart'; import 'package:aves/model/metadata_service.dart';
import 'package:aves/widgets/fullscreen/info/info_page.dart'; import 'package:aves/widgets/fullscreen/info/info_page.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class MetadataSection extends StatefulWidget { class MetadataSection extends StatefulWidget {
final ImageEntry entry; final ImageEntry entry;
@ -37,50 +38,53 @@ class MetadataSectionState extends State<MetadataSection> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return FutureBuilder( return Selector<MediaQueryData, double>(
future: _metadataLoader, selector: (c, mq) => mq.size.width,
builder: (futureContext, AsyncSnapshot<Map> snapshot) { builder: (c, mqWidth, child) => FutureBuilder(
if (snapshot.hasError) return Text(snapshot.error); future: _metadataLoader,
if (snapshot.connectionState != ConnectionState.done) return SizedBox.shrink(); builder: (futureContext, AsyncSnapshot<Map> snapshot) {
final metadataMap = snapshot.data.cast<String, Map>(); if (snapshot.hasError) return Text(snapshot.error);
final directoryNames = metadataMap.keys.toList()..sort(); if (snapshot.connectionState != ConnectionState.done) return SizedBox.shrink();
final metadataMap = snapshot.data.cast<String, Map>();
final directoryNames = metadataMap.keys.toList()..sort();
Widget content; Widget content;
if (MediaQuery.of(context).size.width > 400) { if (mqWidth > 400) {
final first = <String>[], second = <String>[]; final first = <String>[], second = <String>[];
var firstItemCount = 0, secondItemCount = 0; var firstItemCount = 0, secondItemCount = 0;
var firstIndex = 0, secondIndex = directoryNames.length - 1; var firstIndex = 0, secondIndex = directoryNames.length - 1;
while (firstIndex <= secondIndex) { while (firstIndex <= secondIndex) {
if (firstItemCount <= secondItemCount) { if (firstItemCount <= secondItemCount) {
final directoryName = directoryNames[firstIndex++]; final directoryName = directoryNames[firstIndex++];
first.add(directoryName); first.add(directoryName);
firstItemCount += 2 + metadataMap[directoryName].length; firstItemCount += 2 + metadataMap[directoryName].length;
} else { } else {
final directoryName = directoryNames[secondIndex--]; final directoryName = directoryNames[secondIndex--];
second.insert(0, directoryName); second.insert(0, directoryName);
secondItemCount += 2 + metadataMap[directoryName].length; secondItemCount += 2 + metadataMap[directoryName].length;
}
} }
content = Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(child: getMetadataColumn(metadataMap, first)),
SizedBox(width: 8),
Expanded(child: getMetadataColumn(metadataMap, second)),
],
);
} else {
content = getMetadataColumn(metadataMap, directoryNames);
} }
content = Row(
crossAxisAlignment: CrossAxisAlignment.start, return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
Expanded(child: getMetadataColumn(metadataMap, first)), SectionRow('Metadata'),
SizedBox(width: 8), content,
Expanded(child: getMetadataColumn(metadataMap, second)),
], ],
); );
} else { },
content = getMetadataColumn(metadataMap, directoryNames); ),
}
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
SectionRow('Metadata'),
content,
],
);
},
); );
} }

View file

@ -9,6 +9,8 @@ import 'package:aves/utils/geo_utils.dart';
import 'package:aves/widgets/common/blurred.dart'; import 'package:aves/widgets/common/blurred.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import 'package:tuple/tuple.dart';
class FullscreenBottomOverlay extends StatefulWidget { class FullscreenBottomOverlay extends StatefulWidget {
final List<ImageEntry> entries; final List<ImageEntry> entries;
@ -32,6 +34,8 @@ class _FullscreenBottomOverlayState extends State<FullscreenBottomOverlay> {
ImageEntry _lastEntry; ImageEntry _lastEntry;
OverlayMetadata _lastDetails; OverlayMetadata _lastDetails;
static const innerPadding = EdgeInsets.symmetric(vertical: 4, horizontal: 8);
ImageEntry get entry { ImageEntry get entry {
final entries = widget.entries; final entries = widget.entries;
final index = widget.index; final index = widget.index;
@ -56,65 +60,73 @@ class _FullscreenBottomOverlayState extends State<FullscreenBottomOverlay> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final innerPadding = EdgeInsets.symmetric(vertical: 4, horizontal: 8);
final mediaQuery = MediaQuery.of(context);
final viewInsets = widget.viewInsets ?? mediaQuery.viewInsets;
final viewPadding = widget.viewPadding ?? mediaQuery.viewPadding;
final overlayContentMaxWidth = mediaQuery.size.width - viewPadding.horizontal - innerPadding.horizontal;
return IgnorePointer( return IgnorePointer(
child: BlurredRect( child: BlurredRect(
child: Container( child: Selector<MediaQueryData, Tuple3<double, EdgeInsets, EdgeInsets>>(
color: Colors.black26, selector: (c, mq) => Tuple3(mq.size.width, mq.viewInsets, mq.viewPadding),
padding: viewInsets + viewPadding.copyWith(top: 0), builder: (c, mq, child) {
child: Padding( final mqWidth = mq.item1;
padding: innerPadding, final mqViewInsets = mq.item2;
child: FutureBuilder( final mqViewPadding = mq.item3;
future: _detailLoader,
builder: (futureContext, AsyncSnapshot<OverlayMetadata> snapshot) { final viewInsets = widget.viewInsets ?? mqViewInsets;
if (snapshot.connectionState == ConnectionState.done && !snapshot.hasError) { final viewPadding = widget.viewPadding ?? mqViewPadding;
_lastDetails = snapshot.data; final overlayContentMaxWidth = mqWidth - viewPadding.horizontal - innerPadding.horizontal;
_lastEntry = entry;
} return Container(
return _lastEntry == null color: Colors.black26,
? SizedBox.shrink() padding: viewInsets + viewPadding.copyWith(top: 0),
: _FullscreenBottomOverlayContent( child: Padding(
entry: _lastEntry, padding: innerPadding,
details: _lastDetails, child: FutureBuilder(
position: '${widget.index + 1}/${widget.entries.length}', future: _detailLoader,
maxWidth: overlayContentMaxWidth, builder: (futureContext, AsyncSnapshot<OverlayMetadata> snapshot) {
); if (snapshot.connectionState == ConnectionState.done && !snapshot.hasError) {
}, _lastDetails = snapshot.data;
), _lastEntry = entry;
), }
return _lastEntry == null
? SizedBox.shrink()
: _FullscreenBottomOverlayContent(
entry: _lastEntry,
details: _lastDetails,
position: '${widget.index + 1}/${widget.entries.length}',
maxWidth: overlayContentMaxWidth,
);
},
),
),
);
},
), ),
), ),
); );
} }
} }
const double _iconPadding = 8.0;
const double _iconSize = 16.0;
const double _interRowPadding = 2.0;
const double _subRowMinWidth = 300.0;
class _FullscreenBottomOverlayContent extends StatelessWidget { class _FullscreenBottomOverlayContent extends StatelessWidget {
final ImageEntry entry; final ImageEntry entry;
final OverlayMetadata details; final OverlayMetadata details;
final String position; final String position;
final double maxWidth; final double maxWidth;
static const double interRowPadding = 2.0; _FullscreenBottomOverlayContent({
static const double iconPadding = 8.0; this.entry,
static const double iconSize = 16.0; this.details,
static const double subRowMinWidth = 300.0; this.position,
this.maxWidth,
_FullscreenBottomOverlayContent({this.entry, this.details, this.position, this.maxWidth}); });
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// use MediaQuery instead of unreliable OrientationBuilder
final orientation = MediaQuery.of(context).orientation;
final twoColumns = orientation == Orientation.landscape && maxWidth / 2 > subRowMinWidth;
final subRowWidth = twoColumns ? min(subRowMinWidth, maxWidth / 2) : maxWidth;
final hasShootingDetails = details != null && !details.isEmpty;
return DefaultTextStyle( return DefaultTextStyle(
style: Theme.of(context).textTheme.body1.copyWith( style: Theme.of(context).textTheme.body1.copyWith(
shadows: [ shadows: const [
Shadow( Shadow(
color: Colors.black87, color: Colors.black87,
offset: Offset(0.5, 1.0), offset: Offset(0.5, 1.0),
@ -123,49 +135,64 @@ class _FullscreenBottomOverlayContent extends StatelessWidget {
), ),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
maxLines: 1, maxLines: 1,
child: Column( child: Selector<MediaQueryData, Orientation>(
mainAxisSize: MainAxisSize.min, selector: (c, mq) => mq.orientation,
crossAxisAlignment: CrossAxisAlignment.start, builder: (c, orientation, child) {
children: [ final twoColumns = orientation == Orientation.landscape && maxWidth / 2 > _subRowMinWidth;
SizedBox( final subRowWidth = twoColumns ? min(_subRowMinWidth, maxWidth / 2) : maxWidth;
width: maxWidth, final hasShootingDetails = details != null && !details.isEmpty;
child: Text('$position ${entry.title}', strutStyle: Constants.overflowStrutStyle), return Column(
), mainAxisSize: MainAxisSize.min,
if (entry.hasGps) crossAxisAlignment: CrossAxisAlignment.start,
Container( children: [
padding: EdgeInsets.only(top: interRowPadding), SizedBox(
width: subRowWidth, width: maxWidth,
child: _buildLocationRow(), child: Text('$position ${entry.title}', strutStyle: Constants.overflowStrutStyle),
),
if (twoColumns)
Padding(
padding: EdgeInsets.only(top: interRowPadding),
child: Row(
children: [
Container(width: subRowWidth, child: _buildDateRow()),
if (hasShootingDetails) Container(width: subRowWidth, child: _buildShootingRow()),
],
), ),
) if (entry.hasGps)
else ...[ Container(
Container( padding: const EdgeInsets.only(top: _interRowPadding),
padding: EdgeInsets.only(top: interRowPadding), width: subRowWidth,
width: subRowWidth, child: _LocationRow(entry),
child: _buildDateRow(), ),
), if (twoColumns)
if (hasShootingDetails) Padding(
Container( padding: const EdgeInsets.only(top: _interRowPadding),
padding: EdgeInsets.only(top: interRowPadding), child: Row(
width: subRowWidth, children: [
child: _buildShootingRow(), Container(width: subRowWidth, child: _DateRow(entry)),
), if (hasShootingDetails) Container(width: subRowWidth, child: _ShootingRow(details)),
], ],
], ),
)
else ...[
Container(
padding: const EdgeInsets.only(top: _interRowPadding),
width: subRowWidth,
child: _DateRow(entry),
),
if (hasShootingDetails)
Container(
padding: const EdgeInsets.only(top: _interRowPadding),
width: subRowWidth,
child: _ShootingRow(details),
),
],
],
);
},
), ),
); );
} }
}
Widget _buildLocationRow() { class _LocationRow extends StatelessWidget {
final ImageEntry entry;
const _LocationRow(this.entry);
@override
Widget build(BuildContext context) {
String location; String location;
if (entry.isLocated) { if (entry.isLocated) {
location = entry.shortAddress; location = entry.shortAddress;
@ -174,32 +201,46 @@ class _FullscreenBottomOverlayContent extends StatelessWidget {
} }
return Row( return Row(
children: [ children: [
Icon(Icons.place, size: iconSize), const Icon(Icons.place, size: _iconSize),
SizedBox(width: iconPadding), const SizedBox(width: _iconPadding),
Expanded(child: Text(location, strutStyle: Constants.overflowStrutStyle)), Expanded(child: Text(location, strutStyle: Constants.overflowStrutStyle)),
], ],
); );
} }
}
Widget _buildDateRow() { class _DateRow extends StatelessWidget {
final ImageEntry entry;
const _DateRow(this.entry);
@override
Widget build(BuildContext context) {
final date = entry.bestDate; final date = entry.bestDate;
final dateText = '${DateFormat.yMMMd().format(date)} at ${DateFormat.Hm().format(date)}'; final dateText = '${DateFormat.yMMMd().format(date)} at ${DateFormat.Hm().format(date)}';
final resolution = '${entry.width} × ${entry.height}'; final resolution = '${entry.width} × ${entry.height}';
return Row( return Row(
children: [ children: [
Icon(Icons.calendar_today, size: iconSize), const Icon(Icons.calendar_today, size: _iconSize),
SizedBox(width: iconPadding), const SizedBox(width: _iconPadding),
Expanded(flex: 3, child: Text(dateText, strutStyle: Constants.overflowStrutStyle)), Expanded(flex: 3, child: Text(dateText, strutStyle: Constants.overflowStrutStyle)),
Expanded(flex: 2, child: Text(resolution, strutStyle: Constants.overflowStrutStyle)), Expanded(flex: 2, child: Text(resolution, strutStyle: Constants.overflowStrutStyle)),
], ],
); );
} }
}
Widget _buildShootingRow() { class _ShootingRow extends StatelessWidget {
final OverlayMetadata details;
const _ShootingRow(this.details);
@override
Widget build(BuildContext context) {
return Row( return Row(
children: [ children: [
Icon(Icons.camera, size: iconSize), const Icon(Icons.camera, size: _iconSize),
SizedBox(width: iconPadding), const SizedBox(width: _iconPadding),
Expanded(child: Text(details.aperture, strutStyle: Constants.overflowStrutStyle)), Expanded(child: Text(details.aperture, strutStyle: Constants.overflowStrutStyle)),
Expanded(child: Text(details.exposureTime, strutStyle: Constants.overflowStrutStyle)), Expanded(child: Text(details.exposureTime, strutStyle: Constants.overflowStrutStyle)),
Expanded(child: Text(details.focalLength, strutStyle: Constants.overflowStrutStyle)), Expanded(child: Text(details.focalLength, strutStyle: Constants.overflowStrutStyle)),

View file

@ -4,6 +4,8 @@ import 'package:aves/utils/time_utils.dart';
import 'package:aves/widgets/common/blurred.dart'; import 'package:aves/widgets/common/blurred.dart';
import 'package:aves/widgets/fullscreen/overlay/common.dart'; import 'package:aves/widgets/fullscreen/overlay/common.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:tuple/tuple.dart';
import 'package:video_player/video_player.dart'; import 'package:video_player/video_player.dart';
class VideoControlOverlay extends StatefulWidget { class VideoControlOverlay extends StatefulWidget {
@ -75,46 +77,55 @@ class VideoControlOverlayState extends State<VideoControlOverlay> with SingleTic
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final mediaQuery = MediaQuery.of(context); return Selector<MediaQueryData, Tuple3<double, EdgeInsets, EdgeInsets>>(
final viewInsets = widget.viewInsets ?? mediaQuery.viewInsets; selector: (c, mq) => Tuple3(mq.size.width, mq.viewInsets, mq.viewPadding),
final viewPadding = widget.viewPadding ?? mediaQuery.viewPadding; builder: (c, mq, child) {
final safePadding = (viewInsets + viewPadding).copyWith(bottom: 8) + EdgeInsets.symmetric(horizontal: 8.0); final mqWidth = mq.item1;
return Padding( final mqViewInsets = mq.item2;
padding: safePadding, final mqViewPadding = mq.item3;
child: SizedBox(
width: mediaQuery.size.width - safePadding.horizontal, final viewInsets = widget.viewInsets ?? mqViewInsets;
child: Row( final viewPadding = widget.viewPadding ?? mqViewPadding;
mainAxisAlignment: MainAxisAlignment.end, final safePadding = (viewInsets + viewPadding).copyWith(bottom: 8) + EdgeInsets.symmetric(horizontal: 8.0);
children: value.hasError
? [ return Padding(
OverlayButton( padding: safePadding,
scale: scale, child: SizedBox(
child: IconButton( width: mqWidth - safePadding.horizontal,
icon: Icon(Icons.open_in_new), child: Row(
onPressed: () => AndroidAppService.open(entry.uri, entry.mimeType), mainAxisAlignment: MainAxisAlignment.end,
tooltip: 'Open', children: value.hasError
), ? [
), OverlayButton(
] scale: scale,
: [ child: IconButton(
Expanded( icon: Icon(Icons.open_in_new),
child: _buildProgressBar(), onPressed: () => AndroidAppService.open(entry.uri, entry.mimeType),
), tooltip: 'Open',
SizedBox(width: 8), ),
OverlayButton(
scale: scale,
child: IconButton(
icon: AnimatedIcon(
icon: AnimatedIcons.play_pause,
progress: _playPauseAnimation,
), ),
onPressed: _playPause, ]
tooltip: value.isPlaying ? 'Pause' : 'Play', : [
), Expanded(
), child: _buildProgressBar(),
], ),
), SizedBox(width: 8),
), OverlayButton(
scale: scale,
child: IconButton(
icon: AnimatedIcon(
icon: AnimatedIcons.play_pause,
progress: _playPauseAnimation,
),
onPressed: _playPause,
tooltip: value.isPlaying ? 'Pause' : 'Play',
),
),
],
),
),
);
},
); );
} }

View file

@ -4,6 +4,7 @@ import 'dart:ui';
import 'package:aves/model/image_entry.dart'; import 'package:aves/model/image_entry.dart';
import 'package:aves/widgets/common/image_preview.dart'; import 'package:aves/widgets/common/image_preview.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:video_player/video_player.dart'; import 'package:video_player/video_player.dart';
class AvesVideo extends StatefulWidget { class AvesVideo extends StatefulWidget {
@ -57,14 +58,17 @@ class AvesVideoState extends State<AvesVideo> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (value == null) return SizedBox(); if (value == null) return SizedBox();
if (value.hasError) { if (value.hasError) {
final mediaQuery = MediaQuery.of(context); return Selector<MediaQueryData, double>(
final width = min<double>(mediaQuery.size.width, entry.width.toDouble()); selector: (c, mq) => mq.size.width,
return ImagePreview( builder: (c, mqWidth, child) {
entry: entry, final width = min<double>(mqWidth, entry.width.toDouble());
width: width, return ImagePreview(
height: width / entry.aspectRatio, entry: entry,
devicePixelRatio: window.devicePixelRatio, width: width,
builder: (bytes) => Image.memory(bytes), height: width / entry.aspectRatio,
builder: (bytes) => Image.memory(bytes),
);
},
); );
} }
return Center( return Center(

View file

@ -1,6 +1,13 @@
# Generated by pub # Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile # See https://dart.dev/tools/pub/glossary#lockfile
packages: packages:
after_init:
dependency: "direct main"
description:
name: after_init
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.2"
archive: archive:
dependency: transitive dependency: transitive
description: description:
@ -160,6 +167,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.8" version: "1.1.8"
nested:
dependency: transitive
description:
name: nested
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.4"
path: path:
dependency: "direct main" dependency: "direct main"
description: description:
@ -223,6 +237,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "3.0.2" version: "3.0.2"
provider:
dependency: "direct main"
description:
name: provider
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.0"
quiver: quiver:
dependency: transitive dependency: transitive
description: description: