aves/lib/widgets/viewer/info/basic_section.dart
2021-04-26 17:15:32 +09:00

229 lines
7.5 KiB
Dart

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<bool> 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.isMotionPhoto) TypeFilter.motionPhoto,
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?.getAlbumDisplayName(context, album)),
...tags.map((tag) => TagFilter(tag)),
};
return AnimatedBuilder(
animation: favourites,
builder: (context, child) {
final effectiveFilters = [
...filters,
if (entry.isFavourite) FavouriteFilter.instance,
]..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<String, String> _buildVideoRows(BuildContext context) {
return {
context.l10n.viewerInfoLabelDuration: entry.durationText,
};
}
}
class OwnerProp extends StatefulWidget {
final AvesEntry entry;
final ValueNotifier<bool> visibleNotifier;
const OwnerProp({
@required this.entry,
@required this.visibleNotifier,
});
@override
_OwnerPropState createState() => _OwnerPropState();
}
class _OwnerPropState extends State<OwnerProp> {
final ValueNotifier<String> _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<String>(
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<void> _getOwner() async {
if (entry == null) return;
if (_loadedUri.value == entry.uri) return;
final isMediaContent = entry.uri.startsWith('content://media/external/');
if (isVisible && isMediaContent) {
_ownerPackage = await metadataService.getContentResolverProp(entry, 'owner_package_name');
_loadedUri.value = entry.uri;
} else {
_ownerPackage = null;
_loadedUri.value = null;
}
}
}