aves/lib/widgets/viewer/info/info_page.dart
2024-04-28 16:43:16 +02:00

288 lines
9.3 KiB
Dart

import 'dart:async';
import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/multipage.dart';
import 'package:aves/model/events.dart';
import 'package:aves/model/filters/filters.dart';
import 'package:aves/model/selection.dart';
import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/theme/durations.dart';
import 'package:aves/widgets/common/basic/insets.dart';
import 'package:aves/widgets/common/basic/scaffold.dart';
import 'package:aves/widgets/common/basic/tv_edge_focus.dart';
import 'package:aves/widgets/filter_grids/common/action_delegates/chip.dart';
import 'package:aves/widgets/viewer/action/entry_info_action_delegate.dart';
import 'package:aves/widgets/viewer/controls/notifications.dart';
import 'package:aves/widgets/viewer/info/basic_section.dart';
import 'package:aves/widgets/viewer/info/embedded/embedded_data_opener.dart';
import 'package:aves/widgets/viewer/info/info_app_bar.dart';
import 'package:aves/widgets/viewer/info/location_section.dart';
import 'package:aves/widgets/viewer/info/color_section.dart';
import 'package:aves/widgets/viewer/info/metadata/metadata_dir.dart';
import 'package:aves/widgets/viewer/info/metadata/metadata_section.dart';
import 'package:aves/widgets/viewer/multipage/conductor.dart';
import 'package:aves/widgets/viewer/page_entry_builder.dart';
import 'package:aves_model/aves_model.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class InfoPage extends StatefulWidget {
final CollectionLens? collection;
final ValueNotifier<AvesEntry?> entryNotifier;
final ValueNotifier<bool> isScrollingNotifier;
const InfoPage({
super.key,
required this.collection,
required this.entryNotifier,
required this.isScrollingNotifier,
});
@override
State<StatefulWidget> createState() => _InfoPageState();
}
class _InfoPageState extends State<InfoPage> {
final ScrollController _scrollController = ScrollController();
bool _scrollStartFromTop = false;
static const splitScreenWidthThreshold = 600;
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AvesScaffold(
body: GestureAreaProtectorStack(
child: SafeArea(
bottom: false,
child: NotificationListener<ScrollNotification>(
onNotification: _handleTopScroll,
child: ValueListenableBuilder<AvesEntry?>(
valueListenable: widget.entryNotifier,
builder: (context, mainEntry, child) {
if (mainEntry == null) return const SizedBox();
final isSelecting = context.select<Selection<AvesEntry>?, bool>((v) => v?.isSelecting ?? false);
Widget _buildContent({AvesEntry? pageEntry}) {
final targetEntry = pageEntry ?? mainEntry;
return EmbeddedDataOpener(
enabled: !isSelecting,
entry: targetEntry,
child: _InfoPageContent(
collection: widget.collection,
entry: targetEntry,
isScrollingNotifier: widget.isScrollingNotifier,
scrollController: _scrollController,
split: MediaQuery.sizeOf(context).width > splitScreenWidthThreshold,
goToViewer: _goToViewer,
),
);
}
return mainEntry.isBurst
? PageEntryBuilder(
multiPageController: context.read<MultiPageConductor>().getController(mainEntry),
builder: (pageEntry) => _buildContent(pageEntry: pageEntry),
)
: _buildContent();
},
),
),
),
),
resizeToAvoidBottomInset: false,
);
}
bool _handleTopScroll(ScrollNotification notification) {
if (notification is ScrollStartNotification) {
final metrics = notification.metrics;
_scrollStartFromTop = metrics.pixels == metrics.minScrollExtent;
}
if (_scrollStartFromTop) {
if (notification is ScrollUpdateNotification) {
_scrollStartFromTop = notification.scrollDelta! < 0;
} else if (notification is ScrollEndNotification) {
_scrollStartFromTop = false;
} else if (notification is OverscrollNotification) {
if (notification.overscroll < 0) {
_goToViewer();
_scrollStartFromTop = false;
}
}
}
return false;
}
void _goToViewer() {
ShowImageNotification().dispatch(context);
_scrollController.animateTo(
0,
duration: ADurations.pageTransitionAnimation,
curve: Curves.easeInOut,
);
}
}
class _InfoPageContent extends StatefulWidget {
final CollectionLens? collection;
final AvesEntry entry;
final ValueNotifier<bool> isScrollingNotifier;
final ScrollController scrollController;
final bool split;
final VoidCallback goToViewer;
const _InfoPageContent({
required this.collection,
required this.entry,
required this.isScrollingNotifier,
required this.scrollController,
required this.split,
required this.goToViewer,
});
@override
State<_InfoPageContent> createState() => _InfoPageContentState();
}
class _InfoPageContentState extends State<_InfoPageContent> {
final List<StreamSubscription> _subscriptions = [];
late EntryInfoActionDelegate _actionDelegate;
final ValueNotifier<Map<String, MetadataDirectory>> _metadataNotifier = ValueNotifier({});
final ValueNotifier<EntryAction?> _isEditingMetadataNotifier = ValueNotifier(null);
static const horizontalPadding = EdgeInsets.symmetric(horizontal: 8);
CollectionLens? get collection => widget.collection;
AvesEntry get entry => widget.entry;
@override
void initState() {
super.initState();
_registerWidget(widget);
}
@override
void didUpdateWidget(covariant _InfoPageContent oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.entry != widget.entry) {
_unregisterWidget(oldWidget);
_registerWidget(widget);
}
}
@override
void dispose() {
_metadataNotifier.dispose();
_isEditingMetadataNotifier.dispose();
_unregisterWidget(widget);
super.dispose();
}
void _registerWidget(_InfoPageContent widget) {
_actionDelegate = EntryInfoActionDelegate();
_subscriptions.add(_actionDelegate.eventStream.listen(_onActionDelegateEvent));
}
void _unregisterWidget(_InfoPageContent widget) {
_subscriptions
..forEach((sub) => sub.cancel())
..clear();
}
@override
Widget build(BuildContext context) {
final basicSection = BasicSection(
entry: entry,
collection: collection,
actionDelegate: _actionDelegate,
isScrollingNotifier: widget.isScrollingNotifier,
isEditingMetadataNotifier: _isEditingMetadataNotifier,
onFilter: _onFilter,
);
final locationAtTop = widget.split && entry.hasGps;
final locationSection = LocationSection(
collection: collection,
entry: entry,
showTitle: !locationAtTop,
isScrollingNotifier: widget.isScrollingNotifier,
onFilter: _onFilter,
);
final basicAndLocationSliver = locationAtTop
? SliverToBoxAdapter(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(child: basicSection),
const SizedBox(width: 8),
Expanded(child: locationSection),
],
),
)
: SliverList(
delegate: SliverChildListDelegate.fixed(
[
basicSection,
locationSection,
],
),
);
return NotificationListener<FilterNotification>(
onNotification: (notification) {
_onFilter(notification.filter);
return true;
},
child: CustomScrollView(
controller: widget.scrollController,
slivers: [
const SliverToBoxAdapter(
child: TvEdgeFocus(),
),
InfoAppBar(
entry: entry,
collection: collection,
actionDelegate: _actionDelegate,
metadataNotifier: _metadataNotifier,
onBackPressed: widget.goToViewer,
),
SliverPadding(
padding: horizontalPadding + const EdgeInsets.only(top: 8),
sliver: basicAndLocationSliver,
),
SliverPadding(
padding: horizontalPadding + const EdgeInsets.only(bottom: 8),
sliver: MetadataSectionSliver(
entry: entry,
metadataNotifier: _metadataNotifier,
),
),
SliverPadding(
padding: horizontalPadding + const EdgeInsets.only(bottom: 8),
sliver: ColorSectionSliver(entry: entry),
),
const BottomPaddingSliver(),
],
),
);
}
void _onActionDelegateEvent(ActionEvent<EntryAction> event) {
Future.delayed(ADurations.dialogTransitionAnimation).then((_) {
if (event is ActionStartedEvent) {
_isEditingMetadataNotifier.value = event.action;
} else if (event is ActionEndedEvent) {
_isEditingMetadataNotifier.value = null;
}
});
}
void _onFilter(CollectionFilter filter) => FilterSelectedNotification(filter).dispatch(context);
}