feat: unified shell, diagnostics pack, native Office, sticky board
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:
2026-08-05 17:55:27 +08:00
parent 3cabc7e074
commit d346cc2670
49 changed files with 2883 additions and 2515 deletions

View File

@@ -1,90 +1,38 @@
// lib/editor/input/diagnostic_logger.dart
//
// On-device input diagnostics. When the user manually enables the diagnostic
// (the toolbar toggle), every native pen event (raw button/flag/tilt fields)
// and every zoom frame is emitted through the standard `dart:developer` log
// channel (name 'badnote.input') — capturable via `flutter run`, DevTools, or
// any log tool — AND mirrored to a text file as a fallback for the packaged
// GUI build, which has no attached console. Disabled by default (no overhead).
// Compatibility facade over [BadNoteLog] + [PenEventRing]. The PDF editor's
// toolbar toggle still calls start/stop; globally, [BadNoteLog.start] runs at
// app launch so packaged builds always have a session file.
import 'dart:async';
import 'dart:developer' as developer;
import 'dart:io';
import 'package:path_provider/path_provider.dart';
import '../../diagnostics/badnote_log.dart';
class DiagnosticLogger {
DiagnosticLogger._();
static final DiagnosticLogger instance = DiagnosticLogger._();
final List<String> _buffer = <String>[];
File? _file;
Timer? _timer;
int _epochMs = 0;
bool _verbose = false;
bool get isActive => _verbose || BadNoteLog.instance.path != null;
bool _active = false;
bool get isActive => _active;
/// Absolute path of the structured session log (preferred), else null.
String? get path => BadNoteLog.instance.path;
/// Absolute path of the current log file (shown in the overlay), or null.
String? path;
/// Begin a session. Enables the `dart:developer` log channel immediately and
/// opens the fallback file (best-effort). Safe to call repeatedly.
/// Begin a verbose input session (also ensures the global log is running).
Future<void> start() async {
if (_active) return;
_active = true; // developer.log works even if the file can't be opened
_epochMs = DateTime.now().millisecondsSinceEpoch;
developer.log('--- session start ${DateTime.now().toIso8601String()} ---',
name: 'badnote.input');
try {
Directory dir;
try {
dir = await getApplicationDocumentsDirectory();
} catch (_) {
dir = await getTemporaryDirectory();
}
final file = File('${dir.path}${Platform.pathSeparator}badnote_input_log.txt');
await file.writeAsString(
'# BadNote input diagnostic log\n'
'# started ${DateTime.now().toIso8601String()}\n'
'# columns: <ms> <kind> <fields...>\n',
flush: true,
);
_file = file;
path = file.path;
_buffer.clear();
_timer = Timer.periodic(const Duration(seconds: 1), (_) => _flush());
} catch (_) {
// File is a fallback; never break the app over it.
}
_verbose = true;
await BadNoteLog.instance.start();
BadNoteLog.instance.info(LogSubsystem.diag, 'verbose_input_on');
}
/// Emit one diagnostic line through the standard log channel and the file.
void log(String line) {
if (!_active) return;
developer.log(line, name: 'badnote.input');
if (_file == null) return;
final t = DateTime.now().millisecondsSinceEpoch - _epochMs;
_buffer.add('$t $line');
if (_buffer.length >= 1000) _flush();
BadNoteLog.instance.debug(LogSubsystem.penNative, line);
}
Future<void> _flush() async {
final file = _file;
if (file == null || _buffer.isEmpty) return;
final chunk = '${_buffer.join('\n')}\n';
_buffer.clear();
try {
await file.writeAsString(chunk, mode: FileMode.append, flush: true);
} catch (_) {}
}
/// Flush and stop. The file remains on disk for retrieval.
Future<void> stop() async {
if (!_active) return;
_active = false;
_timer?.cancel();
_timer = null;
await _flush();
if (!_verbose) return;
_verbose = false;
BadNoteLog.instance.info(LogSubsystem.diag, 'verbose_input_off');
await BadNoteLog.instance.flush();
}
}

View File

@@ -23,6 +23,8 @@ import 'dart:async';
import 'package:flutter/services.dart';
import '../../diagnostics/badnote_log.dart';
import '../../diagnostics/pen_event_ring.dart';
import 'diagnostic_logger.dart';
/// Latest hardware pen state delivered by the native observer.
@@ -169,6 +171,29 @@ class PenInputService {
final key = '$rawPtr,$rawPen,$rawMask,$btnChange,${_current.tiltX},${_current.tiltY}';
if (key != _lastPenLogKey) {
_lastPenLogKey = key;
PenEventRing.instance.recordHardware(
barrel: _current.barrel,
eraser: _current.eraser,
inverted: _current.inverted,
tiltX: _current.tiltX,
tiltY: _current.tiltY,
);
BadNoteLog.instance.debug(
LogSubsystem.penNative,
'pen_hw',
fields: {
'ptrFlags': '0x${rawPtr.toRadixString(16)}',
'penFlags': '0x${rawPen.toRadixString(16)}',
'mask': '0x${rawMask.toRadixString(16)}',
'btnChg': btnChange,
'tiltX': _current.tiltX,
'tiltY': _current.tiltY,
'resolved': '0x${flags.toRadixString(16)}',
'barrel': _current.barrel,
'eraser': _current.eraser,
'inverted': _current.inverted,
},
);
DiagnosticLogger.instance.log(
'PEN ptrFlags=0x${rawPtr.toRadixString(16)} '
'penFlags=0x${rawPen.toRadixString(16)} '