11import 'dart:async' ;
2+ import 'dart:convert' ;
3+ import 'dart:typed_data' ;
24
35import 'package:aves/model/entry/entry.dart' ;
6+ import 'package:aves/model/entry/extensions/location.dart' ;
47import 'package:aves/model/filters/covered/location.dart' ;
58import 'package:aves/model/filters/covered/stored_album.dart' ;
69import 'package:aves/model/filters/covered/tag.dart' ;
@@ -9,12 +12,16 @@ import 'package:aves/model/filters/rating.dart';
912import 'package:aves/model/settings/settings.dart' ;
1013import 'package:aves/model/source/collection_lens.dart' ;
1114import 'package:aves/model/source/collection_source.dart' ;
15+ import 'package:aves/ref/locales.dart' ;
16+ import 'package:aves/ref/mime_types.dart' ;
17+ import 'package:aves/services/common/services.dart' ;
1218import 'package:aves/theme/durations.dart' ;
1319import 'package:aves/theme/format.dart' ;
1420import 'package:aves/theme/icons.dart' ;
1521import 'package:aves/theme/styles.dart' ;
1622import 'package:aves/theme/themes.dart' ;
1723import 'package:aves/utils/file_utils.dart' ;
24+ import 'package:aves/view/src/metadata/exportable_fields.dart' ;
1825import 'package:aves/widgets/collection/collection_page.dart' ;
1926import 'package:aves/widgets/common/action_mixins/feedback.dart' ;
2027import 'package:aves/widgets/common/action_mixins/vault_aware.dart' ;
@@ -23,16 +30,20 @@ import 'package:aves/widgets/common/basic/scaffold.dart';
2330import 'package:aves/widgets/common/basic/tv_edge_focus.dart' ;
2431import 'package:aves/widgets/common/extensions/build_context.dart' ;
2532import 'package:aves/widgets/common/identity/empty.dart' ;
33+ import 'package:aves/widgets/dialogs/export_collection_stats_dialog.dart' ;
2634import 'package:aves/widgets/stats/date/histogram.dart' ;
2735import 'package:aves/widgets/stats/filter_table.dart' ;
2836import 'package:aves/widgets/stats/mime_donut.dart' ;
2937import 'package:aves/widgets/stats/percent_text.dart' ;
3038import 'package:aves/widgets/stats/top_page.dart' ;
3139import 'package:aves/widgets/viewer/controls/notifications.dart' ;
40+ import 'package:aves_model/aves_model.dart' ;
3241import 'package:collection/collection.dart' ;
42+ import 'package:csv/csv.dart' ;
3343import 'package:flutter/material.dart' ;
3444import 'package:flutter/scheduler.dart' ;
3545import 'package:flutter_staggered_animations/flutter_staggered_animations.dart' ;
46+ import 'package:intl/intl.dart' ;
3647import 'package:percent_indicator/linear_percent_indicator.dart' ;
3748import 'package:provider/provider.dart' ;
3849
@@ -211,6 +222,13 @@ class _StatsPageState extends State<StatsPage> with FeedbackMixin, VaultAwareMix
211222 appBar: AppBar (
212223 automaticallyImplyLeading: ! useTvLayout,
213224 title: Text (l10n.statsPageTitle),
225+ actions: [
226+ IconButton (
227+ icon: Icon (AIcons .fileExport),
228+ onPressed: () => _export (context),
229+ tooltip: context.l10n.settingsActionExport,
230+ ),
231+ ],
214232 ),
215233 body: GestureAreaProtectorStack (
216234 child: SafeArea (
@@ -378,6 +396,81 @@ class _StatsPageState extends State<StatsPage> with FeedbackMixin, VaultAwareMix
378396 (route) => false ,
379397 );
380398 }
399+
400+ Future <void > _export (BuildContext context) async {
401+ final options = await showDialog< (String , Set <ExportableEntryField >)> (
402+ context: context,
403+ builder: (context) => const ExportCollectionStatsDialog (),
404+ routeSettings: const RouteSettings (name: ExportCollectionStatsDialog .routeName),
405+ );
406+ if (options == null ) return ;
407+
408+ final (mimeType, fieldSet) = options;
409+ final index = ExportableEntryField .values.indexOf;
410+ final fieldList = fieldSet.sorted ((a, b) => index (a).compareTo (index (b)));
411+
412+ String body = '' ;
413+ switch (mimeType) {
414+ case MimeTypes .csv:
415+ body = _exportToCsv (fieldList, context);
416+ case MimeTypes .json:
417+ body = _exportToJson (fieldList);
418+ }
419+
420+ final success = await storageService.createFile (
421+ 'aves-stats-${DateFormat ('yyyyMMdd_HHmmss' , asciiLocale ).format (DateTime .now ())}${MimeTypes .extensionFor (mimeType )}' ,
422+ mimeType,
423+ Uint8List .fromList (utf8.encode (body)),
424+ );
425+ if (success != null ) {
426+ if (success) {
427+ showFeedback (context, FeedbackType .info, context.l10n.genericSuccessFeedback);
428+ } else {
429+ showFeedback (context, FeedbackType .warn, context.l10n.genericFailureFeedback);
430+ }
431+ }
432+ }
433+
434+ Object ? _exportEntryField (AvesEntry entry, ExportableEntryField field) {
435+ switch (field) {
436+ case ExportableEntryField .uri:
437+ return entry.uri;
438+ case ExportableEntryField .path:
439+ return entry.path;
440+ case ExportableEntryField .date:
441+ return entry.bestDate? .toIso8601String ();
442+ case ExportableEntryField .size:
443+ return entry.sizeBytes;
444+ case ExportableEntryField .width:
445+ return entry.displaySize.width.toInt ();
446+ case ExportableEntryField .height:
447+ return entry.displaySize.height.toInt ();
448+ case ExportableEntryField .duration:
449+ final durationMillis = entry.durationMillis ?? 0 ;
450+ return durationMillis > 0 ? durationMillis : null ;
451+ case ExportableEntryField .coordinates:
452+ final latLng = entry.latLng;
453+ return latLng != null ? '${latLng .latitude },${latLng .longitude }' : null ;
454+ case ExportableEntryField .address:
455+ final shortAddress = entry.shortAddress;
456+ return shortAddress.isNotEmpty ? shortAddress : null ;
457+ }
458+ }
459+
460+ String _exportToCsv (List <ExportableEntryField > fields, BuildContext context) {
461+ final headers = fields.map ((v) => v.getText (context)).toList ();
462+ List <String ?> toCsvValues (AvesEntry entry) => fields.map ((field) {
463+ return _exportEntryField (entry, field)? .toString ();
464+ }).toList ();
465+ return const ListToCsvConverter ().convert ([headers, ...entries.map (toCsvValues)], convertNullTo: '' );
466+ }
467+
468+ String _exportToJson (List <ExportableEntryField > fields) {
469+ Map <String , Object ?> toJsonMap (AvesEntry entry) => Map .fromEntries (fields.map ((field) {
470+ return MapEntry (field.name, _exportEntryField (entry, field));
471+ }));
472+ return jsonEncode (entries.map (toJsonMap).toList ());
473+ }
381474}
382475
383476class _LocationIndicator extends StatelessWidget {
0 commit comments