nc-photos/app/lib/widget/map_browser/view.dart
2024-09-04 00:34:08 +08:00

446 lines
13 KiB
Dart

part of '../map_browser.dart';
class _MapView extends StatefulWidget {
const _MapView();
@override
State<StatefulWidget> createState() => _MapViewState();
}
class _MapViewState extends State<_MapView> {
@override
Widget build(BuildContext context) {
return MultiBlocListener(
listeners: [
_BlocListenerT<MapCoord?>(
selector: (state) => state.initialPoint,
listener: (context, initialPoint) {
if (initialPoint != null) {
_controller?.setPosition(initialPoint);
}
},
),
],
child: _BlocBuilder(
buildWhen: (previous, current) => previous.data != current.data,
builder: (context, state) {
final prevPosition =
context.read<PrefController>().mapBrowserPrevPositionValue;
return ValueStreamBuilder<GpsMapProvider>(
stream: context.bloc.prefController.gpsMapProvider,
builder: (context, gpsMapProvider) => InteractiveMap(
providerHint: gpsMapProvider.requireData,
initialPosition: prevPosition ?? const MapCoord(0, 0),
initialZoom: prevPosition == null ? 2.5 : 10,
dataPoints: state.data,
onClusterTap: (dataPoints) {
final c = Collection(
name: "",
contentProvider: CollectionAdHocProvider(
account: context.bloc.account,
fileIds: dataPoints
.cast<_DataPoint>()
.map((e) => e.fileId)
.toList(),
),
);
Navigator.of(context).pushNamed(
CollectionBrowser.routeName,
arguments: CollectionBrowserArguments(c),
);
},
googleClusterBuilder: (context, dataPoints) =>
_GoogleMarkerBuilder(context, account: context.bloc.account)
.build(dataPoints.cast()),
osmClusterBuilder: (context, dataPoints) =>
_OsmMarkerBuilder(context, account: context.bloc.account)
.build(dataPoints.cast()),
contentPadding: EdgeInsets.only(
top: MediaQuery.of(context).padding.top,
bottom: MediaQuery.of(context).padding.bottom,
),
onMapCreated: (controller) {
_controller = controller;
if (state.initialPoint != null) {
controller.setPosition(state.initialPoint!);
}
},
),
);
},
),
);
}
InteractiveMapController? _controller;
}
class _OsmMarker extends StatelessWidget {
const _OsmMarker({
required this.account,
required this.fileId,
required this.size,
required this.color,
required this.text,
required this.textSize,
required this.textColor,
});
@override
Widget build(BuildContext context) {
return Center(
child: Container(
width: size,
height: size,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(size / 2),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(.3),
blurRadius: 2,
offset: const Offset(1, 1),
),
],
border: Border.all(
color: Colors.white.withOpacity(.75),
width: 2,
),
color: color,
),
child: ClipRRect(
borderRadius: BorderRadius.circular(size / 2),
child: Stack(
children: [
NetworkRectThumbnail(
account: account,
imageUrl:
NetworkRectThumbnail.imageUrlForFileId(account, fileId),
errorBuilder: (_) => const SizedBox.shrink(),
),
Align(
alignment: Alignment.bottomCenter,
child: Container(
width: double.infinity,
padding: const EdgeInsets.fromLTRB(4, 1, 4, 1),
color: color,
child: Text(
text,
textAlign: TextAlign.center,
style: TextStyle(fontSize: textSize, color: textColor),
),
),
),
],
),
),
),
);
}
final Account account;
final int fileId;
final double size;
final Color color;
final String text;
final double textSize;
final Color textColor;
}
class _PanelContainer extends StatefulWidget {
const _PanelContainer({
required this.isShow,
required this.child,
});
@override
State<StatefulWidget> createState() => _PanelContainerState();
final bool isShow;
final Widget child;
}
class _PanelContainerState extends State<_PanelContainer>
with TickerProviderStateMixin {
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: k.animationDurationNormal,
vsync: this,
value: 0,
);
_animation = CurvedAnimation(
parent: _animationController,
curve: Curves.easeInOut,
);
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
void didUpdateWidget(covariant _PanelContainer oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.isShow != widget.isShow) {
if (widget.isShow) {
_animationController.animateTo(1);
} else {
_animationController.animateBack(0);
}
}
}
@override
Widget build(BuildContext context) {
return MatrixTransition(
animation: _animation,
onTransform: (animationValue) => Matrix4.identity()
..translate(0.0, -(_size.height / 2) * (1 - animationValue), 0.0)
..scale(1.0, animationValue, 1.0),
child: MeasureSize(
onChange: (size) => setState(() {
_size = size;
}),
child: widget.child,
),
);
}
late AnimationController _animationController;
late Animation<double> _animation;
var _size = Size.zero;
}
class _DateRangeToggle extends StatelessWidget {
const _DateRangeToggle();
@override
Widget build(BuildContext context) {
return FloatingActionButton.small(
onPressed: () {
context.addEvent(const _OpenDataRangeControlPanel());
},
child: const Icon(Icons.date_range_outlined),
);
}
}
class _DateRangeControlPanel extends StatelessWidget {
const _DateRangeControlPanel();
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Container(
decoration: BoxDecoration(
color: theme.elevate(theme.colorScheme.surface, 2),
borderRadius: BorderRadius.circular(16),
boxShadow: const [
BoxShadow(
blurRadius: 4,
offset: Offset(0, 2),
color: Colors.black26,
),
],
),
child: Material(
type: MaterialType.transparency,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Expanded(
child: Text(
L10n.global().mapBrowserDateRangeLabel,
style: Theme.of(context).listTileTheme.titleTextStyle,
),
),
Expanded(
child: _BlocSelector<_DateRangeType>(
selector: (state) => state.dateRangeType,
builder: (context, dateRangeType) =>
DropdownButtonFormField<_DateRangeType>(
items: _DateRangeType.values
.map((e) => DropdownMenuItem(
value: e,
child: Text(e.toDisplayString()),
))
.toList(),
value: dateRangeType,
onChanged: (value) {
if (value != null) {
context.addEvent(_SetDateRangeType(value));
}
},
),
),
),
],
),
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Expanded(
child: _BlocSelector<DateRange>(
selector: (state) => state.localDateRange,
builder: (context, localDateRange) => _DateField(
localDateRange.from!,
onChanged: (value) {
context.addEvent(_SetLocalDateRange(
localDateRange.copyWith(from: value.toDate())));
},
),
),
),
const Text(" - "),
Expanded(
child: _BlocSelector<DateRange>(
selector: (state) => state.localDateRange,
builder: (context, localDateRange) => _DateField(
localDateRange.to!,
onChanged: (value) {
context.addEvent(_SetLocalDateRange(
localDateRange.copyWith(to: value.toDate())));
},
),
),
),
],
),
const SizedBox(height: 8),
const Align(
alignment: Alignment.centerRight,
child: _SetAsDefaultSwitch(),
),
const SizedBox(height: 8),
],
),
),
),
);
}
}
class _DateField extends StatefulWidget {
const _DateField(
this.date, {
this.onChanged,
});
@override
State<StatefulWidget> createState() => _DateFieldState();
final Date date;
final ValueChanged<DateTime>? onChanged;
}
class _DateFieldState extends State<_DateField> {
@override
void dispose() {
super.dispose();
_controller.dispose();
}
@override
void didUpdateWidget(covariant _DateField oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.date != oldWidget.date) {
_controller.text = _stringify(widget.date);
}
}
@override
Widget build(BuildContext context) {
return Material(
type: MaterialType.transparency,
child: InkWell(
onTap: () async {
final result = await showDatePicker(
context: context,
firstDate: DateTime.fromMillisecondsSinceEpoch(0),
lastDate: clock.now(),
currentDate: widget.date.toLocalDateTime(),
);
if (result == null) {
return;
}
widget.onChanged?.call(result);
},
child: IgnorePointer(
child: ExcludeFocus(
child: TextFormField(
controller: _controller,
),
),
),
),
);
}
String _stringify(Date date) {
if (date == clock.now().toDate()) {
return L10n.global().todayText;
} else {
return intl.DateFormat(intl.DateFormat.YEAR_ABBR_MONTH_DAY,
Localizations.localeOf(context).languageCode)
.format(date.toLocalDateTime());
}
}
late final _controller = TextEditingController(text: _stringify(widget.date));
}
class _SetAsDefaultSwitch extends StatelessWidget {
const _SetAsDefaultSwitch();
@override
Widget build(BuildContext context) {
return _BlocBuilder(
buildWhen: (previous, current) =>
previous.dateRangeType != current.dateRangeType ||
previous.prefDateRangeType != current.prefDateRangeType ||
previous.localDateRange != current.localDateRange,
builder: (context, state) {
final isChecked = state.dateRangeType == state.prefDateRangeType;
final isEnabled = state.dateRangeType != _DateRangeType.custom ||
state.localDateRange.to == clock.now().toDate();
return InkWell(
customBorder:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(64)),
onTap: isEnabled && !isChecked
? () {
if (!isChecked) {
context.addEvent(const _SetAsDefaultRange());
}
}
: null,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(width: 16),
Text(
L10n.global().mapBrowserSetDefaultDateRangeButton,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: isEnabled ? null : Theme.of(context).disabledColor,
),
),
IgnorePointer(
child: Checkbox(
value: isChecked,
onChanged: isEnabled ? (_) {} : null,
),
),
],
),
);
},
);
}
}