minor page tweaks
This commit is contained in:
@@ -133,6 +133,7 @@ class _RouteCalculatorState extends State<RouteCalculator> {
|
|||||||
bool _loadingStations = false;
|
bool _loadingStations = false;
|
||||||
|
|
||||||
RouteResult? _routeResult;
|
RouteResult? _routeResult;
|
||||||
|
List<String>? _calculatedStations;
|
||||||
RouteResult? get result => _routeResult;
|
RouteResult? get result => _routeResult;
|
||||||
String? _errorMessage;
|
String? _errorMessage;
|
||||||
|
|
||||||
@@ -178,6 +179,7 @@ class _RouteCalculatorState extends State<RouteCalculator> {
|
|||||||
if (cleaned.length < 2) {
|
if (cleaned.length < 2) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_routeResult = null;
|
_routeResult = null;
|
||||||
|
_calculatedStations = null;
|
||||||
_errorMessage = 'Add at least two stations before calculating.';
|
_errorMessage = 'Add at least two stations before calculating.';
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -185,6 +187,7 @@ class _RouteCalculatorState extends State<RouteCalculator> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
_errorMessage = null;
|
_errorMessage = null;
|
||||||
_routeResult = null;
|
_routeResult = null;
|
||||||
|
_calculatedStations = null;
|
||||||
});
|
});
|
||||||
final api = context.read<ApiService>(); // context is valid here
|
final api = context.read<ApiService>(); // context is valid here
|
||||||
try {
|
try {
|
||||||
@@ -195,6 +198,7 @@ class _RouteCalculatorState extends State<RouteCalculator> {
|
|||||||
if (res is Map && res['error'] == false) {
|
if (res is Map && res['error'] == false) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_routeResult = RouteResult.fromJson(Map<String, dynamic>.from(res));
|
_routeResult = RouteResult.fromJson(Map<String, dynamic>.from(res));
|
||||||
|
_calculatedStations = List.from(cleaned);
|
||||||
});
|
});
|
||||||
final distance = (_routeResult?.distance ?? 0);
|
final distance = (_routeResult?.distance ?? 0);
|
||||||
widget.onDistanceComputed?.call(distance);
|
widget.onDistanceComputed?.call(distance);
|
||||||
@@ -205,17 +209,30 @@ class _RouteCalculatorState extends State<RouteCalculator> {
|
|||||||
).msg;
|
).msg;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
setState(() => _errorMessage = 'Failed to calculate route.');
|
setState(() {
|
||||||
|
_errorMessage = 'Failed to calculate route.';
|
||||||
|
_calculatedStations = null;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setState(() => _errorMessage = 'Failed to calculate route: $e');
|
setState(() {
|
||||||
|
_errorMessage = 'Failed to calculate route: $e';
|
||||||
|
_calculatedStations = null;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _markRouteDirty() {
|
||||||
|
_routeResult = null;
|
||||||
|
_calculatedStations = null;
|
||||||
|
_errorMessage = null;
|
||||||
|
}
|
||||||
|
|
||||||
void _addStation() {
|
void _addStation() {
|
||||||
final data = context.read<DataService>();
|
final data = context.read<DataService>();
|
||||||
setState(() {
|
setState(() {
|
||||||
data.stations.add('');
|
data.stations.add('');
|
||||||
|
_markRouteDirty();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,6 +240,7 @@ class _RouteCalculatorState extends State<RouteCalculator> {
|
|||||||
final data = context.read<DataService>();
|
final data = context.read<DataService>();
|
||||||
setState(() {
|
setState(() {
|
||||||
data.stations.removeAt(index);
|
data.stations.removeAt(index);
|
||||||
|
_markRouteDirty();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,6 +248,7 @@ class _RouteCalculatorState extends State<RouteCalculator> {
|
|||||||
final data = context.read<DataService>();
|
final data = context.read<DataService>();
|
||||||
setState(() {
|
setState(() {
|
||||||
data.stations[index] = value;
|
data.stations[index] = value;
|
||||||
|
_markRouteDirty();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,14 +256,91 @@ class _RouteCalculatorState extends State<RouteCalculator> {
|
|||||||
final data = context.read<DataService>();
|
final data = context.read<DataService>();
|
||||||
setState(() {
|
setState(() {
|
||||||
data.stations = [''];
|
data.stations = [''];
|
||||||
_routeResult = null;
|
_markRouteDirty();
|
||||||
_errorMessage = null;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _isResultCurrent(List<String> stations) {
|
||||||
|
if (_routeResult == null || _calculatedStations == null) return false;
|
||||||
|
final cleaned = stations.where((s) => s.trim().isNotEmpty).toList();
|
||||||
|
if (cleaned.length != _calculatedStations!.length) return false;
|
||||||
|
for (var i = 0; i < cleaned.length; i++) {
|
||||||
|
if (cleaned[i] != _calculatedStations![i]) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final data = context.watch<DataService>();
|
final data = context.watch<DataService>();
|
||||||
|
final isCompact = MediaQuery.of(context).size.width < 600;
|
||||||
|
final showApply =
|
||||||
|
widget.onApplyRoute != null && _isResultCurrent(data.stations);
|
||||||
|
final primaryPadding = EdgeInsets.symmetric(
|
||||||
|
horizontal: isCompact ? 14 : 20,
|
||||||
|
vertical: isCompact ? 10 : 14,
|
||||||
|
);
|
||||||
|
final secondaryPadding = EdgeInsets.symmetric(
|
||||||
|
horizontal: isCompact ? 10 : 16,
|
||||||
|
vertical: isCompact ? 8 : 12,
|
||||||
|
);
|
||||||
|
final primaryStyle = FilledButton.styleFrom(
|
||||||
|
padding: primaryPadding,
|
||||||
|
minimumSize: Size(0, isCompact ? 38 : 46),
|
||||||
|
);
|
||||||
|
final secondaryStyle = OutlinedButton.styleFrom(
|
||||||
|
padding: secondaryPadding,
|
||||||
|
minimumSize: Size(0, isCompact ? 34 : 42),
|
||||||
|
);
|
||||||
|
|
||||||
|
Widget buildSecondaryButton({
|
||||||
|
required IconData icon,
|
||||||
|
required String label,
|
||||||
|
required VoidCallback onPressed,
|
||||||
|
}) {
|
||||||
|
if (isCompact) {
|
||||||
|
return Tooltip(
|
||||||
|
message: label,
|
||||||
|
child: OutlinedButton(
|
||||||
|
onPressed: onPressed,
|
||||||
|
style: secondaryStyle,
|
||||||
|
child: Icon(icon, size: 20),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return OutlinedButton.icon(
|
||||||
|
onPressed: onPressed,
|
||||||
|
icon: Icon(icon, size: 20),
|
||||||
|
label: Text(label),
|
||||||
|
style: secondaryStyle,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildPrimaryAction({required bool fullWidth}) {
|
||||||
|
final button = showApply
|
||||||
|
? FilledButton.icon(
|
||||||
|
onPressed: () => widget.onApplyRoute!(_routeResult!),
|
||||||
|
icon: const Icon(Icons.check),
|
||||||
|
label: const Text('Apply to entry'),
|
||||||
|
style: primaryStyle,
|
||||||
|
)
|
||||||
|
: FilledButton.icon(
|
||||||
|
onPressed: () async {
|
||||||
|
await _calculateRoute(data.stations);
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.route),
|
||||||
|
label: const Text('Calculate Route'),
|
||||||
|
style: primaryStyle,
|
||||||
|
);
|
||||||
|
final key =
|
||||||
|
ValueKey<String>(showApply ? 'apply-primary-action' : 'calc-primary-action');
|
||||||
|
if (!fullWidth) return KeyedSubtree(key: key, child: button);
|
||||||
|
return KeyedSubtree(
|
||||||
|
key: key,
|
||||||
|
child: SizedBox(width: double.infinity, child: button),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
Align(
|
Align(
|
||||||
@@ -301,6 +397,7 @@ class _RouteCalculatorState extends State<RouteCalculator> {
|
|||||||
setState(() {
|
setState(() {
|
||||||
final moved = data.stations.removeAt(oldIndex);
|
final moved = data.stations.removeAt(oldIndex);
|
||||||
data.stations.insert(newIndex, moved);
|
data.stations.insert(newIndex, moved);
|
||||||
|
_markRouteDirty();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
children: List.generate(data.stations.length, (index) {
|
children: List.generate(data.stations.length, (index) {
|
||||||
@@ -364,56 +461,94 @@ class _RouteCalculatorState extends State<RouteCalculator> {
|
|||||||
context.push('/calculator/details', extra: result);
|
context.push('/calculator/details', extra: result);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (widget.onApplyRoute != null)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
|
||||||
child: ElevatedButton.icon(
|
|
||||||
onPressed: () => widget.onApplyRoute!(_routeResult!),
|
|
||||||
icon: const Icon(Icons.check),
|
|
||||||
label: const Text('Apply to entry'),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
else
|
else
|
||||||
SizedBox.shrink(),
|
SizedBox.shrink(),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
child: Wrap(
|
child: isCompact
|
||||||
alignment: WrapAlignment.center,
|
? Column(
|
||||||
spacing: 12,
|
children: [
|
||||||
runSpacing: 8,
|
Row(
|
||||||
children: [
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
...(() {
|
children: [
|
||||||
final reverseButton = ElevatedButton.icon(
|
buildSecondaryButton(
|
||||||
icon: const Icon(Icons.swap_horiz),
|
icon: Icons.swap_horiz,
|
||||||
label: const Text('Reverse route'),
|
label: 'Reverse route',
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
setState(() {
|
setState(() {
|
||||||
data.stations = data.stations.reversed.toList();
|
data.stations = data.stations.reversed.toList();
|
||||||
});
|
});
|
||||||
await _calculateRoute(data.stations);
|
await _calculateRoute(data.stations);
|
||||||
},
|
},
|
||||||
);
|
),
|
||||||
final addButton = ElevatedButton.icon(
|
const SizedBox(width: 12),
|
||||||
icon: const Icon(Icons.add),
|
buildSecondaryButton(
|
||||||
label: const Text('Add Station'),
|
icon: Icons.add,
|
||||||
onPressed: _addStation,
|
label: 'Add station',
|
||||||
);
|
onPressed: _addStation,
|
||||||
final calculateButton = ElevatedButton.icon(
|
),
|
||||||
icon: const Icon(Icons.route),
|
],
|
||||||
label: const Text('Calculate Route'),
|
),
|
||||||
onPressed: () async {
|
const SizedBox(height: 10),
|
||||||
await _calculateRoute(data.stations);
|
SizedBox(
|
||||||
},
|
width: double.infinity,
|
||||||
);
|
child: AnimatedSwitcher(
|
||||||
final isMobile = MediaQuery.of(context).size.width < 600;
|
duration: const Duration(milliseconds: 220),
|
||||||
return isMobile
|
transitionBuilder: (child, animation) {
|
||||||
? [addButton, reverseButton, calculateButton]
|
final curved = CurvedAnimation(
|
||||||
: [reverseButton, addButton, calculateButton];
|
parent: animation,
|
||||||
})(),
|
curve: Curves.easeOutBack,
|
||||||
],
|
);
|
||||||
),
|
return ScaleTransition(
|
||||||
|
scale:
|
||||||
|
Tween<double>(begin: 0.94, end: 1.0).animate(curved),
|
||||||
|
child: FadeTransition(opacity: animation, child: child),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: buildPrimaryAction(fullWidth: true),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: Wrap(
|
||||||
|
alignment: WrapAlignment.center,
|
||||||
|
spacing: 12,
|
||||||
|
runSpacing: 8,
|
||||||
|
children: [
|
||||||
|
buildSecondaryButton(
|
||||||
|
icon: Icons.swap_horiz,
|
||||||
|
label: 'Reverse route',
|
||||||
|
onPressed: () async {
|
||||||
|
setState(() {
|
||||||
|
data.stations = data.stations.reversed.toList();
|
||||||
|
});
|
||||||
|
await _calculateRoute(data.stations);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
AnimatedSwitcher(
|
||||||
|
duration: const Duration(milliseconds: 220),
|
||||||
|
transitionBuilder: (child, animation) {
|
||||||
|
final curved = CurvedAnimation(
|
||||||
|
parent: animation,
|
||||||
|
curve: Curves.easeOutBack,
|
||||||
|
);
|
||||||
|
return ScaleTransition(
|
||||||
|
scale:
|
||||||
|
Tween<double>(begin: 0.94, end: 1.0).animate(curved),
|
||||||
|
child: FadeTransition(opacity: animation, child: child),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: buildPrimaryAction(fullWidth: false),
|
||||||
|
),
|
||||||
|
buildSecondaryButton(
|
||||||
|
icon: Icons.add,
|
||||||
|
label: 'Add station',
|
||||||
|
onPressed: _addStation,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'package:mileograph_flutter/objects/objects.dart';
|
|||||||
import 'package:mileograph_flutter/services/authservice.dart';
|
import 'package:mileograph_flutter/services/authservice.dart';
|
||||||
import 'package:mileograph_flutter/services/data_service.dart';
|
import 'package:mileograph_flutter/services/data_service.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
part 'loco_timeline/timeline_grid.dart';
|
part 'loco_timeline/timeline_grid.dart';
|
||||||
part 'loco_timeline/event_editor.dart';
|
part 'loco_timeline/event_editor.dart';
|
||||||
@@ -27,15 +28,22 @@ class LocoTimelinePage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _LocoTimelinePageState extends State<LocoTimelinePage> {
|
class _LocoTimelinePageState extends State<LocoTimelinePage> {
|
||||||
|
static const String _prefsKeyShowPending = 'timeline_show_pending';
|
||||||
|
|
||||||
final List<_EventDraft> _draftEvents = [];
|
final List<_EventDraft> _draftEvents = [];
|
||||||
bool _isSaving = false;
|
bool _isSaving = false;
|
||||||
bool _isDeleting = false;
|
bool _isDeleting = false;
|
||||||
bool _isModerating = false;
|
final Set<int> _moderatingEventIds = {};
|
||||||
|
bool _showPending = true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) => _load());
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
|
await _restorePendingVisibility();
|
||||||
|
if (!mounted) return;
|
||||||
|
await _load();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
dynamic _normalizeFieldValue(_FieldEntry field) {
|
dynamic _normalizeFieldValue(_FieldEntry field) {
|
||||||
@@ -65,10 +73,35 @@ class _LocoTimelinePageState extends State<LocoTimelinePage> {
|
|||||||
data.fetchEventFields();
|
data.fetchEventFields();
|
||||||
return data.fetchLocoTimeline(
|
return data.fetchLocoTimeline(
|
||||||
widget.locoId,
|
widget.locoId,
|
||||||
includeAllPending: auth.isElevated,
|
includeAllPending: auth.isElevated && _showPending,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _restorePendingVisibility() async {
|
||||||
|
final auth = context.read<AuthService>();
|
||||||
|
if (!auth.isElevated) return;
|
||||||
|
try {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final saved = prefs.getBool(_prefsKeyShowPending);
|
||||||
|
if (saved == null) return;
|
||||||
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_showPending = saved;
|
||||||
|
});
|
||||||
|
} catch (_) {
|
||||||
|
// Ignore preference restore failures.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _persistPendingVisibility(bool value) async {
|
||||||
|
try {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.setBool(_prefsKeyShowPending, value);
|
||||||
|
} catch (_) {
|
||||||
|
// Ignore persistence failures.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _addDraftEvent() {
|
void _addDraftEvent() {
|
||||||
setState(() {
|
setState(() {
|
||||||
_draftEvents.add(_EventDraft());
|
_draftEvents.add(_EventDraft());
|
||||||
@@ -247,7 +280,6 @@ class _LocoTimelinePageState extends State<LocoTimelinePage> {
|
|||||||
LocoAttrVersion entry,
|
LocoAttrVersion entry,
|
||||||
_PendingModerationAction action,
|
_PendingModerationAction action,
|
||||||
) async {
|
) async {
|
||||||
if (_isModerating) return;
|
|
||||||
final eventId = entry.sourceEventId;
|
final eventId = entry.sourceEventId;
|
||||||
if (eventId == null) {
|
if (eventId == null) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
@@ -257,6 +289,7 @@ class _LocoTimelinePageState extends State<LocoTimelinePage> {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (_moderatingEventIds.contains(eventId)) return;
|
||||||
final data = context.read<DataService>();
|
final data = context.read<DataService>();
|
||||||
final approve = action == _PendingModerationAction.approve;
|
final approve = action == _PendingModerationAction.approve;
|
||||||
final messenger = ScaffoldMessenger.of(context);
|
final messenger = ScaffoldMessenger.of(context);
|
||||||
@@ -283,7 +316,7 @@ class _LocoTimelinePageState extends State<LocoTimelinePage> {
|
|||||||
if (ok != true || !mounted) return;
|
if (ok != true || !mounted) return;
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_isModerating = true;
|
_moderatingEventIds.add(eventId);
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
if (approve) {
|
if (approve) {
|
||||||
@@ -310,7 +343,7 @@ class _LocoTimelinePageState extends State<LocoTimelinePage> {
|
|||||||
} finally {
|
} finally {
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isModerating = false;
|
_moderatingEventIds.remove(eventId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -499,7 +532,11 @@ class _LocoTimelinePageState extends State<LocoTimelinePage> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final data = context.watch<DataService>();
|
final data = context.watch<DataService>();
|
||||||
final timeline = data.timelineForLoco(widget.locoId);
|
final timeline = data.timelineForLoco(widget.locoId);
|
||||||
|
final isElevated = context.select<AuthService, bool>((auth) => auth.isElevated);
|
||||||
final isLoading = data.isLocoTimelineLoading(widget.locoId);
|
final isLoading = data.isLocoTimelineLoading(widget.locoId);
|
||||||
|
final visibleTimeline = (!isElevated || _showPending)
|
||||||
|
? timeline
|
||||||
|
: timeline.where((entry) => !entry.isPending).toList();
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
@@ -516,7 +553,32 @@ class _LocoTimelinePageState extends State<LocoTimelinePage> {
|
|||||||
if (isLoading && timeline.isEmpty) {
|
if (isLoading && timeline.isEmpty) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
}
|
}
|
||||||
if (timeline.isEmpty) {
|
if (visibleTimeline.isEmpty) {
|
||||||
|
if (timeline.isNotEmpty && isElevated && !_showPending) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Card(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Pending entries hidden',
|
||||||
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
const Text(
|
||||||
|
'Enable "Show pending entries" to view pending timeline blocks.',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.all(16.0),
|
||||||
child: Card(
|
child: Card(
|
||||||
@@ -550,15 +612,27 @@ class _LocoTimelinePageState extends State<LocoTimelinePage> {
|
|||||||
return ListView(
|
return ListView(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
children: [
|
children: [
|
||||||
|
if (isElevated)
|
||||||
|
SwitchListTile.adaptive(
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
title: const Text('Show pending entries'),
|
||||||
|
value: _showPending,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_showPending = value;
|
||||||
|
});
|
||||||
|
_persistPendingVisibility(value);
|
||||||
|
},
|
||||||
|
),
|
||||||
_TimelineGrid(
|
_TimelineGrid(
|
||||||
entries: timeline,
|
entries: visibleTimeline,
|
||||||
onEditEntry: (entry) => _prefillDraftFromEntry(
|
onEditEntry: (entry) => _prefillDraftFromEntry(
|
||||||
entry,
|
entry,
|
||||||
data.eventFields,
|
data.eventFields,
|
||||||
),
|
),
|
||||||
onDeleteEntry: _deleteEntry,
|
onDeleteEntry: _deleteEntry,
|
||||||
onModeratePending: _moderatePendingEntry,
|
onModeratePending: _moderatePendingEntry,
|
||||||
pendingActionsBusy: _isModerating,
|
pendingActionEventIds: _moderatingEventIds,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_EventEditor(
|
_EventEditor(
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ class _TimelineGrid extends StatefulWidget {
|
|||||||
this.onEditEntry,
|
this.onEditEntry,
|
||||||
this.onDeleteEntry,
|
this.onDeleteEntry,
|
||||||
this.onModeratePending,
|
this.onModeratePending,
|
||||||
this.pendingActionsBusy = false,
|
this.pendingActionEventIds = const {},
|
||||||
});
|
});
|
||||||
|
|
||||||
final List<LocoAttrVersion> entries;
|
final List<LocoAttrVersion> entries;
|
||||||
@@ -20,7 +20,7 @@ class _TimelineGrid extends StatefulWidget {
|
|||||||
LocoAttrVersion entry,
|
LocoAttrVersion entry,
|
||||||
_PendingModerationAction action,
|
_PendingModerationAction action,
|
||||||
)? onModeratePending;
|
)? onModeratePending;
|
||||||
final bool pendingActionsBusy;
|
final Set<int> pendingActionEventIds;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<_TimelineGrid> createState() => _TimelineGridState();
|
State<_TimelineGrid> createState() => _TimelineGridState();
|
||||||
@@ -201,7 +201,7 @@ class _TimelineGridState extends State<_TimelineGrid> {
|
|||||||
onEditEntry: widget.onEditEntry,
|
onEditEntry: widget.onEditEntry,
|
||||||
onDeleteEntry: widget.onDeleteEntry,
|
onDeleteEntry: widget.onDeleteEntry,
|
||||||
onModeratePending: widget.onModeratePending,
|
onModeratePending: widget.onModeratePending,
|
||||||
pendingActionsBusy: widget.pendingActionsBusy,
|
pendingActionEventIds: widget.pendingActionEventIds,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -288,7 +288,7 @@ class _AttrRow extends StatelessWidget {
|
|||||||
this.onEditEntry,
|
this.onEditEntry,
|
||||||
this.onDeleteEntry,
|
this.onDeleteEntry,
|
||||||
this.onModeratePending,
|
this.onModeratePending,
|
||||||
this.pendingActionsBusy = false,
|
this.pendingActionEventIds = const {},
|
||||||
});
|
});
|
||||||
|
|
||||||
final double rowHeight;
|
final double rowHeight;
|
||||||
@@ -302,7 +302,7 @@ class _AttrRow extends StatelessWidget {
|
|||||||
LocoAttrVersion entry,
|
LocoAttrVersion entry,
|
||||||
_PendingModerationAction action,
|
_PendingModerationAction action,
|
||||||
)? onModeratePending;
|
)? onModeratePending;
|
||||||
final bool pendingActionsBusy;
|
final Set<int> pendingActionEventIds;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -329,7 +329,7 @@ class _AttrRow extends StatelessWidget {
|
|||||||
onEditEntry: onEditEntry,
|
onEditEntry: onEditEntry,
|
||||||
onDeleteEntry: onDeleteEntry,
|
onDeleteEntry: onDeleteEntry,
|
||||||
onModeratePending: onModeratePending,
|
onModeratePending: onModeratePending,
|
||||||
pendingActionsBusy: pendingActionsBusy,
|
pendingActionEventIds: pendingActionEventIds,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (activeBlock != null)
|
if (activeBlock != null)
|
||||||
@@ -346,7 +346,7 @@ class _AttrRow extends StatelessWidget {
|
|||||||
width: stickyWidth,
|
width: stickyWidth,
|
||||||
),
|
),
|
||||||
clipLeftEdge: scrollOffset > activeBlock.left + 0.1,
|
clipLeftEdge: scrollOffset > activeBlock.left + 0.1,
|
||||||
pendingActionsBusy: pendingActionsBusy,
|
pendingActionEventIds: pendingActionEventIds,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -368,12 +368,12 @@ class _ValueBlockView extends StatelessWidget {
|
|||||||
const _ValueBlockView({
|
const _ValueBlockView({
|
||||||
required this.block,
|
required this.block,
|
||||||
this.clipLeftEdge = false,
|
this.clipLeftEdge = false,
|
||||||
this.pendingActionsBusy = false,
|
this.pendingActionEventIds = const {},
|
||||||
});
|
});
|
||||||
|
|
||||||
final _ValueBlock block;
|
final _ValueBlock block;
|
||||||
final bool clipLeftEdge;
|
final bool clipLeftEdge;
|
||||||
final bool pendingActionsBusy;
|
final Set<int> pendingActionEventIds;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -384,6 +384,11 @@ class _ValueBlockView extends StatelessWidget {
|
|||||||
? Colors.white
|
? Colors.white
|
||||||
: Colors.black87;
|
: Colors.black87;
|
||||||
|
|
||||||
|
final entry = block.entry;
|
||||||
|
final eventId = entry?.sourceEventId;
|
||||||
|
final isPendingAction =
|
||||||
|
entry?.isPending == true && eventId != null && pendingActionEventIds.contains(eventId);
|
||||||
|
|
||||||
final radius = BorderRadius.only(
|
final radius = BorderRadius.only(
|
||||||
topLeft: Radius.circular(clipLeftEdge ? 0 : 12),
|
topLeft: Radius.circular(clipLeftEdge ? 0 : 12),
|
||||||
bottomLeft: Radius.circular(clipLeftEdge ? 0 : 12),
|
bottomLeft: Radius.circular(clipLeftEdge ? 0 : 12),
|
||||||
@@ -425,7 +430,7 @@ class _ValueBlockView extends StatelessWidget {
|
|||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
width: 16,
|
width: 16,
|
||||||
height: 16,
|
height: 16,
|
||||||
child: pendingActionsBusy
|
child: isPendingAction
|
||||||
? CircularProgressIndicator(
|
? CircularProgressIndicator(
|
||||||
strokeWidth: 2,
|
strokeWidth: 2,
|
||||||
valueColor:
|
valueColor:
|
||||||
@@ -484,7 +489,7 @@ class _ValueBlockMenu extends StatelessWidget {
|
|||||||
this.onEditEntry,
|
this.onEditEntry,
|
||||||
this.onDeleteEntry,
|
this.onDeleteEntry,
|
||||||
this.onModeratePending,
|
this.onModeratePending,
|
||||||
this.pendingActionsBusy = false,
|
this.pendingActionEventIds = const {},
|
||||||
});
|
});
|
||||||
|
|
||||||
final _ValueBlock block;
|
final _ValueBlock block;
|
||||||
@@ -494,7 +499,7 @@ class _ValueBlockMenu extends StatelessWidget {
|
|||||||
LocoAttrVersion entry,
|
LocoAttrVersion entry,
|
||||||
_PendingModerationAction action,
|
_PendingModerationAction action,
|
||||||
)? onModeratePending;
|
)? onModeratePending;
|
||||||
final bool pendingActionsBusy;
|
final Set<int> pendingActionEventIds;
|
||||||
|
|
||||||
bool get _hasActions {
|
bool get _hasActions {
|
||||||
final canModerate = block.entry?.isPending == true &&
|
final canModerate = block.entry?.isPending == true &&
|
||||||
@@ -515,6 +520,9 @@ class _ValueBlockMenu extends StatelessWidget {
|
|||||||
block.entry?.canModeratePending == true &&
|
block.entry?.canModeratePending == true &&
|
||||||
onModeratePending != null;
|
onModeratePending != null;
|
||||||
final canEdit = onEditEntry != null && block.entry?.isPending != true;
|
final canEdit = onEditEntry != null && block.entry?.isPending != true;
|
||||||
|
final eventId = block.entry?.sourceEventId;
|
||||||
|
final isPendingAction =
|
||||||
|
eventId != null && pendingActionEventIds.contains(eventId);
|
||||||
|
|
||||||
Future<void> showContextMenuAt(Offset globalPosition) async {
|
Future<void> showContextMenuAt(Offset globalPosition) async {
|
||||||
final overlay = Overlay.of(context);
|
final overlay = Overlay.of(context);
|
||||||
@@ -540,13 +548,13 @@ class _ValueBlockMenu extends StatelessWidget {
|
|||||||
if (canModerate)
|
if (canModerate)
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: _TimelineBlockAction.approve,
|
value: _TimelineBlockAction.approve,
|
||||||
enabled: !pendingActionsBusy,
|
enabled: !isPendingAction,
|
||||||
child: const Text('Approve pending'),
|
child: const Text('Approve pending'),
|
||||||
),
|
),
|
||||||
if (canModerate)
|
if (canModerate)
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: _TimelineBlockAction.reject,
|
value: _TimelineBlockAction.reject,
|
||||||
enabled: !pendingActionsBusy,
|
enabled: !isPendingAction,
|
||||||
child: const Text('Reject pending'),
|
child: const Text('Reject pending'),
|
||||||
),
|
),
|
||||||
if (onDeleteEntry != null)
|
if (onDeleteEntry != null)
|
||||||
@@ -588,7 +596,7 @@ class _ValueBlockMenu extends StatelessWidget {
|
|||||||
},
|
},
|
||||||
child: _ValueBlockView(
|
child: _ValueBlockView(
|
||||||
block: block,
|
block: block,
|
||||||
pendingActionsBusy: pendingActionsBusy,
|
pendingActionEventIds: pendingActionEventIds,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -714,17 +714,37 @@ class _TractionPageState extends State<TractionPage> {
|
|||||||
final items = <PopupMenuEntry<_TractionMoreAction>>[];
|
final items = <PopupMenuEntry<_TractionMoreAction>>[];
|
||||||
if (hasClassActions) {
|
if (hasClassActions) {
|
||||||
items.add(
|
items.add(
|
||||||
const PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: _TractionMoreAction.classStats,
|
value: _TractionMoreAction.classStats,
|
||||||
child: Text('Class stats'),
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
_showClassStatsPanel ? Icons.check : Icons.check_box_outline_blank,
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
const Text('Class stats'),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (hasClassActions) {
|
if (hasClassActions) {
|
||||||
items.add(
|
items.add(
|
||||||
const PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: _TractionMoreAction.classLeaderboard,
|
value: _TractionMoreAction.classLeaderboard,
|
||||||
child: Text('Class leaderboard'),
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
_showClassLeaderboardPanel
|
||||||
|
? Icons.check
|
||||||
|
: Icons.check_box_outline_blank,
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
const Text('Class leaderboard'),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -523,26 +523,34 @@ class _MyHomePageState extends State<MyHomePage> {
|
|||||||
)
|
)
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
|
final logo = Text.rich(
|
||||||
|
TextSpan(
|
||||||
|
children: const [
|
||||||
|
TextSpan(text: "Mile"),
|
||||||
|
TextSpan(
|
||||||
|
text: "O",
|
||||||
|
style: TextStyle(color: Colors.red),
|
||||||
|
),
|
||||||
|
TextSpan(text: "graph"),
|
||||||
|
],
|
||||||
|
style: const TextStyle(
|
||||||
|
decoration: TextDecoration.none,
|
||||||
|
color: Colors.white,
|
||||||
|
fontFamily: "Tomatoes",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
|
||||||
title: Text.rich(
|
title: isWide
|
||||||
TextSpan(
|
? logo
|
||||||
children: const [
|
: FittedBox(
|
||||||
TextSpan(text: "Mile"),
|
fit: BoxFit.scaleDown,
|
||||||
TextSpan(
|
alignment: Alignment.centerLeft,
|
||||||
text: "O",
|
child: logo,
|
||||||
style: TextStyle(color: Colors.red),
|
|
||||||
),
|
),
|
||||||
TextSpan(text: "graph"),
|
|
||||||
],
|
|
||||||
style: const TextStyle(
|
|
||||||
decoration: TextDecoration.none,
|
|
||||||
color: Colors.white,
|
|
||||||
fontFamily: "Tomatoes",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
actions: [
|
actions: [
|
||||||
_buildNotificationAction(context, data),
|
_buildNotificationAction(context, data),
|
||||||
IconButton(
|
IconButton(
|
||||||
|
|||||||
Reference in New Issue
Block a user