491fae10f5
BREAKING CHANGE: DNS and NTP override toggles are removed; picked keys apply to every profile Changelog: Per-key DNS, NTP, sniffer and TUN settings
191 lines
5.4 KiB
Dart
191 lines
5.4 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:fl_clash/common/common.dart';
|
|
import 'package:fl_clash/core/controller.dart';
|
|
import 'package:fl_clash/core/method.dart';
|
|
import 'package:fl_clash/enum/enum.dart';
|
|
import 'package:fl_clash/features/features.dart';
|
|
import 'package:fl_clash/icons/icons.dart';
|
|
import 'package:fl_clash/models/models.dart';
|
|
import 'package:fl_clash/providers/providers.dart';
|
|
import 'package:fl_clash/widgets/widgets.dart';
|
|
import 'package:material_ui/material_ui.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
class ConnectionsView extends ConsumerStatefulWidget {
|
|
final ScrollController? scrollController;
|
|
final Future<List<TrackerInfo>> Function()? connectionsReader;
|
|
|
|
const ConnectionsView({
|
|
super.key,
|
|
this.scrollController,
|
|
@visibleForTesting this.connectionsReader,
|
|
});
|
|
|
|
@override
|
|
ConsumerState<ConnectionsView> createState() => _ConnectionsViewState();
|
|
}
|
|
|
|
class _ConnectionsViewState extends ConsumerState<ConnectionsView>
|
|
with
|
|
WidgetsBindingObserver,
|
|
ActivePollingMixin<ConnectionsView>,
|
|
RouteMotionHoldMixin<ConnectionsView> {
|
|
CoreController get _core => ref.read(coreHandlerProvider);
|
|
|
|
final _listController = TrackerInfoListController();
|
|
final _speedRanker = TrackerSpeedRanker();
|
|
late final ScrollController _scrollController;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_scrollController =
|
|
widget.scrollController ?? sheetScrollController(context);
|
|
}
|
|
|
|
@override
|
|
Duration get pollInterval => const Duration(seconds: 1);
|
|
|
|
List<IconButtonData> _buildActions() {
|
|
return [
|
|
IconButtonData(
|
|
glyph: AppGlyphs.clearAll,
|
|
tooltip: context.appLocalizations.closeConnections,
|
|
onPressed: () async {
|
|
unawaited(_core.closeConnections());
|
|
await _refreshConnections();
|
|
},
|
|
),
|
|
];
|
|
}
|
|
|
|
@override
|
|
Future<void> poll(PollGuard isCurrent) async {
|
|
final trackerInfos = await _readConnections();
|
|
if (!isCurrent()) {
|
|
return;
|
|
}
|
|
updateWhenRouteSettled(() => _applyConnections(trackerInfos));
|
|
}
|
|
|
|
Future<void> _refreshConnections() async {
|
|
final trackerInfos = await _readConnections();
|
|
if (!mounted) {
|
|
return;
|
|
}
|
|
_applyConnections(trackerInfos);
|
|
}
|
|
|
|
Future<List<TrackerInfo>?> _readConnections() async {
|
|
try {
|
|
final connectionsReader = widget.connectionsReader;
|
|
return connectionsReader != null
|
|
? await connectionsReader()
|
|
: await _core.getConnections();
|
|
} catch (error) {
|
|
commonPrint.log(
|
|
'updateConnections error: $error',
|
|
logLevel: coreFailureLogLevel(error),
|
|
);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
void _applyConnections(List<TrackerInfo>? trackerInfos) {
|
|
if (trackerInfos == null) {
|
|
return;
|
|
}
|
|
_listController.setTrackerInfos(
|
|
_speedRanker.rank(trackerInfos, DateTime.now()),
|
|
);
|
|
}
|
|
|
|
Future<void> _handleBlockConnection(String id) async {
|
|
await _core.closeConnection(id);
|
|
await _refreshConnections();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_listController.dispose();
|
|
if (widget.scrollController == null) {
|
|
_scrollController.dispose();
|
|
}
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final appLocalizations = context.appLocalizations;
|
|
return CommonScaffold(
|
|
title: PageLabel.connections.label,
|
|
onKeywordsUpdate: _listController.updateKeywords,
|
|
searchState: AppBarSearchState(onSearch: _listController.search),
|
|
iconActions: _buildActions(),
|
|
body: ValueListenableBuilder<TrackerInfosState>(
|
|
valueListenable: _listController,
|
|
builder: (context, state, _) {
|
|
final connections = state.list;
|
|
return NullStatusSwitcher(
|
|
isEmpty: connections.isEmpty,
|
|
isSearching: state.isSearching,
|
|
holdsArrival: true,
|
|
nullStatus: NullStatus(
|
|
label: appLocalizations.nullTip(appLocalizations.connections),
|
|
illustration: NullStatusIllustration.connections,
|
|
),
|
|
child: TrackerInfoAnimatedList(
|
|
controller: _scrollController,
|
|
padding: EdgeInsets.only(
|
|
top: context.contentTopPadding,
|
|
bottom: 16 + BottomInsetScope.of(context),
|
|
),
|
|
trackerInfos: connections,
|
|
detailTitle: appLocalizations.details(
|
|
appLocalizations.connection,
|
|
),
|
|
actionBuilder: (trackerInfo) => _BlockConnectionButton(
|
|
onPressed: () {
|
|
_handleBlockConnection(trackerInfo.id);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
// One per live row: an IconButton would add a theme animation to each.
|
|
class _BlockConnectionButton extends StatelessWidget {
|
|
final VoidCallback onPressed;
|
|
|
|
const _BlockConnectionButton({required this.onPressed});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Tooltip(
|
|
message: context.appLocalizations.blockConnection,
|
|
child: Semantics(
|
|
button: true,
|
|
child: InkResponse(
|
|
onTap: onPressed,
|
|
radius: 14,
|
|
child: SizedBox.square(
|
|
dimension: 28,
|
|
child: Center(
|
|
child: GlyphIcon(
|
|
AppGlyphs.block,
|
|
size: 16,
|
|
color: context.colorScheme.onSurfaceVariant,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|