All checks were successful
CI / Windows build (push) Successful in 7m47s
Redesign the optional FastAPI companion around vault files (manifest / PUT/GET/DELETE + OCR jobs) instead of legacy strokes_json notes. Wire a client Server settings panel for health/login. Polish shell UX: l10n for settings/home/board, sticky-board empty state, and a narrow-screen diagnostics FAB. Co-authored-by: Cursor <cursoragent@cursor.com>
178 lines
5.6 KiB
Dart
178 lines
5.6 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
|
|
import 'badnote_log.dart';
|
|
import 'diagnostic_export.dart';
|
|
import '../editor/canvas/input_diagnostics.dart';
|
|
import '../editor/input/diagnostic_logger.dart';
|
|
import '../editor/input/pen_input_service.dart';
|
|
import '../l10n/app_localizations.dart';
|
|
|
|
/// Shared diagnostics chrome: overlay readout + export action.
|
|
/// Mount on any document surface (note / PDF / PPT / board).
|
|
class DiagnosticChrome extends StatefulWidget {
|
|
const DiagnosticChrome({
|
|
super.key,
|
|
required this.child,
|
|
this.initiallyVisible = false,
|
|
});
|
|
|
|
final Widget child;
|
|
final bool initiallyVisible;
|
|
|
|
@override
|
|
State<DiagnosticChrome> createState() => DiagnosticChromeState();
|
|
}
|
|
|
|
class DiagnosticChromeState extends State<DiagnosticChrome> {
|
|
late bool _visible = widget.initiallyVisible;
|
|
bool _exporting = false;
|
|
String? _lastExportPath;
|
|
|
|
bool get isVisible => _visible;
|
|
|
|
void toggle() {
|
|
setState(() {
|
|
_visible = !_visible;
|
|
if (_visible) {
|
|
DiagnosticLogger.instance.start();
|
|
InputDiagnostics.instance.reset();
|
|
} else {
|
|
DiagnosticLogger.instance.stop();
|
|
}
|
|
});
|
|
}
|
|
|
|
Future<void> exportPack() async {
|
|
if (_exporting) return;
|
|
setState(() => _exporting = true);
|
|
try {
|
|
final result = await DiagnosticExport.instance.exportPack();
|
|
if (!mounted) return;
|
|
setState(() => _lastExportPath = result.zipPath);
|
|
await Clipboard.setData(ClipboardData(text: result.zipPath));
|
|
if (!mounted) return;
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(
|
|
AppLocalizations.of(context).diagExported(result.bytes),
|
|
),
|
|
duration: const Duration(seconds: 5),
|
|
),
|
|
);
|
|
} catch (e) {
|
|
BadNoteLog.instance.error(LogSubsystem.diag, 'export_failed', fields: {
|
|
'error': '$e',
|
|
});
|
|
if (!mounted) return;
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(AppLocalizations.of(context).diagExportFail('$e')),
|
|
),
|
|
);
|
|
} finally {
|
|
if (mounted) setState(() => _exporting = false);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Stack(
|
|
fit: StackFit.expand,
|
|
children: [
|
|
widget.child,
|
|
if (_visible)
|
|
Positioned(
|
|
left: 8,
|
|
right: 8,
|
|
bottom: 8,
|
|
child: Material(
|
|
elevation: 6,
|
|
borderRadius: BorderRadius.circular(8),
|
|
color: Colors.black.withValues(alpha: 0.82),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(10),
|
|
child: DefaultTextStyle(
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 11,
|
|
fontFamily: 'monospace',
|
|
height: 1.35,
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
ListenableBuilder(
|
|
listenable: InputDiagnostics.instance,
|
|
builder: (context, _) {
|
|
return Text(
|
|
'${InputDiagnostics.instance.summary()}\n'
|
|
'${PenInputService.instance.debugSummary}\n'
|
|
'log: ${BadNoteLog.instance.path ?? "(starting…)"}\n'
|
|
'session: ${BadNoteLog.instance.sessionId}'
|
|
'${_lastExportPath != null ? "\nlast zip: $_lastExportPath" : ""}',
|
|
);
|
|
},
|
|
),
|
|
const SizedBox(height: 8),
|
|
Row(
|
|
children: [
|
|
TextButton(
|
|
onPressed: () => InputDiagnostics.instance.reset(),
|
|
child: const Text('Reset',
|
|
style: TextStyle(color: Colors.white70)),
|
|
),
|
|
TextButton(
|
|
onPressed: _exporting ? null : exportPack,
|
|
child: Text(
|
|
_exporting ? 'Exporting…' : 'Export pack',
|
|
style: const TextStyle(color: Colors.lightGreenAccent),
|
|
),
|
|
),
|
|
TextButton(
|
|
onPressed: toggle,
|
|
child: const Text('Hide',
|
|
style: TextStyle(color: Colors.white54)),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Compact icon button for app bars / toolbars.
|
|
class DiagnosticToggleButton extends StatelessWidget {
|
|
const DiagnosticToggleButton({
|
|
super.key,
|
|
required this.onToggle,
|
|
required this.onExport,
|
|
});
|
|
|
|
final VoidCallback onToggle;
|
|
final VoidCallback onExport;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return PopupMenuButton<String>(
|
|
tooltip: 'Diagnostics',
|
|
icon: const Icon(Icons.bug_report_outlined),
|
|
onSelected: (v) {
|
|
if (v == 'toggle') onToggle();
|
|
if (v == 'export') onExport();
|
|
},
|
|
itemBuilder: (context) => const [
|
|
PopupMenuItem(value: 'toggle', child: Text('Toggle overlay')),
|
|
PopupMenuItem(value: 'export', child: Text('Export diagnostic pack')),
|
|
],
|
|
);
|
|
}
|
|
}
|