tv: stat donut legend focus, map captioned buttons, viewer button focus

This commit is contained in:
Thibault Deckers 2023-01-11 17:04:45 +01:00
parent 4d226e6e38
commit bcced35e66
25 changed files with 219 additions and 110 deletions

View file

@ -25,6 +25,7 @@ class MediaSessionHandler(private val context: Context, private val mediaCommand
private var session: MediaSessionCompat? = null private var session: MediaSessionCompat? = null
private var wasPlaying = false private var wasPlaying = false
private var isNoisyAudioReceiverRegistered = false
private val noisyAudioReceiver = object : BroadcastReceiver() { private val noisyAudioReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) { override fun onReceive(context: Context, intent: Intent) {
if (intent.action == AudioManager.ACTION_AUDIO_BECOMING_NOISY) { if (intent.action == AudioManager.ACTION_AUDIO_BECOMING_NOISY) {
@ -34,7 +35,10 @@ class MediaSessionHandler(private val context: Context, private val mediaCommand
} }
fun dispose() { fun dispose() {
context.unregisterReceiver(noisyAudioReceiver) if (isNoisyAudioReceiverRegistered) {
context.unregisterReceiver(noisyAudioReceiver)
isNoisyAudioReceiverRegistered = false
}
} }
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
@ -110,8 +114,10 @@ class MediaSessionHandler(private val context: Context, private val mediaCommand
val isPlaying = state == PlaybackStateCompat.STATE_PLAYING val isPlaying = state == PlaybackStateCompat.STATE_PLAYING
if (!wasPlaying && isPlaying) { if (!wasPlaying && isPlaying) {
context.registerReceiver(noisyAudioReceiver, IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY)) context.registerReceiver(noisyAudioReceiver, IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY))
isNoisyAudioReceiverRegistered = true
} else if (wasPlaying && !isPlaying) { } else if (wasPlaying && !isPlaying) {
context.unregisterReceiver(noisyAudioReceiver) context.unregisterReceiver(noisyAudioReceiver)
isNoisyAudioReceiverRegistered = false
} }
wasPlaying = isPlaying wasPlaying = isPlaying
} }

View file

@ -0,0 +1,35 @@
import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/widgets.dart';
enum MapAction {
selectStyle,
zoomIn,
zoomOut,
}
extension ExtraMapAction on MapAction {
String getText(BuildContext context) {
switch (this) {
case MapAction.selectStyle:
return context.l10n.mapStyleTooltip;
case MapAction.zoomIn:
return context.l10n.mapZoomInTooltip;
case MapAction.zoomOut:
return context.l10n.mapZoomOutTooltip;
}
}
Widget getIcon() => Icon(_getIconData());
IconData _getIconData() {
switch (this) {
case MapAction.selectStyle:
return AIcons.layers;
case MapAction.zoomIn:
return AIcons.zoomIn;
case MapAction.zoomOut:
return AIcons.zoomOut;
}
}
}

View file

@ -27,8 +27,6 @@ class Device {
bool get isDynamicColorAvailable => _isDynamicColorAvailable; bool get isDynamicColorAvailable => _isDynamicColorAvailable;
bool get isReadOnly => _isTelevision;
bool get isTelevision => _isTelevision; bool get isTelevision => _isTelevision;
bool get showPinShortcutFeedback => _showPinShortcutFeedback; bool get showPinShortcutFeedback => _showPinShortcutFeedback;

View file

@ -3,7 +3,6 @@ import 'dart:io';
import 'dart:ui'; import 'dart:ui';
import 'package:aves/geo/countries.dart'; import 'package:aves/geo/countries.dart';
import 'package:aves/model/device.dart';
import 'package:aves/model/entry_cache.dart'; import 'package:aves/model/entry_cache.dart';
import 'package:aves/model/entry_dirs.dart'; import 'package:aves/model/entry_dirs.dart';
import 'package:aves/model/favourites.dart'; import 'package:aves/model/favourites.dart';
@ -12,6 +11,7 @@ import 'package:aves/model/metadata/address.dart';
import 'package:aves/model/metadata/catalog.dart'; import 'package:aves/model/metadata/catalog.dart';
import 'package:aves/model/metadata/trash.dart'; import 'package:aves/model/metadata/trash.dart';
import 'package:aves/model/multipage.dart'; import 'package:aves/model/multipage.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/trash.dart'; import 'package:aves/model/source/trash.dart';
import 'package:aves/model/video/metadata.dart'; import 'package:aves/model/video/metadata.dart';
import 'package:aves/ref/mime_types.dart'; import 'package:aves/ref/mime_types.dart';
@ -281,7 +281,7 @@ class AvesEntry {
bool get isMediaStoreMediaContent => isMediaStoreContent && {'/external/images/', '/external/video/'}.any(uri.contains); bool get isMediaStoreMediaContent => isMediaStoreContent && {'/external/images/', '/external/video/'}.any(uri.contains);
bool get canEdit => !device.isReadOnly && path != null && !trashed && isMediaStoreContent; bool get canEdit => !settings.isReadOnly && path != null && !trashed && isMediaStoreContent;
bool get canEditDate => canEdit && (canEditExif || canEditXmp); bool get canEditDate => canEdit && (canEditExif || canEditXmp);

View file

@ -403,6 +403,8 @@ class Settings extends ChangeNotifier {
bool get useTvLayout => device.isTelevision || forceTvLayout; bool get useTvLayout => device.isTelevision || forceTvLayout;
bool get isReadOnly => useTvLayout;
// navigation // navigation
bool get mustBackTwiceToExit => getBool(mustBackTwiceToExitKey) ?? SettingsDefaults.mustBackTwiceToExit; bool get mustBackTwiceToExit => getBool(mustBackTwiceToExitKey) ?? SettingsDefaults.mustBackTwiceToExit;

View file

@ -2,7 +2,6 @@ import 'dart:async';
import 'package:aves/app_mode.dart'; import 'package:aves/app_mode.dart';
import 'package:aves/model/actions/entry_set_actions.dart'; import 'package:aves/model/actions/entry_set_actions.dart';
import 'package:aves/model/device.dart';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/filters/query.dart'; import 'package:aves/model/filters/query.dart';
@ -392,7 +391,7 @@ class _CollectionAppBarState extends State<CollectionAppBar> with SingleTickerPr
...(isSelecting ? selectionMenuActions : browsingMenuActions).where(isVisible).map( ...(isSelecting ? selectionMenuActions : browsingMenuActions).where(isVisible).map(
(action) => _toMenuItem(action, enabled: canApply(action), selection: selection), (action) => _toMenuItem(action, enabled: canApply(action), selection: selection),
), ),
if (isSelecting && !device.isReadOnly && appMode == AppMode.main && !isTrash) if (isSelecting && !settings.isReadOnly && appMode == AppMode.main && !isTrash)
PopupMenuItem<EntrySetAction>( PopupMenuItem<EntrySetAction>(
enabled: hasSelection, enabled: hasSelection,
padding: EdgeInsets.zero, padding: EdgeInsets.zero,

View file

@ -55,7 +55,7 @@ class EntrySetActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAware
required int selectedItemCount, required int selectedItemCount,
required bool isTrash, required bool isTrash,
}) { }) {
final canWrite = !device.isReadOnly; final canWrite = !settings.isReadOnly;
final isMain = appMode == AppMode.main; final isMain = appMode == AppMode.main;
switch (action) { switch (action) {
// general // general

View file

@ -34,17 +34,16 @@ class SectionHeader<T> extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
Widget child = _buildContent(context); Widget child = _buildContent(context);
if (settings.useTvLayout) { if (settings.useTvLayout) {
final colors = Theme.of(context).colorScheme; final primaryColor = Theme.of(context).colorScheme.primary;
child = Material( child = Material(
type: MaterialType.transparency, type: MaterialType.transparency,
child: InkResponse( child: InkResponse(
onTap: _onTap(context), onTap: _onTap(context),
onHover: (_) {}, containedInkWell: true,
highlightShape: BoxShape.rectangle, highlightShape: BoxShape.rectangle,
borderRadius: const BorderRadius.all(Radius.circular(123)), borderRadius: const BorderRadius.all(Radius.circular(123)),
containedInkWell: true, hoverColor: primaryColor.withOpacity(0.04),
splashColor: colors.primary.withOpacity(0.12), splashColor: primaryColor.withOpacity(0.12),
hoverColor: colors.primary.withOpacity(0.04),
child: child, child: child,
), ),
); );

View file

@ -73,7 +73,7 @@ class _OverlayButtonState extends State<OverlayButton> {
builder: (context, focused, child) { builder: (context, focused, child) {
final border = AvesBorder.border( final border = AvesBorder.border(
context, context,
width: AvesBorder.curvedBorderWidth * (focused ? 2 : 1), width: AvesBorder.curvedBorderWidth * (focused ? 3 : 1),
); );
return borderRadius != null return borderRadius != null
? BlurredRRect( ? BlurredRRect(

View file

@ -1,28 +1,27 @@
import 'package:aves/model/settings/enums/map_style.dart'; import 'package:aves/model/actions/map_actions.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/map/buttons/button.dart'; import 'package:aves/widgets/common/map/buttons/button.dart';
import 'package:aves/widgets/common/map/buttons/coordinate_filter.dart'; import 'package:aves/widgets/common/map/buttons/coordinate_filter.dart';
import 'package:aves/widgets/common/map/compass.dart'; import 'package:aves/widgets/common/map/compass.dart';
import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; import 'package:aves/widgets/common/map/map_action_delegate.dart';
import 'package:aves_map/aves_map.dart'; import 'package:aves_map/aves_map.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
class MapButtonPanel extends StatelessWidget { class MapButtonPanel extends StatelessWidget {
final AvesMapController? controller;
final ValueNotifier<ZoomedBounds> boundsNotifier; final ValueNotifier<ZoomedBounds> boundsNotifier;
final Future<void> Function(double amount)? zoomBy;
final void Function(BuildContext context)? openMapPage; final void Function(BuildContext context)? openMapPage;
final VoidCallback? resetRotation; final VoidCallback? resetRotation;
const MapButtonPanel({ const MapButtonPanel({
super.key, super.key,
required this.controller,
required this.boundsNotifier, required this.boundsNotifier,
this.zoomBy,
this.openMapPage, this.openMapPage,
this.resetRotation, this.resetRotation,
}); });
@ -123,21 +122,8 @@ class MapButtonPanel extends StatelessWidget {
: const Spacer(), : const Spacer(),
Padding( Padding(
padding: EdgeInsets.only(top: padding), padding: EdgeInsets.only(top: padding),
child: MapOverlayButton( // key is expected by test driver
// key is expected by test driver child: _buildButton(context, MapAction.selectStyle, buttonKey: const Key('map-menu-layers')),
buttonKey: const Key('map-menu-layers'),
icon: const Icon(AIcons.layers),
onPressed: () => showSelectionDialog<EntryMapStyle>(
context: context,
builder: (context) => AvesSelectionDialog<EntryMapStyle?>(
initialValue: settings.mapStyle,
options: Map.fromEntries(availability.mapStyles.map((v) => MapEntry(v, v.getName(context)))),
title: context.l10n.mapStyleDialogTitle,
),
onSelection: (v) => settings.mapStyle = v,
),
tooltip: context.l10n.mapStyleTooltip,
),
), ),
], ],
), ),
@ -148,17 +134,9 @@ class MapButtonPanel extends StatelessWidget {
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
MapOverlayButton( _buildButton(context, MapAction.zoomIn),
icon: const Icon(AIcons.zoomIn),
onPressed: zoomBy != null ? () => zoomBy?.call(1) : null,
tooltip: context.l10n.mapZoomInTooltip,
),
SizedBox(height: padding), SizedBox(height: padding),
MapOverlayButton( _buildButton(context, MapAction.zoomOut),
icon: const Icon(AIcons.zoomOut),
onPressed: zoomBy != null ? () => zoomBy?.call(-1) : null,
tooltip: context.l10n.mapZoomOutTooltip,
),
], ],
), ),
), ),
@ -168,4 +146,11 @@ class MapButtonPanel extends StatelessWidget {
), ),
); );
} }
Widget _buildButton(BuildContext context, MapAction action, {Key? buttonKey}) => MapOverlayButton(
buttonKey: buttonKey,
icon: action.getIcon(),
onPressed: () => MapActionDelegate(controller).onActionSelected(context, action),
tooltip: action.getText(context),
);
} }

