fix: MacBook Pro端iOS/macOS构建修复 - win32 6.x兼容+Swift 6修复
- file_picker升级到12.x兼容win32 6.x - 新增scripts/patch_pub_cache.sh补丁脚本(quill_native_bridge_windows+flutter_vibrate) - bitsdojo_window→window_manager代码迁移 - 更新iOS_macOS_Developer_Guide.md v6(新增§2.6 pub cache补丁文档) - 更新CHANGELOG.md v6.9.51
This commit is contained in:
@@ -122,7 +122,7 @@ Error: The getter 'ohos' isn't defined for the class 'TargetPlatform'
|
||||
| url_launcher | ^6.3.2 | 打开外部URL/应用 |
|
||||
| app_links | ^7.0.0 | 深度链接处理 |
|
||||
| home_widget | ^0.9.1 | iOS/Android桌面小组件 |
|
||||
| file_picker | ^11.0.0 | 文件选择器(⚠️ API变更,见§2.5) |
|
||||
| file_picker | ^12.0.0-beta.5 | 文件选择器(⚠️ 12.x兼容win32 6.x,见§2.6) |
|
||||
| image_picker | ^1.2.2 | 相机/相册选图 |
|
||||
| share_plus | ^13.1.0 | 系统分享面板 |
|
||||
| gal | ^2.3.0 | 保存图片/视频到相册 |
|
||||
@@ -152,8 +152,8 @@ Error: The getter 'ohos' isn't defined for the class 'TargetPlatform'
|
||||
| speech_to_text | ^7.0.0 | 语音转文字 |
|
||||
| live_activities | ^2.0.0 | 灵动岛/实时活动 |
|
||||
| flutter_vibrate | git引用 | 跨平台触觉反馈(⚠️ gitcode引用,见§2.5) |
|
||||
| bitsdojo_window | - | ❌ 已移除,替换为 window_manager |
|
||||
| window_manager | ^0.5.1 | 桌面端窗口管理(替代 bitsdojo_window) |
|
||||
| bitsdojo_window | - | ❌ 已移除,替换为 window_manager(lib/代码已迁移完成) |
|
||||
| window_manager | ^0.5.1 | 桌面端窗口管理(替代 bitsdojo_window,已集成) |
|
||||
|
||||
#### 2.2.2 dependency_overrides 区域 — 整体替换
|
||||
|
||||
@@ -249,18 +249,25 @@ echo "pubspec.yaml" >> .git/info/exclude
|
||||
> 远程版本可能不包含此类型,需要检查兼容性。
|
||||
> 如遇编译错误,可恢复使用本地包版本(`path: packages/pro_image_editor`)。
|
||||
|
||||
#### 2.5.2 file_picker(API 变更)
|
||||
#### 2.5.2 file_picker(API 变更 + win32 6.x 兼容)
|
||||
|
||||
`file_picker ^11.0.0` 的 API 发生了变更:
|
||||
`file_picker` 已从 `^11.0.0` 升级为 `^12.0.0-beta.5`,原因:
|
||||
|
||||
1. **API 变更**(11.x → 12.x 通用):
|
||||
|
||||
```dart
|
||||
// ❌ 旧版 API(file_picker ^8.x)
|
||||
final result = await FilePicker.platform.pickFiles();
|
||||
|
||||
// ✅ 新版 API(file_picker ^11.x)
|
||||
// ✅ 新版 API(file_picker ^11.x / ^12.x)
|
||||
final result = await FilePicker.pickFiles();
|
||||
```
|
||||
|
||||
2. **win32 6.x 兼容**(12.x 专属):
|
||||
- `file_picker ^11.0.0` 依赖 `win32: ^5.9.0`,与 `dependency_overrides` 中的 `win32: ^6.0.1` 冲突
|
||||
- `file_picker ^12.0.0-beta.5` 兼容 `win32 ^6.0.1`,解决 macOS 构建失败
|
||||
- ⚠️ 12.x 为 beta 版本,如遇问题可回退到 11.x(需同时降级 win32 override,见§2.6)
|
||||
|
||||
项目代码已更新为新版 API。鸿蒙端如使用本地包版本(`file_picker ^8.x`),
|
||||
需注意 API 差异,或升级本地包到 `^11.x`。
|
||||
|
||||
@@ -305,6 +312,382 @@ MacBook Pro 端使用 pub.dev 版本 `^0.9.1`,鸿蒙端的 `ohosName` 参数
|
||||
home_widget: ^0.9.1
|
||||
```
|
||||
|
||||
### 2.6 ⚠️ pub cache 补丁(MacBook Pro 端必读)
|
||||
|
||||
> **关键问题**:`dependency_overrides` 中 `win32: ^6.0.1` 导致部分依赖 `win32 ^5.x` 的三方包编译失败。
|
||||
> 这些三方包的 Windows 平台代码在 macOS 构建时也会被编译(Dart 编译器不区分平台)。
|
||||
> 需要手动修补 pub cache 中的文件,使它们兼容 win32 6.x API。
|
||||
|
||||
#### 2.6.1 需要修补的包
|
||||
|
||||
| 包名 | 版本 | 问题 | 修补文件 |
|
||||
|---|---|---|---|
|
||||
| quill_native_bridge_windows | 0.0.2 | 使用 win32 5.x API(`TEXT()`、`OpenClipboard(NULL)==FALSE` 等) | `lib/quill_native_bridge_windows.dart` + `lib/src/clipboard_html_format.dart` |
|
||||
| flutter_vibrate | gitcode | `TARGET_OS_SIMULATOR` 在 Xcode 16+/Swift 6 中不可用 | `ios/Classes/SwiftVibratePlugin.swift` |
|
||||
|
||||
#### 2.6.2 补丁应用流程
|
||||
|
||||
```bash
|
||||
# 1. 确保 flutter pub get 已执行
|
||||
flutter pub get
|
||||
|
||||
# 2. 运行补丁脚本(见 §2.6.3)
|
||||
bash scripts/patch_pub_cache.sh
|
||||
|
||||
# 3. 验证构建
|
||||
flutter build macos
|
||||
flutter build ios --no-codesign
|
||||
```
|
||||
|
||||
> ⚠️ `flutter clean` 或 `flutter pub cache repair` 会清除补丁,需重新执行步骤 2。
|
||||
|
||||
#### 2.6.3 补丁脚本
|
||||
|
||||
在项目根目录创建 `scripts/patch_pub_cache.sh`(已包含在仓库中),内容如下:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# patch_pub_cache.sh — 修补 pub cache 中的兼容性问题
|
||||
# 创建时间: 2026-06-01
|
||||
# 作用: 修补 quill_native_bridge_windows (win32 6.x) 和 flutter_vibrate (Swift 6)
|
||||
|
||||
set -e
|
||||
|
||||
echo "🔧 Applying pub cache patches..."
|
||||
|
||||
# ── Patch 1: quill_native_bridge_windows — clipboard_html_format.dart ──
|
||||
QNBW_DIR="$HOME/.pub-cache/hosted/pub.dev/quill_native_bridge_windows-0.0.2"
|
||||
if [ -d "$QNBW_DIR" ]; then
|
||||
echo " Patching clipboard_html_format.dart..."
|
||||
cat > "$QNBW_DIR/lib/src/clipboard_html_format.dart" << 'DART_EOF'
|
||||
import 'package:ffi/ffi.dart';
|
||||
import 'package:win32/win32.dart';
|
||||
|
||||
import '../quill_native_bridge_windows.dart';
|
||||
|
||||
const _kHtmlFormatName = 'HTML Format';
|
||||
|
||||
int? _cfHtml;
|
||||
|
||||
extension ClipboardHtmlFormatExt on QuillNativeBridgeWindows {
|
||||
int? get cfHtml {
|
||||
_cfHtml ??= _registerHtmlFormat();
|
||||
return _cfHtml;
|
||||
}
|
||||
|
||||
int? _registerHtmlFormat() {
|
||||
final htmlFormatPointer = _kHtmlFormatName.toPcwstr();
|
||||
final result = RegisterClipboardFormat(htmlFormatPointer);
|
||||
free(htmlFormatPointer);
|
||||
|
||||
if (result.error.isError) {
|
||||
return null;
|
||||
}
|
||||
return result.value;
|
||||
}
|
||||
}
|
||||
DART_EOF
|
||||
echo " ✅ clipboard_html_format.dart patched"
|
||||
else
|
||||
echo " ⚠️ quill_native_bridge_windows not found in pub cache"
|
||||
fi
|
||||
|
||||
# ── Patch 2: quill_native_bridge_windows — quill_native_bridge_windows.dart ──
|
||||
if [ -d "$QNBW_DIR" ]; then
|
||||
echo " Patching quill_native_bridge_windows.dart..."
|
||||
cat > "$QNBW_DIR/lib/quill_native_bridge_windows.dart" << 'DART_EOF'
|
||||
// Patched for win32 6.x compatibility (2026-06-01)
|
||||
// Changes: Win32Result API, HGLOBAL/HANDLE extension types, TEXT() → toPcwstr()
|
||||
|
||||
import 'dart:ffi';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:ffi/ffi.dart';
|
||||
import 'package:file_selector_platform_interface/file_selector_platform_interface.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:quill_native_bridge_platform_interface/quill_native_bridge_platform_interface.dart';
|
||||
import 'package:win32/win32.dart';
|
||||
|
||||
import 'src/clipboard_html_format.dart';
|
||||
import 'src/html_cleaner.dart';
|
||||
import 'src/html_formatter.dart';
|
||||
import 'src/image_saver.dart';
|
||||
|
||||
class QuillNativeBridgeWindows extends QuillNativeBridgePlatform {
|
||||
static void registerWith() {
|
||||
QuillNativeBridgePlatform.instance = QuillNativeBridgeWindows();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> isSupported(QuillNativeBridgeFeature feature) async => {
|
||||
QuillNativeBridgeFeature.getClipboardHtml,
|
||||
QuillNativeBridgeFeature.copyHtmlToClipboard,
|
||||
QuillNativeBridgeFeature.saveImage,
|
||||
}.contains(feature);
|
||||
|
||||
@override
|
||||
Future<String?> getClipboardHtml() async {
|
||||
final openResult = OpenClipboard(null);
|
||||
if (openResult.error.isError) {
|
||||
assert(false, 'Unknown error while opening the clipboard. Error code: ${GetLastError()}');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
final htmlFormatId = cfHtml;
|
||||
if (htmlFormatId == null) {
|
||||
assert(false, 'Failed to register clipboard HTML format.');
|
||||
return null;
|
||||
}
|
||||
|
||||
final availableResult = IsClipboardFormatAvailable(htmlFormatId);
|
||||
if (availableResult.error.isError) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final getDataResult = GetClipboardData(htmlFormatId);
|
||||
if (getDataResult.error.isError) {
|
||||
assert(false, 'Failed to get clipboard data. Error code: ${GetLastError()}');
|
||||
return null;
|
||||
}
|
||||
|
||||
final clipboardDataHandle = getDataResult.value;
|
||||
final hglobal = HGLOBAL(clipboardDataHandle);
|
||||
final lockResult = GlobalLock(hglobal);
|
||||
if (lockResult.error.isError) {
|
||||
assert(false, 'Failed to lock global memory. Error code: ${GetLastError()}');
|
||||
return null;
|
||||
}
|
||||
|
||||
final lockedMemoryPointer = lockResult.value;
|
||||
final windowsHtmlWithMetadata = lockedMemoryPointer.cast<Utf8>().toDartString();
|
||||
GlobalUnlock(hglobal);
|
||||
|
||||
final cleanedHtml = stripWindowsHtmlDescriptionHeaders(windowsHtmlWithMetadata);
|
||||
return cleanedHtml;
|
||||
} finally {
|
||||
CloseClipboard();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> copyHtmlToClipboard(String html) async {
|
||||
final openResult = OpenClipboard(null);
|
||||
if (openResult.error.isError) {
|
||||
assert(false, 'Unknown error while opening the clipboard. Error code: ${GetLastError()}');
|
||||
return;
|
||||
}
|
||||
|
||||
final windowsClipboardHtml = constructWindowsHtmlDescriptionHeaders(html);
|
||||
final htmlPointer = windowsClipboardHtml.toNativeUtf8();
|
||||
|
||||
try {
|
||||
final emptyResult = EmptyClipboard();
|
||||
if (emptyResult.error.isError) {
|
||||
assert(false, 'Failed to empty the clipboard. Error code: ${GetLastError()}');
|
||||
return;
|
||||
}
|
||||
|
||||
final htmlFormatId = cfHtml;
|
||||
if (htmlFormatId == null) {
|
||||
assert(false, 'Failed to register clipboard HTML format. Error code: ${GetLastError()}');
|
||||
return;
|
||||
}
|
||||
|
||||
final unitSize = sizeOf<Uint8>();
|
||||
final htmlSize = (htmlPointer.length + 1) * unitSize;
|
||||
|
||||
final allocResult = GlobalAlloc(GMEM_MOVEABLE, htmlSize);
|
||||
if (allocResult.error.isError) {
|
||||
assert(false, 'Failed to allocate memory for the clipboard content. Error code: ${GetLastError()}');
|
||||
return;
|
||||
}
|
||||
|
||||
final clipboardMemoryHandle = allocResult.value;
|
||||
final lockResult = GlobalLock(clipboardMemoryHandle);
|
||||
if (lockResult.error.isError) {
|
||||
GlobalFree(clipboardMemoryHandle);
|
||||
assert(false, 'Failed to lock global memory. Error code: ${GetLastError()}');
|
||||
return;
|
||||
}
|
||||
|
||||
final lockedMemoryPointer = lockResult.value;
|
||||
final targetMemoryPointer = lockedMemoryPointer.cast<Uint8>();
|
||||
final sourcePointer = htmlPointer.cast<Uint8>();
|
||||
|
||||
for (var i = 0; i < htmlPointer.length; i++) {
|
||||
targetMemoryPointer[i] = (sourcePointer + i).value;
|
||||
}
|
||||
(targetMemoryPointer + htmlPointer.length).value = 0;
|
||||
|
||||
GlobalUnlock(clipboardMemoryHandle);
|
||||
|
||||
final setResult = SetClipboardData(htmlFormatId, HANDLE(clipboardMemoryHandle));
|
||||
if (setResult.error.isError) {
|
||||
GlobalFree(clipboardMemoryHandle);
|
||||
assert(false, 'Failed to set the clipboard data: ${GetLastError()}');
|
||||
}
|
||||
} finally {
|
||||
CloseClipboard();
|
||||
calloc.free(htmlPointer);
|
||||
}
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
static ImageSaver imageSaver = ImageSaver();
|
||||
|
||||
@override
|
||||
Future<ImageSaveResult> saveImage(Uint8List imageBytes, {required ImageSaveOptions options}) async {
|
||||
final typeGroup = XTypeGroup(label: 'Images', extensions: [options.fileExtension]);
|
||||
final saveLocation = await imageSaver.fileSelector.getSaveLocation(
|
||||
options: SaveDialogOptions(
|
||||
suggestedName: '${options.name}.${options.fileExtension}',
|
||||
initialDirectory: imageSaver.picturesDirectoryPath,
|
||||
),
|
||||
acceptedTypeGroups: [typeGroup],
|
||||
);
|
||||
final imageFilePath = saveLocation?.path;
|
||||
if (imageFilePath == null) {
|
||||
return ImageSaveResult.io(filePath: null);
|
||||
}
|
||||
final imageFile = File(imageFilePath);
|
||||
await imageFile.writeAsBytes(imageBytes);
|
||||
return ImageSaveResult.io(filePath: imageFile.path);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> openGalleryApp() async {
|
||||
final uriPtr = 'ms-photos:'.toPcwstr();
|
||||
final openPtr = 'open'.toPcwstr();
|
||||
ShellExecute(null, openPtr, uriPtr, null, null, SW_SHOWNORMAL);
|
||||
free(uriPtr);
|
||||
free(openPtr);
|
||||
}
|
||||
}
|
||||
DART_EOF
|
||||
echo " ✅ quill_native_bridge_windows.dart patched"
|
||||
fi
|
||||
|
||||
# ── Patch 3: flutter_vibrate — SwiftVibratePlugin.swift ──
|
||||
FV_DIR=$(find "$HOME/.pub-cache/git" -path "*/fluttertpc_flutter_vibrate*" -maxdepth 2 -type d 2>/dev/null | head -1)
|
||||
if [ -n "$FV_DIR" ]; then
|
||||
echo " Patching SwiftVibratePlugin.swift..."
|
||||
cat > "$FV_DIR/ios/Classes/SwiftVibratePlugin.swift" << 'SWIFT_EOF'
|
||||
import Flutter
|
||||
import UIKit
|
||||
import AudioToolbox
|
||||
|
||||
// Patched for Xcode 16+/Swift 6 compatibility (2026-06-01)
|
||||
// TARGET_OS_SIMULATOR removed; using #if targetEnvironment(simulator)
|
||||
|
||||
#if targetEnvironment(simulator)
|
||||
private let isDevice = false
|
||||
#else
|
||||
private let isDevice = true
|
||||
#endif
|
||||
|
||||
public class SwiftVibratePlugin: NSObject, FlutterPlugin {
|
||||
public static func register(with registrar: FlutterPluginRegistrar) {
|
||||
let channel = FlutterMethodChannel(name: "vibrate", binaryMessenger: registrar.messenger())
|
||||
let instance = SwiftVibratePlugin()
|
||||
registrar.addMethodCallDelegate(instance, channel: channel)
|
||||
}
|
||||
|
||||
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||
switch (call.method) {
|
||||
case "canVibrate":
|
||||
if isDevice { result(true) } else { result(false) }
|
||||
case "vibrate":
|
||||
AudioServicesPlaySystemSound(kSystemSoundID_Vibrate)
|
||||
case "impact":
|
||||
if #available(iOS 10.0, *) {
|
||||
let impact = UIImpactFeedbackGenerator()
|
||||
impact.prepare()
|
||||
impact.impactOccurred()
|
||||
} else { AudioServicesPlaySystemSound(kSystemSoundID_Vibrate) }
|
||||
case "selection":
|
||||
if #available(iOS 10.0, *) {
|
||||
let selection = UISelectionFeedbackGenerator()
|
||||
selection.prepare()
|
||||
selection.selectionChanged()
|
||||
} else { AudioServicesPlaySystemSound(kSystemSoundID_Vibrate) }
|
||||
case "success":
|
||||
if #available(iOS 10.0, *) {
|
||||
let notification = UINotificationFeedbackGenerator()
|
||||
notification.prepare()
|
||||
notification.notificationOccurred(.success)
|
||||
} else { AudioServicesPlaySystemSound(kSystemSoundID_Vibrate) }
|
||||
case "warning":
|
||||
if #available(iOS 10.0, *) {
|
||||
let notification = UINotificationFeedbackGenerator()
|
||||
notification.prepare()
|
||||
notification.notificationOccurred(.warning)
|
||||
} else { AudioServicesPlaySystemSound(kSystemSoundID_Vibrate) }
|
||||
case "error":
|
||||
if #available(iOS 10.0, *) {
|
||||
let notification = UINotificationFeedbackGenerator()
|
||||
notification.prepare()
|
||||
notification.notificationOccurred(.error)
|
||||
} else { AudioServicesPlaySystemSound(kSystemSoundID_Vibrate) }
|
||||
case "heavy":
|
||||
if #available(iOS 10.0, *) {
|
||||
let generator = UIImpactFeedbackGenerator(style: .heavy)
|
||||
generator.prepare()
|
||||
generator.impactOccurred()
|
||||
} else { AudioServicesPlaySystemSound(kSystemSoundID_Vibrate) }
|
||||
case "medium":
|
||||
if #available(iOS 10.0, *) {
|
||||
let generator = UIImpactFeedbackGenerator(style: .medium)
|
||||
generator.prepare()
|
||||
generator.impactOccurred()
|
||||
} else { AudioServicesPlaySystemSound(kSystemSoundID_Vibrate) }
|
||||
case "light":
|
||||
if #available(iOS 10.0, *) {
|
||||
let generator = UIImpactFeedbackGenerator(style: .light)
|
||||
generator.prepare()
|
||||
generator.impactOccurred()
|
||||
} else { AudioServicesPlaySystemSound(kSystemSoundID_Vibrate) }
|
||||
default:
|
||||
result(FlutterMethodNotImplemented)
|
||||
}
|
||||
}
|
||||
}
|
||||
SWIFT_EOF
|
||||
echo " ✅ SwiftVibratePlugin.swift patched"
|
||||
else
|
||||
echo " ⚠️ flutter_vibrate not found in pub cache"
|
||||
fi
|
||||
|
||||
echo "🎉 All patches applied!"
|
||||
echo "⚠️ Note: Run this script again after 'flutter clean' or 'flutter pub cache repair'"
|
||||
```
|
||||
|
||||
#### 2.6.4 win32 5.x → 6.x API 迁移参考
|
||||
|
||||
如需修补其他使用 win32 的三方包,以下是关键 API 变更:
|
||||
|
||||
| win32 5.x | win32 6.x | 说明 |
|
||||
|---|---|---|
|
||||
| `OpenClipboard(NULL) == FALSE` | `OpenClipboard(null).error.isError` | 返回 `Win32Result<bool>` |
|
||||
| `CloseClipboard()` | `CloseClipboard()` | 返回 `Win32Result<bool>`,忽略即可 |
|
||||
| `EmptyClipboard() == FALSE` | `EmptyClipboard().error.isError` | 返回 `Win32Result<bool>` |
|
||||
| `IsClipboardFormatAvailable(id) == FALSE` | `IsClipboardFormatAvailable(id).error.isError` | 返回 `Win32Result<bool>` |
|
||||
| `GetClipboardData(id) == NULL` | `GetClipboardData(id).error.isError` | 返回 `Win32Result<HANDLE>` |
|
||||
| `GlobalAlloc(flags, size) == nullptr` | `GlobalAlloc(flags, size).error.isError` | 返回 `Win32Result<HGLOBAL>` |
|
||||
| `GlobalLock(handle) == nullptr` | `GlobalLock(HGLOBAL(handle)).error.isError` | 返回 `Win32Result<Pointer>` |
|
||||
| `GlobalUnlock(handle)` | `GlobalUnlock(HGLOBAL(handle))` | 返回 `Win32Result<bool>` |
|
||||
| `GlobalFree(handle)` | `GlobalFree(HGLOBAL(handle))` | 返回 `Win32Result<HGLOBAL>` |
|
||||
| `SetClipboardData(id, addr) == NULL` | `SetClipboardData(id, HANDLE(hglobal)).error.isError` | 返回 `Win32Result<HANDLE>` |
|
||||
| `TEXT('str')` | `'str'.toPcwstr()` | `TEXT()` 已移除 |
|
||||
| `RegisterClipboardFormat(ptr)` 返回 `int` | `RegisterClipboardFormat(ptr)` 返回 `Win32Result<int>` | 检查 `.error.isError` |
|
||||
| `ShellExecute(NULL, ...)` | `ShellExecute(null, ...)` | `NULL` → `null`,`nullptr` → `null`(PCWSTR? 类型) |
|
||||
| `HGLOBAL` 为 `int` | `HGLOBAL` 为 `extension type const HGLOBAL(Pointer _)` | 需类型转换 |
|
||||
| `HANDLE` 为 `int` | `HANDLE` 为 `extension type const HANDLE(Pointer _)` | 需类型转换 |
|
||||
|
||||
> **类型转换**:`HANDLE` 和 `HGLOBAL` 都 `implements Pointer`,可互相转换:
|
||||
> - `HANDLE → HGLOBAL`:`HGLOBAL(handle)`
|
||||
> - `HGLOBAL → HANDLE`:`HANDLE(hglobal)`
|
||||
|
||||
---
|
||||
|
||||
## 三、Git 提交与合并规范
|
||||
@@ -678,4 +1061,5 @@ MacBook Pro 开发前,确认以下事项:
|
||||
|
||||
---
|
||||
|
||||
*文档创建时间: 2026-05-21 | 更新时间: 2026-05-22 v3 | 维护者: 闲言APP开发团队*
|
||||
*文档创建时间: 2026-05-21 | 更新时间: 2026-06-01 v6 | 维护者: 闲言APP开发团队*
|
||||
*更新内容: win32 6.x兼容修复(file_picker升级+pub cache补丁);flutter_vibrate Swift 6修复;新增§2.6 pub cache补丁文档*
|
||||
|
||||
Reference in New Issue
Block a user