Support better window position memory

Add windows arm64 and linux arm64 build script

Optimize some details
This commit is contained in:
chen08209
2024-12-06 22:35:28 +08:00
parent ece8a48181
commit 375c4e0884
26 changed files with 353 additions and 157 deletions

View File

@@ -31,9 +31,10 @@
</td> </td>
</tr> </tr>
<tr> <tr>
<td>macOS (v10.15+)</td> <td>macOS</td>
<td> <td>
<a href="https://github.com/chen08209/FlClash/releases/download/vVERSION/FlClash-VERSION-macos-amd64.dmg"><img src="https://img.shields.io/badge/DMG-Universal-ea005e.svg?logo=apple"></a><br> <a href="https://github.com/chen08209/FlClash/releases/download/vVERSION/FlClash-VERSION-macos-arm64.dmg"><img src="https://img.shields.io/badge/DMG-Apple%20Silicon-%23000000.svg?logo=apple"></a><br>
<a href="https://github.com/chen08209/FlClash/releases/download/vVERSION/FlClash-VERSION-macos-amd64.dmg"><img src="https://img.shields.io/badge/DMG-Intel%20X64-%2300A9E0.svg?logo=apple"></a><br>
</td> </td>
</tr> </tr>
<tr> <tr>

View File

@@ -84,6 +84,70 @@ jobs:
path: ./dist path: ./dist
overwrite: true overwrite: true
changelog:
runs-on: ubuntu-latest
needs: [ build ]
steps:
- name: Checkout
if: ${{ !contains(github.ref, '+') }}
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: refs/heads/main
- name: Generate
if: ${{ !contains(github.ref, '+') }}
run: |
tags=($(git tag --merged $(git rev-parse HEAD) --sort=-creatordate))
preTag=$(grep -oP '^## \K.*' CHANGELOG.md | head -n 1)
currentTag=""
for ((i = 0; i <= ${#tags[@]}; i++)); do
if (( i < ${#tags[@]} )); then
tag=${tags[$i]}
else
tag=""
fi
if [ -n "$currentTag" ]; then
if [ "$(echo -e "$currentTag\n$preTag" | sort -V | head -n 1)" == "$currentTag" ]; then
break
fi
fi
if [ -n "$currentTag" ]; then
echo "## $currentTag" >> NEW_CHANGELOG.md
echo "" >> NEW_CHANGELOG.md
if [ -n "$tag" ]; then
git log --pretty=format:"%B" "$tag..$currentTag" | awk 'NF {print "- " $0} !NF {print ""}' >> NEW_CHANGELOG.md
else
git log --pretty=format:"%B" "$currentTag" | awk 'NF {print "- " $0} !NF {print ""}' >> NEW_CHANGELOG.md
fi
echo "" >> NEW_CHANGELOG.md
fi
currentTag=$tag
done
cat CHANGELOG.md >> NEW_CHANGELOG.md
cat NEW_CHANGELOG.md > CHANGELOG.md
- name: Commit
if: ${{ !contains(github.ref, '+') }}
run: |
git add CHANGELOG.md
if ! git diff --cached --quiet; then
echo "Commit pushing"
git config --local user.email "chen08209@gmail.com"
git config --local user.name "chen08209"
git commit -m "Update changelog"
git push
if [ $? -eq 0 ]; then
echo "Push succeeded"
else
echo "Push failed"
exit 1
fi
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
upload: upload:
permissions: write-all permissions: write-all
needs: [ build ] needs: [ build ]
@@ -177,4 +241,5 @@ jobs:
user-email: 'github-actions[bot]@users.noreply.github.com' user-email: 'github-actions[bot]@users.noreply.github.com'
target-branch: action-pr target-branch: action-pr
commit-message: Update from ${{ github.ref_name }} commit-message: Update from ${{ github.ref_name }}
target-directory: /tmp/ target-directory: /tmp/

View File

@@ -1,66 +0,0 @@
name: changelog
on:
push:
tags:
- 'v*'
jobs:
changelog:
if: ${{ !contains(github.ref, '+') }}
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Generate
run: |
tags=($(git tag --merged $(git rev-parse HEAD) --sort=-creatordate))
preTag=$(grep -oP '^## \K.*' CHANGELOG.md | head -n 1)
currentTag=""
for ((i = 0; i <= ${#tags[@]}; i++)); do
if (( i < ${#tags[@]} )); then
tag=${tags[$i]}
else
tag=""
fi
if [ -n "$currentTag" ]; then
if [ "$(echo -e "$currentTag\n$preTag" | sort -V | head -n 1)" == "$currentTag" ]; then
break
fi
fi
if [ -n "$currentTag" ]; then
echo "## $currentTag" >> NEW_CHANGELOG.md
echo "" >> NEW_CHANGELOG.md
if [ -n "$tag" ]; then
git log --pretty=format:"%B" "$tag..$currentTag" | awk 'NF {print "- " $0} !NF {print ""}' >> NEW_CHANGELOG.md
else
git log --pretty=format:"%B" "$currentTag" | awk 'NF {print "- " $0} !NF {print ""}' >> NEW_CHANGELOG.md
fi
echo "" >> NEW_CHANGELOG.md
fi
currentTag=$tag
done
cat CHANGELOG.md >> NEW_CHANGELOG.md
cat NEW_CHANGELOG.md > CHANGELOG.md
- name: Commit
run: |
git add CHANGELOG.md
if ! git diff --cached --quiet; then
echo "Commit pushing"
git config --local user.email "chen08209@gmail.com"
git config --local user.name "chen08209"
git commit -m "Update changelog"
git push
if [ $? -eq 0 ]; then
echo "Push succeeded"
else
echo "Push failed"
exit 1
fi
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -1,3 +1,37 @@
## v0.8.69
- Remake desktop
- Optimize change proxy
- Optimize network check
- Fix fallback issues
- Optimize lots of details
- Update change.yaml
- Fix android tile issues
- Fix windows tray issues
- Support setting bypassDomain
- Update flutter version
- Fix android service issues
- Fix macos dock exit button issues
- Add route address setting
- Optimize provider view
- Update changelog
- Update CHANGELOG.md
## v0.8.67 ## v0.8.67
- Add android shortcuts - Add android shortcuts

View File

@@ -34,6 +34,29 @@ on Mobile:
✨ Support subscription link, Dark mode ✨ Support subscription link, Dark mode
## Use
### Linux
⚠️ Make sure to install the following dependencies before using them
```bash
sudo apt-get install appindicator3-0.1 libappindicator3-dev
sudo apt-get install keybinder-3.0
```
### Android
Support the following actions
```bash
com.follow.clash.action.START
com.follow.clash.action.STOP
com.follow.clash.action.CHANGE
```
## Download ## Download
<a href="https://chen08209.github.io/FlClash-fdroid-repo/repo?fingerprint=789D6D32668712EF7672F9E58DEEB15FBD6DCEEC5AE7A4371EA72F2AAE8A12FD"><img alt="Get it on F-Droid" src="snapshots/get-it-on-fdroid.svg" width="200px"/></a> <a href="https://github.com/chen08209/FlClash/releases"><img alt="Get it on GitHub" src="snapshots/get-it-on-github.svg" width="200px"/></a> <a href="https://chen08209.github.io/FlClash-fdroid-repo/repo?fingerprint=789D6D32668712EF7672F9E58DEEB15FBD6DCEEC5AE7A4371EA72F2AAE8A12FD"><img alt="Get it on F-Droid" src="snapshots/get-it-on-fdroid.svg" width="200px"/></a> <a href="https://github.com/chen08209/FlClash/releases"><img alt="Get it on GitHub" src="snapshots/get-it-on-github.svg" width="200px"/></a>
@@ -70,7 +93,7 @@ on Mobile:
3. Run build script 3. Run build script
```bash ```bash
dart .\setup.dart dart .\setup.dart windows --arch <arm64 | amd64>
``` ```
- linux - linux
@@ -80,7 +103,7 @@ on Mobile:
2. Run build script 2. Run build script
```bash ```bash
dart .\setup.dart dart .\setup.dart linux --arch <arm64 | amd64>
``` ```
- macOS - macOS
@@ -90,7 +113,7 @@ on Mobile:
2. Run build script 2. Run build script
```bash ```bash
dart .\setup.dart dart .\setup.dart macos --arch <arm64 | amd64>
``` ```
## Star ## Star

View File

@@ -10,7 +10,6 @@
[![Channel](https://img.shields.io/badge/Telegram-Channel-blue?style=flat-square&logo=telegram)](https://t.me/FlClash) [![Channel](https://img.shields.io/badge/Telegram-Channel-blue?style=flat-square&logo=telegram)](https://t.me/FlClash)
基于ClashMeta的多平台代理客户端简单易用开源无广告。 基于ClashMeta的多平台代理客户端简单易用开源无广告。
on Desktop: on Desktop:
@@ -35,6 +34,29 @@ on Mobile:
✨ 支持一键导入订阅, 深色模式 ✨ 支持一键导入订阅, 深色模式
## Use
### Linux
⚠️ 使用前请确保安装以下依赖
```bash
sudo apt-get install appindicator3-0.1 libappindicator3-dev
sudo apt-get install keybinder-3.0
```
### Android
支持下列操作
```bash
com.follow.clash.action.START
com.follow.clash.action.STOP
com.follow.clash.action.CHANGE
```
## Download ## Download
<a href="https://chen08209.github.io/FlClash-fdroid-repo/repo?fingerprint=789D6D32668712EF7672F9E58DEEB15FBD6DCEEC5AE7A4371EA72F2AAE8A12FD"><img alt="Get it on F-Droid" src="snapshots/get-it-on-fdroid.svg" width="200px"/></a> <a href="https://github.com/chen08209/FlClash/releases"><img alt="Get it on GitHub" src="snapshots/get-it-on-github.svg" width="200px"/></a> <a href="https://chen08209.github.io/FlClash-fdroid-repo/repo?fingerprint=789D6D32668712EF7672F9E58DEEB15FBD6DCEEC5AE7A4371EA72F2AAE8A12FD"><img alt="Get it on F-Droid" src="snapshots/get-it-on-fdroid.svg" width="200px"/></a> <a href="https://github.com/chen08209/FlClash/releases"><img alt="Get it on GitHub" src="snapshots/get-it-on-github.svg" width="200px"/></a>
@@ -71,7 +93,7 @@ on Mobile:
3. 运行构建脚本 3. 运行构建脚本
```bash ```bash
dart .\setup.dart dart .\setup.dart windows --arch <arm64 | amd64>
``` ```
- linux - linux
@@ -81,7 +103,7 @@ on Mobile:
2. 运行构建脚本 2. 运行构建脚本
```bash ```bash
dart .\setup.dart dart .\setup.dart linux --arch <arm64 | amd64>
``` ```
- macOS - macOS
@@ -91,7 +113,7 @@ on Mobile:
2. 运行构建脚本 2. 运行构建脚本
```bash ```bash
dart .\setup.dart dart .\setup.dart macos --arch <arm64 | amd64>
``` ```
## Star History ## Star History

View File

@@ -72,6 +72,10 @@
android:name=".TempActivity" android:name=".TempActivity"
android:exported="true" android:exported="true"
android:theme="@style/TransparentTheme"> android:theme="@style/TransparentTheme">
<intent-filter>
<category android:name="android.intent.category.DEFAULT" />
<action android:name="${applicationId}.action.START" />
</intent-filter>
<intent-filter> <intent-filter>
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<action android:name="${applicationId}.action.STOP" /> <action android:name="${applicationId}.action.STOP" />
@@ -88,7 +92,8 @@
android:foregroundServiceType="specialUse" android:foregroundServiceType="specialUse"
android:icon="@drawable/ic_stat_name" android:icon="@drawable/ic_stat_name"
android:label="FlClash" android:label="FlClash"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"> android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
tools:targetApi="n">
<intent-filter> <intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" /> <action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter> </intent-filter>

View File

@@ -20,8 +20,6 @@ enum class RunState {
object GlobalState { object GlobalState {
private val lock = ReentrantLock()
val runLock = ReentrantLock() val runLock = ReentrantLock()
val runState: MutableLiveData<RunState> = MutableLiveData<RunState>(RunState.STOP) val runState: MutableLiveData<RunState> = MutableLiveData<RunState>(RunState.STOP)
@@ -47,35 +45,46 @@ object GlobalState {
} }
fun handleToggle(context: Context) { fun handleToggle(context: Context) {
val starting = handleStart(context)
if (!starting) {
handleStop()
}
}
fun handleStart(context: Context): Boolean {
if (runState.value == RunState.STOP) { if (runState.value == RunState.STOP) {
runState.value = RunState.PENDING runState.value = RunState.PENDING
runLock.lock()
val tilePlugin = getCurrentTilePlugin() val tilePlugin = getCurrentTilePlugin()
if (tilePlugin != null) { if (tilePlugin != null) {
tilePlugin.handleStart() tilePlugin.handleStart()
} else { } else {
initServiceEngine(context) initServiceEngine(context)
} }
} else { return true
handleStop()
} }
return false
} }
fun handleStop() { fun handleStop() {
if (runState.value == RunState.START) { if (runState.value == RunState.START) {
runState.value = RunState.PENDING runState.value = RunState.PENDING
runLock.lock()
getCurrentTilePlugin()?.handleStop() getCurrentTilePlugin()?.handleStop()
} }
} }
fun destroyServiceEngine() { fun destroyServiceEngine() {
serviceEngine?.destroy() runLock.withLock {
serviceEngine = null serviceEngine?.destroy()
serviceEngine = null
}
} }
fun initServiceEngine(context: Context) { fun initServiceEngine(context: Context) {
if (serviceEngine != null) return if (serviceEngine != null) return
lock.withLock { destroyServiceEngine()
destroyServiceEngine() runLock.withLock {
serviceEngine = FlutterEngine(context) serviceEngine = FlutterEngine(context)
serviceEngine?.plugins?.add(VpnPlugin()) serviceEngine?.plugins?.add(VpnPlugin())
serviceEngine?.plugins?.add(AppPlugin()) serviceEngine?.plugins?.add(AppPlugin())

View File

@@ -8,6 +8,10 @@ class TempActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
when (intent.action) { when (intent.action) {
wrapAction("START") -> {
GlobalState.handleStart(applicationContext)
}
wrapAction("STOP") -> { wrapAction("STOP") -> {
GlobalState.handleStop() GlobalState.handleStop()
} }

View File

@@ -23,6 +23,7 @@ import java.io.ByteArrayOutputStream
import java.net.Inet4Address import java.net.Inet4Address
import java.net.Inet6Address import java.net.Inet6Address
import java.net.InetAddress import java.net.InetAddress
import java.util.concurrent.locks.ReentrantLock
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
@@ -179,4 +180,19 @@ suspend fun <T> MethodChannel.awaitResult(
} }
}) })
} }
}
fun ReentrantLock.safeLock() {
if (this.isLocked) {
return
}
this.lock()
}
fun ReentrantLock.safeUnlock() {
if (!this.isLocked) {
return
}
this.unlock()
} }

View File

@@ -46,8 +46,6 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
private var activity: Activity? = null private var activity: Activity? = null
private var toast: Toast? = null
private lateinit var context: Context private lateinit var context: Context
private lateinit var channel: MethodChannel private lateinit var channel: MethodChannel

View File

@@ -16,6 +16,8 @@ import com.follow.clash.GlobalState
import com.follow.clash.RunState import com.follow.clash.RunState
import com.follow.clash.extensions.getProtocol import com.follow.clash.extensions.getProtocol
import com.follow.clash.extensions.resolveDns import com.follow.clash.extensions.resolveDns
import com.follow.clash.models.Process
import com.follow.clash.models.VpnOptions
import com.follow.clash.services.FlClashService import com.follow.clash.services.FlClashService
import com.follow.clash.services.FlClashVpnService import com.follow.clash.services.FlClashVpnService
import com.google.gson.Gson import com.google.gson.Gson
@@ -28,8 +30,6 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.net.InetSocketAddress import java.net.InetSocketAddress
import kotlin.concurrent.withLock import kotlin.concurrent.withLock
import com.follow.clash.models.Process
import com.follow.clash.models.VpnOptions
class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler { class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
@@ -111,11 +111,9 @@ class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
"resolverProcess" -> { "resolverProcess" -> {
val data = call.argument<String>("data") val data = call.argument<String>("data")
val process = val process = if (data != null) Gson().fromJson(
if (data != null) Gson().fromJson( data, Process::class.java
data, ) else null
Process::class.java
) else null
val metadata = process?.metadata val metadata = process?.metadata
if (metadata == null) { if (metadata == null) {
result.success(null) result.success(null)
@@ -173,9 +171,7 @@ class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
fun onUpdateNetwork() { fun onUpdateNetwork() {
val dns = networks.flatMap { network -> val dns = networks.flatMap { network ->
connectivity?.resolveDns(network) ?: emptyList() connectivity?.resolveDns(network) ?: emptyList()
} }.toSet().joinToString(",")
.toSet()
.joinToString(",")
scope.launch { scope.launch {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
flutterMethodChannel.invokeMethod("dnsChanged", dns) flutterMethodChannel.invokeMethod("dnsChanged", dns)
@@ -239,8 +235,7 @@ class VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
GlobalState.runState.value = RunState.START GlobalState.runState.value = RunState.START
val fd = flClashService?.start(options) val fd = flClashService?.start(options)
flutterMethodChannel.invokeMethod( flutterMethodChannel.invokeMethod(
"started", "started", fd
fd
) )
} }
} }

View File

@@ -116,13 +116,11 @@ class Other {
return "assets/images/icon_white.png"; return "assets/images/icon_white.png";
} }
final suffix = Platform.isWindows ? "ico" : "png"; final suffix = Platform.isWindows ? "ico" : "png";
if (Platform.isWindows) { return "assets/images/icon.$suffix";
return "assets/images/icon.$suffix"; // return switch (brightness) {
} // Brightness.dark => "assets/images/icon_white.$suffix",
return switch (brightness) { // Brightness.light => "assets/images/icon_black.$suffix",
Brightness.dark => "assets/images/icon_white.$suffix", // };
Brightness.light => "assets/images/icon_black.$suffix",
};
} }
int compareVersions(String version1, String version2) { int compareVersions(String version1, String version2) {

View File

@@ -78,7 +78,7 @@ class Tray {
MenuItem.checkbox( MenuItem.checkbox(
label: Intl.message(mode.name), label: Intl.message(mode.name),
onClick: (_) { onClick: (_) {
globalState.appController.clashConfig.mode = mode; globalState.appController.changeMode(mode);
}, },
checked: mode == clashConfig.mode, checked: mode == clashConfig.mode,
), ),

View File

@@ -3,6 +3,7 @@ import 'dart:io';
import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/models/config.dart'; import 'package:fl_clash/models/config.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:screen_retriever/screen_retriever.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
class Window { class Window {
@@ -23,6 +24,35 @@ class Window {
); );
if (!Platform.isMacOS || version > 10) { if (!Platform.isMacOS || version > 10) {
await windowManager.setTitleBarStyle(TitleBarStyle.hidden); await windowManager.setTitleBarStyle(TitleBarStyle.hidden);
final left = props.left ?? 0;
final top = props.top ?? 0;
final right = left + props.width;
final bottom = top + props.height;
if (left == 0 && top == 0) {
await windowManager.setAlignment(Alignment.center);
} else {
final displays = await screenRetriever.getAllDisplays();
final isPositionValid = displays.any(
(display) {
final displayBounds = Rect.fromLTWH(
display.visiblePosition!.dx,
display.visiblePosition!.dy,
display.size.width,
display.size.height,
);
return displayBounds.contains(Offset(left, top)) ||
displayBounds.contains(Offset(right, bottom));
},
);
if (isPositionValid) {
await windowManager.setPosition(
Offset(
left,
top,
),
);
}
}
} }
await windowManager.waitUntilReadyToShow(windowOptions, () async { await windowManager.waitUntilReadyToShow(windowOptions, () async {
await windowManager.setPreventClose(true); await windowManager.setPreventClose(true);

View File

@@ -582,6 +582,14 @@ class AppController {
updateStatus(!appFlowingState.isStart); updateStatus(!appFlowingState.isStart);
} }
changeMode(Mode mode) {
clashConfig.mode = mode;
if (mode == Mode.global) {
config.updateCurrentGroupName(GroupName.GLOBAL.name);
}
addCheckIpNumDebounce();
}
updateAutoLaunch() { updateAutoLaunch() {
config.appSetting = config.appSetting.copyWith( config.appSetting = config.appSetting.copyWith(
autoLaunch: !config.appSetting.autoLaunch, autoLaunch: !config.appSetting.autoLaunch,

View File

@@ -38,6 +38,9 @@ class _ConnectionsFragmentState extends State<ConnectionsFragment> {
timer = Timer.periodic( timer = Timer.periodic(
const Duration(seconds: 1), const Duration(seconds: 1),
(timer) async { (timer) async {
if (!context.mounted) {
return;
}
connectionsNotifier.value = connectionsNotifier.value.copyWith( connectionsNotifier.value = connectionsNotifier.value.copyWith(
connections: await clashCore.getConnections(), connections: await clashCore.getConnections(),
); );

View File

@@ -10,14 +10,6 @@ import 'package:provider/provider.dart';
class OutboundMode extends StatelessWidget { class OutboundMode extends StatelessWidget {
const OutboundMode({super.key}); const OutboundMode({super.key});
_changeMode(BuildContext context, Mode? value) async {
final appController = globalState.appController;
final clashConfig = appController.clashConfig;
if (value == null || clashConfig.mode == value) return;
clashConfig.mode = value;
appController.addCheckIpNumDebounce();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Selector<ClashConfig, Mode>( return Selector<ClashConfig, Mode>(
@@ -50,7 +42,10 @@ class OutboundMode extends StatelessWidget {
value: item, value: item,
groupValue: mode, groupValue: mode,
onChanged: (value) async { onChanged: (value) async {
_changeMode(context, value); if (value == null) {
return;
}
globalState.appController.changeMode(value);
}, },
), ),
title: Text( title: Text(

View File

@@ -299,7 +299,7 @@ class _ProxiesListFragmentState extends State<ProxiesListFragment> {
headerState.currentIndex > state.groupNames.length - 1 headerState.currentIndex > state.groupNames.length - 1
? 0 ? 0
: headerState.currentIndex; : headerState.currentIndex;
if (index < 0) { if (index < 0 || state.groupNames.isEmpty) {
return Container(); return Container();
} }
return Stack( return Stack(

View File

@@ -117,9 +117,11 @@ class ProxiesTabFragmentState extends State<ProxiesTabFragment>
} }
final currentGroup = currentGroups[index ?? _tabController!.index]; final currentGroup = currentGroups[index ?? _tabController!.index];
currentProxies = currentGroup.all; currentProxies = currentGroup.all;
appController.config.updateCurrentGroupName( WidgetsBinding.instance.addPostFrameCallback((_) {
currentGroup.name, appController.config.updateCurrentGroupName(
); currentGroup.name,
);
});
} }
_destroyTabController() { _destroyTabController() {
@@ -129,6 +131,10 @@ class ProxiesTabFragmentState extends State<ProxiesTabFragment>
} }
_updateTabController(int length, int index) { _updateTabController(int length, int index) {
if (length == 0) {
_destroyTabController();
return;
}
final realIndex = index == -1 ? 0 : index; final realIndex = index == -1 ? 0 : index;
_tabController ??= TabController( _tabController ??= TabController(
length: length, length: length,
@@ -162,6 +168,9 @@ class ProxiesTabFragmentState extends State<ProxiesTabFragment>
(item) => item == state.currentGroupName, (item) => item == state.currentGroupName,
); );
_updateTabController(state.groupNames.length, index); _updateTabController(state.groupNames.length, index);
if (state.groupNames.isEmpty) {
return Container();
}
final GroupNameKeyMap keyMap = {}; final GroupNameKeyMap keyMap = {};
final children = state.groupNames.map((groupName) { final children = state.groupNames.map((groupName) {
keyMap[groupName] = GlobalObjectKey(groupName); keyMap[groupName] = GlobalObjectKey(groupName);
@@ -281,12 +290,15 @@ class ProxyGroupViewState extends State<ProxyGroupView> {
if (_controller.position.maxScrollExtent == 0) { if (_controller.position.maxScrollExtent == 0) {
return; return;
} }
final sortedProxies = globalState.appController.getSortProxies(
currentProxies,
);
_controller.animateTo( _controller.animateTo(
min( min(
16 + 16 +
getScrollToSelectedOffset( getScrollToSelectedOffset(
groupName: groupName, groupName: groupName,
proxies: currentProxies, proxies: sortedProxies,
), ),
_controller.position.maxScrollExtent, _controller.position.maxScrollExtent,
), ),

View File

@@ -67,6 +67,12 @@ class _WindowContainerState extends State<WindowManager>
@override @override
Future<void> onWindowMoved() async { Future<void> onWindowMoved() async {
super.onWindowMoved(); super.onWindowMoved();
final offset = await windowManager.getPosition();
final config = globalState.appController.config;
config.windowProps = config.windowProps.copyWith(
top: offset.dy,
left: offset.dx,
);
} }
@override @override

View File

@@ -212,9 +212,7 @@ class AppState with ChangeNotifier {
case Mode.direct: case Mode.direct:
return []; return [];
case Mode.global: case Mode.global:
return groups return groups.toList();
.where((element) => element.name == GroupName.GLOBAL.name)
.toList();
case Mode.rule: case Mode.rule:
return groups return groups
.where((item) => item.hidden == false) .where((item) => item.hidden == false)

View File

@@ -1002,7 +1002,7 @@ packages:
source: hosted source: hosted
version: "0.28.0" version: "0.28.0"
screen_retriever: screen_retriever:
dependency: transitive dependency: "direct main"
description: description:
name: screen_retriever name: screen_retriever
sha256: "570dbc8e4f70bac451e0efc9c9bb19fa2d6799a11e6ef04f946d7886d2e23d0c" sha256: "570dbc8e4f70bac451e0efc9c9bb19fa2d6799a11e6ef04f946d7886d2e23d0c"

View File

@@ -1,7 +1,7 @@
name: fl_clash name: fl_clash
description: A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free. description: A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free.
publish_to: 'none' publish_to: 'none'
version: 0.8.69+202412061 version: 0.8.70+202412091
environment: environment:
sdk: '>=3.1.0 <4.0.0' sdk: '>=3.1.0 <4.0.0'
@@ -52,6 +52,7 @@ dependencies:
uni_platform: ^0.1.3 uni_platform: ^0.1.3
device_info_plus: ^10.1.2 device_info_plus: ^10.1.2
connectivity_plus: ^6.1.0 connectivity_plus: ^6.1.0
screen_retriever: ^0.2.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter

View File

@@ -21,6 +21,22 @@ extension TargetExt on Target {
return name; return name;
} }
bool get same {
if (this == Target.android) {
return true;
}
if (Platform.isWindows && this == Target.windows) {
return true;
}
if (Platform.isLinux && this == Target.linux) {
return true;
}
if (Platform.isMacOS && this == Target.macos) {
return true;
}
return false;
}
String get dynamicLibExtensionName { String get dynamicLibExtensionName {
final String extensionName; final String extensionName;
switch (this) { switch (this) {
@@ -76,12 +92,27 @@ class Build {
static List<BuildItem> get buildItems => [ static List<BuildItem> get buildItems => [
BuildItem( BuildItem(
target: Target.macos, target: Target.macos,
arch: Arch.arm64,
), ),
BuildItem( BuildItem(
target: Target.windows, target: Target.macos,
arch: Arch.amd64,
), ),
BuildItem( BuildItem(
target: Target.linux, target: Target.linux,
arch: Arch.arm64,
),
BuildItem(
target: Target.linux,
arch: Arch.amd64,
),
BuildItem(
target: Target.windows,
arch: Arch.amd64,
),
BuildItem(
target: Target.windows,
arch: Arch.arm64,
), ),
BuildItem( BuildItem(
target: Target.android, target: Target.android,
@@ -200,11 +231,10 @@ class Build {
final Map<String, String> env = {}; final Map<String, String> env = {};
env["GOOS"] = item.target.os; env["GOOS"] = item.target.os;
if (item.arch != null) {
env["GOARCH"] = item.arch!.name;
}
if (isLib) { if (isLib) {
if (item.arch != null) {
env["GOARCH"] = item.arch!.name;
}
env["CGO_ENABLED"] = "1"; env["CGO_ENABLED"] = "1";
env["CC"] = _getCc(item); env["CC"] = _getCc(item);
env["CFLAGS"] = "-O3 -Werror"; env["CFLAGS"] = "-O3 -Werror";
@@ -272,6 +302,11 @@ class Build {
Build.getExecutable("flutter clean"), Build.getExecutable("flutter clean"),
workingDirectory: distributorDir, workingDirectory: distributorDir,
); );
await exec(
name: "upgrade distributor",
Build.getExecutable("flutter pub upgrade"),
workingDirectory: distributorDir,
);
await exec( await exec(
name: "get distributor", name: "get distributor",
Build.getExecutable("dart pub global activate -s path $distributorDir"), Build.getExecutable("dart pub global activate -s path $distributorDir"),
@@ -303,7 +338,7 @@ class BuildCommand extends Command {
BuildCommand({ BuildCommand({
required this.target, required this.target,
}) { }) {
if (target == Target.android) { if (target == Target.android || target == Target.linux) {
argParser.addOption( argParser.addOption(
"arch", "arch",
valueHelp: arches.map((e) => e.name).join(','), valueHelp: arches.map((e) => e.name).join(','),
@@ -315,11 +350,10 @@ class BuildCommand extends Command {
help: 'The $name build archName', help: 'The $name build archName',
); );
} }
argParser.addOption( argParser.addOption(
"out", "out",
valueHelp: [ valueHelp: [
"app", if (target.same) "app",
"core", "core",
].join(','), ].join(','),
help: 'The $name build arch', help: 'The $name build arch',
@@ -337,7 +371,7 @@ class BuildCommand extends Command {
.map((e) => e.arch!) .map((e) => e.arch!)
.toList(); .toList();
_getLinuxDependencies() async { _getLinuxDependencies(Arch arch) async {
await Build.exec( await Build.exec(
Build.getExecutable("sudo apt update -y"), Build.getExecutable("sudo apt update -y"),
); );
@@ -351,7 +385,7 @@ class BuildCommand extends Command {
Build.getExecutable("sudo apt install -y rpm patchelf"), Build.getExecutable("sudo apt install -y rpm patchelf"),
); );
await Build.exec( await Build.exec(
Build.getExecutable("sudo apt-get install -y libkeybinder-3.0"), Build.getExecutable("sudo apt-get install -y libkeybinder-3.0-dev"),
); );
await Build.exec( await Build.exec(
Build.getExecutable("sudo apt install -y locate"), Build.getExecutable("sudo apt install -y locate"),
@@ -359,9 +393,10 @@ class BuildCommand extends Command {
await Build.exec( await Build.exec(
Build.getExecutable("sudo apt install -y libfuse2"), Build.getExecutable("sudo apt install -y libfuse2"),
); );
final downloadName = arch == Arch.amd64 ? "x86_64" : "aarch_64";
await Build.exec( await Build.exec(
Build.getExecutable( Build.getExecutable(
"wget -O appimagetool https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage", "wget -O appimagetool https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-$downloadName.AppImage",
), ),
); );
await Build.exec( await Build.exec(
@@ -409,14 +444,14 @@ class BuildCommand extends Command {
@override @override
Future<void> run() async { Future<void> run() async {
final mode = target == Target.android ? Mode.lib : Mode.core; final mode = target == Target.android ? Mode.lib : Mode.core;
final String out = argResults?['out'] ?? 'app'; final String out = argResults?["out"] ?? (target.same ? "app" : "core");
Arch? arch; final archName = argResults?["arch"];
var archName = argResults?['arch']; final currentArches =
arches.where((element) => element.name == archName).toList();
final arch = currentArches.isEmpty ? null : currentArches.first;
if (target == Target.android) { if (arch == null && target != Target.android) {
final currentArches = throw "Invalid arch parameter";
arches.where((element) => element.name == archName).toList();
arch = currentArches.isEmpty ? null : currentArches.first;
} }
await Build.buildCore( await Build.buildCore(
@@ -440,13 +475,21 @@ class BuildCommand extends Command {
targets: "exe,zip", targets: "exe,zip",
args: "--description $archName", args: "--description $archName",
); );
return;
case Target.linux: case Target.linux:
await _getLinuxDependencies(); final targetMap = {
Arch.arm64: "linux-arm64",
Arch.amd64: "linux-x64",
};
final defaultTarget = targetMap[arch];
await _getLinuxDependencies(arch!);
_buildDistributor( _buildDistributor(
target: target, target: target,
targets: "appimage,deb,rpm", targets: "appimage,deb",
args: "--description $archName", args:
"--description $archName --build-target-platform $defaultTarget",
); );
return;
case Target.android: case Target.android:
final targetMap = { final targetMap = {
Arch.arm: "android-arm", Arch.arm: "android-arm",
@@ -464,6 +507,7 @@ class BuildCommand extends Command {
args: args:
"--flutter-build-args split-per-abi --build-target-platform ${defaultTargets.join(",")}", "--flutter-build-args split-per-abi --build-target-platform ${defaultTargets.join(",")}",
); );
return;
case Target.macos: case Target.macos:
await _getMacosDependencies(); await _getMacosDependencies();
_buildDistributor( _buildDistributor(
@@ -471,6 +515,7 @@ class BuildCommand extends Command {
targets: "dmg", targets: "dmg",
args: "--description $archName", args: "--description $archName",
); );
return;
} }
} }
} }
@@ -478,14 +523,8 @@ class BuildCommand extends Command {
main(args) async { main(args) async {
final runner = CommandRunner("setup", "build Application"); final runner = CommandRunner("setup", "build Application");
runner.addCommand(BuildCommand(target: Target.android)); runner.addCommand(BuildCommand(target: Target.android));
if (Platform.isWindows) { runner.addCommand(BuildCommand(target: Target.linux));
runner.addCommand(BuildCommand(target: Target.windows)); runner.addCommand(BuildCommand(target: Target.windows));
} runner.addCommand(BuildCommand(target: Target.macos));
if (Platform.isLinux) {
runner.addCommand(BuildCommand(target: Target.linux));
}
if (Platform.isMacOS) {
runner.addCommand(BuildCommand(target: Target.macos));
}
runner.run(args); runner.run(args);
} }