91 lines
2.9 KiB
Dart
91 lines
2.9 KiB
Dart
|
|
// 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).
|
||
|
|
|
||
|
|
import 'dart:async';
|
||
|
|
import 'dart:developer' as developer;
|
||
|
|
import 'dart:io';
|
||
|
|
|
||
|
|
import 'package:path_provider/path_provider.dart';
|
||
|
|
|
||
|
|
class DiagnosticLogger {
|
||
|
|
DiagnosticLogger._();
|
||
|
|
static final DiagnosticLogger instance = DiagnosticLogger._();
|
||
|
|
|
||
|
|
final List<String> _buffer = <String>[];
|
||
|
|
File? _file;
|
||
|
|
Timer? _timer;
|
||
|
|
int _epochMs = 0;
|
||
|
|
|
||
|
|
bool _active = false;
|
||
|
|
bool get isActive => _active;
|
||
|
|
|
||
|
|
/// 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.
|
||
|
|
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.
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// 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();
|
||
|
|
}
|
||
|
|
|
||
|
|
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();
|
||
|
|
}
|
||
|
|
}
|