Remake dashboard

Optimize theme

Optimize more details

Update flutter version
This commit is contained in:
chen08209
2024-12-09 01:40:39 +08:00
parent 9cb75f4814
commit ef97ef40a1
101 changed files with 4951 additions and 1841 deletions

2
.gitignore vendored
View File

@@ -5,9 +5,11 @@
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
# IntelliJ related

View File

@@ -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))
)

View File

@@ -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"

View File

@@ -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{

View File

@@ -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)

View File

@@ -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
}
}

View File

@@ -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<Application> {
late SystemColorSchemes systemColorSchemes;
Timer? timer;
Timer? _autoUpdateGroupTaskTimer;
Timer? _autoUpdateProfilesTaskTimer;
final _pageTransitionsTheme = const PageTransitionsTheme(
builders: <TargetPlatform, PageTransitionsBuilder>{
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<Application> {
@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<Application> {
});
}
_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<Application> {
}
return AndroidManager(
child: TileManager(
child: app,
child: child,
),
);
}
@@ -156,6 +149,17 @@ class ApplicationState extends State<Application> {
);
}
_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<Application> {
@override
Widget build(context) {
return _buildApp(
AppStateManager(
child: ClashManager(
child: Selector2<AppState, Config, ApplicationSelectorState>(
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<AppState, Config, ApplicationSelectorState>(
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<Application> {
}
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<Application> {
@override
Future<void> dispose() async {
linkManager.destroy();
_cancelTimer();
_autoUpdateGroupTaskTimer?.cancel();
_autoUpdateProfilesTaskTimer?.cancel();
await clashService?.destroy();
await globalState.appController.savePreferences();
await globalState.appController.handleExit();

View File

@@ -200,11 +200,27 @@ class ClashCore {
return Traffic.fromMap(json.decode(trafficString));
}
Future<IpInfo?> getCountryCode(String ip) async {
final countryCode = await clashInterface.getCountryCode(ip);
if (countryCode.isEmpty) {
return null;
}
return IpInfo(
ip: ip,
countryCode: countryCode,
);
}
Future<Traffic> getTotalTraffic(bool value) async {
final totalTrafficString = await clashInterface.getTotalTraffic(value);
return Traffic.fromMap(json.decode(totalTrafficString));
}
Future<int> getMemory() async {
final value = await clashInterface.getMemory();
return int.parse(value);
}
resetTraffic() {
clashInterface.resetTraffic();
}

View File

@@ -2348,20 +2348,6 @@ class ClashFFI {
set suboptarg(ffi.Pointer<ffi.Char> value) => _suboptarg.value = value;
void updateDns(
ffi.Pointer<ffi.Char> s,
) {
return _updateDns(
s,
);
}
late final _updateDnsPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Char>)>>(
'updateDns');
late final _updateDns =
_updateDnsPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
void initNativeApiBridge(
ffi.Pointer<ffi.Void> api,
) {
@@ -2581,6 +2567,18 @@ class ClashFFI {
late final _getConnections =
_getConnectionsPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
void getMemory(
int port,
) {
return _getMemory(
port,
);
}
late final _getMemoryPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.LongLong)>>('getMemory');
late final _getMemory = _getMemoryPtr.asFunction<void Function(int)>();
void closeConnections() {
return _closeConnections();
}
@@ -2665,6 +2663,23 @@ class ClashFFI {
late final _updateExternalProvider = _updateExternalProviderPtr
.asFunction<void Function(ffi.Pointer<ffi.Char>, int)>();
void getCountryCode(
ffi.Pointer<ffi.Char> ipChar,
int port,
) {
return _getCountryCode(
ipChar,
port,
);
}
late final _getCountryCodePtr = _lookup<
ffi.NativeFunction<
ffi.Void Function(
ffi.Pointer<ffi.Char>, ffi.LongLong)>>('getCountryCode');
late final _getCountryCode = _getCountryCodePtr
.asFunction<void Function(ffi.Pointer<ffi.Char>, int)>();
void sideLoadExternalProvider(
ffi.Pointer<ffi.Char> providerNameChar,
ffi.Pointer<ffi.Char> dataChar,
@@ -2793,6 +2808,20 @@ class ClashFFI {
'setState');
late final _setState =
_setStatePtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
void updateDns(
ffi.Pointer<ffi.Char> s,
) {
return _updateDns(
s,
);
}
late final _updateDnsPtr =
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Char>)>>(
'updateDns');
late final _updateDns =
_updateDnsPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
}
final class __mbstate_t extends ffi.Union {

View File

@@ -45,6 +45,10 @@ mixin ClashInterface {
FutureOr<String> getTotalTraffic(bool value);
FutureOr<String> getCountryCode(String ip);
FutureOr<String> getMemory();
resetTraffic();
startLog();

View File

@@ -306,6 +306,39 @@ class ClashLib with ClashInterface {
clashFFI.forceGc();
}
@override
FutureOr<String> getCountryCode(String ip) {
final completer = Completer<String>();
final receiver = ReceivePort();
receiver.listen((message) {
if (!completer.isCompleted) {
completer.complete(message);
receiver.close();
}
});
final ipChar = ip.toNativeUtf8().cast<Char>();
clashFFI.getCountryCode(
ipChar,
receiver.sendPort.nativePort,
);
malloc.free(ipChar);
return completer.future;
}
@override
FutureOr<String> getMemory() {
final completer = Completer<String>();
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) {

View File

@@ -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<String> getCountryCode(String ip) {
return _invoke<String>(
method: ActionMethod.getCountryCode,
data: ip,
);
}
@override
FutureOr<String> getMemory() {
return _invoke<String>(
method: ActionMethod.getMemory,
);
}
}
final clashService = system.isDesktop ? ClashService() : null;

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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<CommonScaffoldState>();
}
Size get appSize{
showNotifier(String text) {
return findAncestorStateOfType<MessageManagerState>()?.message(text);
}
Size get appSize {
return MediaQuery.of(this).size;
}

View File

@@ -1,26 +1,33 @@
import 'dart:async';
class Debouncer {
final Duration delay;
Timer? _timer;
Map<dynamic, Timer> operators = {};
Debouncer({required this.delay});
call(
dynamic tag,
Function func, {
List<dynamic>? 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<dynamic> positionalArguments, [Map<Symbol, dynamic>? namedArguments]) {
_timer?.cancel();
_timer = Timer(delay, () => Function.apply(action, positionalArguments, namedArguments));
cancel(dynamic tag) {
operators[tag]?.cancel();
}
}
Function debounce<F extends Function>(F func,{int milliseconds = 600}) {
Timer? timer;
return ([List<dynamic>? args, Map<Symbol, dynamic>? namedArgs]) {
if (timer != null) {
timer!.cancel();
}
timer = Timer(Duration(milliseconds: milliseconds), () async {
await Function.apply(func, args ?? [], namedArgs);
});
};
}
final debouncer = Debouncer();

View File

@@ -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";
};

View File

@@ -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();

View File

@@ -1,11 +1,251 @@
import 'package:fl_clash/common/common.dart';
import 'package:flutter/material.dart';
class BaseNavigator {
static Future<T?> push<T>(BuildContext context, Widget child) async {
return await Navigator.of(context).push<T>(
MaterialPageRoute(
CommonRoute(
builder: (context) => child,
),
);
}
}
class CommonRoute<T> extends MaterialPageRoute<T> {
CommonRoute({
required super.builder,
});
@override
Duration get transitionDuration => const Duration(milliseconds: 500);
@override
Duration get reverseTransitionDuration => const Duration(milliseconds: 300);
}
final Animatable<Offset> _kRightMiddleTween = Tween<Offset>(
begin: const Offset(1.0, 0.0),
end: Offset.zero,
);
final Animatable<Offset> _kMiddleLeftTween = Tween<Offset>(
begin: Offset.zero,
end: const Offset(-1.0 / 3.0, 0.0),
);
class CommonPageTransitionsBuilder extends PageTransitionsBuilder {
const CommonPageTransitionsBuilder();
@override
Widget buildTransitions<T>(
PageRoute<T> route,
BuildContext context,
Animation<double> animation,
Animation<double> 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<double> primaryRouteAnimation;
final Animation<double> secondaryRouteAnimation;
final BuildContext context;
final bool linearTransition;
static Widget? delegatedTransition(
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
bool allowSnapshotting,
Widget? child) {
final Animation<Offset> 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<CommonPageTransition> createState() => _CommonPageTransitionState();
}
class _CommonPageTransitionState extends State<CommonPageTransition> {
late Animation<Offset> _primaryPositionAnimation;
late Animation<Offset> _secondaryPositionAnimation;
late Animation<Decoration> _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(
<Color>[
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<Color>? _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<Color>? 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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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<String?> getLocalIpAddress() async {
List<NetworkInterface> 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();

View File

@@ -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<String, IpInfo Function(Map<String, dynamic>)> _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<String> _ipInfoSources = [
"https://ipwho.is/?fields=ip&output=csv",
"https://ipinfo.io/ip",
"https://ifconfig.me/ip/",
];
Future<IpInfo?> checkIp({CancelToken? cancelToken}) async {
for (final source in _ipInfoSources.entries) {
for (final source in _ipInfoSources) {
try {
final response = await _dio
.get<Map<String, dynamic>>(source.key, cancelToken: cancelToken)
.get<String>(
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;

View File

@@ -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,
);
}

View File

@@ -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<AppState>();
config = context.read<Config>();
clashConfig = context.read<ClashConfig>();
appFlowingState = context.read<AppFlowingState>();
updateClashConfigDebounce = debounce<Function()>(() async {
await updateClashConfig();
}
updateClashConfigDebounce() {
debouncer.call(DebounceTag.updateClashConfig, updateClashConfig);
}
updateGroupsDebounce() {
debouncer.call(DebounceTag.updateGroups, updateGroups);
}
addCheckIpNumDebounce() {
debouncer.call(DebounceTag.addCheckIpNum, () {
appState.checkIpNum++;
});
savePreferencesDebounce = debounce<Function()>(() async {
await savePreferences();
}
applyProfileDebounce() {
debouncer.call(DebounceTag.addCheckIpNum, () {
applyProfile(isPrue: true);
});
applyProfileDebounce = debounce<Function()>(() 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<void> updateProfile(Profile profile) async {
@@ -148,6 +158,9 @@ class AppController {
config.setProfile(
newProfile.copyWith(isUpdating: false),
);
if (profile.id == config.currentProfile?.id) {
applyProfileDebounce();
}
}
Future<void> 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<bool> showDisclaimer() async {
return await globalState.showCommonDialog<bool>(
dismissible: false,

View File

@@ -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<SupportPlatform> 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];
}
}

View File

@@ -104,11 +104,9 @@ class _AccessFragmentState extends State<AccessFragment> {
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<AccessFragment> {
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,

View File

@@ -343,8 +343,8 @@ class _WebDAVFormDialogState extends State<WebDAVFormDialog> {
@override
void dispose() {
super.dispose();
_obscureController.dispose();
super.dispose();
}
@override

View File

@@ -66,9 +66,6 @@ class _ConnectionsFragmentState extends State<ConnectionsFragment> {
},
icon: const Icon(Icons.search),
),
const SizedBox(
width: 8,
),
IconButton(
onPressed: () async {
clashCore.closeConnections();
@@ -112,11 +109,11 @@ class _ConnectionsFragmentState extends State<ConnectionsFragment> {
@override
void dispose() {
super.dispose();
timer?.cancel();
connectionsNotifier.dispose();
_scrollController.dispose();
timer = null;
super.dispose();
}
@override

View File

@@ -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<DashboardFragment> {
_initFab(bool isCurrent) {
final key = GlobalKey<SuperGridState>();
_initScaffold(bool isCurrent) {
if (!isCurrent) {
return;
}
@@ -30,6 +26,47 @@ class _DashboardFragmentState extends State<DashboardFragment> {
final commonScaffoldState =
context.findAncestorStateOfType<CommonScaffoldState>();
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<DashboardFragment> {
return ActiveBuilder(
label: "dashboard",
builder: (isCurrent, child) {
_initFab(isCurrent);
_initScaffold(isCurrent);
return child!;
},
child: Align(
@@ -47,52 +84,52 @@ class _DashboardFragmentState extends State<DashboardFragment> {
padding: const EdgeInsets.all(16).copyWith(
bottom: 88,
),
child: Selector<AppState, double>(
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<AppState, Config, DashboardState>(
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();
},
);
},
),

View File

@@ -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<IntranetIP> createState() => _IntranetIPState();
}
class _IntranetIPState extends State<IntranetIP> {
final ipNotifier = ValueNotifier<String?>("");
late StreamSubscription subscription;
Future<String> 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<String?> getLocalIpAddress() async {
await Future.delayed(animateDuration);
List<NetworkInterface> 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();
}
}

View File

@@ -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<NetworkDetectionState>(
const NetworkDetectionState(
isTesting: true,
ipInfo: null,
),
);
class NetworkDetection extends StatefulWidget {
const NetworkDetection({super.key});
@override
State<NetworkDetection> createState() => _NetworkDetectionState();
}
class _NetworkDetectionState extends State<NetworkDetection> {
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<AppState, num>(
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<NetworkDetectionState>(
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(),
),
),
),
),
)
],
),
);
},
),
);
}
}

View File

@@ -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<NetworkSpeed> createState() => _NetworkSpeedState();
}
class _NetworkSpeedState extends State<NetworkSpeed> {
List<Point> initPoints = const [Point(0, 0), Point(1, 0)];
List<Point> _getPoints(List<Traffic> traffics) {
List<Point> 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<Traffic> 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<AppFlowingState, List<Traffic>>(
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,
),
),
],
),
)
],
),
);
},
),
);
}
}

View File

@@ -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<ClashConfig, Mode>(
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,
),
),
],
),
),
);
},
);
}
}

View File

@@ -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<ClashConfig, bool>(
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<Config, bool>(
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,
],
),
],
),
);
}
}

View File

@@ -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<AppFlowingState, Traffic>(
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,
),
),
],
),
);
},
),
);
}
}

