888 lines
24 KiB
Dart
888 lines
24 KiB
Dart
// lib/widgets/activity/activity_files_page.dart
|
||
import 'dart:async';
|
||
import 'dart:io';
|
||
import 'dart:typed_data';
|
||
|
||
import 'package:aves/model/activity/activity_entry.dart';
|
||
import 'package:aves/model/activity/activity_preview_point.dart';
|
||
import 'package:aves/model/activity/activity_scan_service.dart';
|
||
import 'package:aves/model/activity/activity_track.dart';
|
||
import 'package:aves/model/settings/defaults.dart';
|
||
import 'package:aves/model/settings/settings.dart';
|
||
import 'package:aves/services/common/services.dart';
|
||
import 'package:aves/utils/activity_file_parser.dart';
|
||
import 'package:aves/widgets/activity/activity_auto_sync_controller.dart';
|
||
import 'package:aves/widgets/activity/activity_list_tile.dart';
|
||
import 'package:aves/widgets/activity/activity_map_page.dart';
|
||
import 'package:aves/widgets/common/basic/draggable_scrollbar/notifications.dart';
|
||
import 'package:aves/widgets/common/dialogs/miui_safe_dialog.dart';
|
||
import 'package:aves/widgets/navigation/nav_bar/nav_bar.dart';
|
||
import 'package:file_picker/file_picker.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:aves/widgets/activity/utils/activity_formatters.dart';
|
||
import 'package:aves/widgets/activity/activity_sport_groups_view.dart';
|
||
import 'package:aves/widgets/activity/activity_album_groups_view.dart';
|
||
import 'package:aves/model/activity/activity_grouping.dart';
|
||
import 'package:aves/widgets/activity/dialogs/activity_db_fields_dialog.dart';
|
||
import 'package:aves/widgets/activity/dialogs/activity_recalculate_dialog.dart';
|
||
import 'package:aves/widgets/activity/activity_data_loader.dart';
|
||
|
||
enum ActivityViewMode {
|
||
activities,
|
||
sports,
|
||
albums,
|
||
}
|
||
|
||
class ActivityFilesPage extends StatefulWidget {
|
||
static const routeName = '/activity';
|
||
|
||
const ActivityFilesPage({super.key});
|
||
|
||
@override
|
||
State<ActivityFilesPage> createState() => _ActivityFilesPageState();
|
||
}
|
||
|
||
class _ActivityFilesPageState extends State<ActivityFilesPage> with WidgetsBindingObserver {
|
||
// File aperti manualmente con il pulsante "+"
|
||
final List<ActivityTrack> _activities = [];
|
||
|
||
// Attività salvate nel DB dopo scansione
|
||
final List<ActivityEntry> _dbActivities = [];
|
||
final Map<int, List<ActivityPreviewPoint>> _dbPreviewPoints = {};
|
||
|
||
String? _activityTreeUri;
|
||
bool _loadingDbActivities = false;
|
||
late final ActivityAutoSyncController _autoSyncController;
|
||
bool _activityAutoScanRunning = false;
|
||
|
||
final StreamController<DraggableScrollbarEvent> _navBarEvents = StreamController<DraggableScrollbarEvent>.broadcast();
|
||
|
||
int _activityNavIndex = 0;
|
||
|
||
ActivityViewMode _activityViewMode = ActivityViewMode.activities;
|
||
|
||
bool _activityFavouriteOnly = false;
|
||
bool _activityWithoutSportOnly = false;
|
||
String? _activitySportFilter;
|
||
String? _activityDirectoryFilter;
|
||
|
||
final List<ActivitySportGroup> _sportGroups = [];
|
||
final List<ActivityAlbumGroup> _albumGroups = [];
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
|
||
WidgetsBinding.instance.addObserver(this);
|
||
|
||
_activityTreeUri = settings.activityRootTreeUri;
|
||
|
||
_autoSyncController = ActivityAutoSyncController(
|
||
onScanRequested: _autoScanActivityLibrary,
|
||
)..start();
|
||
|
||
_loadDbActivities();
|
||
|
||
// Prima sincronizzazione silenziosa quando la pagina si apre.
|
||
_autoSyncController.schedule(
|
||
reason: 'page_open',
|
||
delay: const Duration(milliseconds: 800),
|
||
);
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
WidgetsBinding.instance.removeObserver(this);
|
||
_autoSyncController.dispose();
|
||
_navBarEvents.close();
|
||
|
||
super.dispose();
|
||
}
|
||
|
||
@override
|
||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||
if (state == AppLifecycleState.resumed) {
|
||
_autoSyncController.schedule(
|
||
reason: 'app_resumed',
|
||
delay: const Duration(milliseconds: 1200),
|
||
);
|
||
}
|
||
}
|
||
|
||
void _onActivityNavSelected(int index) {
|
||
switch (index) {
|
||
case 0:
|
||
setState(() {
|
||
_activityViewMode = ActivityViewMode.activities;
|
||
_activityNavIndex = 0;
|
||
|
||
_activityFavouriteOnly = false;
|
||
_activityWithoutSportOnly = false;
|
||
_activitySportFilter = null;
|
||
_activityDirectoryFilter = null;
|
||
});
|
||
_loadDbActivities();
|
||
break;
|
||
|
||
case 1:
|
||
setState(() {
|
||
_activityViewMode = ActivityViewMode.sports;
|
||
_activityNavIndex = 1;
|
||
|
||
_activityFavouriteOnly = false;
|
||
_activityWithoutSportOnly = false;
|
||
_activitySportFilter = null;
|
||
_activityDirectoryFilter = null;
|
||
});
|
||
_loadSportGroups();
|
||
break;
|
||
|
||
case 2:
|
||
setState(() {
|
||
_activityViewMode = ActivityViewMode.activities;
|
||
_activityNavIndex = 2;
|
||
|
||
_activityFavouriteOnly = true;
|
||
_activityWithoutSportOnly = false;
|
||
_activitySportFilter = null;
|
||
_activityDirectoryFilter = null;
|
||
});
|
||
_loadDbActivities();
|
||
break;
|
||
|
||
case 3:
|
||
setState(() {
|
||
_activityViewMode = ActivityViewMode.albums;
|
||
_activityNavIndex = 3;
|
||
|
||
_activityFavouriteOnly = false;
|
||
_activityWithoutSportOnly = false;
|
||
_activitySportFilter = null;
|
||
_activityDirectoryFilter = null;
|
||
});
|
||
_loadAlbumGroups();
|
||
break;
|
||
}
|
||
}
|
||
|
||
String _emptyActivityMessage() {
|
||
if (_activityFavouriteOnly) {
|
||
return 'Nessuna attività preferita.';
|
||
}
|
||
|
||
if (_activityWithoutSportOnly || _activitySportFilter != null) {
|
||
return 'Nessuna attività per questo sport.';
|
||
}
|
||
|
||
if (_activityDirectoryFilter != null) {
|
||
return 'Nessuna attività in questo album.';
|
||
}
|
||
|
||
return 'Nessuna attività trovata.';
|
||
}
|
||
|
||
Future<void> _toggleActivityFavorite(ActivityEntry entry) async {
|
||
final id = entry.id;
|
||
|
||
if (id == null) {
|
||
_showMessage('Impossibile modificare preferito: id attività mancante.');
|
||
return;
|
||
}
|
||
|
||
final updated = entry.copyWith(
|
||
isFavorite: !entry.isFavorite,
|
||
dateModifiedMillis: DateTime.now().millisecondsSinceEpoch,
|
||
);
|
||
|
||
await localMediaDb.updateActivity(id, updated);
|
||
|
||
if (!mounted) return;
|
||
|
||
_showMessage(
|
||
updated.isFavorite ? 'Attività aggiunta ai preferiti.' : 'Attività rimossa dai preferiti.',
|
||
);
|
||
|
||
await _loadDbActivities();
|
||
}
|
||
|
||
Future<void> _renameActivity(ActivityEntry entry) async {
|
||
final id = entry.id;
|
||
if (id == null) {
|
||
_showMessage('Impossibile rinominare: id attività mancante.');
|
||
return;
|
||
}
|
||
|
||
final newTitle = await showMiuiSafeDialog<String>(
|
||
context: context,
|
||
builder: (dialogContext) {
|
||
return MiuiSafeTextDialog(
|
||
title: 'Modifica nome attività',
|
||
label: 'Nome attività',
|
||
hint: 'Inserisci un nome personalizzato',
|
||
initialText: entry.bestName,
|
||
);
|
||
},
|
||
);
|
||
|
||
if (newTitle == null) return;
|
||
|
||
final normalizedTitle = newTitle.trim();
|
||
|
||
final updated = entry.copyWith(
|
||
title: normalizedTitle.isEmpty ? null : normalizedTitle,
|
||
dateModifiedMillis: DateTime.now().millisecondsSinceEpoch,
|
||
);
|
||
|
||
await localMediaDb.updateActivity(id, updated);
|
||
|
||
if (!mounted) return;
|
||
|
||
setState(() {
|
||
final index = _dbActivities.indexWhere((e) => e.id == id);
|
||
if (index != -1) {
|
||
_dbActivities[index] = updated;
|
||
}
|
||
});
|
||
|
||
_showMessage(
|
||
normalizedTitle.isEmpty ? 'Nome personalizzato rimosso.' : 'Nome attività aggiornato.',
|
||
);
|
||
}
|
||
|
||
Future<void> _loadDbActivities() async {
|
||
if (_loadingDbActivities) return;
|
||
|
||
setState(() {
|
||
_loadingDbActivities = true;
|
||
});
|
||
|
||
try {
|
||
final result = await ActivityDataLoader.loadEntries(
|
||
favouriteOnly: _activityFavouriteOnly,
|
||
withoutSportOnly: _activityWithoutSportOnly,
|
||
sport: _activitySportFilter,
|
||
directory: _activityDirectoryFilter,
|
||
);
|
||
|
||
if (!mounted) return;
|
||
|
||
setState(() {
|
||
_dbActivities
|
||
..clear()
|
||
..addAll(result.entries);
|
||
|
||
_dbPreviewPoints
|
||
..clear()
|
||
..addAll(result.previewPointsByActivityId);
|
||
|
||
_loadingDbActivities = false;
|
||
});
|
||
} catch (e) {
|
||
if (!mounted) return;
|
||
|
||
setState(() {
|
||
_loadingDbActivities = false;
|
||
});
|
||
|
||
_showMessage('Errore caricamento attività DB: $e');
|
||
}
|
||
}
|
||
|
||
Future<void> _loadSportGroups() async {
|
||
if (_loadingDbActivities) return;
|
||
|
||
setState(() {
|
||
_loadingDbActivities = true;
|
||
});
|
||
|
||
try {
|
||
final result = await ActivityDataLoader.loadSportGroups();
|
||
|
||
if (!mounted) return;
|
||
|
||
setState(() {
|
||
_sportGroups
|
||
..clear()
|
||
..addAll(result.groups);
|
||
|
||
_dbPreviewPoints
|
||
..clear()
|
||
..addAll(result.previewPointsByActivityId);
|
||
|
||
_loadingDbActivities = false;
|
||
});
|
||
} catch (e) {
|
||
if (!mounted) return;
|
||
|
||
setState(() {
|
||
_loadingDbActivities = false;
|
||
});
|
||
|
||
_showMessage('Errore caricamento sport attività: $e');
|
||
}
|
||
}
|
||
|
||
Future<void> _openSportGroup(ActivitySportGroup group) async {
|
||
setState(() {
|
||
_activityViewMode = ActivityViewMode.activities;
|
||
_activityNavIndex = 1;
|
||
|
||
_activityFavouriteOnly = false;
|
||
_activityWithoutSportOnly = group.withoutSport;
|
||
_activitySportFilter = group.sportFilter;
|
||
_activityDirectoryFilter = null;
|
||
});
|
||
|
||
await _loadDbActivities();
|
||
}
|
||
|
||
Future<void> _loadAlbumGroups() async {
|
||
if (_loadingDbActivities) return;
|
||
|
||
setState(() {
|
||
_loadingDbActivities = true;
|
||
});
|
||
|
||
try {
|
||
final result = await ActivityDataLoader.loadAlbumGroups();
|
||
|
||
if (!mounted) return;
|
||
|
||
setState(() {
|
||
_albumGroups
|
||
..clear()
|
||
..addAll(result.groups);
|
||
|
||
_dbPreviewPoints
|
||
..clear()
|
||
..addAll(result.previewPointsByActivityId);
|
||
|
||
_loadingDbActivities = false;
|
||
});
|
||
} catch (e) {
|
||
if (!mounted) return;
|
||
|
||
setState(() {
|
||
_loadingDbActivities = false;
|
||
});
|
||
|
||
_showMessage('Errore caricamento album attività: $e');
|
||
}
|
||
}
|
||
|
||
Future<void> _openAlbumGroup(ActivityAlbumGroup group) async {
|
||
setState(() {
|
||
_activityViewMode = ActivityViewMode.activities;
|
||
_activityNavIndex = 3;
|
||
|
||
_activityFavouriteOnly = false;
|
||
_activityWithoutSportOnly = false;
|
||
_activitySportFilter = null;
|
||
_activityDirectoryFilter = group.directoryFilter;
|
||
});
|
||
|
||
await _loadDbActivities();
|
||
}
|
||
|
||
Future<void> _pickActivityFile() async {
|
||
try {
|
||
final result = await FilePicker.platform.pickFiles(
|
||
type: FileType.any,
|
||
withData: true,
|
||
);
|
||
|
||
final pickedFile = result?.files.single;
|
||
if (pickedFile == null) return;
|
||
|
||
if (!ActivityFileParser.isSupportedFileName(pickedFile.name)) {
|
||
if (!mounted) return;
|
||
_showMessage('Seleziona un file GPX, TCX o FIT.');
|
||
return;
|
||
}
|
||
|
||
final bytes = await _readPickedFileBytes(pickedFile);
|
||
|
||
final activity = await ActivityFileParser.parse(
|
||
bytes,
|
||
fileName: pickedFile.name,
|
||
);
|
||
|
||
if (activity == null) {
|
||
if (!mounted) return;
|
||
_showMessage('Impossibile leggere il file attività.');
|
||
return;
|
||
}
|
||
|
||
if (activity.isEmpty) {
|
||
if (!mounted) return;
|
||
_showMessage('Nessuna traccia GPS valida trovata nel file.');
|
||
return;
|
||
}
|
||
|
||
if (!mounted) return;
|
||
|
||
setState(() {
|
||
_activities.add(activity);
|
||
});
|
||
} catch (e) {
|
||
if (!mounted) return;
|
||
_showMessage('Impossibile leggere il file attività: $e');
|
||
}
|
||
}
|
||
|
||
Future<String?> _pickAndSaveActivityDirectory() async {
|
||
final picked = await storageService.pickActivityDirectory();
|
||
|
||
if (!mounted) return null;
|
||
|
||
if (picked == null || picked.treeUri.isEmpty) {
|
||
_showMessage('Cartella attività non selezionata.');
|
||
return null;
|
||
}
|
||
|
||
final selectedPath = picked.path;
|
||
const expectedPath = SettingsDefaults.activityRootPath;
|
||
|
||
if (!ActivityFormatters.isExpectedActivityRoot(selectedPath, expectedPath)) {
|
||
await _showActivityPermissionDialog(
|
||
title: 'Cartella non corretta',
|
||
message:
|
||
'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 selezionare esattamente la cartella "Activity" nella memoria interna del dispositivo.\n\n'
|
||
'Non selezionare "Activity/main", "Download", "Documents" o altre cartelle.',
|
||
);
|
||
return null;
|
||
}
|
||
|
||
settings.activityRootPath = expectedPath;
|
||
settings.activityRootTreeUri = picked.treeUri;
|
||
settings.activityRootPromptDone = true;
|
||
|
||
final mainReady = await storageService.ensureActivityMainDirectory(
|
||
picked.treeUri,
|
||
);
|
||
|
||
if (!mounted) return picked.treeUri;
|
||
|
||
if (!mainReady) {
|
||
await _showActivityPermissionDialog(
|
||
title: 'Sottocartella main non creata',
|
||
message:
|
||
'Aves ha ottenuto l’accesso alla cartella Activity, ma non è riuscita '
|
||
'a creare automaticamente la sottocartella:\n\n'
|
||
'$expectedPath/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.',
|
||
);
|
||
}
|
||
|
||
setState(() {
|
||
_activityTreeUri = picked.treeUri;
|
||
});
|
||
|
||
return picked.treeUri;
|
||
}
|
||
|
||
Future<void> _pickActivityDirectory() async {
|
||
try {
|
||
final treeUri = await _pickAndSaveActivityDirectory();
|
||
|
||
if (!mounted) return;
|
||
|
||
if (treeUri == null || treeUri.isEmpty) {
|
||
return;
|
||
}
|
||
|
||
_showMessage('Cartella attività selezionata e salvata.');
|
||
|
||
_autoSyncController.schedule(
|
||
reason: 'directory_selected',
|
||
delay: const Duration(milliseconds: 500),
|
||
);
|
||
} catch (e) {
|
||
if (!mounted) return;
|
||
|
||
await _showActivityPermissionDialog(
|
||
title: 'Errore selezione cartella',
|
||
message: 'Impossibile selezionare la cartella attività:\n\n$e',
|
||
);
|
||
}
|
||
}
|
||
|
||
Future<void> _scanActivityLibrary() async {
|
||
try {
|
||
var treeUri = settings.activityRootTreeUri ?? _activityTreeUri;
|
||
|
||
debugPrint(
|
||
'[ActivityPage] settings treeUri=${settings.activityRootTreeUri}',
|
||
);
|
||
debugPrint('[ActivityPage] local treeUri=$_activityTreeUri');
|
||
|
||
if (treeUri == null || treeUri.isEmpty) {
|
||
treeUri = await _pickAndSaveActivityDirectory();
|
||
|
||
if (treeUri == null || treeUri.isEmpty) {
|
||
if (!mounted) return;
|
||
|
||
await _showActivityPermissionDialog(
|
||
title: 'Cartella non selezionata',
|
||
message:
|
||
'Per scansionare le attività devi selezionare la cartella:\n\n'
|
||
'${SettingsDefaults.activityRootPath}\n\n'
|
||
'Questa cartella sarà salvata nei settings.',
|
||
);
|
||
return;
|
||
}
|
||
}
|
||
|
||
final scanner = ActivityScanService(
|
||
db: localMediaDb,
|
||
);
|
||
|
||
final result = await scanner.scanSaf(treeUri);
|
||
|
||
if (!mounted) return;
|
||
|
||
if (result.scannedFiles == 0) {
|
||
final files = await storageService.listActivityFiles(treeUri);
|
||
|
||
await _showActivityPermissionDialog(
|
||
title: 'Nessun file attività trovato',
|
||
message:
|
||
'Aves ha accesso alla cartella selezionata, ma non ha trovato file FIT, GPX o TCX.\n\n'
|
||
'File attività trovati dal layer Android: ${files.length}\n\n'
|
||
'Cartella prevista:\n'
|
||
'${SettingsDefaults.activityRootPath}',
|
||
);
|
||
return;
|
||
}
|
||
|
||
_showMessage(
|
||
'Scan completato: '
|
||
'${result.scannedFiles} file, '
|
||
'${result.inserted} nuovi, '
|
||
'${result.updated} aggiornati, '
|
||
'${result.skippedUnchanged} invariati, '
|
||
'${result.errors.length} errori.',
|
||
);
|
||
|
||
await _loadDbActivities();
|
||
} catch (e) {
|
||
if (!mounted) return;
|
||
|
||
await _showActivityPermissionDialog(
|
||
title: 'Errore scan attività',
|
||
message: 'Errore durante la scansione attività:\n\n$e',
|
||
);
|
||
}
|
||
}
|
||
|
||
Future<void> _autoScanActivityLibrary(String reason) async {
|
||
if (_activityAutoScanRunning) {
|
||
debugPrint(
|
||
'[ActivityPage] auto scan skipped, already running, reason=$reason',
|
||
);
|
||
return;
|
||
}
|
||
|
||
final treeUri = settings.activityRootTreeUri ?? _activityTreeUri;
|
||
|
||
if (treeUri == null || treeUri.isEmpty) {
|
||
debugPrint('[ActivityPage] auto scan skipped, no treeUri, reason=$reason');
|
||
return;
|
||
}
|
||
|
||
_activityAutoScanRunning = true;
|
||
|
||
try {
|
||
debugPrint('[ActivityPage] auto scan started, reason=$reason');
|
||
|
||
final scanner = ActivityScanService(
|
||
db: localMediaDb,
|
||
);
|
||
|
||
final result = await scanner.scanSaf(treeUri);
|
||
|
||
debugPrint(
|
||
'[ActivityPage] auto scan completed, reason=$reason, '
|
||
'scanned=${result.scannedFiles}, '
|
||
'inserted=${result.inserted}, '
|
||
'updated=${result.updated}, '
|
||
'skipped=${result.skippedUnchanged}, '
|
||
'errors=${result.errors.length}',
|
||
);
|
||
|
||
if (!mounted) return;
|
||
|
||
await _loadDbActivities();
|
||
} catch (e, st) {
|
||
debugPrint('[ActivityPage] auto scan failed, reason=$reason, error=$e');
|
||
debugPrint('[ActivityPage] auto scan stack=$st');
|
||
} finally {
|
||
_activityAutoScanRunning = false;
|
||
}
|
||
}
|
||
|
||
Future<Uint8List> _readPickedFileBytes(PlatformFile pickedFile) async {
|
||
final bytes = pickedFile.bytes;
|
||
if (bytes != null) return bytes;
|
||
|
||
final path = pickedFile.path;
|
||
if (path == null) {
|
||
throw Exception('percorso file non disponibile');
|
||
}
|
||
|
||
return File(path).readAsBytes();
|
||
}
|
||
|
||
void _openActivity(ActivityTrack activity) {
|
||
Navigator.of(context).push(
|
||
MaterialPageRoute(
|
||
builder: (_) => ActivityMapPage(activity: activity),
|
||
),
|
||
);
|
||
}
|
||
|
||
Future<void> _openDbActivity(ActivityEntry entry) async {
|
||
try {
|
||
final bytes = await storageService.readActivityFile(entry.uri);
|
||
|
||
if (bytes.isEmpty) {
|
||
if (!mounted) return;
|
||
_showMessage('Impossibile leggere il file attività.');
|
||
return;
|
||
}
|
||
|
||
final activity = await ActivityFileParser.parse(
|
||
bytes,
|
||
fileName: entry.fileName,
|
||
);
|
||
|
||
if (activity == null || activity.isEmpty) {
|
||
if (!mounted) return;
|
||
_showMessage('Nessuna traccia GPS valida trovata nel file.');
|
||
return;
|
||
}
|
||
|
||
if (!mounted) return;
|
||
|
||
_openActivity(activity);
|
||
} catch (e) {
|
||
if (!mounted) return;
|
||
_showMessage('Errore apertura attività: $e');
|
||
}
|
||
}
|
||
|
||
Future<void> _showDbRecalculateDialog(ActivityEntry entry) async {
|
||
try {
|
||
final bytes = await storageService.readActivityFile(entry.uri);
|
||
|
||
if (bytes.isEmpty) {
|
||
if (!mounted) return;
|
||
_showMessage('Impossibile leggere il file attività.');
|
||
return;
|
||
}
|
||
|
||
final activity = await ActivityFileParser.parse(
|
||
bytes,
|
||
fileName: entry.fileName,
|
||
);
|
||
|
||
if (activity == null || activity.isEmpty) {
|
||
if (!mounted) return;
|
||
_showMessage('Nessuna traccia GPS valida trovata nel file.');
|
||
return;
|
||
}
|
||
|
||
if (!mounted) return;
|
||
|
||
showActivityRecalculateDialog(
|
||
context: context,
|
||
activity: activity,
|
||
);
|
||
} catch (e) {
|
||
if (!mounted) return;
|
||
|
||
await _showActivityPermissionDialog(
|
||
title: 'Errore ricalcolo attività',
|
||
message: 'Impossibile ricalcolare l’attività:\n\n$e',
|
||
);
|
||
}
|
||
}
|
||
|
||
void _showMessage(String message) {
|
||
ScaffoldMessenger.maybeOf(context)?.showSnackBar(
|
||
SnackBar(
|
||
content: Text(message),
|
||
),
|
||
);
|
||
}
|
||
|
||
Future<void> _showActivityPermissionDialog({
|
||
required String title,
|
||
required String message,
|
||
}) async {
|
||
if (!mounted) return;
|
||
|
||
await showDialog<void>(
|
||
context: context,
|
||
builder: (context) {
|
||
return AlertDialog(
|
||
title: Text(title),
|
||
content: SingleChildScrollView(
|
||
child: Text(message),
|
||
),
|
||
actions: [
|
||
TextButton(
|
||
onPressed: () => Navigator.of(context).pop(),
|
||
child: const Text('OK'),
|
||
),
|
||
],
|
||
);
|
||
},
|
||
);
|
||
}
|
||
|
||
Widget _buildActivityListBody() {
|
||
if (_dbActivities.isEmpty && _activities.isEmpty) {
|
||
return Center(
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Text(_emptyActivityMessage()),
|
||
const SizedBox(height: 12),
|
||
TextButton.icon(
|
||
onPressed: _pickActivityFile,
|
||
icon: const Icon(Icons.upload_file),
|
||
label: const Text('Apri file GPX, TCX o FIT'),
|
||
),
|
||
const SizedBox(height: 8),
|
||
TextButton.icon(
|
||
onPressed: _pickActivityDirectory,
|
||
icon: const Icon(Icons.folder_open),
|
||
label: const Text('Scegli cartella attività'),
|
||
),
|
||
const SizedBox(height: 8),
|
||
TextButton.icon(
|
||
onPressed: _scanActivityLibrary,
|
||
icon: const Icon(Icons.sync),
|
||
label: const Text('Scansiona attività'),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
return RefreshIndicator(
|
||
onRefresh: _loadDbActivities,
|
||
child: ListView.separated(
|
||
itemCount: _dbActivities.length + _activities.length,
|
||
separatorBuilder: (context, index) => const Divider(height: 1),
|
||
itemBuilder: (context, index) {
|
||
if (index < _dbActivities.length) {
|
||
final entry = _dbActivities[index];
|
||
|
||
final points = entry.id == null ? const <ActivityPreviewPoint>[] : _dbPreviewPoints[entry.id!] ?? const <ActivityPreviewPoint>[];
|
||
|
||
return ActivityEntryListTile(
|
||
entry: entry,
|
||
points: points,
|
||
dateLabel: ActivityFormatters.entryDate(entry),
|
||
distanceLabel: ActivityFormatters.distance(entry.distanceMeters),
|
||
elevationLabel: ActivityFormatters.entryElevation(entry.elevationGainMeters),
|
||
durationLabel: ActivityFormatters.entryDuration(entry.durationMillis),
|
||
onTap: () => _openDbActivity(entry),
|
||
onToggleFavorite: () => _toggleActivityFavorite(entry),
|
||
onRename: () => _renameActivity(entry),
|
||
onRecalculate: () => _showDbRecalculateDialog(entry),
|
||
onInfo: () => showActivityDbFieldsDialog(
|
||
context: context,
|
||
entry: entry,
|
||
),
|
||
);
|
||
}
|
||
|
||
final manualIndex = index - _dbActivities.length;
|
||
final activity = _activities[manualIndex];
|
||
|
||
return ActivityTrackListTile(
|
||
activity: activity,
|
||
onTap: () => _openActivity(activity),
|
||
onRecalculate: () => showActivityRecalculateDialog(
|
||
context: context,
|
||
activity: activity,
|
||
),
|
||
);
|
||
},
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildBody() {
|
||
if (_loadingDbActivities) {
|
||
return const Center(
|
||
child: CircularProgressIndicator(),
|
||
);
|
||
}
|
||
|
||
switch (_activityViewMode) {
|
||
case ActivityViewMode.activities:
|
||
return _buildActivityListBody();
|
||
|
||
case ActivityViewMode.sports:
|
||
return ActivitySportGroupsView(
|
||
groups: _sportGroups,
|
||
previewPointsByActivityId: _dbPreviewPoints,
|
||
onRefresh: _loadSportGroups,
|
||
onTap: _openSportGroup,
|
||
);
|
||
|
||
case ActivityViewMode.albums:
|
||
return ActivityAlbumGroupsView(
|
||
groups: _albumGroups,
|
||
previewPointsByActivityId: _dbPreviewPoints,
|
||
onRefresh: _loadAlbumGroups,
|
||
onTap: _openAlbumGroup,
|
||
);
|
||
}
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Scaffold(
|
||
resizeToAvoidBottomInset: false,
|
||
appBar: AppBar(
|
||
title: const Text('Attività'),
|
||
actions: [
|
||
IconButton(
|
||
icon: const Icon(Icons.folder_open),
|
||
tooltip: 'Scegli cartella attività',
|
||
onPressed: _pickActivityDirectory,
|
||
),
|
||
IconButton(
|
||
icon: const Icon(Icons.sync),
|
||
tooltip: 'Scansiona attività',
|
||
onPressed: _scanActivityLibrary,
|
||
),
|
||
IconButton(
|
||
icon: const Icon(Icons.add),
|
||
tooltip: 'Apri file attività',
|
||
onPressed: _pickActivityFile,
|
||
),
|
||
],
|
||
),
|
||
bottomNavigationBar: AppBottomNavBar(
|
||
events: _navBarEvents.stream,
|
||
mode: AppBottomNavBarMode.activity,
|
||
activityIndex: _activityNavIndex,
|
||
onActivityIndexSelected: _onActivityNavSelected,
|
||
),
|
||
body: _buildBody(),
|
||
);
|
||
}
|
||
}
|