Optimize back navigation
This commit is contained in:
@@ -11,6 +11,7 @@ import 'package:fl_clash/plugins/app.dart';
|
||||
import 'package:fl_clash/providers/providers.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
@@ -46,6 +47,7 @@ class ApplicationState extends ConsumerState<Application> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
SystemNavigator.setFrameworkHandlesBack(true);
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
|
||||
if (globalState.navigatorKey.currentContext != null) {
|
||||
await globalState.attach();
|
||||
@@ -141,6 +143,7 @@ class ApplicationState extends ConsumerState<Application> {
|
||||
return MaterialApp(
|
||||
debugShowCheckedModeBanner: false,
|
||||
navigatorKey: globalState.navigatorKey,
|
||||
onNavigationNotification: (_) => true,
|
||||
localizationsDelegates: const [
|
||||
AppLocalizations.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
|
||||
@@ -78,6 +78,8 @@ class _HotKeyManagerState extends ConsumerState<HotKeyManager> {
|
||||
shortcuts: {
|
||||
utils.controlSingleActivator(LogicalKeyboardKey.keyW):
|
||||
const CloseWindowIntent(),
|
||||
const SingleActivator(LogicalKeyboardKey.escape):
|
||||
const EscapeBackIntent(),
|
||||
},
|
||||
child: Actions(
|
||||
actions: {
|
||||
@@ -86,6 +88,9 @@ class _HotKeyManagerState extends ConsumerState<HotKeyManager> {
|
||||
.read(systemActionProvider.notifier)
|
||||
.handleClose(false),
|
||||
),
|
||||
EscapeBackIntent: CallbackAction<EscapeBackIntent>(
|
||||
onInvoke: (_) => globalState.navigatorKey.currentState?.maybePop(),
|
||||
),
|
||||
DoNothingIntent: CallbackAction<DoNothingIntent>(
|
||||
onInvoke: (_) => null,
|
||||
),
|
||||
|
||||
@@ -14,7 +14,6 @@ typedef DelayMap = Map<String, Map<String, int?>>;
|
||||
abstract class AppState with _$AppState {
|
||||
const factory AppState({
|
||||
@Default(false) bool isInit,
|
||||
@Default(false) bool backBlock,
|
||||
@Default(PageLabel.dashboard) PageLabel pageLabel,
|
||||
@Default([]) List<Package> packages,
|
||||
@Default(0) int sortNum,
|
||||
|
||||
@@ -581,6 +581,10 @@ class CloseWindowIntent extends Intent {
|
||||
const CloseWindowIntent();
|
||||
}
|
||||
|
||||
class EscapeBackIntent extends Intent {
|
||||
const EscapeBackIntent();
|
||||
}
|
||||
|
||||
@freezed
|
||||
abstract class Result<T> with _$Result<T> {
|
||||
const factory Result({
|
||||
|
||||
@@ -14,7 +14,7 @@ T _$identity<T>(T value) => value;
|
||||
/// @nodoc
|
||||
mixin _$AppState {
|
||||
|
||||
bool get isInit; bool get backBlock; PageLabel get pageLabel; List<Package> get packages; int get sortNum; Size get viewSize; double get sideWidth; DelayMap get delayMap; List<Group> get groups; int get checkIpNum; Brightness get brightness; int? get runTime; List<ExternalProvider> get providers; String? get localIp; FixedList<TrackerInfo> get requests; int get version; FixedList<Log> get logs; FixedList<Traffic> get traffics; Traffic get totalTraffic; TunAuthorizationState get authorizedTunEnable; bool get loading; SystemUiOverlayStyle get systemUiOverlayStyle; CoreStatus get coreStatus;
|
||||
bool get isInit; PageLabel get pageLabel; List<Package> get packages; int get sortNum; Size get viewSize; double get sideWidth; DelayMap get delayMap; List<Group> get groups; int get checkIpNum; Brightness get brightness; int? get runTime; List<ExternalProvider> get providers; String? get localIp; FixedList<TrackerInfo> get requests; int get version; FixedList<Log> get logs; FixedList<Traffic> get traffics; Traffic get totalTraffic; TunAuthorizationState get authorizedTunEnable; bool get loading; SystemUiOverlayStyle get systemUiOverlayStyle; CoreStatus get coreStatus;
|
||||
/// Create a copy of AppState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@@ -25,12 +25,12 @@ $AppStateCopyWith<AppState> get copyWith => _$AppStateCopyWithImpl<AppState>(thi
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is AppState&&(identical(other.isInit, isInit) || other.isInit == isInit)&&(identical(other.backBlock, backBlock) || other.backBlock == backBlock)&&(identical(other.pageLabel, pageLabel) || other.pageLabel == pageLabel)&&const DeepCollectionEquality().equals(other.packages, packages)&&(identical(other.sortNum, sortNum) || other.sortNum == sortNum)&&(identical(other.viewSize, viewSize) || other.viewSize == viewSize)&&(identical(other.sideWidth, sideWidth) || other.sideWidth == sideWidth)&&const DeepCollectionEquality().equals(other.delayMap, delayMap)&&const DeepCollectionEquality().equals(other.groups, groups)&&(identical(other.checkIpNum, checkIpNum) || other.checkIpNum == checkIpNum)&&(identical(other.brightness, brightness) || other.brightness == brightness)&&(identical(other.runTime, runTime) || other.runTime == runTime)&&const DeepCollectionEquality().equals(other.providers, providers)&&(identical(other.localIp, localIp) || other.localIp == localIp)&&(identical(other.requests, requests) || other.requests == requests)&&(identical(other.version, version) || other.version == version)&&(identical(other.logs, logs) || other.logs == logs)&&(identical(other.traffics, traffics) || other.traffics == traffics)&&(identical(other.totalTraffic, totalTraffic) || other.totalTraffic == totalTraffic)&&(identical(other.authorizedTunEnable, authorizedTunEnable) || other.authorizedTunEnable == authorizedTunEnable)&&(identical(other.loading, loading) || other.loading == loading)&&(identical(other.systemUiOverlayStyle, systemUiOverlayStyle) || other.systemUiOverlayStyle == systemUiOverlayStyle)&&(identical(other.coreStatus, coreStatus) || other.coreStatus == coreStatus));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is AppState&&(identical(other.isInit, isInit) || other.isInit == isInit)&&(identical(other.pageLabel, pageLabel) || other.pageLabel == pageLabel)&&const DeepCollectionEquality().equals(other.packages, packages)&&(identical(other.sortNum, sortNum) || other.sortNum == sortNum)&&(identical(other.viewSize, viewSize) || other.viewSize == viewSize)&&(identical(other.sideWidth, sideWidth) || other.sideWidth == sideWidth)&&const DeepCollectionEquality().equals(other.delayMap, delayMap)&&const DeepCollectionEquality().equals(other.groups, groups)&&(identical(other.checkIpNum, checkIpNum) || other.checkIpNum == checkIpNum)&&(identical(other.brightness, brightness) || other.brightness == brightness)&&(identical(other.runTime, runTime) || other.runTime == runTime)&&const DeepCollectionEquality().equals(other.providers, providers)&&(identical(other.localIp, localIp) || other.localIp == localIp)&&(identical(other.requests, requests) || other.requests == requests)&&(identical(other.version, version) || other.version == version)&&(identical(other.logs, logs) || other.logs == logs)&&(identical(other.traffics, traffics) || other.traffics == traffics)&&(identical(other.totalTraffic, totalTraffic) || other.totalTraffic == totalTraffic)&&(identical(other.authorizedTunEnable, authorizedTunEnable) || other.authorizedTunEnable == authorizedTunEnable)&&(identical(other.loading, loading) || other.loading == loading)&&(identical(other.systemUiOverlayStyle, systemUiOverlayStyle) || other.systemUiOverlayStyle == systemUiOverlayStyle)&&(identical(other.coreStatus, coreStatus) || other.coreStatus == coreStatus));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hashAll([runtimeType,isInit,backBlock,pageLabel,const DeepCollectionEquality().hash(packages),sortNum,viewSize,sideWidth,const DeepCollectionEquality().hash(delayMap),const DeepCollectionEquality().hash(groups),checkIpNum,brightness,runTime,const DeepCollectionEquality().hash(providers),localIp,requests,version,logs,traffics,totalTraffic,authorizedTunEnable,loading,systemUiOverlayStyle,coreStatus]);
|
||||
int get hashCode => Object.hashAll([runtimeType,isInit,pageLabel,const DeepCollectionEquality().hash(packages),sortNum,viewSize,sideWidth,const DeepCollectionEquality().hash(delayMap),const DeepCollectionEquality().hash(groups),checkIpNum,brightness,runTime,const DeepCollectionEquality().hash(providers),localIp,requests,version,logs,traffics,totalTraffic,authorizedTunEnable,loading,systemUiOverlayStyle,coreStatus]);
|
||||
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ abstract mixin class $AppStateCopyWith<$Res> {
|
||||
factory $AppStateCopyWith(AppState value, $Res Function(AppState) _then) = _$AppStateCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
bool isInit, bool backBlock, PageLabel pageLabel, List<Package> packages, int sortNum, Size viewSize, double sideWidth, DelayMap delayMap, List<Group> groups, int checkIpNum, Brightness brightness, int? runTime, List<ExternalProvider> providers, String? localIp, FixedList<TrackerInfo> requests, int version, FixedList<Log> logs, FixedList<Traffic> traffics, Traffic totalTraffic, TunAuthorizationState authorizedTunEnable, bool loading, SystemUiOverlayStyle systemUiOverlayStyle, CoreStatus coreStatus
|
||||
bool isInit, PageLabel pageLabel, List<Package> packages, int sortNum, Size viewSize, double sideWidth, DelayMap delayMap, List<Group> groups, int checkIpNum, Brightness brightness, int? runTime, List<ExternalProvider> providers, String? localIp, FixedList<TrackerInfo> requests, int version, FixedList<Log> logs, FixedList<Traffic> traffics, Traffic totalTraffic, TunAuthorizationState authorizedTunEnable, bool loading, SystemUiOverlayStyle systemUiOverlayStyle, CoreStatus coreStatus
|
||||
});
|
||||
|
||||
|
||||
@@ -58,10 +58,9 @@ class _$AppStateCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of AppState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? isInit = null,Object? backBlock = null,Object? pageLabel = null,Object? packages = null,Object? sortNum = null,Object? viewSize = null,Object? sideWidth = null,Object? delayMap = null,Object? groups = null,Object? checkIpNum = null,Object? brightness = null,Object? runTime = freezed,Object? providers = null,Object? localIp = freezed,Object? requests = null,Object? version = null,Object? logs = null,Object? traffics = null,Object? totalTraffic = null,Object? authorizedTunEnable = null,Object? loading = null,Object? systemUiOverlayStyle = null,Object? coreStatus = null,}) {
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? isInit = null,Object? pageLabel = null,Object? packages = null,Object? sortNum = null,Object? viewSize = null,Object? sideWidth = null,Object? delayMap = null,Object? groups = null,Object? checkIpNum = null,Object? brightness = null,Object? runTime = freezed,Object? providers = null,Object? localIp = freezed,Object? requests = null,Object? version = null,Object? logs = null,Object? traffics = null,Object? totalTraffic = null,Object? authorizedTunEnable = null,Object? loading = null,Object? systemUiOverlayStyle = null,Object? coreStatus = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
isInit: null == isInit ? _self.isInit : isInit // ignore: cast_nullable_to_non_nullable
|
||||
as bool,backBlock: null == backBlock ? _self.backBlock : backBlock // ignore: cast_nullable_to_non_nullable
|
||||
as bool,pageLabel: null == pageLabel ? _self.pageLabel : pageLabel // ignore: cast_nullable_to_non_nullable
|
||||
as PageLabel,packages: null == packages ? _self.packages : packages // ignore: cast_nullable_to_non_nullable
|
||||
as List<Package>,sortNum: null == sortNum ? _self.sortNum : sortNum // ignore: cast_nullable_to_non_nullable
|
||||
@@ -177,10 +176,10 @@ return $default(_that);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool isInit, bool backBlock, PageLabel pageLabel, List<Package> packages, int sortNum, Size viewSize, double sideWidth, DelayMap delayMap, List<Group> groups, int checkIpNum, Brightness brightness, int? runTime, List<ExternalProvider> providers, String? localIp, FixedList<TrackerInfo> requests, int version, FixedList<Log> logs, FixedList<Traffic> traffics, Traffic totalTraffic, TunAuthorizationState authorizedTunEnable, bool loading, SystemUiOverlayStyle systemUiOverlayStyle, CoreStatus coreStatus)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool isInit, PageLabel pageLabel, List<Package> packages, int sortNum, Size viewSize, double sideWidth, DelayMap delayMap, List<Group> groups, int checkIpNum, Brightness brightness, int? runTime, List<ExternalProvider> providers, String? localIp, FixedList<TrackerInfo> requests, int version, FixedList<Log> logs, FixedList<Traffic> traffics, Traffic totalTraffic, TunAuthorizationState authorizedTunEnable, bool loading, SystemUiOverlayStyle systemUiOverlayStyle, CoreStatus coreStatus)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _AppState() when $default != null:
|
||||
return $default(_that.isInit,_that.backBlock,_that.pageLabel,_that.packages,_that.sortNum,_that.viewSize,_that.sideWidth,_that.delayMap,_that.groups,_that.checkIpNum,_that.brightness,_that.runTime,_that.providers,_that.localIp,_that.requests,_that.version,_that.logs,_that.traffics,_that.totalTraffic,_that.authorizedTunEnable,_that.loading,_that.systemUiOverlayStyle,_that.coreStatus);case _:
|
||||
return $default(_that.isInit,_that.pageLabel,_that.packages,_that.sortNum,_that.viewSize,_that.sideWidth,_that.delayMap,_that.groups,_that.checkIpNum,_that.brightness,_that.runTime,_that.providers,_that.localIp,_that.requests,_that.version,_that.logs,_that.traffics,_that.totalTraffic,_that.authorizedTunEnable,_that.loading,_that.systemUiOverlayStyle,_that.coreStatus);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
@@ -198,10 +197,10 @@ return $default(_that.isInit,_that.backBlock,_that.pageLabel,_that.packages,_tha
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool isInit, bool backBlock, PageLabel pageLabel, List<Package> packages, int sortNum, Size viewSize, double sideWidth, DelayMap delayMap, List<Group> groups, int checkIpNum, Brightness brightness, int? runTime, List<ExternalProvider> providers, String? localIp, FixedList<TrackerInfo> requests, int version, FixedList<Log> logs, FixedList<Traffic> traffics, Traffic totalTraffic, TunAuthorizationState authorizedTunEnable, bool loading, SystemUiOverlayStyle systemUiOverlayStyle, CoreStatus coreStatus) $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool isInit, PageLabel pageLabel, List<Package> packages, int sortNum, Size viewSize, double sideWidth, DelayMap delayMap, List<Group> groups, int checkIpNum, Brightness brightness, int? runTime, List<ExternalProvider> providers, String? localIp, FixedList<TrackerInfo> requests, int version, FixedList<Log> logs, FixedList<Traffic> traffics, Traffic totalTraffic, TunAuthorizationState authorizedTunEnable, bool loading, SystemUiOverlayStyle systemUiOverlayStyle, CoreStatus coreStatus) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _AppState():
|
||||
return $default(_that.isInit,_that.backBlock,_that.pageLabel,_that.packages,_that.sortNum,_that.viewSize,_that.sideWidth,_that.delayMap,_that.groups,_that.checkIpNum,_that.brightness,_that.runTime,_that.providers,_that.localIp,_that.requests,_that.version,_that.logs,_that.traffics,_that.totalTraffic,_that.authorizedTunEnable,_that.loading,_that.systemUiOverlayStyle,_that.coreStatus);case _:
|
||||
return $default(_that.isInit,_that.pageLabel,_that.packages,_that.sortNum,_that.viewSize,_that.sideWidth,_that.delayMap,_that.groups,_that.checkIpNum,_that.brightness,_that.runTime,_that.providers,_that.localIp,_that.requests,_that.version,_that.logs,_that.traffics,_that.totalTraffic,_that.authorizedTunEnable,_that.loading,_that.systemUiOverlayStyle,_that.coreStatus);case _:
|
||||
throw StateError('Unexpected subclass');
|
||||
|
||||
}
|
||||
@@ -218,10 +217,10 @@ return $default(_that.isInit,_that.backBlock,_that.pageLabel,_that.packages,_tha
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool isInit, bool backBlock, PageLabel pageLabel, List<Package> packages, int sortNum, Size viewSize, double sideWidth, DelayMap delayMap, List<Group> groups, int checkIpNum, Brightness brightness, int? runTime, List<ExternalProvider> providers, String? localIp, FixedList<TrackerInfo> requests, int version, FixedList<Log> logs, FixedList<Traffic> traffics, Traffic totalTraffic, TunAuthorizationState authorizedTunEnable, bool loading, SystemUiOverlayStyle systemUiOverlayStyle, CoreStatus coreStatus)? $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool isInit, PageLabel pageLabel, List<Package> packages, int sortNum, Size viewSize, double sideWidth, DelayMap delayMap, List<Group> groups, int checkIpNum, Brightness brightness, int? runTime, List<ExternalProvider> providers, String? localIp, FixedList<TrackerInfo> requests, int version, FixedList<Log> logs, FixedList<Traffic> traffics, Traffic totalTraffic, TunAuthorizationState authorizedTunEnable, bool loading, SystemUiOverlayStyle systemUiOverlayStyle, CoreStatus coreStatus)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _AppState() when $default != null:
|
||||
return $default(_that.isInit,_that.backBlock,_that.pageLabel,_that.packages,_that.sortNum,_that.viewSize,_that.sideWidth,_that.delayMap,_that.groups,_that.checkIpNum,_that.brightness,_that.runTime,_that.providers,_that.localIp,_that.requests,_that.version,_that.logs,_that.traffics,_that.totalTraffic,_that.authorizedTunEnable,_that.loading,_that.systemUiOverlayStyle,_that.coreStatus);case _:
|
||||
return $default(_that.isInit,_that.pageLabel,_that.packages,_that.sortNum,_that.viewSize,_that.sideWidth,_that.delayMap,_that.groups,_that.checkIpNum,_that.brightness,_that.runTime,_that.providers,_that.localIp,_that.requests,_that.version,_that.logs,_that.traffics,_that.totalTraffic,_that.authorizedTunEnable,_that.loading,_that.systemUiOverlayStyle,_that.coreStatus);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
@@ -233,11 +232,10 @@ return $default(_that.isInit,_that.backBlock,_that.pageLabel,_that.packages,_tha
|
||||
|
||||
|
||||
class _AppState implements AppState {
|
||||
const _AppState({this.isInit = false, this.backBlock = false, this.pageLabel = PageLabel.dashboard, final List<Package> packages = const [], this.sortNum = 0, required this.viewSize, this.sideWidth = 0, final DelayMap delayMap = const {}, final List<Group> groups = const [], this.checkIpNum = 0, required this.brightness, this.runTime, final List<ExternalProvider> providers = const [], this.localIp, required this.requests, required this.version, required this.logs, required this.traffics, required this.totalTraffic, this.authorizedTunEnable = TunAuthorizationState.none, this.loading = false, required this.systemUiOverlayStyle, this.coreStatus = CoreStatus.connecting}): _packages = packages,_delayMap = delayMap,_groups = groups,_providers = providers;
|
||||
const _AppState({this.isInit = false, this.pageLabel = PageLabel.dashboard, final List<Package> packages = const [], this.sortNum = 0, required this.viewSize, this.sideWidth = 0, final DelayMap delayMap = const {}, final List<Group> groups = const [], this.checkIpNum = 0, required this.brightness, this.runTime, final List<ExternalProvider> providers = const [], this.localIp, required this.requests, required this.version, required this.logs, required this.traffics, required this.totalTraffic, this.authorizedTunEnable = TunAuthorizationState.none, this.loading = false, required this.systemUiOverlayStyle, this.coreStatus = CoreStatus.connecting}): _packages = packages,_delayMap = delayMap,_groups = groups,_providers = providers;
|
||||
|
||||
|
||||
@override@JsonKey() final bool isInit;
|
||||
@override@JsonKey() final bool backBlock;
|
||||
@override@JsonKey() final PageLabel pageLabel;
|
||||
final List<Package> _packages;
|
||||
@override@JsonKey() List<Package> get packages {
|
||||
@@ -294,12 +292,12 @@ _$AppStateCopyWith<_AppState> get copyWith => __$AppStateCopyWithImpl<_AppState>
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _AppState&&(identical(other.isInit, isInit) || other.isInit == isInit)&&(identical(other.backBlock, backBlock) || other.backBlock == backBlock)&&(identical(other.pageLabel, pageLabel) || other.pageLabel == pageLabel)&&const DeepCollectionEquality().equals(other._packages, _packages)&&(identical(other.sortNum, sortNum) || other.sortNum == sortNum)&&(identical(other.viewSize, viewSize) || other.viewSize == viewSize)&&(identical(other.sideWidth, sideWidth) || other.sideWidth == sideWidth)&&const DeepCollectionEquality().equals(other._delayMap, _delayMap)&&const DeepCollectionEquality().equals(other._groups, _groups)&&(identical(other.checkIpNum, checkIpNum) || other.checkIpNum == checkIpNum)&&(identical(other.brightness, brightness) || other.brightness == brightness)&&(identical(other.runTime, runTime) || other.runTime == runTime)&&const DeepCollectionEquality().equals(other._providers, _providers)&&(identical(other.localIp, localIp) || other.localIp == localIp)&&(identical(other.requests, requests) || other.requests == requests)&&(identical(other.version, version) || other.version == version)&&(identical(other.logs, logs) || other.logs == logs)&&(identical(other.traffics, traffics) || other.traffics == traffics)&&(identical(other.totalTraffic, totalTraffic) || other.totalTraffic == totalTraffic)&&(identical(other.authorizedTunEnable, authorizedTunEnable) || other.authorizedTunEnable == authorizedTunEnable)&&(identical(other.loading, loading) || other.loading == loading)&&(identical(other.systemUiOverlayStyle, systemUiOverlayStyle) || other.systemUiOverlayStyle == systemUiOverlayStyle)&&(identical(other.coreStatus, coreStatus) || other.coreStatus == coreStatus));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _AppState&&(identical(other.isInit, isInit) || other.isInit == isInit)&&(identical(other.pageLabel, pageLabel) || other.pageLabel == pageLabel)&&const DeepCollectionEquality().equals(other._packages, _packages)&&(identical(other.sortNum, sortNum) || other.sortNum == sortNum)&&(identical(other.viewSize, viewSize) || other.viewSize == viewSize)&&(identical(other.sideWidth, sideWidth) || other.sideWidth == sideWidth)&&const DeepCollectionEquality().equals(other._delayMap, _delayMap)&&const DeepCollectionEquality().equals(other._groups, _groups)&&(identical(other.checkIpNum, checkIpNum) || other.checkIpNum == checkIpNum)&&(identical(other.brightness, brightness) || other.brightness == brightness)&&(identical(other.runTime, runTime) || other.runTime == runTime)&&const DeepCollectionEquality().equals(other._providers, _providers)&&(identical(other.localIp, localIp) || other.localIp == localIp)&&(identical(other.requests, requests) || other.requests == requests)&&(identical(other.version, version) || other.version == version)&&(identical(other.logs, logs) || other.logs == logs)&&(identical(other.traffics, traffics) || other.traffics == traffics)&&(identical(other.totalTraffic, totalTraffic) || other.totalTraffic == totalTraffic)&&(identical(other.authorizedTunEnable, authorizedTunEnable) || other.authorizedTunEnable == authorizedTunEnable)&&(identical(other.loading, loading) || other.loading == loading)&&(identical(other.systemUiOverlayStyle, systemUiOverlayStyle) || other.systemUiOverlayStyle == systemUiOverlayStyle)&&(identical(other.coreStatus, coreStatus) || other.coreStatus == coreStatus));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hashAll([runtimeType,isInit,backBlock,pageLabel,const DeepCollectionEquality().hash(_packages),sortNum,viewSize,sideWidth,const DeepCollectionEquality().hash(_delayMap),const DeepCollectionEquality().hash(_groups),checkIpNum,brightness,runTime,const DeepCollectionEquality().hash(_providers),localIp,requests,version,logs,traffics,totalTraffic,authorizedTunEnable,loading,systemUiOverlayStyle,coreStatus]);
|
||||
int get hashCode => Object.hashAll([runtimeType,isInit,pageLabel,const DeepCollectionEquality().hash(_packages),sortNum,viewSize,sideWidth,const DeepCollectionEquality().hash(_delayMap),const DeepCollectionEquality().hash(_groups),checkIpNum,brightness,runTime,const DeepCollectionEquality().hash(_providers),localIp,requests,version,logs,traffics,totalTraffic,authorizedTunEnable,loading,systemUiOverlayStyle,coreStatus]);
|
||||
|
||||
|
||||
|
||||
@@ -310,7 +308,7 @@ abstract mixin class _$AppStateCopyWith<$Res> implements $AppStateCopyWith<$Res>
|
||||
factory _$AppStateCopyWith(_AppState value, $Res Function(_AppState) _then) = __$AppStateCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
bool isInit, bool backBlock, PageLabel pageLabel, List<Package> packages, int sortNum, Size viewSize, double sideWidth, DelayMap delayMap, List<Group> groups, int checkIpNum, Brightness brightness, int? runTime, List<ExternalProvider> providers, String? localIp, FixedList<TrackerInfo> requests, int version, FixedList<Log> logs, FixedList<Traffic> traffics, Traffic totalTraffic, TunAuthorizationState authorizedTunEnable, bool loading, SystemUiOverlayStyle systemUiOverlayStyle, CoreStatus coreStatus
|
||||
bool isInit, PageLabel pageLabel, List<Package> packages, int sortNum, Size viewSize, double sideWidth, DelayMap delayMap, List<Group> groups, int checkIpNum, Brightness brightness, int? runTime, List<ExternalProvider> providers, String? localIp, FixedList<TrackerInfo> requests, int version, FixedList<Log> logs, FixedList<Traffic> traffics, Traffic totalTraffic, TunAuthorizationState authorizedTunEnable, bool loading, SystemUiOverlayStyle systemUiOverlayStyle, CoreStatus coreStatus
|
||||
});
|
||||
|
||||
|
||||
@@ -327,10 +325,9 @@ class __$AppStateCopyWithImpl<$Res>
|
||||
|
||||
/// Create a copy of AppState
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? isInit = null,Object? backBlock = null,Object? pageLabel = null,Object? packages = null,Object? sortNum = null,Object? viewSize = null,Object? sideWidth = null,Object? delayMap = null,Object? groups = null,Object? checkIpNum = null,Object? brightness = null,Object? runTime = freezed,Object? providers = null,Object? localIp = freezed,Object? requests = null,Object? version = null,Object? logs = null,Object? traffics = null,Object? totalTraffic = null,Object? authorizedTunEnable = null,Object? loading = null,Object? systemUiOverlayStyle = null,Object? coreStatus = null,}) {
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? isInit = null,Object? pageLabel = null,Object? packages = null,Object? sortNum = null,Object? viewSize = null,Object? sideWidth = null,Object? delayMap = null,Object? groups = null,Object? checkIpNum = null,Object? brightness = null,Object? runTime = freezed,Object? providers = null,Object? localIp = freezed,Object? requests = null,Object? version = null,Object? logs = null,Object? traffics = null,Object? totalTraffic = null,Object? authorizedTunEnable = null,Object? loading = null,Object? systemUiOverlayStyle = null,Object? coreStatus = null,}) {
|
||||
return _then(_AppState(
|
||||
isInit: null == isInit ? _self.isInit : isInit // ignore: cast_nullable_to_non_nullable
|
||||
as bool,backBlock: null == backBlock ? _self.backBlock : backBlock // ignore: cast_nullable_to_non_nullable
|
||||
as bool,pageLabel: null == pageLabel ? _self.pageLabel : pageLabel // ignore: cast_nullable_to_non_nullable
|
||||
as PageLabel,packages: null == packages ? _self._packages : packages // ignore: cast_nullable_to_non_nullable
|
||||
as List<Package>,sortNum: null == sortNum ? _self.sortNum : sortNum // ignore: cast_nullable_to_non_nullable
|
||||
|
||||
+19
-12
@@ -110,7 +110,24 @@ class HomePage extends ConsumerWidget {
|
||||
onDidRemovePage: (_) {},
|
||||
),
|
||||
);
|
||||
return view;
|
||||
return Consumer(
|
||||
key: ValueKey(navigationItem.label),
|
||||
builder: (_, ref, child) {
|
||||
final isActive = ref.watch(
|
||||
currentPageLabelProvider.select(
|
||||
(label) => label == navigationItem.label,
|
||||
),
|
||||
);
|
||||
return PageActivityScope(
|
||||
isActive: isActive,
|
||||
child: ExcludeFocus(
|
||||
excluding: !isActive,
|
||||
child: child!,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: view,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
@@ -204,12 +221,6 @@ class _HomePageViewState extends ConsumerState<_HomePageView> {
|
||||
final itemCount = ref.watch(
|
||||
currentNavigationItemsStateProvider.select((state) => state.value.length),
|
||||
);
|
||||
final currentIndex = ref.watch(
|
||||
currentPageLabelProvider.select(
|
||||
(label) =>
|
||||
widget.navigationItems.indexWhere((item) => item.label == label),
|
||||
),
|
||||
);
|
||||
return PageView.builder(
|
||||
controller: _pageController,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
@@ -224,11 +235,7 @@ class _HomePageViewState extends ConsumerState<_HomePageView> {
|
||||
return index == -1 ? null : index;
|
||||
},
|
||||
itemBuilder: (context, index) {
|
||||
return ExcludeFocus(
|
||||
key: ValueKey(widget.navigationItems[index].label),
|
||||
excluding: index != currentIndex,
|
||||
child: widget.pageBuilder(context, index),
|
||||
);
|
||||
return widget.pageBuilder(context, index);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -594,9 +594,6 @@ class SystemAction extends _$SystemAction {
|
||||
}
|
||||
|
||||
Future<void> handleClose([bool exit = true]) async {
|
||||
if (!system.isDesktop) {
|
||||
if (ref.read(backBlockProvider)) return;
|
||||
}
|
||||
if (ref.read(appSettingProvider).minimizeOnExit || !exit) {
|
||||
if (system.isDesktop) {
|
||||
await preferences.saveConfig(ref.read(configProvider));
|
||||
|
||||
@@ -217,22 +217,6 @@ class CheckIpNum extends _$CheckIpNum with AutoDisposeNotifierMixin {
|
||||
int add() => state++;
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class BackBlock extends _$BackBlock with AutoDisposeNotifierMixin {
|
||||
@override
|
||||
bool build() {
|
||||
return false;
|
||||
}
|
||||
|
||||
void backBlock() {
|
||||
value = true;
|
||||
}
|
||||
|
||||
void unBackBlock() {
|
||||
value = false;
|
||||
}
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
class Version extends _$Version with AutoDisposeNotifierMixin {
|
||||
@override
|
||||
@@ -464,7 +448,6 @@ class LocationPermissions extends _$LocationPermissions
|
||||
List<Override> buildAppStateOverrides(AppState appState) {
|
||||
return [
|
||||
initProvider.overrideWithBuild((_, _) => appState.isInit),
|
||||
backBlockProvider.overrideWithBuild((_, _) => appState.backBlock),
|
||||
currentPageLabelProvider.overrideWithBuild((_, _) => appState.pageLabel),
|
||||
packagesProvider.overrideWithBuild((_, _) => appState.packages),
|
||||
sortNumProvider.overrideWithBuild((_, _) => appState.sortNum),
|
||||
|
||||
@@ -244,7 +244,7 @@ final class SystemActionProvider extends $NotifierProvider<SystemAction, void> {
|
||||
}
|
||||
}
|
||||
|
||||
String _$systemActionHash() => r'04f09af2702925aff276423a9c224fd8d3aca061';
|
||||
String _$systemActionHash() => r'fe481ef6812b1aad42ed3f1a8a2459c9bbd289d9';
|
||||
|
||||
abstract class _$SystemAction extends $Notifier<void> {
|
||||
void build();
|
||||
|
||||
@@ -999,57 +999,6 @@ abstract class _$CheckIpNum extends $Notifier<int> {
|
||||
}
|
||||
}
|
||||
|
||||
@ProviderFor(BackBlock)
|
||||
final backBlockProvider = BackBlockProvider._();
|
||||
|
||||
final class BackBlockProvider extends $NotifierProvider<BackBlock, bool> {
|
||||
BackBlockProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'backBlockProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$backBlockHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
BackBlock create() => BackBlock();
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(bool value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<bool>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$backBlockHash() => r'c867df920425f9b063807ca758dff8d849ca069e';
|
||||
|
||||
abstract class _$BackBlock extends $Notifier<bool> {
|
||||
bool build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
WhenComplete runBuild() {
|
||||
final ref = this.ref as $Ref<bool, bool>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<bool, bool>,
|
||||
bool,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
return element.handleCreate(ref, build);
|
||||
}
|
||||
}
|
||||
|
||||
@ProviderFor(Version)
|
||||
final versionProvider = VersionProvider._();
|
||||
|
||||
|
||||
@@ -69,12 +69,12 @@ class _DashboardViewState extends ConsumerState<DashboardView> {
|
||||
? IconButton(
|
||||
key: const ValueKey(true),
|
||||
icon: const Icon(Icons.save, key: ValueKey('save-icon')),
|
||||
onPressed: _handleUpdateIsEdit,
|
||||
onPressed: _handleSaveAndExit,
|
||||
)
|
||||
: IconButton(
|
||||
key: const ValueKey(false),
|
||||
icon: const Icon(Icons.edit, key: ValueKey('edit-icon')),
|
||||
onPressed: _handleUpdateIsEdit,
|
||||
onPressed: _handleEnterEdit,
|
||||
),
|
||||
),
|
||||
];
|
||||
@@ -102,11 +102,32 @@ class _DashboardViewState extends ConsumerState<DashboardView> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _handleUpdateIsEdit() async {
|
||||
if (_isEditNotifier.value == true) {
|
||||
await _handleSave();
|
||||
void _handleEnterEdit() {
|
||||
if (_isEditNotifier.value) {
|
||||
return;
|
||||
}
|
||||
_isEditNotifier.value = true;
|
||||
}
|
||||
|
||||
void _handleExitEdit() {
|
||||
if (!_isEditNotifier.value) {
|
||||
return;
|
||||
}
|
||||
final dashboardWidgets = _getDashboardWidgets(key.currentState);
|
||||
if (dashboardWidgets != null) {
|
||||
_saveDashboardWidgets(dashboardWidgets);
|
||||
}
|
||||
_isEditNotifier.value = false;
|
||||
}
|
||||
|
||||
Future<void> _handleSaveAndExit() async {
|
||||
if (!_isEditNotifier.value) {
|
||||
return;
|
||||
}
|
||||
await _handleSave();
|
||||
if (mounted) {
|
||||
_isEditNotifier.value = false;
|
||||
}
|
||||
_isEditNotifier.value = !_isEditNotifier.value;
|
||||
}
|
||||
|
||||
Future<void> _handleSave() async {
|
||||
@@ -114,7 +135,7 @@ class _DashboardViewState extends ConsumerState<DashboardView> {
|
||||
if (currentState == null) {
|
||||
return;
|
||||
}
|
||||
if (!mounted || currentState.children.isEmpty) {
|
||||
if (!mounted || currentState.snapshotChildren.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final transformCompleted = await currentState.isTransformCompleter;
|
||||
@@ -124,9 +145,25 @@ class _DashboardViewState extends ConsumerState<DashboardView> {
|
||||
!identical(key.currentState, currentState)) {
|
||||
return;
|
||||
}
|
||||
final dashboardWidgets = currentState.children
|
||||
.map((item) => DashboardWidget.getDashboardWidget(item))
|
||||
.toList();
|
||||
final dashboardWidgets = _getDashboardWidgets(currentState);
|
||||
if (dashboardWidgets == null) {
|
||||
return;
|
||||
}
|
||||
_saveDashboardWidgets(dashboardWidgets);
|
||||
}
|
||||
|
||||
List<DashboardWidget>? _getDashboardWidgets(SuperGridState? currentState) {
|
||||
if (currentState == null) {
|
||||
return null;
|
||||
}
|
||||
final children = currentState.snapshotChildren;
|
||||
if (children.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return children.map(DashboardWidget.getDashboardWidget).toList();
|
||||
}
|
||||
|
||||
void _saveDashboardWidgets(List<DashboardWidget> dashboardWidgets) {
|
||||
ref
|
||||
.read(appSettingProvider.notifier)
|
||||
.update((state) => state.copyWith(dashboardWidgets: dashboardWidgets));
|
||||
@@ -173,21 +210,16 @@ class _DashboardViewState extends ConsumerState<DashboardView> {
|
||||
_maxCrossAxisCount,
|
||||
);
|
||||
return isEdit
|
||||
? SystemBackBlock(
|
||||
child: CommonPopScope(
|
||||
child: SuperGrid(
|
||||
key: key,
|
||||
crossAxisCount: columns,
|
||||
crossAxisSpacing: spacing,
|
||||
mainAxisSpacing: spacing,
|
||||
children: children,
|
||||
onUpdate: () {
|
||||
_handleSave();
|
||||
},
|
||||
),
|
||||
onPop: (context) {
|
||||
_handleUpdateIsEdit();
|
||||
return false;
|
||||
? BackLayerScope(
|
||||
onBack: _handleExitEdit,
|
||||
child: SuperGrid(
|
||||
key: key,
|
||||
crossAxisCount: columns,
|
||||
crossAxisSpacing: spacing,
|
||||
mainAxisSpacing: spacing,
|
||||
children: children,
|
||||
onUpdate: () {
|
||||
_handleSave();
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
@@ -20,7 +20,6 @@ class ProxiesView extends ConsumerStatefulWidget {
|
||||
}
|
||||
|
||||
class _ProxiesViewState extends ConsumerState<ProxiesView> {
|
||||
final GlobalKey<CommonScaffoldState> _scaffoldKey = GlobalKey();
|
||||
final GlobalKey<ProxiesTabViewState> _proxiesTabKey = GlobalKey();
|
||||
bool _hasProviders = false;
|
||||
bool _isTab = false;
|
||||
@@ -122,14 +121,6 @@ class _ProxiesViewState extends ConsumerState<ProxiesView> {
|
||||
},
|
||||
fireImmediately: true,
|
||||
);
|
||||
ref.listenManual(
|
||||
currentPageLabelProvider.select((state) => state == PageLabel.proxies),
|
||||
(prev, next) {
|
||||
if (prev != next && next == false) {
|
||||
_scaffoldKey.currentState?.handleExitSearching();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -139,7 +130,6 @@ class _ProxiesViewState extends ConsumerState<ProxiesView> {
|
||||
);
|
||||
final isLoading = ref.watch(loadingProvider(LoadingTag.proxies));
|
||||
return CommonScaffold(
|
||||
key: _scaffoldKey,
|
||||
isLoading: isLoading,
|
||||
resizeToAvoidBottomInset: false,
|
||||
floatingActionButton: _buildFAB(),
|
||||
|
||||
@@ -2,6 +2,28 @@ import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/widgets/sheet.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class PageActivityScope extends InheritedWidget {
|
||||
final bool isActive;
|
||||
|
||||
const PageActivityScope({
|
||||
super.key,
|
||||
required this.isActive,
|
||||
required super.child,
|
||||
});
|
||||
|
||||
static bool isActiveOf(BuildContext context) {
|
||||
return context
|
||||
.dependOnInheritedWidgetOfExactType<PageActivityScope>()
|
||||
?.isActive ??
|
||||
true;
|
||||
}
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(PageActivityScope oldWidget) {
|
||||
return isActive != oldWidget.isActive;
|
||||
}
|
||||
}
|
||||
|
||||
class CommonScaffoldBackActionProvider extends InheritedWidget {
|
||||
final VoidCallback? backAction;
|
||||
|
||||
|
||||
+76
-15
@@ -1,9 +1,8 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fl_clash/providers/app.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'inherited.dart';
|
||||
|
||||
class CommonPopScope extends StatelessWidget {
|
||||
final Widget child;
|
||||
@@ -19,8 +18,10 @@ class CommonPopScope extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final route = ModalRoute.of(context);
|
||||
final hasBackLayer = route?.willHandlePopInternally == true;
|
||||
return PopScope(
|
||||
canPop: onPop == null ? true : false,
|
||||
canPop: onPop == null || hasBackLayer,
|
||||
onPopInvokedWithResult: onPop == null
|
||||
? null
|
||||
: (didPop, _) async {
|
||||
@@ -44,30 +45,90 @@ class CommonPopScope extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class SystemBackBlock extends ConsumerStatefulWidget {
|
||||
class BackLayerScope extends StatefulWidget {
|
||||
final Widget child;
|
||||
final VoidCallback onBack;
|
||||
@visibleForTesting
|
||||
final void Function(void Function(Duration) callback)?
|
||||
schedulePostFrameCallback;
|
||||
|
||||
const SystemBackBlock({super.key, required this.child});
|
||||
const BackLayerScope({
|
||||
super.key,
|
||||
required this.onBack,
|
||||
required this.child,
|
||||
@visibleForTesting this.schedulePostFrameCallback,
|
||||
});
|
||||
|
||||
@override
|
||||
ConsumerState<SystemBackBlock> createState() => _SystemBackBlockState();
|
||||
State<BackLayerScope> createState() => _BackLayerScopeState();
|
||||
}
|
||||
|
||||
class _SystemBackBlockState extends ConsumerState<SystemBackBlock> {
|
||||
class _BackLayerScopeState extends State<BackLayerScope> {
|
||||
ModalRoute<dynamic>? _route;
|
||||
LocalHistoryEntry? _entry;
|
||||
bool _isDetaching = false;
|
||||
bool _isPageActive = true;
|
||||
int _syncRevision = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
globalState.container.read(backBlockProvider.notifier).backBlock();
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
final route = ModalRoute.of(context);
|
||||
final isPageActive = PageActivityScope.isActiveOf(context);
|
||||
if (identical(_route, route) && _isPageActive == isPageActive) {
|
||||
return;
|
||||
}
|
||||
_detach();
|
||||
_route = route;
|
||||
_isPageActive = isPageActive;
|
||||
final revision = ++_syncRevision;
|
||||
final schedulePostFrameCallback =
|
||||
widget.schedulePostFrameCallback ??
|
||||
WidgetsBinding.instance.addPostFrameCallback;
|
||||
schedulePostFrameCallback((_) {
|
||||
if (!mounted || revision != _syncRevision) {
|
||||
return;
|
||||
}
|
||||
if (!_isPageActive) {
|
||||
widget.onBack();
|
||||
return;
|
||||
}
|
||||
if (route == null) {
|
||||
return;
|
||||
}
|
||||
final entry = LocalHistoryEntry(
|
||||
impliesAppBarDismissal: false,
|
||||
onRemove: _handleRemove,
|
||||
);
|
||||
_entry = entry;
|
||||
route.addLocalHistoryEntry(entry);
|
||||
});
|
||||
}
|
||||
|
||||
void _handleRemove() {
|
||||
_entry = null;
|
||||
if (!_isDetaching && mounted) {
|
||||
widget.onBack();
|
||||
}
|
||||
}
|
||||
|
||||
void _detach() {
|
||||
final entry = _entry;
|
||||
if (entry == null) {
|
||||
return;
|
||||
}
|
||||
_entry = null;
|
||||
_isDetaching = true;
|
||||
entry.remove();
|
||||
_isDetaching = false;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_syncRevision++;
|
||||
_detach();
|
||||
_route = null;
|
||||
super.dispose();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
globalState.container.read(backBlockProvider.notifier).unBackBlock();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
+18
-16
@@ -139,7 +139,7 @@ class CommonScaffoldState extends State<CommonScaffold> {
|
||||
_handleClearInput();
|
||||
return;
|
||||
}
|
||||
_updateSearchState((state) => state?.copyWith(query: null));
|
||||
_popAppBarLayer();
|
||||
}
|
||||
|
||||
void handleExitSearching() {
|
||||
@@ -150,6 +150,20 @@ class CommonScaffoldState extends State<CommonScaffold> {
|
||||
_updateSearchState((state) => state?.copyWith(query: null));
|
||||
}
|
||||
|
||||
void _handleExitAppBarLayer() {
|
||||
handleExitSearching();
|
||||
if (_isEdit) {
|
||||
_appBarState.value.editState?.onExit();
|
||||
}
|
||||
}
|
||||
|
||||
void _popAppBarLayer() {
|
||||
if (!_isEdit && !_isSearch) {
|
||||
return;
|
||||
}
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_appBarState.dispose();
|
||||
@@ -177,13 +191,13 @@ class CommonScaffoldState extends State<CommonScaffold> {
|
||||
Widget? _buildLeading(VoidCallback? backAction) {
|
||||
if (_isEdit) {
|
||||
return IconButton(
|
||||
onPressed: _appBarState.value.editState?.onExit,
|
||||
onPressed: _popAppBarLayer,
|
||||
icon: const Icon(Icons.close),
|
||||
);
|
||||
}
|
||||
if (_isSearch) {
|
||||
return IconButton(
|
||||
onPressed: handleExitSearching,
|
||||
onPressed: _popAppBarLayer,
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
);
|
||||
}
|
||||
@@ -244,19 +258,7 @@ class CommonScaffoldState extends State<CommonScaffold> {
|
||||
Widget _buildAppBarWrap(Widget child) {
|
||||
final appBar = _isSearch ? _buildSearchingAppBarTheme(child) : child;
|
||||
if (_isEdit || _isSearch) {
|
||||
return SystemBackBlock(
|
||||
child: CommonPopScope(
|
||||
onPop: (context) {
|
||||
if (_isEdit || _isSearch) {
|
||||
handleExitSearching();
|
||||
_appBarState.value.editState?.onExit();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
child: appBar,
|
||||
),
|
||||
);
|
||||
return BackLayerScope(onBack: _handleExitAppBarLayer, child: appBar);
|
||||
}
|
||||
return appBar;
|
||||
}
|
||||
|
||||
@@ -37,6 +37,10 @@ class SuperGridState extends State<SuperGrid> with TickerProviderStateMixin {
|
||||
|
||||
late final ValueNotifier<List<GridItem>> _childrenNotifier;
|
||||
List<GridItem> children = [];
|
||||
List<GridItem>? _pendingChildren;
|
||||
|
||||
List<GridItem> get snapshotChildren =>
|
||||
List<GridItem>.unmodifiable(_pendingChildren ?? children);
|
||||
|
||||
int get length => _childrenNotifier.value.length;
|
||||
List<int> _tempIndexList = [];
|
||||
@@ -355,13 +359,16 @@ class SuperGridState extends State<SuperGrid> with TickerProviderStateMixin {
|
||||
_preTransformState();
|
||||
final indexWhere = _tempIndexList.indexWhere((i) => i == index);
|
||||
_tempIndexList.removeAt(indexWhere);
|
||||
final nextChildren = List<GridItem>.from(_childrenNotifier.value)
|
||||
..removeAt(index);
|
||||
_pendingChildren = nextChildren;
|
||||
final completed = await _transform();
|
||||
if (!completed || !mounted) {
|
||||
_pendingChildren = null;
|
||||
return;
|
||||
}
|
||||
final children = List<GridItem>.from(_childrenNotifier.value);
|
||||
children.removeAt(index);
|
||||
_childrenNotifier.value = children;
|
||||
_childrenNotifier.value = nextChildren;
|
||||
_pendingChildren = null;
|
||||
_initState();
|
||||
}
|
||||
|
||||
@@ -606,6 +613,7 @@ class SuperGridState extends State<SuperGrid> with TickerProviderStateMixin {
|
||||
_childrenNotifier.removeListener(_handleChildrenChanged);
|
||||
_childrenNotifier.value = const [];
|
||||
children = const [];
|
||||
_pendingChildren = null;
|
||||
_itemContexts = [];
|
||||
_sizes = [];
|
||||
_offsets = [];
|
||||
|
||||
@@ -11,6 +11,7 @@ import 'package:fl_clash/providers/providers.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/views/application_setting.dart';
|
||||
import 'package:fl_clash/views/tools.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
@@ -547,6 +548,144 @@ void main() {
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testWidgets('switching home pages exits a generic search layer', (
|
||||
tester,
|
||||
) async {
|
||||
tester.view.physicalSize = const Size(500, 800);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
|
||||
var query = '';
|
||||
final container = ProviderContainer(
|
||||
overrides: [
|
||||
navigationItemsStateProvider.overrideWithValue(
|
||||
NavigationItemsState(
|
||||
value: [
|
||||
NavigationItem(
|
||||
icon: const Icon(Icons.space_dashboard),
|
||||
label: PageLabel.dashboard,
|
||||
builder: (_) => CommonScaffold(
|
||||
title: 'Search page',
|
||||
searchState: AppBarSearchState(
|
||||
onSearch: (value) {
|
||||
query = value;
|
||||
},
|
||||
),
|
||||
body: const SizedBox(),
|
||||
),
|
||||
),
|
||||
NavigationItem(
|
||||
icon: const Icon(Icons.construction),
|
||||
label: PageLabel.tools,
|
||||
builder: (_) => const SizedBox.shrink(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
addTearDown(container.dispose);
|
||||
globalState.container = container;
|
||||
container.read(viewSizeProvider.notifier).value = const Size(500, 800);
|
||||
|
||||
await tester.pumpWidget(
|
||||
UncontrolledProviderScope(
|
||||
container: container,
|
||||
child: const _TestApp(child: HomePage()),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
await tester.tap(find.byIcon(Icons.search));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(TextField), findsOneWidget);
|
||||
await tester.enterText(find.byType(TextField), 'needle');
|
||||
expect(query, 'needle');
|
||||
|
||||
await tester.tap(find.byIcon(Icons.construction));
|
||||
await tester.pumpAndSettle();
|
||||
expect(query, isEmpty);
|
||||
await tester.tap(find.byIcon(Icons.space_dashboard));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(TextField), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('desktop nested route inherits home page activity', (
|
||||
tester,
|
||||
) async {
|
||||
tester.view.physicalSize = const Size(1200, 800);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
|
||||
var query = '';
|
||||
final container = ProviderContainer(
|
||||
overrides: [
|
||||
navigationItemsStateProvider.overrideWithValue(
|
||||
NavigationItemsState(
|
||||
value: [
|
||||
NavigationItem(
|
||||
icon: const Icon(Icons.space_dashboard),
|
||||
label: PageLabel.dashboard,
|
||||
builder: (_) => _NestedSearchLauncher(
|
||||
onSearch: (value) {
|
||||
query = value;
|
||||
},
|
||||
),
|
||||
),
|
||||
NavigationItem(
|
||||
icon: const Icon(Icons.construction),
|
||||
label: PageLabel.tools,
|
||||
builder: (_) => const SizedBox.shrink(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
addTearDown(container.dispose);
|
||||
globalState.container = container;
|
||||
container.read(viewSizeProvider.notifier).value = const Size(1200, 800);
|
||||
|
||||
await tester.pumpWidget(
|
||||
UncontrolledProviderScope(
|
||||
container: container,
|
||||
child: const _TestApp(child: HomePage()),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
await tester.tap(find.text('Open nested search'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byIcon(Icons.search));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.enterText(find.byType(TextField), 'needle');
|
||||
expect(query, 'needle');
|
||||
|
||||
final navigationRail = find.byType(NavigationRail);
|
||||
await tester.tap(
|
||||
find.descendant(
|
||||
of: navigationRail,
|
||||
matching: find.byIcon(Icons.construction),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(query, isEmpty);
|
||||
|
||||
await tester.tap(
|
||||
find.descendant(
|
||||
of: navigationRail,
|
||||
matching: find.byIcon(Icons.space_dashboard),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text('Nested search'), findsOneWidget);
|
||||
expect(find.byType(TextField), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'desktop tabbing past page content does not scroll the PageView',
|
||||
(tester) async {
|
||||
@@ -711,6 +850,32 @@ class _StatefulContentState extends State<_StatefulContent> {
|
||||
}
|
||||
}
|
||||
|
||||
class _NestedSearchLauncher extends StatelessWidget {
|
||||
final ValueChanged<String> onSearch;
|
||||
|
||||
const _NestedSearchLauncher({required this.onSearch});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: FilledButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => CommonScaffold(
|
||||
title: 'Nested search',
|
||||
searchState: AppBarSearchState(onSearch: onSearch),
|
||||
body: const SizedBox(),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text('Open nested search'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _HomeTestProfiles extends Profiles {
|
||||
final List<Profile> initial;
|
||||
|
||||
|
||||
@@ -219,20 +219,6 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
group('BackBlock provider', () {
|
||||
test('default is false', () {
|
||||
expect(container.read(backBlockProvider), false);
|
||||
});
|
||||
|
||||
test('can block and unblock back navigation', () {
|
||||
container.read(backBlockProvider.notifier).backBlock();
|
||||
expect(container.read(backBlockProvider), true);
|
||||
|
||||
container.read(backBlockProvider.notifier).unBackBlock();
|
||||
expect(container.read(backBlockProvider), false);
|
||||
});
|
||||
});
|
||||
|
||||
group('Version provider', () {
|
||||
test('default is 0', () {
|
||||
expect(container.read(versionProvider), 0);
|
||||
|
||||
@@ -0,0 +1,453 @@
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/common/theme.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/l10n/l10n.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/providers/providers.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/views/access.dart';
|
||||
import 'package:fl_clash/views/dashboard/dashboard.dart';
|
||||
import 'package:fl_clash/views/logs.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('back layers are consumed from inner to outer', (tester) async {
|
||||
var innerActive = true;
|
||||
var outerActive = true;
|
||||
var innerBackCount = 0;
|
||||
var outerBackCount = 0;
|
||||
var rootBackCount = 0;
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: CommonPopScope(
|
||||
onPop: (_) {
|
||||
rootBackCount++;
|
||||
return false;
|
||||
},
|
||||
child: StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
Widget child = const SizedBox();
|
||||
if (innerActive) {
|
||||
child = BackLayerScope(
|
||||
onBack: () {
|
||||
innerBackCount++;
|
||||
setState(() {
|
||||
innerActive = false;
|
||||
});
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
if (outerActive) {
|
||||
child = BackLayerScope(
|
||||
onBack: () {
|
||||
outerBackCount++;
|
||||
setState(() {
|
||||
outerActive = false;
|
||||
});
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
return child;
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.binding.handlePopRoute();
|
||||
await tester.pumpAndSettle();
|
||||
expect((innerBackCount, outerBackCount, rootBackCount), (1, 0, 0));
|
||||
|
||||
await tester.binding.handlePopRoute();
|
||||
await tester.pumpAndSettle();
|
||||
expect((innerBackCount, outerBackCount, rootBackCount), (1, 1, 0));
|
||||
|
||||
await tester.binding.handlePopRoute();
|
||||
await tester.pump();
|
||||
expect((innerBackCount, outerBackCount, rootBackCount), (1, 1, 1));
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'a pending inactive sync is cancelled when the page reactivates',
|
||||
(tester) async {
|
||||
final isActive = ValueNotifier(true);
|
||||
addTearDown(isActive.dispose);
|
||||
final pendingCallbacks = <void Function(Duration)>[];
|
||||
var backCount = 0;
|
||||
var rootBackCount = 0;
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: CommonPopScope(
|
||||
onPop: (_) {
|
||||
rootBackCount++;
|
||||
return false;
|
||||
},
|
||||
child: _PageActivityTestScope(
|
||||
isActive: isActive,
|
||||
child: BackLayerScope(
|
||||
onBack: () {
|
||||
backCount++;
|
||||
},
|
||||
schedulePostFrameCallback: pendingCallbacks.add,
|
||||
child: const SizedBox(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
expect(pendingCallbacks, hasLength(1));
|
||||
pendingCallbacks.removeAt(0)(Duration.zero);
|
||||
await tester.pump();
|
||||
|
||||
isActive.value = false;
|
||||
await tester.pump();
|
||||
isActive.value = true;
|
||||
await tester.pump();
|
||||
expect(pendingCallbacks, hasLength(2));
|
||||
|
||||
for (final callback in pendingCallbacks.toList()) {
|
||||
callback(Duration.zero);
|
||||
}
|
||||
pendingCallbacks.clear();
|
||||
await tester.pump();
|
||||
expect(backCount, 0);
|
||||
|
||||
await tester.binding.handlePopRoute();
|
||||
await tester.pump();
|
||||
expect(backCount, 1);
|
||||
expect(rootBackCount, 0);
|
||||
|
||||
await tester.binding.handlePopRoute();
|
||||
await tester.pump();
|
||||
expect(backCount, 1);
|
||||
expect(rootBackCount, 1);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('disposing a back layer does not invoke its callback', (
|
||||
tester,
|
||||
) async {
|
||||
final showLayer = ValueNotifier(true);
|
||||
addTearDown(showLayer.dispose);
|
||||
var backCount = 0;
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: ValueListenableBuilder(
|
||||
valueListenable: showLayer,
|
||||
builder: (_, value, _) {
|
||||
if (!value) {
|
||||
return const SizedBox();
|
||||
}
|
||||
return BackLayerScope(
|
||||
onBack: () {
|
||||
backCount++;
|
||||
},
|
||||
child: const SizedBox(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
showLayer.value = false;
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(backCount, 0);
|
||||
});
|
||||
|
||||
testWidgets('system back exits search without reaching the root fallback', (
|
||||
tester,
|
||||
) async {
|
||||
final container = ProviderContainer();
|
||||
addTearDown(container.dispose);
|
||||
globalState.container = container;
|
||||
var rootBackCount = 0;
|
||||
|
||||
await tester.pumpWidget(
|
||||
UncontrolledProviderScope(
|
||||
container: container,
|
||||
child: MaterialApp(
|
||||
localizationsDelegates: const [
|
||||
AppLocalizations.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
],
|
||||
supportedLocales: AppLocalizations.delegate.supportedLocales,
|
||||
home: CommonPopScope(
|
||||
onPop: (_) {
|
||||
rootBackCount++;
|
||||
return false;
|
||||
},
|
||||
child: CommonScaffold(
|
||||
title: 'Logs',
|
||||
searchState: AppBarSearchState(onSearch: (_) {}),
|
||||
body: const SizedBox(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.byIcon(Icons.search));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(TextField), findsOneWidget);
|
||||
|
||||
await tester.binding.handlePopRoute();
|
||||
await tester.pump();
|
||||
|
||||
expect(find.byType(TextField), findsNothing);
|
||||
expect(rootBackCount, 0);
|
||||
});
|
||||
|
||||
testWidgets('inactive page scope exits the kept search layer', (
|
||||
tester,
|
||||
) async {
|
||||
final container = ProviderContainer();
|
||||
addTearDown(container.dispose);
|
||||
globalState.container = container;
|
||||
final isActive = ValueNotifier(true);
|
||||
addTearDown(isActive.dispose);
|
||||
|
||||
await tester.pumpWidget(
|
||||
UncontrolledProviderScope(
|
||||
container: container,
|
||||
child: MaterialApp(
|
||||
localizationsDelegates: const [
|
||||
AppLocalizations.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
],
|
||||
supportedLocales: AppLocalizations.delegate.supportedLocales,
|
||||
home: _PageActivityTestScope(
|
||||
isActive: isActive,
|
||||
child: const LogsView(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.byIcon(Icons.search));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(TextField), findsOneWidget);
|
||||
|
||||
isActive.value = false;
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(TextField), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('inactive page scope exits dashboard edit layer', (tester) async {
|
||||
final container = ProviderContainer(
|
||||
overrides: [
|
||||
dashboardStateProvider.overrideWithValue(
|
||||
const DashboardState(dashboardWidgets: []),
|
||||
),
|
||||
],
|
||||
);
|
||||
addTearDown(container.dispose);
|
||||
globalState.container = container;
|
||||
final isActive = ValueNotifier(true);
|
||||
addTearDown(isActive.dispose);
|
||||
|
||||
await tester.pumpWidget(
|
||||
UncontrolledProviderScope(
|
||||
container: container,
|
||||
child: _DashboardTestApp(
|
||||
child: _PageActivityTestScope(
|
||||
isActive: isActive,
|
||||
child: const DashboardView(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
await tester.tap(find.byKey(const ValueKey('edit-icon')));
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
expect(find.byKey(const ValueKey('save-icon')), findsOneWidget);
|
||||
|
||||
isActive.value = false;
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
|
||||
expect(find.byKey(const ValueKey('edit-icon')), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('inactive page scope exits access search layer', (tester) async {
|
||||
final container = ProviderContainer();
|
||||
addTearDown(container.dispose);
|
||||
globalState.container = container;
|
||||
final isActive = ValueNotifier(true);
|
||||
addTearDown(isActive.dispose);
|
||||
|
||||
await tester.pumpWidget(
|
||||
UncontrolledProviderScope(
|
||||
container: container,
|
||||
child: _DashboardTestApp(
|
||||
child: _PageActivityTestScope(
|
||||
isActive: isActive,
|
||||
child: const AccessView(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump(const Duration(milliseconds: 301));
|
||||
|
||||
await tester.tap(find.byIcon(Icons.more_vert));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.byIcon(Icons.search));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(TextField), findsOneWidget);
|
||||
|
||||
isActive.value = false;
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(TextField), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('save and system back cannot re-enter dashboard edit mode', (
|
||||
tester,
|
||||
) async {
|
||||
final container = ProviderContainer(
|
||||
overrides: [
|
||||
dashboardStateProvider.overrideWithValue(
|
||||
const DashboardState(dashboardWidgets: []),
|
||||
),
|
||||
],
|
||||
);
|
||||
addTearDown(container.dispose);
|
||||
globalState.container = container;
|
||||
|
||||
await tester.pumpWidget(
|
||||
UncontrolledProviderScope(
|
||||
container: container,
|
||||
child: const _DashboardTestApp(),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
await tester.tap(find.byKey(const ValueKey('edit-icon')));
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
expect(find.byKey(const ValueKey('save-icon')), findsOneWidget);
|
||||
|
||||
tester.widget<IconButton>(find.byKey(const ValueKey(true))).onPressed!();
|
||||
await tester.binding.handlePopRoute();
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
|
||||
expect(find.byKey(const ValueKey('edit-icon')), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('system back preserves a pending dashboard deletion', (
|
||||
tester,
|
||||
) async {
|
||||
final container = ProviderContainer(
|
||||
overrides: [
|
||||
dashboardStateProvider.overrideWithValue(
|
||||
const DashboardState(
|
||||
dashboardWidgets: [
|
||||
DashboardWidget.networkSpeed,
|
||||
DashboardWidget.outboundModeV2,
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
addTearDown(container.dispose);
|
||||
final appSettingSubscription = container.listen(
|
||||
appSettingProvider,
|
||||
(_, _) {},
|
||||
fireImmediately: true,
|
||||
);
|
||||
addTearDown(appSettingSubscription.close);
|
||||
globalState.container = container;
|
||||
|
||||
await tester.pumpWidget(
|
||||
UncontrolledProviderScope(
|
||||
container: container,
|
||||
child: const _DashboardTestApp(),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
await tester.tap(find.byKey(const ValueKey('edit-icon')));
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
|
||||
final deleteButton = find.ancestor(
|
||||
of: find.byIcon(Icons.close).first,
|
||||
matching: find.byType(IconButton),
|
||||
);
|
||||
tester.widget<IconButton>(deleteButton).onPressed!();
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 301));
|
||||
await tester.pump();
|
||||
expect(
|
||||
tester.state<SuperGridState>(find.byType(SuperGrid)).snapshotChildren,
|
||||
[DashboardWidget.outboundModeV2.widget],
|
||||
);
|
||||
await tester.binding.handlePopRoute();
|
||||
await tester.pump(const Duration(milliseconds: 500));
|
||||
|
||||
expect(find.byKey(const ValueKey('edit-icon')), findsOneWidget);
|
||||
expect(container.read(appSettingProvider).dashboardWidgets, [
|
||||
DashboardWidget.outboundModeV2,
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
class _DashboardTestApp extends StatelessWidget {
|
||||
final Widget child;
|
||||
|
||||
const _DashboardTestApp({this.child = const DashboardView()});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
localizationsDelegates: const [
|
||||
AppLocalizations.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
],
|
||||
supportedLocales: AppLocalizations.delegate.supportedLocales,
|
||||
builder: (context, child) {
|
||||
globalState.measure = Measure.of(context, 1);
|
||||
globalState.theme = CommonTheme.of(context, 1);
|
||||
return child!;
|
||||
},
|
||||
home: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PageActivityTestScope extends StatelessWidget {
|
||||
final ValueNotifier<bool> isActive;
|
||||
final Widget child;
|
||||
|
||||
const _PageActivityTestScope({required this.isActive, required this.child});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: isActive,
|
||||
builder: (_, value, child) {
|
||||
return PageActivityScope(isActive: value, child: child!);
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/l10n/l10n.dart';
|
||||
import 'package:fl_clash/manager/hotkey_manager.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/pages/home.dart';
|
||||
import 'package:fl_clash/providers/providers.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('desktop escape runs the back flow and exits search', (
|
||||
tester,
|
||||
) async {
|
||||
debugDefaultTargetPlatformOverride = TargetPlatform.macOS;
|
||||
final container = ProviderContainer();
|
||||
addTearDown(container.dispose);
|
||||
globalState.container = container;
|
||||
|
||||
await tester.pumpWidget(
|
||||
UncontrolledProviderScope(
|
||||
container: container,
|
||||
child: MaterialApp(
|
||||
navigatorKey: globalState.navigatorKey,
|
||||
localizationsDelegates: const [
|
||||
AppLocalizations.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
],
|
||||
supportedLocales: AppLocalizations.delegate.supportedLocales,
|
||||
home: HotKeyManager(
|
||||
child: CommonScaffold(
|
||||
title: 'Logs',
|
||||
searchState: AppBarSearchState(onSearch: (_) {}),
|
||||
body: Center(
|
||||
child: TextButton(
|
||||
onPressed: () {},
|
||||
child: const Text('focus target'),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.tap(find.byIcon(Icons.search));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(TextField), findsOneWidget);
|
||||
|
||||
// Move focus off the text field: EditableText's own shortcuts swallow
|
||||
// escape on some platforms, so the escape binding applies outside the
|
||||
// text input. (tap does not focus widgets inside a scope in tests.)
|
||||
Focus.of(tester.element(find.text('focus target'))).requestFocus();
|
||||
await tester.pump();
|
||||
expect(
|
||||
FocusManager.instance.primaryFocus?.context
|
||||
?.findAncestorWidgetOfExactType<TextField>(),
|
||||
isNull,
|
||||
);
|
||||
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
find.byType(TextField),
|
||||
findsNothing,
|
||||
reason: 'escape must exit search through the back flow',
|
||||
);
|
||||
debugDefaultTargetPlatformOverride = null;
|
||||
});
|
||||
|
||||
testWidgets('TV back exits search on the desktop nested route', (
|
||||
tester,
|
||||
) async {
|
||||
tester.view.physicalSize = const Size(1200, 800);
|
||||
tester.view.devicePixelRatio = 1;
|
||||
addTearDown(tester.view.resetPhysicalSize);
|
||||
addTearDown(tester.view.resetDevicePixelRatio);
|
||||
|
||||
var query = '';
|
||||
var closeCount = 0;
|
||||
final container = ProviderContainer(
|
||||
overrides: [
|
||||
navigationItemsStateProvider.overrideWithValue(
|
||||
NavigationItemsState(
|
||||
value: [
|
||||
NavigationItem(
|
||||
icon: const Icon(Icons.space_dashboard),
|
||||
label: PageLabel.dashboard,
|
||||
builder: (_) => CommonScaffold(
|
||||
key: const GlobalObjectKey(PageLabel.dashboard),
|
||||
title: 'Search page',
|
||||
searchState: AppBarSearchState(
|
||||
onSearch: (value) {
|
||||
query = value;
|
||||
},
|
||||
),
|
||||
body: const SizedBox(),
|
||||
),
|
||||
),
|
||||
NavigationItem(
|
||||
icon: const Icon(Icons.construction),
|
||||
label: PageLabel.tools,
|
||||
builder: (_) => const SizedBox.shrink(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
systemActionProvider.overrideWith(
|
||||
() => _TestSystemAction(() => closeCount++),
|
||||
),
|
||||
],
|
||||
);
|
||||
addTearDown(container.dispose);
|
||||
globalState.container = container;
|
||||
container.read(viewSizeProvider.notifier).value = const Size(1200, 800);
|
||||
|
||||
await tester.pumpWidget(
|
||||
UncontrolledProviderScope(
|
||||
container: container,
|
||||
child: MaterialApp(
|
||||
localizationsDelegates: const [
|
||||
AppLocalizations.delegate,
|
||||
GlobalMaterialLocalizations.delegate,
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
GlobalWidgetsLocalizations.delegate,
|
||||
],
|
||||
supportedLocales: AppLocalizations.delegate.supportedLocales,
|
||||
home: const HomePage(),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pump();
|
||||
expect(find.byType(NavigationRail), findsOneWidget);
|
||||
|
||||
await tester.tap(find.byIcon(Icons.search));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(TextField), findsOneWidget);
|
||||
await tester.enterText(find.byType(TextField), 'needle');
|
||||
expect(query, 'needle');
|
||||
|
||||
await tester.binding.handlePopRoute();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(closeCount, 0, reason: 'back must not fall through to app close');
|
||||
expect(find.byType(TextField), findsNothing);
|
||||
expect(query, isEmpty);
|
||||
});
|
||||
}
|
||||
|
||||
class _TestSystemAction extends SystemAction {
|
||||
final int Function() closeCount;
|
||||
|
||||
_TestSystemAction(this.closeCount);
|
||||
|
||||
@override
|
||||
void build() {}
|
||||
|
||||
@override
|
||||
Future<void> handleClose([bool exit = true]) async {
|
||||
closeCount();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user