neoagent 2.4.1-beta.10 → 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.
@@ -1,5 +1,8 @@
1
+ import 'dart:convert';
2
+ import 'package:flutter/foundation.dart';
1
3
  import 'package:flutter/material.dart';
2
4
  import 'package:flutter_animate/flutter_animate.dart';
5
+ import 'package:http/http.dart' as http;
3
6
  import 'package:url_launcher/url_launcher.dart';
4
7
 
5
8
  import '../../main.dart';
@@ -21,6 +24,149 @@ class OnboardingCompanionStep extends StatefulWidget {
21
24
 
22
25
  class _OnboardingCompanionStepState extends State<OnboardingCompanionStep> {
23
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
+ }
24
170
 
25
171
  Future<void> _launchUrl(String urlString) async {
26
172
  final url = Uri.parse(urlString);
@@ -29,6 +175,75 @@ class _OnboardingCompanionStepState extends State<OnboardingCompanionStep> {
29
175
  }
30
176
  }
31
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
+
32
247
  @override
33
248
  Widget build(BuildContext context) {
34
249
  final width = MediaQuery.sizeOf(context).width;
@@ -62,10 +277,14 @@ class _OnboardingCompanionStepState extends State<OnboardingCompanionStep> {
62
277
  icon: Icons.laptop_mac_rounded,
63
278
  accentColor: desktopConnected ? const Color(0xFF10A37F) : const Color(0xFF8A5CF5),
64
279
  connected: desktopConnected,
65
- buttonText: desktopConnected ? 'Desktop Connected' : 'Get Desktop App',
280
+ buttonText: desktopConnected
281
+ ? 'Desktop Connected'
282
+ : 'Download for ${_selectedDesktopPlatform.name.toUpperCase()} (${_getFileExtensionForPlatform(_selectedDesktopPlatform)})',
66
283
  onTap: () async {
67
284
  setState(() => _clickedDownloads.add('desktop'));
68
- await _launchUrl('https://github.com/NeoLabs-Systems/NeoAgent/releases');
285
+ final url = _downloadUrls[_selectedChannel]?[_selectedDesktopPlatform] ??
286
+ _getFallbackUrl(_selectedChannel, _selectedDesktopPlatform);
287
+ await _launchUrl(url);
69
288
  },
70
289
  ),
71
290
  _CompanionItemData(
@@ -78,7 +297,9 @@ class _OnboardingCompanionStepState extends State<OnboardingCompanionStep> {
78
297
  buttonText: 'Download Android APK',
79
298
  onTap: () async {
80
299
  setState(() => _clickedDownloads.add('mobile'));
81
- await _launchUrl('https://github.com/NeoLabs-Systems/NeoAgent/releases');
300
+ final url = _downloadUrls[_selectedChannel]?[TargetPlatform.android] ??
301
+ _getFallbackUrl(_selectedChannel, TargetPlatform.android);
302
+ await _launchUrl(url);
82
303
  },
83
304
  ),
84
305
  ];
@@ -103,42 +324,79 @@ class _OnboardingCompanionStepState extends State<OnboardingCompanionStep> {
103
324
  ),
104
325
  ],
105
326
  ),
106
- child: useGrid
107
- ? GridView.builder(
108
- gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
109
- crossAxisCount: columns,
110
- crossAxisSpacing: 14,
111
- mainAxisSpacing: 14,
112
- childAspectRatio: width >= 1050 ? 1.42 : 1.25,
113
- ),
114
- itemCount: items.length,
115
- itemBuilder: (context, index) {
116
- final item = items[index];
117
- return _CompanionCard(
118
- item: item,
119
- compact: true,
120
- isClicked: _clickedDownloads.contains(item.id),
121
- )
122
- .animate()
123
- .fadeIn(duration: 420.ms, delay: (180 + (index * 80)).ms)
124
- .slideY(begin: 0.16, end: 0);
125
- },
126
- )
127
- : ListView.separated(
128
- itemCount: items.length,
129
- separatorBuilder: (_, __) => const SizedBox(height: 14),
130
- itemBuilder: (context, index) {
131
- final item = items[index];
132
- return _CompanionCard(
133
- item: item,
134
- compact: false,
135
- isClicked: _clickedDownloads.contains(item.id),
136
- )
137
- .animate()
138
- .fadeIn(duration: 420.ms, delay: (180 + (index * 80)).ms)
139
- .slideY(begin: 0.16, end: 0);
140
- },
141
- ),
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
+ ),
142
400
  );
143
401
  },
144
402
  );
