From ef97ef40a1898b180841e3fe5bc2bc271ba9f95e Mon Sep 17 00:00:00 2001 From: chen08209 Date: Mon, 9 Dec 2024 01:40:39 +0800 Subject: [PATCH] Remake dashboard Optimize theme Optimize more details Update flutter version --- .gitignore | 2 + core/common.go | 2 +- core/constant.go | 2 + core/hub.go | 22 + core/lib.go | 17 + core/server.go | 11 + lib/application.dart | 176 ++-- lib/clash/core.dart | 16 + lib/clash/generated/clash_ffi.dart | 57 +- lib/clash/interface.dart | 4 + lib/clash/lib.dart | 33 + lib/clash/service.dart | 29 +- lib/common/color.dart | 43 +- lib/common/constant.dart | 9 + lib/common/context.dart | 8 +- lib/common/function.dart | 43 +- lib/common/http.dart | 2 +- lib/common/launch.dart | 4 +- lib/common/navigator.dart | 242 ++++- lib/common/num.dart | 40 +- lib/common/other.dart | 41 +- lib/common/request.dart | 27 +- lib/common/text.dart | 8 +- lib/controller.dart | 82 +- lib/enum/enum.dart | 119 +++ lib/fragments/access.dart | 16 +- lib/fragments/backup_and_recovery.dart | 2 +- lib/fragments/connections.dart | 5 +- lib/fragments/dashboard/dashboard.dart | 141 ++- lib/fragments/dashboard/intranet_ip.dart | 140 --- .../dashboard/network_detection.dart | 233 ----- lib/fragments/dashboard/network_speed.dart | 166 ---- lib/fragments/dashboard/outbound_mode.dart | 64 -- lib/fragments/dashboard/status_button.dart | 131 --- lib/fragments/dashboard/traffic_usage.dart | 95 -- .../dashboard/{ => widgets}/core_info.dart | 0 .../dashboard/widgets/intranet_ip.dart | 64 ++ .../dashboard/widgets/memory_info.dart | 111 +++ .../dashboard/widgets/network_detection.dart | 250 +++++ .../dashboard/widgets/network_speed.dart | 124 +++ .../dashboard/widgets/outbound_mode.dart | 73 ++ .../dashboard/widgets/quick_options.dart | 156 +++ .../dashboard/{ => widgets}/start_button.dart | 0 .../dashboard/widgets/traffic_usage.dart | 220 +++++ lib/fragments/dashboard/widgets/widgets.dart | 7 + lib/fragments/logs.dart | 7 +- lib/fragments/profiles/profiles.dart | 12 +- lib/fragments/profiles/view_profile.dart | 2 +- lib/fragments/proxies/card.dart | 21 +- lib/fragments/proxies/list.dart | 13 +- lib/fragments/proxies/providers.dart | 6 +- lib/fragments/proxies/proxies.dart | 15 +- lib/fragments/proxies/tab.dart | 87 +- lib/fragments/requests.dart | 2 +- lib/l10n/arb/intl_en.arb | 3 +- lib/l10n/arb/intl_zh_CN.arb | 3 +- lib/l10n/intl/messages_en.dart | 1 + lib/l10n/intl/messages_zh_CN.dart | 1 + lib/l10n/l10n.dart | 10 + lib/manager/clash_manager.dart | 20 +- lib/manager/connectivity_manager.dart | 43 + lib/manager/manager.dart | 4 +- lib/manager/message_manager.dart | 326 +++++++ lib/manager/vpn_manager.dart | 32 +- lib/manager/window_manager.dart | 26 +- lib/models/app.dart | 12 +- lib/models/common.dart | 10 +- lib/models/config.dart | 28 + lib/models/generated/config.freezed.dart | 42 +- lib/models/generated/config.g.dart | 17 + lib/models/generated/core.g.dart | 2 + lib/models/generated/selector.freezed.dart | 292 +++--- lib/models/generated/widget.freezed.dart | 312 ++++++ lib/models/models.dart | 1 + lib/models/selector.dart | 15 +- lib/models/widget.dart | 19 + lib/pages/home.dart | 219 +++-- lib/pages/scan.dart | 13 +- lib/plugins/app.dart | 2 +- lib/state.dart | 43 +- lib/widgets/activate_box.dart | 20 + lib/widgets/bar_chart.dart | 148 +++ lib/widgets/builder.dart | 21 +- lib/widgets/card.dart | 52 +- lib/widgets/donut_chart.dart | 175 ++++ lib/widgets/grid.dart | 40 +- lib/widgets/icon.dart | 4 +- lib/widgets/line_chart.dart | 227 ++--- lib/widgets/list.dart | 1 + lib/widgets/scaffold.dart | 125 +-- lib/widgets/sheet.dart | 8 +- lib/widgets/subscription_info_view.dart | 2 +- lib/widgets/super_grid.dart | 889 ++++++++++++++++++ lib/widgets/text.dart | 2 +- lib/widgets/wave.dart | 108 +++ lib/widgets/widgets.dart | 4 + macos/Podfile.lock | 2 +- macos/Runner/AppDelegate.swift | 4 + pubspec.lock | 214 +++-- pubspec.yaml | 3 +- setup.dart | 45 +- 101 files changed, 4951 insertions(+), 1841 deletions(-) delete mode 100644 lib/fragments/dashboard/intranet_ip.dart delete mode 100644 lib/fragments/dashboard/network_detection.dart delete mode 100644 lib/fragments/dashboard/network_speed.dart delete mode 100644 lib/fragments/dashboard/outbound_mode.dart delete mode 100644 lib/fragments/dashboard/status_button.dart delete mode 100644 lib/fragments/dashboard/traffic_usage.dart rename lib/fragments/dashboard/{ => widgets}/core_info.dart (100%) create mode 100644 lib/fragments/dashboard/widgets/intranet_ip.dart create mode 100644 lib/fragments/dashboard/widgets/memory_info.dart create mode 100644 lib/fragments/dashboard/widgets/network_detection.dart create mode 100644 lib/fragments/dashboard/widgets/network_speed.dart create mode 100644 lib/fragments/dashboard/widgets/outbound_mode.dart create mode 100644 lib/fragments/dashboard/widgets/quick_options.dart rename lib/fragments/dashboard/{ => widgets}/start_button.dart (100%) create mode 100644 lib/fragments/dashboard/widgets/traffic_usage.dart create mode 100644 lib/fragments/dashboard/widgets/widgets.dart create mode 100644 lib/manager/connectivity_manager.dart create mode 100644 lib/manager/message_manager.dart create mode 100644 lib/models/generated/widget.freezed.dart create mode 100644 lib/models/widget.dart create mode 100644 lib/widgets/activate_box.dart create mode 100644 lib/widgets/bar_chart.dart create mode 100644 lib/widgets/donut_chart.dart create mode 100644 lib/widgets/super_grid.dart create mode 100644 lib/widgets/wave.dart diff --git a/.gitignore b/.gitignore index 92a10f2..1fc3c53 100644 --- a/.gitignore +++ b/.gitignore @@ -5,9 +5,11 @@ *.swp .DS_Store .atom/ +.build/ .buildlog/ .history .svn/ +.swiftpm/ migrate_working_dir/ # IntelliJ related diff --git a/core/common.go b/core/common.go index 61cbfda..d9c9329 100644 --- a/core/common.go +++ b/core/common.go @@ -33,7 +33,7 @@ import ( var ( isRunning = false runLock sync.Mutex - ips = []string{"ipinfo.io", "ipapi.co", "api.ip.sb", "ipwho.is"} + ips = []string{"ipwho.is", "ifconfig.me", "icanhazip.com", "api.ip.sb", "ipinfo.io"} b, _ = batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](50)) ) diff --git a/core/constant.go b/core/constant.go index e39fea3..f00c75f 100644 --- a/core/constant.go +++ b/core/constant.go @@ -65,6 +65,8 @@ const ( closeConnectionMethod Method = "closeConnection" getExternalProvidersMethod Method = "getExternalProviders" getExternalProviderMethod Method = "getExternalProvider" + getCountryCodeMethod Method = "getCountryCode" + getMemoryMethod Method = "getMemory" updateGeoDataMethod Method = "updateGeoData" updateExternalProviderMethod Method = "updateExternalProvider" sideLoadExternalProviderMethod Method = "sideLoadExternalProvider" diff --git a/core/hub.go b/core/hub.go index c26f102..6ae6e7a 100644 --- a/core/hub.go +++ b/core/hub.go @@ -8,6 +8,7 @@ import ( "github.com/metacubex/mihomo/adapter/outboundgroup" "github.com/metacubex/mihomo/common/observable" "github.com/metacubex/mihomo/common/utils" + "github.com/metacubex/mihomo/component/mmdb" "github.com/metacubex/mihomo/component/updater" "github.com/metacubex/mihomo/config" "github.com/metacubex/mihomo/constant" @@ -17,8 +18,10 @@ import ( "github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/tunnel" "github.com/metacubex/mihomo/tunnel/statistic" + "net" "runtime" "sort" + "strconv" "time" ) @@ -404,6 +407,25 @@ func handleStopLog() { } } +func handleGetCountryCode(ip string, fn func(value string)) { + go func() { + runLock.Lock() + defer runLock.Unlock() + codes := mmdb.IPInstance().LookupCode(net.ParseIP(ip)) + if len(codes) == 0 { + fn("") + return + } + fn(codes[0]) + }() +} + +func handleGetMemory(fn func(value string)) { + go func() { + fn(strconv.FormatUint(statistic.DefaultManager.Memory(), 10)) + }() +} + func init() { adapter.UrlTestHook = func(name string, delay uint16) { delayData := &Delay{ diff --git a/core/lib.go b/core/lib.go index 143e3b6..c5968db 100644 --- a/core/lib.go +++ b/core/lib.go @@ -120,6 +120,14 @@ func getConnections() *C.char { return C.CString(handleGetConnections()) } +//export getMemory +func getMemory(port C.longlong) { + i := int64(port) + handleGetMemory(func(value string) { + bridge.SendToPort(i, value) + }) +} + //export closeConnections func closeConnections() { handleCloseConnections() @@ -161,6 +169,15 @@ func updateExternalProvider(providerNameChar *C.char, port C.longlong) { }) } +//export getCountryCode +func getCountryCode(ipChar *C.char, port C.longlong) { + ip := C.GoString(ipChar) + i := int64(port) + handleGetCountryCode(ip, func(value string) { + bridge.SendToPort(i, value) + }) +} + //export sideLoadExternalProvider func sideLoadExternalProvider(providerNameChar *C.char, dataChar *C.char, port C.longlong) { i := int64(port) diff --git a/core/server.go b/core/server.go index e84080b..dcc0534 100644 --- a/core/server.go +++ b/core/server.go @@ -157,6 +157,17 @@ func handleAction(action *Action) { case stopListenerMethod: action.callback(handleStopListener()) return + case getCountryCodeMethod: + ip := action.Data.(string) + handleGetCountryCode(ip, func(value string) { + action.callback(value) + }) + return + case getMemoryMethod: + handleGetMemory(func(value string) { + action.callback(value) + }) + return } } diff --git a/lib/application.dart b/lib/application.dart index 60fff14..1a7c512 100644 --- a/lib/application.dart +++ b/lib/application.dart @@ -1,6 +1,5 @@ import 'dart:async'; -import 'package:animations/animations.dart'; import 'package:dynamic_color/dynamic_color.dart'; import 'package:fl_clash/clash/clash.dart'; import 'package:fl_clash/common/common.dart'; @@ -59,22 +58,15 @@ class Application extends StatefulWidget { class ApplicationState extends State { late SystemColorSchemes systemColorSchemes; - Timer? timer; + Timer? _autoUpdateGroupTaskTimer; + Timer? _autoUpdateProfilesTaskTimer; final _pageTransitionsTheme = const PageTransitionsTheme( builders: { - TargetPlatform.android: SharedAxisPageTransitionsBuilder( - transitionType: SharedAxisTransitionType.horizontal, - ), - TargetPlatform.windows: SharedAxisPageTransitionsBuilder( - transitionType: SharedAxisTransitionType.horizontal, - ), - TargetPlatform.linux: SharedAxisPageTransitionsBuilder( - transitionType: SharedAxisTransitionType.horizontal, - ), - TargetPlatform.macOS: SharedAxisPageTransitionsBuilder( - transitionType: SharedAxisTransitionType.horizontal, - ), + TargetPlatform.android: CommonPageTransitionsBuilder(), + TargetPlatform.windows: CommonPageTransitionsBuilder(), + TargetPlatform.linux: CommonPageTransitionsBuilder(), + TargetPlatform.macOS: CommonPageTransitionsBuilder(), }, ); @@ -96,7 +88,8 @@ class ApplicationState extends State { @override void initState() { super.initState(); - _initTimer(); + _autoUpdateGroupTask(); + _autoUpdateProfilesTask(); globalState.appController = AppController(context); globalState.measure = Measure.of(context); WidgetsBinding.instance.addPostFrameCallback((timeStamp) async { @@ -110,29 +103,29 @@ class ApplicationState extends State { }); } - _initTimer() { - _cancelTimer(); - timer = Timer.periodic(const Duration(milliseconds: 20000), (_) { + _autoUpdateGroupTask() { + _autoUpdateGroupTaskTimer = Timer(const Duration(milliseconds: 20000), () { WidgetsBinding.instance.addPostFrameCallback((_) { - globalState.appController.updateGroupDebounce(); + globalState.appController.updateGroupsDebounce(); + _autoUpdateGroupTask(); }); }); } - _cancelTimer() { - if (timer != null) { - timer?.cancel(); - timer = null; - } + _autoUpdateProfilesTask() { + _autoUpdateProfilesTaskTimer = Timer(const Duration(seconds: 5), () async { + await globalState.appController.autoUpdateProfiles(); + _autoUpdateProfilesTask(); + }); } - _buildApp(Widget app) { + _buildPlatformWrap(Widget child) { if (system.isDesktop) { return WindowManager( child: TrayManager( child: HotKeyManager( child: ProxyManager( - child: app, + child: child, ), ), ), @@ -140,7 +133,7 @@ class ApplicationState extends State { } return AndroidManager( child: TileManager( - child: app, + child: child, ), ); } @@ -156,6 +149,17 @@ class ApplicationState extends State { ); } + _buildWrap(Widget child) { + return AppStateManager( + child: ClashManager( + child: ConnectivityManager( + onConnectivityChanged: globalState.appController.updateLocalIp, + child: child, + ), + ), + ); + } + _updateSystemColorSchemes( ColorScheme? lightDynamic, ColorScheme? darkDynamic, @@ -171,31 +175,31 @@ class ApplicationState extends State { @override Widget build(context) { - return _buildApp( - AppStateManager( - child: ClashManager( - child: Selector2( - selector: (_, appState, config) => ApplicationSelectorState( - locale: config.appSetting.locale, - themeMode: config.themeProps.themeMode, - primaryColor: config.themeProps.primaryColor, - prueBlack: config.themeProps.prueBlack, - fontFamily: config.themeProps.fontFamily, - ), - builder: (_, state, child) { - return DynamicColorBuilder( - builder: (lightDynamic, darkDynamic) { - _updateSystemColorSchemes(lightDynamic, darkDynamic); - return MaterialApp( - navigatorKey: globalState.navigatorKey, - localizationsDelegates: const [ - AppLocalizations.delegate, - GlobalMaterialLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - GlobalWidgetsLocalizations.delegate - ], - builder: (_, child) { - return LayoutBuilder( + return _buildWrap( + _buildPlatformWrap( + Selector2( + selector: (_, appState, config) => ApplicationSelectorState( + locale: config.appSetting.locale, + themeMode: config.themeProps.themeMode, + primaryColor: config.themeProps.primaryColor, + prueBlack: config.themeProps.prueBlack, + fontFamily: config.themeProps.fontFamily, + ), + builder: (_, state, child) { + return DynamicColorBuilder( + builder: (lightDynamic, darkDynamic) { + _updateSystemColorSchemes(lightDynamic, darkDynamic); + return MaterialApp( + navigatorKey: globalState.navigatorKey, + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate + ], + builder: (_, child) { + return MessageManager( + child: LayoutBuilder( builder: (_, container) { final appController = globalState.appController; final maxWidth = container.maxWidth; @@ -204,41 +208,40 @@ class ApplicationState extends State { } return _buildPage(child!); }, - ); - }, - scrollBehavior: BaseScrollBehavior(), - title: appName, - locale: other.getLocaleForString(state.locale), - supportedLocales: - AppLocalizations.delegate.supportedLocales, - themeMode: state.themeMode, - theme: ThemeData( - useMaterial3: true, - fontFamily: state.fontFamily.value, - pageTransitionsTheme: _pageTransitionsTheme, - colorScheme: _getAppColorScheme( - brightness: Brightness.light, - systemColorSchemes: systemColorSchemes, - primaryColor: state.primaryColor, ), + ); + }, + scrollBehavior: BaseScrollBehavior(), + title: appName, + locale: other.getLocaleForString(state.locale), + supportedLocales: AppLocalizations.delegate.supportedLocales, + themeMode: state.themeMode, + theme: ThemeData( + useMaterial3: true, + fontFamily: state.fontFamily.value, + pageTransitionsTheme: _pageTransitionsTheme, + colorScheme: _getAppColorScheme( + brightness: Brightness.light, + systemColorSchemes: systemColorSchemes, + primaryColor: state.primaryColor, ), - darkTheme: ThemeData( - useMaterial3: true, - fontFamily: state.fontFamily.value, - pageTransitionsTheme: _pageTransitionsTheme, - colorScheme: _getAppColorScheme( - brightness: Brightness.dark, - systemColorSchemes: systemColorSchemes, - primaryColor: state.primaryColor, - ).toPrueBlack(state.prueBlack), - ), - home: child, - ); - }, - ); - }, - child: const HomePage(), - ), + ), + darkTheme: ThemeData( + useMaterial3: true, + fontFamily: state.fontFamily.value, + pageTransitionsTheme: _pageTransitionsTheme, + colorScheme: _getAppColorScheme( + brightness: Brightness.dark, + systemColorSchemes: systemColorSchemes, + primaryColor: state.primaryColor, + ).toPrueBlack(state.prueBlack), + ), + home: child, + ); + }, + ); + }, + child: const HomePage(), ), ), ); @@ -247,7 +250,8 @@ class ApplicationState extends State { @override Future dispose() async { linkManager.destroy(); - _cancelTimer(); + _autoUpdateGroupTaskTimer?.cancel(); + _autoUpdateProfilesTaskTimer?.cancel(); await clashService?.destroy(); await globalState.appController.savePreferences(); await globalState.appController.handleExit(); diff --git a/lib/clash/core.dart b/lib/clash/core.dart index 25c44fb..83cc019 100644 --- a/lib/clash/core.dart +++ b/lib/clash/core.dart @@ -200,11 +200,27 @@ class ClashCore { return Traffic.fromMap(json.decode(trafficString)); } + Future getCountryCode(String ip) async { + final countryCode = await clashInterface.getCountryCode(ip); + if (countryCode.isEmpty) { + return null; + } + return IpInfo( + ip: ip, + countryCode: countryCode, + ); + } + Future getTotalTraffic(bool value) async { final totalTrafficString = await clashInterface.getTotalTraffic(value); return Traffic.fromMap(json.decode(totalTrafficString)); } + Future getMemory() async { + final value = await clashInterface.getMemory(); + return int.parse(value); + } + resetTraffic() { clashInterface.resetTraffic(); } diff --git a/lib/clash/generated/clash_ffi.dart b/lib/clash/generated/clash_ffi.dart index 4d37d9a..0c36c53 100644 --- a/lib/clash/generated/clash_ffi.dart +++ b/lib/clash/generated/clash_ffi.dart @@ -2348,20 +2348,6 @@ class ClashFFI { set suboptarg(ffi.Pointer value) => _suboptarg.value = value; - void updateDns( - ffi.Pointer s, - ) { - return _updateDns( - s, - ); - } - - late final _updateDnsPtr = - _lookup)>>( - 'updateDns'); - late final _updateDns = - _updateDnsPtr.asFunction)>(); - void initNativeApiBridge( ffi.Pointer api, ) { @@ -2581,6 +2567,18 @@ class ClashFFI { late final _getConnections = _getConnectionsPtr.asFunction Function()>(); + void getMemory( + int port, + ) { + return _getMemory( + port, + ); + } + + late final _getMemoryPtr = + _lookup>('getMemory'); + late final _getMemory = _getMemoryPtr.asFunction(); + void closeConnections() { return _closeConnections(); } @@ -2665,6 +2663,23 @@ class ClashFFI { late final _updateExternalProvider = _updateExternalProviderPtr .asFunction, int)>(); + void getCountryCode( + ffi.Pointer ipChar, + int port, + ) { + return _getCountryCode( + ipChar, + port, + ); + } + + late final _getCountryCodePtr = _lookup< + ffi.NativeFunction< + ffi.Void Function( + ffi.Pointer, ffi.LongLong)>>('getCountryCode'); + late final _getCountryCode = _getCountryCodePtr + .asFunction, int)>(); + void sideLoadExternalProvider( ffi.Pointer providerNameChar, ffi.Pointer dataChar, @@ -2793,6 +2808,20 @@ class ClashFFI { 'setState'); late final _setState = _setStatePtr.asFunction)>(); + + void updateDns( + ffi.Pointer s, + ) { + return _updateDns( + s, + ); + } + + late final _updateDnsPtr = + _lookup)>>( + 'updateDns'); + late final _updateDns = + _updateDnsPtr.asFunction)>(); } final class __mbstate_t extends ffi.Union { diff --git a/lib/clash/interface.dart b/lib/clash/interface.dart index 96fb782..cac611e 100644 --- a/lib/clash/interface.dart +++ b/lib/clash/interface.dart @@ -45,6 +45,10 @@ mixin ClashInterface { FutureOr getTotalTraffic(bool value); + FutureOr getCountryCode(String ip); + + FutureOr getMemory(); + resetTraffic(); startLog(); diff --git a/lib/clash/lib.dart b/lib/clash/lib.dart index 923c26d..56f5912 100644 --- a/lib/clash/lib.dart +++ b/lib/clash/lib.dart @@ -306,6 +306,39 @@ class ClashLib with ClashInterface { clashFFI.forceGc(); } + @override + FutureOr getCountryCode(String ip) { + final completer = Completer(); + final receiver = ReceivePort(); + receiver.listen((message) { + if (!completer.isCompleted) { + completer.complete(message); + receiver.close(); + } + }); + final ipChar = ip.toNativeUtf8().cast(); + clashFFI.getCountryCode( + ipChar, + receiver.sendPort.nativePort, + ); + malloc.free(ipChar); + return completer.future; + } + + @override + FutureOr getMemory() { + final completer = Completer(); + final receiver = ReceivePort(); + receiver.listen((message) { + if (!completer.isCompleted) { + completer.complete(message); + receiver.close(); + } + }); + clashFFI.getMemory(receiver.sendPort.nativePort); + return completer.future; + } + /// Android startTun(int fd, int port) { diff --git a/lib/clash/service.dart b/lib/clash/service.dart index 61c688c..26d3eb6 100644 --- a/lib/clash/service.dart +++ b/lib/clash/service.dart @@ -138,6 +138,8 @@ class ClashService with ClashInterface { case ActionMethod.updateGeoData: case ActionMethod.updateExternalProvider: case ActionMethod.sideLoadExternalProvider: + case ActionMethod.getCountryCode: + case ActionMethod.getMemory: completer?.complete(action.data as String); return; case ActionMethod.message: @@ -146,7 +148,6 @@ class ClashService with ClashInterface { case ActionMethod.forceGc: case ActionMethod.startLog: case ActionMethod.stopLog: - default: return; } } @@ -174,7 +175,16 @@ class ClashService with ClashInterface { onLast: () { callbackCompleterMap.remove(id); }, - onTimeout: onTimeout, + onTimeout: onTimeout ?? + () { + if (T is String) { + return "" as T; + } + if (T is bool) { + return false as T; + } + return null as T; + }, functionName: id, ); } @@ -409,6 +419,21 @@ class ClashService with ClashInterface { await server.close(); await _deleteSocketFile(); } + + @override + FutureOr getCountryCode(String ip) { + return _invoke( + method: ActionMethod.getCountryCode, + data: ip, + ); + } + + @override + FutureOr getMemory() { + return _invoke( + method: ActionMethod.getMemory, + ); + } } final clashService = system.isDesktop ? ClashService() : null; diff --git a/lib/common/color.dart b/lib/common/color.dart index 86002bf..e7caf0e 100644 --- a/lib/common/color.dart +++ b/lib/common/color.dart @@ -1,19 +1,20 @@ import 'package:flutter/material.dart'; extension ColorExtension on Color { - toLight() { + + Color get toLight { + return withOpacity(0.8); + } + + Color get toLighter { return withOpacity(0.6); } - toLighter() { - return withOpacity(0.4); - } - - toSoft() { + Color get toSoft { return withOpacity(0.12); } - toLittle() { + Color get toLittle { return withOpacity(0.03); } @@ -23,13 +24,39 @@ extension ColorExtension on Color { final hslDark = hsl.withLightness((hsl.lightness - amount).clamp(0.0, 1.0)); return hslDark.toColor(); } + + Color blendDarken( + BuildContext context, { + double factor = 0.1, + }) { + final brightness = Theme.of(context).brightness; + return Color.lerp( + this, + brightness == Brightness.dark ? Colors.white : Colors.black, + factor, + )!; + } + + Color blendLighten( + BuildContext context, { + double factor = 0.1, + }) { + final brightness = Theme.of(context).brightness; + return Color.lerp( + this, + brightness == Brightness.dark ? Colors.black : Colors.white, + factor, + )!; + } } extension ColorSchemeExtension on ColorScheme { ColorScheme toPrueBlack(bool isPrueBlack) => isPrueBlack ? copyWith( surface: Colors.black, - surfaceContainer: surfaceContainer.darken(0.05), + surfaceContainer: surfaceContainer.darken( + 0.05, + ), ) : this; } diff --git a/lib/common/constant.dart b/lib/common/constant.dart index 786e935..0338ccb 100644 --- a/lib/common/constant.dart +++ b/lib/common/constant.dart @@ -15,9 +15,14 @@ const packageName = "com.follow.clash"; final unixSocketPath = "/tmp/FlClashSocket_${Random().nextInt(10000)}.sock"; const helperPort = 47890; const helperTag = "2024125"; +const baseInfoEdgeInsets = EdgeInsets.symmetric( + vertical: 16, + horizontal: 16, +); const httpTimeoutDuration = Duration(milliseconds: 5000); const moreDuration = Duration(milliseconds: 100); const animateDuration = Duration(milliseconds: 100); +const commonDuration = Duration(milliseconds: 300); const defaultUpdateDuration = Duration(days: 1); const mmdbFileName = "geoip.metadb"; const asnFileName = "ASN.mmdb"; @@ -79,3 +84,7 @@ const viewModeColumnsMap = { }; const defaultPrimaryColor = Colors.brown; + +double getWidgetHeight(num lines) { + return max(lines * 84 + (lines - 1) * 16, 0); +} diff --git a/lib/common/context.dart b/lib/common/context.dart index eba0e6d..692b130 100644 --- a/lib/common/context.dart +++ b/lib/common/context.dart @@ -1,13 +1,17 @@ +import 'package:fl_clash/manager/manager.dart'; import 'package:fl_clash/widgets/scaffold.dart'; import 'package:flutter/material.dart'; extension BuildContextExtension on BuildContext { - CommonScaffoldState? get commonScaffoldState { return findAncestorStateOfType(); } - Size get appSize{ + showNotifier(String text) { + return findAncestorStateOfType()?.message(text); + } + + Size get appSize { return MediaQuery.of(this).size; } diff --git a/lib/common/function.dart b/lib/common/function.dart index 6b8c8d5..1b9c077 100644 --- a/lib/common/function.dart +++ b/lib/common/function.dart @@ -1,26 +1,33 @@ import 'dart:async'; class Debouncer { - final Duration delay; - Timer? _timer; + Map operators = {}; - Debouncer({required this.delay}); + call( + dynamic tag, + Function func, { + List? args, + Duration duration = const Duration(milliseconds: 600), + }) { + final timer = operators[tag]; + if (timer != null) { + timer.cancel(); + } + operators[tag] = Timer( + duration, + () { + operators.remove(tag); + Function.apply( + func, + args, + ); + }, + ); + } - void call(Function action, List positionalArguments, [Map? namedArguments]) { - _timer?.cancel(); - _timer = Timer(delay, () => Function.apply(action, positionalArguments, namedArguments)); + cancel(dynamic tag) { + operators[tag]?.cancel(); } } -Function debounce(F func,{int milliseconds = 600}) { - Timer? timer; - - return ([List? args, Map? namedArgs]) { - if (timer != null) { - timer!.cancel(); - } - timer = Timer(Duration(milliseconds: milliseconds), () async { - await Function.apply(func, args ?? [], namedArgs); - }); - }; -} \ No newline at end of file +final debouncer = Debouncer(); diff --git a/lib/common/http.dart b/lib/common/http.dart index 4702777..2eb302e 100644 --- a/lib/common/http.dart +++ b/lib/common/http.dart @@ -14,10 +14,10 @@ class FlClashHttpOverrides extends HttpOverrides { if ([localhost].contains(url.host)) { return "DIRECT"; } - debugPrint("find $url"); final appController = globalState.appController; final port = appController.clashConfig.mixedPort; final isStart = appController.appFlowingState.isStart; + debugPrint("find $url proxy:$isStart"); if (!isStart) return "DIRECT"; return "PROXY localhost:$port"; }; diff --git a/lib/common/launch.dart b/lib/common/launch.dart index 0429857..d1a52dc 100644 --- a/lib/common/launch.dart +++ b/lib/common/launch.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'dart:io'; -import 'package:fl_clash/models/models.dart'; import 'package:launch_at_startup/launch_at_startup.dart'; import 'constant.dart'; @@ -34,8 +33,7 @@ class AutoLaunch { return await launchAtStartup.disable(); } - updateStatus(AutoLaunchState state) async { - final isAutoLaunch = state.isAutoLaunch; + updateStatus(bool isAutoLaunch) async { if (await isEnable == isAutoLaunch) return; if (isAutoLaunch == true) { enable(); diff --git a/lib/common/navigator.dart b/lib/common/navigator.dart index c3059f3..77ed57e 100644 --- a/lib/common/navigator.dart +++ b/lib/common/navigator.dart @@ -1,11 +1,251 @@ +import 'package:fl_clash/common/common.dart'; import 'package:flutter/material.dart'; class BaseNavigator { static Future push(BuildContext context, Widget child) async { return await Navigator.of(context).push( - MaterialPageRoute( + CommonRoute( builder: (context) => child, ), ); } } + +class CommonRoute extends MaterialPageRoute { + CommonRoute({ + required super.builder, + }); + + @override + Duration get transitionDuration => const Duration(milliseconds: 500); + + @override + Duration get reverseTransitionDuration => const Duration(milliseconds: 300); +} + +final Animatable _kRightMiddleTween = Tween( + begin: const Offset(1.0, 0.0), + end: Offset.zero, +); +final Animatable _kMiddleLeftTween = Tween( + begin: Offset.zero, + end: const Offset(-1.0 / 3.0, 0.0), +); + +class CommonPageTransitionsBuilder extends PageTransitionsBuilder { + const CommonPageTransitionsBuilder(); + + @override + Widget buildTransitions( + PageRoute route, + BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child, + ) { + return CommonPageTransition( + context: context, + primaryRouteAnimation: animation, + secondaryRouteAnimation: secondaryAnimation, + linearTransition: false, + child: child, + ); + } +} + +class CommonPageTransition extends StatefulWidget { + const CommonPageTransition({ + super.key, + required this.context, + required this.primaryRouteAnimation, + required this.secondaryRouteAnimation, + required this.child, + required this.linearTransition, + }); + + final Widget child; + + final Animation primaryRouteAnimation; + + final Animation secondaryRouteAnimation; + + final BuildContext context; + + final bool linearTransition; + + static Widget? delegatedTransition( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + bool allowSnapshotting, + Widget? child) { + final Animation delegatedPositionAnimation = CurvedAnimation( + parent: secondaryAnimation, + curve: Curves.linearToEaseOut, + reverseCurve: Curves.easeInToLinear, + ).drive(_kMiddleLeftTween); + + assert(debugCheckHasDirectionality(context)); + final TextDirection textDirection = Directionality.of(context); + return SlideTransition( + position: delegatedPositionAnimation, + textDirection: textDirection, + transformHitTests: false, + child: child, + ); + } + + @override + State createState() => _CommonPageTransitionState(); +} + +class _CommonPageTransitionState extends State { + late Animation _primaryPositionAnimation; + late Animation _secondaryPositionAnimation; + late Animation _primaryShadowAnimation; + CurvedAnimation? _primaryPositionCurve; + CurvedAnimation? _secondaryPositionCurve; + CurvedAnimation? _primaryShadowCurve; + + @override + void initState() { + super.initState(); + _setupAnimation(); + } + + @override + void didUpdateWidget(covariant CommonPageTransition oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.primaryRouteAnimation != widget.primaryRouteAnimation || + oldWidget.secondaryRouteAnimation != widget.secondaryRouteAnimation || + oldWidget.linearTransition != widget.linearTransition) { + _disposeCurve(); + _setupAnimation(); + } + } + + @override + void dispose() { + _disposeCurve(); + super.dispose(); + } + + void _disposeCurve() { + _primaryPositionCurve?.dispose(); + _secondaryPositionCurve?.dispose(); + _primaryShadowCurve?.dispose(); + _primaryPositionCurve = null; + _secondaryPositionCurve = null; + _primaryShadowCurve = null; + } + + void _setupAnimation() { + if (!widget.linearTransition) { + _primaryPositionCurve = CurvedAnimation( + parent: widget.primaryRouteAnimation, + curve: Curves.fastEaseInToSlowEaseOut, + reverseCurve: Curves.easeInOut, + ); + _secondaryPositionCurve = CurvedAnimation( + parent: widget.secondaryRouteAnimation, + curve: Curves.linearToEaseOut, + reverseCurve: Curves.easeInToLinear, + ); + _primaryShadowCurve = CurvedAnimation( + parent: widget.primaryRouteAnimation, + curve: Curves.linearToEaseOut, + ); + } + _primaryPositionAnimation = + (_primaryPositionCurve ?? widget.primaryRouteAnimation) + .drive(_kRightMiddleTween); + _secondaryPositionAnimation = + (_secondaryPositionCurve ?? widget.secondaryRouteAnimation) + .drive(_kMiddleLeftTween); + _primaryShadowAnimation = + (_primaryShadowCurve ?? widget.primaryRouteAnimation).drive( + DecorationTween( + begin: const _CommonEdgeShadowDecoration(), + end: _CommonEdgeShadowDecoration( + [ + widget.context.colorScheme.inverseSurface.withOpacity( + 0.06, + ), + Colors.transparent, + ], + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + assert(debugCheckHasDirectionality(context)); + final TextDirection textDirection = Directionality.of(context); + return SlideTransition( + position: _secondaryPositionAnimation, + textDirection: textDirection, + transformHitTests: false, + child: SlideTransition( + position: _primaryPositionAnimation, + textDirection: textDirection, + child: DecoratedBoxTransition( + decoration: _primaryShadowAnimation, + child: widget.child, + ), + ), + ); + } +} + +class _CommonEdgeShadowDecoration extends Decoration { + final List? _colors; + + const _CommonEdgeShadowDecoration([this._colors]); + + @override + BoxPainter createBoxPainter([VoidCallback? onChanged]) { + return _CommonEdgeShadowPainter(this, onChanged); + } +} + +class _CommonEdgeShadowPainter extends BoxPainter { + _CommonEdgeShadowPainter( + this._decoration, + super.onChanged, + ) : assert(_decoration._colors == null || _decoration._colors!.length > 1); + + final _CommonEdgeShadowDecoration _decoration; + + @override + void paint(Canvas canvas, Offset offset, ImageConfiguration configuration) { + final List? colors = _decoration._colors; + if (colors == null) { + return; + } + + final double shadowWidth = 0.05 * configuration.size!.width; + final double shadowHeight = configuration.size!.height; + final double bandWidth = shadowWidth / (colors.length - 1); + + final TextDirection? textDirection = configuration.textDirection; + assert(textDirection != null); + final (double shadowDirection, double start) = switch (textDirection!) { + TextDirection.rtl => (1, offset.dx + configuration.size!.width), + TextDirection.ltr => (-1, offset.dx), + }; + + int bandColorIndex = 0; + for (int dx = 0; dx < shadowWidth; dx += 1) { + if (dx ~/ bandWidth != bandColorIndex) { + bandColorIndex += 1; + } + final Paint paint = Paint() + ..color = Color.lerp(colors[bandColorIndex], colors[bandColorIndex + 1], + (dx % bandWidth) / bandWidth)!; + final double x = start + shadowDirection * dx; + canvas.drawRect( + Rect.fromLTWH(x - 1.0, offset.dy, 1.0, shadowHeight), paint); + } + } +} diff --git a/lib/common/num.dart b/lib/common/num.dart index 9b5a435..acb8fab 100644 --- a/lib/common/num.dart +++ b/lib/common/num.dart @@ -1,5 +1,43 @@ -extension NumExtension on num { +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; + +extension NumExt on num { String fixed({digit = 2}) { return toStringAsFixed(truncateToDouble() == this ? 0 : digit); } } + +extension DoubleExt on double { + moreOrEqual(double value) { + return this > value || (value - this).abs() < precisionErrorTolerance + 1; + } +} + +extension OffsetExt on Offset { + double getCrossAxisOffset(Axis direction) { + return direction == Axis.vertical ? dx : dy; + } + + double getMainAxisOffset(Axis direction) { + return direction == Axis.vertical ? dy : dx; + } + + bool less(Offset offset) { + if (dy < offset.dy) { + return true; + } + if (dy == offset.dy && dx < offset.dx) { + return true; + } + return false; + } +} + +extension RectExt on Rect { + doRectIntersect(Rect rect) { + return left < rect.right && + right > rect.left && + top < rect.bottom && + bottom > rect.top; + } +} diff --git a/lib/common/other.dart b/lib/common/other.dart index 91f7774..6ce58f4 100644 --- a/lib/common/other.dart +++ b/lib/common/other.dart @@ -34,6 +34,19 @@ class Other { ); } + String get uuidV4 { + final Random random = Random(); + final bytes = List.generate(16, (_) => random.nextInt(256)); + + bytes[6] = (bytes[6] & 0x0F) | 0x40; + bytes[8] = (bytes[8] & 0x3F) | 0x80; + + final hex = + bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join(); + + return '${hex.substring(0, 8)}-${hex.substring(8, 12)}-${hex.substring(12, 16)}-${hex.substring(16, 20)}-${hex.substring(20, 32)}'; + } + String getTimeDifference(DateTime dateTime) { var currentDateTime = DateTime.now(); var difference = currentDateTime.difference(dateTime); @@ -225,7 +238,7 @@ class Other { } int getProfilesColumns(double viewWidth) { - return max((viewWidth / 400).floor(), 1); + return max((viewWidth / 350).floor(), 1); } String getBackupFileName() { @@ -240,6 +253,32 @@ class Other { final view = WidgetsBinding.instance.platformDispatcher.views.first; return view.physicalSize / view.devicePixelRatio; } + + Future getLocalIpAddress() async { + List interfaces = await NetworkInterface.list( + includeLoopback: false, + ) + ..sort((a, b) { + if (a.isWifi && !b.isWifi) return -1; + if (!a.isWifi && b.isWifi) return 1; + if (a.includesIPv4 && !b.includesIPv4) return -1; + if (!a.includesIPv4 && b.includesIPv4) return 1; + return 0; + }); + for (final interface in interfaces) { + final addresses = interface.addresses; + if (addresses.isEmpty) { + continue; + } + addresses.sort((a, b) { + if (a.isIPv4 && !b.isIPv4) return -1; + if (!a.isIPv4 && b.isIPv4) return 1; + return 0; + }); + return addresses.first.address; + } + return ""; + } } final other = Other(); diff --git a/lib/common/request.dart b/lib/common/request.dart index 4a5f5ef..956edc0 100644 --- a/lib/common/request.dart +++ b/lib/common/request.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'dart:typed_data'; import 'package:dio/dio.dart'; +import 'package:fl_clash/clash/clash.dart'; import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/state.dart'; @@ -70,28 +71,34 @@ class Request { return data; } - final Map)> _ipInfoSources = { - "https://ipwho.is/": IpInfo.fromIpwhoIsJson, - "https://api.ip.sb/geoip/": IpInfo.fromIpSbJson, - "https://ipapi.co/json/": IpInfo.fromIpApiCoJson, - "https://ipinfo.io/json/": IpInfo.fromIpInfoIoJson, - }; + final List _ipInfoSources = [ + "https://ipwho.is/?fields=ip&output=csv", + "https://ipinfo.io/ip", + "https://ifconfig.me/ip/", + ]; Future checkIp({CancelToken? cancelToken}) async { - for (final source in _ipInfoSources.entries) { + for (final source in _ipInfoSources) { try { final response = await _dio - .get>(source.key, cancelToken: cancelToken) + .get( + source, + cancelToken: cancelToken, + ) .timeout(httpTimeoutDuration); if (response.statusCode != 200 || response.data == null) { continue; } - return source.value(response.data!); + final ipInfo = await clashCore.getCountryCode(response.data!); + if (ipInfo == null && source != _ipInfoSources.last) { + continue; + } + return ipInfo; } catch (e) { + debugPrint("checkIp error ===> $e"); if (e is DioException && e.type == DioExceptionType.cancel) { throw "cancelled"; } - debugPrint("checkIp error ===> $e"); } } return null; diff --git a/lib/common/text.dart b/lib/common/text.dart index 4257949..2488cb2 100644 --- a/lib/common/text.dart +++ b/lib/common/text.dart @@ -2,13 +2,15 @@ import 'package:flutter/material.dart'; import 'color.dart'; extension TextStyleExtension on TextStyle { - TextStyle get toLight => copyWith(color: color?.toLight()); + TextStyle get toLight => copyWith(color: color?.toLight); - TextStyle get toLighter => copyWith(color: color?.toLighter()); + TextStyle get toLighter => copyWith(color: color?.toLighter); TextStyle get toSoftBold => copyWith(fontWeight: FontWeight.w500); TextStyle get toBold => copyWith(fontWeight: FontWeight.bold); - TextStyle get toMinus => copyWith(fontSize: fontSize! - 2); + TextStyle adjustSize(int size) => copyWith( + fontSize: fontSize! + size, + ); } diff --git a/lib/controller.dart b/lib/controller.dart index 96f5478..9e72e36 100644 --- a/lib/controller.dart +++ b/lib/controller.dart @@ -23,40 +23,47 @@ class AppController { late AppFlowingState appFlowingState; late Config config; late ClashConfig clashConfig; - late Function updateClashConfigDebounce; - late Function updateGroupDebounce; - late Function addCheckIpNumDebounce; - late Function applyProfileDebounce; - late Function savePreferencesDebounce; - late Function changeProxyDebounce; AppController(this.context) { appState = context.read(); config = context.read(); clashConfig = context.read(); appFlowingState = context.read(); - updateClashConfigDebounce = debounce(() async { - await updateClashConfig(); + } + + updateClashConfigDebounce() { + debouncer.call(DebounceTag.updateClashConfig, updateClashConfig); + } + + updateGroupsDebounce() { + debouncer.call(DebounceTag.updateGroups, updateGroups); + } + + addCheckIpNumDebounce() { + debouncer.call(DebounceTag.addCheckIpNum, () { + appState.checkIpNum++; }); - savePreferencesDebounce = debounce(() async { - await savePreferences(); + } + + applyProfileDebounce() { + debouncer.call(DebounceTag.addCheckIpNum, () { + applyProfile(isPrue: true); }); - applyProfileDebounce = debounce(() async { - await applyProfile(isPrue: true); - }); - changeProxyDebounce = debounce((String groupName, String proxyName) async { + } + + savePreferencesDebounce() { + debouncer.call(DebounceTag.savePreferences, savePreferences); + } + + changeProxyDebounce(String groupName, String proxyName) { + debouncer.call(DebounceTag.changeProxy, + (String groupName, String proxyName) async { await changeProxy( groupName: groupName, proxyName: proxyName, ); await updateGroups(); - }); - addCheckIpNumDebounce = debounce(() { - appState.checkIpNum++; - }); - updateGroupDebounce = debounce(() async { - await updateGroups(); - }); + }, args: [groupName, proxyName]); } restartCore() async { @@ -94,9 +101,6 @@ class AppController { appFlowingState.traffics = []; appFlowingState.totalTraffic = Traffic(); appFlowingState.runTime = null; - await Future.delayed( - Duration(milliseconds: 300), - ); addCheckIpNumDebounce(); } } @@ -139,8 +143,14 @@ class AppController { } } - updateProviders() { - globalState.updateProviders(appState); + updateProviders() async { + await globalState.updateProviders(appState); + } + + updateLocalIp() async { + appFlowingState.localIp = null; + await Future.delayed(commonDuration); + appFlowingState.localIp = await other.getLocalIpAddress(); } Future updateProfile(Profile profile) async { @@ -148,6 +158,9 @@ class AppController { config.setProfile( newProfile.copyWith(isUpdating: false), ); + if (profile.id == config.currentProfile?.id) { + applyProfileDebounce(); + } } Future updateClashConfig({bool isPatch = true}) async { @@ -333,6 +346,9 @@ class AppController { config: config, ); await _initStatus(); + autoLaunch?.updateStatus( + config.appSetting.autoLaunch, + ); autoUpdateProfiles(); autoCheckUpdate(); } @@ -341,10 +357,12 @@ class AppController { if (Platform.isAndroid) { globalState.updateStartTime(); } - if (globalState.isStart) { - await updateStatus(true); - } else { - await updateStatus(config.appSetting.autoRun); + final status = + globalState.isStart == true ? true : config.appSetting.autoRun; + + await updateStatus(status); + if (!status) { + addCheckIpNumDebounce(); } } @@ -406,10 +424,6 @@ class AppController { ); } - showSnackBar(String message) { - globalState.showSnackBar(context, message: message); - } - Future showDisclaimer() async { return await globalState.showCommonDialog( dismissible: false, diff --git a/lib/enum/enum.dart b/lib/enum/enum.dart index 970f9b3..728f674 100644 --- a/lib/enum/enum.dart +++ b/lib/enum/enum.dart @@ -1,9 +1,39 @@ // ignore_for_file: constant_identifier_names +import 'dart:io'; + +import 'package:fl_clash/fragments/dashboard/widgets/widgets.dart'; +import 'package:fl_clash/widgets/widgets.dart'; import 'package:flutter/services.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:hotkey_manager/hotkey_manager.dart'; +enum SupportPlatform { + Windows, + MacOS, + Linux, + Android; + + static SupportPlatform get currentPlatform { + if (Platform.isWindows) { + return SupportPlatform.Windows; + } else if (Platform.isMacOS) { + return SupportPlatform.MacOS; + } else if (Platform.isLinux) { + return SupportPlatform.Linux; + } else if (Platform.isAndroid) { + return SupportPlatform.Android; + } + throw "invalid platform"; + } +} + +const desktopPlatforms = [ + SupportPlatform.Linux, + SupportPlatform.MacOS, + SupportPlatform.Windows, +]; + enum GroupType { Selector, URLTest, Fallback, LoadBalance, Relay } enum GroupName { GLOBAL, Proxy, Auto, Fallback } @@ -91,6 +121,10 @@ enum RecoveryOption { enum ChipType { action, delete } enum CommonCardType { plain, filled } +// +// extension CommonCardTypeExt on CommonCardType { +// CommonCardType get variant => CommonCardType.plain; +// } enum ProxiesType { tab, list } @@ -205,6 +239,8 @@ enum ActionMethod { stopLog, startListener, stopListener, + getCountryCode, + getMemory, } enum AuthorizeCode { none, success, error } @@ -214,3 +250,86 @@ enum WindowsHelperServiceStatus { presence, running, } + +enum DebounceTag { + updateClashConfig, + updateGroups, + addCheckIpNum, + applyProfile, + savePreferences, + changeProxy, + checkIp, + handleWill, + updateDelay, + vpnTip, + autoLaunch +} + +enum DashboardWidget { + networkSpeed( + GridItem( + crossAxisCellCount: 8, + child: NetworkSpeed(), + ), + ), + outboundMode( + GridItem( + crossAxisCellCount: 4, + child: OutboundMode(), + ), + ), + trafficUsage( + GridItem( + crossAxisCellCount: 4, + child: TrafficUsage(), + ), + ), + networkDetection( + GridItem( + crossAxisCellCount: 4, + child: NetworkDetection(), + ), + ), + tunButton( + GridItem( + crossAxisCellCount: 4, + child: TUNButton(), + ), + platforms: desktopPlatforms, + ), + systemProxyButton( + GridItem( + crossAxisCellCount: 4, + child: SystemProxyButton(), + ), + platforms: desktopPlatforms, + ), + intranetIp( + GridItem( + crossAxisCellCount: 4, + child: IntranetIP(), + ), + ), + memoryInfo( + GridItem( + crossAxisCellCount: 4, + child: MemoryInfo(), + ), + ); + + final GridItem widget; + final List platforms; + + const DashboardWidget( + this.widget, { + this.platforms = SupportPlatform.values, + }); + + static DashboardWidget getDashboardWidget(GridItem gridItem) { + final dashboardWidgets = DashboardWidget.values; + final index = dashboardWidgets.indexWhere( + (item) => item.widget == gridItem, + ); + return dashboardWidgets[index]; + } +} diff --git a/lib/fragments/access.dart b/lib/fragments/access.dart index e7d0c05..2d134c1 100644 --- a/lib/fragments/access.dart +++ b/lib/fragments/access.dart @@ -104,11 +104,9 @@ class _AccessFragmentState extends State { showSheet( title: appLocalizations.proxiesSetting, context: context, - builder: (_) { - return AccessControlWidget( - context: context, - ); - }, + body: AccessControlWidget( + context: context, + ), ); }, icon: const Icon(Icons.tune), @@ -178,8 +176,8 @@ class _AccessFragmentState extends State { status: !isAccessControl, child: Column( children: [ - AbsorbPointer( - absorbing: !isAccessControl, + ActivateBox( + active: isAccessControl, child: Padding( padding: const EdgeInsets.only( top: 4, @@ -332,8 +330,8 @@ class PackageListItem extends StatelessWidget { @override Widget build(BuildContext context) { - return AbsorbPointer( - absorbing: !isActive, + return ActivateBox( + active: isActive, child: ListItem.checkbox( leading: SizedBox( width: 48, diff --git a/lib/fragments/backup_and_recovery.dart b/lib/fragments/backup_and_recovery.dart index d210971..4f0081a 100644 --- a/lib/fragments/backup_and_recovery.dart +++ b/lib/fragments/backup_and_recovery.dart @@ -343,8 +343,8 @@ class _WebDAVFormDialogState extends State { @override void dispose() { - super.dispose(); _obscureController.dispose(); + super.dispose(); } @override diff --git a/lib/fragments/connections.dart b/lib/fragments/connections.dart index 32ec5be..ffcd0e7 100644 --- a/lib/fragments/connections.dart +++ b/lib/fragments/connections.dart @@ -66,9 +66,6 @@ class _ConnectionsFragmentState extends State { }, icon: const Icon(Icons.search), ), - const SizedBox( - width: 8, - ), IconButton( onPressed: () async { clashCore.closeConnections(); @@ -112,11 +109,11 @@ class _ConnectionsFragmentState extends State { @override void dispose() { - super.dispose(); timer?.cancel(); connectionsNotifier.dispose(); _scrollController.dispose(); timer = null; + super.dispose(); } @override diff --git a/lib/fragments/dashboard/dashboard.dart b/lib/fragments/dashboard/dashboard.dart index 4b5402b..ef587ba 100644 --- a/lib/fragments/dashboard/dashboard.dart +++ b/lib/fragments/dashboard/dashboard.dart @@ -1,18 +1,12 @@ import 'dart:math'; -import 'package:fl_clash/common/common.dart'; -import 'package:fl_clash/fragments/dashboard/intranet_ip.dart'; -import 'package:fl_clash/fragments/dashboard/status_button.dart'; +import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/models/models.dart'; +import 'package:fl_clash/state.dart'; import 'package:fl_clash/widgets/widgets.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; - -import 'network_detection.dart'; -import 'network_speed.dart'; -import 'outbound_mode.dart'; -import 'start_button.dart'; -import 'traffic_usage.dart'; +import 'widgets/start_button.dart'; class DashboardFragment extends StatefulWidget { const DashboardFragment({super.key}); @@ -22,7 +16,9 @@ class DashboardFragment extends StatefulWidget { } class _DashboardFragmentState extends State { - _initFab(bool isCurrent) { + final key = GlobalKey(); + + _initScaffold(bool isCurrent) { if (!isCurrent) { return; } @@ -30,6 +26,47 @@ class _DashboardFragmentState extends State { final commonScaffoldState = context.findAncestorStateOfType(); commonScaffoldState?.floatingActionButton = const StartButton(); + commonScaffoldState?.actions = [ + ValueListenableBuilder( + valueListenable: key.currentState!.addedChildrenNotifier, + builder: (_, addedChildren, child) { + return ValueListenableBuilder( + valueListenable: key.currentState!.isEditNotifier, + builder: (_, isEdit, child) { + if (!isEdit || addedChildren.isEmpty) { + return Container(); + } + return child!; + }, + child: child, + ); + }, + child: IconButton( + onPressed: () { + key.currentState!.showAddModal(); + }, + icon: Icon( + Icons.add_circle, + ), + ), + ), + IconButton( + icon: ValueListenableBuilder( + valueListenable: key.currentState!.isEditNotifier, + builder: (_, isEdit, ___) { + return isEdit + ? Icon(Icons.save) + : Icon( + Icons.edit, + ); + }, + ), + onPressed: () { + key.currentState!.isEditNotifier.value = + !key.currentState!.isEditNotifier.value; + }, + ), + ]; }); } @@ -38,7 +75,7 @@ class _DashboardFragmentState extends State { return ActiveBuilder( label: "dashboard", builder: (isCurrent, child) { - _initFab(isCurrent); + _initScaffold(isCurrent); return child!; }, child: Align( @@ -47,52 +84,52 @@ class _DashboardFragmentState extends State { padding: const EdgeInsets.all(16).copyWith( bottom: 88, ), - child: Selector( - selector: (_, appState) => appState.viewWidth, - builder: (_, viewWidth, ___) { - final columns = max(4 * ((viewWidth / 350).ceil()), 8); - final int switchCount = (4 / columns) * viewWidth < 200 ? 8 : 4; - return Grid( + child: Selector2( + selector: (_, appState, config) => DashboardState( + dashboardWidgets: config.appSetting.dashboardWidgets, + viewWidth: appState.viewWidth, + ), + builder: (_, state, ___) { + final columns = max(4 * ((state.viewWidth / 350).ceil()), 8); + return SuperGrid( + key: key, crossAxisCount: columns, crossAxisSpacing: 16, mainAxisSpacing: 16, children: [ - const GridItem( - crossAxisCellCount: 8, - child: NetworkSpeed(), - ), - // if (Platform.isAndroid) - // GridItem( - // crossAxisCellCount: switchCount, - // child: const VPNSwitch(), - // ), - if (system.isDesktop) ...[ - GridItem( - crossAxisCellCount: switchCount, - child: const TUNButton(), - ), - GridItem( - crossAxisCellCount: switchCount, - child: const SystemProxyButton(), - ), - ], - const GridItem( - crossAxisCellCount: 4, - child: OutboundMode(), - ), - const GridItem( - crossAxisCellCount: 4, - child: NetworkDetection(), - ), - const GridItem( - crossAxisCellCount: 4, - child: TrafficUsage(), - ), - const GridItem( - crossAxisCellCount: 4, - child: IntranetIP(), - ), + ...state.dashboardWidgets + .where( + (item) => item.platforms.contains( + SupportPlatform.currentPlatform, + ), + ) + .map( + (item) => item.widget, + ), ], + onSave: (girdItems) { + final dashboardWidgets = girdItems + .map( + (item) => DashboardWidget.getDashboardWidget(item), + ) + .toList(); + final config = globalState.appController.config; + config.appSetting = config.appSetting.copyWith( + dashboardWidgets: dashboardWidgets, + ); + }, + addedItemsBuilder: (girdItems) { + return DashboardWidget.values + .where( + (item) => + !girdItems.contains(item.widget) && + item.platforms.contains( + SupportPlatform.currentPlatform, + ), + ) + .map((item) => item.widget) + .toList(); + }, ); }, ), diff --git a/lib/fragments/dashboard/intranet_ip.dart b/lib/fragments/dashboard/intranet_ip.dart deleted file mode 100644 index a8ed02a..0000000 --- a/lib/fragments/dashboard/intranet_ip.dart +++ /dev/null @@ -1,140 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:connectivity_plus/connectivity_plus.dart'; -import 'package:fl_clash/common/common.dart'; -import 'package:fl_clash/state.dart'; -import 'package:fl_clash/widgets/widgets.dart'; -import 'package:flutter/material.dart'; - -class IntranetIP extends StatefulWidget { - const IntranetIP({super.key}); - - @override - State createState() => _IntranetIPState(); -} - -class _IntranetIPState extends State { - final ipNotifier = ValueNotifier(""); - late StreamSubscription subscription; - - Future getNetworkType() async { - try { - final interfaces = await NetworkInterface.list( - includeLoopback: false, - type: InternetAddressType.any, - ); - for (var interface in interfaces) { - if (interface.name.toLowerCase().contains('wlan') || - interface.name.toLowerCase().contains('wi-fi')) { - return 'WiFi'; - } - if (interface.name.toLowerCase().contains('rmnet') || - interface.name.toLowerCase().contains('ccmni') || - interface.name.toLowerCase().contains('cellular')) { - return 'Mobile Data'; - } - } - return 'Unknown'; - } catch (e) { - return 'Error'; - } - } - - Future getLocalIpAddress() async { - await Future.delayed(animateDuration); - List interfaces = await NetworkInterface.list( - includeLoopback: false, - ) - ..sort((a, b) { - if (a.isWifi && !b.isWifi) return -1; - if (!a.isWifi && b.isWifi) return 1; - if (a.includesIPv4 && !b.includesIPv4) return -1; - if (!a.includesIPv4 && b.includesIPv4) return 1; - return 0; - }); - for (final interface in interfaces) { - final addresses = interface.addresses; - if (addresses.isEmpty) { - continue; - } - addresses.sort((a, b) { - if (a.isIPv4 && !b.isIPv4) return -1; - if (!a.isIPv4 && b.isIPv4) return 1; - return 0; - }); - return addresses.first.address; - } - return null; - } - - @override - void initState() { - super.initState(); - subscription = Connectivity().onConnectivityChanged.listen((_) async { - ipNotifier.value = null; - debugPrint("[App] Connection change"); - ipNotifier.value = await getLocalIpAddress() ?? ""; - }); - WidgetsBinding.instance.addPostFrameCallback((_) async { - ipNotifier.value = await getLocalIpAddress() ?? ""; - }); - } - - @override - Widget build(BuildContext context) { - return CommonCard( - info: Info( - label: appLocalizations.intranetIP, - iconData: Icons.devices, - ), - onPressed: () {}, - child: Container( - padding: const EdgeInsets.all(16).copyWith(top: 0), - height: globalState.measure.titleMediumHeight + 24 - 2, - child: ValueListenableBuilder( - valueListenable: ipNotifier, - builder: (_, value, __) { - return FadeBox( - child: value != null - ? Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - flex: 1, - child: TooltipText( - text: Text( - value.isNotEmpty - ? value - : appLocalizations.noNetwork, - style: context - .textTheme.titleLarge?.toSoftBold.toMinus, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - ), - ], - ) - : const Padding( - padding: EdgeInsets.all(2), - child: AspectRatio( - aspectRatio: 1, - child: CircularProgressIndicator(), - ), - ), - ); - }, - ), - ), - ); - } - - @override - void dispose() { - super.dispose(); - subscription.cancel(); - ipNotifier.dispose(); - } -} diff --git a/lib/fragments/dashboard/network_detection.dart b/lib/fragments/dashboard/network_detection.dart deleted file mode 100644 index b9b5691..0000000 --- a/lib/fragments/dashboard/network_detection.dart +++ /dev/null @@ -1,233 +0,0 @@ -import 'dart:async'; - -import 'package:dio/dio.dart'; -import 'package:fl_clash/common/common.dart'; -import 'package:fl_clash/enum/enum.dart'; -import 'package:fl_clash/models/models.dart'; -import 'package:fl_clash/state.dart'; -import 'package:fl_clash/widgets/widgets.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -final networkDetectionState = ValueNotifier( - const NetworkDetectionState( - isTesting: true, - ipInfo: null, - ), -); - -class NetworkDetection extends StatefulWidget { - const NetworkDetection({super.key}); - - @override - State createState() => _NetworkDetectionState(); -} - -class _NetworkDetectionState extends State { - bool? _preIsStart; - Function? _checkIpDebounce; - Timer? _setTimeoutTimer; - CancelToken? cancelToken; - - _checkIp() async { - final appState = globalState.appController.appState; - final appFlowingState = globalState.appController.appFlowingState; - final isInit = appState.isInit; - if (!isInit) return; - final isStart = appFlowingState.isStart; - if (_preIsStart == false && _preIsStart == isStart) return; - _clearSetTimeoutTimer(); - networkDetectionState.value = networkDetectionState.value.copyWith( - isTesting: true, - ipInfo: null, - ); - _preIsStart = isStart; - if (cancelToken != null) { - cancelToken!.cancel(); - cancelToken = null; - } - cancelToken = CancelToken(); - try { - final ipInfo = await request.checkIp(cancelToken: cancelToken); - if (ipInfo != null) { - networkDetectionState.value = networkDetectionState.value.copyWith( - isTesting: false, - ipInfo: ipInfo, - ); - return; - } - _clearSetTimeoutTimer(); - _setTimeoutTimer = Timer(const Duration(milliseconds: 300), () { - networkDetectionState.value = networkDetectionState.value.copyWith( - isTesting: false, - ipInfo: null, - ); - }); - } catch (e) { - if (e.toString() == "cancelled") { - networkDetectionState.value = networkDetectionState.value.copyWith( - isTesting: true, - ipInfo: null, - ); - } - } - } - - _clearSetTimeoutTimer() { - if (_setTimeoutTimer != null) { - _setTimeoutTimer?.cancel(); - _setTimeoutTimer = null; - } - } - - _checkIpContainer(Widget child) { - return Selector( - selector: (_, appState) { - return appState.checkIpNum; - }, - builder: (_, checkIpNum, child) { - if (_checkIpDebounce != null) { - _checkIpDebounce!(); - } - return child!; - }, - child: child, - ); - } - - @override - dispose() { - super.dispose(); - } - - String countryCodeToEmoji(String countryCode) { - final String code = countryCode.toUpperCase(); - if (code.length != 2) { - return countryCode; - } - final int firstLetter = code.codeUnitAt(0) - 0x41 + 0x1F1E6; - final int secondLetter = code.codeUnitAt(1) - 0x41 + 0x1F1E6; - return String.fromCharCode(firstLetter) + String.fromCharCode(secondLetter); - } - - @override - Widget build(BuildContext context) { - _checkIpDebounce ??= debounce(_checkIp); - return _checkIpContainer( - ValueListenableBuilder( - valueListenable: networkDetectionState, - builder: (_, state, __) { - final ipInfo = state.ipInfo; - final isTesting = state.isTesting; - return CommonCard( - onPressed: () {}, - child: Column( - children: [ - Flexible( - flex: 0, - child: Container( - padding: const EdgeInsets.all(16), - child: Row( - children: [ - Icon( - Icons.network_check, - color: Theme.of(context).colorScheme.primary, - ), - const SizedBox( - width: 8, - ), - Flexible( - flex: 1, - child: FadeBox( - child: isTesting - ? Text( - appLocalizations.checking, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: - Theme.of(context).textTheme.titleMedium, - ) - : ipInfo != null - ? Container( - alignment: Alignment.centerLeft, - height: globalState - .measure.titleMediumHeight, - child: Text( - countryCodeToEmoji( - ipInfo.countryCode), - style: Theme.of(context) - .textTheme - .titleLarge - ?.copyWith( - fontFamily: - FontFamily.twEmoji.value, - ), - ), - ) - : Text( - appLocalizations.checkError, - style: Theme.of(context) - .textTheme - .titleMedium, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - ), - ], - ), - ), - ), - Container( - height: globalState.measure.titleLargeHeight + 24 - 2, - alignment: Alignment.centerLeft, - padding: const EdgeInsets.all(16).copyWith(top: 0), - child: FadeBox( - child: ipInfo != null - ? Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - flex: 1, - child: TooltipText( - text: Text( - ipInfo.ip, - style: context.textTheme.titleLarge - ?.toSoftBold.toMinus, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - ), - ], - ) - : FadeBox( - child: isTesting == false && ipInfo == null - ? Text( - "timeout", - style: context.textTheme.titleLarge - ?.copyWith(color: Colors.red) - .toSoftBold - .toMinus, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ) - : Container( - padding: const EdgeInsets.all(2), - child: const AspectRatio( - aspectRatio: 1, - child: CircularProgressIndicator(), - ), - ), - ), - ), - ) - ], - ), - ); - }, - ), - ); - } -} diff --git a/lib/fragments/dashboard/network_speed.dart b/lib/fragments/dashboard/network_speed.dart deleted file mode 100644 index a999f43..0000000 --- a/lib/fragments/dashboard/network_speed.dart +++ /dev/null @@ -1,166 +0,0 @@ -import 'package:fl_clash/common/common.dart'; -import 'package:fl_clash/models/models.dart'; -import 'package:fl_clash/state.dart'; -import 'package:fl_clash/widgets/widgets.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -class NetworkSpeed extends StatefulWidget { - const NetworkSpeed({super.key}); - - @override - State createState() => _NetworkSpeedState(); -} - -class _NetworkSpeedState extends State { - List initPoints = const [Point(0, 0), Point(1, 0)]; - - List _getPoints(List traffics) { - List trafficPoints = traffics - .toList() - .asMap() - .map( - (index, e) => MapEntry( - index, - Point( - (index + initPoints.length).toDouble(), - e.speed.toDouble(), - ), - ), - ) - .values - .toList(); - - return [...initPoints, ...trafficPoints]; - } - - Traffic _getLastTraffic(List traffics) { - if (traffics.isEmpty) return Traffic(); - return traffics.last; - } - - Widget _getLabel({ - required String label, - required IconData iconData, - required TrafficValue value, - }) { - final showValue = value.showValue; - final showUnit = "${value.showUnit}/s"; - final titleLargeSoftBold = - Theme.of(context).textTheme.titleLarge?.toSoftBold; - final bodyMedium = Theme.of(context).textTheme.bodySmall?.toLight; - final valueText = Text( - showValue, - style: titleLargeSoftBold, - maxLines: 1, - ); - final unitText = Text( - showUnit, - style: bodyMedium, - maxLines: 1, - ); - final size = globalState.measure.computeTextSize(valueText); - - return Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: MainAxisSize.max, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.max, - children: [ - Flexible( - child: Icon(iconData), - ), - Flexible( - child: Text( - label, - style: Theme.of(context).textTheme.titleSmall?.toSoftBold, - ), - ), - ], - ), - SizedBox( - width: size.width, - height: size.height, - child: OverflowBox( - maxWidth: 156, - alignment: Alignment.centerLeft, - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Flexible( - child: valueText, - ), - const Flexible( - flex: 0, - child: SizedBox( - width: 4, - ), - ), - Flexible( - child: unitText, - ), - ], - ), - )) - ], - ); - } - - @override - Widget build(BuildContext context) { - return CommonCard( - onPressed: () {}, - info: Info( - label: appLocalizations.networkSpeed, - iconData: Icons.speed_sharp, - ), - child: Selector>( - selector: (_, appFlowingState) => appFlowingState.traffics, - builder: (_, traffics, __) { - return Container( - padding: const EdgeInsets.all(16), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - flex: 0, - child: LineChart( - color: Theme.of(context).colorScheme.primary, - points: _getPoints(traffics), - height: 100, - ), - ), - const Flexible(child: SizedBox(height: 16)), - Flexible( - flex: 0, - child: Row( - mainAxisSize: MainAxisSize.max, - children: [ - Expanded( - child: _getLabel( - iconData: Icons.upload, - label: appLocalizations.upload, - value: _getLastTraffic(traffics).up, - ), - ), - Expanded( - child: _getLabel( - iconData: Icons.download, - label: appLocalizations.download, - value: _getLastTraffic(traffics).down, - ), - ), - ], - ), - ) - ], - ), - ); - }, - ), - ); - } -} diff --git a/lib/fragments/dashboard/outbound_mode.dart b/lib/fragments/dashboard/outbound_mode.dart deleted file mode 100644 index acec28a..0000000 --- a/lib/fragments/dashboard/outbound_mode.dart +++ /dev/null @@ -1,64 +0,0 @@ -import 'package:fl_clash/common/common.dart'; -import 'package:fl_clash/enum/enum.dart'; -import 'package:fl_clash/models/models.dart'; -import 'package:fl_clash/state.dart'; -import 'package:fl_clash/widgets/widgets.dart'; -import 'package:flutter/material.dart'; -import 'package:intl/intl.dart'; -import 'package:provider/provider.dart'; - -class OutboundMode extends StatelessWidget { - const OutboundMode({super.key}); - - @override - Widget build(BuildContext context) { - return Selector( - selector: (_, clashConfig) => clashConfig.mode, - builder: (_, mode, __) { - return CommonCard( - onPressed: () {}, - info: Info( - label: appLocalizations.outboundMode, - iconData: Icons.call_split_sharp, - ), - child: Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - for (final item in Mode.values) - ListItem.radio( - horizontalTitleGap: 4, - prue: true, - padding: const EdgeInsets.only( - left: 12, - right: 16, - top: 8, - bottom: 8, - ), - delegate: RadioDelegate( - value: item, - groupValue: mode, - onChanged: (value) async { - if (value == null) { - return; - } - globalState.appController.changeMode(value); - }, - ), - title: Text( - Intl.message(item.name), - style: - Theme.of(context).textTheme.titleMedium?.toSoftBold, - ), - ), - ], - ), - ), - ); - }, - ); - } -} diff --git a/lib/fragments/dashboard/status_button.dart b/lib/fragments/dashboard/status_button.dart deleted file mode 100644 index cb6e4b9..0000000 --- a/lib/fragments/dashboard/status_button.dart +++ /dev/null @@ -1,131 +0,0 @@ -import 'package:fl_clash/common/app_localizations.dart'; -import 'package:fl_clash/common/system.dart'; -import 'package:fl_clash/models/models.dart'; -import 'package:fl_clash/state.dart'; -import 'package:fl_clash/widgets/widgets.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -import '../config/network.dart'; - -class TUNButton extends StatelessWidget { - const TUNButton({super.key}); - - @override - Widget build(BuildContext context) { - return ButtonContainer( - onPressed: () { - showSheet( - context: context, - builder: (_) { - return generateListView(generateSection( - items: [ - if (system.isDesktop) const TUNItem(), - const TunStackItem(), - ], - )); - }, - title: appLocalizations.tun, - ); - }, - info: Info( - label: appLocalizations.tun, - iconData: Icons.stacked_line_chart, - ), - child: Selector( - selector: (_, clashConfig) => clashConfig.tun.enable, - builder: (_, enable, __) { - return LocaleBuilder( - builder: (_) => Switch( - value: enable, - onChanged: (value) { - final clashConfig = globalState.appController.clashConfig; - clashConfig.tun = clashConfig.tun.copyWith( - enable: value, - ); - }, - ), - ); - }, - ), - ); - } -} - -class SystemProxyButton extends StatelessWidget { - const SystemProxyButton({super.key}); - - @override - Widget build(BuildContext context) { - return ButtonContainer( - onPressed: () { - showSheet( - context: context, - builder: (_) { - return generateListView( - generateSection( - items: [ - SystemProxyItem(), - BypassDomainItem(), - ], - ), - ); - }, - title: appLocalizations.systemProxy, - ); - }, - info: Info( - label: appLocalizations.systemProxy, - iconData: Icons.shuffle, - ), - child: Selector( - selector: (_, config) => config.networkProps.systemProxy, - builder: (_, systemProxy, __) { - return LocaleBuilder( - builder: (_) => Switch( - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - value: systemProxy, - onChanged: (value) { - final config = globalState.appController.config; - config.networkProps = - config.networkProps.copyWith(systemProxy: value); - }, - ), - ); - }, - ), - ); - } -} - -class ButtonContainer extends StatelessWidget { - final Info info; - final Widget child; - final VoidCallback onPressed; - - const ButtonContainer({ - super.key, - required this.info, - required this.child, - required this.onPressed, - }); - - @override - Widget build(BuildContext context) { - return CommonCard( - onPressed: onPressed, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - InfoHeader( - info: info, - actions: [ - child, - ], - ), - ], - ), - ); - } -} diff --git a/lib/fragments/dashboard/traffic_usage.dart b/lib/fragments/dashboard/traffic_usage.dart deleted file mode 100644 index 99605a6..0000000 --- a/lib/fragments/dashboard/traffic_usage.dart +++ /dev/null @@ -1,95 +0,0 @@ -import 'package:fl_clash/common/common.dart'; -import 'package:fl_clash/models/models.dart'; -import 'package:fl_clash/widgets/widgets.dart'; -import 'package:flutter/material.dart'; -import 'package:provider/provider.dart'; - -class TrafficUsage extends StatelessWidget { - const TrafficUsage({super.key}); - - Widget getTrafficDataItem( - BuildContext context, - IconData iconData, - TrafficValue trafficValue, - ) { - return Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - mainAxisSize: MainAxisSize.max, - children: [ - Flexible( - flex: 1, - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Icon( - iconData, - size: 18, - ), - const SizedBox( - width: 8, - ), - Flexible( - flex: 1, - child: Text( - trafficValue.showValue, - style: context.textTheme.labelLarge?.copyWith(fontSize: 18), - maxLines: 1, - ), - ), - ], - ), - ), - Text( - trafficValue.showUnit, - style: context.textTheme.labelMedium?.toLight, - ), - ], - ); - } - - @override - Widget build(BuildContext context) { - return CommonCard( - onPressed: () {}, - info: Info( - label: appLocalizations.trafficUsage, - iconData: Icons.data_saver_off, - ), - child: Selector( - selector: (_, appFlowingState) => appFlowingState.totalTraffic, - builder: (_, totalTraffic, __) { - final upTotalTrafficValue = totalTraffic.up; - final downTotalTrafficValue = totalTraffic.down; - return Padding( - padding: const EdgeInsets.all(16).copyWith(top: 0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - flex: 1, - child: getTrafficDataItem( - context, - Icons.arrow_upward, - upTotalTrafficValue, - ), - ), - const SizedBox( - height: 4, - ), - Flexible( - flex: 1, - child: getTrafficDataItem( - context, - Icons.arrow_downward, - downTotalTrafficValue, - ), - ), - ], - ), - ); - }, - ), - ); - } -} diff --git a/lib/fragments/dashboard/core_info.dart b/lib/fragments/dashboard/widgets/core_info.dart similarity index 100% rename from lib/fragments/dashboard/core_info.dart rename to lib/fragments/dashboard/widgets/core_info.dart diff --git a/lib/fragments/dashboard/widgets/intranet_ip.dart b/lib/fragments/dashboard/widgets/intranet_ip.dart new file mode 100644 index 0000000..2586d41 --- /dev/null +++ b/lib/fragments/dashboard/widgets/intranet_ip.dart @@ -0,0 +1,64 @@ +import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/models/app.dart'; +import 'package:fl_clash/state.dart'; +import 'package:fl_clash/widgets/widgets.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class IntranetIP extends StatelessWidget { + const IntranetIP({super.key}); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: getWidgetHeight(1), + child: CommonCard( + info: Info( + label: appLocalizations.intranetIP, + iconData: Icons.devices, + ), + onPressed: () {}, + child: Container( + padding: baseInfoEdgeInsets.copyWith( + top: 0, + ), + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + SizedBox( + height: globalState.measure.bodyMediumHeight + 2, + child: Selector( + selector: (_, appFlowingState) => appFlowingState.localIp, + builder: (_, value, __) { + return FadeBox( + child: value != null + ? TooltipText( + text: Text( + value.isNotEmpty + ? value + : appLocalizations.noNetwork, + style: context.textTheme.bodyMedium?.toLight + .adjustSize(1), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ) + : Container( + padding: EdgeInsets.all(2), + child: AspectRatio( + aspectRatio: 1, + child: CircularProgressIndicator(), + ), + ), + ); + }, + ), + ) + ], + ), + ), + ), + ); + } +} diff --git a/lib/fragments/dashboard/widgets/memory_info.dart b/lib/fragments/dashboard/widgets/memory_info.dart new file mode 100644 index 0000000..3d68863 --- /dev/null +++ b/lib/fragments/dashboard/widgets/memory_info.dart @@ -0,0 +1,111 @@ +import 'dart:async'; + +import 'package:fl_clash/clash/clash.dart'; +import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/models/common.dart'; +import 'package:fl_clash/widgets/widgets.dart'; +import 'package:flutter/material.dart'; + +final _memoryInfoStateNotifier = + ValueNotifier(TrafficValue(value: 0)); + +class MemoryInfo extends StatefulWidget { + const MemoryInfo({super.key}); + + @override + State createState() => _MemoryInfoState(); +} + +class _MemoryInfoState extends State { + Timer? timer; + + @override + void initState() { + super.initState(); + clashCore.getMemory().then((memory) { + _memoryInfoStateNotifier.value = TrafficValue(value: memory); + }); + _updateMemoryData(); + } + + @override + void dispose() { + timer?.cancel(); + super.dispose(); + } + + _updateMemoryData() { + timer = Timer(Duration(seconds: 2), () async { + final memory = await clashCore.getMemory(); + _memoryInfoStateNotifier.value = TrafficValue(value: memory); + _updateMemoryData(); + }); + } + + @override + Widget build(BuildContext context) { + return SizedBox( + height: getWidgetHeight(2), + child: CommonCard( + info: Info( + iconData: Icons.memory, + label: appLocalizations.memoryInfo, + ), + onPressed: () {}, + child: ValueListenableBuilder( + valueListenable: _memoryInfoStateNotifier, + builder: (_, trafficValue, __) { + return Column( + children: [ + Padding( + padding: baseInfoEdgeInsets.copyWith( + bottom: 0, + top: 12, + ), + child: Row( + children: [ + Text( + trafficValue.showValue, + style: context.textTheme.titleLarge?.toLight, + ), + SizedBox( + width: 8, + ), + Text( + trafficValue.showUnit, + style: context.textTheme.titleLarge?.toLight, + ) + ], + ), + ), + Flexible( + child: Stack( + children: [ + Positioned.fill( + child: WaveView( + waveAmplitude: 12.0, + waveFrequency: 0.35, + waveColor: context.colorScheme.secondaryContainer + .blendDarken(context, factor: 0.1) + .toLighter, + ), + ), + Positioned.fill( + child: WaveView( + waveAmplitude: 12.0, + waveFrequency: 0.9, + waveColor: context.colorScheme.secondaryContainer + .blendDarken(context, factor: 0.1), + ), + ), + ], + ), + ) + ], + ); + }, + ), + ), + ); + } +} diff --git a/lib/fragments/dashboard/widgets/network_detection.dart b/lib/fragments/dashboard/widgets/network_detection.dart new file mode 100644 index 0000000..78b90a0 --- /dev/null +++ b/lib/fragments/dashboard/widgets/network_detection.dart @@ -0,0 +1,250 @@ +import 'dart:async'; + +import 'package:dio/dio.dart'; +import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/enum/enum.dart'; +import 'package:fl_clash/models/models.dart'; +import 'package:fl_clash/state.dart'; +import 'package:fl_clash/widgets/widgets.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +final _networkDetectionState = ValueNotifier( + const NetworkDetectionState( + isTesting: true, + ipInfo: null, + ), +); + +class NetworkDetection extends StatefulWidget { + const NetworkDetection({super.key}); + + @override + State createState() => _NetworkDetectionState(); +} + +class _NetworkDetectionState extends State { + bool? _preIsStart; + Timer? _setTimeoutTimer; + CancelToken? cancelToken; + Completer? checkedCompleter; + + @override + void initState() { + super.initState(); + } + + _startCheck() async { + await checkedCompleter?.future; + if (cancelToken != null) { + cancelToken!.cancel(); + cancelToken = null; + } + debouncer.call( + DebounceTag.checkIp, + _checkIp, + ); + } + + _checkIp() async { + final appState = globalState.appController.appState; + final appFlowingState = globalState.appController.appFlowingState; + final isInit = appState.isInit; + if (!isInit) return; + final isStart = appFlowingState.isStart; + if (_preIsStart == false && _preIsStart == isStart) return; + _clearSetTimeoutTimer(); + _networkDetectionState.value = _networkDetectionState.value.copyWith( + isTesting: true, + ipInfo: null, + ); + _preIsStart = isStart; + if (cancelToken != null) { + cancelToken!.cancel(); + cancelToken = null; + } + cancelToken = CancelToken(); + try { + final ipInfo = await request.checkIp(cancelToken: cancelToken); + if (ipInfo != null) { + checkedCompleter = Completer(); + checkedCompleter?.complete( + Future.delayed( + Duration(milliseconds: 3000), + ), + ); + _networkDetectionState.value = _networkDetectionState.value.copyWith( + isTesting: false, + ipInfo: ipInfo, + ); + return; + } + _clearSetTimeoutTimer(); + _setTimeoutTimer = Timer(const Duration(milliseconds: 300), () { + _networkDetectionState.value = _networkDetectionState.value.copyWith( + isTesting: false, + ipInfo: null, + ); + }); + } catch (e) { + if (e.toString() == "cancelled") { + _networkDetectionState.value = _networkDetectionState.value.copyWith( + isTesting: true, + ipInfo: null, + ); + } + } + } + + @override + void dispose() { + _clearSetTimeoutTimer(); + super.dispose(); + } + + _clearSetTimeoutTimer() { + if (_setTimeoutTimer != null) { + _setTimeoutTimer?.cancel(); + _setTimeoutTimer = null; + } + } + + _checkIpContainer(Widget child) { + return Selector( + selector: (_, appState) { + return appState.checkIpNum; + }, + shouldRebuild: (prev, next) { + if (prev != next) { + _startCheck(); + } + return prev != next; + }, + builder: (_, checkIpNum, child) { + return child!; + }, + child: child, + ); + } + + _countryCodeToEmoji(String countryCode) { + final String code = countryCode.toUpperCase(); + if (code.length != 2) { + return countryCode; + } + final int firstLetter = code.codeUnitAt(0) - 0x41 + 0x1F1E6; + final int secondLetter = code.codeUnitAt(1) - 0x41 + 0x1F1E6; + return String.fromCharCode(firstLetter) + String.fromCharCode(secondLetter); + } + + @override + Widget build(BuildContext context) { + return SizedBox( + height: getWidgetHeight(1), + child: _checkIpContainer( + ValueListenableBuilder( + valueListenable: _networkDetectionState, + builder: (_, state, __) { + final ipInfo = state.ipInfo; + final isTesting = state.isTesting; + return CommonCard( + onPressed: () {}, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + height: globalState.measure.titleMediumHeight + 16, + padding: baseInfoEdgeInsets.copyWith( + bottom: 0, + ), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + ipInfo != null + ? Text( + _countryCodeToEmoji( + ipInfo.countryCode, + ), + style: Theme.of(context) + .textTheme + .titleMedium + ?.toLight + .copyWith( + fontFamily: FontFamily.twEmoji.value, + ), + ) + : Icon( + Icons.network_check, + color: Theme.of(context) + .colorScheme + .onSurfaceVariant, + ), + const SizedBox( + width: 8, + ), + Flexible( + flex: 1, + child: TooltipText( + text: Text( + appLocalizations.networkDetection, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context) + .textTheme + .titleSmall + ?.copyWith( + color: context.colorScheme.onSurfaceVariant, + ), + ), + ), + ), + ], + ), + ), + Container( + padding: baseInfoEdgeInsets.copyWith( + top: 0, + ), + child: SizedBox( + height: globalState.measure.bodyMediumHeight + 2, + child: FadeBox( + child: ipInfo != null + ? TooltipText( + text: Text( + ipInfo.ip, + style: context.textTheme.bodyMedium?.toLight + .adjustSize(1), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ) + : FadeBox( + child: isTesting == false && ipInfo == null + ? Text( + "timeout", + style: context.textTheme.bodyMedium + ?.copyWith(color: Colors.red) + .adjustSize(1), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ) + : Container( + padding: const EdgeInsets.all(2), + child: const AspectRatio( + aspectRatio: 1, + child: CircularProgressIndicator(), + ), + ), + ), + ), + ), + ) + ], + ), + ); + }, + ), + ), + ); + } +} diff --git a/lib/fragments/dashboard/widgets/network_speed.dart b/lib/fragments/dashboard/widgets/network_speed.dart new file mode 100644 index 0000000..c06b557 --- /dev/null +++ b/lib/fragments/dashboard/widgets/network_speed.dart @@ -0,0 +1,124 @@ +import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/models/models.dart'; +import 'package:fl_clash/widgets/widgets.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class NetworkSpeed extends StatefulWidget { + const NetworkSpeed({super.key}); + + @override + State createState() => _NetworkSpeedState(); +} + +class _NetworkSpeedState extends State { + List initPoints = const [Point(0, 0), Point(1, 0)]; + + List _getPoints(List traffics) { + List trafficPoints = traffics + .toList() + .asMap() + .map( + (index, e) => MapEntry( + index, + Point( + (index + initPoints.length).toDouble(), + e.speed.toDouble(), + ), + ), + ) + .values + .toList(); + + return [...initPoints, ...trafficPoints]; + } + + Traffic _getLastTraffic(List traffics) { + if (traffics.isEmpty) return Traffic(); + return traffics.last; + } + + @override + Widget build(BuildContext context) { + final color = context.colorScheme.onSurfaceVariant.toLight; + return SizedBox( + height: getWidgetHeight(2), + child: CommonCard( + onPressed: () {}, + info: Info( + label: appLocalizations.networkSpeed, + iconData: Icons.speed_sharp, + ), + child: Selector>( + selector: (_, appFlowingState) => appFlowingState.traffics, + builder: (_, traffics, __) { + return Stack( + children: [ + Positioned.fill( + child: Padding( + padding: EdgeInsets.all(16).copyWith( + bottom: 0, + left: 0, + right: 0, + ), + child: LineChart( + gradient: true, + color: Theme.of(context).colorScheme.primary, + points: _getPoints(traffics), + ), + ), + ), + Positioned( + top: 0, + right: 0, + child: Transform.translate( + offset: Offset( + -16, + -20, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Icon( + Icons.arrow_upward, + color: color, + size: 16, + ), + SizedBox( + width: 2, + ), + Text( + "${_getLastTraffic(traffics).up}/s", + style: context.textTheme.bodySmall?.copyWith( + color: color, + ), + ), + SizedBox( + width: 16, + ), + Icon( + Icons.arrow_downward, + color: color, + size: 16, + ), + SizedBox( + width: 2, + ), + Text( + "${_getLastTraffic(traffics).down}/s", + style: context.textTheme.bodySmall?.copyWith( + color: color, + ), + ), + ], + ), + ), + ), + ], + ); + }, + ), + ), + ); + } +} diff --git a/lib/fragments/dashboard/widgets/outbound_mode.dart b/lib/fragments/dashboard/widgets/outbound_mode.dart new file mode 100644 index 0000000..6727c41 --- /dev/null +++ b/lib/fragments/dashboard/widgets/outbound_mode.dart @@ -0,0 +1,73 @@ +import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/enum/enum.dart'; +import 'package:fl_clash/models/models.dart'; +import 'package:fl_clash/state.dart'; +import 'package:fl_clash/widgets/widgets.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; + +class OutboundMode extends StatelessWidget { + const OutboundMode({super.key}); + + @override + Widget build(BuildContext context) { + final height = getWidgetHeight(2); + return SizedBox( + height: height, + child: Selector( + selector: (_, clashConfig) => clashConfig.mode, + builder: (_, mode, __) { + return CommonCard( + onPressed: () {}, + info: Info( + label: appLocalizations.outboundMode, + iconData: Icons.call_split_sharp, + ), + child: Padding( + padding: const EdgeInsets.only( + top: 12, + bottom: 16, + ), + child: Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + for (final item in Mode.values) + Flexible( + child: ListItem.radio( + prue: true, + horizontalTitleGap: 4, + padding: const EdgeInsets.only( + left: 12, + right: 16, + ), + delegate: RadioDelegate( + value: item, + groupValue: mode, + onChanged: (value) async { + if (value == null) { + return; + } + globalState.appController.changeMode(value); + }, + ), + title: Text( + Intl.message(item.name), + style: Theme.of(context) + .textTheme + .bodyMedium + ?.toSoftBold, + ), + ), + ), + ], + ), + ), + ); + }, + ), + ); + } +} diff --git a/lib/fragments/dashboard/widgets/quick_options.dart b/lib/fragments/dashboard/widgets/quick_options.dart new file mode 100644 index 0000000..2a69bfb --- /dev/null +++ b/lib/fragments/dashboard/widgets/quick_options.dart @@ -0,0 +1,156 @@ +import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/fragments/config/network.dart'; +import 'package:fl_clash/models/models.dart'; +import 'package:fl_clash/state.dart'; +import 'package:fl_clash/widgets/widgets.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class TUNButton extends StatelessWidget { + const TUNButton({super.key}); + + @override + Widget build(BuildContext context) { + return LocaleBuilder( + builder: (_) => SizedBox( + height: getWidgetHeight(1), + child: CommonCard( + onPressed: () { + showSheet( + context: context, + body: generateListView(generateSection( + items: [ + if (system.isDesktop) const TUNItem(), + const TunStackItem(), + ], + )), + title: appLocalizations.tun, + ); + }, + info: Info( + label: appLocalizations.tun, + iconData: Icons.stacked_line_chart, + ), + child: Container( + padding: baseInfoEdgeInsets.copyWith( + top: 4, + bottom: 8, + right: 8, + ), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + flex: 1, + child: TooltipText( + text: Text( + appLocalizations.options, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context) + .textTheme + .titleSmall + ?.adjustSize(-2) + .toLight, + ), + ), + ), + Selector( + selector: (_, clashConfig) => clashConfig.tun.enable, + builder: (_, enable, __) { + return Switch( + value: enable, + onChanged: (value) { + final clashConfig = + globalState.appController.clashConfig; + clashConfig.tun = clashConfig.tun.copyWith( + enable: value, + ); + }, + ); + }, + ) + ], + ), + ), + ), + ), + ); + } +} + +class SystemProxyButton extends StatelessWidget { + const SystemProxyButton({super.key}); + + @override + Widget build(BuildContext context) { + return SizedBox( + height: getWidgetHeight(1), + child: LocaleBuilder( + builder: (_) => CommonCard( + onPressed: () { + showSheet( + context: context, + body: generateListView( + generateSection( + items: [ + SystemProxyItem(), + BypassDomainItem(), + ], + ), + ), + title: appLocalizations.systemProxy, + ); + }, + info: Info( + label: appLocalizations.systemProxy, + iconData: Icons.shuffle, + ), + child: Container( + padding: baseInfoEdgeInsets.copyWith( + top: 4, + bottom: 8, + right: 8, + ), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + flex: 1, + child: TooltipText( + text: Text( + appLocalizations.options, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context) + .textTheme + .titleSmall + ?.adjustSize(-2) + .toLight, + ), + ), + ), + Selector( + selector: (_, config) => config.networkProps.systemProxy, + builder: (_, systemProxy, __) { + return Switch( + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + value: systemProxy, + onChanged: (value) { + final config = globalState.appController.config; + config.networkProps = + config.networkProps.copyWith(systemProxy: value); + }, + ); + }, + ) + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/fragments/dashboard/start_button.dart b/lib/fragments/dashboard/widgets/start_button.dart similarity index 100% rename from lib/fragments/dashboard/start_button.dart rename to lib/fragments/dashboard/widgets/start_button.dart diff --git a/lib/fragments/dashboard/widgets/traffic_usage.dart b/lib/fragments/dashboard/widgets/traffic_usage.dart new file mode 100644 index 0000000..7a0e121 --- /dev/null +++ b/lib/fragments/dashboard/widgets/traffic_usage.dart @@ -0,0 +1,220 @@ +import 'dart:math'; + +import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/models/models.dart'; +import 'package:fl_clash/state.dart'; +import 'package:fl_clash/widgets/widgets.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +class TrafficUsage extends StatelessWidget { + const TrafficUsage({super.key}); + + Widget getTrafficDataItem( + BuildContext context, + Icon icon, + TrafficValue trafficValue, + ) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.max, + children: [ + Flexible( + flex: 1, + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + icon, + const SizedBox( + width: 8, + ), + Flexible( + flex: 1, + child: Text( + trafficValue.showValue, + style: context.textTheme.bodySmall, + maxLines: 1, + ), + ), + ], + ), + ), + Text( + trafficValue.showUnit, + style: context.textTheme.bodySmall?.toLighter, + ), + ], + ); + } + + @override + Widget build(BuildContext context) { + final primaryColor = + context.colorScheme.surfaceContainer.blendDarken(context, factor: 0.2); + final secondaryColor = + context.colorScheme.primaryContainer.blendDarken(context, factor: 0.3); + return SizedBox( + height: getWidgetHeight(2), + child: CommonCard( + info: Info( + label: appLocalizations.trafficUsage, + iconData: Icons.data_saver_off, + ), + onPressed: () {}, + child: Selector( + selector: (_, appFlowingState) => appFlowingState.totalTraffic, + builder: (_, totalTraffic, __) { + final upTotalTrafficValue = totalTraffic.up; + final downTotalTrafficValue = totalTraffic.down; + return Padding( + padding: baseInfoEdgeInsets.copyWith( + top: 0, + ), + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Flexible( + child: Container( + padding: EdgeInsets.symmetric( + vertical: 12, + ), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + AspectRatio( + aspectRatio: 1, + child: DonutChart( + data: [ + DonutChartData( + value: upTotalTrafficValue.value.toDouble(), + color: primaryColor, + ), + DonutChartData( + value: downTotalTrafficValue.value.toDouble(), + color: secondaryColor, + ), + ], + ), + ), + SizedBox( + width: 8, + ), + Flexible( + child: LayoutBuilder( + builder: (_, container) { + final uploadText = Text( + maxLines: 1, + appLocalizations.upload, + overflow: TextOverflow.ellipsis, + style: context.textTheme.bodySmall, + ); + final downloadText = Text( + maxLines: 1, + appLocalizations.download, + overflow: TextOverflow.ellipsis, + style: context.textTheme.bodySmall, + ); + final uploadTextSize = globalState.measure + .computeTextSize(uploadText); + final downloadTextSize = globalState.measure + .computeTextSize(downloadText); + final maxTextWidth = max(uploadTextSize.width, + downloadTextSize.width); + if (maxTextWidth + 24 > container.maxWidth) { + return Container(); + } + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 20, + height: 8, + decoration: BoxDecoration( + color: primaryColor, + borderRadius: + BorderRadius.circular(2), + ), + ), + SizedBox( + width: 4, + ), + Text( + maxLines: 1, + appLocalizations.upload, + overflow: TextOverflow.ellipsis, + style: context.textTheme.bodySmall, + ), + ], + ), + SizedBox( + height: 4, + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 20, + height: 8, + decoration: BoxDecoration( + color: secondaryColor, + borderRadius: + BorderRadius.circular(2), + ), + ), + SizedBox( + width: 4, + ), + Text( + maxLines: 1, + appLocalizations.download, + overflow: TextOverflow.ellipsis, + style: context.textTheme.bodySmall, + ), + ], + ), + ], + ); + }, + ), + ), + ], + ), + ), + ), + getTrafficDataItem( + context, + Icon( + Icons.arrow_upward, + color: primaryColor, + size: 14, + ), + upTotalTrafficValue, + ), + const SizedBox( + height: 8, + ), + getTrafficDataItem( + context, + Icon( + Icons.arrow_downward, + color: secondaryColor, + size: 14, + ), + downTotalTrafficValue, + ) + ], + ), + ); + }, + ), + ), + ); + } +} diff --git a/lib/fragments/dashboard/widgets/widgets.dart b/lib/fragments/dashboard/widgets/widgets.dart new file mode 100644 index 0000000..fdb22e5 --- /dev/null +++ b/lib/fragments/dashboard/widgets/widgets.dart @@ -0,0 +1,7 @@ +export 'intranet_ip.dart'; +export 'network_detection.dart'; +export 'network_speed.dart'; +export 'outbound_mode.dart'; +export 'quick_options.dart'; +export 'traffic_usage.dart'; +export 'memory_info.dart'; \ No newline at end of file diff --git a/lib/fragments/logs.dart b/lib/fragments/logs.dart index 30bfb3f..409b34b 100644 --- a/lib/fragments/logs.dart +++ b/lib/fragments/logs.dart @@ -49,11 +49,11 @@ class _LogsFragmentState extends State { @override void dispose() { - super.dispose(); timer?.cancel(); logsNotifier.dispose(); scrollController.dispose(); timer = null; + super.dispose(); } _handleExport() async { @@ -87,9 +87,6 @@ class _LogsFragmentState extends State { }, icon: const Icon(Icons.search), ), - const SizedBox( - width: 8, - ), IconButton( onPressed: () { _handleExport(); @@ -235,8 +232,8 @@ class LogsSearchDelegate extends SearchDelegate { @override void dispose() { - super.dispose(); logsNotifier.dispose(); + super.dispose(); } get state => logsNotifier.value; diff --git a/lib/fragments/profiles/profiles.dart b/lib/fragments/profiles/profiles.dart index a1dd48c..d60af57 100644 --- a/lib/fragments/profiles/profiles.dart +++ b/lib/fragments/profiles/profiles.dart @@ -52,9 +52,6 @@ class _ProfilesFragmentState extends State { ); try { await appController.updateProfile(profile); - if (profile.id == appController.config.currentProfile?.id) { - appController.applyProfileDebounce(); - } } catch (e) { messages.add("${profile.label ?? profile.id}: $e \n"); config.setProfile( @@ -93,16 +90,13 @@ class _ProfilesFragmentState extends State { }, icon: const Icon(Icons.sync), ), - const SizedBox( - width: 8, - ), IconButton( onPressed: () { final profiles = globalState.appController.config.profiles; showSheet( title: appLocalizations.profilesSort, context: context, - builder: (_) => SizedBox( + body: SizedBox( height: 400, child: ReorderableProfiles(profiles: profiles), ), @@ -221,9 +215,6 @@ class ProfileItem extends StatelessWidget { ), ); await appController.updateProfile(profile); - if (profile.id == appController.config.currentProfile?.id) { - appController.applyProfileDebounce(); - } } catch (e) { config.setProfile( profile.copyWith( @@ -296,6 +287,7 @@ class ProfileItem extends StatelessWidget { child: CircularProgressIndicator(), ) : CommonPopupMenu( + icon: Icon(Icons.more_vert), items: [ CommonPopupMenuItem( action: ProfileActions.edit, diff --git a/lib/fragments/profiles/view_profile.dart b/lib/fragments/profiles/view_profile.dart index cf410cd..f05d47b 100644 --- a/lib/fragments/profiles/view_profile.dart +++ b/lib/fragments/profiles/view_profile.dart @@ -41,9 +41,9 @@ class _ViewProfileState extends State { @override void dispose() { - super.dispose(); _controller.dispose(); _focusNode.dispose(); + super.dispose(); } Profile get profile => widget.profile; diff --git a/lib/fragments/proxies/card.dart b/lib/fragments/proxies/card.dart index f239bcf..638a3af 100644 --- a/lib/fragments/proxies/card.dart +++ b/lib/fragments/proxies/card.dart @@ -11,7 +11,6 @@ class ProxyCard extends StatelessWidget { final String groupName; final Proxy proxy; final GroupType groupType; - final CommonCardType style; final ProxyCardType type; const ProxyCard({ @@ -19,7 +18,6 @@ class ProxyCard extends StatelessWidget { required this.groupName, required this.proxy, required this.groupType, - this.style = CommonCardType.plain, required this.type, }); @@ -115,15 +113,11 @@ class ProxyCard extends StatelessWidget { groupName, nextProxyName, ); - await appController.changeProxyDebounce([ - groupName, - nextProxyName, - ]); + await appController.changeProxyDebounce(groupName, nextProxyName); return; } - globalState.showSnackBar( - context, - message: appLocalizations.notSelectedTip, + globalState.showNotifier( + appLocalizations.notSelectedTip, ); } @@ -138,7 +132,6 @@ class ProxyCard extends StatelessWidget { return Stack( children: [ CommonCard( - type: style, key: key, onPressed: () { _changeProxy(context); @@ -167,8 +160,8 @@ class ProxyCard extends StatelessWidget { desc, overflow: TextOverflow.ellipsis, style: context.textTheme.bodySmall?.copyWith( - color: context.textTheme.bodySmall?.color - ?.toLight(), + color: + context.textTheme.bodySmall?.color?.toLight, ), ); }, @@ -192,8 +185,8 @@ class ProxyCard extends StatelessWidget { proxy.type, style: context.textTheme.bodySmall?.copyWith( overflow: TextOverflow.ellipsis, - color: context.textTheme.bodySmall?.color - ?.toLight(), + color: context + .textTheme.bodySmall?.color?.toLight, ), ), ), diff --git a/lib/fragments/proxies/list.dart b/lib/fragments/proxies/list.dart index 738d2ac..8549f14 100644 --- a/lib/fragments/proxies/list.dart +++ b/lib/fragments/proxies/list.dart @@ -65,10 +65,10 @@ class _ProxiesListFragmentState extends State { @override void dispose() { - super.dispose(); _headerStateNotifier.dispose(); _controller.removeListener(_adjustHeader); _controller.dispose(); + super.dispose(); } _handleChange(Set currentUnfoldSet, String groupName) { @@ -442,10 +442,10 @@ class _ListHeaderState extends State padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: context.colorScheme.secondaryContainer, - borderRadius: BorderRadius.circular(16), + borderRadius: BorderRadius.circular(12), ), clipBehavior: Clip.antiAlias, - child: CommonIcon( + child: CommonTargetIcon( src: icon, size: 32, ), @@ -454,7 +454,7 @@ class _ListHeaderState extends State margin: const EdgeInsets.only( right: 16, ), - child: CommonIcon( + child: CommonTargetIcon( src: icon, size: 42, ), @@ -471,7 +471,10 @@ class _ListHeaderState extends State Widget build(BuildContext context) { return CommonCard( key: widget.key, - radius: 18, + backgroundColor: WidgetStatePropertyAll( + context.colorScheme.surfaceContainer, + ), + radius: 14, type: CommonCardType.filled, child: Container( padding: const EdgeInsets.symmetric( diff --git a/lib/fragments/proxies/providers.dart b/lib/fragments/proxies/providers.dart index bc3d271..6e7836f 100644 --- a/lib/fragments/proxies/providers.dart +++ b/lib/fragments/proxies/providers.dart @@ -61,7 +61,7 @@ class _ProvidersState extends State { }, ); await Future.wait(updateProviders); - await globalState.appController.updateGroupDebounce(); + await globalState.appController.updateGroupsDebounce(); } @override @@ -125,7 +125,7 @@ class ProviderItem extends StatelessWidget { await clashCore.getExternalProvider(provider.name), ); }); - await globalState.appController.updateGroupDebounce(); + await globalState.appController.updateGroupsDebounce(); } _handleSideLoadProvider() async { @@ -147,7 +147,7 @@ class ProviderItem extends StatelessWidget { ); if (message.isNotEmpty) throw message; }); - await globalState.appController.updateGroupDebounce(); + await globalState.appController.updateGroupsDebounce(); } String _buildProviderDesc() { diff --git a/lib/fragments/proxies/proxies.dart b/lib/fragments/proxies/proxies.dart index 87b614f..bbfcb21 100644 --- a/lib/fragments/proxies/proxies.dart +++ b/lib/fragments/proxies/proxies.dart @@ -40,9 +40,6 @@ class _ProxiesFragmentState extends State { Icons.poll_outlined, ), ), - const SizedBox( - width: 8, - ), ], if (proxiesType == ProxiesType.tab) ...[ IconButton( @@ -53,9 +50,6 @@ class _ProxiesFragmentState extends State { Icons.adjust_outlined, ), ), - const SizedBox( - width: 8, - ) ] else ...[ IconButton( onPressed: () { @@ -85,7 +79,7 @@ class _ProxiesFragmentState extends State { borderRadius: BorderRadius.circular(16), ), clipBehavior: Clip.antiAlias, - child: CommonIcon( + child: CommonTargetIcon( src: item.value, size: 42, ), @@ -110,18 +104,13 @@ class _ProxiesFragmentState extends State { Icons.style_outlined, ), ), - const SizedBox( - width: 8, - ) ], IconButton( onPressed: () { showSheet( title: appLocalizations.proxiesSetting, context: context, - builder: (context) { - return const ProxiesSetting(); - }, + body: const ProxiesSetting(), ); }, icon: const Icon( diff --git a/lib/fragments/proxies/tab.dart b/lib/fragments/proxies/tab.dart index 6ecc388..5babe29 100644 --- a/lib/fragments/proxies/tab.dart +++ b/lib/fragments/proxies/tab.dart @@ -30,8 +30,8 @@ class ProxiesTabFragmentState extends State @override void dispose() { - super.dispose(); _destroyTabController(); + super.dispose(); } scrollToGroupSelected() { @@ -62,49 +62,46 @@ class ProxiesTabFragmentState extends State context: context, width: 380, isScrollControlled: false, - builder: (context) { - return SingleChildScrollView( - padding: const EdgeInsets.all(16), - child: Selector2( - selector: (_, appState, config) { - final currentGroups = appState.currentGroups; - final groupNames = currentGroups.map((e) => e.name).toList(); - return ProxiesSelectorState( - groupNames: groupNames, - currentGroupName: config.currentGroupName, - ); - }, - builder: (_, state, __) { - return SizedBox( - width: double.infinity, - child: Wrap( - alignment: WrapAlignment.center, - runSpacing: 8, - spacing: 8, - children: [ - for (final groupName in state.groupNames) - SettingTextCard( - groupName, - onPressed: () { - final index = state.groupNames - .indexWhere((item) => item == groupName); - if (index == -1) return; - _tabController?.animateTo(index); - globalState.appController.config - .updateCurrentGroupName( - groupName, - ); - Navigator.of(context).pop(); - }, - isSelected: groupName == state.currentGroupName, - ) - ], - ), - ); - }, - ), - ); - }, + body: SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Selector2( + selector: (_, appState, config) { + final currentGroups = appState.currentGroups; + final groupNames = currentGroups.map((e) => e.name).toList(); + return ProxiesSelectorState( + groupNames: groupNames, + currentGroupName: config.currentGroupName, + ); + }, + builder: (_, state, __) { + return SizedBox( + width: double.infinity, + child: Wrap( + alignment: WrapAlignment.center, + runSpacing: 8, + spacing: 8, + children: [ + for (final groupName in state.groupNames) + SettingTextCard( + groupName, + onPressed: () { + final index = state.groupNames + .indexWhere((item) => item == groupName); + if (index == -1) return; + _tabController?.animateTo(index); + globalState.appController.config.updateCurrentGroupName( + groupName, + ); + Navigator.of(context).pop(); + }, + isSelected: groupName == state.currentGroupName, + ) + ], + ), + ); + }, + ), + ), title: appLocalizations.proxyGroup, ); } @@ -282,8 +279,8 @@ class ProxyGroupViewState extends State { @override void dispose() { - super.dispose(); _controller.dispose(); + super.dispose(); } scrollToSelected() { diff --git a/lib/fragments/requests.dart b/lib/fragments/requests.dart index 328ae9e..53433c0 100644 --- a/lib/fragments/requests.dart +++ b/lib/fragments/requests.dart @@ -95,10 +95,10 @@ class _RequestsFragmentState extends State { @override void dispose() { - super.dispose(); timer?.cancel(); _scrollController.dispose(); timer = null; + super.dispose(); } @override diff --git a/lib/l10n/arb/intl_en.arb b/lib/l10n/arb/intl_en.arb index 68a5e16..940c2f3 100644 --- a/lib/l10n/arb/intl_en.arb +++ b/lib/l10n/arb/intl_en.arb @@ -332,5 +332,6 @@ "routeAddress": "Route address", "routeAddressDesc": "Config listen route address", "pleaseInputAdminPassword": "Please enter the admin password", - "copyEnvVar": "Copying environment variables" + "copyEnvVar": "Copying environment variables", + "memoryInfo": "Memory info" } \ 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 f49bb37..731aa01 100644 --- a/lib/l10n/arb/intl_zh_CN.arb +++ b/lib/l10n/arb/intl_zh_CN.arb @@ -332,5 +332,6 @@ "routeAddress": "路由地址", "routeAddressDesc": "配置监听路由地址", "pleaseInputAdminPassword": "请输入管理员密码", - "copyEnvVar": "复制环境变量" + "copyEnvVar": "复制环境变量", + "memoryInfo": "内存信息" } diff --git a/lib/l10n/intl/messages_en.dart b/lib/l10n/intl/messages_en.dart index 8757a49..21166c9 100644 --- a/lib/l10n/intl/messages_en.dart +++ b/lib/l10n/intl/messages_en.dart @@ -259,6 +259,7 @@ class MessageLookup extends MessageLookupByLibrary { "loopbackDesc": MessageLookupByLibrary.simpleMessage( "Used for UWP loopback unlocking"), "loose": MessageLookupByLibrary.simpleMessage("Loose"), + "memoryInfo": MessageLookupByLibrary.simpleMessage("Memory info"), "min": MessageLookupByLibrary.simpleMessage("Min"), "minimizeOnExit": MessageLookupByLibrary.simpleMessage("Minimize on exit"), diff --git a/lib/l10n/intl/messages_zh_CN.dart b/lib/l10n/intl/messages_zh_CN.dart index eab038d..b4a4dcd 100644 --- a/lib/l10n/intl/messages_zh_CN.dart +++ b/lib/l10n/intl/messages_zh_CN.dart @@ -205,6 +205,7 @@ class MessageLookup extends MessageLookupByLibrary { "loopback": MessageLookupByLibrary.simpleMessage("回环解锁工具"), "loopbackDesc": MessageLookupByLibrary.simpleMessage("用于UWP回环解锁"), "loose": MessageLookupByLibrary.simpleMessage("宽松"), + "memoryInfo": MessageLookupByLibrary.simpleMessage("内存信息"), "min": MessageLookupByLibrary.simpleMessage("最小"), "minimizeOnExit": MessageLookupByLibrary.simpleMessage("退出时最小化"), "minimizeOnExitDesc": diff --git a/lib/l10n/l10n.dart b/lib/l10n/l10n.dart index 6e3c9dd..2b9df9d 100644 --- a/lib/l10n/l10n.dart +++ b/lib/l10n/l10n.dart @@ -3389,6 +3389,16 @@ class AppLocalizations { args: [], ); } + + /// `Memory info` + String get memoryInfo { + return Intl.message( + 'Memory info', + name: 'memoryInfo', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/lib/manager/clash_manager.dart b/lib/manager/clash_manager.dart index 2ba2753..33a89b9 100644 --- a/lib/manager/clash_manager.dart +++ b/lib/manager/clash_manager.dart @@ -1,4 +1,5 @@ import 'package:fl_clash/clash/clash.dart'; +import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/state.dart'; @@ -20,8 +21,6 @@ class ClashManager extends StatefulWidget { } class _ClashContainerState extends State with AppMessageListener { - Function? updateDelayDebounce; - Widget _updateContainer(Widget child) { return Selector2( selector: (_, config, clashConfig) => ClashConfigState( @@ -103,18 +102,21 @@ class _ClashContainerState extends State with AppMessageListener { final appController = globalState.appController; appController.setDelay(delay); super.onDelay(delay); - updateDelayDebounce ??= debounce(() async { - await appController.updateGroupDebounce(); - await appController.addCheckIpNumDebounce(); - }, milliseconds: 5000); - updateDelayDebounce!(); + debouncer.call( + DebounceTag.updateDelay, + () async { + await appController.updateGroupsDebounce(); + // await appController.addCheckIpNumDebounce(); + }, + duration: const Duration(milliseconds: 5000), + ); } @override void onLog(Log log) { globalState.appController.appFlowingState.addLog(log); if (log.logLevel == LogLevel.error) { - globalState.appController.showSnackBar(log.payload ?? ''); + globalState.showNotifier(log.payload ?? ''); } super.onLog(log); } @@ -139,7 +141,7 @@ class _ClashContainerState extends State with AppMessageListener { providerName, ), ); - await appController.updateGroupDebounce(); + await appController.updateGroupsDebounce(); super.onLoaded(providerName); } } diff --git a/lib/manager/connectivity_manager.dart b/lib/manager/connectivity_manager.dart new file mode 100644 index 0000000..ee012aa --- /dev/null +++ b/lib/manager/connectivity_manager.dart @@ -0,0 +1,43 @@ +import 'dart:async'; + +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:flutter/material.dart'; + +class ConnectivityManager extends StatefulWidget { + final VoidCallback? onConnectivityChanged; + final Widget child; + + const ConnectivityManager({ + super.key, + this.onConnectivityChanged, + required this.child, + }); + + @override + State createState() => _ConnectivityManagerState(); +} + +class _ConnectivityManagerState extends State { + late StreamSubscription subscription; + + @override + void initState() { + super.initState(); + subscription = Connectivity().onConnectivityChanged.listen((_) async { + if (widget.onConnectivityChanged != null) { + widget.onConnectivityChanged!(); + } + }); + } + + @override + void dispose() { + subscription.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return widget.child; + } +} diff --git a/lib/manager/manager.dart b/lib/manager/manager.dart index 97d859d..d818169 100644 --- a/lib/manager/manager.dart +++ b/lib/manager/manager.dart @@ -6,4 +6,6 @@ export 'tile_manager.dart'; export 'app_state_manager.dart'; export 'vpn_manager.dart'; export 'media_manager.dart'; -export 'proxy_manager.dart'; \ No newline at end of file +export 'proxy_manager.dart'; +export 'connectivity_manager.dart'; +export 'message_manager.dart'; \ No newline at end of file diff --git a/lib/manager/message_manager.dart b/lib/manager/message_manager.dart new file mode 100644 index 0000000..385d18e --- /dev/null +++ b/lib/manager/message_manager.dart @@ -0,0 +1,326 @@ +import 'dart:async'; + +import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/models/models.dart'; +import 'package:fl_clash/state.dart'; +import 'package:flutter/material.dart'; + +class MessageManager extends StatefulWidget { + final Widget child; + + const MessageManager({ + super.key, + required this.child, + }); + + @override + State createState() => MessageManagerState(); +} + +class MessageManagerState extends State + with SingleTickerProviderStateMixin { + final _floatMessageKey = GlobalKey(); + List bufferMessages = []; + final _messagesNotifier = ValueNotifier>([]); + final _floatMessageNotifier = ValueNotifier(null); + double maxWidth = 0; + + late AnimationController _animationController; + + Completer? _animationCompleter; + late Animation _floatOffsetAnimation; + late Animation _commonOffsetAnimation; + final animationDuration = commonDuration * 2; + + _initTransformState() { + _floatMessageNotifier.value = null; + _floatOffsetAnimation = Tween( + begin: Offset.zero, + end: Offset.zero, + ).animate(_animationController); + _commonOffsetAnimation = _floatOffsetAnimation = Tween( + begin: Offset.zero, + end: Offset.zero, + ).animate(_animationController); + } + + @override + void initState() { + super.initState(); + _animationController = AnimationController( + vsync: this, + duration: Duration(milliseconds: 200), + ); + _initTransformState(); + } + + @override + void dispose() { + _messagesNotifier.dispose(); + _floatMessageNotifier.dispose(); + _animationController.dispose(); + super.dispose(); + } + + message(String text) async { + final commonMessage = CommonMessage( + id: other.uuidV4, + text: text, + ); + bufferMessages.add(commonMessage); + await _animationCompleter?.future; + _showMessage(); + } + + _showMessage() { + final commonMessage = bufferMessages.removeAt(0); + _floatOffsetAnimation = Tween( + begin: Offset(-maxWidth, 0), + end: Offset.zero, + ).animate( + CurvedAnimation( + parent: _animationController, + curve: Interval( + 0.5, + 1, + curve: Curves.easeInOut, + ), + ), + ); + _floatMessageNotifier.value = commonMessage; + WidgetsBinding.instance.addPostFrameCallback((_) async { + final size = _floatMessageKey.currentContext?.size ?? Size.zero; + _commonOffsetAnimation = Tween( + begin: Offset.zero, + end: Offset(0, -size.height - 12), + ).animate( + CurvedAnimation( + parent: _animationController, + curve: Interval( + 0, + 0.7, + curve: Curves.easeInOut, + ), + ), + ); + _animationCompleter = Completer(); + _animationCompleter?.complete(_animationController.forward(from: 0)); + await _animationCompleter?.future; + _initTransformState(); + _messagesNotifier.value = List.from(_messagesNotifier.value) + ..add(commonMessage); + Future.delayed( + commonMessage.duration, + () { + _removeMessage(commonMessage); + }, + ); + }); + } + + Widget _wrapOffset(Widget child) { + return AnimatedBuilder( + animation: _animationController.view, + builder: (context, child) { + return Transform.translate( + offset: _commonOffsetAnimation.value, + child: child!, + ); + }, + child: child, + ); + } + + Widget _wrapMessage(CommonMessage message) { + return Material( + elevation: 2, + borderRadius: BorderRadius.circular(8), + color: context.colorScheme.secondaryFixedDim, + clipBehavior: Clip.antiAlias, + child: Padding( + padding: EdgeInsets.all(16), + child: Text( + message.text, + style: context.textTheme.bodyMedium?.copyWith( + color: context.colorScheme.onSecondaryFixedVariant, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + ); + } + + Widget _floatMessage() { + return ValueListenableBuilder( + valueListenable: _floatMessageNotifier, + builder: (_, message, ___) { + if (message == null) { + return SizedBox(); + } + return AnimatedBuilder( + key: _floatMessageKey, + animation: _animationController.view, + builder: (_, child) { + if (!_animationController.isAnimating) { + return Opacity( + opacity: 0, + child: child, + ); + } + return Transform.translate( + offset: _floatOffsetAnimation.value, + child: child, + ); + }, + child: _wrapMessage( + message, + ), + ); + }, + ); + } + + _removeMessage(CommonMessage commonMessage) async { + final itemWrapState = GlobalObjectKey(commonMessage.id).currentState + as _MessageItemWrapState?; + await itemWrapState?.transform( + Offset(-maxWidth, 0), + ); + _messagesNotifier.value = List.from(_messagesNotifier.value) + ..remove(commonMessage); + } + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + widget.child, + LayoutBuilder( + builder: (context, container) { + maxWidth = container.maxWidth / 2 + 16; + return SizedBox( + width: maxWidth, + child: ValueListenableBuilder( + valueListenable: globalState.safeMessageOffsetNotifier, + builder: (_, offset, child) { + if (offset == Offset.zero) { + return SizedBox(); + } + return Transform.translate( + offset: offset, + child: child!, + ); + }, + child: Container( + padding: EdgeInsets.only( + right: 0, + left: 8, + top: 0, + bottom: 16, + ), + alignment: Alignment.bottomLeft, + child: Stack( + alignment: Alignment.bottomLeft, + children: [ + SingleChildScrollView( + reverse: true, + physics: NeverScrollableScrollPhysics(), + child: ValueListenableBuilder( + valueListenable: _messagesNotifier, + builder: (_, messages, ___) { + return Column( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 12, + children: [ + for (final message in messages) + _MessageItemWrap( + key: GlobalObjectKey(message.id), + child: _wrapOffset( + _wrapMessage(message), + ), + ), + ], + ); + }, + ), + ), + _floatMessage(), + ], + ), + ), + ), + ); + }, + ) + ], + ); + } +} + +class _MessageItemWrap extends StatefulWidget { + final Widget child; + + const _MessageItemWrap({ + super.key, + required this.child, + }); + + @override + State<_MessageItemWrap> createState() => _MessageItemWrapState(); +} + +class _MessageItemWrapState extends State<_MessageItemWrap> + with SingleTickerProviderStateMixin { + late AnimationController _controller; + Offset _nextOffset = Offset.zero; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: commonDuration * 1.5, + ); + } + + transform(Offset offset) async { + _nextOffset = offset; + await _controller.forward(from: 0); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _controller.view, + builder: (_, child) { + if (_nextOffset == Offset.zero) { + return child!; + } + final offset = Tween( + begin: Offset.zero, + end: _nextOffset, + ) + .animate( + CurvedAnimation( + parent: _controller, + curve: Curves.easeOut, + ), + ) + .value; + return Transform.translate( + offset: offset, + child: child!, + ); + }, + child: widget.child, + ); + } +} diff --git a/lib/manager/vpn_manager.dart b/lib/manager/vpn_manager.dart index 2dd1e7b..6b734f3 100644 --- a/lib/manager/vpn_manager.dart +++ b/lib/manager/vpn_manager.dart @@ -1,11 +1,10 @@ -import 'package:fl_clash/common/app_localizations.dart'; +import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/state.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -import '../../common/function.dart'; - class VpnManager extends StatefulWidget { final Widget child; @@ -19,21 +18,20 @@ class VpnManager extends StatefulWidget { } class _VpnContainerState extends State { - Function? vpnTipDebounce; - showTip() { - vpnTipDebounce ??= debounce(() async { - WidgetsBinding.instance.addPostFrameCallback((_) { - final appFlowingState = globalState.appController.appFlowingState; - if (appFlowingState.isStart) { - globalState.showSnackBar( - context, - message: appLocalizations.vpnTip, - ); - } - }); - }); - vpnTipDebounce!(); + debouncer.call( + DebounceTag.vpnTip, + () { + WidgetsBinding.instance.addPostFrameCallback((_) { + final appFlowingState = globalState.appController.appFlowingState; + if (appFlowingState.isStart) { + globalState.showNotifier( + appLocalizations.vpnTip, + ); + } + }); + }, + ); } @override diff --git a/lib/manager/window_manager.dart b/lib/manager/window_manager.dart index 8cacdea..3ba7586 100644 --- a/lib/manager/window_manager.dart +++ b/lib/manager/window_manager.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/state.dart'; import 'package:flutter/material.dart'; @@ -25,15 +26,20 @@ class _WindowContainerState extends State Function? updateLaunchDebounce; _autoLaunchContainer(Widget child) { - return Selector( - selector: (_, config) => AutoLaunchState( - isAutoLaunch: config.appSetting.autoLaunch, - ), + return Selector( + selector: (_, config) => config.appSetting.autoLaunch, + shouldRebuild: (prev, next) { + if (prev != next) { + debouncer.call( + DebounceTag.autoLaunch, + () { + autoLaunch?.updateStatus(next); + }, + ); + } + return prev != next; + }, builder: (_, state, child) { - updateLaunchDebounce ??= debounce((AutoLaunchState state) { - autoLaunch?.updateStatus(state); - }); - updateLaunchDebounce!([state]); return child!; }, child: child, @@ -169,9 +175,9 @@ class _WindowHeaderState extends State { @override void dispose() { - super.dispose(); isMaximizedNotifier.dispose(); isPinNotifier.dispose(); + super.dispose(); } _updateMaximized() { @@ -261,7 +267,7 @@ class _WindowHeaderState extends State { _updateMaximized(); }, child: Container( - color: context.colorScheme.secondary.toSoft(), + color: context.colorScheme.secondary.toSoft, alignment: Alignment.centerLeft, height: kHeaderHeight, ), diff --git a/lib/models/app.dart b/lib/models/app.dart index 87c8f58..b12d607 100644 --- a/lib/models/app.dart +++ b/lib/models/app.dart @@ -306,6 +306,7 @@ class AppFlowingState with ChangeNotifier { List _logs; List _traffics; Traffic _totalTraffic; + String? _localIp; AppFlowingState() : _logs = [], @@ -350,7 +351,7 @@ class AppFlowingState with ChangeNotifier { addTraffic(Traffic traffic) { _traffics = List.from(_traffics)..add(traffic); - const maxLength = 60; + const maxLength = 30; _traffics = _traffics.safeSublist(_traffics.length - maxLength); notifyListeners(); } @@ -363,4 +364,13 @@ class AppFlowingState with ChangeNotifier { notifyListeners(); } } + + String? get localIp => _localIp; + + set localIp(String? value) { + if (_localIp != value) { + _localIp = value; + notifyListeners(); + } + } } diff --git a/lib/models/common.dart b/lib/models/common.dart index 4f95f79..0112daa 100644 --- a/lib/models/common.dart +++ b/lib/models/common.dart @@ -180,7 +180,7 @@ class Traffic { TrafficValue up; TrafficValue down; - Traffic({num? up, num? down}) + Traffic({int? up, int? down}) : id = DateTime.now().millisecondsSinceEpoch, up = TrafficValue(value: up), down = TrafficValue(value: down); @@ -225,11 +225,11 @@ class TrafficValueShow { @immutable class TrafficValue { - final num _value; + final int _value; - const TrafficValue({num? value}) : _value = value ?? 0; + const TrafficValue({int? value}) : _value = value ?? 0; - num get value => _value; + int get value => _value; String get show => "$showValue $showUnit"; @@ -343,7 +343,7 @@ class SystemColorSchemes { ); } return lightColorScheme != null - ? ColorScheme.fromSeed(seedColor: darkColorScheme!.primary) + ? ColorScheme.fromSeed(seedColor: lightColorScheme!.primary) : ColorScheme.fromSeed(seedColor: defaultPrimaryColor); } } diff --git a/lib/models/config.dart b/lib/models/config.dart index bc8bc77..c389c17 100644 --- a/lib/models/config.dart +++ b/lib/models/config.dart @@ -1,3 +1,5 @@ +// ignore_for_file: invalid_annotation_target + import 'dart:io'; import 'package:fl_clash/common/common.dart'; @@ -8,16 +10,42 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'models.dart'; part 'generated/config.freezed.dart'; + part 'generated/config.g.dart'; final defaultAppSetting = const AppSetting().copyWith( isAnimateToPage: system.isDesktop ? false : true, ); +const List defaultDashboardWidgets = [ + DashboardWidget.networkSpeed, + DashboardWidget.systemProxyButton, + DashboardWidget.tunButton, + DashboardWidget.outboundMode, + DashboardWidget.networkDetection, + DashboardWidget.trafficUsage, + DashboardWidget.intranetIp, +]; + +List dashboardWidgetsRealFormJson( + List? dashboardWidgets) { + try { + return dashboardWidgets + ?.map((e) => $enumDecode(_$DashboardWidgetEnumMap, e)) + .toList() ?? + defaultDashboardWidgets; + } catch (_) { + return defaultDashboardWidgets; + } +} + @freezed class AppSetting with _$AppSetting { const factory AppSetting({ String? locale, + @JsonKey(fromJson: dashboardWidgetsRealFormJson) + @Default(defaultDashboardWidgets) + List dashboardWidgets, @Default(false) bool onlyProxy, @Default(false) bool autoLaunch, @Default(false) bool silentLaunch, diff --git a/lib/models/generated/config.freezed.dart b/lib/models/generated/config.freezed.dart index 7402046..e60efc9 100644 --- a/lib/models/generated/config.freezed.dart +++ b/lib/models/generated/config.freezed.dart @@ -21,6 +21,9 @@ AppSetting _$AppSettingFromJson(Map json) { /// @nodoc mixin _$AppSetting { String? get locale => throw _privateConstructorUsedError; + @JsonKey(fromJson: dashboardWidgetsRealFormJson) + List get dashboardWidgets => + throw _privateConstructorUsedError; bool get onlyProxy => throw _privateConstructorUsedError; bool get autoLaunch => throw _privateConstructorUsedError; bool get silentLaunch => throw _privateConstructorUsedError; @@ -53,6 +56,8 @@ abstract class $AppSettingCopyWith<$Res> { @useResult $Res call( {String? locale, + @JsonKey(fromJson: dashboardWidgetsRealFormJson) + List dashboardWidgets, bool onlyProxy, bool autoLaunch, bool silentLaunch, @@ -84,6 +89,7 @@ class _$AppSettingCopyWithImpl<$Res, $Val extends AppSetting> @override $Res call({ Object? locale = freezed, + Object? dashboardWidgets = null, Object? onlyProxy = null, Object? autoLaunch = null, Object? silentLaunch = null, @@ -103,6 +109,10 @@ class _$AppSettingCopyWithImpl<$Res, $Val extends AppSetting> ? _value.locale : locale // ignore: cast_nullable_to_non_nullable as String?, + dashboardWidgets: null == dashboardWidgets + ? _value.dashboardWidgets + : dashboardWidgets // ignore: cast_nullable_to_non_nullable + as List, onlyProxy: null == onlyProxy ? _value.onlyProxy : onlyProxy // ignore: cast_nullable_to_non_nullable @@ -169,6 +179,8 @@ abstract class _$$AppSettingImplCopyWith<$Res> @useResult $Res call( {String? locale, + @JsonKey(fromJson: dashboardWidgetsRealFormJson) + List dashboardWidgets, bool onlyProxy, bool autoLaunch, bool silentLaunch, @@ -198,6 +210,7 @@ class __$$AppSettingImplCopyWithImpl<$Res> @override $Res call({ Object? locale = freezed, + Object? dashboardWidgets = null, Object? onlyProxy = null, Object? autoLaunch = null, Object? silentLaunch = null, @@ -217,6 +230,10 @@ class __$$AppSettingImplCopyWithImpl<$Res> ? _value.locale : locale // ignore: cast_nullable_to_non_nullable as String?, + dashboardWidgets: null == dashboardWidgets + ? _value._dashboardWidgets + : dashboardWidgets // ignore: cast_nullable_to_non_nullable + as List, onlyProxy: null == onlyProxy ? _value.onlyProxy : onlyProxy // ignore: cast_nullable_to_non_nullable @@ -278,6 +295,8 @@ class __$$AppSettingImplCopyWithImpl<$Res> class _$AppSettingImpl implements _AppSetting { const _$AppSettingImpl( {this.locale, + @JsonKey(fromJson: dashboardWidgetsRealFormJson) + final List dashboardWidgets = defaultDashboardWidgets, this.onlyProxy = false, this.autoLaunch = false, this.silentLaunch = false, @@ -290,13 +309,24 @@ class _$AppSettingImpl implements _AppSetting { this.showLabel = false, this.disclaimerAccepted = false, this.minimizeOnExit = true, - this.hidden = false}); + this.hidden = false}) + : _dashboardWidgets = dashboardWidgets; factory _$AppSettingImpl.fromJson(Map json) => _$$AppSettingImplFromJson(json); @override final String? locale; + final List _dashboardWidgets; + @override + @JsonKey(fromJson: dashboardWidgetsRealFormJson) + List get dashboardWidgets { + if (_dashboardWidgets is EqualUnmodifiableListView) + return _dashboardWidgets; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_dashboardWidgets); + } + @override @JsonKey() final bool onlyProxy; @@ -339,7 +369,7 @@ class _$AppSettingImpl implements _AppSetting { @override String toString() { - return 'AppSetting(locale: $locale, onlyProxy: $onlyProxy, autoLaunch: $autoLaunch, silentLaunch: $silentLaunch, autoRun: $autoRun, openLogs: $openLogs, closeConnections: $closeConnections, testUrl: $testUrl, isAnimateToPage: $isAnimateToPage, autoCheckUpdate: $autoCheckUpdate, showLabel: $showLabel, disclaimerAccepted: $disclaimerAccepted, minimizeOnExit: $minimizeOnExit, hidden: $hidden)'; + return 'AppSetting(locale: $locale, dashboardWidgets: $dashboardWidgets, onlyProxy: $onlyProxy, autoLaunch: $autoLaunch, silentLaunch: $silentLaunch, autoRun: $autoRun, openLogs: $openLogs, closeConnections: $closeConnections, testUrl: $testUrl, isAnimateToPage: $isAnimateToPage, autoCheckUpdate: $autoCheckUpdate, showLabel: $showLabel, disclaimerAccepted: $disclaimerAccepted, minimizeOnExit: $minimizeOnExit, hidden: $hidden)'; } @override @@ -348,6 +378,8 @@ class _$AppSettingImpl implements _AppSetting { (other.runtimeType == runtimeType && other is _$AppSettingImpl && (identical(other.locale, locale) || other.locale == locale) && + const DeepCollectionEquality() + .equals(other._dashboardWidgets, _dashboardWidgets) && (identical(other.onlyProxy, onlyProxy) || other.onlyProxy == onlyProxy) && (identical(other.autoLaunch, autoLaunch) || @@ -378,6 +410,7 @@ class _$AppSettingImpl implements _AppSetting { int get hashCode => Object.hash( runtimeType, locale, + const DeepCollectionEquality().hash(_dashboardWidgets), onlyProxy, autoLaunch, silentLaunch, @@ -411,6 +444,8 @@ class _$AppSettingImpl implements _AppSetting { abstract class _AppSetting implements AppSetting { const factory _AppSetting( {final String? locale, + @JsonKey(fromJson: dashboardWidgetsRealFormJson) + final List dashboardWidgets, final bool onlyProxy, final bool autoLaunch, final bool silentLaunch, @@ -431,6 +466,9 @@ abstract class _AppSetting implements AppSetting { @override String? get locale; @override + @JsonKey(fromJson: dashboardWidgetsRealFormJson) + List get dashboardWidgets; + @override bool get onlyProxy; @override bool get autoLaunch; diff --git a/lib/models/generated/config.g.dart b/lib/models/generated/config.g.dart index e45c516..1ccbfe1 100644 --- a/lib/models/generated/config.g.dart +++ b/lib/models/generated/config.g.dart @@ -54,6 +54,9 @@ Map _$ConfigToJson(Config instance) => { _$AppSettingImpl _$$AppSettingImplFromJson(Map json) => _$AppSettingImpl( locale: json['locale'] as String?, + dashboardWidgets: json['dashboardWidgets'] == null + ? defaultDashboardWidgets + : dashboardWidgetsRealFormJson(json['dashboardWidgets'] as List?), onlyProxy: json['onlyProxy'] as bool? ?? false, autoLaunch: json['autoLaunch'] as bool? ?? false, silentLaunch: json['silentLaunch'] as bool? ?? false, @@ -72,6 +75,9 @@ _$AppSettingImpl _$$AppSettingImplFromJson(Map json) => Map _$$AppSettingImplToJson(_$AppSettingImpl instance) => { 'locale': instance.locale, + 'dashboardWidgets': instance.dashboardWidgets + .map((e) => _$DashboardWidgetEnumMap[e]!) + .toList(), 'onlyProxy': instance.onlyProxy, 'autoLaunch': instance.autoLaunch, 'silentLaunch': instance.silentLaunch, @@ -87,6 +93,17 @@ Map _$$AppSettingImplToJson(_$AppSettingImpl instance) => 'hidden': instance.hidden, }; +const _$DashboardWidgetEnumMap = { + DashboardWidget.networkSpeed: 'networkSpeed', + DashboardWidget.outboundMode: 'outboundMode', + DashboardWidget.trafficUsage: 'trafficUsage', + DashboardWidget.networkDetection: 'networkDetection', + DashboardWidget.tunButton: 'tunButton', + DashboardWidget.systemProxyButton: 'systemProxyButton', + DashboardWidget.intranetIp: 'intranetIp', + DashboardWidget.memoryInfo: 'memoryInfo', +}; + _$AccessControlImpl _$$AccessControlImplFromJson(Map json) => _$AccessControlImpl( mode: $enumDecodeNullable(_$AccessControlModeEnumMap, json['mode']) ?? diff --git a/lib/models/generated/core.g.dart b/lib/models/generated/core.g.dart index 0ba39ae..d296742 100644 --- a/lib/models/generated/core.g.dart +++ b/lib/models/generated/core.g.dart @@ -329,4 +329,6 @@ const _$ActionMethodEnumMap = { ActionMethod.stopLog: 'stopLog', ActionMethod.startListener: 'startListener', ActionMethod.stopListener: 'stopListener', + ActionMethod.getCountryCode: 'getCountryCode', + ActionMethod.getMemory: 'getMemory', }; diff --git a/lib/models/generated/selector.freezed.dart b/lib/models/generated/selector.freezed.dart index 8f63339..a683036 100644 --- a/lib/models/generated/selector.freezed.dart +++ b/lib/models/generated/selector.freezed.dart @@ -3209,137 +3209,6 @@ abstract class _ProxiesActionsState implements ProxiesActionsState { throw _privateConstructorUsedError; } -/// @nodoc -mixin _$AutoLaunchState { - bool get isAutoLaunch => throw _privateConstructorUsedError; - - /// Create a copy of AutoLaunchState - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - $AutoLaunchStateCopyWith get copyWith => - throw _privateConstructorUsedError; -} - -/// @nodoc -abstract class $AutoLaunchStateCopyWith<$Res> { - factory $AutoLaunchStateCopyWith( - AutoLaunchState value, $Res Function(AutoLaunchState) then) = - _$AutoLaunchStateCopyWithImpl<$Res, AutoLaunchState>; - @useResult - $Res call({bool isAutoLaunch}); -} - -/// @nodoc -class _$AutoLaunchStateCopyWithImpl<$Res, $Val extends AutoLaunchState> - implements $AutoLaunchStateCopyWith<$Res> { - _$AutoLaunchStateCopyWithImpl(this._value, this._then); - - // ignore: unused_field - final $Val _value; - // ignore: unused_field - final $Res Function($Val) _then; - - /// Create a copy of AutoLaunchState - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? isAutoLaunch = null, - }) { - return _then(_value.copyWith( - isAutoLaunch: null == isAutoLaunch - ? _value.isAutoLaunch - : isAutoLaunch // ignore: cast_nullable_to_non_nullable - as bool, - ) as $Val); - } -} - -/// @nodoc -abstract class _$$AutoLaunchStateImplCopyWith<$Res> - implements $AutoLaunchStateCopyWith<$Res> { - factory _$$AutoLaunchStateImplCopyWith(_$AutoLaunchStateImpl value, - $Res Function(_$AutoLaunchStateImpl) then) = - __$$AutoLaunchStateImplCopyWithImpl<$Res>; - @override - @useResult - $Res call({bool isAutoLaunch}); -} - -/// @nodoc -class __$$AutoLaunchStateImplCopyWithImpl<$Res> - extends _$AutoLaunchStateCopyWithImpl<$Res, _$AutoLaunchStateImpl> - implements _$$AutoLaunchStateImplCopyWith<$Res> { - __$$AutoLaunchStateImplCopyWithImpl( - _$AutoLaunchStateImpl _value, $Res Function(_$AutoLaunchStateImpl) _then) - : super(_value, _then); - - /// Create a copy of AutoLaunchState - /// with the given fields replaced by the non-null parameter values. - @pragma('vm:prefer-inline') - @override - $Res call({ - Object? isAutoLaunch = null, - }) { - return _then(_$AutoLaunchStateImpl( - isAutoLaunch: null == isAutoLaunch - ? _value.isAutoLaunch - : isAutoLaunch // ignore: cast_nullable_to_non_nullable - as bool, - )); - } -} - -/// @nodoc - -class _$AutoLaunchStateImpl implements _AutoLaunchState { - const _$AutoLaunchStateImpl({required this.isAutoLaunch}); - - @override - final bool isAutoLaunch; - - @override - String toString() { - return 'AutoLaunchState(isAutoLaunch: $isAutoLaunch)'; - } - - @override - bool operator ==(Object other) { - return identical(this, other) || - (other.runtimeType == runtimeType && - other is _$AutoLaunchStateImpl && - (identical(other.isAutoLaunch, isAutoLaunch) || - other.isAutoLaunch == isAutoLaunch)); - } - - @override - int get hashCode => Object.hash(runtimeType, isAutoLaunch); - - /// Create a copy of AutoLaunchState - /// with the given fields replaced by the non-null parameter values. - @JsonKey(includeFromJson: false, includeToJson: false) - @override - @pragma('vm:prefer-inline') - _$$AutoLaunchStateImplCopyWith<_$AutoLaunchStateImpl> get copyWith => - __$$AutoLaunchStateImplCopyWithImpl<_$AutoLaunchStateImpl>( - this, _$identity); -} - -abstract class _AutoLaunchState implements AutoLaunchState { - const factory _AutoLaunchState({required final bool isAutoLaunch}) = - _$AutoLaunchStateImpl; - - @override - bool get isAutoLaunch; - - /// Create a copy of AutoLaunchState - /// with the given fields replaced by the non-null parameter values. - @override - @JsonKey(includeFromJson: false, includeToJson: false) - _$$AutoLaunchStateImplCopyWith<_$AutoLaunchStateImpl> get copyWith => - throw _privateConstructorUsedError; -} - /// @nodoc mixin _$ProxyState { bool get isStart => throw _privateConstructorUsedError; @@ -4235,6 +4104,167 @@ abstract class _ClashConfigState implements ClashConfigState { throw _privateConstructorUsedError; } +/// @nodoc +mixin _$DashboardState { + List get dashboardWidgets => + throw _privateConstructorUsedError; + double get viewWidth => throw _privateConstructorUsedError; + + /// Create a copy of DashboardState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $DashboardStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $DashboardStateCopyWith<$Res> { + factory $DashboardStateCopyWith( + DashboardState value, $Res Function(DashboardState) then) = + _$DashboardStateCopyWithImpl<$Res, DashboardState>; + @useResult + $Res call({List dashboardWidgets, double viewWidth}); +} + +/// @nodoc +class _$DashboardStateCopyWithImpl<$Res, $Val extends DashboardState> + implements $DashboardStateCopyWith<$Res> { + _$DashboardStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of DashboardState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? dashboardWidgets = null, + Object? viewWidth = null, + }) { + return _then(_value.copyWith( + dashboardWidgets: null == dashboardWidgets + ? _value.dashboardWidgets + : dashboardWidgets // ignore: cast_nullable_to_non_nullable + as List, + viewWidth: null == viewWidth + ? _value.viewWidth + : viewWidth // ignore: cast_nullable_to_non_nullable + as double, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$DashboardStateImplCopyWith<$Res> + implements $DashboardStateCopyWith<$Res> { + factory _$$DashboardStateImplCopyWith(_$DashboardStateImpl value, + $Res Function(_$DashboardStateImpl) then) = + __$$DashboardStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({List dashboardWidgets, double viewWidth}); +} + +/// @nodoc +class __$$DashboardStateImplCopyWithImpl<$Res> + extends _$DashboardStateCopyWithImpl<$Res, _$DashboardStateImpl> + implements _$$DashboardStateImplCopyWith<$Res> { + __$$DashboardStateImplCopyWithImpl( + _$DashboardStateImpl _value, $Res Function(_$DashboardStateImpl) _then) + : super(_value, _then); + + /// Create a copy of DashboardState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? dashboardWidgets = null, + Object? viewWidth = null, + }) { + return _then(_$DashboardStateImpl( + dashboardWidgets: null == dashboardWidgets + ? _value._dashboardWidgets + : dashboardWidgets // ignore: cast_nullable_to_non_nullable + as List, + viewWidth: null == viewWidth + ? _value.viewWidth + : viewWidth // ignore: cast_nullable_to_non_nullable + as double, + )); + } +} + +/// @nodoc + +class _$DashboardStateImpl implements _DashboardState { + const _$DashboardStateImpl( + {required final List dashboardWidgets, + required this.viewWidth}) + : _dashboardWidgets = dashboardWidgets; + + final List _dashboardWidgets; + @override + List get dashboardWidgets { + if (_dashboardWidgets is EqualUnmodifiableListView) + return _dashboardWidgets; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_dashboardWidgets); + } + + @override + final double viewWidth; + + @override + String toString() { + return 'DashboardState(dashboardWidgets: $dashboardWidgets, viewWidth: $viewWidth)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$DashboardStateImpl && + const DeepCollectionEquality() + .equals(other._dashboardWidgets, _dashboardWidgets) && + (identical(other.viewWidth, viewWidth) || + other.viewWidth == viewWidth)); + } + + @override + int get hashCode => Object.hash(runtimeType, + const DeepCollectionEquality().hash(_dashboardWidgets), viewWidth); + + /// Create a copy of DashboardState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$DashboardStateImplCopyWith<_$DashboardStateImpl> get copyWith => + __$$DashboardStateImplCopyWithImpl<_$DashboardStateImpl>( + this, _$identity); +} + +abstract class _DashboardState implements DashboardState { + const factory _DashboardState( + {required final List dashboardWidgets, + required final double viewWidth}) = _$DashboardStateImpl; + + @override + List get dashboardWidgets; + @override + double get viewWidth; + + /// Create a copy of DashboardState + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$DashboardStateImplCopyWith<_$DashboardStateImpl> get copyWith => + throw _privateConstructorUsedError; +} + /// @nodoc mixin _$VPNState { AccessControl? get accessControl => throw _privateConstructorUsedError; diff --git a/lib/models/generated/widget.freezed.dart b/lib/models/generated/widget.freezed.dart new file mode 100644 index 0000000..c6a930f --- /dev/null +++ b/lib/models/generated/widget.freezed.dart @@ -0,0 +1,312 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of '../widget.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$ActivateState { + bool get active => throw _privateConstructorUsedError; + + /// Create a copy of ActivateState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $ActivateStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ActivateStateCopyWith<$Res> { + factory $ActivateStateCopyWith( + ActivateState value, $Res Function(ActivateState) then) = + _$ActivateStateCopyWithImpl<$Res, ActivateState>; + @useResult + $Res call({bool active}); +} + +/// @nodoc +class _$ActivateStateCopyWithImpl<$Res, $Val extends ActivateState> + implements $ActivateStateCopyWith<$Res> { + _$ActivateStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of ActivateState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? active = null, + }) { + return _then(_value.copyWith( + active: null == active + ? _value.active + : active // ignore: cast_nullable_to_non_nullable + as bool, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$ActivateStateImplCopyWith<$Res> + implements $ActivateStateCopyWith<$Res> { + factory _$$ActivateStateImplCopyWith( + _$ActivateStateImpl value, $Res Function(_$ActivateStateImpl) then) = + __$$ActivateStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({bool active}); +} + +/// @nodoc +class __$$ActivateStateImplCopyWithImpl<$Res> + extends _$ActivateStateCopyWithImpl<$Res, _$ActivateStateImpl> + implements _$$ActivateStateImplCopyWith<$Res> { + __$$ActivateStateImplCopyWithImpl( + _$ActivateStateImpl _value, $Res Function(_$ActivateStateImpl) _then) + : super(_value, _then); + + /// Create a copy of ActivateState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? active = null, + }) { + return _then(_$ActivateStateImpl( + active: null == active + ? _value.active + : active // ignore: cast_nullable_to_non_nullable + as bool, + )); + } +} + +/// @nodoc + +class _$ActivateStateImpl implements _ActivateState { + const _$ActivateStateImpl({required this.active}); + + @override + final bool active; + + @override + String toString() { + return 'ActivateState(active: $active)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ActivateStateImpl && + (identical(other.active, active) || other.active == active)); + } + + @override + int get hashCode => Object.hash(runtimeType, active); + + /// Create a copy of ActivateState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$ActivateStateImplCopyWith<_$ActivateStateImpl> get copyWith => + __$$ActivateStateImplCopyWithImpl<_$ActivateStateImpl>(this, _$identity); +} + +abstract class _ActivateState implements ActivateState { + const factory _ActivateState({required final bool active}) = + _$ActivateStateImpl; + + @override + bool get active; + + /// Create a copy of ActivateState + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$ActivateStateImplCopyWith<_$ActivateStateImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +mixin _$CommonMessage { + String get id => throw _privateConstructorUsedError; + String get text => throw _privateConstructorUsedError; + Duration get duration => throw _privateConstructorUsedError; + + /// Create a copy of CommonMessage + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $CommonMessageCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $CommonMessageCopyWith<$Res> { + factory $CommonMessageCopyWith( + CommonMessage value, $Res Function(CommonMessage) then) = + _$CommonMessageCopyWithImpl<$Res, CommonMessage>; + @useResult + $Res call({String id, String text, Duration duration}); +} + +/// @nodoc +class _$CommonMessageCopyWithImpl<$Res, $Val extends CommonMessage> + implements $CommonMessageCopyWith<$Res> { + _$CommonMessageCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of CommonMessage + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? text = null, + Object? duration = null, + }) { + return _then(_value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + text: null == text + ? _value.text + : text // ignore: cast_nullable_to_non_nullable + as String, + duration: null == duration + ? _value.duration + : duration // ignore: cast_nullable_to_non_nullable + as Duration, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$CommonMessageImplCopyWith<$Res> + implements $CommonMessageCopyWith<$Res> { + factory _$$CommonMessageImplCopyWith( + _$CommonMessageImpl value, $Res Function(_$CommonMessageImpl) then) = + __$$CommonMessageImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({String id, String text, Duration duration}); +} + +/// @nodoc +class __$$CommonMessageImplCopyWithImpl<$Res> + extends _$CommonMessageCopyWithImpl<$Res, _$CommonMessageImpl> + implements _$$CommonMessageImplCopyWith<$Res> { + __$$CommonMessageImplCopyWithImpl( + _$CommonMessageImpl _value, $Res Function(_$CommonMessageImpl) _then) + : super(_value, _then); + + /// Create a copy of CommonMessage + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? text = null, + Object? duration = null, + }) { + return _then(_$CommonMessageImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + text: null == text + ? _value.text + : text // ignore: cast_nullable_to_non_nullable + as String, + duration: null == duration + ? _value.duration + : duration // ignore: cast_nullable_to_non_nullable + as Duration, + )); + } +} + +/// @nodoc + +class _$CommonMessageImpl implements _CommonMessage { + const _$CommonMessageImpl( + {required this.id, + required this.text, + this.duration = const Duration(seconds: 3)}); + + @override + final String id; + @override + final String text; + @override + @JsonKey() + final Duration duration; + + @override + String toString() { + return 'CommonMessage(id: $id, text: $text, duration: $duration)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$CommonMessageImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.text, text) || other.text == text) && + (identical(other.duration, duration) || + other.duration == duration)); + } + + @override + int get hashCode => Object.hash(runtimeType, id, text, duration); + + /// Create a copy of CommonMessage + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$CommonMessageImplCopyWith<_$CommonMessageImpl> get copyWith => + __$$CommonMessageImplCopyWithImpl<_$CommonMessageImpl>(this, _$identity); +} + +abstract class _CommonMessage implements CommonMessage { + const factory _CommonMessage( + {required final String id, + required final String text, + final Duration duration}) = _$CommonMessageImpl; + + @override + String get id; + @override + String get text; + @override + Duration get duration; + + /// Create a copy of CommonMessage + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$CommonMessageImplCopyWith<_$CommonMessageImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/models/models.dart b/lib/models/models.dart index b55c23e..06c8e3c 100644 --- a/lib/models/models.dart +++ b/lib/models/models.dart @@ -5,3 +5,4 @@ export 'config.dart'; export 'core.dart'; export 'profile.dart'; export 'selector.dart'; +export 'widget.dart'; diff --git a/lib/models/selector.dart b/lib/models/selector.dart index 98004b4..3ee6736 100644 --- a/lib/models/selector.dart +++ b/lib/models/selector.dart @@ -195,13 +195,6 @@ class ProxiesActionsState with _$ProxiesActionsState { }) = _ProxiesActionsState; } -@freezed -class AutoLaunchState with _$AutoLaunchState { - const factory AutoLaunchState({ - required bool isAutoLaunch, - }) = _AutoLaunchState; -} - @freezed class ProxyState with _$ProxyState { const factory ProxyState({ @@ -244,6 +237,14 @@ class ClashConfigState with _$ClashConfigState { }) = _ClashConfigState; } +@freezed +class DashboardState with _$DashboardState { + const factory DashboardState({ + required List dashboardWidgets, + required double viewWidth, + }) = _DashboardState; +} + @freezed class VPNState with _$VPNState { const factory VPNState({ diff --git a/lib/models/widget.dart b/lib/models/widget.dart new file mode 100644 index 0000000..49b47a5 --- /dev/null +++ b/lib/models/widget.dart @@ -0,0 +1,19 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'generated/widget.freezed.dart'; + +@freezed +class ActivateState with _$ActivateState { + const factory ActivateState({ + required bool active, + }) = _ActivateState; +} + +@freezed +class CommonMessage with _$CommonMessage { + const factory CommonMessage({ + required String id, + required String text, + @Default(Duration(seconds: 3)) Duration duration, + }) = _CommonMessage; +} diff --git a/lib/pages/home.dart b/lib/pages/home.dart index 07858d0..52032f7 100644 --- a/lib/pages/home.dart +++ b/lib/pages/home.dart @@ -13,104 +13,6 @@ typedef OnSelected = void Function(int index); class HomePage extends StatelessWidget { const HomePage({super.key}); - _getNavigationBar({ - required BuildContext context, - required ViewMode viewMode, - required List navigationItems, - required int currentIndex, - }) { - if (viewMode == ViewMode.mobile) { - return NavigationBar( - destinations: navigationItems - .map( - (e) => NavigationDestination( - icon: e.icon, - label: Intl.message(e.label), - ), - ) - .toList(), - onDestinationSelected: globalState.appController.toPage, - selectedIndex: currentIndex, - ); - } - return LayoutBuilder( - builder: (_, container) { - return Material( - color: context.colorScheme.surfaceContainer, - child: Container( - padding: const EdgeInsets.symmetric( - vertical: 16, - ), - height: container.maxHeight, - child: Column( - children: [ - Expanded( - child: SingleChildScrollView( - child: IntrinsicHeight( - child: Selector( - selector: (_, config) => config.appSetting.showLabel, - builder: (_, showLabel, __) { - return NavigationRail( - backgroundColor: - context.colorScheme.surfaceContainer, - selectedIconTheme: IconThemeData( - color: context.colorScheme.onSurfaceVariant, - ), - unselectedIconTheme: IconThemeData( - color: context.colorScheme.onSurfaceVariant, - ), - selectedLabelTextStyle: - context.textTheme.labelLarge!.copyWith( - color: context.colorScheme.onSurface, - ), - unselectedLabelTextStyle: - context.textTheme.labelLarge!.copyWith( - color: context.colorScheme.onSurface, - ), - destinations: navigationItems - .map( - (e) => NavigationRailDestination( - icon: e.icon, - label: Text( - Intl.message(e.label), - ), - ), - ) - .toList(), - onDestinationSelected: - globalState.appController.toPage, - extended: false, - selectedIndex: currentIndex, - labelType: showLabel - ? NavigationRailLabelType.all - : NavigationRailLabelType.none, - ); - }, - ), - ), - ), - ), - const SizedBox( - height: 16, - ), - IconButton( - onPressed: () { - final config = globalState.appController.config; - final appSetting = config.appSetting; - config.appSetting = appSetting.copyWith( - showLabel: !appSetting.showLabel, - ); - }, - icon: const Icon(Icons.menu), - ) - ], - ), - ), - ); - }, - ); - } - _updatePageController(List navigationItems) { final currentLabel = globalState.appController.appState.currentLabel; final index = navigationItems.lastIndexWhere( @@ -177,8 +79,7 @@ class HomePage extends StatelessWidget { (element) => element.label == currentLabel, ); final currentIndex = index == -1 ? 0 : index; - final navigationBar = _getNavigationBar( - context: context, + final navigationBar = CommonNavigationBar( viewMode: viewMode, navigationItems: navigationItems, currentIndex: currentIndex, @@ -202,3 +103,121 @@ class HomePage extends StatelessWidget { ); } } + +class CommonNavigationBar extends StatelessWidget { + final ViewMode viewMode; + final List navigationItems; + final int currentIndex; + + const CommonNavigationBar({ + super.key, + required this.viewMode, + required this.navigationItems, + required this.currentIndex, + }); + + _updateSafeMessageOffset(BuildContext context) { + WidgetsBinding.instance.addPostFrameCallback((_) { + final size = context.size; + if (viewMode == ViewMode.mobile) { + globalState.safeMessageOffsetNotifier.value = Offset( + 0, + -(size?.height ?? 0), + ); + } else { + globalState.safeMessageOffsetNotifier.value = Offset( + size?.width ?? 0, + 0, + ); + } + }); + } + + @override + Widget build(BuildContext context) { + _updateSafeMessageOffset(context); + if (viewMode == ViewMode.mobile) { + return NavigationBar( + destinations: navigationItems + .map( + (e) => NavigationDestination( + icon: e.icon, + label: Intl.message(e.label), + ), + ) + .toList(), + onDestinationSelected: globalState.appController.toPage, + selectedIndex: currentIndex, + ); + } + return Material( + color: context.colorScheme.surfaceContainer, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 16, + ), + child: Column( + children: [ + Expanded( + child: SingleChildScrollView( + child: IntrinsicHeight( + child: Selector( + selector: (_, config) => config.appSetting.showLabel, + builder: (_, showLabel, __) { + return NavigationRail( + backgroundColor: context.colorScheme.surfaceContainer, + selectedIconTheme: IconThemeData( + color: context.colorScheme.onSurfaceVariant, + ), + unselectedIconTheme: IconThemeData( + color: context.colorScheme.onSurfaceVariant, + ), + selectedLabelTextStyle: + context.textTheme.labelLarge!.copyWith( + color: context.colorScheme.onSurface, + ), + unselectedLabelTextStyle: + context.textTheme.labelLarge!.copyWith( + color: context.colorScheme.onSurface, + ), + destinations: navigationItems + .map( + (e) => NavigationRailDestination( + icon: e.icon, + label: Text( + Intl.message(e.label), + ), + ), + ) + .toList(), + onDestinationSelected: globalState.appController.toPage, + extended: false, + selectedIndex: currentIndex, + labelType: showLabel + ? NavigationRailLabelType.all + : NavigationRailLabelType.none, + ); + }, + ), + ), + ), + ), + const SizedBox( + height: 16, + ), + IconButton( + onPressed: () { + final config = globalState.appController.config; + final appSetting = config.appSetting; + config.appSetting = appSetting.copyWith( + showLabel: !appSetting.showLabel, + ); + }, + icon: const Icon(Icons.menu), + ) + ], + ), + ), + ); + } +} diff --git a/lib/pages/scan.dart b/lib/pages/scan.dart index 79e645a..114a76a 100644 --- a/lib/pages/scan.dart +++ b/lib/pages/scan.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:math'; import 'package:fl_clash/state.dart'; +import 'package:fl_clash/widgets/activate_box.dart'; import 'package:flutter/material.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; @@ -113,14 +114,16 @@ class _ScanPageState extends State with WidgetsBindingObserver { } return Container( margin: const EdgeInsets.symmetric(horizontal: 8), - child: AbsorbPointer( - absorbing: state.torchState == TorchState.unavailable, + child: ActivateBox( + active: state.torchState != TorchState.unavailable, child: IconButton( color: Colors.white, icon: icon, style: ButtonStyle( - foregroundColor: const WidgetStatePropertyAll(Colors.white), - backgroundColor: WidgetStatePropertyAll(backgroundColor), + foregroundColor: + const WidgetStatePropertyAll(Colors.white), + backgroundColor: + WidgetStatePropertyAll(backgroundColor), ), onPressed: () => controller.toggleTorch(), ), @@ -155,8 +158,8 @@ class _ScanPageState extends State with WidgetsBindingObserver { WidgetsBinding.instance.removeObserver(this); unawaited(_subscription?.cancel()); _subscription = null; - super.dispose(); await controller.dispose(); + super.dispose(); } } diff --git a/lib/plugins/app.dart b/lib/plugins/app.dart index f92fa5c..19d0edb 100644 --- a/lib/plugins/app.dart +++ b/lib/plugins/app.dart @@ -49,7 +49,7 @@ class App { return Isolate.run>(() { final List packagesRaw = packagesString != null ? json.decode(packagesString) : []; - return packagesRaw.map((e) => Package.fromJson(e)).toList(); + return packagesRaw.map((e) => Package.fromJson(e)).toSet().toList(); }); } diff --git a/lib/state.dart b/lib/state.dart index 69eed36..e9fd397 100644 --- a/lib/state.dart +++ b/lib/state.dart @@ -24,6 +24,7 @@ class GlobalState { PageController? pageController; late Measure measure; DateTime? startTime; + final safeMessageOffsetNotifier = ValueNotifier(Offset.zero); final navigatorKey = GlobalKey(); late AppController appController; GlobalKey homeScaffoldKey = GlobalKey(); @@ -301,37 +302,6 @@ class GlobalState { } } - showSnackBar( - BuildContext context, { - required String message, - SnackBarAction? action, - }) { - final width = context.viewWidth; - EdgeInsets margin; - if (width < 600) { - margin = const EdgeInsets.only( - bottom: 16, - right: 16, - left: 16, - ); - } else { - margin = EdgeInsets.only( - bottom: 16, - left: 16, - right: width - 316, - ); - } - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - action: action, - content: Text(message), - behavior: SnackBarBehavior.floating, - duration: const Duration(milliseconds: 1500), - margin: margin, - ), - ); - } - Future safeRun( FutureOr Function() futureFunction, { String? title, @@ -340,16 +310,15 @@ class GlobalState { final res = await futureFunction(); return res; } catch (e) { - showMessage( - title: title ?? appLocalizations.tip, - message: TextSpan( - text: e.toString(), - ), - ); + showNotifier(e.toString()); return null; } } + showNotifier(String text) { + navigatorKey.currentContext?.showNotifier(text); + } + openUrl(String url) { showMessage( message: TextSpan(text: url), diff --git a/lib/widgets/activate_box.dart b/lib/widgets/activate_box.dart new file mode 100644 index 0000000..bcb335c --- /dev/null +++ b/lib/widgets/activate_box.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; + +class ActivateBox extends StatelessWidget { + final Widget child; + final bool active; + + const ActivateBox({ + super.key, + required this.child, + this.active = false, + }); + + @override + Widget build(BuildContext context) { + return IgnorePointer( + ignoring: !active, + child: child, + ); + } +} diff --git a/lib/widgets/bar_chart.dart b/lib/widgets/bar_chart.dart new file mode 100644 index 0000000..ea811fc --- /dev/null +++ b/lib/widgets/bar_chart.dart @@ -0,0 +1,148 @@ +import 'dart:math'; +import 'dart:ui'; + +import 'package:fl_clash/common/constant.dart'; +import 'package:flutter/material.dart'; + +@immutable +class BarChartData { + final double value; + final String label; + + const BarChartData({ + required this.value, + required this.label, + }); +} + +class BarChart extends StatefulWidget { + final List data; + final Duration duration; + + const BarChart({ + super.key, + required this.data, + this.duration = commonDuration, + }); + + @override + State createState() => _BarChartState(); +} + +class _BarChartState extends State + with SingleTickerProviderStateMixin { + late AnimationController _animationController; + + late List _oldData; + + @override + void initState() { + super.initState(); + _oldData = widget.data; + _animationController = AnimationController( + vsync: this, + duration: widget.duration, + )..forward(from: 0); + } + + @override + void didUpdateWidget(BarChart oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.data != widget.data) { + _oldData = oldWidget.data; + _animationController.forward(from: 0); + } + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder(builder: (_, container) { + return AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + return CustomPaint( + painter: BarChartPainter( + _oldData, + widget.data, + _animationController.value, + ), + size: Size(container.maxWidth, container.maxHeight), + ); + }, + ); + }); + } +} + +class BarChartPainter extends CustomPainter { + final List oldData; + final List newData; + final double progress; + + BarChartPainter(this.oldData, this.newData, this.progress); + + Map getRectMap(List dataList, Size size) { + final spacing = size.width * 0.05; + final maxBarWidth = 30; + final barWidth = + (size.width - spacing * (dataList.length - 1)) / dataList.length; + final maxValue = + dataList.fold(0.0, (max, item) => max > item.value ? max : item.value); + final rects = {}; + for (int i = 0; i < dataList.length; i++) { + final data = dataList[i]; + double barHeight = (data.value / maxValue) * size.height; + + final adjustLeft = + barWidth > maxBarWidth ? (barWidth - maxBarWidth) / 2 : 0; + double left = i * (barWidth + spacing) + adjustLeft; + double top = size.height - barHeight; + rects[data.label] = Rect.fromLTWH( + left, + top, + min(barWidth, 30), + barHeight, + ); + } + return rects; + } + + @override + void paint(Canvas canvas, Size size) { + final oldRectMap = getRectMap(oldData, size); + final newRectMap = getRectMap(newData, size); + + final paint = Paint() + ..color = Colors.blue + ..style = PaintingStyle.fill; + final newRectEntries = newRectMap.entries.toList(); + for (int i = 0; i < newRectEntries.length; i++) { + final newRectEntry = newRectEntries[i]; + final newRect = newRectEntry.value; + final oldRect = oldRectMap[newRectEntry.key] ?? + newRect.translate(newRect.left * (progress - 1), 0); + + final interpolatedRect = Rect.fromLTRB( + lerpDouble(oldRect.left, newRect.left, progress)!, + lerpDouble(oldRect.top, newRect.top, progress)!, + lerpDouble(oldRect.right, newRect.right, progress)!, + lerpDouble(oldRect.bottom, newRect.bottom, progress)!, + ); + + canvas.drawRect(interpolatedRect, paint); + } + } + + @override + bool shouldRepaint(BarChartPainter oldDelegate) { + return oldDelegate.progress != progress || + oldDelegate.oldData != oldData || + oldDelegate.newData != newData; + } +} diff --git a/lib/widgets/builder.dart b/lib/widgets/builder.dart index b4bd9c4..5294f84 100644 --- a/lib/widgets/builder.dart +++ b/lib/widgets/builder.dart @@ -19,8 +19,8 @@ class _ScrollOverBuilderState extends State { @override void dispose() { - super.dispose(); isOverNotifier.dispose(); + super.dispose(); } @override @@ -115,3 +115,22 @@ class ActiveBuilder extends StatelessWidget { ); } } + +class ThemeModeBuilder extends StatelessWidget { + final StateWidgetBuilder builder; + + const ThemeModeBuilder({ + super.key, + required this.builder, + }); + + @override + Widget build(BuildContext context) { + return Selector( + selector: (_, config) => config.themeProps.themeMode, + builder: (_, state, __) { + return builder(state); + }, + ); + } +} diff --git a/lib/widgets/card.dart b/lib/widgets/card.dart index 0110bd0..81bbe0b 100644 --- a/lib/widgets/card.dart +++ b/lib/widgets/card.dart @@ -17,17 +17,19 @@ class Info { class InfoHeader extends StatelessWidget { final Info info; final List actions; + final EdgeInsetsGeometry? padding; const InfoHeader({ super.key, required this.info, + this.padding, List? actions, }) : actions = actions ?? const []; @override Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.all(16), + return Padding( + padding: padding ?? baseInfoEdgeInsets, child: Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -40,7 +42,7 @@ class InfoHeader extends StatelessWidget { if (info.iconData != null) ...[ Icon( info.iconData, - color: Theme.of(context).colorScheme.primary, + color: Theme.of(context).colorScheme.onSurfaceVariant, ), const SizedBox( width: 8, @@ -53,7 +55,9 @@ class InfoHeader extends StatelessWidget { info.label, maxLines: 1, overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.titleMedium, + style: Theme.of(context).textTheme.titleSmall?.copyWith( + color: context.colorScheme.onSurfaceVariant, + ), ), ), ), @@ -80,12 +84,13 @@ class CommonCard extends StatelessWidget { const CommonCard({ super.key, bool? isSelected, - this.type = CommonCardType.plain, + this.type = CommonCardType.filled, this.onPressed, - this.info, this.selectWidget, + this.backgroundColor, this.radius = 12, required this.child, + this.info, }) : isSelected = isSelected ?? false; final bool isSelected; @@ -95,15 +100,16 @@ class CommonCard extends StatelessWidget { final Info? info; final CommonCardType type; final double radius; + final WidgetStateProperty? backgroundColor; BorderSide getBorderSide(BuildContext context, Set states) { - if (type == CommonCardType.filled) { - return BorderSide.none; - } - final colorScheme = Theme.of(context).colorScheme; + final colorScheme = context.colorScheme; + // if (type == CommonCardType.filled) { + // return BorderSide.none; + // } final hoverColor = isSelected - ? colorScheme.primary.toLight() - : colorScheme.primary.toLighter(); + ? colorScheme.primary.toLight + : colorScheme.primary.toLighter; if (states.contains(WidgetState.hovered) || states.contains(WidgetState.focused) || states.contains(WidgetState.pressed)) { @@ -112,19 +118,19 @@ class CommonCard extends StatelessWidget { ); } return BorderSide( - color: isSelected ? colorScheme.primary : colorScheme.onSurface.toSoft(), + color: isSelected ? colorScheme.primary : colorScheme.onSurface.toSoft, ); } Color? getBackgroundColor(BuildContext context, Set states) { - final colorScheme = Theme.of(context).colorScheme; + final colorScheme = context.colorScheme; switch (type) { case CommonCardType.plain: if (isSelected) { return colorScheme.secondaryContainer; } if (states.isEmpty) { - return colorScheme.secondaryContainer.toLittle(); + return colorScheme.surface; } return Theme.of(context) .outlinedButtonTheme @@ -135,7 +141,7 @@ class CommonCard extends StatelessWidget { if (isSelected) { return colorScheme.secondaryContainer; } - return colorScheme.surfaceContainer; + return colorScheme.surfaceContainerLow; } } @@ -148,6 +154,9 @@ class CommonCard extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ InfoHeader( + padding: baseInfoEdgeInsets.copyWith( + bottom: 0, + ), info: info!, ), Flexible( @@ -157,6 +166,7 @@ class CommonCard extends StatelessWidget { ], ); } + if (selectWidget != null && isSelected) { final List children = []; children.add(childWidget); @@ -169,6 +179,7 @@ class CommonCard extends StatelessWidget { children: children, ); } + return OutlinedButton( clipBehavior: Clip.antiAlias, style: ButtonStyle( @@ -178,9 +189,12 @@ class CommonCard extends StatelessWidget { borderRadius: BorderRadius.circular(radius), ), ), - backgroundColor: WidgetStateProperty.resolveWith( - (states) => getBackgroundColor(context, states), - ), + iconColor: WidgetStatePropertyAll(context.colorScheme.primary), + iconSize: WidgetStateProperty.all(20), + backgroundColor: backgroundColor ?? + WidgetStateProperty.resolveWith( + (states) => getBackgroundColor(context, states), + ), side: WidgetStateProperty.resolveWith( (states) => getBorderSide(context, states), ), diff --git a/lib/widgets/donut_chart.dart b/lib/widgets/donut_chart.dart new file mode 100644 index 0000000..a57959f --- /dev/null +++ b/lib/widgets/donut_chart.dart @@ -0,0 +1,175 @@ +import 'dart:math'; +import 'package:fl_clash/common/common.dart'; +import 'package:flutter/material.dart'; + +@immutable +class DonutChartData { + final double _value; + final Color color; + + const DonutChartData({ + required double value, + required this.color, + }) : _value = value + 1; + + double get value => _value; + + @override + String toString() { + return 'DonutChartData{_value: $_value}'; + } +} + +class DonutChart extends StatefulWidget { + final List data; + final Duration duration; + + const DonutChart({ + super.key, + required this.data, + this.duration = commonDuration, + }); + + @override + State createState() => _DonutChartState(); +} + +class _DonutChartState extends State + with SingleTickerProviderStateMixin { + late AnimationController _animationController; + late List _oldData; + + @override + void initState() { + super.initState(); + _oldData = widget.data; + _animationController = AnimationController( + vsync: this, + duration: widget.duration, + ); + } + + @override + void didUpdateWidget(DonutChart oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.data != widget.data) { + _oldData = oldWidget.data; + _animationController.forward(from: 0); + } + } + + @override + void dispose() { + _animationController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _animationController, + builder: (context, child) { + return CustomPaint( + painter: DonutChartPainter( + _oldData, + widget.data, + _animationController.value, + ), + ); + }, + ); + } +} + +class DonutChartPainter extends CustomPainter { + final List oldData; + final List newData; + final double progress; + + DonutChartPainter(this.oldData, this.newData, this.progress); + + double _logTransform(double value) { + const base = 10.0; + const minValue = 0.1; + if (value < minValue) return 0; + return log(value) / log(base) + 1; + } + + double _expTransform(double value) { + const base = 10.0; + if (value <= 0) return 0; + return pow(base, value - 1).toDouble(); + } + + List get interpolatedData { + if (oldData.length != newData.length) return newData; + + final interpolatedData = List.generate(newData.length, (index) { + final oldValue = oldData[index].value; + final newValue = newData[index].value; + + final logOldValue = _logTransform(oldValue); + final logNewValue = _logTransform(newValue); + final interpolatedLogValue = + logOldValue + (logNewValue - logOldValue) * progress; + + final interpolatedValue = _expTransform(interpolatedLogValue); + + return DonutChartData( + value: interpolatedValue, + color: newData[index].color, + ); + }); + + return interpolatedData; + } + + @override + void paint(Canvas canvas, Size size) { + final center = Offset(size.width / 2, size.height / 2); + const strokeWidth = 10.0; + final radius = min(size.width / 2, size.height / 2) - strokeWidth / 2; + + final gapAngle = 2 * asin(strokeWidth * 1 / (2 * radius)) * 1.2; + + final data = interpolatedData; + final total = data.fold( + 0, + (sum, item) => sum + item.value, + ); + + if (total <= 0) return; + + final availableAngle = 2 * pi - (data.length * gapAngle); + double startAngle = -pi / 2 + gapAngle / 2; + + for (final item in data) { + final sweepAngle = availableAngle * (item.value / total); + + if (sweepAngle <= 0) continue; + + final paint = Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = strokeWidth + ..strokeCap = StrokeCap.round + ..color = item.color; + + canvas.drawArc( + Rect.fromCircle(center: center, radius: radius), + startAngle, + sweepAngle, + false, + paint, + ); + + startAngle += sweepAngle + gapAngle; + } + } + + @override + bool shouldRepaint(DonutChartPainter oldDelegate) { + return oldDelegate.progress != progress || + oldDelegate.oldData != oldData || + oldDelegate.newData != newData; + } +} diff --git a/lib/widgets/grid.dart b/lib/widgets/grid.dart index 4db448f..25afb35 100644 --- a/lib/widgets/grid.dart +++ b/lib/widgets/grid.dart @@ -1,8 +1,10 @@ -import 'package:flutter/foundation.dart'; +import 'package:fl_clash/common/common.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'dart:math' as math; +typedef WrapBuilder = Widget Function(Widget child); + class Grid extends MultiChildRenderObjectWidget { final double mainAxisSpacing; @@ -362,6 +364,18 @@ class GridItem extends ParentDataWidget { @override Type get debugTypicalAncestorWidgetClass => GridItem; + + GridItem wrap({ + required WrapBuilder builder, + }) { + return GridItem( + mainAxisCellCount: mainAxisCellCount, + crossAxisCellCount: crossAxisCellCount, + child: builder( + child, + ), + ); + } } class _Origin { @@ -372,11 +386,11 @@ class _Origin { } _Origin _getOrigin(List offsets, int crossAxisCount) { - var length = offsets.length; - var origin = const _Origin(0, double.infinity); + final length = offsets.length; + _Origin origin = const _Origin(0, double.infinity); for (int i = 0; i < length; i++) { final offset = offsets[i]; - if (offset.lessOrEqual(origin.mainAxisOffset)) { + if (offset.moreOrEqual(origin.mainAxisOffset)) { continue; } int start = 0; @@ -386,7 +400,7 @@ _Origin _getOrigin(List offsets, int crossAxisCount) { j < length && length - j >= crossAxisCount - span; j++) { - if (offset.lessOrEqual(offsets[j])) { + if (offset.moreOrEqual(offsets[j])) { span++; if (span == crossAxisCount) { origin = _Origin(start, offset); @@ -399,19 +413,3 @@ _Origin _getOrigin(List offsets, int crossAxisCount) { } return origin; } - -extension on double { - lessOrEqual(double value) { - return value < this || (value - this).abs() < precisionErrorTolerance + 1; - } -} - -extension on Offset { - double getCrossAxisOffset(Axis direction) { - return direction == Axis.vertical ? dx : dy; - } - - double getMainAxisOffset(Axis direction) { - return direction == Axis.vertical ? dy : dx; - } -} diff --git a/lib/widgets/icon.dart b/lib/widgets/icon.dart index 082b43e..ee194dd 100644 --- a/lib/widgets/icon.dart +++ b/lib/widgets/icon.dart @@ -2,11 +2,11 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:fl_clash/common/common.dart'; import 'package:flutter/material.dart'; -class CommonIcon extends StatelessWidget { +class CommonTargetIcon extends StatelessWidget { final String src; final double size; - const CommonIcon({ + const CommonTargetIcon({ super.key, required this.src, required this.size, diff --git a/lib/widgets/line_chart.dart b/lib/widgets/line_chart.dart index d405858..5fd3c3f 100644 --- a/lib/widgets/line_chart.dart +++ b/lib/widgets/line_chart.dart @@ -1,5 +1,4 @@ import 'dart:ui'; - import 'package:flutter/material.dart'; class Point { @@ -7,40 +6,30 @@ class Point { final double y; const Point(this.x, this.y); - - @override - String toString() { - return 'Point{x: $x, y: $y}'; - } } class LineChart extends StatefulWidget { final List points; final Color color; - final double height; final Duration duration; + final bool gradient; const LineChart({ super.key, + this.gradient = false, required this.points, required this.color, - this.duration = const Duration(milliseconds: 0), - required this.height, + this.duration = Duration.zero, }); @override State createState() => _LineChartState(); } -typedef ComputedPath = Path Function(Size size); - class _LineChartState extends State with SingleTickerProviderStateMixin { late AnimationController _controller; - - double progress = 0; List prevPoints = []; - List nextPoints = []; List points = []; @override @@ -59,41 +48,77 @@ class _LineChartState extends State super.didUpdateWidget(oldWidget); if (widget.points != points) { prevPoints = points; - if (!_controller.isCompleted) { - prevPoints = nextPoints; - } points = widget.points; _controller.forward(from: 0); } } + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder(builder: (_, container) { + return AnimatedBuilder( + animation: _controller.view, + builder: (_, __) { + return CustomPaint( + painter: LineChartPainter( + prevPoints: prevPoints, + points: points, + progress: _controller.value, + gradient: widget.gradient, + color: widget.color, + ), + child: SizedBox( + height: container.maxHeight, + width: container.maxWidth, + ), + ); + }, + ); + }); + } +} + +class LineChartPainter extends CustomPainter { + final List prevPoints; + final List points; + final double progress; + final Color color; + final bool gradient; + + LineChartPainter({ + required this.prevPoints, + required this.points, + required this.progress, + required this.color, + required this.gradient, + }); + List getRenderPoints(List points) { if (points.isEmpty) return []; double maxX = points[0].x; double minX = points[0].x; double maxY = points[0].y; double minY = points[0].y; - for (var point in points) { + + for (final point in points) { if (point.x > maxX) maxX = point.x; if (point.x < minX) minX = point.x; if (point.y > maxY) maxY = point.y; if (point.y < minY) minY = point.y; } + return points.map((e) { var x = (e.x - minX) / (maxX - minX); - if (x.isNaN) { - x = 0.5; - } - + if (x.isNaN) x = 0; var y = (e.y - minY) / (maxY - minY); - if (y.isNaN) { - y = 0.5; - } - - return Point( - x, - y, - ); + if (y.isNaN) y = 0; + return Point(x, y); }).toList(); } @@ -102,18 +127,16 @@ class _LineChartState extends State List points, double t, ) { - var renderPrevPoints = getRenderPoints(prevPoints); - var renderPotions = getRenderPoints(points); - return List.generate(renderPotions.length, (i) { + final renderPrevPoints = getRenderPoints(prevPoints); + final renderPoints = getRenderPoints(points); + + return List.generate(renderPoints.length, (i) { if (i > renderPrevPoints.length - 1) { - return renderPotions[i]; + return renderPoints[i]; } - var x = lerpDouble(renderPrevPoints[i].x, renderPotions[i].x, t)!; - var y = lerpDouble(renderPrevPoints[i].y, renderPotions[i].y, t)!; - return Point( - x, - y, - ); + final x = lerpDouble(renderPrevPoints[i].x, renderPoints[i].x, t)!; + final y = lerpDouble(renderPrevPoints[i].y, renderPoints[i].y, t)!; + return Point(x, y); }); } @@ -126,86 +149,78 @@ class _LineChartState extends State final currentPoint = points[i]; final midX = (currentPoint.x + nextPoint.x) / 2; final midY = (currentPoint.y + nextPoint.y) / 2; + path.quadraticBezierTo( - currentPoint.x * size.width, (1 - currentPoint.y) * size.height, - midX * size.width, (1 - midY) * size.height, + currentPoint.x * size.width, + (1 - currentPoint.y) * size.height, + midX * size.width, + (1 - midY) * size.height, ); } - path.lineTo(points.last.x * size.width, (1 - points.last.y) * size.height); + path.lineTo(points.last.x * size.width, (1 - points.last.y) * size.height); return path; } - ComputedPath getComputedPath({ - required List prevPoints, - required List points, - required progress, - }) { - nextPoints = getInterpolatePoints(prevPoints, points, progress); - return (size) { - final prevPath = getPath(prevPoints, size); - final nextPath = getPath(nextPoints, size); - final prevMetric = prevPath.computeMetrics().first; - final nextMetric = nextPath.computeMetrics().first; - final prevLength = prevMetric.length; - final nextLength = nextMetric.length; - return nextMetric.extractPath( - 0, - prevLength + (nextLength - prevLength) * progress, - ); - }; + Path getAnimatedPath(Size size) { + final interpolatedPoints = + getInterpolatePoints(prevPoints, points, progress); + final path = getPath(interpolatedPoints, size); + + final metric = path.computeMetrics().first; + final length = metric.length; + return metric.extractPath( + 0, + length, + ); } - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return AnimatedBuilder( - animation: _controller.view, - builder: (_, __) { - return CustomPaint( - painter: LineChartPainter( - color: widget.color, - computedPath: getComputedPath( - prevPoints: prevPoints, - points: points, - progress: _controller.value, - ), - ), - child: SizedBox( - height: widget.height, - width: double.infinity, - ), - ); - }); - } -} - -class LineChartPainter extends CustomPainter { - final ComputedPath computedPath; - final Color color; - - LineChartPainter({ - required this.computedPath, - required this.color, - }); - @override void paint(Canvas canvas, Size size) { - final paint = Paint() - ..color = color - ..strokeWidth = 2.0 - ..style = PaintingStyle.stroke; + final strokeWidth = 2.0; + final chartSize = Size(size.width, size.height * 0.7); + final path = getAnimatedPath(chartSize); - canvas.drawPath(computedPath(size), paint); + if (gradient) { + final fillPath = Path.from(path); + fillPath.lineTo(size.width, size.height + strokeWidth * 2); + fillPath.lineTo(0, size.height + strokeWidth * 2); + fillPath.close(); + + final gradient = LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + color.withOpacity(0.3), + color.withOpacity(0.1), + ], + ); + + final shader = gradient.createShader( + Rect.fromLTWH(0, 0, size.width, size.height + strokeWidth * 2), + ); + + canvas.drawPath( + fillPath, + Paint() + ..shader = shader + ..style = PaintingStyle.fill); + } + + canvas.drawPath( + path, + Paint() + ..color = color + ..strokeWidth = strokeWidth + ..style = PaintingStyle.stroke); } @override - bool shouldRepaint(covariant CustomPainter oldDelegate) { - return true; + bool shouldRepaint(covariant LineChartPainter oldDelegate) { + return oldDelegate.progress != progress || + oldDelegate.prevPoints != prevPoints || + oldDelegate.points != points || + oldDelegate.color != color || + oldDelegate.gradient != gradient; } } diff --git a/lib/widgets/list.dart b/lib/widgets/list.dart index 365cf75..fb2dd56 100644 --- a/lib/widgets/list.dart +++ b/lib/widgets/list.dart @@ -253,6 +253,7 @@ class ListItem extends StatelessWidget { splashFactory: NoSplash.splashFactory, onTap: onTap, child: Container( + constraints: BoxConstraints.expand(), padding: padding, child: Row( children: children, diff --git a/lib/widgets/scaffold.dart b/lib/widgets/scaffold.dart index e685958..508606e 100644 --- a/lib/widgets/scaffold.dart +++ b/lib/widgets/scaffold.dart @@ -3,6 +3,7 @@ import 'package:fl_clash/state.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; + class CommonScaffold extends StatefulWidget { final Widget body; final Widget? bottomNavigationBar; @@ -50,7 +51,7 @@ class CommonScaffold extends StatefulWidget { class CommonScaffoldState extends State { final ValueNotifier> _actions = ValueNotifier([]); - final ValueNotifier _floatingActionButton = ValueNotifier(null); + final ValueNotifier _floatingActionButton = ValueNotifier(null); final ValueNotifier _loading = ValueNotifier(false); set actions(List actions) { @@ -108,68 +109,70 @@ class CommonScaffoldState extends State { @override Widget build(BuildContext context) { - final scaffold = ValueListenableBuilder( - valueListenable: _floatingActionButton, - builder: (_, value, __) { - return Scaffold( - resizeToAvoidBottomInset: true, - appBar: PreferredSize( - preferredSize: const Size.fromHeight(kToolbarHeight), - child: Stack( - alignment: Alignment.bottomCenter, - children: [ - ValueListenableBuilder>( - valueListenable: _actions, - builder: (_, actions, __) { - final realActions = - actions.isNotEmpty ? actions : widget.actions; - return AppBar( - centerTitle: false, - systemOverlayStyle: SystemUiOverlayStyle( - statusBarColor: Colors.transparent, - statusBarIconBrightness: - Theme.of(context).brightness == Brightness.dark - ? Brightness.light - : Brightness.dark, - systemNavigationBarIconBrightness: - Theme.of(context).brightness == Brightness.dark - ? Brightness.light - : Brightness.dark, - systemNavigationBarColor: - widget.bottomNavigationBar != null - ? context.colorScheme.surfaceContainer - : context.colorScheme.surface, - systemNavigationBarDividerColor: Colors.transparent, + final scaffold = Scaffold( + resizeToAvoidBottomInset: true, + appBar: PreferredSize( + preferredSize: const Size.fromHeight(kToolbarHeight), + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + ValueListenableBuilder>( + valueListenable: _actions, + builder: (_, actions, __) { + final realActions = + actions.isNotEmpty ? actions : widget.actions ?? []; + return AppBar( + centerTitle: false, + systemOverlayStyle: SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + statusBarIconBrightness: + Theme.of(context).brightness == Brightness.dark + ? Brightness.light + : Brightness.dark, + systemNavigationBarIconBrightness: + Theme.of(context).brightness == Brightness.dark + ? Brightness.light + : Brightness.dark, + systemNavigationBarColor: widget.bottomNavigationBar != null + ? context.colorScheme.surfaceContainer + : context.colorScheme.surface, + systemNavigationBarDividerColor: Colors.transparent, + ), + automaticallyImplyLeading: widget.automaticallyImplyLeading, + leading: widget.leading, + title: Text(widget.title), + actions: [ + ...realActions.separated( + SizedBox( + width: 4, ), - automaticallyImplyLeading: - widget.automaticallyImplyLeading, - leading: widget.leading, - title: Text(widget.title), - actions: [ - ...?realActions, - const SizedBox( - width: 8, - ) - ], - ); - }, - ), - ValueListenableBuilder( - valueListenable: _loading, - builder: (_, value, __) { - return value == true - ? const LinearProgressIndicator() - : Container(); - }, - ), - ], + ), + SizedBox( + width: 8, + ) + ], + ); + }, ), - ), - body: body, - floatingActionButton: value, - bottomNavigationBar: widget.bottomNavigationBar, - ); - }, + ValueListenableBuilder( + valueListenable: _loading, + builder: (_, value, __) { + return value == true + ? const LinearProgressIndicator() + : Container(); + }, + ), + ], + ), + ), + body: body, + floatingActionButton: ValueListenableBuilder( + valueListenable: _floatingActionButton, + builder: (_, value, __) { + return value ?? Container(); + }, + ), + bottomNavigationBar: widget.bottomNavigationBar, ); return _sideNavigationBar != null ? Row( diff --git a/lib/widgets/sheet.dart b/lib/widgets/sheet.dart index 69d966f..e45f55a 100644 --- a/lib/widgets/sheet.dart +++ b/lib/widgets/sheet.dart @@ -65,7 +65,7 @@ showExtendPage( showSheet({ required BuildContext context, - required WidgetBuilder builder, + required Widget body, required String title, bool isScrollControlled = true, double width = 320, @@ -78,9 +78,7 @@ showSheet({ isScrollControlled: isScrollControlled, builder: (context) { return SafeArea( - child: builder( - context, - ), + child: body, ); }, showDragHandle: true, @@ -95,7 +93,7 @@ showSheet({ maxWidth: width, ), body: SafeArea( - child: builder(context), + child: body, ), title: title, ); diff --git a/lib/widgets/subscription_info_view.dart b/lib/widgets/subscription_info_view.dart index 636d997..c7c43a9 100644 --- a/lib/widgets/subscription_info_view.dart +++ b/lib/widgets/subscription_info_view.dart @@ -35,7 +35,7 @@ class SubscriptionInfoView extends StatelessWidget { LinearProgressIndicator( minHeight: 6, value: progress, - backgroundColor: context.colorScheme.primary.toSoft(), + backgroundColor: context.colorScheme.primary.toSoft, ), const SizedBox( height: 8, diff --git a/lib/widgets/super_grid.dart b/lib/widgets/super_grid.dart new file mode 100644 index 0000000..563c378 --- /dev/null +++ b/lib/widgets/super_grid.dart @@ -0,0 +1,889 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:defer_pointer/defer_pointer.dart'; +import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/enum/enum.dart'; +import 'package:fl_clash/widgets/activate_box.dart'; +import 'package:fl_clash/widgets/card.dart'; +import 'package:fl_clash/widgets/grid.dart'; +import 'package:fl_clash/widgets/sheet.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/physics.dart'; + +typedef VoidCallback = void Function(); + +class SuperGrid extends StatefulWidget { + final List children; + final double mainAxisSpacing; + final double crossAxisSpacing; + final int crossAxisCount; + final void Function(List newChildren)? onSave; + final List Function(List newChildren)? addedItemsBuilder; + + const SuperGrid({ + super.key, + required this.children, + this.crossAxisCount = 1, + this.mainAxisSpacing = 0, + this.crossAxisSpacing = 0, + this.onSave, + this.addedItemsBuilder, + }); + + @override + State createState() => SuperGridState(); +} + +class SuperGridState extends State with TickerProviderStateMixin { + final ValueNotifier> _childrenNotifier = ValueNotifier([]); + final ValueNotifier> addedChildrenNotifier = ValueNotifier([]); + + int get length => _childrenNotifier.value.length; + List _tempIndexList = []; + List _itemContexts = []; + Size _containerSize = Size.zero; + int _targetIndex = -1; + Offset _targetOffset = Offset.zero; + List _sizes = []; + List _offsets = []; + Offset _parentOffset = Offset.zero; + EdgeDraggingAutoScroller? _edgeDraggingAutoScroller; + final ValueNotifier isEditNotifier = ValueNotifier(false); + + Map> _transformTweenMap = {}; + + final ValueNotifier _animating = ValueNotifier(false); + + final _dragWidgetSizeNotifier = ValueNotifier(Size.zero); + final _dragIndexNotifier = ValueNotifier(-1); + + late AnimationController _transformController; + + Completer? _transformCompleter; + + Map> _transformAnimationMap = {}; + + late AnimationController _fakeDragWidgetController; + Animation? _fakeDragWidgetAnimation; + + late AnimationController _shakeController; + late Animation _shakeAnimation; + Rect _dragRect = Rect.zero; + Scrollable? _scrollable; + + int get crossCount => widget.crossAxisCount; + + _onChildrenChange() { + _tempIndexList = List.generate(length, (index) => index); + _itemContexts = List.filled( + length, + null, + ); + } + + _preTransformState() { + _sizes = _itemContexts.map((item) => item!.size!).toList(); + _parentOffset = + (context.findRenderObject() as RenderBox).localToGlobal(Offset.zero); + _offsets = _itemContexts + .map((item) => + (item!.findRenderObject() as RenderBox).localToGlobal(Offset.zero) - + _parentOffset) + .toList(); + _containerSize = context.size!; + } + + showAddModal() { + if (!isEditNotifier.value) { + return; + } + showSheet( + width: 360, + context: context, + body: ValueListenableBuilder( + valueListenable: addedChildrenNotifier, + builder: (_, value, __) { + return _AddedWidgetsModal( + items: value, + onAdd: (gridItem) { + _childrenNotifier.value = List.from(_childrenNotifier.value) + ..add( + gridItem, + ); + }, + ); + }, + ), + title: appLocalizations.add, + ); + } + + _initState() { + _transformController.value = 0; + _sizes = List.generate(length, (index) => Size.zero); + _offsets = []; + _transformTweenMap.clear(); + _transformAnimationMap.clear(); + _containerSize = Size.zero; + _dragIndexNotifier.value = -1; + _dragWidgetSizeNotifier.value = Size.zero; + _targetOffset = Offset.zero; + _parentOffset = Offset.zero; + _dragRect = Rect.zero; + _targetIndex = -1; + } + + _handleChildrenNotifierChange() { + addedChildrenNotifier.value = widget.addedItemsBuilder != null + ? widget.addedItemsBuilder!(_childrenNotifier.value) + : []; + } + + @override + void initState() { + super.initState(); + + _childrenNotifier.value = widget.children; + + _childrenNotifier.addListener(_handleChildrenNotifierChange); + + isEditNotifier.addListener(_handleIsEditChange); + + _fakeDragWidgetController = AnimationController.unbounded( + vsync: this, + duration: commonDuration, + ); + + _shakeController = AnimationController( + vsync: this, + duration: Duration(milliseconds: 120), + ); + _shakeAnimation = Tween( + begin: -0.012, + end: 0.012, + ).animate( + CurvedAnimation( + parent: _shakeController, + curve: Curves.easeInOut, + ), + ); + + _transformController = AnimationController( + vsync: this, + duration: commonDuration, + ); + + _initState(); + } + + _handleIsEditChange() async { + _handleChildrenNotifierChange(); + if (isEditNotifier.value == false) { + if (widget.onSave != null) { + await _transformCompleter?.future; + await Future.delayed(commonDuration); + widget.onSave!(_childrenNotifier.value); + } + } + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + + final scrollable = context.findAncestorWidgetOfExactType(); + if (scrollable == null) { + return; + } + if (_scrollable != scrollable) { + _edgeDraggingAutoScroller = EdgeDraggingAutoScroller( + Scrollable.of(context), + onScrollViewScrolled: () { + _edgeDraggingAutoScroller?.startAutoScrollIfNecessary(_dragRect); + }, + velocityScalar: 40, + ); + } + } + + _transform() async { + List layoutOffsets = [ + Offset(_containerSize.width, 0), + ]; + final List nextOffsets = []; + + for (final index in _tempIndexList) { + final size = _sizes[index]; + final offset = _getNextOffset(layoutOffsets, size); + final layoutOffset = Offset( + min( + offset.dx + size.width + widget.crossAxisSpacing, + _containerSize.width, + ), + min( + offset.dy + size.height + widget.mainAxisSpacing, + _containerSize.height, + ), + ); + final startLayoutOffsetX = offset.dx; + final endLayoutOffsetX = layoutOffset.dx; + nextOffsets.add(offset); + + final startIndex = + layoutOffsets.indexWhere((i) => i.dx >= startLayoutOffsetX); + final endIndex = + layoutOffsets.indexWhere((i) => i.dx >= endLayoutOffsetX); + final endOffset = layoutOffsets[endIndex]; + + if (startIndex != endIndex) { + final startOffset = layoutOffsets[startIndex]; + if (startOffset.dx != startLayoutOffsetX) { + layoutOffsets[startIndex] = Offset( + startLayoutOffsetX, + startOffset.dy, + ); + } + } + if (endOffset.dx == endLayoutOffsetX) { + layoutOffsets[endIndex] = layoutOffset; + } else { + layoutOffsets.insert(endIndex, layoutOffset); + } + layoutOffsets.removeRange(min(startIndex + 1, endIndex), endIndex); + } + + final Map> transformTweenMap = {}; + + for (final index in _tempIndexList) { + final nextIndex = _tempIndexList.indexWhere((i) => i == index); + transformTweenMap[index] = Tween( + begin: _transformTweenMap[index]?.begin ?? Offset.zero, + end: nextOffsets[nextIndex] - _offsets[index], + ); + } + + _transformTweenMap = transformTweenMap; + + _transformAnimationMap = transformTweenMap.map( + (key, value) { + final preAnimationValue = _transformAnimationMap[key]?.value; + return MapEntry( + key, + Tween( + begin: preAnimationValue ?? Offset.zero, + end: value.end, + ).animate(_transformController), + ); + }, + ); + + if (_targetIndex != -1) { + _targetOffset = nextOffsets[_targetIndex]; + } + return _transformController.forward(from: 0); + } + + _handleDragStarted(int index) { + _initState(); + _preTransformState(); + _dragIndexNotifier.value = index; + _dragWidgetSizeNotifier.value = _sizes[index]; + _targetIndex = index; + _targetOffset = _offsets[index]; + _dragRect = Rect.fromLTWH( + _targetOffset.dx + _parentOffset.dx, + _targetOffset.dy + _parentOffset.dy, + _sizes[index].width, + _sizes[index].height, + ); + } + + _handleDragEnd(DraggableDetails details) async { + debouncer.cancel(DebounceTag.handleWill); + if (_targetIndex == -1) { + return; + } + const spring = SpringDescription( + mass: 1, + stiffness: 100, + damping: 10, + ); + final simulation = SpringSimulation(spring, 0, 1, 0); + _fakeDragWidgetAnimation = Tween( + begin: details.offset - _parentOffset, + end: _targetOffset, + ).animate(_fakeDragWidgetController); + _animating.value = true; + + _transformCompleter = Completer(); + final animateWith = _fakeDragWidgetController.animateWith(simulation); + _transformCompleter?.complete(animateWith); + await animateWith; + _animating.value = false; + _fakeDragWidgetAnimation = null; + _transformTweenMap.clear(); + _transformAnimationMap.clear(); + final children = List.from(_childrenNotifier.value); + children.insert(_targetIndex, children.removeAt(_dragIndexNotifier.value)); + _childrenNotifier.value = children; + _initState(); + } + + _handleDragUpdate(DragUpdateDetails details) { + _dragRect = _dragRect.translate( + 0, + details.delta.dy, + ); + _edgeDraggingAutoScroller?.startAutoScrollIfNecessary(_dragRect); + } + + _handleWill(int index) async { + final dragIndex = _dragIndexNotifier.value; + if (dragIndex < 0 || dragIndex > _offsets.length - 1) { + return; + } + final targetIndex = _tempIndexList.indexWhere((i) => i == index); + if (_targetIndex == targetIndex) { + return; + } + _tempIndexList = List.generate(length, (i) { + if (i == targetIndex) return _dragIndexNotifier.value; + if (_targetIndex > targetIndex && i > targetIndex && i <= _targetIndex) { + return _tempIndexList[i - 1]; + } else if (_targetIndex < targetIndex && + i >= _targetIndex && + i < targetIndex) { + return _tempIndexList[i + 1]; + } + return _tempIndexList[i]; + }).toList(); + + _targetIndex = targetIndex; + + await _transform(); + } + + _handleDelete(int index) async { + _preTransformState(); + final indexWhere = _tempIndexList.indexWhere((i) => i == index); + _tempIndexList.removeAt(indexWhere); + await _transform(); + final children = List.from(_childrenNotifier.value); + children.removeAt(index); + _childrenNotifier.value = children; + _initState(); + } + + Widget _wrapTransform(Widget rawChild, int index) { + return ValueListenableBuilder( + valueListenable: _animating, + builder: (_, animating, child) { + if (animating) { + if (_dragIndexNotifier.value == index) { + return _sizeBoxWrap( + Container(), + index, + ); + } + } + return child!; + }, + child: AnimatedBuilder( + builder: (_, child) { + return Transform.translate( + offset: _transformAnimationMap[index]?.value ?? Offset.zero, + child: child, + ); + }, + animation: _transformController.view, + child: rawChild, + ), + ); + } + + Offset _getNextOffset(List offsets, Size size) { + final length = offsets.length; + Offset nextOffset = Offset(0, double.infinity); + for (int i = 0; i < length; i++) { + final offset = offsets[i]; + if (offset.dy.moreOrEqual(nextOffset.dy)) { + continue; + } + double offsetX = 0; + double span = 0; + for (int j = 0; + span < size.width && + j < length && + _containerSize.width.moreOrEqual(offsetX + size.width); + j++) { + final tempOffset = offsets[j]; + if (offset.dy.moreOrEqual(tempOffset.dy)) { + span = tempOffset.dx - offsetX; + if (span.moreOrEqual(size.width)) { + nextOffset = Offset(offsetX, offset.dy); + } + } else { + offsetX = tempOffset.dx; + span = 0; + } + } + } + return nextOffset; + } + + Widget _sizeBoxWrap(Widget child, int index) { + return ValueListenableBuilder( + valueListenable: _dragWidgetSizeNotifier, + builder: (_, size, child) { + return SizedBox.fromSize( + size: size, + child: child!, + ); + }, + child: child, + ); + } + + Widget _ignoreWrap(Widget child) { + return ValueListenableBuilder( + valueListenable: _animating, + builder: (_, animating, child) { + if (animating) { + return ActivateBox( + child: child!, + ); + } else { + return child!; + } + }, + child: child, + ); + } + + Widget _shakeWrap(Widget child) { + final random = 0.7 + Random().nextDouble() * 0.3; + _shakeController.stop(); + _shakeController.repeat(reverse: true); + return AnimatedBuilder( + animation: _shakeAnimation, + builder: (_, child) { + return Transform.rotate( + angle: _shakeAnimation.value * random, + child: child!, + ); + }, + child: child, + ); + } + + Widget _draggableWrap({ + required Widget childWhenDragging, + required Widget feedback, + required Widget target, + required int index, + }) { + final shakeTarget = ValueListenableBuilder( + valueListenable: _dragIndexNotifier, + builder: (_, dragIndex, child) { + if (dragIndex == index) { + return child!; + } + return _shakeWrap( + _DeletableContainer( + onDelete: () { + _handleDelete(index); + }, + child: child!, + ), + ); + }, + child: target, + ); + final draggableChild = system.isDesktop + ? Draggable( + childWhenDragging: childWhenDragging, + data: index, + feedback: feedback, + onDragStarted: () { + _handleDragStarted(index); + }, + onDragUpdate: (details) { + _handleDragUpdate(details); + }, + onDragEnd: (details) { + _handleDragEnd(details); + }, + child: shakeTarget, + ) + : LongPressDraggable( + childWhenDragging: childWhenDragging, + data: index, + feedback: feedback, + onDragStarted: () { + _handleDragStarted(index); + }, + onDragUpdate: (details) { + _handleDragUpdate(details); + }, + onDragEnd: (details) { + _handleDragEnd(details); + }, + child: shakeTarget, + ); + return ValueListenableBuilder( + valueListenable: isEditNotifier, + builder: (_, isEdit, child) { + if (!isEdit) { + return target; + } + return child!; + }, + child: draggableChild, + ); + } + + Widget _builderItem(int index) { + final girdItem = _childrenNotifier.value[index]; + final child = girdItem.child; + return GridItem( + mainAxisCellCount: girdItem.mainAxisCellCount, + crossAxisCellCount: girdItem.crossAxisCellCount, + child: Builder( + builder: (context) { + _itemContexts[index] = context; + final childWhenDragging = ActivateBox( + child: Opacity( + opacity: 0.3, + child: _sizeBoxWrap( + CommonCard( + child: Container( + color: context.colorScheme.secondaryContainer, + ), + ), + index, + ), + ), + ); + final feedback = ActivateBox( + child: _sizeBoxWrap( + CommonCard( + child: Material( + elevation: 6, + child: child, + ), + ), + index, + ), + ); + final target = DragTarget( + builder: (_, __, ___) { + return child; + }, + onWillAcceptWithDetails: (_) { + debouncer.call( + DebounceTag.handleWill, + _handleWill, + args: [index], + ); + return false; + }, + ); + + return _wrapTransform( + _draggableWrap( + childWhenDragging: childWhenDragging, + feedback: feedback, + target: target, + index: index, + ), + index, + ); + }, + ), + ); + } + + Widget _buildFakeTransformWidget() { + return ValueListenableBuilder( + valueListenable: _animating, + builder: (_, animating, __) { + final index = _dragIndexNotifier.value; + if (!animating || _fakeDragWidgetAnimation == null || index == -1) { + return Container(); + } + return _sizeBoxWrap( + AnimatedBuilder( + animation: _fakeDragWidgetAnimation!, + builder: (_, child) { + return Transform.translate( + offset: _fakeDragWidgetAnimation!.value, + child: child!, + ); + }, + child: ActivateBox( + child: _childrenNotifier.value[index].child, + ), + ), + index, + ); + }, + ); + } + + @override + void dispose() { + _scrollable = null; + _fakeDragWidgetController.dispose(); + _shakeController.dispose(); + _transformController.dispose(); + _dragIndexNotifier.dispose(); + _animating.dispose(); + _childrenNotifier.removeListener(_handleChildrenNotifierChange); + _childrenNotifier.dispose(); + isEditNotifier.removeListener(_handleIsEditChange); + isEditNotifier.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return DeferredPointerHandler( + child: Stack( + children: [ + _ignoreWrap( + ValueListenableBuilder( + valueListenable: _childrenNotifier, + builder: (_, children, __) { + _onChildrenChange(); + return Grid( + axisDirection: AxisDirection.down, + crossAxisCount: crossCount, + crossAxisSpacing: widget.crossAxisSpacing, + mainAxisSpacing: widget.mainAxisSpacing, + children: [ + for (int i = 0; i < children.length; i++) _builderItem(i), + ], + ); + }, + ), + ), + _buildFakeTransformWidget(), + ], + ), + ); + } +} + +class _AddedWidgetsModal extends StatelessWidget { + final List items; + final Function(GridItem item) onAdd; + + const _AddedWidgetsModal({ + required this.items, + required this.onAdd, + }); + + @override + Widget build(BuildContext context) { + return DeferredPointerHandler( + child: SingleChildScrollView( + padding: EdgeInsets.all( + 16, + ), + child: Grid( + crossAxisCount: 8, + crossAxisSpacing: 16, + mainAxisSpacing: 16, + children: items + .map( + (item) => item.wrap( + builder: (child) { + return _AddedContainer( + onAdd: () { + onAdd(item); + }, + child: child, + ); + }, + ), + ) + .toList(), + ), + ), + ); + } +} + +class _DeletableContainer extends StatefulWidget { + final Widget child; + final VoidCallback onDelete; + + const _DeletableContainer({ + required this.child, + required this.onDelete, + }); + + @override + State<_DeletableContainer> createState() => _DeletableContainerState(); +} + +class _DeletableContainerState extends State<_DeletableContainer> + with SingleTickerProviderStateMixin { + late AnimationController _controller; + late Animation _scaleAnimation; + late Animation _fadeAnimation; + bool _deleteButtonVisible = true; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: commonDuration, + ); + _scaleAnimation = Tween(begin: 1.0, end: 0.4).animate( + CurvedAnimation( + parent: _controller, + curve: Curves.easeIn, + ), + ); + _fadeAnimation = Tween(begin: 1.0, end: 0.0).animate( + CurvedAnimation( + parent: _controller, + curve: Curves.easeIn, + ), + ); + } + + @override + void didUpdateWidget(_DeletableContainer oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.child != widget.child) { + setState(() { + _controller.value = 0; + _deleteButtonVisible = true; + }); + } + } + + _handleDel() async { + setState(() { + _deleteButtonVisible = false; + }); + await _controller.forward(from: 0); + widget.onDelete(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Stack( + clipBehavior: Clip.none, + children: [ + AnimatedBuilder( + animation: _controller.view, + builder: (_, child) { + return Transform.scale( + scale: _scaleAnimation.value, + child: Opacity( + opacity: _fadeAnimation.value, + child: child!, + ), + ); + }, + child: widget.child, + ), + if (_deleteButtonVisible) + Positioned( + top: -8, + right: -8, + child: DeferPointer( + child: SizedBox( + width: 24, + height: 24, + child: IconButton.filled( + iconSize: 20, + padding: EdgeInsets.all(2), + onPressed: _handleDel, + icon: Icon( + Icons.close, + ), + ), + ), + ), + ) + ], + ); + } +} + +class _AddedContainer extends StatefulWidget { + final Widget child; + final VoidCallback onAdd; + + const _AddedContainer({ + required this.child, + required this.onAdd, + }); + + @override + State<_AddedContainer> createState() => _AddedContainerState(); +} + +class _AddedContainerState extends State<_AddedContainer> { + @override + void initState() { + super.initState(); + } + + @override + void didUpdateWidget(_AddedContainer oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.child != widget.child) {} + } + + _handleAdd() async { + widget.onAdd(); + } + + @override + void dispose() { + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Stack( + clipBehavior: Clip.none, + children: [ + ActivateBox( + child: widget.child, + ), + Positioned( + top: -8, + right: -8, + child: DeferPointer( + child: SizedBox( + width: 24, + height: 24, + child: IconButton.filled( + iconSize: 20, + padding: EdgeInsets.all(2), + onPressed: _handleAdd, + icon: Icon( + Icons.add, + ), + ), + ), + ), + ) + ], + ); + } +} diff --git a/lib/widgets/text.dart b/lib/widgets/text.dart index ffcb8fe..fe7bbd4 100644 --- a/lib/widgets/text.dart +++ b/lib/widgets/text.dart @@ -61,7 +61,7 @@ class EmojiText extends StatelessWidget { } spans.add( TextSpan( - text:match.group(0), + text: match.group(0), style: style?.copyWith( fontFamily: FontFamily.twEmoji.value, ), diff --git a/lib/widgets/wave.dart b/lib/widgets/wave.dart new file mode 100644 index 0000000..e1ead3b --- /dev/null +++ b/lib/widgets/wave.dart @@ -0,0 +1,108 @@ +import 'dart:math'; +import 'package:flutter/material.dart'; + +class WaveView extends StatefulWidget { + final double waveAmplitude; + final double waveFrequency; + final Color waveColor; + final Duration duration; + + const WaveView({ + super.key, + this.waveAmplitude = 50.0, + this.waveFrequency = 1.5, + required this.waveColor, + this.duration = const Duration(seconds: 2), + }); + + @override + State createState() => _WaveViewState(); +} + +class _WaveViewState extends State + with SingleTickerProviderStateMixin { + late AnimationController _controller; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: widget.duration, + )..repeat(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder(builder: (_, container) { + return AnimatedBuilder( + animation: _controller, + builder: (context, child) { + return CustomPaint( + painter: WavePainter( + animationValue: _controller.value, + waveAmplitude: widget.waveAmplitude, + waveFrequency: widget.waveFrequency, + waveColor: widget.waveColor, + ), + size: Size( + container.maxHeight, + container.maxHeight, + ), + ); + }, + ); + }); + } +} + +class WavePainter extends CustomPainter { + final double animationValue; + final double waveAmplitude; + final double waveFrequency; + final Color waveColor; + + WavePainter({ + required this.animationValue, + required this.waveAmplitude, + required this.waveFrequency, + required this.waveColor, + }); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = waveColor + ..style = PaintingStyle.fill; + + final path = Path(); + + final baseHeight = size.height / 3; + + path.moveTo(0, baseHeight); + + for (double x = 0; x <= size.width; x++) { + final y = waveAmplitude * + sin((x / size.width * 2 * pi * waveFrequency) + + (animationValue * 2 * pi)); + path.lineTo(x, baseHeight + y); + } + + path.lineTo(size.width, size.height); + path.lineTo(0, size.height); + path.close(); + + canvas.drawPath(path, paint); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + return true; + } +} diff --git a/lib/widgets/widgets.dart b/lib/widgets/widgets.dart index 5804977..abd8847 100644 --- a/lib/widgets/widgets.dart +++ b/lib/widgets/widgets.dart @@ -23,3 +23,7 @@ export 'sheet.dart'; export 'side_sheet.dart'; export 'subscription_info_view.dart'; export 'text.dart'; +export 'super_grid.dart'; +export 'donut_chart.dart'; +export 'activate_box.dart'; +export 'wave.dart'; diff --git a/macos/Podfile.lock b/macos/Podfile.lock index a525f4d..f190719 100755 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -100,7 +100,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: app_links: 10e0a0ab602ffaf34d142cd4862f29d34b303b2a - connectivity_plus: 4c41c08fc6d7c91f63bc7aec70ffe3730b04f563 + connectivity_plus: 18382e7311ba19efcaee94442b23b32507b20695 device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720 dynamic_color: 2eaa27267de1ca20d879fbd6e01259773fb1670f file_selector_macos: cc3858c981fe6889f364731200d6232dac1d812d diff --git a/macos/Runner/AppDelegate.swift b/macos/Runner/AppDelegate.swift index 9a1b339..8a18446 100755 --- a/macos/Runner/AppDelegate.swift +++ b/macos/Runner/AppDelegate.swift @@ -13,6 +13,10 @@ class AppDelegate: FlutterAppDelegate { WindowExtPlugin.instance?.handleShouldTerminate() return .terminateCancel } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } override func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool { if !flag { diff --git a/pubspec.lock b/pubspec.lock index 199f900..fcc0ca7 100755 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,23 +5,23 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834 + sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" url: "https://pub.dev" source: hosted - version: "72.0.0" + version: "76.0.0" _macros: dependency: transitive description: dart source: sdk - version: "0.3.2" + version: "0.3.3" analyzer: dependency: transitive description: name: analyzer - sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139 + sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" url: "https://pub.dev" source: hosted - version: "6.7.0" + version: "6.11.0" animations: dependency: "direct main" description: @@ -74,50 +74,50 @@ packages: dependency: transitive description: name: build - sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0 url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2" build_config: dependency: transitive description: name: build_config - sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + sha256: "4ae2de3e1e67ea270081eaee972e1bd8f027d459f249e0f1186730784c2e7e33" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" build_daemon: dependency: transitive description: name: build_daemon - sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" + sha256: "294a2edaf4814a378725bfe6358210196f5ea37af89ecd81bfa32960113d4948" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.0.3" build_resolvers: dependency: transitive description: name: build_resolvers - sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" + sha256: "99d3980049739a985cf9b21f30881f46db3ebc62c5b8d5e60e27440876b1ba1e" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.3" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" + sha256: "74691599a5bc750dc96a6b4bfd48f7d9d66453eab04c7f4063134800d6a5c573" url: "https://pub.dev" source: hosted - version: "2.4.13" + version: "2.4.14" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 + sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021" url: "https://pub.dev" source: hosted - version: "7.3.2" + version: "8.0.0" built_collection: dependency: transitive description: @@ -138,10 +138,10 @@ packages: dependency: "direct main" description: name: cached_network_image - sha256: "4a5d8d2c728b0f3d0245f69f921d7be90cae4c2fd5288f773088672c0893f819" + sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" url: "https://pub.dev" source: hosted - version: "3.4.0" + version: "3.4.1" cached_network_image_platform_interface: dependency: transitive description: @@ -154,10 +154,10 @@ packages: dependency: transitive description: name: cached_network_image_web - sha256: "6322dde7a5ad92202e64df659241104a43db20ed594c41ca18de1014598d7996" + sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.3.1" characters: dependency: transitive description: @@ -170,10 +170,10 @@ packages: dependency: transitive description: name: charcode - sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 + sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.4.0" checked_yaml: dependency: transitive description: @@ -210,18 +210,18 @@ packages: dependency: "direct main" description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.0" connectivity_plus: dependency: "direct main" description: name: connectivity_plus - sha256: "876849631b0c7dc20f8b471a2a03142841b482438e3b707955464f5ffca3e4c3" + sha256: e0817759ec6d2d8e57eb234e6e57d2173931367a865850c7acea40d4b4f9c27d url: "https://pub.dev" source: hosted - version: "6.1.0" + version: "6.1.1" connectivity_plus_platform_interface: dependency: transitive description: @@ -270,6 +270,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.10" + defer_pointer: + dependency: "direct main" + description: + name: defer_pointer + sha256: d69e6f8c1d0f052d2616cc1db3782e0ea73f42e4c6f6122fd1a548dfe79faf02 + url: "https://pub.dev" + source: hosted + version: "0.0.2" device_info_plus: dependency: "direct main" description: @@ -282,10 +290,10 @@ packages: dependency: transitive description: name: device_info_plus_platform_interface - sha256: "282d3cf731045a2feb66abfe61bbc40870ae50a3ed10a4d3d217556c35c8c2ba" + sha256: "0b04e02b30791224b31969eb1b50d723498f402971bff3630bca2ba839bd1ed2" url: "https://pub.dev" source: hosted - version: "7.0.1" + version: "7.0.2" dio: dependency: "direct main" description: @@ -318,6 +326,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.0.5" + equatable: + dependency: transitive + description: + name: equatable + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" + url: "https://pub.dev" + source: hosted + version: "2.0.7" fake_async: dependency: transitive description: @@ -354,18 +370,18 @@ packages: dependency: "direct main" description: name: file_picker - sha256: "825aec673606875c33cd8d3c4083f1a3c3999015a84178b317b7ef396b7384f3" + sha256: c2376a6aae82358a9f9ccdd7d1f4006d08faa39a2767cce01031d9f593a8bd3b url: "https://pub.dev" source: hosted - version: "8.0.7" + version: "8.1.6" file_selector_linux: dependency: transitive description: name: file_selector_linux - sha256: "712ce7fab537ba532c8febdb1a8f167b32441e74acd68c3ccb2e36dcb52c4ab2" + sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33" url: "https://pub.dev" source: hosted - version: "0.9.3" + version: "0.9.3+2" file_selector_macos: dependency: transitive description: @@ -550,10 +566,10 @@ packages: dependency: transitive description: name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + sha256: "76d306a1c3afb33fe82e2bbacad62a61f409b5634c915fceb0d799de1a913360" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.1.1" image: dependency: "direct main" description: @@ -574,10 +590,10 @@ packages: dependency: transitive description: name: image_picker_android - sha256: "8faba09ba361d4b246dc0a17cb4289b3324c2b9f6db7b3d457ee69106a86bd32" + sha256: fa8141602fde3f7e2f81dbf043613eb44dfa325fa0bcf93c0f142c9f7a2c193e url: "https://pub.dev" source: hosted - version: "0.8.12+17" + version: "0.8.12+18" image_picker_for_web: dependency: transitive description: @@ -638,26 +654,26 @@ packages: dependency: transitive description: name: io - sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.5" isolate_contactor: dependency: transitive description: name: isolate_contactor - sha256: f1be0a90f91e4309ef37cc45280b2a84e769e848aae378318dd3dd263cfc482a + sha256: "6ba8434ceb58238a1389d6365111a3efe7baa1c68a66f4db6d63d351cf6c3a0f" url: "https://pub.dev" source: hosted - version: "4.2.0" + version: "4.1.0" isolate_manager: dependency: transitive description: name: isolate_manager - sha256: "8fb916c4444fd408f089448f904f083ac3e169ea1789fd4d987b25809af92188" + sha256: "22ed0c25f80ec3b5f21e3a55d060f4650afff33f27c2dff34c0f9409d5759ae5" url: "https://pub.dev" source: hosted - version: "4.3.1" + version: "4.1.5+1" js: dependency: transitive description: @@ -678,10 +694,10 @@ packages: dependency: "direct dev" description: name: json_serializable - sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b + sha256: c2fcb3920cf2b6ae6845954186420fca40bc0a8abcc84903b7801f17d7050d7c url: "https://pub.dev" source: hosted - version: "6.8.0" + version: "6.9.0" launch_at_startup: dependency: "direct main" description: @@ -694,18 +710,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" url: "https://pub.dev" source: hosted - version: "10.0.5" + version: "10.0.7" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.8" leak_tracker_testing: dependency: transitive description: @@ -718,10 +734,10 @@ packages: dependency: transitive description: name: lints - sha256: "3315600f3fb3b135be672bf4a178c55f274bebe368325ae18462c89ac1e3b413" + sha256: "4a16b3f03741e1252fda5de3ce712666d010ba2122f8e912c94f9f7b90e1a4c3" url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "5.1.0" logging: dependency: transitive description: @@ -742,10 +758,10 @@ packages: dependency: transitive description: name: macros - sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" + sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656" url: "https://pub.dev" source: hosted - version: "0.1.2-main.4" + version: "0.1.3-main.0" matcher: dependency: transitive description: @@ -822,26 +838,26 @@ packages: dependency: transitive description: name: package_config - sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" package_info_plus: dependency: "direct main" description: name: package_info_plus - sha256: da8d9ac8c4b1df253d1a328b7bf01ae77ef132833479ab40763334db13b91cce + sha256: "70c421fe9d9cc1a9a7f3b05ae56befd469fe4f8daa3b484823141a55442d858d" url: "https://pub.dev" source: hosted - version: "8.1.1" + version: "8.1.2" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - sha256: ac1f4a4847f1ade8e6a87d1f39f5d7c67490738642e2542f559ec38c37489a66 + sha256: a5ef9986efc7bf772f2696183a3992615baa76c1ffb1189318dd8803778fb05b url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" path: dependency: "direct main" description: @@ -862,18 +878,18 @@ packages: dependency: transitive description: name: path_provider_android - sha256: c464428172cb986b758c6d1724c603097febb8fb855aa265aeecc9280c294d4a + sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" url: "https://pub.dev" source: hosted - version: "2.2.12" + version: "2.2.15" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" path_provider_linux: dependency: transitive description: @@ -934,10 +950,10 @@ packages: dependency: "direct main" description: name: process_run - sha256: "5736140acb1c54a11bd4c1e8d4821bfd684de69a4bf88835316cb05e596d8091" + sha256: a68fa9727392edad97a2a96a77ce8b0c17d28336ba1b284b1dfac9595a4299ea url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.2.2+1" provider: dependency: "direct main" description: @@ -957,10 +973,10 @@ packages: dependency: transitive description: name: pub_semver - sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" + sha256: "7b3cfbf654f3edd0c6298ecd5be782ce997ddf0e00531b9464b55245185bbbbd" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.5" pubspec_parse: dependency: transitive description: @@ -1053,18 +1069,18 @@ packages: dependency: transitive description: name: shared_preferences_android - sha256: "3b9febd815c9ca29c9e3520d50ec32f49157711e143b7a4ca039eb87e8ade5ab" + sha256: "02a7d8a9ef346c9af715811b01fbd8e27845ad2c41148eefd31321471b41863d" url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.4.0" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "07e050c7cd39bad516f8d64c455f04508d09df104be326d8c02551590a0d513d" + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" url: "https://pub.dev" source: hosted - version: "2.5.3" + version: "2.5.4" shared_preferences_linux: dependency: transitive description: @@ -1101,18 +1117,18 @@ packages: dependency: transitive description: name: shelf - sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.2" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" + sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.0.1" shortid: dependency: transitive description: @@ -1125,7 +1141,7 @@ packages: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_gen: dependency: transitive description: @@ -1178,10 +1194,10 @@ packages: dependency: transitive description: name: sqflite_common - sha256: "4468b24876d673418a7b7147e5a08a715b4998a7ae69227acafaab762e0e5490" + sha256: "761b9740ecbd4d3e66b8916d784e581861fd3c3553eda85e167bc49fdb68f709" url: "https://pub.dev" source: hosted - version: "2.5.4+5" + version: "2.5.4+6" sqflite_darwin: dependency: transitive description: @@ -1202,10 +1218,10 @@ packages: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.0" stream_channel: dependency: transitive description: @@ -1226,10 +1242,10 @@ packages: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" synchronized: dependency: transitive description: @@ -1250,18 +1266,18 @@ packages: dependency: transitive description: name: test_api - sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.7.3" timing: dependency: transitive description: name: timing - sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.2" tray_manager: dependency: "direct main" description: @@ -1306,26 +1322,26 @@ packages: dependency: transitive description: name: url_launcher_ios - sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e + sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626" url: "https://pub.dev" source: hosted - version: "6.3.1" + version: "6.3.2" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: e2b9622b4007f97f504cd64c0128309dfb978ae66adbe944125ed9e1750f06af + sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "3.2.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: "769549c999acdb42b8bcfa7c43d72bf79a382ca7441ab18a808e101149daf672" + sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.2.2" url_launcher_platform_interface: dependency: transitive description: @@ -1370,10 +1386,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b url: "https://pub.dev" source: hosted - version: "14.2.5" + version: "14.3.0" watcher: dependency: transitive description: @@ -1386,10 +1402,10 @@ packages: dependency: transitive description: name: web - sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "1.1.0" web_socket: dependency: transitive description: @@ -1418,10 +1434,10 @@ packages: dependency: "direct main" description: name: win32 - sha256: "84ba388638ed7a8cb3445a320c8273136ab2631cd5f2c57888335504ddab1bc2" + sha256: "8b338d4486ab3fbc0ba0db9f9b4f5239b6697fcee427939a40e720cbb9ee0a69" url: "https://pub.dev" source: hosted - version: "5.8.0" + version: "5.9.0" win32_registry: dependency: "direct main" description: @@ -1494,5 +1510,5 @@ packages: source: hosted version: "0.2.3" sdks: - dart: ">=3.5.0 <4.0.0" + dart: ">=3.6.0 <4.0.0" flutter: ">=3.24.0" diff --git a/pubspec.yaml b/pubspec.yaml index 20dee14..03c1a2e 100755 --- 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.70+202412091 +version: 0.8.71+202501091 environment: sdk: '>=3.1.0 <4.0.0' @@ -53,6 +53,7 @@ dependencies: device_info_plus: ^10.1.2 connectivity_plus: ^6.1.0 screen_retriever: ^0.2.0 + defer_pointer: ^0.0.2 dev_dependencies: flutter_test: sdk: flutter diff --git a/setup.dart b/setup.dart index 30951a0..801ce13 100755 --- a/setup.dart +++ b/setup.dart @@ -381,29 +381,31 @@ class BuildCommand extends Command { await Build.exec( Build.getExecutable("sudo apt install -y libayatana-appindicator3-dev"), ); - await Build.exec( - Build.getExecutable("sudo apt install -y rpm patchelf"), - ); await Build.exec( Build.getExecutable("sudo apt-get install -y libkeybinder-3.0-dev"), ); await Build.exec( Build.getExecutable("sudo apt install -y locate"), ); - await Build.exec( - Build.getExecutable("sudo apt install -y libfuse2"), - ); - final downloadName = arch == Arch.amd64 ? "x86_64" : "aarch_64"; - await Build.exec( - Build.getExecutable( - "wget -O appimagetool https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-$downloadName.AppImage", - ), - ); - await Build.exec( - Build.getExecutable( - "chmod +x appimagetool", - ), - ); + if (arch == Arch.amd64) { + await Build.exec( + Build.getExecutable("sudo apt install -y rpm patchelf"), + ); + await Build.exec( + Build.getExecutable("sudo apt install -y libfuse2"), + ); + final downloadName = arch == Arch.amd64 ? "x86_64" : "aarch_64"; + await Build.exec( + Build.getExecutable( + "wget -O appimagetool https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-$downloadName.AppImage", + ), + ); + await Build.exec( + Build.getExecutable( + "chmod +x appimagetool", + ), + ); + } await Build.exec( Build.getExecutable( "sudo mv appimagetool /usr/local/bin/", @@ -481,11 +483,18 @@ class BuildCommand extends Command { Arch.arm64: "linux-arm64", Arch.amd64: "linux-x64", }; + final targets = [ + "deb", + if (arch == Arch.amd64) ...[ + "appimage", + "rpm", + ], + ].join(","); final defaultTarget = targetMap[arch]; await _getLinuxDependencies(arch!); _buildDistributor( target: target, - targets: "appimage,deb", + targets: targets, args: "--description $archName --build-target-platform $defaultTarget", );