diff --git a/ios/Runner/RunnerProfile.entitlements b/ios/Runner/RunnerProfile.entitlements
new file mode 100644
index 000000000..0c67376eb
--- /dev/null
+++ b/ios/Runner/RunnerProfile.entitlements
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/lib/data/models/appconfig_model.dart b/lib/data/models/appconfig_model.dart
index 65ea7838e..e656a62c9 100644
--- a/lib/data/models/appconfig_model.dart
+++ b/lib/data/models/appconfig_model.dart
@@ -79,6 +79,7 @@ class DBProduct {
final String? productimage;
final String scanuuid;
final String? version;
+ bool? isforceupdate;
final bool? iscontinuouschat;
final String? updatedescription;
final String? updatepackageaddress;
@@ -91,6 +92,7 @@ class DBProduct {
required this.productimage,
required this.scanuuid,
required this.version,
+ required this.isforceupdate, //该产品固件版本是否强制更新
required this.iscontinuouschat,
required this.updatedescription,
required this.updatepackageaddress,
diff --git a/lib/data/models/appconfig_model.g.dart b/lib/data/models/appconfig_model.g.dart
index f99ba35c6..fc97723ed 100644
--- a/lib/data/models/appconfig_model.g.dart
+++ b/lib/data/models/appconfig_model.g.dart
@@ -79,6 +79,7 @@ DBProduct _$DBProductFromJson(Map json) => DBProduct(
productimage: json['productimage'] as String?,
scanuuid: json['scanuuid'] as String,
version: json['version'] as String?,
+ isforceupdate: json['isforceupdate'] as bool?,
iscontinuouschat: json['iscontinuouschat'] as bool?,
updatedescription: json['updatedescription'] as String?,
updatepackageaddress: json['updatepackageaddress'] as String?,
@@ -91,6 +92,7 @@ Map _$DBProductToJson(DBProduct instance) => {
'productimage': instance.productimage,
'scanuuid': instance.scanuuid,
'version': instance.version,
+ 'isforceupdate': instance.isforceupdate,
'iscontinuouschat': instance.iscontinuouschat,
'devicename': instance.devicename,
'updatedescription': instance.updatedescription,
diff --git a/lib/data/services/ota_check_service.dart b/lib/data/services/ota_check_service.dart
index 5caa31493..13a5f09d5 100644
--- a/lib/data/services/ota_check_service.dart
+++ b/lib/data/services/ota_check_service.dart
@@ -57,6 +57,7 @@ class OtaCheckService extends GetxService {
/// [isFullyConnected] 设备是否完全连接
/// 返回是否成功执行了检查
Future performOtaCheck(bool isFullyConnected) async {
+ bool isForceUpdate = false;
try {
// 检查是否应该进行OTA检查
if (!shouldCheckOta()) {
@@ -95,7 +96,12 @@ class OtaCheckService extends GetxService {
_otaCoreService.isNewVersionAvailable(currentVersion, latestVersion);
if (hasUpdate) {
- Logger.d(_tag, '发现新版本,准备显示升级提醒');
+ isForceUpdate = latestInfo['isForceUpdate'] as bool? ?? false;
+ if (isForceUpdate) {
+ Logger.d(_tag, '发现强制更新版本,准备显示升级提醒');
+ } else {
+ Logger.d(_tag, '发现新版本,准备显示升级提醒');
+ }
// 再次确认当前在Home页面(防止异步执行期间页面切换)
if (_isCurrentlyOnHomePage()) {
@@ -109,12 +115,18 @@ class OtaCheckService extends GetxService {
}
// 标记为已检查(无论是否有更新都标记为已检查)
- _markAsChecked();
+ if (!isForceUpdate) {
+ _markAsChecked();
+ }
+ _isCheckingInProgress = false;
return true;
} catch (e) {
Logger.e(_tag, '检查OTA升级失败: $e');
// 即使检查失败,也标记为已检查,避免重复尝试
- _markAsChecked();
+ if (!isForceUpdate) {
+ _markAsChecked();
+ }
+ _isCheckingInProgress = false;
return false;
}
}
diff --git a/lib/modules/home/controllers/home_controller.dart b/lib/modules/home/controllers/home_controller.dart
index 466b4792b..ab2478230 100644
--- a/lib/modules/home/controllers/home_controller.dart
+++ b/lib/modules/home/controllers/home_controller.dart
@@ -277,7 +277,7 @@ class HomeController extends GetxController {
// 连接成功后检查OTA升级
if (noviceGuideService.isHomeGuide) {
- Future.delayed(const Duration(seconds: 2), () {
+ Future.delayed(const Duration(seconds: 1), () {
_checkOtaUpgrade();
});
}
diff --git a/lib/modules/ota_upgrade/controllers/ota_core_contropller.dart b/lib/modules/ota_upgrade/controllers/ota_core_contropller.dart
index cd05687f0..146645d90 100644
--- a/lib/modules/ota_upgrade/controllers/ota_core_contropller.dart
+++ b/lib/modules/ota_upgrade/controllers/ota_core_contropller.dart
@@ -525,17 +525,20 @@ class OtaCoreService {
}
}
+ product.isforceupdate = product.version?.contains("+");
+
return {
- 'latestVersion': product.version ?? "",
+ 'latestVersion': product.version?.split('+').first ?? "",
'updateDescription': product.updatedescription ?? "",
'updatePackageAddress': product.updatepackageaddress ?? "",
+ 'isForceUpdate': product.isforceupdate ?? false,
};
}
/// 比较版本号,判断是否有新版本可用
/// [currentVersion] 当前版本号
/// [latestVersion] 最新版本号
- /// 返回true表示有新版本可用(支持升级和降级)
+ /// 返回true表示有新版本可用(只支持升级)
bool isNewVersionAvailable(String currentVersion, String latestVersion) {
try {
List currentParts =
@@ -555,10 +558,12 @@ class OtaCoreService {
latestParts.add(0);
}
- // 比较每一段,只要有不同就返回true(支持升级和降级)
+ // 从高位到低位比较版本号
for (int i = 0; i < maxLength; i++) {
- if (latestParts[i] != currentParts[i]) {
+ if (latestParts[i] > currentParts[i]) {
return true;
+ } else if (latestParts[i] < currentParts[i]) {
+ return false;
}
}
diff --git a/lib/modules/translation/controllers/translation_controller.dart b/lib/modules/translation/controllers/translation_controller.dart
index 79cac6ab1..f15b1a0d0 100644
--- a/lib/modules/translation/controllers/translation_controller.dart
+++ b/lib/modules/translation/controllers/translation_controller.dart
@@ -2,12 +2,15 @@ import 'dart:io';
import 'dart:async';
import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:intl/intl.dart';
+import 'package:native_plugin/native_plugin.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:device_info_plus/device_info_plus.dart';
+import 'package:pip/pip.dart';
import '../../../data/services/ast_service.dart';
import '../../../data/services/bluetooth_manager.dart';
import '../../../data/services/music_manager.dart';
@@ -49,7 +52,12 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
final BleManager bleManager = Get.find();
final _musiceManager = Get.find();
late final TranslationUsageService _usageService;
+ // 安卓悬浮窗管理器
late final FloatingWindowManager _floatingWindowManager;
+ // 苹果悬浮窗管理器
+ late final Pip _pip;
+ late final NativePlugin _nativePlugin;
+ int _pipContentView = 0;
// ==================== 悬浮窗相关 ====================
final isFloatingWindowEnabled = false.obs; // 悬浮窗状态
@@ -135,7 +143,7 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
// 底部控制栏高度
double _bottomBarHeight = 0.0;
- /// 初始化悬浮窗管理器
+ /// 初始化安卓悬浮窗管理器
void _initFloatingWindowManager() {
try {
if (!Get.isRegistered()) {
@@ -148,13 +156,68 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
}
}
+ /// 初始化苹果悬浮窗管理器
+ Future initPlatformState() async {
+ _pip = Pip();
+ _nativePlugin = NativePlugin();
+ try {
+ isFloatingWindowEnabled.value = await _pip.isActived();
+ await _pip.registerStateChangedObserver(
+ PipStateChangedObserver(
+ onPipStateChanged: (state, error) {
+ isFloatingWindowEnabled.value = state == PipState.pipStateStarted;
+
+ if (state == PipState.pipStateFailed) {
+ _pip.dispose();
+ }
+ },
+ ),
+ );
+ } on PlatformException {
+ isFloatingWindowEnabled.value = false;
+ }
+ }
+
/// 切换悬浮窗功能
Future toggleFloatingWindow() async {
if (isFloatingWindowEnabled.value) {
- await _disableFloatingWindow();
+ if (Platform.isAndroid) {
+ await _disableFloatingWindow();
+ } else if (Platform.isIOS) {
+ await _pip.dispose();
+ }
} else {
- await _enableFloatingWindow();
+ if (Platform.isAndroid) {
+ await _enableFloatingWindow();
+ } else if (Platform.isIOS) {
+ await _startPip();
+ }
+ }
+ }
+
+ Future _startPip() async {
+ if (_pipContentView == 0) {
+ _pipContentView = await _nativePlugin.createPipContentView();
}
+ final options = PipOptions(
+ autoEnterEnabled: true,
+ aspectRatioX: 375,
+ aspectRatioY: 400,
+ sourceRectHintLeft: 0,
+ sourceRectHintTop: 0,
+ sourceRectHintRight: 0,
+ sourceRectHintBottom: 0,
+ seamlessResizeEnabled: true,
+ useExternalStateMonitor: true,
+ externalStateMonitorInterval: 100,
+ contentView: _pipContentView,
+ preferredContentWidth: 375,
+ preferredContentHeight: 400,
+ controlStyle: 2,
+ );
+
+ await _pip.setup(options);
+ await _pip.start();
}
/// 启用悬浮窗功能
@@ -217,16 +280,24 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
_languageManager.getChineseNameByAsrCode(item.targetLanguageCode) ??
'unknown';
- final data = FloatingWindowData(
- sourceText: item.sourceText,
- translatedText: item.translatedText,
- sourceLanguage: sourceLanguageName,
- targetLanguage: targetLanguageName,
- timestamp: item.timestamp,
- isIntermediate: item.isIntermediate,
- );
-
- await _floatingWindowManager.updateFloatingWindowContent(data);
+ if (Platform.isAndroid) {
+ final data = FloatingWindowData(
+ sourceText: item.sourceText,
+ translatedText: item.translatedText,
+ sourceLanguage: sourceLanguageName,
+ targetLanguage: targetLanguageName,
+ timestamp: item.timestamp,
+ isIntermediate: item.isIntermediate,
+ );
+ await _floatingWindowManager.updateFloatingWindowContent(data);
+ } else if (Platform.isIOS) {
+ await _nativePlugin.updatePipContentView({
+ 'viewId': _pipContentView,
+ 'cn': item.sourceText,
+ 'en': item.translatedText,
+ 'lang': '$sourceLanguageName -> $targetLanguageName',
+ });
+ }
}
/// 更改翻译模式
@@ -374,8 +445,13 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
_timerManager = RecordTimerController();
_historyManager = TranslationHistoryManager();
- // 初始化悬浮窗管理器
- _initFloatingWindowManager();
+ if (Platform.isAndroid) {
+ // 初始化安卓悬浮窗管理器
+ _initFloatingWindowManager();
+ } else if (Platform.isIOS) {
+ // 初始化苹果悬浮窗管理器
+ initPlatformState();
+ }
// 预先检查权限,避免在用户交互时阻塞
_preCheckPermissions();
@@ -665,6 +741,15 @@ class TranslationController extends GetxController with WidgetsBindingObserver {
// _asrService.dispose();
// _astService.dispose();
+ if (Platform.isAndroid) {
+ _disableFloatingWindow();
+ } else if (Platform.isIOS) {
+ _pip.dispose();
+ if (_pipContentView != 0) {
+ _nativePlugin.disposePipContentView(_pipContentView);
+ }
+ }
+
// 释放管理器资源
_timerManager.dispose();
_historyManager.dispose();
diff --git a/lib/modules/translation/views/translation_view.dart b/lib/modules/translation/views/translation_view.dart
index b51cf7fa0..c089c0ff2 100644
--- a/lib/modules/translation/views/translation_view.dart
+++ b/lib/modules/translation/views/translation_view.dart
@@ -80,6 +80,31 @@ class TranslationView extends GetView {
controller: controller,
),
+ Positioned(
+ top: 2,
+ left: 0,
+ right: 0,
+ child: SafeArea(
+ top: true,
+ child: Container(
+ height: 56.h,
+ padding: EdgeInsets.symmetric(horizontal: 8.w),
+ color: Colors.transparent,
+ child: Align(
+ alignment: Alignment.centerLeft,
+ child: IconButton(
+ icon: Icon(
+ Icons.arrow_back_ios,
+ color: isDarkMode ? Colors.white : Colors.black,
+ size: 20.sp,
+ ),
+ onPressed: () => Get.back(),
+ ),
+ ),
+ ),
+ ),
+ ),
+
// 顶部按钮(倒过来)
Positioned(
top: 50.h,
@@ -210,28 +235,27 @@ class TranslationView extends GetView {
return Row(
children: [
// 悬浮窗切换按钮(仅在支持的模式下显示)
- if (Platform.isAndroid)
- Obx(() {
- if (controller.currentMode.value == 'call' ||
- controller.currentMode.value == 'audioVideo') {
- return IconButton(
- icon: Icon(
- controller.isFloatingWindowEnabled.value
- ? Icons.picture_in_picture
- : Icons.picture_in_picture_alt_outlined,
- color: controller.isFloatingWindowEnabled.value
- ? Colors.blue
- : (isDarkMode ? Colors.white : Colors.black),
- size: 24.sp,
- ),
- onPressed: controller.toggleFloatingWindow,
- tooltip: controller.isFloatingWindowEnabled.value
- ? '禁用悬浮窗'
- : '启用悬浮窗',
- );
- }
- return const SizedBox.shrink();
- }),
+ Obx(() {
+ if (controller.currentMode.value == 'call' ||
+ controller.currentMode.value == 'audioVideo') {
+ return IconButton(
+ icon: Icon(
+ controller.isFloatingWindowEnabled.value
+ ? Icons.picture_in_picture
+ : Icons.picture_in_picture_alt_outlined,
+ color: controller.isFloatingWindowEnabled.value
+ ? Colors.blue
+ : (isDarkMode ? Colors.white : Colors.black),
+ size: 24.sp,
+ ),
+ onPressed: controller.toggleFloatingWindow,
+ tooltip: controller.isFloatingWindowEnabled.value
+ ? '禁用悬浮窗'
+ : '启用悬浮窗',
+ );
+ }
+ return const SizedBox.shrink();
+ }),
IconButton(
icon: Icon(
Icons.radio_button_checked,
diff --git a/local_plugins/native_plugin/.gitignore b/local_plugins/native_plugin/.gitignore
new file mode 100644
index 000000000..e7d347d9d
--- /dev/null
+++ b/local_plugins/native_plugin/.gitignore
@@ -0,0 +1,33 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.build/
+.buildlog/
+.history
+.svn/
+.swiftpm/
+migrate_working_dir/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
+/pubspec.lock
+**/doc/api/
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
+build/
diff --git a/local_plugins/native_plugin/.metadata b/local_plugins/native_plugin/.metadata
new file mode 100644
index 000000000..a5700b901
--- /dev/null
+++ b/local_plugins/native_plugin/.metadata
@@ -0,0 +1,33 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+ revision: "35c388afb57ef061d06a39b537336c87e0e3d1b1"
+ channel: "stable"
+
+project_type: plugin
+
+# Tracks metadata for the flutter migrate command
+migration:
+ platforms:
+ - platform: root
+ create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
+ base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
+ - platform: android
+ create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
+ base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
+ - platform: ios
+ create_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
+ base_revision: 35c388afb57ef061d06a39b537336c87e0e3d1b1
+
+ # User provided section
+
+ # List of Local paths (relative to this file) that should be
+ # ignored by the migrate tool.
+ #
+ # Files that are not part of the templates will be ignored by default.
+ unmanaged_files:
+ - 'lib/main.dart'
+ - 'ios/Runner.xcodeproj/project.pbxproj'
diff --git a/local_plugins/native_plugin/CHANGELOG.md b/local_plugins/native_plugin/CHANGELOG.md
new file mode 100644
index 000000000..41cc7d819
--- /dev/null
+++ b/local_plugins/native_plugin/CHANGELOG.md
@@ -0,0 +1,3 @@
+## 0.0.1
+
+* TODO: Describe initial release.
diff --git a/local_plugins/native_plugin/LICENSE b/local_plugins/native_plugin/LICENSE
new file mode 100644
index 000000000..ba75c69f7
--- /dev/null
+++ b/local_plugins/native_plugin/LICENSE
@@ -0,0 +1 @@
+TODO: Add your license here.
diff --git a/local_plugins/native_plugin/README.md b/local_plugins/native_plugin/README.md
new file mode 100644
index 000000000..bc8261f01
--- /dev/null
+++ b/local_plugins/native_plugin/README.md
@@ -0,0 +1,15 @@
+# native_plugin
+
+A new Flutter plugin project.
+
+## Getting Started
+
+This project is a starting point for a Flutter
+[plug-in package](https://flutter.dev/to/develop-plugins),
+a specialized package that includes platform-specific implementation code for
+Android and/or iOS.
+
+For help getting started with Flutter development, view the
+[online documentation](https://docs.flutter.dev), which offers tutorials,
+samples, guidance on mobile development, and a full API reference.
+
diff --git a/local_plugins/native_plugin/analysis_options.yaml b/local_plugins/native_plugin/analysis_options.yaml
new file mode 100644
index 000000000..a5744c1cf
--- /dev/null
+++ b/local_plugins/native_plugin/analysis_options.yaml
@@ -0,0 +1,4 @@
+include: package:flutter_lints/flutter.yaml
+
+# Additional information about this file can be found at
+# https://dart.dev/guides/language/analysis-options
diff --git a/local_plugins/native_plugin/android/.gitignore b/local_plugins/native_plugin/android/.gitignore
new file mode 100644
index 000000000..161bdcdaf
--- /dev/null
+++ b/local_plugins/native_plugin/android/.gitignore
@@ -0,0 +1,9 @@
+*.iml
+.gradle
+/local.properties
+/.idea/workspace.xml
+/.idea/libraries
+.DS_Store
+/build
+/captures
+.cxx
diff --git a/local_plugins/native_plugin/android/build.gradle b/local_plugins/native_plugin/android/build.gradle
new file mode 100644
index 000000000..948c5ef6f
--- /dev/null
+++ b/local_plugins/native_plugin/android/build.gradle
@@ -0,0 +1,52 @@
+group = "com.example.native_plugin"
+version = "1.0"
+
+buildscript {
+ repositories {
+ google()
+ mavenCentral()
+ }
+
+ dependencies {
+ classpath("com.android.tools.build:gradle:8.7.0")
+ }
+}
+
+rootProject.allprojects {
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+apply plugin: "com.android.library"
+
+android {
+ namespace = "com.example.native_plugin"
+
+ compileSdk = 35
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_11
+ targetCompatibility = JavaVersion.VERSION_11
+ }
+
+ defaultConfig {
+ minSdk = 21
+ }
+
+ dependencies {
+ testImplementation("junit:junit:4.13.2")
+ testImplementation("org.mockito:mockito-core:5.0.0")
+ }
+
+ testOptions {
+ unitTests.all {
+ testLogging {
+ events "passed", "skipped", "failed", "standardOut", "standardError"
+ outputs.upToDateWhen {false}
+ showStandardStreams = true
+ }
+ }
+ }
+}
diff --git a/local_plugins/native_plugin/android/settings.gradle b/local_plugins/native_plugin/android/settings.gradle
new file mode 100644
index 000000000..9f756a98c
--- /dev/null
+++ b/local_plugins/native_plugin/android/settings.gradle
@@ -0,0 +1 @@
+rootProject.name = 'native_plugin'
diff --git a/local_plugins/native_plugin/android/src/main/AndroidManifest.xml b/local_plugins/native_plugin/android/src/main/AndroidManifest.xml
new file mode 100644
index 000000000..5153b5dc6
--- /dev/null
+++ b/local_plugins/native_plugin/android/src/main/AndroidManifest.xml
@@ -0,0 +1,3 @@
+
+
diff --git a/local_plugins/native_plugin/android/src/main/java/com/example/native_plugin/NativePlugin.java b/local_plugins/native_plugin/android/src/main/java/com/example/native_plugin/NativePlugin.java
new file mode 100644
index 000000000..265796e9d
--- /dev/null
+++ b/local_plugins/native_plugin/android/src/main/java/com/example/native_plugin/NativePlugin.java
@@ -0,0 +1,38 @@
+package com.example.native_plugin;
+
+import androidx.annotation.NonNull;
+
+import io.flutter.embedding.engine.plugins.FlutterPlugin;
+import io.flutter.plugin.common.MethodCall;
+import io.flutter.plugin.common.MethodChannel;
+import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
+import io.flutter.plugin.common.MethodChannel.Result;
+
+/** NativePlugin */
+public class NativePlugin implements FlutterPlugin, MethodCallHandler {
+ /// The MethodChannel that will the communication between Flutter and native Android
+ ///
+ /// This local reference serves to register the plugin with the Flutter Engine and unregister it
+ /// when the Flutter Engine is detached from the Activity
+ private MethodChannel channel;
+
+ @Override
+ public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
+ channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "native_plugin");
+ channel.setMethodCallHandler(this);
+ }
+
+ @Override
+ public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
+ if (call.method.equals("getPlatformVersion")) {
+ result.success("Android " + android.os.Build.VERSION.RELEASE);
+ } else {
+ result.notImplemented();
+ }
+ }
+
+ @Override
+ public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
+ channel.setMethodCallHandler(null);
+ }
+}
diff --git a/local_plugins/native_plugin/android/src/test/java/com/example/native_plugin/NativePluginTest.java b/local_plugins/native_plugin/android/src/test/java/com/example/native_plugin/NativePluginTest.java
new file mode 100644
index 000000000..8034030e9
--- /dev/null
+++ b/local_plugins/native_plugin/android/src/test/java/com/example/native_plugin/NativePluginTest.java
@@ -0,0 +1,29 @@
+package com.example.native_plugin;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import io.flutter.plugin.common.MethodCall;
+import io.flutter.plugin.common.MethodChannel;
+import org.junit.Test;
+
+/**
+ * This demonstrates a simple unit test of the Java portion of this plugin's implementation.
+ *
+ * Once you have built the plugin's example app, you can run these tests from the command
+ * line by running `./gradlew testDebugUnitTest` in the `example/android/` directory, or
+ * you can run them directly from IDEs that support JUnit such as Android Studio.
+ */
+
+public class NativePluginTest {
+ @Test
+ public void onMethodCall_getPlatformVersion_returnsExpectedValue() {
+ NativePlugin plugin = new NativePlugin();
+
+ final MethodCall call = new MethodCall("getPlatformVersion", null);
+ MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
+ plugin.onMethodCall(call, mockResult);
+
+ verify(mockResult).success("Android " + android.os.Build.VERSION.RELEASE);
+ }
+}
diff --git a/local_plugins/native_plugin/ios/.gitignore b/local_plugins/native_plugin/ios/.gitignore
new file mode 100644
index 000000000..034771fc9
--- /dev/null
+++ b/local_plugins/native_plugin/ios/.gitignore
@@ -0,0 +1,38 @@
+.idea/
+.vagrant/
+.sconsign.dblite
+.svn/
+
+.DS_Store
+*.swp
+profile
+
+DerivedData/
+build/
+GeneratedPluginRegistrant.h
+GeneratedPluginRegistrant.m
+
+.generated/
+
+*.pbxuser
+*.mode1v3
+*.mode2v3
+*.perspectivev3
+
+!default.pbxuser
+!default.mode1v3
+!default.mode2v3
+!default.perspectivev3
+
+xcuserdata
+
+*.moved-aside
+
+*.pyc
+*sync/
+Icon?
+.tags*
+
+/Flutter/Generated.xcconfig
+/Flutter/ephemeral/
+/Flutter/flutter_export_environment.sh
diff --git a/local_plugins/native_plugin/ios/Assets/.gitkeep b/local_plugins/native_plugin/ios/Assets/.gitkeep
new file mode 100644
index 000000000..e69de29bb
diff --git a/local_plugins/native_plugin/ios/Classes/FLNativeView.h b/local_plugins/native_plugin/ios/Classes/FLNativeView.h
new file mode 100644
index 000000000..4c49e00f4
--- /dev/null
+++ b/local_plugins/native_plugin/ios/Classes/FLNativeView.h
@@ -0,0 +1,15 @@
+#import
+
+@interface FLNativeViewFactory : NSObject
+- (instancetype)initWithMessenger:(NSObject *)messenger;
+@end
+
+@interface FLNativeView : NSObject
+
+- (instancetype)initWithFrame:(CGRect)frame
+ viewIdentifier:(int64_t)viewId
+ arguments:(id _Nullable)args
+ binaryMessenger:(NSObject *)messenger;
+
+- (UIView *)view;
+@end
diff --git a/local_plugins/native_plugin/ios/Classes/FLNativeView.m b/local_plugins/native_plugin/ios/Classes/FLNativeView.m
new file mode 100644
index 000000000..848785a6b
--- /dev/null
+++ b/local_plugins/native_plugin/ios/Classes/FLNativeView.m
@@ -0,0 +1,83 @@
+#import "FLNativeView.h"
+#import "PlayerView.h"
+
+@implementation FLNativeViewFactory {
+ NSObject *_messenger;
+}
+
+- (instancetype)initWithMessenger:
+ (NSObject *)messenger {
+ self = [super init];
+ if (self) {
+ _messenger = messenger;
+ }
+ return self;
+}
+
+- (NSObject *)createWithFrame:(CGRect)frame
+ viewIdentifier:(int64_t)viewId
+ arguments:(id _Nullable)args {
+ return [[FLNativeView alloc] initWithFrame:frame
+ viewIdentifier:viewId
+ arguments:args
+ binaryMessenger:_messenger];
+}
+
+/// Implementing this method is only necessary when the `arguments` in
+/// `createWithFrame` is not `nil`.
+- (NSObject *)createArgsCodec {
+ return [FlutterStandardMessageCodec sharedInstance];
+}
+
+@end
+
+@implementation FLNativeView {
+ PlayerView *_view;
+ FlutterMethodChannel *_methodChannel;
+}
+
+- (instancetype)initWithFrame:(CGRect)frame
+ viewIdentifier:(int64_t)viewId
+ arguments:(id _Nullable)args
+ binaryMessenger:(NSObject *)messenger {
+ if (self = [super init]) {
+ _view = [[PlayerView alloc] init];
+ _methodChannel = [FlutterMethodChannel
+ methodChannelWithName:
+ [NSString
+ stringWithFormat:@"native_plugin/native_view_%lld", viewId]
+ binaryMessenger:messenger];
+ __weak typeof(self) weakSelf = self;
+ [_methodChannel setMethodCallHandler:^(FlutterMethodCall *_Nonnull call,
+ FlutterResult _Nonnull result) {
+ typeof(self) strongSelf = weakSelf;
+ if (strongSelf != nil) {
+ [strongSelf onMethodCall:call result:result];
+ }
+ }];
+ }
+ return self;
+}
+
+- (UIView *)view {
+ return _view;
+}
+
+- (void)onMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result {
+ if ([@"getInternalView" isEqualToString:call.method]) {
+ result(@((uint64_t)(_view)));
+ } else if ([@"updateContent" isEqualToString:call.method]) {
+ NSDictionary *data = call.arguments;
+ NSString *lang = data[@"lang"];
+ NSString *cn = data[@"cn"];
+ NSString *en = data[@"en"];
+ dispatch_async(dispatch_get_main_queue(), ^{
+ [_view updateWithLang:lang cn:cn en:en];
+ });
+ result(nil);
+ } else {
+ result(@(0));
+ }
+}
+
+@end
diff --git a/local_plugins/native_plugin/ios/Classes/NativePlugin.h b/local_plugins/native_plugin/ios/Classes/NativePlugin.h
new file mode 100644
index 000000000..2d0b3293e
--- /dev/null
+++ b/local_plugins/native_plugin/ios/Classes/NativePlugin.h
@@ -0,0 +1,4 @@
+#import
+
+@interface NativePlugin : NSObject
+@end
diff --git a/local_plugins/native_plugin/ios/Classes/NativePlugin.m b/local_plugins/native_plugin/ios/Classes/NativePlugin.m
new file mode 100644
index 000000000..bee63b337
--- /dev/null
+++ b/local_plugins/native_plugin/ios/Classes/NativePlugin.m
@@ -0,0 +1,114 @@
+#import "NativePlugin.h"
+#import "FLNativeView.h"
+#import "PlayerView.h"
+
+#import
+
+@interface NativePlugin ()
+
+@property(nonatomic, strong) NSMutableArray *_Nonnull pipContentViewArray;
+
+@end
+
+@implementation NativePlugin
+
++ (void)updateAudioSession {
+ AVAudioSession *audioSession = [AVAudioSession sharedInstance];
+ NSError *categoryError = nil;
+ if (@available(iOS 14.5, *)) {
+ [audioSession
+ setCategory:AVAudioSessionCategoryPlayback
+ mode:AVAudioSessionModeMoviePlayback
+ options:
+ AVAudioSessionCategoryOptionOverrideMutedMicrophoneInterruption
+ error:&categoryError];
+ } else {
+ // Fallback on earlier versions
+ }
+ if (categoryError) {
+ NSLog(@"Set audio session category error: %@",
+ categoryError.localizedDescription);
+ }
+ NSError *activeError = nil;
+ [audioSession setActive:YES error:&activeError];
+ if (activeError) {
+ NSLog(@"Set audio session active error: %@",
+ activeError.localizedDescription);
+ }
+}
+
++ (void)registerWithRegistrar:(NSObject *)registrar {
+ // must call this, otherwise pip will not possible
+ // https://github.com/jazzychad/PiPBugDemo
+ [NativePlugin updateAudioSession];
+
+ FlutterMethodChannel *channel =
+ [FlutterMethodChannel methodChannelWithName:@"native_plugin"
+ binaryMessenger:[registrar messenger]];
+ NativePlugin *instance = [[NativePlugin alloc] init];
+
+ FLNativeViewFactory *factory =
+ [[FLNativeViewFactory alloc] initWithMessenger:registrar.messenger];
+ [registrar registerViewFactory:factory withId:@"native_view"];
+
+ [registrar addMethodCallDelegate:instance channel:channel];
+}
+
+- (instancetype)init {
+ self = [super init];
+ if (self) {
+ _pipContentViewArray = [NSMutableArray array];
+ }
+ return self;
+}
+
+- (void)handleMethodCall:(FlutterMethodCall *)call
+ result:(FlutterResult)result {
+ if ([@"getPlatformVersion" isEqualToString:call.method]) {
+ result([@"iOS "
+ stringByAppendingString:[[UIDevice currentDevice] systemVersion]]);
+ } else if ([@"createPipContentView" isEqualToString:call.method]) {
+ PlayerView *playerView = [[PlayerView alloc] init];
+
+ // should control the play state by event of pip, optimize it later
+ [playerView play];
+
+ [self.pipContentViewArray addObject:playerView];
+
+ result(@((uint64_t)playerView));
+ } else if ([@"disposePipContentView" isEqualToString:call.method]) {
+ uint64_t viewId = [call.arguments unsignedLongLongValue];
+ for (PlayerView *playerView in self.pipContentViewArray) {
+ if ((uint64_t)playerView == viewId) {
+ [playerView pause];
+ [self.pipContentViewArray removeObject:playerView];
+ break;
+ }
+ }
+ } else if ([@"updatePipContentView" isEqualToString:call.method]) {
+ NSDictionary *data = call.arguments;
+ NSString *lang = data[@"lang"];
+ NSString *cn = data[@"cn"];
+ NSString *en = data[@"en"];
+ NSNumber *viewIdNum = data[@"viewId"];
+ dispatch_async(dispatch_get_main_queue(), ^{
+ if (viewIdNum != nil) {
+ uint64_t targetId = viewIdNum.unsignedLongLongValue;
+ for (PlayerView *playerView in self.pipContentViewArray) {
+ if ((uint64_t)playerView == targetId) {
+ [playerView updateWithLang:lang cn:cn en:en];
+ break;
+ }
+ }
+ } else {
+ PlayerView *playerView = [self.pipContentViewArray lastObject];
+ [playerView updateWithLang:lang cn:cn en:en];
+ }
+ });
+ result(nil);
+ } else {
+ result(FlutterMethodNotImplemented);
+ }
+}
+
+@end
diff --git a/local_plugins/native_plugin/ios/Classes/PlayerView.h b/local_plugins/native_plugin/ios/Classes/PlayerView.h
new file mode 100644
index 000000000..50044ae41
--- /dev/null
+++ b/local_plugins/native_plugin/ios/Classes/PlayerView.h
@@ -0,0 +1,15 @@
+#import
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface PlayerView : UIView
+
+- (void)play;
+
+- (void)pause;
+
+- (void)updateWithLang:(NSString *)lang cn:(NSString *)cn en:(NSString *)en;
+
+@end
+
+NS_ASSUME_NONNULL_END
\ No newline at end of file
diff --git a/local_plugins/native_plugin/ios/Classes/PlayerView.m b/local_plugins/native_plugin/ios/Classes/PlayerView.m
new file mode 100644
index 000000000..1272aef4c
--- /dev/null
+++ b/local_plugins/native_plugin/ios/Classes/PlayerView.m
@@ -0,0 +1,100 @@
+#import "PlayerView.h"
+
+@interface PlayerView ()
+
+@property(nonatomic, strong) UILabel *titleLabel;
+@property(nonatomic, strong) UILabel *langLabel;
+@property(nonatomic, strong) UILabel *cnLabel;
+@property(nonatomic, strong) UILabel *enLabel;
+@property(nonatomic, strong) UIStackView *stackView;
+
+@end
+
+@implementation PlayerView
+
+- (instancetype)init {
+ if (self = [super init]) {
+ self.backgroundColor = [UIColor whiteColor];
+
+ UILabel *title = [[UILabel alloc] init];
+ title.text = @"实时翻译";
+ title.font = [UIFont boldSystemFontOfSize:22];
+ title.textColor = [UIColor blackColor];
+ title.textAlignment = NSTextAlignmentCenter;
+ title.numberOfLines = 1;
+ self.titleLabel = title;
+
+ UILabel *lang = [[UILabel alloc] init];
+ lang.text = @"中文(简体) -> 英语";
+ lang.font = [UIFont systemFontOfSize:14];
+ lang.textColor = [UIColor darkGrayColor];
+ lang.textAlignment = NSTextAlignmentLeft;
+ lang.numberOfLines = 1;
+ self.langLabel = lang;
+
+ UILabel *cn = [[UILabel alloc] init];
+ cn.font = [UIFont systemFontOfSize:18];
+ cn.textColor = [UIColor blackColor];
+ cn.textAlignment = NSTextAlignmentLeft;
+ cn.numberOfLines = 0;
+ self.cnLabel = cn;
+
+ UILabel *en = [[UILabel alloc] init];
+ en.font = [UIFont systemFontOfSize:18];
+ en.textColor = [UIColor colorWithRed:0.24 green:0.54 blue:0.90 alpha:1.0];
+ en.textAlignment = NSTextAlignmentLeft;
+ en.numberOfLines = 0;
+ self.enLabel = en;
+
+ NSMutableParagraphStyle *para = [[NSMutableParagraphStyle alloc] init];
+ para.lineSpacing = 4;
+
+ self.cnLabel.attributedText = [[NSAttributedString alloc] initWithString:@"正在识别..." attributes:@{NSParagraphStyleAttributeName: para}];
+
+ self.enLabel.attributedText = [[NSAttributedString alloc] initWithString:@"翻译中..." attributes:@{NSParagraphStyleAttributeName: para}];
+
+ UIStackView *stack = [[UIStackView alloc] initWithArrangedSubviews:@[self.titleLabel, self.langLabel, self.cnLabel, self.enLabel]];
+ stack.axis = UILayoutConstraintAxisVertical;
+ stack.spacing = 12;
+ stack.alignment = UIStackViewAlignmentFill;
+ stack.distribution = UIStackViewDistributionFill;
+ stack.translatesAutoresizingMaskIntoConstraints = NO;
+ self.stackView = stack;
+
+ self.layoutMargins = UIEdgeInsetsMake(16, 16, 16, 16);
+ [self addSubview:self.stackView];
+
+ UILayoutGuide *guide = self.layoutMarginsGuide;
+ [NSLayoutConstraint activateConstraints:@[
+ [self.stackView.topAnchor constraintEqualToAnchor:guide.topAnchor],
+ [self.stackView.leadingAnchor constraintEqualToAnchor:guide.leadingAnchor],
+ [self.stackView.trailingAnchor constraintEqualToAnchor:guide.trailingAnchor],
+ [self.stackView.bottomAnchor constraintLessThanOrEqualToAnchor:guide.bottomAnchor]
+ ]];
+ }
+ return self;
+}
+
+- (void)play {
+
+}
+
+- (void)pause {
+
+}
+
+- (void)updateWithLang:(NSString *)lang cn:(NSString *)cn en:(NSString *)en {
+ if (lang != nil) {
+ self.langLabel.text = lang;
+ }
+ NSMutableParagraphStyle *para = [[NSMutableParagraphStyle alloc] init];
+ para.lineSpacing = 4;
+ if (cn != nil) {
+ self.cnLabel.attributedText = [[NSAttributedString alloc] initWithString:cn attributes:@{NSParagraphStyleAttributeName: para}];
+ }
+ if (en != nil) {
+ self.enLabel.attributedText = [[NSAttributedString alloc] initWithString:en attributes:@{NSParagraphStyleAttributeName: para}];
+ }
+}
+
+@end
diff --git a/local_plugins/native_plugin/ios/Resources/PrivacyInfo.xcprivacy b/local_plugins/native_plugin/ios/Resources/PrivacyInfo.xcprivacy
new file mode 100644
index 000000000..a34b7e2e6
--- /dev/null
+++ b/local_plugins/native_plugin/ios/Resources/PrivacyInfo.xcprivacy
@@ -0,0 +1,14 @@
+
+
+
+
+ NSPrivacyTrackingDomains
+
+ NSPrivacyAccessedAPITypes
+
+ NSPrivacyCollectedDataTypes
+
+ NSPrivacyTracking
+
+
+
diff --git a/local_plugins/native_plugin/ios/native_plugin.podspec b/local_plugins/native_plugin/ios/native_plugin.podspec
new file mode 100644
index 000000000..c84e9ffba
--- /dev/null
+++ b/local_plugins/native_plugin/ios/native_plugin.podspec
@@ -0,0 +1,29 @@
+#
+# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
+# Run `pod lib lint native_plugin.podspec` to validate before publishing.
+#
+Pod::Spec.new do |s|
+ s.name = 'native_plugin'
+ s.version = '0.0.1'
+ s.summary = 'A new Flutter plugin project.'
+ s.description = <<-DESC
+A new Flutter plugin project.
+ DESC
+ s.homepage = 'http://example.com'
+ s.license = { :file => '../LICENSE' }
+ s.author = { 'Your Company' => 'email@example.com' }
+ s.source = { :path => '.' }
+ s.source_files = 'Classes/**/*'
+ s.public_header_files = 'Classes/**/*.h'
+ s.dependency 'Flutter'
+ s.platform = :ios, '12.0'
+
+ # Flutter.framework does not contain a i386 slice.
+ s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' }
+
+ # If your plugin requires a privacy manifest, for example if it uses any
+ # required reason APIs, update the PrivacyInfo.xcprivacy file to describe your
+ # plugin's privacy impact, and then uncomment this line. For more information,
+ # see https://developer.apple.com/documentation/bundleresources/privacy_manifest_files
+ # s.resource_bundles = {'native_plugin_privacy' => ['Resources/PrivacyInfo.xcprivacy']}
+end
diff --git a/local_plugins/native_plugin/lib/native_plugin.dart b/local_plugins/native_plugin/lib/native_plugin.dart
new file mode 100644
index 000000000..c54721f1f
--- /dev/null
+++ b/local_plugins/native_plugin/lib/native_plugin.dart
@@ -0,0 +1,19 @@
+import 'native_plugin_platform_interface.dart';
+
+class NativePlugin {
+ Future getPlatformVersion() {
+ return NativePluginPlatform.instance.getPlatformVersion();
+ }
+
+ Future createPipContentView() {
+ return NativePluginPlatform.instance.createPipContentView();
+ }
+
+ Future updatePipContentView(Map data) {
+ return NativePluginPlatform.instance.updatePipContentView(data);
+ }
+
+ Future disposePipContentView(int viewId) {
+ return NativePluginPlatform.instance.disposePipContentView(viewId);
+ }
+}
diff --git a/local_plugins/native_plugin/lib/native_plugin_method_channel.dart b/local_plugins/native_plugin/lib/native_plugin_method_channel.dart
new file mode 100644
index 000000000..ec8e18ac5
--- /dev/null
+++ b/local_plugins/native_plugin/lib/native_plugin_method_channel.dart
@@ -0,0 +1,37 @@
+import 'package:flutter/foundation.dart';
+import 'package:flutter/services.dart';
+
+import 'native_plugin_platform_interface.dart';
+
+/// An implementation of [NativePluginPlatform] that uses method channels.
+class MethodChannelNativePlugin extends NativePluginPlatform {
+ /// The method channel used to interact with the native platform.
+ @visibleForTesting
+ final methodChannel = const MethodChannel('native_plugin');
+
+ @override
+ Future getPlatformVersion() async {
+ final version = await methodChannel.invokeMethod(
+ 'getPlatformVersion',
+ );
+ return version;
+ }
+
+ @override
+ Future createPipContentView() async {
+ final viewId = await methodChannel.invokeMethod(
+ 'createPipContentView',
+ );
+ return viewId ?? 0;
+ }
+
+ @override
+ Future updatePipContentView(Map data) async {
+ await methodChannel.invokeMethod('updatePipContentView', data);
+ }
+
+ @override
+ Future disposePipContentView(int viewId) async {
+ await methodChannel.invokeMethod('disposePipContentView', viewId);
+ }
+}
diff --git a/local_plugins/native_plugin/lib/native_plugin_platform_interface.dart b/local_plugins/native_plugin/lib/native_plugin_platform_interface.dart
new file mode 100644
index 000000000..c26495c2f
--- /dev/null
+++ b/local_plugins/native_plugin/lib/native_plugin_platform_interface.dart
@@ -0,0 +1,47 @@
+import 'package:plugin_platform_interface/plugin_platform_interface.dart';
+
+import 'native_plugin_method_channel.dart';
+
+abstract class NativePluginPlatform extends PlatformInterface {
+ /// Constructs a NativePluginPlatform.
+ NativePluginPlatform() : super(token: _token);
+
+ static final Object _token = Object();
+
+ static NativePluginPlatform _instance = MethodChannelNativePlugin();
+
+ /// The default instance of [NativePluginPlatform] to use.
+ ///
+ /// Defaults to [MethodChannelNativePlugin].
+ static NativePluginPlatform get instance => _instance;
+
+ /// Platform-specific implementations should set this with their own
+ /// platform-specific class that extends [NativePluginPlatform] when
+ /// they register themselves.
+ static set instance(NativePluginPlatform instance) {
+ PlatformInterface.verifyToken(instance, _token);
+ _instance = instance;
+ }
+
+ Future getPlatformVersion() {
+ throw UnimplementedError('platformVersion() has not been implemented.');
+ }
+
+ Future createPipContentView() {
+ throw UnimplementedError(
+ 'createPipContentView() has not been implemented.',
+ );
+ }
+
+ Future updatePipContentView(Map data) {
+ throw UnimplementedError(
+ 'updatePipContentView() has not been implemented.',
+ );
+ }
+
+ Future disposePipContentView(int viewId) {
+ throw UnimplementedError(
+ 'disposePipContentView() has not been implemented.',
+ );
+ }
+}
diff --git a/local_plugins/native_plugin/pubspec.yaml b/local_plugins/native_plugin/pubspec.yaml
new file mode 100644
index 000000000..a284bfce8
--- /dev/null
+++ b/local_plugins/native_plugin/pubspec.yaml
@@ -0,0 +1,72 @@
+name: native_plugin
+description: "A new Flutter plugin project."
+version: 0.0.1
+homepage:
+
+environment:
+ sdk: ^3.7.0
+ flutter: '>=3.3.0'
+
+dependencies:
+ flutter:
+ sdk: flutter
+ plugin_platform_interface: ^2.0.2
+
+dev_dependencies:
+ flutter_test:
+ sdk: flutter
+ flutter_lints: ^5.0.0
+
+# For information on the generic Dart part of this file, see the
+# following page: https://dart.dev/tools/pub/pubspec
+
+# The following section is specific to Flutter packages.
+flutter:
+ # This section identifies this Flutter project as a plugin project.
+ # The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.)
+ # which should be registered in the plugin registry. This is required for
+ # using method channels.
+ # The Android 'package' specifies package in which the registered class is.
+ # This is required for using method channels on Android.
+ # The 'ffiPlugin' specifies that native code should be built and bundled.
+ # This is required for using `dart:ffi`.
+ # All these are used by the tooling to maintain consistency when
+ # adding or updating assets for this project.
+ plugin:
+ platforms:
+ android:
+ package: com.example.native_plugin
+ pluginClass: NativePlugin
+ ios:
+ pluginClass: NativePlugin
+
+ # To add assets to your plugin package, add an assets section, like this:
+ # assets:
+ # - images/a_dot_burr.jpeg
+ # - images/a_dot_ham.jpeg
+ #
+ # For details regarding assets in packages, see
+ # https://flutter.dev/to/asset-from-package
+ #
+ # An image asset can refer to one or more resolution-specific "variants", see
+ # https://flutter.dev/to/resolution-aware-images
+
+ # To add custom fonts to your plugin package, add a fonts section here,
+ # in this "flutter" section. Each entry in this list should have a
+ # "family" key with the font family name, and a "fonts" key with a
+ # list giving the asset and other descriptors for the font. For
+ # example:
+ # fonts:
+ # - family: Schyler
+ # fonts:
+ # - asset: fonts/Schyler-Regular.ttf
+ # - asset: fonts/Schyler-Italic.ttf
+ # style: italic
+ # - family: Trajan Pro
+ # fonts:
+ # - asset: fonts/TrajanPro.ttf
+ # - asset: fonts/TrajanPro_Bold.ttf
+ # weight: 700
+ #
+ # For details regarding fonts in packages, see
+ # https://flutter.dev/to/font-from-package
diff --git a/pubspec.yaml b/pubspec.yaml
index c2cbdf8c3..d58b22f96 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,7 +1,7 @@
name: voitrans
description: "Voitrans - AI Voice Assistant."
publish_to: "none"
-version: 1.0.18+54
+version: 1.0.19+61
environment:
sdk: ">=3.3.0 <4.0.0"
@@ -76,6 +76,7 @@ dependencies:
dio: ^5.8.0+1
sqflite: ^2.4.2
path: ^1.9.1
+ pip: ^0.0.3
connectivity_plus: ^6.1.5 # 网络连接检测依赖
speech:
path: local_plugins/speech
@@ -111,6 +112,8 @@ dependencies:
path: local_plugins/user_portrait
qq_music:
path: local_plugins/qq_music
+ native_plugin:
+ path: local_plugins/native_plugin
test:
path: local_plugins/test
in_app_purchase: ^3.2.3 # 应用内购买插件