deviceauth_view.dart 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  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 '../../../../config/theme/dark_theme_colors.dart';
  11. import '../../../../config/translations/strings_enum.dart';
  12. import '../../../constants/iconfont/iconfont.dart';
  13. import '../../../widgets/info_card.dart';
  14. import '../controllers/deviceauth_controller.dart';
  15. import '../widgets/device_card.dart';
  16. class DeviceauthView extends BaseView<DeviceauthController> {
  17. const DeviceauthView({super.key});
  18. @override
  19. PreferredSizeWidget? get appBar => IXAppBar(title: Strings.deviceAuthorization.tr);
  20. @override
  21. Widget buildContent(BuildContext context) {
  22. return Obx(() {
  23. final isPremium = controller.isPremium.value;
  24. return SingleChildScrollView(
  25. padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 20.w),
  26. child: Column(
  27. children: [
  28. if (isPremium) ...[
  29. // VIP用户界面
  30. _buildVIPUserInterface(),
  31. ] else ...[
  32. // 免费用户界面
  33. _buildFreeUserInterface(),
  34. ],
  35. ],
  36. ),
  37. );
  38. });
  39. }
  40. /// 免费用户界面 - 显示授权码和等待授权
  41. Widget _buildFreeUserInterface() {
  42. return Column(
  43. children: [
  44. // 授权码显示区域
  45. _buildAuthCodeDisplay(),
  46. // 说明卡片
  47. _buildInstructionCards(),
  48. ],
  49. );
  50. }
  51. /// VIP用户界面 - 输入授权码和管理设备
  52. Widget _buildVIPUserInterface() {
  53. return Column(
  54. children: [
  55. // 6个输入框在顶部
  56. _buildTopCodeInput(),
  57. 20.verticalSpaceFromWidth,
  58. Padding(
  59. padding: EdgeInsets.symmetric(horizontal: 8.w),
  60. child: Divider(height: 1.w, color: Get.reactiveTheme.dividerColor),
  61. ),
  62. // 设备管理区域
  63. _buildDeviceManagementSection(),
  64. // 授权步骤说明
  65. _buildAuthorizationSteps(),
  66. ],
  67. );
  68. }
  69. /// 授权码显示区域
  70. Widget _buildAuthCodeDisplay() {
  71. return Container(
  72. padding: EdgeInsets.symmetric(horizontal: 14.w, vertical: 10.w),
  73. child: Column(
  74. children: [
  75. // 6位授权码
  76. Obx(
  77. () => Text(
  78. controller.authCode.value,
  79. style: TextStyle(
  80. fontSize: 34.sp,
  81. fontWeight: FontWeight.w500,
  82. height: 1.2,
  83. color: Get.reactiveTheme.textTheme.bodyLarge!.color,
  84. letterSpacing: 10.w,
  85. ),
  86. ),
  87. ),
  88. 14.verticalSpaceFromWidth,
  89. // 倒计时和复制按钮
  90. Row(
  91. mainAxisAlignment: MainAxisAlignment.center,
  92. children: [
  93. Icon(
  94. IconFont.icon48,
  95. size: 20.w,
  96. color: Get.reactiveTheme.hintColor,
  97. ),
  98. 4.horizontalSpace,
  99. Obx(
  100. () => Text(
  101. controller.countdownText,
  102. style: TextStyle(
  103. fontSize: 12.sp,
  104. height: 1.7,
  105. color: Get.reactiveTheme.hintColor,
  106. ),
  107. ),
  108. ),
  109. 20.horizontalSpace,
  110. Container(
  111. width: 1.w,
  112. height: 20.w,
  113. color: Get.reactiveTheme.dividerColor,
  114. ),
  115. 20.horizontalSpace,
  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: 12.sp,
  130. height: 1.7,
  131. color: Get.reactiveTheme.hintColor,
  132. ),
  133. ),
  134. 4.horizontalSpace,
  135. Icon(
  136. IconFont.icon57,
  137. size: 20.w,
  138. color: Get.reactiveTheme.hintColor,
  139. ),
  140. ],
  141. ),
  142. ),
  143. ],
  144. ),
  145. 20.verticalSpaceFromWidth,
  146. // 提示文字
  147. Text(
  148. 'Please keep this page open.',
  149. style: TextStyle(
  150. fontSize: 13.sp,
  151. color: DarkThemeColors.validTermColor,
  152. height: 1.4,
  153. ),
  154. ),
  155. ],
  156. ),
  157. );
  158. }
  159. /// 说明卡片
  160. Widget _buildInstructionCards() {
  161. return InfoCard(
  162. title: '',
  163. items: [
  164. InfoItem(
  165. icon: IconFont.icon11,
  166. title: 'Authorization Code',
  167. description:
  168. 'This 6-digit code allows a VIP user to link your device. It refreshes every 15 minutes.',
  169. iconColor: DarkThemeColors.shadowColor,
  170. ),
  171. InfoItem(
  172. icon: IconFont.icon23,
  173. title: 'Share with Pre User',
  174. description:
  175. 'Tell the VIP user this code so they can enter it on their device to authorize you.',
  176. iconColor: DarkThemeColors.shadowColor,
  177. ),
  178. InfoItem(
  179. icon: IconFont.icon49,
  180. title: 'Waiting for Authorization',
  181. description:
  182. 'Please keep this page open.\nOnce approved, your account will automatically upgrade and reconnect.',
  183. iconColor: DarkThemeColors.shadowColor,
  184. ),
  185. ],
  186. );
  187. }
  188. /// 顶部6个输入框
  189. Widget _buildTopCodeInput() {
  190. return Obx(
  191. () => Pinput(
  192. length: 6,
  193. defaultPinTheme: PinTheme(
  194. width: 50.w,
  195. height: 50.w,
  196. textStyle: TextStyle(
  197. fontSize: 27.sp,
  198. fontWeight: FontWeight.w600,
  199. color: Get.reactiveTheme.primaryColor,
  200. ),
  201. decoration: BoxDecoration(
  202. color: Get.reactiveTheme.highlightColor,
  203. borderRadius: BorderRadius.circular(5.r),
  204. border: Border.all(
  205. color: Get.reactiveTheme.dividerColor,
  206. width: 1.5.w,
  207. ),
  208. ),
  209. ),
  210. focusedPinTheme: PinTheme(
  211. width: 50.w,
  212. height: 50.w,
  213. textStyle: TextStyle(
  214. fontSize: 27.sp,
  215. fontWeight: FontWeight.w600,
  216. color: Get.reactiveTheme.primaryColor,
  217. ),
  218. decoration: BoxDecoration(
  219. color: Get.reactiveTheme.cardColor,
  220. borderRadius: BorderRadius.circular(5.r),
  221. border: Border.all(
  222. color: Get.reactiveTheme.primaryColor,
  223. width: 1.5.w,
  224. ),
  225. ),
  226. ),
  227. submittedPinTheme: PinTheme(
  228. width: 50.w,
  229. height: 50.w,
  230. textStyle: TextStyle(
  231. fontSize: 27.sp,
  232. fontWeight: FontWeight.w600,
  233. color: Get.reactiveTheme.primaryColor,
  234. ),
  235. decoration: BoxDecoration(
  236. color: Get.reactiveTheme.cardColor,
  237. borderRadius: BorderRadius.circular(5.r),
  238. border: Border.all(
  239. color: Get.reactiveTheme.dividerColor,
  240. width: 1.5.w,
  241. ),
  242. ),
  243. ),
  244. onChanged: (value) {
  245. controller.setInputCode(value);
  246. },
  247. onCompleted: (pin) {
  248. controller.submitAuthCode();
  249. },
  250. inputFormatters: [FilteringTextInputFormatter.digitsOnly],
  251. keyboardType: TextInputType.number,
  252. showCursor: true,
  253. cursor: Container(
  254. width: 1.w,
  255. height: 30.w,
  256. color: Get.reactiveTheme.textTheme.bodyLarge!.color,
  257. ),
  258. ),
  259. );
  260. }
  261. /// 设备管理区域
  262. Widget _buildDeviceManagementSection() {
  263. return Obx(() {
  264. final authStatus = controller.authorizationStatus.value;
  265. final isConfiguring = authStatus == AuthorizationStatus.configuring;
  266. return Container(
  267. margin: EdgeInsets.only(top: 20.w),
  268. decoration: BoxDecoration(
  269. color: Get.reactiveTheme.highlightColor,
  270. borderRadius: BorderRadius.circular(12.r),
  271. ),
  272. child: Column(
  273. crossAxisAlignment: CrossAxisAlignment.start,
  274. children: [
  275. // 设备列表
  276. ...controller.currentDevices.map((device) {
  277. return DeviceCard(
  278. device: device,
  279. onRelieve: () => controller.removeDevice(device.id),
  280. showRelieveButton: true,
  281. );
  282. }),
  283. // 加载中状态或等待激活的设备
  284. if (isConfiguring || controller.inputCode.value.isEmpty)
  285. const AwaitingActivationCard(),
  286. ],
  287. ),
  288. );
  289. });
  290. }
  291. /// 授权步骤说明
  292. Widget _buildAuthorizationSteps() {
  293. return InfoCard(
  294. title: '',
  295. items: [
  296. InfoItem(
  297. icon: IconFont.icon46,
  298. title: 'Enter Code',
  299. description:
  300. 'Input the 6-digit code shown on the other device (free user).This code refreshes every 15 minutes.',
  301. iconColor: DarkThemeColors.shadowColor,
  302. ),
  303. InfoItem(
  304. icon: IconFont.icon16,
  305. title: 'Verify Device',
  306. description:
  307. 'We’ll check if the entered code matches an active device waiting for authorization.',
  308. iconColor: DarkThemeColors.shadowColor,
  309. ),
  310. InfoItem(
  311. icon: IconFont.icon43,
  312. title: 'Authorization Successful',
  313. description:
  314. 'Once confirmed, the device will automatically upgrade and link to your account.',
  315. iconColor: DarkThemeColors.shadowColor,
  316. ),
  317. ],
  318. );
  319. }
  320. }