diff --git a/lib/main.dart b/lib/main.dart index 971c9fc62..0698d452c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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/common/fake_app_bar.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/services.dart'; import 'package:flutter_native_timezone/flutter_native_timezone.dart'; @@ -73,7 +74,9 @@ class _HomePageState extends State { Future setup() async { debugPrint('$runtimeType setup start, elapsed=${stopwatch.elapsed}'); // 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) { unawaited(SystemNavigator.pop()); return; @@ -123,12 +126,14 @@ class _HomePageState extends State { @override Widget build(BuildContext context) { - return Scaffold( - // fake app bar so that content is safe from status bar, even though we use a SliverAppBar - appBar: FakeAppBar(), - body: AllCollectionPage(collection: localMediaCollection), - drawer: AllCollectionDrawer(collection: localMediaCollection), - resizeToAvoidBottomInset: false, + return MediaQueryDataProvider( + child: Scaffold( + // fake app bar so that content is safe from status bar, even though we use a SliverAppBar + appBar: FakeAppBar(), + body: AllCollectionPage(collection: localMediaCollection), + drawer: AllCollectionDrawer(collection: localMediaCollection), + resizeToAvoidBottomInset: false, + ), ); } } diff --git a/lib/model/image_collection.dart b/lib/model/image_collection.dart index 94fc46728..37dcc183c 100644 --- a/lib/model/image_collection.dart +++ b/lib/model/image_collection.dart @@ -59,7 +59,9 @@ class ImageCollection with ChangeNotifier { } break; case SortFactor.size: - sections = Map.fromEntries([MapEntry('All', _rawEntries)]); + sections = Map.fromEntries([ + MapEntry('All', _rawEntries) + ]); break; } notifyListeners(); diff --git a/lib/model/image_entry.dart b/lib/model/image_entry.dart index cab1688fd..55f4e1317 100644 --- a/lib/model/image_entry.dart +++ b/lib/model/image_entry.dart @@ -181,7 +181,11 @@ class ImageEntry { // admin area examples: Seoul, Geneva, null // locality examples: Mapo-gu, Geneva, Annecy return LinkedHashSet.of( - [addressDetails.countryName, addressDetails.adminArea, addressDetails.locality], + [ + addressDetails.countryName, + addressDetails.adminArea, + addressDetails.locality + ], ).where((part) => part != null && part.isNotEmpty).join(', '); } diff --git a/lib/model/settings.dart b/lib/model/settings.dart index 785dec2b8..16e9f2c06 100644 --- a/lib/model/settings.dart +++ b/lib/model/settings.dart @@ -1,5 +1,3 @@ -import 'dart:ui'; - import 'package:aves/model/image_collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; @@ -22,28 +20,8 @@ class Settings { static const infoMapZoomKey = 'info_map_zoom'; static const catalogTimeZoneKey = 'catalog_time_zone'; - // state - static const windowMetricsKey = 'window_metrics'; - Future init() async { 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); @@ -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, - }); -} diff --git a/lib/utils/constants.dart b/lib/utils/constants.dart index 312efcb70..98ee294d0 100644 --- a/lib/utils/constants.dart +++ b/lib/utils/constants.dart @@ -4,4 +4,4 @@ class Constants { // 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 static const overflowStrutStyle = StrutStyle(height: 1.3); -} \ No newline at end of file +} diff --git a/lib/utils/geo_utils.dart b/lib/utils/geo_utils.dart index 0a98c2510..fa24c3bab 100644 --- a/lib/utils/geo_utils.dart +++ b/lib/utils/geo_utils.dart @@ -11,7 +11,10 @@ String _decimal2sexagesimal(final double dec) { // NumberFormat is necessary to create digit after comma if the value // has no decimal point (only necessary for browser) final List tmp = NumberFormat('0.0#####').format(_round(value, decimals: 10)).split('.'); - return [int.parse(tmp[0]).abs(), int.parse(tmp[1])]; + return [ + int.parse(tmp[0]).abs(), + int.parse(tmp[1]) + ]; } final List parts = _split(dec); @@ -34,5 +37,8 @@ List toDMS(Tuple2 latLng) { if (latLng == null) return []; final lat = latLng.item1; 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'}' + ]; } diff --git a/lib/widgets/album/all_collection_drawer.dart b/lib/widgets/album/all_collection_drawer.dart index 71308e3bc..6dc52a838 100644 --- a/lib/widgets/album/all_collection_drawer.dart +++ b/lib/widgets/album/all_collection_drawer.dart @@ -52,10 +52,26 @@ class AllCollectionDrawer extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row(children: [Icon(Icons.photo_library), SizedBox(width: 4), 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}')]), + Row(children: [ + Icon(Icons.photo_library), + SizedBox(width: 4), + 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}') + ]), ], ), ], diff --git a/lib/widgets/album/filtered_collection_page.dart b/lib/widgets/album/filtered_collection_page.dart index 5c67770ed..e36c3c10d 100644 --- a/lib/widgets/album/filtered_collection_page.dart +++ b/lib/widgets/album/filtered_collection_page.dart @@ -1,6 +1,7 @@ import 'package:aves/model/image_collection.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/widgets/album/thumbnail_collection.dart'; +import 'package:aves/widgets/common/media_query_data_provider.dart'; import 'package:flutter/material.dart'; class FilteredCollectionPage extends StatelessWidget { @@ -14,15 +15,17 @@ class FilteredCollectionPage extends StatelessWidget { @override Widget build(BuildContext context) { - return Scaffold( - body: ThumbnailCollection( - collection: collection, - appBar: SliverAppBar( - title: Text(title), - floating: true, + return MediaQueryDataProvider( + child: Scaffold( + body: ThumbnailCollection( + collection: collection, + appBar: SliverAppBar( + title: Text(title), + floating: true, + ), ), + resizeToAvoidBottomInset: false, ), - resizeToAvoidBottomInset: false, ); } } diff --git a/lib/widgets/album/thumbnail.dart b/lib/widgets/album/thumbnail.dart index e9b402547..6d4ecbeaf 100644 --- a/lib/widgets/album/thumbnail.dart +++ b/lib/widgets/album/thumbnail.dart @@ -8,13 +8,11 @@ import 'package:flutter/material.dart'; class Thumbnail extends StatelessWidget { final ImageEntry entry; final double extent; - final double devicePixelRatio; const Thumbnail({ Key key, @required this.entry, @required this.extent, - @required this.devicePixelRatio, }) : super(key: key); @override @@ -23,7 +21,6 @@ class Thumbnail extends StatelessWidget { entry: entry, width: extent, height: extent, - devicePixelRatio: devicePixelRatio, builder: (bytes) { return Hero( tag: entry.uri, diff --git a/lib/widgets/album/thumbnail_collection.dart b/lib/widgets/album/thumbnail_collection.dart index 2d30cd097..9e95eb82d 100644 --- a/lib/widgets/album/thumbnail_collection.dart +++ b/lib/widgets/album/thumbnail_collection.dart @@ -1,5 +1,3 @@ -import 'dart:ui'; - import 'package:aves/model/image_collection.dart'; import 'package:aves/model/image_entry.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:flutter/material.dart'; import 'package:flutter_sticky_header/flutter_sticky_header.dart'; +import 'package:provider/provider.dart'; class ThumbnailCollection extends AnimatedWidget { final ImageCollection collection; @@ -22,10 +21,13 @@ class ThumbnailCollection extends AnimatedWidget { @override Widget build(BuildContext context) { - return ThumbnailCollectionContent( - collection: collection, - appBar: appBar, - screenWidth: MediaQuery.of(context).size.width, + return Selector( + selector: (c, mq) => mq.size.width, + builder: (c, mqWidth, child) => ThumbnailCollectionContent( + collection: collection, + appBar: appBar, + screenWidth: mqWidth, + ), ); } } @@ -48,7 +50,6 @@ class ThumbnailCollectionContent extends StatelessWidget { @override Widget build(BuildContext context) { - final bottomInsets = MediaQuery.of(context).viewInsets.bottom; final sectionKeys = _sections.keys.toList(); double topPadding = 0; if (appBar != null) { @@ -61,35 +62,39 @@ class ThumbnailCollectionContent extends StatelessWidget { } return SafeArea( - child: DraggableScrollbar.arrows( - child: CustomScrollView( - controller: _scrollController, - slivers: [ - if (appBar != null) appBar, - ...sectionKeys.map((sectionKey) { - Widget sliver = SectionSliver( - // need key to prevent section header mismatch - // but it should not be unique key, otherwise sections are rebuilt when changing page - key: ValueKey(sectionKey), - collection: collection, - sections: _sections, - sectionKey: sectionKey, - screenWidth: screenWidth, - ); - if (sectionKey == sectionKeys.last) { - sliver = SliverPadding( - padding: EdgeInsets.only(bottom: bottomInsets), - sliver: sliver, + child: Selector( + selector: (c, mq) => mq.viewInsets.bottom, + builder: (c, mqViewInsetsBottom, child) => DraggableScrollbar.arrows( + child: CustomScrollView( + controller: _scrollController, + slivers: [ + if (appBar != null) appBar, + ...sectionKeys.map((sectionKey) { + Widget sliver = SectionSliver( + // need key to prevent section header mismatch + // but it should not be unique key, otherwise sections are rebuilt when changing page + key: ValueKey(sectionKey), + collection: collection, + sections: _sections, + sectionKey: sectionKey, + screenWidth: screenWidth, ); - } - return sliver; - }), - ], - ), - controller: _scrollController, - padding: EdgeInsets.only( - top: topPadding, - bottom: bottomInsets, + if (sectionKey == sectionKeys.last) { + sliver = SliverPadding( + padding: EdgeInsets.only(bottom: mqViewInsetsBottom), + sliver: sliver, + ); + } + return sliver; + }), + ], + ), + 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( entry: entry, extent: screenWidth / columnCount, - devicePixelRatio: window.devicePixelRatio, ), ); }, diff --git a/lib/widgets/common/image_preview.dart b/lib/widgets/common/image_preview.dart index 6ce2374a2..7ce43a46a 100644 --- a/lib/widgets/common/image_preview.dart +++ b/lib/widgets/common/image_preview.dart @@ -1,13 +1,15 @@ import 'dart:typed_data'; +import 'package:after_init/after_init.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/model/image_file_service.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; import 'package:transparent_image/transparent_image.dart'; class ImagePreview extends StatefulWidget { final ImageEntry entry; - final double width, height, devicePixelRatio; + final double width, height; final Widget Function(Uint8List bytes) builder; const ImagePreview({ @@ -15,7 +17,6 @@ class ImagePreview extends StatefulWidget { @required this.entry, @required this.width, @required this.height, - @required this.devicePixelRatio, @required this.builder, }) : super(key: key); @@ -23,9 +24,10 @@ class ImagePreview extends StatefulWidget { State createState() => ImagePreviewState(); } -class ImagePreviewState extends State { +class ImagePreviewState extends State with AfterInitMixin { Future _byteLoader; Listenable _entryChangeNotifier; + double _devicePixelRatio; ImageEntry get entry => widget.entry; @@ -34,8 +36,16 @@ class ImagePreviewState extends State { @override void initState() { super.initState(); - _entryChangeNotifier = Listenable.merge([entry.imageChangeNotifier, entry.metadataChangeNotifier]); + _entryChangeNotifier = Listenable.merge([ + entry.imageChangeNotifier, + entry.metadataChangeNotifier + ]); _entryChangeNotifier.addListener(onEntryChange); + } + + @override + void didInitState() { + _devicePixelRatio = Provider.of(context, listen: false).devicePixelRatio; initByteLoader(); } @@ -47,8 +57,8 @@ class ImagePreviewState extends State { } initByteLoader() { - final width = (widget.width * widget.devicePixelRatio).round(); - final height = (widget.height * widget.devicePixelRatio).round(); + final width = (widget.width * _devicePixelRatio).round(); + final height = (widget.height * _devicePixelRatio).round(); _byteLoader = ImageFileService.getImageBytes(widget.entry, width, height); } diff --git a/lib/widgets/common/media_query_data_provider.dart b/lib/widgets/common/media_query_data_provider.dart new file mode 100644 index 000000000..25ba8c4c7 --- /dev/null +++ b/lib/widgets/common/media_query_data_provider.dart @@ -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.value( + value: MediaQuery.of(context), + child: child, + ); + } +} diff --git a/lib/widgets/debug_page.dart b/lib/widgets/debug_page.dart index b5c3ef090..780b12a05 100644 --- a/lib/widgets/debug_page.dart +++ b/lib/widgets/debug_page.dart @@ -3,6 +3,7 @@ import 'package:aves/model/image_metadata.dart'; import 'package:aves/model/metadata_db.dart'; import 'package:aves/model/settings.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:flutter/material.dart'; import 'package:flutter/scheduler.dart'; @@ -35,59 +36,61 @@ class DebugPageState extends State { final catalogued = entries.where((entry) => entry.isCatalogued); final withGps = catalogued.where((entry) => entry.hasGps); final located = withGps.where((entry) => entry.isLocated); - return Scaffold( - appBar: AppBar( - title: Text('Info'), - ), - body: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Paths'), - Text('DCIM path: ${androidFileUtils.dcimPath}'), - Text('pictures path: ${androidFileUtils.picturesPath}'), - Divider(), - Text('Settings'), - Text('collectionGroupFactor: ${settings.collectionGroupFactor}'), - Text('collectionSortFactor: ${settings.collectionSortFactor}'), - Text('infoMapZoom: ${settings.infoMapZoom}'), - Divider(), - Text('Entries: ${entries.length}'), - ...byMimeTypes.keys.map((mimeType) => Text('- $mimeType: ${byMimeTypes[mimeType].length}')), - Text('Catalogued: ${catalogued.length}'), - Text('With GPS: ${withGps.length}'), - Text('With address: ${located.length}'), - Divider(), - RaisedButton( - onPressed: () => metadataDb.reset(), - child: Text('Reset DB'), - ), - FutureBuilder( - future: _dbMetadataLoader, - builder: (futureContext, AsyncSnapshot> snapshot) { - if (snapshot.hasError) return Text(snapshot.error); - if (snapshot.connectionState != ConnectionState.done) return SizedBox.shrink(); - return Text('DB metadata rows: ${snapshot.data.length}'); - }, - ), - FutureBuilder( - future: _dbAddressLoader, - builder: (futureContext, AsyncSnapshot> snapshot) { - if (snapshot.hasError) return Text(snapshot.error); - if (snapshot.connectionState != ConnectionState.done) return SizedBox.shrink(); - return Text('DB address rows: ${snapshot.data.length}'); - }, - ), - Divider(), - Text('Time dilation'), - Slider( - value: timeDilation, - onChanged: (v) => setState(() => timeDilation = v), - min: 1.0, - max: 10.0, - divisions: 9, - label: '$timeDilation', - ), - ], + return MediaQueryDataProvider( + child: Scaffold( + appBar: AppBar( + title: Text('Info'), + ), + body: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Paths'), + Text('DCIM path: ${androidFileUtils.dcimPath}'), + Text('pictures path: ${androidFileUtils.picturesPath}'), + Divider(), + Text('Settings'), + Text('collectionGroupFactor: ${settings.collectionGroupFactor}'), + Text('collectionSortFactor: ${settings.collectionSortFactor}'), + Text('infoMapZoom: ${settings.infoMapZoom}'), + Divider(), + Text('Entries: ${entries.length}'), + ...byMimeTypes.keys.map((mimeType) => Text('- $mimeType: ${byMimeTypes[mimeType].length}')), + Text('Catalogued: ${catalogued.length}'), + Text('With GPS: ${withGps.length}'), + Text('With address: ${located.length}'), + Divider(), + RaisedButton( + onPressed: () => metadataDb.reset(), + child: Text('Reset DB'), + ), + FutureBuilder( + future: _dbMetadataLoader, + builder: (futureContext, AsyncSnapshot> snapshot) { + if (snapshot.hasError) return Text(snapshot.error); + if (snapshot.connectionState != ConnectionState.done) return SizedBox.shrink(); + return Text('DB metadata rows: ${snapshot.data.length}'); + }, + ), + FutureBuilder( + future: _dbAddressLoader, + builder: (futureContext, AsyncSnapshot> snapshot) { + if (snapshot.hasError) return Text(snapshot.error); + if (snapshot.connectionState != ConnectionState.done) return SizedBox.shrink(); + return Text('DB address rows: ${snapshot.data.length}'); + }, + ), + Divider(), + Text('Time dilation'), + Slider( + value: timeDilation, + onChanged: (v) => setState(() => timeDilation = v), + min: 1.0, + max: 10.0, + divisions: 9, + label: '$timeDilation', + ), + ], + ), ), ); } diff --git a/lib/widgets/fullscreen/fullscreen_page.dart b/lib/widgets/fullscreen/fullscreen_page.dart index 8b6e95afc..751a589bf 100644 --- a/lib/widgets/fullscreen/fullscreen_page.dart +++ b/lib/widgets/fullscreen/fullscreen_page.dart @@ -3,6 +3,7 @@ import 'dart:math'; import 'package:aves/model/image_collection.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/image_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/services.dart'; import 'package:photo_view/photo_view.dart'; +import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; import 'package:video_player/video_player.dart'; @@ -28,13 +30,15 @@ class FullscreenPage extends AnimatedWidget { @override Widget build(BuildContext context) { - return Scaffold( - body: FullscreenBody( - collection: collection, - initialUri: initialUri, + return MediaQueryDataProvider( + child: Scaffold( + body: FullscreenBody( + collection: collection, + initialUri: initialUri, + ), + backgroundColor: Colors.black, + resizeToAvoidBottomInset: false, ), - backgroundColor: Colors.black, - resizeToAvoidBottomInset: false, ); } } @@ -92,25 +96,25 @@ class FullscreenBodyState extends State with SingleTickerProvide parent: _overlayAnimationController, curve: Curves.easeOutQuart, )); - _overlayVisible.addListener(onOverlayVisibleChange); + _overlayVisible.addListener(_onOverlayVisibleChange); _actionDelegate = FullscreenActionDelegate( collection: collection, - showInfo: () => goToVerticalPage(infoPage), + showInfo: () => _goToVerticalPage(infoPage), ); - initVideoController(); - initOverlay(); + _initVideoController(); + _initOverlay(); } - initOverlay() async { + _initOverlay() async { // wait for MaterialPageRoute.transitionDuration // to show overlay after hero animation is complete await Future.delayed(Duration(milliseconds: (300 * timeDilation).toInt())); - onOverlayVisibleChange(); + await _onOverlayVisibleChange(); } @override void dispose() { - _overlayVisible.removeListener(onOverlayVisibleChange); + _overlayVisible.removeListener(_onOverlayVisibleChange); _videoControllers.forEach((kv) => kv.item2.dispose()); super.dispose(); } @@ -121,7 +125,7 @@ class FullscreenBodyState extends State with SingleTickerProvide return WillPopScope( onWillPop: () { if (_currentVerticalPage == infoPage) { - goToVerticalPage(imagePage); + _goToVerticalPage(imagePage); return Future.value(false); } _onLeave(); @@ -142,14 +146,14 @@ class FullscreenBodyState extends State with SingleTickerProvide collection: collection, pageController: _horizontalPager, onTap: () => _overlayVisible.value = !_overlayVisible.value, - onPageChanged: onHorizontalPageChanged, + onPageChanged: _onHorizontalPageChanged, onScaleChanged: (state) => setState(() => _isInitialScale = state == PhotoViewScaleState.initial), videoControllers: _videoControllers, ), ), NotificationListener( onNotification: (notification) { - if (notification is BackUpNotification) goToVerticalPage(imagePage); + if (notification is BackUpNotification) _goToVerticalPage(imagePage); return false; }, child: InfoPage(collection: collection, entry: entry), @@ -201,7 +205,7 @@ class FullscreenBodyState extends State with SingleTickerProvide ]; } - goToVerticalPage(int page) { + Future _goToVerticalPage(int page) { return _verticalPager.animateToPage( page, duration: Duration(milliseconds: 350), @@ -209,7 +213,7 @@ class FullscreenBodyState extends State with SingleTickerProvide ); } - _onVerticalPageChanged(page) { + void _onVerticalPageChanged(page) { setState(() => _currentVerticalPage = page); if (_currentVerticalPage == transitionPage) { _onLeave(); @@ -217,20 +221,22 @@ class FullscreenBodyState extends State 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 _onOverlayVisibleChange() async { if (_overlayVisible.value) { _showSystemUI(); _overlayAnimationController.forward(); } else { - final mediaQuery = MediaQuery.of(context); - _frozenViewInsets = mediaQuery.viewInsets; - _frozenViewPadding = mediaQuery.viewPadding; + final mediaQuery = Provider.of(context, listen: false); + setState(() { + _frozenViewInsets = mediaQuery.viewInsets; + _frozenViewPadding = mediaQuery.viewPadding; + }); _hideSystemUI(); await _overlayAnimationController.reverse(); _frozenViewInsets = null; @@ -238,16 +244,16 @@ class FullscreenBodyState extends State with SingleTickerProvide } } - onHorizontalPageChanged(int page) { + void _onHorizontalPageChanged(int page) { _currentHorizontalPage = page; - pauseVideoControllers(); - initVideoController(); + _pauseVideoControllers(); + _initVideoController(); 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; if (entry == null || !entry.isVideo) return; diff --git a/lib/widgets/fullscreen/image_page.dart b/lib/widgets/fullscreen/image_page.dart index ab37cb527..117e74a69 100644 --- a/lib/widgets/fullscreen/image_page.dart +++ b/lib/widgets/fullscreen/image_page.dart @@ -6,6 +6,7 @@ import 'package:aves/widgets/fullscreen/video.dart'; import 'package:flutter/material.dart'; import 'package:photo_view/photo_view.dart'; import 'package:photo_view/photo_view_gallery.dart'; +import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; import 'package:video_player/video_player.dart'; @@ -36,45 +37,48 @@ class ImagePageState extends State with AutomaticKeepAliveClientMixin @override Widget build(BuildContext context) { super.build(context); - return PhotoViewGallery.builder( - itemCount: entries.length, - builder: (galleryContext, index) { - final entry = entries[index]; - if (entry.isVideo) { - final videoController = widget.videoControllers.firstWhere((kv) => kv.item1 == entry.path, orElse: () => null)?.item2; - return PhotoViewGalleryPageOptions.customChild( - child: videoController != null - ? AvesVideo( - entry: entry, - controller: videoController, - ) - : SizedBox(), - childSize: MediaQuery.of(galleryContext).size, - // no hero as most videos fullscreen image is different from its thumbnail + return Selector( + selector: (c, mq) => mq.size, + builder: (c, mqSize, child) => PhotoViewGallery.builder( + itemCount: entries.length, + builder: (galleryContext, index) { + final entry = entries[index]; + if (entry.isVideo) { + final videoController = widget.videoControllers.firstWhere((kv) => kv.item1 == entry.path, orElse: () => null)?.item2; + return PhotoViewGalleryPageOptions.customChild( + child: videoController != null + ? AvesVideo( + entry: entry, + controller: videoController, + ) + : 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, 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, - initialScale: PhotoViewComputedScale.contained, - onTapUp: (tapContext, details, value) => widget.onTap?.call(), - ); - }, - loadingChild: Center( - child: CircularProgressIndicator(), + }, + loadingChild: Center( + child: CircularProgressIndicator(), + ), + backgroundDecoration: BoxDecoration(color: Colors.transparent), + pageController: widget.pageController, + onPageChanged: widget.onPageChanged, + scaleStateChangedCallback: widget.onScaleChanged, + scrollPhysics: BouncingScrollPhysics(), ), - backgroundDecoration: BoxDecoration(color: Colors.transparent), - pageController: widget.pageController, - onPageChanged: widget.onPageChanged, - scaleStateChangedCallback: widget.onScaleChanged, - scrollPhysics: BouncingScrollPhysics(), ); } diff --git a/lib/widgets/fullscreen/info/basic_section.dart b/lib/widgets/fullscreen/info/basic_section.dart index d6bb14565..78c9bc191 100644 --- a/lib/widgets/fullscreen/info/basic_section.dart +++ b/lib/widgets/fullscreen/info/basic_section.dart @@ -35,6 +35,9 @@ class BasicSection extends StatelessWidget { List _buildVideoRows() { final rotation = entry.catalogMetadata?.videoRotation; 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°') + ]; } } diff --git a/lib/widgets/fullscreen/info/info_page.dart b/lib/widgets/fullscreen/info/info_page.dart index 92cff2065..d0d839b12 100644 --- a/lib/widgets/fullscreen/info/info_page.dart +++ b/lib/widgets/fullscreen/info/info_page.dart @@ -1,10 +1,13 @@ import 'package:aves/model/image_collection.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/location_section.dart'; import 'package:aves/widgets/fullscreen/info/metadata_section.dart'; import 'package:aves/widgets/fullscreen/info/xmp_section.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:tuple/tuple.dart'; class InfoPage extends StatefulWidget { final ImageCollection collection; @@ -33,44 +36,51 @@ class InfoPageState extends State { @override Widget build(BuildContext context) { - // use MediaQuery instead of unreliable OrientationBuilder - final orientation = MediaQuery.of(context).orientation; - final bottomInsets = MediaQuery.of(context).viewInsets.bottom; - return Scaffold( - appBar: AppBar( - leading: IconButton( - icon: Icon(Icons.arrow_upward), - onPressed: () => BackUpNotification().dispatch(context), - tooltip: 'Back to image', + return MediaQueryDataProvider( + child: Scaffold( + appBar: AppBar( + leading: IconButton( + icon: const Icon(Icons.arrow_upward), + onPressed: () => BackUpNotification().dispatch(context), + tooltip: 'Back to image', + ), + title: const Text('Info'), ), - title: Text('Info'), - ), - body: SafeArea( - child: NotificationListener( - onNotification: _handleTopScroll, - child: ListView( - padding: EdgeInsets.all(8.0) + EdgeInsets.only(bottom: bottomInsets), - children: [ - if (orientation == Orientation.landscape && entry.hasGps) - Row( - crossAxisAlignment: CrossAxisAlignment.start, + body: SafeArea( + child: NotificationListener( + onNotification: _handleTopScroll, + child: Selector>( + selector: (c, mq) => Tuple2(mq.orientation, mq.viewInsets.bottom), + builder: (c, mq, child) { + final mqOrientation = mq.item1; + final mqViewInsetsBottom = mq.item2; + + return ListView( + padding: const EdgeInsets.all(8.0) + EdgeInsets.only(bottom: mqViewInsetsBottom), children: [ - Expanded(child: BasicSection(entry: entry)), - SizedBox(width: 8), - Expanded(child: LocationSection(entry: entry, showTitle: false)), + if (mqOrientation == Orientation.landscape && entry.hasGps) + Row( + 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) { return Row( children: [ - Expanded(child: Divider(color: Colors.white70)), + const Expanded(child: Divider(color: Colors.white70)), Padding( - padding: EdgeInsets.all(16.0), + padding: const EdgeInsets.all(16.0), child: Text( title, 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 Widget build(BuildContext context) { return Padding( - padding: EdgeInsets.symmetric(vertical: 4.0), + padding: const EdgeInsets.symmetric(vertical: 4.0), child: RichText( text: TextSpan( style: TextStyle(fontFamily: 'Concourse'), diff --git a/lib/widgets/fullscreen/info/location_section.dart b/lib/widgets/fullscreen/info/location_section.dart index d96b2c122..aa5f9252c 100644 --- a/lib/widgets/fullscreen/info/location_section.dart +++ b/lib/widgets/fullscreen/info/location_section.dart @@ -13,7 +13,12 @@ class LocationSection extends AnimatedWidget { Key key, @required this.entry, @required this.showTitle, - }) : super(key: key, listenable: Listenable.merge([entry.metadataChangeNotifier, entry.addressChangeNotifier])); + }) : super( + key: key, + listenable: Listenable.merge([ + entry.metadataChangeNotifier, + entry.addressChangeNotifier + ])); @override Widget build(BuildContext context) { diff --git a/lib/widgets/fullscreen/info/metadata_section.dart b/lib/widgets/fullscreen/info/metadata_section.dart index 68c6a48da..34c1616a1 100644 --- a/lib/widgets/fullscreen/info/metadata_section.dart +++ b/lib/widgets/fullscreen/info/metadata_section.dart @@ -4,6 +4,7 @@ import 'package:aves/model/image_entry.dart'; import 'package:aves/model/metadata_service.dart'; import 'package:aves/widgets/fullscreen/info/info_page.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; class MetadataSection extends StatefulWidget { final ImageEntry entry; @@ -37,50 +38,53 @@ class MetadataSectionState extends State { @override Widget build(BuildContext context) { - return FutureBuilder( - future: _metadataLoader, - builder: (futureContext, AsyncSnapshot snapshot) { - if (snapshot.hasError) return Text(snapshot.error); - if (snapshot.connectionState != ConnectionState.done) return SizedBox.shrink(); - final metadataMap = snapshot.data.cast(); - final directoryNames = metadataMap.keys.toList()..sort(); + return Selector( + selector: (c, mq) => mq.size.width, + builder: (c, mqWidth, child) => FutureBuilder( + future: _metadataLoader, + builder: (futureContext, AsyncSnapshot snapshot) { + if (snapshot.hasError) return Text(snapshot.error); + if (snapshot.connectionState != ConnectionState.done) return SizedBox.shrink(); + final metadataMap = snapshot.data.cast(); + final directoryNames = metadataMap.keys.toList()..sort(); - Widget content; - if (MediaQuery.of(context).size.width > 400) { - final first = [], second = []; - var firstItemCount = 0, secondItemCount = 0; - var firstIndex = 0, secondIndex = directoryNames.length - 1; - while (firstIndex <= secondIndex) { - if (firstItemCount <= secondItemCount) { - final directoryName = directoryNames[firstIndex++]; - first.add(directoryName); - firstItemCount += 2 + metadataMap[directoryName].length; - } else { - final directoryName = directoryNames[secondIndex--]; - second.insert(0, directoryName); - secondItemCount += 2 + metadataMap[directoryName].length; + Widget content; + if (mqWidth > 400) { + final first = [], second = []; + var firstItemCount = 0, secondItemCount = 0; + var firstIndex = 0, secondIndex = directoryNames.length - 1; + while (firstIndex <= secondIndex) { + if (firstItemCount <= secondItemCount) { + final directoryName = directoryNames[firstIndex++]; + first.add(directoryName); + firstItemCount += 2 + metadataMap[directoryName].length; + } else { + final directoryName = directoryNames[secondIndex--]; + second.insert(0, directoryName); + 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: [ - Expanded(child: getMetadataColumn(metadataMap, first)), - SizedBox(width: 8), - Expanded(child: getMetadataColumn(metadataMap, second)), + SectionRow('Metadata'), + content, ], ); - } else { - content = getMetadataColumn(metadataMap, directoryNames); - } - - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - SectionRow('Metadata'), - content, - ], - ); - }, + }, + ), ); } diff --git a/lib/widgets/fullscreen/overlay/bottom.dart b/lib/widgets/fullscreen/overlay/bottom.dart index 86772d958..606d7aa76 100644 --- a/lib/widgets/fullscreen/overlay/bottom.dart +++ b/lib/widgets/fullscreen/overlay/bottom.dart @@ -9,6 +9,8 @@ import 'package:aves/utils/geo_utils.dart'; import 'package:aves/widgets/common/blurred.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; +import 'package:tuple/tuple.dart'; class FullscreenBottomOverlay extends StatefulWidget { final List entries; @@ -32,6 +34,8 @@ class _FullscreenBottomOverlayState extends State { ImageEntry _lastEntry; OverlayMetadata _lastDetails; + static const innerPadding = EdgeInsets.symmetric(vertical: 4, horizontal: 8); + ImageEntry get entry { final entries = widget.entries; final index = widget.index; @@ -56,65 +60,73 @@ class _FullscreenBottomOverlayState extends State { @override 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( child: BlurredRect( - child: Container( - color: Colors.black26, - padding: viewInsets + viewPadding.copyWith(top: 0), - child: Padding( - padding: innerPadding, - child: FutureBuilder( - future: _detailLoader, - builder: (futureContext, AsyncSnapshot 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, - ); - }, - ), - ), + child: Selector>( + selector: (c, mq) => Tuple3(mq.size.width, mq.viewInsets, mq.viewPadding), + builder: (c, mq, child) { + final mqWidth = mq.item1; + final mqViewInsets = mq.item2; + final mqViewPadding = mq.item3; + + final viewInsets = widget.viewInsets ?? mqViewInsets; + final viewPadding = widget.viewPadding ?? mqViewPadding; + final overlayContentMaxWidth = mqWidth - viewPadding.horizontal - innerPadding.horizontal; + + return Container( + color: Colors.black26, + padding: viewInsets + viewPadding.copyWith(top: 0), + child: Padding( + padding: innerPadding, + child: FutureBuilder( + future: _detailLoader, + builder: (futureContext, AsyncSnapshot 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 { final ImageEntry entry; final OverlayMetadata details; final String position; final double maxWidth; - static const double interRowPadding = 2.0; - static const double iconPadding = 8.0; - static const double iconSize = 16.0; - static const double subRowMinWidth = 300.0; - - _FullscreenBottomOverlayContent({this.entry, this.details, this.position, this.maxWidth}); + _FullscreenBottomOverlayContent({ + this.entry, + this.details, + this.position, + this.maxWidth, + }); @override 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( style: Theme.of(context).textTheme.body1.copyWith( - shadows: [ + shadows: const [ Shadow( color: Colors.black87, offset: Offset(0.5, 1.0), @@ -123,49 +135,64 @@ class _FullscreenBottomOverlayContent extends StatelessWidget { ), overflow: TextOverflow.ellipsis, maxLines: 1, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox( - width: maxWidth, - child: Text('$position – ${entry.title}', strutStyle: Constants.overflowStrutStyle), - ), - if (entry.hasGps) - Container( - padding: EdgeInsets.only(top: interRowPadding), - width: subRowWidth, - child: _buildLocationRow(), - ), - if (twoColumns) - Padding( - padding: EdgeInsets.only(top: interRowPadding), - child: Row( - children: [ - Container(width: subRowWidth, child: _buildDateRow()), - if (hasShootingDetails) Container(width: subRowWidth, child: _buildShootingRow()), - ], + child: Selector( + selector: (c, mq) => mq.orientation, + builder: (c, orientation, child) { + final twoColumns = orientation == Orientation.landscape && maxWidth / 2 > _subRowMinWidth; + final subRowWidth = twoColumns ? min(_subRowMinWidth, maxWidth / 2) : maxWidth; + final hasShootingDetails = details != null && !details.isEmpty; + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: maxWidth, + child: Text('$position – ${entry.title}', strutStyle: Constants.overflowStrutStyle), ), - ) - else ...[ - Container( - padding: EdgeInsets.only(top: interRowPadding), - width: subRowWidth, - child: _buildDateRow(), - ), - if (hasShootingDetails) - Container( - padding: EdgeInsets.only(top: interRowPadding), - width: subRowWidth, - child: _buildShootingRow(), - ), - ], - ], + if (entry.hasGps) + Container( + padding: const EdgeInsets.only(top: _interRowPadding), + width: subRowWidth, + child: _LocationRow(entry), + ), + if (twoColumns) + Padding( + padding: const EdgeInsets.only(top: _interRowPadding), + child: Row( + children: [ + 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; if (entry.isLocated) { location = entry.shortAddress; @@ -174,32 +201,46 @@ class _FullscreenBottomOverlayContent extends StatelessWidget { } return Row( children: [ - Icon(Icons.place, size: iconSize), - SizedBox(width: iconPadding), + const Icon(Icons.place, size: _iconSize), + const SizedBox(width: _iconPadding), 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 dateText = '${DateFormat.yMMMd().format(date)} at ${DateFormat.Hm().format(date)}'; final resolution = '${entry.width} × ${entry.height}'; return Row( children: [ - Icon(Icons.calendar_today, size: iconSize), - SizedBox(width: iconPadding), + const Icon(Icons.calendar_today, size: _iconSize), + const SizedBox(width: _iconPadding), Expanded(flex: 3, child: Text(dateText, 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( children: [ - Icon(Icons.camera, size: iconSize), - SizedBox(width: iconPadding), + const Icon(Icons.camera, size: _iconSize), + const SizedBox(width: _iconPadding), Expanded(child: Text(details.aperture, strutStyle: Constants.overflowStrutStyle)), Expanded(child: Text(details.exposureTime, strutStyle: Constants.overflowStrutStyle)), Expanded(child: Text(details.focalLength, strutStyle: Constants.overflowStrutStyle)), diff --git a/lib/widgets/fullscreen/overlay/video.dart b/lib/widgets/fullscreen/overlay/video.dart index 8d27c42cc..7d9354f12 100644 --- a/lib/widgets/fullscreen/overlay/video.dart +++ b/lib/widgets/fullscreen/overlay/video.dart @@ -4,6 +4,8 @@ import 'package:aves/utils/time_utils.dart'; import 'package:aves/widgets/common/blurred.dart'; import 'package:aves/widgets/fullscreen/overlay/common.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:tuple/tuple.dart'; import 'package:video_player/video_player.dart'; class VideoControlOverlay extends StatefulWidget { @@ -75,46 +77,55 @@ class VideoControlOverlayState extends State with SingleTic @override Widget build(BuildContext context) { - final mediaQuery = MediaQuery.of(context); - final viewInsets = widget.viewInsets ?? mediaQuery.viewInsets; - final viewPadding = widget.viewPadding ?? mediaQuery.viewPadding; - final safePadding = (viewInsets + viewPadding).copyWith(bottom: 8) + EdgeInsets.symmetric(horizontal: 8.0); - return Padding( - padding: safePadding, - child: SizedBox( - width: mediaQuery.size.width - safePadding.horizontal, - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: value.hasError - ? [ - OverlayButton( - scale: scale, - child: IconButton( - icon: Icon(Icons.open_in_new), - onPressed: () => AndroidAppService.open(entry.uri, entry.mimeType), - tooltip: 'Open', - ), - ), - ] - : [ - Expanded( - child: _buildProgressBar(), - ), - SizedBox(width: 8), - OverlayButton( - scale: scale, - child: IconButton( - icon: AnimatedIcon( - icon: AnimatedIcons.play_pause, - progress: _playPauseAnimation, + return Selector>( + selector: (c, mq) => Tuple3(mq.size.width, mq.viewInsets, mq.viewPadding), + builder: (c, mq, child) { + final mqWidth = mq.item1; + final mqViewInsets = mq.item2; + final mqViewPadding = mq.item3; + + final viewInsets = widget.viewInsets ?? mqViewInsets; + final viewPadding = widget.viewPadding ?? mqViewPadding; + final safePadding = (viewInsets + viewPadding).copyWith(bottom: 8) + EdgeInsets.symmetric(horizontal: 8.0); + + return Padding( + padding: safePadding, + child: SizedBox( + width: mqWidth - safePadding.horizontal, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: value.hasError + ? [ + OverlayButton( + scale: scale, + child: IconButton( + icon: Icon(Icons.open_in_new), + onPressed: () => AndroidAppService.open(entry.uri, entry.mimeType), + tooltip: 'Open', + ), ), - 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', + ), + ), + ], + ), + ), + ); + }, ); } diff --git a/lib/widgets/fullscreen/video.dart b/lib/widgets/fullscreen/video.dart index 8eeab3313..74cd4bba4 100644 --- a/lib/widgets/fullscreen/video.dart +++ b/lib/widgets/fullscreen/video.dart @@ -4,6 +4,7 @@ import 'dart:ui'; import 'package:aves/model/image_entry.dart'; import 'package:aves/widgets/common/image_preview.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; import 'package:video_player/video_player.dart'; class AvesVideo extends StatefulWidget { @@ -57,14 +58,17 @@ class AvesVideoState extends State { Widget build(BuildContext context) { if (value == null) return SizedBox(); if (value.hasError) { - final mediaQuery = MediaQuery.of(context); - final width = min(mediaQuery.size.width, entry.width.toDouble()); - return ImagePreview( - entry: entry, - width: width, - height: width / entry.aspectRatio, - devicePixelRatio: window.devicePixelRatio, - builder: (bytes) => Image.memory(bytes), + return Selector( + selector: (c, mq) => mq.size.width, + builder: (c, mqWidth, child) { + final width = min(mqWidth, entry.width.toDouble()); + return ImagePreview( + entry: entry, + width: width, + height: width / entry.aspectRatio, + builder: (bytes) => Image.memory(bytes), + ); + }, ); } return Center( diff --git a/pubspec.lock b/pubspec.lock index 430e32b80..c6f6ccad1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,13 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + after_init: + dependency: "direct main" + description: + name: after_init + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.2" archive: dependency: transitive description: @@ -160,6 +167,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.8" + nested: + dependency: transitive + description: + name: nested + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.4" path: dependency: "direct main" description: @@ -223,6 +237,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.0.2" + provider: + dependency: "direct main" + description: + name: provider + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.0" quiver: dependency: transitive description: