pd_log 0.7.3
pd_log: ^0.7.3 copied to clipboard
Cross-platform Flutter logging with pure Dart; buffered file writing; no platform channels.
example/lib/main.dart
import 'package:flutter/material.dart';
import 'dart:async';
// 删除示例内文件读取工具,改用插件 API。
import 'package:flutter/services.dart';
import 'package:pd_log/pd_log.dart';
// import removed: platform interface no longer used
/// 应用入口:启动示例应用。
void main() {
runApp(const MyApp());
}
class MyApp extends StatefulWidget {
/// 构造函数:创建应用根部件。
const MyApp({super.key});
@override
/// 创建应用的状态对象。
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
String _platformVersion = 'Unknown';
bool _useConsole = false;
LogLevel _minLevel = LogLevel.verbose;
bool _showCaller = false;
bool _showTimestamp = false;
// 文件日志相关状态
bool _nativeFileEnabled = true;
final _flushIntervalCtrl = TextEditingController(text: '2000');
final _maxEntriesCtrl = TextEditingController(text: '100');
final _maxBytesCtrl = TextEditingController(text: '65536');
String? _logRootPath;
List<PDLogFile> _files = [];
int _totalSize = 0;
// 预览内容改为新页面展示
// 元数据与历史(快照)相关状态
List<LogRecord> _summary = [];
UploadState? _filterState;
String? _selectedPath;
// 文件事件订阅相关状态
final List<Map<String, dynamic>> _events = <Map<String, dynamic>>[];
StreamSubscription<Map<String, dynamic>>? _eventsSub;
bool _listening = false;
// 查询与筛选(ListOptions)相关状态
SortBy _sortBy = SortBy.time;
SortDirection _sortDirection = SortDirection.desc;
final _pageSizeCtrl = TextEditingController(text: '20');
final _pageCtrl = TextEditingController(text: '1');
final _yearCtrl = TextEditingController(text: '2025');
final _monthCtrl = TextEditingController(text: '10');
@override
/// 初始化:配置日志并拉取平台信息与初始文件列表。
void initState() {
super.initState();
// Configure logging defaults.
PDLog.configure(PDLogConfig(
defaultTag: 'Example 测试项目',
minLevel: _minLevel,
useConsole: _useConsole,
showCaller: _showCaller,
showTimestamp: _showTimestamp,
fileLoggingMinLevel: LogLevel.info,
nativeFileLoggingEnabled: _nativeFileEnabled,
nativeFileLoggingFlushIntervalMs:
int.tryParse(_flushIntervalCtrl.text) ?? 2000,
nativeFileLoggingMaxBufferEntries:
int.tryParse(_maxEntriesCtrl.text) ?? 100,
nativeFileLoggingMaxBufferBytes:
int.tryParse(_maxBytesCtrl.text) ?? 65536,
));
PDLog.i('App starting...');
initPlatformState();
_refreshRootPath();
_refreshFiles();
// 初始化年份与月份默认值
final now = DateTime.now();
_yearCtrl.text = now.year.toString();
_monthCtrl.text = now.month.toString();
}
// Platform messages are asynchronous, so we initialize in an async method.
/// 异步获取平台版本信息并更新界面状态。
Future<void> initPlatformState() async {
String platformVersion;
try {
platformVersion =
await PDLog.getPlatformVersion() ?? 'Unknown platform version';
} on PlatformException {
platformVersion = 'Failed to get platform version.';
}
if (!mounted) return;
setState(() {
_platformVersion = platformVersion;
});
}
@override
/// 构建应用界面,包括日志配置与文件列表展示。
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Plugin example app'),
),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Running on: $_platformVersion'),
const SizedBox(height: 12),
Row(
children: [
const Text('Use console logging'),
Switch(
value: _useConsole,
onChanged: (v) {
setState(() {
_useConsole = v;
PDLog.updateConfigure(
useConsole: _useConsole,
);
PDLog.d(
'Console logging: ${_useConsole ? 'ON' : 'OFF'}',
);
});
},
),
],
),
const SizedBox(height: 12),
const SizedBox(height: 12),
Row(
children: [
const Text('Show caller info'),
Switch(
value: _showCaller,
onChanged: (v) {
setState(() {
_showCaller = v;
PDLog.updateConfigure(
showCaller: _showCaller,
);
PDLog.d(
'Show caller: ${_showCaller ? 'ON' : 'OFF'}',
);
});
},
),
],
),
const SizedBox(height: 12),
Row(
children: [
const Text('Show Timestamp'),
Switch(
value: _showTimestamp,
onChanged: (v) {
setState(() {
_showTimestamp = v;
PDLog.updateConfigure(
showTimestamp: _showTimestamp,
);
PDLog.d(
'Show Timestamp: ${_showTimestamp ? 'ON' : 'OFF'}',
);
});
},
),
],
),
const SizedBox(height: 12),
Row(
children: [
const Text('Min level:'),
const SizedBox(width: 8),
DropdownButton<LogLevel>(
value: _minLevel,
onChanged: (level) {
if (level == null) return;
setState(() {
_minLevel = level;
PDLog.updateConfigure(
minLevel: _minLevel,
);
PDLog.d('Min level set to $level');
});
},
items: const [
DropdownMenuItem(
value: LogLevel.verbose,
child: Text('Verbose'),
),
DropdownMenuItem(
value: LogLevel.debug,
child: Text('Debug'),
),
DropdownMenuItem(
value: LogLevel.info,
child: Text('Info'),
),
DropdownMenuItem(
value: LogLevel.warn,
child: Text('Warn'),
),
DropdownMenuItem(
value: LogLevel.error,
child: Text('Error'),
),
],
),
],
),
const SizedBox(height: 16),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
ElevatedButton(
onPressed: () => PDLog.v('Verbose pressed'),
child: const Text('Verbose'),
),
ElevatedButton(
onPressed: () => PDLog.d('Debug pressed'),
child: const Text('Debug'),
),
ElevatedButton(
onPressed: () => PDLog.i('Info pressed'),
child: const Text('Info'),
),
ElevatedButton(
onPressed: () => PDLog.w('Warn pressed'),
child: const Text('Warn'),
),
ElevatedButton(
onPressed: () => PDLog.e('Error pressed'),
child: const Text('Error'),
),
ElevatedButton(
onPressed: () {
PDLog.out(
'自定义的错误警告输出: Customize pressed \n 测试换行后的文本样式',
tag: '自定义输出',
useConsole: false,
toFile: true,
showTimestamp: false,
style: const LogStyleConfig(
foreground: 37, // 白色文本
background: 41, // 红色背景
styles: [1, 4, 5], // 粗体, 下划线, 闪烁
),
);
},
child: const Text('Customize'),
),
ElevatedButton(
onPressed: _logFromHelper,
child: const Text('Log from helper method'),
),
ElevatedButton(
onPressed: _runStressTest,
child: const Text('压力测试(1000 条)'),
),
],
),
const Divider(height: 32),
const Text(
'文件事件订阅(创建/删除)',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: Text(
_listening ? '事件监听中(跨平台桌面,仅 Web 不支持)' : '未监听(点击开始订阅)',
),
),
ElevatedButton(
onPressed: _listening ? _stopEvents : _startEvents,
child: Text(_listening ? '停止监听' : '开始监听'),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: () async {
PDLog.i('触发事件:写一条日志以创建/滚动文件', tag: 'Events');
// 写入一条到原生日志(可能创建新文件或增加大小)
PDLog.out(
'事件演示:${DateTime.now().toIso8601String()} 来自示例',
tag: 'Events',
useConsole: false,
toFile: true,
showTimestamp: true,
);
await PDLog.flushNativeLogs();
_refreshFiles();
},
child: const Text('写一条日志'),
),
],
),
const SizedBox(height: 8),
SizedBox(
height: 180,
child: Card(
child: ListView.builder(
itemCount: _events.length,
itemBuilder: (context, index) {
final e = _events[index];
final type = e['type'];
final path = e['path'];
final size = e['sizeBytes'];
final modMs = e['modifiedMs'];
final tsMs = e['tsMs'];
return ListTile(
dense: true,
title: Text('$type ${path ?? ''}'),
subtitle: Text([
if (size != null) 'size=$size',
if (modMs != null)
'modified=${DateTime.fromMillisecondsSinceEpoch(modMs is int ? modMs : int.tryParse('$modMs') ?? 0)}',
if (tsMs != null)
'ts=${DateTime.fromMillisecondsSinceEpoch(tsMs is int ? tsMs : int.tryParse('$tsMs') ?? 0)}',
].join(' ')),
);
},
),
),
),
const Divider(height: 32),
const Text(
'Native File Logging',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Row(
children: [
const Text('Enable native file logging'),
Switch(
value: _nativeFileEnabled,
onChanged: (v) {
setState(() {
_nativeFileEnabled = v;
_applyConfig();
});
},
),
],
),
const SizedBox(height: 8),
Row(children: [
const SizedBox(
width: 140,
child: Text('Flush interval (ms)'),
),
Expanded(
child: TextField(
controller: _flushIntervalCtrl,
keyboardType: TextInputType.number,
),
),
]),
const SizedBox(height: 8),
Row(children: [
const SizedBox(
width: 140,
child: Text('Max buffer entries'),
),
Expanded(
child: TextField(
controller: _maxEntriesCtrl,
keyboardType: TextInputType.number,
),
),
]),
const SizedBox(height: 8),
Row(children: [
const SizedBox(
width: 140,
child: Text('Max buffer bytes'),
),
Expanded(
child: TextField(
controller: _maxBytesCtrl,
keyboardType: TextInputType.number,
),
),
]),
const SizedBox(height: 8),
Wrap(spacing: 8, children: [
ElevatedButton(
onPressed: _applyConfig,
child: const Text('应用配置'),
),
ElevatedButton(
onPressed: _flushNow,
child: const Text('立即刷新写盘'),
),
ElevatedButton(
onPressed: _refreshRootPath,
child: const Text('获取日志根路径'),
),
ElevatedButton(
onPressed: _printFileStructure,
child: const Text('打印文件结构'),
),
ElevatedButton(
onPressed: _printMetaFiles,
child: const Text('打印元数据文件'),
),
ElevatedButton(
onPressed: _refreshFiles,
child: const Text('列出日志文件'),
),
ElevatedButton(
onPressed: _deleteAllLogs,
child: const Text('删除全部日志'),
),
]),
const Divider(height: 32),
const Text(
'查询与筛选(ListOptions)',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Card(
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide(color: Colors.grey.shade300),
),
child: Padding(
padding: const EdgeInsets.all(12),
child: Wrap(
spacing: 12,
runSpacing: 12,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
SizedBox(
width: 150,
child: DropdownButtonFormField<SortBy>(
value: _sortBy,
decoration: const InputDecoration(
labelText: '排序字段',
border: OutlineInputBorder(),
),
onChanged: (v) =>
setState(() => _sortBy = v ?? SortBy.time),
items: const [
DropdownMenuItem(
value: SortBy.time,
child: Text('按时间'),
),
DropdownMenuItem(
value: SortBy.name,
child: Text('按名称'),
),
],
),
),
SizedBox(
width: 150,
child: DropdownButtonFormField<SortDirection>(
value: _sortDirection,
decoration: const InputDecoration(
labelText: '排序方向',
border: OutlineInputBorder(),
),
onChanged: (v) => setState(
() => _sortDirection = v ?? SortDirection.desc),
items: const [
DropdownMenuItem(
value: SortDirection.asc,
child: Text('升序'),
),
DropdownMenuItem(
value: SortDirection.desc,
child: Text('降序'),
),
],
),
),
SizedBox(
width: 150,
child: TextField(
controller: _pageSizeCtrl,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: '分页大小(可选)',
hintText: '例如 20,留空或 0 表示不分页',
border: OutlineInputBorder(),
),
),
),
SizedBox(
width: 150,
child: TextField(
controller: _pageCtrl,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: '页码',
hintText: '默认为 1',
border: OutlineInputBorder(),
),
),
),
SizedBox(
width: 150,
child: TextField(
controller: _yearCtrl,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: '年份',
border: OutlineInputBorder(),
),
),
),
SizedBox(
width: 150,
child: TextField(
controller: _monthCtrl,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
labelText: '月份',
border: OutlineInputBorder(),
),
),
),
],
),
),
),
const SizedBox(height: 8),
Wrap(spacing: 8, children: [
ElevatedButton(
onPressed: _refreshFilesWithOptions,
child: const Text('全部日志(排序/分页)'),
),
ElevatedButton(
onPressed: _queryByYear,
child: const Text('按年份查询'),
),
ElevatedButton(
onPressed: _queryByYearMonth,
child: const Text('按年月查询'),
),
]),
const SizedBox(height: 8),
Text('日志根路径: ${_logRootPath ?? '(当前平台不支持或目录未创建)'}'),
const SizedBox(height: 8),
Text('当前总日志大小: $_totalSize 字节'),
const SizedBox(height: 8),
SizedBox(
height: 240,
child: Card(
child: ListView.builder(
itemCount: _files.length,
itemBuilder: (context, index) {
final f = _files[index];
final y = f.year, m = f.month, d = f.day;
return ListTile(
title: Text(f.fileName.isNotEmpty
? '${f.fileName} (${f.path})'
: f.path),
subtitle: Text(
'size=${f.sizeBytes}B modified=${DateTime.fromMillisecondsSinceEpoch(f.modifiedMs)}\n'
'derived=${y ?? '-'}-${m ?? '-'}-${d ?? '-'}',
),
trailing: IconButton(
icon: const Icon(Icons.delete_outline),
onPressed: () => _deleteFile(f),
),
onTap: () => _readFileContent(f),
);
},
),
),
),
// 预览使用新页面
const Divider(height: 32),
const Text(
'元数据与历史(快照与上传状态)',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Card(
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide(color: Colors.grey.shade300),
),
child: Padding(
padding: const EdgeInsets.all(12),
child: Wrap(
spacing: 12,
runSpacing: 12,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
SizedBox(
width: 200,
child: DropdownButtonFormField<UploadState?>(
value: _filterState,
decoration: const InputDecoration(
labelText: '上传状态筛选',
border: OutlineInputBorder(),
),
onChanged: (v) => setState(() => _filterState = v),
items: const [
DropdownMenuItem(
value: null,
child: Text('全部'),
),
DropdownMenuItem(
value: UploadState.unknown,
child: Text('未知'),
),
DropdownMenuItem(
value: UploadState.pending,
child: Text('上传中'),
),
DropdownMenuItem(
value: UploadState.success,
child: Text('成功'),
),
DropdownMenuItem(
value: UploadState.failed,
child: Text('失败'),
),
],
),
),
ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 360),
child: DropdownButtonFormField<String>(
value: _files.any((f) => f.path == _selectedPath)
? _selectedPath
: null,
decoration: const InputDecoration(
labelText: '选择一个日志文件路径(用于上传标记)',
border: OutlineInputBorder(),
),
isExpanded: true,
onChanged: (v) => setState(() => _selectedPath = v),
selectedItemBuilder: (context) => _files
.map((f) => Align(
alignment: Alignment.centerLeft,
child: Text(
f.fileName.isNotEmpty
? '${f.fileName} (${f.path})'
: f.path,
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
))
.toList(),
items: _files
.map((f) => DropdownMenuItem(
value: f.path,
child: Text(
f.fileName.isNotEmpty
? '${f.fileName} (${f.path})'
: f.path,
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
))
.toList(),
),
),
Wrap(
spacing: 8,
children: [
ElevatedButton(
onPressed: _refreshSummary,
child: const Text('刷新快照列表'),
),
ElevatedButton(
onPressed: _markStarted,
child: const Text('标记上传开始'),
),
ElevatedButton(
onPressed: _markSuccess,
child: const Text('标记上传成功'),
),
ElevatedButton(
onPressed: _markFailed,
child: const Text('标记上传失败'),
),
],
),
],
),
),
),
const SizedBox(height: 8),
SizedBox(
height: 260,
child: Card(
child: ListView.builder(
itemCount: _summary.length,
itemBuilder: (context, index) {
final s = _summary[index];
final created =
DateTime.fromMillisecondsSinceEpoch(s.createdMs);
final modified =
DateTime.fromMillisecondsSinceEpoch(s.modifiedMs);
final deleted = s.deletedMs != null
? DateTime.fromMillisecondsSinceEpoch(s.deletedMs!)
: null;
return ListTile(
title: Text(s.path),
subtitle: Text(
'exists=${s.exists} size=${s.sizeBytes}B upload=${s.uploadState}\n'
'created=$created modified=$modified deleted=${deleted ?? '-'} attempts=${s.uploadAttempts} error=${s.lastError ?? '-'}',
),
);
},
),
),
),
],
),
),
),
),
);
}
/// 示例:从辅助方法写入一条日志。
void _logFromHelper() async {
/// json美化打印, 测试数据
final Map arg = {
'name': 'pedro',
'age': 30,
'height': 183,
'no.': '9527',
'nickName': ['大头', '狗蛋'],
1: '下标',
'active': true,
'balance': 1234.56,
'createdAt': DateTime.now(),
'tags': {'flutter', 'dart', '日志'},
'addresses': [
{'type': 'home', 'city': '上海', 'zip': 200000},
{'type': 'work', 'city': '北京', 'zip': 100000},
],
'preferences': {
'notifications': {'email': true, 'sms': false},
'theme': {'mode': 'dark', 'accentColor': '#00FFFF'},
},
'metrics': {
'cpu': [0.12, 0.34, 0.56],
'mem': {'used': 2048, 'total': 8192},
'latencyMs': [12, 25, 8, 16],
},
'mixedList': [
null,
'文本',
42,
3.1415,
{
'nested': [
'a',
'b',
{'deep': 1}
]
},
],
'mapWithNonStringKeys': {1: '一', true: '真', 3.14: 'π'},
'emoji': '🚀🔥',
'longText': '这是一个多行文本\n用于测试换行\n以及格式化效果',
'url': Uri.parse('https://example.com/api?v=1'),
'bigInt': BigInt.from(9007199254740991),
'mapDepthTest': {
'level1': {
'level2': {
'level3': {
'list': [
1,
2,
{'level4': 'ok'}
],
},
},
},
},
};
final list = [arg, arg];
PDLog.formated(list, level: LogLevel.info, toFile: true);
/// 检查指定日期的日志文件
final date = DateTime.now();
final dayPath = await PDLog.logFilePathIfExists(date);
if (dayPath.isNotEmpty) {
// 文件存在
_readFileContent(PDLogFile(
path: dayPath,
sizeBytes: 1024,
modifiedMs: 1760172725000,
));
} else {
PDLog.e('${date.toIso8601String()}日期没有日志文件.');
}
final logFiles = await PDLog.listLogFilesByYear(date.year);
PDLog.v(logFiles);
}
/// 压力测试:批量写入大量日志以验证缓冲与刷新表现。
///
/// 注意:为避免控制台性能影响,此处禁用控制台输出,仅写入原生缓冲与文件。
Future<void> _runStressTest() async {
const total = 1000;
final started = DateTime.now();
PDLog.v('压力测试开始: $total 条, started=$started', tag: 'Stress');
for (var i = 0; i < total; i++) {
final msg = '压力测试第 $i 条消息\n多行样式验证:第 ${(i % 3) + 1} 行';
// 使用自定义输出以覆盖控制台/文件开关,避免刷屏影响性能
PDLog.out(
msg,
tag: 'Stress',
useConsole: false,
toFile: false,
showTimestamp: true,
style: const LogStyleConfig(
foreground: 36, // 青色
styles: [1], // 粗体
),
);
}
await PDLog.flushNativeLogs();
final elapsed = DateTime.now().difference(started);
PDLog.v('压力测试完成,用时 ${elapsed.inMilliseconds} ms', tag: 'Stress');
_refreshFiles();
}
/// 应用当前配置到原生日志系统(刷新间隔、缓冲阈值等)。
void _applyConfig() {
final flushMs = int.tryParse(_flushIntervalCtrl.text) ?? 2000;
final maxEntries = int.tryParse(_maxEntriesCtrl.text) ?? 100;
final maxBytes = int.tryParse(_maxBytesCtrl.text) ?? 65536;
PDLog.updateConfigure(
nativeFileLoggingEnabled: _nativeFileEnabled,
nativeFileLoggingFlushIntervalMs: flushMs,
nativeFileLoggingMaxBufferEntries: maxEntries,
nativeFileLoggingMaxBufferBytes: maxBytes,
);
PDLog.i(
'Applied file logging config: enabled=$_nativeFileEnabled, interval=${flushMs}ms, entries=$maxEntries, bytes=$maxBytes',
);
}
/// 立即触发原生侧刷新,将缓冲区写入磁盘并刷新文件列表。
Future<void> _flushNow() async {
await PDLog.flushNativeLogs();
_refreshFiles();
}
/// 获取日志根路径并打印年份目录(仅一级目录)。
Future<void> _refreshRootPath() async {
final path = await PDLog.logRootPath();
setState(() => _logRootPath = path);
final years = await PDLog.listYearFolders();
for (String e in years) {
PDLog.d(e);
}
}
/// 打印插件在本地的文件结构(树状)。
Future<void> _printFileStructure() async {
final text = await PDLog.fileTreeString(maxDepth: 6);
PDLog.v(text);
}
/// 打印元数据文件(pd_log_ledger.jsonl 与 pd_log_summary.json)内容。
Future<void> _printMetaFiles() async {
PDLog.v('=== 元数据: pd_log_ledger.jsonl 开始 ===');
PDLog.v(await PDLog.metaLedgerContent());
PDLog.v('=== 元数据: pd_log_ledger.jsonl 结束 ===');
PDLog.v('=== 元数据: pd_log_summary.json 开始 ===');
PDLog.v(await PDLog.metaSummaryContent());
PDLog.v('=== 元数据: pd_log_summary.json 结束 ===');
}
/// 列出所有日志文件,计算总大小并更新界面。
Future<void> _refreshFiles() async {
final files = await PDLog.listLogFiles();
for (PDLogFile e in files) {
PDLog.d(e.toJson());
}
final total = files.fold<int>(0, (sum, f) => sum + f.sizeBytes);
setState(() {
_files = files;
_totalSize = total;
_reconcileSelectedPath();
});
}
/// 根据当前文件列表修正下拉选择的路径,避免 value 不在 items 中导致断言失败。
void _reconcileSelectedPath() {
final exists = _files.any((f) => f.path == _selectedPath);
if (!exists) {
_selectedPath = _files.isNotEmpty ? _files.first.path : null;
}
}
ListOptions _buildOptions() {
final by = _sortBy;
final dir = _sortDirection;
final ps = int.tryParse(_pageSizeCtrl.text);
final page = int.tryParse(_pageCtrl.text) ?? 1;
return ListOptions(
by: by,
direction: dir,
pageSize: (ps == null || ps <= 0) ? null : ps,
page: page <= 0 ? 1 : page,
);
}
Future<void> _refreshFilesWithOptions() async {
final options = _buildOptions();
final files = await PDLog.listLogFiles(options: options);
final total = files.fold<int>(0, (sum, f) => sum + f.sizeBytes);
setState(() {
_files = files;
_totalSize = total;
_reconcileSelectedPath();
});
}
Future<void> _queryByYear() async {
final y = int.tryParse(_yearCtrl.text);
if (y == null) {
PDLog.w('请输入有效的年份');
return;
}
final options = _buildOptions();
final files = await PDLog.listLogFilesByYear(y, options: options);
final total = files.fold<int>(0, (sum, f) => sum + f.sizeBytes);
setState(() {
_files = files;
_totalSize = total;
_reconcileSelectedPath();
});
}
Future<void> _queryByYearMonth() async {
final y = int.tryParse(_yearCtrl.text);
final m = int.tryParse(_monthCtrl.text);
if (y == null || m == null) {
PDLog.w('请输入有效的年份与月份');
return;
}
final options = _buildOptions();
final files = await PDLog.listLogFilesByYearMonth(y, m, options: options);
final total = files.fold<int>(0, (sum, f) => sum + f.sizeBytes);
setState(() {
_files = files;
_totalSize = total;
_reconcileSelectedPath();
});
}
/// 读取指定日志文件内容并打印到控制台。
Future<void> _readFileContent(PDLogFile f) async {
try {
final content = await PDLog.readFileContent(f.path);
PDLog.v('=== 文件: ${f.path} 内容开始 ===');
PDLog.v(content);
PDLog.v('=== 文件: ${f.path} 内容结束 ===');
} catch (e) {
PDLog.v('读取文件失败: $e');
}
}
/// 删除指定日志文件并刷新列表。
Future<void> _deleteFile(PDLogFile f) async {
final ok = await PDLog.deleteLogFile(f.path);
if (ok) {
_refreshFiles();
}
}
/// 删除所有日志文件并刷新列表。
Future<void> _deleteAllLogs() async {
final n = await PDLog.deleteAllLogFiles();
PDLog.w('Deleted $n log files');
_refreshFiles();
}
/// 刷新快照列表(可选按上传状态筛选)。
Future<void> _refreshSummary() async {
final options = _buildOptions();
final list =
await PDLog.listSummary(options: options, uploadState: _filterState);
setState(() => _summary = list);
}
Future<void> _markStarted() async {
final p = _selectedPath;
if (p == null || p.isEmpty) {
PDLog.w('请选择一个日志文件路径用于上传标记');
return;
}
await PDLog.markUploadStarted(p);
await _refreshSummary();
}
Future<void> _markSuccess() async {
final p = _selectedPath;
if (p == null || p.isEmpty) {
PDLog.w('请选择一个日志文件路径用于上传标记');
return;
}
await PDLog.markUploadSuccess(p);
await _refreshSummary();
}
Future<void> _markFailed() async {
final p = _selectedPath;
if (p == null || p.isEmpty) {
PDLog.w('请选择一个日志文件路径用于上传标记');
return;
}
await PDLog.markUploadFailed(p, 'network error');
await _refreshSummary();
}
// --- 文件事件订阅 ---
void _startEvents() {
try {
_events.clear();
_eventsSub?.cancel();
_eventsSub = PDLog.fileEvents().listen((e) {
setState(() {
PDLog.v('事件: $e');
_events.insert(0, e);
if (_events.length > 100) _events.removeLast();
});
}, onError: (err) {
PDLog.w('事件流错误: $err');
});
setState(() => _listening = true);
PDLog.v('开始订阅文件事件(created/deleted)');
} catch (e) {
PDLog.w('无法订阅事件: $e');
}
}
void _stopEvents() {
_eventsSub?.cancel();
_eventsSub = null;
setState(() => _listening = false);
PDLog.i('已停止事件订阅');
}
@override
void dispose() {
_eventsSub?.cancel();
super.dispose();
}
}