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:
Developer
2026-06-01 11:15:59 +08:00
parent d4d8746252
commit 22987e4ad1
5 changed files with 800 additions and 20 deletions

View File

@@ -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_managerlib/代码已迁移完成) |
| 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_pickerAPI 变更)
#### 2.5.2 file_pickerAPI 变更 + 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
// ❌ 旧版 APIfile_picker ^8.x
final result = await FilePicker.platform.pickFiles();
// ✅ 新版 APIfile_picker ^11.x
// ✅ 新版 APIfile_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补丁文档*