Files
FlClash/lib/views/logs.dart
T
chen08209 9271d59f72 Fix windows tun issues
Fix windows backup recovery issues

Optimize overwrite handle

Optimize access control page

Optimize some details
2025-11-28 17:35:27 +08:00

293 lines
8.1 KiB
Dart

import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/providers/providers.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../models/models.dart';
import '../widgets/widgets.dart';
class LogsView extends ConsumerStatefulWidget {
const LogsView({super.key});
@override
ConsumerState<LogsView> createState() => _LogsViewState();
}
class _LogsViewState extends ConsumerState<LogsView> {
final _logsStateNotifier = ValueNotifier<LogsState>(LogsState());
late ScrollController _scrollController;
List<Log> _logs = [];
@override
void initState() {
super.initState();
_logs = globalState.appState.logs.list;
_scrollController = ScrollController(
initialScrollOffset: _logs.length * LogItem.height,
);
_logsStateNotifier.value = _logsStateNotifier.value.copyWith(logs: _logs);
ref.listenManual(logsProvider.select((state) => state.list), (prev, next) {
if (prev != next) {
final isEquality = logListEquality.equals(prev, next);
if (!isEquality) {
_logs = next;
updateLogsThrottler();
}
}
});
}
List<Widget> _buildActions() {
return [
ValueListenableBuilder(
valueListenable: _logsStateNotifier,
builder: (_, state, _) {
return IconButton(
style: state.autoScrollToEnd
? IconButton.styleFrom(
backgroundColor: context.colorScheme.secondaryContainer,
foregroundColor: context.colorScheme.onSecondaryContainer,
)
: null,
onPressed: () {
_logsStateNotifier.value = _logsStateNotifier.value.copyWith(
autoScrollToEnd: !_logsStateNotifier.value.autoScrollToEnd,
);
},
icon: const Icon(Icons.vertical_align_top_outlined),
);
},
),
IconButton(
onPressed: () {
_handleExport();
},
icon: const Icon(Icons.save_as_outlined),
),
];
}
void _onSearch(String value) {
_logsStateNotifier.value = _logsStateNotifier.value.copyWith(query: value);
}
void _onKeywordsUpdate(List<String> keywords) {
_logsStateNotifier.value = _logsStateNotifier.value.copyWith(
keywords: keywords,
);
}
@override
void dispose() {
_logsStateNotifier.dispose();
_scrollController.dispose();
super.dispose();
}
Future<void> _handleExport() async {
final res = await globalState.appController.safeRun<bool>(
() async {
return await globalState.appController.exportLogs();
},
needLoading: true,
title: appLocalizations.exportLogs,
);
if (res != true) return;
globalState.showMessage(
title: appLocalizations.tip,
message: TextSpan(text: appLocalizations.exportSuccess),
);
}
void updateLogsThrottler() {
throttler.call(FunctionTag.logs, () {
if (!mounted) {
return;
}
final isEquality = logListEquality.equals(
_logs,
_logsStateNotifier.value.logs,
);
if (isEquality) {
return;
}
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
_logsStateNotifier.value = _logsStateNotifier.value.copyWith(
logs: _logs,
);
}
});
}, duration: commonDuration);
}
@override
Widget build(BuildContext context) {
return CommonScaffold(
actions: _buildActions(),
onKeywordsUpdate: _onKeywordsUpdate,
searchState: AppBarSearchState(onSearch: _onSearch),
title: appLocalizations.logs,
body: ValueListenableBuilder<LogsState>(
valueListenable: _logsStateNotifier,
builder: (context, state, _) {
final logs = state.list;
if (logs.isEmpty) {
return NullStatus(
illustration: LogEmptyIllustration(),
label: appLocalizations.nullTip(appLocalizations.logs),
);
}
final items = logs
.map<Widget>(
(log) => LogItem(
key: Key(log.dateTime),
log: log,
onClick: (value) {
context.commonScaffoldState?.addKeyword(value);
},
),
)
.separated(const Divider(height: 0))
.toList();
return Align(
alignment: Alignment.topCenter,
child: ScrollToEndBox(
onCancelToEnd: () {
_logsStateNotifier.value = _logsStateNotifier.value.copyWith(
autoScrollToEnd: false,
);
},
controller: _scrollController,
enable: state.autoScrollToEnd,
dataSource: logs,
child: CommonScrollBar(
controller: _scrollController,
child: ListView.builder(
physics: NextClampingScrollPhysics(),
reverse: true,
shrinkWrap: true,
controller: _scrollController,
itemBuilder: (_, index) {
return items[index];
},
itemExtentBuilder: (index, _) {
if (index.isOdd) {
return 0;
}
return LogItem.height;
},
itemCount: items.length,
),
),
),
);
},
),
);
}
}
class LogItem extends StatelessWidget {
final Log log;
final Function(String)? onClick;
static double get height {
final measure = globalState.measure;
return measure.bodyLargeHeight * 2 +
8 +
24 +
globalState.measure.labelMediumHeight +
16 +
20;
}
const LogItem({super.key, required this.log, this.onClick});
@override
Widget build(BuildContext context) {
return ListItem(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
onTap: () {
globalState.showCommonDialog(child: LogDetailDialog(log: log));
},
title: SizedBox(
height: globalState.measure.bodyLargeHeight * 2,
child: Text(
log.payload,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: context.textTheme.bodyLarge?.copyWith(
color: log.logLevel.color,
),
),
),
subtitle: Column(
children: [
SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
CommonChip(
onPressed: () {
if (onClick == null) return;
onClick!(log.logLevel.name);
},
label: log.logLevel.name,
),
Text(
log.dateTime,
style: context.textTheme.bodySmall?.copyWith(
color: context.colorScheme.onSurface.opacity80,
),
),
],
),
],
),
);
}
}
class LogDetailDialog extends StatelessWidget {
final Log log;
const LogDetailDialog({super.key, required this.log});
@override
Widget build(BuildContext context) {
return CommonDialog(
title: appLocalizations.details(appLocalizations.log),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop(true);
},
child: Text(appLocalizations.confirm),
),
],
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: 6,
children: [
SelectableText(
log.payload,
style: context.textTheme.bodyLarge?.copyWith(
color: log.logLevel.color,
),
),
SelectableText(
log.dateTime,
style: context.textTheme.bodySmall?.copyWith(
color: context.colorScheme.onSurfaceVariant,
),
),
],
),
);
}
}