@@ -172,11 +430,15 @@ class _CompanionCard extends StatelessWidget {
172
430
  required this.item,
173
431
  required this.compact,
174
432
  required this.isClicked,
433
+ required this.selectedDesktopPlatform,
434
+ required this.onDesktopPlatformChanged,
175
435
  });
176
436
 
177
437
  final _CompanionItemData item;
178
438
  final bool compact;
179
439
  final bool isClicked;
440
+ final TargetPlatform selectedDesktopPlatform;
441
+ final ValueChanged<TargetPlatform> onDesktopPlatformChanged;
180
442
 
181
443
  @override
182
444
  Widget build(BuildContext context) {
@@ -231,6 +493,11 @@ class _CompanionCard extends StatelessWidget {
231
493
  ),
232
494
  ),
233
495
  const SizedBox(height: 12),
496
+ if (item.id == 'desktop' && !item.connected)
497
+ _PlatformSelector(
498
+ selectedPlatform: selectedDesktopPlatform,
499
+ onPlatformChanged: onDesktopPlatformChanged,
500
+ ),
234
501
  _DownloadButton(item: item, isClicked: isClicked),
235
502
  ],
236
503
  )
@@ -274,6 +541,11 @@ class _CompanionCard extends StatelessWidget {
274
541
  ),
275
542
  ),
276
543
  const SizedBox(height: 14),
544
+ if (item.id == 'desktop' && !item.connected)
545
+ _PlatformSelector(
546
+ selectedPlatform: selectedDesktopPlatform,
547
+ onPlatformChanged: onDesktopPlatformChanged,
548
+ ),
277
549
  _DownloadButton(item: item, isClicked: isClicked),
278
550
  ],
279
551
  ),
@@ -357,13 +629,13 @@ class _DownloadButton extends StatelessWidget {
357
629
  return Container(
358
630
  padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8),
359
631
  decoration: BoxDecoration(
360
- color: isClicked
361
- ? Colors.white.withValues(alpha: 0.08)
632
+ color: isClicked
633
+ ? Colors.white.withValues(alpha: 0.08)
362
634
  : item.accentColor.withValues(alpha: 0.12),
363
635
  borderRadius: BorderRadius.circular(12),
364
636
  border: Border.all(
365
- color: isClicked
366
- ? Colors.white.withValues(alpha: 0.2)
637
+ color: isClicked
638
+ ? Colors.white.withValues(alpha: 0.2)
367
639
  : item.accentColor.withValues(alpha: 0.25),
368
640
  ),
369
641
  ),
@@ -389,3 +661,61 @@ class _DownloadButton extends StatelessWidget {
389
661
  );
390
662
  }
391
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
+ }
@@ -100,9 +100,15 @@ class DesktopCompanionActions {
100
100
  if (bytes is! Uint8List || bytes.isEmpty) {
101
101
  return null;
102
102
  }
103
- final decoded = img.decodeImage(bytes);
104
- final width = (frame['width'] as num?)?.round() ?? decoded?.width ?? 0;
105
- final height = (frame['height'] as num?)?.round() ?? decoded?.height ?? 0;
103
+ // Prefer dimensions reported by the native bridge; only fall back to a
104
+ // pure-Dart image decode (which is slow) when the bridge omits them.
105
+ final nativeWidth = (frame['width'] as num?)?.round();
106
+ final nativeHeight = (frame['height'] as num?)?.round();
107
+ final decoded = (nativeWidth == null || nativeHeight == null)
108
+ ? img.decodeImage(bytes)
109
+ : null;
110
+ final width = nativeWidth ?? decoded?.width ?? 0;
111
+ final height = nativeHeight ?? decoded?.height ?? 0;
106
112
  final displays = _normalizeDisplays(
107
113
  frame['displays'],
108
114
  fallbackDisplayId:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "neoagent",
3
- "version": "2.4.1-beta.10",
3
+ "version": "2.4.1-beta.11",
4
4
  "description": "Proactive personal AI agent with no limits",
5
5
  "license": "AGPL-3.0-only",
6
6
  "main": "server/index.js",
@@ -1 +1 @@
1
- 523e49e35d448c4b94454f78c7beebb8
1
+ 178383462d4c7125ce032b3783b45564
@@ -37,6 +37,6 @@ _flutter.buildConfig = {"engineRevision":"4c525dac5ebe5971c5708ef73558ed8edcf4a3
37
37
 
38
38
  _flutter.loader.load({
39
39
  serviceWorkerSettings: {
40
- serviceWorkerVersion: "3099276130" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
40
+ serviceWorkerVersion: "559469558" /* Flutter's service worker is deprecated and will be removed in a future Flutter release. */
41
41
  }
42
42
  });