View File

@@ -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<AppFlowingState, String?>(
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(),
),
),
);
},
),
)
],
),
),
),
);
}
}

View File

@@ -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>(TrafficValue(value: 0));
class MemoryInfo extends StatefulWidget {
const MemoryInfo({super.key});
@override
State<MemoryInfo> createState() => _MemoryInfoState();
}
class _MemoryInfoState extends State<MemoryInfo> {
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),
),
),
],
),
)
],
);
},
),
),
);
}
}

View File

@@ -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<NetworkDetectionState>(
const NetworkDetectionState(
isTesting: true,
ipInfo: null,
),
);
class NetworkDetection extends StatefulWidget {
const NetworkDetection({super.key});
@override
State<NetworkDetection> createState() => _NetworkDetectionState();
}
class _NetworkDetectionState extends State<NetworkDetection> {
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<AppState, num>(
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<NetworkDetectionState>(
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(),
),
),
),
),
),
)
],
),
);
},
),
),
);
}
}

View File

@@ -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<NetworkSpeed> createState() => _NetworkSpeedState();
}
class _NetworkSpeedState extends State<NetworkSpeed> {
List<Point> initPoints = const [Point(0, 0), Point(1, 0)];
List<Point> _getPoints(List<Traffic> traffics) {
List<Point> 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<Traffic> 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<AppFlowingState, List<Traffic>>(
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,
),
),
],
),
),
),
],
);
},
),
),
);
}
}

