Compare commits

..

4 Commits

Author SHA1 Message Date
chen08209
6de89d7de4 Optimize proxies page
Fix ua issues

Optimize more details
2024-07-25 13:57:37 +08:00
chen08209
c36df8cb4a Fix windows build error 2024-07-22 15:49:59 +08:00
chen08209
530817b268 Update app icon
Fix desktop backup error
2024-07-22 15:05:05 +08:00
chen08209
721dd20251 Optimize request ua
Change android icon

Optimize dashboard
2024-07-20 18:05:49 +08:00
103 changed files with 2223 additions and 896 deletions

View File

@@ -24,8 +24,9 @@
android:icon="@mipmap/ic_launcher"
android:networkSecurityConfig="@xml/network_security_config"
android:extractNativeLibs="true"
android:enableOnBackInvokedCallback="true"
android:label="FlClash"
tools:targetApi="n">
tools:targetApi="tiramisu">
<activity
android:name="com.follow.clash.MainActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
@@ -74,7 +75,7 @@
<service
android:name=".services.FlClashTileService"
android:exported="true"
android:icon="@drawable/icon"
android:icon="@drawable/ic_stat_name"
android:foregroundServiceType="specialUse"
android:label="FlClash"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -1,6 +1,7 @@
package com.follow.clash
import android.content.Context
import android.util.Log
import androidx.lifecycle.MutableLiveData
import com.follow.clash.plugins.AppPlugin
import com.follow.clash.plugins.ProxyPlugin
@@ -56,6 +57,8 @@ object GlobalState {
serviceEngine?.dartExecutor?.executeDartEntrypoint(
vpnService,
)
Log.e("FlClashVpnService", "initServiceEngine ===>")
}
}
}

View File

