fullscreen: modified widget rebuild logic
This commit is contained in:
parent
5fe985537f
commit
be664f0967
3 changed files with 286 additions and 198 deletions
|
@ -38,7 +38,7 @@ class FullscreenBody extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProviderStateMixin, WidgetsBindingObserver {
|
class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProviderStateMixin, WidgetsBindingObserver {
|
||||||
ImageEntry _entry;
|
final ValueNotifier<ImageEntry> _entryNotifier = ValueNotifier(null);
|
||||||
int _currentHorizontalPage;
|
int _currentHorizontalPage;
|
||||||
ValueNotifier<int> _currentVerticalPage;
|
ValueNotifier<int> _currentVerticalPage;
|
||||||
PageController _horizontalPager, _verticalPager;
|
PageController _horizontalPager, _verticalPager;
|
||||||
|
@ -57,19 +57,18 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
|
||||||
|
|
||||||
List<ImageEntry> get entries => hasCollection ? collection.sortedEntries : [widget.initialEntry];
|
List<ImageEntry> get entries => hasCollection ? collection.sortedEntries : [widget.initialEntry];
|
||||||
|
|
||||||
List<String> get pages => ['transition', 'image', 'info'];
|
static const int transitionPage = 0;
|
||||||
|
|
||||||
int get transitionPage => pages.indexOf('transition');
|
static const int imagePage = 1;
|
||||||
|
|
||||||
int get imagePage => pages.indexOf('image');
|
static const int infoPage = 2;
|
||||||
|
|
||||||
int get infoPage => pages.indexOf('info');
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_entry = widget.initialEntry;
|
final entry = widget.initialEntry;
|
||||||
_currentHorizontalPage = max(0, entries.indexOf(_entry));
|
_entryNotifier.value = entry;
|
||||||
|
_currentHorizontalPage = max(0, entries.indexOf(entry));
|
||||||
_currentVerticalPage = ValueNotifier(imagePage);
|
_currentVerticalPage = ValueNotifier(imagePage);
|
||||||
_horizontalPager = PageController(initialPage: _currentHorizontalPage);
|
_horizontalPager = PageController(initialPage: _currentHorizontalPage);
|
||||||
_verticalPager = PageController(initialPage: _currentVerticalPage.value)..addListener(_onVerticalPageControllerChange);
|
_verticalPager = PageController(initialPage: _currentVerticalPage.value)..addListener(_onVerticalPageControllerChange);
|
||||||
|
@ -138,76 +137,6 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final topOverlay = ValueListenableBuilder<int>(
|
|
||||||
valueListenable: _currentVerticalPage,
|
|
||||||
builder: (context, page, child) {
|
|
||||||
final showOverlay = _entry != null && page == imagePage;
|
|
||||||
return showOverlay
|
|
||||||
? FullscreenTopOverlay(
|
|
||||||
entries: entries,
|
|
||||||
index: _currentHorizontalPage,
|
|
||||||
scale: _topOverlayScale,
|
|
||||||
canToggleFavourite: hasCollection,
|
|
||||||
viewInsets: _frozenViewInsets,
|
|
||||||
viewPadding: _frozenViewPadding,
|
|
||||||
onActionSelected: (action) => _actionDelegate.onActionSelected(context, _entry, action),
|
|
||||||
)
|
|
||||||
: const SizedBox.shrink();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
final videoController = _entry != null && _entry.isVideo ? _videoControllers.firstWhere((kv) => kv.item1 == _entry.uri, orElse: () => null)?.item2 : null;
|
|
||||||
Widget bottomOverlay = Column(
|
|
||||||
children: [
|
|
||||||
if (videoController != null)
|
|
||||||
VideoControlOverlay(
|
|
||||||
entry: _entry,
|
|
||||||
controller: videoController,
|
|
||||||
scale: _bottomOverlayScale,
|
|
||||||
viewInsets: _frozenViewInsets,
|
|
||||||
viewPadding: _frozenViewPadding,
|
|
||||||
),
|
|
||||||
SlideTransition(
|
|
||||||
position: _bottomOverlayOffset,
|
|
||||||
child: FullscreenBottomOverlay(
|
|
||||||
entries: entries,
|
|
||||||
index: _currentHorizontalPage,
|
|
||||||
showPosition: hasCollection,
|
|
||||||
viewInsets: _frozenViewInsets,
|
|
||||||
viewPadding: _frozenViewPadding,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
bottomOverlay = ValueListenableBuilder<double>(
|
|
||||||
valueListenable: _overlayAnimationController,
|
|
||||||
builder: (context, animation, child) {
|
|
||||||
return Visibility(
|
|
||||||
visible: _entry != null && _overlayAnimationController.status != AnimationStatus.dismissed,
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: bottomOverlay,
|
|
||||||
);
|
|
||||||
|
|
||||||
bottomOverlay = Selector<MediaQueryData, double>(
|
|
||||||
selector: (c, mq) => mq.size.height,
|
|
||||||
builder: (c, mqHeight, child) {
|
|
||||||
// when orientation change, the `PageController` offset is not updated right away
|
|
||||||
// and it does not trigger its listeners when it does, so we force a refresh in the next frame
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) => _onVerticalPageControllerChange());
|
|
||||||
return AnimatedBuilder(
|
|
||||||
animation: _verticalScrollNotifier,
|
|
||||||
builder: (context, child) => Positioned(
|
|
||||||
bottom: (_verticalPager.offset ?? 0) - mqHeight,
|
|
||||||
child: child,
|
|
||||||
),
|
|
||||||
child: child,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: bottomOverlay,
|
|
||||||
);
|
|
||||||
|
|
||||||
return WillPopScope(
|
return WillPopScope(
|
||||||
onWillPop: () {
|
onWillPop: () {
|
||||||
if (_currentVerticalPage.value == infoPage) {
|
if (_currentVerticalPage.value == infoPage) {
|
||||||
|
@ -227,7 +156,7 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
|
||||||
children: [
|
children: [
|
||||||
FullscreenVerticalPageView(
|
FullscreenVerticalPageView(
|
||||||
collection: collection,
|
collection: collection,
|
||||||
entry: _entry,
|
entryNotifier: _entryNotifier,
|
||||||
videoControllers: _videoControllers,
|
videoControllers: _videoControllers,
|
||||||
verticalPager: _verticalPager,
|
verticalPager: _verticalPager,
|
||||||
horizontalPager: _horizontalPager,
|
horizontalPager: _horizontalPager,
|
||||||
|
@ -236,14 +165,106 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
|
||||||
onImageTap: () => _overlayVisible.value = !_overlayVisible.value,
|
onImageTap: () => _overlayVisible.value = !_overlayVisible.value,
|
||||||
onImagePageRequested: () => _goToVerticalPage(imagePage),
|
onImagePageRequested: () => _goToVerticalPage(imagePage),
|
||||||
),
|
),
|
||||||
topOverlay,
|
_buildTopOverlay(),
|
||||||
bottomOverlay,
|
_buildBottomOverlay(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildTopOverlay() {
|
||||||
|
final child = ValueListenableBuilder<ImageEntry>(
|
||||||
|
valueListenable: _entryNotifier,
|
||||||
|
builder: (context, entry, child) {
|
||||||
|
if (entry == null) return const SizedBox.shrink();
|
||||||
|
return FullscreenTopOverlay(
|
||||||
|
entry: entry,
|
||||||
|
scale: _topOverlayScale,
|
||||||
|
canToggleFavourite: hasCollection,
|
||||||
|
viewInsets: _frozenViewInsets,
|
||||||
|
viewPadding: _frozenViewPadding,
|
||||||
|
onActionSelected: (action) => _actionDelegate.onActionSelected(context, entry, action),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return ValueListenableBuilder<int>(
|
||||||
|
valueListenable: _currentVerticalPage,
|
||||||
|
builder: (context, page, child) {
|
||||||
|
return Visibility(
|
||||||
|
visible: page == imagePage,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildBottomOverlay() {
|
||||||
|
Widget bottomOverlay = ValueListenableBuilder<ImageEntry>(
|
||||||
|
valueListenable: _entryNotifier,
|
||||||
|
builder: (context, entry, child) {
|
||||||
|
Widget videoOverlay;
|
||||||
|
if (entry != null) {
|
||||||
|
final videoController = entry.isVideo ? _videoControllers.firstWhere((kv) => kv.item1 == entry.uri, orElse: () => null)?.item2 : null;
|
||||||
|
if (videoController != null) {
|
||||||
|
videoOverlay = VideoControlOverlay(
|
||||||
|
entry: entry,
|
||||||
|
controller: videoController,
|
||||||
|
scale: _bottomOverlayScale,
|
||||||
|
viewInsets: _frozenViewInsets,
|
||||||
|
viewPadding: _frozenViewPadding,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final child = Column(
|
||||||
|
children: [
|
||||||
|
if (videoOverlay != null) videoOverlay,
|
||||||
|
SlideTransition(
|
||||||
|
position: _bottomOverlayOffset,
|
||||||
|
child: FullscreenBottomOverlay(
|
||||||
|
entries: entries,
|
||||||
|
index: _currentHorizontalPage,
|
||||||
|
showPosition: hasCollection,
|
||||||
|
viewInsets: _frozenViewInsets,
|
||||||
|
viewPadding: _frozenViewPadding,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
return ValueListenableBuilder<double>(
|
||||||
|
valueListenable: _overlayAnimationController,
|
||||||
|
builder: (context, animation, child) {
|
||||||
|
return Visibility(
|
||||||
|
visible: entry != null && _overlayAnimationController.status != AnimationStatus.dismissed,
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
bottomOverlay = Selector<MediaQueryData, double>(
|
||||||
|
selector: (c, mq) => mq.size.height,
|
||||||
|
builder: (c, mqHeight, child) {
|
||||||
|
// when orientation change, the `PageController` offset is not updated right away
|
||||||
|
// and it does not trigger its listeners when it does, so we force a refresh in the next frame
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) => _onVerticalPageControllerChange());
|
||||||
|
return AnimatedBuilder(
|
||||||
|
animation: _verticalScrollNotifier,
|
||||||
|
builder: (context, child) => Positioned(
|
||||||
|
bottom: (_verticalPager.offset ?? 0) - mqHeight,
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: bottomOverlay,
|
||||||
|
);
|
||||||
|
return bottomOverlay;
|
||||||
|
}
|
||||||
|
|
||||||
void _onVerticalPageControllerChange() {
|
void _onVerticalPageControllerChange() {
|
||||||
_verticalScrollNotifier.notifyListeners();
|
_verticalScrollNotifier.notifyListeners();
|
||||||
}
|
}
|
||||||
|
@ -286,11 +307,10 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
|
||||||
|
|
||||||
void _updateEntry() {
|
void _updateEntry() {
|
||||||
final newEntry = _currentHorizontalPage != null && _currentHorizontalPage < entries.length ? entries[_currentHorizontalPage] : null;
|
final newEntry = _currentHorizontalPage != null && _currentHorizontalPage < entries.length ? entries[_currentHorizontalPage] : null;
|
||||||
if (_entry == newEntry) return;
|
if (_entryNotifier.value == newEntry) return;
|
||||||
_entry = newEntry;
|
_entryNotifier.value = newEntry;
|
||||||
_pauseVideoControllers();
|
_pauseVideoControllers();
|
||||||
_initVideoController();
|
_initVideoController();
|
||||||
setState(() {});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onLeave() {
|
void _onLeave() {
|
||||||
|
@ -301,13 +321,13 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
|
||||||
_showSystemUI();
|
_showSystemUI();
|
||||||
}
|
}
|
||||||
|
|
||||||
// system UI
|
// system UI
|
||||||
|
|
||||||
static void _showSystemUI() => SystemChrome.setEnabledSystemUIOverlays(SystemUiOverlay.values);
|
static void _showSystemUI() => SystemChrome.setEnabledSystemUIOverlays(SystemUiOverlay.values);
|
||||||
|
|
||||||
static void _hideSystemUI() => SystemChrome.setEnabledSystemUIOverlays([]);
|
static void _hideSystemUI() => SystemChrome.setEnabledSystemUIOverlays([]);
|
||||||
|
|
||||||
// overlay
|
// overlay
|
||||||
|
|
||||||
Future<void> _initOverlay() async {
|
Future<void> _initOverlay() async {
|
||||||
// wait for MaterialPageRoute.transitionDuration
|
// wait for MaterialPageRoute.transitionDuration
|
||||||
|
@ -348,9 +368,10 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
|
||||||
void _pauseVideoControllers() => _videoControllers.forEach((e) => e.item2.pause());
|
void _pauseVideoControllers() => _videoControllers.forEach((e) => e.item2.pause());
|
||||||
|
|
||||||
Future<void> _initVideoController() async {
|
Future<void> _initVideoController() async {
|
||||||
if (_entry == null || !_entry.isVideo) return;
|
final entry = _entryNotifier.value;
|
||||||
|
if (entry == null || !entry.isVideo) return;
|
||||||
|
|
||||||
final uri = _entry.uri;
|
final uri = entry.uri;
|
||||||
var controllerEntry = _videoControllers.firstWhere((kv) => kv.item1 == uri, orElse: () => null);
|
var controllerEntry = _videoControllers.firstWhere((kv) => kv.item1 == uri, orElse: () => null);
|
||||||
if (controllerEntry != null) {
|
if (controllerEntry != null) {
|
||||||
_videoControllers.remove(controllerEntry);
|
_videoControllers.remove(controllerEntry);
|
||||||
|
@ -362,12 +383,13 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
|
||||||
while (_videoControllers.length > 3) {
|
while (_videoControllers.length > 3) {
|
||||||
_videoControllers.removeLast().item2.dispose();
|
_videoControllers.removeLast().item2.dispose();
|
||||||
}
|
}
|
||||||
|
setState(() {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class FullscreenVerticalPageView extends StatefulWidget {
|
class FullscreenVerticalPageView extends StatefulWidget {
|
||||||
final CollectionLens collection;
|
final CollectionLens collection;
|
||||||
final ImageEntry entry;
|
final ValueNotifier<ImageEntry> entryNotifier;
|
||||||
final List<Tuple2<String, IjkMediaController>> videoControllers;
|
final List<Tuple2<String, IjkMediaController>> videoControllers;
|
||||||
final PageController horizontalPager, verticalPager;
|
final PageController horizontalPager, verticalPager;
|
||||||
final void Function(int page) onVerticalPageChanged, onHorizontalPageChanged;
|
final void Function(int page) onVerticalPageChanged, onHorizontalPageChanged;
|
||||||
|
@ -375,7 +397,7 @@ class FullscreenVerticalPageView extends StatefulWidget {
|
||||||
|
|
||||||
const FullscreenVerticalPageView({
|
const FullscreenVerticalPageView({
|
||||||
@required this.collection,
|
@required this.collection,
|
||||||
@required this.entry,
|
@required this.entryNotifier,
|
||||||
@required this.videoControllers,
|
@required this.videoControllers,
|
||||||
@required this.verticalPager,
|
@required this.verticalPager,
|
||||||
@required this.horizontalPager,
|
@required this.horizontalPager,
|
||||||
|
@ -392,12 +414,13 @@ class FullscreenVerticalPageView extends StatefulWidget {
|
||||||
class _FullscreenVerticalPageViewState extends State<FullscreenVerticalPageView> {
|
class _FullscreenVerticalPageViewState extends State<FullscreenVerticalPageView> {
|
||||||
final ValueNotifier<Color> _backgroundColorNotifier = ValueNotifier(Colors.black);
|
final ValueNotifier<Color> _backgroundColorNotifier = ValueNotifier(Colors.black);
|
||||||
final ValueNotifier<bool> _infoPageVisibleNotifier = ValueNotifier(false);
|
final ValueNotifier<bool> _infoPageVisibleNotifier = ValueNotifier(false);
|
||||||
|
ImageEntry _oldEntry;
|
||||||
|
|
||||||
CollectionLens get collection => widget.collection;
|
CollectionLens get collection => widget.collection;
|
||||||
|
|
||||||
bool get hasCollection => collection != null;
|
bool get hasCollection => collection != null;
|
||||||
|
|
||||||
ImageEntry get entry => widget.entry;
|
ImageEntry get entry => widget.entryNotifier.value;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
@ -420,12 +443,12 @@ class _FullscreenVerticalPageViewState extends State<FullscreenVerticalPageView>
|
||||||
|
|
||||||
void _registerWidget(FullscreenVerticalPageView widget) {
|
void _registerWidget(FullscreenVerticalPageView widget) {
|
||||||
widget.verticalPager.addListener(_onVerticalPageControllerChanged);
|
widget.verticalPager.addListener(_onVerticalPageControllerChanged);
|
||||||
widget.entry?.imageChangeNotifier?.addListener(_onImageChanged);
|
widget.entryNotifier.addListener(_onEntryChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _unregisterWidget(FullscreenVerticalPageView widget) {
|
void _unregisterWidget(FullscreenVerticalPageView widget) {
|
||||||
widget.verticalPager.removeListener(_onVerticalPageControllerChanged);
|
widget.verticalPager.removeListener(_onVerticalPageControllerChanged);
|
||||||
widget.entry?.imageChangeNotifier?.removeListener(_onImageChanged);
|
widget.entryNotifier.removeListener(_onEntryChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -453,7 +476,7 @@ class _FullscreenVerticalPageViewState extends State<FullscreenVerticalPageView>
|
||||||
},
|
},
|
||||||
child: InfoPage(
|
child: InfoPage(
|
||||||
collection: collection,
|
collection: collection,
|
||||||
entry: entry,
|
entryNotifier: widget.entryNotifier,
|
||||||
visibleNotifier: _infoPageVisibleNotifier,
|
visibleNotifier: _infoPageVisibleNotifier,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -482,6 +505,14 @@ class _FullscreenVerticalPageViewState extends State<FullscreenVerticalPageView>
|
||||||
_backgroundColorNotifier.value = _backgroundColorNotifier.value.withOpacity(opacity * opacity);
|
_backgroundColorNotifier.value = _backgroundColorNotifier.value.withOpacity(opacity * opacity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// when the entry changed (e.g. by scrolling through the PageView, or if the entry got deleted)
|
||||||
|
void _onEntryChanged() {
|
||||||
|
_oldEntry?.imageChangeNotifier?.removeListener(_onImageChanged);
|
||||||
|
entry?.imageChangeNotifier?.addListener(_onImageChanged);
|
||||||
|
_oldEntry = entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
// when the entry image itself changed (e.g. after rotation)
|
||||||
void _onImageChanged() async {
|
void _onImageChanged() async {
|
||||||
await UriImage(uri: entry.uri, mimeType: entry.mimeType).evict();
|
await UriImage(uri: entry.uri, mimeType: entry.mimeType).evict();
|
||||||
// TODO TLAD also evict `ThumbnailProvider` with specified extents
|
// TODO TLAD also evict `ThumbnailProvider` with specified extents
|
||||||
|
|
|
@ -14,14 +14,14 @@ import 'package:tuple/tuple.dart';
|
||||||
|
|
||||||
class InfoPage extends StatefulWidget {
|
class InfoPage extends StatefulWidget {
|
||||||
final CollectionLens collection;
|
final CollectionLens collection;
|
||||||
final ImageEntry entry;
|
final ValueNotifier<ImageEntry> entryNotifier;
|
||||||
final ValueNotifier<bool> visibleNotifier;
|
final ValueNotifier<bool> visibleNotifier;
|
||||||
|
|
||||||
const InfoPage({
|
const InfoPage({
|
||||||
Key key,
|
Key key,
|
||||||
@required this.collection,
|
@required this.collection,
|
||||||
@required this.entry,
|
@required this.entryNotifier,
|
||||||
this.visibleNotifier,
|
@required this.visibleNotifier,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -34,12 +34,31 @@ class InfoPageState extends State<InfoPage> {
|
||||||
|
|
||||||
CollectionLens get collection => widget.collection;
|
CollectionLens get collection => widget.collection;
|
||||||
|
|
||||||
ImageEntry get entry => widget.entry;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
entry.locate();
|
_registerWidget(widget);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(InfoPage oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
_unregisterWidget(oldWidget);
|
||||||
|
_registerWidget(widget);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_unregisterWidget(widget);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _registerWidget(InfoPage widget) {
|
||||||
|
widget.entryNotifier.addListener(_onEntryChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _unregisterWidget(InfoPage widget) {
|
||||||
|
widget.entryNotifier.removeListener(_onEntryChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -67,8 +86,11 @@ class InfoPageState extends State<InfoPage> {
|
||||||
final mqWidth = mq.item1;
|
final mqWidth = mq.item1;
|
||||||
final mqViewInsetsBottom = mq.item2;
|
final mqViewInsetsBottom = mq.item2;
|
||||||
final split = mqWidth > 400;
|
final split = mqWidth > 400;
|
||||||
final locationAtTop = split && entry.hasGps;
|
|
||||||
|
|
||||||
|
return ValueListenableBuilder(
|
||||||
|
valueListenable: widget.entryNotifier,
|
||||||
|
builder: (context, entry, child) {
|
||||||
|
final locationAtTop = split && entry.hasGps;
|
||||||
final locationSection = LocationSection(
|
final locationSection = LocationSection(
|
||||||
collection: collection,
|
collection: collection,
|
||||||
entry: entry,
|
entry: entry,
|
||||||
|
@ -115,6 +137,8 @@ class InfoPageState extends State<InfoPage> {
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -123,6 +147,10 @@ class InfoPageState extends State<InfoPage> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onEntryChanged() {
|
||||||
|
widget.entryNotifier.value?.locate();
|
||||||
|
}
|
||||||
|
|
||||||
bool _handleTopScroll(Notification notification) {
|
bool _handleTopScroll(Notification notification) {
|
||||||
if (notification is ScrollNotification) {
|
if (notification is ScrollNotification) {
|
||||||
if (notification is ScrollStartNotification) {
|
if (notification is ScrollStartNotification) {
|
||||||
|
|
|
@ -9,17 +9,15 @@ import 'package:aves/widgets/fullscreen/overlay/common.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:tuple/tuple.dart';
|
||||||
|
|
||||||
class FullscreenTopOverlay extends StatelessWidget {
|
class FullscreenTopOverlay extends StatelessWidget {
|
||||||
final List<ImageEntry> entries;
|
final ImageEntry entry;
|
||||||
final int index;
|
|
||||||
final Animation<double> scale;
|
final Animation<double> scale;
|
||||||
final EdgeInsets viewInsets, viewPadding;
|
final EdgeInsets viewInsets, viewPadding;
|
||||||
final Function(EntryAction value) onActionSelected;
|
final Function(EntryAction value) onActionSelected;
|
||||||
final bool canToggleFavourite;
|
final bool canToggleFavourite;
|
||||||
|
|
||||||
ImageEntry get entry => entries[index];
|
|
||||||
|
|
||||||
static const double padding = 8;
|
static const double padding = 8;
|
||||||
|
|
||||||
static const int landscapeActionCount = 3;
|
static const int landscapeActionCount = 3;
|
||||||
|
@ -28,13 +26,12 @@ class FullscreenTopOverlay extends StatelessWidget {
|
||||||
|
|
||||||
const FullscreenTopOverlay({
|
const FullscreenTopOverlay({
|
||||||
Key key,
|
Key key,
|
||||||
@required this.entries,
|
@required this.entry,
|
||||||
@required this.index,
|
|
||||||
@required this.scale,
|
@required this.scale,
|
||||||
this.canToggleFavourite = false,
|
@required this.canToggleFavourite,
|
||||||
this.viewInsets,
|
@required this.viewInsets,
|
||||||
this.viewPadding,
|
@required this.viewPadding,
|
||||||
this.onActionSelected,
|
@required this.onActionSelected,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
@ -43,49 +40,31 @@ class FullscreenTopOverlay extends StatelessWidget {
|
||||||
minimum: (viewInsets ?? EdgeInsets.zero) + (viewPadding ?? EdgeInsets.zero),
|
minimum: (viewInsets ?? EdgeInsets.zero) + (viewPadding ?? EdgeInsets.zero),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(padding),
|
padding: const EdgeInsets.all(padding),
|
||||||
child: Selector<MediaQueryData, Orientation>(
|
child: Selector<MediaQueryData, Tuple2<double, Orientation>>(
|
||||||
selector: (c, mq) => mq.orientation,
|
selector: (c, mq) => Tuple2(mq.size.width, mq.orientation),
|
||||||
builder: (c, orientation, child) {
|
builder: (c, mq, child) {
|
||||||
final targetCount = orientation == Orientation.landscape ? landscapeActionCount : portraitActionCount;
|
final mqWidth = mq.item1;
|
||||||
return LayoutBuilder(
|
final mqOrientation = mq.item2;
|
||||||
builder: (context, constraints) {
|
|
||||||
final availableCount = (constraints.maxWidth / (kMinInteractiveDimension + padding)).floor() - 2;
|
final targetCount = mqOrientation == Orientation.landscape ? landscapeActionCount : portraitActionCount;
|
||||||
|
final availableCount = (mqWidth / (kMinInteractiveDimension + padding)).floor() - 2;
|
||||||
final quickActionCount = min(targetCount, availableCount);
|
final quickActionCount = min(targetCount, availableCount);
|
||||||
|
|
||||||
final quickActions = [
|
final quickActions = [
|
||||||
EntryAction.toggleFavourite,
|
EntryAction.toggleFavourite,
|
||||||
EntryAction.share,
|
EntryAction.share,
|
||||||
EntryAction.delete,
|
EntryAction.delete,
|
||||||
].where(_canDo).take(quickActionCount);
|
].where(_canDo).take(quickActionCount).toList();
|
||||||
final inAppActions = EntryActions.inApp.where((action) => !quickActions.contains(action)).where(_canDo);
|
final inAppActions = EntryActions.inApp.where((action) => !quickActions.contains(action)).where(_canDo).toList();
|
||||||
final externalAppActions = EntryActions.externalApp.where(_canDo);
|
final externalAppActions = EntryActions.externalApp.where(_canDo).toList();
|
||||||
|
|
||||||
return Row(
|
return _TopOverlayRow(
|
||||||
children: [
|
quickActions: quickActions,
|
||||||
OverlayButton(
|
inAppActions: inAppActions,
|
||||||
|
externalAppActions: externalAppActions,
|
||||||
scale: scale,
|
scale: scale,
|
||||||
child: ModalRoute.of(context)?.canPop ?? true ? const BackButton() : const CloseButton(),
|
isFavouriteNotifier: entry.isFavouriteNotifier,
|
||||||
),
|
onActionSelected: onActionSelected,
|
||||||
const Spacer(),
|
|
||||||
...quickActions.map(_buildOverlayButton),
|
|
||||||
OverlayButton(
|
|
||||||
scale: scale,
|
|
||||||
child: PopupMenuButton<EntryAction>(
|
|
||||||
itemBuilder: (context) => [
|
|
||||||
...inAppActions.map(_buildPopupMenuItem),
|
|
||||||
const PopupMenuDivider(),
|
|
||||||
...externalAppActions.map(_buildPopupMenuItem),
|
|
||||||
if (kDebugMode) ...[
|
|
||||||
const PopupMenuDivider(),
|
|
||||||
_buildPopupMenuItem(EntryAction.debug),
|
|
||||||
]
|
|
||||||
],
|
|
||||||
onSelected: onActionSelected,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -118,6 +97,56 @@ class FullscreenTopOverlay extends StatelessWidget {
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TopOverlayRow extends StatelessWidget {
|
||||||
|
final List<EntryAction> quickActions;
|
||||||
|
final List<EntryAction> inAppActions;
|
||||||
|
final List<EntryAction> externalAppActions;
|
||||||
|
final Animation<double> scale;
|
||||||
|
final ValueNotifier<bool> isFavouriteNotifier;
|
||||||
|
final Function(EntryAction value) onActionSelected;
|
||||||
|
|
||||||
|
const _TopOverlayRow({
|
||||||
|
Key key,
|
||||||
|
@required this.quickActions,
|
||||||
|
@required this.inAppActions,
|
||||||
|
@required this.externalAppActions,
|
||||||
|
@required this.scale,
|
||||||
|
@required this.isFavouriteNotifier,
|
||||||
|
@required this.onActionSelected,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
static const double padding = 8;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Row(
|
||||||
|
children: [
|
||||||
|
OverlayButton(
|
||||||
|
scale: scale,
|
||||||
|
child: ModalRoute.of(context)?.canPop ?? true ? const BackButton() : const CloseButton(),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
...quickActions.map(_buildOverlayButton),
|
||||||
|
OverlayButton(
|
||||||
|
scale: scale,
|
||||||
|
child: PopupMenuButton<EntryAction>(
|
||||||
|
itemBuilder: (context) => [
|
||||||
|
...inAppActions.map(_buildPopupMenuItem),
|
||||||
|
const PopupMenuDivider(),
|
||||||
|
...externalAppActions.map(_buildPopupMenuItem),
|
||||||
|
if (kDebugMode) ...[
|
||||||
|
const PopupMenuDivider(),
|
||||||
|
_buildPopupMenuItem(EntryAction.debug),
|
||||||
|
]
|
||||||
|
],
|
||||||
|
onSelected: onActionSelected,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildOverlayButton(EntryAction action) {
|
Widget _buildOverlayButton(EntryAction action) {
|
||||||
Widget child;
|
Widget child;
|
||||||
|
@ -125,7 +154,7 @@ class FullscreenTopOverlay extends StatelessWidget {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case EntryAction.toggleFavourite:
|
case EntryAction.toggleFavourite:
|
||||||
child = ValueListenableBuilder<bool>(
|
child = ValueListenableBuilder<bool>(
|
||||||
valueListenable: entry.isFavouriteNotifier,
|
valueListenable: isFavouriteNotifier,
|
||||||
builder: (context, isFavourite, child) => Stack(
|
builder: (context, isFavourite, child) => Stack(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
children: [
|
children: [
|
||||||
|
@ -136,7 +165,7 @@ class FullscreenTopOverlay extends StatelessWidget {
|
||||||
),
|
),
|
||||||
Sweeper(
|
Sweeper(
|
||||||
builder: (context) => const Icon(AIcons.favourite, color: Colors.redAccent),
|
builder: (context) => const Icon(AIcons.favourite, color: Colors.redAccent),
|
||||||
toggledNotifier: entry.isFavouriteNotifier,
|
toggledNotifier: isFavouriteNotifier,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
@ -178,7 +207,7 @@ class FullscreenTopOverlay extends StatelessWidget {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
// in app actions
|
// in app actions
|
||||||
case EntryAction.toggleFavourite:
|
case EntryAction.toggleFavourite:
|
||||||
child = entry.isFavouriteNotifier.value
|
child = isFavouriteNotifier.value
|
||||||
? const MenuRow(
|
? const MenuRow(
|
||||||
text: 'Remove from favourites',
|
text: 'Remove from favourites',
|
||||||
icon: AIcons.favouriteActive,
|
icon: AIcons.favouriteActive,
|
||||||
|
|
Loading…
Reference in a new issue