Offline Support

This commit is contained in:
2026-04-27 06:20:39 +08:00
parent 7d8851a94a
commit 48cd3bcd60
121 changed files with 14798 additions and 2214 deletions
@@ -57,9 +57,12 @@ class _AnnouncementCommentsSectionState
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Failed to post comment: $e')),
);
if (isOfflineSaveError(e)) {
_controller.clear();
showSuccessSnackBar(context, 'Comment saved offline — will sync when connected.');
} else {
showErrorSnackBar(context, 'Failed to post comment: $e');
}
}
} finally {
if (mounted) setState(() => _sending = false);
@@ -14,6 +14,7 @@ import '../../widgets/app_page_header.dart';
import '../../widgets/m3_card.dart';
import '../../widgets/profile_avatar.dart';
import '../../widgets/responsive_body.dart';
import '../../widgets/sync_pending_badge.dart';
import 'announcement_comments_section.dart';
import 'create_announcement_dialog.dart';
@@ -317,6 +318,14 @@ class _AnnouncementCardState extends ConsumerState<_AnnouncementCard> {
ref.watch(announcementCommentsProvider(widget.announcement.id));
final commentCount = commentsAsync.valueOrNull?.length ?? 0;
// Pending-sync state
final pendingNew = ref
.watch(offlinePendingAnnouncementsProvider)
.any((a) => a.id == widget.announcement.id);
final pendingUpdates = ref.watch(offlinePendingAnnouncementUpdatesProvider);
final isAnnouncementPending =
pendingNew || pendingUpdates.containsKey(widget.announcement.id);
// Rebuild UI when cooldown is active.
if (_inCooldown) {
Future.delayed(const Duration(milliseconds: 500), () {
@@ -428,9 +437,21 @@ class _AnnouncementCardState extends ConsumerState<_AnnouncementCard> {
// Title + Body
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
child: Text(
widget.announcement.title,
style: tt.titleMedium?.copyWith(fontWeight: FontWeight.w600),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Text(
widget.announcement.title,
style: tt.titleMedium
?.copyWith(fontWeight: FontWeight.w600),
),
),
if (isAnnouncementPending) ...[
const SizedBox(width: 8),
SyncPendingBadge(isPending: true, compact: true),
],
],
),
),
Padding(
@@ -269,7 +269,14 @@ class _CreateAnnouncementContentState
);
}
} catch (e) {
if (mounted) showErrorSnackBar(context, 'Failed to save: $e');
if (mounted) {
if (isOfflineSaveError(e)) {
Navigator.of(context).pop();
showSuccessSnackBarGlobal('Announcement saved offline — will sync when connected.');
} else {
showErrorSnackBar(context, 'Failed to save: $e');
}
}
} finally {
if (mounted) setState(() => _submitting = false);
}
@@ -537,7 +544,14 @@ class _BannerSettingsContentState
if (mounted) Navigator.of(context).pop();
showSuccessSnackBarGlobal('Banner settings saved.');
} catch (e) {
if (mounted) showErrorSnackBar(context, 'Failed to save: $e');
if (mounted) {
if (isOfflineSaveError(e)) {
Navigator.of(context).pop();
showSuccessSnackBarGlobal('Banner settings saved offline — will sync when connected.');
} else {
showErrorSnackBar(context, 'Failed to save: $e');
}
}
} finally {
if (mounted) setState(() => _submitting = false);
}