View File

@@ -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<ClashConfig, Mode>(
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,
),
),
),
],
),
),
);
},
),
);
}
}

View File

@@ -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<ClashConfig, bool>(
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<Config, bool>(
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);
},
);
},
)
],
),
),
),
),
);
}
}

View File

@@ -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<AppFlowingState, Traffic>(
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,
)
],
),
);
},
),
),
);
}
}

View File

@@ -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';

View File

@@ -49,11 +49,11 @@ class _LogsFragmentState extends State<LogsFragment> {
@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<LogsFragment> {
},
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;

View File

@@ -52,9 +52,6 @@ class _ProfilesFragmentState extends State<ProfilesFragment> {
);
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<ProfilesFragment> {
},
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<ProfileActions>(
icon: Icon(Icons.more_vert),
items: [
CommonPopupMenuItem(
action: ProfileActions.edit,

View File

@@ -41,9 +41,9 @@ class _ViewProfileState extends State<ViewProfile> {
@override
void dispose() {
super.dispose();
_controller.dispose();
_focusNode.dispose();
super.dispose();
}
Profile get profile => widget.profile;

View File

@@ -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,
),
),
),

View File

@@ -65,10 +65,10 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
@override
void dispose() {
super.dispose();
_headerStateNotifier.dispose();
_controller.removeListener(_adjustHeader);
_controller.dispose();
super.dispose();
}
_handleChange(Set<String> currentUnfoldSet, String groupName) {
@@ -442,10 +442,10 @@ class _ListHeaderState extends State<ListHeader>
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<ListHeader>
margin: const EdgeInsets.only(
right: 16,
),
child: CommonIcon(
child: CommonTargetIcon(
src: icon,
size: 42,
),
@@ -471,7 +471,10 @@ class _ListHeaderState extends State<ListHeader>
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(

View File

@@ -61,7 +61,7 @@ class _ProvidersState extends State<Providers> {
},
);
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() {

View File

@@ -40,9 +40,6 @@ class _ProxiesFragmentState extends State<ProxiesFragment> {
Icons.poll_outlined,
),
),
const SizedBox(
width: 8,
),
],
if (proxiesType == ProxiesType.tab) ...[
IconButton(
@@ -53,9 +50,6 @@ class _ProxiesFragmentState extends State<ProxiesFragment> {
Icons.adjust_outlined,
),
),
const SizedBox(
width: 8,
)
] else ...[
IconButton(
onPressed: () {
@@ -85,7 +79,7 @@ class _ProxiesFragmentState extends State<ProxiesFragment> {
borderRadius: BorderRadius.circular(16),
),
clipBehavior: Clip.antiAlias,
child: CommonIcon(
child: CommonTargetIcon(
src: item.value,
size: 42,
),
@@ -110,18 +104,13 @@ class _ProxiesFragmentState extends State<ProxiesFragment> {
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(

View File

@@ -30,8 +30,8 @@ class ProxiesTabFragmentState extends State<ProxiesTabFragment>
@override
void dispose() {
super.dispose();
_destroyTabController();
super.dispose();
}
scrollToGroupSelected() {
@@ -62,49 +62,46 @@ class ProxiesTabFragmentState extends State<ProxiesTabFragment>
context: context,
width: 380,
isScrollControlled: false,
builder: (context) {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Selector2<AppState, Config, ProxiesSelectorState>(
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<AppState, Config, ProxiesSelectorState>(
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<ProxyGroupView> {
@override
void dispose() {
super.dispose();
_controller.dispose();
super.dispose();
}
scrollToSelected() {

View File

@@ -95,10 +95,10 @@ class _RequestsFragmentState extends State<RequestsFragment> {
@override
void dispose() {
super.dispose();
timer?.cancel();
_scrollController.dispose();
timer = null;
super.dispose();
}
@override

View File

@@ -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"
}

View File

@@ -332,5 +332,6 @@
"routeAddress": "路由地址",
"routeAddressDesc": "配置监听路由地址",
"pleaseInputAdminPassword": "请输入管理员密码",
"copyEnvVar": "复制环境变量"
"copyEnvVar": "复制环境变量",
"memoryInfo": "内存信息"
}

View File

@@ -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"),

View File

@@ -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":

View File

@@ -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<AppLocalizations> {

View File

@@ -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<ClashManager> with AppMessageListener {
Function? updateDelayDebounce;
Widget _updateContainer(Widget child) {
return Selector2<Config, ClashConfig, ClashConfigState>(
selector: (_, config, clashConfig) => ClashConfigState(
@@ -103,18 +102,21 @@ class _ClashContainerState extends State<ClashManager> 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<ClashManager> with AppMessageListener {
providerName,
),
);
await appController.updateGroupDebounce();
await appController.updateGroupsDebounce();
super.onLoaded(providerName);
}
}

View File

@@ -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<ConnectivityManager> createState() => _ConnectivityManagerState();
}
class _ConnectivityManagerState extends State<ConnectivityManager> {
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;
}
}

View File

@@ -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';
export 'proxy_manager.dart';
export 'connectivity_manager.dart';
export 'message_manager.dart';

View File

@@ -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<MessageManager> createState() => MessageManagerState();
}
class MessageManagerState extends State<MessageManager>
with SingleTickerProviderStateMixin {
final _floatMessageKey = GlobalKey();
List<CommonMessage> bufferMessages = [];
final _messagesNotifier = ValueNotifier<List<CommonMessage>>([]);
final _floatMessageNotifier = ValueNotifier<CommonMessage?>(null);
double maxWidth = 0;
late AnimationController _animationController;
Completer? _animationCompleter;
late Animation<Offset> _floatOffsetAnimation;
late Animation<Offset> _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<CommonMessage>.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,
);
}
}

View File

@@ -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<VpnManager> {
Function? vpnTipDebounce;
showTip() {
vpnTipDebounce ??= debounce<Function()>(() 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

View File

@@ -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<WindowManager>
Function? updateLaunchDebounce;
_autoLaunchContainer(Widget child) {
return Selector<Config, AutoLaunchState>(
selector: (_, config) => AutoLaunchState(
isAutoLaunch: config.appSetting.autoLaunch,
),
return Selector<Config, bool>(
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<WindowHeader> {
@override
void dispose() {
super.dispose();
isMaximizedNotifier.dispose();
isPinNotifier.dispose();
super.dispose();
}
_updateMaximized() {
@@ -261,7 +267,7 @@ class _WindowHeaderState extends State<WindowHeader> {
_updateMaximized();
},
child: Container(
color: context.colorScheme.secondary.toSoft(),
color: context.colorScheme.secondary.toSoft,
alignment: Alignment.centerLeft,
height: kHeaderHeight,
),

View File

@@ -306,6 +306,7 @@ class AppFlowingState with ChangeNotifier {
List<Log> _logs;
List<Traffic> _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();
}
}
}

View File

@@ -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);
}
}

View File

@@ -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<DashboardWidget> defaultDashboardWidgets = [
DashboardWidget.networkSpeed,
DashboardWidget.systemProxyButton,
DashboardWidget.tunButton,
DashboardWidget.outboundMode,
DashboardWidget.networkDetection,
DashboardWidget.trafficUsage,
DashboardWidget.intranetIp,
];
List<DashboardWidget> dashboardWidgetsRealFormJson(
List<dynamic>? 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<DashboardWidget> dashboardWidgets,
@Default(false) bool onlyProxy,
@Default(false) bool autoLaunch,
@Default(false) bool silentLaunch,

View File

@@ -21,6 +21,9 @@ AppSetting _$AppSettingFromJson(Map<String, dynamic> json) {
/// @nodoc
mixin _$AppSetting {
String? get locale => throw _privateConstructorUsedError;
@JsonKey(fromJson: dashboardWidgetsRealFormJson)
List<DashboardWidget> 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<DashboardWidget> 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<DashboardWidget>,
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<DashboardWidget> 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<DashboardWidget>,
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<DashboardWidget> 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<String, dynamic> json) =>
_$$AppSettingImplFromJson(json);
@override
final String? locale;
final List<DashboardWidget> _dashboardWidgets;
@override
@JsonKey(fromJson: dashboardWidgetsRealFormJson)
List<DashboardWidget> 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<DashboardWidget> 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<DashboardWidget> get dashboardWidgets;
@override
bool get onlyProxy;
@override
bool get autoLaunch;

View File

@@ -54,6 +54,9 @@ Map<String, dynamic> _$ConfigToJson(Config instance) => <String, dynamic>{
_$AppSettingImpl _$$AppSettingImplFromJson(Map<String, dynamic> 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<String, dynamic> json) =>
Map<String, dynamic> _$$AppSettingImplToJson(_$AppSettingImpl instance) =>
<String, dynamic>{
'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<String, dynamic> _$$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<String, dynamic> json) =>
_$AccessControlImpl(
mode: $enumDecodeNullable(_$AccessControlModeEnumMap, json['mode']) ??

View File

@@ -329,4 +329,6 @@ const _$ActionMethodEnumMap = {
ActionMethod.stopLog: 'stopLog',
ActionMethod.startListener: 'startListener',
ActionMethod.stopListener: 'stopListener',
ActionMethod.getCountryCode: 'getCountryCode',
ActionMethod.getMemory: 'getMemory',
};

View File

@@ -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<AutoLaunchState> 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<DashboardWidget> 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<DashboardState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $DashboardStateCopyWith<$Res> {
factory $DashboardStateCopyWith(
DashboardState value, $Res Function(DashboardState) then) =
_$DashboardStateCopyWithImpl<$Res, DashboardState>;
@useResult
$Res call({List<DashboardWidget> 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<DashboardWidget>,
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<DashboardWidget> 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<DashboardWidget>,
viewWidth: null == viewWidth
? _value.viewWidth
: viewWidth // ignore: cast_nullable_to_non_nullable
as double,
));
}
}
/// @nodoc
class _$DashboardStateImpl implements _DashboardState {
const _$DashboardStateImpl(
{required final List<DashboardWidget> dashboardWidgets,
required this.viewWidth})
: _dashboardWidgets = dashboardWidgets;
final List<DashboardWidget> _dashboardWidgets;
@override
List<DashboardWidget> 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<DashboardWidget> dashboardWidgets,
required final double viewWidth}) = _$DashboardStateImpl;
@override
List<DashboardWidget> 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;

View File

@@ -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>(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<ActivateState> 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<CommonMessage> 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;
}

View File

@@ -5,3 +5,4 @@ export 'config.dart';
export 'core.dart';
export 'profile.dart';
export 'selector.dart';
export 'widget.dart';

View File

@@ -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<DashboardWidget> dashboardWidgets,
required double viewWidth,
}) = _DashboardState;
}
@freezed
class VPNState with _$VPNState {
const factory VPNState({

19
lib/models/widget.dart Normal file
View File

@@ -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;
}

View File

@@ -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<NavigationItem> 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<Config, bool>(
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<NavigationItem> 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<NavigationItem> 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<Config, bool>(
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),
)
],
),
),
);
}
}

View File

@@ -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<ScanPage> 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<ScanPage> with WidgetsBindingObserver {
WidgetsBinding.instance.removeObserver(this);
unawaited(_subscription?.cancel());
_subscription = null;
super.dispose();
await controller.dispose();
super.dispose();
}
}

View File

@@ -49,7 +49,7 @@ class App {
return Isolate.run<List<Package>>(() {
final List<dynamic> packagesRaw =
packagesString != null ? json.decode(packagesString) : [];
return packagesRaw.map((e) => Package.fromJson(e)).toList();
return packagesRaw.map((e) => Package.fromJson(e)).toSet().toList();
});
}

View File

@@ -24,6 +24,7 @@ class GlobalState {
PageController? pageController;
late Measure measure;
DateTime? startTime;
final safeMessageOffsetNotifier = ValueNotifier(Offset.zero);
final navigatorKey = GlobalKey<NavigatorState>();
late AppController appController;
GlobalKey<CommonScaffoldState> 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<T?> safeRun<T>(
FutureOr<T> 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),

View File

@@ -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,
);
}
}

148
lib/widgets/bar_chart.dart Normal file
View File

@@ -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<BarChartData> data;
final Duration duration;
const BarChart({
super.key,
required this.data,
this.duration = commonDuration,
});
@override
State<BarChart> createState() => _BarChartState();
}
class _BarChartState extends State<BarChart>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late List<BarChartData> _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<BarChartData> oldData;
final List<BarChartData> newData;
final double progress;
BarChartPainter(this.oldData, this.newData, this.progress);
Map<String, Rect> getRectMap(List<BarChartData> 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 = <String, Rect>{};
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;
}
}

View File

@@ -19,8 +19,8 @@ class _ScrollOverBuilderState extends State<ScrollOverBuilder> {
@override
void dispose() {
super.dispose();
isOverNotifier.dispose();
super.dispose();
}
@override
@@ -115,3 +115,22 @@ class ActiveBuilder extends StatelessWidget {
);
}
}
class ThemeModeBuilder extends StatelessWidget {
final StateWidgetBuilder<ThemeMode> builder;
const ThemeModeBuilder({
super.key,
required this.builder,
});
@override
Widget build(BuildContext context) {
return Selector<Config, ThemeMode>(
selector: (_, config) => config.themeProps.themeMode,
builder: (_, state, __) {
return builder(state);
},
);
}
}

View File

@@ -17,17 +17,19 @@ class Info {
class InfoHeader extends StatelessWidget {
final Info info;
final List<Widget> actions;
final EdgeInsetsGeometry? padding;
const InfoHeader({
super.key,
required this.info,
this.padding,
List<Widget>? 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<Color?>? backgroundColor;
BorderSide getBorderSide(BuildContext context, Set<WidgetState> 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<WidgetState> 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<Widget> 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),
),

View File

@@ -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<DonutChartData> data;
final Duration duration;
const DonutChart({
super.key,
required this.data,
this.duration = commonDuration,
});
@override
State<DonutChart> createState() => _DonutChartState();
}
class _DonutChartState extends State<DonutChart>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late List<DonutChartData> _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<DonutChartData> oldData;
final List<DonutChartData> 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<DonutChartData> 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<double>(
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;
}
}

View File

@@ -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<GridParentData> {
@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<double> 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<double> 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<double> 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;
}
}

View File

@@ -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,

View File

@@ -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<Point> 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<LineChart> createState() => _LineChartState();
}
typedef ComputedPath = Path Function(Size size);
class _LineChartState extends State<LineChart>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
double progress = 0;
List<Point> prevPoints = [];
List<Point> nextPoints = [];
List<Point> points = [];
@override
@@ -59,41 +48,77 @@ class _LineChartState extends State<LineChart>
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<Point> prevPoints;
final List<Point> 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<Point> getRenderPoints(List<Point> 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<LineChart>
List<Point> 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<LineChart>
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<Point> prevPoints,
required List<Point> 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;
}
}

View File

@@ -253,6 +253,7 @@ class ListItem<T> extends StatelessWidget {
splashFactory: NoSplash.splashFactory,
onTap: onTap,
child: Container(
constraints: BoxConstraints.expand(),
padding: padding,
child: Row(
children: children,

View File

@@ -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<CommonScaffold> {
final ValueNotifier<List<Widget>> _actions = ValueNotifier([]);
final ValueNotifier<dynamic> _floatingActionButton = ValueNotifier(null);
final ValueNotifier<Widget?> _floatingActionButton = ValueNotifier(null);
final ValueNotifier<bool> _loading = ValueNotifier(false);
set actions(List<Widget> actions) {
@@ -108,68 +109,70 @@ class CommonScaffoldState extends State<CommonScaffold> {
@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<List<Widget>>(
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<List<Widget>>(
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<Widget?>(
valueListenable: _floatingActionButton,
builder: (_, value, __) {
return value ?? Container();
},
),
bottomNavigationBar: widget.bottomNavigationBar,
);
return _sideNavigationBar != null
? Row(

View File

@@ -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,
);

View File

@@ -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,

889
lib/widgets/super_grid.dart Normal file
View File

@@ -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<GridItem> children;
final double mainAxisSpacing;
final double crossAxisSpacing;
final int crossAxisCount;
final void Function(List<GridItem> newChildren)? onSave;
final List<GridItem> Function(List<GridItem> newChildren)? addedItemsBuilder;
const SuperGrid({
super.key,
required this.children,
this.crossAxisCount = 1,
this.mainAxisSpacing = 0,
this.crossAxisSpacing = 0,
this.onSave,
this.addedItemsBuilder,
});
@override
State<SuperGrid> createState() => SuperGridState();
}
class SuperGridState extends State<SuperGrid> with TickerProviderStateMixin {
final ValueNotifier<List<GridItem>> _childrenNotifier = ValueNotifier([]);
final ValueNotifier<List<GridItem>> addedChildrenNotifier = ValueNotifier([]);
int get length => _childrenNotifier.value.length;
List<int> _tempIndexList = [];
List<BuildContext?> _itemContexts = [];
Size _containerSize = Size.zero;
int _targetIndex = -1;
Offset _targetOffset = Offset.zero;
List<Size> _sizes = [];
List<Offset> _offsets = [];
Offset _parentOffset = Offset.zero;
EdgeDraggingAutoScroller? _edgeDraggingAutoScroller;
final ValueNotifier<bool> isEditNotifier = ValueNotifier(false);
Map<int, Tween<Offset>> _transformTweenMap = {};
final ValueNotifier<bool> _animating = ValueNotifier(false);
final _dragWidgetSizeNotifier = ValueNotifier(Size.zero);
final _dragIndexNotifier = ValueNotifier(-1);
late AnimationController _transformController;
Completer? _transformCompleter;
Map<int, Animation<Offset>> _transformAnimationMap = {};
late AnimationController _fakeDragWidgetController;
Animation<Offset>? _fakeDragWidgetAnimation;
late AnimationController _shakeController;
late Animation<double> _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<double>(
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<Scrollable>();
if (scrollable == null) {
return;
}
if (_scrollable != scrollable) {
_edgeDraggingAutoScroller = EdgeDraggingAutoScroller(
Scrollable.of(context),
onScrollViewScrolled: () {
_edgeDraggingAutoScroller?.startAutoScrollIfNecessary(_dragRect);
},
velocityScalar: 40,
);
}
}
_transform() async {
List<Offset> layoutOffsets = [
Offset(_containerSize.width, 0),
];
final List<Offset> 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<int, Tween<Offset>> 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<GridItem>.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<GridItem>.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<Offset> 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<int>(
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<bool>(
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<GridItem> 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<double> _scaleAnimation;
late Animation<double> _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,
),
),
),
),
)
],
);
}
}

View File

@@ -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,
),

108
lib/widgets/wave.dart Normal file
View File

@@ -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<WaveView> createState() => _WaveViewState();
}
class _WaveViewState extends State<WaveView>
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;
}
}

View File

@@ -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';

View File

@@ -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

View File

@@ -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 {

View File

@@ -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"

View File

@@ -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

Some files were not shown because too many files have changed in this diff Show More