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 {
|
||||
ImageEntry _entry;
|
||||
final ValueNotifier<ImageEntry> _entryNotifier = ValueNotifier(null);
|
||||
int _currentHorizontalPage;
|
||||
ValueNotifier<int> _currentVerticalPage;
|
||||
PageController _horizontalPager, _verticalPager;
|
||||
|
@ -57,19 +57,18 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
|
|||
|
||||
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');
|
||||
|
||||
int get infoPage => pages.indexOf('info');
|
||||
static const int infoPage = 2;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_entry = widget.initialEntry;
|
||||
_currentHorizontalPage = max(0, entries.indexOf(_entry));
|
||||
final entry = widget.initialEntry;
|
||||
_entryNotifier.value = entry;
|
||||
_currentHorizontalPage = max(0, entries.indexOf(entry));
|
||||
_currentVerticalPage = ValueNotifier(imagePage);
|
||||
_horizontalPager = PageController(initialPage: _currentHorizontalPage);
|
||||
_verticalPager = PageController(initialPage: _currentVerticalPage.value)..addListener(_onVerticalPageControllerChange);
|
||||
|
@ -138,76 +137,6 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
|
|||
|
||||
@override
|
||||
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(
|
||||
onWillPop: () {
|
||||
if (_currentVerticalPage.value == infoPage) {
|
||||
|
@ -227,7 +156,7 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
|
|||
children: [
|
||||
FullscreenVerticalPageView(
|
||||
collection: collection,
|
||||
entry: _entry,
|
||||
entryNotifier: _entryNotifier,
|
||||
videoControllers: _videoControllers,
|
||||
verticalPager: _verticalPager,
|
||||
horizontalPager: _horizontalPager,
|
||||
|
@ -236,14 +165,106 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
|
|||
onImageTap: () => _overlayVisible.value = !_overlayVisible.value,
|
||||
onImagePageRequested: () => _goToVerticalPage(imagePage),
|
||||
),
|
||||
topOverlay,
|
||||
bottomOverlay,
|
||||
_buildTopOverlay(),
|
||||
_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() {
|
||||
_verticalScrollNotifier.notifyListeners();
|
||||
}
|
||||
|
@ -286,11 +307,10 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
|
|||
|
||||
void _updateEntry() {
|
||||
final newEntry = _currentHorizontalPage != null && _currentHorizontalPage < entries.length ? entries[_currentHorizontalPage] : null;
|
||||
if (_entry == newEntry) return;
|
||||
_entry = newEntry;
|
||||
if (_entryNotifier.value == newEntry) return;
|
||||
_entryNotifier.value = newEntry;
|
||||
_pauseVideoControllers();
|
||||
_initVideoController();
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void _onLeave() {
|
||||
|
@ -301,13 +321,13 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
|
|||
_showSystemUI();
|
||||
}
|
||||
|
||||
// system UI
|
||||
// system UI
|
||||
|
||||
static void _showSystemUI() => SystemChrome.setEnabledSystemUIOverlays(SystemUiOverlay.values);
|
||||
|
||||
static void _hideSystemUI() => SystemChrome.setEnabledSystemUIOverlays([]);
|
||||
|
||||
// overlay
|
||||
// overlay
|
||||
|
||||
Future<void> _initOverlay() async {
|
||||
// wait for MaterialPageRoute.transitionDuration
|
||||
|
@ -348,9 +368,10 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
|
|||
void _pauseVideoControllers() => _videoControllers.forEach((e) => e.item2.pause());
|
||||
|
||||
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);
|
||||
if (controllerEntry != null) {
|
||||
_videoControllers.remove(controllerEntry);
|
||||
|
@ -362,12 +383,13 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
|
|||
while (_videoControllers.length > 3) {
|
||||
_videoControllers.removeLast().item2.dispose();
|
||||
}
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
class FullscreenVerticalPageView extends StatefulWidget {
|
||||
final CollectionLens collection;
|
||||
final ImageEntry entry;
|
||||
final ValueNotifier<ImageEntry> entryNotifier;
|
||||
final List<Tuple2<String, IjkMediaController>> videoControllers;
|
||||
final PageController horizontalPager, verticalPager;
|
||||
final void Function(int page) onVerticalPageChanged, onHorizontalPageChanged;
|
||||
|
@ -375,7 +397,7 @@ class FullscreenVerticalPageView extends StatefulWidget {
|
|||
|
||||
const FullscreenVerticalPageView({
|
||||
@required this.collection,
|
||||
@required this.entry,
|
||||
@required this.entryNotifier,
|
||||
@required this.videoControllers,
|
||||
@required this.verticalPager,
|
||||
@required this.horizontalPager,
|
||||
|
@ -392,12 +414,13 @@ class FullscreenVerticalPageView extends StatefulWidget {
|
|||
class _FullscreenVerticalPageViewState extends State<FullscreenVerticalPageView> {
|
||||
final ValueNotifier<Color> _backgroundColorNotifier = ValueNotifier(Colors.black);
|
||||
final ValueNotifier<bool> _infoPageVisibleNotifier = ValueNotifier(false);
|
||||
ImageEntry _oldEntry;
|
||||
|
||||
CollectionLens get collection => widget.collection;
|
||||
|
||||
bool get hasCollection => collection != null;
|
||||
|
||||
ImageEntry get entry => widget.entry;
|
||||
ImageEntry get entry => widget.entryNotifier.value;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
@ -420,12 +443,12 @@ class _FullscreenVerticalPageViewState extends State<FullscreenVerticalPageView>
|
|||
|
||||
void _registerWidget(FullscreenVerticalPageView widget) {
|
||||
widget.verticalPager.addListener(_onVerticalPageControllerChanged);
|
||||
widget.entry?.imageChangeNotifier?.addListener(_onImageChanged);
|
||||
widget.entryNotifier.addListener(_onEntryChanged);
|
||||
}
|
||||
|
||||
void _unregisterWidget(FullscreenVerticalPageView widget) {
|
||||
widget.verticalPager.removeListener(_onVerticalPageControllerChanged);
|
||||
widget.entry?.imageChangeNotifier?.removeListener(_onImageChanged);
|
||||
widget.entryNotifier.removeListener(_onEntryChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
|
@ -453,7 +476,7 @@ class _FullscreenVerticalPageViewState extends State<FullscreenVerticalPageView>
|
|||
},
|
||||
child: InfoPage(
|
||||
collection: collection,
|
||||
entry: entry,
|
||||
entryNotifier: widget.entryNotifier,
|
||||
visibleNotifier: _infoPageVisibleNotifier,
|
||||
),
|
||||
),
|
||||
|
@ -482,6 +505,14 @@ class _FullscreenVerticalPageViewState extends State<FullscreenVerticalPageView>
|
|||
_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 {
|
||||
await UriImage(uri: entry.uri, mimeType: entry.mimeType).evict();
|
||||
// TODO TLAD also evict `ThumbnailProvider` with specified extents
|
||||
|
|
|
@ -14,14 +14,14 @@ import 'package:tuple/tuple.dart';
|
|||
|
||||
class InfoPage extends StatefulWidget {
|
||||
final CollectionLens collection;
|
||||
final ImageEntry entry;
|
||||
final ValueNotifier<ImageEntry> entryNotifier;
|
||||
final ValueNotifier<bool> visibleNotifier;
|
||||
|
||||
const InfoPage({
|
||||
Key key,
|
||||
@required this.collection,
|
||||
@required this.entry,
|
||||
this.visibleNotifier,
|
||||
@required this.entryNotifier,
|
||||
@required this.visibleNotifier,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
|
@ -34,12 +34,31 @@ class InfoPageState extends State<InfoPage> {
|
|||
|
||||
CollectionLens get collection => widget.collection;
|
||||
|
||||
ImageEntry get entry => widget.entry;
|
||||
|
||||
@override
|
||||
void 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
|
||||
|
@ -67,52 +86,57 @@ class InfoPageState extends State<InfoPage> {
|
|||
final mqWidth = mq.item1;
|
||||
final mqViewInsetsBottom = mq.item2;
|
||||
final split = mqWidth > 400;
|
||||
final locationAtTop = split && entry.hasGps;
|
||||
|
||||
final locationSection = LocationSection(
|
||||
collection: collection,
|
||||
entry: entry,
|
||||
showTitle: !locationAtTop,
|
||||
visibleNotifier: widget.visibleNotifier,
|
||||
onFilter: _goToCollection,
|
||||
);
|
||||
final basicAndLocationSliver = locationAtTop
|
||||
? SliverToBoxAdapter(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(child: BasicSection(entry: entry, collection: collection, onFilter: _goToCollection)),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: locationSection),
|
||||
],
|
||||
),
|
||||
)
|
||||
: SliverList(
|
||||
delegate: SliverChildListDelegate.fixed(
|
||||
[
|
||||
BasicSection(entry: entry, collection: collection, onFilter: _goToCollection),
|
||||
locationSection,
|
||||
],
|
||||
),
|
||||
);
|
||||
final metadataSliver = MetadataSectionSliver(
|
||||
entry: entry,
|
||||
visibleNotifier: widget.visibleNotifier,
|
||||
);
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: widget.entryNotifier,
|
||||
builder: (context, entry, child) {
|
||||
final locationAtTop = split && entry.hasGps;
|
||||
final locationSection = LocationSection(
|
||||
collection: collection,
|
||||
entry: entry,
|
||||
showTitle: !locationAtTop,
|
||||
visibleNotifier: widget.visibleNotifier,
|
||||
onFilter: _goToCollection,
|
||||
);
|
||||
final basicAndLocationSliver = locationAtTop
|
||||
? SliverToBoxAdapter(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(child: BasicSection(entry: entry, collection: collection, onFilter: _goToCollection)),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(child: locationSection),
|
||||
],
|
||||
),
|
||||
)
|
||||
: SliverList(
|
||||
delegate: SliverChildListDelegate.fixed(
|
||||
[
|
||||
BasicSection(entry: entry, collection: collection, onFilter: _goToCollection),
|
||||
locationSection,
|
||||
],
|
||||
),
|
||||
);
|
||||
final metadataSliver = MetadataSectionSliver(
|
||||
entry: entry,
|
||||
visibleNotifier: widget.visibleNotifier,
|
||||
);
|
||||
|
||||
return CustomScrollView(
|
||||
controller: _scrollController,
|
||||
slivers: [
|
||||
appBar,
|
||||
SliverPadding(
|
||||
padding: horizontalPadding + const EdgeInsets.only(top: 8),
|
||||
sliver: basicAndLocationSliver,
|
||||
),
|
||||
SliverPadding(
|
||||
padding: horizontalPadding + EdgeInsets.only(bottom: 8 + mqViewInsetsBottom),
|
||||
sliver: metadataSliver,
|
||||
),
|
||||
],
|
||||
return CustomScrollView(
|
||||
controller: _scrollController,
|
||||
slivers: [
|
||||
appBar,
|
||||
SliverPadding(
|
||||
padding: horizontalPadding + const EdgeInsets.only(top: 8),
|
||||
sliver: basicAndLocationSliver,
|
||||
),
|
||||
SliverPadding(
|
||||
padding: horizontalPadding + EdgeInsets.only(bottom: 8 + mqViewInsetsBottom),
|
||||
sliver: metadataSliver,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
|
@ -123,6 +147,10 @@ class InfoPageState extends State<InfoPage> {
|
|||
);
|
||||
}
|
||||
|
||||
void _onEntryChanged() {
|
||||
widget.entryNotifier.value?.locate();
|
||||
}
|
||||
|
||||
bool _handleTopScroll(Notification notification) {
|
||||
if (notification is ScrollNotification) {
|
||||
if (notification is ScrollStartNotification) {
|
||||
|
|
|
@ -9,17 +9,15 @@ import 'package:aves/widgets/fullscreen/overlay/common.dart';
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:tuple/tuple.dart';
|
||||
|
||||
class FullscreenTopOverlay extends StatelessWidget {
|
||||
final List<ImageEntry> entries;
|
||||
final int index;
|
||||
final ImageEntry entry;
|
||||
final Animation<double> scale;
|
||||
final EdgeInsets viewInsets, viewPadding;
|
||||
final Function(EntryAction value) onActionSelected;
|
||||
final bool canToggleFavourite;
|
||||
|
||||
ImageEntry get entry => entries[index];
|
||||
|
||||
static const double padding = 8;
|
||||
|
||||
static const int landscapeActionCount = 3;
|
||||
|
@ -28,13 +26,12 @@ class FullscreenTopOverlay extends StatelessWidget {
|
|||
|
||||
const FullscreenTopOverlay({
|
||||
Key key,
|
||||
@required this.entries,
|
||||
@required this.index,
|
||||
@required this.entry,
|
||||
@required this.scale,
|
||||
this.canToggleFavourite = false,
|
||||
this.viewInsets,
|
||||
this.viewPadding,
|
||||
this.onActionSelected,
|
||||
@required this.canToggleFavourite,
|
||||
@required this.viewInsets,
|
||||
@required this.viewPadding,
|
||||
@required this.onActionSelected,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
|
@ -43,49 +40,31 @@ class FullscreenTopOverlay extends StatelessWidget {
|
|||
minimum: (viewInsets ?? EdgeInsets.zero) + (viewPadding ?? EdgeInsets.zero),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(padding),
|
||||
child: Selector<MediaQueryData, Orientation>(
|
||||
selector: (c, mq) => mq.orientation,
|
||||
builder: (c, orientation, child) {
|
||||
final targetCount = orientation == Orientation.landscape ? landscapeActionCount : portraitActionCount;
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final availableCount = (constraints.maxWidth / (kMinInteractiveDimension + padding)).floor() - 2;
|
||||
final quickActionCount = min(targetCount, availableCount);
|
||||
child: Selector<MediaQueryData, Tuple2<double, Orientation>>(
|
||||
selector: (c, mq) => Tuple2(mq.size.width, mq.orientation),
|
||||
builder: (c, mq, child) {
|
||||
final mqWidth = mq.item1;
|
||||
final mqOrientation = mq.item2;
|
||||
|
||||
final quickActions = [
|
||||
EntryAction.toggleFavourite,
|
||||
EntryAction.share,
|
||||
EntryAction.delete,
|
||||
].where(_canDo).take(quickActionCount);
|
||||
final inAppActions = EntryActions.inApp.where((action) => !quickActions.contains(action)).where(_canDo);
|
||||
final externalAppActions = EntryActions.externalApp.where(_canDo);
|
||||
final targetCount = mqOrientation == Orientation.landscape ? landscapeActionCount : portraitActionCount;
|
||||
final availableCount = (mqWidth / (kMinInteractiveDimension + padding)).floor() - 2;
|
||||
final quickActionCount = min(targetCount, availableCount);
|
||||
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
final quickActions = [
|
||||
EntryAction.toggleFavourite,
|
||||
EntryAction.share,
|
||||
EntryAction.delete,
|
||||
].where(_canDo).take(quickActionCount).toList();
|
||||
final inAppActions = EntryActions.inApp.where((action) => !quickActions.contains(action)).where(_canDo).toList();
|
||||
final externalAppActions = EntryActions.externalApp.where(_canDo).toList();
|
||||
|
||||
return _TopOverlayRow(
|
||||
quickActions: quickActions,
|
||||
inAppActions: inAppActions,
|
||||
externalAppActions: externalAppActions,
|
||||
scale: scale,
|
||||
isFavouriteNotifier: entry.isFavouriteNotifier,
|
||||
onActionSelected: onActionSelected,
|
||||
);
|
||||
},
|
||||
),
|
||||
|
@ -118,6 +97,56 @@ class FullscreenTopOverlay extends StatelessWidget {
|
|||
}
|
||||
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 child;
|
||||
|
@ -125,7 +154,7 @@ class FullscreenTopOverlay extends StatelessWidget {
|
|||
switch (action) {
|
||||
case EntryAction.toggleFavourite:
|
||||
child = ValueListenableBuilder<bool>(
|
||||
valueListenable: entry.isFavouriteNotifier,
|
||||
valueListenable: isFavouriteNotifier,
|
||||
builder: (context, isFavourite, child) => Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
|
@ -136,7 +165,7 @@ class FullscreenTopOverlay extends StatelessWidget {
|
|||
),
|
||||
Sweeper(
|
||||
builder: (context) => const Icon(AIcons.favourite, color: Colors.redAccent),
|
||||
toggledNotifier: entry.isFavouriteNotifier,
|
||||
toggledNotifier: isFavouriteNotifier,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -178,7 +207,7 @@ class FullscreenTopOverlay extends StatelessWidget {
|
|||
switch (action) {
|
||||
// in app actions
|
||||
case EntryAction.toggleFavourite:
|
||||
child = entry.isFavouriteNotifier.value
|
||||
child = isFavouriteNotifier.value
|
||||
? const MenuRow(
|
||||
text: 'Remove from favourites',
|
||||
icon: AIcons.favouriteActive,
|
||||
|
|
Loading…
Reference in a new issue