Prechádzať zdrojové kódy

[new]增加shortcut插件

zk 1 rok pred
rodič
commit
93c7fa572c
40 zmenil súbory, kde vykonal 1443 pridanie a 18 odobranie
  1. BIN
      assets/images/bg_desktop_shortcut_header.webp
  2. BIN
      assets/images/icon_desktop_shortcut_close.webp
  3. 5 0
      assets/string/base/string.xml
  4. 0 1
      lib/data/repositories/config_repository.dart
  5. 120 0
      lib/dialog/desktop_shortcut_dialog.dart
  6. 4 0
      lib/main.dart
  7. 24 0
      lib/module/main/controller.dart
  8. 19 3
      lib/module/record/controller.dart
  9. 1 1
      lib/module/record/view.dart
  10. 22 4
      lib/module/splash/controller.dart
  11. 45 0
      lib/utils/android_shortcut.dart
  12. 95 0
      lib/utils/desktop_shortcut_utils.dart
  13. 1 1
      lib/utils/mmkv_util.dart
  14. 29 0
      plugin/shortcut/.gitignore
  15. 33 0
      plugin/shortcut/.metadata
  16. 3 0
      plugin/shortcut/CHANGELOG.md
  17. 1 0
      plugin/shortcut/LICENSE
  18. 15 0
      plugin/shortcut/README.md
  19. 4 0
      plugin/shortcut/analysis_options.yaml
  20. 9 0
      plugin/shortcut/android/.gitignore
  21. 74 0
      plugin/shortcut/android/build.gradle
  22. BIN
      plugin/shortcut/android/gradle/wrapper/gradle-wrapper.jar
  23. 7 0
      plugin/shortcut/android/gradle/wrapper/gradle-wrapper.properties
  24. 249 0
      plugin/shortcut/android/gradlew
  25. 92 0
      plugin/shortcut/android/gradlew.bat
  26. 1 0
      plugin/shortcut/android/settings.gradle
  27. 2 0
      plugin/shortcut/android/src/main/AndroidManifest.xml
  28. 95 0
      plugin/shortcut/android/src/main/java/com/atmob/shortcut/ShortcutPlugin.java
  29. 168 0
      plugin/shortcut/android/src/main/java/com/atmob/shortcut/ShortcutUtil.java
  30. 38 0
      plugin/shortcut/ios/.gitignore
  31. 0 0
      plugin/shortcut/ios/Assets/.gitkeep
  32. 19 0
      plugin/shortcut/ios/Classes/ShortcutPlugin.swift
  33. 14 0
      plugin/shortcut/ios/Resources/PrivacyInfo.xcprivacy
  34. 29 0
      plugin/shortcut/ios/shortcut.podspec
  35. 26 0
      plugin/shortcut/lib/shortcut.dart
  36. 68 0
      plugin/shortcut/lib/shortcut_method_channel.dart
  37. 41 0
      plugin/shortcut/lib/shortcut_platform_interface.dart
  38. 72 0
      plugin/shortcut/pubspec.yaml
  39. 15 8
      pubspec.lock
  40. 3 0
      pubspec.yaml

BIN
assets/images/bg_desktop_shortcut_header.webp


BIN
assets/images/icon_desktop_shortcut_close.webp


+ 5 - 0
assets/string/base/string.xml

@@ -135,4 +135,9 @@
     <string name="please_choice_local_audio_file">请选择本地音频文件</string>
     <string name="please_choice_local_audio_file">请选择本地音频文件</string>
     <string name="recording_duration_cannot_less_than_3s">录音时长不能低于3秒</string>
     <string name="recording_duration_cannot_less_than_3s">录音时长不能低于3秒</string>
     <string name="audio_not_support_type">不支持该格式</string>
     <string name="audio_not_support_type">不支持该格式</string>
+    <string name="add_desktop_shortcut">添加快听到桌面</string>
+    <string name="add_desktop_shortcut_tips">想更快打开录音吗?\n添加“快听”到桌面,让小听更快听见
+    </string>
+    <string name="add_desktop_shortcut_btn_txt">立即添加</string>
+    <string name="desktop_shortcut_record_name">小听快听</string>
 </resources>
 </resources>

+ 0 - 1
lib/data/repositories/config_repository.dart

@@ -1,7 +1,6 @@
 import 'package:electronic_assistant/base/app_base_request.dart';
 import 'package:electronic_assistant/base/app_base_request.dart';
 import 'package:electronic_assistant/data/api/atmob_api.dart';
 import 'package:electronic_assistant/data/api/atmob_api.dart';
 import 'package:electronic_assistant/utils/async_util.dart';
 import 'package:electronic_assistant/utils/async_util.dart';
-import 'package:electronic_assistant/utils/cancel_future.dart';
 import 'package:electronic_assistant/utils/http_handler.dart';
 import 'package:electronic_assistant/utils/http_handler.dart';
 
 
 import '../api/response/example_info_response.dart';
 import '../api/response/example_info_response.dart';

+ 120 - 0
lib/dialog/desktop_shortcut_dialog.dart

