|
| 1 | +import 'dart:math'; |
| 2 | + |
| 3 | +import 'package:catalyst_voices/common/ext/build_context_ext.dart'; |
| 4 | +import 'package:catalyst_voices/widgets/buttons/voices_filled_button.dart'; |
| 5 | +import 'package:catalyst_voices/widgets/buttons/voices_text_button.dart'; |
| 6 | +import 'package:catalyst_voices_assets/catalyst_voices_assets.dart'; |
| 7 | +import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; |
| 8 | +import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; |
| 9 | +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; |
| 10 | +import 'package:flutter/material.dart'; |
| 11 | + |
| 12 | +typedef _LayoutData = ({ |
| 13 | + TextStyle? titleStyle, |
| 14 | + TextStyle? subtitleStyle, |
| 15 | + TextStyle? descriptionStyle, |
| 16 | + bool isMobile, |
| 17 | +}); |
| 18 | + |
| 19 | +class AppMobileAccessRestriction extends StatelessWidget { |
| 20 | + final Widget child; |
| 21 | + |
| 22 | + const AppMobileAccessRestriction({ |
| 23 | + super.key, |
| 24 | + required this.child, |
| 25 | + }); |
| 26 | + |
| 27 | + @override |
| 28 | + Widget build(BuildContext context) { |
| 29 | + return PlatformAwareBuilder<Widget>( |
| 30 | + mobileWeb: ResponsiveBuilder<_LayoutData>( |
| 31 | + xs: ( |
| 32 | + titleStyle: context.textTheme.displayMedium?.copyWith( |
| 33 | + color: context.colorScheme.primary, |
| 34 | + ), |
| 35 | + subtitleStyle: context.textTheme.titleSmall, |
| 36 | + descriptionStyle: context.textTheme.bodyMedium, |
| 37 | + isMobile: true, |
| 38 | + ), |
| 39 | + other: ( |
| 40 | + titleStyle: context.textTheme.displayMedium?.copyWith( |
| 41 | + color: context.colorScheme.primary, |
| 42 | + fontSize: 78, |
| 43 | + height: 1.15, |
| 44 | + ), |
| 45 | + subtitleStyle: context.textTheme.titleMedium, |
| 46 | + descriptionStyle: context.textTheme.bodyLarge, |
| 47 | + isMobile: false, |
| 48 | + ), |
| 49 | + builder: (context, data) => _MobileSplashScreen( |
| 50 | + data: data, |
| 51 | + ), |
| 52 | + ), |
| 53 | + other: child, |
| 54 | + builder: (context, child) => child!, |
| 55 | + ); |
| 56 | + } |
| 57 | +} |
| 58 | + |
| 59 | +class _Actions extends StatelessWidget { |
| 60 | + const _Actions(); |
| 61 | + |
| 62 | + @override |
| 63 | + Widget build(BuildContext context) { |
| 64 | + return Center( |
| 65 | + child: Column( |
| 66 | + mainAxisSize: MainAxisSize.min, |
| 67 | + children: [ |
| 68 | + const SizedBox(height: 52), |
| 69 | + VoicesFilledButton( |
| 70 | + child: Text(context.l10n.joinNewsletter), |
| 71 | + onTap: () { |
| 72 | + // TODO(LynxLynxx): implement url launching |
| 73 | + }, |
| 74 | + ), |
| 75 | + const SizedBox(height: 12), |
| 76 | + VoicesTextButton( |
| 77 | + child: Text(context.l10n.visitGitbook), |
| 78 | + onTap: () { |
| 79 | + // TODO(LynxLynxx): implement url launching |
| 80 | + }, |
| 81 | + ), |
| 82 | + const SizedBox(height: 50), |
| 83 | + ], |
| 84 | + ), |
| 85 | + ); |
| 86 | + } |
| 87 | +} |
| 88 | + |
| 89 | +class _Background extends StatelessWidget { |
| 90 | + final bool isMobile; |
| 91 | + |
| 92 | + const _Background({ |
| 93 | + required this.isMobile, |
| 94 | + }); |
| 95 | + |
| 96 | + @override |
| 97 | + Widget build(BuildContext context) { |
| 98 | + return CustomPaint( |
| 99 | + painter: _BubblePainter(isMobile: isMobile), |
| 100 | + size: Size.infinite, |
| 101 | + ); |
| 102 | + } |
| 103 | +} |
| 104 | + |
| 105 | +class _BubblePainter extends CustomPainter { |
| 106 | + final bool isMobile; |
| 107 | + |
| 108 | + _BubblePainter({required this.isMobile}); |
| 109 | + |
| 110 | + @override |
| 111 | + void paint(Canvas canvas, Size size) { |
| 112 | + // Background |
| 113 | + canvas.drawRect( |
| 114 | + Rect.fromLTWH(0, 0, size.width, size.height), |
| 115 | + Paint()..color = const Color(0xff9BDDF7), |
| 116 | + ); |
| 117 | + |
| 118 | + // Left bubble |
| 119 | + _drawBubble( |
| 120 | + canvas, |
| 121 | + x: isMobile ? 0 - 70 : 0 - 90, |
| 122 | + y: size.height * 0.25, |
| 123 | + radius: isMobile ? 110 : 200, |
| 124 | + gradientColors: const [Color(0xFFE5F6FF), Color(0xCCE5F6FF)], |
| 125 | + gradientStops: const [0.0, 1.0], |
| 126 | + ); |
| 127 | + |
| 128 | + // Right bubble |
| 129 | + _drawBubble( |
| 130 | + canvas, |
| 131 | + x: isMobile ? size.width + 70 : size.width + 140, |
| 132 | + y: isMobile ? size.height : size.height + 140, |
| 133 | + radius: isMobile ? 140 : 430, |
| 134 | + gradientColors: const [Color(0xFFE5F6FF), Color(0xCCE5F6FF)], |
| 135 | + gradientStops: const [0.0, 1.0], |
| 136 | + ); |
| 137 | + |
| 138 | + // Left shape |
| 139 | + _drawShape( |
| 140 | + canvas, |
| 141 | + size, |
| 142 | + controlPoints: [ |
| 143 | + Point(0, size.height * .7), |
| 144 | + Point(size.width * .13, size.height * .82), |
| 145 | + Point(size.width * .15, size.height), |
| 146 | + Point(0, size.height), |
| 147 | + ], |
| 148 | + gradient: const RadialGradient( |
| 149 | + center: Alignment(0.2822, -0.3306), |
| 150 | + radius: 0.5, |
| 151 | + colors: [Color(0x99F9E7FD), Color(0x99F6CEFF)], |
| 152 | + stops: [0.0, 0.0], |
| 153 | + ), |
| 154 | + ); |
| 155 | + |
| 156 | + // First right shape |
| 157 | + _drawShape( |
| 158 | + canvas, |
| 159 | + size, |
| 160 | + controlPoints: [ |
| 161 | + Point(size.width * .75, 0), |
| 162 | + Point( |
| 163 | + isMobile ? size.width * .8 : size.width * .7, |
| 164 | + isMobile ? size.height * .15 : size.height * .3, |
| 165 | + ), |
| 166 | + Point( |
| 167 | + size.width, |
| 168 | + isMobile ? size.height * .25 : size.height * .4, |
| 169 | + ), |
| 170 | + Point(size.width, 0), |
| 171 | + ], |
| 172 | + color: Color.fromARGB((255 * 0.1).toInt(), 192, 20, 235), |
| 173 | + ); |
| 174 | + |
| 175 | + // Second right shape |
| 176 | + _drawShape( |
| 177 | + canvas, |
| 178 | + size, |
| 179 | + controlPoints: [ |
| 180 | + Point(size.width, size.height * .2), |
| 181 | + Point(size.width * .7, size.height * .45), |
| 182 | + Point(size.width, size.height * .6), |
| 183 | + ], |
| 184 | + gradient: const RadialGradient( |
| 185 | + center: Alignment(0.2814, -0.3306), |
| 186 | + radius: 0.5, |
| 187 | + colors: [ |
| 188 | + Color.fromRGBO(205, 213, 254, 0.7), |
| 189 | + Color(0x99C6C5FF), |
| 190 | + ], |
| 191 | + stops: [0.0, 1.0], |
| 192 | + ), |
| 193 | + ); |
| 194 | + } |
| 195 | + |
| 196 | + @override |
| 197 | + bool shouldRepaint(_BubblePainter oldDelegate) => |
| 198 | + isMobile != oldDelegate.isMobile; |
| 199 | + |
| 200 | + void _drawBubble( |
| 201 | + Canvas canvas, { |
| 202 | + required double x, |
| 203 | + required double y, |
| 204 | + required double radius, |
| 205 | + required List<Color> gradientColors, |
| 206 | + required List<double> gradientStops, |
| 207 | + }) { |
| 208 | + final rect = Rect.fromCircle(center: Offset(x, y), radius: radius); |
| 209 | + final shadowPath = Path()..addOval(rect); |
| 210 | + |
| 211 | + canvas |
| 212 | + ..save() |
| 213 | + ..translate(-9.99, -10.99) |
| 214 | + ..drawShadow( |
| 215 | + shadowPath, |
| 216 | + const Color.fromRGBO(150, 142, 253, 0.4), |
| 217 | + 62.46, |
| 218 | + true, |
| 219 | + ) |
| 220 | + ..restore(); |
| 221 | + |
| 222 | + final paintGradient = Paint() |
| 223 | + ..shader = RadialGradient( |
| 224 | + colors: gradientColors, |
| 225 | + stops: gradientStops, |
| 226 | + center: Alignment.center, |
| 227 | + radius: 0.8, |
| 228 | + ).createShader(rect) |
| 229 | + ..blendMode = BlendMode.softLight; |
| 230 | + |
| 231 | + canvas.drawCircle(Offset(x, y), radius, paintGradient); |
| 232 | + } |
| 233 | + |
| 234 | + void _drawShape( |
| 235 | + Canvas canvas, |
| 236 | + Size size, { |
| 237 | + required List<Point<double>> controlPoints, |
| 238 | + Color? color, |
| 239 | + RadialGradient? gradient, |
| 240 | + }) { |
| 241 | + final path = Path()..moveTo(controlPoints[0].x, controlPoints[0].y); |
| 242 | + |
| 243 | + if (controlPoints.length == 4) { |
| 244 | + path |
| 245 | + ..quadraticBezierTo( |
| 246 | + controlPoints[1].x, |
| 247 | + controlPoints[1].y, |
| 248 | + controlPoints[2].x, |
| 249 | + controlPoints[2].y, |
| 250 | + ) |
| 251 | + ..lineTo(controlPoints[3].x, controlPoints[3].y); |
| 252 | + } else if (controlPoints.length == 3) { |
| 253 | + path.quadraticBezierTo( |
| 254 | + controlPoints[1].x, |
| 255 | + controlPoints[1].y, |
| 256 | + controlPoints[2].x, |
| 257 | + controlPoints[2].y, |
| 258 | + ); |
| 259 | + } |
| 260 | + |
| 261 | + path.close(); |
| 262 | + |
| 263 | + final paint = Paint()..style = PaintingStyle.fill; |
| 264 | + |
| 265 | + if (gradient != null) { |
| 266 | + paint.shader = gradient.createShader( |
| 267 | + Rect.fromLTWH(0, 0, size.width, size.height), |
| 268 | + ); |
| 269 | + } else if (color != null) { |
| 270 | + paint.color = color; |
| 271 | + } |
| 272 | + |
| 273 | + canvas.drawPath(path, paint); |
| 274 | + } |
| 275 | +} |
| 276 | + |
| 277 | +class _Foreground extends StatelessWidget { |
| 278 | + final _LayoutData data; |
| 279 | + |
| 280 | + const _Foreground({ |
| 281 | + required this.data, |
| 282 | + }); |
| 283 | + |
| 284 | + @override |
| 285 | + Widget build(BuildContext context) { |
| 286 | + return SingleChildScrollView( |
| 287 | + child: Column( |
| 288 | + crossAxisAlignment: CrossAxisAlignment.start, |
| 289 | + mainAxisSize: MainAxisSize.min, |
| 290 | + children: [ |
| 291 | + Padding( |
| 292 | + padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 30), |
| 293 | + child: Theme.of(context) |
| 294 | + .brandAssets |
| 295 | + .brand |
| 296 | + .logo(context) |
| 297 | + .buildPicture(), |
| 298 | + ), |
| 299 | + Center( |
| 300 | + child: Padding( |
| 301 | + padding: const EdgeInsets.symmetric(horizontal: 40), |
| 302 | + child: ConstrainedBox( |
| 303 | + constraints: BoxConstraints( |
| 304 | + maxWidth: data.isMobile ? 400 : 620, |
| 305 | + ), |
| 306 | + child: Column( |
| 307 | + crossAxisAlignment: CrossAxisAlignment.stretch, |
| 308 | + mainAxisAlignment: MainAxisAlignment.center, |
| 309 | + mainAxisSize: MainAxisSize.min, |
| 310 | + children: [ |
| 311 | + CatalystImage.asset( |
| 312 | + VoicesAssets.images.mobileRestrictAccess.path, |
| 313 | + height: data.isMobile ? 203 : 400, |
| 314 | + ), |
| 315 | + Text( |
| 316 | + context.l10n.mobileAccessTitle, |
| 317 | + style: data.titleStyle, |
| 318 | + ), |
| 319 | + const SizedBox(height: 24), |
| 320 | + Text( |
| 321 | + context.l10n.mobileAccessSubtitle, |
| 322 | + style: data.subtitleStyle, |
| 323 | + ), |
| 324 | + const SizedBox(height: 24), |
| 325 | + Text( |
| 326 | + context.l10n.mobileAccessDescription, |
| 327 | + style: data.descriptionStyle, |
| 328 | + ), |
| 329 | + ], |
| 330 | + ), |
| 331 | + ), |
| 332 | + ), |
| 333 | + ), |
| 334 | + const _Actions(), |
| 335 | + ], |
| 336 | + ), |
| 337 | + ); |
| 338 | + } |
| 339 | +} |
| 340 | + |
| 341 | +class _MobileSplashScreen extends StatelessWidget { |
| 342 | + final _LayoutData data; |
| 343 | + |
| 344 | + const _MobileSplashScreen({ |
| 345 | + required this.data, |
| 346 | + }); |
| 347 | + |
| 348 | + @override |
| 349 | + Widget build(BuildContext context) { |
| 350 | + return Stack( |
| 351 | + fit: StackFit.expand, |
| 352 | + children: [ |
| 353 | + _Background(isMobile: data.isMobile), |
| 354 | + _Foreground(data: data), |
| 355 | + ], |
| 356 | + ); |
| 357 | + } |
| 358 | +} |
0 commit comments