use Provider/Selector for MediaQueryData
This commit is contained in:
parent
65b51c7e83
commit
4761e16208
23 changed files with 569 additions and 434 deletions
|
@ -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(
|
||||||
|
child: Scaffold(
|
||||||
// fake app bar so that content is safe from status bar, even though we use a SliverAppBar
|
// fake app bar so that content is safe from status bar, even though we use a SliverAppBar
|
||||||
appBar: FakeAppBar(),
|
appBar: FakeAppBar(),
|
||||||
body: AllCollectionPage(collection: localMediaCollection),
|
body: AllCollectionPage(collection: localMediaCollection),
|
||||||
drawer: AllCollectionDrawer(collection: localMediaCollection),
|
drawer: AllCollectionDrawer(collection: localMediaCollection),
|
||||||
resizeToAvoidBottomInset: false,
|
resizeToAvoidBottomInset: false,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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(', ');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
|
@ -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'}'
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
|
@ -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}')
|
||||||
|
]),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
@ -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,7 +15,8 @@ class FilteredCollectionPage extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return MediaQueryDataProvider(
|
||||||
|
child: Scaffold(
|
||||||
body: ThumbnailCollection(
|
body: ThumbnailCollection(
|
||||||
collection: collection,
|
collection: collection,
|
||||||
appBar: SliverAppBar(
|
appBar: SliverAppBar(
|
||||||
|
@ -23,6 +25,7 @@ class FilteredCollectionPage extends StatelessWidget {
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
resizeToAvoidBottomInset: false,
|
resizeToAvoidBottomInset: false,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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>(
|
||||||
|
selector: (c, mq) => mq.size.width,
|
||||||
|
builder: (c, mqWidth, child) => ThumbnailCollectionContent(
|
||||||
collection: collection,
|
collection: collection,
|
||||||
appBar: appBar,
|
appBar: appBar,
|
||||||
screenWidth: MediaQuery.of(context).size.width,
|
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,7 +62,9 @@ class ThumbnailCollectionContent extends StatelessWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
return SafeArea(
|
return SafeArea(
|
||||||
child: DraggableScrollbar.arrows(
|
child: Selector<MediaQueryData, double>(
|
||||||
|
selector: (c, mq) => mq.viewInsets.bottom,
|
||||||
|
builder: (c, mqViewInsetsBottom, child) => DraggableScrollbar.arrows(
|
||||||
child: CustomScrollView(
|
child: CustomScrollView(
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
slivers: [
|
slivers: [
|
||||||
|
@ -78,7 +81,7 @@ class ThumbnailCollectionContent extends StatelessWidget {
|
||||||
);
|
);
|
||||||
if (sectionKey == sectionKeys.last) {
|
if (sectionKey == sectionKeys.last) {
|
||||||
sliver = SliverPadding(
|
sliver = SliverPadding(
|
||||||
padding: EdgeInsets.only(bottom: bottomInsets),
|
padding: EdgeInsets.only(bottom: mqViewInsetsBottom),
|
||||||
sliver: sliver,
|
sliver: sliver,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -88,8 +91,10 @@ class ThumbnailCollectionContent extends StatelessWidget {
|
||||||
),
|
),
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
|
// top padding to adjust scroll thumb
|
||||||
top: topPadding,
|
top: topPadding,
|
||||||
bottom: bottomInsets,
|
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,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
16
lib/widgets/common/media_query_data_provider.dart
Normal file
16
lib/widgets/common/media_query_data_provider.dart
Normal 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,7 +36,8 @@ 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(
|
||||||
|
child: Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text('Info'),
|
title: Text('Info'),
|
||||||
),
|
),
|
||||||
|
@ -89,6 +91,7 @@ class DebugPageState extends State<DebugPage> {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(
|
||||||
|
child: Scaffold(
|
||||||
body: FullscreenBody(
|
body: FullscreenBody(
|
||||||
collection: collection,
|
collection: collection,
|
||||||
initialUri: initialUri,
|
initialUri: initialUri,
|
||||||
),
|
),
|
||||||
backgroundColor: Colors.black,
|
backgroundColor: Colors.black,
|
||||||
resizeToAvoidBottomInset: false,
|
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);
|
||||||
|
setState(() {
|
||||||
_frozenViewInsets = mediaQuery.viewInsets;
|
_frozenViewInsets = mediaQuery.viewInsets;
|
||||||
_frozenViewPadding = mediaQuery.viewPadding;
|
_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;
|
||||||
|
|
||||||
|
|
|
@ -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,7 +37,9 @@ 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>(
|
||||||
|
selector: (c, mq) => mq.size,
|
||||||
|
builder: (c, mqSize, child) => PhotoViewGallery.builder(
|
||||||
itemCount: entries.length,
|
itemCount: entries.length,
|
||||||
builder: (galleryContext, index) {
|
builder: (galleryContext, index) {
|
||||||
final entry = entries[index];
|
final entry = entries[index];
|
||||||
|
@ -49,7 +52,7 @@ class ImagePageState extends State<ImagePage> with AutomaticKeepAliveClientMixin
|
||||||
controller: videoController,
|
controller: videoController,
|
||||||
)
|
)
|
||||||
: SizedBox(),
|
: SizedBox(),
|
||||||
childSize: MediaQuery.of(galleryContext).size,
|
childSize: mqSize,
|
||||||
// no hero as most videos fullscreen image is different from its thumbnail
|
// no hero as most videos fullscreen image is different from its thumbnail
|
||||||
minScale: PhotoViewComputedScale.contained,
|
minScale: PhotoViewComputedScale.contained,
|
||||||
initialScale: PhotoViewComputedScale.contained,
|
initialScale: PhotoViewComputedScale.contained,
|
||||||
|
@ -75,6 +78,7 @@ class ImagePageState extends State<ImagePage> with AutomaticKeepAliveClientMixin
|
||||||
onPageChanged: widget.onPageChanged,
|
onPageChanged: widget.onPageChanged,
|
||||||
scaleStateChangedCallback: widget.onScaleChanged,
|
scaleStateChangedCallback: widget.onScaleChanged,
|
||||||
scrollPhysics: BouncingScrollPhysics(),
|
scrollPhysics: BouncingScrollPhysics(),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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°')
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,30 +36,34 @@ 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;
|
|
||||||
return Scaffold(
|
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: Icon(Icons.arrow_upward),
|
icon: const Icon(Icons.arrow_upward),
|
||||||
onPressed: () => BackUpNotification().dispatch(context),
|
onPressed: () => BackUpNotification().dispatch(context),
|
||||||
tooltip: 'Back to image',
|
tooltip: 'Back to image',
|
||||||
),
|
),
|
||||||
title: Text('Info'),
|
title: const Text('Info'),
|
||||||
),
|
),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: NotificationListener(
|
child: NotificationListener(
|
||||||
onNotification: _handleTopScroll,
|
onNotification: _handleTopScroll,
|
||||||
child: ListView(
|
child: Selector<MediaQueryData, Tuple2<Orientation, double>>(
|
||||||
padding: EdgeInsets.all(8.0) + EdgeInsets.only(bottom: bottomInsets),
|
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: [
|
children: [
|
||||||
if (orientation == Orientation.landscape && entry.hasGps)
|
if (mqOrientation == Orientation.landscape && entry.hasGps)
|
||||||
Row(
|
Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Expanded(child: BasicSection(entry: entry)),
|
Expanded(child: BasicSection(entry: entry)),
|
||||||
SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(child: LocationSection(entry: entry, showTitle: false)),
|
Expanded(child: LocationSection(entry: entry, showTitle: false)),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -67,10 +74,13 @@ class InfoPageState extends State<InfoPage> {
|
||||||
XmpTagSection(collection: widget.collection, entry: entry),
|
XmpTagSection(collection: widget.collection, entry: entry),
|
||||||
MetadataSection(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'),
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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,7 +38,9 @@ class MetadataSectionState extends State<MetadataSection> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return FutureBuilder(
|
return Selector<MediaQueryData, double>(
|
||||||
|
selector: (c, mq) => mq.size.width,
|
||||||
|
builder: (c, mqWidth, child) => FutureBuilder(
|
||||||
future: _metadataLoader,
|
future: _metadataLoader,
|
||||||
builder: (futureContext, AsyncSnapshot<Map> snapshot) {
|
builder: (futureContext, AsyncSnapshot<Map> snapshot) {
|
||||||
if (snapshot.hasError) return Text(snapshot.error);
|
if (snapshot.hasError) return Text(snapshot.error);
|
||||||
|
@ -46,7 +49,7 @@ class MetadataSectionState extends State<MetadataSection> {
|
||||||
final directoryNames = metadataMap.keys.toList()..sort();
|
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;
|
||||||
|
@ -81,6 +84,7 @@ class MetadataSectionState extends State<MetadataSection> {
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,14 +60,20 @@ 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>>(
|
||||||
|
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,
|
color: Colors.black26,
|
||||||
padding: viewInsets + viewPadding.copyWith(top: 0),
|
padding: viewInsets + viewPadding.copyWith(top: 0),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
|
@ -86,35 +96,37 @@ class _FullscreenBottomOverlayState extends State<FullscreenBottomOverlay> {
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,7 +135,13 @@ class _FullscreenBottomOverlayContent extends StatelessWidget {
|
||||||
),
|
),
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
child: Column(
|
child: Selector<MediaQueryData, Orientation>(
|
||||||
|
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,
|
mainAxisSize: MainAxisSize.min,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
@ -133,39 +151,48 @@ class _FullscreenBottomOverlayContent extends StatelessWidget {
|
||||||
),
|
),
|
||||||
if (entry.hasGps)
|
if (entry.hasGps)
|
||||||
Container(
|
Container(
|
||||||
padding: EdgeInsets.only(top: interRowPadding),
|
padding: const EdgeInsets.only(top: _interRowPadding),
|
||||||
width: subRowWidth,
|
width: subRowWidth,
|
||||||
child: _buildLocationRow(),
|
child: _LocationRow(entry),
|
||||||
),
|
),
|
||||||
if (twoColumns)
|
if (twoColumns)
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.only(top: interRowPadding),
|
padding: const EdgeInsets.only(top: _interRowPadding),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Container(width: subRowWidth, child: _buildDateRow()),
|
Container(width: subRowWidth, child: _DateRow(entry)),
|
||||||
if (hasShootingDetails) Container(width: subRowWidth, child: _buildShootingRow()),
|
if (hasShootingDetails) Container(width: subRowWidth, child: _ShootingRow(details)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
else ...[
|
else ...[
|
||||||
Container(
|
Container(
|
||||||
padding: EdgeInsets.only(top: interRowPadding),
|
padding: const EdgeInsets.only(top: _interRowPadding),
|
||||||
width: subRowWidth,
|
width: subRowWidth,
|
||||||
child: _buildDateRow(),
|
child: _DateRow(entry),
|
||||||
),
|
),
|
||||||
if (hasShootingDetails)
|
if (hasShootingDetails)
|
||||||
Container(
|
Container(
|
||||||
padding: EdgeInsets.only(top: interRowPadding),
|
padding: const EdgeInsets.only(top: _interRowPadding),
|
||||||
width: subRowWidth,
|
width: subRowWidth,
|
||||||
child: _buildShootingRow(),
|
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)),
|
||||||
|
|
|
@ -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,14 +77,21 @@ 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 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);
|
final safePadding = (viewInsets + viewPadding).copyWith(bottom: 8) + EdgeInsets.symmetric(horizontal: 8.0);
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: safePadding,
|
padding: safePadding,
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: mediaQuery.size.width - safePadding.horizontal,
|
width: mqWidth - safePadding.horizontal,
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: value.hasError
|
children: value.hasError
|
||||||
|
@ -116,6 +125,8 @@ class VideoControlOverlayState extends State<VideoControlOverlay> with SingleTic
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildProgressBar() {
|
Widget _buildProgressBar() {
|
||||||
|
|
|
@ -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,15 +58,18 @@ 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,
|
||||||
|
builder: (c, mqWidth, child) {
|
||||||
|
final width = min<double>(mqWidth, entry.width.toDouble());
|
||||||
return ImagePreview(
|
return ImagePreview(
|
||||||
entry: entry,
|
entry: entry,
|
||||||
width: width,
|
width: width,
|
||||||
height: width / entry.aspectRatio,
|
height: width / entry.aspectRatio,
|
||||||
devicePixelRatio: window.devicePixelRatio,
|
|
||||||
builder: (bytes) => Image.memory(bytes),
|
builder: (bytes) => Image.memory(bytes),
|
||||||
);
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return Center(
|
return Center(
|
||||||
child: AspectRatio(
|
child: AspectRatio(
|
||||||
|
|
21
pubspec.lock
21
pubspec.lock
|
@ -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:
|
||||||
|
|
Loading…
Reference in a new issue