neoagent 2.4.0 → 2.4.1-beta.11
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.
- package/LICENSE +619 -21
- package/README.md +1 -1
- package/extensions/chrome-browser/background.mjs +19 -7
- package/extensions/chrome-browser/icons/icon128.png +0 -0
- package/extensions/chrome-browser/icons/icon16.png +0 -0
- package/extensions/chrome-browser/icons/icon48.png +0 -0
- package/extensions/chrome-browser/icons/logo.svg +12 -0
- package/extensions/chrome-browser/manifest.json +13 -2
- package/extensions/chrome-browser/popup.css +5 -0
- package/extensions/chrome-browser/popup.html +7 -5
- package/extensions/chrome-browser/popup.js +16 -7
- package/flutter_app/lib/features/onboarding/onboarding_companion_step.dart +721 -0
- package/flutter_app/lib/features/onboarding/onboarding_shell.dart +6 -0
- package/flutter_app/lib/features/onboarding/onboarding_welcome_step.dart +1 -1
- package/flutter_app/lib/main.dart +1 -0
- package/flutter_app/lib/main_controller.dart +156 -3
- package/flutter_app/lib/main_devices.dart +485 -119
- package/flutter_app/lib/main_settings.dart +289 -30
- package/flutter_app/lib/src/backend_client.dart +89 -0
- package/flutter_app/lib/src/desktop_companion_actions.dart +153 -3
- package/flutter_app/lib/src/desktop_companion_io.dart +145 -4
- package/flutter_app/lib/src/desktop_native_bridge.dart +13 -0
- package/flutter_app/lib/src/stream_renderer.dart +286 -0
- package/flutter_app/macos/Runner/AppDelegate.swift +56 -1
- package/package.json +2 -2
- package/server/guest_agent.js +19 -1
- package/server/http/routes.js +191 -0
- package/server/http/socket.js +1 -1
- package/server/index.js +4 -1
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +75438 -74005
- package/server/routes/browser.js +14 -0
- package/server/routes/browser_extension.js +21 -4
- package/server/routes/desktop.js +10 -0
- package/server/routes/settings.js +4 -0
- package/server/routes/stream.js +187 -0
- package/server/services/ai/tools.js +40 -29
- package/server/services/android/controller.js +41 -2
- package/server/services/browser/controller.js +34 -0
- package/server/services/browser/extension/manifest.js +33 -0
- package/server/services/browser/extension/provider.js +12 -6
- package/server/services/browser/extension/registry.js +188 -18
- package/server/services/desktop/gateway.js +28 -3
- package/server/services/desktop/protocol.js +34 -0
- package/server/services/desktop/provider.js +25 -0
- package/server/services/desktop/registry.js +92 -10
- package/server/services/manager.js +19 -2
- package/server/services/runtime/backends/local-vm.js +6 -0
- package/server/services/runtime/docker-vm-manager.js +26 -3
- package/server/services/runtime/manager.js +36 -5
- package/server/services/runtime/settings.js +17 -0
- package/server/services/streaming/android-stream.js +298 -0
- package/server/services/streaming/browser-stream.js +87 -0
- package/server/services/streaming/stream-hub.js +231 -0
- package/server/services/websocket.js +73 -0
|
@@ -0,0 +1,721 @@
|
|
|
1
|
+
import 'dart:convert';
|
|
2
|
+
import 'package:flutter/foundation.dart';
|
|
3
|
+
import 'package:flutter/material.dart';
|
|
4
|
+
import 'package:flutter_animate/flutter_animate.dart';
|
|
5
|
+
import 'package:http/http.dart' as http;
|
|
6
|
+
import 'package:url_launcher/url_launcher.dart';
|
|
7
|
+
|
|
8
|
+
import '../../main.dart';
|
|
9
|
+
import 'onboarding_chrome.dart';
|
|
10
|
+
|
|
11
|
+
class OnboardingCompanionStep extends StatefulWidget {
|
|
12
|
+
const OnboardingCompanionStep({
|
|
13
|
+
super.key,
|
|
14
|
+
required this.onNext,
|
|
15
|
+
required this.controller,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
final VoidCallback onNext;
|
|
19
|
+
final NeoAgentController controller;
|
|
20
|
+
|
|
21
|
+
@override
|
|
22
|
+
State<OnboardingCompanionStep> createState() => _OnboardingCompanionStepState();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
class _OnboardingCompanionStepState extends State<OnboardingCompanionStep> {
|
|
26
|
+
final Set<String> _clickedDownloads = <String>{};
|
|
27
|
+
String _selectedChannel = 'stable';
|
|
28
|
+
TargetPlatform _selectedDesktopPlatform = TargetPlatform.macOS;
|
|
29
|
+
|
|
30
|
+
bool _isLoadingReleases = false;
|
|
31
|
+
Map<String, Map<TargetPlatform, String>> _downloadUrls = {
|
|
32
|
+
'stable': <TargetPlatform, String>{},
|
|
33
|
+
'beta': <TargetPlatform, String>{},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
@override
|
|
37
|
+
void initState() {
|
|
38
|
+
super.initState();
|
|
39
|
+
switch (defaultTargetPlatform) {
|
|
40
|
+
case TargetPlatform.windows:
|
|
41
|
+
_selectedDesktopPlatform = TargetPlatform.windows;
|
|
42
|
+
break;
|
|
43
|
+
case TargetPlatform.linux:
|
|
44
|
+
_selectedDesktopPlatform = TargetPlatform.linux;
|
|
45
|
+
break;
|
|
46
|
+
default:
|
|
47
|
+
_selectedDesktopPlatform = TargetPlatform.macOS;
|
|
48
|
+
}
|
|
49
|
+
_fetchReleases();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
Future<void> _fetchReleases() async {
|
|
53
|
+
if (mounted) {
|
|
54
|
+
setState(() {
|
|
55
|
+
_isLoadingReleases = true;
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
final response = await http.get(
|
|
60
|
+
Uri.parse('https://api.github.com/repos/NeoLabs-Systems/NeoAgent/releases'),
|
|
61
|
+
headers: const <String, String>{
|
|
62
|
+
'Accept': 'application/vnd.github.v3+json',
|
|
63
|
+
},
|
|
64
|
+
).timeout(const Duration(seconds: 8));
|
|
65
|
+
|
|
66
|
+
if (response.statusCode == 200) {
|
|
67
|
+
final List<dynamic> releasesJson = jsonDecode(response.body) as List<dynamic>;
|
|
68
|
+
final urls = _parseReleases(releasesJson);
|
|
69
|
+
if (mounted) {
|
|
70
|
+
setState(() {
|
|
71
|
+
_downloadUrls = urls;
|
|
72
|
+
_isLoadingReleases = false;
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
throw Exception('Failed to load releases: status ${response.statusCode}');
|
|
77
|
+
}
|
|
78
|
+
} catch (e) {
|
|
79
|
+
debugPrint('Error fetching releases: $e');
|
|
80
|
+
if (mounted) {
|
|
81
|
+
setState(() {
|
|
82
|
+
_isLoadingReleases = false;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
Map<String, Map<TargetPlatform, String>> _parseReleases(List<dynamic> releases) {
|
|
89
|
+
final Map<String, Map<TargetPlatform, String>> result = {
|
|
90
|
+
'stable': <TargetPlatform, String>{},
|
|
91
|
+
'beta': <TargetPlatform, String>{},
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
dynamic latestStable;
|
|
95
|
+
dynamic latestBeta;
|
|
96
|
+
|
|
97
|
+
for (final release in releases) {
|
|
98
|
+
if (release is! Map<String, dynamic>) continue;
|
|
99
|
+
final bool isPrerelease = release['prerelease'] == true;
|
|
100
|
+
final String tagName = (release['tag_name'] ?? '').toString();
|
|
101
|
+
final bool isBetaTag = tagName.contains('-beta');
|
|
102
|
+
|
|
103
|
+
if ((isPrerelease || isBetaTag) && latestBeta == null) {
|
|
104
|
+
latestBeta = release;
|
|
105
|
+
} else if (!isPrerelease && !isBetaTag && latestStable == null) {
|
|
106
|
+
latestStable = release;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (latestStable != null && latestBeta != null) {
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
latestStable ??= latestBeta;
|
|
115
|
+
latestBeta ??= latestStable;
|
|
116
|
+
|
|
117
|
+
if (latestStable != null) {
|
|
118
|
+
result['stable'] = _extractUrlsFromRelease(latestStable);
|
|
119
|
+
}
|
|
120
|
+
if (latestBeta != null) {
|
|
121
|
+
result['beta'] = _extractUrlsFromRelease(latestBeta);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return result;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
Map<TargetPlatform, String> _extractUrlsFromRelease(dynamic release) {
|
|
128
|
+
final Map<TargetPlatform, String> urls = <TargetPlatform, String>{};
|
|
129
|
+
if (release is! Map<String, dynamic>) return urls;
|
|
130
|
+
final List<dynamic> assets = release['assets'] as List<dynamic>? ?? const <dynamic>[];
|
|
131
|
+
|
|
132
|
+
for (final asset in assets) {
|
|
133
|
+
if (asset is! Map<String, dynamic>) continue;
|
|
134
|
+
final String name = (asset['name'] ?? '').toString().toLowerCase();
|
|
135
|
+
final String downloadUrl = (asset['browser_download_url'] ?? '').toString();
|
|
136
|
+
|
|
137
|
+
if (name.endsWith('.dmg')) {
|
|
138
|
+
urls[TargetPlatform.macOS] = downloadUrl;
|
|
139
|
+
} else if (name.endsWith('.exe') && name.contains('setup')) {
|
|
140
|
+
urls[TargetPlatform.windows] = downloadUrl;
|
|
141
|
+
} else if (name.endsWith('.exe')) {
|
|
142
|
+
urls.putIfAbsent(TargetPlatform.windows, () => downloadUrl);
|
|
143
|
+
} else if (name.endsWith('.deb')) {
|
|
144
|
+
urls[TargetPlatform.linux] = downloadUrl;
|
|
145
|
+
} else if (name.endsWith('.apk') && !name.contains('-launcher')) {
|
|
146
|
+
urls[TargetPlatform.android] = downloadUrl;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return urls;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
String _getFallbackUrl(String channel, TargetPlatform platform) {
|
|
153
|
+
final String tag = channel == 'beta' ? 'v2.4.1-beta.9' : 'v2.4.0';
|
|
154
|
+
final String ver = channel == 'beta' ? '2.4.1-beta.9' : '2.4.0';
|
|
155
|
+
final String debVer = ver.replaceAll('-', '~');
|
|
156
|
+
|
|
157
|
+
switch (platform) {
|
|
158
|
+
case TargetPlatform.macOS:
|
|
159
|
+
return 'https://github.com/NeoLabs-Systems/NeoAgent/releases/download/$tag/neoagent-macos-$ver.dmg';
|
|
160
|
+
case TargetPlatform.windows:
|
|
161
|
+
return 'https://github.com/NeoLabs-Systems/NeoAgent/releases/download/$tag/neoagent-windows-x64-setup-$ver.exe';
|
|
162
|
+
case TargetPlatform.linux:
|
|
163
|
+
return 'https://github.com/NeoLabs-Systems/NeoAgent/releases/download/$tag/neoagent-linux-amd64-$debVer.deb';
|
|
164
|
+
case TargetPlatform.android:
|
|
165
|
+
return 'https://github.com/NeoLabs-Systems/NeoAgent/releases/download/$tag/neoagent-android-$ver.apk';
|
|
166
|
+
default:
|
|
167
|
+
return 'https://github.com/NeoLabs-Systems/NeoAgent/releases/tag/$tag';
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
Future<void> _launchUrl(String urlString) async {
|
|
172
|
+
final url = Uri.parse(urlString);
|
|
173
|
+
if (await canLaunchUrl(url)) {
|
|
174
|
+
await launchUrl(url, mode: LaunchMode.externalApplication);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
String _getFileExtensionForPlatform(TargetPlatform platform) {
|
|
179
|
+
switch (platform) {
|
|
180
|
+
case TargetPlatform.macOS:
|
|
181
|
+
return '.dmg';
|
|
182
|
+
case TargetPlatform.windows:
|
|
183
|
+
return '.exe';
|
|
184
|
+
case TargetPlatform.linux:
|
|
185
|
+
return '.deb';
|
|
186
|
+
default:
|
|
187
|
+
return '';
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
Widget _buildChannelSelector() {
|
|
192
|
+
return Container(
|
|
193
|
+
padding: const EdgeInsets.all(4),
|
|
194
|
+
decoration: BoxDecoration(
|
|
195
|
+
color: Colors.white.withValues(alpha: 0.05),
|
|
196
|
+
borderRadius: BorderRadius.circular(16),
|
|
197
|
+
border: Border.all(color: Colors.white.withValues(alpha: 0.08)),
|
|
198
|
+
),
|
|
199
|
+
child: Row(
|
|
200
|
+
mainAxisSize: MainAxisSize.min,
|
|
201
|
+
children: <Widget>[
|
|
202
|
+
_buildChannelButton('stable', 'Stable Release'),
|
|
203
|
+
_buildChannelButton('beta', 'Beta Release'),
|
|
204
|
+
],
|
|
205
|
+
),
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
Widget _buildChannelButton(String channel, String label) {
|
|
210
|
+
final isSelected = _selectedChannel == channel;
|
|
211
|
+
return Material(
|
|
212
|
+
color: Colors.transparent,
|
|
213
|
+
child: InkWell(
|
|
214
|
+
onTap: () {
|
|
215
|
+
setState(() {
|
|
216
|
+
_selectedChannel = channel;
|
|
217
|
+
});
|
|
218
|
+
},
|
|
219
|
+
borderRadius: BorderRadius.circular(12),
|
|
220
|
+
child: AnimatedContainer(
|
|
221
|
+
duration: const Duration(milliseconds: 200),
|
|
222
|
+
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
|
223
|
+
decoration: BoxDecoration(
|
|
224
|
+
color: isSelected
|
|
225
|
+
? Theme.of(context).colorScheme.primary.withValues(alpha: 0.16)
|
|
226
|
+
: Colors.transparent,
|
|
227
|
+
borderRadius: BorderRadius.circular(12),
|
|
228
|
+
border: Border.all(
|
|
229
|
+
color: isSelected
|
|
230
|
+
? Theme.of(context).colorScheme.primary.withValues(alpha: 0.6)
|
|
231
|
+
: Colors.transparent,
|
|
232
|
+
),
|
|
233
|
+
),
|
|
234
|
+
child: Text(
|
|
235
|
+
label,
|
|
236
|
+
style: TextStyle(
|
|
237
|
+
color: isSelected ? Colors.white : Colors.white.withValues(alpha: 0.6),
|
|
238
|
+
fontSize: 14,
|
|
239
|
+
fontWeight: FontWeight.w700,
|
|
240
|
+
),
|
|
241
|
+
),
|
|
242
|
+
),
|
|
243
|
+
),
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
@override
|
|
248
|
+
Widget build(BuildContext context) {
|
|
249
|
+
final width = MediaQuery.sizeOf(context).width;
|
|
250
|
+
final useGrid = width >= 700;
|
|
251
|
+
final columns = width >= 1050 ? 3 : (useGrid ? 2 : 1);
|
|
252
|
+
|
|
253
|
+
return AnimatedBuilder(
|
|
254
|
+
animation: widget.controller,
|
|
255
|
+
builder: (context, _) {
|
|
256
|
+
final extConnected = widget.controller.browserExtensionConnected;
|
|
257
|
+
final desktopConnected = widget.controller.desktopCompanionConnected;
|
|
258
|
+
|
|
259
|
+
final items = <_CompanionItemData>[
|
|
260
|
+
_CompanionItemData(
|
|
261
|
+
id: 'extension',
|
|
262
|
+
title: 'Chrome Extension',
|
|
263
|
+
subtitle: 'Automate browser tasks and capture web page context directly.',
|
|
264
|
+
icon: Icons.extension_rounded,
|
|
265
|
+
accentColor: extConnected ? const Color(0xFF10A37F) : const Color(0xFF4285F4),
|
|
266
|
+
connected: extConnected,
|
|
267
|
+
buttonText: extConnected ? 'Extension Connected' : 'Download Extension',
|
|
268
|
+
onTap: () async {
|
|
269
|
+
setState(() => _clickedDownloads.add('extension'));
|
|
270
|
+
await widget.controller.downloadBrowserExtension();
|
|
271
|
+
},
|
|
272
|
+
),
|
|
273
|
+
_CompanionItemData(
|
|
274
|
+
id: 'desktop',
|
|
275
|
+
title: 'Desktop App',
|
|
276
|
+
subtitle: 'Enable native command run, system capture, and global controls.',
|
|
277
|
+
icon: Icons.laptop_mac_rounded,
|
|
278
|
+
accentColor: desktopConnected ? const Color(0xFF10A37F) : const Color(0xFF8A5CF5),
|
|
279
|
+
connected: desktopConnected,
|
|
280
|
+
buttonText: desktopConnected
|
|
281
|
+
? 'Desktop Connected'
|
|
282
|
+
: 'Download for ${_selectedDesktopPlatform.name.toUpperCase()} (${_getFileExtensionForPlatform(_selectedDesktopPlatform)})',
|
|
283
|
+
onTap: () async {
|
|
284
|
+
setState(() => _clickedDownloads.add('desktop'));
|
|
285
|
+
final url = _downloadUrls[_selectedChannel]?[_selectedDesktopPlatform] ??
|
|
286
|
+
_getFallbackUrl(_selectedChannel, _selectedDesktopPlatform);
|
|
287
|
+
await _launchUrl(url);
|
|
288
|
+
},
|
|
289
|
+
),
|
|
290
|
+
_CompanionItemData(
|
|
291
|
+
id: 'mobile',
|
|
292
|
+
title: 'Mobile Companion',
|
|
293
|
+
subtitle: 'Sync notifications, phone calls, and health connect metrics.',
|
|
294
|
+
icon: Icons.phone_iphone_rounded,
|
|
295
|
+
accentColor: const Color(0xFFF5A623),
|
|
296
|
+
connected: false,
|
|
297
|
+
buttonText: 'Download Android APK',
|
|
298
|
+
onTap: () async {
|
|
299
|
+
setState(() => _clickedDownloads.add('mobile'));
|
|
300
|
+
final url = _downloadUrls[_selectedChannel]?[TargetPlatform.android] ??
|
|
301
|
+
_getFallbackUrl(_selectedChannel, TargetPlatform.android);
|
|
302
|
+
await _launchUrl(url);
|
|
303
|
+
},
|
|
304
|
+
),
|
|
305
|
+
];
|
|
306
|
+
|
|
307
|
+
return OnboardingScaffold(
|
|
308
|
+
step: 1,
|
|
309
|
+
totalSteps: 4,
|
|
310
|
+
eyebrow: 'INTEGRATION',
|
|
311
|
+
title: 'Connect your\ndevices & apps.',
|
|
312
|
+
description: 'NeoOS works best when integrated with your desktop, browser, and mobile devices.',
|
|
313
|
+
footer: Row(
|
|
314
|
+
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
315
|
+
children: <Widget>[
|
|
316
|
+
OnboardingGhostButton(
|
|
317
|
+
label: 'Skip integration',
|
|
318
|
+
onPressed: widget.onNext,
|
|
319
|
+
),
|
|
320
|
+
OnboardingPrimaryButton(
|
|
321
|
+
label: 'Continue',
|
|
322
|
+
icon: Icons.arrow_forward_rounded,
|
|
323
|
+
onPressed: widget.onNext,
|
|
324
|
+
),
|
|
325
|
+
],
|
|
326
|
+
),
|
|
327
|
+
child: Column(
|
|
328
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
329
|
+
children: <Widget>[
|
|
330
|
+
Row(
|
|
331
|
+
mainAxisAlignment: MainAxisAlignment.center,
|
|
332
|
+
children: [
|
|
333
|
+
_buildChannelSelector(),
|
|
334
|
+
if (_isLoadingReleases) ...[
|
|
335
|
+
const SizedBox(width: 12),
|
|
336
|
+
const SizedBox(
|
|
337
|
+
width: 14,
|
|
338
|
+
height: 14,
|
|
339
|
+
child: CircularProgressIndicator(
|
|
340
|
+
strokeWidth: 2,
|
|
341
|
+
valueColor: AlwaysStoppedAnimation<Color>(Colors.white54),
|
|
342
|
+
),
|
|
343
|
+
),
|
|
344
|
+
],
|
|
345
|
+
],
|
|
346
|
+
),
|
|
347
|
+
const SizedBox(height: 20),
|
|
348
|
+
Expanded(
|
|
349
|
+
child: useGrid
|
|
350
|
+
? GridView.builder(
|
|
351
|
+
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
|
352
|
+
crossAxisCount: columns,
|
|
353
|
+
crossAxisSpacing: 14,
|
|
354
|
+
mainAxisSpacing: 14,
|
|
355
|
+
childAspectRatio: width >= 1050 ? 1.42 : 1.25,
|
|
356
|
+
),
|
|
357
|
+
itemCount: items.length,
|
|
358
|
+
itemBuilder: (context, index) {
|
|
359
|
+
final item = items[index];
|
|
360
|
+
return _CompanionCard(
|
|
361
|
+
item: item,
|
|
362
|
+
compact: true,
|
|
363
|
+
isClicked: _clickedDownloads.contains(item.id),
|
|
364
|
+
selectedDesktopPlatform: _selectedDesktopPlatform,
|
|
365
|
+
onDesktopPlatformChanged: (platform) {
|
|
366
|
+
setState(() {
|
|
367
|
+
_selectedDesktopPlatform = platform;
|
|
368
|
+
});
|
|
369
|
+
},
|
|
370
|
+
)
|
|
371
|
+
.animate()
|
|
372
|
+
.fadeIn(duration: 420.ms, delay: (180 + (index * 80)).ms)
|
|
373
|
+
.slideY(begin: 0.16, end: 0);
|
|
374
|
+
},
|
|
375
|
+
)
|
|
376
|
+
: ListView.separated(
|
|
377
|
+
itemCount: items.length,
|
|
378
|
+
separatorBuilder: (_, __) => const SizedBox(height: 14),
|
|
379
|
+
itemBuilder: (context, index) {
|
|
380
|
+
final item = items[index];
|
|
381
|
+
return _CompanionCard(
|
|
382
|
+
item: item,
|
|
383
|
+
compact: false,
|
|
384
|
+
isClicked: _clickedDownloads.contains(item.id),
|
|
385
|
+
selectedDesktopPlatform: _selectedDesktopPlatform,
|
|
386
|
+
onDesktopPlatformChanged: (platform) {
|
|
387
|
+
setState(() {
|
|
388
|
+
_selectedDesktopPlatform = platform;
|
|
389
|
+
});
|
|
390
|
+
},
|
|
391
|
+
)
|
|
392
|
+
.animate()
|
|
393
|
+
.fadeIn(duration: 420.ms, delay: (180 + (index * 80)).ms)
|
|
394
|
+
.slideY(begin: 0.16, end: 0);
|
|
395
|
+
},
|
|
396
|
+
),
|
|
397
|
+
),
|
|
398
|
+
],
|
|
399
|
+
),
|
|
400
|
+
);
|
|
401
|
+
},
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
class _CompanionItemData {
|
|
407
|
+
const _CompanionItemData({
|
|
408
|
+
required this.id,
|
|
409
|
+
required this.title,
|
|
410
|
+
required this.subtitle,
|
|
411
|
+
required this.icon,
|
|
412
|
+
required this.accentColor,
|
|
413
|
+
required this.connected,
|
|
414
|
+
required this.buttonText,
|
|
415
|
+
required this.onTap,
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
final String id;
|
|
419
|
+
final String title;
|
|
420
|
+
final String subtitle;
|
|
421
|
+
final IconData icon;
|
|
422
|
+
final Color accentColor;
|
|
423
|
+
final bool connected;
|
|
424
|
+
final String buttonText;
|
|
425
|
+
final VoidCallback onTap;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
class _CompanionCard extends StatelessWidget {
|
|
429
|
+
const _CompanionCard({
|
|
430
|
+
required this.item,
|
|
431
|
+
required this.compact,
|
|
432
|
+
required this.isClicked,
|
|
433
|
+
required this.selectedDesktopPlatform,
|
|
434
|
+
required this.onDesktopPlatformChanged,
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
final _CompanionItemData item;
|
|
438
|
+
final bool compact;
|
|
439
|
+
final bool isClicked;
|
|
440
|
+
final TargetPlatform selectedDesktopPlatform;
|
|
441
|
+
final ValueChanged<TargetPlatform> onDesktopPlatformChanged;
|
|
442
|
+
|
|
443
|
+
@override
|
|
444
|
+
Widget build(BuildContext context) {
|
|
445
|
+
final shellSize = compact ? 48.0 : 58.0;
|
|
446
|
+
final iconSize = compact ? 24.0 : 30.0;
|
|
447
|
+
|
|
448
|
+
final cardContent = compact
|
|
449
|
+
? Column(
|
|
450
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
451
|
+
children: <Widget>[
|
|
452
|
+
Row(
|
|
453
|
+
children: <Widget>[
|
|
454
|
+
Container(
|
|
455
|
+
width: shellSize,
|
|
456
|
+
height: shellSize,
|
|
457
|
+
decoration: BoxDecoration(
|
|
458
|
+
color: item.accentColor.withValues(alpha: 0.18),
|
|
459
|
+
borderRadius: BorderRadius.circular(16),
|
|
460
|
+
),
|
|
461
|
+
child: Icon(
|
|
462
|
+
item.icon,
|
|
463
|
+
color: item.accentColor,
|
|
464
|
+
size: iconSize,
|
|
465
|
+
),
|
|
466
|
+
),
|
|
467
|
+
const Spacer(),
|
|
468
|
+
_StatusIndicator(connected: item.connected, color: item.accentColor),
|
|
469
|
+
],
|
|
470
|
+
),
|
|
471
|
+
const SizedBox(height: 14),
|
|
472
|
+
Text(
|
|
473
|
+
item.title,
|
|
474
|
+
maxLines: 1,
|
|
475
|
+
overflow: TextOverflow.ellipsis,
|
|
476
|
+
style: const TextStyle(
|
|
477
|
+
color: Colors.white,
|
|
478
|
+
fontSize: 18,
|
|
479
|
+
fontWeight: FontWeight.w800,
|
|
480
|
+
),
|
|
481
|
+
),
|
|
482
|
+
const SizedBox(height: 6),
|
|
483
|
+
Expanded(
|
|
484
|
+
child: Text(
|
|
485
|
+
item.subtitle,
|
|
486
|
+
maxLines: 3,
|
|
487
|
+
overflow: TextOverflow.ellipsis,
|
|
488
|
+
style: TextStyle(
|
|
489
|
+
color: Colors.white.withValues(alpha: 0.68),
|
|
490
|
+
fontSize: 13,
|
|
491
|
+
height: 1.35,
|
|
492
|
+
),
|
|
493
|
+
),
|
|
494
|
+
),
|
|
495
|
+
const SizedBox(height: 12),
|
|
496
|
+
if (item.id == 'desktop' && !item.connected)
|
|
497
|
+
_PlatformSelector(
|
|
498
|
+
selectedPlatform: selectedDesktopPlatform,
|
|
499
|
+
onPlatformChanged: onDesktopPlatformChanged,
|
|
500
|
+
),
|
|
501
|
+
_DownloadButton(item: item, isClicked: isClicked),
|
|
502
|
+
],
|
|
503
|
+
)
|
|
504
|
+
: Row(
|
|
505
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
506
|
+
children: <Widget>[
|
|
507
|
+
Container(
|
|
508
|
+
width: shellSize,
|
|
509
|
+
height: shellSize,
|
|
510
|
+
decoration: BoxDecoration(
|
|
511
|
+
color: item.accentColor.withValues(alpha: 0.18),
|
|
512
|
+
borderRadius: BorderRadius.circular(18),
|
|
513
|
+
),
|
|
514
|
+
child: Icon(
|
|
515
|
+
item.icon,
|
|
516
|
+
color: item.accentColor,
|
|
517
|
+
size: iconSize,
|
|
518
|
+
),
|
|
519
|
+
),
|
|
520
|
+
const SizedBox(width: 18),
|
|
521
|
+
Expanded(
|
|
522
|
+
child: Column(
|
|
523
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
524
|
+
mainAxisSize: MainAxisSize.min,
|
|
525
|
+
children: <Widget>[
|
|
526
|
+
Text(
|
|
527
|
+
item.title,
|
|
528
|
+
style: const TextStyle(
|
|
529
|
+
color: Colors.white,
|
|
530
|
+
fontSize: 20,
|
|
531
|
+
fontWeight: FontWeight.w800,
|
|
532
|
+
),
|
|
533
|
+
),
|
|
534
|
+
const SizedBox(height: 5),
|
|
535
|
+
Text(
|
|
536
|
+
item.subtitle,
|
|
537
|
+
style: TextStyle(
|
|
538
|
+
color: Colors.white.withValues(alpha: 0.68),
|
|
539
|
+
fontSize: 14,
|
|
540
|
+
height: 1.45,
|
|
541
|
+
),
|
|
542
|
+
),
|
|
543
|
+
const SizedBox(height: 14),
|
|
544
|
+
if (item.id == 'desktop' && !item.connected)
|
|
545
|
+
_PlatformSelector(
|
|
546
|
+
selectedPlatform: selectedDesktopPlatform,
|
|
547
|
+
onPlatformChanged: onDesktopPlatformChanged,
|
|
548
|
+
),
|
|
549
|
+
_DownloadButton(item: item, isClicked: isClicked),
|
|
550
|
+
],
|
|
551
|
+
),
|
|
552
|
+
),
|
|
553
|
+
const SizedBox(width: 12),
|
|
554
|
+
_StatusIndicator(connected: item.connected, color: item.accentColor),
|
|
555
|
+
],
|
|
556
|
+
);
|
|
557
|
+
|
|
558
|
+
return OnboardingOptionCard(
|
|
559
|
+
selected: item.connected,
|
|
560
|
+
accent: item.accentColor,
|
|
561
|
+
compact: compact,
|
|
562
|
+
onTap: item.onTap,
|
|
563
|
+
child: cardContent,
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
class _StatusIndicator extends StatelessWidget {
|
|
569
|
+
const _StatusIndicator({required this.connected, required this.color});
|
|
570
|
+
|
|
571
|
+
final bool connected;
|
|
572
|
+
final Color color;
|
|
573
|
+
|
|
574
|
+
@override
|
|
575
|
+
Widget build(BuildContext context) {
|
|
576
|
+
return AnimatedSwitcher(
|
|
577
|
+
duration: const Duration(milliseconds: 220),
|
|
578
|
+
child: connected
|
|
579
|
+
? Icon(
|
|
580
|
+
Icons.check_circle_rounded,
|
|
581
|
+
key: const ValueKey<String>('connected'),
|
|
582
|
+
color: color,
|
|
583
|
+
size: 28,
|
|
584
|
+
)
|
|
585
|
+
: Icon(
|
|
586
|
+
Icons.arrow_circle_down_rounded,
|
|
587
|
+
key: const ValueKey<String>('downloadable'),
|
|
588
|
+
color: Colors.white.withValues(alpha: 0.26),
|
|
589
|
+
size: 28,
|
|
590
|
+
),
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
class _DownloadButton extends StatelessWidget {
|
|
596
|
+
const _DownloadButton({required this.item, required this.isClicked});
|
|
597
|
+
|
|
598
|
+
final _CompanionItemData item;
|
|
599
|
+
final bool isClicked;
|
|
600
|
+
|
|
601
|
+
@override
|
|
602
|
+
Widget build(BuildContext context) {
|
|
603
|
+
if (item.connected) {
|
|
604
|
+
return Container(
|
|
605
|
+
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
|
606
|
+
decoration: BoxDecoration(
|
|
607
|
+
color: item.accentColor.withValues(alpha: 0.16),
|
|
608
|
+
borderRadius: BorderRadius.circular(12),
|
|
609
|
+
border: Border.all(color: item.accentColor.withValues(alpha: 0.3)),
|
|
610
|
+
),
|
|
611
|
+
child: Row(
|
|
612
|
+
mainAxisSize: MainAxisSize.min,
|
|
613
|
+
children: <Widget>[
|
|
614
|
+
Icon(Icons.done_all_rounded, size: 14, color: item.accentColor),
|
|
615
|
+
const SizedBox(width: 6),
|
|
616
|
+
Text(
|
|
617
|
+
'Connected',
|
|
618
|
+
style: TextStyle(
|
|
619
|
+
color: item.accentColor,
|
|
620
|
+
fontSize: 12,
|
|
621
|
+
fontWeight: FontWeight.w700,
|
|
622
|
+
),
|
|
623
|
+
),
|
|
624
|
+
],
|
|
625
|
+
),
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
return Container(
|
|
630
|
+
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
|
|
631
|
+
decoration: BoxDecoration(
|
|
632
|
+
color: isClicked
|
|
633
|
+
? Colors.white.withValues(alpha: 0.08)
|
|
634
|
+
: item.accentColor.withValues(alpha: 0.12),
|
|
635
|
+
borderRadius: BorderRadius.circular(12),
|
|
636
|
+
border: Border.all(
|
|
637
|
+
color: isClicked
|
|
638
|
+
? Colors.white.withValues(alpha: 0.2)
|
|
639
|
+
: item.accentColor.withValues(alpha: 0.25),
|
|
640
|
+
),
|
|
641
|
+
),
|
|
642
|
+
child: Row(
|
|
643
|
+
mainAxisSize: MainAxisSize.min,
|
|
644
|
+
children: <Widget>[
|
|
645
|
+
Icon(
|
|
646
|
+
isClicked ? Icons.hourglass_empty_rounded : Icons.open_in_new_rounded,
|
|
647
|
+
size: 14,
|
|
648
|
+
color: isClicked ? Colors.white70 : item.accentColor,
|
|
649
|
+
),
|
|
650
|
+
const SizedBox(width: 6),
|
|
651
|
+
Text(
|
|
652
|
+
isClicked ? 'Waiting for pairing...' : item.buttonText,
|
|
653
|
+
style: TextStyle(
|
|
654
|
+
color: isClicked ? Colors.white70 : Colors.white,
|
|
655
|
+
fontSize: 12,
|
|
656
|
+
fontWeight: FontWeight.w700,
|
|
657
|
+
),
|
|
658
|
+
),
|
|
659
|
+
],
|
|
660
|
+
),
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
class _PlatformSelector extends StatelessWidget {
|
|
666
|
+
const _PlatformSelector({
|
|
667
|
+
required this.selectedPlatform,
|
|
668
|
+
required this.onPlatformChanged,
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
final TargetPlatform selectedPlatform;
|
|
672
|
+
final ValueChanged<TargetPlatform> onPlatformChanged;
|
|
673
|
+
|
|
674
|
+
@override
|
|
675
|
+
Widget build(BuildContext context) {
|
|
676
|
+
return Container(
|
|
677
|
+
margin: const EdgeInsets.only(bottom: 12),
|
|
678
|
+
padding: const EdgeInsets.all(2),
|
|
679
|
+
decoration: BoxDecoration(
|
|
680
|
+
color: Colors.white.withValues(alpha: 0.04),
|
|
681
|
+
borderRadius: BorderRadius.circular(10),
|
|
682
|
+
border: Border.all(color: Colors.white.withValues(alpha: 0.06)),
|
|
683
|
+
),
|
|
684
|
+
child: Row(
|
|
685
|
+
mainAxisSize: MainAxisSize.min,
|
|
686
|
+
children: <Widget>[
|
|
687
|
+
_buildTab(TargetPlatform.macOS, 'macOS'),
|
|
688
|
+
_buildTab(TargetPlatform.windows, 'Windows'),
|
|
689
|
+
_buildTab(TargetPlatform.linux, 'Linux'),
|
|
690
|
+
],
|
|
691
|
+
),
|
|
692
|
+
);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
Widget _buildTab(TargetPlatform platform, String label) {
|
|
696
|
+
final isSelected = selectedPlatform == platform;
|
|
697
|
+
return Material(
|
|
698
|
+
color: Colors.transparent,
|
|
699
|
+
child: InkWell(
|
|
700
|
+
onTap: () => onPlatformChanged(platform),
|
|
701
|
+
borderRadius: BorderRadius.circular(8),
|
|
702
|
+
child: AnimatedContainer(
|
|
703
|
+
duration: const Duration(milliseconds: 180),
|
|
704
|
+
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
|
705
|
+
decoration: BoxDecoration(
|
|
706
|
+
color: isSelected ? Colors.white.withValues(alpha: 0.08) : Colors.transparent,
|
|
707
|
+
borderRadius: BorderRadius.circular(8),
|
|
708
|
+
),
|
|
709
|
+
child: Text(
|
|
710
|
+
label,
|
|
711
|
+
style: TextStyle(
|
|
712
|
+
color: isSelected ? Colors.white : Colors.white.withValues(alpha: 0.5),
|
|
713
|
+
fontSize: 11,
|
|
714
|
+
fontWeight: FontWeight.w700,
|
|
715
|
+
),
|
|
716
|
+
),
|
|
717
|
+
),
|
|
718
|
+
),
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
}
|