import 'package:aves/image_providers/app_icon_image_provider.dart'; import 'package:aves/model/entry.dart'; import 'package:aves/model/favourites.dart'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/favourite.dart'; import 'package:aves/model/filters/mime.dart'; import 'package:aves/model/filters/tag.dart'; import 'package:aves/model/filters/type.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/services/services.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/file_utils.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:aves/widgets/viewer/info/common.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; class BasicSection extends StatelessWidget { final AvesEntry entry; final CollectionLens collection; final ValueNotifier visibleNotifier; final FilterCallback onFilter; const BasicSection({ Key key, @required this.entry, this.collection, @required this.visibleNotifier, @required this.onFilter, }) : super(key: key); int get megaPixels => entry.megaPixels; bool get showMegaPixels => entry.isPhoto && megaPixels != null && megaPixels > 0; String get rasterResolutionText => '${entry.resolutionText}${showMegaPixels ? ' • $megaPixels MP' : ''}'; @override Widget build(BuildContext context) { final l10n = context.l10n; final infoUnknown = l10n.viewerInfoUnknown; final date = entry.bestDate; final locale = l10n.localeName; final dateText = date != null ? '${DateFormat.yMMMd(locale).format(date)} • ${DateFormat.Hm(locale).format(date)}' : infoUnknown; // TODO TLAD line break on all characters for the following fields when this is fixed: https://github.com/flutter/flutter/issues/61081 // inserting ZWSP (\u200B) between characters does help, but it messes with width and height computation (another Flutter issue) final title = entry.bestTitle ?? infoUnknown; final uri = entry.uri ?? infoUnknown; final path = entry.path; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ InfoRowGroup({ l10n.viewerInfoLabelTitle: title, l10n.viewerInfoLabelDate: dateText, if (entry.isVideo) ..._buildVideoRows(context), if (!entry.isSvg && entry.isSized) l10n.viewerInfoLabelResolution: rasterResolutionText, l10n.viewerInfoLabelSize: entry.sizeBytes != null ? formatFilesize(entry.sizeBytes) : infoUnknown, l10n.viewerInfoLabelUri: uri, if (path != null) l10n.viewerInfoLabelPath: path, }), OwnerProp( entry: entry, visibleNotifier: visibleNotifier, ), _buildChips(context), ], ); } Widget _buildChips(BuildContext context) { final tags = entry.xmpSubjects..sort(compareAsciiUpperCase); final album = entry.directory; final filters = { MimeFilter(entry.mimeType), if (entry.isAnimated) TypeFilter.animated, if (entry.isGeotiff) TypeFilter.geotiff, if (entry.isImage && entry.is360) TypeFilter.panorama, if (entry.isVideo && entry.is360) TypeFilter.sphericalVideo, if (entry.isVideo && !entry.is360) MimeFilter.video, if (album != null) AlbumFilter(album, collection?.source?.getUniqueAlbumName(context, album)), ...tags.map((tag) => TagFilter(tag)), }; return AnimatedBuilder( animation: favourites, builder: (context, child) { final effectiveFilters = [ ...filters, if (entry.isFavourite) FavouriteFilter(), ]..sort(); if (effectiveFilters.isEmpty) return SizedBox.shrink(); return Padding( padding: EdgeInsets.symmetric(horizontal: AvesFilterChip.outlineWidth / 2) + EdgeInsets.only(top: 8), child: Wrap( spacing: 8, runSpacing: 8, children: effectiveFilters .map((filter) => AvesFilterChip( filter: filter, onTap: onFilter, )) .toList(), ), ); }, ); } Map _buildVideoRows(BuildContext context) { return { context.l10n.viewerInfoLabelDuration: entry.durationText, }; } } class OwnerProp extends StatefulWidget { final AvesEntry entry; final ValueNotifier visibleNotifier; const OwnerProp({ @required this.entry, @required this.visibleNotifier, }); @override _OwnerPropState createState() => _OwnerPropState(); } class _OwnerPropState extends State { final ValueNotifier _loadedUri = ValueNotifier(null); String _ownerPackage; AvesEntry get entry => widget.entry; bool get isVisible => widget.visibleNotifier.value; static const iconSize = 20.0; @override void initState() { super.initState(); _registerWidget(widget); _getOwner(); } @override void didUpdateWidget(covariant OwnerProp oldWidget) { super.didUpdateWidget(oldWidget); _unregisterWidget(oldWidget); _registerWidget(widget); _getOwner(); } @override void dispose() { _unregisterWidget(widget); super.dispose(); } void _registerWidget(OwnerProp widget) { widget.visibleNotifier.addListener(_getOwner); } void _unregisterWidget(OwnerProp widget) { widget.visibleNotifier.removeListener(_getOwner); } @override Widget build(BuildContext context) { return ValueListenableBuilder( valueListenable: _loadedUri, builder: (context, uri, child) { if (_ownerPackage == null) return SizedBox(); final appName = androidFileUtils.getCurrentAppName(_ownerPackage) ?? _ownerPackage; // as of Flutter v1.22.6, `SelectableText` cannot contain `WidgetSpan` // so be use a basic `Text` instead return Text.rich( TextSpan( children: [ TextSpan( text: context.l10n.viewerInfoLabelOwner, style: InfoRowGroup.keyStyle, ), // `com.android.shell` is the package reported // for images copied to the device by ADB for Test Driver if (_ownerPackage != 'com.android.shell') WidgetSpan( alignment: PlaceholderAlignment.middle, child: Padding( padding: EdgeInsets.symmetric(horizontal: 4), child: Image( image: AppIconImage( packageName: _ownerPackage, size: iconSize, ), width: iconSize, height: iconSize, ), ), ), TextSpan( text: appName, style: InfoRowGroup.baseStyle, ), ], ), ); }, ); } Future _getOwner() async { if (entry == null) return; if (_loadedUri.value == entry.uri) return; if (isVisible) { _ownerPackage = await metadataService.getContentResolverProp(widget.entry, 'owner_package_name'); _loadedUri.value = entry.uri; } else { _ownerPackage = null; _loadedUri.value = null; } } }