neoagent 2.4.1-beta.10 → 2.4.1-beta.12
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/flutter_app/lib/features/onboarding/onboarding_companion_step.dart +373 -43
- package/flutter_app/lib/main_devices.dart +89 -2
- package/flutter_app/lib/src/android_apk_drop_zone.dart +24 -0
- package/flutter_app/lib/src/android_apk_drop_zone_stub.dart +13 -0
- package/flutter_app/lib/src/android_apk_drop_zone_web.dart +217 -0
- package/flutter_app/lib/src/desktop_companion_actions.dart +9 -3
- package/lib/manager.js +131 -2
- package/package.json +1 -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 +56797 -56605
- package/server/services/ai/engine.js +148 -19
- package/server/services/ai/loopPolicy.js +11 -0
- package/server/services/ai/models.js +15 -0
- package/server/services/ai/providers/grokOauth.js +141 -0
- package/server/services/ai/settings.js +10 -0
- package/server/services/ai/taskAnalysis.js +56 -0
- package/server/services/ai/tools.js +3 -3
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
)
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
+
}
|
|
@@ -706,8 +706,8 @@ class _DevicesPanelState extends State<DevicesPanel> {
|
|
|
706
706
|
onAction: _runQuickAction,
|
|
707
707
|
),
|
|
708
708
|
if (kIsWeb) ...<Widget>[
|
|
709
|
-
const SizedBox(height:
|
|
710
|
-
|
|
709
|
+
const SizedBox(height: 12),
|
|
710
|
+
_AndroidActionsBox(
|
|
711
711
|
enabled: _androidOnline,
|
|
712
712
|
busy: _isCurrentSurfaceBusy,
|
|
713
713
|
onInstall: ({required filename, required bytes}) {
|
|
@@ -1264,6 +1264,77 @@ class _AndroidNavDock extends StatelessWidget {
|
|
|
1264
1264
|
}
|
|
1265
1265
|
}
|
|
1266
1266
|
|
|
1267
|
+
/// Tiny pill shown in the top-right corner of the preview to indicate no audio.
|
|
1268
|
+
class _MutedBadge extends StatelessWidget {
|
|
1269
|
+
const _MutedBadge();
|
|
1270
|
+
|
|
1271
|
+
@override
|
|
1272
|
+
Widget build(BuildContext context) {
|
|
1273
|
+
return Container(
|
|
1274
|
+
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 4),
|
|
1275
|
+
decoration: BoxDecoration(
|
|
1276
|
+
color: Colors.black54,
|
|
1277
|
+
borderRadius: BorderRadius.circular(8),
|
|
1278
|
+
),
|
|
1279
|
+
child: const Icon(Icons.volume_off_rounded, size: 11, color: Colors.white),
|
|
1280
|
+
);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
/// Compact expandable actions box shown beneath the Android nav dock.
|
|
1285
|
+
/// Starts with APK install; more actions can be added as tiles.
|
|
1286
|
+
class _AndroidActionsBox extends StatelessWidget {
|
|
1287
|
+
const _AndroidActionsBox({
|
|
1288
|
+
required this.enabled,
|
|
1289
|
+
required this.busy,
|
|
1290
|
+
required this.onInstall,
|
|
1291
|
+
});
|
|
1292
|
+
|
|
1293
|
+
final bool enabled;
|
|
1294
|
+
final bool busy;
|
|
1295
|
+
final AndroidApkInstallCallback onInstall;
|
|
1296
|
+
|
|
1297
|
+
@override
|
|
1298
|
+
Widget build(BuildContext context) {
|
|
1299
|
+
return Container(
|
|
1300
|
+
width: double.infinity,
|
|
1301
|
+
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
|
1302
|
+
decoration: BoxDecoration(
|
|
1303
|
+
color: _bgSecondary,
|
|
1304
|
+
borderRadius: BorderRadius.circular(18),
|
|
1305
|
+
border: Border.all(color: _borderLight),
|
|
1306
|
+
),
|
|
1307
|
+
child: Column(
|
|
1308
|
+
crossAxisAlignment: CrossAxisAlignment.start,
|
|
1309
|
+
mainAxisSize: MainAxisSize.min,
|
|
1310
|
+
children: <Widget>[
|
|
1311
|
+
Text(
|
|
1312
|
+
'ACTIONS',
|
|
1313
|
+
style: TextStyle(
|
|
1314
|
+
fontSize: 10,
|
|
1315
|
+
fontWeight: FontWeight.w700,
|
|
1316
|
+
letterSpacing: 0.8,
|
|
1317
|
+
color: _textSecondary,
|
|
1318
|
+
),
|
|
1319
|
+
),
|
|
1320
|
+
const SizedBox(height: 8),
|
|
1321
|
+
Wrap(
|
|
1322
|
+
spacing: 8,
|
|
1323
|
+
runSpacing: 8,
|
|
1324
|
+
children: <Widget>[
|
|
1325
|
+
AndroidApkTile(
|
|
1326
|
+
enabled: enabled,
|
|
1327
|
+
busy: busy,
|
|
1328
|
+
onInstall: onInstall,
|
|
1329
|
+
),
|
|
1330
|
+
],
|
|
1331
|
+
),
|
|
1332
|
+
],
|
|
1333
|
+
),
|
|
1334
|
+
);
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1267
1338
|
class _SurfaceSwitcher extends StatelessWidget {
|
|
1268
1339
|
const _SurfaceSwitcher({required this.surface, required this.onSelect});
|
|
1269
1340
|
|
|
@@ -1636,6 +1707,14 @@ class _InteractiveSurfacePreviewState
|
|
|
1636
1707
|
? null
|
|
1637
1708
|
: (x, y) => widget.onHover?.call(Offset(x, y)),
|
|
1638
1709
|
),
|
|
1710
|
+
const Positioned(
|
|
1711
|
+
top: 8,
|
|
1712
|
+
right: 8,
|
|
1713
|
+
child: Opacity(
|
|
1714
|
+
opacity: 0.45,
|
|
1715
|
+
child: _MutedBadge(),
|
|
1716
|
+
),
|
|
1717
|
+
),
|
|
1639
1718
|
Positioned(
|
|
1640
1719
|
left: 12,
|
|
1641
1720
|
right: 12,
|
|
@@ -1743,6 +1822,14 @@ class _InteractiveSurfacePreviewState
|
|
|
1743
1822
|
fit: BoxFit.contain,
|
|
1744
1823
|
gaplessPlayback: true,
|
|
1745
1824
|
),
|
|
1825
|
+
const Positioned(
|
|
1826
|
+
top: 8,
|
|
1827
|
+
right: 8,
|
|
1828
|
+
child: Opacity(
|
|
1829
|
+
opacity: 0.45,
|
|
1830
|
+
child: _MutedBadge(),
|
|
1831
|
+
),
|
|
1832
|
+
),
|
|
1746
1833
|
Positioned(
|
|
1747
1834
|
left: 12,
|
|
1748
1835
|
right: 12,
|
|
@@ -30,3 +30,27 @@ class AndroidApkDropZone extends StatelessWidget {
|
|
|
30
30
|
);
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
|
+
|
|
34
|
+
/// Compact tile variant — fits inside an actions row.
|
|
35
|
+
class AndroidApkTile extends StatelessWidget {
|
|
36
|
+
const AndroidApkTile({
|
|
37
|
+
super.key,
|
|
38
|
+
required this.enabled,
|
|
39
|
+
required this.busy,
|
|
40
|
+
required this.onInstall,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
final bool enabled;
|
|
44
|
+
final bool busy;
|
|
45
|
+
final AndroidApkInstallCallback onInstall;
|
|
46
|
+
|
|
47
|
+
@override
|
|
48
|
+
Widget build(BuildContext context) {
|
|
49
|
+
return buildAndroidApkTile(
|
|
50
|
+
context,
|
|
51
|
+
enabled: enabled,
|
|
52
|
+
busy: busy,
|
|
53
|
+
onInstall: onInstall,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -14,3 +14,16 @@ Widget buildAndroidApkDropZone(
|
|
|
14
14
|
}) {
|
|
15
15
|
return const SizedBox.shrink();
|
|
16
16
|
}
|
|
17
|
+
|
|
18
|
+
Widget buildAndroidApkTile(
|
|
19
|
+
BuildContext context, {
|
|
20
|
+
required bool enabled,
|
|
21
|
+
required bool busy,
|
|
22
|
+
required Future<void> Function({
|
|
23
|
+
required String filename,
|
|
24
|
+
required Uint8List bytes,
|
|
25
|
+
})
|
|
26
|
+
onInstall,
|
|
27
|
+
}) {
|
|
28
|
+
return const SizedBox.shrink();
|
|
29
|
+
}
|