@@ -0,0 +1,120 @@
+import 'package:electronic_assistant/resource/assets.gen.dart';
+import 'package:electronic_assistant/resource/colors.gen.dart';
+import 'package:electronic_assistant/resource/string.gen.dart';
+import 'package:electronic_assistant/utils/common_style.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
+import 'package:get/get.dart';
+
+void showAddDesktopShortcutDialog(
+    {required void Function() onConfirm, required void Function() onCancel}) {
+  SmartDialog.show(
+      backType: SmartBackType.block,
+      clickMaskDismiss: false,
+      builder: (_) {
+        return IntrinsicHeight(
+          child: Column(
+            children: [
+              Stack(
+                children: [
+                  Assets.images.bgDesktopShortcutHeader
+                      .image(width: 300.w, height: 142.w),
+                  Positioned(
+                    top: 14.w,
+                    right: 14.w,
+                    child: GestureDetector(
+                      onTap: () {
+                        onCancel.call();
+                        SmartDialog.dismiss();
+                      },
+                      child: Assets.images.iconDesktopShortcutClose
+                          .image(width: 28.w, height: 28.w),
+                    ),
+                  ),
+                ],
+              ),
+              Container(
+                decoration: BoxDecoration(
+                    color: ColorName.white,
+                    borderRadius: BorderRadius.only(
+                        bottomLeft: Radius.circular(12.w),
+                        bottomRight: Radius.circular(12.w))),
+                width: 300.w,
+                child: Column(
+                  children: [
+                    SizedBox(height: 24.h),
+                    Text(
+                      StringName.addDesktopShortcut.tr,
+                      style: TextStyle(
+                          fontWeight: FontWeight.bold,
+                          fontSize: 18.sp,
+                          color: ColorName.primaryTextColor),
+                    ),
+                    SizedBox(height: 6.h),
+                    SizedBox(
+                        width: 238.w,
+                        child: Text(
+                          textAlign: TextAlign.center,
+                          StringName.addDesktopShortcutTips.tr,
+                          style: TextStyle(
+                              fontSize: 14.sp,
+                              color: ColorName.secondaryTextColor),
+                        )),
+                    SizedBox(height: 33.h),
+                    GestureDetector(
+                      onTap: () {
+                        SmartDialog.dismiss();
+                        onConfirm.call();
+                      },
+                      child: Container(
+                          width: 268.w,
+                          height: 48.w,
+                          decoration: getCommonDecoration(8.w),
+                          child: Center(
+                              child: Text(
+                            StringName.addDesktopShortcutBtnTxt.tr,
+                            style: TextStyle(
+                                fontSize: 16.sp, color: ColorName.white),
+                          ))),
+                    ),
+                    SizedBox(height: 16.h),
+                  ],
+                ),
+              )
+            ],
+          ),
+        );
+      });
+}
+
+void showAddDesktopShortcutTipsDialog(
+    {required void Function() onConfirm, required void Function() onDismiss}) {
+  SmartDialog.show(
+      onDismiss: onDismiss,
+      builder: (_) {
+        return IntrinsicHeight(
+          child: Container(
+            color: ColorName.white,
+            child: Column(
+              children: [
+                Text(
+                  '添加到桌面可能会失败,需要去设置页-权限进行授权,授权后再次尝试即可',
+                  style: TextStyle(
+                      fontSize: 14.sp, color: ColorName.primaryTextColor),
+                ),
+                GestureDetector(
+                    onTap: () {
+                      SmartDialog.dismiss();
+                      onConfirm.call();
+                    },
+                    child: SizedBox(
+                        width: 100.w,
+                        height: 30.w,
+                        child: Center(child: Text('确认'))))
+              ],
+            ),
+          ),
+        );
+      });
+}

+ 4 - 0
lib/main.dart

@@ -9,6 +9,7 @@ import 'package:electronic_assistant/router/app_pages.dart';
 import 'package:electronic_assistant/sdk/gravity/gravity_helper.dart';
 import 'package:electronic_assistant/sdk/gravity/gravity_helper.dart';
 import 'package:electronic_assistant/utils/app_info_util.dart';
 import 'package:electronic_assistant/utils/app_info_util.dart';
 import 'package:electronic_assistant/device/device_info_util.dart';
 import 'package:electronic_assistant/device/device_info_util.dart';
+import 'package:electronic_assistant/utils/desktop_shortcut_utils.dart';
 import 'package:electronic_assistant/utils/mmkv_util.dart';
 import 'package:electronic_assistant/utils/mmkv_util.dart';
 import 'package:electronic_assistant/utils/toast_util.dart';
 import 'package:electronic_assistant/utils/toast_util.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
@@ -27,6 +28,9 @@ void main() async {
 
 
   FlutterForegroundTask.initCommunicationPort();
   FlutterForegroundTask.initCommunicationPort();
 
 
+  //注册快捷方式
+  await DesktopShortcutUtils.registerDesktopShortcut();
+
   //全局配置smartDialog
   //全局配置smartDialog
   smartConfig();
   smartConfig();
   //mmkv
   //mmkv

+ 24 - 0
lib/module/main/controller.dart

@@ -11,6 +11,8 @@ import 'package:flutter/cupertino.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/material.dart';
 import 'package:get/get.dart';
 import 'package:get/get.dart';
 
 
+import '../../router/app_pages.dart';
+import '../../utils/desktop_shortcut_utils.dart';
 import '../login/view.dart';
 import '../login/view.dart';
 
 
 class MainController extends BaseController {
 class MainController extends BaseController {
@@ -39,6 +41,28 @@ class MainController extends BaseController {
 
 
   DateTime? get lastPressedAt => _lastPressedAt;
   DateTime? get lastPressedAt => _lastPressedAt;
 
 
+  @override
+  void onInit() {
+    super.onInit();
+    accountRepository;
+  }
+
+  @override
+  void onReady() {
+    super.onReady();
+    _initParameters();
+  }
+
+  void _initParameters() {
+    if (parameters == null) {
+      return;
+    }
+    String? action = parameters?[LaunchAction.key];
+    if (action == LaunchAction.recordAudioAction) {
+      Get.toNamed(RoutePath.record);
+    }
+  }
+
   void changeIndex(int index) {
   void changeIndex(int index) {
     _currentIndex.value = index;
     _currentIndex.value = index;
   }
   }

+ 19 - 3
lib/module/record/controller.dart

@@ -2,6 +2,7 @@ import 'dart:io';
 import 'dart:typed_data';
 import 'dart:typed_data';
 
 
 import 'package:electronic_assistant/base/base_controller.dart';
 import 'package:electronic_assistant/base/base_controller.dart';
+import 'package:electronic_assistant/data/bean/talks.dart';
 import 'package:electronic_assistant/data/consts/error_code.dart';
 import 'package:electronic_assistant/data/consts/error_code.dart';
 import 'package:electronic_assistant/data/consts/event_report_id.dart';
 import 'package:electronic_assistant/data/consts/event_report_id.dart';
 import 'package:electronic_assistant/data/repositories/talk_repository.dart';
 import 'package:electronic_assistant/data/repositories/talk_repository.dart';
@@ -12,6 +13,7 @@ import 'package:electronic_assistant/module/record/record_task.dart';
 import 'package:electronic_assistant/module/talk/view.dart';
 import 'package:electronic_assistant/module/talk/view.dart';
 import 'package:electronic_assistant/resource/string.gen.dart';
 import 'package:electronic_assistant/resource/string.gen.dart';
 import 'package:electronic_assistant/router/app_pages.dart';
 import 'package:electronic_assistant/router/app_pages.dart';
+import 'package:electronic_assistant/utils/desktop_shortcut_utils.dart';
 import 'package:electronic_assistant/utils/http_handler.dart';
 import 'package:electronic_assistant/utils/http_handler.dart';
 import 'package:electronic_assistant/utils/mmkv_util.dart';
 import 'package:electronic_assistant/utils/mmkv_util.dart';
 import 'package:electronic_assistant/utils/toast_util.dart';
 import 'package:electronic_assistant/utils/toast_util.dart';
@@ -23,6 +25,7 @@ import 'package:record/record.dart';
 import 'package:uuid/uuid.dart';
 import 'package:uuid/uuid.dart';
 import 'package:wakelock_plus/wakelock_plus.dart';
 import 'package:wakelock_plus/wakelock_plus.dart';
 
 
+import '../../dialog/desktop_shortcut_dialog.dart';
 import '../../utils/pcm_wav_converter.dart';
 import '../../utils/pcm_wav_converter.dart';
 import '../../widget/frame_animation_view.dart';
 import '../../widget/frame_animation_view.dart';
 
 
@@ -114,7 +117,9 @@ class RecordController extends BaseController {
             ));
             ));
   }
   }
 
 
