Commit 086e13f15b83e1d0bf9d367fed5c45cf66c63f13

Authored by 刘宏哲
1 parent 7ee5ba32

feat(app): update ui

@@ -73,7 +73,7 @@ class AppReviewPromptLogic { @@ -73,7 +73,7 @@ class AppReviewPromptLogic {
73 final result = await showDialog<AppReviewPromptResult>( 73 final result = await showDialog<AppReviewPromptResult>(
74 context: context, 74 context: context,
75 barrierColor: Colors.black.withValues(alpha: 0.7), 75 barrierColor: Colors.black.withValues(alpha: 0.7),
76 - barrierDismissible: false, 76 + barrierDismissible: true,
77 builder: (_) => const AppReviewFeedbackDialog(), 77 builder: (_) => const AppReviewFeedbackDialog(),
78 ); 78 );
79 79
@@ -81,98 +81,95 @@ class _ReviewPromptCard extends StatelessWidget { @@ -81,98 +81,95 @@ class _ReviewPromptCard extends StatelessWidget {
81 @override 81 @override
82 Widget build(BuildContext context) { 82 Widget build(BuildContext context) {
83 final isFeedback = height > 400; 83 final isFeedback = height > 400;
84 - return PopScope(  
85 - canPop: false,  
86 - child: Align(  
87 - alignment: _dialogAlignment,  
88 - child: Material(  
89 - color: Colors.transparent,  
90 - child: Container(  
91 - width: _cardWidth,  
92 - height: height,  
93 - clipBehavior: Clip.antiAlias,  
94 - decoration: BoxDecoration(  
95 - borderRadius: BorderRadius.circular(16),  
96 - gradient: const LinearGradient(  
97 - begin: Alignment.topCenter,  
98 - end: Alignment.bottomCenter,  
99 - colors: [  
100 - Color(0xFFE6DBFF),  
101 - Color(0xFFEDE4FF),  
102 - Color(0xFFEEE6FF),  
103 - Color(0xFFF9F6FF),  
104 - ],  
105 - stops: [0, 0.25, 0.75, 1],  
106 - ), 84 + return Align(
  85 + alignment: _dialogAlignment,
  86 + child: Material(
  87 + color: Colors.transparent,
  88 + child: Container(
  89 + width: _cardWidth,
  90 + height: height,
  91 + clipBehavior: Clip.antiAlias,
  92 + decoration: BoxDecoration(
  93 + borderRadius: BorderRadius.circular(16),
  94 + gradient: const LinearGradient(
  95 + begin: Alignment.topCenter,
  96 + end: Alignment.bottomCenter,
  97 + colors: [
  98 + Color(0xFFE6DBFF),
  99 + Color(0xFFEDE4FF),
  100 + Color(0xFFEEE6FF),
  101 + Color(0xFFF9F6FF),
  102 + ],
  103 + stops: [0, 0.25, 0.75, 1],
107 ), 104 ),
108 - child: Stack(  
109 - children: [  
110 - Positioned(  
111 - top: 0,  
112 - left: 0,  
113 - right: 0,  
114 - height: _illustrationHeight,  
115 - child: Image.asset(  
116 - illustrationAsset,  
117 - fit: BoxFit.cover,  
118 - ),  
119 - ),  
120 - Positioned(  
121 - top: isFeedback ? 162 : 163,  
122 - left: 38,  
123 - right: 38,  
124 - child: Text(  
125 - title,  
126 - textAlign: TextAlign.center,  
127 - style: const TextStyle(  
128 - color: _h1,  
129 - fontSize: 18,  
130 - height: 1.35,  
131 - fontWeight: FontWeight.w600,  
132 - ),  
133 - ),  
134 - ),  
135 - Positioned(  
136 - top: isFeedback ? 222 : 195,  
137 - left: 20,  
138 - right: 20,  
139 - child: Text(  
140 - message,  
141 - textAlign: TextAlign.center,  
142 - style: const TextStyle(  
143 - color: _h2,  
144 - fontSize: 14,  
145 - height: 1.35,  
146 - fontWeight: FontWeight.w400,  
147 - ),  
148 - ), 105 + ),
  106 + child: Stack(
  107 + children: [
  108 + Positioned(
  109 + top: 0,
  110 + left: 0,
  111 + right: 0,
  112 + height: _illustrationHeight,
  113 + child: Image.asset(
  114 + illustrationAsset,
  115 + fit: BoxFit.cover,
149 ), 116 ),
150 - Positioned(  
151 - top: isFeedback ? 305 : 260,  
152 - left: 40,  
153 - right: 40,  
154 - child: _PromptButton(  
155 - emoji: primaryEmoji,  
156 - text: primaryText,  
157 - textColor: Colors.white, 117 + ),
  118 + Positioned(
  119 + top: isFeedback ? 162 : 163,
  120 + left: 38,
  121 + right: 38,
  122 + child: Text(
  123 + title,
  124 + textAlign: TextAlign.center,
  125 + style: const TextStyle(
  126 + color: _h1,
  127 + fontSize: 18,
  128 + height: 1.35,
158 fontWeight: FontWeight.w600, 129 fontWeight: FontWeight.w600,
159 - backgroundColor: _brandColor,  
160 - onTap: onPrimaryTap,  
161 ), 130 ),
162 ), 131 ),
163 - Positioned(  
164 - top: isFeedback ? 357 : 312,  
165 - left: 40,  
166 - right: 40,  
167 - child: _PromptButton(  
168 - text: secondaryText,  
169 - textColor: _h2, 132 + ),
  133 + Positioned(
  134 + top: isFeedback ? 222 : 195,
  135 + left: 20,
  136 + right: 20,
  137 + child: Text(
  138 + message,
  139 + textAlign: TextAlign.center,
  140 + style: const TextStyle(
  141 + color: _h2,
  142 + fontSize: 14,
  143 + height: 1.35,
170 fontWeight: FontWeight.w400, 144 fontWeight: FontWeight.w400,
171 - onTap: onSecondaryTap,  
172 ), 145 ),
173 ), 146 ),
174 - ],  
175 - ), 147 + ),
  148 + Positioned(
  149 + top: isFeedback ? 305 : 260,
  150 + left: 40,
  151 + right: 40,
  152 + child: _PromptButton(
  153 + emoji: primaryEmoji,
  154 + text: primaryText,
  155 + textColor: Colors.white,
  156 + fontWeight: FontWeight.w600,
  157 + backgroundColor: _brandColor,
  158 + onTap: onPrimaryTap,
  159 + ),
  160 + ),
  161 + Positioned(
  162 + top: isFeedback ? 357 : 312,
  163 + left: 40,
  164 + right: 40,
  165 + child: _PromptButton(
  166 + text: secondaryText,
  167 + textColor: _h2,
  168 + fontWeight: FontWeight.w400,
  169 + onTap: onSecondaryTap,
  170 + ),
  171 + ),
  172 + ],
176 ), 173 ),
177 ), 174 ),
178 ), 175 ),
@@ -347,7 +347,7 @@ class _FriendsHeader extends StatelessWidget { @@ -347,7 +347,7 @@ class _FriendsHeader extends StatelessWidget {
347 const Spacer(), 347 const Spacer(),
348 IconButton( 348 IconButton(
349 onPressed: () { 349 onPressed: () {
350 - _navigatorToPrivacySettings(); 350 + _navigatorToPrivacySettings(context);
351 }, 351 },
352 icon: Image.asset( 352 icon: Image.asset(
353 R.assetsImagesSettings, 353 R.assetsImagesSettings,
@@ -364,7 +364,7 @@ class _FriendsHeader extends StatelessWidget { @@ -364,7 +364,7 @@ class _FriendsHeader extends StatelessWidget {
364 } 364 }
365 } 365 }
366 366
367 -_navigatorToPrivacySettings() { 367 +_navigatorToPrivacySettings(BuildContext context) {
368 Get.toNamed(Routes.PRIVACY_SETTINGS); 368 Get.toNamed(Routes.PRIVACY_SETTINGS);
369 } 369 }
370 370
@@ -419,6 +419,57 @@ class _LegalText extends StatelessWidget { @@ -419,6 +419,57 @@ class _LegalText extends StatelessWidget {
419 final String? linkSuffix; 419 final String? linkSuffix;
420 final VoidCallback? onLinkTap; 420 final VoidCallback? onLinkTap;
421 421
  422 + static const _baseStyle = TextStyle(
  423 + color: PurchaseColors.subtitle,
  424 + fontSize: 12,
  425 + height: 1.4,
  426 + );
  427 +
  428 + static const _linkStyle = TextStyle(
  429 + color: Color(0xFF845EEE),
  430 + fontWeight: FontWeight.w500,
  431 + decoration: TextDecoration.underline,
  432 + decorationColor: Color(0xFF845EEE),
  433 + );
  434 +
  435 + TextSpan _buildTextSpan() => TextSpan(
  436 + style: _baseStyle,
  437 + children: [
  438 + TextSpan(text: text),
  439 + if (linkText case final linkText?)
  440 + TextSpan(text: linkText, style: _linkStyle),
  441 + if (linkSuffix case final linkSuffix?) TextSpan(text: linkSuffix),
  442 + ],
  443 + );
  444 +
  445 + void _handleTapUp(
  446 + BuildContext context,
  447 + BoxConstraints constraints,
  448 + TapUpDetails details,
  449 + ) {
  450 + final linkText = this.linkText;
  451 + if (linkText == null || onLinkTap == null) return;
  452 +
  453 + final painter = TextPainter(
  454 + text: _buildTextSpan(),
  455 + textDirection: Directionality.of(context),
  456 + textScaler: MediaQuery.textScalerOf(context),
  457 + )..layout(maxWidth: constraints.maxWidth);
  458 + final linkStart = text.length;
  459 + final linkBoxes = painter.getBoxesForSelection(
  460 + TextSelection(
  461 + baseOffset: linkStart,
  462 + extentOffset: linkStart + linkText.length,
  463 + ),
  464 + );
  465 +
  466 + if (linkBoxes.any(
  467 + (box) => box.toRect().inflate(10).contains(details.localPosition),
  468 + )) {
  469 + onLinkTap!.call();
  470 + }
  471 + }
  472 +
422 @override 473 @override
423 Widget build(BuildContext context) { 474 Widget build(BuildContext context) {
424 return Row( 475 return Row(
@@ -433,38 +484,16 @@ class _LegalText extends StatelessWidget { @@ -433,38 +484,16 @@ class _LegalText extends StatelessWidget {
433 ), 484 ),
434 ), 485 ),
435 Expanded( 486 Expanded(
436 - child: Text.rich(  
437 - TextSpan(  
438 - children: [  
439 - TextSpan(text: text),  
440 - if (linkText case final linkText?)  
441 - WidgetSpan(  
442 - alignment: PlaceholderAlignment.baseline,  
443 - baseline: TextBaseline.alphabetic,  
444 - child: GestureDetector(  
445 - behavior: HitTestBehavior.opaque,  
446 - onTap: onLinkTap,  
447 - child: Text(  
448 - linkText,  
449 - style: const TextStyle(  
450 - color: Color(0xFF845EEE),  
451 - fontSize: 12,  
452 - height: 1.4,  
453 - fontWeight: FontWeight.w500,  
454 - decoration: TextDecoration.underline,  
455 - decorationColor: Color(0xFF845EEE),  
456 - ),  
457 - ),  
458 - ),  
459 - ),  
460 - if (linkSuffix case final linkSuffix?)  
461 - TextSpan(text: linkSuffix),  
462 - ],  
463 - ),  
464 - style: const TextStyle(  
465 - color: PurchaseColors.subtitle,  
466 - fontSize: 12,  
467 - height: 1.4, 487 + child: LayoutBuilder(
  488 + builder: (context, constraints) => GestureDetector(
  489 + behavior: HitTestBehavior.opaque,
  490 + onTapUp: linkText == null || onLinkTap == null
  491 + ? null
  492 + : (details) => _handleTapUp(context, constraints, details),
  493 + child: Text.rich(
  494 + _buildTextSpan(),
  495 + textScaler: MediaQuery.textScalerOf(context),
  496 + ),
468 ), 497 ),
469 ), 498 ),
470 ), 499 ),
@@ -38,13 +38,15 @@ class PurchaseView extends GetView<PurchaseController> { @@ -38,13 +38,15 @@ class PurchaseView extends GetView<PurchaseController> {
38 child: SingleChildScrollView( 38 child: SingleChildScrollView(
39 controller: controller.scrollController, 39 controller: controller.scrollController,
40 physics: const ClampingScrollPhysics(), 40 physics: const ClampingScrollPhysics(),
41 - padding: const EdgeInsets.only(bottom: 170), 41 + padding: EdgeInsets.only(
  42 + bottom: 170 + MediaQuery.paddingOf(context).bottom,
  43 + ),
42 child: Column( 44 child: Column(
43 crossAxisAlignment: CrossAxisAlignment.start, 45 crossAxisAlignment: CrossAxisAlignment.start,
44 children: [ 46 children: [
45 const _HeroHeader(), 47 const _HeroHeader(),
46 Padding( 48 Padding(
47 - padding: EdgeInsets.only(left: 18, top: 9.h), 49 + padding: EdgeInsets.only(left: 18, top: 9.h),
48 child: Text( 50 child: Text(
49 context.l10n.purchaseHeroTitle, 51 context.l10n.purchaseHeroTitle,
50 style: const TextStyle( 52 style: const TextStyle(
@@ -299,6 +301,57 @@ class _LegalText extends StatelessWidget { @@ -299,6 +301,57 @@ class _LegalText extends StatelessWidget {
299 final String? linkSuffix; 301 final String? linkSuffix;
300 final VoidCallback? onLinkTap; 302 final VoidCallback? onLinkTap;
301 303
  304 + static const _baseStyle = TextStyle(
  305 + color: PurchaseColors.subtitle,
  306 + fontSize: 12,
  307 + height: 1.4,
  308 + );
  309 +
  310 + static const _linkStyle = TextStyle(
  311 + color: Color(0xFF845EEE),
  312 + fontWeight: FontWeight.w500,
  313 + decoration: TextDecoration.underline,
  314 + decorationColor: Color(0xFF845EEE),
  315 + );
  316 +
  317 + TextSpan _buildTextSpan() => TextSpan(
  318 + style: _baseStyle,
  319 + children: [
  320 + TextSpan(text: text),
  321 + if (linkText case final linkText?)
  322 + TextSpan(text: linkText, style: _linkStyle),
  323 + if (linkSuffix case final linkSuffix?) TextSpan(text: linkSuffix),
  324 + ],
  325 + );
  326 +
  327 + void _handleTapUp(
  328 + BuildContext context,
  329 + BoxConstraints constraints,
  330 + TapUpDetails details,
  331 + ) {
  332 + final linkText = this.linkText;
  333 + if (linkText == null || onLinkTap == null) return;
  334 +
  335 + final painter = TextPainter(
  336 + text: _buildTextSpan(),
  337 + textDirection: Directionality.of(context),
  338 + textScaler: MediaQuery.textScalerOf(context),
  339 + )..layout(maxWidth: constraints.maxWidth);
  340 + final linkStart = text.length;
  341 + final linkBoxes = painter.getBoxesForSelection(
  342 + TextSelection(
  343 + baseOffset: linkStart,
  344 + extentOffset: linkStart + linkText.length,
  345 + ),
  346 + );
  347 +
  348 + if (linkBoxes.any(
  349 + (box) => box.toRect().inflate(10).contains(details.localPosition),
  350 + )) {
  351 + onLinkTap!.call();
  352 + }
  353 + }
  354 +
302 @override 355 @override
303 Widget build(BuildContext context) { 356 Widget build(BuildContext context) {
304 return Row( 357 return Row(
@@ -313,38 +366,16 @@ class _LegalText extends StatelessWidget { @@ -313,38 +366,16 @@ class _LegalText extends StatelessWidget {
313 ), 366 ),
314 ), 367 ),
315 Expanded( 368 Expanded(
316 - child: Text.rich(  
317 - TextSpan(  
318 - children: [  
319 - TextSpan(text: text),  
320 - if (linkText case final linkText?)  
321 - WidgetSpan(  
322 - alignment: PlaceholderAlignment.baseline,  
323 - baseline: TextBaseline.alphabetic,  
324 - child: GestureDetector(  
325 - behavior: HitTestBehavior.opaque,  
326 - onTap: onLinkTap,  
327 - child: Text(  
328 - linkText,  
329 - style: const TextStyle(  
330 - color: Color(0xFF845EEE),  
331 - fontSize: 12,  
332 - height: 1.4,  
333 - fontWeight: FontWeight.w500,  
334 - decoration: TextDecoration.underline,  
335 - decorationColor: Color(0xFF845EEE),  
336 - ),  
337 - ),  
338 - ),  
339 - ),  
340 - if (linkSuffix case final linkSuffix?)  
341 - TextSpan(text: linkSuffix),  
342 - ],  
343 - ),  
344 - style: const TextStyle(  
345 - color: PurchaseColors.subtitle,  
346 - fontSize: 12,  
347 - height: 1.4, 369 + child: LayoutBuilder(
  370 + builder: (context, constraints) => GestureDetector(
  371 + behavior: HitTestBehavior.opaque,
  372 + onTapUp: linkText == null || onLinkTap == null
  373 + ? null
  374 + : (details) => _handleTapUp(context, constraints, details),
  375 + child: Text.rich(
  376 + _buildTextSpan(),
  377 + textScaler: MediaQuery.textScalerOf(context),
  378 + ),
348 ), 379 ),
349 ), 380 ),
350 ), 381 ),
@@ -26,79 +26,91 @@ class PurchaseBottomBar extends StatelessWidget { @@ -26,79 +26,91 @@ class PurchaseBottomBar extends StatelessWidget {
26 final bottomInset = MediaQuery.paddingOf(context).bottom; 26 final bottomInset = MediaQuery.paddingOf(context).bottom;
27 final buttonWidth = math.min(280.0, MediaQuery.sizeOf(context).width - 74); 27 final buttonWidth = math.min(280.0, MediaQuery.sizeOf(context).width - 74);
28 28
29 - return IgnorePointer(  
30 - ignoring: loading,  
31 - child: Container(  
32 - height: 147 + bottomInset,  
33 - padding: EdgeInsets.only(bottom: bottomInset),  
34 - decoration: const BoxDecoration(  
35 - gradient: LinearGradient(  
36 - begin: Alignment.topCenter,  
37 - end: Alignment.center,  
38 - colors: [Color(0x00F5F2FF), PurchaseColors.background],  
39 - ),  
40 - ),  
41 - child: Column(  
42 - mainAxisAlignment: MainAxisAlignment.end,  
43 - children: [  
44 - SizedBox(  
45 - width: buttonWidth,  
46 - height: 48,  
47 - child: ElevatedButton(  
48 - onPressed: onPressed,  
49 - style: ElevatedButton.styleFrom(  
50 - elevation: 0,  
51 - shadowColor: Colors.transparent,  
52 - backgroundColor: PurchaseColors.proYellow,  
53 - foregroundColor: PurchaseColors.title,  
54 - shape: RoundedRectangleBorder(  
55 - borderRadius: BorderRadius.circular(24),  
56 - ),  
57 - textStyle: const TextStyle(  
58 - fontSize: 16,  
59 - fontWeight: FontWeight.w600,  
60 - letterSpacing: 0, 29 + return SizedBox(
  30 + height: 147 + bottomInset,
  31 + child: Stack(
  32 + children: [
  33 + const Positioned.fill(
  34 + child: IgnorePointer(
  35 + child: DecoratedBox(
  36 + decoration: BoxDecoration(
  37 + gradient: LinearGradient(
  38 + begin: Alignment.topCenter,
  39 + end: Alignment.center,
  40 + colors: [Color(0x00F5F2FF), PurchaseColors.background],
61 ), 41 ),
62 ), 42 ),
63 - child: loading  
64 - ? const SizedBox(  
65 - width: 18,  
66 - height: 18,  
67 - child: CircularProgressIndicator(  
68 - strokeWidth: 2,  
69 - color: PurchaseColors.title,  
70 - ),  
71 - )  
72 - : Text(label),  
73 ), 43 ),
74 ), 44 ),
75 - const SizedBox(height: 12),  
76 - Row(  
77 - mainAxisAlignment: MainAxisAlignment.center,  
78 - children: [  
79 - _BottomLink(  
80 - label: context.l10n.purchaseTermsOfService,  
81 - onTap: onTermsTap,  
82 - ),  
83 - const Padding(  
84 - padding: EdgeInsets.symmetric(horizontal: 8),  
85 - child: Text(  
86 - '|',  
87 - style: TextStyle(  
88 - color: PurchaseColors.subtitle,  
89 - fontSize: 12, 45 + ),
  46 + IgnorePointer(
  47 + ignoring: loading,
  48 + child: Padding(
  49 + padding: EdgeInsets.only(bottom: bottomInset),
  50 + child: Column(
  51 + mainAxisAlignment: MainAxisAlignment.end,
  52 + children: [
  53 + SizedBox(
  54 + width: buttonWidth,
  55 + height: 48,
  56 + child: ElevatedButton(
  57 + onPressed: onPressed,
  58 + style: ElevatedButton.styleFrom(
  59 + elevation: 0,
  60 + shadowColor: Colors.transparent,
  61 + backgroundColor: PurchaseColors.proYellow,
  62 + foregroundColor: PurchaseColors.title,
  63 + shape: RoundedRectangleBorder(
  64 + borderRadius: BorderRadius.circular(24),
  65 + ),
  66 + textStyle: const TextStyle(
  67 + fontSize: 16,
  68 + fontWeight: FontWeight.w600,
  69 + letterSpacing: 0,
  70 + ),
  71 + ),
  72 + child: loading
  73 + ? const SizedBox(
  74 + width: 18,
  75 + height: 18,
  76 + child: CircularProgressIndicator(
  77 + strokeWidth: 2,
  78 + color: PurchaseColors.title,
  79 + ),
  80 + )
  81 + : Text(label),
90 ), 82 ),
91 ), 83 ),
92 - ),  
93 - _BottomLink(  
94 - label: context.l10n.purchasePrivacyPolicy,  
95 - onTap: onPrivacyTap,  
96 - ),  
97 - ], 84 + const SizedBox(height: 12),
  85 + Row(
  86 + mainAxisAlignment: MainAxisAlignment.center,
  87 + children: [
  88 + _BottomLink(
  89 + label: context.l10n.purchaseTermsOfService,
  90 + onTap: onTermsTap,
  91 + ),
  92 + const Padding(
  93 + padding: EdgeInsets.symmetric(horizontal: 8),
  94 + child: Text(
  95 + '|',
  96 + style: TextStyle(
  97 + color: PurchaseColors.subtitle,
  98 + fontSize: 12,
  99 + ),
  100 + ),
  101 + ),
  102 + _BottomLink(
  103 + label: context.l10n.purchasePrivacyPolicy,
  104 + onTap: onPrivacyTap,
  105 + ),
  106 + ],
  107 + ),
  108 + const SizedBox(height: 18),
  109 + ],
  110 + ),
98 ), 111 ),
99 - const SizedBox(height: 18),  
100 - ],  
101 - ), 112 + ),
  113 + ],
102 ), 114 ),
103 ); 115 );
104 } 116 }