app-starlock/lib/tools/remote_unlock_overlay.dart
2025-12-25 18:08:05 +08:00

207 lines
7.6 KiB
Dart

import 'dart:async';
import 'dart:math';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
import 'package:star_lock/app_settings/app_colors.dart';
import 'package:star_lock/network/api_repository.dart';
class RemoteUnlockOverlay {
static Future<void> show({required int lockId, required String lockAlias, required int operateDate, int timeoutSeconds = 60}) async {
await Get.to(() => RemoteUnlockOverlayPage(lockId: lockId, lockAlias: lockAlias, operateDate: operateDate, timeoutSeconds: timeoutSeconds), opaque: false);
}
}
class RemoteUnlockOverlayPage extends StatefulWidget {
const RemoteUnlockOverlayPage({required this.lockId, required this.lockAlias, required this.operateDate, this.timeoutSeconds = 60, Key? key}) : super(key: key);
final int lockId;
final String lockAlias;
final int operateDate;
final int timeoutSeconds;
@override
State<RemoteUnlockOverlayPage> createState() => _RemoteUnlockOverlayPageState();
}
class _RemoteUnlockOverlayPageState extends State<RemoteUnlockOverlayPage> {
late int seconds;
Timer? timer;
@override
void initState() {
super.initState();
seconds = widget.timeoutSeconds;
timer = Timer.periodic(const Duration(seconds: 1), (Timer t) {
if (!mounted) {
return;
}
if (seconds <= 0) {
t.cancel();
timer?.cancel();
setState(() {
seconds = 0;
});
Get.back();
return;
}
setState(() {
seconds = seconds - 1;
});
});
}
@override
void dispose() {
timer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
final double cardWidth = 0.9.sw;
final double ringSize = 260.r;
return PopScope(
canPop: false,
child: Stack(
children: [
Positioned.fill(
child: BackdropFilter(
filter: ui.ImageFilter.blur(sigmaX: 3, sigmaY: 3),
child: Container(color: Colors.black.withOpacity(0.18)),
),
),
Center(
child: Container(
width: cardWidth,
padding: EdgeInsets.symmetric(vertical: 24.h, horizontal: 20.w),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16.r),
boxShadow: [BoxShadow(color: Colors.black.withOpacity(0.08), blurRadius: 20.r, offset: const Offset(0, 6))],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Icon(Icons.lock, color: AppColors.mainColor, size: 24.r),
SizedBox(width: 8.w),
Expanded(child: Text(widget.lockAlias, style: TextStyle(fontSize: 22.sp, color: AppColors.blackColor))),
],
),
SizedBox(height: 12.h),
Text('远程开锁请求'.tr, style: TextStyle(fontSize: 28.sp, color: AppColors.blackColor, fontWeight: FontWeight.w700)),
SizedBox(height: 20.h),
SizedBox(
width: ringSize,
height: ringSize,
child: Stack(
alignment: Alignment.center,
children: [
_DottedCountdownRing(seconds: seconds, size: ringSize),
Text('${seconds} s', style: TextStyle(fontSize: 48.sp, color: AppColors.mainColor, fontWeight: FontWeight.w700)),
],
),
),
SizedBox(height: 16.h),
Text('请确认是否允许该门锁进行远程开锁'.tr, style: TextStyle(fontSize: 18.sp, color: AppColors.btnDisableColor)),
SizedBox(height: 8.h),
Text('请求将在倒计时结束后自动取消'.tr, style: TextStyle(fontSize: 16.sp, color: AppColors.btnDisableColor)),
SizedBox(height: 20.h),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
GestureDetector(
onTap: _reject,
child: Container(
width: 0.35.sw,
height: 48.h,
alignment: Alignment.center,
decoration: BoxDecoration(color: Colors.redAccent, borderRadius: BorderRadius.circular(8.r)),
child: Text('拒绝'.tr, style: TextStyle(color: Colors.white, fontSize: 22.sp)),
),
),
SizedBox(width: 20.w),
GestureDetector(
onTap: _accept,
child: Container(
width: 0.35.sw,
height: 48.h,
alignment: Alignment.center,
decoration: BoxDecoration(color: AppColors.mainColor, borderRadius: BorderRadius.circular(8.r)),
child: Text('同意'.tr, style: TextStyle(color: Colors.white, fontSize: 22.sp)),
),
),
],
),
SizedBox(height: 8.h),
Container(
padding: EdgeInsets.symmetric(vertical: 10.h, horizontal: 12.w),
decoration: BoxDecoration(color: const Color(0xFFF7F9FC), borderRadius: BorderRadius.circular(8.r)),
child: Row(children: [Icon(Icons.info_outline, color: AppColors.mainColor, size: 18.r), SizedBox(width: 6.w), Expanded(child: Text('远程开锁需确保现场安全与人员确认'.tr, style: TextStyle(color: AppColors.btnDisableColor, fontSize: 14.sp)))]),
),
],
),
),
),
],
),
);
}
Future<void> _accept() async {
timer?.cancel();
await ApiRepository.to.remoteOpenLock(lockId: widget.lockId.toString(), timeOut: 60);
Get.back();
}
void _reject() {
timer?.cancel();
Get.back();
}
}
class _DottedCountdownRing extends StatelessWidget {
const _DottedCountdownRing({required this.seconds, required this.size});
final int seconds;
final double size;
@override
Widget build(BuildContext context) {
final double progress = seconds.clamp(0, 60) / 60.0;
return CustomPaint(size: Size(size, size), painter: _DottedCirclePainter(progress: progress));
}
}
class _DottedCirclePainter extends CustomPainter {
_DottedCirclePainter({required this.progress});
final double progress;
@override
void paint(Canvas canvas, Size size) {
final Paint paint = Paint()
..color = AppColors.mainColor
..strokeWidth = 4.w
..style = PaintingStyle.stroke;
final double radius = size.width / 2 - 2.w;
final Offset center = Offset(size.width / 2, size.height / 2);
final Path path = Path();
final double angle = -pi / 2 + 2 * pi * progress.clamp(0.0, 1.0);
for (int i = 0; i <= 60; i++) {
final double startAngle = (2 * pi / 60) * i - pi / 2;
final double endAngle = startAngle + (2 * pi / 60) * 0.7;
if (startAngle <= angle) {
final Path segmentPath = Path();
segmentPath.arcTo(Rect.fromCircle(center: center, radius: radius), startAngle, endAngle - startAngle, true);
path.addPath(segmentPath, Offset.zero);
}
}
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}