aves/lib/widgets/viewer/overlay/details/details.dart
2024-01-27 00:37:08 +01:00

329 lines
11 KiB
Dart

import 'dart:math';
import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/metadata/overlay.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/styles.dart';
import 'package:aves/widgets/viewer/multipage/controller.dart';
import 'package:aves/widgets/viewer/overlay/details/date.dart';
import 'package:aves/widgets/viewer/overlay/details/description.dart';
import 'package:aves/widgets/viewer/overlay/details/expander.dart';
import 'package:aves/widgets/viewer/overlay/details/location.dart';
import 'package:aves/widgets/viewer/overlay/details/position_title.dart';
import 'package:aves/widgets/viewer/overlay/details/rating_tags.dart';
import 'package:aves/widgets/viewer/overlay/details/shooting.dart';
import 'package:aves/widgets/viewer/page_entry_builder.dart';
import 'package:aves_model/aves_model.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class ViewerDetailOverlay extends StatefulWidget {
final List<AvesEntry> entries;
final int index;
final bool hasCollection;
final MultiPageController? multiPageController;
final ValueNotifier<bool> expandedNotifier;
final Size availableSize;
const ViewerDetailOverlay({
super.key,
required this.entries,
required this.index,
required this.hasCollection,
required this.multiPageController,
required this.expandedNotifier,
required this.availableSize,
});
@override
State<ViewerDetailOverlay> createState() => _ViewerDetailOverlayState();
}
class _ViewerDetailOverlayState extends State<ViewerDetailOverlay> {
List<AvesEntry> get entries => widget.entries;
AvesEntry? get entry => entryForIndex(widget.index);
AvesEntry? entryForIndex(int index) => index < entries.length ? entries[index] : null;
late Future<OverlayMetadata> _detailLoader;
AvesEntry? _lastEntry;
OverlayMetadata _lastDetails = const OverlayMetadata();
@override
void initState() {
super.initState();
_initDetailLoader();
}
@override
void didUpdateWidget(covariant ViewerDetailOverlay oldWidget) {
super.didUpdateWidget(oldWidget);
final newEntry = entryForIndex(widget.index);
if (newEntry != entryForIndex(oldWidget.index) && newEntry != _lastEntry) {
_initDetailLoader();
}
}
void _initDetailLoader() {
final requestEntry = entry;
if (requestEntry == null) {
_detailLoader = SynchronousFuture(const OverlayMetadata());
} else {
_detailLoader = metadataFetchService.getFields(requestEntry, {
if (settings.showOverlayShootingDetails) ...{
MetadataSyntheticField.aperture,
MetadataSyntheticField.exposureTime,
MetadataSyntheticField.focalLength,
MetadataSyntheticField.iso,
},
if (settings.showOverlayDescription) MetadataSyntheticField.description,
});
}
}
@override
Widget build(BuildContext context) {
return SafeArea(
top: false,
bottom: false,
child: FutureBuilder<OverlayMetadata>(
future: _detailLoader,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done && !snapshot.hasError) {
_lastDetails = snapshot.data!;
_lastEntry = entry;
}
if (_lastEntry == null) return const SizedBox();
final mainEntry = _lastEntry!;
final multiPageController = widget.multiPageController;
Widget _buildContent({AvesEntry? pageEntry}) => ViewerDetailOverlayContent(
pageEntry: pageEntry ?? mainEntry,
details: _lastDetails,
position: widget.hasCollection ? '${widget.index + 1}/${entries.length}' : null,
availableWidth: widget.availableSize.width,
multiPageController: multiPageController,
expandedNotifier: widget.expandedNotifier,
);
return multiPageController != null
? PageEntryBuilder(
multiPageController: multiPageController,
builder: (pageEntry) => _buildContent(pageEntry: pageEntry),
)
: _buildContent();
},
),
);
}
}
class ViewerDetailOverlayContent extends StatelessWidget {
final AvesEntry pageEntry;
final OverlayMetadata details;
final String? position;
final double availableWidth;
final MultiPageController? multiPageController;
final ValueNotifier<bool> expandedNotifier;
static const double _interRowPadding = 2.0;
static const double _subRowMinWidth = 300.0;
static const padding = EdgeInsets.symmetric(vertical: 4, horizontal: 8);
static const double iconPadding = 8.0;
static const double iconSize = 16.0;
static List<Shadow>? shadows(BuildContext context) => Theme.of(context).brightness == Brightness.dark ? AStyles.embossShadows : null;
const ViewerDetailOverlayContent({
super.key,
required this.pageEntry,
required this.details,
required this.position,
required this.availableWidth,
required this.multiPageController,
required this.expandedNotifier,
});
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: pageEntry.metadataChangeNotifier,
builder: (context, child) => DefaultTextStyle(
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
shadows: shadows(context),
),
softWrap: false,
overflow: TextOverflow.fade,
maxLines: 1,
child: Padding(
padding: padding,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: _buildRows(context),
),
),
),
);
}
List<Widget> _buildRows(BuildContext context) {
final infoMaxWidth = availableWidth - padding.horizontal;
final showRatingTags = settings.showOverlayRatingTags;
final showShootingDetails = settings.showOverlayShootingDetails;
final showDescription = settings.showOverlayDescription;
final isLandscape = MediaQuery.orientationOf(context) == Orientation.landscape;
final twoColumns = isLandscape && infoMaxWidth / 2 > _subRowMinWidth;
final subRowWidth = twoColumns ? min(_subRowMinWidth, infoMaxWidth / 2) : infoMaxWidth;
final collapsedShooting = twoColumns && showShootingDetails;
final collapsedLocation = twoColumns && !showShootingDetails;
final positionTitle = OverlayPositionTitleRow(
entry: pageEntry,
collectionPosition: position,
multiPageController: multiPageController,
);
final rows = <Widget>[];
if (positionTitle.isNotEmpty) {
rows.add(OverlayRowExpander(
expandedNotifier: expandedNotifier,
child: positionTitle,
));
rows.add(const SizedBox(height: _interRowPadding));
}
if (twoColumns) {
rows.add(
Row(
children: [
_buildDateSubRow(subRowWidth),
if (collapsedShooting) _buildShootingSubRow(context, subRowWidth),
if (collapsedLocation) _buildLocationSubRow(context, subRowWidth),
],
),
);
} else {
rows.add(_buildDateSubRow(subRowWidth));
if (showShootingDetails) {
rows.add(_buildShootingFullRow(context, subRowWidth));
}
}
if (!collapsedLocation) {
rows.add(_buildLocationFullRow(context));
}
if (showRatingTags) {
rows.add(_buildRatingTagsFullRow(context));
}
if (showDescription) {
rows.add(_buildDescriptionFullRow(context));
}
return rows;
}
Widget _buildDateSubRow(double subRowWidth) => SizedBox(
width: subRowWidth,
child: OverlayDateRow(
entry: pageEntry,
multiPageController: multiPageController,
),
);
Widget _buildRatingTagsFullRow(BuildContext context) => _buildFullRowSwitcher(
context: context,
visible: pageEntry.rating != 0 || pageEntry.tags.isNotEmpty,
builder: (context) => OverlayRowExpander(
expandedNotifier: expandedNotifier,
child: OverlayRatingTagsRow(entry: pageEntry),
),
);
Widget _buildDescriptionFullRow(BuildContext context) => _buildFullRowSwitcher(
context: context,
visible: details.description != null,
builder: (context) => OverlayRowExpander(
expandedNotifier: expandedNotifier,
child: OverlayDescriptionRow(description: details.description!),
),
);
Widget _buildShootingFullRow(BuildContext context, double subRowWidth) => _buildFullRowSwitcher(
context: context,
visible: details.hasShootingDetails,
builder: (context) => SizedBox(
width: subRowWidth,
child: OverlayShootingRow(details: details),
),
);
Widget _buildShootingSubRow(BuildContext context, double subRowWidth) => _buildSubRowSwitcher(
context: context,
subRowWidth: subRowWidth,
visible: details.hasShootingDetails,
builder: (context) => OverlayShootingRow(details: details),
);
Widget _buildLocationFullRow(BuildContext context) => _buildFullRowSwitcher(
context: context,
visible: pageEntry.hasGps,
builder: (context) => OverlayLocationRow(entry: pageEntry),
);
Widget _buildLocationSubRow(BuildContext context, double subRowWidth) => _buildSubRowSwitcher(
context: context,
subRowWidth: subRowWidth,
visible: pageEntry.hasGps,
builder: (context) => OverlayLocationRow(entry: pageEntry),
);
Widget _buildSubRowSwitcher({
required BuildContext context,
required double subRowWidth,
required bool visible,
required WidgetBuilder builder,
}) =>
AnimatedSwitcher(
duration: context.select<DurationsData, Duration>((v) => v.viewerOverlayChangeAnimation),
switchInCurve: Curves.easeInOutCubic,
switchOutCurve: Curves.easeInOutCubic,
transitionBuilder: (child, animation) => FadeTransition(
opacity: animation,
child: child,
),
child: visible
? SizedBox(
width: subRowWidth,
child: builder(context),
)
: const SizedBox(),
);
Widget _buildFullRowSwitcher({
required BuildContext context,
required bool visible,
required WidgetBuilder builder,
}) =>
AnimatedSwitcher(
duration: context.select<DurationsData, Duration>((v) => v.viewerOverlayChangeAnimation),
switchInCurve: Curves.easeInOutCubic,
switchOutCurve: Curves.easeInOutCubic,
transitionBuilder: (child, animation) => FadeTransition(
opacity: animation,
child: SizeTransition(
axisAlignment: 1,
sizeFactor: animation,
child: child,
),
),
child: visible
? Padding(
padding: const EdgeInsets.only(top: _interRowPadding),
child: builder(context),
)
: const SizedBox(),
);
}