This commit is contained in:
Thibault Deckers 2023-01-31 19:10:38 +01:00
parent d5e702266f
commit 32bbee8bfd
12 changed files with 416 additions and 196 deletions

View file

@ -7,6 +7,7 @@ All notable changes to this project will be documented in this file.
### Added ### Added
- Viewer: overlay details expand/collapse on tap - Viewer: overlay details expand/collapse on tap
- TV: improved support for Info
### Changed ### Changed

View file

@ -1,6 +1,7 @@
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/widgets/common/basic/markdown_container.dart'; import 'package:aves/widgets/common/basic/markdown_container.dart';
import 'package:aves/widgets/common/basic/scaffold.dart'; import 'package:aves/widgets/common/basic/scaffold.dart';
import 'package:aves/widgets/common/behaviour/intents.dart';
import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
@ -38,11 +39,11 @@ class _PolicyPageState extends State<PolicyPage> {
child: FocusableActionDetector( child: FocusableActionDetector(
autofocus: true, autofocus: true,
shortcuts: const { shortcuts: const {
SingleActivator(LogicalKeyboardKey.arrowUp): _ScrollIntent.up(), SingleActivator(LogicalKeyboardKey.arrowUp): VerticalScrollIntent.up(),
SingleActivator(LogicalKeyboardKey.arrowDown): _ScrollIntent.down(), SingleActivator(LogicalKeyboardKey.arrowDown): VerticalScrollIntent.down(),
}, },
actions: { actions: {
_ScrollIntent: CallbackAction<_ScrollIntent>(onInvoke: _onScrollIntent), VerticalScrollIntent: VerticalScrollIntentAction(scrollController: _scrollController),
}, },
child: Center( child: Center(
child: FutureBuilder<String>( child: FutureBuilder<String>(
@ -65,38 +66,4 @@ class _PolicyPageState extends State<PolicyPage> {
), ),
); );
} }
void _onScrollIntent(_ScrollIntent intent) {
late int factor;
switch (intent.type) {
case _ScrollDirection.up:
factor = -1;
break;
case _ScrollDirection.down:
factor = 1;
break;
}
_scrollController.animateTo(
_scrollController.offset + factor * 150,
duration: const Duration(milliseconds: 500),
curve: Curves.easeOutCubic,
);
}
}
class _ScrollIntent extends Intent {
const _ScrollIntent({
required this.type,
});
const _ScrollIntent.up() : type = _ScrollDirection.up;
const _ScrollIntent.down() : type = _ScrollDirection.down;
final _ScrollDirection type;
}
enum _ScrollDirection {
up,
down,
} }

View file

