Add linux deb dependencies Add backup recovery strategy select Support custom text scaling Optimize the display of different text scale Optimize windows setup experience Optimize startTun performance Optimize android tv experience Optimize default option Optimize computed text size Optimize hyperOS freeform window Add developer mode Update core Optimize more details
314 lines
8.6 KiB
Dart
314 lines
8.6 KiB
Dart
import 'dart:math';
|
|
|
|
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 LogsFragment extends ConsumerStatefulWidget {
|
|
const LogsFragment({super.key});
|
|
|
|
@override
|
|
ConsumerState<LogsFragment> createState() => _LogsFragmentState();
|
|
}
|
|
|
|
class _LogsFragmentState extends ConsumerState<LogsFragment> with PageMixin {
|
|
final _logsStateNotifier = ValueNotifier<LogsState>(
|
|
LogsState(loading: true),
|
|
);
|
|
late ScrollController _scrollController;
|
|
|
|
double _currentMaxWidth = 0;
|
|
final _tag = CacheTag.rules;
|
|
bool _isLoad = false;
|
|
|
|
List<Log> _logs = [];
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
final position = globalState.cacheScrollPosition[_tag] ?? -1;
|
|
_scrollController = ScrollController(
|
|
initialScrollOffset: position > 0 ? position : double.maxFinite,
|
|
);
|
|
_logs = globalState.appState.logs.list;
|
|
_logsStateNotifier.value = _logsStateNotifier.value.copyWith(
|
|
logs: _logs,
|
|
);
|
|
ref.listenManual(
|
|
isCurrentPageProvider(
|
|
PageLabel.logs,
|
|
handler: (pageLabel, viewMode) =>
|
|
pageLabel == PageLabel.tools && viewMode == ViewMode.mobile,
|
|
),
|
|
(prev, next) {
|
|
if (prev != next && next == true) {
|
|
initPageState();
|
|
}
|
|
},
|
|
fireImmediately: true,
|
|
);
|
|
ref.listenManual(
|
|
logsProvider.select((state) => state.list),
|
|
(prev, next) {
|
|
if (prev != next) {
|
|
final isEquality = logListEquality.equals(prev, next);
|
|
if (!isEquality) {
|
|
_logs = next;
|
|
updateLogsThrottler();
|
|
}
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
@override
|
|
List<Widget> get actions => [
|
|
IconButton(
|
|
onPressed: () {
|
|
_handleExport();
|
|
},
|
|
icon: const Icon(
|
|
Icons.file_download_outlined,
|
|
),
|
|
),
|
|
];
|
|
|
|
@override
|
|
get onSearch => (value) {
|
|
_logsStateNotifier.value = _logsStateNotifier.value.copyWith(
|
|
query: value,
|
|
);
|
|
};
|
|
|
|
@override
|
|
get onKeywordsUpdate => (keywords) {
|
|
_logsStateNotifier.value =
|
|
_logsStateNotifier.value.copyWith(keywords: keywords);
|
|
};
|
|
|
|
@override
|
|
void dispose() {
|
|
_logsStateNotifier.dispose();
|
|
_scrollController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
_handleExport() async {
|
|
final commonScaffoldState = context.commonScaffoldState;
|
|
final res = await commonScaffoldState?.loadingRun<bool>(
|
|
() async {
|
|
return await globalState.appController.exportLogs();
|
|
},
|
|
title: appLocalizations.exportLogs,
|
|
);
|
|
if (res != true) return;
|
|
globalState.showMessage(
|
|
title: appLocalizations.tip,
|
|
message: TextSpan(text: appLocalizations.exportSuccess),
|
|
);
|
|
}
|
|
|
|
double _getItemHeight(Log log) {
|
|
final measure = globalState.measure;
|
|
final bodySmallHeight = measure.bodySmallHeight;
|
|
final bodyMediumHeight = measure.bodyMediumHeight;
|
|
final height = globalState.measure
|
|
.computeTextSize(
|
|
Text(
|
|
log.payload,
|
|
style: context.textTheme.bodyLarge,
|
|
),
|
|
maxWidth: _currentMaxWidth,
|
|
)
|
|
.height;
|
|
return height + bodySmallHeight + 8 + bodyMediumHeight + 40 + 8;
|
|
}
|
|
|
|
updateLogsThrottler() {
|
|
throttler.call("logs", () {
|
|
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);
|
|
}
|
|
|
|
_preLoad() {
|
|
if (_isLoad == true) {
|
|
return;
|
|
}
|
|
_isLoad = true;
|
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
|
if (!mounted) {
|
|
return;
|
|
}
|
|
final isMobileView = ref.read(isMobileViewProvider);
|
|
if (isMobileView) {
|
|
await Future.delayed(Duration(milliseconds: 300));
|
|
}
|
|
final parts = _logs.batch(10);
|
|
globalState.cacheHeightMap[_tag] ??= FixedMap(
|
|
_logs.length,
|
|
);
|
|
for (int i = 0; i < parts.length; i++) {
|
|
final part = parts[i];
|
|
await Future(
|
|
() {
|
|
for (final log in part) {
|
|
globalState.cacheHeightMap[_tag]?.updateCacheValue(
|
|
log.payload,
|
|
() => _getItemHeight(log),
|
|
);
|
|
}
|
|
},
|
|
);
|
|
}
|
|
_logsStateNotifier.value = _logsStateNotifier.value.copyWith(
|
|
loading: false,
|
|
);
|
|
});
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return LayoutBuilder(
|
|
builder: (_, constraints) {
|
|
_currentMaxWidth = constraints.maxWidth - 40;
|
|
return ValueListenableBuilder<LogsState>(
|
|
valueListenable: _logsStateNotifier,
|
|
builder: (_, state, __) {
|
|
_preLoad();
|
|
final logs = state.list;
|
|
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();
|
|
final content = logs.isEmpty
|
|
? NullStatus(
|
|
label: appLocalizations.nullLogsDesc,
|
|
)
|
|
: Align(
|
|
alignment: Alignment.topCenter,
|
|
child: CommonScrollBar(
|
|
controller: _scrollController,
|
|
child: ScrollToEndBox(
|
|
controller: _scrollController,
|
|
tag: _tag,
|
|
dataSource: logs,
|
|
child: CacheItemExtentListView(
|
|
tag: _tag,
|
|
reverse: true,
|
|
shrinkWrap: true,
|
|
physics: NextClampingScrollPhysics(),
|
|
controller: _scrollController,
|
|
itemBuilder: (_, index) {
|
|
return items[index];
|
|
},
|
|
itemExtentBuilder: (index) {
|
|
if (index.isOdd) {
|
|
return 0;
|
|
}
|
|
return _getItemHeight(logs[index ~/ 2]);
|
|
},
|
|
itemCount: items.length,
|
|
keyBuilder: (int index) {
|
|
if (index.isOdd) {
|
|
return "divider";
|
|
}
|
|
return logs[index ~/ 2].payload;
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
return FadeBox(
|
|
child: state.loading
|
|
? Center(
|
|
child: CircularProgressIndicator(),
|
|
)
|
|
: content,
|
|
);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
class LogItem extends StatelessWidget {
|
|
final Log log;
|
|
final Function(String)? onClick;
|
|
|
|
const LogItem({
|
|
super.key,
|
|
required this.log,
|
|
this.onClick,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return ListItem(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 16,
|
|
vertical: 4,
|
|
),
|
|
title: SelectableText(
|
|
log.payload,
|
|
style: context.textTheme.bodyLarge,
|
|
),
|
|
subtitle: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
SelectableText(
|
|
log.dateTime,
|
|
style: context.textTheme.bodySmall?.copyWith(
|
|
color: context.colorScheme.primary,
|
|
),
|
|
),
|
|
const SizedBox(
|
|
height: 8,
|
|
),
|
|
Container(
|
|
alignment: Alignment.centerLeft,
|
|
child: CommonChip(
|
|
onPressed: () {
|
|
if (onClick == null) return;
|
|
onClick!(log.logLevel.name);
|
|
},
|
|
label: log.logLevel.name,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|