deviceauth_view.dart 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter/services.dart';
  3. import 'package:flutter_screenutil/flutter_screenutil.dart';
  4. import 'package:get/get.dart';
  5. import 'package:nomo/app/base/base_view.dart';
  6. import 'package:nomo/app/widgets/click_opacity.dart';
  7. import 'package:nomo/app/widgets/ix_app_bar.dart';
  8. import 'package:nomo/config/theme/theme_extensions/theme_extension.dart';
  9. import 'package:pinput/pinput.dart';
  10. import '../controllers/deviceauth_controller.dart';
  11. class DeviceauthView extends BaseView<DeviceauthController> {
  12. const DeviceauthView({super.key});
  13. @override
  14. Widget buildContent(BuildContext context) {
  15. return Column(
  16. children: [
  17. IXAppBar(title: 'Device Authorization'),
  18. Expanded(
  19. child: Obx(() {
  20. final isPremium = controller.isPremium.value;
  21. return SingleChildScrollView(
  22. padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 20.h),
  23. child: Column(
  24. children: [
  25. if (isPremium) ...[
  26. // VIP用户界面
  27. _buildVIPUserInterface(),
  28. ] else ...[
  29. // 免费用户界面
  30. _buildFreeUserInterface(),
  31. ],
  32. ],
  33. ),
  34. );
  35. }),
  36. ),
  37. ],
  38. );
  39. }
  40. /// 免费用户界面 - 显示授权码和等待授权
  41. Widget _buildFreeUserInterface() {
  42. return Column(
  43. children: [
  44. // 授权码显示区域
  45. _buildAuthCodeDisplay(),
  46. 30.verticalSpaceFromWidth,
  47. // 说明卡片
  48. _buildInstructionCards(),
  49. ],
  50. );
  51. }
  52. /// VIP用户界面 - 输入授权码和管理设备
  53. Widget _buildVIPUserInterface() {
  54. return Column(
  55. children: [
  56. // 6个输入框在顶部
  57. _buildTopCodeInput(),
  58. 20.verticalSpaceFromWidth,
  59. // 设备管理区域
  60. _buildDeviceManagementSection(),
  61. 20.verticalSpaceFromWidth,
  62. // 授权步骤说明
  63. _buildAuthorizationSteps(),
  64. ],
  65. );
  66. }
  67. /// 授权码显示区域
  68. Widget _buildAuthCodeDisplay() {
  69. return Container(
  70. padding: EdgeInsets.all(24.w),
  71. decoration: BoxDecoration(
  72. color: Get.reactiveTheme.highlightColor,
  73. borderRadius: BorderRadius.circular(16.r),
  74. ),
  75. child: Column(
  76. children: [
  77. // 6位授权码
  78. Obx(
  79. () => Text(
  80. controller.authCode.value,
  81. style: TextStyle(
  82. fontSize: 48.sp,
  83. fontWeight: FontWeight.bold,
  84. color: Colors.white,
  85. letterSpacing: 4.w,
  86. ),
  87. ),
  88. ),
  89. 20.verticalSpaceFromWidth,
  90. // 倒计时和复制按钮
  91. Row(
  92. mainAxisAlignment: MainAxisAlignment.center,
  93. children: [
  94. Icon(
  95. Icons.access_time,
  96. size: 16.w,
  97. color: Get.reactiveTheme.hintColor,
  98. ),
  99. SizedBox(width: 8.w),
  100. Obx(
  101. () => Text(
  102. controller.countdownText,
  103. style: TextStyle(
  104. fontSize: 16.sp,
  105. color: Get.reactiveTheme.hintColor,
  106. ),
  107. ),
  108. ),
  109. SizedBox(width: 16.w),
  110. Container(
  111. width: 1,
  112. height: 20.h,
  113. color: Get.reactiveTheme.hintColor.withOpacity(0.3),
  114. ),
  115. SizedBox(width: 16.w),
  116. ClickOpacity(
  117. onTap: () {
  118. Clipboard.setData(
  119. ClipboardData(text: controller.authCode.value),
  120. );
  121. controller.copyAuthCode();
  122. },
  123. child: Row(
  124. mainAxisSize: MainAxisSize.min,
  125. children: [
  126. Text(
  127. 'Copy',
  128. style: TextStyle(
  129. fontSize: 16.sp,
  130. color: Get.reactiveTheme.hintColor,
  131. ),
  132. ),
  133. SizedBox(width: 4.w),
  134. Icon(
  135. Icons.copy,
  136. size: 16.w,
  137. color: Get.reactiveTheme.hintColor,
  138. ),
  139. ],
  140. ),
  141. ),
  142. ],
  143. ),
  144. 20.verticalSpaceFromWidth,
  145. // 提示文字
  146. Text(
  147. 'Please keep this page open.',
  148. style: TextStyle(
  149. fontSize: 16.sp,
  150. color: const Color(0xFFFFB800),
  151. fontWeight: FontWeight.w500,
  152. ),
  153. ),
  154. ],
  155. ),
  156. );
  157. }
  158. /// 说明卡片
  159. Widget _buildInstructionCards() {
  160. return Container(
  161. padding: EdgeInsets.all(20.w),
  162. decoration: BoxDecoration(
  163. color: Get.reactiveTheme.hintColor.withOpacity(0.1),
  164. borderRadius: BorderRadius.circular(16.r),
  165. border: Border.all(
  166. color: const Color(0xFF00A8E8).withOpacity(0.3),
  167. width: 1,
  168. ),
  169. ),
  170. child: Column(
  171. children: [
  172. _buildInstructionItem(
  173. icon: Icons.vpn_key,
  174. title: 'Authorization Code',
  175. description:
  176. 'This 6-digit code allows a VIP user to link your device. It refreshes every 15 minutes.',
  177. ),
  178. 20.verticalSpaceFromWidth,
  179. _buildInstructionItem(
  180. icon: Icons.workspace_premium,
  181. title: 'Share with Pre User',
  182. description:
  183. 'Tell the VIP user this code so they can enter it on their device to authorize you.',
  184. ),
  185. 20.verticalSpaceFromWidth,
  186. _buildInstructionItem(
  187. icon: Icons.access_time,
  188. title: 'Waiting for Authorization',
  189. description:
  190. 'Please keep this page open.\nOnce approved, your account will automatically upgrade and reconnect.',
  191. highlightText: 'Please keep this page open.',
  192. ),
  193. ],
  194. ),
  195. );
  196. }
  197. /// 说明条目
  198. Widget _buildInstructionItem({
  199. required IconData icon,
  200. required String title,
  201. required String description,
  202. String? highlightText,
  203. }) {
  204. return Row(
  205. crossAxisAlignment: CrossAxisAlignment.start,
  206. children: [
  207. Container(
  208. width: 40.w,
  209. height: 40.w,
  210. decoration: BoxDecoration(
  211. color: const Color(0xFF00A8E8).withOpacity(0.2),
  212. borderRadius: BorderRadius.circular(8.r),
  213. ),
  214. child: Icon(icon, size: 20.w, color: const Color(0xFF00A8E8)),
  215. ),
  216. SizedBox(width: 16.w),
  217. Expanded(
  218. child: Column(
  219. crossAxisAlignment: CrossAxisAlignment.start,
  220. children: [
  221. Text(
  222. title,
  223. style: TextStyle(
  224. fontSize: 16.sp,
  225. fontWeight: FontWeight.bold,
  226. color: Colors.white,
  227. ),
  228. ),
  229. SizedBox(height: 8.h),
  230. highlightText != null
  231. ? RichText(
  232. text: TextSpan(
  233. children: [
  234. TextSpan(
  235. text: highlightText,
  236. style: TextStyle(
  237. fontSize: 14.sp,
  238. color: const Color(0xFF00A8E8),
  239. ),
  240. ),
  241. TextSpan(
  242. text:
  243. '\n${description.substring(highlightText.length + 1)}',
  244. style: TextStyle(
  245. fontSize: 14.sp,
  246. color: Get.reactiveTheme.hintColor,
  247. ),
  248. ),
  249. ],
  250. ),
  251. )
  252. : Text(
  253. description,
  254. style: TextStyle(
  255. fontSize: 14.sp,
  256. color: Get.reactiveTheme.hintColor,
  257. height: 1.4,
  258. ),
  259. ),
  260. ],
  261. ),
  262. ),
  263. ],
  264. );
  265. }
  266. /// 顶部6个输入框
  267. Widget _buildTopCodeInput() {
  268. return Container(
  269. padding: EdgeInsets.symmetric(horizontal: 20.w, vertical: 16.h),
  270. decoration: BoxDecoration(
  271. color: Get.reactiveTheme.highlightColor,
  272. borderRadius: BorderRadius.circular(16.r),
  273. ),
  274. child: Obx(
  275. () => Pinput(
  276. length: 6,
  277. defaultPinTheme: PinTheme(
  278. width: 40.w,
  279. height: 50.h,
  280. textStyle: TextStyle(
  281. fontSize: 24.sp,
  282. fontWeight: FontWeight.bold,
  283. color: Colors.white,
  284. ),
  285. decoration: BoxDecoration(
  286. color: Get.reactiveTheme.hintColor.withOpacity(0.1),
  287. borderRadius: BorderRadius.circular(8.r),
  288. border: Border.all(color: Colors.transparent, width: 2),
  289. ),
  290. ),
  291. focusedPinTheme: PinTheme(
  292. width: 40.w,
  293. height: 50.h,
  294. textStyle: TextStyle(
  295. fontSize: 24.sp,
  296. fontWeight: FontWeight.bold,
  297. color: Colors.white,
  298. ),
  299. decoration: BoxDecoration(
  300. color: const Color(0xFF00A8E8).withOpacity(0.2),
  301. borderRadius: BorderRadius.circular(8.r),
  302. border: Border.all(color: const Color(0xFF00A8E8), width: 2),
  303. ),
  304. ),
  305. submittedPinTheme: PinTheme(
  306. width: 40.w,
  307. height: 50.h,
  308. textStyle: TextStyle(
  309. fontSize: 24.sp,
  310. fontWeight: FontWeight.bold,
  311. color: Colors.white,
  312. ),
  313. decoration: BoxDecoration(
  314. color: const Color(0xFF00A8E8).withOpacity(0.2),
  315. borderRadius: BorderRadius.circular(8.r),
  316. border: Border.all(color: Colors.transparent, width: 2),
  317. ),
  318. ),
  319. onChanged: (value) {
  320. controller.setInputCode(value);
  321. },
  322. onCompleted: (pin) {
  323. controller.submitAuthCode();
  324. },
  325. inputFormatters: [FilteringTextInputFormatter.digitsOnly],
  326. keyboardType: TextInputType.number,
  327. showCursor: true,
  328. cursor: Container(
  329. width: 2,
  330. height: 20.h,
  331. color: const Color(0xFF00A8E8),
  332. ),
  333. ),
  334. ),
  335. );
  336. }
  337. /// 设备管理区域
  338. Widget _buildDeviceManagementSection() {
  339. return Container(
  340. padding: EdgeInsets.all(20.w),
  341. decoration: BoxDecoration(
  342. color: Get.reactiveTheme.highlightColor,
  343. borderRadius: BorderRadius.circular(16.r),
  344. ),
  345. child: Column(
  346. crossAxisAlignment: CrossAxisAlignment.start,
  347. children: [
  348. // 标题
  349. Row(
  350. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  351. children: [
  352. Text(
  353. 'Manage Devices',
  354. style: TextStyle(
  355. fontSize: 18.sp,
  356. fontWeight: FontWeight.bold,
  357. color: Colors.white,
  358. ),
  359. ),
  360. Obx(
  361. () => Text(
  362. controller.deviceCountText,
  363. style: TextStyle(
  364. fontSize: 16.sp,
  365. color: Get.reactiveTheme.hintColor,
  366. ),
  367. ),
  368. ),
  369. ],
  370. ),
  371. 20.verticalSpaceFromWidth,
  372. // 设备列表
  373. Obx(
  374. () => Column(
  375. children: [
  376. ...controller.currentDevices.map((device) {
  377. return _buildDeviceCard(device);
  378. }).toList(),
  379. // 等待激活的设备
  380. if (controller.inputCode.value.isEmpty)
  381. _buildAwaitingDeviceCard(),
  382. ],
  383. ),
  384. ),
  385. 20.verticalSpaceFromWidth,
  386. // 授权限制说明
  387. Text(
  388. 'Authorize up to 4 devices as Premium (${controller.deviceCountText})',
  389. style: TextStyle(
  390. fontSize: 14.sp,
  391. color: Get.reactiveTheme.hintColor,
  392. ),
  393. ),
  394. ],
  395. ),
  396. );
  397. }
  398. /// 等待激活的设备卡片
  399. Widget _buildAwaitingDeviceCard() {
  400. return Container(
  401. margin: EdgeInsets.only(bottom: 12.h),
  402. padding: EdgeInsets.all(16.w),
  403. decoration: BoxDecoration(
  404. color: Get.reactiveTheme.hintColor.withOpacity(0.1),
  405. borderRadius: BorderRadius.circular(12.r),
  406. ),
  407. child: Row(
  408. children: [
  409. // 设备图标
  410. Container(
  411. width: 40.w,
  412. height: 40.w,
  413. decoration: BoxDecoration(
  414. color: Get.reactiveTheme.hintColor.withOpacity(0.2),
  415. borderRadius: BorderRadius.circular(8.r),
  416. ),
  417. child: Icon(
  418. Icons.phone_iphone,
  419. size: 20.w,
  420. color: Get.reactiveTheme.hintColor,
  421. ),
  422. ),
  423. SizedBox(width: 12.w),
  424. // 设备信息
  425. Expanded(
  426. child: Text(
  427. 'Awaiting Activation',
  428. style: TextStyle(
  429. fontSize: 16.sp,
  430. fontWeight: FontWeight.w500,
  431. color: Colors.white,
  432. ),
  433. ),
  434. ),
  435. ],
  436. ),
  437. );
  438. }
  439. /// 设备卡片
  440. Widget _buildDeviceCard(DeviceInfo device) {
  441. return Container(
  442. margin: EdgeInsets.only(bottom: 12.h),
  443. padding: EdgeInsets.all(16.w),
  444. decoration: BoxDecoration(
  445. color: Get.reactiveTheme.hintColor.withOpacity(0.1),
  446. borderRadius: BorderRadius.circular(12.r),
  447. ),
  448. child: Row(
  449. children: [
  450. // 设备图标
  451. Container(
  452. width: 40.w,
  453. height: 40.w,
  454. decoration: BoxDecoration(
  455. color: Get.reactiveTheme.hintColor.withOpacity(0.2),
  456. borderRadius: BorderRadius.circular(8.r),
  457. ),
  458. child: Icon(
  459. _getDeviceIcon(device.type),
  460. size: 20.w,
  461. color: Get.reactiveTheme.hintColor,
  462. ),
  463. ),
  464. SizedBox(width: 12.w),
  465. // 设备信息
  466. Expanded(
  467. child: Column(
  468. crossAxisAlignment: CrossAxisAlignment.start,
  469. children: [
  470. Text(
  471. device.name,
  472. style: TextStyle(
  473. fontSize: 16.sp,
  474. fontWeight: FontWeight.w500,
  475. color: Colors.white,
  476. ),
  477. ),
  478. if (device.uid != null) ...[
  479. SizedBox(height: 4.h),
  480. Text(
  481. device.uid!,
  482. style: TextStyle(
  483. fontSize: 14.sp,
  484. color: Get.reactiveTheme.hintColor,
  485. ),
  486. ),
  487. ],
  488. if (device.date != null) ...[
  489. SizedBox(height: 4.h),
  490. Text(
  491. device.date!,
  492. style: TextStyle(
  493. fontSize: 12.sp,
  494. color: Get.reactiveTheme.hintColor,
  495. ),
  496. ),
  497. ],
  498. ],
  499. ),
  500. ),
  501. // 操作按钮
  502. if (device.isCurrent)
  503. ClickOpacity(
  504. onTap: () => controller.removeDevice(device.id),
  505. child: Container(
  506. padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h),
  507. decoration: BoxDecoration(
  508. color: Get.reactiveTheme.hintColor.withOpacity(0.2),
  509. borderRadius: BorderRadius.circular(6.r),
  510. ),
  511. child: Row(
  512. mainAxisSize: MainAxisSize.min,
  513. children: [
  514. Icon(
  515. Icons.delete,
  516. size: 16.w,
  517. color: Get.reactiveTheme.hintColor,
  518. ),
  519. SizedBox(width: 4.w),
  520. Text(
  521. 'Relieve',
  522. style: TextStyle(
  523. fontSize: 12.sp,
  524. color: Get.reactiveTheme.hintColor,
  525. ),
  526. ),
  527. ],
  528. ),
  529. ),
  530. )
  531. else
  532. ClickOpacity(
  533. onTap: () => controller.removeDevice(device.id),
  534. child: Container(
  535. padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 6.h),
  536. decoration: BoxDecoration(
  537. color: const Color(0xFF00A8E8),
  538. borderRadius: BorderRadius.circular(6.r),
  539. ),
  540. child: Text(
  541. 'Relieve',
  542. style: TextStyle(
  543. fontSize: 12.sp,
  544. color: Colors.white,
  545. fontWeight: FontWeight.w500,
  546. ),
  547. ),
  548. ),
  549. ),
  550. ],
  551. ),
  552. );
  553. }
  554. /// 获取设备图标
  555. IconData _getDeviceIcon(DeviceTypeEnum type) {
  556. switch (type) {
  557. case DeviceTypeEnum.ios:
  558. return Icons.phone_iphone;
  559. case DeviceTypeEnum.android:
  560. return Icons.android;
  561. case DeviceTypeEnum.windows:
  562. return Icons.laptop_windows;
  563. case DeviceTypeEnum.mac:
  564. return Icons.laptop_mac;
  565. }
  566. }
  567. /// 授权步骤说明
  568. Widget _buildAuthorizationSteps() {
  569. return Container(
  570. padding: EdgeInsets.all(20.w),
  571. decoration: BoxDecoration(
  572. color: Get.reactiveTheme.highlightColor,
  573. borderRadius: BorderRadius.circular(16.r),
  574. ),
  575. child: Column(
  576. crossAxisAlignment: CrossAxisAlignment.start,
  577. children: [
  578. Text(
  579. 'Authorization Steps',
  580. style: TextStyle(
  581. fontSize: 18.sp,
  582. fontWeight: FontWeight.bold,
  583. color: Colors.white,
  584. ),
  585. ),
  586. 20.verticalSpaceFromWidth,
  587. _buildStepItem(
  588. stepNumber: 1,
  589. title: 'Enter Code',
  590. description:
  591. 'Input the 6-digit code shown on the other device (free user). This code refreshes every 15 minutes.',
  592. icon: Icons.input,
  593. isCompleted: controller.inputCode.value.isNotEmpty,
  594. ),
  595. 16.verticalSpaceFromWidth,
  596. _buildStepItem(
  597. stepNumber: 2,
  598. title: 'Verify Device',
  599. description:
  600. 'We\'ll check if the entered code matches an active device waiting for authorization.',
  601. icon: Icons.verified_user,
  602. isCompleted: controller.isCodeComplete.value,
  603. ),
  604. 16.verticalSpaceFromWidth,
  605. _buildStepItem(
  606. stepNumber: 3,
  607. title: 'Authorization Successful',
  608. description:
  609. 'Once confirmed, the device will automatically upgrade and link to your account.',
  610. icon: Icons.check_circle,
  611. isCompleted:
  612. controller.authorizationStatus.value ==
  613. AuthorizationStatus.success,
  614. ),
  615. ],
  616. ),
  617. );
  618. }
  619. /// 步骤条目
  620. Widget _buildStepItem({
  621. required int stepNumber,
  622. required String title,
  623. required String description,
  624. required IconData icon,
  625. required bool isCompleted,
  626. }) {
  627. return Row(
  628. crossAxisAlignment: CrossAxisAlignment.start,
  629. children: [
  630. Container(
  631. width: 32.w,
  632. height: 32.w,
  633. decoration: BoxDecoration(
  634. color: isCompleted
  635. ? const Color(0xFF00D9A3)
  636. : Get.reactiveTheme.hintColor.withOpacity(0.2),
  637. borderRadius: BorderRadius.circular(8.r),
  638. ),
  639. child: Center(
  640. child: isCompleted
  641. ? Icon(Icons.check, size: 16.w, color: Colors.white)
  642. : Icon(icon, size: 16.w, color: Get.reactiveTheme.hintColor),
  643. ),
  644. ),
  645. SizedBox(width: 12.w),
  646. Expanded(
  647. child: Column(
  648. crossAxisAlignment: CrossAxisAlignment.start,
  649. children: [
  650. Text(
  651. title,
  652. style: TextStyle(
  653. fontSize: 16.sp,
  654. fontWeight: FontWeight.w500,
  655. color: Colors.white,
  656. ),
  657. ),
  658. SizedBox(height: 4.h),
  659. Text(
  660. description,
  661. style: TextStyle(
  662. fontSize: 14.sp,
  663. color: Get.reactiveTheme.hintColor,
  664. height: 1.4,
  665. ),
  666. ),
  667. ],
  668. ),
  669. ),
  670. ],
  671. );
  672. }
  673. }