Skip to content

Commit 7678f84

Browse files
committed
fix: refine mobile feedback and console layout
1 parent bc56453 commit 7678f84

21 files changed

Lines changed: 621 additions & 220 deletions

.github/workflows/build-and-release-app.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@ jobs:
246246
uses: softprops/action-gh-release@v1
247247
with:
248248
tag_name: ${{ needs.get-version.outputs.version }}
249+
target_commitish: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.sha }}
249250
name: Release ${{ needs.get-version.outputs.version }}
250251
prerelease: ${{ needs.get-version.outputs.prerelease }}
251252
generate_release_notes: true

lib/controllers/notification_controller.dart

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ class NotificationController with ChangeNotifier {
88
static const _storageKey = 'notificationHistory';
99
static const _maximumEntries = 100;
1010
static const _popupsKey = 'notificationPopupsEnabled';
11+
static const _duplicateWindow = Duration(seconds: 30);
1112

1213
final SharedPreferences _preferences;
1314
late List<AppNotification> _entries;
@@ -36,12 +37,21 @@ class NotificationController with ChangeNotifier {
3637
notifyListeners();
3738
}
3839

39-
void add({
40+
bool add({
4041
required AppNotificationKind kind,
4142
required String title,
4243
required String message,
4344
}) {
4445
final now = DateTime.now().toUtc();
46+
if (_entries.isNotEmpty) {
47+
final latest = _entries.last;
48+
final duplicate =
49+
latest.kind == kind &&
50+
latest.title == title &&
51+
latest.message == message &&
52+
now.difference(latest.createdAt) <= _duplicateWindow;
53+
if (duplicate) return false;
54+
}
4555
_entries.add(
4656
AppNotification(
4757
id: now.microsecondsSinceEpoch.toString(),
@@ -56,6 +66,7 @@ class NotificationController with ChangeNotifier {
5666
}
5767
_persist();
5868
notifyListeners();
69+
return true;
5970
}
6071

6172
void markAllRead() {

lib/main.dart

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import 'package:admincraft/utils/toast_utils.dart';
77
import 'package:flutter/material.dart';
88
import 'package:provider/provider.dart';
99
import 'package:shared_preferences/shared_preferences.dart';
10-
import 'package:toastification/toastification.dart';
1110

1211
import 'models/model.dart';
1312
import 'services/secret_migration.dart';
@@ -105,15 +104,14 @@ class Admincraft extends StatelessWidget {
105104
Widget build(BuildContext context) {
106105
final model = Provider.of<Model>(context);
107106

108-
return ToastificationWrapper(
109-
child: MaterialApp(
110-
title: "Admincraft",
111-
debugShowCheckedModeBanner: false,
112-
themeMode: model.themeMode,
113-
theme: _theme(model, Brightness.light),
114-
darkTheme: _theme(model, Brightness.dark),
115-
home: const Tabs(),
116-
),
107+
return MaterialApp(
108+
navigatorKey: ToastUtils.navigatorKey,
109+
title: "Admincraft",
110+
debugShowCheckedModeBanner: false,
111+
themeMode: model.themeMode,
112+
theme: _theme(model, Brightness.light),
113+
darkTheme: _theme(model, Brightness.dark),
114+
home: const Tabs(),
117115
);
118116
}
119117
}

lib/models/app_notification.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,12 @@ class AppNotification {
1717
this.read = false,
1818
});
1919

20-
AppNotification copyWith({bool? read}) => AppNotification(
20+
AppNotification copyWith({bool? read, DateTime? createdAt}) => AppNotification(
2121
id: id,
2222
kind: kind,
2323
title: title,
2424
message: message,
25-
createdAt: createdAt,
25+
createdAt: createdAt ?? this.createdAt,
2626
read: read ?? this.read,
2727
);
2828

lib/services/persistence_service.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -249,11 +249,11 @@ class PersistenceService {
249249

250250
String get font => _prefs.getString(_fontKey) ?? 'Roboto';
251251
double get fontSize => _prefs.getDouble(_fontSizeKey) ?? 16;
252-
String get terminalFont => _prefs.getString(_terminalFontKey) ?? 'Monocraft';
252+
String get terminalFont => _prefs.getString(_terminalFontKey) ?? 'Consolas';
253253
double get terminalFontSize => _prefs.getDouble(_terminalFontSizeKey) ?? 14;
254254
bool get terminalAutoScroll => _prefs.getBool(_terminalAutoScrollKey) ?? true;
255255
String get consoleTimestampMode =>
256-
_prefs.getString(_consoleTimestampModeKey) ?? 'short';
256+
_prefs.getString(_consoleTimestampModeKey) ?? 'hidden';
257257
String get consoleFilterPattern =>
258258
_prefs.getString(_consoleFilterPatternKey) ?? '';
259259

lib/utils/toast_utils.dart

Lines changed: 159 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,194 @@
1+
import 'dart:async';
2+
13
import 'package:admincraft/controllers/notification_controller.dart';
24
import 'package:admincraft/models/app_notification.dart';
35
import 'package:flutter/material.dart';
4-
import 'package:toastification/toastification.dart';
56

67
class ToastUtils {
78
static NotificationController? _notifications;
9+
static final navigatorKey = GlobalKey<NavigatorState>();
10+
static OverlayEntry? _popupEntry;
11+
static Timer? _popupTimer;
812

913
static void initialize(NotificationController notifications) {
1014
_notifications = notifications;
1115
}
1216

1317
static void showToastError(String message) {
14-
_notifications?.add(
18+
final added = _notifications?.add(
1519
kind: AppNotificationKind.error,
1620
title: 'Connection or command error',
1721
message: message,
1822
);
19-
if (_notifications?.popupsEnabled == false) return;
20-
toastification.show(
21-
type: ToastificationType.error,
22-
style: ToastificationStyle.flat,
23-
autoCloseDuration: const Duration(seconds: 8),
24-
title: const Text('Error'),
25-
description: Text(message),
26-
alignment: Alignment.topRight,
27-
margin: const EdgeInsets.fromLTRB(12, 64, 12, 8),
28-
animationDuration: const Duration(milliseconds: 100),
23+
if (added == false || _notifications?.popupsEnabled == false) return;
24+
_showPopup(
25+
kind: AppNotificationKind.error,
26+
title: 'Error',
27+
message: message,
28+
duration: const Duration(milliseconds: 3500),
2929
);
3030
}
3131

3232
static void showToastSuccess(String message) {
33-
_notifications?.add(
33+
final added = _notifications?.add(
3434
kind: AppNotificationKind.success,
3535
title: 'Completed',
3636
message: message,
3737
);
38-
if (_notifications?.popupsEnabled == false) return;
39-
toastification.show(
40-
type: ToastificationType.success,
41-
style: ToastificationStyle.flat,
42-
autoCloseDuration: const Duration(seconds: 2),
43-
title: Text(message),
44-
alignment: Alignment.topRight,
45-
margin: const EdgeInsets.fromLTRB(12, 64, 12, 8),
46-
animationDuration: const Duration(milliseconds: 100),
38+
if (added == false || _notifications?.popupsEnabled == false) return;
39+
_showPopup(
40+
kind: AppNotificationKind.success,
41+
title: message,
42+
duration: const Duration(milliseconds: 1400),
4743
);
4844
}
4945

5046
static void showInfo(String title, String message) {
51-
_notifications?.add(
47+
final added = _notifications?.add(
48+
kind: AppNotificationKind.info,
49+
title: title,
50+
message: message,
51+
);
52+
if (added == false || _notifications?.popupsEnabled == false) return;
53+
_showPopup(
5254
kind: AppNotificationKind.info,
5355
title: title,
5456
message: message,
57+
duration: const Duration(milliseconds: 2400),
58+
);
59+
}
60+
61+
static void _showPopup({
62+
required AppNotificationKind kind,
63+
required String title,
64+
String? message,
65+
required Duration duration,
66+
}) {
67+
// Popups are only a glanceable hint; the inbox keeps the durable copy.
68+
// The entire overlay ignores pointers, not just the visible card, so its
69+
// full-screen layout can never intercept controls underneath.
70+
dismissPopups();
71+
final overlay = navigatorKey.currentState?.overlay;
72+
if (overlay == null) return;
73+
74+
_popupEntry = OverlayEntry(
75+
builder: (context) => Positioned.fill(
76+
child: IgnorePointer(
77+
child: SafeArea(
78+
bottom: false,
79+
child: Align(
80+
alignment: Alignment.topCenter,
81+
child: Padding(
82+
padding: const EdgeInsets.fromLTRB(
83+
12,
84+
kToolbarHeight + 6,
85+
12,
86+
0,
87+
),
88+
child: ConstrainedBox(
89+
constraints: const BoxConstraints(maxWidth: 520),
90+
child: _CompactNotification(
91+
kind: kind,
92+
title: title,
93+
message: message,
94+
duration: duration,
95+
),
96+
),
97+
),
98+
),
99+
),
100+
),
101+
),
55102
);
56-
if (_notifications?.popupsEnabled == false) return;
57-
toastification.show(
58-
type: ToastificationType.info,
59-
style: ToastificationStyle.flat,
60-
autoCloseDuration: const Duration(seconds: 5),
61-
title: Text(title),
62-
description: Text(message),
63-
alignment: Alignment.topRight,
64-
margin: const EdgeInsets.fromLTRB(12, 64, 12, 8),
65-
animationDuration: const Duration(milliseconds: 100),
103+
overlay.insert(_popupEntry!);
104+
_popupTimer = Timer(duration, dismissPopups);
105+
}
106+
107+
static void dismissPopups() {
108+
_popupTimer?.cancel();
109+
_popupTimer = null;
110+
_popupEntry?.remove();
111+
_popupEntry = null;
112+
}
113+
}
114+
115+
class _CompactNotification extends StatelessWidget {
116+
final AppNotificationKind kind;
117+
final String title;
118+
final String? message;
119+
final Duration duration;
120+
121+
const _CompactNotification({
122+
required this.kind,
123+
required this.title,
124+
required this.duration,
125+
this.message,
126+
});
127+
128+
@override
129+
Widget build(BuildContext context) {
130+
final scheme = Theme.of(context).colorScheme;
131+
final (icon, color) = switch (kind) {
132+
AppNotificationKind.error => (Icons.error_outline, scheme.error),
133+
AppNotificationKind.warning => (Icons.warning_amber, Colors.orange),
134+
AppNotificationKind.success => (Icons.check_circle_outline, Colors.green),
135+
AppNotificationKind.info => (Icons.info_outline, scheme.primary),
136+
};
137+
final detail = message?.trim();
138+
139+
return Material(
140+
elevation: 6,
141+
color: scheme.surfaceContainerHigh,
142+
borderRadius: BorderRadius.circular(12),
143+
clipBehavior: Clip.antiAlias,
144+
child: Column(
145+
mainAxisSize: MainAxisSize.min,
146+
children: [
147+
Padding(
148+
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 9),
149+
child: Row(
150+
mainAxisSize: MainAxisSize.min,
151+
children: [
152+
Icon(icon, color: color, size: 20),
153+
const SizedBox(width: 10),
154+
Flexible(
155+
child: Column(
156+
mainAxisSize: MainAxisSize.min,
157+
crossAxisAlignment: CrossAxisAlignment.start,
158+
children: [
159+
Text(
160+
title,
161+
maxLines: 1,
162+
overflow: TextOverflow.ellipsis,
163+
style: Theme.of(context).textTheme.labelLarge,
164+
),
165+
if (detail != null &&
166+
detail.isNotEmpty &&
167+
detail != title)
168+
Text(
169+
detail,
170+
maxLines: 2,
171+
overflow: TextOverflow.ellipsis,
172+
style: Theme.of(context).textTheme.bodySmall,
173+
),
174+
],
175+
),
176+
),
177+
],
178+
),
179+
),
180+
TweenAnimationBuilder<double>(
181+
tween: Tween(begin: 0, end: 1),
182+
duration: duration,
183+
builder: (context, value, _) => LinearProgressIndicator(
184+
value: value,
185+
minHeight: 2,
186+
color: color,
187+
backgroundColor: color.withValues(alpha: 0.12),
188+
),
189+
),
190+
],
191+
),
66192
);
67193
}
68194
}

0 commit comments

Comments
 (0)