neoagent 2.3.1-beta.57 → 2.3.1-beta.59

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,12 @@
1
1
  import 'dart:io' show Platform;
2
+
2
3
  import 'package:flutter/foundation.dart' show kIsWeb;
3
4
  import 'package:flutter/material.dart';
5
+ import 'package:flutter_animate/flutter_animate.dart';
4
6
  import 'package:video_player/video_player.dart';
5
7
 
8
+ import 'onboarding_chrome.dart';
9
+
6
10
  class OnboardingVideoStep extends StatefulWidget {
7
11
  const OnboardingVideoStep({super.key, required this.onComplete});
8
12
 
@@ -16,7 +20,6 @@ class _OnboardingVideoStepState extends State<OnboardingVideoStep> {
16
20
  VideoPlayerController? _controller;
17
21
  bool _isInitialized = false;
18
22
  bool _hasError = false;
19
-
20
23
  bool _hasCompleted = false;
21
24
 
22
25
  @override
@@ -26,11 +29,8 @@ class _OnboardingVideoStepState extends State<OnboardingVideoStep> {
26
29
  }
27
30
 
28
31
  Future<void> _initVideo() async {
29
- // video_player doesn't support Windows/Linux out of the box
30
32
  if (!kIsWeb && (Platform.isWindows || Platform.isLinux)) {
31
- setState(() {
32
- _hasError = true;
33
- });
33
+ setState(() => _hasError = true);
34
34
  return;
35
35
  }
36
36
 
@@ -40,26 +40,22 @@ class _OnboardingVideoStepState extends State<OnboardingVideoStep> {
40
40
  );
41
41
  await _controller!.initialize();
42
42
  if (!mounted) return;
43
- _controller!.addListener(_videoListener);
44
- setState(() {
45
- _isInitialized = true;
46
- });
43
+ _controller!
44
+ ..setLooping(false)
45
+ ..setVolume(1)
46
+ ..addListener(_videoListener);
47
+ setState(() => _isInitialized = true);
47
48
  await _controller!.play();
48
- if (!mounted) return;
49
- } catch (e) {
50
- if (!mounted) return;
51
- // If the asset is missing or invalid (like our dummy file), just show error/skip
52
- setState(() {
53
- _hasError = true;
54
- });
49
+ } catch (_) {
50
+ if (mounted) {
51
+ setState(() => _hasError = true);
52
+ }
55
53
  }
56
54
  }
57
55
 
58
56
  void _videoListener() {
59
57
  if (!mounted || _hasCompleted) return;
60
58
  if (_controller == null || !_controller!.value.isInitialized) return;
61
-
62
- // When video reaches the end
63
59
  if (_controller!.value.position >= _controller!.value.duration) {
64
60
  _hasCompleted = true;
65
61
  _controller!.removeListener(_videoListener);
@@ -76,110 +72,147 @@ class _OnboardingVideoStepState extends State<OnboardingVideoStep> {
76
72
 
77
73
  @override
78
74
  Widget build(BuildContext context) {
75
+ final orientation = MediaQuery.orientationOf(context);
76
+
79
77
  if (_hasError) {
80
- return Center(
81
- child: Column(
82
- mainAxisSize: MainAxisSize.min,
83
- children: <Widget>[
84
- const Icon(Icons.error_outline, color: Colors.white54, size: 48),
85
- const SizedBox(height: 16),
86
- const Text(
87
- 'Video placeholder. Paste real mp4 over assets/branding/onboarding_intro.mp4',
88
- style: TextStyle(color: Colors.white54),
89
- ),
90
- const SizedBox(height: 24),
91
- FilledButton(
92
- onPressed: widget.onComplete,
93
- child: const Text('Skip Video'),
78
+ return Scaffold(
79
+ backgroundColor: Colors.black,
80
+ body: Center(
81
+ child: Padding(
82
+ padding: const EdgeInsets.all(32),
83
+ child: Column(
84
+ mainAxisSize: MainAxisSize.min,
85
+ children: <Widget>[
86
+ const Icon(
87
+ Icons.play_circle_outline_rounded,
88
+ color: Colors.white70,
89
+ size: 56,
90
+ ),
91
+ const SizedBox(height: 18),
92
+ const Text(
93
+ 'Continue to setup',
94
+ style: TextStyle(
95
+ color: Colors.white,
96
+ fontSize: 26,
97
+ fontWeight: FontWeight.w800,
98
+ ),
99
+ ),
100
+ const SizedBox(height: 12),
101
+ Text(
102
+ 'The intro is not available on this device.',
103
+ textAlign: TextAlign.center,
104
+ style: TextStyle(
105
+ color: Colors.white.withValues(alpha: 0.68),
106
+ fontSize: 15,
107
+ height: 1.5,
108
+ ),
109
+ ),
110
+ const SizedBox(height: 28),
111
+ OnboardingPrimaryButton(
112
+ label: 'Continue',
113
+ icon: Icons.arrow_forward_rounded,
114
+ onPressed: widget.onComplete,
115
+ ),
116
+ ],
94
117
  ),
95
- ],
118
+ ),
96
119
  ),
97
120
  );
98
121
  }
99
122
 
100
- if (!_isInitialized) {
101
- return const Center(child: CircularProgressIndicator(color: Colors.white));
123
+ if (!_isInitialized || _controller == null) {
124
+ return const Scaffold(
125
+ backgroundColor: Colors.black,
126
+ body: Center(child: CircularProgressIndicator(color: Colors.white)),
127
+ );
102
128
  }
103
129
 
104
- final isPortrait = MediaQuery.of(context).orientation == Orientation.portrait;
130
+ final portrait = orientation == Orientation.portrait;
131
+ if (portrait && _controller!.value.isPlaying) {
132
+ _controller!.pause();
133
+ } else if (!portrait && !_controller!.value.isPlaying) {
134
+ _controller!.play();
135
+ }
105
136
 
106
- if (isPortrait) {
107
- // Pause video while asking to rotate
108
- if (_controller!.value.isPlaying) {
109
- _controller!.pause();
110
- }
111
- return Center(
112
- child: Padding(
113
- padding: const EdgeInsets.all(32.0),
114
- child: Column(
115
- mainAxisSize: MainAxisSize.min,
116
- children: <Widget>[
117
- const Icon(Icons.screen_rotation, color: Colors.white, size: 64),
118
- const SizedBox(height: 24),
119
- const Text(
120
- 'Please rotate your device',
121
- textAlign: TextAlign.center,
122
- style: TextStyle(
123
- color: Colors.white,
124
- fontSize: 24,
125
- fontWeight: FontWeight.w700,
137
+ return Scaffold(
138
+ backgroundColor: Colors.black,
139
+ body: portrait
140
+ ? const _RotatePrompt()
141
+ : Stack(
142
+ fit: StackFit.expand,
143
+ children: <Widget>[
144
+ FittedBox(
145
+ fit: BoxFit.cover,
146
+ child: SizedBox(
147
+ width: _controller!.value.size.width,
148
+ height: _controller!.value.size.height,
149
+ child: VideoPlayer(_controller!),
150
+ ),
126
151
  ),
127
- ),
128
- const SizedBox(height: 16),
129
- Text(
130
- 'The intro video is exclusively formatted for landscape viewing.',
131
- textAlign: TextAlign.center,
132
- style: TextStyle(
133
- color: Colors.white.withValues(alpha: 0.7),
134
- fontSize: 16,
135
- height: 1.4,
152
+ Positioned.fill(
153
+ child: DecoratedBox(
154
+ decoration: BoxDecoration(
155
+ gradient: LinearGradient(
156
+ colors: <Color>[
157
+ Colors.transparent,
158
+ Colors.transparent,
159
+ Colors.black.withValues(alpha: 0.22),
160
+ ],
161
+ begin: Alignment.topCenter,
162
+ end: Alignment.bottomCenter,
163
+ ),
164
+ ),
165
+ ),
136
166
  ),
137
- ),
138
- const SizedBox(height: 40),
139
- FilledButton.tonal(
140
- onPressed: () {
141
- // If they really want to proceed without rotating
142
- widget.onComplete();
143
- },
144
- style: FilledButton.styleFrom(
145
- padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16),
167
+ Positioned(
168
+ top: 28,
169
+ right: 28,
170
+ child: OnboardingGhostButton(
171
+ label: 'Skip',
172
+ onPressed: widget.onComplete,
173
+ ).animate().fadeIn(duration: 280.ms, delay: 220.ms),
146
174
  ),
147
- child: const Text('Skip Video'),
148
- ),
149
- ],
150
- ),
151
- ),
152
- );
153
- } else {
154
- // Ensure it resumes if they rotated back
155
- if (!_controller!.value.isPlaying && _isInitialized) {
156
- _controller!.play();
157
- }
158
- }
175
+ ],
176
+ ),
177
+ );
178
+ }
179
+ }
159
180
 
160
- return Stack(
161
- fit: StackFit.expand,
162
- children: <Widget>[
163
- FittedBox(
164
- fit: BoxFit.cover,
165
- child: SizedBox(
166
- width: _controller!.value.size.width,
167
- height: _controller!.value.size.height,
168
- child: VideoPlayer(_controller!),
169
- ),
170
- ),
171
- Positioned(
172
- bottom: 40,
173
- right: 40,
174
- child: TextButton(
175
- onPressed: widget.onComplete,
176
- style: TextButton.styleFrom(
177
- foregroundColor: Colors.white.withValues(alpha: 0.5),
181
+ class _RotatePrompt extends StatelessWidget {
182
+ const _RotatePrompt();
183
+
184
+ @override
185
+ Widget build(BuildContext context) {
186
+ return Center(
187
+ child: Padding(
188
+ padding: const EdgeInsets.all(32),
189
+ child: Column(
190
+ mainAxisSize: MainAxisSize.min,
191
+ children: <Widget>[
192
+ const Icon(Icons.screen_rotation, color: Colors.white, size: 54),
193
+ const SizedBox(height: 18),
194
+ const Text(
195
+ 'Rotate to continue',
196
+ textAlign: TextAlign.center,
197
+ style: TextStyle(
198
+ color: Colors.white,
199
+ fontSize: 24,
200
+ fontWeight: FontWeight.w800,
201
+ ),
178
202
  ),
179
- child: const Text('Skip'),
180
- ),
203
+ const SizedBox(height: 10),
204
+ Text(
205
+ 'This intro is designed for full-screen landscape playback.',
206
+ textAlign: TextAlign.center,
207
+ style: TextStyle(
208
+ color: Colors.white.withValues(alpha: 0.7),
209
+ fontSize: 15,
210
+ height: 1.5,
211
+ ),
212
+ ),
213
+ ],
181
214
  ),
182
- ],
215
+ ),
183
216
  );
184
217
  }
185
218
  }
@@ -1,6 +1,7 @@
1
1
  import 'package:flutter/material.dart';
2
2
  import 'package:flutter_animate/flutter_animate.dart';
3
- import 'package:google_fonts/google_fonts.dart';
3
+
4
+ import 'onboarding_chrome.dart';
4
5
 
5
6
  class OnboardingWelcomeStep extends StatelessWidget {
6
7
  const OnboardingWelcomeStep({super.key, required this.onNext});
@@ -9,156 +10,37 @@ class OnboardingWelcomeStep extends StatelessWidget {
9
10
 
10
11
  @override
11
12
  Widget build(BuildContext context) {
12
- final theme = Theme.of(context);
13
- final accent = theme.colorScheme.primary;
14
-
15
- return Stack(
16
- children: <Widget>[
17
- // Abstract animated background
18
- Positioned.fill(
19
- child: CustomPaint(
20
- painter: _AbstractBackgroundPainter(accentColor: accent),
21
- ).animate(onPlay: (controller) => controller.repeat(reverse: true))
22
- .scaleXY(begin: 1.0, end: 1.1, curve: Curves.easeInOutSine, duration: 6000.ms)
23
- .shimmer(duration: 8000.ms, color: accent.withValues(alpha: 0.1)),
24
- ),
25
-
26
- // Grid pattern overlay
27
- Positioned.fill(
28
- child: Opacity(
29
- opacity: 0.05,
30
- child: CustomPaint(
31
- painter: _GridPainter(),
32
- ),
33
- ),
34
- ),
35
-
36
- SafeArea(
37
- child: Padding(
38
- padding: const EdgeInsets.all(40),
39
- child: Column(
40
- crossAxisAlignment: CrossAxisAlignment.start,
41
- mainAxisAlignment: MainAxisAlignment.center,
42
- children: <Widget>[
43
- const Spacer(flex: 3),
44
-
45
- // Welcome text
46
- Text(
47
- 'Welcome to\nNeoOS',
48
- style: GoogleFonts.spaceGrotesk(
49
- fontSize: 72,
50
- fontWeight: FontWeight.w800,
51
- height: 1.05,
52
- letterSpacing: -2.5,
53
- color: Colors.white,
54
- ),
55
- ).animate()
56
- .fadeIn(duration: 800.ms, delay: 200.ms)
57
- .slideY(begin: 0.2, end: 0, curve: Curves.easeOutCubic),
58
-
59
- const SizedBox(height: 24),
60
-
61
- Text(
62
- 'A new era of intelligent interaction.\nPrivate, powerful, and deeply integrated into your workflow.',
63
- style: TextStyle(
64
- fontSize: 20,
65
- color: Colors.white.withValues(alpha: 0.7),
66
- height: 1.5,
67
- ),
68
- ).animate()
69
- .fadeIn(duration: 800.ms, delay: 400.ms)
70
- .slideY(begin: 0.2, end: 0, curve: Curves.easeOutCubic),
71
-
72
- const Spacer(flex: 4),
73
-
74
- // Action Button
75
- Align(
76
- alignment: Alignment.bottomRight,
77
- child: Container(
78
- decoration: BoxDecoration(
79
- boxShadow: <BoxShadow>[
80
- BoxShadow(
81
- color: accent.withValues(alpha: 0.4),
82
- blurRadius: 30,
83
- offset: const Offset(0, 10),
84
- ),
85
- ],
86
- ),
87
- child: FilledButton.icon(
88
- onPressed: onNext,
89
- style: FilledButton.styleFrom(
90
- padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 20),
91
- shape: RoundedRectangleBorder(
92
- borderRadius: BorderRadius.circular(24),
93
- ),
94
- ),
95
- icon: const Icon(Icons.arrow_forward_rounded),
96
- label: const Text(
97
- 'Get Started',
98
- style: TextStyle(fontSize: 18, fontWeight: FontWeight.w700),
99
- ),
100
- ),
101
- ).animate()
102
- .fadeIn(duration: 800.ms, delay: 800.ms)
103
- .slideX(begin: 0.2, end: 0, curve: Curves.easeOutCubic),
104
- ),
105
- ],
106
- ),
13
+ return OnboardingScaffold(
14
+ step: 1,
15
+ totalSteps: 4,
16
+ eyebrow: 'WELCOME',
17
+ title: 'Welcome to\nNeoOS',
18
+ description: 'Your assistant layer for capture, context, and action.',
19
+ footer: Row(
20
+ mainAxisAlignment: MainAxisAlignment.end,
21
+ children: <Widget>[
22
+ OnboardingPrimaryButton(
23
+ label: 'Continue',
24
+ icon: Icons.arrow_forward_rounded,
25
+ onPressed: onNext,
26
+ )
27
+ .animate()
28
+ .fadeIn(duration: 600.ms, delay: 600.ms)
29
+ .slideY(begin: 0.2),
30
+ ],
31
+ ),
32
+ child: Align(
33
+ alignment: Alignment.topLeft,
34
+ child: Text(
35
+ 'Set up your workspace in a few steps and start using NeoOS immediately.',
36
+ style: TextStyle(
37
+ color: Colors.white.withValues(alpha: 0.72),
38
+ fontSize: 18,
39
+ height: 1.55,
40
+ fontWeight: FontWeight.w500,
107
41
  ),
108
- ),
109
- ],
42
+ ).animate().fadeIn(duration: 600.ms, delay: 380.ms),
43
+ ),
110
44
  );
111
45
  }
112
46
  }
113
-
114
- class _AbstractBackgroundPainter extends CustomPainter {
115
- _AbstractBackgroundPainter({required this.accentColor});
116
- final Color accentColor;
117
-
118
- @override
119
- void paint(Canvas canvas, Size size) {
120
- final paint = Paint()
121
- ..color = accentColor.withValues(alpha: 0.15)
122
- ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 100);
123
-
124
- canvas.drawCircle(
125
- Offset(size.width * 0.8, size.height * 0.2),
126
- size.width * 0.4,
127
- paint,
128
- );
129
-
130
- paint.color = const Color(0xFF6EDBFF).withValues(alpha: 0.1);
131
- canvas.drawCircle(
132
- Offset(size.width * 0.2, size.height * 0.8),
133
- size.width * 0.5,
134
- paint,
135
- );
136
- }
137
-
138
- @override
139
- bool shouldRepaint(covariant _AbstractBackgroundPainter oldDelegate) {
140
- return oldDelegate.accentColor != accentColor;
141
- }
142
- }
143
-
144
- class _GridPainter extends CustomPainter {
145
- @override
146
- void paint(Canvas canvas, Size size) {
147
- final paint = Paint()
148
- ..color = Colors.white
149
- ..strokeWidth = 1
150
- ..style = PaintingStyle.stroke;
151
-
152
- const double spacing = 40.0;
153
-
154
- for (double i = 0; i < size.width; i += spacing) {
155
- canvas.drawLine(Offset(i, 0), Offset(i, size.height), paint);
156
- }
157
- for (double i = 0; i < size.height; i += spacing) {
158
- canvas.drawLine(Offset(0, i), Offset(size.width, i), paint);
159
- }
160
- }
161
-
162
- @override
163
- bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
164
- }