@ -441,7 +441,7 @@ class _CollectionScrollViewState extends State<_CollectionScrollView> with Widge
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final scrollView = _buildScrollView(widget.appBar, widget.collection); final scrollView = _buildScrollView(widget.appBar, widget.collection);
return _buildDraggableScrollView(scrollView, widget.collection); return settings.useTvLayout ? scrollView : _buildDraggableScrollView(scrollView, widget.collection);
} }
Widget _buildDraggableScrollView(ScrollView scrollView, CollectionLens collection) { Widget _buildDraggableScrollView(ScrollView scrollView, CollectionLens collection) {

View file

@ -0,0 +1,44 @@
import 'package:flutter/widgets.dart';
class VerticalScrollIntent extends Intent {
const VerticalScrollIntent({
required this.type,
});
const VerticalScrollIntent.up() : type = VerticalScrollDirection.up;
const VerticalScrollIntent.down() : type = VerticalScrollDirection.down;
final VerticalScrollDirection type;
}
enum VerticalScrollDirection {
up,
down,
}
class VerticalScrollIntentAction extends CallbackAction<VerticalScrollIntent> {
VerticalScrollIntentAction({
required ScrollController scrollController,
}) : super(onInvoke: (intent) => _onScrollIntent(intent, scrollController));
static void _onScrollIntent(
VerticalScrollIntent intent,
ScrollController scrollController,
) {
late int factor;
switch (intent.type) {
case VerticalScrollDirection.up:
factor = -1;
break;
case VerticalScrollDirection.down:
factor = 1;
break;
}
scrollController.animateTo(
scrollController.offset + factor * 150,
duration: const Duration(milliseconds: 500),
curve: Curves.easeOutCubic,
);
}
}

View file

@ -620,7 +620,7 @@ class _FilterScrollView<T extends CollectionFilter> extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final scrollView = _buildScrollView(context); final scrollView = _buildScrollView(context);
return _buildDraggableScrollView(scrollView); return settings.useTvLayout ? scrollView : _buildDraggableScrollView(scrollView);
} }
Widget _buildDraggableScrollView(ScrollView scrollView) { Widget _buildDraggableScrollView(ScrollView scrollView) {

View file

@ -97,49 +97,8 @@ class _SettingsPageState extends State<SettingsPage> with FeedbackMixin {
primary: false, primary: false,
), ),
), ),
Expanded( const Expanded(
child: ValueListenableBuilder<int>( child: _TvRail(),
valueListenable: _tvSelectedIndexNotifier,
builder: (context, selectedIndex, child) {
final rail = NavigationRail(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
extended: true,
destinations: sections
.map((section) => NavigationRailDestination(
icon: section.icon(context),
label: Text(section.title(context)),
))
.toList(),
selectedIndex: selectedIndex,
onDestinationSelected: (index) => _tvSelectedIndexNotifier.value = index,
minExtendedWidth: TvRail.minExtendedWidth,
);
return LayoutBuilder(
builder: (context, constraints) {
return Row(
children: [
SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: constraints.maxHeight),
child: IntrinsicHeight(child: rail),
),
),
Expanded(
child: MediaQuery.removePadding(
context: context,
removeLeft: !context.isRtl,
removeRight: context.isRtl,
child: _SettingsSectionBody(
loader: Future.value(sections[selectedIndex].tiles(context)),
),
),
),
],
);
},
);
},
),
), ),
], ],
), ),
@ -359,3 +318,68 @@ class _SettingsSectionBody extends StatelessWidget {
); );
} }
} }
class _TvRail extends StatefulWidget {
const _TvRail();
@override
State<_TvRail> createState() => _TvRailState();
}
class _TvRailState extends State<_TvRail> {
final ValueNotifier<int> _indexNotifier = ValueNotifier(0);
@override
void dispose() {
_indexNotifier.dispose();
super.dispose();
}
static final List<SettingsSection> sections = _SettingsPageState.sections;
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<int>(
valueListenable: _indexNotifier,
builder: (context, selectedIndex, child) {
final rail = NavigationRail(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
extended: true,
destinations: sections
.map((section) => NavigationRailDestination(
icon: section.icon(context),
label: Text(section.title(context)),
))
.toList(),
selectedIndex: selectedIndex,
onDestinationSelected: (index) => _indexNotifier.value = index,
minExtendedWidth: TvRail.minExtendedWidth,
);
return LayoutBuilder(
builder: (context, constraints) {
return Row(
children: [
SingleChildScrollView(
child: ConstrainedBox(
constraints: BoxConstraints(minHeight: constraints.maxHeight),
child: IntrinsicHeight(child: rail),
),
),
Expanded(
child: MediaQuery.removePadding(
context: context,
removeLeft: !context.isRtl,
removeRight: context.isRtl,
child: _SettingsSectionBody(
loader: Future.value(sections[selectedIndex].tiles(context)),
),
),
),
],
);
},
);
},
);
}
}

View file

