diff --git a/lib/features/device/data/device_api.dart b/lib/features/device/data/device_api.dart index 19d5bd1..94aa710 100644 --- a/lib/features/device/data/device_api.dart +++ b/lib/features/device/data/device_api.dart @@ -100,18 +100,21 @@ class DeviceApi { } } - /// 向设备下发操作指令(重启/关机/截屏/刷新/定位等)。 + /// 向设备下发操作指令(重启/关机/截屏/刷新/定位/拍照等)。 /// - /// [op] 为后端操作标识,如 `reboot` / `shutdown` / `screenshot` / `refresh` / `locate`, + /// [op] 为后端操作标识,如 `reboot` / `shutdown` / `screenshot` / `refresh` / `locate` / `take_photo`, /// 对应接口路径 `/api/v1/client/sn/ops/{op}?sn=xxx`。 + /// [extra] 为可选的附加 query 参数(如远程拍照时的 `camera`:`0` 后置 / `1` 前置)。 /// 成功返回 null;失败返回后台错误提示。 - Future sendDeviceOp(String sn, String op) async { + Future sendDeviceOp(String sn, String op, {Map? extra}) async { try { + final queryParameters = {'sn': sn}; + if (extra != null) queryParameters.addAll(extra); final data = await DioClient.post( 'v1/client/sn/ops/$op', - queryParameters: {'sn': sn}, + queryParameters: queryParameters, ); - debugPrint('[DeviceApi] sendDeviceOp op=$op sn=$sn data=$data'); + debugPrint('[DeviceApi] sendDeviceOp op=$op sn=$sn extra=$extra data=$data'); return null; } on ApiException catch (e) { debugPrint('[DeviceApi] sendDeviceOp op=$op sn=$sn failed: ${e.message}'); diff --git a/lib/features/device/data/device_repository_impl.dart b/lib/features/device/data/device_repository_impl.dart index b3c9f44..3bd6a21 100644 --- a/lib/features/device/data/device_repository_impl.dart +++ b/lib/features/device/data/device_repository_impl.dart @@ -33,8 +33,8 @@ class DeviceRepositoryImpl implements DeviceRepository { _api.getRecentScreenshots(sn: sn); @override - Future sendDeviceOp(String sn, String op) => - _api.sendDeviceOp(sn, op); + Future sendDeviceOp(String sn, String op, {Map? extra}) => + _api.sendDeviceOp(sn, op, extra: extra); @override Future sendAppOp(String sn, String op, String packageName) => diff --git a/lib/features/device/domain/device_repository.dart b/lib/features/device/domain/device_repository.dart index 7418ea3..9716790 100644 --- a/lib/features/device/domain/device_repository.dart +++ b/lib/features/device/domain/device_repository.dart @@ -27,11 +27,12 @@ abstract class DeviceRepository { /// 已绑定设备最近截图列表。 Future> getRecentScreenshots(String sn); - /// 向设备下发操作指令(重启/关机/截屏/刷新/定位等)。 + /// 向设备下发操作指令(重启/关机/截屏/刷新/定位/拍照等)。 /// - /// [op] 为后端操作标识,如 `reboot` / `shutdown` / `screenshot` / `refresh` / `locate`。 + /// [op] 为后端操作标识,如 `reboot` / `shutdown` / `screenshot` / `refresh` / `locate` / `take_photo`。 + /// [extra] 为可选的附加 query 参数(如远程拍照时的 `camera`:`0` 后置 / `1` 前置)。 /// 成功返回 null;失败返回错误提示。 - Future sendDeviceOp(String sn, String op); + Future sendDeviceOp(String sn, String op, {Map? extra}); /// 对指定应用执行操作(打开/停止/卸载/清除数据)。 /// diff --git a/lib/features/home/presentation/home_tab.dart b/lib/features/home/presentation/home_tab.dart index 08a32d4..b8a2a91 100644 --- a/lib/features/home/presentation/home_tab.dart +++ b/lib/features/home/presentation/home_tab.dart @@ -1,6 +1,7 @@ import 'package:cupertino_ui/cupertino_ui.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; +import 'package:material_ui/material_ui.dart'; import '../../../core/utils/time_format.dart'; import '../../../core/widgets/feature_ui.dart'; @@ -576,6 +577,54 @@ class HomeTab extends ConsumerWidget { await _runDeviceOp(context, ref, l10n, sn, op); } + /// 远程拍照:弹窗选择摄像头(后置/前置),确认后下发拍照指令。 + /// + /// 后端 [DeviceService.takePhoto] 通过 `camera` 参数区分摄像头 + /// (`0` 后置,`1` 前置),随指令一并下发给设备端。 + Future _pickCameraAndTakePhoto( + BuildContext context, + WidgetRef ref, + AppLocalizations l10n, + String? sn, + ) async { + if (sn == null || sn.isEmpty) { + _showOpResult(context, l10n, l10n.opCmdFailed); + return; + } + final int? camera = await showCupertinoDialog( + context: context, + builder: (dialogContext) => CupertinoAlertDialog( + title: Text(l10n.cfTakePhotoTitle), + actions: [ + CupertinoDialogAction( + onPressed: () => Navigator.of(dialogContext).pop(0), + child: Text(l10n.cfCamRear), + ), + CupertinoDialogAction( + onPressed: () => Navigator.of(dialogContext).pop(1), + child: Text(l10n.cfCamFront), + ), + CupertinoDialogAction( + isDestructiveAction: true, + onPressed: () => Navigator.of(dialogContext).pop(), + child: Text(l10n.cancel), + ), + ], + ), + ); + if (camera == null) return; + if (!context.mounted) return; + await _runDeviceOp( + context, + ref, + l10n, + sn, + 'take_photo', + refreshScreenshots: true, + extra: {'camera': camera}, + ); + } + /// 下发设备操作指令并弹出结果提示。 Future _runDeviceOp( BuildContext context, @@ -585,12 +634,13 @@ class HomeTab extends ConsumerWidget { String op, { bool refreshScreenshots = false, bool refreshLocation = false, + Map? extra, }) async { if (sn == null || sn.isEmpty) { _showOpResult(context, l10n, l10n.opCmdFailed); return; } - final error = await ref.read(deviceRepositoryProvider).sendDeviceOp(sn, op); + final error = await ref.read(deviceRepositoryProvider).sendDeviceOp(sn, op, extra: extra); if (!context.mounted) return; if (error == null) { if (refreshScreenshots) ref.invalidate(recentScreenshotsProvider(sn)); @@ -647,12 +697,89 @@ class HomeTab extends ConsumerWidget { ); } + /// 「更多」弹窗:以图标网格列出未在常用区直接展示的远程推送操作, + /// 确保后端 ClientDeviceOpsController 暴露的其余推送接口(如恢复出厂、开发者模式) + /// 在家属端 UI 也有入口,实现「每个推送接口都对接」。 + Future _showMoreOpsSheet( + BuildContext context, + WidgetRef ref, + AppLocalizations l10n, + String? sn, + ) async { + final items = <_MoreOpItem>[ + _MoreOpItem(emoji: '♻️', color: AppColors.orange, label: l10n.cfRestore, op: 'restore'), + _MoreOpItem(emoji: '🛠️', color: AppColors.blue, label: l10n.cfDeveloper, op: 'developer'), + ]; + if (sn == null || sn.isEmpty) { + _showOpResult(context, l10n, l10n.opCmdFailed); + return; + } + final String? op = await showCupertinoModalPopup( + context: context, + builder: (ctx) => SafeArea( + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 16), + decoration: BoxDecoration( + color: Theme.of(ctx).scaffoldBackgroundColor, + borderRadius: const BorderRadius.vertical(top: Radius.circular(20)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + l10n.cfMoreOpsTitle, + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600), + ), + const SizedBox(height: 16), + GridView.count( + crossAxisCount: 3, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + childAspectRatio: 0.95, + mainAxisSpacing: 10, + crossAxisSpacing: 10, + children: [ + for (final item in items) + IconTile( + emoji: item.emoji, + color: item.color, + label: item.label, + size: 54, + iconSize: 24, + onTap: () => Navigator.of(ctx).pop(item.op), + ), + ], + ), + const SizedBox(height: 12), + CupertinoButton( + onPressed: () => Navigator.of(ctx).pop(), + child: Text(l10n.cancel), + ), + ], + ), + ), + ), + ); + if (op == null || !context.mounted) return; + await _runDeviceOp(context, ref, l10n, sn, op); + } + Widget _commonCard(BuildContext context, WidgetRef ref, AppLocalizations l10n, String? sn) => CardBox( child: Column( children: [ Row( children: [ - Expanded(child: IconTile(emoji: '📁', color: AppColors.blue, label: l10n.cfFileTransfer)), + Expanded( + child: IconTile( + emoji: '📁', + color: AppColors.blue, + label: l10n.cfFileTransfer, + size: 50, + iconSize: 20, + onTap: () => _runDeviceOp(context, ref, l10n, sn, 'file_transfer'), + ), + ), Expanded( child: IconTile( emoji: '📷', @@ -660,24 +787,129 @@ class HomeTab extends ConsumerWidget { label: l10n.cfRemoteCam, size: 50, iconSize: 20, - onTap: () => _runDeviceOp(context, ref, l10n, sn, 'take_photo', - refreshScreenshots: true), + onTap: () => _pickCameraAndTakePhoto(context, ref, l10n, sn), + ), + ), + Expanded( + child: IconTile( + emoji: '💬', + color: AppColors.orange, + label: l10n.cfMsgSync, + size: 50, + iconSize: 20, + onTap: () => _runDeviceOp(context, ref, l10n, sn, 'msg_sync'), + ), + ), + Expanded( + child: IconTile( + emoji: '🧹', + color: AppColors.purple, + label: l10n.cfClean, + size: 50, + iconSize: 20, + onTap: () => _runDeviceOp(context, ref, l10n, sn, 'clean'), ), ), - Expanded(child: IconTile(emoji: '💬', color: AppColors.orange, label: l10n.cfMsgSync)), - Expanded(child: IconTile(emoji: '🧹', color: AppColors.purple, label: l10n.cfClean)), ], ), const SizedBox(height: 14), Row( children: [ - Expanded(child: IconTile(emoji: '📺', color: AppColors.red, label: l10n.cfMirror)), - Expanded(child: IconTile(emoji: '🔔', color: AppColors.teal, label: l10n.cfLost)), - Expanded(child: IconTile(emoji: '🔒', color: const Color(0xFF22C55E), label: l10n.cfAppLock)), - Expanded(child: IconTile(emoji: '➕', color: AppColors.gray, label: l10n.cfMore)), + Expanded( + child: IconTile( + emoji: '📺', + color: AppColors.red, + label: l10n.cfMirror, + size: 50, + iconSize: 20, + onTap: () => _runDeviceOp(context, ref, l10n, sn, 'mirror'), + ), + ), + Expanded( + child: IconTile( + emoji: '🔔', + color: AppColors.teal, + label: l10n.cfLost, + size: 50, + iconSize: 20, + onTap: () => _runDeviceOp(context, ref, l10n, sn, 'locate', + refreshLocation: true), + ), + ), + Expanded( + child: IconTile( + emoji: '🔐', + color: AppColors.blue, + label: l10n.cfScreenLock, + size: 50, + iconSize: 20, + onTap: () => _runDeviceOp(context, ref, l10n, sn, 'screen_lock'), + ), + ), + Expanded( + child: IconTile( + emoji: '🔒', + color: const Color(0xFF22C55E), + label: l10n.cfAppLock, + size: 50, + iconSize: 20, + onTap: () => _runDeviceOp(context, ref, l10n, sn, 'refresh'), + ), + ), + ], + ), + const SizedBox(height: 14), + Row( + children: [ + Expanded( + child: IconTile( + emoji: '♻️', + color: AppColors.orange, + label: l10n.cfRestore, + size: 50, + iconSize: 20, + onTap: () => _runDeviceOp(context, ref, l10n, sn, 'restore'), + ), + ), + Expanded( + child: IconTile( + emoji: '🛠️', + color: AppColors.blue, + label: l10n.cfDeveloper, + size: 50, + iconSize: 20, + onTap: () => _runDeviceOp(context, ref, l10n, sn, 'developer'), + ), + ), + Expanded( + child: IconTile( + emoji: '⋯', + color: AppColors.gray, + label: l10n.cfMore, + size: 50, + iconSize: 20, + onTap: () => _showMoreOpsSheet(context, ref, l10n, sn), + ), + ), + // 占位,保持每行 4 个 + const Expanded(child: SizedBox.shrink()), ], ), ], ), ); } + +class _MoreOpItem { + const _MoreOpItem({ + required this.emoji, + required this.color, + required this.label, + required this.op, + }); + + final String emoji; + final Color color; + final String label; + final String op; +} diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 64740ac..f41045e 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -536,12 +536,48 @@ abstract class AppLocalizations { /// **'远程相机'** String get cfRemoteCam; + /// 远程拍照弹窗标题 + /// + /// In zh, this message translates to: + /// **'选择摄像头'** + String get cfTakePhotoTitle; + + /// 远程拍照:后置摄像头 + /// + /// In zh, this message translates to: + /// **'后置摄像头'** + String get cfCamRear; + + /// 远程拍照:前置摄像头 + /// + /// In zh, this message translates to: + /// **'前置摄像头'** + String get cfCamFront; + /// 常用功能:更多 /// /// In zh, this message translates to: /// **'更多'** String get cfMore; + /// 更多操作弹窗标题 + /// + /// In zh, this message translates to: + /// **'更多操作'** + String get cfMoreOpsTitle; + + /// 更多操作:恢复出厂设置 + /// + /// In zh, this message translates to: + /// **'恢复出厂设置'** + String get cfRestore; + + /// 更多操作:开发者模式 + /// + /// In zh, this message translates to: + /// **'开发者模式'** + String get cfDeveloper; + /// 联系人页标题 /// /// In zh, this message translates to: @@ -989,9 +1025,15 @@ abstract class AppLocalizations { /// 常用功能:防丢失 /// /// In zh, this message translates to: - /// **'丢失提醒'** + /// **'丢失模式'** String get cfLost; + /// 常用功能:屏幕锁定 + /// + /// In zh, this message translates to: + /// **'屏幕锁定'** + String get cfScreenLock; + /// 常用功能:应用锁 /// /// In zh, this message translates to: diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 2f0eb64..0520a9a 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -230,9 +230,27 @@ class AppLocalizationsEn extends AppLocalizations { @override String get cfRemoteCam => 'Remote Camera'; + @override + String get cfTakePhotoTitle => 'Choose Camera'; + + @override + String get cfCamRear => 'Rear Camera'; + + @override + String get cfCamFront => 'Front Camera'; + @override String get cfMore => 'More'; + @override + String get cfMoreOpsTitle => 'More Operations'; + + @override + String get cfRestore => 'Factory Reset'; + + @override + String get cfDeveloper => 'Developer Mode'; + @override String get contactsTitle => 'Contacts'; @@ -461,7 +479,10 @@ class AppLocalizationsEn extends AppLocalizations { String get cfMirror => 'Screen Mirror'; @override - String get cfLost => 'Loss Alert'; + String get cfLost => 'Lost Mode'; + + @override + String get cfScreenLock => 'Screen Lock'; @override String get cfAppLock => 'App Lock'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index af32cf3..dbf9784 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -229,9 +229,27 @@ class AppLocalizationsZh extends AppLocalizations { @override String get cfRemoteCam => '远程相机'; + @override + String get cfTakePhotoTitle => '选择摄像头'; + + @override + String get cfCamRear => '后置摄像头'; + + @override + String get cfCamFront => '前置摄像头'; + @override String get cfMore => '更多'; + @override + String get cfMoreOpsTitle => '更多操作'; + + @override + String get cfRestore => '恢复出厂设置'; + + @override + String get cfDeveloper => '开发者模式'; + @override String get contactsTitle => '联系人'; @@ -457,7 +475,10 @@ class AppLocalizationsZh extends AppLocalizations { String get cfMirror => '屏幕镜像'; @override - String get cfLost => '丢失提醒'; + String get cfLost => '丢失模式'; + + @override + String get cfScreenLock => '屏幕锁定'; @override String get cfAppLock => '应用锁'; diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index fe9898c..00b41ee 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -73,7 +73,13 @@ "menuApps": "Apps", "menuMore": "More", "cfRemoteCam": "Remote Camera", + "cfTakePhotoTitle": "Choose Camera", + "cfCamRear": "Rear Camera", + "cfCamFront": "Front Camera", "cfMore": "More", + "cfMoreOpsTitle": "More Operations", + "cfRestore": "Factory Reset", + "cfDeveloper": "Developer Mode", "contactsTitle": "Contacts", "contactName": "Name", "contactNick": "Nickname (optional)", @@ -160,7 +166,8 @@ "cfMsgSync": "Message Sync", "cfClean": "Clean & Boost", "cfMirror": "Screen Mirror", - "cfLost": "Loss Alert", + "cfLost": "Lost Mode", + "cfScreenLock": "Screen Lock", "cfAppLock": "App Lock", "msgOnline": "Device Online Alert", "msgScreenshot": "Screenshot Sync", diff --git a/lib/l10n/intl_zh.arb b/lib/l10n/intl_zh.arb index 14a070f..d6386f6 100644 --- a/lib/l10n/intl_zh.arb +++ b/lib/l10n/intl_zh.arb @@ -292,10 +292,34 @@ "@cfRemoteCam": { "description": "常用功能:远程相机" }, + "cfTakePhotoTitle": "选择摄像头", + "@cfTakePhotoTitle": { + "description": "远程拍照弹窗标题" + }, + "cfCamRear": "后置摄像头", + "@cfCamRear": { + "description": "远程拍照:后置摄像头" + }, + "cfCamFront": "前置摄像头", + "@cfCamFront": { + "description": "远程拍照:前置摄像头" + }, "cfMore": "更多", "@cfMore": { "description": "常用功能:更多" }, + "cfMoreOpsTitle": "更多操作", + "@cfMoreOpsTitle": { + "description": "更多操作弹窗标题" + }, + "cfRestore": "恢复出厂设置", + "@cfRestore": { + "description": "更多操作:恢复出厂设置" + }, + "cfDeveloper": "开发者模式", + "@cfDeveloper": { + "description": "更多操作:开发者模式" + }, "contactsTitle": "联系人", "@contactsTitle": { "description": "联系人页标题" @@ -592,10 +616,14 @@ "@cfMirror": { "description": "常用功能:屏幕镜像" }, - "cfLost": "丢失提醒", + "cfLost": "丢失模式", "@cfLost": { "description": "常用功能:防丢失" }, + "cfScreenLock": "屏幕锁定", + "@cfScreenLock": { + "description": "常用功能:屏幕锁定" + }, "cfAppLock": "应用锁", "@cfAppLock": { "description": "常用功能:应用锁"