feat: unified shell, diagnostics pack, native Office, sticky board
All checks were successful
CI / Windows build (push) Successful in 14m22s
All checks were successful
CI / Windows build (push) Successful in 14m22s
Make Surface remote debugging and classroom workflows viable: always-on structured logs with one-click zip export, a single AppShell chrome, OOXML PPTX/DOCX annotation without LibreOffice, and a first-class sticky board. Also drop spike/legacy ink widgets and tighten pen feel (predictor, PenInfoHistory, page-tile layer). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
174
lib/diagnostics/diagnostic_chrome.dart
Normal file
174
lib/diagnostics/diagnostic_chrome.dart
Normal file
@@ -0,0 +1,174 @@
|
||||
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';
|
||||
|
||||
/// 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(
|
||||
'诊断包已导出 (${result.bytes} bytes)\n路径已复制到剪贴板',
|
||||
),
|
||||
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('导出失败: $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')),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user