viewer: minimap in overlay

This commit is contained in:
Thibault Deckers 2020-11-02 19:18:27 +09:00
parent c742b72f70
commit 9b9dc1db40
7 changed files with 243 additions and 37 deletions

View file

@ -45,6 +45,7 @@ class Settings extends ChangeNotifier {
static const pinnedFiltersKey = 'pinned_filters'; static const pinnedFiltersKey = 'pinned_filters';
// viewer // viewer
static const showOverlayMinimapKey = 'show_overlay_minimap';
static const showOverlayShootingDetailsKey = 'show_overlay_shooting_details'; static const showOverlayShootingDetailsKey = 'show_overlay_shooting_details';
// info // info
@ -159,6 +160,10 @@ class Settings extends ChangeNotifier {
// viewer // viewer
bool get showOverlayMinimap => getBoolOrDefault(showOverlayMinimapKey, false);
set showOverlayMinimap(bool newValue) => setAndNotify(showOverlayMinimapKey, newValue);
bool get showOverlayShootingDetails => getBoolOrDefault(showOverlayShootingDetailsKey, true); bool get showOverlayShootingDetails => getBoolOrDefault(showOverlayShootingDetailsKey, true);
set showOverlayShootingDetails(bool newValue) => setAndNotify(showOverlayShootingDetailsKey, newValue); set showOverlayShootingDetails(bool newValue) => setAndNotify(showOverlayShootingDetailsKey, newValue);

View file

@ -10,6 +10,7 @@ import 'package:aves/utils/durations.dart';
import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/collection/collection_page.dart';
import 'package:aves/widgets/common/action_delegates/entry_action_delegate.dart'; import 'package:aves/widgets/common/action_delegates/entry_action_delegate.dart';
import 'package:aves/widgets/fullscreen/image_page.dart'; import 'package:aves/widgets/fullscreen/image_page.dart';
import 'package:aves/widgets/fullscreen/image_view.dart';
import 'package:aves/widgets/fullscreen/info/info_page.dart'; import 'package:aves/widgets/fullscreen/info/info_page.dart';
import 'package:aves/widgets/fullscreen/info/notifications.dart'; import 'package:aves/widgets/fullscreen/info/notifications.dart';
import 'package:aves/widgets/fullscreen/overlay/bottom.dart'; import 'package:aves/widgets/fullscreen/overlay/bottom.dart';
@ -52,6 +53,7 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
EdgeInsets _frozenViewInsets, _frozenViewPadding; EdgeInsets _frozenViewInsets, _frozenViewPadding;
EntryActionDelegate _actionDelegate; EntryActionDelegate _actionDelegate;
final List<Tuple2<String, IjkMediaController>> _videoControllers = []; final List<Tuple2<String, IjkMediaController>> _videoControllers = [];
final List<Tuple2<String, ValueNotifier<ViewState>>> _viewStateNotifiers = [];
CollectionLens get collection => widget.collection; CollectionLens get collection => widget.collection;
@ -97,7 +99,7 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
collection: collection, collection: collection,
showInfo: () => _goToVerticalPage(infoPage), showInfo: () => _goToVerticalPage(infoPage),
); );
_initVideoController(); _initViewStateControllers();
_registerWidget(widget); _registerWidget(widget);
WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addObserver(this);
WidgetsBinding.instance.addPostFrameCallback((_) => _initOverlay()); WidgetsBinding.instance.addPostFrameCallback((_) => _initOverlay());
@ -169,6 +171,7 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
onHorizontalPageChanged: _onHorizontalPageChanged, onHorizontalPageChanged: _onHorizontalPageChanged,
onImageTap: () => _overlayVisible.value = !_overlayVisible.value, onImageTap: () => _overlayVisible.value = !_overlayVisible.value,
onImagePageRequested: () => _goToVerticalPage(imagePage), onImagePageRequested: () => _goToVerticalPage(imagePage),
viewStateNotifiers: _viewStateNotifiers,
), ),
_buildTopOverlay(), _buildTopOverlay(),
_buildBottomOverlay(), _buildBottomOverlay(),
@ -183,6 +186,7 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
valueListenable: _entryNotifier, valueListenable: _entryNotifier,
builder: (context, entry, child) { builder: (context, entry, child) {
if (entry == null) return SizedBox.shrink(); if (entry == null) return SizedBox.shrink();
final viewStateNotifier = _viewStateNotifiers.firstWhere((kv) => kv.item1 == entry.uri, orElse: () => null)?.item2;
return FullscreenTopOverlay( return FullscreenTopOverlay(
entry: entry, entry: entry,
scale: _topOverlayScale, scale: _topOverlayScale,
@ -190,6 +194,7 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
viewInsets: _frozenViewInsets, viewInsets: _frozenViewInsets,
viewPadding: _frozenViewPadding, viewPadding: _frozenViewPadding,
onActionSelected: (action) => _actionDelegate.onActionSelected(context, entry, action), onActionSelected: (action) => _actionDelegate.onActionSelected(context, entry, action),
viewStateNotifier: viewStateNotifier,
); );
}, },
); );
@ -324,7 +329,7 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
if (_entryNotifier.value == newEntry) return; if (_entryNotifier.value == newEntry) return;
_entryNotifier.value = newEntry; _entryNotifier.value = newEntry;
_pauseVideoControllers(); _pauseVideoControllers();
_initVideoController(); _initViewStateControllers();
} }
void _onLeave() { void _onLeave() {
@ -381,33 +386,51 @@ class FullscreenBodyState extends State<FullscreenBody> with SingleTickerProvide
} }
} }
// video controller // state controllers/monitors
void _pauseVideoControllers() => _videoControllers.forEach((e) => e.item2.pause()); void _initViewStateControllers() {
Future<void> _initVideoController() async {
final entry = _entryNotifier.value; final entry = _entryNotifier.value;
if (entry == null || !entry.isVideo) return; if (entry == null) return;
final uri = entry.uri; final uri = entry.uri;
var controllerEntry = _videoControllers.firstWhere((kv) => kv.item1 == uri, orElse: () => null); _initViewSpecificController<ValueNotifier<ViewState>>(
if (controllerEntry != null) { uri,
_videoControllers.remove(controllerEntry); _viewStateNotifiers,
} else { () => ValueNotifier<ViewState>(ViewState.zero),
// do not set data source of IjkMediaController here (_) => _.dispose(),
controllerEntry = Tuple2(uri, IjkMediaController()); );
} if (entry.isVideo) {
_videoControllers.insert(0, controllerEntry); _initViewSpecificController<IjkMediaController>(
while (_videoControllers.length > 3) { uri,
_videoControllers.removeLast().item2.dispose(); _videoControllers,
() => IjkMediaController(),
(_) => _.dispose(),
);
} }
setState(() {}); setState(() {});
} }
void _initViewSpecificController<T>(String uri, List<Tuple2<String, T>> controllers, T Function() builder, void Function(T controller) disposer) {
var controller = controllers.firstWhere((kv) => kv.item1 == uri, orElse: () => null);
if (controller != null) {
controllers.remove(controller);
} else {
controller = Tuple2(uri, builder());
}
controllers.insert(0, controller);
while (controllers.length > 3) {
disposer?.call(controllers.removeLast().item2);
}
}
void _pauseVideoControllers() => _videoControllers.forEach((e) => e.item2.pause());
} }
class FullscreenVerticalPageView extends StatefulWidget { class FullscreenVerticalPageView extends StatefulWidget {
final CollectionLens collection; final CollectionLens collection;
final ValueNotifier<ImageEntry> entryNotifier; final ValueNotifier<ImageEntry> entryNotifier;
final List<Tuple2<String, ValueNotifier<ViewState>>> viewStateNotifiers;
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;
@ -416,6 +439,7 @@ class FullscreenVerticalPageView extends StatefulWidget {
const FullscreenVerticalPageView({ const FullscreenVerticalPageView({
@required this.collection, @required this.collection,
@required this.entryNotifier, @required this.entryNotifier,
@required this.viewStateNotifiers,
@required this.videoControllers, @required this.videoControllers,
@required this.verticalPager, @required this.verticalPager,
@required this.horizontalPager, @required this.horizontalPager,
@ -482,11 +506,13 @@ class _FullscreenVerticalPageViewState extends State<FullscreenVerticalPageView>
pageController: widget.horizontalPager, pageController: widget.horizontalPager,
onTap: widget.onImageTap, onTap: widget.onImageTap,
onPageChanged: widget.onHorizontalPageChanged, onPageChanged: widget.onHorizontalPageChanged,
viewStateNotifiers: widget.viewStateNotifiers,
videoControllers: widget.videoControllers, videoControllers: widget.videoControllers,
) )
: SingleImagePage( : SingleImagePage(
entry: entry, entry: entry,
onTap: widget.onImageTap, onTap: widget.onImageTap,
viewStateNotifiers: widget.viewStateNotifiers,
videoControllers: widget.videoControllers, videoControllers: widget.videoControllers,
), ),
NotificationListener( NotificationListener(

View file

@ -10,16 +10,16 @@ class MultiImagePage extends StatefulWidget {
final CollectionLens collection; final CollectionLens collection;
final PageController pageController; final PageController pageController;
final ValueChanged<int> onPageChanged; final ValueChanged<int> onPageChanged;
final ValueChanged<PhotoViewScaleState> onScaleChanged;
final VoidCallback onTap; final VoidCallback onTap;
final List<Tuple2<String, ValueNotifier<ViewState>>> viewStateNotifiers;
final List<Tuple2<String, IjkMediaController>> videoControllers; final List<Tuple2<String, IjkMediaController>> videoControllers;
const MultiImagePage({ const MultiImagePage({
this.collection, this.collection,
this.pageController, this.pageController,
this.onPageChanged, this.onPageChanged,
this.onScaleChanged,
this.onTap, this.onTap,
this.viewStateNotifiers,
this.videoControllers, this.videoControllers,
}); });
@ -49,8 +49,8 @@ class MultiImagePageState extends State<MultiImagePage> with AutomaticKeepAliveC
key: Key('imageview'), key: Key('imageview'),
entry: entry, entry: entry,
heroTag: widget.collection.heroTag(entry), heroTag: widget.collection.heroTag(entry),
onScaleChanged: widget.onScaleChanged,
onTap: widget.onTap, onTap: widget.onTap,
viewStateNotifiers: widget.viewStateNotifiers,
videoControllers: widget.videoControllers, videoControllers: widget.videoControllers,
), ),
); );
@ -66,14 +66,14 @@ class MultiImagePageState extends State<MultiImagePage> with AutomaticKeepAliveC
class SingleImagePage extends StatefulWidget { class SingleImagePage extends StatefulWidget {
final ImageEntry entry; final ImageEntry entry;
final ValueChanged<PhotoViewScaleState> onScaleChanged;
final VoidCallback onTap; final VoidCallback onTap;
final List<Tuple2<String, ValueNotifier<ViewState>>> viewStateNotifiers;
final List<Tuple2<String, IjkMediaController>> videoControllers; final List<Tuple2<String, IjkMediaController>> videoControllers;
const SingleImagePage({ const SingleImagePage({
this.entry, this.entry,
this.onScaleChanged,
this.onTap, this.onTap,
this.viewStateNotifiers,
this.videoControllers, this.videoControllers,
}); });
@ -90,8 +90,8 @@ class SingleImagePageState extends State<SingleImagePage> with AutomaticKeepAliv
axis: [Axis.vertical], axis: [Axis.vertical],
child: ImageView( child: ImageView(
entry: widget.entry, entry: widget.entry,
onScaleChanged: widget.onScaleChanged,
onTap: widget.onTap, onTap: widget.onTap,
viewStateNotifiers: widget.viewStateNotifiers,
videoControllers: widget.videoControllers, videoControllers: widget.videoControllers,
), ),
); );

View file

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:aves/model/image_entry.dart'; import 'package:aves/model/image_entry.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/widgets/collection/empty.dart'; import 'package:aves/widgets/collection/empty.dart';
@ -6,28 +8,56 @@ import 'package:aves/widgets/common/image_providers/thumbnail_provider.dart';
import 'package:aves/widgets/common/image_providers/uri_image_provider.dart'; import 'package:aves/widgets/common/image_providers/uri_image_provider.dart';
import 'package:aves/widgets/common/image_providers/uri_picture_provider.dart'; import 'package:aves/widgets/common/image_providers/uri_picture_provider.dart';
import 'package:aves/widgets/fullscreen/video_view.dart'; import 'package:aves/widgets/fullscreen/video_view.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_ijkplayer/flutter_ijkplayer.dart'; import 'package:flutter_ijkplayer/flutter_ijkplayer.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:photo_view/photo_view.dart'; import 'package:photo_view/photo_view.dart';
import 'package:tuple/tuple.dart'; import 'package:tuple/tuple.dart';
class ImageView extends StatelessWidget { class ImageView extends StatefulWidget {
final ImageEntry entry; final ImageEntry entry;
final Object heroTag; final Object heroTag;
final ValueChanged<PhotoViewScaleState> onScaleChanged;
final VoidCallback onTap; final VoidCallback onTap;
final List<Tuple2<String, ValueNotifier<ViewState>>> viewStateNotifiers;
final List<Tuple2<String, IjkMediaController>> videoControllers; final List<Tuple2<String, IjkMediaController>> videoControllers;
const ImageView({ const ImageView({
Key key, Key key,
this.entry, @required this.entry,
this.heroTag, this.heroTag,
this.onScaleChanged, @required this.onTap,
this.onTap, @required this.viewStateNotifiers,
this.videoControllers, @required this.videoControllers,
}) : super(key: key); }) : super(key: key);
@override
_ImageViewState createState() => _ImageViewState();
}
class _ImageViewState extends State<ImageView> {
final PhotoViewController _photoViewController = PhotoViewController();
StreamSubscription<PhotoViewControllerValue> _subscription;
ImageEntry get entry => widget.entry;
VoidCallback get onTap => widget.onTap;
ValueNotifier<ViewState> get viewStateNotifier => widget.viewStateNotifiers.firstWhere((kv) => kv.item1 == entry.uri, orElse: () => null)?.item2;
@override
void initState() {
super.initState();
_subscription = _photoViewController.outputStateStream.listen(_onViewChanged);
}
@override
void dispose() {
_subscription.cancel();
_subscription = null;
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
const backgroundDecoration = BoxDecoration(color: Colors.transparent); const backgroundDecoration = BoxDecoration(color: Colors.transparent);
@ -35,7 +65,7 @@ class ImageView extends StatelessWidget {
// no hero for videos, as a typical video first frame is different from its thumbnail // no hero for videos, as a typical video first frame is different from its thumbnail
if (entry.isVideo) { if (entry.isVideo) {
final videoController = videoControllers.firstWhere((kv) => kv.item1 == entry.uri, orElse: () => null)?.item2; final videoController = widget.videoControllers.firstWhere((kv) => kv.item1 == entry.uri, orElse: () => null)?.item2;
return PhotoView.customChild( return PhotoView.customChild(
child: videoController != null child: videoController != null
? AvesVideo( ? AvesVideo(
@ -44,7 +74,7 @@ class ImageView extends StatelessWidget {
) )
: SizedBox(), : SizedBox(),
backgroundDecoration: backgroundDecoration, backgroundDecoration: backgroundDecoration,
scaleStateChangedCallback: onScaleChanged, controller: _photoViewController,
minScale: PhotoViewComputedScale.contained, minScale: PhotoViewComputedScale.contained,
initialScale: PhotoViewComputedScale.contained, initialScale: PhotoViewComputedScale.contained,
onTapUp: (tapContext, details, value) => onTap?.call(), onTapUp: (tapContext, details, value) => onTap?.call(),
@ -88,7 +118,7 @@ class ImageView extends StatelessWidget {
colorFilter: colorFilter, colorFilter: colorFilter,
), ),
backgroundDecoration: backgroundDecoration, backgroundDecoration: backgroundDecoration,
scaleStateChangedCallback: onScaleChanged, controller: _photoViewController,
minScale: PhotoViewComputedScale.contained, minScale: PhotoViewComputedScale.contained,
initialScale: PhotoViewComputedScale.contained, initialScale: PhotoViewComputedScale.contained,
onTapUp: (tapContext, details, value) => onTap?.call(), onTapUp: (tapContext, details, value) => onTap?.call(),
@ -113,7 +143,7 @@ class ImageView extends StatelessWidget {
), ),
loadFailedChild: _buildError(), loadFailedChild: _buildError(),
backgroundDecoration: backgroundDecoration, backgroundDecoration: backgroundDecoration,
scaleStateChangedCallback: onScaleChanged, controller: _photoViewController,
minScale: PhotoViewComputedScale.contained, minScale: PhotoViewComputedScale.contained,
initialScale: PhotoViewComputedScale.contained, initialScale: PhotoViewComputedScale.contained,
onTapUp: (tapContext, details, value) => onTap?.call(), onTapUp: (tapContext, details, value) => onTap?.call(),
@ -123,9 +153,9 @@ class ImageView extends StatelessWidget {
child = _buildError(); child = _buildError();
} }
return heroTag != null return widget.heroTag != null
? Hero( ? Hero(
tag: heroTag, tag: widget.heroTag,
transitionOnUserGestures: true, transitionOnUserGestures: true,
child: child, child: child,
) )
@ -145,4 +175,22 @@ class ImageView extends StatelessWidget {
), ),
), ),
); );
void _onViewChanged(PhotoViewControllerValue v) {
viewStateNotifier?.value = ViewState(v.position, v.scale);
}
}
class ViewState {
final Offset position;
final double scale;
static const ViewState zero = ViewState(Offset(0.0, 0.0), 0);
const ViewState(this.position, this.scale);
@override
String toString() {
return '$runtimeType#${shortHash(this)}{position=$position, scale=$scale}';
}
} }

View file

@ -0,0 +1,101 @@
import 'dart:math';
import 'package:aves/model/image_entry.dart';
import 'package:aves/widgets/fullscreen/image_view.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class Minimap extends StatelessWidget {
final ImageEntry entry;
final ValueNotifier<ViewState> viewStateNotifier;
final Size size;
static const defaultSize = Size(96, 96);
const Minimap({
@required this.entry,
@required this.viewStateNotifier,
this.size = defaultSize,
});
@override
Widget build(BuildContext context) {
return Selector<MediaQueryData, Size>(
selector: (context, mq) => mq.size,
builder: (context, mqSize, child) {
return AnimatedBuilder(
animation: viewStateNotifier,
builder: (context, child) {
return CustomPaint(
painter: MinimapPainter(
entrySize: entry.displaySize,
viewportSize: mqSize,
viewCenterOffset: viewStateNotifier.value.position,
viewScale: viewStateNotifier.value.scale,
minimapBorderColor: Colors.white30,
),
size: size,
);
});
});
}
}
class MinimapPainter extends CustomPainter {
final Size entrySize, viewportSize;
final Offset viewCenterOffset;
final double viewScale;
final Color minimapBorderColor, viewportBorderColor;
const MinimapPainter({
@required this.entrySize,
@required this.viewportSize,
@required this.viewCenterOffset,
@required this.viewScale,
this.minimapBorderColor = Colors.white,
this.viewportBorderColor = Colors.white,
});
@override
void paint(Canvas canvas, Size size) {
final viewSize = entrySize * viewScale;
if (viewSize.isEmpty) return;
final canvasScale = size.longestSide / viewSize.longestSide;
final scaledEntrySize = viewSize * canvasScale;
final scaledViewportSize = viewportSize * canvasScale;
// hide minimap when image is in full view
if (scaledViewportSize >= scaledEntrySize) return;
final entryRect = Rect.fromCenter(
center: size.center(Offset.zero),
width: scaledEntrySize.width,
height: scaledEntrySize.height,
);
final viewportRect = Rect.fromCenter(
center: size.center(Offset.zero) - viewCenterOffset * canvasScale,
width: min(scaledEntrySize.width, scaledViewportSize.width),
height: min(scaledEntrySize.height, scaledViewportSize.height),
);
canvas.translate((entryRect.width - size.width) / 2, (entryRect.height - size.height) / 2);
final fill = Paint()
..style = PaintingStyle.fill
..color = Color(0x33000000);
final minimapStroke = Paint()
..style = PaintingStyle.stroke
..color = minimapBorderColor;
final viewportStroke = Paint()
..style = PaintingStyle.stroke
..color = viewportBorderColor;
canvas.drawRect(viewportRect, fill);
canvas.drawRect(entryRect, fill);
canvas.drawRect(entryRect, minimapStroke);
canvas.drawRect(viewportRect, viewportStroke);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}

View file

@ -2,11 +2,14 @@ import 'dart:math';
import 'package:aves/model/favourite_repo.dart'; import 'package:aves/model/favourite_repo.dart';
import 'package:aves/model/image_entry.dart'; import 'package:aves/model/image_entry.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/widgets/common/entry_actions.dart'; import 'package:aves/widgets/common/entry_actions.dart';
import 'package:aves/widgets/common/fx/sweeper.dart'; import 'package:aves/widgets/common/fx/sweeper.dart';
import 'package:aves/widgets/common/icons.dart'; import 'package:aves/widgets/common/icons.dart';
import 'package:aves/widgets/common/menu_row.dart'; import 'package:aves/widgets/common/menu_row.dart';
import 'package:aves/widgets/fullscreen/image_view.dart';
import 'package:aves/widgets/fullscreen/overlay/common.dart'; import 'package:aves/widgets/fullscreen/overlay/common.dart';
import 'package:aves/widgets/fullscreen/overlay/minimap.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';
@ -18,6 +21,7 @@ class FullscreenTopOverlay extends StatelessWidget {
final EdgeInsets viewInsets, viewPadding; final EdgeInsets viewInsets, viewPadding;
final Function(EntryAction value) onActionSelected; final Function(EntryAction value) onActionSelected;
final bool canToggleFavourite; final bool canToggleFavourite;
final ValueNotifier<ViewState> viewStateNotifier;
static const double padding = 8; static const double padding = 8;
@ -33,6 +37,7 @@ class FullscreenTopOverlay extends StatelessWidget {
@required this.viewInsets, @required this.viewInsets,
@required this.viewPadding, @required this.viewPadding,
@required this.onActionSelected, @required this.onActionSelected,
this.viewStateNotifier,
}) : super(key: key); }) : super(key: key);
@override @override
@ -58,8 +63,7 @@ class FullscreenTopOverlay extends StatelessWidget {
].where(_canDo).take(quickActionCount).toList(); ].where(_canDo).take(quickActionCount).toList();
final inAppActions = EntryActions.inApp.where((action) => !quickActions.contains(action)).where(_canDo).toList(); final inAppActions = EntryActions.inApp.where((action) => !quickActions.contains(action)).where(_canDo).toList();
final externalAppActions = EntryActions.externalApp.where(_canDo).toList(); final externalAppActions = EntryActions.externalApp.where(_canDo).toList();
final buttonRow = _TopOverlayRow(
return _TopOverlayRow(
quickActions: quickActions, quickActions: quickActions,
inAppActions: inAppActions, inAppActions: inAppActions,
externalAppActions: externalAppActions, externalAppActions: externalAppActions,
@ -67,6 +71,23 @@ class FullscreenTopOverlay extends StatelessWidget {
entry: entry, entry: entry,
onActionSelected: onActionSelected, onActionSelected: onActionSelected,
); );
return settings.showOverlayMinimap && viewStateNotifier != null
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
buttonRow,
SizedBox(height: 8),
FadeTransition(
opacity: scale,
child: Minimap(
entry: entry,
viewStateNotifier: viewStateNotifier,
),
)
],
)
: buttonRow;
}, },
), ),
), ),

View file

@ -177,6 +177,11 @@ class _SettingsPageState extends State<SettingsPage> {
title: 'Viewer', title: 'Viewer',
expandedNotifier: _expandedNotifier, expandedNotifier: _expandedNotifier,
children: [ children: [
SwitchListTile(
value: settings.showOverlayMinimap,
onChanged: (v) => settings.showOverlayMinimap = v,
title: Text('Show minimap'),
),
SwitchListTile( SwitchListTile(
value: settings.showOverlayShootingDetails, value: settings.showOverlayShootingDetails,
onChanged: (v) => settings.showOverlayShootingDetails = v, onChanged: (v) => settings.showOverlayShootingDetails = v,