View file

@ -144,6 +144,7 @@ class _GeoMapState extends State<GeoMap> {
); );
bool _isMarkerImageReady(MarkerKey<AvesEntry> key) => key.entry.isThumbnailReady(extent: MapThemeData.markerImageExtent); bool _isMarkerImageReady(MarkerKey<AvesEntry> key) => key.entry.isThumbnailReady(extent: MapThemeData.markerImageExtent);
final controller = widget.controller;
Widget child = const SizedBox(); Widget child = const SizedBox();
if (mapStyle != null) { if (mapStyle != null) {
switch (mapStyle) { switch (mapStyle) {
@ -153,7 +154,7 @@ class _GeoMapState extends State<GeoMap> {
case EntryMapStyle.hmsNormal: case EntryMapStyle.hmsNormal:
case EntryMapStyle.hmsTerrain: case EntryMapStyle.hmsTerrain:
child = mobileServices.buildMap<AvesEntry>( child = mobileServices.buildMap<AvesEntry>(
controller: widget.controller, controller: controller,
clusterListenable: _clusterChangeNotifier, clusterListenable: _clusterChangeNotifier,
boundsNotifier: _boundsNotifier, boundsNotifier: _boundsNotifier,
style: mapStyle, style: mapStyle,
@ -175,7 +176,7 @@ class _GeoMapState extends State<GeoMap> {
case EntryMapStyle.stamenToner: case EntryMapStyle.stamenToner:
case EntryMapStyle.stamenWatercolor: case EntryMapStyle.stamenWatercolor:
child = EntryLeafletMap<AvesEntry>( child = EntryLeafletMap<AvesEntry>(
controller: widget.controller, controller: controller,
clusterListenable: _clusterChangeNotifier, clusterListenable: _clusterChangeNotifier,
boundsNotifier: _boundsNotifier, boundsNotifier: _boundsNotifier,
minZoom: 2, minZoom: 2,
@ -260,6 +261,7 @@ class _GeoMapState extends State<GeoMap> {
children: [ children: [
const MapDecorator(), const MapDecorator(),
MapButtonPanel( MapButtonPanel(
controller: controller,
boundsNotifier: _boundsNotifier, boundsNotifier: _boundsNotifier,
openMapPage: widget.openMapPage, openMapPage: widget.openMapPage,
), ),
@ -485,14 +487,13 @@ class _GeoMapState extends State<GeoMap> {
Widget _decorateMap(BuildContext context, Widget? child) => MapDecorator(child: child); Widget _decorateMap(BuildContext context, Widget? child) => MapDecorator(child: child);
Widget _buildButtonPanel( Widget _buildButtonPanel(VoidCallback resetRotation) {
Future<void> Function(double amount) zoomBy, if (settings.useTvLayout) return const SizedBox();
VoidCallback resetRotation, return MapButtonPanel(
) => controller: widget.controller,
MapButtonPanel( boundsNotifier: _boundsNotifier,
boundsNotifier: _boundsNotifier, openMapPage: widget.openMapPage,
zoomBy: zoomBy, resetRotation: resetRotation,
openMapPage: widget.openMapPage, );
resetRotation: resetRotation, }
);
} }

View file

@ -95,6 +95,7 @@ class _EntryLeafletMapState<T> extends State<EntryLeafletMap<T>> with TickerProv
final avesMapController = widget.controller; final avesMapController = widget.controller;
if (avesMapController != null) { if (avesMapController != null) {
_subscriptions.add(avesMapController.moveCommands.listen((event) => _moveTo(event.latLng))); _subscriptions.add(avesMapController.moveCommands.listen((event) => _moveTo(event.latLng)));
_subscriptions.add(avesMapController.zoomCommands.listen((event) => _zoomBy(event.delta)));
} }
_subscriptions.add(_leafletMapController.mapEventStream.listen((event) => _updateVisibleRegion())); _subscriptions.add(_leafletMapController.mapEventStream.listen((event) => _updateVisibleRegion()));
widget.clusterListenable.addListener(_updateMarkers); widget.clusterListenable.addListener(_updateMarkers);
@ -114,7 +115,7 @@ class _EntryLeafletMapState<T> extends State<EntryLeafletMap<T>> with TickerProv
return Stack( return Stack(
children: [ children: [
widget.decoratorBuilder(context, _buildMap()), widget.decoratorBuilder(context, _buildMap()),
widget.buttonPanelBuilder(_zoomBy, _resetRotation), widget.buttonPanelBuilder(_resetRotation),
], ],
); );
} }

View file

@ -0,0 +1,37 @@
import 'package:aves/model/actions/map_actions.dart';
import 'package:aves/model/settings/enums/map_style.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/dialogs/aves_selection_dialog.dart';
import 'package:aves_map/aves_map.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
class MapActionDelegate {
final AvesMapController? controller;
const MapActionDelegate(this.controller);
void onActionSelected(BuildContext context, MapAction action) {
switch (action) {
case MapAction.selectStyle:
showSelectionDialog<EntryMapStyle>(
context: context,
builder: (context) => AvesSelectionDialog<EntryMapStyle?>(
initialValue: settings.mapStyle,
options: Map.fromEntries(availability.mapStyles.map((v) => MapEntry(v, v.getName(context)))),
title: context.l10n.mapStyleDialogTitle,
),
onSelection: (v) => settings.mapStyle = v,
);
break;
case MapAction.zoomIn:
controller?.zoomBy(1);
break;
case MapAction.zoomOut:
controller?.zoomBy(-1);
break;
}
}
}

View file

@ -3,7 +3,6 @@ import 'dart:io';
import 'package:aves/app_mode.dart'; import 'package:aves/app_mode.dart';
import 'package:aves/model/actions/chip_set_actions.dart'; import 'package:aves/model/actions/chip_set_actions.dart';
import 'package:aves/model/actions/move_type.dart'; import 'package:aves/model/actions/move_type.dart';
import 'package:aves/model/device.dart';
import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/album.dart';
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/highlight.dart'; import 'package:aves/model/highlight.dart';
@ -76,10 +75,10 @@ class AlbumChipSetActionDelegate extends ChipSetActionDelegate<AlbumFilter> with
}) { }) {
switch (action) { switch (action) {
case ChipSetAction.createAlbum: case ChipSetAction.createAlbum:
return !device.isReadOnly && appMode == AppMode.main && !isSelecting; return !settings.isReadOnly && appMode == AppMode.main && !isSelecting;
case ChipSetAction.delete: case ChipSetAction.delete:
case ChipSetAction.rename: case ChipSetAction.rename:
return !device.isReadOnly && appMode == AppMode.main && isSelecting; return !settings.isReadOnly && appMode == AppMode.main && isSelecting;
default: default:
return super.isVisible( return super.isVisible(
action, action,

View file

@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:aves/app_mode.dart'; import 'package:aves/app_mode.dart';
import 'package:aves/model/actions/map_actions.dart';
import 'package:aves/model/actions/map_cluster_actions.dart'; import 'package:aves/model/actions/map_cluster_actions.dart';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/model/filters/coordinate.dart'; import 'package:aves/model/filters/coordinate.dart';
@ -16,11 +17,14 @@ import 'package:aves/theme/icons.dart';
import 'package:aves/utils/debouncer.dart'; import 'package:aves/utils/debouncer.dart';
import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/collection/collection_page.dart';
import 'package:aves/widgets/collection/entry_set_action_delegate.dart'; import 'package:aves/widgets/collection/entry_set_action_delegate.dart';
import 'package:aves/widgets/common/basic/insets.dart';
import 'package:aves/widgets/common/basic/menu.dart'; import 'package:aves/widgets/common/basic/menu.dart';
import 'package:aves/widgets/common/behaviour/routes.dart'; import 'package:aves/widgets/common/behaviour/routes.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/identity/buttons/captioned_button.dart';
import 'package:aves/widgets/common/identity/empty.dart'; import 'package:aves/widgets/common/identity/empty.dart';
import 'package:aves/widgets/common/map/geo_map.dart'; import 'package:aves/widgets/common/map/geo_map.dart';
import 'package:aves/widgets/common/map/map_action_delegate.dart';
import 'package:aves/widgets/common/providers/highlight_info_provider.dart'; import 'package:aves/widgets/common/providers/highlight_info_provider.dart';
import 'package:aves/widgets/common/providers/map_theme_provider.dart'; import 'package:aves/widgets/common/providers/map_theme_provider.dart';
import 'package:aves/widgets/common/thumbnail/scroller.dart'; import 'package:aves/widgets/common/thumbnail/scroller.dart';
@ -231,7 +235,7 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin
} }
Widget _buildMap() { Widget _buildMap() {
return MapTheme( Widget child = MapTheme(
interactive: true, interactive: true,
showCoordinateFilter: true, showCoordinateFilter: true,
navigationButton: MapNavigationButton.back, navigationButton: MapNavigationButton.back,
@ -259,6 +263,37 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin
onMarkerLongPress: _onMarkerLongPress, onMarkerLongPress: _onMarkerLongPress,
), ),
); );
if (settings.useTvLayout) {
child = DirectionalSafeArea(
top: false,
end: false,
bottom: false,
child: Row(
children: [
Column(
mainAxisSize: MainAxisSize.min,
children: [
MapAction.selectStyle,
MapAction.zoomIn,
MapAction.zoomOut,
]
.map((action) => Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: CaptionedButton(
icon: action.getIcon(),
caption: action.getText(context),
onPressed: () => MapActionDelegate(_mapController).onActionSelected(context, action),
),
))
.toList(),
),
const SizedBox(width: 16),
Expanded(child: child),
],
),
);
}
return child;
} }
Widget _buildOverlayController() { Widget _buildOverlayController() {

View file

@ -111,35 +111,45 @@ class _MimeDonutState extends State<MimeDonut> with AutomaticKeepAliveClientMixi
], ],
), ),
); );
final primaryColor = Theme.of(context).colorScheme.primary;
final legend = SizedBox( final legend = SizedBox(
width: dim, width: dim,
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: seriesData children: seriesData
.map((d) => GestureDetector( .map((d) => Material(
onTap: () => widget.onFilterSelection(MimeFilter(d.mimeType)), type: MaterialType.transparency,
child: Row( child: InkResponse(
mainAxisSize: MainAxisSize.min, onTap: () => widget.onFilterSelection(MimeFilter(d.mimeType)),
children: [ containedInkWell: true,
Icon(AIcons.disc, color: d.color), highlightShape: BoxShape.rectangle,
const SizedBox(width: 8), borderRadius: const BorderRadius.all(Radius.circular(123)),
Flexible( hoverColor: primaryColor.withOpacity(0.04),
child: Text( splashColor: primaryColor.withOpacity(0.12),
d.displayText, child: Row(
overflow: TextOverflow.fade, mainAxisSize: MainAxisSize.min,
softWrap: false, children: [
maxLines: 1, Icon(AIcons.disc, color: d.color),
const SizedBox(width: 8),
Flexible(
child: Text(
d.displayText,
overflow: TextOverflow.fade,
softWrap: false,
maxLines: 1,
),
), ),
), const SizedBox(width: 8),
const SizedBox(width: 8), Text(
Text( numberFormat.format(d.entryCount),
numberFormat.format(d.entryCount), style: TextStyle(
style: TextStyle( color: Theme.of(context).textTheme.bodySmall!.color,
color: Theme.of(context).textTheme.bodySmall!.color, ),
), ),
), const SizedBox(width: 4),
], ],
),
), ),
)) ))
.toList(), .toList(),

View file

@ -281,7 +281,7 @@ class _StatsPageState extends State<StatsPage> {
style: Constants.knownTitleTextStyle, style: Constants.knownTitleTextStyle,
); );
if (settings.useTvLayout) { if (settings.useTvLayout) {
final colors = Theme.of(context).colorScheme; final primaryColor = Theme.of(context).colorScheme.primary;
header = Container( header = Container(
padding: const EdgeInsets.symmetric(vertical: 12), padding: const EdgeInsets.symmetric(vertical: 12),
alignment: AlignmentDirectional.centerStart, alignment: AlignmentDirectional.centerStart,
@ -289,12 +289,11 @@ class _StatsPageState extends State<StatsPage> {
type: MaterialType.transparency, type: MaterialType.transparency,
child: InkResponse( child: InkResponse(
onTap: onHeaderPressed, onTap: onHeaderPressed,
onHover: (_) {}, containedInkWell: true,
highlightShape: BoxShape.rectangle, highlightShape: BoxShape.rectangle,
borderRadius: const BorderRadius.all(Radius.circular(123)), borderRadius: const BorderRadius.all(Radius.circular(123)),
containedInkWell: true, hoverColor: primaryColor.withOpacity(0.04),
splashColor: colors.primary.withOpacity(0.12), splashColor: primaryColor.withOpacity(0.12),
hoverColor: colors.primary.withOpacity(0.04),
child: Padding( child: Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: header, child: header,

View file

@ -67,7 +67,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
} }
} else { } else {
final targetEntry = EntryActions.pageActions.contains(action) ? pageEntry : mainEntry; final targetEntry = EntryActions.pageActions.contains(action) ? pageEntry : mainEntry;
final canWrite = !device.isReadOnly; final canWrite = !settings.isReadOnly;
switch (action) { switch (action) {
case EntryAction.toggleFavourite: case EntryAction.toggleFavourite:
return collection != null; return collection != null;

View file

@ -3,12 +3,12 @@ import 'dart:convert';
import 'package:aves/model/actions/entry_actions.dart'; import 'package:aves/model/actions/entry_actions.dart';
import 'package:aves/model/actions/events.dart'; import 'package:aves/model/actions/events.dart';
import 'package:aves/model/device.dart';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/model/entry_info.dart'; import 'package:aves/model/entry_info.dart';
import 'package:aves/model/entry_metadata_edition.dart'; import 'package:aves/model/entry_metadata_edition.dart';
import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/geotiff.dart'; import 'package:aves/model/geotiff.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/ref/mime_types.dart'; import 'package:aves/ref/mime_types.dart';
import 'package:aves/services/common/services.dart'; import 'package:aves/services/common/services.dart';
@ -30,7 +30,7 @@ class EntryInfoActionDelegate with FeedbackMixin, PermissionAwareMixin, EntryEdi
Stream<ActionEvent<EntryAction>> get eventStream => _eventStreamController.stream; Stream<ActionEvent<EntryAction>> get eventStream => _eventStreamController.stream;
bool isVisible(AvesEntry targetEntry, EntryAction action) { bool isVisible(AvesEntry targetEntry, EntryAction action) {
final canWrite = !device.isReadOnly; final canWrite = !settings.isReadOnly;
switch (action) { switch (action) {
// general // general
case EntryAction.editDate: case EntryAction.editDate:

View file

@ -135,6 +135,7 @@ class _BottomOverlayContentState extends State<_BottomOverlayContent> {
void initState() { void initState() {
super.initState(); super.initState();
_registerWidget(widget); _registerWidget(widget);
WidgetsBinding.instance.addPostFrameCallback((_) => _requestFocus());
} }
@override @override
@ -162,11 +163,10 @@ class _BottomOverlayContentState extends State<_BottomOverlayContent> {
parent: animationController, parent: animationController,
curve: Curves.easeOutQuad, curve: Curves.easeOutQuad,
); );
animationController.addStatusListener(_onAnimationStatusChanged);
} }
void _unregisterWidget(_BottomOverlayContent widget) { void _unregisterWidget(_BottomOverlayContent widget) {
widget.animationController.removeStatusListener(_onAnimationStatusChanged); // nothing
} }
@override @override
@ -266,11 +266,7 @@ class _BottomOverlayContentState extends State<_BottomOverlayContent> {
); );
} }
void _onAnimationStatusChanged(AnimationStatus status) { void _requestFocus() => _buttonRowFocusScopeNode.children.firstOrNull?.requestFocus();
if (status == AnimationStatus.completed) {
_buttonRowFocusScopeNode.children.firstOrNull?.requestFocus();
}
}
} }
class ExtraBottomOverlay extends StatelessWidget { class ExtraBottomOverlay extends StatelessWidget {

View file

@ -36,6 +36,7 @@ class _SlideshowButtonsState extends State<SlideshowButtons> {
void initState() { void initState() {
super.initState(); super.initState();
_registerWidget(widget); _registerWidget(widget);
WidgetsBinding.instance.addPostFrameCallback((_) => _requestFocus());
} }
@override @override
@ -53,17 +54,15 @@ class _SlideshowButtonsState extends State<SlideshowButtons> {
} }
void _registerWidget(SlideshowButtons widget) { void _registerWidget(SlideshowButtons widget) {
final animationController = widget.animationController;
_buttonScale = CurvedAnimation( _buttonScale = CurvedAnimation(
parent: animationController, parent: widget.animationController,
// a little bounce at the top // a little bounce at the top
curve: Curves.easeOutBack, curve: Curves.easeOutBack,
); );
animationController.addStatusListener(_onAnimationStatusChanged);
} }
void _unregisterWidget(SlideshowButtons widget) { void _unregisterWidget(SlideshowButtons widget) {
widget.animationController.removeStatusListener(_onAnimationStatusChanged); // nothing
} }
@override @override
@ -111,9 +110,5 @@ class _SlideshowButtonsState extends State<SlideshowButtons> {
void _onAction(BuildContext context, SlideshowAction action) => SlideshowActionNotification(action).dispatch(context); void _onAction(BuildContext context, SlideshowAction action) => SlideshowActionNotification(action).dispatch(context);
void _onAnimationStatusChanged(AnimationStatus status) { void _requestFocus() => _buttonRowFocusScopeNode.children.firstOrNull?.requestFocus();
if (status == AnimationStatus.completed) {
_buttonRowFocusScopeNode.children.firstOrNull?.requestFocus();
}
}
} }

View file

@ -10,6 +10,8 @@ class AvesMapController {
Stream<MapControllerMoveEvent> get moveCommands => _events.where((event) => event is MapControllerMoveEvent).cast<MapControllerMoveEvent>(); Stream<MapControllerMoveEvent> get moveCommands => _events.where((event) => event is MapControllerMoveEvent).cast<MapControllerMoveEvent>();
Stream<MapControllerZoomEvent> get zoomCommands => _events.where((event) => event is MapControllerZoomEvent).cast<MapControllerZoomEvent>();
Stream<MapIdleUpdate> get idleUpdates => _events.where((event) => event is MapIdleUpdate).cast<MapIdleUpdate>(); Stream<MapIdleUpdate> get idleUpdates => _events.where((event) => event is MapIdleUpdate).cast<MapIdleUpdate>();
Stream<MapMarkerLocationChangeEvent> get markerLocationChanges => _events.where((event) => event is MapMarkerLocationChangeEvent).cast<MapMarkerLocationChangeEvent>(); Stream<MapMarkerLocationChangeEvent> get markerLocationChanges => _events.where((event) => event is MapMarkerLocationChangeEvent).cast<MapMarkerLocationChangeEvent>();
@ -20,6 +22,8 @@ class AvesMapController {
void moveTo(LatLng latLng) => _streamController.add(MapControllerMoveEvent(latLng)); void moveTo(LatLng latLng) => _streamController.add(MapControllerMoveEvent(latLng));
void zoomBy(double delta) => _streamController.add(MapControllerZoomEvent(delta));
void notifyIdle(ZoomedBounds bounds) => _streamController.add(MapIdleUpdate(bounds)); void notifyIdle(ZoomedBounds bounds) => _streamController.add(MapIdleUpdate(bounds));
void notifyMarkerLocationChange() => _streamController.add(MapMarkerLocationChangeEvent()); void notifyMarkerLocationChange() => _streamController.add(MapMarkerLocationChangeEvent());
@ -31,6 +35,12 @@ class MapControllerMoveEvent {
MapControllerMoveEvent(this.latLng); MapControllerMoveEvent(this.latLng);
} }
class MapControllerZoomEvent {
final double delta;
MapControllerZoomEvent(this.delta);
}
class MapIdleUpdate { class MapIdleUpdate {
final ZoomedBounds bounds; final ZoomedBounds bounds;

View file

@ -3,7 +3,7 @@ import 'package:aves_map/src/marker/key.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
typedef ButtonPanelBuilder = Widget Function(Future<void> Function(double amount) zoomBy, VoidCallback resetRotation); typedef ButtonPanelBuilder = Widget Function(VoidCallback resetRotation);
typedef MarkerClusterBuilder<T> = Map<MarkerKey<T>, GeoEntry<T>> Function(); typedef MarkerClusterBuilder<T> = Map<MarkerKey<T>, GeoEntry<T>> Function();
typedef MarkerWidgetBuilder<T> = Widget Function(MarkerKey<T> key); typedef MarkerWidgetBuilder<T> = Widget Function(MarkerKey<T> key);
typedef MarkerImageReadyChecker<T> = bool Function(MarkerKey<T> key); typedef MarkerImageReadyChecker<T> = bool Function(MarkerKey<T> key);

View file

@ -95,6 +95,7 @@ class _EntryGoogleMapState<T> extends State<EntryGoogleMap<T>> with WidgetsBindi
final avesMapController = widget.controller; final avesMapController = widget.controller;
if (avesMapController != null) { if (avesMapController != null) {
_subscriptions.add(avesMapController.moveCommands.listen((event) => _moveTo(_toServiceLatLng(event.latLng)))); _subscriptions.add(avesMapController.moveCommands.listen((event) => _moveTo(_toServiceLatLng(event.latLng))));
_subscriptions.add(avesMapController.zoomCommands.listen((event) => _zoomBy(event.delta)));
} }
widget.clusterListenable.addListener(_updateMarkers); widget.clusterListenable.addListener(_updateMarkers);
} }
@ -139,7 +140,7 @@ class _EntryGoogleMapState<T> extends State<EntryGoogleMap<T>> with WidgetsBindi
}, },
), ),
widget.decoratorBuilder(context, _buildMap()), widget.decoratorBuilder(context, _buildMap()),
widget.buttonPanelBuilder(_zoomBy, _resetRotation), widget.buttonPanelBuilder(_resetRotation),
], ],
); );
} }

View file

@ -89,6 +89,7 @@ class _EntryHmsMapState<T> extends State<EntryHmsMap<T>> {
final avesMapController = widget.controller; final avesMapController = widget.controller;
if (avesMapController != null) { if (avesMapController != null) {
_subscriptions.add(avesMapController.moveCommands.listen((event) => _moveTo(_toServiceLatLng(event.latLng)))); _subscriptions.add(avesMapController.moveCommands.listen((event) => _moveTo(_toServiceLatLng(event.latLng))));
_subscriptions.add(avesMapController.zoomCommands.listen((event) => _zoomBy(event.delta)));
} }
widget.clusterListenable.addListener(_updateMarkers); widget.clusterListenable.addListener(_updateMarkers);
} }
@ -118,7 +119,7 @@ class _EntryHmsMapState<T> extends State<EntryHmsMap<T>> {
}, },
), ),
widget.decoratorBuilder(context, _buildMap()), widget.decoratorBuilder(context, _buildMap()),
widget.buttonPanelBuilder(_zoomBy, _resetRotation), widget.buttonPanelBuilder(_resetRotation),
], ],
); );
} }