9271d59f72
Fix windows backup recovery issues Optimize overwrite handle Optimize access control page Optimize some details
293 lines
8.1 KiB
Dart
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,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|