@@ -10,6 +10,7 @@ import android.content.pm.PackageManager
import android.net.VpnService
import android.os.Build
import android.os.IBinder
import android.util.Log
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.follow.clash.GlobalState
@@ -131,7 +132,13 @@ class ProxyPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAwar
}
if (GlobalState.runState.value == RunState.START) return
GlobalState.runState.value = RunState.START
flutterMethodChannel.invokeMethod("started", flClashVpnService?.start(port, props))
val intent = VpnService.prepare(context)
if (intent != null) {
stopVpn()
return
}
val fd = flClashVpnService?.start(port, props)
flutterMethodChannel.invokeMethod("started", fd)
}
private fun stopVpn() {

View File

@@ -13,6 +13,7 @@ import android.os.Build
import android.os.IBinder
import android.os.Parcel
import android.os.RemoteException
import android.util.Log
import androidx.core.app.NotificationCompat
import com.follow.clash.GlobalState
import com.follow.clash.MainActivity

Binary file not shown.

Before

Width:  |  Height:  |  Size: 763 B

After

Width:  |  Height:  |  Size: 618 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 520 B

After

Width:  |  Height:  |  Size: 423 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 803 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -0,0 +1,25 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="240"
android:viewportHeight="240">
<group android:scaleX="0.924"
android:scaleY="0.924"
android:translateX="9.12"
android:translateY="9.12">
<group android:scaleX="0.63461536"
android:scaleY="0.63461536"
android:translateX="45.96154"
android:translateY="43.846153">
<path
android:pathData="M60.65,89.6L154.18,35.6A18,18 107.59,0 1,178.77 42.19L178.77,42.19A18,18 107.59,0 1,172.18 66.78L78.65,120.78A18,18 106.67,0 1,54.06 114.19L54.06,114.19A18,18 106.67,0 1,60.65 89.6z"
android:fillColor="#6666FB"/>
<path
android:pathData="M84.65,131.17L131.42,104.17A18,18 107.83,0 1,156 110.76L156,110.76A18,18 107.83,0 1,149.42 135.35L102.65,162.35A18,18 106.67,0 1,78.06 155.76L78.06,155.76A18,18 106.67,0 1,84.65 131.17z"
android:fillColor="#336AB6"/>
<path
android:pathData="M108.65,172.74L108.65,172.74A18,18 116.03,0 1,133.24 179.33L133.24,179.33A18,18 116.03,0 1,126.65 203.92L126.65,203.92A18,18 116.03,0 1,102.06 197.33L102.06,197.33A18,18 116.03,0 1,108.65 172.74z"
android:fillColor="#5CA8E9"/>
</group>
</group>
</vector>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<monochrome android:drawable="@mipmap/ic_launcher_foreground" />
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<monochrome android:drawable="@mipmap/ic_launcher_foreground" />
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 886 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@@ -6,7 +6,7 @@
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
<item name="android:windowSplashScreenBackground" tools:targetApi="s">#121212</item>
<item name="android:windowSplashScreenAnimatedIcon" tools:targetApi="s">@mipmap/ic_launcher_foreground</item>
<item name="android:windowSplashScreenAnimatedIcon" tools:targetApi="s">@drawable/ic_launcher_foreground</item>
<item name="postSplashScreenTheme">@style/NormalTheme</item>
</style>
</resources>

View File

@@ -6,7 +6,7 @@
<item name="android:windowDrawsSystemBarBackgrounds">false</item>
<item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
<item name="android:windowSplashScreenBackground" tools:targetApi="s">@color/ic_launcher_background</item>
<item name="android:windowSplashScreenAnimatedIcon" tools:targetApi="s">@mipmap/ic_launcher_foreground</item>
<item name="android:windowSplashScreenAnimatedIcon" tools:targetApi="s">@drawable/ic_launcher_foreground</item>
<item name="postSplashScreenTheme">@style/NormalTheme</item>
</style>
</resources>

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#EFEFEF</color>
<color name="ic_launcher_background">#FAFAFA</color>
</resources>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
assets/images/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

BIN
assets/images/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

View File

@@ -423,6 +423,7 @@ func applyConfig() {
if configParams.IsPatch {
patchConfig(cfg.General)
} else {
closeConnections()
runtime.GC()
hub.UltraApplyConfig(cfg, true)
patchSelectGroup()

View File

@@ -299,7 +299,7 @@ func getConnections() *C.char {
}
//export closeConnections
func closeConnections() bool {
func closeConnections() {
statistic.DefaultManager.Range(func(c statistic.Tracker) bool {
err := c.Close()
if err != nil {
@@ -307,17 +307,16 @@ func closeConnections() bool {
}
return true
})
return true
}
//export closeConnection
func closeConnection(id *C.char) bool {
func closeConnection(id *C.char) {
connectionId := C.GoString(id)
err := statistic.DefaultManager.Get(connectionId).Close()
if err != nil {
return false
c := statistic.DefaultManager.Get(connectionId)
if c == nil {
return
}
return true
_ = c.Close()
}
//export getProviders

View File

@@ -137,56 +137,57 @@ class ApplicationState extends State<Application> {
Widget build(context) {
return AppStateContainer(
child: ClashMessageContainer(
child: _buildApp(
Selector2<AppState, Config, ApplicationSelectorState>(
selector: (_, appState, config) => ApplicationSelectorState(
locale: config.locale,
themeMode: config.themeMode,
primaryColor: config.primaryColor,
),
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
],
scrollBehavior: BaseScrollBehavior(),
title: appName,
locale: other.getLocaleForString(state.locale),
supportedLocales:
AppLocalizations.delegate.supportedLocales,
themeMode: state.themeMode,
theme: ThemeData(
useMaterial3: true,
pageTransitionsTheme: _pageTransitionsTheme,
colorScheme: _getAppColorScheme(
brightness: Brightness.light,
systemColorSchemes: systemColorSchemes,
primaryColor: state.primaryColor,
),
),
darkTheme: ThemeData(
useMaterial3: true,
pageTransitionsTheme: _pageTransitionsTheme,
colorScheme: _getAppColorScheme(
brightness: Brightness.dark,
systemColorSchemes: systemColorSchemes,
primaryColor: state.primaryColor,
),
),
home: child,
);
},
);
},
child: const HomePage(),
child: Selector2<AppState, Config, ApplicationSelectorState>(
selector: (_, appState, config) => ApplicationSelectorState(
locale: config.locale,
themeMode: config.themeMode,
primaryColor: config.primaryColor,
),
builder: (_, state, child) {
return DynamicColorBuilder(
builder: (lightDynamic, darkDynamic) {
_updateSystemColorSchemes(lightDynamic, darkDynamic);
return MaterialApp(
debugShowCheckedModeBanner: false,
navigatorKey: globalState.navigatorKey,
localizationsDelegates: const [
AppLocalizations.delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate
],
builder: (_, child) {
return _buildApp(child!);
},
scrollBehavior: BaseScrollBehavior(),
title: appName,
locale: other.getLocaleForString(state.locale),
supportedLocales: AppLocalizations.delegate.supportedLocales,
themeMode: state.themeMode,
theme: ThemeData(
useMaterial3: true,
pageTransitionsTheme: _pageTransitionsTheme,
colorScheme: _getAppColorScheme(
brightness: Brightness.light,
systemColorSchemes: systemColorSchemes,
primaryColor: state.primaryColor,
),
),
darkTheme: ThemeData(
useMaterial3: true,
pageTransitionsTheme: _pageTransitionsTheme,
colorScheme: _getAppColorScheme(
brightness: Brightness.dark,
systemColorSchemes: systemColorSchemes,
primaryColor: state.primaryColor,
),
),
home: child,
);
},
);
},
child: const HomePage(),
),
),
);

View File

@@ -319,11 +319,15 @@ class ClashCore {
return connectionsRaw.map((e) => Connection.fromJson(e)).toList();
}
closeConnections(String id) {
closeConnection(String id) {
final idChar = id.toNativeUtf8().cast<Char>();
clashFFI.closeConnection(idChar);
malloc.free(idChar);
}
closeConnections() {
clashFFI.closeConnections();
}
}
final clashCore = ClashCore();

View File

@@ -5351,16 +5351,16 @@ class ClashFFI {
late final _getConnections =
_getConnectionsPtr.asFunction<ffi.Pointer<ffi.Char> Function()>();
int closeConnections() {
void closeConnections() {
return _closeConnections();
}
late final _closeConnectionsPtr =
_lookup<ffi.NativeFunction<GoUint8 Function()>>('closeConnections');
_lookup<ffi.NativeFunction<ffi.Void Function()>>('closeConnections');
late final _closeConnections =
_closeConnectionsPtr.asFunction<int Function()>();
_closeConnectionsPtr.asFunction<void Function()>();
int closeConnection(
void closeConnection(
ffi.Pointer<ffi.Char> id,
) {
return _closeConnection(
@@ -5369,10 +5369,10 @@ class ClashFFI {
}
late final _closeConnectionPtr =
_lookup<ffi.NativeFunction<GoUint8 Function(ffi.Pointer<ffi.Char>)>>(
_lookup<ffi.NativeFunction<ffi.Void Function(ffi.Pointer<ffi.Char>)>>(
'closeConnection');
late final _closeConnection =
_closeConnectionPtr.asFunction<int Function(ffi.Pointer<ffi.Char>)>();
_closeConnectionPtr.asFunction<void Function(ffi.Pointer<ffi.Char>)>();
ffi.Pointer<ffi.Char> getProviders() {
return _getProviders();

View File

@@ -1,8 +1,10 @@
import 'dart:io';
import 'dart:ui';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/clash_config.dart';
import 'package:flutter/material.dart';
import 'system.dart';
const appName = "FlClash";
const coreName = "clash.meta";
@@ -15,6 +17,7 @@ const mmdbFileName = "geoip.metadb";
const asnFileName = "ASN.mmdb";
const geoIpFileName = "GeoIP.dat";
const geoSiteFileName = "GeoSite.dat";
final double kHeaderHeight = system.isDesktop ? (Platform.isMacOS ? 28 : 40) : 0;
const GeoXMap defaultGeoXMap = {
"mmdb":
"https://github.com/MetaCubeX/meta-rules-dat/releases/download/latest/geoip.metadb",

View File

@@ -10,4 +10,58 @@ extension IterableExt<T> on Iterable<T> {
yield iterator.current;
}
}
Iterable<List<T>> chunks(int size) sync* {
if (length == 0) return;
var iterator = this.iterator;
while (iterator.moveNext()) {
var chunk = [iterator.current];
for (var i = 1; i < size && iterator.moveNext(); i++) {
chunk.add(iterator.current);
}
yield chunk;
}
}
Iterable<T> fill(
int length, {
required T Function(int count) filler,
}) sync* {
int count = 0;
for (var item in this) {
yield item;
count++;
if (count >= length) return;
}
while (count < length) {
yield filler(count);
count++;
}
}
}
extension DoubleListExt on List<double> {
int findInterval(num target) {
if (isEmpty) return -1;
if (target < first) return -1;
if (target >= last) return length - 1;
int left = 0;
int right = length - 1;
while (left <= right) {
int mid = left + (right - left) ~/ 2;
if (mid == length - 1 ||
(this[mid] <= target && target < this[mid + 1])) {
return mid;
} else if (target < this[mid]) {
right = mid - 1;
} else {
left = mid + 1;
}
}
return -1; // 这行理论上不会执行到,但为了完整性保留
}
}

View File

@@ -101,9 +101,9 @@ class Other {
String getTrayIconPath() {
if (Platform.isWindows) {
return "assets/images/app_icon.ico";
return "assets/images/icon.ico";
} else {
return "assets/images/launch_icon.png";
return "assets/images/icon_monochrome.png";
}
}

View File

@@ -36,6 +36,7 @@ class Request {
createHttpClient: () {
final client = HttpClient();
if (!_isStart) return client;
client.userAgent = globalState.appController.clashConfig.globalUa;
client.findProxy = (url) {
return "PROXY localhost:$_port;DIRECT";
};

View File

@@ -14,3 +14,14 @@ class BaseScrollBehavior extends MaterialScrollBehavior {
PointerDeviceKind.unknown,
};
}
class HiddenBarScrollBehavior extends BaseScrollBehavior {
@override
Widget buildScrollbar(
BuildContext context,
Widget child,
ScrollableDetails details,
) {
return child;
}
}

View File

@@ -19,7 +19,8 @@ class Window {
await windowManager.ensureInitialized();
WindowOptions windowOptions = WindowOptions(
size: Size(props.width, props.height),
minimumSize: const Size(380, 600),
minimumSize: const Size(380, 500),
titleBarStyle: TitleBarStyle.hidden,
);
if (props.left != null || props.top != null) {
await windowManager.setPosition(
@@ -28,6 +29,9 @@ class Window {
} else {
await windowManager.setAlignment(Alignment.center);
}
// if(Platform.isWindows){
// await windowManager.setTitleBarStyle(TitleBarStyle.hidden);
// }
await windowManager.waitUntilReadyToShow(windowOptions, () async {
await windowManager.setPreventClose(true);
});

View File

@@ -90,7 +90,7 @@ class AppController {
final updateId = config.profiles.first.id;
changeProfile(updateId);
} else {
changeProfile(null);
updateSystemProxy(false);
}
}
}
@@ -193,6 +193,7 @@ class AppController {
}
handleBackOrExit() async {
print(config.isMinimizeOnExit);
if (config.isMinimizeOnExit) {
if (system.isDesktop) {
await savePreferences();
@@ -410,8 +411,7 @@ class AppController {
addProfileFormURL(url);
}
int get columns =>
other.getColumns(appState.viewMode, config.proxiesColumns);
int get columns => other.getColumns(appState.viewMode, config.proxiesColumns);
updateViewWidth(double width) {
WidgetsBinding.instance.addPostFrameCallback((_) {
@@ -453,4 +453,9 @@ class AppController {
ProxiesSortType.name => _sortOfName(proxies),
};
}
String getCurrentSelectedName(String groupName) {
final group = appState.getGroupWithName(groupName);
return config.currentSelectedMap[groupName] ?? group?.now ?? '';
}
}

View File

@@ -1,7 +1,20 @@
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/state.dart';
import 'package:fl_clash/widgets/list.dart';
import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';
@immutable
class Contributor {
final String avatar;
final String name;
final String link;
const Contributor({
required this.avatar,
required this.name,
required this.link,
});
}
class AboutFragment extends StatelessWidget {
const AboutFragment({super.key});
@@ -9,8 +22,7 @@ class AboutFragment extends StatelessWidget {
_checkUpdate(BuildContext context) async {
final commonScaffoldState = context.commonScaffoldState;
if (commonScaffoldState?.mounted != true) return;
final data =
await commonScaffoldState?.loadingRun<Map<String, dynamic>?>(
final data = await commonScaffoldState?.loadingRun<Map<String, dynamic>?>(
request.checkForUpdate,
title: appLocalizations.checkUpdate,
);
@@ -20,84 +32,40 @@ class AboutFragment extends StatelessWidget {
);
}
@override
Widget build(BuildContext context) {
return ListView(
padding: kMaterialListPadding.copyWith(
top: 16,
bottom: 16,
),
children: [
ListTile(
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Wrap(
spacing: 16,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
Image.asset(
'assets/images/launch_icon.png',
width: 100,
height: 100,
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
appName,
style: Theme.of(context).textTheme.headlineSmall,
),
Text(
globalState.packageInfo.version,
style: Theme.of(context).textTheme.labelLarge,
)
],
)
],
),
const SizedBox(
height: 24,
),
Text(
appLocalizations.desc,
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
const SizedBox(
height: 12,
),
ListTile(
List<Widget> _buildMoreSection(BuildContext context) {
return generateSection(
separated: false,
title: appLocalizations.more,
items: [
ListItem(
title: Text(appLocalizations.checkUpdate),
onTap: () {
_checkUpdate(context);
},
),
ListTile(
ListItem(
title: const Text("Telegram"),
onTap: () {
launchUrl(
Uri.parse("https://t.me/+G-veVtwBOl4wODc1"),
globalState.openUrl(
"https://t.me/+G-veVtwBOl4wODc1",
);
},
trailing: const Icon(Icons.launch),
),
ListTile(
ListItem(
title: Text(appLocalizations.project),
onTap: () {
launchUrl(
Uri.parse("https://github.com/$repository"),
globalState.openUrl(
"https://github.com/$repository",
);
},
trailing: const Icon(Icons.launch),
),
ListTile(
ListItem(
title: Text(appLocalizations.core),
onTap: () {
launchUrl(
Uri.parse("https://github.com/chen08209/Clash.Meta/tree/FlClash"),
globalState.openUrl(
"https://github.com/chen08209/Clash.Meta/tree/FlClash",
);
},
trailing: const Icon(Icons.launch),
@@ -105,4 +73,139 @@ class AboutFragment extends StatelessWidget {
],
);
}
List<Widget> _buildContributorsSection() {
const contributors = [
Contributor(
avatar: "assets/images/avatars/june2.jpg",
name: "June2",
link: "https://t.me/Jibadong",
),
Contributor(
avatar: "assets/images/avatars/arue.jpg",
name: "Arue",
link: "https://t.me/xrcm6868",
),
];
return generateSection(
separated: false,
title: appLocalizations.otherContributors,
items: [
ListItem(
title: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Wrap(
spacing: 24,
children: [
for (final contributor in contributors)
Avatar(
contributor: contributor,
),
],
),
),
)
],
);
}
@override
Widget build(BuildContext context) {
final items = [
ListTile(
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Wrap(
spacing: 16,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
Padding(
padding: const EdgeInsets.all(12),
child: Image.asset(
'assets/images/icon.png',
width: 64,
height: 64,
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
appName,
style: Theme.of(context).textTheme.headlineSmall,
),
Text(
globalState.packageInfo.version,
style: Theme.of(context).textTheme.labelLarge,
)
],
)
],
),
const SizedBox(
height: 24,
),
Text(
appLocalizations.desc,
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
const SizedBox(
height: 12,
),
..._buildContributorsSection(),
..._buildMoreSection(context),
];
return Padding(
padding: kMaterialListPadding.copyWith(
top: 16,
bottom: 16,
),
child: generateListView(items),
);
}
}
class Avatar extends StatelessWidget {
final Contributor contributor;
const Avatar({
super.key,
required this.contributor,
});
@override
Widget build(BuildContext context) {
return InkWell(
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
hoverColor: Colors.transparent,
child: Column(
children: [
SizedBox(
width: 36,
height: 36,
child: CircleAvatar(
foregroundImage: AssetImage(
contributor.avatar,
),
),
),
const SizedBox(
height: 4,
),
Text(
contributor.name,
style: context.textTheme.bodySmall,
)
],
),
onTap: () {
globalState.openUrl(contributor.link);
},
);
}
}

View File

@@ -113,8 +113,7 @@ class _BackupAndRecoveryState extends State<BackupAndRecovery> {
return Center(
child: FadeBox(
key: const Key("fade_box_1"),
child: snapshot.connectionState ==
ConnectionState.waiting
child: snapshot.connectionState == ConnectionState.waiting
? const SizedBox(
width: 12,
height: 12,
@@ -159,12 +158,12 @@ class _BackupAndRecoveryState extends State<BackupAndRecovery> {
ListHeader(
title: appLocalizations.backupAndRecovery),
ListItem(
onTab: _backup,
onTap: _backup,
title: Text(appLocalizations.backup),
subtitle: Text(appLocalizations.backupDesc),
),
ListItem(
onTab: _handleRecovery,
onTap: _handleRecovery,
title: Text(appLocalizations.recovery),
subtitle: Text(appLocalizations.recoveryDesc),
),
@@ -341,13 +340,13 @@ class _RecoveryOptionsDialogState extends State<RecoveryOptionsDialog> {
child: Wrap(
children: [
ListItem(
onTab: () {
onTap: () {
_handleOnTab(RecoveryOption.onlyProfiles);
},
title: Text(appLocalizations.recoveryProfiles),
),
ListItem(
onTab: () {
onTap: () {
_handleOnTab(RecoveryOption.all);
},
title: Text(appLocalizations.recoveryAll),

View File

@@ -141,7 +141,7 @@ class _ConfigFragmentState extends State<ConfigFragment> {
return generateSection(
title: appLocalizations.app,
items: [
if (Platform.isAndroid)
if (Platform.isAndroid)...[
Selector<Config, bool>(
selector: (_, config) => config.allowBypass,
builder: (_, allowBypass, __) {
@@ -159,7 +159,6 @@ class _ConfigFragmentState extends State<ConfigFragment> {
);
},
),
if (Platform.isAndroid)
Selector<Config, bool>(
selector: (_, config) => config.systemProxy,
builder: (_, systemProxy, __) {
@@ -177,6 +176,24 @@ class _ConfigFragmentState extends State<ConfigFragment> {
);
},
),
],
Selector<Config, bool>(
selector: (_, config) => config.isCloseConnections,
builder: (_, isCloseConnections, __) {
return ListItem.switchItem(
leading: const Icon(Icons.auto_delete_outlined),
title: Text(appLocalizations.autoCloseConnections),
subtitle: Text(appLocalizations.autoCloseConnectionsDesc),
delegate: SwitchDelegate(
value: isCloseConnections,
onChanged: (bool value) async {
final appController = globalState.appController;
appController.config.isCloseConnections = value;
},
),
);
},
),
Selector<Config, bool>(
selector: (_, config) => config.isCompatible,
builder: (_, isCompatible, __) {
@@ -210,7 +227,7 @@ class _ConfigFragmentState extends State<ConfigFragment> {
leading: const Icon(Icons.info_outline),
title: Text(appLocalizations.logLevel),
subtitle: Text(value.name),
onTab: () {
onTap: () {
_showLogLevelDialog(value);
},
);
@@ -223,7 +240,7 @@ class _ConfigFragmentState extends State<ConfigFragment> {
leading: const Icon(Icons.computer_outlined),
title: const Text("UA"),
subtitle: Text(value ?? appLocalizations.defaultText),
onTab: () {
onTap: () {
_showUaDialog(value);
},
);
@@ -236,7 +253,7 @@ class _ConfigFragmentState extends State<ConfigFragment> {
leading: const Icon(Icons.timeline),
title: Text(appLocalizations.testUrl),
subtitle: Text(value),
onTab: () {
onTap: () {
_modifyTestUrl(value);
},
);
@@ -246,7 +263,7 @@ class _ConfigFragmentState extends State<ConfigFragment> {
selector: (_, clashConfig) => clashConfig.mixedPort,
builder: (_, mixedPort, __) {
return ListItem(
onTab: () {
onTap: () {
_modifyMixedPort(mixedPort);
},
leading: const Icon(Icons.adjust_outlined),

View File

@@ -37,8 +37,9 @@ class _ConnectionsFragmentState extends State<ConnectionsFragment> {
timer = Timer.periodic(
const Duration(seconds: 1),
(timer) {
connectionsNotifier.value = connectionsNotifier.value
.copyWith(connections: clashCore.getConnections());
connectionsNotifier.value = connectionsNotifier.value.copyWith(
connections: clashCore.getConnections(),
);
},
);
});
@@ -50,6 +51,18 @@ class _ConnectionsFragmentState extends State<ConnectionsFragment> {
final commonScaffoldState =
context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.actions = [
IconButton(
onPressed: () {
clashCore.closeConnections();
connectionsNotifier.value = connectionsNotifier.value.copyWith(
connections: clashCore.getConnections(),
);
},
icon: const Icon(Icons.delete_sweep_outlined),
),
const SizedBox(
width: 8,
),
IconButton(
onPressed: () {
showSearch(
@@ -87,7 +100,7 @@ class _ConnectionsFragmentState extends State<ConnectionsFragment> {
}
_handleBlockConnection(String id) {
clashCore.closeConnections(id);
clashCore.closeConnection(id);
connectionsNotifier.value = connectionsNotifier.value
.copyWith(connections: clashCore.getConnections());
}
@@ -227,7 +240,7 @@ class ConnectionsSearchDelegate extends SearchDelegate {
}
_handleBlockConnection(String id) {
clashCore.closeConnections(id);
clashCore.closeConnection(id);
connectionsNotifier.value = connectionsNotifier.value.copyWith(
connections: clashCore.getConnections(),
);

View File

@@ -1,10 +1,10 @@
import 'package:fl_clash/enum/enum.dart';
import 'dart:math';
import 'package:fl_clash/fragments/dashboard/intranet_ip.dart';
import 'package:fl_clash/models/models.dart';
import 'package:flutter/material.dart';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:provider/provider.dart';
import 'network_detection.dart';
import 'outbound_mode.dart';
import 'start_button.dart';
@@ -29,34 +29,35 @@ class _DashboardFragmentState extends State<DashboardFragment> {
alignment: Alignment.topCenter,
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Selector<AppState, ViewMode>(
selector: (_, appState) => appState.viewMode,
builder: (_, viewMode, ___) {
final isDesktop = viewMode == ViewMode.desktop;
child: Selector<AppState, double>(
selector: (_, appState) => appState.viewWidth,
builder: (_, viewWidth, ___) {
// final viewMode = other.getViewMode(viewWidth);
// final isDesktop = viewMode == ViewMode.desktop;
return Grid(
crossAxisCount: 12,
crossAxisCount: max(4 * ((viewWidth / 350).ceil()), 8),
crossAxisSpacing: 16,
mainAxisSpacing: 16,
children: [
children: const [
GridItem(
crossAxisCellCount: isDesktop ? 8 : 12,
child: const NetworkSpeed(),
crossAxisCellCount: 8,
child: NetworkSpeed(),
),
GridItem(
crossAxisCellCount: isDesktop ? 4 : 6,
child: const OutboundMode(),
crossAxisCellCount: 4,
child: OutboundMode(),
),
GridItem(
crossAxisCellCount: isDesktop ? 4 : 6,
child: const NetworkDetection(),
crossAxisCellCount: 4,
child: NetworkDetection(),
),
GridItem(
crossAxisCellCount: isDesktop ? 4 : 6,
child: const TrafficUsage(),
crossAxisCellCount: 4,
child: TrafficUsage(),
),
GridItem(
crossAxisCellCount: isDesktop ? 4 : 6,
child: const IntranetIP(),
crossAxisCellCount: 4,
child: IntranetIP(),
),
],
);

View File

@@ -48,19 +48,19 @@ class AddProfile extends StatelessWidget {
leading: const Icon(Icons.qr_code),
title: Text(appLocalizations.qrcode),
subtitle: Text(appLocalizations.qrcodeDesc),
onTab: _toScan,
onTap: _toScan,
),
ListItem(
leading: const Icon(Icons.upload_file),
title: Text(appLocalizations.file),
subtitle: Text(appLocalizations.fileDesc),
onTab: _handleAddProfileFormFile,
onTap: _handleAddProfileFormFile,
),
ListItem(
leading: const Icon(Icons.cloud_download),
title: Text(appLocalizations.url),
subtitle: Text(appLocalizations.urlDesc),
onTab: _toAdd,
onTap: _toAdd,
)
],
);

View File

@@ -313,13 +313,6 @@ class _ProfileItemState extends State<ProfileItem> {
),
Row(
children: [
Text(
appLocalizations.expirationTime,
style: textTheme.labelMedium?.toLighter,
),
const SizedBox(
width: 4,
),
Text(
expireShow,
style: textTheme.labelMedium?.toLighter,

View File

@@ -105,11 +105,10 @@ class ProxyCard extends StatelessWidget {
groupName,
proxy.name,
);
clashCore.changeProxy(
ChangeProxyParams(
groupName: groupName,
proxyName: proxy.name,
),
globalState.changeProxy(
config: appController.config,
groupName: groupName,
proxyName: proxy.name,
);
}

View File

@@ -0,0 +1,75 @@
import 'dart:math';
import 'package:fl_clash/clash/clash.dart';
import 'package:fl_clash/common/constant.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';
Widget currentProxyNameBuilder({
required String groupName,
required Widget Function(String) builder,
}) {
return Selector2<AppState, Config, String>(
selector: (_, appState, config) {
final group = appState.getGroupWithName(groupName);
return config.currentSelectedMap[groupName] ?? group?.now ?? '';
},
builder: (_, value, ___) {
return builder(value);
},
);
}
double get listHeaderHeight {
final measure = globalState.appController.measure;
return 24 + measure.titleMediumHeight + 4 + measure.bodyMediumHeight;
}
double getItemHeight(ProxyCardType proxyCardType) {
final measure = globalState.appController.measure;
final baseHeight =
12 * 2 + measure.bodyMediumHeight * 2 + measure.bodySmallHeight + 8;
return switch (proxyCardType) {
ProxyCardType.expand => baseHeight + measure.labelSmallHeight + 8,
ProxyCardType.shrink => baseHeight,
ProxyCardType.min => baseHeight - measure.bodyMediumHeight,
};
}
delayTest(List<Proxy> proxies) async {
final appController = globalState.appController;
for (final proxy in proxies) {
final proxyName =
appController.appState.getRealProxyName(proxy.name) ?? proxy.name;
globalState.appController.setDelay(
Delay(
name: proxyName,
value: 0,
),
);
clashCore.getDelay(proxyName).then((delay) {
globalState.appController.setDelay(delay);
});
}
await Future.delayed(httpTimeoutDuration + moreDuration);
appController.appState.sortNum++;
}
double getScrollToSelectedOffset({
required String groupName,
required List<Proxy> proxies,
}) {
final appController = globalState.appController;
final columns = appController.columns;
final proxyCardType = appController.config.proxyCardType;
final selectedName = appController.getCurrentSelectedName(groupName);
final findSelectedIndex = proxies.indexWhere(
(proxy) => proxy.name == selectedName,
);
final selectedIndex = findSelectedIndex != -1 ? findSelectedIndex : 0;
final rows = ((selectedIndex - 1) / columns).ceil();
return max(rows * (getItemHeight(proxyCardType) + 8) - 8, 0);
}

View File

@@ -1,404 +0,0 @@
import 'dart:math';
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';
import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'card.dart';
class ProxyGroupView extends StatefulWidget {
final String groupName;
final ProxiesType type;
const ProxyGroupView({
super.key,
required this.groupName,
required this.type,
});
@override
State<ProxyGroupView> createState() => _ProxyGroupViewState();
}
class _ProxyGroupViewState extends State<ProxyGroupView> {
var isLock = false;
final scrollController = ScrollController();
var isEnd = false;
String get groupName => widget.groupName;
ProxiesType get type => widget.type;
double _getItemHeight(ProxyCardType proxyCardType) {
final measure = globalState.appController.measure;
final baseHeight =
12 * 2 + measure.bodyMediumHeight * 2 + measure.bodySmallHeight + 8;
return switch(proxyCardType){
ProxyCardType.expand => baseHeight + measure.labelSmallHeight + 8,
ProxyCardType.shrink => baseHeight,
ProxyCardType.min => baseHeight - measure.bodyMediumHeight,
};
}
_delayTest(List<Proxy> proxies) async {
if (isLock) return;
isLock = true;
final appController = globalState.appController;
for (final proxy in proxies) {
final proxyName =
appController.appState.getRealProxyName(proxy.name) ?? proxy.name;
globalState.appController.setDelay(
Delay(
name: proxyName,
value: 0,
),
);
clashCore.getDelay(proxyName).then((delay) {
globalState.appController.setDelay(delay);
});
}
await Future.delayed(httpTimeoutDuration + moreDuration);
appController.appState.sortNum++;
isLock = false;
}
Widget _currentProxyNameBuilder({
required Widget Function(String) builder,
}) {
return Selector2<AppState, Config, String>(
selector: (_, appState, config) {
final group = appState.getGroupWithName(groupName)!;
return config.currentSelectedMap[groupName] ?? group.now ?? '';
},
builder: (_, value, ___) {
return builder(value);
},
);
}
Widget _buildTabGroupView({
required List<Proxy> proxies,
required int columns,
required ProxyCardType proxyCardType,
}) {
final sortedProxies = globalState.appController.getSortProxies(
proxies,
);
return DelayTestButtonContainer(
onClick: () async {
await _delayTest(
proxies,
);
},
child: Align(
alignment: Alignment.topCenter,
child: GridView.builder(
padding: const EdgeInsets.all(16),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: columns,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
mainAxisExtent: _getItemHeight(proxyCardType),
),
itemCount: sortedProxies.length,
itemBuilder: (_, index) {
final proxy = sortedProxies[index];
return _currentProxyNameBuilder(builder: (value) {
return ProxyCard(
type: proxyCardType,
key: ValueKey('$groupName.${proxy.name}'),
isSelected: value == proxy.name,
proxy: proxy,
groupName: groupName,
);
});
},
),
),
);
}
Widget _buildExpansionGroupView({
required List<Proxy> proxies,
required int columns,
required ProxyCardType proxyCardType,
}) {
final sortedProxies = globalState.appController.getSortProxies(
proxies,
);
final group =
globalState.appController.appState.getGroupWithName(groupName)!;
final itemHeight = _getItemHeight(proxyCardType);
final innerHeight = context.appSize.height - 200;
final lines = (sortedProxies.length / columns).ceil();
final minLines =
innerHeight >= 200 ? (innerHeight / itemHeight).floor() : 3;
final height = (itemHeight + 8) * min(lines, minLines) - 8;
return Selector<Config, Set<String>>(
selector: (_, config) => config.currentUnfoldSet,
builder: (_, currentUnfoldSet, __) {
return CommonCard(
child: ExpansionTile(
childrenPadding: const EdgeInsets.all(8),
initiallyExpanded: currentUnfoldSet.contains(groupName),
iconColor: context.colorScheme.onSurfaceVariant,
onExpansionChanged: (value) {
final tempUnfoldSet = Set<String>.from(currentUnfoldSet);
if (value) {
tempUnfoldSet.add(groupName);
} else {
tempUnfoldSet.remove(groupName);
}
globalState.appController.config.updateCurrentUnfoldSet(
tempUnfoldSet,
);
},
controlAffinity: ListTileControlAffinity.trailing,
title: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
flex: 1,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(groupName),
const SizedBox(
height: 4,
),
Flexible(
flex: 1,
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
group.type.name,
style: context.textTheme.labelMedium?.toLight,
),
Flexible(
flex: 1,
child: _currentProxyNameBuilder(
builder: (value) {
return Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment:
CrossAxisAlignment.center,
children: [
if (value.isNotEmpty) ...[
Icon(
Icons.arrow_right,
color: context
.colorScheme.onSurfaceVariant,
),
Flexible(
flex: 1,
child: Text(
overflow: TextOverflow.ellipsis,
value,
style: context
.textTheme.labelMedium?.toLight,
),
),
]
],
);
},
),
),
],
),
),
const SizedBox(
height: 4,
),
],
),
),
IconButton(
icon: Icon(
Icons.network_ping,
size: 20,
color: context.colorScheme.onSurfaceVariant,
),
onPressed: () {
_delayTest(sortedProxies);
},
),
],
),
shape: const RoundedRectangleBorder(
side: BorderSide.none,
),
collapsedShape: const RoundedRectangleBorder(
side: BorderSide.none,
),
children: [
SizedBox(
height: height,
child: GridView.builder(
key: widget.key,
controller: scrollController,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: columns,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
mainAxisExtent: _getItemHeight(proxyCardType),
),
itemCount: sortedProxies.length,
itemBuilder: (_, index) {
final proxy = sortedProxies[index];
return _currentProxyNameBuilder(
builder: (value) {
return ProxyCard(
style: CommonCardType.filled,
type: proxyCardType,
isSelected: value == proxy.name,
key: ValueKey('$groupName.${proxy.name}'),
proxy: proxy,
groupName: groupName,
);
},
);
},
),
),
],
),
);
},
);
}
@override
void dispose() {
super.dispose();
scrollController.dispose();
}
@override
Widget build(BuildContext context) {
return Selector2<AppState, Config, ProxyGroupSelectorState>(
selector: (_, appState, config) {
final group = appState.getGroupWithName(groupName)!;
return ProxyGroupSelectorState(
proxyCardType: config.proxyCardType,
proxiesSortType: config.proxiesSortType,
columns: globalState.appController.columns,
sortNum: appState.sortNum,
proxies: group.all,
);
},
builder: (_, state, __) {
final proxies = state.proxies;
final columns = state.columns;
final proxyCardType = state.proxyCardType;
return switch (type) {
ProxiesType.tab => _buildTabGroupView(
proxies: proxies,
columns: columns,
proxyCardType: proxyCardType,
),
ProxiesType.list => _buildExpansionGroupView(
proxies: proxies,
columns: columns,
proxyCardType: proxyCardType,
),
};
},
);
}
}
class DelayTestButtonContainer extends StatefulWidget {
final Widget child;
final Future Function() onClick;
const DelayTestButtonContainer({
super.key,
required this.child,
required this.onClick,
});
@override
State<DelayTestButtonContainer> createState() =>
_DelayTestButtonContainerState();
}
class _DelayTestButtonContainerState extends State<DelayTestButtonContainer>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _scale;
_healthcheck() async {
_controller.forward();
await widget.onClick();
_controller.reverse();
}
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(
milliseconds: 200,
),
);
_scale = Tween<double>(
begin: 1.0,
end: 0.0,
).animate(
CurvedAnimation(
parent: _controller,
curve: const Interval(
0,
1,
),
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
_controller.reverse();
return FloatLayout(
floatingWidget: FloatWrapper(
child: AnimatedBuilder(
animation: _controller.view,
builder: (_, child) {
return SizedBox(
width: 56,
height: 56,
child: Transform.scale(
scale: _scale.value,
child: child,
),
);
},
child: FloatingActionButton(
heroTag: null,
onPressed: _healthcheck,
child: const Icon(Icons.network_ping),
),
),
),
child: widget.child,
);
}
}

View File

@@ -1,50 +1,512 @@
import 'package:collection/collection.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/card.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'group.dart';
import 'card.dart';
import 'common.dart';
typedef GroupNameProxiesMap = Map<String, List<Proxy>>;
class ProxiesListFragment extends StatefulWidget {
const ProxiesListFragment({super.key});
@override
State<ProxiesListFragment> createState() =>
_ProxiesListFragmentState();
State<ProxiesListFragment> createState() => _ProxiesListFragmentState();
}
class _ProxiesListFragmentState
extends State<ProxiesListFragment> {
class _ProxiesListFragmentState extends State<ProxiesListFragment> {
final _controller = ScrollController();
final _headerStateNotifier = ValueNotifier<ProxiesListHeaderSelectorState>(
const ProxiesListHeaderSelectorState(
offset: 0,
currentIndex: 0,
),
);
List<double> _headerOffset = [];
GroupNameProxiesMap _lastGroupNameProxiesMap = {};
@override
void initState() {
super.initState();
_controller.addListener(_adjustHeader);
}
_adjustHeader() {
final offset = _controller.offset;
final index = _headerOffset.findInterval(offset);
final currentIndex = index;
double headerOffset = 0.0;
if (index + 1 <= _headerOffset.length - 1) {
final endOffset = _headerOffset[index + 1];
final startOffset = endOffset - listHeaderHeight - 8;
if (offset > startOffset && offset < endOffset) {
headerOffset = offset - startOffset;
}
}
_headerStateNotifier.value = _headerStateNotifier.value.copyWith(
currentIndex: currentIndex,
offset: headerOffset,
);
}
double _getListItemHeight(Type type, ProxyCardType proxyCardType) {
return switch (type) {
const (SizedBox) => 8,
const (ListHeader) => listHeaderHeight,
Type() => getItemHeight(proxyCardType),
};
}
@override
void dispose() {
super.dispose();
_headerStateNotifier.dispose();
_controller.removeListener(_adjustHeader);
_controller.dispose();
}
_handleChange(Set<String> currentUnfoldSet, String groupName) {
final tempUnfoldSet = Set<String>.from(currentUnfoldSet);
if (tempUnfoldSet.contains(groupName)) {
tempUnfoldSet.remove(groupName);
} else {
tempUnfoldSet.add(groupName);
}
globalState.appController.config.updateCurrentUnfoldSet(
tempUnfoldSet,
);
WidgetsBinding.instance.addPostFrameCallback((_) {
_adjustHeader();
});
}
List<double> _getItemHeightList(
List<Widget> items,
ProxyCardType proxyCardType,
) {
final itemHeightList = <double>[];
List<double> headerOffset = [];
double currentHeight = 0;
for (final item in items) {
if (item.runtimeType == ListHeader) {
headerOffset.add(currentHeight);
}
final itemHeight = _getListItemHeight(item.runtimeType, proxyCardType);
itemHeightList.add(itemHeight);
currentHeight = currentHeight + itemHeight;
}
_headerOffset = headerOffset;
return itemHeightList;
}
List<Widget> _buildItems({
required List<String> groupNames,
required int columns,
required Set<String> currentUnfoldSet,
required ProxyCardType type,
}) {
final items = <Widget>[];
final GroupNameProxiesMap groupNameProxiesMap = {};
for (final groupName in groupNames) {
final group =
globalState.appController.appState.getGroupWithName(groupName)!;
final isExpand = currentUnfoldSet.contains(groupName);
items.addAll([
ListHeader(
onScrollToSelected: _scrollToGroupSelected,
key: Key(groupName),
isExpand: isExpand,
group: group,
onChange: (String groupName) {
_handleChange(currentUnfoldSet, groupName);
},
),
const SizedBox(
height: 8,
),
]);
if (isExpand) {
final sortedProxies = globalState.appController.getSortProxies(
group.all,
);
groupNameProxiesMap[groupName] = sortedProxies;
final chunks = sortedProxies.chunks(columns);
final rows = chunks.map<Widget>((proxies) {
final children = proxies
.map<Widget>(
(proxy) => Flexible(
child: currentProxyNameBuilder(
groupName: group.name,
builder: (currentProxyName) {
return ProxyCard(
type: type,
isSelected: currentProxyName == proxy.name,
key: ValueKey('$groupName.${proxy.name}'),
proxy: proxy,
groupName: groupName,
);
}),
),
)
.fill(
columns,
filler: (_) => const Flexible(
child: SizedBox(),
),
)
.separated(
const SizedBox(
width: 8,
),
);
return Row(
children: children.toList(),
);
}).separated(
const SizedBox(
height: 8,
),
);
items.addAll(
[
...rows,
const SizedBox(
height: 8,
),
],
);
}
}
_lastGroupNameProxiesMap = groupNameProxiesMap;
return items;
}
_buildHeader({
required String groupName,
required Set<String> currentUnfoldSet,
}) {
final group =
globalState.appController.appState.getGroupWithName(groupName)!;
final isExpand = currentUnfoldSet.contains(groupName);
return SizedBox(
height: listHeaderHeight,
child: ListHeader(
onScrollToSelected: _scrollToGroupSelected,
key: Key(groupName),
isExpand: isExpand,
group: group,
onChange: (String groupName) {
_handleChange(currentUnfoldSet, groupName);
},
),
);
}
_scrollToGroupSelected(String groupName) {
final appController = globalState.appController;
final currentGroups = appController.appState.currentGroups;
final groupNames = currentGroups.map((e) => e.name).toList();
final findIndex = groupNames.indexWhere((item) => item == groupName);
final index = findIndex != -1 ? findIndex : 0;
final currentInitOffset = _headerOffset[index];
final proxies = _lastGroupNameProxiesMap[groupName];
_controller.animateTo(
currentInitOffset +
getScrollToSelectedOffset(
groupName: groupName,
proxies: proxies ?? [],
),
duration: const Duration(milliseconds: 300),
curve: Curves.easeIn,
);
}
@override
Widget build(BuildContext context) {
return Selector2<AppState, Config, ProxiesSelectorState>(
return Selector2<AppState, Config, ProxiesListSelectorState>(
selector: (_, appState, config) {
final currentGroups = appState.currentGroups;
final groupNames = currentGroups.map((e) => e.name).toList();
return ProxiesSelectorState(
return ProxiesListSelectorState(
groupNames: groupNames,
currentGroupName: config.currentGroupName,
currentUnfoldSet: config.currentUnfoldSet,
proxyCardType: config.proxyCardType,
proxiesSortType: config.proxiesSortType,
columns: globalState.appController.columns,
sortNum: appState.sortNum,
);
},
shouldRebuild: (prev, next) {
if (!const ListEquality<String>()
.equals(prev.groupNames, next.groupNames)) {
_headerStateNotifier.value = const ProxiesListHeaderSelectorState(
offset: 0,
currentIndex: 0,
);
}
return prev != next;
},
builder: (_, state, __) {
return ListView.separated(
padding: const EdgeInsets.all(16),
itemCount: state.groupNames.length,
itemBuilder: (_, index) {
final groupName = state.groupNames[index];
return ProxyGroupView(
key: PageStorageKey(groupName),
groupName: groupName,
type: ProxiesType.list,
);
},
separatorBuilder: (BuildContext context, int index) {
return const SizedBox(
height: 16,
);
},
final items = _buildItems(
groupNames: state.groupNames,
currentUnfoldSet: state.currentUnfoldSet,
columns: state.columns,
type: state.proxyCardType,
);
final itemsOffset = _getItemHeightList(items, state.proxyCardType);
return Scrollbar(
controller: _controller,
thumbVisibility: true,
trackVisibility: true,
thickness: 8,
radius: const Radius.circular(8),
interactive: true,
child: Stack(
children: [
Positioned.fill(
child: ScrollConfiguration(
behavior: HiddenBarScrollBehavior(),
child: ListView.builder(
padding: const EdgeInsets.all(16),
controller: _controller,
itemExtentBuilder: (index, __) {
return itemsOffset[index];
},
itemCount: items.length,
itemBuilder: (_, index) {
return items[index];
},
),
),
),
LayoutBuilder(builder: (_, container) {
return ValueListenableBuilder(
valueListenable: _headerStateNotifier,
builder: (_, headerState, ___) {
final index =
headerState.currentIndex > state.groupNames.length - 1
? 0
: headerState.currentIndex;
return Stack(
children: [
Positioned(
top: -headerState.offset,
child: Container(
width: container.maxWidth,
color: context.colorScheme.surface,
padding: const EdgeInsets.only(
top: 16,
left: 16,
right: 16,
bottom: 8,
),
child: _buildHeader(
groupName: state.groupNames[index],
currentUnfoldSet: state.currentUnfoldSet,
),
),
),
],
);
},
);
}),
],
),
);
},
);
}
}
}
class ListHeader extends StatefulWidget {
final Group group;
final Function(String groupName) onChange;
final Function(String groupName) onScrollToSelected;
final bool isExpand;
const ListHeader({
super.key,
required this.group,
required this.onChange,
required this.onScrollToSelected,
required this.isExpand,
});
@override
State<ListHeader> createState() => _ListHeaderState();
}
class _ListHeaderState extends State<ListHeader>
with SingleTickerProviderStateMixin {
late AnimationController _animationController;
late Animation<double> _iconTurns;
var isLock = false;
String get groupName => widget.group.name;
String get groupType => widget.group.type.name;
bool get isExpand => widget.isExpand;
_delayTest(List<Proxy> proxies) async {
if (isLock) return;
isLock = true;
await delayTest(proxies);
isLock = false;
}
_handleChange(String groupName) {
if (isExpand) {
_animationController.reverse();
} else {
_animationController.forward();
}
widget.onChange(groupName);
}
@override
void initState() {
super.initState();
_animationController = AnimationController(
duration: const Duration(milliseconds: 200),
vsync: this,
);
_iconTurns = _animationController.drive(
Tween<double>(begin: 0.0, end: 0.5),
);
if (isExpand) {
_animationController.value = 1.0;
}
}
@override
void dispose() {
_animationController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return CommonCard(
key: widget.key,
type: CommonCardType.filled,
child: Container(
padding: const EdgeInsets.all(12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
groupName,
style: context.textTheme.titleMedium?.copyWith(
color: context.colorScheme.primary,
),
),
const SizedBox(
height: 4,
),
Flexible(
flex: 1,
child: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
groupType,
style: context.textTheme.labelMedium?.toLight,
),
Flexible(
flex: 1,
child: currentProxyNameBuilder(
groupName: groupName,
builder: (value) {
return Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (value.isNotEmpty) ...[
Flexible(
flex: 1,
child: Text(
overflow: TextOverflow.ellipsis,
" · $value",
style: context
.textTheme.labelMedium?.toLight,
),
),
]
],
);
},
),
),
],
),
),
],
),
),
Row(
children: [
if (isExpand) ...[
IconButton(
onPressed: () {
widget.onScrollToSelected(groupName);
},
icon: const Icon(
Icons.adjust,
),
),
IconButton(
onPressed: () {
_delayTest(widget.group.all);
},
icon: const Icon(
Icons.network_ping,
),
),
const SizedBox(
width: 4,
),
],
AnimatedBuilder(
animation: _animationController.view,
builder: (_, __) {
return IconButton.filledTonal(
onPressed: () {
_handleChange(groupName);
},
icon: RotationTransition(
turns: _iconTurns,
child: const Icon(
Icons.expand_more,
),
),
);
},
)
],
)
],
),
),
onPressed: () {
_handleChange(groupName);
},
);
}
}

View File

@@ -17,12 +17,26 @@ class ProxiesFragment extends StatefulWidget {
}
class _ProxiesFragmentState extends State<ProxiesFragment> {
final GlobalKey<ProxiesTabFragmentState> _proxiesTabKey = GlobalKey();
_initActions() {
_initActions(ProxiesType proxiesType) {
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
final commonScaffoldState =
context.findAncestorStateOfType<CommonScaffoldState>();
commonScaffoldState?.actions = [
if (proxiesType == ProxiesType.tab) ...[
IconButton(
onPressed: () {
_proxiesTabKey.currentState?.scrollToGroupSelected();
},
icon: const Icon(
Icons.gps_fixed,
),
),
const SizedBox(
width: 8,
)
],
IconButton(
onPressed: () {
showSheet(
@@ -43,23 +57,24 @@ class _ProxiesFragmentState extends State<ProxiesFragment> {
@override
Widget build(BuildContext context) {
return Selector<AppState, bool>(
selector: (_, appState) => appState.currentLabel == 'proxies',
builder: (_, isCurrent, child) {
if (isCurrent) {
_initActions();
}
return child!;
return Selector<Config, ProxiesType>(
selector: (_, config) => config.proxiesType,
builder: (_, proxiesType, __) {
return Selector<AppState, bool>(
selector: (_, appState) => appState.currentLabel == 'proxies',
builder: (_, isCurrent, child) {
if (isCurrent) {
_initActions(proxiesType);
}
return switch (proxiesType) {
ProxiesType.tab => ProxiesTabFragment(
key: _proxiesTabKey,
),
ProxiesType.list => const ProxiesListFragment(),
};
},
);
},
child: Selector<Config, ProxiesType>(
selector: (_, config) => config.proxiesType,
builder: (_, proxiesType, __) {
return switch (proxiesType) {
ProxiesType.tab => const ProxiesTabFragment(),
ProxiesType.list => const ProxiesListFragment(),
};
},
),
);
}
}

View File

@@ -8,19 +8,23 @@ import 'package:fl_clash/widgets/widgets.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'group.dart';
import 'card.dart';
import 'common.dart';
typedef GroupNameKeyMap = Map<String, GlobalObjectKey<ProxyGroupViewState>>;
class ProxiesTabFragment extends StatefulWidget {
const ProxiesTabFragment({super.key});
@override
State<ProxiesTabFragment> createState() => _ProxiesTabFragmentState();
State<ProxiesTabFragment> createState() => ProxiesTabFragmentState();
}
class _ProxiesTabFragmentState extends State<ProxiesTabFragment>
class ProxiesTabFragmentState extends State<ProxiesTabFragment>
with TickerProviderStateMixin {
TabController? _tabController;
final hasMoreButtonNotifier = ValueNotifier<bool>(false);
final _hasMoreButtonNotifier = ValueNotifier<bool>(false);
GroupNameKeyMap _keyMap = {};
@override
void dispose() {
@@ -28,6 +32,11 @@ class _ProxiesTabFragmentState extends State<ProxiesTabFragment>
_tabController?.dispose();
}
scrollToGroupSelected() {
final currentGroupName = globalState.appController.config.currentGroupName;
_keyMap[currentGroupName]?.currentState?.scrollToSelected();
}
_buildMoreButton() {
return Selector<AppState, bool>(
selector: (_, appState) => appState.viewMode == ViewMode.mobile,
@@ -83,6 +92,7 @@ class _ProxiesTabFragmentState extends State<ProxiesTabFragment>
.updateCurrentGroupName(
groupName,
);
Navigator.of(context).pop();
},
isSelected: groupName == state.currentGroupName,
)
@@ -126,18 +136,29 @@ class _ProxiesTabFragmentState extends State<ProxiesTabFragment>
initialIndex: index == -1 ? 0 : index,
vsync: this,
);
GroupNameKeyMap keyMap = {};
final children = state.groupNames.map((groupName) {
keyMap[groupName] = GlobalObjectKey(groupName);
return KeepContainer(
child: ProxyGroupView(
key: keyMap[groupName],
groupName: groupName,
),
);
}).toList();
_keyMap = keyMap;
return Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
NotificationListener<ScrollMetricsNotification>(
onNotification: (scrollNotification) {
hasMoreButtonNotifier.value =
_hasMoreButtonNotifier.value =
scrollNotification.metrics.maxScrollExtent > 0;
return true;
},
child: ValueListenableBuilder(
valueListenable: hasMoreButtonNotifier,
valueListenable: _hasMoreButtonNotifier,
builder: (_, value, child) {
return Stack(
alignment: AlignmentDirectional.centerStart,
@@ -199,16 +220,7 @@ class _ProxiesTabFragmentState extends State<ProxiesTabFragment>
Expanded(
child: TabBarView(
controller: _tabController,
children: [
for (final groupName in state.groupNames)
KeepContainer(
key: ObjectKey(groupName),
child: ProxyGroupView(
groupName: groupName,
type: ProxiesType.tab,
),
),
],
children: children,
),
)
],
@@ -217,3 +229,205 @@ class _ProxiesTabFragmentState extends State<ProxiesTabFragment>
);
}
}
class ProxyGroupView extends StatefulWidget {
final String groupName;
const ProxyGroupView({
super.key,
required this.groupName,
});
@override
State<ProxyGroupView> createState() => ProxyGroupViewState();
}
class ProxyGroupViewState extends State<ProxyGroupView> {
var isLock = false;
final _controller = ScrollController();
List<Proxy> _lastProxies = [];
String get groupName => widget.groupName;
_delayTest(List<Proxy> proxies) async {
if (isLock) return;
isLock = true;
await delayTest(proxies);
isLock = false;
}
@override
void dispose() {
super.dispose();
_controller.dispose();
}
Widget _buildTabGroupView({
required List<Proxy> proxies,
required int columns,
required ProxyCardType proxyCardType,
}) {
final sortedProxies = globalState.appController.getSortProxies(
proxies,
);
_lastProxies = sortedProxies;
return DelayTestButtonContainer(
onClick: () async {
await _delayTest(
proxies,
);
},
child: Align(
alignment: Alignment.topCenter,
child: GridView.builder(
controller: _controller,
padding: const EdgeInsets.all(16),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: columns,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
mainAxisExtent: getItemHeight(proxyCardType),
),
itemCount: sortedProxies.length,
itemBuilder: (_, index) {
final proxy = sortedProxies[index];
return currentProxyNameBuilder(
builder: (value) {
return ProxyCard(
type: proxyCardType,
key: ValueKey('$groupName.${proxy.name}'),
isSelected: value == proxy.name,
proxy: proxy,
groupName: groupName,
);
},
groupName: groupName,
);
},
),
),
);
}
scrollToSelected() {
_controller.animateTo(
16 +
getScrollToSelectedOffset(
groupName: groupName,
proxies: _lastProxies,
),
duration: const Duration(milliseconds: 300),
curve: Curves.easeIn,
);
}
@override
Widget build(BuildContext context) {
return Selector2<AppState, Config, ProxyGroupSelectorState>(
selector: (_, appState, config) {
final group = appState.getGroupWithName(groupName)!;
return ProxyGroupSelectorState(
proxyCardType: config.proxyCardType,
proxiesSortType: config.proxiesSortType,
columns: globalState.appController.columns,
sortNum: appState.sortNum,
proxies: group.all,
);
},
builder: (_, state, __) {
final proxies = state.proxies;
final columns = state.columns;
final proxyCardType = state.proxyCardType;
return _buildTabGroupView(
proxies: proxies,
columns: columns,
proxyCardType: proxyCardType,
);
},
);
}
}
class DelayTestButtonContainer extends StatefulWidget {
final Widget child;
final Future Function() onClick;
const DelayTestButtonContainer({
super.key,
required this.child,
required this.onClick,
});
@override
State<DelayTestButtonContainer> createState() =>
_DelayTestButtonContainerState();
}
class _DelayTestButtonContainerState extends State<DelayTestButtonContainer>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _scale;
_healthcheck() async {
_controller.forward();
await widget.onClick();
_controller.reverse();
}
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(
milliseconds: 200,
),
);
_scale = Tween<double>(
begin: 1.0,
end: 0.0,
).animate(
CurvedAnimation(
parent: _controller,
curve: const Interval(
0,
1,
),
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
_controller.reverse();
return FloatLayout(
floatingWidget: FloatWrapper(
child: AnimatedBuilder(
animation: _controller.view,
builder: (_, child) {
return SizedBox(
width: 56,
height: 56,
child: Transform.scale(
scale: _scale.value,
child: child,
),
);
},
child: FloatingActionButton(
heroTag: null,
onPressed: _healthcheck,
child: const Icon(Icons.network_ping),
),
),
),
child: widget.child,
);
}
}

View File

@@ -1,5 +1,3 @@
import 'dart:ui';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
@@ -23,27 +21,6 @@ class ThemeModeItem {
class ThemeFragment extends StatelessWidget {
const ThemeFragment({super.key});
Widget _itemCard({
required BuildContext context,
required Info info,
required Widget child,
}) {
return Padding(
padding: const EdgeInsets.only(
top: 16,
),
child: Wrap(
runSpacing: 16,
children: [
InfoHeader(
info: info,
),
child,
],
),
);
}
@override
Widget build(BuildContext context) {
final previewCard = Padding(

View File

@@ -177,14 +177,14 @@
"geodataLoader": "Geo Low Memory Mode",
"geodataLoaderDesc": "Enabling will use the Geo low memory loader",
"requests": "Requests",
"requestsDesc": "View recently requested data",
"requestsDesc": "View recently request records",
"findProcessMode": "Find process",
"findProcessModeDesc": "There is a risk of flashback after opening",
"init": "Init",
"infiniteTime": "Long term effective",
"expirationTime": "Expiration time",
"connections": "Connections",
"connectionsDesc": "View current connection",
"connectionsDesc": "View current connections data",
"nullRequestsDesc": "No requests",
"nullConnectionsDesc": "No connections",
"intranetIP": "Intranet IP",
@@ -211,5 +211,10 @@
"sort": "Sort",
"columns": "Columns",
"proxiesSetting": "Proxies setting",
"proxyGroup": "Proxy group"
"proxyGroup": "Proxy group",
"go": "Go",
"externalLink": "External link",
"otherContributors": "Other contributors",
"autoCloseConnections": "Auto lose connections",
"autoCloseConnectionsDesc": "Auto close connections after change node"
}

View File

@@ -71,7 +71,7 @@
"qrcode": "二维码",
"qrcodeDesc": "扫描二维码获取配置文件",
"url": "URL",
"urlDesc": "直接上传配置文件",
"urlDesc": "通过URL获取配置文件",
"file": "文件",
"fileDesc": "直接上传配置文件",
"name": "名称",
@@ -177,14 +177,14 @@
"geodataLoader": "Geo低内存模式",
"geodataLoaderDesc": "开启将使用Geo低内存加载器",
"requests": "请求",
"requestsDesc": "查看最近请求数据",
"requestsDesc": "查看最近请求记录",
"findProcessMode": "查找进程",
"findProcessModeDesc": "开启后存在闪退风险",
"init": "初始化",
"infiniteTime": "长期有效",
"expirationTime": "到期时间",
"connections": "连接",
"connectionsDesc": "查看当前连接",
"connectionsDesc": "查看当前连接数据",
"nullRequestsDesc": "暂无请求",
"nullConnectionsDesc": "暂无连接",
"intranetIP": "内网 IP",
@@ -211,5 +211,10 @@
"sort": "排序",
"columns": "列数",
"proxiesSetting": "代理设置",
"proxyGroup": "代理组"
"proxyGroup": "代理组",
"go": "前往",
"externalLink": "外部链接",
"otherContributors": "其他贡献者",
"autoCloseConnections": "自动关闭连接",
"autoCloseConnectionsDesc": "切换节点后自动关闭连接"
}

View File

@@ -58,6 +58,10 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Auto check updates"),
"autoCheckUpdateDesc": MessageLookupByLibrary.simpleMessage(
"Auto check for updates when the app starts"),
"autoCloseConnections":
MessageLookupByLibrary.simpleMessage("Auto lose connections"),
"autoCloseConnectionsDesc": MessageLookupByLibrary.simpleMessage(
"Auto close connections after change node"),
"autoLaunch": MessageLookupByLibrary.simpleMessage("AutoLaunch"),
"autoLaunchDesc": MessageLookupByLibrary.simpleMessage(
"Follow the system self startup"),
@@ -94,8 +98,8 @@ class MessageLookup extends MessageLookupByLibrary {
"Opening it will lose part of its application ability and gain the support of full amount of Clash."),
"confirm": MessageLookupByLibrary.simpleMessage("Confirm"),
"connections": MessageLookupByLibrary.simpleMessage("Connections"),
"connectionsDesc":
MessageLookupByLibrary.simpleMessage("View current connection"),
"connectionsDesc": MessageLookupByLibrary.simpleMessage(
"View current connections data"),
"connectivity": MessageLookupByLibrary.simpleMessage("Connectivity"),
"copy": MessageLookupByLibrary.simpleMessage("Copy"),
"core": MessageLookupByLibrary.simpleMessage("Core"),
@@ -135,6 +139,7 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("ExternalController"),
"externalControllerDesc": MessageLookupByLibrary.simpleMessage(
"Once enabled, the Clash kernel can be controlled on port 9090"),
"externalLink": MessageLookupByLibrary.simpleMessage("External link"),
"externalResources":
MessageLookupByLibrary.simpleMessage("External resources"),
"file": MessageLookupByLibrary.simpleMessage("File"),
@@ -153,6 +158,7 @@ class MessageLookup extends MessageLookupByLibrary {
"geodataLoaderDesc": MessageLookupByLibrary.simpleMessage(
"Enabling will use the Geo low memory loader"),
"global": MessageLookupByLibrary.simpleMessage("Global"),
"go": MessageLookupByLibrary.simpleMessage("Go"),
"goDownload": MessageLookupByLibrary.simpleMessage("Go to download"),
"hours": MessageLookupByLibrary.simpleMessage("Hours"),
"importFromURL":
@@ -203,6 +209,8 @@ class MessageLookup extends MessageLookupByLibrary {
"nullRequestsDesc": MessageLookupByLibrary.simpleMessage("No requests"),
"oneColumn": MessageLookupByLibrary.simpleMessage("One column"),
"other": MessageLookupByLibrary.simpleMessage("Other"),
"otherContributors":
MessageLookupByLibrary.simpleMessage("Other contributors"),
"outboundMode": MessageLookupByLibrary.simpleMessage("Outbound mode"),
"override": MessageLookupByLibrary.simpleMessage("Override"),
"overrideDesc": MessageLookupByLibrary.simpleMessage(
@@ -257,7 +265,7 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("Recovery success"),
"requests": MessageLookupByLibrary.simpleMessage("Requests"),
"requestsDesc": MessageLookupByLibrary.simpleMessage(
"View recently requested data"),
"View recently request records"),
"resources": MessageLookupByLibrary.simpleMessage("Resources"),
"resourcesDesc": MessageLookupByLibrary.simpleMessage(
"External resource related info"),

View File

@@ -49,6 +49,9 @@ class MessageLookup extends MessageLookupByLibrary {
"autoCheckUpdate": MessageLookupByLibrary.simpleMessage("自动检查更新"),
"autoCheckUpdateDesc":
MessageLookupByLibrary.simpleMessage("应用启动时自动检查更新"),
"autoCloseConnections": MessageLookupByLibrary.simpleMessage("自动关闭连接"),
"autoCloseConnectionsDesc":
MessageLookupByLibrary.simpleMessage("切换节点后自动关闭连接"),
"autoLaunch": MessageLookupByLibrary.simpleMessage("自启动"),
"autoLaunchDesc": MessageLookupByLibrary.simpleMessage("跟随系统自启动"),
"autoRun": MessageLookupByLibrary.simpleMessage("自动运行"),
@@ -77,7 +80,7 @@ class MessageLookup extends MessageLookupByLibrary {
MessageLookupByLibrary.simpleMessage("开启将失去部分应用能力获得全量的Clash的支持"),
"confirm": MessageLookupByLibrary.simpleMessage("确定"),
"connections": MessageLookupByLibrary.simpleMessage("连接"),
"connectionsDesc": MessageLookupByLibrary.simpleMessage("查看当前连接"),
"connectionsDesc": MessageLookupByLibrary.simpleMessage("查看当前连接数据"),
"connectivity": MessageLookupByLibrary.simpleMessage("连通性:"),
"copy": MessageLookupByLibrary.simpleMessage("复制"),
"core": MessageLookupByLibrary.simpleMessage("内核"),
@@ -111,6 +114,7 @@ class MessageLookup extends MessageLookupByLibrary {
"externalController": MessageLookupByLibrary.simpleMessage("外部控制器"),
"externalControllerDesc":
MessageLookupByLibrary.simpleMessage("开启后将可以通过9090端口控制Clash内核"),
"externalLink": MessageLookupByLibrary.simpleMessage("外部链接"),
"externalResources": MessageLookupByLibrary.simpleMessage("外部资源"),
"file": MessageLookupByLibrary.simpleMessage("文件"),
"fileDesc": MessageLookupByLibrary.simpleMessage("直接上传配置文件"),
@@ -125,6 +129,7 @@ class MessageLookup extends MessageLookupByLibrary {
"geodataLoaderDesc":
MessageLookupByLibrary.simpleMessage("开启将使用Geo低内存加载器"),
"global": MessageLookupByLibrary.simpleMessage("全局"),
"go": MessageLookupByLibrary.simpleMessage("前往"),
"goDownload": MessageLookupByLibrary.simpleMessage("前往下载"),
"hours": MessageLookupByLibrary.simpleMessage("小时"),
"importFromURL": MessageLookupByLibrary.simpleMessage("从URL导入"),
@@ -166,6 +171,7 @@ class MessageLookup extends MessageLookupByLibrary {
"nullRequestsDesc": MessageLookupByLibrary.simpleMessage("暂无请求"),
"oneColumn": MessageLookupByLibrary.simpleMessage("一列"),
"other": MessageLookupByLibrary.simpleMessage("其他"),
"otherContributors": MessageLookupByLibrary.simpleMessage("其他贡献者"),
"outboundMode": MessageLookupByLibrary.simpleMessage("出站模式"),
"override": MessageLookupByLibrary.simpleMessage("覆写"),
"overrideDesc": MessageLookupByLibrary.simpleMessage("覆写代理相关配置"),
@@ -206,7 +212,7 @@ class MessageLookup extends MessageLookupByLibrary {
"recoveryProfiles": MessageLookupByLibrary.simpleMessage("仅恢复配置文件"),
"recoverySuccess": MessageLookupByLibrary.simpleMessage("恢复成功"),
"requests": MessageLookupByLibrary.simpleMessage("请求"),
"requestsDesc": MessageLookupByLibrary.simpleMessage("查看最近请求数据"),
"requestsDesc": MessageLookupByLibrary.simpleMessage("查看最近请求记录"),
"resources": MessageLookupByLibrary.simpleMessage("资源"),
"resourcesDesc": MessageLookupByLibrary.simpleMessage("外部资源相关信息"),
"rule": MessageLookupByLibrary.simpleMessage("规则"),
@@ -255,7 +261,7 @@ class MessageLookup extends MessageLookupByLibrary {
"update": MessageLookupByLibrary.simpleMessage("更新"),
"upload": MessageLookupByLibrary.simpleMessage("上传"),
"url": MessageLookupByLibrary.simpleMessage("URL"),
"urlDesc": MessageLookupByLibrary.simpleMessage("直接上传配置文件"),
"urlDesc": MessageLookupByLibrary.simpleMessage("通过URL获取配置文件"),
"view": MessageLookupByLibrary.simpleMessage("查看"),
"webDAVConfiguration": MessageLookupByLibrary.simpleMessage("WebDAV配置"),
"whitelistMode": MessageLookupByLibrary.simpleMessage("白名单模式"),

View File

@@ -1830,10 +1830,10 @@ class AppLocalizations {
);
}
/// `View recently requested data`
/// `View recently request records`
String get requestsDesc {
return Intl.message(
'View recently requested data',
'View recently request records',
name: 'requestsDesc',
desc: '',
args: [],
@@ -1900,10 +1900,10 @@ class AppLocalizations {
);
}
/// `View current connection`
/// `View current connections data`
String get connectionsDesc {
return Intl.message(
'View current connection',
'View current connections data',
name: 'connectionsDesc',
desc: '',
args: [],
@@ -2179,6 +2179,56 @@ class AppLocalizations {
args: [],
);
}
/// `Go`
String get go {
return Intl.message(
'Go',
name: 'go',
desc: '',
args: [],
);
}
/// `External link`
String get externalLink {
return Intl.message(
'External link',
name: 'externalLink',
desc: '',
args: [],
);
}
/// `Other contributors`
String get otherContributors {
return Intl.message(
'Other contributors',
name: 'otherContributors',
desc: '',
args: [],
);
}
/// `Auto lose connections`
String get autoCloseConnections {
return Intl.message(
'Auto lose connections',
name: 'autoCloseConnections',
desc: '',
args: [],
);
}
/// `Auto close connections after change node`
String get autoCloseConnectionsDesc {
return Intl.message(
'Auto close connections after change node',
name: 'autoCloseConnectionsDesc',
desc: '',
args: [],
);
}
}
class AppLocalizationDelegate extends LocalizationsDelegate<AppLocalizations> {

View File

@@ -47,6 +47,7 @@ Future<void> main() async {
Future<void> vpnService() async {
WidgetsFlutterBinding.ensureInitialized();
globalState.isVpnService = true;
globalState.packageInfo = await PackageInfo.fromPlatform();
final config = await preferences.getConfig() ?? Config();
final clashConfig = await preferences.getClashConfig() ?? ClashConfig();
final appState = AppState(
@@ -86,11 +87,10 @@ Future<void> vpnService() async {
final currentSelectedMap = config.currentSelectedMap;
final proxyName = currentSelectedMap[groupName];
if (proxyName == null) return;
clashCore.changeProxy(
ChangeProxyParams(
groupName: groupName,
proxyName: proxyName,
),
globalState.changeProxy(
config: config,
groupName: groupName,
proxyName: proxyName,
);
},
),
@@ -119,7 +119,7 @@ Future<void> vpnService() async {
globalState.updateTraffic();
globalState.updateFunctionLists = [
() {
() {
globalState.updateTraffic();
}
];
@@ -137,8 +137,7 @@ class ServiceMessageHandler with ServiceMessageListener {
required Function(Process process) onProcess,
required Function(String runTime) onStarted,
required Function(String groupName) onLoaded,
})
: _onProtect = onProtect,
}) : _onProtect = onProtect,
_onProcess = onProcess,
_onStarted = onStarted,
_onLoaded = onLoaded;

View File

@@ -275,7 +275,7 @@ class ClashConfig extends ChangeNotifier {
}
}
@JsonKey(name: "global-ua", defaultValue: null)
@JsonKey(name: "global-ua", includeFromJson: false, includeToJson: true)
String get globalUa {
if (_globalRealUa == null) {
return globalState.packageInfo.ua;
@@ -320,7 +320,6 @@ class ClashConfig extends ChangeNotifier {
_geodataLoader = clashConfig._geodataLoader;
_dns = clashConfig._dns;
_rules = clashConfig._rules;
_globalRealUa = clashConfig.globalRealUa;
}
notifyListeners();
}

View File

@@ -73,6 +73,7 @@ class Config extends ChangeNotifier {
bool _systemProxy;
bool _isExclude;
DAV? _dav;
bool _isCloseConnections;
ProxiesType _proxiesType;
ProxyCardType _proxyCardType;
int _proxiesColumns;
@@ -84,6 +85,7 @@ class Config extends ChangeNotifier {
_autoLaunch = false,
_silentLaunch = false,
_autoRun = false,
_isCloseConnections = false,
_themeMode = ThemeMode.system,
_openLog = false,
_isCompatible = true,
@@ -92,7 +94,7 @@ class Config extends ChangeNotifier {
_isMinimizeOnExit = true,
_isAccessControl = false,
_autoCheckUpdate = true,
_systemProxy = true,
_systemProxy = false,
_testUrl = defaultTestUrl,
_accessControl = const AccessControl(),
_isAnimateToPage = true,
@@ -393,7 +395,7 @@ class Config extends ChangeNotifier {
}
}
@JsonKey(defaultValue: true)
@JsonKey(defaultValue: false)
bool get systemProxy {
return _systemProxy;
}
@@ -405,6 +407,18 @@ class Config extends ChangeNotifier {
}
}
@JsonKey(defaultValue: false)
bool get isCloseConnections {
return _isCloseConnections;
}
set isCloseConnections(bool value) {
if (_isCloseConnections != value) {
_isCloseConnections = value;
notifyListeners();
}
}
@JsonKey(
defaultValue: ProxiesType.tab,
unknownEnumValue: ProxiesType.tab,
@@ -482,6 +496,7 @@ class Config extends ChangeNotifier {
}
if (onlyProfiles) return;
_currentProfileId = config._currentProfileId;
_isCloseConnections = config._isCloseConnections;
_isCompatible = config._isCompatible;
_autoLaunch = config._autoLaunch;
_silentLaunch = config._silentLaunch;
@@ -498,7 +513,6 @@ class Config extends ChangeNotifier {
_accessControl = config._accessControl;
_isAnimateToPage = config._isAnimateToPage;
_autoCheckUpdate = config._autoCheckUpdate;
_dav = config._dav;
_testUrl = config._testUrl;
_isExclude = config._isExclude;
_windowProps = config._windowProps;

View File

@@ -1,8 +1,6 @@
// ignore_for_file: invalid_annotation_target
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/models/clash_config.dart';
import 'package:fl_clash/models/connection.dart';
import 'package:fl_clash/models/models.dart';
import 'package:freezed_annotation/freezed_annotation.dart';

View File

@@ -82,6 +82,7 @@ Map<String, dynamic> _$ClashConfigToJson(ClashConfig instance) =>
'tun': instance.tun,
'dns': instance.dns,
'rules': instance.rules,
'global-ua': instance.globalUa,
'global-real-ua': instance.globalRealUa,
'geox-url': instance.geoXUrl,
};

View File

@@ -34,7 +34,8 @@ Config _$ConfigFromJson(Map<String, dynamic> json) => Config()
..isCompatible = json['isCompatible'] as bool? ?? true
..autoCheckUpdate = json['autoCheckUpdate'] as bool? ?? true
..allowBypass = json['allowBypass'] as bool? ?? true
..systemProxy = json['systemProxy'] as bool? ?? true
..systemProxy = json['systemProxy'] as bool? ?? false
..isCloseConnections = json['isCloseConnections'] as bool? ?? false
..proxiesType = $enumDecodeNullable(_$ProxiesTypeEnumMap, json['proxiesType'],
unknownValue: ProxiesType.tab) ??
ProxiesType.tab
@@ -68,6 +69,7 @@ Map<String, dynamic> _$ConfigToJson(Config instance) => <String, dynamic>{
'autoCheckUpdate': instance.autoCheckUpdate,
'allowBypass': instance.allowBypass,
'systemProxy': instance.systemProxy,
'isCloseConnections': instance.isCloseConnections,
'proxiesType': _$ProxiesTypeEnumMap[instance.proxiesType]!,
'proxyCardType': _$ProxyCardTypeEnumMap[instance.proxyCardType]!,
'proxiesColumns': instance.proxiesColumns,

View File

@@ -1756,6 +1756,257 @@ abstract class _ProxiesSelectorState implements ProxiesSelectorState {
get copyWith => throw _privateConstructorUsedError;
}
/// @nodoc
mixin _$ProxiesListSelectorState {
List<String> get groupNames => throw _privateConstructorUsedError;
Set<String> get currentUnfoldSet => throw _privateConstructorUsedError;
ProxiesSortType get proxiesSortType => throw _privateConstructorUsedError;
ProxyCardType get proxyCardType => throw _privateConstructorUsedError;
num get sortNum => throw _privateConstructorUsedError;
int get columns => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$ProxiesListSelectorStateCopyWith<ProxiesListSelectorState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $ProxiesListSelectorStateCopyWith<$Res> {
factory $ProxiesListSelectorStateCopyWith(ProxiesListSelectorState value,
$Res Function(ProxiesListSelectorState) then) =
_$ProxiesListSelectorStateCopyWithImpl<$Res, ProxiesListSelectorState>;
@useResult
$Res call(
{List<String> groupNames,
Set<String> currentUnfoldSet,
ProxiesSortType proxiesSortType,
ProxyCardType proxyCardType,
num sortNum,
int columns});
}
/// @nodoc
class _$ProxiesListSelectorStateCopyWithImpl<$Res,
$Val extends ProxiesListSelectorState>
implements $ProxiesListSelectorStateCopyWith<$Res> {
_$ProxiesListSelectorStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? groupNames = null,
Object? currentUnfoldSet = null,
Object? proxiesSortType = null,
Object? proxyCardType = null,
Object? sortNum = null,
Object? columns = null,
}) {
return _then(_value.copyWith(
groupNames: null == groupNames
? _value.groupNames
: groupNames // ignore: cast_nullable_to_non_nullable
as List<String>,
currentUnfoldSet: null == currentUnfoldSet
? _value.currentUnfoldSet
: currentUnfoldSet // ignore: cast_nullable_to_non_nullable
as Set<String>,
proxiesSortType: null == proxiesSortType
? _value.proxiesSortType
: proxiesSortType // ignore: cast_nullable_to_non_nullable
as ProxiesSortType,
proxyCardType: null == proxyCardType
? _value.proxyCardType
: proxyCardType // ignore: cast_nullable_to_non_nullable
as ProxyCardType,
sortNum: null == sortNum
? _value.sortNum
: sortNum // ignore: cast_nullable_to_non_nullable
as num,
columns: null == columns
? _value.columns
: columns // ignore: cast_nullable_to_non_nullable
as int,
) as $Val);
}
}
/// @nodoc
abstract class _$$ProxiesListSelectorStateImplCopyWith<$Res>
implements $ProxiesListSelectorStateCopyWith<$Res> {
factory _$$ProxiesListSelectorStateImplCopyWith(
_$ProxiesListSelectorStateImpl value,
$Res Function(_$ProxiesListSelectorStateImpl) then) =
__$$ProxiesListSelectorStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call(
{List<String> groupNames,
Set<String> currentUnfoldSet,
ProxiesSortType proxiesSortType,
ProxyCardType proxyCardType,
num sortNum,
int columns});
}
/// @nodoc
class __$$ProxiesListSelectorStateImplCopyWithImpl<$Res>
extends _$ProxiesListSelectorStateCopyWithImpl<$Res,
_$ProxiesListSelectorStateImpl>
implements _$$ProxiesListSelectorStateImplCopyWith<$Res> {
__$$ProxiesListSelectorStateImplCopyWithImpl(
_$ProxiesListSelectorStateImpl _value,
$Res Function(_$ProxiesListSelectorStateImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? groupNames = null,
Object? currentUnfoldSet = null,
Object? proxiesSortType = null,
Object? proxyCardType = null,
Object? sortNum = null,
Object? columns = null,
}) {
return _then(_$ProxiesListSelectorStateImpl(
groupNames: null == groupNames
? _value._groupNames
: groupNames // ignore: cast_nullable_to_non_nullable
as List<String>,
currentUnfoldSet: null == currentUnfoldSet
? _value._currentUnfoldSet
: currentUnfoldSet // ignore: cast_nullable_to_non_nullable
as Set<String>,
proxiesSortType: null == proxiesSortType
? _value.proxiesSortType
: proxiesSortType // ignore: cast_nullable_to_non_nullable
as ProxiesSortType,
proxyCardType: null == proxyCardType
? _value.proxyCardType
: proxyCardType // ignore: cast_nullable_to_non_nullable
as ProxyCardType,
sortNum: null == sortNum
? _value.sortNum
: sortNum // ignore: cast_nullable_to_non_nullable
as num,
columns: null == columns
? _value.columns
: columns // ignore: cast_nullable_to_non_nullable
as int,
));
}
}
/// @nodoc
class _$ProxiesListSelectorStateImpl implements _ProxiesListSelectorState {
const _$ProxiesListSelectorStateImpl(
{required final List<String> groupNames,
required final Set<String> currentUnfoldSet,
required this.proxiesSortType,
required this.proxyCardType,
required this.sortNum,
required this.columns})
: _groupNames = groupNames,
_currentUnfoldSet = currentUnfoldSet;
final List<String> _groupNames;
@override
List<String> get groupNames {
if (_groupNames is EqualUnmodifiableListView) return _groupNames;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_groupNames);
}
final Set<String> _currentUnfoldSet;
@override
Set<String> get currentUnfoldSet {
if (_currentUnfoldSet is EqualUnmodifiableSetView) return _currentUnfoldSet;
// ignore: implicit_dynamic_type
return EqualUnmodifiableSetView(_currentUnfoldSet);
}
@override
final ProxiesSortType proxiesSortType;
@override
final ProxyCardType proxyCardType;
@override
final num sortNum;
@override
final int columns;
@override
String toString() {
return 'ProxiesListSelectorState(groupNames: $groupNames, currentUnfoldSet: $currentUnfoldSet, proxiesSortType: $proxiesSortType, proxyCardType: $proxyCardType, sortNum: $sortNum, columns: $columns)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ProxiesListSelectorStateImpl &&
const DeepCollectionEquality()
.equals(other._groupNames, _groupNames) &&
const DeepCollectionEquality()
.equals(other._currentUnfoldSet, _currentUnfoldSet) &&
(identical(other.proxiesSortType, proxiesSortType) ||
other.proxiesSortType == proxiesSortType) &&
(identical(other.proxyCardType, proxyCardType) ||
other.proxyCardType == proxyCardType) &&
(identical(other.sortNum, sortNum) || other.sortNum == sortNum) &&
(identical(other.columns, columns) || other.columns == columns));
}
@override
int get hashCode => Object.hash(
runtimeType,
const DeepCollectionEquality().hash(_groupNames),
const DeepCollectionEquality().hash(_currentUnfoldSet),
proxiesSortType,
proxyCardType,
sortNum,
columns);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$ProxiesListSelectorStateImplCopyWith<_$ProxiesListSelectorStateImpl>
get copyWith => __$$ProxiesListSelectorStateImplCopyWithImpl<
_$ProxiesListSelectorStateImpl>(this, _$identity);
}
abstract class _ProxiesListSelectorState implements ProxiesListSelectorState {
const factory _ProxiesListSelectorState(
{required final List<String> groupNames,
required final Set<String> currentUnfoldSet,
required final ProxiesSortType proxiesSortType,
required final ProxyCardType proxyCardType,
required final num sortNum,
required final int columns}) = _$ProxiesListSelectorStateImpl;
@override
List<String> get groupNames;
@override
Set<String> get currentUnfoldSet;
@override
ProxiesSortType get proxiesSortType;
@override
ProxyCardType get proxyCardType;
@override
num get sortNum;
@override
int get columns;
@override
@JsonKey(ignore: true)
_$$ProxiesListSelectorStateImplCopyWith<_$ProxiesListSelectorStateImpl>
get copyWith => throw _privateConstructorUsedError;
}
/// @nodoc
mixin _$ProxyGroupSelectorState {
ProxiesSortType get proxiesSortType => throw _privateConstructorUsedError;
@@ -2401,3 +2652,151 @@ abstract class _ColumnsSelectorState implements ColumnsSelectorState {
_$$ColumnsSelectorStateImplCopyWith<_$ColumnsSelectorStateImpl>
get copyWith => throw _privateConstructorUsedError;
}
/// @nodoc
mixin _$ProxiesListHeaderSelectorState {
double get offset => throw _privateConstructorUsedError;
int get currentIndex => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
$ProxiesListHeaderSelectorStateCopyWith<ProxiesListHeaderSelectorState>
get copyWith => throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $ProxiesListHeaderSelectorStateCopyWith<$Res> {
factory $ProxiesListHeaderSelectorStateCopyWith(
ProxiesListHeaderSelectorState value,
$Res Function(ProxiesListHeaderSelectorState) then) =
_$ProxiesListHeaderSelectorStateCopyWithImpl<$Res,
ProxiesListHeaderSelectorState>;
@useResult
$Res call({double offset, int currentIndex});
}
/// @nodoc
class _$ProxiesListHeaderSelectorStateCopyWithImpl<$Res,
$Val extends ProxiesListHeaderSelectorState>
implements $ProxiesListHeaderSelectorStateCopyWith<$Res> {
_$ProxiesListHeaderSelectorStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
@pragma('vm:prefer-inline')
@override
$Res call({
Object? offset = null,
Object? currentIndex = null,
}) {
return _then(_value.copyWith(
offset: null == offset
? _value.offset
: offset // ignore: cast_nullable_to_non_nullable
as double,
currentIndex: null == currentIndex
? _value.currentIndex
: currentIndex // ignore: cast_nullable_to_non_nullable
as int,
) as $Val);
}
}
/// @nodoc
abstract class _$$ProxiesListHeaderSelectorStateImplCopyWith<$Res>
implements $ProxiesListHeaderSelectorStateCopyWith<$Res> {
factory _$$ProxiesListHeaderSelectorStateImplCopyWith(
_$ProxiesListHeaderSelectorStateImpl value,
$Res Function(_$ProxiesListHeaderSelectorStateImpl) then) =
__$$ProxiesListHeaderSelectorStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({double offset, int currentIndex});
}
/// @nodoc
class __$$ProxiesListHeaderSelectorStateImplCopyWithImpl<$Res>
extends _$ProxiesListHeaderSelectorStateCopyWithImpl<$Res,
_$ProxiesListHeaderSelectorStateImpl>
implements _$$ProxiesListHeaderSelectorStateImplCopyWith<$Res> {
__$$ProxiesListHeaderSelectorStateImplCopyWithImpl(
_$ProxiesListHeaderSelectorStateImpl _value,
$Res Function(_$ProxiesListHeaderSelectorStateImpl) _then)
: super(_value, _then);
@pragma('vm:prefer-inline')
@override
$Res call({
Object? offset = null,
Object? currentIndex = null,
}) {
return _then(_$ProxiesListHeaderSelectorStateImpl(
offset: null == offset
? _value.offset
: offset // ignore: cast_nullable_to_non_nullable
as double,
currentIndex: null == currentIndex
? _value.currentIndex
: currentIndex // ignore: cast_nullable_to_non_nullable
as int,
));
}
}
/// @nodoc
class _$ProxiesListHeaderSelectorStateImpl
implements _ProxiesListHeaderSelectorState {
const _$ProxiesListHeaderSelectorStateImpl(
{required this.offset, required this.currentIndex});
@override
final double offset;
@override
final int currentIndex;
@override
String toString() {
return 'ProxiesListHeaderSelectorState(offset: $offset, currentIndex: $currentIndex)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ProxiesListHeaderSelectorStateImpl &&
(identical(other.offset, offset) || other.offset == offset) &&
(identical(other.currentIndex, currentIndex) ||
other.currentIndex == currentIndex));
}
@override
int get hashCode => Object.hash(runtimeType, offset, currentIndex);
@JsonKey(ignore: true)
@override
@pragma('vm:prefer-inline')
_$$ProxiesListHeaderSelectorStateImplCopyWith<
_$ProxiesListHeaderSelectorStateImpl>
get copyWith => __$$ProxiesListHeaderSelectorStateImplCopyWithImpl<
_$ProxiesListHeaderSelectorStateImpl>(this, _$identity);
}
abstract class _ProxiesListHeaderSelectorState
implements ProxiesListHeaderSelectorState {
const factory _ProxiesListHeaderSelectorState(
{required final double offset,
required final int currentIndex}) = _$ProxiesListHeaderSelectorStateImpl;
@override
double get offset;
@override
int get currentIndex;
@override
@JsonKey(ignore: true)
_$$ProxiesListHeaderSelectorStateImplCopyWith<
_$ProxiesListHeaderSelectorStateImpl>
get copyWith => throw _privateConstructorUsedError;
}

View File

@@ -99,6 +99,18 @@ class ProxiesSelectorState with _$ProxiesSelectorState {
}) = _ProxiesSelectorState;
}
@freezed
class ProxiesListSelectorState with _$ProxiesListSelectorState {
const factory ProxiesListSelectorState({
required List<String> groupNames,
required Set<String> currentUnfoldSet,
required ProxiesSortType proxiesSortType,
required ProxyCardType proxyCardType,
required num sortNum,
required int columns,
}) = _ProxiesListSelectorState;
}
@freezed
class ProxyGroupSelectorState with _$ProxyGroupSelectorState {
const factory ProxyGroupSelectorState({
@@ -133,3 +145,11 @@ class ColumnsSelectorState with _$ColumnsSelectorState {
required ViewMode viewMode,
}) = _ColumnsSelectorState;
}
@freezed
class ProxiesListHeaderSelectorState with _$ProxiesListHeaderSelectorState {
const factory ProxiesListHeaderSelectorState({
required double offset,
required int currentIndex,
}) = _ProxiesListHeaderSelectorState;
}

View File

@@ -13,6 +13,20 @@ typedef OnSelected = void Function(int index);
class HomePage extends StatelessWidget {
const HomePage({super.key});
_navigationBarContainer({
required BuildContext context,
required Widget child,
}) {
// if (!system.isDesktop) return child;
return Container(
padding: const EdgeInsets.all(16).copyWith(
right: 0,
),
color: context.colorScheme.surface,
child: child,
);
}
_getNavigationBar({
required BuildContext context,
required ViewMode viewMode,
@@ -34,9 +48,31 @@ class HomePage extends StatelessWidget {
);
}
final extended = viewMode == ViewMode.desktop;
return _navigationBarContainer(
context: context,
child: NavigationRail(
groupAlignment: -0.8,
destinations: navigationItems
.map(
(e) => NavigationRailDestination(
icon: e.icon,
label: Text(
Intl.message(e.label),
),
),
)
.toList(),
onDestinationSelected: globalState.appController.toPage,
extended: extended,
minExtendedWidth: 172,
selectedIndex: currentIndex,
labelType: extended
? NavigationRailLabelType.none
: NavigationRailLabelType.selected,
),
);
return NavigationRail(
backgroundColor: context.colorScheme.surfaceContainer,
groupAlignment: -0.8,
groupAlignment: -0.95,
destinations: navigationItems
.map(
(e) => NavigationRailDestination(
@@ -92,29 +128,14 @@ class HomePage extends StatelessWidget {
);
final bottomNavigationBar =
viewMode == ViewMode.mobile ? navigationBar : null;
if (viewMode != ViewMode.mobile) {
return Row(
children: [
navigationBar,
Expanded(
flex: 1,
child: CommonScaffold(
key: globalState.homeScaffoldKey,
title: Intl.message(
currentLabel,
),
body: child!,
bottomNavigationBar: bottomNavigationBar,
),
)
],
);
}
final sideNavigationBar =
viewMode != ViewMode.mobile ? navigationBar : null;
return CommonScaffold(
key: globalState.homeScaffoldKey,
title: Intl.message(
currentLabel,
),
sideNavigationBar: sideNavigationBar,
body: child!,
bottomNavigationBar: bottomNavigationBar,
);

View File

@@ -103,6 +103,7 @@ class Proxy extends ProxyPlatform {
_handleServiceMessage(String message) {
final m = ServiceMessage.fromJson(json.decode(message));
debugPrint(m.toString());
switch (m.type) {
case ServiceMessageType.protect:
_serviceMessageHandler?.onProtect(Fd.fromJson(m.data));

View File

@@ -7,6 +7,7 @@ import 'package:fl_clash/plugins/proxy.dart';
import 'package:fl_clash/widgets/scaffold.dart';
import 'package:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:url_launcher/url_launcher.dart';
import 'controller.dart';
@@ -52,7 +53,7 @@ class GlobalState {
config: clashConfig,
params: ConfigExtendedParams(
isPatch: isPatch,
isCompatible: config.isCompatible,
isCompatible: true,
selectedMap: config.currentSelectedMap,
testUrl: config.testUrl,
),
@@ -123,7 +124,7 @@ class GlobalState {
}) async {
appState.isInit = clashCore.isInit;
if (!appState.isInit) {
if(Platform.isAndroid){
if (Platform.isAndroid) {
clashCore.setProps(
Props(
accessControl: config.isAccessControl ? config.accessControl : null,
@@ -183,6 +184,22 @@ class GlobalState {
);
}
changeProxy({
required Config config,
required String groupName,
required String proxyName,
}) {
clashCore.changeProxy(
ChangeProxyParams(
groupName: groupName,
proxyName: proxyName,
),
);
if(config.isCloseConnections){
clashCore.closeConnections();
}
}
Future<T?> showCommonDialog<T>({
required Widget child,
}) async {
@@ -261,6 +278,17 @@ class GlobalState {
return null;
}
}
openUrl(String url) {
showMessage(
message: TextSpan(text: url),
title: appLocalizations.externalLink,
confirmText: appLocalizations.go,
onTab: () {
launchUrl(Uri.parse(url));
},
);
}
}
final globalState = GlobalState();

View File

@@ -20,7 +20,7 @@ class AndroidContainer extends StatefulWidget {
class _AndroidContainerState extends State<AndroidContainer>
with WidgetsBindingObserver {
_excludeContainer(Widget child) {
Widget _excludeContainer(Widget child) {
return Selector<Config, bool>(
selector: (_, config) => config.isExclude,
builder: (_, isExclude, child) {
@@ -31,6 +31,20 @@ class _AndroidContainerState extends State<AndroidContainer>
);
}
Widget _systemUiOverlayContainer(Widget child) {
return AnnotatedRegion(
value: SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Theme.of(context).brightness == Brightness.dark
? Brightness.light
: Brightness.dark,
systemNavigationBarColor: Colors.transparent,
systemNavigationBarDividerColor: Colors.transparent,
),
child: child,
);
}
@override
void initState() {
super.initState();
@@ -48,7 +62,9 @@ class _AndroidContainerState extends State<AndroidContainer>
@override
Widget build(BuildContext context) {
return _excludeContainer(widget.child);
return _systemUiOverlayContainer(
_excludeContainer(widget.child),
);
}
@override

View File

@@ -3,7 +3,6 @@ import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/plugins/proxy.dart';
import 'package:fl_clash/state.dart';
import 'package:flutter/material.dart';
import 'package:fl_clash/plugins/app.dart';
class ClashMessageContainer extends StatefulWidget {
final Widget child;
@@ -61,11 +60,10 @@ class _ClashMessageContainerState extends State<ClashMessageContainer>
final currentSelectedMap = appController.config.currentSelectedMap;
final proxyName = currentSelectedMap[groupName];
if (proxyName == null) return;
clashCore.changeProxy(
ChangeProxyParams(
groupName: groupName,
proxyName: proxyName,
),
globalState.changeProxy(
config: appController.config,
groupName: groupName,
proxyName: proxyName,
);
appController.addCheckIpNumDebounce();
super.onLoaded(proxyName);

View File

@@ -78,7 +78,7 @@ class ListItem<T> extends StatelessWidget {
final Widget? trailing;
final Delegate delegate;
final double? horizontalTitleGap;
final void Function()? onTab;
final void Function()? onTap;
const ListItem({
super.key,
@@ -89,7 +89,7 @@ class ListItem<T> extends StatelessWidget {
this.trailing,
this.horizontalTitleGap,
this.prue,
this.onTab,
this.onTap,
this.tileTitleAlignment = ListTileTitleAlignment.center,
}) : delegate = const Delegate();
@@ -104,7 +104,7 @@ class ListItem<T> extends StatelessWidget {
this.horizontalTitleGap,
this.prue,
this.tileTitleAlignment = ListTileTitleAlignment.center,
}) : onTab = null;
}) : onTap = null;
const ListItem.next({
super.key,
@@ -117,7 +117,7 @@ class ListItem<T> extends StatelessWidget {
this.horizontalTitleGap,
this.prue,
this.tileTitleAlignment = ListTileTitleAlignment.center,
}) : onTab = null;
}) : onTap = null;
const ListItem.checkbox({
super.key,
@@ -130,7 +130,7 @@ class ListItem<T> extends StatelessWidget {
this.prue,
this.tileTitleAlignment = ListTileTitleAlignment.center,
}) : trailing = null,
onTab = null;
onTap = null;
const ListItem.switchItem({
super.key,
@@ -143,7 +143,7 @@ class ListItem<T> extends StatelessWidget {
this.prue,
this.tileTitleAlignment = ListTileTitleAlignment.center,
}) : trailing = null,
onTab = null;
onTap = null;
const ListItem.radio({
super.key,
@@ -156,10 +156,10 @@ class ListItem<T> extends StatelessWidget {
this.prue,
this.tileTitleAlignment = ListTileTitleAlignment.center,
}) : leading = null,
onTab = null;
onTap = null;
_buildListTile({
void Function()? onTab,
void Function()? onTap,
Widget? trailing,
Widget? leading,
}) {
@@ -188,7 +188,7 @@ class ListItem<T> extends StatelessWidget {
}
return InkWell(
splashFactory: NoSplash.splashFactory,
onTap: onTab,
onTap: onTap,
child: Container(
padding: padding,
child: Row(
@@ -203,7 +203,7 @@ class ListItem<T> extends StatelessWidget {
title: title,
subtitle: subtitle,
titleAlignment: tileTitleAlignment,
onTap: onTab,
onTap: onTap,
trailing: trailing ?? this.trailing,
contentPadding: padding,
);
@@ -230,7 +230,7 @@ class ListItem<T> extends StatelessWidget {
action();
}
return _buildListTile(onTab: openAction);
return _buildListTile(onTap: openAction);
},
openBuilder: (_, action) {
return CommonScaffold.open(
@@ -245,7 +245,7 @@ class ListItem<T> extends StatelessWidget {
if (delegate is NextDelegate) {
final nextDelegate = delegate as NextDelegate;
return _buildListTile(
onTab: () {
onTap: () {
final isMobile =
globalState.appController.appState.viewMode == ViewMode.mobile;
if (!isMobile) {
@@ -272,7 +272,7 @@ class ListItem<T> extends StatelessWidget {
if (delegate is CheckboxDelegate) {
final checkboxDelegate = delegate as CheckboxDelegate;
return _buildListTile(
onTab: () {
onTap: () {
if (checkboxDelegate.onChanged != null) {
checkboxDelegate.onChanged!(!checkboxDelegate.value);
}
@@ -286,7 +286,7 @@ class ListItem<T> extends StatelessWidget {
if (delegate is SwitchDelegate) {
final switchDelegate = delegate as SwitchDelegate;
return _buildListTile(
onTab: () {
onTap: () {
if (switchDelegate.onChanged != null) {
switchDelegate.onChanged!(!switchDelegate.value);
}
@@ -300,7 +300,7 @@ class ListItem<T> extends StatelessWidget {
if (delegate is RadioDelegate) {
final radioDelegate = delegate as RadioDelegate<T>;
return _buildListTile(
onTab: () {
onTap: () {
if (radioDelegate.onChanged != null) {
radioDelegate.onChanged!(radioDelegate.value);
}
@@ -319,7 +319,7 @@ class ListItem<T> extends StatelessWidget {
}
return _buildListTile(
onTab: onTab,
onTap: onTap,
);
}
}

View File

@@ -19,7 +19,7 @@ class _PopContainerState extends State<PopContainer> {
if (Platform.isAndroid) {
return PopScope(
canPop: false,
onPopInvoked: (didPop) async {
onPopInvoked: (_) async {
final canPop = Navigator.canPop(context);
if (canPop) {
Navigator.pop(context);

View File

@@ -1,12 +1,11 @@
import 'package:fl_clash/common/app_localizations.dart';
import 'package:fl_clash/common/system.dart';
import 'package:fl_clash/common/common.dart';
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;
final Widget? sideNavigationBar;
final String title;
final Widget? leading;
final List<Widget>? actions;
@@ -15,6 +14,7 @@ class CommonScaffold extends StatefulWidget {
const CommonScaffold({
super.key,
required this.body,
this.sideNavigationBar,
this.bottomNavigationBar,
this.leading,
required this.title,
@@ -92,85 +92,66 @@ class CommonScaffoldState extends State<CommonScaffold> {
}
}
_platformContainer({required Widget child}) {
if (system.isDesktop) {
return child;
}
return AnnotatedRegion(
value: SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Theme.of(context).brightness == Brightness.dark
? Brightness.light
: Brightness.dark,
systemNavigationBarColor: Colors.transparent,
systemNavigationBarDividerColor: Colors.transparent,
),
child: child,
);
}
Widget? get _sideNavigationBar => widget.sideNavigationBar;
@override
Widget build(BuildContext context) {
return _platformContainer(
child: Scaffold(
resizeToAvoidBottomInset: true,
appBar: PreferredSize(
preferredSize: const Size.fromHeight(kToolbarHeight),
child: Stack(
alignment: Alignment.bottomCenter,
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,
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();
},
),
],
),
),
body: widget.body,
bottomNavigationBar: widget.bottomNavigationBar,
);
return _sideNavigationBar != null
? Row(
mainAxisSize: MainAxisSize.min,
children: [
ValueListenableBuilder<List<Widget>>(
valueListenable: _actions,
builder: (_, actions, __) {
final realActions =
actions.isNotEmpty ? actions : widget.actions;
return AppBar(
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();
},
_sideNavigationBar!,
Expanded(
flex: 1,
child: Material(
child: Container(
padding: const EdgeInsets.symmetric(vertical: 8),
child: scaffold,
),
),
),
],
),
),
body: widget.body,
bottomNavigationBar: widget.bottomNavigationBar,
),
);
}
}
class AppIcon extends StatelessWidget {
const AppIcon({super.key});
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 16,
),
width: 30,
height: 30,
child: const CircleAvatar(
foregroundImage: AssetImage("assets/images/launch_icon.png"),
backgroundColor: Colors.transparent,
),
);
)
: scaffold;
}
}

View File

@@ -85,10 +85,7 @@ class _SideSheetState extends State<SideSheet> {
);
final BoxConstraints constraints = widget.constraints ??
const BoxConstraints(
maxWidth: 320,
minWidth: 320
);
const BoxConstraints(maxWidth: 320, minWidth: 320);
final Clip clipBehavior = widget.clipBehavior ?? Clip.none;
@@ -594,31 +591,17 @@ Future<T?> showModalSideSheet<T>({
builder: (context) {
return Column(
children: [
Flexible(
flex: 0,
child: Row(
children: [
const SizedBox(
height: kToolbarHeight,
width: kToolbarHeight,
child: BackButton(),
),
const SizedBox(
width: 8,
),
Expanded(
child: Text(
title,
style: Theme.of(context).textTheme.titleMedium,
),
),
const SizedBox(
height: kToolbarHeight,
width: kToolbarHeight,
child: CloseButton(),
),
],
),
AppBar(
automaticallyImplyLeading: false,
title: Text(title),
centerTitle: false,
actions: const [
SizedBox(
height: kToolbarHeight,
width: kToolbarHeight,
child: CloseButton(),
)
],
),
Expanded(
flex: 1,

View File

@@ -1,3 +1,5 @@
import 'dart:io';
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/models.dart';
import 'package:fl_clash/state.dart';
@@ -31,7 +33,22 @@ class _WindowContainerState extends State<WindowContainer> with WindowListener {
@override
Widget build(BuildContext context) {
return _autoLaunchContainer(widget.child);
return Stack(
children: [
Column(
children: [
SizedBox(
height: kHeaderHeight,
),
Expanded(
flex: 1,
child: _autoLaunchContainer(widget.child),
),
],
),
const WindowHeader(),
],
);
}
@override
@@ -80,3 +97,170 @@ class _WindowContainerState extends State<WindowContainer> with WindowListener {
super.dispose();
}
}
class WindowHeader extends StatefulWidget {
const WindowHeader({super.key});
@override
State<WindowHeader> createState() => _WindowHeaderState();
}
class _WindowHeaderState extends State<WindowHeader> {
final isMaximizedNotifier = ValueNotifier<bool>(false);
final isPinNotifier = ValueNotifier<bool>(false);
@override
void initState() {
super.initState();
_initNotifier();
}
_initNotifier() async {
isMaximizedNotifier.value = await windowManager.isMaximized();
isPinNotifier.value = await windowManager.isAlwaysOnTop();
}
@override
void dispose() {
super.dispose();
isMaximizedNotifier.dispose();
isPinNotifier.dispose();
}
_updateMaximized() {
isMaximizedNotifier.value = !isMaximizedNotifier.value;
switch (isMaximizedNotifier.value) {
case true:
windowManager.maximize();
case false:
windowManager.unmaximize();
}
}
_updatePin() {
isPinNotifier.value = !isPinNotifier.value;
windowManager.setAlwaysOnTop(isPinNotifier.value);
}
_buildActions() {
return Row(
children: [
IconButton(
onPressed: () async {
_updatePin();
},
icon: ValueListenableBuilder(
valueListenable: isPinNotifier,
builder: (_, value, ___) {
return value
? const Icon(
Icons.push_pin,
)
: const Icon(
Icons.push_pin_outlined,
);
},
),
),
IconButton(
onPressed: () {
windowManager.minimize();
},
icon: const Icon(Icons.remove),
),
IconButton(
onPressed: () async {
_updateMaximized();
},
icon: ValueListenableBuilder(
valueListenable: isMaximizedNotifier,
builder: (_, value, ___) {
return value
? const Icon(
Icons.filter_none,
size: 20,
)
: const Icon(
Icons.crop_square,
);
},
),
),
IconButton(
onPressed: () {
windowManager.close();
},
icon: const Icon(Icons.close),
),
// const SizedBox(
// width: 8,
// ),
],
);
}
@override
Widget build(BuildContext context) {
return Material(
child: Stack(
alignment: AlignmentDirectional.center,
children: [
Positioned(
child: GestureDetector(
onPanStart: (_) {
windowManager.startDragging();
},
onDoubleTap: () {
_updateMaximized();
},
child: Container(
color: context.colorScheme.surface,
alignment: Alignment.centerLeft,
height: kHeaderHeight,
),
),
),
if (!Platform.isMacOS) ...[
const Positioned(
left: 0,
child: AppIcon(),
),
Positioned(
right: 0,
child: _buildActions(),
),
]
],
),
);
}
}
class AppIcon extends StatelessWidget {
const AppIcon({super.key});
@override
Widget build(BuildContext context) {
return Container(
margin: const EdgeInsets.only(left: 8),
child: const Row(
children: [
SizedBox(
width: 24,
height: 24,
child: CircleAvatar(
foregroundImage: AssetImage("assets/images/icon.png"),
backgroundColor: Colors.transparent,
),
),
SizedBox(
width: 8,
),
Text(
appName,
),
],
),
);
}
}

View File

@@ -1,6 +1,6 @@
display_name: FlClash
icon: ./assets/images/launch_icon.png
icon: ./assets/images/icon.png
keywords:
- FlClash

View File

@@ -8,7 +8,7 @@ priority: optional
section: x11
installed_size: 6604
essential: false
icon: ./assets/images/launch_icon.png
icon: ./assets/images/icon.png
keywords:

View File

@@ -8,7 +8,7 @@ priority: optional
section: x11
installed_size: 6604
essential: false
icon: ./assets/images/launch_icon.png
icon: ./assets/images/icon.png
keywords:
- FlClash

Binary file not shown.

Before

Width:  |  Height:  |  Size: 222 KiB

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 645 B

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@@ -317,10 +317,10 @@ packages:
dependency: transitive
description:
name: file_selector_windows
sha256: d3547240c20cabf205c7c7f01a50ecdbc413755814d6677f3cb366f04abcead0
sha256: "2ad726953f6e8affbc4df8dc78b77c3b4a060967a291e528ef72ae846c60fb69"
url: "https://pub.dev"
source: hosted
version: "0.9.3+1"
version: "0.9.3+2"
fixnum:
dependency: transitive
description:
@@ -377,10 +377,10 @@ packages:
dependency: "direct main"
description:
name: freezed_annotation
sha256: f54946fdb1fa7b01f780841937b1a80783a20b393485f3f6cdf336fd6f4705f2
sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2
url: "https://pub.dev"
source: hosted
version: "2.4.2"
version: "2.4.4"
frontend_server_client:
dependency: transitive
description:
@@ -401,10 +401,10 @@ packages:
dependency: transitive
description:
name: graphs
sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19
sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0"
url: "https://pub.dev"
source: hosted
version: "2.3.1"
version: "2.3.2"
gtk:
dependency: transitive
description:
@@ -417,10 +417,10 @@ packages:
dependency: transitive
description:
name: http
sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938"
sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010
url: "https://pub.dev"
source: hosted
version: "1.2.1"
version: "1.2.2"
http_multi_server:
dependency: transitive
description:
@@ -457,10 +457,10 @@ packages:
dependency: transitive
description:
name: image_picker_android
sha256: "4161e1f843d8480d2e9025ee22411778c3c9eb7e40076dcf2da23d8242b7b51c"
sha256: a26dc9a03fe042440c1e4be554fb0fceae2bf6d887d7467fc48c688fa4a81889
url: "https://pub.dev"
source: hosted
version: "0.8.12+3"
version: "0.8.12+7"
image_picker_for_web:
dependency: transitive
description:
@@ -729,10 +729,10 @@ packages:
dependency: transitive
description:
name: path_provider_android
sha256: bca87b0165ffd7cdb9cad8edd22d18d2201e886d9a9f19b4fb3452ea7df3a72a
sha256: "30c5aa827a6ae95ce2853cdc5fe3971daaac00f6f081c419c013f7f57bff2f5e"
url: "https://pub.dev"
source: hosted
version: "2.2.6"
version: "2.2.7"
path_provider_foundation:
dependency: transitive
description:
@@ -761,10 +761,10 @@ packages:
dependency: transitive
description:
name: path_provider_windows
sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170"
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev"
source: hosted
version: "2.2.1"
version: "2.3.0"
petitparser:
dependency: transitive
description:
@@ -904,10 +904,10 @@ packages:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b"
sha256: "034650b71e73629ca08a0bd789fd1d83cc63c2d1e405946f7cef7bc37432f93a"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
version: "2.4.0"
shared_preferences_web:
dependency: transitive
description:
@@ -1061,18 +1061,18 @@ packages:
dependency: transitive
description:
name: url_launcher_android
sha256: ceb2625f0c24ade6ef6778d1de0b2e44f2db71fded235eb52295247feba8c5cf
sha256: "95d8027db36a0e52caf55680f91e33ea6aa12a3ce608c90b06f4e429a21067ac"
url: "https://pub.dev"
source: hosted
version: "6.3.3"
version: "6.3.5"
url_launcher_ios:
dependency: transitive
description:
name: url_launcher_ios
sha256: "7068716403343f6ba4969b4173cbf3b84fc768042124bc2c011e5d782b24fe89"
sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e
url: "https://pub.dev"
source: hosted
version: "6.3.0"
version: "6.3.1"
url_launcher_linux:
dependency: transitive
description:
@@ -1109,10 +1109,10 @@ packages:
dependency: transitive
description:
name: url_launcher_windows
sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7
sha256: "49c10f879746271804767cb45551ec5592cdab00ee105c06dddde1a98f73b185"
url: "https://pub.dev"
source: hosted
version: "3.1.1"
version: "3.1.2"
vector_math:
dependency: transitive
description:
@@ -1149,18 +1149,18 @@ packages:
dependency: transitive
description:
name: web_socket
sha256: "24301d8c293ce6fe327ffe6f59d8fd8834735f0ec36e4fd383ec7ff8a64aa078"
sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83"
url: "https://pub.dev"
source: hosted
version: "0.1.5"
version: "0.1.6"
web_socket_channel:
dependency: transitive
description:
name: web_socket_channel
sha256: a2d56211ee4d35d9b344d9d4ce60f362e4f5d1aafb988302906bd732bc731276
sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
version: "3.0.1"
webdav_client:
dependency: "direct main"
description:

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