@ -27,10 +27,11 @@ 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';
class BasicSection extends StatelessWidget { class BasicSection extends StatefulWidget {
final AvesEntry entry; final AvesEntry entry;
final CollectionLens? collection; final CollectionLens? collection;
final EntryInfoActionDelegate actionDelegate; final EntryInfoActionDelegate actionDelegate;
final ValueNotifier<bool> isScrollingNotifier;
final ValueNotifier<EntryAction?> isEditingMetadataNotifier; final ValueNotifier<EntryAction?> isEditingMetadataNotifier;
final FilterCallback onFilter; final FilterCallback onFilter;
@ -39,12 +40,54 @@ class BasicSection extends StatelessWidget {
required this.entry, required this.entry,
this.collection, this.collection,
required this.actionDelegate, required this.actionDelegate,
required this.isScrollingNotifier,
required this.isEditingMetadataNotifier, required this.isEditingMetadataNotifier,
required this.onFilter, required this.onFilter,
}); });
@override
State<BasicSection> createState() => _BasicSectionState();
}
class _BasicSectionState extends State<BasicSection> {
final FocusNode _chipFocusNode = FocusNode();
CollectionLens? get collection => widget.collection;
EntryInfoActionDelegate get actionDelegate => widget.actionDelegate;
@override
void initState() {
super.initState();
_registerWidget(widget);
_onScrollingChanged();
}
@override
void didUpdateWidget(covariant BasicSection oldWidget) {
super.didUpdateWidget(oldWidget);
_unregisterWidget(oldWidget);
_registerWidget(widget);
}
@override
void dispose() {
_unregisterWidget(widget);
_chipFocusNode.dispose();
super.dispose();
}
void _registerWidget(BasicSection widget) {
widget.isScrollingNotifier.addListener(_onScrollingChanged);
}
void _unregisterWidget(BasicSection widget) {
widget.isScrollingNotifier.removeListener(_onScrollingChanged);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final entry = widget.entry;
return AnimatedBuilder( return AnimatedBuilder(
animation: entry.metadataChangeNotifier, animation: entry.metadataChangeNotifier,
builder: (context, child) { builder: (context, child) {
@ -52,7 +95,12 @@ class BasicSection extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_BasicInfo(entry: entry), _BasicInfo(entry: entry),
_buildChips(context), Focus(
focusNode: _chipFocusNode,
skipTraversal: true,
canRequestFocus: false,
child: _buildChips(context),
),
_buildEditButtons(context), _buildEditButtons(context),
], ],
); );
@ -60,6 +108,7 @@ class BasicSection extends StatelessWidget {
} }
Widget _buildChips(BuildContext context) { Widget _buildChips(BuildContext context) {
final entry = widget.entry;
final tags = entry.tags.toList()..sort(compareAsciiUpperCaseNatural); final tags = entry.tags.toList()..sort(compareAsciiUpperCaseNatural);
final album = entry.directory; final album = entry.directory;
final filters = { final filters = {
@ -91,7 +140,7 @@ class BasicSection extends StatelessWidget {
children: effectiveFilters children: effectiveFilters
.map((filter) => AvesFilterChip( .map((filter) => AvesFilterChip(
filter: filter, filter: filter,
onTap: onFilter, onTap: widget.onFilter,
)) ))
.toList(), .toList(),
), ),
@ -101,6 +150,7 @@ class BasicSection extends StatelessWidget {
} }
Widget _buildEditButtons(BuildContext context) { Widget _buildEditButtons(BuildContext context) {
final entry = widget.entry;
final children = [ final children = [
EntryAction.editRating, EntryAction.editRating,
EntryAction.editTags, EntryAction.editTags,
@ -124,8 +174,9 @@ class BasicSection extends StatelessWidget {
} }
Widget _buildEditMetadataButton(BuildContext context, EntryAction action) { Widget _buildEditMetadataButton(BuildContext context, EntryAction action) {
final entry = widget.entry;
return ValueListenableBuilder<EntryAction?>( return ValueListenableBuilder<EntryAction?>(
valueListenable: isEditingMetadataNotifier, valueListenable: widget.isEditingMetadataNotifier,
builder: (context, editingAction, child) { builder: (context, editingAction, child) {
final isEditing = editingAction != null; final isEditing = editingAction != null;
final onPressed = isEditing ? null : () => actionDelegate.onActionSelected(context, entry, collection, action); final onPressed = isEditing ? null : () => actionDelegate.onActionSelected(context, entry, collection, action);
@ -181,6 +232,14 @@ class BasicSection extends StatelessWidget {
}, },
); );
} }
void _onScrollingChanged() {
if (!widget.isScrollingNotifier.value) {
// using `autofocus` while scrolling seems to fail for widget built offscreen
// so we give focus to this page when the screen is no longer scrolling
_chipFocusNode.children.firstOrNull?.requestFocus();
}
}
} }
class _BasicInfo extends StatefulWidget { class _BasicInfo extends StatefulWidget {

View file

@ -40,50 +40,17 @@ class InfoPage extends StatefulWidget {
} }
class _InfoPageState extends State<InfoPage> { class _InfoPageState extends State<InfoPage> {
final FocusNode _focusNode = FocusNode();
final ScrollController _scrollController = ScrollController(); final ScrollController _scrollController = ScrollController();
bool _scrollStartFromTop = false; bool _scrollStartFromTop = false;
static const splitScreenWidthThreshold = 600; static const splitScreenWidthThreshold = 600;
@override
void initState() {
super.initState();
_registerWidget(widget);
_onScrollingChanged();
}
@override
void didUpdateWidget(covariant InfoPage oldWidget) {
super.didUpdateWidget(oldWidget);
_unregisterWidget(oldWidget);
_registerWidget(widget);
}
@override @override
void dispose() { void dispose() {
_unregisterWidget(widget);
_focusNode.dispose();
_scrollController.dispose(); _scrollController.dispose();
super.dispose(); super.dispose();
} }
void _registerWidget(InfoPage widget) {
widget.isScrollingNotifier.addListener(_onScrollingChanged);
}
void _unregisterWidget(InfoPage widget) {
widget.isScrollingNotifier.removeListener(_onScrollingChanged);
}
void _onScrollingChanged() {
if (!widget.isScrollingNotifier.value) {
// using `autofocus` while scrolling seems to fail for widget built offscreen
// so we give focus to this page when the screen is no longer scrolling
_focusNode.requestFocus();
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AvesScaffold( return AvesScaffold(
@ -104,8 +71,6 @@ class _InfoPageState extends State<InfoPage> {
final targetEntry = pageEntry ?? mainEntry; final targetEntry = pageEntry ?? mainEntry;
return EmbeddedDataOpener( return EmbeddedDataOpener(
entry: targetEntry, entry: targetEntry,
child: Focus(
focusNode: _focusNode,
child: _InfoPageContent( child: _InfoPageContent(
collection: widget.collection, collection: widget.collection,
entry: targetEntry, entry: targetEntry,
@ -114,7 +79,6 @@ class _InfoPageState extends State<InfoPage> {
split: mqWidth > splitScreenWidthThreshold, split: mqWidth > splitScreenWidthThreshold,
goToViewer: _goToViewer, goToViewer: _goToViewer,
), ),
),
); );
} }
@ -240,6 +204,7 @@ class _InfoPageContentState extends State<_InfoPageContent> {
entry: entry, entry: entry,
collection: collection, collection: collection,
actionDelegate: _actionDelegate, actionDelegate: _actionDelegate,
isScrollingNotifier: widget.isScrollingNotifier,
isEditingMetadataNotifier: _isEditingMetadataNotifier, isEditingMetadataNotifier: _isEditingMetadataNotifier,
onFilter: _onFilter, onFilter: _onFilter,
); );

View file

@ -40,16 +40,60 @@ class MetadataDirTile extends StatelessWidget {
var tags = dir.tags; var tags = dir.tags;
if (tags.isEmpty) return const SizedBox(); if (tags.isEmpty) return const SizedBox();
final dirName = dir.name; return AvesExpansionTile(
if (dirName == MetadataDirectory.xmpDirectory) {
return XmpDirTile(
entry: entry,
title: title, title: title,
allTags: dir.allTags, highlightColor: getTitleColor(context, dir),
tags: tags,
expandedNotifier: expandedDirectoryNotifier, expandedNotifier: expandedDirectoryNotifier,
initiallyExpanded: initiallyExpanded, initiallyExpanded: initiallyExpanded,
children: [
MetadataDirTileBody(
entry: entry,
dir: dir,
showThumbnails: showThumbnails,
),
],
); );
}
static Color getTitleColor(BuildContext context, MetadataDirectory dir) {
final dirName = dir.name;
if (dirName == MetadataDirectory.xmpDirectory) {
return context.select<AvesColorsData, Color>((v) => v.xmp);
} else {
final colors = context.watch<AvesColorsData>();
return dir.color ?? colors.fromBrandColor(BrandColors.get(dirName)) ?? colors.fromString(dirName);
}
}
}
class MetadataDirTileBody extends StatelessWidget {
final AvesEntry entry;
final MetadataDirectory dir;
final bool showThumbnails;
const MetadataDirTileBody({
super.key,
required this.entry,
required this.dir,
this.showThumbnails = true,
});
@override
Widget build(BuildContext context) {
var tags = dir.tags;
late final List<Widget> children;
final dirName = dir.name;
if (dirName == MetadataDirectory.xmpDirectory) {
children = [
Padding(
padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8),
child: XmpDirTileBody(
allTags: dir.allTags,
tags: tags,
),
),
];
} else { } else {
Map<String, InfoValueSpanBuilder>? linkHandlers; Map<String, InfoValueSpanBuilder>? linkHandlers;
switch (dirName) { switch (dirName) {
@ -67,13 +111,7 @@ class MetadataDirTile extends StatelessWidget {
break; break;
} }
final colors = context.watch<AvesColorsData>(); children = [
return AvesExpansionTile(
title: title,
highlightColor: dir.color ?? colors.fromBrandColor(BrandColors.get(dirName)) ?? colors.fromString(dirName),
expandedNotifier: expandedDirectoryNotifier,
initiallyExpanded: initiallyExpanded,
children: [
if (showThumbnails && dirName == MetadataDirectory.exifThumbnailDirectory) MetadataThumbnails(entry: entry), if (showThumbnails && dirName == MetadataDirectory.exifThumbnailDirectory) MetadataThumbnails(entry: entry),
Padding( Padding(
padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8), padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8),
@ -83,9 +121,13 @@ class MetadataDirTile extends StatelessWidget {
spanBuilders: linkHandlers, spanBuilders: linkHandlers,
), ),
), ),
], ];
);
} }
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: children,
);
} }
static Map<String, InfoValueSpanBuilder> getSvgLinkHandlers(SplayTreeMap<String, String> tags) { static Map<String, InfoValueSpanBuilder> getSvgLinkHandlers(SplayTreeMap<String, String> tags) {

View file

@ -2,11 +2,14 @@ import 'dart:async';
import 'package:aves/model/entry.dart'; import 'package:aves/model/entry.dart';
import 'package:aves/model/entry_info.dart'; import 'package:aves/model/entry_info.dart';
import 'package:aves/model/settings/settings.dart';
import 'package:aves/theme/durations.dart'; import 'package:aves/theme/durations.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/identity/buttons/outlined_button.dart';
import 'package:aves/widgets/viewer/info/common.dart'; import 'package:aves/widgets/viewer/info/common.dart';
import 'package:aves/widgets/viewer/info/metadata/metadata_dir.dart'; import 'package:aves/widgets/viewer/info/metadata/metadata_dir.dart';
import 'package:aves/widgets/viewer/info/metadata/metadata_dir_tile.dart'; import 'package:aves/widgets/viewer/info/metadata/metadata_dir_tile.dart';
import 'package:aves/widgets/viewer/info/metadata/tv_page.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart';
@ -92,7 +95,24 @@ class _MetadataSectionSliverState extends State<MetadataSectionSliver> {
child: child, child: child,
), ),
), ),
children: [ children: settings.useTvLayout
? [
AvesOutlinedButton(
label: MaterialLocalizations.of(context).moreButtonTooltip,
onPressed: () {
Navigator.maybeOf(context)?.push(
MaterialPageRoute(
settings: const RouteSettings(name: TvMetadataPage.routeName),
builder: (context) => TvMetadataPage(
entry: entry,
metadata: metadata,
),
),
);
},
),
]
: [
const SectionRow(icon: AIcons.info), const SectionRow(icon: AIcons.info),
...metadata.entries.map((kv) => MetadataDirTile( ...metadata.entries.map((kv) => MetadataDirTile(
entry: entry, entry: entry,

View file

@ -0,0 +1,123 @@
import 'package:aves/model/entry.dart';
import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/basic/scaffold.dart';
import 'package:aves/widgets/common/behaviour/intents.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/navigation/tv_rail.dart';
import 'package:aves/widgets/viewer/info/metadata/metadata_dir.dart';
import 'package:aves/widgets/viewer/info/metadata/metadata_dir_tile.dart';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
class TvMetadataPage extends StatefulWidget {
static const routeName = '/info/metadata';
final AvesEntry entry;
final Map<String, MetadataDirectory> metadata;
const TvMetadataPage({
super.key,
required this.entry,
required this.metadata,
});
@override
State<TvMetadataPage> createState() => _TvMetadataPageState();
}
class _TvMetadataPageState extends State<TvMetadataPage> {
final ValueNotifier<int> _railIndexNotifier = ValueNotifier(0);
final FocusNode _railFocusNode = FocusNode();
final ScrollController _detailsScrollController = ScrollController();
Map<String, MetadataDirectory> get metadata => widget.metadata;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_railFocusNode.children.firstOrNull?.requestFocus();
});
}
@override
void dispose() {
_railIndexNotifier.dispose();
_railFocusNode.dispose();
_detailsScrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AvesScaffold(
appBar: AppBar(
automaticallyImplyLeading: false,
title: Text(context.l10n.viewerInfoPageTitle),
),
body: ValueListenableBuilder<int>(
valueListenable: _railIndexNotifier,
builder: (context, selectedIndex, child) {
final titles = metadata.keys.toList();
final selectedDir = metadata[titles[selectedIndex]];
if (selectedDir == null) return const SizedBox();
final rail = NavigationRail(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
extended: true,
destinations: titles.mapIndexed((i, title) {
final dir = metadata[titles[i]]!;
final color = MetadataDirTile.getTitleColor(context, dir);
return NavigationRailDestination(
icon: Icon(AIcons.disc, color: color),
label: Text(title),
);
}).toList(),
selectedIndex: selectedIndex,
onDestinationSelected: (index) => _railIndexNotifier.value = index,
minExtendedWidth: TvRail.minExtendedWidth,
);
return SafeArea(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(width: 16),
Focus(
focusNode: _railFocusNode,
skipTraversal: true,
canRequestFocus: false,
child: SingleChildScrollView(
child: IntrinsicHeight(
child: rail,
),
),
),
Expanded(
child: FocusableActionDetector(
shortcuts: const {
SingleActivator(LogicalKeyboardKey.arrowUp): VerticalScrollIntent.up(),
SingleActivator(LogicalKeyboardKey.arrowDown): VerticalScrollIntent.down(),
},
actions: {
VerticalScrollIntent: VerticalScrollIntentAction(scrollController: _detailsScrollController),
},
child: SingleChildScrollView(
controller: _detailsScrollController,
padding: const EdgeInsets.all(16),
child: MetadataDirTileBody(
entry: widget.entry,
dir: selectedDir,
),
),
),
),
],
),
);
},
),
);
}
}

View file

@ -1,41 +1,27 @@
import 'dart:collection'; import 'dart:collection';
import 'dart:convert'; import 'dart:convert';
import 'package:aves/model/entry.dart';
import 'package:aves/theme/colors.dart';
import 'package:aves/utils/xmp_utils.dart'; import 'package:aves/utils/xmp_utils.dart';
import 'package:aves/widgets/common/identity/aves_expansion_tile.dart';
import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart'; import 'package:aves/widgets/viewer/info/metadata/xmp_namespaces.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
class XmpDirTile extends StatefulWidget { class XmpDirTileBody extends StatefulWidget {
final AvesEntry entry;
final String title;
final SplayTreeMap<String, String> allTags, tags; final SplayTreeMap<String, String> allTags, tags;
final ValueNotifier<String?>? expandedNotifier;
final bool initiallyExpanded;
const XmpDirTile({ const XmpDirTileBody({
super.key, super.key,
required this.entry,
required this.title,
required this.allTags, required this.allTags,
required this.tags, required this.tags,
required this.expandedNotifier,
required this.initiallyExpanded,
}); });
@override @override
State<XmpDirTile> createState() => _XmpDirTileState(); State<XmpDirTileBody> createState() => _XmpDirTileBodyState();
} }
class _XmpDirTileState extends State<XmpDirTile> { class _XmpDirTileBodyState extends State<XmpDirTileBody> {
late final Map<String, String> _schemaRegistryPrefixes, _tags; late final Map<String, String> _schemaRegistryPrefixes, _tags;
AvesEntry get entry => widget.entry;
static const schemaRegistryPrefixesKey = 'schemaRegistryPrefixes'; static const schemaRegistryPrefixesKey = 'schemaRegistryPrefixes';
@override @override
@ -60,21 +46,10 @@ class _XmpDirTileState extends State<XmpDirTile> {
return XmpNamespace.create(_schemaRegistryPrefixes, nsPrefix, rawProps); return XmpNamespace.create(_schemaRegistryPrefixes, nsPrefix, rawProps);
}).toList() }).toList()
..sort((a, b) => compareAsciiUpperCase(a.displayTitle, b.displayTitle)); ..sort((a, b) => compareAsciiUpperCase(a.displayTitle, b.displayTitle));
return AvesExpansionTile(
// title may contain parent to distinguish multiple XMP directories return Column(
title: widget.title,
highlightColor: context.select<AvesColorsData, Color>((v) => v.xmp),
expandedNotifier: widget.expandedNotifier,
initiallyExpanded: widget.initiallyExpanded,
children: [
Padding(
padding: const EdgeInsets.only(left: 8, right: 8, bottom: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: sections.expand((section) => section.buildNamespaceSection(context)).toList(), children: sections.expand((section) => section.buildNamespaceSection(context)).toList(),
),
),
],
); );
} }
} }