-  void addShortcut() {}
+  void addShortcut() {
+    DesktopShortcutUtils.requestAddDesktopShortcut();
+  }
 
 
   void onBackClick() {
   void onBackClick() {
     if (currentStatus.value == RecordStatus.pending ||
     if (currentStatus.value == RecordStatus.pending ||
@@ -264,8 +269,7 @@ class RecordController extends BaseController {
             _recordConfig.numChannels, 16);
             _recordConfig.numChannels, 16);
         pcmFile.delete();
         pcmFile.delete();
         KVUtil.putString(keyLastRecordId, "");
         KVUtil.putString(keyLastRecordId, "");
-        Get.back();
-        TalkPage.start(talkInfo, eventTag: EventId.id_001);
+        _dealSuccessNextStep(talkInfo);
       } else {
       } else {
         throw Exception("pcm file not found");
         throw Exception("pcm file not found");
       }
       }
@@ -285,6 +289,18 @@ class RecordController extends BaseController {
     });
     });
   }
   }
 
 
+  void _dealSuccessNextStep(TalkBean talkInfo) {
+    DesktopShortcutUtils.isShowTipsDialog(() {
+      _routerToTalkPage(talkInfo);
+    });
+  }
+
+  //跳转至谈话详情界面
+  void _routerToTalkPage(TalkBean talkInfo) {
+    Get.back();
+    TalkPage.start(talkInfo, eventTag: EventId.id_001);
+  }
+
   void _changeRecordStatus(RecordStatus status) {
   void _changeRecordStatus(RecordStatus status) {
     currentStatus.value = status;
     currentStatus.value = status;
     status == RecordStatus.recording ? frameAnimationController.play() : null;
     status == RecordStatus.recording ? frameAnimationController.play() : null;

+ 1 - 1
lib/module/record/view.dart

@@ -51,7 +51,7 @@ class RecordPage extends BasePage<RecordController> {
           backgroundColor: ColorName.transparent,
           backgroundColor: ColorName.transparent,
           systemOverlayStyle: SystemUiOverlayStyle.light,
           systemOverlayStyle: SystemUiOverlayStyle.light,
           actions: [
           actions: [
-            _buildAddShortcut(false),
+            _buildAddShortcut(true),
           ],
           ],
         ),
         ),
         backgroundColor: ColorName.transparent,
         backgroundColor: ColorName.transparent,

+ 22 - 4
lib/module/splash/controller.dart

@@ -5,6 +5,7 @@ import 'package:electronic_assistant/base/base_controller.dart';
 import 'package:electronic_assistant/data/consts/constants.dart';
 import 'package:electronic_assistant/data/consts/constants.dart';
 import 'package:electronic_assistant/module/browser/view.dart';
 import 'package:electronic_assistant/module/browser/view.dart';
 import 'package:electronic_assistant/router/app_pages.dart';
 import 'package:electronic_assistant/router/app_pages.dart';
+import 'package:electronic_assistant/utils/desktop_shortcut_utils.dart';
 import 'package:electronic_assistant/utils/expand.dart';
 import 'package:electronic_assistant/utils/expand.dart';
 import 'package:electronic_assistant/widget/alert_dialog.dart';
 import 'package:electronic_assistant/widget/alert_dialog.dart';
 import 'package:flutter/cupertino.dart';
 import 'package:flutter/cupertino.dart';
@@ -18,14 +19,11 @@ class SplashController extends BaseController {
 
 
   @override
   @override
   void onReady() {
   void onReady() {
-    // TODO: implement onInit
     super.onReady();
     super.onReady();
 
 
     final isAgreePrivacy = isAgreePrivacyPolicy();
     final isAgreePrivacy = isAgreePrivacyPolicy();
     if (isAgreePrivacy) {
     if (isAgreePrivacy) {
-      Timer(Duration(seconds: splashDelayedTime), () {
-        Get.offNamed(RoutePath.mainTab);
-      });
+      isAgreePrivacyNextStep();
     } else {
     } else {
       EAAlertDialog.show(
       EAAlertDialog.show(
         title: "隐私政策及权限说明",
         title: "隐私政策及权限说明",
@@ -89,4 +87,24 @@ class SplashController extends BaseController {
       );
       );
     }
     }
   }
   }
+
+  isAgreePrivacyNextStep() {
+    //判断是否有额外操作执行
+    if (DesktopShortcutUtils.getRouteMap() != null) {
+      final routeMap = DesktopShortcutUtils.getRouteMap();
+      if (routeMap?[LaunchAction.key] == LaunchAction.recordAudioAction) {
+        _goMain(Duration.zero, arguments: routeMap);
+      } else {
+        _goMain(Duration(seconds: splashDelayedTime));
+      }
+    } else {
+      _goMain(Duration(seconds: splashDelayedTime));
+    }
+  }
+
+  void _goMain(Duration delayTime, {Map<String, dynamic>? arguments}) {
+    Timer(delayTime, () {
+      Get.offNamed(RoutePath.mainTab, arguments: arguments);
+    });
+  }
 }
 }

+ 45 - 0
lib/utils/android_shortcut.dart

@@ -0,0 +1,45 @@
+import 'package:electronic_assistant/resource/assets.gen.dart';
+import 'package:electronic_assistant/resource/string.gen.dart';
+import 'package:electronic_assistant/utils/desktop_shortcut_utils.dart';
+import 'package:electronic_assistant/utils/toast_util.dart';
+import 'package:flutter/widgets.dart';
+import 'package:get/get.dart';
+import 'package:shortcut/shortcut.dart';
+
+class AndroidShortCut {
+  AndroidShortCut._();
+
+  final _flutterPinnedShortcutPlugin = FlutterShortcut();
+
+  FlutterShortcut get flutterPinnedShortcutPlugin =>
+      _flutterPinnedShortcutPlugin;
+
+  void addRecordShortcut() {
+    _flutterPinnedShortcutPlugin.createShortcut(
+        id: "electronic_1",
+        label: StringName.desktopShortcutRecordName.tr,
+        action: LaunchAction.recordAudioAction,
+        iconAssetName: Assets.images.iconFilesFile.path);
+  }
+
+  void register() {
+    _flutterPinnedShortcutPlugin.actionStream().listen((action) {
+      debugPrint('actionStream: $action');
+      switch (action) {
+        case LaunchAction.recordAudioAction:
+          DesktopShortcutUtils.setRouteAction(LaunchAction.recordAudioAction);
+          break;
+      }
+    });
+    _flutterPinnedShortcutPlugin.getLaunchAction((action) {
+      debugPrint('getLaunchAction: $action');
+      switch (action) {
+        case LaunchAction.recordAudioAction:
+          DesktopShortcutUtils.setLaunchAction(LaunchAction.recordAudioAction);
+          break;
+      }
+    });
+  }
+}
+
+final androidShortCut = AndroidShortCut._();

+ 95 - 0
lib/utils/desktop_shortcut_utils.dart

@@ -0,0 +1,95 @@
+import 'dart:io';
+
+import 'package:electronic_assistant/base/base_controller.dart';
+import 'package:electronic_assistant/router/app_pages.dart';
+import 'package:electronic_assistant/utils/mmkv_util.dart';
+import 'package:get/get.dart';
+import 'package:get/get_core/src/get_main.dart';
+import '../dialog/desktop_shortcut_dialog.dart';
+import 'android_shortcut.dart';
+
+class DesktopShortcutUtils {
+  DesktopShortcutUtils._();
+
+  static const String routePath = 'route_path';
+
+  static const String _showMaxTimeTag = 'showMaxTime';
+  static const int _showMaxFrequency = 1;
+
+  static Map<String, dynamic>? intentMap;
+
+  static Future<void> registerDesktopShortcut() async {
+    if (Platform.isAndroid) {
+      androidShortCut.register();
+    } else if (Platform.isIOS) {
+      //TODO IOS
+    }
+  }
+
+  static void isShowTipsDialog(void Function() nextCallback) {
+    if (!_isShow()) {
+      nextCallback();
+      return;
+    }
+    showAddDesktopShortcutDialog(onConfirm: () {
+      if (Platform.isAndroid) {
+        showAddDesktopShortcutTipsDialog(onConfirm: () {
+          androidShortCut.addRecordShortcut();
+        }, onDismiss: () {
+          nextCallback();
+        });
+      } else if (Platform.isIOS) {
+        //TODO IOS
+      }
+    }, onCancel: () {
+      nextCallback();
+    });
+    _setShowOnce();
+  }
+
+  static bool _isShow() {
+    return KVUtil.getInt(_showMaxTimeTag, 0) < _showMaxFrequency;
+  }
+
+  static void _setShowOnce() {
+    int maxFrequency = KVUtil.getInt(_showMaxTimeTag, 0);
+    maxFrequency++;
+    KVUtil.putInt(_showMaxTimeTag, maxFrequency);
+  }
+
+  static void requestAddDesktopShortcut() {
+    if (Platform.isAndroid) {
+      showAddDesktopShortcutTipsDialog(
+          onConfirm: () {
+            androidShortCut.addRecordShortcut();
+          },
+          onDismiss: () {});
+    } else if (Platform.isIOS) {
+      //TODO IOS
+    }
+  }
+
+  static void setLaunchAction(String action) {
+    intentMap ??= {};
+    intentMap?[LaunchAction.key] = action;
+  }
+
+  static Map<String, dynamic>? getRouteMap() {
+    return intentMap;
+  }
+
+  static void clearIntentMap() {
+    intentMap?.clear();
+  }
+
+  static void setRouteAction(String recordAudioAction) {
+    if (recordAudioAction == LaunchAction.recordAudioAction) {
+      Get.toNamed(RoutePath.record);
+    }
+  }
+}
+
+class LaunchAction {
+  static const String key = 'launchAction';
+  static const String recordAudioAction = 'record_audio';
+}

+ 1 - 1
lib/utils/mmkv_util.dart

@@ -25,7 +25,7 @@ class KVUtil {
     _getMMKV().encodeInt(key, value);
     _getMMKV().encodeInt(key, value);
   }
   }
 
 
