#98 info: owner value alignment, empty icon collapse
This commit is contained in:
parent
44ed934a8c
commit
988bc0093e
9 changed files with 183 additions and 185 deletions
|
@ -759,7 +759,7 @@
|
||||||
"viewerInfoLabelUri": "URI",
|
"viewerInfoLabelUri": "URI",
|
||||||
"viewerInfoLabelPath": "Path",
|
"viewerInfoLabelPath": "Path",
|
||||||
"viewerInfoLabelDuration": "Duration",
|
"viewerInfoLabelDuration": "Duration",
|
||||||
"viewerInfoLabelOwner": "Owned by",
|
"viewerInfoLabelOwner": "Owner",
|
||||||
"viewerInfoLabelCoordinates": "Coordinates",
|
"viewerInfoLabelCoordinates": "Coordinates",
|
||||||
"viewerInfoLabelAddress": "Address",
|
"viewerInfoLabelAddress": "Address",
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'package:aves/app_mode.dart';
|
||||||
|
import 'package:aves/image_providers/app_icon_image_provider.dart';
|
||||||
import 'package:aves/model/actions/entry_info_actions.dart';
|
import 'package:aves/model/actions/entry_info_actions.dart';
|
||||||
import 'package:aves/model/entry.dart';
|
import 'package:aves/model/entry.dart';
|
||||||
import 'package:aves/model/favourites.dart';
|
import 'package:aves/model/favourites.dart';
|
||||||
|
@ -7,16 +9,19 @@ import 'package:aves/model/filters/mime.dart';
|
||||||
import 'package:aves/model/filters/rating.dart';
|
import 'package:aves/model/filters/rating.dart';
|
||||||
import 'package:aves/model/filters/tag.dart';
|
import 'package:aves/model/filters/tag.dart';
|
||||||
import 'package:aves/model/filters/type.dart';
|
import 'package:aves/model/filters/type.dart';
|
||||||
|
import 'package:aves/model/settings/settings.dart';
|
||||||
import 'package:aves/model/source/collection_lens.dart';
|
import 'package:aves/model/source/collection_lens.dart';
|
||||||
|
import 'package:aves/services/common/services.dart';
|
||||||
import 'package:aves/theme/colors.dart';
|
import 'package:aves/theme/colors.dart';
|
||||||
import 'package:aves/theme/format.dart';
|
import 'package:aves/theme/format.dart';
|
||||||
|
import 'package:aves/utils/android_file_utils.dart';
|
||||||
import 'package:aves/utils/file_utils.dart';
|
import 'package:aves/utils/file_utils.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
|
import 'package:aves/widgets/common/identity/aves_filter_chip.dart';
|
||||||
import 'package:aves/widgets/viewer/action/entry_info_action_delegate.dart';
|
import 'package:aves/widgets/viewer/action/entry_info_action_delegate.dart';
|
||||||
import 'package:aves/widgets/viewer/info/common.dart';
|
import 'package:aves/widgets/viewer/info/common.dart';
|
||||||
import 'package:aves/widgets/viewer/info/owner.dart';
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
@ -36,46 +41,15 @@ class BasicSection extends StatelessWidget {
|
||||||
required this.onFilter,
|
required this.onFilter,
|
||||||
});
|
});
|
||||||
|
|
||||||
int get megaPixels => entry.megaPixels;
|
|
||||||
|
|
||||||
bool get showMegaPixels => entry.isPhoto && megaPixels > 0;
|
|
||||||
|
|
||||||
String get rasterResolutionText => '${entry.resolutionText}${showMegaPixels ? ' • $megaPixels MP' : ''}';
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final l10n = context.l10n;
|
|
||||||
final infoUnknown = l10n.viewerInfoUnknown;
|
|
||||||
final locale = l10n.localeName;
|
|
||||||
final use24hour = context.select<MediaQueryData, bool>((v) => v.alwaysUse24HourFormat);
|
|
||||||
|
|
||||||
return AnimatedBuilder(
|
return AnimatedBuilder(
|
||||||
animation: entry.metadataChangeNotifier,
|
animation: entry.metadataChangeNotifier,
|
||||||
builder: (context, child) {
|
builder: (context, child) {
|
||||||
// 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 date = entry.bestDate;
|
|
||||||
final dateText = date != null ? formatDateTime(date, locale, use24hour) : infoUnknown;
|
|
||||||
final showResolution = !entry.isSvg && entry.isSized;
|
|
||||||
final sizeText = entry.sizeBytes != null ? formatFileSize(locale, entry.sizeBytes!) : infoUnknown;
|
|
||||||
final path = entry.path;
|
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
InfoRowGroup(
|
_BasicInfo(entry: entry),
|
||||||
info: {
|
|
||||||
l10n.viewerInfoLabelTitle: title,
|
|
||||||
l10n.viewerInfoLabelDate: dateText,
|
|
||||||
if (entry.isVideo) ..._buildVideoRows(context),
|
|
||||||
if (showResolution) l10n.viewerInfoLabelResolution: rasterResolutionText,
|
|
||||||
l10n.viewerInfoLabelSize: sizeText,
|
|
||||||
if (!entry.trashed) l10n.viewerInfoLabelUri: entry.uri,
|
|
||||||
if (path != null) l10n.viewerInfoLabelPath: path,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (!entry.trashed) OwnerProp(entry: entry),
|
|
||||||
_buildChips(context),
|
_buildChips(context),
|
||||||
_buildEditButtons(context),
|
_buildEditButtons(context),
|
||||||
],
|
],
|
||||||
|
@ -184,10 +158,130 @@ class BasicSection extends StatelessWidget {
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BasicInfo extends StatefulWidget {
|
||||||
|
final AvesEntry entry;
|
||||||
|
|
||||||
|
const _BasicInfo({
|
||||||
|
required this.entry,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_BasicInfo> createState() => _BasicInfoState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _BasicInfoState extends State<_BasicInfo> {
|
||||||
|
Future<String?> _ownerPackageLoader = SynchronousFuture(null);
|
||||||
|
Future<void> _appNameLoader = SynchronousFuture(null);
|
||||||
|
|
||||||
|
AvesEntry get entry => widget.entry;
|
||||||
|
|
||||||
|
int get megaPixels => entry.megaPixels;
|
||||||
|
|
||||||
|
bool get showMegaPixels => entry.isPhoto && megaPixels > 0;
|
||||||
|
|
||||||
|
String get rasterResolutionText => '${entry.resolutionText}${showMegaPixels ? ' • $megaPixels MP' : ''}';
|
||||||
|
|
||||||
|
static const ownerPackageNamePropKey = 'owner_package_name';
|
||||||
|
static const iconSize = 20.0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
if (!entry.trashed) {
|
||||||
|
final isMediaContent = entry.uri.startsWith('content://media/external/');
|
||||||
|
if (isMediaContent) {
|
||||||
|
_ownerPackageLoader = metadataFetchService.hasContentResolverProp(ownerPackageNamePropKey).then((exists) {
|
||||||
|
return exists ? metadataFetchService.getContentResolverProp(entry, ownerPackageNamePropKey) : SynchronousFuture(null);
|
||||||
|
});
|
||||||
|
final isViewerMode = context.read<ValueNotifier<AppMode>>().value == AppMode.view;
|
||||||
|
if (isViewerMode && settings.isInstalledAppAccessAllowed) {
|
||||||
|
_appNameLoader = androidFileUtils.initAppNames();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final l10n = context.l10n;
|
||||||
|
final infoUnknown = l10n.viewerInfoUnknown;
|
||||||
|
final locale = l10n.localeName;
|
||||||
|
final use24hour = context.select<MediaQueryData, bool>((v) => v.alwaysUse24HourFormat);
|
||||||
|
|
||||||
|
// 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 date = entry.bestDate;
|
||||||
|
final dateText = date != null ? formatDateTime(date, locale, use24hour) : infoUnknown;
|
||||||
|
final showResolution = !entry.isSvg && entry.isSized;
|
||||||
|
final sizeText = entry.sizeBytes != null ? formatFileSize(locale, entry.sizeBytes!) : infoUnknown;
|
||||||
|
final path = entry.path;
|
||||||
|
|
||||||
|
return FutureBuilder<String?>(
|
||||||
|
future: _ownerPackageLoader,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
final ownerPackage = snapshot.data;
|
||||||
|
return FutureBuilder<void>(
|
||||||
|
future: _appNameLoader,
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
return InfoRowGroup(
|
||||||
|
info: {
|
||||||
|
l10n.viewerInfoLabelTitle: title,
|
||||||
|
l10n.viewerInfoLabelDate: dateText,
|
||||||
|
if (entry.isVideo) ..._buildVideoRows(context),
|
||||||
|
if (showResolution) l10n.viewerInfoLabelResolution: rasterResolutionText,
|
||||||
|
l10n.viewerInfoLabelSize: sizeText,
|
||||||
|
if (!entry.trashed) l10n.viewerInfoLabelUri: entry.uri,
|
||||||
|
if (path != null) l10n.viewerInfoLabelPath: path,
|
||||||
|
if (ownerPackage != null) l10n.viewerInfoLabelOwner: ownerPackage,
|
||||||
|
},
|
||||||
|
spanBuilders: {
|
||||||
|
l10n.viewerInfoLabelOwner: _ownerHandler(ownerPackage),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Map<String, String> _buildVideoRows(BuildContext context) {
|
Map<String, String> _buildVideoRows(BuildContext context) {
|
||||||
return {
|
return {
|
||||||
context.l10n.viewerInfoLabelDuration: entry.durationText,
|
context.l10n.viewerInfoLabelDuration: entry.durationText,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
InfoValueSpanBuilder _ownerHandler(String? ownerPackage) {
|
||||||
|
if (ownerPackage == null) return (context, key, value) => [];
|
||||||
|
|
||||||
|
final appName = androidFileUtils.getCurrentAppName(ownerPackage) ?? ownerPackage;
|
||||||
|
return (context, key, value) => [
|
||||||
|
WidgetSpan(
|
||||||
|
alignment: PlaceholderAlignment.middle,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsetsDirectional.only(end: 4),
|
||||||
|
child: ConstrainedBox(
|
||||||
|
// use constraints instead of sizing `Image`,
|
||||||
|
// so that it can collapse when handling an empty image
|
||||||
|
constraints: const BoxConstraints(
|
||||||
|
maxWidth: iconSize,
|
||||||
|
maxHeight: iconSize,
|
||||||
|
),
|
||||||
|
child: Image(
|
||||||
|
image: AppIconImage(
|
||||||
|
packageName: ownerPackage,
|
||||||
|
size: iconSize,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: appName,
|
||||||
|
style: InfoRowGroup.valueStyle,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,7 +43,7 @@ class SectionRow extends StatelessWidget {
|
||||||
class InfoRowGroup extends StatefulWidget {
|
class InfoRowGroup extends StatefulWidget {
|
||||||
final Map<String, String> info;
|
final Map<String, String> info;
|
||||||
final int maxValueLength;
|
final int maxValueLength;
|
||||||
final Map<String, InfoLinkHandler>? linkHandlers;
|
final Map<String, InfoValueSpanBuilder> spanBuilders;
|
||||||
|
|
||||||
static const keyValuePadding = 16;
|
static const keyValuePadding = 16;
|
||||||
static const fontSize = 13.0;
|
static const fontSize = 13.0;
|
||||||
|
@ -56,11 +56,28 @@ class InfoRowGroup extends StatefulWidget {
|
||||||
super.key,
|
super.key,
|
||||||
required this.info,
|
required this.info,
|
||||||
this.maxValueLength = 0,
|
this.maxValueLength = 0,
|
||||||
this.linkHandlers,
|
Map<String, InfoValueSpanBuilder>? spanBuilders,
|
||||||
});
|
}) : spanBuilders = spanBuilders ?? const {};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<InfoRowGroup> createState() => _InfoRowGroupState();
|
State<InfoRowGroup> createState() => _InfoRowGroupState();
|
||||||
|
|
||||||
|
static InfoValueSpanBuilder linkSpanBuilder({
|
||||||
|
required String Function(BuildContext context) linkText,
|
||||||
|
required void Function(BuildContext context) onTap,
|
||||||
|
}) {
|
||||||
|
return (context, key, value) {
|
||||||
|
value = linkText(context);
|
||||||
|
// open link on tap
|
||||||
|
final recognizer = TapGestureRecognizer()..onTap = () => onTap(context);
|
||||||
|
// `colorScheme.secondary` is overridden upstream as an `ExpansionTileCard` theming workaround,
|
||||||
|
// so we use `colorScheme.primary` instead
|
||||||
|
final linkColor = Theme.of(context).colorScheme.primary;
|
||||||
|
final style = InfoRowGroup.valueStyle.copyWith(color: linkColor, decoration: TextDecoration.underline);
|
||||||
|
|
||||||
|
return [TextSpan(text: '${Constants.fsi}$value${Constants.pdi}', style: style, recognizer: recognizer)];
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _InfoRowGroupState extends State<InfoRowGroup> {
|
class _InfoRowGroupState extends State<InfoRowGroup> {
|
||||||
|
@ -70,7 +87,7 @@ class _InfoRowGroupState extends State<InfoRowGroup> {
|
||||||
|
|
||||||
int get maxValueLength => widget.maxValueLength;
|
int get maxValueLength => widget.maxValueLength;
|
||||||
|
|
||||||
Map<String, InfoLinkHandler>? get linkHandlers => widget.linkHandlers;
|
Map<String, InfoValueSpanBuilder> get spanBuilders => widget.spanBuilders;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
@ -94,34 +111,8 @@ class _InfoRowGroupState extends State<InfoRowGroup> {
|
||||||
children: keyValues.entries.expand(
|
children: keyValues.entries.expand(
|
||||||
(kv) {
|
(kv) {
|
||||||
final key = kv.key;
|
final key = kv.key;
|
||||||
String value;
|
final value = kv.value;
|
||||||
TextStyle? style;
|
final spanBuilder = spanBuilders[key] ?? _buildTextValueSpans;
|
||||||
GestureRecognizer? recognizer;
|
|
||||||
|
|
||||||
if (linkHandlers?.containsKey(key) == true) {
|
|
||||||
final handler = linkHandlers![key]!;
|
|
||||||
value = handler.linkText(context);
|
|
||||||
// open link on tap
|
|
||||||
recognizer = TapGestureRecognizer()..onTap = () => handler.onTap(context);
|
|
||||||
// `colorScheme.secondary` is overridden upstream as an `ExpansionTileCard` theming workaround,
|
|
||||||
// so we use `colorScheme.primary` instead
|
|
||||||
final linkColor = Theme.of(context).colorScheme.primary;
|
|
||||||
style = InfoRowGroup.valueStyle.copyWith(color: linkColor, decoration: TextDecoration.underline);
|
|
||||||
} else {
|
|
||||||
value = kv.value;
|
|
||||||
// long values are clipped, and made expandable by tapping them
|
|
||||||
final showPreviewOnly = maxValueLength > 0 && value.length > maxValueLength && !_expandedKeys.contains(key);
|
|
||||||
if (showPreviewOnly) {
|
|
||||||
value = '${value.substring(0, maxValueLength)}…';
|
|
||||||
// show full value on tap
|
|
||||||
recognizer = TapGestureRecognizer()..onTap = () => setState(() => _expandedKeys.add(key));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (key != lastKey) {
|
|
||||||
value = '$value\n';
|
|
||||||
}
|
|
||||||
|
|
||||||
final thisSpaceSize = max(0.0, (baseValueX - keySizes[key]!)) + InfoRowGroup.keyValuePadding;
|
final thisSpaceSize = max(0.0, (baseValueX - keySizes[key]!)) + InfoRowGroup.keyValuePadding;
|
||||||
|
|
||||||
// each text span embeds and pops a Bidi isolate,
|
// each text span embeds and pops a Bidi isolate,
|
||||||
|
@ -138,7 +129,8 @@ class _InfoRowGroupState extends State<InfoRowGroup> {
|
||||||
child: const Text(''),
|
child: const Text(''),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
TextSpan(text: '${Constants.fsi}$value${Constants.pdi}', style: style, recognizer: recognizer),
|
...spanBuilder(context, key, value),
|
||||||
|
if (key != lastKey) const TextSpan(text: '\n'),
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
).toList(),
|
).toList(),
|
||||||
|
@ -157,14 +149,20 @@ class _InfoRowGroupState extends State<InfoRowGroup> {
|
||||||
)..layout(const BoxConstraints(), parentUsesSize: true);
|
)..layout(const BoxConstraints(), parentUsesSize: true);
|
||||||
return para.getMaxIntrinsicWidth(double.infinity);
|
return para.getMaxIntrinsicWidth(double.infinity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<InlineSpan> _buildTextValueSpans(BuildContext context, String key, String value) {
|
||||||
|
GestureRecognizer? recognizer;
|
||||||
|
|
||||||
|
// long values are clipped, and made expandable by tapping them
|
||||||
|
final showPreviewOnly = maxValueLength > 0 && value.length > maxValueLength && !_expandedKeys.contains(key);
|
||||||
|
if (showPreviewOnly) {
|
||||||
|
value = '${value.substring(0, maxValueLength)}…';
|
||||||
|
// show full value on tap
|
||||||
|
recognizer = TapGestureRecognizer()..onTap = () => setState(() => _expandedKeys.add(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
return [TextSpan(text: '${Constants.fsi}$value${Constants.pdi}', recognizer: recognizer)];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class InfoLinkHandler {
|
typedef InfoValueSpanBuilder = List<InlineSpan> Function(BuildContext context, String key, String value);
|
||||||
final String Function(BuildContext context) linkText;
|
|
||||||
final void Function(BuildContext context) onTap;
|
|
||||||
|
|
||||||
const InfoLinkHandler({
|
|
||||||
required this.linkText,
|
|
||||||
required this.onTap,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
|
@ -50,7 +50,7 @@ class MetadataDirTile extends StatelessWidget {
|
||||||
initiallyExpanded: initiallyExpanded,
|
initiallyExpanded: initiallyExpanded,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
Map<String, InfoLinkHandler>? linkHandlers;
|
Map<String, InfoValueSpanBuilder>? linkHandlers;
|
||||||
switch (dirName) {
|
switch (dirName) {
|
||||||
case SvgMetadataService.metadataDirectory:
|
case SvgMetadataService.metadataDirectory:
|
||||||
linkHandlers = getSvgLinkHandlers(tags);
|
linkHandlers = getSvgLinkHandlers(tags);
|
||||||
|
@ -79,7 +79,7 @@ class MetadataDirTile extends StatelessWidget {
|
||||||
child: InfoRowGroup(
|
child: InfoRowGroup(
|
||||||
info: tags,
|
info: tags,
|
||||||
maxValueLength: Constants.infoGroupMaxValueLength,
|
maxValueLength: Constants.infoGroupMaxValueLength,
|
||||||
linkHandlers: linkHandlers,
|
spanBuilders: linkHandlers,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -87,9 +87,9 @@ class MetadataDirTile extends StatelessWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static Map<String, InfoLinkHandler> getSvgLinkHandlers(SplayTreeMap<String, String> tags) {
|
static Map<String, InfoValueSpanBuilder> getSvgLinkHandlers(SplayTreeMap<String, String> tags) {
|
||||||
return {
|
return {
|
||||||
'Metadata': InfoLinkHandler(
|
'Metadata': InfoRowGroup.linkSpanBuilder(
|
||||||
linkText: (context) => context.l10n.viewerInfoViewXmlLinkText,
|
linkText: (context) => context.l10n.viewerInfoViewXmlLinkText,
|
||||||
onTap: (context) {
|
onTap: (context) {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
|
@ -106,9 +106,9 @@ class MetadataDirTile extends StatelessWidget {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static Map<String, InfoLinkHandler> getVideoCoverLinkHandlers(SplayTreeMap<String, String> tags) {
|
static Map<String, InfoValueSpanBuilder> getVideoCoverLinkHandlers(SplayTreeMap<String, String> tags) {
|
||||||
return {
|
return {
|
||||||
'Image': InfoLinkHandler(
|
'Image': InfoRowGroup.linkSpanBuilder(
|
||||||
linkText: (context) => context.l10n.viewerInfoOpenLinkText,
|
linkText: (context) => context.l10n.viewerInfoOpenLinkText,
|
||||||
onTap: (context) => OpenEmbeddedDataNotification.videoCover().dispatch(context),
|
onTap: (context) => OpenEmbeddedDataNotification.videoCover().dispatch(context),
|
||||||
),
|
),
|
||||||
|
|
|
@ -145,7 +145,7 @@ class XmpNamespace extends Equatable {
|
||||||
InfoRowGroup(
|
InfoRowGroup(
|
||||||
info: Map.fromEntries(props.map((prop) => MapEntry(prop.displayKey, formatValue(prop)))),
|
info: Map.fromEntries(props.map((prop) => MapEntry(prop.displayKey, formatValue(prop)))),
|
||||||
maxValueLength: Constants.infoGroupMaxValueLength,
|
maxValueLength: Constants.infoGroupMaxValueLength,
|
||||||
linkHandlers: linkifyValues(props),
|
spanBuilders: linkifyValues(props),
|
||||||
),
|
),
|
||||||
...buildFromExtractedData(),
|
...buildFromExtractedData(),
|
||||||
];
|
];
|
||||||
|
@ -194,7 +194,7 @@ class XmpNamespace extends Equatable {
|
||||||
|
|
||||||
String formatValue(XmpProp prop) => prop.value;
|
String formatValue(XmpProp prop) => prop.value;
|
||||||
|
|
||||||
Map<String, InfoLinkHandler> linkifyValues(List<XmpProp> props) => {};
|
Map<String, InfoValueSpanBuilder> linkifyValues(List<XmpProp> props) => {};
|
||||||
}
|
}
|
||||||
|
|
||||||
class XmpProp {
|
class XmpProp {
|
||||||
|
|
|
@ -13,7 +13,7 @@ abstract class XmpGoogleNamespace extends XmpNamespace {
|
||||||
List<Tuple2<String, String>> get dataProps;
|
List<Tuple2<String, String>> get dataProps;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Map<String, InfoLinkHandler> linkifyValues(List<XmpProp> props) {
|
Map<String, InfoValueSpanBuilder> linkifyValues(List<XmpProp> props) {
|
||||||
return Map.fromEntries(dataProps.map((t) {
|
return Map.fromEntries(dataProps.map((t) {
|
||||||
final dataPropPath = t.item1;
|
final dataPropPath = t.item1;
|
||||||
final mimePropPath = t.item2;
|
final mimePropPath = t.item2;
|
||||||
|
@ -22,7 +22,7 @@ abstract class XmpGoogleNamespace extends XmpNamespace {
|
||||||
return (dataProp != null && mimeProp != null)
|
return (dataProp != null && mimeProp != null)
|
||||||
? MapEntry(
|
? MapEntry(
|
||||||
dataProp.displayKey,
|
dataProp.displayKey,
|
||||||
InfoLinkHandler(
|
InfoRowGroup.linkSpanBuilder(
|
||||||
linkText: (context) => context.l10n.viewerInfoOpenLinkText,
|
linkText: (context) => context.l10n.viewerInfoOpenLinkText,
|
||||||
onTap: (context) => OpenEmbeddedDataNotification.xmp(
|
onTap: (context) => OpenEmbeddedDataNotification.xmp(
|
||||||
propPath: dataProp.path,
|
propPath: dataProp.path,
|
||||||
|
|
|
@ -29,7 +29,7 @@ class XmpBasicNamespace extends XmpNamespace {
|
||||||
final struct = thumbnails[index]!;
|
final struct = thumbnails[index]!;
|
||||||
return {
|
return {
|
||||||
if (struct.containsKey(thumbnailDataDisplayKey))
|
if (struct.containsKey(thumbnailDataDisplayKey))
|
||||||
thumbnailDataDisplayKey: InfoLinkHandler(
|
thumbnailDataDisplayKey: InfoRowGroup.linkSpanBuilder(
|
||||||
linkText: (context) => context.l10n.viewerInfoOpenLinkText,
|
linkText: (context) => context.l10n.viewerInfoOpenLinkText,
|
||||||
onTap: (context) => OpenEmbeddedDataNotification.xmp(
|
onTap: (context) => OpenEmbeddedDataNotification.xmp(
|
||||||
propPath: 'xmp:Thumbnails[$index]/xmpGImg:image',
|
propPath: 'xmp:Thumbnails[$index]/xmpGImg:image',
|
||||||
|
|
|
@ -13,7 +13,7 @@ import 'package:flutter/material.dart';
|
||||||
class XmpStructArrayCard extends StatefulWidget {
|
class XmpStructArrayCard extends StatefulWidget {
|
||||||
final String title;
|
final String title;
|
||||||
late final List<Map<String, String>> structs;
|
late final List<Map<String, String>> structs;
|
||||||
final Map<String, InfoLinkHandler> Function(int index)? linkifier;
|
final Map<String, InfoValueSpanBuilder> Function(int index)? linkifier;
|
||||||
|
|
||||||
XmpStructArrayCard({
|
XmpStructArrayCard({
|
||||||
super.key,
|
super.key,
|
||||||
|
@ -95,7 +95,7 @@ class _XmpStructArrayCardState extends State<XmpStructArrayCard> {
|
||||||
child: InfoRowGroup(
|
child: InfoRowGroup(
|
||||||
info: structs[_index],
|
info: structs[_index],
|
||||||
maxValueLength: Constants.infoGroupMaxValueLength,
|
maxValueLength: Constants.infoGroupMaxValueLength,
|
||||||
linkHandlers: widget.linkifier?.call(_index + 1),
|
spanBuilders: widget.linkifier?.call(_index + 1),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -108,7 +108,7 @@ class _XmpStructArrayCardState extends State<XmpStructArrayCard> {
|
||||||
class XmpStructCard extends StatelessWidget {
|
class XmpStructCard extends StatelessWidget {
|
||||||
final String title;
|
final String title;
|
||||||
final Map<String, String> struct;
|
final Map<String, String> struct;
|
||||||
final Map<String, InfoLinkHandler> Function()? linkifier;
|
final Map<String, InfoValueSpanBuilder> Function()? linkifier;
|
||||||
|
|
||||||
static const cardMargin = EdgeInsets.symmetric(vertical: 8, horizontal: 0);
|
static const cardMargin = EdgeInsets.symmetric(vertical: 8, horizontal: 0);
|
||||||
|
|
||||||
|
@ -137,7 +137,7 @@ class XmpStructCard extends StatelessWidget {
|
||||||
InfoRowGroup(
|
InfoRowGroup(
|
||||||
info: struct,
|
info: struct,
|
||||||
maxValueLength: Constants.infoGroupMaxValueLength,
|
maxValueLength: Constants.infoGroupMaxValueLength,
|
||||||
linkHandlers: linkifier?.call(),
|
spanBuilders: linkifier?.call(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
@ -1,94 +0,0 @@
|
||||||
import 'package:aves/app_mode.dart';
|
|
||||||
import 'package:aves/image_providers/app_icon_image_provider.dart';
|
|
||||||
import 'package:aves/model/entry.dart';
|
|
||||||
import 'package:aves/model/settings/settings.dart';
|
|
||||||
import 'package:aves/services/common/services.dart';
|
|
||||||
import 'package:aves/utils/android_file_utils.dart';
|
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
|
||||||
import 'package:aves/widgets/viewer/info/common.dart';
|
|
||||||
import 'package:flutter/foundation.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:provider/provider.dart';
|
|
||||||
|
|
||||||
class OwnerProp extends StatefulWidget {
|
|
||||||
final AvesEntry entry;
|
|
||||||
|
|
||||||
const OwnerProp({
|
|
||||||
super.key,
|
|
||||||
required this.entry,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
State<OwnerProp> createState() => _OwnerPropState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _OwnerPropState extends State<OwnerProp> {
|
|
||||||
Future<String?> _ownerPackageLoader = SynchronousFuture(null);
|
|
||||||
Future<void> _appNameLoader = SynchronousFuture(null);
|
|
||||||
|
|
||||||
AvesEntry get entry => widget.entry;
|
|
||||||
|
|
||||||
static const ownerPackageNamePropKey = 'owner_package_name';
|
|
||||||
static const iconSize = 20.0;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
final isMediaContent = entry.uri.startsWith('content://media/external/');
|
|
||||||
if (isMediaContent) {
|
|
||||||
_ownerPackageLoader = metadataFetchService.hasContentResolverProp(ownerPackageNamePropKey).then((exists) {
|
|
||||||
return exists ? metadataFetchService.getContentResolverProp(entry, ownerPackageNamePropKey) : SynchronousFuture(null);
|
|
||||||
});
|
|
||||||
final isViewerMode = context.read<ValueNotifier<AppMode>>().value == AppMode.view;
|
|
||||||
if (isViewerMode && settings.isInstalledAppAccessAllowed) {
|
|
||||||
_appNameLoader = androidFileUtils.initAppNames();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return FutureBuilder<String?>(
|
|
||||||
future: _ownerPackageLoader,
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
final ownerPackage = snapshot.data;
|
|
||||||
if (ownerPackage == null) return const SizedBox();
|
|
||||||
|
|
||||||
return FutureBuilder<void>(
|
|
||||||
future: _appNameLoader,
|
|
||||||
builder: (context, snapshot) {
|
|
||||||
final appName = androidFileUtils.getCurrentAppName(ownerPackage) ?? ownerPackage;
|
|
||||||
return SelectableText.rich(
|
|
||||||
TextSpan(
|
|
||||||
children: [
|
|
||||||
TextSpan(
|
|
||||||
text: context.l10n.viewerInfoLabelOwner,
|
|
||||||
style: InfoRowGroup.keyStyle(context),
|
|
||||||
),
|
|
||||||
WidgetSpan(
|
|
||||||
alignment: PlaceholderAlignment.middle,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
|
||||||
child: Image(
|
|
||||||
image: AppIconImage(
|
|
||||||
packageName: ownerPackage,
|
|
||||||
size: iconSize,
|
|
||||||
),
|
|
||||||
width: iconSize,
|
|
||||||
height: iconSize,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TextSpan(
|
|
||||||
text: appName,
|
|
||||||
style: InfoRowGroup.valueStyle,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in a new issue