Aves_mio0.88/lib/model/activity/activity_library_bootstrap.dart
2026-10-03 12:51:15 +02:00

388 lines
12 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// lib/model/activity/activity_library_bootstrap.dart
import 'dart:io';
import 'package:aves/model/settings/defaults.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/services/common/services.dart';
import 'package:flutter/material.dart';
class ActivityLibraryBootstrap {
static bool _running = false;
static Future<void> ensureConfigured(BuildContext context) async {
if (_running) return;
_running = true;
try {
final existingTreeUri = settings.activityRootTreeUri;
// Già configurata: non chiedere più nulla.
// Però proviamo comunque a garantire che esista Activity/main.
if (existingTreeUri != null && existingTreeUri.isNotEmpty) {
final mainReady = await storageService.ensureActivityMainDirectory(
existingTreeUri,
);
if (!mainReady && context.mounted) {
await _showActivityMainCreationFailedDialog(
context: context,
);
}
return;
}
final rootPath = settings.activityRootPath;
final createdOrExists = await _tryCreateRootDirectory(rootPath);
if (!context.mounted) return;
if (createdOrExists) {
final shouldContinue = await _showActivityRootReadyDialog(
context: context,
rootPath: rootPath,
);
if (!shouldContinue) {
settings.activityRootPromptDone = true;
return;
}
} else {
final understood = await _showActivityRootMissingDialog(
context: context,
rootPath: rootPath,
);
if (!understood) {
settings.activityRootPromptDone = true;
return;
}
if (!context.mounted) return;
final shouldOpenPicker = await _showOpenPickerDialog(
context: context,
rootPath: rootPath,
);
if (!shouldOpenPicker) {
settings.activityRootPromptDone = true;
return;
}
}
if (!context.mounted) return;
await _pickValidateAndSaveActivityRoot(
context: context,
rootPath: rootPath,
);
} catch (e, st) {
debugPrint('[ActivityBootstrap] failed to configure activity library: $e');
debugPrint('[ActivityBootstrap] stack: $st');
} finally {
_running = false;
}
}
static Future<void> _pickValidateAndSaveActivityRoot({
required BuildContext context,
required String rootPath,
}) async {
while (context.mounted) {
// Piccola pausa per lasciare chiudere completamente il dialog Flutter
// prima di aprire il picker Android SAF.
await Future.delayed(const Duration(milliseconds: 300));
if (!context.mounted) return;
final picked = await storageService.pickActivityDirectory();
if (picked == null || picked.treeUri.isEmpty) {
settings.activityRootPromptDone = true;
debugPrint('[ActivityBootstrap] user did not select activity directory');
return;
}
final selectedPath = picked.path;
debugPrint('[ActivityBootstrap] picked treeUri=${picked.treeUri}');
debugPrint('[ActivityBootstrap] picked path=$selectedPath');
if (!_isExpectedActivityRoot(selectedPath, rootPath)) {
if (!context.mounted) return;
final retry = await _showWrongActivityRootDialog(
context: context,
expectedPath: rootPath,
selectedPath: selectedPath,
);
if (!retry) {
settings.activityRootPromptDone = true;
return;
}
if (!context.mounted) return;
final shouldOpenPicker = await _showOpenPickerDialog(
context: context,
rootPath: rootPath,
);
if (!shouldOpenPicker) {
settings.activityRootPromptDone = true;
return;
}
continue;
}
settings.activityRootPath = rootPath;
settings.activityRootTreeUri = picked.treeUri;
settings.activityRootPromptDone = true;
debugPrint('[ActivityBootstrap] saved activity root path=$rootPath');
debugPrint('[ActivityBootstrap] saved activity treeUri=${picked.treeUri}');
final mainReady = await storageService.ensureActivityMainDirectory(
picked.treeUri,
);
debugPrint('[ActivityBootstrap] Activity/main ready=$mainReady');
if (!mainReady && context.mounted) {
await _showActivityMainCreationFailedDialog(
context: context,
);
}
return;
}
}
static Future<bool> _tryCreateRootDirectory(String rootPath) async {
try {
final dir = Directory(rootPath);
if (!await dir.exists()) {
await dir.create(recursive: true);
debugPrint('[ActivityBootstrap] attempted to create root directory: $rootPath');
}
final existsAfterCreate = await dir.exists();
if (existsAfterCreate) {
debugPrint('[ActivityBootstrap] root directory exists: $rootPath');
} else {
debugPrint('[ActivityBootstrap] root directory still missing after create attempt: $rootPath');
}
return existsAfterCreate;
} catch (e, st) {
debugPrint('[ActivityBootstrap] failed to create root directory: $rootPath');
debugPrint('[ActivityBootstrap] error: $e');
debugPrint('[ActivityBootstrap] stack: $st');
return false;
}
}
static String _normalizePath(String path) {
final trimmed = path.trim();
return trimmed.endsWith('/') ? trimmed : '$trimmed/';
}
static bool _isExpectedActivityRoot(
String? selectedPath,
String expectedPath,
) {
if (selectedPath == null || selectedPath.trim().isEmpty) {
return false;
}
return _normalizePath(selectedPath) == _normalizePath(expectedPath);
}
static Future<bool> _showActivityRootReadyDialog({
required BuildContext context,
required String rootPath,
}) async {
final result = await showDialog<bool>(
context: context,
barrierDismissible: false,
builder: (dialogContext) {
return AlertDialog(
title: const Text('Cartella attività'),
content: Text(
'Aves deve accedere alla cartella:\n\n'
'$rootPath\n\n'
'La cartella esiste già o è stata creata correttamente.\n\n'
'Nella schermata successiva seleziona questa cartella per permettere '
'ad Aves di scansionare i file FIT, GPX e TCX.\n\n'
'Dopo il consenso Aves creerà automaticamente anche la sottocartella:\n\n'
'$rootPath/main',
),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(false),
child: const Text('Più tardi'),
),
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(true),
child: const Text('Continua'),
),
],
);
},
);
return result ?? false;
}
static Future<bool> _showActivityRootMissingDialog({
required BuildContext context,
required String rootPath,
}) async {
final result = await showDialog<bool>(
context: context,
barrierDismissible: false,
builder: (dialogContext) {
return AlertDialog(
title: const Text('Cartella Activity mancante'),
content: Text(
'Aves non è riuscita a creare automaticamente la cartella:\n\n'
'$rootPath\n\n'
'Questo può succedere su Android moderno per le limitazioni di accesso '
'allo storage.\n\n'
'Ora dovrai creare manualmente la cartella "Activity" nella memoria interna '
'del dispositivo, oppure selezionarla se esiste già.\n\n'
'Percorso richiesto:\n'
'/storage/emulated/0/Activity\n\n'
'Dopo il consenso Aves proverà a creare automaticamente la sottocartella:\n\n'
'/storage/emulated/0/Activity/main',
),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(false),
child: const Text('Annulla'),
),
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(true),
child: const Text('OK, ho capito'),
),
],
);
},
);
return result ?? false;
}
static Future<bool> _showOpenPickerDialog({
required BuildContext context,
required String rootPath,
}) async {
final result = await showDialog<bool>(
context: context,
barrierDismissible: false,
builder: (dialogContext) {
return AlertDialog(
title: const Text('Seleziona cartella Activity'),
content: Text(
'Nella schermata successiva seleziona esattamente questa cartella:\n\n'
'$rootPath\n\n'
'Se non esiste, creala dal selettore file o da un file manager, poi torna qui '
'e selezionala.\n\n'
'Aves userà questa cartella come root della libreria attività.\n\n'
'Dopo il consenso Aves creerà automaticamente anche:\n\n'
'$rootPath/main',
),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(false),
child: const Text('Più tardi'),
),
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(true),
child: const Text('Apri selettore cartella'),
),
],
);
},
);
return result ?? false;
}
static Future<bool> _showWrongActivityRootDialog({
required BuildContext context,
required String expectedPath,
required String? selectedPath,
}) async {
final result = await showDialog<bool>(
context: context,
barrierDismissible: false,
builder: (dialogContext) {
return AlertDialog(
title: const Text('Cartella non corretta'),
content: Text(
'Hai selezionato una cartella diversa da quella richiesta.\n\n'
'Cartella selezionata:\n'
'${selectedPath == null || selectedPath.isEmpty ? 'non riconosciuta' : selectedPath}\n\n'
'Cartella richiesta:\n'
'$expectedPath\n\n'
'Devi creare o selezionare esattamente la cartella "Activity" nella memoria interna del dispositivo.\n\n'
'Non selezionare "Activity/main", "Download", "Documents" o altre cartelle.',
),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(false),
child: const Text('Più tardi'),
),
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(true),
child: const Text('Riprova'),
),
],
);
},
);
return result ?? false;
}
static Future<void> _showActivityMainCreationFailedDialog({
required BuildContext context,
}) async {
await showDialog<void>(
context: context,
barrierDismissible: true,
builder: (dialogContext) {
return AlertDialog(
title: const Text('Sottocartella main non creata'),
content: Text(
'Aves ha ottenuto l’accesso alla cartella Activity, ma non è riuscita '
'a creare automaticamente la sottocartella:\n\n'
'${SettingsDefaults.activityRootPath}/main\n\n'
'Puoi crearla manualmente con un file manager.\n\n'
'La scansione continuerà comunque a funzionare sulla cartella Activity '
'e sulle sue sottocartelle esistenti.',
),
actions: [
TextButton(
onPressed: () => Navigator.of(dialogContext).pop(),
child: const Text('OK'),
),
],
);
},
);
}
static Future<void> resetConfiguration() async {
settings.activityRootTreeUri = null;
settings.activityRootPromptDone = false;
settings.activityRootPath = SettingsDefaults.activityRootPath;
}
}