-  static int? getInt(String key, int defaultValue) {
+  static int getInt(String key, int defaultValue) {
     return _getMMKV().decodeInt(key, defaultValue: defaultValue);
     return _getMMKV().decodeInt(key, defaultValue: defaultValue);
   }
   }
 
 

+ 29 - 0
plugin/shortcut/.gitignore

@@ -0,0 +1,29 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.buildlog/
+.history
+.svn/
+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/
+build/

+ 33 - 0
plugin/shortcut/.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: "80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819"
+  channel: "stable"
+
+project_type: plugin
+
+# Tracks metadata for the flutter migrate command
+migration:
+  platforms:
+    - platform: root
+      create_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819
+      base_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819
+    - platform: android
+      create_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819
+      base_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819
+    - platform: ios
+      create_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819
+      base_revision: 80c2e84975bbd28ecf5f8d4bd4ca5a2490bfc819
+
+  # 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'

+ 3 - 0
plugin/shortcut/CHANGELOG.md

@@ -0,0 +1,3 @@
+## 0.0.1
+
+* TODO: Describe initial release.

+ 1 - 0
plugin/shortcut/LICENSE

@@ -0,0 +1 @@
+TODO: Add your license here.

+ 15 - 0
plugin/shortcut/README.md

@@ -0,0 +1,15 @@
+# shortcut
+
+创建桌面快捷方式
+
+## 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.
+

+ 4 - 0
plugin/shortcut/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

+ 9 - 0
plugin/shortcut/android/.gitignore

