neoagent 3.1.0 → 3.1.1-beta.0

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.
@@ -231,12 +231,262 @@ void _openOfficialIntegrationSetupDialog(
231
231
  case 'home_assistant':
232
232
  _showHomeAssistantSetupDialog(context, controller);
233
233
  return;
234
+ case 'neomail':
235
+ _showNeoMailSetupDialog(context, controller);
236
+ return;
234
237
  case 'trello':
235
238
  _showTrelloSetupDialog(context, controller);
236
239
  return;
237
240
  }
238
241
  }
239
242
 
243
+ Future<void> _showNeoMailSetupDialog(
244
+ BuildContext context,
245
+ NeoAgentController controller,
246
+ ) async {
247
+ Map<String, dynamic> existing;
248
+ try {
249
+ existing = await controller.getOfficialIntegrationConfig('neomail');
250
+ } catch (error) {
251
+ if (context.mounted) {
252
+ ScaffoldMessenger.of(context).showSnackBar(
253
+ SnackBar(content: Text(controller.errorMessage ?? error.toString())),
254
+ );
255
+ }
256
+ return;
257
+ }
258
+
259
+ final savedBaseUrl = existing['baseUrl']?.toString() ?? '';
260
+ final accountCount = (existing['accountCount'] as num?)?.toInt() ?? 0;
261
+ final hasConnectedAccount =
262
+ existing['hasConnectedAccount'] == true || accountCount > 0;
263
+ var formError = '';
264
+ var busy = false;
265
+
266
+ final baseUrlController = TextEditingController(text: savedBaseUrl);
267
+
268
+ Future<void> saveConfig(
269
+ StateSetter setState, {
270
+ required BuildContext dialogContext,
271
+ required bool connectAfterSave,
272
+ }) async {
273
+ setState(() {
274
+ formError = '';
275
+ busy = true;
276
+ });
277
+ try {
278
+ final baseUrl = baseUrlController.text.trim();
279
+ if (baseUrl.isEmpty) {
280
+ setState(() {
281
+ formError = 'NeoMail backend URL is required.';
282
+ busy = false;
283
+ });
284
+ return;
285
+ }
286
+ await controller.saveOfficialIntegrationConfig(
287
+ 'neomail',
288
+ config: <String, dynamic>{'baseUrl': baseUrl},
289
+ );
290
+ if (connectAfterSave) {
291
+ await controller.connectOfficialIntegration(
292
+ 'neomail',
293
+ appId: 'mailbox',
294
+ );
295
+ if ((controller.errorMessage ?? '').trim().isNotEmpty) {
296
+ setState(() {
297
+ formError = controller.errorMessage!;
298
+ busy = false;
299
+ });
300
+ return;
301
+ }
302
+ }
303
+ if (dialogContext.mounted) {
304
+ Navigator.of(dialogContext).pop();
305
+ }
306
+ } catch (_) {
307
+ setState(() {
308
+ formError = controller.errorMessage ?? 'Could not save NeoMail setup.';
309
+ busy = false;
310
+ });
311
+ }
312
+ }
313
+
314
+ if (!context.mounted) return;
315
+ await showDialog<void>(
316
+ context: context,
317
+ barrierDismissible: false,
318
+ builder: (dialogContext) {
319
+ return StatefulBuilder(
320
+ builder: (dialogContext, setState) {
321
+ return AlertDialog(
322
+ title: const Text('NeoMail Setup'),
323
+ content: SizedBox(
324
+ width: 540,
325
+ child: Column(
326
+ mainAxisSize: MainAxisSize.min,
327
+ crossAxisAlignment: CrossAxisAlignment.start,
328
+ children: <Widget>[
329
+ Text(
330
+ 'Add the NeoMail backend URL once. NeoAgent will then open NeoMail OAuth so the user can sign in and approve mailbox access without API keys.',
331
+ style: TextStyle(color: _textSecondary),
332
+ ),
333
+ const SizedBox(height: 16),
334
+ const _IntegrationSetupStatusItem(
335
+ label: 'Connection Method',
336
+ status: 'OAuth companion flow',
337
+ isConnected: true,
338
+ ),
339
+ const SizedBox(height: 12),
340
+ _IntegrationSetupStatusItem(
341
+ label: 'Backend URL',
342
+ status: savedBaseUrl.trim().isNotEmpty
343
+ ? 'Configured'
344
+ : 'Not configured',
345
+ isConnected: savedBaseUrl.trim().isNotEmpty,
346
+ ),
347
+ const SizedBox(height: 12),
348
+ _IntegrationSetupStatusItem(
349
+ label: 'Connected NeoMail User',
350
+ status: hasConnectedAccount
351
+ ? '$accountCount ${accountCount == 1 ? 'connected user' : 'connected users'}'
352
+ : 'Not connected',
353
+ isConnected: hasConnectedAccount,
354
+ ),
355
+ const SizedBox(height: 12),
356
+ TextField(
357
+ controller: baseUrlController,
358
+ onChanged: (_) => setState(() {}),
359
+ keyboardType: TextInputType.url,
360
+ decoration: const InputDecoration(
361
+ labelText: 'NeoMail Backend URL',
362
+ hintText: 'https://mail.example.com',
363
+ border: OutlineInputBorder(),
364
+ ),
365
+ ),
366
+ const SizedBox(height: 8),
367
+ Text(
368
+ 'Use the public base URL of the NeoMail server. Local self-hosted URLs are also supported when NeoAgent can reach them.',
369
+ style: TextStyle(color: _textSecondary, fontSize: 12),
370
+ ),
371
+ if (formError.isNotEmpty) ...<Widget>[
372
+ const SizedBox(height: 12),
373
+ Container(
374
+ padding: const EdgeInsets.all(8),
375
+ decoration: BoxDecoration(
376
+ color: _danger.withValues(alpha: 0.1),
377
+ borderRadius: BorderRadius.circular(4),
378
+ border: Border.all(
379
+ color: _danger.withValues(alpha: 0.3),
380
+ ),
381
+ ),
382
+ child: Text(
383
+ formError,
384
+ style: TextStyle(color: _danger, fontSize: 12),
385
+ ),
386
+ ),
387
+ ],
388
+ ],
389
+ ),
390
+ ),
391
+ actions: <Widget>[
392
+ if (savedBaseUrl.trim().isNotEmpty)
393
+ TextButton(
394
+ onPressed: busy
395
+ ? null
396
+ : () async {
397
+ final shouldClear =
398
+ await showDialog<bool>(
399
+ context: dialogContext,
400
+ builder: (context) {
401
+ return AlertDialog(
402
+ title: const Text('Disconnect NeoMail?'),
403
+ content: const Text(
404
+ 'This removes the NeoMail backend URL and all connected NeoMail accounts for this agent.',
405
+ ),
406
+ actions: [
407
+ TextButton(
408
+ onPressed: () =>
409
+ Navigator.of(context).pop(false),
410
+ child: const Text('Cancel'),
411
+ ),
412
+ FilledButton(
413
+ onPressed: () =>
414
+ Navigator.of(context).pop(true),
415
+ child: const Text('Disconnect'),
416
+ ),
417
+ ],
418
+ );
419
+ },
420
+ ) ??
421
+ false;
422
+ if (!shouldClear) {
423
+ return;
424
+ }
425
+ setState(() {
426
+ formError = '';
427
+ busy = true;
428
+ });
429
+ try {
430
+ await controller.clearOfficialIntegrationConfig(
431
+ 'neomail',
432
+ );
433
+ if (dialogContext.mounted) {
434
+ Navigator.of(dialogContext).pop();
435
+ }
436
+ } catch (_) {
437
+ setState(() {
438
+ formError =
439
+ controller.errorMessage ??
440
+ 'Could not disconnect NeoMail.';
441
+ busy = false;
442
+ });
443
+ }
444
+ },
445
+ child: const Text('Disconnect'),
446
+ ),
447
+ TextButton(
448
+ onPressed: busy
449
+ ? null
450
+ : () => Navigator.of(dialogContext).pop(),
451
+ child: const Text('Close'),
452
+ ),
453
+ if (!hasConnectedAccount)
454
+ TextButton(
455
+ onPressed: busy
456
+ ? null
457
+ : () => saveConfig(
458
+ setState,
459
+ dialogContext: dialogContext,
460
+ connectAfterSave: false,
461
+ ),
462
+ child: const Text('Save Only'),
463
+ ),
464
+ FilledButton(
465
+ onPressed: busy
466
+ ? null
467
+ : () => saveConfig(
468
+ setState,
469
+ dialogContext: dialogContext,
470
+ connectAfterSave: !hasConnectedAccount,
471
+ ),
472
+ child: Text(
473
+ busy
474
+ ? 'Working...'
475
+ : hasConnectedAccount
476
+ ? 'Update Setup'
477
+ : 'Save & Connect',
478
+ ),
479
+ ),
480
+ ],
481
+ );
482
+ },
483
+ );
484
+ },
485
+ );
486
+
487
+ baseUrlController.dispose();
488
+ }
489
+
240
490
  Future<void> _showHomeAssistantSetupDialog(
241
491
  BuildContext context,
242
492
  NeoAgentController controller,
@@ -1132,12 +1382,14 @@ class _OfficialIntegrationIcon extends StatelessWidget {
1132
1382
  final color = switch (item.icon) {
1133
1383
  'google' => const Color(0xFF4285F4),
1134
1384
  'home_assistant' => const Color(0xFF41BDF5),
1385
+ 'neomail' => const Color(0xFF0F9D8A),
1135
1386
  'trello' => const Color(0xFF0C66E4),
1136
1387
  _ => _accent,
1137
1388
  };
1138
1389
  final label = switch (item.icon) {
1139
1390
  'google' => 'G',
1140
1391
  'home_assistant' => 'H',
1392
+ 'neomail' => 'N',
1141
1393
  'trello' => 'T',
1142
1394
  _ => item.label.isNotEmpty ? item.label[0] : '?',
1143
1395
  };