diff --git a/lib/common/datetime.dart b/lib/common/datetime.dart index 7175436..d514320 100644 --- a/lib/common/datetime.dart +++ b/lib/common/datetime.dart @@ -35,4 +35,8 @@ extension DateTimeExtension on DateTime { } return appLocalizations.just; } -} \ No newline at end of file + + String get show { + return toIso8601String().substring(0, 10); + } +} diff --git a/lib/common/measure.dart b/lib/common/measure.dart index cdd08c1..3f60606 100644 --- a/lib/common/measure.dart +++ b/lib/common/measure.dart @@ -23,6 +23,7 @@ class Measure { double? _bodyMediumHeight; double? _bodySmallHeight; double? _labelSmallHeight; + double? _labelMediumHeight; double? _titleLargeHeight; double? _titleMediumHeight; @@ -56,6 +57,16 @@ class Measure { return _labelSmallHeight!; } + double get labelMediumHeight { + _labelMediumHeight ??= computeTextSize( + Text( + "", + style: context.textTheme.labelMedium, + ), + ).height; + return _labelMediumHeight!; + } + double get titleLargeHeight { _titleLargeHeight ??= computeTextSize( Text( diff --git a/lib/common/request.dart b/lib/common/request.dart index 552166f..b109d67 100644 --- a/lib/common/request.dart +++ b/lib/common/request.dart @@ -14,9 +14,6 @@ class Request { Request() { _dio = Dio( BaseOptions( - connectTimeout: httpTimeoutDuration, - sendTimeout: httpTimeoutDuration, - receiveTimeout: httpTimeoutDuration, headers: {"User-Agent": coreName}, ), ); @@ -37,7 +34,7 @@ class Request { _dio.httpClientAdapter = IOHttpClientAdapter( createHttpClient: () { final client = HttpClient(); - if(!_isStart) return client; + if (!_isStart) return client; client.findProxy = (url) { return "PROXY localhost:$_port;DIRECT"; }; @@ -56,7 +53,7 @@ class Request { ), ) .timeout( - httpTimeoutDuration, + httpTimeoutDuration * 2, ); return response; } @@ -86,12 +83,14 @@ class Request { "https://ipinfo.io/json/": IpInfo.fromIpInfoIoJson, }; - Future checkIp() async { + Future checkIp(CancelToken? cancelToken) async { for (final source in _ipInfoSources.entries) { try { - final response = await _dio.get>( - source.key, - ); + final response = await _dio + .get>(source.key, cancelToken: cancelToken) + .timeout( + httpTimeoutDuration, + ); if (response.statusCode == 200 && response.data != null) { return source.value(response.data!); } @@ -103,4 +102,4 @@ class Request { } } -final request = Request(); \ No newline at end of file +final request = Request(); diff --git a/lib/common/text.dart b/lib/common/text.dart index 78b011b..797b496 100644 --- a/lib/common/text.dart +++ b/lib/common/text.dart @@ -6,6 +6,11 @@ extension TextStyleExtension on TextStyle { return copyWith(color: color?.toLight()); } + toLighter() { + return copyWith(color: color?.toLighter()); + } + + toSoftBold() { return copyWith(fontWeight: FontWeight.w500); } diff --git a/lib/controller.dart b/lib/controller.dart index 3feaa84..6531a2d 100644 --- a/lib/controller.dart +++ b/lib/controller.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/state.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -135,15 +136,24 @@ class AppController { autoUpdateProfiles() async { for (final profile in config.profiles) { - if (!profile.autoUpdate) return; + if (!profile.autoUpdate) continue; final isNotNeedUpdate = profile.lastUpdateDate ?.add( profile.autoUpdateDuration, ) .isBeforeNow; - if (isNotNeedUpdate == false || - profile.url == null || - profile.url!.isEmpty) continue; + if (isNotNeedUpdate == false || profile.type == ProfileType.file) { + continue; + } + await updateProfile(profile.id); + } + } + + updateProfiles() async { + for (final profile in config.profiles) { + if (profile.type == ProfileType.file) { + continue; + } await updateProfile(profile.id); } } diff --git a/lib/fragments/dashboard/network_detection.dart b/lib/fragments/dashboard/network_detection.dart index 76a5268..dc9f3b2 100644 --- a/lib/fragments/dashboard/network_detection.dart +++ b/lib/fragments/dashboard/network_detection.dart @@ -1,4 +1,5 @@ import 'package:country_flags/country_flags.dart'; +import 'package:dio/dio.dart'; import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/state.dart'; @@ -16,20 +17,28 @@ class NetworkDetection extends StatefulWidget { class _NetworkDetectionState extends State { final ipInfoNotifier = ValueNotifier(null); final timeoutNotifier = ValueNotifier(false); + bool? _preIsStart; + CancelToken? cancelToken; _checkIp( bool isInit, + bool isStart, ) async { if (!isInit) return; + if (_preIsStart == false && _preIsStart == isStart) return; + if (cancelToken != null) { + cancelToken!.cancel(); + cancelToken = null; + } ipInfoNotifier.value = null; - final ipInfo = await request.checkIp(); + final ipInfo = await request.checkIp(cancelToken); if (ipInfo == null) { timeoutNotifier.value = true; return; } else { timeoutNotifier.value = false; } - ipInfoNotifier.value = await request.checkIp(); + ipInfoNotifier.value = ipInfo; } _checkIpContainer(Widget child) { @@ -42,9 +51,7 @@ class _NetworkDetectionState extends State { ); }, builder: (_, state, __) { - _checkIp( - state.isInit, - ); + _checkIp(state.isInit, state.isStart); return child; }, child: child, @@ -82,15 +89,30 @@ class _NetworkDetectionState extends State { width: 24, height: 24, ) - : TooltipText( - text: Text( - appLocalizations.checking, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: Theme.of(context) - .textTheme - .titleMedium, - ), + : ValueListenableBuilder( + valueListenable: timeoutNotifier, + builder: (_, timeout, __) { + if (timeout) { + return Text( + appLocalizations.checkError, + style: Theme.of(context) + .textTheme + .titleMedium, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ); + } + return TooltipText( + text: Text( + appLocalizations.checking, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context) + .textTheme + .titleMedium, + ), + ); + }, ), ), ), @@ -126,17 +148,21 @@ class _NetworkDetectionState extends State { : ValueListenableBuilder( valueListenable: timeoutNotifier, builder: (_, timeout, __) { - if(timeout){ + if (timeout) { return Text( - appLocalizations.ipCheckError, - style: context.textTheme.bodyMedium + appLocalizations.ipCheckTimeout, + style: context.textTheme.titleLarge ?.toSoftBold(), maxLines: 1, overflow: TextOverflow.ellipsis, ); } - return const SizedBox( - child: CircularProgressIndicator(), + return Container( + padding: const EdgeInsets.all(2), + child: const AspectRatio( + aspectRatio: 1, + child: CircularProgressIndicator(), + ), ); }, ), diff --git a/lib/fragments/logs.dart b/lib/fragments/logs.dart index 480eeac..ef5b736 100644 --- a/lib/fragments/logs.dart +++ b/lib/fragments/logs.dart @@ -56,6 +56,9 @@ class _LogsFragmentState extends State { ); }, icon: const Icon(Icons.search), + ), + const SizedBox( + width: 8, ) ]; }); @@ -139,6 +142,9 @@ class LogsSearchDelegate extends SearchDelegate { }, icon: const Icon(Icons.clear), ), + const SizedBox( + width: 8, + ) ]; } diff --git a/lib/fragments/profiles/profiles.dart b/lib/fragments/profiles/profiles.dart index 95c9d16..6a9d467 100644 --- a/lib/fragments/profiles/profiles.dart +++ b/lib/fragments/profiles/profiles.dart @@ -17,14 +17,9 @@ enum ProfileActions { delete, } -class ProfilesFragment extends StatefulWidget { +class ProfilesFragment extends StatelessWidget { const ProfilesFragment({super.key}); - @override - State createState() => _ProfilesFragmentState(); -} - -class _ProfilesFragmentState extends State { _handleShowAddExtendPage() { showExtendPage( globalState.navigatorKey.currentState!.context, @@ -35,30 +30,6 @@ class _ProfilesFragmentState extends State { ); } - _buildGrid({ - required ProfilesSelectorState state, - int crossAxisCount = 1, - }) { - return SingleChildScrollView( - padding: crossAxisCount > 1 - ? const EdgeInsets.symmetric(horizontal: 16) - : EdgeInsets.zero, - child: Grid.baseGap( - crossAxisCount: crossAxisCount, - children: [ - for (final profile in state.profiles) - GridItem( - child: ProfileItem( - profile: profile, - groupValue: state.currentProfileId, - onChanged: globalState.appController.changeProfile, - ), - ), - ], - ), - ); - } - _getColumns(ViewMode viewMode) { switch (viewMode) { case ViewMode.mobile: @@ -70,17 +41,47 @@ class _ProfilesFragmentState extends State { } } - @override - Widget build(BuildContext context) { - return FloatLayout( - floatingWidget: Container( - margin: const EdgeInsets.all(kFloatingActionButtonMargin), - child: FloatingActionButton( + _initScaffoldState(BuildContext context) { + WidgetsBinding.instance.addPostFrameCallback( + (_) { + final commonScaffoldState = + context.findAncestorStateOfType(); + commonScaffoldState?.actions = [ + IconButton( + onPressed: () { + commonScaffoldState.loadingRun( + () async { + await globalState.appController.updateProfiles(); + }, + ); + }, + icon: const Icon(Icons.download), + ), + const SizedBox( + width: 8, + ) + ]; + commonScaffoldState?.floatingActionButton = FloatingActionButton( heroTag: null, onPressed: _handleShowAddExtendPage, - child: const Icon(Icons.add), - ), - ), + child: const Icon( + Icons.add, + ), + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + return Selector( + selector: (_, appState) => appState.currentLabel == 'profiles', + builder: (_, isCurrent, child) { + if (isCurrent) { + _initScaffoldState(context); + } + return child!; + }, child: Selector2( selector: (_, appState, config) => ProfilesSelectorState( profiles: config.profiles, @@ -93,11 +94,32 @@ class _ProfilesFragmentState extends State { label: appLocalizations.nullProfileDesc, ); } + final columns = _getColumns(state.viewMode); + final isMobile = state.viewMode == ViewMode.mobile; return Align( alignment: Alignment.topCenter, - child: _buildGrid( - state: state, - crossAxisCount: _getColumns(state.viewMode), + child: SingleChildScrollView( + padding: !isMobile + ? const EdgeInsets.symmetric( + horizontal: 16, + vertical: 16, + ) + : EdgeInsets.zero, + child: Grid( + mainAxisSpacing: isMobile ? 8 : 16, + crossAxisSpacing: 16, + crossAxisCount: columns, + children: [ + for (final profile in state.profiles) + GridItem( + child: ProfileItem( + profile: profile, + groupValue: state.currentProfileId, + onChanged: globalState.appController.changeProfile, + ), + ), + ], + ), ), ); }, @@ -123,15 +145,18 @@ class ProfileItem extends StatefulWidget { } class _ProfileItemState extends State { + final isUpdating = ValueNotifier(false); _handleDeleteProfile(String id) async { globalState.appController.deleteProfile(id); } _handleUpdateProfile(String id) async { - context.findAncestorStateOfType()?.loadingRun( - () => globalState.appController.updateProfile(id), - ); + isUpdating.value = true; + await globalState.safeRun(() async { + await globalState.appController.updateProfile(id); + }); + isUpdating.value = false; } _handleShowEditExtendPage( @@ -147,115 +172,144 @@ class _ProfileItemState extends State { ); } - @override - Widget build(BuildContext context) { - final profile = widget.profile; - final groupValue = widget.groupValue; - final onChanged = widget.onChanged; - String useShow; - String totalShow; - double progress; - final userInfo = profile.userInfo; - if (userInfo == null) { - useShow = "Infinite"; - totalShow = "Infinite"; - progress = 1; - } else { - final use = userInfo.upload + userInfo.download; - final total = userInfo.total; - useShow = TrafficValue(value: use).show; - totalShow = TrafficValue(value: total).show; - progress = total == 0 ? 0.0 : use / total; - } - return ListItem.radio( - horizontalTitleGap: 16, - delegate: RadioDelegate( - value: profile.id, - groupValue: groupValue, - onChanged: onChanged, - ), - padding: const EdgeInsets.symmetric(horizontal: 16), - trailing: CommonPopupMenu( - items: [ - CommonPopupMenuItem( - action: ProfileActions.edit, - label: appLocalizations.edit, - iconData: Icons.edit, - ), - if (profile.type == ProfileType.url) - CommonPopupMenuItem( - action: ProfileActions.update, - label: appLocalizations.update, - iconData: Icons.sync, - ), - CommonPopupMenuItem( - action: ProfileActions.delete, - label: appLocalizations.delete, - iconData: Icons.delete, - ), - ], - onSelected: (ProfileActions? action) async { - switch (action) { - case ProfileActions.edit: - _handleShowEditExtendPage(profile); - break; - case ProfileActions.delete: - _handleDeleteProfile(profile.id); - break; - case ProfileActions.update: - _handleUpdateProfile(profile.id); - break; - case null: - break; - } - }, - ), - title: Column( - mainAxisSize: MainAxisSize.min, + _buildTitle(Profile profile) { + final textTheme = context.textTheme; + final userInfo = profile.userInfo ?? UserInfo(); + final use = userInfo.upload + userInfo.download; + final total = userInfo.total; + final useShow = TrafficValue(value: use).show; + final totalShow = TrafficValue(value: total).show; + final progress = total == 0 ? 0.0 : use / total; + final expireShow = userInfo.expire == 0 + ? "长期有效" + : DateTime.fromMillisecondsSinceEpoch(userInfo.expire * 1000).show; + return Container( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.center, children: [ - Flexible( - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Flexible( - child: Text( - profile.label ?? profile.id, - style: Theme.of(context).textTheme.titleMedium, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - Flexible( - child: Text( - profile.lastUpdateDate?.lastUpdateTimeDesc ?? '', - style: Theme.of(context).textTheme.labelMedium?.toLight(), - ), - ), - ], - ), - ), - Flexible( - child: Container( - margin: const EdgeInsets.symmetric( - vertical: 8, + Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + profile.label ?? profile.id, + style: textTheme.titleMedium, + maxLines: 1, + overflow: TextOverflow.ellipsis, ), - child: LinearProgressIndicator( - minHeight: 6, - value: progress, + Text( + profile.lastUpdateDate?.lastUpdateTimeDesc ?? '', + style: textTheme.labelMedium?.toLight(), ), - ), + ], ), - Flexible( - child: Text( - "$useShow / $totalShow", - style: Theme.of(context).textTheme.labelMedium?.toLight(), - ), + Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + margin: const EdgeInsets.symmetric( + vertical: 8, + ), + child: LinearProgressIndicator( + minHeight: 6, + value: progress, + ), + ), + Text( + "$useShow / $totalShow", + style: textTheme.labelMedium?.toLight(), + ), + const SizedBox( + height: 2, + ), + Row( + children: [ + Text( + "到期时间:", + style: textTheme.labelMedium?.toLighter(), + ), + const SizedBox( + width: 4, + ), + Text( + expireShow, + style: textTheme.labelMedium?.toLighter(), + ), + ], + ) + ], ), ], ), ); } + + @override + Widget build(BuildContext context) { + final profile = widget.profile; + final groupValue = widget.groupValue; + final onChanged = widget.onChanged; + return Selector( + selector: (_, appState) => appState.viewMode, + builder: (_, viewMode, child) { + if (viewMode == ViewMode.mobile) { + return child!; + } + return CommonCard( + child: child!, + ); + }, + child: ListItem.radio( + key: Key(profile.id), + horizontalTitleGap: 16, + delegate: RadioDelegate( + value: profile.id, + groupValue: groupValue, + onChanged: onChanged, + ), + padding: const EdgeInsets.symmetric(horizontal: 16), + trailing: CommonPopupMenu( + items: [ + CommonPopupMenuItem( + action: ProfileActions.edit, + label: appLocalizations.edit, + iconData: Icons.edit, + ), + if (profile.type == ProfileType.url) + CommonPopupMenuItem( + action: ProfileActions.update, + label: appLocalizations.update, + iconData: Icons.sync, + ), + CommonPopupMenuItem( + action: ProfileActions.delete, + label: appLocalizations.delete, + iconData: Icons.delete, + ), + ], + onSelected: (ProfileActions? action) async { + switch (action) { + case ProfileActions.edit: + _handleShowEditExtendPage(profile); + break; + case ProfileActions.delete: + _handleDeleteProfile(profile.id); + break; + case ProfileActions.update: + _handleUpdateProfile(profile.id); + break; + case null: + break; + } + }, + ), + title: _buildTitle(profile), + tileTitleAlignment: ListTileTitleAlignment.titleHeight, + ), + ); + } } diff --git a/lib/fragments/proxies.dart b/lib/fragments/proxies.dart index 3774b3b..9588125 100644 --- a/lib/fragments/proxies.dart +++ b/lib/fragments/proxies.dart @@ -51,6 +51,9 @@ class _ProxiesFragmentState extends State selectedValue: proxiesSortType, ); }, + ), + const SizedBox( + width: 8, ) ]; }); @@ -431,7 +434,7 @@ class _DelayTestButtonContainerState extends State _controller = AnimationController( vsync: this, duration: const Duration( - milliseconds: 1200, + milliseconds: 600, ), ); _scale = Tween( @@ -443,7 +446,6 @@ class _DelayTestButtonContainerState extends State curve: const Interval( 0, 1, - curve: Curves.elasticInOut, ), ), ); diff --git a/lib/l10n/arb/intl_en.arb b/lib/l10n/arb/intl_en.arb index 51ed2b5..55d979c 100644 --- a/lib/l10n/arb/intl_en.arb +++ b/lib/l10n/arb/intl_en.arb @@ -159,5 +159,6 @@ "externalResources": "External resources", "checking": "Checking...", "country": "Country", - "ipCheckError": "Ip check timeout" + "checkError": "Check error", + "ipCheckTimeout": "Ip check timeout" } \ No newline at end of file diff --git a/lib/l10n/arb/intl_zh_CN.arb b/lib/l10n/arb/intl_zh_CN.arb index 73fb0a5..adaa7af 100644 --- a/lib/l10n/arb/intl_zh_CN.arb +++ b/lib/l10n/arb/intl_zh_CN.arb @@ -159,5 +159,6 @@ "externalResources": "外部资源", "checking": "检测中...", "country": "区域", - "ipCheckError": "Ip检测超时" + "checkError": "检测失败", + "ipCheckTimeout": "Ip检测超时" } \ No newline at end of file diff --git a/lib/l10n/intl/messages_en.dart b/lib/l10n/intl/messages_en.dart index 2a8922d..8bfaf53 100644 --- a/lib/l10n/intl/messages_en.dart +++ b/lib/l10n/intl/messages_en.dart @@ -76,6 +76,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Cancel filter system app"), "cancelSelectAll": MessageLookupByLibrary.simpleMessage("Cancel select all"), + "checkError": MessageLookupByLibrary.simpleMessage("Check error"), "checkUpdate": MessageLookupByLibrary.simpleMessage("Check for updates"), "checkUpdateError": MessageLookupByLibrary.simpleMessage( @@ -124,7 +125,7 @@ class MessageLookup extends MessageLookupByLibrary { "hours": MessageLookupByLibrary.simpleMessage("Hours"), "importFromURL": MessageLookupByLibrary.simpleMessage("Import from URL"), - "ipCheckError": + "ipCheckTimeout": MessageLookupByLibrary.simpleMessage("Ip check timeout"), "just": MessageLookupByLibrary.simpleMessage("Just"), "language": MessageLookupByLibrary.simpleMessage("Language"), diff --git a/lib/l10n/intl/messages_zh_CN.dart b/lib/l10n/intl/messages_zh_CN.dart index 6591e51..0dbffa6 100644 --- a/lib/l10n/intl/messages_zh_CN.dart +++ b/lib/l10n/intl/messages_zh_CN.dart @@ -63,6 +63,7 @@ class MessageLookup extends MessageLookupByLibrary { "cancelFilterSystemApp": MessageLookupByLibrary.simpleMessage("取消过滤系统应用"), "cancelSelectAll": MessageLookupByLibrary.simpleMessage("取消全选"), + "checkError": MessageLookupByLibrary.simpleMessage("检测失败"), "checkUpdate": MessageLookupByLibrary.simpleMessage("检查更新"), "checkUpdateError": MessageLookupByLibrary.simpleMessage("当前应用已经是最新版了"), "checking": MessageLookupByLibrary.simpleMessage("检测中..."), @@ -101,7 +102,7 @@ class MessageLookup extends MessageLookupByLibrary { "goDownload": MessageLookupByLibrary.simpleMessage("前往下载"), "hours": MessageLookupByLibrary.simpleMessage("小时"), "importFromURL": MessageLookupByLibrary.simpleMessage("从URL导入"), - "ipCheckError": MessageLookupByLibrary.simpleMessage("Ip检测超时"), + "ipCheckTimeout": MessageLookupByLibrary.simpleMessage("Ip检测超时"), "just": MessageLookupByLibrary.simpleMessage("刚刚"), "language": MessageLookupByLibrary.simpleMessage("语言"), "light": MessageLookupByLibrary.simpleMessage("浅色"), diff --git a/lib/l10n/l10n.dart b/lib/l10n/l10n.dart index 8ea02d5..0ff10e8 100644 --- a/lib/l10n/l10n.dart +++ b/lib/l10n/l10n.dart @@ -1650,11 +1650,21 @@ class AppLocalizations { ); } + /// `Check error` + String get checkError { + return Intl.message( + 'Check error', + name: 'checkError', + desc: '', + args: [], + ); + } + /// `Ip check timeout` - String get ipCheckError { + String get ipCheckTimeout { return Intl.message( 'Ip check timeout', - name: 'ipCheckError', + name: 'ipCheckTimeout', desc: '', args: [], ); diff --git a/lib/models/profile.dart b/lib/models/profile.dart index 6dbf28a..7156602 100644 --- a/lib/models/profile.dart +++ b/lib/models/profile.dart @@ -16,16 +16,17 @@ class UserInfo { int upload; int download; int total; - int? expire; + int expire; UserInfo({ int? upload, int? download, int? total, - this.expire, + int? expire, }) : upload = upload ?? 0, download = download ?? 0, - total = total ?? 0; + total = total ?? 0, + expire = expire ?? 0; Map toJson() { return _$UserInfoToJson(this); @@ -37,10 +38,10 @@ class UserInfo { factory UserInfo.formHString(String? info) { if (info == null) return UserInfo(); - var list = info.split(";"); + final list = info.split(";"); Map map = {}; - for (var i in list) { - var keyValue = i.trim().split("="); + for (final i in list) { + final keyValue = i.trim().split("="); map[keyValue[0]] = int.tryParse(keyValue[1]); } return UserInfo( diff --git a/lib/widgets/list.dart b/lib/widgets/list.dart index 58c99ee..9641e60 100644 --- a/lib/widgets/list.dart +++ b/lib/widgets/list.dart @@ -71,6 +71,7 @@ class ListItem extends StatelessWidget { final Widget title; final Widget? subtitle; final EdgeInsets padding; + final ListTileTitleAlignment tileTitleAlignment; final bool? prue; final Widget? trailing; final Delegate delegate; @@ -87,6 +88,7 @@ class ListItem extends StatelessWidget { this.horizontalTitleGap, this.prue, this.onTab, + this.tileTitleAlignment = ListTileTitleAlignment.center, }) : delegate = const Delegate(); const ListItem.open({ @@ -99,6 +101,7 @@ class ListItem extends StatelessWidget { required OpenDelegate this.delegate, this.horizontalTitleGap, this.prue, + this.tileTitleAlignment = ListTileTitleAlignment.center, }) : onTab = null; const ListItem.next({ @@ -111,6 +114,7 @@ class ListItem extends StatelessWidget { required NextDelegate this.delegate, this.horizontalTitleGap, this.prue, + this.tileTitleAlignment = ListTileTitleAlignment.center, }) : onTab = null; const ListItem.checkbox({ @@ -122,6 +126,7 @@ class ListItem extends StatelessWidget { required CheckboxDelegate this.delegate, this.horizontalTitleGap, this.prue, + this.tileTitleAlignment = ListTileTitleAlignment.center, }) : trailing = null, onTab = null; @@ -134,6 +139,7 @@ class ListItem extends StatelessWidget { required SwitchDelegate this.delegate, this.horizontalTitleGap, this.prue, + this.tileTitleAlignment = ListTileTitleAlignment.center, }) : trailing = null, onTab = null; @@ -146,6 +152,7 @@ class ListItem extends StatelessWidget { required RadioDelegate this.delegate, this.horizontalTitleGap = 8, this.prue, + this.tileTitleAlignment = ListTileTitleAlignment.center, }) : leading = null, onTab = null; @@ -193,6 +200,7 @@ class ListItem extends StatelessWidget { horizontalTitleGap: horizontalTitleGap, title: title, subtitle: subtitle, + titleAlignment: tileTitleAlignment, onTap: onTab, trailing: trailing ?? this.trailing, contentPadding: padding, diff --git a/lib/widgets/scaffold.dart b/lib/widgets/scaffold.dart index 72f232d..2fe8699 100644 --- a/lib/widgets/scaffold.dart +++ b/lib/widgets/scaffold.dart @@ -49,6 +49,7 @@ class CommonScaffold extends StatefulWidget { class CommonScaffoldState extends State { final ValueNotifier> _actions = ValueNotifier([]); + final ValueNotifier _floatingActionButton = ValueNotifier(null); final ValueNotifier _loading = ValueNotifier(false); @@ -58,11 +59,16 @@ class CommonScaffoldState extends State { } } + set floatingActionButton(Widget? actions) { + if (_floatingActionButton.value != actions) { + _floatingActionButton.value = actions; + } + } + Future loadingRun( Future Function() futureFunction, { String? title, }) async { - if (_loading.value == true) return null; _loading.value = true; try { final res = await futureFunction(); @@ -85,6 +91,7 @@ class CommonScaffoldState extends State { super.didUpdateWidget(oldWidget); if (oldWidget.title != widget.title) { _actions.value = []; + _floatingActionButton.value = null; } } @@ -109,6 +116,13 @@ class CommonScaffoldState extends State { Widget build(BuildContext context) { return _platformContainer( child: Scaffold( + floatingActionButton: ValueListenableBuilder( + valueListenable: _floatingActionButton, + builder: (_, floatingActionButton, __) { + return floatingActionButton ?? Container(); + }, + ), + floatingActionButtonAnimator: FloatingActionButtonAnimator.scaling, appBar: PreferredSize( preferredSize: const Size.fromHeight(kToolbarHeight), child: Stack( diff --git a/pubspec.yaml b/pubspec.yaml index e3e1396..f462e5c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: fl_clash description: A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free. publish_to: 'none' -version: 0.8.15 +version: 0.8.16 environment: sdk: '>=3.1.0 <4.0.0'