@@ -0,0 +1,9 @@
+*.iml
+.gradle
+/local.properties
+/.idea/workspace.xml
+/.idea/libraries
+.DS_Store
+/build
+/captures
+.cxx

+ 74 - 0
plugin/shortcut/android/build.gradle

@@ -0,0 +1,74 @@
+group = "com.atmob.shortcut"
+version = "1.0"
+
+buildscript {
+    repositories {
+        google()
+        mavenCentral()
+    }
+
+    dependencies {
+        classpath("com.android.tools.build:gradle:7.3.0")
+    }
+}
+
+rootProject.allprojects {
+    repositories {
+        google()
+        mavenCentral()
+    }
+}
+// 加载 local.properties 文件
+def localProperties = new Properties()
+def localPropertiesFile = rootProject.file('local.properties')
+if (localPropertiesFile.exists()) {
+    localPropertiesFile.withInputStream { stream ->
+        localProperties.load(stream)
+    }
+}
+
+// 读取变量
+def flutterSdk = localProperties.getProperty('flutter.sdk')
+
+apply plugin: "com.android.library"
+
+android {
+    if (project.android.hasProperty("namespace")) {
+        namespace = "com.atmob.shortcut"
+    }
+
+    compileSdk = 34
+
+    compileOptions {
+        sourceCompatibility = JavaVersion.VERSION_1_8
+        targetCompatibility = JavaVersion.VERSION_1_8
+    }
+
+    defaultConfig {
+        minSdk = 21
+    }
+
+    dependencies {
+        //flutter
+        compileOnly files("$flutterSdk/bin/cache/artifacts/engine/android-arm/flutter.jar")
+
+        //AndroidX
+        compileOnly "androidx.annotation:annotation:1.1.0"
+
+        //AndroidX
+        compileOnly "androidx.core:core:1.13.1"
+
+        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
+            }
+        }
+    }
+}

BIN
plugin/shortcut/android/gradle/wrapper/gradle-wrapper.jar


+ 7 - 0
plugin/shortcut/android/gradle/wrapper/gradle-wrapper.properties

@@ -0,0 +1,7 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists

+ 249 - 0
plugin/shortcut/android/gradlew

@@ -0,0 +1,249 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+#
+#   Gradle start up script for POSIX generated by Gradle.
+#
+#   Important for running:
+#
+#   (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+#       noncompliant, but you have some other compliant shell such as ksh or
+#       bash, then to run this script, type that shell name before the whole
+#       command line, like:
+#
+#           ksh Gradle
+#
+#       Busybox and similar reduced shells will NOT work, because this script
+#       requires all of these POSIX shell features:
+#         * functions;
+#         * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+#           «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+#         * compound commands having a testable exit status, especially «case»;
+#         * various built-in commands including «command», «set», and «ulimit».
+#
+#   Important for patching:
+#
+#   (2) This script targets any POSIX shell, so it avoids extensions provided
+#       by Bash, Ksh, etc; in particular arrays are avoided.
+#
+#       The "traditional" practice of packing multiple parameters into a
+#       space-separated string is a well documented source of bugs and security
+#       problems, so this is (mostly) avoided, by progressively accumulating
+#       options in "$@", and eventually passing that to Java.
+#
+#       Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+#       and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+#       see the in-line comments for details.
+#
+#       There are tweaks for specific operating systems such as AIX, CygWin,
+#       Darwin, MinGW, and NonStop.
+#
+#   (3) This script is generated from the Groovy template
+#       https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+#       within the Gradle project.
+#
+#       You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+    APP_HOME=${app_path%"${app_path##*/}"}  # leaves a trailing /; empty if no leading path
+    [ -h "$app_path" ]
+do
+    ls=$( ls -ld "$app_path" )
+    link=${ls#*' -> '}
+    case $link in             #(
+      /*)   app_path=$link ;; #(
+      *)    app_path=$APP_HOME$link ;;
+    esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+    echo "$*"
+} >&2
+
+die () {
+    echo
+    echo "$*"
+    echo
+    exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in                #(
+  CYGWIN* )         cygwin=true  ;; #(
+  Darwin* )         darwin=true  ;; #(
+  MSYS* | MINGW* )  msys=true    ;; #(
+  NONSTOP* )        nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+        # IBM's JDK on AIX uses strange locations for the executables
+        JAVACMD=$JAVA_HOME/jre/sh/java
+    else
+        JAVACMD=$JAVA_HOME/bin/java
+    fi
+    if [ ! -x "$JAVACMD" ] ; then
+        die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+else
+    JAVACMD=java
+    if ! command -v java >/dev/null 2>&1
+    then
+        die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+    fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+    case $MAX_FD in #(
+      max*)
+        # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+        # shellcheck disable=SC2039,SC3045
+        MAX_FD=$( ulimit -H -n ) ||
+            warn "Could not query maximum file descriptor limit"
+    esac
+    case $MAX_FD in  #(
+      '' | soft) :;; #(
+      *)
+        # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+        # shellcheck disable=SC2039,SC3045
+        ulimit -n "$MAX_FD" ||
+            warn "Could not set maximum file descriptor limit to $MAX_FD"
+    esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+#   * args from the command line
+#   * the main class name
+#   * -classpath
+#   * -D...appname settings
+#   * --module-path (only if needed)
+#   * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+    APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+    CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+    JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+    # Now convert the arguments - kludge to limit ourselves to /bin/sh
+    for arg do
+        if
+            case $arg in                                #(
+              -*)   false ;;                            # don't mess with options #(
+              /?*)  t=${arg#/} t=/${t%%/*}              # looks like a POSIX filepath
+                    [ -e "$t" ] ;;                      #(
+              *)    false ;;
+            esac
+        then
+            arg=$( cygpath --path --ignore --mixed "$arg" )
+        fi
+        # Roll the args list around exactly as many times as the number of
+        # args, so each arg winds up back in the position where it started, but
+        # possibly modified.
+        #
+        # NB: a `for` loop captures its iteration list before it begins, so
+        # changing the positional parameters here affects neither the number of
+        # iterations, nor the values presented in `arg`.
+        shift                   # remove old arg
+        set -- "$@" "$arg"      # push replacement arg
+    done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+#   * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+#     and any embedded shellness will be escaped.
+#   * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+#     treated as '${Hostname}' itself on the command line.
+
+set -- \
+        "-Dorg.gradle.appname=$APP_BASE_NAME" \
+        -classpath "$CLASSPATH" \
+        org.gradle.wrapper.GradleWrapperMain \
+        "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+    die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+#   readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+#   set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+        printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+        xargs -n1 |
+        sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+        tr '\n' ' '
+    )" '"$@"'
+
+exec "$JAVACMD" "$@"

+ 92 - 0
plugin/shortcut/android/gradlew.bat

@@ -0,0 +1,92 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem      https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem  Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega

+ 1 - 0
plugin/shortcut/android/settings.gradle

@@ -0,0 +1 @@
+rootProject.name = 'shortcut'

+ 2 - 0
plugin/shortcut/android/src/main/AndroidManifest.xml

@@ -0,0 +1,2 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.atmob.shortcut"></manifest>

+ 95 - 0
plugin/shortcut/android/src/main/java/com/atmob/shortcut/ShortcutPlugin.java

@@ -0,0 +1,95 @@
+package com.atmob.shortcut;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.RequiresApi;
+
+import io.flutter.Log;
+import io.flutter.embedding.engine.plugins.FlutterPlugin;
+import io.flutter.embedding.engine.plugins.activity.ActivityAware;
+import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
+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;
+import io.flutter.plugin.common.PluginRegistry;
+
+
+/**
+ * FlutterShortcutPlugin
+ */
+public class ShortcutPlugin implements FlutterPlugin, MethodCallHandler, ActivityAware, PluginRegistry.NewIntentListener {
+    /// 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;
+    private Context context;
+    private Activity activity;
+    private ShortcutUtil shortcutUtil;
+
+    @Override
+    public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
+        context = flutterPluginBinding.getApplicationContext();
+        shortcutUtil = new ShortcutUtil(context);
+        channel = new MethodChannel(flutterPluginBinding.getBinaryMessenger(), "flutter_shortcut");
+        channel.setMethodCallHandler(this);
+    }
+
+    @RequiresApi(api = Build.VERSION_CODES.N_MR1)
+    @Override
+    public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
+        if (call.method.equals("createShortcut")) {
+            result.success(shortcutUtil.setShortcutItem(call));
+        } else if (call.method.equals("getLaunchAction")) {
+            shortcutUtil.getLaunchAction(result, activity);
+        } else {
+            result.notImplemented();
+        }
+    }
+
+    @Override
+    public boolean onNewIntent(@NonNull Intent intent) {
+        String action = shortcutUtil.extraAction(intent);
+        if (action != null) {
+            Log.d("ShortcutPlugin", "onNewIntent: " + action + " " + channel);
+            if (channel != null) channel.invokeMethod("shortcutAction", action);
+            return true;
+        }
+        return false;
+    }
+
+
+    @Override
+    public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
+        channel.setMethodCallHandler(null);
+    }
+
+    @Override
+    public void onAttachedToActivity(@NonNull ActivityPluginBinding binding) {
+        activity = binding.getActivity();
+        binding.addOnNewIntentListener(this);
+
+    }
+
+    @Override
+    public void onDetachedFromActivityForConfigChanges() {
+
+    }
+
+    @Override
+    public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding binding) {
+        activity = binding.getActivity();
+    }
+
+    @Override
+    public void onDetachedFromActivity() {
+        if (activity != null) {
+            activity = null;
+        }
+    }
+}

