minor page tweaks
This commit is contained in:
@@ -8,6 +8,7 @@ import 'package:mileograph_flutter/objects/objects.dart';
|
||||
import 'package:mileograph_flutter/services/authservice.dart';
|
||||
import 'package:mileograph_flutter/services/data_service.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
part 'loco_timeline/timeline_grid.dart';
|
||||
part 'loco_timeline/event_editor.dart';
|
||||
@@ -27,15 +28,22 @@ class LocoTimelinePage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _LocoTimelinePageState extends State<LocoTimelinePage> {
|
||||
static const String _prefsKeyShowPending = 'timeline_show_pending';
|
||||
|
||||
final List<_EventDraft> _draftEvents = [];
|
||||
bool _isSaving = false;
|
||||
bool _isDeleting = false;
|
||||
bool _isModerating = false;
|
||||
final Set<int> _moderatingEventIds = {};
|
||||
bool _showPending = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _load());
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
await _restorePendingVisibility();
|
||||
if (!mounted) return;
|
||||
await _load();
|
||||
});
|
||||
}
|
||||
|
||||
dynamic _normalizeFieldValue(_FieldEntry field) {
|
||||
@@ -65,10 +73,35 @@ class _LocoTimelinePageState extends State<LocoTimelinePage> {
|
||||
data.fetchEventFields();
|
||||
return data.fetchLocoTimeline(
|
||||
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() {
|
||||
setState(() {
|
||||
_draftEvents.add(_EventDraft());
|
||||
@@ -247,7 +280,6 @@ class _LocoTimelinePageState extends State<LocoTimelinePage> {
|
||||
LocoAttrVersion entry,
|
||||
_PendingModerationAction action,
|
||||
) async {
|
||||
if (_isModerating) return;
|
||||
final eventId = entry.sourceEventId;
|
||||
if (eventId == null) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
@@ -257,6 +289,7 @@ class _LocoTimelinePageState extends State<LocoTimelinePage> {
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (_moderatingEventIds.contains(eventId)) return;
|
||||
final data = context.read<DataService>();
|
||||
final approve = action == _PendingModerationAction.approve;
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
@@ -283,7 +316,7 @@ class _LocoTimelinePageState extends State<LocoTimelinePage> {
|
||||
if (ok != true || !mounted) return;
|
||||
|
||||
setState(() {
|
||||
_isModerating = true;
|
||||
_moderatingEventIds.add(eventId);
|
||||
});
|
||||
try {
|
||||
if (approve) {
|
||||
@@ -310,7 +343,7 @@ class _LocoTimelinePageState extends State<LocoTimelinePage> {
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isModerating = false;
|
||||
_moderatingEventIds.remove(eventId);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -499,7 +532,11 @@ class _LocoTimelinePageState extends State<LocoTimelinePage> {
|
||||
Widget build(BuildContext context) {
|
||||
final data = context.watch<DataService>();
|
||||
final timeline = data.timelineForLoco(widget.locoId);
|
||||
final isElevated = context.select<AuthService, bool>((auth) => auth.isElevated);
|
||||
final isLoading = data.isLocoTimelineLoading(widget.locoId);
|
||||
final visibleTimeline = (!isElevated || _showPending)
|
||||
? timeline
|
||||
: timeline.where((entry) => !entry.isPending).toList();
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
@@ -516,7 +553,32 @@ class _LocoTimelinePageState extends State<LocoTimelinePage> {
|
||||
if (isLoading && timeline.isEmpty) {
|
||||
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(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Card(
|
||||
@@ -550,15 +612,27 @@ class _LocoTimelinePageState extends State<LocoTimelinePage> {
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
if (isElevated)
|
||||
SwitchListTile.adaptive(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
title: const Text('Show pending entries'),
|
||||
value: _showPending,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_showPending = value;
|
||||
});
|
||||
_persistPendingVisibility(value);
|
||||
},
|
||||
),
|
||||
_TimelineGrid(
|
||||
entries: timeline,
|
||||
entries: visibleTimeline,
|
||||
onEditEntry: (entry) => _prefillDraftFromEntry(
|
||||
entry,
|
||||
data.eventFields,
|
||||
),
|
||||
onDeleteEntry: _deleteEntry,
|
||||
onModeratePending: _moderatePendingEntry,
|
||||
pendingActionsBusy: _isModerating,
|
||||
pendingActionEventIds: _moderatingEventIds,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_EventEditor(
|
||||
|
||||
@@ -10,7 +10,7 @@ class _TimelineGrid extends StatefulWidget {
|
||||
this.onEditEntry,
|
||||
this.onDeleteEntry,
|
||||
this.onModeratePending,
|
||||
this.pendingActionsBusy = false,
|
||||
this.pendingActionEventIds = const {},
|
||||
});
|
||||
|
||||
final List<LocoAttrVersion> entries;
|
||||
@@ -20,7 +20,7 @@ class _TimelineGrid extends StatefulWidget {
|
||||
LocoAttrVersion entry,
|
||||
_PendingModerationAction action,
|
||||
)? onModeratePending;
|
||||
final bool pendingActionsBusy;
|
||||
final Set<int> pendingActionEventIds;
|
||||
|
||||
@override
|
||||
State<_TimelineGrid> createState() => _TimelineGridState();
|
||||
@@ -201,7 +201,7 @@ class _TimelineGridState extends State<_TimelineGrid> {
|
||||
onEditEntry: widget.onEditEntry,
|
||||
onDeleteEntry: widget.onDeleteEntry,
|
||||
onModeratePending: widget.onModeratePending,
|
||||
pendingActionsBusy: widget.pendingActionsBusy,
|
||||
pendingActionEventIds: widget.pendingActionEventIds,
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -288,7 +288,7 @@ class _AttrRow extends StatelessWidget {
|
||||
this.onEditEntry,
|
||||
this.onDeleteEntry,
|
||||
this.onModeratePending,
|
||||
this.pendingActionsBusy = false,
|
||||
this.pendingActionEventIds = const {},
|
||||
});
|
||||
|
||||
final double rowHeight;
|
||||
@@ -302,7 +302,7 @@ class _AttrRow extends StatelessWidget {
|
||||
LocoAttrVersion entry,
|
||||
_PendingModerationAction action,
|
||||
)? onModeratePending;
|
||||
final bool pendingActionsBusy;
|
||||
final Set<int> pendingActionEventIds;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -329,7 +329,7 @@ class _AttrRow extends StatelessWidget {
|
||||
onEditEntry: onEditEntry,
|
||||
onDeleteEntry: onDeleteEntry,
|
||||
onModeratePending: onModeratePending,
|
||||
pendingActionsBusy: pendingActionsBusy,
|
||||
pendingActionEventIds: pendingActionEventIds,
|
||||
),
|
||||
),
|
||||
if (activeBlock != null)
|
||||
@@ -346,7 +346,7 @@ class _AttrRow extends StatelessWidget {
|
||||
width: stickyWidth,
|
||||
),
|
||||
clipLeftEdge: scrollOffset > activeBlock.left + 0.1,
|
||||
pendingActionsBusy: pendingActionsBusy,
|
||||
pendingActionEventIds: pendingActionEventIds,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -368,12 +368,12 @@ class _ValueBlockView extends StatelessWidget {
|
||||
const _ValueBlockView({
|
||||
required this.block,
|
||||
this.clipLeftEdge = false,
|
||||
this.pendingActionsBusy = false,
|
||||
this.pendingActionEventIds = const {},
|
||||
});
|
||||
|
||||
final _ValueBlock block;
|
||||
final bool clipLeftEdge;
|
||||
final bool pendingActionsBusy;
|
||||
final Set<int> pendingActionEventIds;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -384,6 +384,11 @@ class _ValueBlockView extends StatelessWidget {
|
||||
? Colors.white
|
||||
: Colors.black87;
|
||||
|
||||
final entry = block.entry;
|
||||
final eventId = entry?.sourceEventId;
|
||||
final isPendingAction =
|
||||
entry?.isPending == true && eventId != null && pendingActionEventIds.contains(eventId);
|
||||
|
||||
final radius = BorderRadius.only(
|
||||
topLeft: Radius.circular(clipLeftEdge ? 0 : 12),
|
||||
bottomLeft: Radius.circular(clipLeftEdge ? 0 : 12),
|
||||
@@ -425,7 +430,7 @@ class _ValueBlockView extends StatelessWidget {
|
||||
child: SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: pendingActionsBusy
|
||||
child: isPendingAction
|
||||
? CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor:
|
||||
@@ -484,7 +489,7 @@ class _ValueBlockMenu extends StatelessWidget {
|
||||
this.onEditEntry,
|
||||
this.onDeleteEntry,
|
||||
this.onModeratePending,
|
||||
this.pendingActionsBusy = false,
|
||||
this.pendingActionEventIds = const {},
|
||||
});
|
||||
|
||||
final _ValueBlock block;
|
||||
@@ -494,7 +499,7 @@ class _ValueBlockMenu extends StatelessWidget {
|
||||
LocoAttrVersion entry,
|
||||
_PendingModerationAction action,
|
||||
)? onModeratePending;
|
||||
final bool pendingActionsBusy;
|
||||
final Set<int> pendingActionEventIds;
|
||||
|
||||
bool get _hasActions {
|
||||
final canModerate = block.entry?.isPending == true &&
|
||||
@@ -515,6 +520,9 @@ class _ValueBlockMenu extends StatelessWidget {
|
||||
block.entry?.canModeratePending == true &&
|
||||
onModeratePending != null;
|
||||
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 {
|
||||
final overlay = Overlay.of(context);
|
||||
@@ -540,13 +548,13 @@ class _ValueBlockMenu extends StatelessWidget {
|
||||
if (canModerate)
|
||||
PopupMenuItem(
|
||||
value: _TimelineBlockAction.approve,
|
||||
enabled: !pendingActionsBusy,
|
||||
enabled: !isPendingAction,
|
||||
child: const Text('Approve pending'),
|
||||
),
|
||||
if (canModerate)
|
||||
PopupMenuItem(
|
||||
value: _TimelineBlockAction.reject,
|
||||
enabled: !pendingActionsBusy,
|
||||
enabled: !isPendingAction,
|
||||
child: const Text('Reject pending'),
|
||||
),
|
||||
if (onDeleteEntry != null)
|
||||
@@ -588,7 +596,7 @@ class _ValueBlockMenu extends StatelessWidget {
|
||||
},
|
||||
child: _ValueBlockView(
|
||||
block: block,
|
||||
pendingActionsBusy: pendingActionsBusy,
|
||||
pendingActionEventIds: pendingActionEventIds,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -714,17 +714,37 @@ class _TractionPageState extends State<TractionPage> {
|
||||
final items = <PopupMenuEntry<_TractionMoreAction>>[];
|
||||
if (hasClassActions) {
|
||||
items.add(
|
||||
const PopupMenuItem(
|
||||
PopupMenuItem(
|
||||
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) {
|
||||
items.add(
|
||||
const PopupMenuItem(
|
||||
PopupMenuItem(
|
||||
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'),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user