info: selectable text, use expansion panels for metadata directories
This commit is contained in:
parent
7958fa33eb
commit
0093b715d1
8 changed files with 179 additions and 190 deletions
|
@ -128,7 +128,7 @@ class CollectionLens with ChangeNotifier {
|
||||||
final ub = CollectionSource.getUniqueAlbumName(b, albums);
|
final ub = CollectionSource.getUniqueAlbumName(b, albums);
|
||||||
return compareAsciiUpperCase(ua, ub);
|
return compareAsciiUpperCase(ua, ub);
|
||||||
};
|
};
|
||||||
sections = Map.unmodifiable(SplayTreeMap.from(byAlbum, compare));
|
sections = Map.unmodifiable(SplayTreeMap.of(byAlbum, compare));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:aves/model/collection_lens.dart';
|
import 'package:aves/model/collection_lens.dart';
|
||||||
import 'package:aves/widgets/album/collection_scaling.dart';
|
import 'package:aves/widgets/album/collection_scaling.dart';
|
||||||
import 'package:aves/widgets/album/collection_section.dart';
|
import 'package:aves/widgets/album/collection_section.dart';
|
||||||
|
import 'package:aves/widgets/common/scroll_thumb.dart';
|
||||||
import 'package:draggable_scrollbar/draggable_scrollbar.dart';
|
import 'package:draggable_scrollbar/draggable_scrollbar.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
|
@ -52,7 +53,7 @@ class ThumbnailCollection extends StatelessWidget {
|
||||||
if (appBar != null) appBar,
|
if (appBar != null) appBar,
|
||||||
if (collection.isEmpty && emptyBuilder != null)
|
if (collection.isEmpty && emptyBuilder != null)
|
||||||
SliverFillViewport(
|
SliverFillViewport(
|
||||||
delegate: SliverChildListDelegate(
|
delegate: SliverChildListDelegate.fixed(
|
||||||
[emptyBuilder(context)],
|
[emptyBuilder(context)],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -73,9 +74,9 @@ class ThumbnailCollection extends StatelessWidget {
|
||||||
);
|
);
|
||||||
|
|
||||||
return DraggableScrollbar(
|
return DraggableScrollbar(
|
||||||
heightScrollThumb: 48,
|
heightScrollThumb: avesScrollThumbHeight,
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: Colors.white,
|
||||||
scrollThumbBuilder: _thumbArrowBuilder(false),
|
scrollThumbBuilder: avesScrollThumbBuilder(),
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
// padding to get scroll thumb below app bar, above nav bar
|
// padding to get scroll thumb below app bar, above nav bar
|
||||||
|
@ -91,47 +92,4 @@ class ThumbnailCollection extends StatelessWidget {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static ScrollThumbBuilder _thumbArrowBuilder(bool alwaysVisibleScrollThumb) {
|
|
||||||
return (
|
|
||||||
Color backgroundColor,
|
|
||||||
Animation<double> thumbAnimation,
|
|
||||||
Animation<double> labelAnimation,
|
|
||||||
double height, {
|
|
||||||
Widget labelText,
|
|
||||||
}) {
|
|
||||||
final scrollThumb = Container(
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.black26,
|
|
||||||
borderRadius: const BorderRadius.all(
|
|
||||||
Radius.circular(12.0),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
height: height,
|
|
||||||
margin: const EdgeInsets.only(right: .5),
|
|
||||||
padding: const EdgeInsets.all(2),
|
|
||||||
child: ClipPath(
|
|
||||||
child: Container(
|
|
||||||
width: 20.0,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: backgroundColor,
|
|
||||||
borderRadius: const BorderRadius.all(
|
|
||||||
Radius.circular(12.0),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
clipper: ArrowClipper(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
return DraggableScrollbar.buildScrollThumbAndLabel(
|
|
||||||
scrollThumb: scrollThumb,
|
|
||||||
backgroundColor: backgroundColor,
|
|
||||||
thumbAnimation: thumbAnimation,
|
|
||||||
labelAnimation: labelAnimation,
|
|
||||||
labelText: labelText,
|
|
||||||
alwaysVisibleScrollThumb: alwaysVisibleScrollThumb,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
47
lib/widgets/common/scroll_thumb.dart
Normal file
47
lib/widgets/common/scroll_thumb.dart
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import 'package:draggable_scrollbar/draggable_scrollbar.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
const double avesScrollThumbHeight = 48;
|
||||||
|
|
||||||
|
ScrollThumbBuilder avesScrollThumbBuilder() {
|
||||||
|
return (
|
||||||
|
Color backgroundColor,
|
||||||
|
Animation<double> thumbAnimation,
|
||||||
|
Animation<double> labelAnimation,
|
||||||
|
double height, {
|
||||||
|
Widget labelText,
|
||||||
|
}) {
|
||||||
|
final scrollThumb = Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black26,
|
||||||
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(12.0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
height: height,
|
||||||
|
margin: const EdgeInsets.only(right: .5),
|
||||||
|
padding: const EdgeInsets.all(2),
|
||||||
|
child: ClipPath(
|
||||||
|
child: Container(
|
||||||
|
width: 20.0,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: backgroundColor,
|
||||||
|
borderRadius: const BorderRadius.all(
|
||||||
|
Radius.circular(12.0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
clipper: ArrowClipper(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return DraggableScrollbar.buildScrollThumbAndLabel(
|
||||||
|
scrollThumb: scrollThumb,
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
|
thumbAnimation: thumbAnimation,
|
||||||
|
labelAnimation: labelAnimation,
|
||||||
|
labelText: labelText,
|
||||||
|
alwaysVisibleScrollThumb: false,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'dart:collection';
|
||||||
|
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/utils/file_utils.dart';
|
import 'package:aves/utils/file_utils.dart';
|
||||||
import 'package:aves/widgets/fullscreen/info/info_page.dart';
|
import 'package:aves/widgets/fullscreen/info/info_page.dart';
|
||||||
|
@ -19,25 +21,22 @@ class BasicSection extends StatelessWidget {
|
||||||
final showMegaPixels = !entry.isVideo && !entry.isGif && entry.megaPixels != null && entry.megaPixels > 0;
|
final showMegaPixels = !entry.isVideo && !entry.isGif && entry.megaPixels != null && entry.megaPixels > 0;
|
||||||
final resolutionText = '${entry.width ?? '?'} × ${entry.height ?? '?'}${showMegaPixels ? ' (${entry.megaPixels} MP)' : ''}';
|
final resolutionText = '${entry.width ?? '?'} × ${entry.height ?? '?'}${showMegaPixels ? ' (${entry.megaPixels} MP)' : ''}';
|
||||||
|
|
||||||
return Column(
|
return InfoRowGroup({
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
'Title': entry.title ?? '?',
|
||||||
children: [
|
'Date': dateText,
|
||||||
InfoRow('Title', entry.title ?? '?'),
|
|
||||||
InfoRow('Date', dateText),
|
|
||||||
if (entry.isVideo) ..._buildVideoRows(),
|
if (entry.isVideo) ..._buildVideoRows(),
|
||||||
InfoRow('Resolution', resolutionText),
|
'Resolution': resolutionText,
|
||||||
InfoRow('Size', entry.sizeBytes != null ? formatFilesize(entry.sizeBytes) : '?'),
|
'Size': entry.sizeBytes != null ? formatFilesize(entry.sizeBytes) : '?',
|
||||||
InfoRow('URI', entry.uri ?? '?'),
|
'URI': entry.uri ?? '?',
|
||||||
if (entry.path != null) InfoRow('Path', entry.path),
|
if (entry.path != null) 'Path': entry.path,
|
||||||
],
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Widget> _buildVideoRows() {
|
Map<String, String> _buildVideoRows() {
|
||||||
final rotation = entry.catalogMetadata?.videoRotation;
|
final rotation = entry.catalogMetadata?.videoRotation;
|
||||||
return [
|
return {
|
||||||
InfoRow('Duration', entry.durationText),
|
'Duration': entry.durationText,
|
||||||
if (rotation != null) InfoRow('Rotation', '$rotation°'),
|
if (rotation != null) 'Rotation': '$rotation°',
|
||||||
];
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,6 +44,16 @@ class InfoPageState extends State<InfoPage> {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
const horizontalPadding = EdgeInsets.symmetric(horizontal: 8);
|
const horizontalPadding = EdgeInsets.symmetric(horizontal: 8);
|
||||||
|
|
||||||
|
final appBar = SliverAppBar(
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(OMIcons.arrowUpward),
|
||||||
|
onPressed: _goToImage,
|
||||||
|
tooltip: 'Back to image',
|
||||||
|
),
|
||||||
|
title: const Text('Info'),
|
||||||
|
floating: true,
|
||||||
|
);
|
||||||
|
|
||||||
return MediaQueryDataProvider(
|
return MediaQueryDataProvider(
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
|
@ -55,72 +65,56 @@ class InfoPageState extends State<InfoPage> {
|
||||||
final mqWidth = mq.item1;
|
final mqWidth = mq.item1;
|
||||||
final mqViewInsetsBottom = mq.item2;
|
final mqViewInsetsBottom = mq.item2;
|
||||||
final split = mqWidth > 400;
|
final split = mqWidth > 400;
|
||||||
|
final locationAtTop = split && entry.hasGps;
|
||||||
|
|
||||||
return CustomScrollView(
|
final locationSection = LocationSection(
|
||||||
controller: _scrollController,
|
entry: entry,
|
||||||
slivers: [
|
showTitle: !locationAtTop,
|
||||||
SliverAppBar(
|
visibleNotifier: widget.visibleNotifier,
|
||||||
leading: IconButton(
|
);
|
||||||
icon: const Icon(OMIcons.arrowUpward),
|
final basicAndLocationSliver = locationAtTop
|
||||||
onPressed: _goToImage,
|
? SliverToBoxAdapter(
|
||||||
tooltip: 'Back to image',
|
|
||||||
),
|
|
||||||
title: const Text('Info'),
|
|
||||||
floating: true,
|
|
||||||
),
|
|
||||||
const SliverPadding(
|
|
||||||
padding: EdgeInsets.only(top: 8),
|
|
||||||
),
|
|
||||||
if (split && entry.hasGps)
|
|
||||||
SliverPadding(
|
|
||||||
padding: horizontalPadding,
|
|
||||||
sliver: SliverToBoxAdapter(
|
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Expanded(child: BasicSection(entry: entry)),
|
Expanded(child: BasicSection(entry: entry)),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(child: locationSection),
|
||||||
child: LocationSection(
|
|
||||||
entry: entry,
|
|
||||||
showTitle: false,
|
|
||||||
visibleNotifier: widget.visibleNotifier,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
)
|
)
|
||||||
else
|
: SliverList(
|
||||||
SliverPadding(
|
delegate: SliverChildListDelegate.fixed(
|
||||||
padding: horizontalPadding,
|
|
||||||
sliver: SliverList(
|
|
||||||
delegate: SliverChildListDelegate(
|
|
||||||
[
|
[
|
||||||
BasicSection(entry: entry),
|
BasicSection(entry: entry),
|
||||||
LocationSection(
|
locationSection,
|
||||||
entry: entry,
|
|
||||||
showTitle: true,
|
|
||||||
visibleNotifier: widget.visibleNotifier,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
),
|
final tagSliver = XmpTagSectionSliver(
|
||||||
SliverPadding(
|
collection: widget.collection,
|
||||||
padding: horizontalPadding,
|
entry: entry,
|
||||||
sliver: XmpTagSectionSliver(collection: widget.collection, entry: entry),
|
);
|
||||||
),
|
final metadataSliver = MetadataSectionSliver(
|
||||||
SliverPadding(
|
|
||||||
padding: horizontalPadding,
|
|
||||||
sliver: MetadataSectionSliver(
|
|
||||||
entry: entry,
|
entry: entry,
|
||||||
columnCount: split ? 2 : 1,
|
|
||||||
visibleNotifier: widget.visibleNotifier,
|
visibleNotifier: widget.visibleNotifier,
|
||||||
),
|
);
|
||||||
|
|
||||||
|
return CustomScrollView(
|
||||||
|
controller: _scrollController,
|
||||||
|
slivers: [
|
||||||
|
appBar,
|
||||||
|
SliverPadding(
|
||||||
|
padding: horizontalPadding + const EdgeInsets.only(top: 8),
|
||||||
|
sliver: basicAndLocationSliver,
|
||||||
),
|
),
|
||||||
SliverPadding(
|
SliverPadding(
|
||||||
padding: EdgeInsets.only(bottom: 8 + mqViewInsetsBottom),
|
padding: horizontalPadding,
|
||||||
|
sliver: tagSliver,
|
||||||
|
),
|
||||||
|
SliverPadding(
|
||||||
|
padding: horizontalPadding + EdgeInsets.only(bottom: 8 + mqViewInsetsBottom),
|
||||||
|
sliver: metadataSliver,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
@ -189,24 +183,32 @@ class SectionRow extends StatelessWidget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class InfoRow extends StatelessWidget {
|
class InfoRowGroup extends StatelessWidget {
|
||||||
final String label, value;
|
final Map<String, String> keyValues;
|
||||||
|
|
||||||
const InfoRow(this.label, this.value);
|
const InfoRowGroup(this.keyValues);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Padding(
|
if (keyValues.isEmpty) return const SizedBox.shrink();
|
||||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
final lastKey = keyValues.keys.last;
|
||||||
child: RichText(
|
return Column(
|
||||||
text: TextSpan(
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
style: const TextStyle(fontFamily: 'Concourse'),
|
|
||||||
children: [
|
children: [
|
||||||
TextSpan(text: '$label ', style: const TextStyle(color: Colors.white70)),
|
SelectableText.rich(
|
||||||
TextSpan(text: value),
|
TextSpan(
|
||||||
|
children: keyValues.entries
|
||||||
|
.expand(
|
||||||
|
(kv) => [
|
||||||
|
TextSpan(text: '${kv.key} ', style: const TextStyle(color: Colors.white70, height: 1.7)),
|
||||||
|
TextSpan(text: '${kv.value}${kv.key == lastKey ? '' : '\n'}'),
|
||||||
],
|
],
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
),
|
),
|
||||||
|
style: const TextStyle(fontFamily: 'Concourse'),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -90,7 +90,7 @@ class _LocationSectionState extends State<LocationSection> {
|
||||||
if (location.isNotEmpty)
|
if (location.isNotEmpty)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(top: 8),
|
padding: const EdgeInsets.only(top: 8),
|
||||||
child: InfoRow('Address', location),
|
child: InfoRowGroup({'Address': location}),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:collection';
|
||||||
|
|
||||||
import 'package:aves/model/image_entry.dart';
|
import 'package:aves/model/image_entry.dart';
|
||||||
import 'package:aves/model/metadata_service.dart';
|
import 'package:aves/model/metadata_service.dart';
|
||||||
|
@ -6,17 +7,13 @@ import 'package:aves/utils/color_utils.dart';
|
||||||
import 'package:aves/widgets/common/fx/highlight_decoration.dart';
|
import 'package:aves/widgets/common/fx/highlight_decoration.dart';
|
||||||
import 'package:aves/widgets/fullscreen/info/info_page.dart';
|
import 'package:aves/widgets/fullscreen/info/info_page.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
|
||||||
import 'package:tuple/tuple.dart';
|
|
||||||
|
|
||||||
class MetadataSectionSliver extends StatefulWidget {
|
class MetadataSectionSliver extends StatefulWidget {
|
||||||
final ImageEntry entry;
|
final ImageEntry entry;
|
||||||
final int columnCount;
|
|
||||||
final ValueNotifier<bool> visibleNotifier;
|
final ValueNotifier<bool> visibleNotifier;
|
||||||
|
|
||||||
const MetadataSectionSliver({
|
const MetadataSectionSliver({
|
||||||
@required this.entry,
|
@required this.entry,
|
||||||
@required this.columnCount,
|
|
||||||
@required this.visibleNotifier,
|
@required this.visibleNotifier,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -28,8 +25,6 @@ class _MetadataSectionSliverState extends State<MetadataSectionSliver> with Auto
|
||||||
List<_MetadataDirectory> _metadata = [];
|
List<_MetadataDirectory> _metadata = [];
|
||||||
String _loadedMetadataUri;
|
String _loadedMetadataUri;
|
||||||
|
|
||||||
int get columnCount => widget.columnCount;
|
|
||||||
|
|
||||||
bool get isVisible => widget.visibleNotifier.value;
|
bool get isVisible => widget.visibleNotifier.value;
|
||||||
|
|
||||||
static const int maxValueLength = 140;
|
static const int maxValueLength = 140;
|
||||||
|
@ -67,24 +62,34 @@ class _MetadataSectionSliverState extends State<MetadataSectionSliver> with Auto
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
super.build(context);
|
super.build(context);
|
||||||
|
|
||||||
final itemCount = _metadata.isEmpty ? 0 : _metadata.length + 1;
|
final directoriesWithoutTitle = _metadata.where((dir) => dir.name.isEmpty);
|
||||||
final itemBuilder = (context, index) => index == 0 ? const SectionRow('Metadata') : _DirectoryWidget(_metadata[index - 1]);
|
final directoriesWithTitle = _metadata.where((dir) => dir.name.isNotEmpty);
|
||||||
|
return SliverList(
|
||||||
// SliverStaggeredGrid is not as efficient as SliverList when there is only one column
|
delegate: SliverChildListDelegate.fixed(
|
||||||
return columnCount == 1
|
[
|
||||||
? SliverList(
|
const SectionRow('Metadata'),
|
||||||
delegate: SliverChildBuilderDelegate(
|
...directoriesWithoutTitle.map((dir) => InfoRowGroup(dir.tags)),
|
||||||
itemBuilder,
|
ExpansionPanelList.radio(
|
||||||
childCount: itemCount,
|
expandedHeaderPadding: EdgeInsets.zero,
|
||||||
|
children: directoriesWithTitle.map<ExpansionPanelRadio>((dir) {
|
||||||
|
return ExpansionPanelRadio(
|
||||||
|
value: dir.name,
|
||||||
|
canTapOnHeader: true,
|
||||||
|
headerBuilder: (BuildContext context, bool isExpanded) {
|
||||||
|
return ListTile(
|
||||||
|
title: _DirectoryTitle(dir.name),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
body: Container(
|
||||||
|
alignment: Alignment.topLeft,
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
child: InfoRowGroup(dir.tags),
|
||||||
),
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
)
|
)
|
||||||
: SliverStaggeredGrid.countBuilder(
|
],
|
||||||
crossAxisCount: columnCount,
|
),
|
||||||
staggeredTileBuilder: (index) => StaggeredTile.fit(index == 0 ? columnCount : 1),
|
|
||||||
itemBuilder: itemBuilder,
|
|
||||||
itemCount: itemCount,
|
|
||||||
mainAxisSpacing: 0,
|
|
||||||
crossAxisSpacing: 8,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,19 +97,15 @@ class _MetadataSectionSliverState extends State<MetadataSectionSliver> with Auto
|
||||||
if (_loadedMetadataUri == widget.entry.uri) return;
|
if (_loadedMetadataUri == widget.entry.uri) return;
|
||||||
if (isVisible) {
|
if (isVisible) {
|
||||||
final rawMetadata = await MetadataService.getAllMetadata(widget.entry) ?? {};
|
final rawMetadata = await MetadataService.getAllMetadata(widget.entry) ?? {};
|
||||||
_metadata = rawMetadata.entries.map((kv) {
|
_metadata = rawMetadata.entries.map((dirKV) {
|
||||||
final String directoryName = kv.key as String ?? '';
|
final directoryName = dirKV.key as String ?? '';
|
||||||
final Map rawTags = kv.value as Map ?? {};
|
final rawTags = dirKV.value as Map ?? {};
|
||||||
final List<Tuple2<String, String>> tags = rawTags.entries
|
final tags = SplayTreeMap.of(Map.fromEntries(rawTags.entries.map((tagKV) {
|
||||||
.map((kv) {
|
final value = tagKV.value as String ?? '';
|
||||||
final value = kv.value as String ?? '';
|
|
||||||
if (value.isEmpty) return null;
|
if (value.isEmpty) return null;
|
||||||
final tagName = kv.key as String ?? '';
|
final tagName = tagKV.key as String ?? '';
|
||||||
return Tuple2(tagName, value.length > maxValueLength ? '${value.substring(0, maxValueLength)}…' : value);
|
return MapEntry(tagName, value.length > maxValueLength ? '${value.substring(0, maxValueLength)}…' : value);
|
||||||
})
|
}).where((kv) => kv != null)));
|
||||||
.where((tag) => tag != null)
|
|
||||||
.toList()
|
|
||||||
..sort((a, b) => a.item1.compareTo(b.item1));
|
|
||||||
return _MetadataDirectory(directoryName, tags);
|
return _MetadataDirectory(directoryName, tags);
|
||||||
}).toList()
|
}).toList()
|
||||||
..sort((a, b) => a.name.compareTo(b.name));
|
..sort((a, b) => a.name.compareTo(b.name));
|
||||||
|
@ -120,24 +121,6 @@ class _MetadataSectionSliverState extends State<MetadataSectionSliver> with Auto
|
||||||
bool get wantKeepAlive => true;
|
bool get wantKeepAlive => true;
|
||||||
}
|
}
|
||||||
|
|
||||||
class _DirectoryWidget extends StatelessWidget {
|
|
||||||
final _MetadataDirectory directory;
|
|
||||||
|
|
||||||
const _DirectoryWidget(this.directory);
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
if (directory.name.isNotEmpty) _DirectoryTitle(directory.name),
|
|
||||||
...directory.tags.map((tag) => InfoRow(tag.item1, tag.item2)),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _DirectoryTitle extends StatelessWidget {
|
class _DirectoryTitle extends StatelessWidget {
|
||||||
final String name;
|
final String name;
|
||||||
|
|
||||||
|
@ -173,7 +156,7 @@ class _DirectoryTitle extends StatelessWidget {
|
||||||
|
|
||||||
class _MetadataDirectory {
|
class _MetadataDirectory {
|
||||||
final String name;
|
final String name;
|
||||||
final List<Tuple2<String, String>> tags;
|
final SplayTreeMap<String, String> tags;
|
||||||
|
|
||||||
const _MetadataDirectory(this.name, this.tags);
|
const _MetadataDirectory(this.name, this.tags);
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ class XmpTagSectionSliver extends AnimatedWidget {
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final tags = entry.xmpSubjects;
|
final tags = entry.xmpSubjects;
|
||||||
return SliverList(
|
return SliverList(
|
||||||
delegate: SliverChildListDelegate(
|
delegate: SliverChildListDelegate.fixed(
|
||||||
tags.isEmpty
|
tags.isEmpty
|
||||||
? []
|
? []
|
||||||
: [
|
: [
|
||||||
|
|
Loading…
Reference in a new issue