+ 168 - 0
plugin/shortcut/android/src/main/java/com/atmob/shortcut/ShortcutUtil.java

@@ -0,0 +1,168 @@
+package com.atmob.shortcut;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.AssetFileDescriptor;
+import android.content.res.AssetManager;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.drawable.Icon;
+import android.os.Build;
+
+import androidx.annotation.RequiresApi;
+import androidx.core.content.pm.ShortcutInfoCompat;
+import androidx.core.content.pm.ShortcutManagerCompat;
+import androidx.core.graphics.drawable.IconCompat;
+
+import java.io.IOException;
+import java.util.Map;
+
+import io.flutter.FlutterInjector;
+import io.flutter.embedding.engine.loader.FlutterLoader;
+import io.flutter.plugin.common.MethodCall;
+import io.flutter.plugin.common.MethodChannel;
+
+public class ShortcutUtil {
+    public static final String EXTRA_ACTION = "flutter_shortcuts_extra";
+
+    private final Context context;
+
+
+    ShortcutUtil(Context context) {
+        this.context = context;
+    }
+
+
+    @RequiresApi(api = Build.VERSION_CODES.N_MR1)
+    public void getLaunchAction(MethodChannel.Result result, Activity activity) {
+        if (activity == null) {
+            result.error(
+                    "flutter_shortcuts_no_activity",
+                    "There is no activity available when launching action",
+                    null);
+            return;
+        }
+        final Intent intent = activity.getIntent();
+        final String launchAction = intent.getStringExtra(EXTRA_ACTION);
+
+        if (launchAction != null && !launchAction.isEmpty()) {
+            ShortcutManagerCompat.reportShortcutUsed(context, launchAction);
+            intent.removeExtra(EXTRA_ACTION);
+        }
+
+        result.success(launchAction);
+    }
+
+
+    @RequiresApi(api = Build.VERSION_CODES.N_MR1)
+    public String setShortcutItem(MethodCall call) {
+        Map<String, Object> args = call.arguments();
+        ShortcutInfoCompat shortcut;
+        try {
+            shortcut = shortcutInfoCompat(args);
+            ShortcutManagerCompat.requestPinShortcut(context, shortcut, null);
+            return "Success";
+        } catch (Exception e) {
+            return e.getLocalizedMessage();
+        }
+    }
+
+
+    /* ********************   Utility Functions   ********************* */
+
+    @RequiresApi(api = Build.VERSION_CODES.N_MR1)
+    private ShortcutInfoCompat shortcutInfoCompat(
+            Map<String, Object> shortcut) {
+
+        final String id = (String) shortcut.get("id");
+        final String icon = (String) shortcut.get("icon");
+        final String action = (String) shortcut.get("action");
+
+        // Short Label for the shortcut
+        // Name for Person
+        final String shortLabel = (String) shortcut.get("shortLabel");
+
+
+        final int iconType = 1;
+
+        ShortcutInfoCompat.Builder shortcutInfoCompat = buildShortcutUsingCompat(
+                id, icon, action, shortLabel, iconType);
+
+        return shortcutInfoCompat.build();
+
+    }
+
+    @RequiresApi(api = Build.VERSION_CODES.KITKAT)
+    private ShortcutInfoCompat.Builder buildShortcutUsingCompat(
+            String id, String icon, String action, String shortLabel, int iconType) {
+
+        assert id != null;
+        ShortcutInfoCompat.Builder shortcutInfoCompat = new ShortcutInfoCompat.Builder(context, id);
+
+        if (action != null) {
+            Intent intent;
+            intent = getIntentToOpenMainActivity(action);
+            shortcutInfoCompat.setIntent(intent);
+        }
+
+
+        if (icon != null) {
+            setIconFromFlutterCompat(shortcutInfoCompat, icon);
+        }
+
+        if (shortLabel != null) {
+            shortcutInfoCompat.setShortLabel(shortLabel);
+        }
+
+        return shortcutInfoCompat;
+    }
+
+
+    private void setIconFromFlutterCompat(ShortcutInfoCompat.Builder shortcutBuilder, String icon) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            shortcutBuilder.setIcon(IconCompat.createFromIcon(context, getIconFromFlutterAsset(context, icon)));
+        }
+    }
+
+    @RequiresApi(api = Build.VERSION_CODES.O)
+    private Icon getIconFromFlutterAsset(Context context, String path) {
+        AssetManager assetManager = context.getAssets();
+        FlutterLoader loader = FlutterInjector.instance().flutterLoader();
+        String key = loader.getLookupKeyForAsset(path);
+        AssetFileDescriptor fd = null;
+        try {
+            fd = assetManager.openFd(key);
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+        Bitmap image = null;
+        try {
+            assert fd != null;
+            image = BitmapFactory.decodeStream(fd.createInputStream());
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+        return Icon.createWithAdaptiveBitmap(image);
+    }
+
+
+    private Intent getIntentToOpenMainActivity(String type) {
+        final String packageName = context.getPackageName();
+
+        return context
+                .getPackageManager()
+                .getLaunchIntentForPackage(packageName)
+                .setAction(Intent.ACTION_RUN)
+                .putExtra(EXTRA_ACTION, type);
+    }
+
+    public String extraAction(Intent intent) {
+        String action;
+        if (intent != null && intent.getExtras() != null && (action = intent.getStringExtra(EXTRA_ACTION)) != null) {
+            return action;
+        }
+        return null;
+    }
+}
+

+ 38 - 0
plugin/shortcut/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

+ 0 - 0
plugin/shortcut/ios/Assets/.gitkeep


+ 19 - 0
plugin/shortcut/ios/Classes/ShortcutPlugin.swift

@@ -0,0 +1,19 @@
+import Flutter
+import UIKit
+
+public class ShortcutPlugin: NSObject, FlutterPlugin {
+  public static func register(with registrar: FlutterPluginRegistrar) {
+    let channel = FlutterMethodChannel(name: "shortcut", binaryMessenger: registrar.messenger())
+    let instance = ShortcutPlugin()
+    registrar.addMethodCallDelegate(instance, channel: channel)
+  }
+
+  public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
+    switch call.method {
+    case "getPlatformVersion":
+      result("iOS " + UIDevice.current.systemVersion)
+    default:
+      result(FlutterMethodNotImplemented)
+    }
+  }
+}

