aves/lib/widgets/stats/mime_donut.dart
Thibault Deckers d5e702266f minor
2023-01-30 13:58:23 +01:00

185 lines
5.8 KiB
Dart

import 'dart:math';
import 'package:aves/model/filters/mime.dart';
import 'package:aves/model/settings/enums/accessibility_animations.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/theme/colors.dart';
import 'package:aves/theme/icons.dart';
import 'package:aves/utils/mime_utils.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
import 'package:charts_flutter/flutter.dart' as charts;
import 'package:collection/collection.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
class MimeDonut extends StatefulWidget {
final IconData icon;
final Map<String, int> byMimeTypes;
final Duration animationDuration;
final FilterCallback onFilterSelection;
const MimeDonut({
super.key,
required this.icon,
required this.byMimeTypes,
required this.animationDuration,
required this.onFilterSelection,
});
@override
State<MimeDonut> createState() => _MimeDonutState();
}
class _MimeDonutState extends State<MimeDonut> with AutomaticKeepAliveClientMixin {
Map<String, int> get byMimeTypes => widget.byMimeTypes;
static const mimeDonutMinWidth = 124.0;
@override
Widget build(BuildContext context) {
super.build(context);
if (byMimeTypes.isEmpty) return const SizedBox();
final l10n = context.l10n;
final locale = l10n.localeName;
final numberFormat = NumberFormat.decimalPattern(locale);
final sum = byMimeTypes.values.sum;
final colors = context.watch<AvesColorsData>();
final seriesData = byMimeTypes.entries.map((kv) {
final mimeType = kv.key;
final displayText = MimeUtils.displayType(mimeType);
return EntryByMimeDatum(
mimeType: mimeType,
displayText: displayText,
color: colors.fromString(displayText),
entryCount: kv.value,
);
}).toList();
seriesData.sort((d1, d2) {
final c = d2.entryCount.compareTo(d1.entryCount);
return c != 0 ? c : compareAsciiUpperCase(d1.displayText, d2.displayText);
});
final series = [
charts.Series<EntryByMimeDatum, String>(
id: 'mime',
colorFn: (d, i) => charts.ColorUtil.fromDartColor(d.color),
domainFn: (d, i) => d.displayText,
measureFn: (d, i) => d.entryCount,
data: seriesData,
labelAccessorFn: (d, _) => '${d.displayText}: ${d.entryCount}',
),
];
return LayoutBuilder(builder: (context, constraints) {
final textScaleFactor = MediaQuery.textScaleFactorOf(context);
final minWidth = mimeDonutMinWidth * textScaleFactor;
final availableWidth = constraints.maxWidth;
final dim = max(minWidth, availableWidth / (availableWidth > 4 * minWidth ? 4 : (availableWidth > 2 * minWidth ? 2 : 1)));
final donut = SizedBox(
width: dim,
height: dim,
child: Stack(
children: [
charts.PieChart(
series,
animate: context.select<Settings, bool>((v) => v.accessibilityAnimations.animate),
animationDuration: widget.animationDuration,
defaultRenderer: charts.ArcRendererConfig<String>(
arcWidth: 16,
),
),
Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(widget.icon),
Text(
numberFormat.format(sum),
textAlign: TextAlign.center,
),
],
),
),
],
),
);
final legend = SizedBox(
width: dim,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: seriesData
.map((d) => InkWell(
onTap: () => widget.onFilterSelection(MimeFilter(d.mimeType)),
borderRadius: const BorderRadius.all(Radius.circular(123)),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(AIcons.disc, color: d.color),
const SizedBox(width: 8),
Flexible(
child: Text(
d.displayText,
overflow: TextOverflow.fade,
softWrap: false,
maxLines: 1,
),
),
const SizedBox(width: 8),
Text(
numberFormat.format(d.entryCount),
style: TextStyle(
color: Theme.of(context).textTheme.bodySmall!.color,
),
),
const SizedBox(width: 4),
],
),
))
.toList(),
),
);
final children = [
donut,
legend,
];
return availableWidth > minWidth * 2
? Row(
mainAxisSize: MainAxisSize.min,
children: children,
)
: Column(
mainAxisSize: MainAxisSize.min,
children: children,
);
});
}
@override
bool get wantKeepAlive => true;
}
@immutable
class EntryByMimeDatum extends Equatable {
final String mimeType, displayText;
final Color color;
final int entryCount;
@override
List<Object?> get props => [mimeType, displayText, color, entryCount];
const EntryByMimeDatum({
required this.mimeType,
required this.displayText,
required this.color,
required this.entryCount,
});
}