Skip to content

Commit 8bc22bf

Browse files
LynxLynxxdt-iohk
andauthored
feat(cat-voices): mobile access restriction (#2103)
* feat: mobile access restriction * Update catalyst_voices/packages/internal/catalyst_voices_localization/lib/l10n/intl_en.arb Co-authored-by: Dominik Toton <[email protected]> * fix: unknown word * fix: review * fix: remove mobile_access asset path from pubspec.yaml * Update catalyst_voices/apps/voices/lib/app/view/app_mobile_access_restriction.dart Co-authored-by: Dominik Toton <[email protected]> * fix: format * fix: update shouldRepaint _BubblePainter --------- Co-authored-by: Dominik Toton <[email protected]>
1 parent 607771e commit 8bc22bf

File tree

7 files changed

+474
-64
lines changed

7 files changed

+474
-64
lines changed

.config/dictionaries/project.dic

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@ loguru
192192
lovelace
193193
lovelaces
194194
LTRB
195+
ltwh
195196
LynxLynxx
196197
Lynxx
197198
mdlint
@@ -259,6 +260,7 @@ pytest
259260
qrcode
260261
rapidoc
261262
ratelimit
263+
RGBO
262264
redoc
263265
reloadable
264266
Replayability
@@ -361,6 +363,7 @@ vsync
361363
wallclock
362364
wasmtime
363365
Wconditional
366+
webos
364367
Werror
365368
Wireframes
366369
Wmissing

catalyst_voices/apps/voices/lib/app/view/app_content.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'package:catalyst_voices/app/view/app_active_state_listener.dart';
2+
import 'package:catalyst_voices/app/view/app_mobile_access_restriction.dart';
23
import 'package:catalyst_voices/app/view/app_precache_image_assets.dart';
34
import 'package:catalyst_voices/app/view/app_session_listener.dart';
45
import 'package:catalyst_voices/common/ext/preferences_ext.dart';
@@ -76,7 +77,9 @@ final class _AppContent extends StatelessWidget {
7677
body: AppActiveStateListener(
7778
child: GlobalPrecacheImages(
7879
child: GlobalSessionListener(
79-
child: child ?? const SizedBox.shrink(),
80+
child: AppMobileAccessRestriction(
81+
child: child ?? const SizedBox.shrink(),
82+
),
8083
),
8184
),
8285
),
Lines changed: 358 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,358 @@
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

Comments
 (0)