+ 14 - 0
plugin/shortcut/ios/Resources/PrivacyInfo.xcprivacy

@@ -0,0 +1,14 @@
+<?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>NSPrivacyTrackingDomains</key>
+	<array/>
+	<key>NSPrivacyAccessedAPITypes</key>
+	<array/>
+	<key>NSPrivacyCollectedDataTypes</key>
+	<array/>
+	<key>NSPrivacyTracking</key>
+	<false/>
+</dict>
+</plist>

+ 29 - 0
plugin/shortcut/ios/shortcut.podspec

@@ -0,0 +1,29 @@
+#
+# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
+# Run `pod lib lint shortcut.podspec` to validate before publishing.
+#
+Pod::Spec.new do |s|
+  s.name             = 'shortcut'
+  s.version          = '0.0.1'
+  s.summary          = '创建桌面快捷方式'
+  s.description      = <<-DESC
+创建桌面快捷方式
+                       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.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' }
+  s.swift_version = '5.0'
+
+  # 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 = {'shortcut_privacy' => ['Resources/PrivacyInfo.xcprivacy']}
+end

+ 26 - 0
plugin/shortcut/lib/shortcut.dart

@@ -0,0 +1,26 @@
+import 'shortcut_platform_interface.dart';
+
+class FlutterShortcut {
+  Future<String?> createShortcut({
+    required String id,
+    required String label,
+    required String action,
+    String? iconAssetName,
+  }) {
+    return FlutterShortcutPlatform.instance.createShortcut(
+      id: id,
+      label: label,
+      action: action,
+      iconAssetName: iconAssetName,
+    );
+  }
+
+  Future<void> getLaunchAction(
+      void Function(String action) onActionReceived) async {
+    return FlutterShortcutPlatform.instance.getLaunchAction(onActionReceived);
+  }
+
+  Stream<String> actionStream() {
+    return FlutterShortcutPlatform.instance.actionStream();
+  }
+}

+ 68 - 0
plugin/shortcut/lib/shortcut_method_channel.dart

@@ -0,0 +1,68 @@
+import 'dart:async';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/services.dart';
+
+import 'shortcut_platform_interface.dart';
+
+/// An implementation of [FlutterShortcutPlatform] that uses method channels.
+class MethodChannelFlutterShortcut extends FlutterShortcutPlatform {
+  /// The method channel used to interact with the native platform.
+  ///
+  ///
+  ///
+  MethodChannelFlutterShortcut() {
+    methodChannel.setMethodCallHandler(_handleMethod);
+  }
+
+  @visibleForTesting
+  final methodChannel = const MethodChannel('flutter_shortcut');
+
+  final StreamController<String> _actionStreamController =
+      StreamController<String>.broadcast();
+
+  @override
+  Future<String?> createShortcut({
+    required String id,
+    required String label,
+    required String action,
+    String? iconAssetName,
+  }) async {
+    return await methodChannel.invokeMethod("createShortcut", {
+      "id": id,
+      "shortLabel": label,
+      "action": action,
+      "icon": iconAssetName,
+    });
+  }
+
+  Future<dynamic> _handleMethod(MethodCall call) async {
+    debugPrint('MethodChannelFlutterShortcut  _handleMethod  ${call.method}');
+    switch (call.method) {
+      case "shortcutAction":
+        _shortcutAction(call.arguments);
+        break;
+    }
+  }
+
+  void _shortcutAction(dynamic action) {
+    if (action is String) {
+      _actionStreamController.add(action);
+    }
+  }
+
+  @override
+  Stream<String> actionStream() {
+    return _actionStreamController.stream;
+  }
+
+  @override
+  Future<void> getLaunchAction(
+      void Function(String action) onActionReceived) async {
+    debugPrint('getLaunchAction   触发了');
+    String? response = await methodChannel.invokeMethod("getLaunchAction");
+    if (response != null) {
+      onActionReceived(response);
+    }
+  }
+}

