mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-13 11:53:41 +00:00
fix(ui): fix various UI issues (#1518)
This commit is contained in:
25 files changed
+792
-282
No files matched your search
@@ -21,6 +21,7 @@ class AppChoiceSegmentedControl<T extends Object> extends StatelessWidget {
|
||||
required this.onChanged,
|
||||
this.buttonKeyPrefix = 'choice',
|
||||
this.alignment,
|
||||
this.showIcons = true,
|
||||
});
|
||||
|
||||
final T value;
|
||||
@@ -28,6 +29,7 @@ class AppChoiceSegmentedControl<T extends Object> extends StatelessWidget {
|
||||
final ValueChanged<T> onChanged;
|
||||
final String buttonKeyPrefix;
|
||||
final WrapAlignment? alignment;
|
||||
final bool showIcons;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -44,6 +46,7 @@ class AppChoiceSegmentedControl<T extends Object> extends StatelessWidget {
|
||||
option: option,
|
||||
selected: option.value == value,
|
||||
palette: palette,
|
||||
showIcon: showIcons,
|
||||
onPressed: () => onChanged(option.value),
|
||||
),
|
||||
],
|
||||
@@ -58,12 +61,14 @@ class _ChoiceButton<T extends Object> extends StatelessWidget {
|
||||
required this.selected,
|
||||
required this.palette,
|
||||
required this.onPressed,
|
||||
required this.showIcon,
|
||||
});
|
||||
|
||||
final AppChoiceOption<T> option;
|
||||
final bool selected;
|
||||
final AppPalette palette;
|
||||
final VoidCallback onPressed;
|
||||
final bool showIcon;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -82,8 +87,10 @@ class _ChoiceButton<T extends Object> extends StatelessWidget {
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(option.icon, size: 15, color: selected ? palette.textPrimary : palette.textSecondary),
|
||||
const SizedBox(width: 7),
|
||||
if (showIcon) ...[
|
||||
Icon(option.icon, size: 15, color: selected ? palette.textPrimary : palette.textSecondary),
|
||||
const SizedBox(width: 7),
|
||||
],
|
||||
Text(
|
||||
option.label,
|
||||
style: TextStyle(
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import '../theme/app_design_tokens.dart';
|
||||
import 'app_form_row.dart';
|
||||
|
||||
/// Related fields that share a row on wide forms and get individual labels when stacked.
|
||||
class AppFormPair extends StatelessWidget {
|
||||
const AppFormPair({
|
||||
super.key,
|
||||
required this.firstLabel,
|
||||
required this.secondLabel,
|
||||
required this.first,
|
||||
required this.second,
|
||||
this.direction = Axis.horizontal,
|
||||
this.firstFlex = 1,
|
||||
this.secondFlex = 1,
|
||||
});
|
||||
|
||||
final String firstLabel;
|
||||
final String secondLabel;
|
||||
final Widget first;
|
||||
final Widget second;
|
||||
final Axis direction;
|
||||
final int firstFlex;
|
||||
final int secondFlex;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (direction == Axis.vertical) {
|
||||
return Column(
|
||||
children: [
|
||||
AppFormRow(label: firstLabel, direction: direction, child: first),
|
||||
const SizedBox(height: AppDesignTokens.space12),
|
||||
AppFormRow(label: secondLabel, direction: direction, child: second),
|
||||
],
|
||||
);
|
||||
}
|
||||
return AppFormRow(
|
||||
label: firstLabel,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(flex: firstFlex, child: first),
|
||||
const SizedBox(width: AppDesignTokens.space8),
|
||||
Expanded(flex: secondFlex, child: second),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import '../theme/app_design_tokens.dart';
|
||||
import '../theme/app_palette.dart';
|
||||
|
||||
/// A labelled control whose caller chooses the layout for the available width.
|
||||
class AppFormRow extends StatelessWidget {
|
||||
const AppFormRow({super.key, required this.label, required this.child, this.direction = Axis.horizontal});
|
||||
|
||||
final String label;
|
||||
final Widget child;
|
||||
final Axis direction;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final title = Text(
|
||||
label,
|
||||
style: TextStyle(color: AppPalette.of(context).textPrimary, fontSize: 13, fontWeight: FontWeight.w500),
|
||||
);
|
||||
if (direction == Axis.vertical) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
title,
|
||||
const SizedBox(height: AppDesignTokens.space8),
|
||||
SizedBox(width: double.infinity, child: child),
|
||||
],
|
||||
);
|
||||
}
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 104,
|
||||
child: Padding(padding: const EdgeInsets.only(top: 10), child: title),
|
||||
),
|
||||
const SizedBox(width: AppDesignTokens.space12),
|
||||
Expanded(child: child),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import '../theme/app_design_tokens.dart';
|
||||
import '../theme/app_palette.dart';
|
||||
import '../../l10n/l10n.dart';
|
||||
import 'app_text_field.dart';
|
||||
import 'app_form_row.dart';
|
||||
|
||||
class AppHttpHeadersController extends ChangeNotifier {
|
||||
AppHttpHeadersController({Map<String, String>? headers, Iterable<String> defaultNames = const []}) {
|
||||
@@ -69,84 +70,74 @@ class AppHttpHeadersController extends ChangeNotifier {
|
||||
}
|
||||
|
||||
class AppHttpHeadersEditor extends StatelessWidget {
|
||||
const AppHttpHeadersEditor({super.key, required this.controller, required this.label, required this.keyPrefix});
|
||||
const AppHttpHeadersEditor({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.label,
|
||||
required this.keyPrefix,
|
||||
this.stacked = false,
|
||||
});
|
||||
|
||||
final AppHttpHeadersController controller;
|
||||
final String label;
|
||||
final String keyPrefix;
|
||||
final bool stacked;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final palette = AppPalette.of(context);
|
||||
return Row(
|
||||
key: ValueKey('$keyPrefix-editor'),
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 104,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 10),
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(color: palette.textPrimary, fontSize: 13, fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: AnimatedBuilder(
|
||||
animation: controller,
|
||||
builder: (context, _) => Column(
|
||||
Widget build(BuildContext context) => AppFormRow(
|
||||
key: ValueKey('$keyPrefix-editor'),
|
||||
label: label,
|
||||
direction: stacked ? Axis.vertical : Axis.horizontal,
|
||||
child: AnimatedBuilder(
|
||||
animation: controller,
|
||||
builder: (context, _) => Column(
|
||||
children: [
|
||||
for (var index = 0; index < controller._rows.length; index++) ...[
|
||||
Row(
|
||||
children: [
|
||||
for (var index = 0; index < controller._rows.length; index++) ...[
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 382,
|
||||
child: _HeaderTextField(
|
||||
key: ValueKey('$keyPrefix-name-$index'),
|
||||
controller: controller._rows[index].name,
|
||||
hintText: context.l10n.httpHeaderName,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
flex: 618,
|
||||
child: _HeaderTextField(
|
||||
key: ValueKey('$keyPrefix-value-$index'),
|
||||
controller: controller._rows[index].value,
|
||||
hintText: context.l10n.httpHeaderValue,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
_HeaderAction(
|
||||
key: ValueKey('$keyPrefix-remove-$index'),
|
||||
icon: Icons.remove,
|
||||
onPressed: controller._rows.length == 1 ? null : () => controller.removeAt(index),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Visibility(
|
||||
visible: index == controller._rows.length - 1,
|
||||
maintainAnimation: true,
|
||||
maintainSize: true,
|
||||
maintainState: true,
|
||||
child: _HeaderAction(
|
||||
key: index == controller._rows.length - 1 ? ValueKey('$keyPrefix-add') : null,
|
||||
icon: Icons.add,
|
||||
onPressed: controller.add,
|
||||
),
|
||||
),
|
||||
],
|
||||
Expanded(
|
||||
flex: 382,
|
||||
child: _HeaderTextField(
|
||||
key: ValueKey('$keyPrefix-name-$index'),
|
||||
controller: controller._rows[index].name,
|
||||
hintText: context.l10n.httpHeaderName,
|
||||
),
|
||||
if (index != controller._rows.length - 1) const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
flex: 618,
|
||||
child: _HeaderTextField(
|
||||
key: ValueKey('$keyPrefix-value-$index'),
|
||||
controller: controller._rows[index].value,
|
||||
hintText: context.l10n.httpHeaderValue,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
_HeaderAction(
|
||||
key: ValueKey('$keyPrefix-remove-$index'),
|
||||
icon: Icons.remove,
|
||||
onPressed: controller._rows.length == 1 ? null : () => controller.removeAt(index),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Visibility(
|
||||
visible: index == controller._rows.length - 1,
|
||||
maintainAnimation: true,
|
||||
maintainSize: true,
|
||||
maintainState: true,
|
||||
child: _HeaderAction(
|
||||
key: index == controller._rows.length - 1 ? ValueKey('$keyPrefix-add') : null,
|
||||
icon: Icons.add,
|
||||
onPressed: controller.add,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
if (index != controller._rows.length - 1) const SizedBox(height: 8),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _HttpHeaderRowControllers {
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart';
|
||||
|
||||
import '../../l10n/l10n.dart';
|
||||
import '../theme/app_design_tokens.dart';
|
||||
import '../theme/app_palette.dart';
|
||||
|
||||
/// Explains supported date variables and inserts one at the path's selection.
|
||||
class AppPathPlaceholderButton extends StatelessWidget {
|
||||
const AppPathPlaceholderButton({super.key, required this.controller});
|
||||
|
||||
final TextEditingController controller;
|
||||
|
||||
void _showPlaceholders(BuildContext context) {
|
||||
final selection = controller.selection;
|
||||
final now = DateTime.now();
|
||||
final year = now.year.toString();
|
||||
final month = now.month.toString().padLeft(2, '0');
|
||||
final day = now.day.toString().padLeft(2, '0');
|
||||
final l10n = context.l10n;
|
||||
final placeholders = [
|
||||
('%year%', l10n.placeholderYear, year),
|
||||
('%month%', l10n.placeholderMonth, month),
|
||||
('%day%', l10n.placeholderDay, day),
|
||||
('%date%', l10n.placeholderDate, '$year-$month-$day'),
|
||||
];
|
||||
const PopoverOverlayHandler().show<void>(
|
||||
context: context,
|
||||
alignment: Alignment.topLeft,
|
||||
anchorAlignment: Alignment.bottomLeft,
|
||||
offset: const Offset(0, AppDesignTokens.space4),
|
||||
builder: (popoverContext) => Card(
|
||||
padding: const EdgeInsets.all(AppDesignTokens.space8),
|
||||
child: SizedBox(
|
||||
width: (MediaQuery.sizeOf(context).width - 80).clamp(180.0, 320.0),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxHeight: 280),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
for (final (variable, description, example) in placeholders)
|
||||
GhostButton(
|
||||
key: ValueKey('path-placeholder-$variable'),
|
||||
onPressed: () {
|
||||
final text = controller.text;
|
||||
final valid = selection.isValid && selection.end <= text.length;
|
||||
final start = valid ? selection.start : text.length;
|
||||
final end = valid ? selection.end : text.length;
|
||||
controller.value = TextEditingValue(
|
||||
text: text.replaceRange(start, end, variable),
|
||||
selection: TextSelection.collapsed(offset: start + variable.length),
|
||||
);
|
||||
closeOverlay(popoverContext);
|
||||
},
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('$variable — $description', style: const TextStyle(fontSize: 12)),
|
||||
Text(
|
||||
l10n.example.replaceAll('@value', example),
|
||||
style: TextStyle(color: AppPalette.of(context).textMuted, fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Builder(
|
||||
builder: (anchorContext) =>
|
||||
GhostButton(onPressed: () => _showPlaceholders(anchorContext), child: Text(context.l10n.insertPlaceholder)),
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
/// Switches adjacent tabs after a deliberate horizontal swipe over the content.
|
||||
class AppSwipeTabs<T> extends StatefulWidget {
|
||||
const AppSwipeTabs({
|
||||
super.key,
|
||||
required this.values,
|
||||
required this.selectedValue,
|
||||
required this.onSelected,
|
||||
required this.child,
|
||||
this.enabled = true,
|
||||
});
|
||||
|
||||
final List<T> values;
|
||||
final T selectedValue;
|
||||
final ValueChanged<T> onSelected;
|
||||
final Widget child;
|
||||
final bool enabled;
|
||||
|
||||
@override
|
||||
State<AppSwipeTabs<T>> createState() => _AppSwipeTabsState<T>();
|
||||
}
|
||||
|
||||
class _AppSwipeTabsState<T> extends State<AppSwipeTabs<T>> {
|
||||
double _distance = 0;
|
||||
|
||||
void _finish(DragEndDetails details) {
|
||||
final distance = _distance;
|
||||
_distance = 0;
|
||||
final velocity = details.primaryVelocity ?? 0;
|
||||
if (distance.abs() < 48 && (distance.abs() < 12 || velocity.abs() < 400)) return;
|
||||
final direction = velocity.abs() >= 400 ? velocity : distance;
|
||||
final index = widget.values.indexOf(widget.selectedValue);
|
||||
if (index < 0) return;
|
||||
final forward = Directionality.of(context) == TextDirection.rtl ? direction > 0 : direction < 0;
|
||||
final nextIndex = index + (forward ? 1 : -1);
|
||||
if (nextIndex >= 0 && nextIndex < widget.values.length) widget.onSelected(widget.values[nextIndex]);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onHorizontalDragStart: widget.enabled ? (_) => _distance = 0 : null,
|
||||
onHorizontalDragUpdate: widget.enabled ? (details) => _distance += details.primaryDelta ?? 0 : null,
|
||||
onHorizontalDragEnd: widget.enabled ? _finish : null,
|
||||
onHorizontalDragCancel: widget.enabled ? () => _distance = 0 : null,
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/gestures.dart' show Drag;
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:shadcn_flutter/shadcn_flutter.dart' hide Column, Expanded, Row;
|
||||
|
||||
@@ -69,6 +70,7 @@ class FileTreeView<T> extends StatefulWidget {
|
||||
this.rowHeight = _treeRowHeight,
|
||||
this.contentTextStyle,
|
||||
this.iconSize = 18,
|
||||
this.initialExpandedDepth,
|
||||
});
|
||||
|
||||
final List<FileTreeItem<T>> items;
|
||||
@@ -85,6 +87,9 @@ class FileTreeView<T> extends StatefulWidget {
|
||||
final TextStyle? contentTextStyle;
|
||||
final double iconSize;
|
||||
|
||||
/// Number of directory levels initially expanded; null expands all levels.
|
||||
final int? initialExpandedDepth;
|
||||
|
||||
@override
|
||||
State<FileTreeView<T>> createState() => _FileTreeViewState<T>();
|
||||
}
|
||||
@@ -95,6 +100,7 @@ class _FileTreeViewState<T> extends State<FileTreeView<T>> with SingleTickerProv
|
||||
late final Animation<double> _reveal;
|
||||
final _scrollController = ScrollController();
|
||||
final _horizontalScrollController = ScrollController();
|
||||
Drag? _horizontalDrag;
|
||||
final Map<String, double> _labelWidthCache = {};
|
||||
TextStyle? _labelMeasurementStyle;
|
||||
TextDirection? _labelMeasurementDirection;
|
||||
@@ -109,7 +115,7 @@ class _FileTreeViewState<T> extends State<FileTreeView<T>> with SingleTickerProv
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_nodes = _buildTree(widget.items);
|
||||
_nodes = _buildTree(widget.items, expandedDepth: widget.initialExpandedDepth);
|
||||
_allFoldersExpanded = _foldersAreExpanded(_nodes);
|
||||
_transitionController = AnimationController(vsync: this, duration: _treeTransitionDuration, value: 1)
|
||||
..addStatusListener(_onTransitionStatus);
|
||||
@@ -134,6 +140,7 @@ class _FileTreeViewState<T> extends State<FileTreeView<T>> with SingleTickerProv
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_horizontalDrag?.cancel();
|
||||
_transitionController.removeStatusListener(_onTransitionStatus);
|
||||
_transitionController.dispose();
|
||||
_scrollController.dispose();
|
||||
@@ -286,26 +293,36 @@ class _FileTreeViewState<T> extends State<FileTreeView<T>> with SingleTickerProv
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: ClipRect(
|
||||
child: AnimatedBuilder(
|
||||
animation: _horizontalScrollController,
|
||||
builder: (context, child) {
|
||||
final offset = horizontalScrollEnabled && _horizontalScrollController.hasClients
|
||||
? _horizontalScrollController.offset
|
||||
: 0.0;
|
||||
return Transform.translate(offset: Offset(-offset, 0), child: child);
|
||||
},
|
||||
child: OverflowBox(
|
||||
alignment: Alignment.centerLeft,
|
||||
minWidth: 0,
|
||||
maxWidth: double.infinity,
|
||||
child: SizedBox(
|
||||
width: nameContentWidth,
|
||||
child: Text(
|
||||
data.label,
|
||||
maxLines: 1,
|
||||
softWrap: false,
|
||||
style: contentTextStyle.copyWith(color: palette.textPrimary),
|
||||
child: GestureDetector(
|
||||
key: ValueKey('${widget.keyPrefix}-name-scroll-${data.key}'),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onHorizontalDragStart: horizontalScrollEnabled ? _startNameDrag : null,
|
||||
onHorizontalDragUpdate: horizontalScrollEnabled
|
||||
? (details) => _horizontalDrag?.update(details)
|
||||
: null,
|
||||
onHorizontalDragEnd: horizontalScrollEnabled ? (details) => _horizontalDrag?.end(details) : null,
|
||||
onHorizontalDragCancel: horizontalScrollEnabled ? () => _horizontalDrag?.cancel() : null,
|
||||
child: ClipRect(
|
||||
child: AnimatedBuilder(
|
||||
animation: _horizontalScrollController,
|
||||
builder: (context, child) {
|
||||
final offset = horizontalScrollEnabled && _horizontalScrollController.hasClients
|
||||
? _horizontalScrollController.offset
|
||||
: 0.0;
|
||||
return Transform.translate(offset: Offset(-offset, 0), child: child);
|
||||
},
|
||||
child: OverflowBox(
|
||||
alignment: Alignment.centerLeft,
|
||||
minWidth: 0,
|
||||
maxWidth: double.infinity,
|
||||
child: SizedBox(
|
||||
width: nameContentWidth,
|
||||
child: Text(
|
||||
data.label,
|
||||
maxLines: 1,
|
||||
softWrap: false,
|
||||
style: contentTextStyle.copyWith(color: palette.textPrimary),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -327,6 +344,12 @@ class _FileTreeViewState<T> extends State<FileTreeView<T>> with SingleTickerProv
|
||||
);
|
||||
}
|
||||
|
||||
void _startNameDrag(DragStartDetails details) {
|
||||
_horizontalDrag?.cancel();
|
||||
if (!_horizontalScrollController.hasClients) return;
|
||||
_horizontalDrag = _horizontalScrollController.position.drag(details, () => _horizontalDrag = null);
|
||||
}
|
||||
|
||||
Widget _buildTreeTransition(BuildContext context, VirtualTreeEntry<FileTreeNode<T>> entry, Widget row) {
|
||||
if (_transitioningNodeKeys.contains(entry.node.data.key)) {
|
||||
return SizeTransition(
|
||||
@@ -830,8 +853,10 @@ class _BuildNode<T> {
|
||||
|
||||
int get size => item?.size ?? children.fold<int>(0, (sum, child) => sum + child.size);
|
||||
|
||||
TreeItemNode<FileTreeNode<T>> toTreeItem() {
|
||||
final childItems = children.map((child) => child.toTreeItem()).toList(growable: false);
|
||||
TreeItemNode<FileTreeNode<T>> toTreeItem({int? expandedDepth, int depth = 0}) {
|
||||
final childItems = children
|
||||
.map((child) => child.toTreeItem(expandedDepth: expandedDepth, depth: depth + 1))
|
||||
.toList(growable: false);
|
||||
final leaves = item == null ? [for (final child in childItems) ...child.data.leafItems] : <FileTreeItem<T>>[item!];
|
||||
return TreeItemNode(
|
||||
data: FileTreeNode(
|
||||
@@ -842,13 +867,13 @@ class _BuildNode<T> {
|
||||
originalIndex: originalIndex,
|
||||
item: item,
|
||||
),
|
||||
expanded: true,
|
||||
expanded: expandedDepth == null || depth < expandedDepth,
|
||||
children: childItems,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
List<TreeNode<FileTreeNode<T>>> _buildTree<T>(List<FileTreeItem<T>> items) {
|
||||
List<TreeNode<FileTreeNode<T>>> _buildTree<T>(List<FileTreeItem<T>> items, {int? expandedDepth}) {
|
||||
final root = <_BuildNode<T>>[];
|
||||
final folders = <String, _BuildNode<T>>{};
|
||||
var originalIndex = 0;
|
||||
@@ -885,7 +910,7 @@ List<TreeNode<FileTreeNode<T>>> _buildTree<T>(List<FileTreeItem<T>> items) {
|
||||
}
|
||||
}
|
||||
|
||||
return root.map((node) => node.toTreeItem()).toList(growable: false);
|
||||
return root.map((node) => node.toTreeItem(expandedDepth: expandedDepth)).toList(growable: false);
|
||||
}
|
||||
|
||||
List<String> _visibleNodeKeysInOrder<T>(List<TreeNode<FileTreeNode<T>>> nodes) {
|
||||
|
||||
@@ -89,7 +89,7 @@ class _ResponsiveMenuLayoutState<T> extends State<ResponsiveMenuLayout<T>> {
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
selectedValue: widget.selectedValue,
|
||||
selectedValue: null,
|
||||
onSelected: widget.onSelected,
|
||||
footer: widget.sidebarFooter,
|
||||
mobile: true,
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'package:flutter/widgets.dart';
|
||||
import '../../core/utils/breakpoints.dart';
|
||||
import '../theme/app_palette.dart';
|
||||
import 'secondary_navigation_pane.dart';
|
||||
import 'app_swipe_tabs.dart';
|
||||
|
||||
class ResponsiveNavigationItem<T> {
|
||||
const ResponsiveNavigationItem({required this.value, required this.label, this.icon, this.count});
|
||||
@@ -72,7 +73,14 @@ class ResponsiveNavigationLayout<T> extends StatelessWidget {
|
||||
onSelected: onSelected,
|
||||
reserveTopSafeArea: mobileHeader == null,
|
||||
),
|
||||
Expanded(child: child),
|
||||
Expanded(
|
||||
child: AppSwipeTabs<T>(
|
||||
values: items.map((item) => item.value).toList(growable: false),
|
||||
selectedValue: selectedValue,
|
||||
onSelected: onSelected,
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ class SecondaryNavigationPane<T> extends StatelessWidget {
|
||||
|
||||
final String title;
|
||||
final List<SecondaryNavigationPaneItem<T>> items;
|
||||
final T selectedValue;
|
||||
final T? selectedValue;
|
||||
final ValueChanged<T> onSelected;
|
||||
final Widget? footer;
|
||||
final bool mobile;
|
||||
|
||||
Reference in new issue
Block a user