Commit f326e052 by ethanlamzs

Merge branch 'feature-2601-testing' into feature-2601

线上版本 1.0.8 已经发布
2 parents 62cba1b0 b98c3568
......@@ -9,8 +9,12 @@ flutter run -d macos
flutter run -d 00008030-001C75810E42402E --release
flutter run -d 00008030-001C75810E42402E --dart-define=env=dev --dart-define=version=1.0.0205
flutter run -d 00008140-001068C93AB8801C --release --dart-define=env=pro
flutter run -d 00008140-001068C93AB8801C --dart-define=env=dev --dart-define=version=1.0.0205
flutter run -d c165c6eb6ae36f56bf23091342bfd4641dc2a9f0 --release --dart-define=env=dev --dart-define=version=1.0.21
ipad air
......
......@@ -91,6 +91,9 @@ flutter {
}
dependencies {
// 添加 AppCompat 支持库(PrivacyActivity需要)
implementation("androidx.appcompat:appcompat:1.6.1")
// implementation("com.tencent.timpush:timpush:8.7.7201")
// implementation("com.tencent.liteav.tuikit:tuicore:8.7.7201")
// 版本号 "VERSION" 请前往 更新日志 中获取配置。
......@@ -99,9 +102,9 @@ dependencies {
// XiaoMi
//implementation("com.tencent.timpush:xiaomi:8.7.7201")
// OPPO
//implementation("com.tencent.timpush:oppo:8.7.7201")
implementation("com.tencent.timpush:oppo:8.8.7357")
// vivo
implementation("com.tencent.timpush:vivo:8.8.7357")
//implementation("com.tencent.timpush:vivo:8.8.7357")
// Honor
//implementation("com.tencent.timpush:honor:8.7.7201")
// Meizu
......
......@@ -10,10 +10,10 @@
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<!--<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />-->
<!--<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />-->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<!--<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />-->
<!--<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />-->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
......@@ -24,27 +24,35 @@
android:name="cn.banxe.bxe.MyApplication"
android:icon="@mipmap/launcher_icon"
android:networkSecurityConfig="@xml/network_security_config">
<!-- 隐私政策Activity - 启动入口 -->
<activity
android:name=".PrivacyActivity"
android:exported="true"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
while the Android UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
......
{"version":"1.0.1","cn.banxe.bxe":{"manifestPlaceholders":{"VIVO_APPKEY":"f7e37b20f48234c425d9e7f82fdd16c0","VIVO_APPID":"105993977","HONOR_APPID":""},"vivoPushBussinessId":"44580"}}
\ No newline at end of file
{"version":"1.0.1","cn.banxe.bxe":{"manifestPlaceholders":{"VIVO_APPKEY":"f7e37b20f48234c425d9e7f82fdd16c0","VIVO_APPID":"105993977","HONOR_APPID":""},"vivoPushBussinessId":"44580","oppoPushBussinessId":"46190","oppoPushAppKey":"922926ff488b4c2cb76bb4103ca8a93d","oppoPushAppSecret":"30636ddc19d94571aa1378db15d61ac0"}}
\ No newline at end of file
package cn.banxe.bxe;
import android.content.Context;
import android.content.SharedPreferences;
import com.tencent.chat.flutter.push.tencent_cloud_chat_push.application.TencentCloudChatPushApplication;
public class MyApplication extends TencentCloudChatPushApplication {
private static final String PREFS_NAME = "privacy_prefs";
private static final String KEY_AGREED = "privacy_agreed";
private static MyApplication instance;
@Override
public void onCreate() {
instance = this;
// 只有在用户已同意隐私政策时才执行初始化
if (isPrivacyAgreed()) {
super.onCreate();
}
}
private boolean isPrivacyAgreed() {
SharedPreferences prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
return prefs.getBoolean(KEY_AGREED, false);
}
/**
* 用户同意隐私政策后调用,执行延迟初始化
*/
public static void onPrivacyAgreed() {
if (instance != null && !instance.isPrivacyAgreed()) {
// 先保存同意状态
instance.savePrivacyAgreed();
// 再执行初始化
instance.initSDKs();
}
}
private void savePrivacyAgreed() {
SharedPreferences prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
prefs.edit().putBoolean(KEY_AGREED, true).apply();
}
private void initSDKs() {
// 执行父类的初始化逻辑v
super.onCreate();
}
}
\ No newline at end of file
package cn.banxe.bxe;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Typeface;
import android.net.Uri;
import android.os.Bundle;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.TextPaint;
import android.text.method.LinkMovementMethod;
import android.text.style.ClickableSpan;
import android.view.Gravity;
import android.view.View;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.TextView;
public class PrivacyActivity extends Activity {
private static final String PREFS_NAME = "privacy_prefs";
private static final String KEY_AGREED = "privacy_agreed";
private static final String USER_AGREEMENT_URL = "https://bxr.banxiaoer.net/apps/useragreement.html";
private static final String PRIVACY_POLICY_URL = "https://bxr.banxiaoer.net/apps/privacysettings.html";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 检查是否已同意隐私政策
if (isPrivacyAgreed()) {
startMainActivity();
return;
}
setContentView(createContentView());
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
// 如果已同意隐私政策,将新Intent转发给MainActivity
if (isPrivacyAgreed()) {
forwardToMainActivity(intent);
}
}
private boolean isPrivacyAgreed() {
SharedPreferences prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
return prefs.getBoolean(KEY_AGREED, false);
}
private void savePrivacyAgreed() {
SharedPreferences prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
prefs.edit().putBoolean(KEY_AGREED, true).apply();
}
private View createContentView() {
// 根布局
LinearLayout rootLayout = new LinearLayout(this);
rootLayout.setOrientation(LinearLayout.VERTICAL);
rootLayout.setGravity(Gravity.CENTER_VERTICAL);
rootLayout.setPadding(dpToPx(32), dpToPx(48), dpToPx(32), dpToPx(48));
rootLayout.setBackgroundColor(0xFFFFFFFF);
// 内容容器
LinearLayout contentLayout = new LinearLayout(this);
contentLayout.setOrientation(LinearLayout.VERTICAL);
LinearLayout.LayoutParams contentLayoutParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT);
rootLayout.addView(contentLayout, contentLayoutParams);
// 标题
TextView titleView = new TextView(this);
titleView.setText("用户协议与隐私政策");
titleView.setTextSize(22);
titleView.setTextColor(0xFF333333);
titleView.setTypeface(null, Typeface.BOLD);
titleView.setGravity(Gravity.CENTER);
titleView.setPadding(0, 0, 0, dpToPx(32));
contentLayout.addView(titleView);
// 内容(带可点击链接)
TextView contentView = new TextView(this);
contentView.setText(createClickableContent());
contentView.setMovementMethod(LinkMovementMethod.getInstance());
contentView.setHighlightColor(0x00000000);
contentView.setTextSize(17);
contentView.setTextColor(0xFF666666);
contentView.setLineSpacing(dpToPx(6), 1.0f);
contentView.setGravity(Gravity.START);
contentLayout.addView(contentView);
// 按钮容器
LinearLayout buttonLayout = new LinearLayout(this);
buttonLayout.setOrientation(LinearLayout.HORIZONTAL);
buttonLayout.setGravity(Gravity.CENTER);
LinearLayout.LayoutParams buttonLayoutParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT);
buttonLayoutParams.topMargin = dpToPx(48);
contentLayout.addView(buttonLayout, buttonLayoutParams);
// 同意按钮
Button agreeButton = new Button(this);
agreeButton.setText("同意");
agreeButton.setTextColor(0xFFFFFFFF);
agreeButton.setBackgroundColor(0xFF4CAF50);
agreeButton.setTextSize(16);
agreeButton.setGravity(Gravity.CENTER);
agreeButton.setSingleLine(true);
agreeButton.setMinWidth(dpToPx(120));
agreeButton.setMinHeight(dpToPx(48));
int btnPaddingH = dpToPx(24);
int btnPaddingV = dpToPx(12);
agreeButton.setPadding(btnPaddingH, btnPaddingV, btnPaddingH, btnPaddingV);
agreeButton.setOnClickListener(v -> onAgreeClicked());
LinearLayout.LayoutParams agreeParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT);
agreeParams.setMargins(dpToPx(16), 0, dpToPx(16), 0);
buttonLayout.addView(agreeButton, agreeParams);
// 不同意按钮
Button disagreeButton = new Button(this);
disagreeButton.setText("不同意");
disagreeButton.setTextColor(0xFF666666);
disagreeButton.setBackgroundColor(0xFFEEEEEE);
disagreeButton.setTextSize(16);
disagreeButton.setGravity(Gravity.CENTER);
disagreeButton.setSingleLine(true);
disagreeButton.setMinWidth(dpToPx(120));
disagreeButton.setMinHeight(dpToPx(48));
disagreeButton.setPadding(btnPaddingH, btnPaddingV, btnPaddingH, btnPaddingV);
disagreeButton.setOnClickListener(v -> onDisagreeClicked());
LinearLayout.LayoutParams disagreeParams = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT);
disagreeParams.setMargins(dpToPx(16), 0, dpToPx(16), 0);
buttonLayout.addView(disagreeButton, disagreeParams);
return rootLayout;
}
private int dpToPx(int dp) {
float density = getResources().getDisplayMetrics().density;
return (int) (dp * density + 0.5f);
}
private SpannableString createClickableContent() {
String text = "请您仔细阅读《用户协议》和《隐私政策》,充分理解协议内容。\n\n" +
"我们将严格遵守相关法律法规,保护您的个人信息安全。\n\n" +
"点击“同意”即表示您已阅读并同意相关协议。";
SpannableString spannableString = new SpannableString(text);
// 用户协议点击
int userAgreementStart = text.indexOf("《用户协议》");
int userAgreementEnd = userAgreementStart + "《用户协议》".length();
spannableString.setSpan(new ClickableSpan() {
@Override
public void onClick(View widget) {
openBrowser(USER_AGREEMENT_URL);
}
@Override
public void updateDrawState(TextPaint ds) {
super.updateDrawState(ds);
ds.setColor(0xFF4CAF50);
ds.setUnderlineText(true);
}
}, userAgreementStart, userAgreementEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
// 隐私政策点击
int privacyPolicyStart = text.indexOf("《隐私政策》");
int privacyPolicyEnd = privacyPolicyStart + "《隐私政策》".length();
spannableString.setSpan(new ClickableSpan() {
@Override
public void onClick(View widget) {
openBrowser(PRIVACY_POLICY_URL);
}
@Override
public void updateDrawState(TextPaint ds) {
super.updateDrawState(ds);
ds.setColor(0xFF4CAF50);
ds.setUnderlineText(true);
}
}, privacyPolicyStart, privacyPolicyEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
return spannableString;
}
private void openBrowser(String url) {
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
startActivity(intent);
}
private void onAgreeClicked() {
// 触发 Application 中的延迟初始化
MyApplication.onPrivacyAgreed();
startMainActivity();
}
private void onDisagreeClicked() {
new AlertDialog.Builder(this)
.setTitle("提示")
.setMessage("您需要同意隐私政策才能使用本应用,是否重新考虑?")
.setPositiveButton("重新考虑", (dialog, which) -> {
// 不做任何操作,保持当前界面
})
.setNegativeButton("仍不同意", (dialog, which) -> {
// 退出应用
finishAffinity();
})
.setCancelable(false)
.show();
}
private void startMainActivity() {
Intent intent = new Intent(this, MainActivity.class);
copyIntentData(getIntent(), intent);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
finish();
}
private void forwardToMainActivity(Intent sourceIntent) {
Intent intent = new Intent(this, MainActivity.class);
copyIntentData(sourceIntent, intent);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
finish();
}
private void copyIntentData(Intent source, Intent dest) {
if (source == null) return;
// 复制Extras
if (source.getExtras() != null) {
dest.putExtras(source.getExtras());
}
// 复制Data URI(微信回调可能使用)
if (source.getData() != null) {
dest.setData(source.getData());
}
// 复制Action
if (source.getAction() != null) {
dest.setAction(source.getAction());
}
// 复制所有分类
if (source.getCategories() != null) {
for (String category : source.getCategories()) {
dest.addCategory(category);
}
}
// 复制Flags(排除某些会导致问题的flags)
int flags = source.getFlags();
flags &= ~Intent.FLAG_ACTIVITY_NEW_TASK;
flags &= ~Intent.FLAG_ACTIVITY_CLEAR_TASK;
dest.setFlags(flags);
}
@Override
public void onBackPressed() {
// 禁止返回键
}
}
......@@ -63,6 +63,7 @@
9A5019F25CF19E1678164379 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
B2450CB3B5E968BD7CC513E6 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
D900F592122DCCA6D37CEE01 /* Runner.entitlements */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
DC07F8F62F347DCF00949AFA /* RunnerProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerProfile.entitlements; sourceTree = "<group>"; };
DC39D7842EFB981B00D795A8 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Main.strings"; sourceTree = "<group>"; };
DC39D7852EFB981B00D795A8 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/LaunchScreen.strings"; sourceTree = "<group>"; };
DC39D7862EFB985300D795A8 /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Base; path = Base.lproj/Info.plist; sourceTree = "<group>"; };
......@@ -135,6 +136,7 @@
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
DC07F8F62F347DCF00949AFA /* RunnerProfile.entitlements */,
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
......@@ -509,7 +511,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
......
import UIKit
import Flutter
// Add these two import lines
import TIMPush
import tencent_cloud_chat_push
@main
@objc class AppDelegate: FlutterAppDelegate {
@objc class AppDelegate: FlutterAppDelegate,TIMPushDelegate {
private var edgePanRecognizer: UIScreenEdgePanGestureRecognizer?
private var methodChannel: FlutterMethodChannel?
......@@ -57,6 +61,30 @@ import Flutter
print("👈 左边缘滑动检测到!已通知 Flutter")
}
}
// To be deprecated,please use the new field businessID below.
@objc func offlinePushCertificateID() -> Int32 {
return TencentCloudChatPushFlutterModal.shared.offlinePushCertificateID();
}
// Add this function
@objc func businessID() -> Int32 {
return TencentCloudChatPushFlutterModal.shared.businessID();
}
// Add this function
@objc func applicationGroupID() -> String {
return TencentCloudChatPushFlutterModal.shared.applicationGroupID()
}
// Add this function
@objc func onRemoteNotificationReceived(_ notice: String?) -> Bool {
TencentCloudChatPushPlugin.shared.tryNotifyDartOnNotificationClickEvent(notice)
return true
}
}
// 可选:防止冲突的手势识别设置
......
......@@ -90,17 +90,21 @@
</array>
<key>io.flutter.embedded_views_preview</key>
<true/>
<!-- <key>NSLocalNetworkUsageDescription</key>
<string>此应用需要访问本地网络以发现和连接智能设备</string> -->
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>提交打卡签到信息中需要使用当前的位置信息</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>提交打卡签到信息中需要使用当前的位置信息</string>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>为了搜索、连接并管理您的智能蓝牙设备</string>
<key>NSBonjourServices</key>
<array>
<string>_dartvm._tcp</string>
<string>_dartobservatory._tcp</string>
</array>
<!-- <key>UIBackgroundModes</key>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array> -->
<string>remote-notification</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<true/>
</dict>
......
......@@ -90,32 +90,28 @@
</array>
<key>io.flutter.embedded_views_preview</key>
<true/>
<!-- <key>NSLocalNetworkUsageDescription</key>
<string>此应用需要访问本地网络以发现和连接智能设备</string> -->
<key>NSBonjourServices</key>
<array>
<string>_dartvm._tcp</string>
<string>_dartobservatory._tcp</string>
</array>
<!-- <key>UIBackgroundModes</key>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array> -->
<string>remote-notification</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<true/>
<!-- 持续定位权限:用于后台持续获取位置(如导航、运动追踪) -->
<!-- <key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>为了在您针对老师布置信息反馈或作业消息提交时,可能需要涉及位置信息的提交要求,我们需要访问您的位置信息</string> -->
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>提交打卡签到信息中需要使用当前的位置信息</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>提交打卡签到信息中需要使用当前的位置信息</string>
<!-- 蓝牙权限:用于连接蓝牙设备 -->
<!-- <key>NSBluetoothAlwaysUsageDescription</key>
<string>为了搜索、连接并管理您的智能蓝牙设备(如手环、传感器),我们需要使用蓝牙功能</string> -->
<key>NSBluetoothAlwaysUsageDescription</key>
<string>为了搜索、连接并管理您的智能蓝牙设备</string>
<!-- 在使用时定位权限:用于应用在前台时获取位置 -->
<!-- <key>NSLocationWhenInUseUsageDescription</key>
<string>为了在您针对老师布置信息反馈或作业消息提交时,可能需要涉及位置信息的提交要求,我们需要在使用应用时获取您的位置信息。</string>
-->
<key>App Uses Non-Exempt Encryption</key>
<false/>
</dict>
......
......@@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.applesignin</key>
<array>
<string>Default</string>
......
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.developer.applesignin</key>
<array>
<string>Default</string>
</array>
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:dev.banxiaoer.net</string>
</array>
</dict>
</plist>
......@@ -90,18 +90,22 @@
</array>
<key>io.flutter.embedded_views_preview</key>
<true/>
<!-- <key>NSLocalNetworkUsageDescription</key>
<string>此应用需要访问本地网络以发现和连接智能设备</string> -->
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>提交打卡签到信息中需要使用当前的位置信息</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>提交打卡签到信息中需要使用当前的位置信息</string>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>为了搜索、连接并管理您的智能蓝牙设备</string>
<key>NSBonjourServices</key>
<array>
<string>_dartvm._tcp</string>
<!-- 对于更新的Flutter版本,有时还需要添加以下行 -->
<string>_dartobservatory._tcp</string>
</array>
<!-- <key>UIBackgroundModes</key>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array> -->
<string>remote-notification</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<true/>
</dict>
......
......@@ -90,18 +90,22 @@
</array>
<key>io.flutter.embedded_views_preview</key>
<true/>
<!-- <key>NSLocalNetworkUsageDescription</key>
<string>此应用需要访问本地网络以发现和连接智能设备</string> -->
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>提交打卡签到信息中需要使用当前的位置信息</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>提交打卡签到信息中需要使用当前的位置信息</string>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>为了搜索、连接并管理您的智能蓝牙设备</string>
<key>NSBonjourServices</key>
<array>
<string>_dartvm._tcp</string>
<!-- 对于更新的Flutter版本,有时还需要添加以下行 -->
<string>_dartobservatory._tcp</string>
</array>
<!-- <key>UIBackgroundModes</key>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array> -->
<string>remote-notification</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<true/>
</dict>
......
import 'dart:convert';
import 'package:appframe/config/routes.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:webview_flutter/webview_flutter.dart';
......@@ -8,22 +11,26 @@ class LinkState extends Equatable {
final bool loaded;
final String url;
final String title;
final int screenType; // 1: 竖屏, 2: 横屏
const LinkState({
this.loaded = false,
this.url = '',
this.title = '',
this.screenType = 1,
});
LinkState copyWith({
bool? loaded,
String? url,
String? title,
int? screenType,
}) {
return LinkState(
loaded: loaded ?? this.loaded,
url: url ?? this.url,
title: title ?? this.title,
screenType: screenType ?? this.screenType,
);
}
......@@ -32,15 +39,21 @@ class LinkState extends Equatable {
loaded,
url,
title,
screenType,
];
}
class LinkCubit extends Cubit<LinkState> {
late final WebViewController _controller;
String? msg;
WebViewController get controller => _controller;
LinkCubit(super.initialState) {
if (state.screenType != 1) {
_setOrientation(2);
}
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setNavigationDelegate(
......@@ -57,26 +70,81 @@ class LinkCubit extends Cubit<LinkState> {
final pageTitle = await _controller.runJavaScriptReturningResult('document.title') as String?;
// 移除可能存在的引号
final cleanTitle = pageTitle?.replaceAll('"', '');
// 检查 Cubit 是否已关闭,避免调用 emit 时抛出异常
if (!isClosed) {
emit(state.copyWith(title: cleanTitle ?? ''));
}
}
_finishLoading();
},
),
)
..addJavaScriptChannel("xeJsBridge", onMessageReceived: (JavaScriptMessage message) {})
..loadRequest(Uri.parse(state.url));
}
void _onMessageReceived(JavaScriptMessage message) async {
// try {
// _dispatcher.dispatch(message.message, (response) {
// _sendResponse(response);
// }, webCubit: this);
// } catch (e) {
// debugPrint('消息解析错误: $e');
// }
}
// 向H5发送响应
void _sendResponse(Map<String, dynamic> response) {
String jsonString = jsonEncode(response);
String escapedJson = jsonString.replaceAll('"', '\\"');
final String script = 'xeJsBridgeCallback("$escapedJson");';
_controller.runJavaScript(script);
}
void _finishLoading() {
// 检查 Cubit 是否已关闭,避免调用 emit 时抛出异常
if (!isClosed) {
emit(state.copyWith(loaded: true));
}
}
Future<void> handleBack(BuildContext context) async {
// 设置屏幕方向
void _setOrientation(int screenType) {
if (screenType == 2) {
// 横屏模式
SystemChrome.setPreferredOrientations([
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
]);
// 隐藏状态栏
SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
} else {
// 竖屏模式(默认)
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
]);
// 显示状态栏
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
}
}
Future<void> handleBack() async {
if (await _controller.canGoBack()) {
_controller.goBack();
} else {
// context.pop(true);
router.pop('ok');
router.pop(msg);
}
}
@override
Future<void> close() {
// 恢复为竖屏模式
if (state.screenType != 1) {
_setOrientation(1);
}
return super.close();
}
}
import 'dart:io';
import 'package:appframe/config/constant.dart';
import 'package:appframe/config/locator.dart';
import 'package:appframe/config/routes.dart';
......@@ -19,6 +21,7 @@ class LoginMainState extends Equatable {
final String appleUserIdentifier;
final bool loading;
final bool wechatInstalled;
final bool showPrivacyFirstTime;
const LoginMainState({
this.agreed = false,
......@@ -28,6 +31,7 @@ class LoginMainState extends Equatable {
this.appleUserIdentifier = '',
this.loading = false,
this.wechatInstalled = false,
this.showPrivacyFirstTime = false,
});
LoginMainState copyWith({
......@@ -38,6 +42,7 @@ class LoginMainState extends Equatable {
String? appleUserIdentifier,
bool? loading,
bool? wechatInstalled,
bool? showPrivacyFirstTime,
}) {
return LoginMainState(
agreed: agreed ?? this.agreed,
......@@ -47,6 +52,7 @@ class LoginMainState extends Equatable {
appleUserIdentifier: appleUserIdentifier ?? this.appleUserIdentifier,
loading: loading ?? this.loading,
wechatInstalled: wechatInstalled ?? this.wechatInstalled,
showPrivacyFirstTime: showPrivacyFirstTime ?? this.showPrivacyFirstTime,
);
}
......@@ -59,6 +65,7 @@ class LoginMainState extends Equatable {
appleUserIdentifier,
loading,
wechatInstalled,
showPrivacyFirstTime,
];
}
......@@ -79,6 +86,27 @@ class LoginMainCubit extends Cubit<LoginMainState> {
_fluwxCancelable = _fluwx.addSubscriber(_responseListener);
_wechatAuthRepository = getIt.get<WechatAuthRepository>();
_userAuthRepository = getIt.get<UserAuthRepository>();
// 检查是否首次打开登录页面,显示个人信息收集提示
if(Platform.isIOS) {
_checkFirstTimePrivacy();
}
}
// 检查是否首次打开,显示个人信息收集提示
void _checkFirstTimePrivacy() async {
var sharedPreferences = getIt.get<SharedPreferences>();
var hasShownPrivacy = sharedPreferences.getBool(Constant.hasShownPrivacyFirstTimeKey) ?? false;
if (!hasShownPrivacy) {
emit(state.copyWith(showPrivacyFirstTime: true));
}
}
// 确认已显示个人信息收集提示
void confirmPrivacyFirstTime() async {
var sharedPreferences = getIt.get<SharedPreferences>();
await sharedPreferences.setBool(Constant.hasShownPrivacyFirstTimeKey, true);
emit(state.copyWith(showPrivacyFirstTime: false));
}
void toggleAgreed(bool value) {
......
......@@ -380,9 +380,9 @@ class WebCubit extends Cubit<WebState> with WidgetsBindingObserver {
debugPrint("缓存自动登录处,IM 登录成功");
// 注册推送服务
imService.registerPush();
//
// 处理加群和退群
if (state.loginOpFlag) {
_getPendingGroup();
// imService.joinAndLeaveGroup(state.userCode!);
}
} else {
debugPrint("缓存自动登录处,IM 登录失败");
......@@ -390,51 +390,6 @@ class WebCubit extends Cubit<WebState> with WidgetsBindingObserver {
}
}
///
/// 获取待加群和待退群
///
Future<Map<String, List<String>>> _getPendingGroup() async {
var classIds = getIt.get<SharedPreferences>().getStringList(Constant.classIdSetKey);
List<String> classIdList = [];
if (classIds != null) {
// 转换和去重
classIdList = Set<String>.from(classIds).toList();
}
var unjoinedGroupIdList = <String>[];
var leaveGroupIdList = <String>[];
Map<String, List<String>> result = {
'unjoinedGroupIdList': unjoinedGroupIdList,
'leaveGroupIdList': leaveGroupIdList,
};
var imService = getIt.get<ImService>();
var joinedGroupIdList = await imService.getJoinedGroupList();
// 获取群组列表失败,joinedGroupIdList=null
if (joinedGroupIdList == null) {
return result;
}
// 需要加群
for (var classId in classIdList) {
if (!joinedGroupIdList.contains(classId)) {
unjoinedGroupIdList.add(classId);
}
}
// 需要退群
for (var joinedGroupId in joinedGroupIdList) {
if (!classIdList.contains(joinedGroupId)) {
leaveGroupIdList.add(joinedGroupId);
}
}
debugPrint('待加群: $unjoinedGroupIdList');
debugPrint('待退群: $leaveGroupIdList');
return result;
}
void _onMessageReceived(JavaScriptMessage message) async {
try {
_dispatcher.dispatch(message.message, (response) {
......@@ -515,6 +470,36 @@ class WebCubit extends Cubit<WebState> with WidgetsBindingObserver {
_sendResponse(resp);
}
///
/// 设置主Web状态
/// active: 1-激活状态 2-隐藏状态
///
void handleWebStatus(int active, {String extData = ''}) {
// setWebStatus指令
var resp = {
'unique': '',
'cmd': 'setWebStatus',
'data': {'active': active, 'extData': extData},
'errMsg': ''
};
_sendResponse(resp);
}
Future<void> handleToggleDebug() async {
var sharedPreferences = getIt.get<SharedPreferences>();
var debug = sharedPreferences.getInt('debug') ?? 0;
debug = (debug == 0 ? 1 : 0);
sharedPreferences.setInt('debug', debug);
var resp = {
'unique': '',
'cmd': 'toggleDebug',
'data': {'debug': debug},
'errMsg': ''
};
_sendResponse(resp);
}
bool setTitleBar(String title, String color, String bgColor, String icon) {
int parsedTitleColor = _hexStringToInt(color);
int parsedBgColor = _hexStringToInt(bgColor);
......
......@@ -53,7 +53,7 @@ class Constant {
static const String appVersion = EnvConfig.version;
/// H5的起始终最低版本号规则
static String h5Version = '0.1.6';
static String h5Version = '0.2.6';
/// H5的版本号存储的key
static const String h5VersionKey = 'h5_version';
......@@ -91,17 +91,31 @@ class Constant {
/// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
///
/// IM SDK
static const int imSdkAppId = EnvConfig.env == 'dev' ? 1400310691 : 1600117207;
static const String imClientSecure = EnvConfig.env == 'dev'
? 'kM4yqbehB3io9UiLvH6eHvM7xAhfYxoyyaO1tLoHgKltcaI7MZXkUbpFaWdeQIqe'
: 'GkMkhAnrCThYrZxApCBdFidcAC8USwVnhoqMGzqmSvmcegRCvETtDR2Te9btarnG';
// static const int imSdkAppId = EnvConfig.env == 'dev' ? 1400310691 : 1600117207;
// static const String imClientSecure = EnvConfig.env == 'dev'
// ? 'kM4yqbehB3io9UiLvH6eHvM7xAhfYxoyyaO1tLoHgKltcaI7MZXkUbpFaWdeQIqe'
// : 'GkMkhAnrCThYrZxApCBdFidcAC8USwVnhoqMGzqmSvmcegRCvETtDR2Te9btarnG';
static const int imSdkAppId = 1400310691; // 线上只保留这个实例
static const String imClientSecure = 'kM4yqbehB3io9UiLvH6eHvM7xAhfYxoyyaO1tLoHgKltcaI7MZXkUbpFaWdeQIqe';
// 苹果是根据发布环境来确定是采用什么推送
static const int apnsCertificateID = EnvConfig.apnsEnv == 'product'? 47804:47801;
/// Key
/// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
///
static const String classIdSetKey = 'auth_class_ids';
/// 隐私政策相关
/// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
///
/// 首次打开登录页面时,是否已显示个人信息收集提示
static const String hasShownPrivacyFirstTimeKey = 'has_shown_privacy_first_time';
/// 测试阶段使用
static const bool needIM = false;
static const bool needIM = true;
static const bool needUpgrade = true;
}
......@@ -3,6 +3,8 @@ class EnvConfig {
static const String version = String.fromEnvironment('version', defaultValue: '0.0.0');
static const String apnsEnv = String.fromEnvironment('apns', defaultValue: 'development');
static bool isDev() {
return env == 'dev';
}
......
......@@ -24,6 +24,7 @@ import 'package:appframe/data/repositories/message/role_info_handler.dart';
import 'package:appframe/data/repositories/message/save_file_to_disk_handler.dart';
import 'package:appframe/data/repositories/message/save_to_album_handler.dart';
import 'package:appframe/data/repositories/message/scan_code_handler.dart';
import 'package:appframe/data/repositories/message/screen_handler.dart';
import 'package:appframe/data/repositories/message/share_handler.dart';
import 'package:appframe/data/repositories/message/share_to_wx_handler.dart';
import 'package:appframe/data/repositories/message/storage_handler.dart';
......@@ -35,6 +36,7 @@ import 'package:appframe/data/repositories/message/wifi_info_handler.dart';
import 'package:appframe/data/repositories/message/window_info_handler.dart';
import 'package:appframe/data/repositories/phone_auth_repository.dart';
import 'package:appframe/data/repositories/user_auth_repository.dart';
import 'package:appframe/data/repositories/subs_repository.dart';
import 'package:appframe/data/repositories/wechat_auth_repository.dart';
import 'package:appframe/services/api_service.dart';
import 'package:appframe/services/dispatcher.dart';
......@@ -193,6 +195,9 @@ Future<void> setupLocator() async {
/// 设置用户角色信息
getIt.registerLazySingleton<MessageHandler>(() => RoleInfoHandler(), instanceName: 'setRoleInfo');
/// 设置屏幕模式
getIt.registerLazySingleton<MessageHandler>(() => ScreenHandler(), instanceName: 'setScreen');
/// service
///
/// local server
......@@ -221,4 +226,6 @@ Future<void> setupLocator() async {
getIt.registerLazySingleton<WechatAuthRepository>(() => WechatAuthRepository());
getIt.registerLazySingleton<PhoneAuthRepository>(() => PhoneAuthRepository());
getIt.registerLazySingleton<UserAuthRepository>(() => UserAuthRepository());
getIt.registerLazySingleton<SubsRepository>(() => SubsRepository());
}
......@@ -21,7 +21,7 @@ import 'package:go_router/go_router.dart';
final GoRouter router = GoRouter(
initialLocation: '/web',
observers: Platform.isIOS ? [AppRouteObserver()] : [],
observers: Platform.isIOS ? [LinkPageObserver(), IosGestureObserver()] : [LinkPageObserver()],
routes: <RouteBase>[
GoRoute(
path: '/web',
......@@ -113,53 +113,98 @@ final GoRouter router = GoRouter(
///
/// 只针对iOS使用
///
class AppRouteObserver extends NavigatorObserver {
@override
void didPush(Route route, Route? previousRoute) {
super.didPush(route, previousRoute);
class IosGestureObserver extends NavigatorObserver {
// @override
// void didPush(Route route, Route? previousRoute) {
// super.didPush(route, previousRoute);
//
// if (route.settings.name == '/web') {
// // push时,当前路由为 /web,代表 /web 路由被push进栈,展示web页面
// // 设置手势监听回调
// debugPrint("设置监听--------");
// IosEdgeSwipeDetector.onEdgeSwipe(
// () {
// WebCubitHolder.instance?.handleBack();
// },
// );
// } else if (previousRoute?.settings.name == '/web') {
// // push时,前一个路由是 /web,代表是从web页进入此页面
// // 将手势监听回调取消
// debugPrint("取消监听--------");
// IosEdgeSwipeDetector.dispose();
// }
// }
//
// @override
// void didPop(Route route, Route? previousRoute) {
// super.didPop(route, previousRoute);
//
// if (previousRoute?.settings.name == '/web') {
// // Pop时, 前一个路由是/web,代表回到web页面
// // 设置手势监听回调
// debugPrint("设置监听--------");
// IosEdgeSwipeDetector.onEdgeSwipe(
// () {
// WebCubitHolder.instance?.handleBack();
// },
// );
// }
// }
//
// @override
// void didRemove(Route route, Route? previousRoute) {
// super.didRemove(route, previousRoute);
//
// if (route.settings.name == '/web') {
// // remove时, 当前路由为 /web, 代表 /web 路由被删除,展示的不是web页面
// // 将手势监听回调取消
// debugPrint("取消监听--------");
// IosEdgeSwipeDetector.dispose();
// }
// }
if (route.settings.name == '/web') {
// push时,当前路由为 /web,代表 /web 路由被push进栈,展示web页面
// 设置手势监听回调
@override
void didChangeTop(Route<dynamic> topRoute, Route<dynamic>? previousTopRoute) {
if (topRoute.settings.name == '/web') {
debugPrint("设置监听--------");
IosEdgeSwipeDetector.onEdgeSwipe(
() {
WebCubitHolder.instance?.handleBack();
},
);
} else if (previousRoute?.settings.name == '/web') {
// push时,前一个路由是 /web,代表是从web页进入此页面
// 将手势监听回调取消
} else {
debugPrint("取消监听--------");
IosEdgeSwipeDetector.dispose();
}
}
}
///
/// 监控 /link 路由,作相应处理
///
class LinkPageObserver extends NavigatorObserver {
@override
void didPop(Route route, Route? previousRoute) {
super.didPop(route, previousRoute);
void didPush(Route route, Route? previousRoute) {
super.didPush(route, previousRoute);
if (previousRoute?.settings.name == '/web') {
// Pop时, 前一个路由是/web,代表回到web页面
// 设置手势监听回调
debugPrint("设置监听--------");
IosEdgeSwipeDetector.onEdgeSwipe(
() {
WebCubitHolder.instance?.handleBack();
},
);
if (route.settings.name == '/link' && previousRoute?.settings.name == '/web') {
debugPrint('---didPush--- route: ${route.settings.name}, previousRoute ${previousRoute?.settings.name}');
debugPrint('设置 WebStatus 为 2');
WebCubitHolder.instance?.handleWebStatus(2);
// 在 push 时就设置 pop 返回值的监听
route.popped.then((result) {
debugPrint('-------------路由 ${route.settings.name} 被 pop,返回值: $result');
debugPrint('设置 WebStatus 为 1');
WebCubitHolder.instance?.handleWebStatus(1, extData: result ?? '');
});
}
}
@override
void didRemove(Route route, Route? previousRoute) {
super.didRemove(route, previousRoute);
if (route.settings.name == '/web') {
// remove时, 当前路由为 /web, 代表 /web 路由被删除,展示的不是web页面
// 将手势监听回调取消
debugPrint("取消监听--------");
IosEdgeSwipeDetector.dispose();
void didPop(Route route, Route? previousRoute) {
if (previousRoute?.settings.name == '/web' && route.settings.name == '/link') {
debugPrint('---didPop--- route: ${route.settings.name}, previousRoute: ${previousRoute?.settings.name}');
}
}
}
......
......@@ -3,7 +3,7 @@ import 'package:appframe/services/dispatcher.dart';
class OpenLinkHandler extends MessageHandler {
@override
Future<bool> handleMessage(params) async {
Future<dynamic> handleMessage(params) async {
if (params is! Map<String, dynamic>) {
throw Exception('参数错误');
}
......@@ -12,12 +12,10 @@ class OpenLinkHandler extends MessageHandler {
if (url.isEmpty) {
throw Exception('参数错误');
}
int screenType = params['screenType'] ?? 1;
return _openLink(url);
}
router.push('/link', extra: {'url': url, 'screenType': screenType});
bool _openLink(String url) {
router.push('/link', extra: {'url': url});
return true;
}
}
import 'dart:convert';
import 'dart:io';
import 'package:appframe/services/dispatcher.dart';
import 'package:gallery_saver_plus/gallery_saver.dart';
import 'package:path_provider/path_provider.dart';
class SaveToAlbumHandler extends MessageHandler {
@override
......@@ -13,23 +16,21 @@ class SaveToAlbumHandler extends MessageHandler {
return false;
}
bool isVideo =
filePath.endsWith('.mp4') ||
filePath.endsWith('.avi') ||
filePath.endsWith('.mov') ||
filePath.endsWith('.mkv') ||
filePath.endsWith('.wmv') ||
filePath.endsWith('.flv') ||
filePath.endsWith('.webm') ||
filePath.endsWith('.m4v') ||
filePath.endsWith('.3gp');
String actualFilePath;
if (_isBase64(filePath)) {
actualFilePath = await _saveBase64ToFile(filePath);
} else {
actualFilePath = filePath;
}
bool isVideo = _isVideoFile(actualFilePath);
try {
if (isVideo) {
final bool? success = await GallerySaver.saveVideo(filePath);
final bool? success = await GallerySaver.saveVideo(actualFilePath);
return success ?? false;
} else {
final bool? success = await GallerySaver.saveImage(filePath);
final bool? success = await GallerySaver.saveImage(actualFilePath);
return success ?? false;
}
} catch (e) {
......@@ -37,4 +38,49 @@ class SaveToAlbumHandler extends MessageHandler {
return false;
}
}
bool _isBase64(String str) {
if (str.startsWith('data:image/')) {
return true;
}
try {
base64Decode(str);
return true;
} catch (e) {
return false;
}
}
Future<String> _saveBase64ToFile(String base64Str) async {
String base64Data = base64Str;
String fileExtension = 'png';
if (base64Str.startsWith('data:image/')) {
final regExp = RegExp(r'data:image/(\w+);base64,(.+)');
final match = regExp.firstMatch(base64Str);
if (match != null) {
fileExtension = match.group(1) ?? 'png';
base64Data = match.group(2) ?? '';
}
}
final bytes = base64Decode(base64Data);
final tempDir = await getTemporaryDirectory();
final filePath = '${tempDir.path}/${DateTime.now().millisecondsSinceEpoch}.$fileExtension';
final file = File(filePath);
await file.writeAsBytes(bytes);
return filePath;
}
bool _isVideoFile(String filePath) {
return filePath.endsWith('.mp4') ||
filePath.endsWith('.avi') ||
filePath.endsWith('.mov') ||
filePath.endsWith('.mkv') ||
filePath.endsWith('.wmv') ||
filePath.endsWith('.flv') ||
filePath.endsWith('.webm') ||
filePath.endsWith('.m4v') ||
filePath.endsWith('.3gp');
}
}
import 'package:appframe/services/dispatcher.dart';
import 'package:flutter/services.dart' hide MessageHandler;
class ScreenHandler extends MessageHandler {
@override
Future<dynamic> handleMessage(dynamic params) async {
if (params is! Map<String, dynamic>) {
throw Exception('参数错误');
}
int type = params['type'];
await _setOrientation(type);
return true;
}
Future<void> _setOrientation(int screenType) async {
if (screenType == 2) {
// 横屏模式
await SystemChrome.setPreferredOrientations([
DeviceOrientation.landscapeLeft,
DeviceOrientation.landscapeRight,
]);
// 隐藏状态栏
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive);
} else {
// 竖屏模式(默认)
await SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
]);
// 显示状态栏
await SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
}
}
}
......@@ -7,6 +7,7 @@ import 'package:appframe/config/locator.dart';
import 'package:appframe/services/api_service.dart';
import 'package:appframe/services/dispatcher.dart';
import 'package:appframe/utils/file_type_util.dart';
import 'package:appframe/utils/image_util.dart';
import 'package:appframe/utils/video_util.dart';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
......@@ -40,7 +41,7 @@ class UploadFileHandler extends MessageHandler {
final startTime = DateTime.now();
final result = await _handle(tempFilePath, busi, subBusi);
final endTime = DateTime.now();
print('====================>上传耗时:${endTime.millisecondsSinceEpoch - startTime.millisecondsSinceEpoch} 毫秒');
debugPrint('====================>上传耗时:${endTime.millisecondsSinceEpoch - startTime.millisecondsSinceEpoch} 毫秒');
// result['startTime'] = startTime.toString();
// result['endTime'] = endTime.toString();
......@@ -65,34 +66,57 @@ class UploadFileHandler extends MessageHandler {
if (!file.existsSync()) {
throw Exception('文件不存在');
}
//暂时仅支持200M的文件上传
var fileSize = file.lengthSync();
if (fileSize > 1024 * 1024 * 200) {
throw Exception('上传的文件过大');
}
print('原始文件大小:$fileSize 字节');
debugPrint('原始文件大小:$fileSize 字节');
///
/// 视频文件上传之前进行压缩
/// 非 mp4 格式的视频文件需先转码
///
String? mimeType = await FileTypeUtil.getMimeType(file);
if (mimeType?.startsWith('video/') ?? false) {
if (mimeType?.toLowerCase().startsWith('video/') ?? false) {
final inputPath = filePath;
final tempDir = await getTemporaryDirectory();
final outputPath = '${tempDir.path}/${Uuid().v4()}.mp4';
bool success = false;
var startTime = DateTime.now();
if (mimeType != 'video/mp4') {
await VideoUtil.convertToMp4(inputPath, outputPath);
success = await VideoUtil.convertToMp4(inputPath, outputPath);
} else {
await VideoUtil.compressVideo(inputPath, outputPath, 'low');
success = await VideoUtil.compressVideo(inputPath, outputPath, 'low');
}
var endTime = DateTime.now();
debugPrint('====================>压缩耗时:${endTime.millisecondsSinceEpoch - startTime.millisecondsSinceEpoch} 毫秒');
if (success) {
file = File(outputPath);
fileSize = file.lengthSync();
debugPrint('====================>视频压缩后大小:$fileSize 字节');
}
} else if (mimeType?.toLowerCase().startsWith('image/') ?? false) { // 对于图片文件,进行压缩
// 对于图片文件,进行压缩
final inputPath = filePath;
final tempDir = await getTemporaryDirectory();
final outputPath = '${tempDir.path}/${Uuid().v4()}.jpg';
var startTime = DateTime.now();
final success = await ImageUtil.compressImage(inputPath, outputPath, maxWidth: 1920, quality: 18);
var endTime = DateTime.now();
print('====================>压缩耗时:${endTime.millisecondsSinceEpoch - startTime.millisecondsSinceEpoch} 毫秒');
debugPrint('====================>图片压缩耗时:${endTime.millisecondsSinceEpoch - startTime.millisecondsSinceEpoch} 毫秒');
if (success) {
file = File(outputPath);
fileSize = file.lengthSync();
debugPrint('====================>图片压缩后大小:$fileSize 字节');
}
}
// 限制压缩后仍然大于300M的文件上传
if (fileSize > 1024 * 1024 * 300) {
throw Exception('上传的文件过大');
}
/// 2
......@@ -118,14 +142,14 @@ class UploadFileHandler extends MessageHandler {
///
final chunkSize = Constant.obsUploadChunkSize;
final totalChunks = (fileSize / chunkSize).ceil();
print('上传文件大小:$fileSize 字节');
print('分片数量:$totalChunks');
debugPrint('上传文件大小:$fileSize 字节');
debugPrint('分片数量:$totalChunks');
///
/// 5 sig
///
var startTime1 = DateTime.now();
print('====================>签名开始 $startTime1');
debugPrint('====================>签名开始 $startTime1');
final bxeApiService = ApiService(baseUrl: Constant.iotAppBaseUrl);
late String uploadId;
var signUrls = [];
......@@ -142,7 +166,7 @@ class UploadFileHandler extends MessageHandler {
}
}
var endTime1 = DateTime.now();
print('====================>签名耗时:${endTime1.millisecondsSinceEpoch - startTime1.millisecondsSinceEpoch} 毫秒');
debugPrint('====================>签名耗时:${endTime1.millisecondsSinceEpoch - startTime1.millisecondsSinceEpoch} 毫秒');
///
/// 6 上传
......@@ -187,7 +211,7 @@ class UploadFileHandler extends MessageHandler {
var startTime2 = DateTime.now();
String location = await _merge(bxeApiService, objectKey, bucket, uploadId, tagsMap);
var endTime2 = DateTime.now();
print('====================>合并签名耗时:${endTime2.millisecondsSinceEpoch - startTime2.millisecondsSinceEpoch} 毫秒');
debugPrint('====================>合并签名耗时:${endTime2.millisecondsSinceEpoch - startTime2.millisecondsSinceEpoch} 毫秒');
///
/// 8 针对视频生成封面
......@@ -241,7 +265,7 @@ class UploadFileHandler extends MessageHandler {
final resp = await _uploadChunk(dio, signUrl, chunk, chunkIndex);
var endTime = DateTime.now();
if (resp.statusCode == 200) {
print(
debugPrint(
'====================> 分片$chunkIndex${attempt + 1}次, $endTime 上传耗时:${endTime.millisecondsSinceEpoch - starTime.millisecondsSinceEpoch} 毫秒');
final etags = resp.headers['etag'] as List<String>;
return Future.value({'idx': chunkIndex + 1, 'etag': etags[0]}); // 上传成功
......@@ -249,7 +273,7 @@ class UploadFileHandler extends MessageHandler {
throw Exception('Chunk $chunkIndex upload failed: ${resp.statusCode}');
}
} catch (e) {
print('====================> 分片$chunkIndex${attempt + 1}次, 上传失败:${e.toString()}');
debugPrint('====================> 分片$chunkIndex${attempt + 1}次, 上传失败:${e.toString()}');
if (attempt == maxRetries) {
throw Exception('Chunk $chunkIndex upload failed after $maxRetries attempts: $e');
}
......@@ -265,14 +289,14 @@ class UploadFileHandler extends MessageHandler {
var url = signUrl.replaceFirst('AWSAccessKeyId=', 'AccessKeyId=').replaceFirst(':443', '');
try {
// Response response = await _put(url, chunk);
print('====================> 分片$chunkIndex , 开始上传 ${DateTime.now()}');
debugPrint('====================> 分片$chunkIndex , 开始上传 ${DateTime.now()}');
final response = await dio.put(
url,
// data: Stream.fromIterable(chunk.map((e) => [e])),
// data: Stream.fromIterable([chunk]),
data: chunk,
);
print('====================> 分片$chunkIndex , 上传成功 ${DateTime.now()}');
debugPrint('====================> 分片$chunkIndex , 上传成功 ${DateTime.now()}');
return response;
} catch (e) {
......@@ -365,7 +389,7 @@ class UploadFileHandler extends MessageHandler {
),
);
} catch (e) {
print(e);
debugPrint(e.toString());
}
}
}
import 'package:appframe/config/locator.dart';
import 'package:appframe/services/api_service.dart';
import 'package:dio/dio.dart';
class SubsRepository {
late final ApiService _appService;
SubsRepository() {
_appService = getIt<ApiService>(instanceName: 'appApiService');
}
Future<dynamic> userGroups(String type, String userid, String classCode) async {
Response resp = await _appService.post(
'/api/v1/comm/subs/usergroups',
{
"type": type,
"userid": userid,
"classCode": classCode,
},
);
return resp.data;
}
}
......@@ -4,7 +4,6 @@ import 'dart:io';
import 'package:appframe/config/constant.dart';
import 'package:appframe/config/env_config.dart';
import 'package:appframe/config/locator.dart';
import 'package:appframe/services/im_service.dart';
import 'package:appframe/ui/widgets/ios_edge_swipe_detector.dart';
import 'package:archive/archive.dart';
import 'package:flutter/material.dart';
......@@ -18,7 +17,6 @@ void main() async {
await setupLocator();
await _initH5Version();
await _initImSdk();
await _initIOSGesture();
runApp(const App());
......@@ -58,12 +56,6 @@ Future<void> _initH5Version() async {
}
}
Future<void> _initImSdk() async {
if (Constant.needIM) {
await getIt.get<ImService>().initSdk();
}
}
Future<void> _initIOSGesture() async {
if (Platform.isIOS) {
// ios边缘滑动检测
......
......@@ -55,7 +55,8 @@ class MessageDispatcher {
h5Message.cmd == "chooseVideo" ||
h5Message.cmd == "goLogin" ||
h5Message.cmd.startsWith("setTitlebar") ||
h5Message.cmd == "audioPlay") {
h5Message.cmd == "audioPlay" ||
h5Message.cmd == "openLink") {
handler.setCubit(webCubit!);
handler.setMessage(message);
}
......
import 'package:appframe/config/constant.dart';
import 'package:appframe/config/locator.dart';
import 'package:appframe/data/repositories/subs_repository.dart';
import 'package:appframe/services/api_service.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:tencent_cloud_chat_push/common/tim_push_listener.dart';
import 'package:tencent_cloud_chat_push/common/tim_push_message.dart';
import 'package:tencent_cloud_chat_push/tencent_cloud_chat_push.dart';
......@@ -63,13 +65,13 @@ class ImService {
},
onRecvMessageReadReceipts: (List<V2TimMessageReceipt> receiptList) {
//群聊已读回调
receiptList.forEach((element) {
for (var element in receiptList) {
element.groupID; // 群id
element.msgID; // 已读回执消息 ID
element.readCount; // 群消息最新已读数
element.unreadCount; // 群消息最新未读数
element.userID; // C2C 消息对方 ID
});
}
},
onRecvMessageRevoked: (String messageid) {
// 在本地维护的消息中处理被对方撤回的消息
......@@ -81,7 +83,7 @@ class ImService {
// 时间戳转换
DateTime dt = DateTime.fromMillisecondsSinceEpoch(message.timestamp! * 1000);
print(
debugPrint(
"收到IM消息—— 时间:${dt.year}-${dt.month}-${dt.day} ${dt.hour}:${dt.minute}:${dt.second} 发送者:${message.sender} 内容:${message.textElem?.text}");
// 目前只会有文本消息,所以其他消息类型暂不处理,直接return
......@@ -171,7 +173,7 @@ class ImService {
msgID: message.msgID!,
);
if (download.code == 0) {
List<V2TimMessage>? messageList = download.data;
//List<V2TimMessage>? messageList = download.data;
}
}
if (message.textElem?.nextElem != null) {
......@@ -185,11 +187,19 @@ class ImService {
V2TimAdvancedMsgListener get msgListener => _msgListener;
///
// 添加初始化状态标记
bool _isInitialized = false;
bool get isInitialized => _isInitialized;
/// 初始化 IM SDK
///
Future<bool> initSdk() async {
// 初始化SDK
// 防止重复初始化
if (_isInitialized) {
debugPrint("IM SDK 已经初始化,跳过");
return true;
}
var initSDKRes = await TencentImSDKPlugin.v2TIMManager.initSDK(
sdkAppID: Constant.imSdkAppId,
loglevel: LogLevelEnum.V2TIM_LOG_ALL,
......@@ -197,10 +207,11 @@ class ImService {
);
if (initSDKRes.code == 0) {
print("IM SDK 初始化成功-------- ${initSDKRes.data}");
_isInitialized = true; // 标记已初始化
debugPrint("IM SDK 初始化成功-------- ${initSDKRes.data}");
return true;
} else {
print("IM SDK 初始化失败-------- ${initSDKRes.data}");
debugPrint("IM SDK 初始化失败-------- ${initSDKRes.data}");
return false;
}
}
......@@ -223,6 +234,9 @@ class ImService {
/// 登录 IM
///
Future<bool> login(String userID) async {
// 登录前先初始化SDK
await initSdk();
// 登录前先判断登录状态
var loginStatus = await TencentImSDKPlugin.v2TIMManager.getLoginStatus();
if (loginStatus.code == 0) {
......@@ -244,21 +258,21 @@ class ImService {
V2TimCallback res = await TencentImSDKPlugin.v2TIMManager.login(userID: userID, userSig: userSig);
loginStatus = await TencentImSDKPlugin.v2TIMManager.getLoginStatus();
print('IM 登录状态:${loginStatus.data}');
debugPrint('IM 登录状态:${loginStatus.data}');
if (res.code == 0) {
print("IM 登录成功--------");
debugPrint("IM 登录成功--------");
// 添加消息的事件监听器
// await TencentImSDKPlugin.v2TIMManager.getMessageManager().addAdvancedMsgListener(listener: msgListener);
await addMsgListener(_msgListener);
var loginUserResp = await TencentImSDKPlugin.v2TIMManager.getLoginUser();
print("当前登录用户:${loginUserResp.data}");
debugPrint("当前登录用户:${loginUserResp.data}");
return true;
} else {
// 登录失败逻辑
print("IM 登录失败--------");
debugPrint("IM 登录失败--------");
return false;
}
}
......@@ -271,14 +285,14 @@ class ImService {
var logoutRes = await TencentImSDKPlugin.v2TIMManager.logout();
if (logoutRes.code == 0) {
// 登出成功逻辑
print("IM 登出成功--------");
debugPrint("IM 登出成功--------");
return true;
} else {
return false;
}
}
Future<List<String>?> getJoinedGroupList() async {
Future<List<String>?> _getJoinedGroupList() async {
var groupListRes = await TencentImSDKPlugin.v2TIMManager.getGroupManager().getJoinedGroupList();
if (groupListRes.code == 0) {
debugPrint("获取群列表成功--------");
......@@ -290,9 +304,51 @@ class ImService {
}
}
void joinAndLeaveGroup(String userId) async {
List<String>? classIds = getIt.get<SharedPreferences>().getStringList(Constant.classIdSetKey);
List<String> classIdList = [];
if (classIds != null) {
// 转换和去重
classIdList = Set<String>.from(classIds).toList();
}
var unjoinedGroupIdList = <String>[];
var leaveGroupIdList = <String>[];
var joinedGroupIdList = await _getJoinedGroupList();
// 获取群组列表失败,joinedGroupIdList=null
if (joinedGroupIdList == null) {
return;
}
// 需要加群
for (var classId in classIdList) {
if (!joinedGroupIdList.contains(classId)) {
unjoinedGroupIdList.add(classId);
}
}
// 需要退群
for (var joinedGroupId in joinedGroupIdList) {
if (!classIdList.contains(joinedGroupId)) {
leaveGroupIdList.add(joinedGroupId);
}
}
// 发送请求
var subsRepository = getIt.get<SubsRepository>();
if (unjoinedGroupIdList.isNotEmpty) {
debugPrint("需要加入:$unjoinedGroupIdList");
await subsRepository.userGroups('addMember', userId, unjoinedGroupIdList.join(","));
}
if (leaveGroupIdList.isNotEmpty) {
debugPrint("需要退出:$leaveGroupIdList");
await subsRepository.userGroups('deleteMember', userId, leaveGroupIdList.join(","));
}
}
void _onNotificationClicked({required String ext, String? userID, String? groupID}) {
print("收到推送消息--------");
print("_onNotificationClicked: $ext, userID: $userID, groupID: $groupID");
debugPrint("_onNotificationClicked: $ext, userID: $userID, groupID: $groupID");
if (userID != null || groupID != null) {
// 根据 userID 或 groupID 跳转至对应 Message 页面.
} else {
......@@ -302,16 +358,23 @@ class ImService {
TIMPushListener timPushListener = TIMPushListener(
onRecvPushMessage: (TimPushMessage message) {
print('推送监听器 onRecvPushMessage-------------');
debugPrint('推送监听器 onRecvPushMessage-------------');
String messageLog = message.toLogString();
debugPrint("message: $messageLog");
// // 手机消息通知
// getIt.get<NotificationService>().showNotification(
// id: DateTime.now().millisecondsSinceEpoch % 1000000,
// title: message.title ?? '',
// body: message.desc ?? '',
// );
},
onRevokePushMessage: (String messageId) {
print('推送监听器 onRevokePushMessage-------------');
debugPrint('推送监听器 onRevokePushMessage-------------');
debugPrint("message: $messageId");
},
onNotificationClicked: (String ext) {
print('推送监听器 onNotificationClicked-------------');
debugPrint('推送监听器 onNotificationClicked-------------');
debugPrint("ext: $ext");
},
);
......@@ -323,9 +386,10 @@ class ImService {
onNotificationClicked: _onNotificationClicked,
sdkAppId: Constant.imSdkAppId,
appKey: Constant.imClientSecure,
apnsCertificateID: Constant.apnsCertificateID,
);
if (res.code == 0) {
print('注册推送成功--------');
debugPrint('注册推送成功--------');
/// 添加监听器
///
......@@ -333,20 +397,20 @@ class ImService {
var getIdRes = await TencentCloudChatPush().getRegistrationID();
if (getIdRes.code == 0) {
print('getRegistrationID: ${getIdRes.data}');
debugPrint('getRegistrationID: ${getIdRes.data}');
} else {
print('getRegistrationID: ${getIdRes.errorMessage}');
debugPrint('getRegistrationID error: ${getIdRes.errorMessage}');
}
var tokenRes = await TencentCloudChatPush().getAndroidPushToken();
if (tokenRes.code == 0) {
print('android Token: ${tokenRes.data}');
debugPrint('android Token: ${tokenRes.data}');
} else {
print('android Token: ${tokenRes.errorMessage}');
debugPrint('android Token error: ${tokenRes.errorMessage}');
}
} else {
print('注册推送失败--------');
print('${res.errorMessage}');
debugPrint('注册推送失败--------');
debugPrint('${res.errorMessage}');
}
}
}
......@@ -12,11 +12,12 @@ class LinkPage extends StatelessWidget {
@override
Widget build(BuildContext buildContext) {
final Map<String, dynamic>? extraData = GoRouterState.of(buildContext).extra as Map<String, dynamic>?;
final String? url = extraData?['url'];
final String url = extraData?['url'];
final String? title = extraData?['title'];
final int screenType = extraData?['screenType'] ?? 1; // 1: 竖屏, 2: 横屏
return BlocProvider(
create: (context) => LinkCubit(LinkState(loaded: false, url: url!, title: title ?? '')),
create: (context) => LinkCubit(LinkState(url: url, title: title ?? '', screenType: screenType)),
child: BlocConsumer<LinkCubit, LinkState>(
builder: (ctx, state) {
final scaffold = Scaffold(
......@@ -25,6 +26,12 @@ class LinkPage extends StatelessWidget {
centerTitle: true,
backgroundColor: Color(0xFF7691FA),
iconTheme: IconThemeData(color: Colors.white),
leading: IconButton(
icon: const Icon(Icons.arrow_back, color: Colors.white),
onPressed: () {
ctx.read<LinkCubit>().handleBack();
},
),
),
body: state.loaded
? SizedBox(
......@@ -46,7 +53,7 @@ class LinkPage extends StatelessWidget {
if (didPop) {
return;
}
ctx.read<LinkCubit>().handleBack(ctx);
ctx.read<LinkCubit>().handleBack();
},
child: scaffold,
);
......
......@@ -162,6 +162,8 @@ class LoginMainPage extends StatelessWidget {
_showAgreementDialog(context, context.read<LoginMainCubit>());
} else if (state.showNeedWechatForApple) {
_showNeedWechatDialogForApple(context, context.read<LoginMainCubit>());
} else if (state.showPrivacyFirstTime) {
_showPrivacyFirstTimeDialog(context, context.read<LoginMainCubit>());
}
},
),
......@@ -236,7 +238,7 @@ class LoginMainPage extends StatelessWidget {
style: TextStyle(color: Color(0xFF666666), fontSize: 14),
),
TextSpan(
text: '《班小二数据安全和隐私政策》',
text: '《班小二数据安全和隐私政策》',
style: TextStyle(color: Color(0xFF7691FA), fontSize: 14),
recognizer: TapGestureRecognizer()
..onTap = () {
......@@ -262,7 +264,7 @@ class LoginMainPage extends StatelessWidget {
},
),
TextSpan(
text: '。如您同意上述文件的全部内容,请点击“同意”以继续。',
text: '。如您同意上述文件的全部内容,请点击"同意"以继续。',
style: TextStyle(color: Color(0xFF666666), fontSize: 14),
),
],
......@@ -432,4 +434,111 @@ class LoginMainPage extends StatelessWidget {
},
);
}
// 首次打开显示个人信息收集提示弹窗
Future<void> _showPrivacyFirstTimeDialog(BuildContext context, LoginMainCubit loginMainCubit) async {
await showDialog(
context: context,
barrierDismissible: false,
builder: (BuildContext context) {
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(5),
),
),
title: Text(
'重要提示',
style: TextStyle(
fontSize: 17,
color: Color(0xFF000000),
),
textAlign: TextAlign.center,
),
content: SingleChildScrollView(
child: Text.rich(
TextSpan(
children: [
TextSpan(
text: '尊敬的用户,欢迎使用班小二APP!\n\n',
style: TextStyle(color: Color(0xFF666666), fontSize: 14),
),
// TextSpan(
// text: '为了向您提供更好的服务,我们需要收集以下个人信息:\n\n',
// style: TextStyle(color: Color(0xFF666666), fontSize: 14),
// ),
// TextSpan(
// text: '• 收集目的:用于用户身份识别、登录验证、提供教育相关服务\n',
// style: TextStyle(color: Color(0xFF666666), fontSize: 14),
// ),
// TextSpan(
// text: '• 收集方式:通过微信授权、手机号验证等方式获取\n',
// style: TextStyle(color: Color(0xFF666666), fontSize: 14),
// ),
// TextSpan(
// text: '• 信息范围:包括您的微信昵称、头像、手机号、学生信息等\n\n',
// style: TextStyle(color: Color(0xFF666666), fontSize: 14),
// ),
TextSpan(
text: '我们将严格遵守',
style: TextStyle(color: Color(0xFF666666), fontSize: 14),
),
TextSpan(
text: '《班小二数据安全和隐私政策》',
style: TextStyle(color: Color(0xFF7691FA), fontSize: 14),
recognizer: TapGestureRecognizer()
..onTap = () {
router.push(
'/link',
extra: {'url': 'https://bxr.banxiaoer.net/apps/privacysettings.html', 'title': '隐私保障'},
);
},
),
TextSpan(
text: '与',
style: TextStyle(color: Color(0xFF666666), fontSize: 14),
),
TextSpan(
text: '《用户协议》',
style: TextStyle(color: Color(0xFF7691FA), fontSize: 14),
recognizer: TapGestureRecognizer()
..onTap = () {
router.push(
'/link',
extra: {'url': 'https://bxr.banxiaoer.net/apps/useragreement.html', 'title': '用户协议'},
);
},
),
TextSpan(
text: '保护您的个人信息安全。',
style: TextStyle(color: Color(0xFF666666), fontSize: 14),
),
],
),
),
),
actions: [
Center(
child: TextButton(
onPressed: () {
Navigator.of(context).pop();
loginMainCubit.confirmPrivacyFirstTime();
},
style: TextButton.styleFrom(
foregroundColor: Color(0xFF7691FA),
textStyle: TextStyle(fontSize: 17),
minimumSize: Size.fromHeight(40),
padding: EdgeInsets.zero,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.zero,
),
),
child: Text('我知道了'),
),
),
],
);
},
);
}
}
......@@ -5,12 +5,12 @@ import 'package:mime/mime.dart';
class FileTypeUtil {
static Future<bool> isImage(File file) async {
var mimeType = await getMimeType(file);
return mimeType?.startsWith('image/') ?? false;
return mimeType?.toLowerCase().startsWith('image/') ?? false;
}
static Future<bool> isVideo(File file) async {
var mimeType = await getMimeType(file);
return mimeType?.startsWith('video/') ?? false;
return mimeType?.toLowerCase().startsWith('video/') ?? false;
}
static Future<String?> getMimeType(File file) async {
......
......@@ -49,4 +49,44 @@ class ImageUtil {
return null;
}
}
/// 使用 FFmpeg 压缩图片
///
/// [inputPath] 原始图片路径
/// [outputPath] 压缩后图片输出路径
/// [maxWidth] 最大宽度,超过此宽度将等比缩放,默认1920
/// [quality] JPEG 输出质量 (1-31,数值越小质量越高),默认18
/// 返回是否压缩成功
static Future<bool> compressImage(String inputPath, String outputPath, {int maxWidth = 1920, int quality = 18}) async {
try {
// 检查源文件是否存在
final imageFile = File(inputPath);
if (!await imageFile.exists()) {
print('源图片文件不存在: $inputPath');
return false;
}
// 构建 FFmpeg 命令行参数
String cmd = '-i "$inputPath" ' // 指定输入文件路径
'-vf "scale=min($maxWidth\\,iw):-1:flags=lanczos" ' // 等比缩放,宽度不超过maxWidth,使用Lanczos算法
'-q:v $quality ' // 设置JPEG输出质量
'-y ' // 覆盖已存在的输出文件
'"$outputPath"'; // 指定输出文件路径
// 执行 FFmpeg 命令
final session = await FFmpegKit.execute(cmd);
final returnCode = await session.getReturnCode();
// 检查执行结果
if (ReturnCode.isSuccess(returnCode)) {
return true;
} else {
print('图片压缩失败: ${await session.getFailStackTrace()}');
return false;
}
} catch (e) {
print('图片压缩出错: $e');
return false;
}
}
}
......@@ -52,13 +52,13 @@ cd ios
#flutter build ipa --export-method ad-hoc --dart-define=env=$env --dart-define=version=$_main_ver$_ver
if [ "$env" == 'pub' ]; then
flutter build ipa --release --dart-define=env=$env --dart-define=version=$_main_ver$_ver
flutter build ipa --release --dart-define=env=$env --dart-define=version=$_main_ver$_ver --dart-define=apns=product
cd $base_root
mkdir -p dist
cp -f build/ios/ipa/banxiaoer.ipa dist/'banxiaoer_release_'$env'_'$_ver'.ipa'
echo 'build and publish release package is done '$env
else
flutter build ipa --export-method ad-hoc --dart-define=env=$env --dart-define=version=$_main_ver$_ver
flutter build ipa --export-method ad-hoc --dart-define=env=$env --dart-define=version=$_main_ver$_ver --dart-define=apns=product
cd $base_root
mkdir -p dist
cp -f build/ios/ipa/banxiaoer.ipa dist/'banxiaoer_adhoc_'$env'_'$_ver'.ipa'
......
name: appframe
description: "app frame project."
publish_to: 'none'
version: 1.0.6
version: 1.0.8
environment:
sdk: ">=3.5.0 <4.0.0"
......
Styling with Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!