+ 41 - 0
plugin/shortcut/lib/shortcut_platform_interface.dart

@@ -0,0 +1,41 @@
+import 'package:plugin_platform_interface/plugin_platform_interface.dart';
+import 'package:shortcut/shortcut_method_channel.dart';
+
+abstract class FlutterShortcutPlatform extends PlatformInterface {
+  /// Constructs a FlutterShortcutPlatform.
+  FlutterShortcutPlatform() : super(token: _token);
+
+  static final Object _token = Object();
+
+  static FlutterShortcutPlatform _instance = MethodChannelFlutterShortcut();
+
+  /// The default instance of [FlutterPinnedShortcutPlatform] to use.
+  ///
+  /// Defaults to [MethodChannelFlutterPinnedShortcut].
+  static FlutterShortcutPlatform get instance => _instance;
+
+  /// Platform-specific implementations should set this with their own
+  /// platform-specific class that extends [FlutterPinnedShortcutPlatform] when
+  /// they register themselves.
+  static set instance(FlutterShortcutPlatform instance) {
+    PlatformInterface.verifyToken(instance, _token);
+    _instance = instance;
+  }
+
+  Future<String?> createShortcut({
+    required String id,
+    required String label,
+    required String action,
+    String? iconAssetName,
+  }) async {
+    throw UnimplementedError('platformVersion() has not been implemented.');
+  }
+
+  Stream<String> actionStream() {
+    throw UnimplementedError('actionStream() has not been implemented.');
+  }
+
+  void getLaunchAction(void Function(String action) onActionReceived) {
+    throw UnimplementedError('platformVersion() has not been implemented.');
+  }
+}

+ 72 - 0
plugin/shortcut/pubspec.yaml

@@ -0,0 +1,72 @@
+name: shortcut
+description: "创建桌面快捷方式"
+version: 0.0.1
+homepage:
+
+environment:
+  sdk: ^3.5.0
+  flutter: '>=3.3.0'
+
+dependencies:
+  flutter:
+    sdk: flutter
+  plugin_platform_interface: ^2.0.2
+
+dev_dependencies:
+  flutter_test:
+    sdk: flutter
+  flutter_lints: ^4.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.atmob.shortcut
+        pluginClass: ShortcutPlugin
+      ios:
+        pluginClass: ShortcutPlugin
+
+  # 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

+ 15 - 8
pubspec.lock

@@ -66,10 +66,10 @@ packages:
     dependency: transitive
     dependency: transitive
     description:
     description:
       name: args
       name: args
-      sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a"
+      sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6
       url: "https://pub.dev"
       url: "https://pub.dev"
     source: hosted
     source: hosted
-    version: "2.5.0"
+    version: "2.6.0"
   async:
   async:
     dependency: transitive
     dependency: transitive
     description:
     description:
@@ -791,10 +791,10 @@ packages:
     dependency: "direct main"
     dependency: "direct main"
     description:
     description:
       name: lottie
       name: lottie
-      sha256: "6a24ade5d3d918c306bb1c21a6b9a04aab0489d51a2582522eea820b4093b62b"
+      sha256: "7afc60865a2429d994144f7d66ced2ae4305fe35d82890b8766e3359872d872c"
       url: "https://pub.dev"
       url: "https://pub.dev"
     source: hosted
     source: hosted
-    version: "3.1.2"
+    version: "3.1.3"
   macros:
   macros:
     dependency: transitive
     dependency: transitive
     description:
     description:
@@ -1007,10 +1007,10 @@ packages:
     dependency: "direct main"
     dependency: "direct main"
     description:
     description:
       name: photo_manager
       name: photo_manager
-      sha256: "32a1ce1095aeaaa792a29f28c1f74613aa75109f21c2d4ab85be3ad9964230a4"
+      sha256: "70159eee32203e8162d49d588232f0299ed3f383c63eef1e899cb6b83dee6b26"
       url: "https://pub.dev"
       url: "https://pub.dev"
     source: hosted
     source: hosted
-    version: "3.5.0"
+    version: "3.5.1"
   platform:
   platform:
     dependency: transitive
     dependency: transitive
     description:
     description:
@@ -1227,6 +1227,13 @@ packages:
       url: "https://pub.dev"
       url: "https://pub.dev"
     source: hosted
     source: hosted
     version: "2.0.0"
     version: "2.0.0"
+  shortcut:
+    dependency: "direct main"
+    description:
+      path: "plugin/shortcut"
+      relative: true
+    source: path
+    version: "0.0.1"
   sky_engine:
   sky_engine:
     dependency: transitive
     dependency: transitive
     description: flutter
     description: flutter
@@ -1636,10 +1643,10 @@ packages:
     dependency: transitive
     dependency: transitive
     description:
     description:
       name: win32
       name: win32
-      sha256: "4d45dc9069dba4619dc0ebd93c7cec5e66d8482cb625a370ac806dcc8165f2ec"
+      sha256: e5c39a90447e7c81cfec14b041cdbd0d0916bd9ebbc7fe02ab69568be703b9bd
       url: "https://pub.dev"
       url: "https://pub.dev"
     source: hosted
     source: hosted
-    version: "5.5.5"
+    version: "5.6.0"
   win32_registry:
   win32_registry:
     dependency: transitive
     dependency: transitive
     description:
     description:

+ 3 - 0
pubspec.yaml

@@ -114,6 +114,9 @@ dependencies:
   #获取音频资源列表
   #获取音频资源列表
   photo_manager: ^3.5.0
   photo_manager: ^3.5.0
 
 
+  #快捷方式
+  shortcut:
+    path: plugin/shortcut
 
 
 dev_dependencies:
 dev_dependencies:
   flutter_test:
   flutter_test: