mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-10-20 22:41:38 +02:00
refactor(metadata): move editor form primitives into a widgets file
Options group, quick-select button, switch field, selection field and section card become public StatelessWidgets in metadata_editor_fields.dart with unchanged build bodies; State fields they read (color scheme, fetching flag) are now constructor parameters.
This commit is contained in:
1 parent
714bec6aaf
commit
237690cd89
4 files changed
+391
-314
No files matched your search
@@ -175,7 +175,7 @@ extension _EditMetadataSheetAutoFill on _EditMetadataSheetState {
|
||||
builder: (sheetContext) => ListView(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
children: [
|
||||
_editorOptions(
|
||||
MetadataEditorOptions(
|
||||
children: [
|
||||
for (var index = 0; index < candidates.length; index++) ...[
|
||||
MetadataCandidateTile(
|
||||
@@ -476,7 +476,8 @@ extension _EditMetadataSheetAutoFill on _EditMetadataSheetState {
|
||||
}
|
||||
|
||||
Widget _buildAutoFillSection(ColorScheme cs) {
|
||||
return _sectionCard(
|
||||
return MetadataEditorSectionCard(
|
||||
colorScheme: widget.colorScheme,
|
||||
icon: Icons.travel_explore,
|
||||
title: context.l10n.editMetadataAutoFill,
|
||||
onHeaderTap: () => _setState(() => _showAutoFill = !_showAutoFill),
|
||||
@@ -500,7 +501,8 @@ extension _EditMetadataSheetAutoFill on _EditMetadataSheetState {
|
||||
final selectedId = selectedProviderIsAvailable
|
||||
? _selectedMetadataProviderId
|
||||
: null;
|
||||
return _selectionField(
|
||||
return MetadataEditorSelectionField(
|
||||
colorScheme: widget.colorScheme,
|
||||
label: context.l10n.editMetadataAutoFillSource,
|
||||
value: _metadataProviderName(selectedId),
|
||||
enabled: !_fetching && !_saving,
|
||||
@@ -512,20 +514,23 @@ extension _EditMetadataSheetAutoFill on _EditMetadataSheetState {
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
_quickSelectButton(
|
||||
MetadataEditorQuickSelectButton(
|
||||
label: context.l10n.editMetadataSelectAll,
|
||||
onTap: _selectAllFields,
|
||||
cs: cs,
|
||||
fetching: _fetching,
|
||||
),
|
||||
_quickSelectButton(
|
||||
MetadataEditorQuickSelectButton(
|
||||
label: context.l10n.editMetadataSelectEmpty,
|
||||
onTap: _selectEmptyFields,
|
||||
cs: cs,
|
||||
fetching: _fetching,
|
||||
),
|
||||
_quickSelectButton(
|
||||
MetadataEditorQuickSelectButton(
|
||||
label: context.l10n.editMetadataSelectNone,
|
||||
onTap: _selectNoFields,
|
||||
cs: cs,
|
||||
fetching: _fetching,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -153,7 +153,8 @@ extension _EditMetadataSheetCover on _EditMetadataSheetState {
|
||||
Widget _buildCoverEditor(ColorScheme cs) {
|
||||
final hasSelectedCover = _hasValue(_selectedCoverPath);
|
||||
final hasCurrentCover = _hasValue(_currentCoverPath);
|
||||
return _sectionCard(
|
||||
return MetadataEditorSectionCard(
|
||||
colorScheme: widget.colorScheme,
|
||||
icon: Icons.image_outlined,
|
||||
title: context.l10n.editMetadataFieldCover,
|
||||
children: [
|
||||
@@ -248,7 +249,8 @@ extension _EditMetadataSheetCover on _EditMetadataSheetState {
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 12),
|
||||
_selectionField(
|
||||
MetadataEditorSelectionField(
|
||||
colorScheme: widget.colorScheme,
|
||||
label: context.l10n.trackCoverResolution,
|
||||
value: _coverMaxDimension == null
|
||||
? _originalCoverResolutionLabel(
|
||||
|
||||
@@ -24,16 +24,14 @@ import 'package:spotiflac_android/utils/logger.dart';
|
||||
import 'package:spotiflac_android/widgets/app_action_button.dart';
|
||||
import 'package:spotiflac_android/widgets/app_bottom_sheet.dart';
|
||||
import 'package:spotiflac_android/widgets/app_choice_chip.dart';
|
||||
import 'package:spotiflac_android/widgets/app_content_card.dart';
|
||||
import 'package:spotiflac_android/widgets/app_progress_indicator.dart';
|
||||
import 'package:spotiflac_android/widgets/app_icon_button.dart';
|
||||
import 'package:spotiflac_android/widgets/app_loading_indicator.dart';
|
||||
import 'package:spotiflac_android/widgets/app_snack_bar.dart';
|
||||
import 'package:spotiflac_android/widgets/app_switch.dart';
|
||||
import 'package:spotiflac_android/widgets/metadata_candidate_tile.dart';
|
||||
import 'package:spotiflac_android/widgets/metadata_editor_fields.dart';
|
||||
import 'package:spotiflac_android/widgets/metadata_text_field.dart';
|
||||
import 'package:spotiflac_android/widgets/mornye_chrome.dart';
|
||||
import 'package:spotiflac_android/widgets/settings_group.dart';
|
||||
|
||||
part 'track_metadata_edit_autofill.dart';
|
||||
part 'track_metadata_edit_cover.dart';
|
||||
@@ -133,7 +131,7 @@ class _EditMetadataSheetState extends State<EditMetadataSheet> {
|
||||
Flexible(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: _editorOptions(
|
||||
child: MetadataEditorOptions(
|
||||
children: [
|
||||
for (var index = 0; index < items.length; index++)
|
||||
Column(
|
||||
@@ -373,7 +371,8 @@ class _EditMetadataSheetState extends State<EditMetadataSheet> {
|
||||
children: [
|
||||
_buildCoverEditor(cs),
|
||||
_buildAutoFillSection(cs),
|
||||
_sectionCard(
|
||||
MetadataEditorSectionCard(
|
||||
colorScheme: widget.colorScheme,
|
||||
icon: Icons.info_outline,
|
||||
title: context.l10n.trackMetadata,
|
||||
children: [
|
||||
@@ -457,7 +456,8 @@ class _EditMetadataSheetState extends State<EditMetadataSheet> {
|
||||
),
|
||||
],
|
||||
),
|
||||
_sectionCard(
|
||||
MetadataEditorSectionCard(
|
||||
colorScheme: widget.colorScheme,
|
||||
icon: Icons.lyrics_outlined,
|
||||
title: context.l10n.trackLyrics,
|
||||
children: [
|
||||
@@ -470,7 +470,8 @@ class _EditMetadataSheetState extends State<EditMetadataSheet> {
|
||||
),
|
||||
],
|
||||
),
|
||||
_sectionCard(
|
||||
MetadataEditorSectionCard(
|
||||
colorScheme: widget.colorScheme,
|
||||
icon: Icons.tune,
|
||||
title: context.l10n.editMetadataAdvanced,
|
||||
onHeaderTap: () =>
|
||||
@@ -499,7 +500,8 @@ class _EditMetadataSheetState extends State<EditMetadataSheet> {
|
||||
_form['album_type']!,
|
||||
hint: context.l10n.editMetadataFieldAlbumTypeHint,
|
||||
),
|
||||
_switchField(
|
||||
MetadataEditorSwitchField(
|
||||
colorScheme: widget.colorScheme,
|
||||
label: context.l10n.editMetadataFieldExplicit,
|
||||
subtitle:
|
||||
context.l10n.editMetadataFieldExplicitHint,
|
||||
@@ -537,302 +539,4 @@ class _EditMetadataSheetState extends State<EditMetadataSheet> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _editorOptions({required List<Widget> children}) {
|
||||
if (!context.isMornye) return SettingsGroup(children: children);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Material(
|
||||
color: MornyeTheme.controlFill(context),
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: children),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _quickSelectButton({
|
||||
required String label,
|
||||
required VoidCallback onTap,
|
||||
required ColorScheme cs,
|
||||
}) {
|
||||
if (context.isMornye) {
|
||||
return CupertinoButton(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||
onPressed: _fetching ? null : onTap,
|
||||
child: Text(
|
||||
label,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall?.copyWith(color: cs.primary),
|
||||
),
|
||||
);
|
||||
}
|
||||
return InkWell(
|
||||
onTap: _fetching ? null : onTap,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: cs.outline.withValues(alpha: 0.5)),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.labelSmall?.copyWith(color: cs.primary),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _switchField({
|
||||
required String label,
|
||||
required String subtitle,
|
||||
required bool value,
|
||||
required ValueChanged<bool> onChanged,
|
||||
}) {
|
||||
final cs = widget.colorScheme;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 14),
|
||||
child: Column(
|
||||
children: [
|
||||
Material(
|
||||
color: metadataEditorFieldFill(context, cs),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: AppSwitchListTile(
|
||||
adaptive: true,
|
||||
value: value,
|
||||
onChanged: onChanged,
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: context.isMornye ? 4 : 16,
|
||||
),
|
||||
title: Text(label, style: Theme.of(context).textTheme.bodyLarge),
|
||||
subtitle: Text(
|
||||
subtitle,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall?.copyWith(color: cs.onSurfaceVariant),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (context.isMornye) const Divider(height: 0.5),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _selectionField({
|
||||
required String label,
|
||||
required String value,
|
||||
required VoidCallback onTap,
|
||||
bool enabled = true,
|
||||
}) {
|
||||
final cs = widget.colorScheme;
|
||||
if (context.isMornye) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 14),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 4, bottom: 6),
|
||||
child: Text(
|
||||
label,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall?.copyWith(color: cs.onSurfaceVariant),
|
||||
),
|
||||
),
|
||||
CupertinoButton(
|
||||
color: metadataEditorFieldFill(context, cs),
|
||||
disabledColor: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 14),
|
||||
onPressed: enabled ? onTap : null,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: enabled ? cs.onSurface : cs.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Icon(
|
||||
CupertinoIcons.chevron_up_chevron_down,
|
||||
size: 16,
|
||||
color: cs.primary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 0.5),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
final radius = BorderRadius.circular(14);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 14),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 4, bottom: 6),
|
||||
child: Text(
|
||||
label,
|
||||
style: Theme.of(context).textTheme.labelMedium?.copyWith(
|
||||
color: enabled ? cs.onSurfaceVariant : cs.outline,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 0.1,
|
||||
),
|
||||
),
|
||||
),
|
||||
Material(
|
||||
color: metadataEditorFieldFill(context, cs),
|
||||
borderRadius: radius,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: InkWell(
|
||||
onTap: enabled ? onTap : null,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 14, 12, 14),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: enabled ? cs.onSurface : cs.outline,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Icon(
|
||||
Icons.expand_more_rounded,
|
||||
color: enabled ? cs.onSurfaceVariant : cs.outline,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
RoundedRectangleBorder _sectionCardShape(ColorScheme cs) {
|
||||
return RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
side: BorderSide(color: cs.outlineVariant.withValues(alpha: 0.5)),
|
||||
);
|
||||
}
|
||||
|
||||
/// Titled section card. When [onHeaderTap] is set the header is a full-width
|
||||
/// tappable row (ripple clipped to the card) with an auto chevron.
|
||||
Widget _sectionCard({
|
||||
required IconData icon,
|
||||
required String title,
|
||||
required List<Widget> children,
|
||||
VoidCallback? onHeaderTap,
|
||||
bool expanded = true,
|
||||
}) {
|
||||
final cs = widget.colorScheme;
|
||||
final collapsible = onHeaderTap != null;
|
||||
const inset = 16.0;
|
||||
|
||||
final headerRow = Row(
|
||||
children: [
|
||||
Icon(context.adaptiveIcon(icon), size: 20, color: cs.primary),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: cs.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (collapsible)
|
||||
AnimatedRotation(
|
||||
turns: expanded ? 0.5 : 0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeOutCubic,
|
||||
child: Icon(
|
||||
context.isMornye
|
||||
? CupertinoIcons.chevron_down
|
||||
: Icons.expand_more,
|
||||
size: 22,
|
||||
color: cs.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
final Widget header = collapsible
|
||||
? InkWell(
|
||||
onTap: onHeaderTap,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(inset, 16, inset, 16),
|
||||
child: headerRow,
|
||||
),
|
||||
)
|
||||
: Padding(
|
||||
padding: EdgeInsets.fromLTRB(inset, 16, inset, 8),
|
||||
child: headerRow,
|
||||
);
|
||||
|
||||
final content = AnimatedSize(
|
||||
duration: const Duration(milliseconds: 220),
|
||||
curve: Curves.easeOutCubic,
|
||||
alignment: Alignment.topCenter,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
header,
|
||||
if (children.isNotEmpty)
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(inset, 0, inset, 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: children,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: context.isMornye
|
||||
? Material(
|
||||
color: MornyeTheme.controlFill(context),
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: DividerTheme(
|
||||
data: DividerTheme.of(
|
||||
context,
|
||||
).copyWith(color: MornyeTheme.metadataDividerColor(context)),
|
||||
child: content,
|
||||
),
|
||||
)
|
||||
: AppContentCard(
|
||||
elevation: 0,
|
||||
margin: EdgeInsets.zero,
|
||||
color: settingsGroupColor(context),
|
||||
shape: _sectionCardShape(cs),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: content,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,366 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:spotiflac_android/theme/mornye_icons.dart';
|
||||
import 'package:spotiflac_android/theme/mornye_theme.dart';
|
||||
import 'package:spotiflac_android/widgets/app_content_card.dart';
|
||||
import 'package:spotiflac_android/widgets/app_switch.dart';
|
||||
import 'package:spotiflac_android/widgets/metadata_text_field.dart';
|
||||
import 'package:spotiflac_android/widgets/settings_group.dart';
|
||||
|
||||
/// Grouped option list used by the metadata editor pickers.
|
||||
class MetadataEditorOptions extends StatelessWidget {
|
||||
final List<Widget> children;
|
||||
|
||||
const MetadataEditorOptions({super.key, required this.children});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!context.isMornye) return SettingsGroup(children: children);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Material(
|
||||
color: MornyeTheme.controlFill(context),
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Column(mainAxisSize: MainAxisSize.min, children: children),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MetadataEditorQuickSelectButton extends StatelessWidget {
|
||||
final String label;
|
||||
final VoidCallback onTap;
|
||||
final ColorScheme cs;
|
||||
final bool fetching;
|
||||
|
||||
const MetadataEditorQuickSelectButton({
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.onTap,
|
||||
required this.cs,
|
||||
required this.fetching,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (context.isMornye) {
|
||||
return CupertinoButton(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||||
onPressed: fetching ? null : onTap,
|
||||
child: Text(
|
||||
label,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall?.copyWith(color: cs.primary),
|
||||
),
|
||||
);
|
||||
}
|
||||
return InkWell(
|
||||
onTap: fetching ? null : onTap,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: cs.outline.withValues(alpha: 0.5)),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.labelSmall?.copyWith(color: cs.primary),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MetadataEditorSwitchField extends StatelessWidget {
|
||||
final ColorScheme colorScheme;
|
||||
final String label;
|
||||
final String subtitle;
|
||||
final bool value;
|
||||
final ValueChanged<bool> onChanged;
|
||||
|
||||
const MetadataEditorSwitchField({
|
||||
super.key,
|
||||
required this.colorScheme,
|
||||
required this.label,
|
||||
required this.subtitle,
|
||||
required this.value,
|
||||
required this.onChanged,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = colorScheme;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 14),
|
||||
child: Column(
|
||||
children: [
|
||||
Material(
|
||||
color: metadataEditorFieldFill(context, cs),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: AppSwitchListTile(
|
||||
adaptive: true,
|
||||
value: value,
|
||||
onChanged: onChanged,
|
||||
contentPadding: EdgeInsets.symmetric(
|
||||
horizontal: context.isMornye ? 4 : 16,
|
||||
),
|
||||
title: Text(label, style: Theme.of(context).textTheme.bodyLarge),
|
||||
subtitle: Text(
|
||||
subtitle,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall?.copyWith(color: cs.onSurfaceVariant),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (context.isMornye) const Divider(height: 0.5),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MetadataEditorSelectionField extends StatelessWidget {
|
||||
final ColorScheme colorScheme;
|
||||
final String label;
|
||||
final String value;
|
||||
final VoidCallback onTap;
|
||||
final bool enabled;
|
||||
|
||||
const MetadataEditorSelectionField({
|
||||
super.key,
|
||||
required this.colorScheme,
|
||||
required this.label,
|
||||
required this.value,
|
||||
required this.onTap,
|
||||
this.enabled = true,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = colorScheme;
|
||||
if (context.isMornye) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 14),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 4, bottom: 6),
|
||||
child: Text(
|
||||
label,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall?.copyWith(color: cs.onSurfaceVariant),
|
||||
),
|
||||
),
|
||||
CupertinoButton(
|
||||
color: metadataEditorFieldFill(context, cs),
|
||||
disabledColor: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 14),
|
||||
onPressed: enabled ? onTap : null,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: enabled ? cs.onSurface : cs.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Icon(
|
||||
CupertinoIcons.chevron_up_chevron_down,
|
||||
size: 16,
|
||||
color: cs.primary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 0.5),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
final radius = BorderRadius.circular(14);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 14),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 4, bottom: 6),
|
||||
child: Text(
|
||||
label,
|
||||
style: Theme.of(context).textTheme.labelMedium?.copyWith(
|
||||
color: enabled ? cs.onSurfaceVariant : cs.outline,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 0.1,
|
||||
),
|
||||
),
|
||||
),
|
||||
Material(
|
||||
color: metadataEditorFieldFill(context, cs),
|
||||
borderRadius: radius,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: InkWell(
|
||||
onTap: enabled ? onTap : null,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 14, 12, 14),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: enabled ? cs.onSurface : cs.outline,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Icon(
|
||||
Icons.expand_more_rounded,
|
||||
color: enabled ? cs.onSurfaceVariant : cs.outline,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Titled section card. When [onHeaderTap] is set the header is a full-width
|
||||
/// tappable row (ripple clipped to the card) with an auto chevron.
|
||||
class MetadataEditorSectionCard extends StatelessWidget {
|
||||
final ColorScheme colorScheme;
|
||||
final IconData icon;
|
||||
final String title;
|
||||
final List<Widget> children;
|
||||
final VoidCallback? onHeaderTap;
|
||||
final bool expanded;
|
||||
|
||||
const MetadataEditorSectionCard({
|
||||
super.key,
|
||||
required this.colorScheme,
|
||||
required this.icon,
|
||||
required this.title,
|
||||
required this.children,
|
||||
this.onHeaderTap,
|
||||
this.expanded = true,
|
||||
});
|
||||
|
||||
RoundedRectangleBorder _sectionCardShape(ColorScheme cs) {
|
||||
return RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
side: BorderSide(color: cs.outlineVariant.withValues(alpha: 0.5)),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cs = colorScheme;
|
||||
final collapsible = onHeaderTap != null;
|
||||
const inset = 16.0;
|
||||
|
||||
final headerRow = Row(
|
||||
children: [
|
||||
Icon(context.adaptiveIcon(icon), size: 20, color: cs.primary),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: cs.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (collapsible)
|
||||
AnimatedRotation(
|
||||
turns: expanded ? 0.5 : 0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeOutCubic,
|
||||
child: Icon(
|
||||
context.isMornye
|
||||
? CupertinoIcons.chevron_down
|
||||
: Icons.expand_more,
|
||||
size: 22,
|
||||
color: cs.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
final Widget header = collapsible
|
||||
? InkWell(
|
||||
onTap: onHeaderTap,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.fromLTRB(inset, 16, inset, 16),
|
||||
child: headerRow,
|
||||
),
|
||||
)
|
||||
: Padding(
|
||||
padding: EdgeInsets.fromLTRB(inset, 16, inset, 8),
|
||||
child: headerRow,
|
||||
);
|
||||
|
||||
final content = AnimatedSize(
|
||||
duration: const Duration(milliseconds: 220),
|
||||
curve: Curves.easeOutCubic,
|
||||
alignment: Alignment.topCenter,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
header,
|
||||
if (children.isNotEmpty)
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(inset, 0, inset, 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: children,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: context.isMornye
|
||||
? Material(
|
||||
color: MornyeTheme.controlFill(context),
|
||||
borderRadius: BorderRadius.circular(28),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: DividerTheme(
|
||||
data: DividerTheme.of(
|
||||
context,
|
||||
).copyWith(color: MornyeTheme.metadataDividerColor(context)),
|
||||
child: content,
|
||||
),
|
||||
)
|
||||
: AppContentCard(
|
||||
elevation: 0,
|
||||
margin: EdgeInsets.zero,
|
||||
color: settingsGroupColor(context),
|
||||
shape: _sectionCardShape(cs),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: content,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user