102 lines
3.0 KiB
Dart
102 lines
3.0 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import '../../theme/app_theme.dart';
|
|
import '../models/sync_run_event.dart';
|
|
|
|
String formatRelativeTime(DateTime startedAt, {DateTime? now}) {
|
|
final DateTime reference = (now ?? DateTime.now()).toUtc();
|
|
final Duration delta = reference.difference(startedAt.toUtc());
|
|
if (delta.inMinutes < 1) {
|
|
return 'just now';
|
|
}
|
|
if (delta.inHours < 1) {
|
|
return '${delta.inMinutes}m ago';
|
|
}
|
|
if (delta.inDays < 1) {
|
|
return '${delta.inHours}h ago';
|
|
}
|
|
return '${delta.inDays}d ago';
|
|
}
|
|
|
|
String formatMarketHistorySlotWire(DateTime value) {
|
|
final DateTime utc = value.toUtc();
|
|
final int slotHour = (utc.hour ~/ 4) * 4;
|
|
final DateTime slotStart = DateTime.utc(
|
|
utc.year,
|
|
utc.month,
|
|
utc.day,
|
|
slotHour,
|
|
);
|
|
String two(int n) => n.toString().padLeft(2, '0');
|
|
return '${slotStart.year.toString().padLeft(4, '0')}-'
|
|
'${two(slotStart.month)}-${two(slotStart.day)}T'
|
|
'${two(slotStart.hour)}:${two(slotStart.minute)}:${two(slotStart.second)}Z';
|
|
}
|
|
|
|
String formatUtcTimestamp(DateTime? value) {
|
|
if (value == null) {
|
|
return '—';
|
|
}
|
|
final DateTime utc = value.toUtc();
|
|
final String hour = utc.hour.toString().padLeft(2, '0');
|
|
final String minute = utc.minute.toString().padLeft(2, '0');
|
|
return '${utc.year}-${utc.month.toString().padLeft(2, '0')}-'
|
|
'${utc.day.toString().padLeft(2, '0')} $hour:$minute UTC';
|
|
}
|
|
|
|
String formatLocalTimestamp(DateTime? value) {
|
|
if (value == null) {
|
|
return '—';
|
|
}
|
|
final DateTime local = value.toLocal();
|
|
final String hour = local.hour.toString().padLeft(2, '0');
|
|
final String minute = local.minute.toString().padLeft(2, '0');
|
|
return '${local.year}-${local.month.toString().padLeft(2, '0')}-'
|
|
'${local.day.toString().padLeft(2, '0')} $hour:$minute';
|
|
}
|
|
|
|
String formatDurationMs(int? durationMs) {
|
|
if (durationMs == null) {
|
|
return '—';
|
|
}
|
|
if (durationMs < 1000) {
|
|
return '${durationMs}ms';
|
|
}
|
|
return '${(durationMs / 1000).toStringAsFixed(1)}s';
|
|
}
|
|
|
|
String shortStatusLabel(SyncRunEvent event) {
|
|
return switch (event.status) {
|
|
SyncRunStatus.success => 'Success',
|
|
SyncRunStatus.failed => 'Failed',
|
|
SyncRunStatus.partial => 'Partial success',
|
|
SyncRunStatus.inProgress => 'In progress',
|
|
};
|
|
}
|
|
|
|
IconData severityIcon(SyncRunSeverity severity) {
|
|
return switch (severity) {
|
|
SyncRunSeverity.ok => Icons.check_circle_outline,
|
|
SyncRunSeverity.warning => Icons.schedule,
|
|
SyncRunSeverity.error => Icons.error_outline,
|
|
SyncRunSeverity.rateLimit => Icons.speed,
|
|
};
|
|
}
|
|
|
|
Color severityColor(SyncRunSeverity severity) {
|
|
return switch (severity) {
|
|
SyncRunSeverity.ok => AppColors.success,
|
|
SyncRunSeverity.warning => Colors.orange,
|
|
SyncRunSeverity.error => const Color(0xFFF87171),
|
|
SyncRunSeverity.rateLimit => Colors.amber,
|
|
};
|
|
}
|
|
|
|
String? parseHttpStatus(String? error) {
|
|
if (error == null || error.trim().isEmpty) {
|
|
return null;
|
|
}
|
|
final RegExpMatch? match = RegExp(r'\b(\d{3})\b').firstMatch(error);
|
|
return match?.group(1);
|
|
}
|