v7
This commit is contained in:
@@ -2,32 +2,47 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
class HoverIconButton extends StatefulWidget {
|
class HoverIconButton extends StatefulWidget {
|
||||||
final VoidCallback onPressed;
|
final VoidCallback? onPressed;
|
||||||
final IconData icon;
|
final IconData icon;
|
||||||
final double size;
|
final double size;
|
||||||
final Color? hoverColor;
|
final Color? hoverColor;
|
||||||
final Color? defaultColor;
|
final Color? defaultColor;
|
||||||
const HoverIconButton({super.key,required this.onPressed ,required this.icon,this.hoverColor,this.size=24,this.defaultColor});
|
final MouseCursor cursor;
|
||||||
|
|
||||||
|
const HoverIconButton({
|
||||||
|
super.key,
|
||||||
|
required this.onPressed,
|
||||||
|
required this.icon,
|
||||||
|
this.hoverColor,
|
||||||
|
this.size = 24,
|
||||||
|
this.defaultColor,
|
||||||
|
this.cursor = SystemMouseCursors.cell,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<HoverIconButton> createState() => _HoverIconButtonState();
|
State<HoverIconButton> createState() => _HoverIconButtonState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _HoverIconButtonState extends State<HoverIconButton> {
|
class _HoverIconButtonState extends State<HoverIconButton> {
|
||||||
|
|
||||||
bool _hover = false;
|
bool _hover = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Color? color = (_hover)?widget.hoverColor??Theme.of(context).primaryColor:(widget.defaultColor??null);
|
Color? color = (_hover)
|
||||||
|
? widget.hoverColor ?? Theme.of(context).primaryColor
|
||||||
|
: (widget.defaultColor ?? null);
|
||||||
|
|
||||||
return MouseRegion(
|
return MouseRegion(
|
||||||
cursor: SystemMouseCursors.click,
|
cursor: widget.cursor,
|
||||||
onEnter: _onEnter,
|
onEnter: _onEnter,
|
||||||
onExit: _onExit,
|
onExit: _onExit,
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: widget.onPressed,
|
onTap: widget.onPressed,
|
||||||
child: Icon(widget.icon,size: widget.size,color: color,)),
|
child: Icon(
|
||||||
|
widget.icon,
|
||||||
|
size: widget.size,
|
||||||
|
color: color,
|
||||||
|
)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
117
lib/components/toly_ui/popable/pop_menu.dart
Normal file
117
lib/components/toly_ui/popable/pop_menu.dart
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'toly_pop_menu.dart';
|
||||||
|
|
||||||
|
class PopPanel<T> extends StatefulWidget {
|
||||||
|
final Widget child;
|
||||||
|
final Size size;
|
||||||
|
final Offset offset;
|
||||||
|
|
||||||
|
/// Builds the context menu.
|
||||||
|
final Widget panel;
|
||||||
|
|
||||||
|
const PopPanel({
|
||||||
|
super.key,
|
||||||
|
required this.child,
|
||||||
|
required this.panel,
|
||||||
|
this.offset = Offset.zero,
|
||||||
|
this.size = const Size(250, 0),
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<PopPanel> createState() => _PopPanelState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PopPanelState<T> extends State<PopPanel<T>> {
|
||||||
|
final ContextMenuController _contextMenuController = ContextMenuController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: _onTap,
|
||||||
|
child: widget.child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _show() {
|
||||||
|
TolyPopupMenuEntry<T> item = CustomTolyMenuItem(
|
||||||
|
widget.panel,
|
||||||
|
size: widget.size,
|
||||||
|
);
|
||||||
|
|
||||||
|
final RenderBox button = context.findRenderObject()! as RenderBox;
|
||||||
|
final RenderBox overlay =
|
||||||
|
Navigator.of(context).overlay!.context.findRenderObject()! as RenderBox;
|
||||||
|
final Offset offset = Offset(button.size.width / 2, button.size.height)+widget.offset;
|
||||||
|
final RelativeRect position = RelativeRect.fromRect(
|
||||||
|
Rect.fromPoints(
|
||||||
|
button.localToGlobal(offset, ancestor: overlay),
|
||||||
|
button.localToGlobal(button.size.bottomRight(Offset.zero) + offset,
|
||||||
|
ancestor: overlay),
|
||||||
|
),
|
||||||
|
Offset.zero & overlay.size,
|
||||||
|
);
|
||||||
|
showTolyMenu<T?>(
|
||||||
|
elevation: 0,
|
||||||
|
color: Colors.transparent,
|
||||||
|
context: context,
|
||||||
|
items: [item],
|
||||||
|
position: position,
|
||||||
|
).then<void>((T? newValue) {
|
||||||
|
if (!mounted) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (newValue == null) {
|
||||||
|
// widget.onCanceled?.call();
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// widget.onSelected?.call(newValue);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onTap() {
|
||||||
|
_show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CustomTolyMenuItem<T> extends TolyPopupMenuEntry<T> {
|
||||||
|
final Size size;
|
||||||
|
final Widget content;
|
||||||
|
|
||||||
|
const CustomTolyMenuItem(this.content, {super.key, required this.size});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<StatefulWidget> createState() => _CustomTolyMenuItemState();
|
||||||
|
|
||||||
|
@override
|
||||||
|
// TODO: implement height
|
||||||
|
double get height => kMinInteractiveDimension;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool represents(value) => true;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CustomTolyMenuItemState<T> extends State<CustomTolyMenuItem> {
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(covariant CustomTolyMenuItem oldWidget) {
|
||||||
|
print('============_CustomTolyMenuItemState#didUpdateWidget');
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
width: widget.size.width,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(color: Colors.black.withOpacity(0.1), blurRadius: 6)
|
||||||
|
]),
|
||||||
|
// padding: EdgeInsets.symmetric(horizontal: 12, vertical: 16),
|
||||||
|
child: widget.content,
|
||||||
|
// color: Colors.lightBlueAccent,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
52
lib/components/toly_ui/popable/popover.dart
Normal file
52
lib/components/toly_ui/popable/popover.dart
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
/// A builder that includes an Offset to draw the context menu at.
|
||||||
|
typedef ContextMenuBuilder = Widget Function(BuildContext context, Offset offset);
|
||||||
|
|
||||||
|
|
||||||
|
class Popover extends StatefulWidget {
|
||||||
|
final Widget child;
|
||||||
|
/// Builds the context menu.
|
||||||
|
final ContextMenuBuilder contextMenuBuilder;
|
||||||
|
|
||||||
|
const Popover({
|
||||||
|
super.key,
|
||||||
|
required this.child,
|
||||||
|
required this.contextMenuBuilder,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<Popover> createState() => _PopoverState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PopoverState extends State<Popover> {
|
||||||
|
|
||||||
|
final ContextMenuController _contextMenuController = ContextMenuController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: _onTap,
|
||||||
|
child: widget.child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _show(Offset position) {
|
||||||
|
_contextMenuController.show(
|
||||||
|
context: context,
|
||||||
|
contextMenuBuilder: (BuildContext context) {
|
||||||
|
return widget.contextMenuBuilder(context, position);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final double width = 200;
|
||||||
|
|
||||||
|
void _onTap() {
|
||||||
|
final RenderBox button = context.findRenderObject()! as RenderBox;
|
||||||
|
final RenderBox overlay = Navigator.of(context).overlay!.context.findRenderObject()! as RenderBox;
|
||||||
|
final Offset offset = Offset(button.size.width / 2, button.size.height);
|
||||||
|
Offset boxOffset = button.localToGlobal(offset, ancestor: overlay);
|
||||||
|
_show(boxOffset.translate(-width, 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
1433
lib/components/toly_ui/popable/toly_pop_menu.dart
Normal file
1433
lib/components/toly_ui/popable/toly_pop_menu.dart
Normal file
File diff suppressed because it is too large
Load Diff
@@ -3,3 +3,5 @@ export 'navigation/menu_meta.dart';
|
|||||||
export 'navigation/toly_breadcrumb.dart';
|
export 'navigation/toly_breadcrumb.dart';
|
||||||
export 'navigation/toly_navigation_rail.dart';
|
export 'navigation/toly_navigation_rail.dart';
|
||||||
export 'popable/drop_selectable_widget.dart';
|
export 'popable/drop_selectable_widget.dart';
|
||||||
|
export 'popable/popover.dart';
|
||||||
|
export 'popable/pop_menu.dart';
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
// This sample demonstrates allowing a context menu to be shown in a widget
|
||||||
|
// subtree in response to user gestures.
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
void main() => runApp(const ContextMenuControllerExampleApp());
|
||||||
|
|
||||||
|
/// A builder that includes an Offset to draw the context menu at.
|
||||||
|
typedef ContextMenuBuilder = Widget Function(BuildContext context, Offset offset);
|
||||||
|
|
||||||
|
class ContextMenuControllerExampleApp extends StatefulWidget {
|
||||||
|
const ContextMenuControllerExampleApp({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ContextMenuControllerExampleApp> createState() => _ContextMenuControllerExampleAppState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ContextMenuControllerExampleAppState extends State<ContextMenuControllerExampleApp> {
|
||||||
|
void _showDialog(BuildContext context) {
|
||||||
|
Navigator.of(context).push(
|
||||||
|
DialogRoute<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) => const AlertDialog(title: Text('You clicked print!')),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
// On web, disable the browser's context menu since this example uses a custom
|
||||||
|
// Flutter-rendered context menu.
|
||||||
|
if (kIsWeb) {
|
||||||
|
BrowserContextMenu.disableContextMenu();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
if (kIsWeb) {
|
||||||
|
BrowserContextMenu.enableContextMenu();
|
||||||
|
}
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Context menu outside of text'),
|
||||||
|
),
|
||||||
|
body: _ContextMenuRegion(
|
||||||
|
contextMenuBuilder: (BuildContext context, Offset offset) {
|
||||||
|
// The custom context menu will look like the default context menu
|
||||||
|
// on the current platform with a single 'Print' button.
|
||||||
|
return AdaptiveTextSelectionToolbar.buttonItems(
|
||||||
|
anchors: TextSelectionToolbarAnchors(
|
||||||
|
primaryAnchor: offset,
|
||||||
|
),
|
||||||
|
buttonItems: <ContextMenuButtonItem>[
|
||||||
|
ContextMenuButtonItem(
|
||||||
|
onPressed: () {
|
||||||
|
ContextMenuController.removeAny();
|
||||||
|
_showDialog(context);
|
||||||
|
},
|
||||||
|
label: 'Print',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
// In this case this wraps a big open space in a GestureDetector in
|
||||||
|
// order to show the context menu, but it could also wrap a single
|
||||||
|
// widget like an Image to give it a context menu.
|
||||||
|
child: ListView(
|
||||||
|
children: <Widget>[
|
||||||
|
Container(height: 20.0),
|
||||||
|
const Text(
|
||||||
|
'Right click (desktop) or long press (mobile) anywhere, not just on this text, to show the custom menu.'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shows and hides the context menu based on user gestures.
|
||||||
|
///
|
||||||
|
/// By default, shows the menu on right clicks and long presses.
|
||||||
|
class _ContextMenuRegion extends StatefulWidget {
|
||||||
|
/// Creates an instance of [_ContextMenuRegion].
|
||||||
|
const _ContextMenuRegion({
|
||||||
|
required this.child,
|
||||||
|
required this.contextMenuBuilder,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Builds the context menu.
|
||||||
|
final ContextMenuBuilder contextMenuBuilder;
|
||||||
|
|
||||||
|
/// The child widget that will be listened to for gestures.
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_ContextMenuRegion> createState() => _ContextMenuRegionState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ContextMenuRegionState extends State<_ContextMenuRegion> {
|
||||||
|
Offset? _longPressOffset;
|
||||||
|
|
||||||
|
final ContextMenuController _contextMenuController = ContextMenuController();
|
||||||
|
|
||||||
|
static bool get _longPressEnabled {
|
||||||
|
switch (defaultTargetPlatform) {
|
||||||
|
case TargetPlatform.android:
|
||||||
|
case TargetPlatform.iOS:
|
||||||
|
return true;
|
||||||
|
case TargetPlatform.macOS:
|
||||||
|
case TargetPlatform.fuchsia:
|
||||||
|
case TargetPlatform.linux:
|
||||||
|
case TargetPlatform.windows:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onSecondaryTapUp(TapUpDetails details) {
|
||||||
|
_show(details.globalPosition);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onTap() {
|
||||||
|
if (!_contextMenuController.isShown) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onLongPressStart(LongPressStartDetails details) {
|
||||||
|
_longPressOffset = details.globalPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onLongPress() {
|
||||||
|
assert(_longPressOffset != null);
|
||||||
|
_show(_longPressOffset!);
|
||||||
|
_longPressOffset = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _show(Offset position) {
|
||||||
|
_contextMenuController.show(
|
||||||
|
context: context,
|
||||||
|
contextMenuBuilder: (BuildContext context) {
|
||||||
|
return widget.contextMenuBuilder(context, position);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _hide() {
|
||||||
|
_contextMenuController.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_hide();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return GestureDetector(
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
onSecondaryTapUp: _onSecondaryTapUp,
|
||||||
|
onTap: _onTap,
|
||||||
|
onLongPress: _longPressEnabled ? _onLongPress : null,
|
||||||
|
onLongPressStart: _longPressEnabled ? _onLongPressStart : null,
|
||||||
|
child: widget.child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
// This example demonstrates showing the default buttons, but customizing their
|
||||||
|
// appearance.
|
||||||
|
|
||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
void main() => runApp(const EditableTextToolbarBuilderExampleApp());
|
||||||
|
|
||||||
|
class EditableTextToolbarBuilderExampleApp extends StatefulWidget {
|
||||||
|
const EditableTextToolbarBuilderExampleApp({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<EditableTextToolbarBuilderExampleApp> createState() => _EditableTextToolbarBuilderExampleAppState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EditableTextToolbarBuilderExampleAppState extends State<EditableTextToolbarBuilderExampleApp> {
|
||||||
|
final TextEditingController _controller = TextEditingController(
|
||||||
|
text: 'Right click (desktop) or long press (mobile) to see the menu with custom buttons.',
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
// On web, disable the browser's context menu since this example uses a custom
|
||||||
|
// Flutter-rendered context menu.
|
||||||
|
if (kIsWeb) {
|
||||||
|
BrowserContextMenu.disableContextMenu();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
if (kIsWeb) {
|
||||||
|
BrowserContextMenu.enableContextMenu();
|
||||||
|
}
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Custom button appearance'),
|
||||||
|
),
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
const SizedBox(height: 20.0),
|
||||||
|
TextField(
|
||||||
|
controller: _controller,
|
||||||
|
contextMenuBuilder: (BuildContext context, EditableTextState editableTextState) {
|
||||||
|
return AdaptiveTextSelectionToolbar(
|
||||||
|
anchors: editableTextState.contextMenuAnchors,
|
||||||
|
// Build the default buttons, but make them look custom.
|
||||||
|
// In a real project you may want to build different
|
||||||
|
// buttons depending on the platform.
|
||||||
|
children: editableTextState.contextMenuButtonItems.map((ContextMenuButtonItem buttonItem) {
|
||||||
|
return CupertinoButton(
|
||||||
|
borderRadius: null,
|
||||||
|
color: const Color(0xffaaaa00),
|
||||||
|
disabledColor: const Color(0xffaaaaff),
|
||||||
|
onPressed: buttonItem.onPressed,
|
||||||
|
padding: const EdgeInsets.all(10.0),
|
||||||
|
pressedOpacity: 0.7,
|
||||||
|
child: SizedBox(
|
||||||
|
width: 200.0,
|
||||||
|
child: Text(
|
||||||
|
CupertinoTextSelectionToolbarButton.getButtonLabel(context, buttonItem),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
// This example demonstrates showing a custom context menu only when some
|
||||||
|
// narrowly defined text is selected.
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
void main() => runApp(const EditableTextToolbarBuilderExampleApp());
|
||||||
|
|
||||||
|
const String emailAddress = 'me@example.com';
|
||||||
|
const String text = 'Select the email address and open the menu: $emailAddress';
|
||||||
|
|
||||||
|
class EditableTextToolbarBuilderExampleApp extends StatefulWidget {
|
||||||
|
const EditableTextToolbarBuilderExampleApp({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<EditableTextToolbarBuilderExampleApp> createState() => _EditableTextToolbarBuilderExampleAppState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EditableTextToolbarBuilderExampleAppState extends State<EditableTextToolbarBuilderExampleApp> {
|
||||||
|
final TextEditingController _controller = TextEditingController(
|
||||||
|
text: text,
|
||||||
|
);
|
||||||
|
|
||||||
|
void _showDialog(BuildContext context) {
|
||||||
|
Navigator.of(context).push(
|
||||||
|
DialogRoute<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) => const AlertDialog(title: Text('You clicked send email!')),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
// On web, disable the browser's context menu since this example uses a custom
|
||||||
|
// Flutter-rendered context menu.
|
||||||
|
if (kIsWeb) {
|
||||||
|
BrowserContextMenu.disableContextMenu();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
if (kIsWeb) {
|
||||||
|
BrowserContextMenu.enableContextMenu();
|
||||||
|
}
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Custom button for emails'),
|
||||||
|
),
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
Container(height: 20.0),
|
||||||
|
TextField(
|
||||||
|
controller: _controller,
|
||||||
|
contextMenuBuilder: (BuildContext context, EditableTextState editableTextState) {
|
||||||
|
final List<ContextMenuButtonItem> buttonItems = editableTextState.contextMenuButtonItems;
|
||||||
|
// Here we add an "Email" button to the default TextField
|
||||||
|
// context menu for the current platform, but only if an email
|
||||||
|
// address is currently selected.
|
||||||
|
final TextEditingValue value = _controller.value;
|
||||||
|
if (_isValidEmail(value.selection.textInside(value.text))) {
|
||||||
|
buttonItems.insert(
|
||||||
|
0,
|
||||||
|
ContextMenuButtonItem(
|
||||||
|
label: 'Send email',
|
||||||
|
onPressed: () {
|
||||||
|
ContextMenuController.removeAny();
|
||||||
|
_showDialog(context);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return AdaptiveTextSelectionToolbar.buttonItems(
|
||||||
|
anchors: editableTextState.contextMenuAnchors,
|
||||||
|
buttonItems: buttonItems,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isValidEmail(String text) {
|
||||||
|
return RegExp(
|
||||||
|
r'(?<name>[a-zA-Z0-9]+)'
|
||||||
|
r'@'
|
||||||
|
r'(?<domain>[a-zA-Z0-9]+)'
|
||||||
|
r'\.'
|
||||||
|
r'(?<topLevelDomain>[a-zA-Z0-9]+)',
|
||||||
|
).hasMatch(text);
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
// This example demonstrates how to create a custom toolbar that retains the
|
||||||
|
// look of the default buttons for the current platform.
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
void main() => runApp(const EditableTextToolbarBuilderExampleApp());
|
||||||
|
|
||||||
|
class EditableTextToolbarBuilderExampleApp extends StatefulWidget {
|
||||||
|
const EditableTextToolbarBuilderExampleApp({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<EditableTextToolbarBuilderExampleApp> createState() => _EditableTextToolbarBuilderExampleAppState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EditableTextToolbarBuilderExampleAppState extends State<EditableTextToolbarBuilderExampleApp> {
|
||||||
|
final TextEditingController _controller = TextEditingController(
|
||||||
|
text: 'Right click (desktop) or long press (mobile) to see the menu with a custom toolbar.',
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
// On web, disable the browser's context menu since this example uses a custom
|
||||||
|
// Flutter-rendered context menu.
|
||||||
|
if (kIsWeb) {
|
||||||
|
BrowserContextMenu.disableContextMenu();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
if (kIsWeb) {
|
||||||
|
BrowserContextMenu.enableContextMenu();
|
||||||
|
}
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Custom toolbar, default-looking buttons'),
|
||||||
|
),
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
const SizedBox(height: 20.0),
|
||||||
|
TextField(
|
||||||
|
controller: _controller,
|
||||||
|
contextMenuBuilder: (BuildContext context, EditableTextState editableTextState) {
|
||||||
|
return _MyTextSelectionToolbar(
|
||||||
|
anchor: editableTextState.contextMenuAnchors.primaryAnchor,
|
||||||
|
// getAdaptiveButtons creates the default button widgets for
|
||||||
|
// the current platform.
|
||||||
|
children: AdaptiveTextSelectionToolbar.getAdaptiveButtons(
|
||||||
|
context,
|
||||||
|
// These buttons just close the menu when clicked.
|
||||||
|
<ContextMenuButtonItem>[
|
||||||
|
ContextMenuButtonItem(
|
||||||
|
label: 'One',
|
||||||
|
onPressed: () => ContextMenuController.removeAny(),
|
||||||
|
),
|
||||||
|
ContextMenuButtonItem(
|
||||||
|
label: 'Two',
|
||||||
|
onPressed: () => ContextMenuController.removeAny(),
|
||||||
|
),
|
||||||
|
ContextMenuButtonItem(
|
||||||
|
label: 'Three',
|
||||||
|
onPressed: () => ContextMenuController.removeAny(),
|
||||||
|
),
|
||||||
|
ContextMenuButtonItem(
|
||||||
|
label: 'Four',
|
||||||
|
onPressed: () => ContextMenuController.removeAny(),
|
||||||
|
),
|
||||||
|
ContextMenuButtonItem(
|
||||||
|
label: 'Five',
|
||||||
|
onPressed: () => ContextMenuController.removeAny(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).toList(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A simple, yet totally custom, text selection toolbar.
|
||||||
|
///
|
||||||
|
/// Displays its children in a scrollable grid.
|
||||||
|
class _MyTextSelectionToolbar extends StatelessWidget {
|
||||||
|
const _MyTextSelectionToolbar({
|
||||||
|
required this.anchor,
|
||||||
|
required this.children,
|
||||||
|
});
|
||||||
|
|
||||||
|
final Offset anchor;
|
||||||
|
final List<Widget> children;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Stack(
|
||||||
|
children: <Widget>[
|
||||||
|
Positioned(
|
||||||
|
top: anchor.dy,
|
||||||
|
left: anchor.dx,
|
||||||
|
child: Container(
|
||||||
|
width: 200.0,
|
||||||
|
height: 200.0,
|
||||||
|
color: Colors.cyanAccent.withOpacity(0.5),
|
||||||
|
child: GridView.count(
|
||||||
|
padding: const EdgeInsets.all(12.0),
|
||||||
|
crossAxisCount: 2,
|
||||||
|
children: children,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
// Copyright 2014 The Flutter Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
// This example demonstrates a custom context menu in non-editable text using
|
||||||
|
// SelectionArea.
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
void main() => runApp(const SelectableRegionToolbarBuilderExampleApp());
|
||||||
|
|
||||||
|
const String text =
|
||||||
|
'I am some text inside of SelectionArea. Right click (desktop) or long press (mobile) me to show the customized context menu.';
|
||||||
|
|
||||||
|
class SelectableRegionToolbarBuilderExampleApp extends StatefulWidget {
|
||||||
|
const SelectableRegionToolbarBuilderExampleApp({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SelectableRegionToolbarBuilderExampleApp> createState() => _SelectableRegionToolbarBuilderExampleAppState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SelectableRegionToolbarBuilderExampleAppState extends State<SelectableRegionToolbarBuilderExampleApp> {
|
||||||
|
void _showDialog(BuildContext context) {
|
||||||
|
Navigator.of(context).push(
|
||||||
|
DialogRoute<void>(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) => const AlertDialog(title: Text('You clicked print!')),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
// On web, disable the browser's context menu since this example uses a custom
|
||||||
|
// Flutter-rendered context menu.
|
||||||
|
if (kIsWeb) {
|
||||||
|
BrowserContextMenu.disableContextMenu();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
if (kIsWeb) {
|
||||||
|
BrowserContextMenu.enableContextMenu();
|
||||||
|
}
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp(
|
||||||
|
home: Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Context menu anywhere'),
|
||||||
|
),
|
||||||
|
body: Center(
|
||||||
|
child: SizedBox(
|
||||||
|
width: 200.0,
|
||||||
|
child: SelectionArea(
|
||||||
|
contextMenuBuilder: (
|
||||||
|
BuildContext context,
|
||||||
|
SelectableRegionState selectableRegionState,
|
||||||
|
) {
|
||||||
|
return AdaptiveTextSelectionToolbar.buttonItems(
|
||||||
|
anchors: selectableRegionState.contextMenuAnchors,
|
||||||
|
buttonItems: <ContextMenuButtonItem>[
|
||||||
|
...selectableRegionState.contextMenuButtonItems,
|
||||||
|
ContextMenuButtonItem(
|
||||||
|
onPressed: () {
|
||||||
|
ContextMenuController.removeAny();
|
||||||
|
_showDialog(context);
|
||||||
|
},
|
||||||
|
label: 'Print',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: ListView(
|
||||||
|
children: const <Widget>[
|
||||||
|
SizedBox(height: 20.0),
|
||||||
|
Text(text),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:window_manager/window_manager.dart';
|
import 'package:window_manager/window_manager.dart';
|
||||||
|
|
||||||
import 'v6/app.dart';
|
import 'v7/app.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|||||||
@@ -1,8 +1,17 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'iroute.dart';
|
import '../../../pages/color/color_detail_page.dart';
|
||||||
|
import '../../../pages/color/color_page.dart';
|
||||||
|
import '../../../pages/empty/empty_page.dart';
|
||||||
|
import '../../../pages/settings/settings_page.dart';
|
||||||
|
import '../../../pages/counter/counter_page.dart';
|
||||||
|
import '../../../pages/user/user_page.dart';
|
||||||
|
import '../transition/fade_transition_page.dart';
|
||||||
|
import '../../../pages/color/color_add_page.dart';
|
||||||
|
import 'route_history.dart';
|
||||||
|
|
||||||
const List<String> kDestinationsPaths = [
|
const List<String> kDestinationsPaths = [
|
||||||
'/color',
|
'/color',
|
||||||
@@ -17,8 +26,65 @@ class AppRouterDelegate extends RouterDelegate<Object> with ChangeNotifier {
|
|||||||
String _path = '/color';
|
String _path = '/color';
|
||||||
|
|
||||||
String get path => _path;
|
String get path => _path;
|
||||||
|
|
||||||
AppRouterDelegate() {
|
AppRouterDelegate() {
|
||||||
// keepAlivePath.add('/color');
|
// keepAlivePath.add('/color');
|
||||||
|
_histories.add(RouteHistory(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<RouteHistory> _histories = [];
|
||||||
|
final List<RouteHistory> _backHistories = [];
|
||||||
|
|
||||||
|
List<RouteHistory> get histories => _histories.reversed.toList();
|
||||||
|
|
||||||
|
bool get hasHistory => _histories.length > 1;
|
||||||
|
|
||||||
|
bool get hasBackHistory => _backHistories.isNotEmpty;
|
||||||
|
|
||||||
|
/// 历史回退操作
|
||||||
|
/// 将当前顶层移除,并加入 _backHistories 撤销列表
|
||||||
|
/// 并转到前一路径
|
||||||
|
void back() {
|
||||||
|
if (!hasHistory) return;
|
||||||
|
RouteHistory top = _histories.removeLast();
|
||||||
|
_backHistories.add(top);
|
||||||
|
if (_histories.isNotEmpty) {
|
||||||
|
_path = _histories.last.path;
|
||||||
|
if (_histories.last.extra != null) {
|
||||||
|
_pathExtraMap[_path] = _histories.last.extra;
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void toHistory(RouteHistory history) {
|
||||||
|
_path = history.path;
|
||||||
|
if (history.extra != null) {
|
||||||
|
_pathExtraMap[_path] = history.extra;
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void closeHistory(int index) {
|
||||||
|
_histories.removeAt(index);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearHistory() {
|
||||||
|
_histories.clear();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 撤销回退操作
|
||||||
|
/// 取出回退列表的最后元素,跳转到该路径
|
||||||
|
void revocation() {
|
||||||
|
RouteHistory target = _backHistories.removeLast();
|
||||||
|
_path = target.path;
|
||||||
|
if (target.extra != null) {
|
||||||
|
_pathExtraMap[_path] = target.extra;
|
||||||
|
}
|
||||||
|
_histories.add(target);
|
||||||
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
int? get activeIndex {
|
int? get activeIndex {
|
||||||
@@ -37,48 +103,58 @@ class AppRouterDelegate extends RouterDelegate<Object> with ChangeNotifier {
|
|||||||
|
|
||||||
final List<String> keepAlivePath = [];
|
final List<String> keepAlivePath = [];
|
||||||
|
|
||||||
FutureOr<dynamic> changePath(String value,
|
void setPathKeepLive(String value) {
|
||||||
{bool forResult = false, Object? extra, bool keepAlive = false}) {
|
if (keepAlivePath.contains(value)) {
|
||||||
if (forResult) {
|
keepAlivePath.remove(value);
|
||||||
_completerMap[value] = Completer();
|
|
||||||
}
|
|
||||||
if (keepAlive) {
|
|
||||||
if (keepAlivePath.contains(value)) {
|
|
||||||
keepAlivePath.remove(value);
|
|
||||||
}
|
|
||||||
keepAlivePath.add(value);
|
|
||||||
}
|
|
||||||
if (extra != null) {
|
|
||||||
_pathExtraMap[value] = extra;
|
|
||||||
}
|
}
|
||||||
|
keepAlivePath.add(value);
|
||||||
path = value;
|
path = value;
|
||||||
if (forResult) {
|
}
|
||||||
return _completerMap[value]!.future;
|
|
||||||
}
|
void setPathForData(String value, dynamic data) {
|
||||||
|
_pathExtraMap[value] = data;
|
||||||
|
path = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<dynamic> changePathForResult(String value) async {
|
||||||
|
Completer<dynamic> completer = Completer();
|
||||||
|
_completerMap[value] = completer;
|
||||||
|
path = value;
|
||||||
|
return completer.future;
|
||||||
}
|
}
|
||||||
|
|
||||||
set path(String value) {
|
set path(String value) {
|
||||||
if (_path == value) return;
|
if (_path == value) return;
|
||||||
_path = value;
|
_path = value;
|
||||||
|
/// 将路由加入历史列表
|
||||||
|
_addPathToHistory(value,_pathExtraMap[path]);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _addPathToHistory(String value, Object? extra) {
|
||||||
|
if (_histories.isNotEmpty && value == _histories.last.path) return;
|
||||||
|
_histories.add(RouteHistory(
|
||||||
|
value,
|
||||||
|
extra: _pathExtraMap[path],
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Navigator(
|
return Navigator(
|
||||||
onPopPage: _onPopPage,
|
onPopPage: _onPopPage,
|
||||||
pages: _buildPages(context, path),
|
pages: _buildPages(path),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Page> _buildPages(BuildContext context,String path) {
|
List<Page> _buildPages(path) {
|
||||||
List<Page> pages = [];
|
List<Page> pages = [];
|
||||||
List<Page> topPages = _buildPageByPath(context,path);
|
List<Page> topPages = _buildPageByPath(path);
|
||||||
|
|
||||||
if (keepAlivePath.isNotEmpty) {
|
if (keepAlivePath.isNotEmpty) {
|
||||||
for (String alivePath in keepAlivePath) {
|
for (String alivePath in keepAlivePath) {
|
||||||
if (alivePath != path) {
|
if (alivePath != path) {
|
||||||
pages.addAll(_buildPageByPath(context,alivePath));
|
pages.addAll(_buildPageByPath(alivePath));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,36 +167,64 @@ class AppRouterDelegate extends RouterDelegate<Object> with ChangeNotifier {
|
|||||||
return pages;
|
return pages;
|
||||||
}
|
}
|
||||||
|
|
||||||
Page? _buildPageByPathFromTree(BuildContext context, String path) {
|
List<Page> _buildPageByPath(String path) {
|
||||||
// 1. 根据 path 在 iroute 树中查询节点
|
Widget? child;
|
||||||
IRoute? iroute = root.match(path);
|
if (path.startsWith('/color')) {
|
||||||
Object? extra = _pathExtraMap[path];
|
return buildColorPages(path);
|
||||||
bool keepAlive = keepAlivePath.contains(path);
|
|
||||||
bool forResult = _completerMap.containsKey(path);
|
|
||||||
Page? page;
|
|
||||||
if (iroute != null) {
|
|
||||||
page = iroute.builder?.call(
|
|
||||||
context,
|
|
||||||
IRouteData(
|
|
||||||
uri: Uri.parse(path),
|
|
||||||
extra: extra,
|
|
||||||
keepAlive: keepAlive,
|
|
||||||
forResult: forResult,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return page;
|
|
||||||
|
if (path == kDestinationsPaths[1]) {
|
||||||
|
child = const CounterPage();
|
||||||
|
}
|
||||||
|
if (path == kDestinationsPaths[2]) {
|
||||||
|
child = const UserPage();
|
||||||
|
}
|
||||||
|
if (path == kDestinationsPaths[3]) {
|
||||||
|
child = const SettingPage();
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
FadeTransitionPage(
|
||||||
|
key: ValueKey(path),
|
||||||
|
child: child ?? const EmptyPage(),
|
||||||
|
)
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Page> _buildPageByPath(BuildContext context,String path) {
|
List<Page> buildColorPages(String path) {
|
||||||
List<Page> result = [];
|
List<Page> result = [];
|
||||||
Uri uri = Uri.parse(path);
|
Uri uri = Uri.parse(path);
|
||||||
String dist = '';
|
|
||||||
for (String segment in uri.pathSegments) {
|
for (String segment in uri.pathSegments) {
|
||||||
dist += '/$segment';
|
if (segment == 'color') {
|
||||||
Page? page = _buildPageByPathFromTree(context,dist);
|
result.add(const FadeTransitionPage(
|
||||||
if(page!=null){
|
key: ValueKey('/color'),
|
||||||
result.add(page);
|
child: ColorPage(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if (segment == 'detail') {
|
||||||
|
final Map<String, String> queryParams = uri.queryParameters;
|
||||||
|
String? selectedColor = queryParams['color'];
|
||||||
|
if (selectedColor != null) {
|
||||||
|
Color color = Color(int.parse(selectedColor, radix: 16));
|
||||||
|
result.add(FadeTransitionPage(
|
||||||
|
key: const ValueKey('/color/detail'),
|
||||||
|
child: ColorDetailPage(color: color),
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
Color? selectedColor = _pathExtraMap[path];
|
||||||
|
if (selectedColor != null) {
|
||||||
|
result.add(FadeTransitionPage(
|
||||||
|
key: const ValueKey('/color/detail'),
|
||||||
|
child: ColorDetailPage(color: selectedColor),
|
||||||
|
));
|
||||||
|
_pathExtraMap.remove(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (segment == 'add') {
|
||||||
|
result.add(const FadeTransitionPage(
|
||||||
|
key: ValueKey('/color/add'),
|
||||||
|
child: ColorAddPage(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -1,131 +1,37 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
|
|
||||||
import '../../../pages/color/color_add_page.dart';
|
|
||||||
import '../../../pages/color/color_detail_page.dart';
|
|
||||||
import '../../../pages/color/color_page.dart';
|
|
||||||
import '../../../pages/counter/counter_page.dart';
|
|
||||||
import '../../../pages/user/user_page.dart';
|
|
||||||
import '../../../pages/settings/settings_page.dart';
|
|
||||||
import '../transition/fade_transition_page.dart';
|
|
||||||
|
|
||||||
class IRoute {
|
class IRoute {
|
||||||
final String path;
|
final String path;
|
||||||
final IRoutePageBuilder? builder;
|
|
||||||
final List<IRoute> children;
|
final List<IRoute> children;
|
||||||
|
|
||||||
const IRoute({
|
const IRoute({required this.path, this.children = const []});
|
||||||
required this.path,
|
|
||||||
this.children = const [],
|
|
||||||
this.builder,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'IRoute{path: $path, children: $children}';
|
return 'IRoute{path: $path, children: $children}';
|
||||||
}
|
}
|
||||||
|
|
||||||
IRoute? match(String path) {
|
List<String> list(){
|
||||||
return matchRoute(this, path);
|
|
||||||
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
IRoute? matchRoute(IRoute route, String path) {
|
|
||||||
if (route.path == path) {
|
|
||||||
return route;
|
|
||||||
} else {
|
|
||||||
if (route.children.isNotEmpty) {
|
|
||||||
for (int i = 0; i < route.children.length; i++) {
|
|
||||||
IRoute current = route.children[i];
|
|
||||||
IRoute? target = matchRoute(current, path);
|
|
||||||
if (target != null) {
|
|
||||||
return target;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
typedef IRoutePageBuilder = Page? Function(
|
|
||||||
BuildContext context, IRouteData data);
|
|
||||||
|
|
||||||
class IRouteData {
|
const List<IRoute> kDestinationsIRoutes = [
|
||||||
final Object? extra;
|
|
||||||
final bool forResult;
|
|
||||||
final Uri uri;
|
|
||||||
final bool keepAlive;
|
|
||||||
|
|
||||||
IRouteData({
|
|
||||||
this.extra,
|
|
||||||
required this.uri,
|
|
||||||
this.forResult = false,
|
|
||||||
this.keepAlive = false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
IRoute root = IRoute(path: '/', children: kDestinationsIRoutes);
|
|
||||||
|
|
||||||
List<IRoute> kDestinationsIRoutes = [
|
|
||||||
IRoute(
|
IRoute(
|
||||||
path: '/color',
|
path: '/color',
|
||||||
builder: (ctx, data) {
|
|
||||||
return const FadeTransitionPage(
|
|
||||||
key: ValueKey('/color'),
|
|
||||||
child: ColorPage(),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
children: [
|
children: [
|
||||||
IRoute(
|
IRoute(path: '/color/add'),
|
||||||
path: '/color/detail',
|
IRoute(path: '/color/detail'),
|
||||||
builder: (ctx, data) {
|
|
||||||
final Map<String, String> queryParams = data.uri.queryParameters;
|
|
||||||
String? selectedColor = queryParams['color'];
|
|
||||||
Color color = Colors.black;
|
|
||||||
if (selectedColor != null) {
|
|
||||||
color = Color(int.parse(selectedColor, radix: 16));
|
|
||||||
} else if (data.extra is Color) {
|
|
||||||
color = data.extra as Color;
|
|
||||||
}
|
|
||||||
return FadeTransitionPage(
|
|
||||||
key: const ValueKey('/color/detail'),
|
|
||||||
child: ColorDetailPage(color: color),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
IRoute(
|
|
||||||
path: '/color/add',
|
|
||||||
builder: (ctx, data) {
|
|
||||||
return const FadeTransitionPage(
|
|
||||||
key: ValueKey('/color/add'),
|
|
||||||
child: ColorAddPage(),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
IRoute(
|
IRoute(
|
||||||
path: '/counter',
|
path: '/counter',
|
||||||
builder: (ctx, data) {
|
),
|
||||||
return const FadeTransitionPage(
|
|
||||||
key: ValueKey('/counter'),
|
|
||||||
child: CounterPage(),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
IRoute(
|
IRoute(
|
||||||
path: '/user',
|
path: '/user',
|
||||||
builder: (ctx, data) {
|
),
|
||||||
return const FadeTransitionPage(
|
|
||||||
key: ValueKey('/user'),
|
|
||||||
child: UserPage(),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
IRoute(
|
IRoute(
|
||||||
path: '/settings',
|
path: '/settings',
|
||||||
builder: (ctx, data) {
|
),
|
||||||
return const FadeTransitionPage(
|
|
||||||
key: ValueKey('/settings'),
|
|
||||||
child: SettingPage(),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
];
|
];
|
||||||
|
|||||||
6
lib/v6/app/navigation/router/route_history.dart
Normal file
6
lib/v6/app/navigation/router/route_history.dart
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
class RouteHistory{
|
||||||
|
final String path;
|
||||||
|
final Object? extra;
|
||||||
|
|
||||||
|
RouteHistory(this.path, { this.extra});
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import '../router/app_router_delegate.dart';
|
import '../router/app_router_delegate.dart';
|
||||||
import 'app_navigation_rail.dart';
|
import 'app_navigation_rail.dart';
|
||||||
import 'app_top_bar.dart';
|
import 'app_top_bar/app_top_bar.dart';
|
||||||
|
|
||||||
class AppNavigation extends StatelessWidget {
|
class AppNavigation extends StatelessWidget {
|
||||||
const AppNavigation({super.key});
|
const AppNavigation({super.key});
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ class _AppNavigationRailState extends State<AppNavigationRail> {
|
|||||||
),
|
),
|
||||||
tail: Padding(
|
tail: Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 6.0),
|
padding: const EdgeInsets.only(bottom: 6.0),
|
||||||
child: Text('V0.0.5',style: TextStyle(color: Colors.white,fontSize: 12),),
|
child: Text('V0.0.6',style: TextStyle(color: Colors.white,fontSize: 12),),
|
||||||
),
|
),
|
||||||
backgroundColor: const Color(0xff3975c6),
|
backgroundColor: const Color(0xff3975c6),
|
||||||
onDestinationSelected: _onDestinationSelected,
|
onDestinationSelected: _onDestinationSelected,
|
||||||
@@ -53,9 +53,9 @@ class _AppNavigationRailState extends State<AppNavigationRail> {
|
|||||||
|
|
||||||
void _onDestinationSelected(int index) {
|
void _onDestinationSelected(int index) {
|
||||||
if(index==1){
|
if(index==1){
|
||||||
router.changePath(kDestinationsPaths[index],keepAlive: true);
|
router.setPathKeepLive(kDestinationsPaths[index]);
|
||||||
}else{
|
}else{
|
||||||
router.path = kDestinationsPaths[index];
|
router.path=kDestinationsPaths[index];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:iroute/components/toly_ui/button/hover_icon_button.dart';
|
||||||
|
import '../../router/app_router_delegate.dart';
|
||||||
|
|
||||||
|
class AppRouterEditor extends StatefulWidget {
|
||||||
|
final ValueChanged<String>? onSubmit;
|
||||||
|
const AppRouterEditor({super.key, this.onSubmit});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AppRouterEditor> createState() => _AppRouterEditorState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AppRouterEditorState extends State<AppRouterEditor> {
|
||||||
|
|
||||||
|
final TextEditingController _controller = TextEditingController();
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_onRouteChange();
|
||||||
|
router.addListener(_onRouteChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onRouteChange() {
|
||||||
|
_controller.text=router.path;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
router.removeListener(_onRouteChange);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Stack(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
child: CupertinoTextField(
|
||||||
|
controller: _controller,
|
||||||
|
style: TextStyle(fontSize: 14),
|
||||||
|
padding: EdgeInsets.only(left:12,top: 6,bottom: 6,right: 32),
|
||||||
|
placeholder: '输入路由地址导航',
|
||||||
|
onSubmitted: widget.onSubmit,
|
||||||
|
decoration: BoxDecoration(color: Color(0xffF1F2F3),borderRadius: BorderRadius.circular(6)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 8.0),
|
||||||
|
child: HoverIconButton(
|
||||||
|
icon: Icons.directions_outlined,
|
||||||
|
defaultColor: Color(0xff68696B),
|
||||||
|
onPressed:()=>widget.onSubmit?.call(_controller.text),
|
||||||
|
size: 20
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
110
lib/v6/app/navigation/views/app_top_bar/app_top_bar.dart
Normal file
110
lib/v6/app/navigation/views/app_top_bar/app_top_bar.dart
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:iroute/components/components.dart';
|
||||||
|
import '../../router/app_router_delegate.dart';
|
||||||
|
import 'app_router_editor.dart';
|
||||||
|
import 'history_view_icon.dart';
|
||||||
|
import 'route_history_button.dart';
|
||||||
|
|
||||||
|
class AppTopBar extends StatelessWidget {
|
||||||
|
const AppTopBar({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return DragToMoveWrap(
|
||||||
|
child: Container(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
height: 46,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
const RouterIndicator(),
|
||||||
|
Expanded(
|
||||||
|
child: Row(children: [
|
||||||
|
const Spacer(),
|
||||||
|
RouteHistoryButton(),
|
||||||
|
const SizedBox(width: 12,),
|
||||||
|
SizedBox(
|
||||||
|
width: 250,
|
||||||
|
child: AppRouterEditor(
|
||||||
|
onSubmit: (path) => router.path = path,
|
||||||
|
)),
|
||||||
|
const SizedBox(width: 12,),
|
||||||
|
HistoryViewIcon(),
|
||||||
|
const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 12.0),
|
||||||
|
child: VerticalDivider(
|
||||||
|
width: 32,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
])),
|
||||||
|
const WindowButtons()
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RouterIndicator extends StatefulWidget {
|
||||||
|
const RouterIndicator({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<RouterIndicator> createState() => _RouterIndicatorState();
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, String> kRouteLabelMap = {
|
||||||
|
'/color': '颜色板',
|
||||||
|
'/color/add': '添加颜色',
|
||||||
|
'/color/detail': '颜色详情',
|
||||||
|
'/counter': '计数器',
|
||||||
|
'/user': '我的',
|
||||||
|
'/settings': '系统设置',
|
||||||
|
};
|
||||||
|
|
||||||
|
class _RouterIndicatorState extends State<RouterIndicator> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
router.addListener(_onRouterChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
router.removeListener(_onRouterChange);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return TolyBreadcrumb(
|
||||||
|
items: pathToBreadcrumbItems(router.path),
|
||||||
|
onTapItem: (item) {
|
||||||
|
if (item.to != null) {
|
||||||
|
router.path = item.to!;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onRouterChange() {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
|
List<BreadcrumbItem> pathToBreadcrumbItems(String path) {
|
||||||
|
Uri uri = Uri.parse(path);
|
||||||
|
List<BreadcrumbItem> result = [];
|
||||||
|
String to = '';
|
||||||
|
|
||||||
|
String distPath = '';
|
||||||
|
for (String segment in uri.pathSegments) {
|
||||||
|
distPath += '/$segment';
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String segment in uri.pathSegments) {
|
||||||
|
to += '/$segment';
|
||||||
|
String label = kRouteLabelMap[to] ?? '未知路由';
|
||||||
|
result.add(BreadcrumbItem(to: to, label: label, active: to == distPath));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
156
lib/v6/app/navigation/views/app_top_bar/history_view_icon.dart
Normal file
156
lib/v6/app/navigation/views/app_top_bar/history_view_icon.dart
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:iroute/components/components.dart';
|
||||||
|
import '../../router/app_router_delegate.dart';
|
||||||
|
import '../../router/route_history.dart';
|
||||||
|
import 'app_top_bar.dart';
|
||||||
|
|
||||||
|
class HistoryViewIcon extends StatelessWidget{
|
||||||
|
const HistoryViewIcon({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
|
||||||
|
return MouseRegion(
|
||||||
|
cursor: SystemMouseCursors.click,
|
||||||
|
child: PopPanel(
|
||||||
|
offset: const Offset(0, 10),
|
||||||
|
panel: SizedBox(
|
||||||
|
height: 350,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
_buildTopBar(),
|
||||||
|
const Expanded(
|
||||||
|
child:HistoryPanel(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: const Icon(
|
||||||
|
Icons.history,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTopBar() {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xffFAFAFC),
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
),
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.only(top: 10, left: 12, right: 12, bottom: 8),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'浏览历史',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
TextButton(onPressed: router.clearHistory, child: const Text('清空历史'))
|
||||||
|
],
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HistoryItem extends StatefulWidget {
|
||||||
|
final RouteHistory history;
|
||||||
|
final VoidCallback onPressed;
|
||||||
|
final VoidCallback onDelete;
|
||||||
|
|
||||||
|
const HistoryItem({super.key, required this.history, required this.onPressed, required this.onDelete});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<HistoryItem> createState() => _HistoryItemState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HistoryItemState extends State<HistoryItem> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MouseRegion(
|
||||||
|
cursor: SystemMouseCursors.click,
|
||||||
|
child: GestureDetector(
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
onTap: widget.onPressed,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(widget.history.path),
|
||||||
|
const SizedBox(
|
||||||
|
height: 2,
|
||||||
|
),
|
||||||
|
Text(kRouteLabelMap[widget.history.path]!),
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: widget.onDelete,
|
||||||
|
child: const Icon(
|
||||||
|
Icons.close,
|
||||||
|
size: 18,
|
||||||
|
color: Color(0xff8E92A9),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class HistoryPanel extends StatefulWidget {
|
||||||
|
const HistoryPanel({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<HistoryPanel> createState() => _HistoryPanelState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HistoryPanelState extends State<HistoryPanel> {
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
router.addListener(_onChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
router.removeListener(_onChange);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if(router.histories.isEmpty){
|
||||||
|
return const Center(
|
||||||
|
child: Text(
|
||||||
|
'暂无浏览历史记录',
|
||||||
|
style: TextStyle(fontSize: 14, color: Colors.grey),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return ListView.builder(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||||
|
itemExtent: 46,
|
||||||
|
itemCount: router.histories.length,
|
||||||
|
itemBuilder: (_, index) =>
|
||||||
|
HistoryItem(
|
||||||
|
onDelete: (){
|
||||||
|
int fixIndex = router.histories.length - 1 - index;
|
||||||
|
router.closeHistory(fixIndex);
|
||||||
|
},
|
||||||
|
onPressed: (){
|
||||||
|
router.toHistory(router.histories[index]);
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
},
|
||||||
|
history: router.histories[index]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onChange() {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:iroute/components/toly_ui/button/hover_icon_button.dart';
|
||||||
|
import '../../router/app_router_delegate.dart';
|
||||||
|
|
||||||
|
class RouteHistoryButton extends StatefulWidget {
|
||||||
|
const RouteHistoryButton({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<RouteHistoryButton> createState() => _RouteHistoryButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _RouteHistoryButtonState extends State<RouteHistoryButton> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
router.addListener(_onChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
router.removeListener(_onChange);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
bool hasHistory = router.hasHistory;
|
||||||
|
bool hasBackHistory = router.hasBackHistory;
|
||||||
|
Color activeColor = const Color(0xff9195AC);
|
||||||
|
Color inActiveColor = const Color(0xffC7CAD5);
|
||||||
|
Color historyColor = hasHistory?activeColor:inActiveColor;
|
||||||
|
Color backHistoryColor = hasBackHistory?activeColor:inActiveColor;
|
||||||
|
return Wrap(
|
||||||
|
children: [
|
||||||
|
HoverIconButton(
|
||||||
|
size: 20,
|
||||||
|
hoverColor: historyColor,
|
||||||
|
defaultColor: historyColor,
|
||||||
|
icon: CupertinoIcons.arrow_left_circle,
|
||||||
|
onPressed: hasHistory?router.back:null,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8,),
|
||||||
|
HoverIconButton(
|
||||||
|
size: 20,
|
||||||
|
hoverColor: backHistoryColor,
|
||||||
|
defaultColor: backHistoryColor,
|
||||||
|
icon: CupertinoIcons.arrow_right_circle,
|
||||||
|
onPressed: hasBackHistory?router.revocation:null,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onChange() {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -38,11 +38,12 @@ class _ColorPageState extends State<ColorPage> {
|
|||||||
void _selectColor(Color color){
|
void _selectColor(Color color){
|
||||||
// String value = color.value.toRadixString(16);
|
// String value = color.value.toRadixString(16);
|
||||||
// router.path = '/color/detail?color=$value';
|
// router.path = '/color/detail?color=$value';
|
||||||
router.changePath('/color/detail',extra: color);
|
router.setPathForData('/color/detail',color);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _toAddPage() async {
|
void _toAddPage() async {
|
||||||
Color? color = await router.changePath('/color/add',forResult: true);
|
Color? color = await router.changePathForResult('/color/add');
|
||||||
if (color != null) {
|
if (color != null) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_colors.add(color);
|
_colors.add(color);
|
||||||
|
|||||||
1
lib/v6_result_/app.dart
Normal file
1
lib/v6_result_/app.dart
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export 'app/unit_app.dart';
|
||||||
200
lib/v6_result_/app/navigation/router/app_router_delegate.dart
Normal file
200
lib/v6_result_/app/navigation/router/app_router_delegate.dart
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'iroute.dart';
|
||||||
|
import 'route_history.dart';
|
||||||
|
|
||||||
|
const List<String> kDestinationsPaths = [
|
||||||
|
'/color',
|
||||||
|
'/counter',
|
||||||
|
'/user',
|
||||||
|
'/settings',
|
||||||
|
];
|
||||||
|
|
||||||
|
AppRouterDelegate router = AppRouterDelegate();
|
||||||
|
|
||||||
|
class AppRouterDelegate extends RouterDelegate<Object> with ChangeNotifier {
|
||||||
|
String _path = '/color';
|
||||||
|
|
||||||
|
String get path => _path;
|
||||||
|
|
||||||
|
AppRouterDelegate() {
|
||||||
|
// keepAlivePath.add('/color');
|
||||||
|
}
|
||||||
|
|
||||||
|
int? get activeIndex {
|
||||||
|
if (path.startsWith('/color')) return 0;
|
||||||
|
if (path.startsWith('/counter')) return 1;
|
||||||
|
if (path.startsWith('/user')) return 2;
|
||||||
|
if (path.startsWith('/settings')) return 3;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<RouteHistory> _histories = [];
|
||||||
|
final List<RouteHistory> _backHistories = [];
|
||||||
|
|
||||||
|
bool get hasHistory => _histories.length > 1;
|
||||||
|
|
||||||
|
bool get hasBackHistory => _backHistories.isNotEmpty;
|
||||||
|
|
||||||
|
final Map<String, Completer<dynamic>> _completerMap = {};
|
||||||
|
|
||||||
|
Completer<dynamic>? completer;
|
||||||
|
|
||||||
|
final Map<String, dynamic> _pathExtraMap = {};
|
||||||
|
|
||||||
|
final List<String> keepAlivePath = [];
|
||||||
|
|
||||||
|
/// 历史回退操作
|
||||||
|
/// 将当前顶层移除,并加入 _backHistories 撤销列表
|
||||||
|
/// 并转到前一路径
|
||||||
|
void back() {
|
||||||
|
if (!hasHistory) return;
|
||||||
|
RouteHistory top = _histories.removeLast();
|
||||||
|
_backHistories.add(top);
|
||||||
|
if (_histories.isNotEmpty) {
|
||||||
|
_path = _histories.last.path;
|
||||||
|
if(_histories.last.extra!=null){
|
||||||
|
_pathExtraMap[_path] = _histories.last.extra;
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 撤销回退操作
|
||||||
|
/// 取出回退列表的最后元素,跳转到该路径
|
||||||
|
void revocation() {
|
||||||
|
RouteHistory target = _backHistories.removeLast();
|
||||||
|
_path = target.path;
|
||||||
|
if(target.extra!=null){
|
||||||
|
_pathExtraMap[_path] = target.extra;
|
||||||
|
}
|
||||||
|
_histories.add(target);
|
||||||
|
notifyListeners();
|
||||||
|
// changePath(
|
||||||
|
// target.path,
|
||||||
|
// extra: target.extra,
|
||||||
|
// );
|
||||||
|
}
|
||||||
|
|
||||||
|
FutureOr<dynamic> changePath(
|
||||||
|
String value, {
|
||||||
|
bool forResult = false,
|
||||||
|
Object? extra,
|
||||||
|
bool keepAlive = false,
|
||||||
|
bool recordHistory = false,
|
||||||
|
}) {
|
||||||
|
if (forResult) {
|
||||||
|
_completerMap[value] = Completer();
|
||||||
|
}
|
||||||
|
if (keepAlive) {
|
||||||
|
if (keepAlivePath.contains(value)) {
|
||||||
|
keepAlivePath.remove(value);
|
||||||
|
}
|
||||||
|
keepAlivePath.add(value);
|
||||||
|
}
|
||||||
|
if (extra != null) {
|
||||||
|
_pathExtraMap[value] = extra;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recordHistory) {
|
||||||
|
_histories.add(RouteHistory(
|
||||||
|
value,
|
||||||
|
extra: extra,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
path = value;
|
||||||
|
if (forResult) {
|
||||||
|
return _completerMap[value]!.future;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set path(String value) {
|
||||||
|
if (_path == value) return;
|
||||||
|
_path = value;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Navigator(
|
||||||
|
onPopPage: _onPopPage,
|
||||||
|
pages: _buildPages(context, path),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Page> _buildPages(BuildContext context, String path) {
|
||||||
|
List<Page> pages = [];
|
||||||
|
List<Page> topPages = _buildPageByPathFromTree(context, path);
|
||||||
|
|
||||||
|
if (keepAlivePath.isNotEmpty) {
|
||||||
|
for (String alivePath in keepAlivePath) {
|
||||||
|
if (alivePath != path) {
|
||||||
|
pages.addAll(_buildPageByPathFromTree(context, alivePath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 去除和 topPages 中重复的界面
|
||||||
|
pages.removeWhere(
|
||||||
|
(element) => topPages.map((e) => e.key).contains(element.key));
|
||||||
|
}
|
||||||
|
|
||||||
|
pages.addAll(topPages);
|
||||||
|
return pages;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Page> _buildPageByPathFromTree(BuildContext context, String path) {
|
||||||
|
List<Page> result = [];
|
||||||
|
List<IRoute> iRoutes = root.find(path);
|
||||||
|
if (iRoutes.isNotEmpty) {
|
||||||
|
for (int i = 0; i < iRoutes.length; i++) {
|
||||||
|
IRoute iroute = iRoutes[i];
|
||||||
|
String path = iroute.path;
|
||||||
|
Object? extra = _pathExtraMap[path];
|
||||||
|
bool keepAlive = keepAlivePath.contains(path);
|
||||||
|
bool forResult = _completerMap.containsKey(path);
|
||||||
|
Page? page = iroute.builder?.call(
|
||||||
|
context,
|
||||||
|
IRouteData(
|
||||||
|
uri: Uri.parse(path),
|
||||||
|
extra: extra,
|
||||||
|
keepAlive: keepAlive,
|
||||||
|
forResult: forResult,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (page != null) {
|
||||||
|
result.add(page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> popRoute() async {
|
||||||
|
print('=======popRoute=========');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _onPopPage(Route route, result) {
|
||||||
|
if (_completerMap.containsKey(path)) {
|
||||||
|
_completerMap[path]?.complete(result);
|
||||||
|
_completerMap.remove(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
path = backPath(path);
|
||||||
|
return route.didPop(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
String backPath(String path) {
|
||||||
|
Uri uri = Uri.parse(path);
|
||||||
|
if (uri.pathSegments.length == 1) return path;
|
||||||
|
List<String> parts = List.of(uri.pathSegments)..removeLast();
|
||||||
|
return '/${parts.join('/')}';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> setNewRoutePath(configuration) async {}
|
||||||
|
}
|
||||||
185
lib/v6_result_/app/navigation/router/iroute.dart
Normal file
185
lib/v6_result_/app/navigation/router/iroute.dart
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../../../pages/color/color_add_page.dart';
|
||||||
|
import '../../../pages/color/color_detail_page.dart';
|
||||||
|
import '../../../pages/color/color_page.dart';
|
||||||
|
import '../../../pages/counter/counter_page.dart';
|
||||||
|
import '../../../pages/user/user_page.dart';
|
||||||
|
import '../../../pages/settings/settings_page.dart';
|
||||||
|
import '../transition/fade_transition_page.dart';
|
||||||
|
|
||||||
|
class IRoute {
|
||||||
|
final String path;
|
||||||
|
final IRoutePageBuilder? builder;
|
||||||
|
final List<IRoute> children;
|
||||||
|
|
||||||
|
const IRoute({
|
||||||
|
required this.path,
|
||||||
|
this.children = const [],
|
||||||
|
this.builder,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'IRoute{path: $path, children: $children}';
|
||||||
|
}
|
||||||
|
|
||||||
|
IRoute? match(String path) {
|
||||||
|
return matchRoute(this, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<IRoute> find(String input){
|
||||||
|
String fixInput = input.substring(1);
|
||||||
|
List<IRoute> nodes = findNodes(this,fixInput,0,'/',[]);
|
||||||
|
if(nodes.isNotEmpty&&nodes.last.path!=input){
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<IRoute> findNodes(IRoute node,String input,int deep,String prefix,List<IRoute> result){
|
||||||
|
String separator = '/';
|
||||||
|
|
||||||
|
List<String> parts = input.split(separator);
|
||||||
|
if(deep>parts.length-1){
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
String target = parts[deep];
|
||||||
|
if(node.children.isNotEmpty){
|
||||||
|
List<IRoute> nodes = node.children.where((e) => e.path==prefix+target).toList();
|
||||||
|
bool match = nodes.isNotEmpty;
|
||||||
|
if(match){
|
||||||
|
IRoute matched = nodes.first;
|
||||||
|
result.add(matched);
|
||||||
|
String nextPrefix = '${matched.path}$separator';
|
||||||
|
findNodes(matched, input, ++deep,nextPrefix,result);
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// List<IRoute> findNodes(IRoute node,String input,int deep,String prefix,List<IRoute> result){
|
||||||
|
// String separator = '/';
|
||||||
|
// List<String> parts = input.split(separator);
|
||||||
|
// if(deep>parts.length-1){
|
||||||
|
// return result;
|
||||||
|
// }
|
||||||
|
// String target = parts[deep];
|
||||||
|
// if(node.children.isNotEmpty){
|
||||||
|
// List<IRoute> nodes = node.children.where((e) => e.path==prefix+target).toList();
|
||||||
|
// bool match = nodes.isNotEmpty;
|
||||||
|
// if(match){
|
||||||
|
// IRoute matched = nodes.first;
|
||||||
|
// result.add(matched);
|
||||||
|
// String nextPrefix = '${matched.path}$separator';
|
||||||
|
// findNodes(matched, input, ++deep,nextPrefix,result);
|
||||||
|
// }
|
||||||
|
// }else{
|
||||||
|
// return result;
|
||||||
|
// }
|
||||||
|
// return result;
|
||||||
|
// }
|
||||||
|
|
||||||
|
IRoute? matchRoute(IRoute route, String path) {
|
||||||
|
if (route.path == path) {
|
||||||
|
return route;
|
||||||
|
} else {
|
||||||
|
if (route.children.isNotEmpty) {
|
||||||
|
for (int i = 0; i < route.children.length; i++) {
|
||||||
|
IRoute current = route.children[i];
|
||||||
|
IRoute? target = matchRoute(current, path);
|
||||||
|
if (target != null) {
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef IRoutePageBuilder = Page? Function(
|
||||||
|
BuildContext context, IRouteData data);
|
||||||
|
|
||||||
|
class IRouteData {
|
||||||
|
final Object? extra;
|
||||||
|
final bool forResult;
|
||||||
|
final Uri uri;
|
||||||
|
final bool keepAlive;
|
||||||
|
|
||||||
|
IRouteData({
|
||||||
|
this.extra,
|
||||||
|
required this.uri,
|
||||||
|
this.forResult = false,
|
||||||
|
this.keepAlive = false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
IRoute root = IRoute(path: 'root', children: kDestinationsIRoutes);
|
||||||
|
|
||||||
|
List<IRoute> kDestinationsIRoutes = [
|
||||||
|
IRoute(
|
||||||
|
path: '/color',
|
||||||
|
builder: (ctx, data) {
|
||||||
|
return const FadeTransitionPage(
|
||||||
|
key: ValueKey('/color'),
|
||||||
|
child: ColorPage(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
IRoute(
|
||||||
|
path: '/color/detail',
|
||||||
|
builder: (ctx, data) {
|
||||||
|
final Map<String, String> queryParams = data.uri.queryParameters;
|
||||||
|
String? selectedColor = queryParams['color'];
|
||||||
|
Color color = Colors.black;
|
||||||
|
if (selectedColor != null) {
|
||||||
|
color = Color(int.parse(selectedColor, radix: 16));
|
||||||
|
} else if (data.extra is Color) {
|
||||||
|
color = data.extra as Color;
|
||||||
|
}
|
||||||
|
return FadeTransitionPage(
|
||||||
|
key: const ValueKey('/color/detail'),
|
||||||
|
child: ColorDetailPage(color: color),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
IRoute(
|
||||||
|
path: '/color/add',
|
||||||
|
builder: (ctx, data) {
|
||||||
|
return const FadeTransitionPage(
|
||||||
|
key: ValueKey('/color/add'),
|
||||||
|
child: ColorAddPage(),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
IRoute(
|
||||||
|
path: '/counter',
|
||||||
|
builder: (ctx, data) {
|
||||||
|
return const FadeTransitionPage(
|
||||||
|
key: ValueKey('/counter'),
|
||||||
|
child: CounterPage(),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
IRoute(
|
||||||
|
path: '/user',
|
||||||
|
builder: (ctx, data) {
|
||||||
|
return const FadeTransitionPage(
|
||||||
|
key: ValueKey('/user'),
|
||||||
|
child: UserPage(),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
IRoute(
|
||||||
|
path: '/settings',
|
||||||
|
builder: (ctx, data) {
|
||||||
|
return const FadeTransitionPage(
|
||||||
|
key: ValueKey('/settings'),
|
||||||
|
child: SettingPage(),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
];
|
||||||
6
lib/v6_result_/app/navigation/router/route_history.dart
Normal file
6
lib/v6_result_/app/navigation/router/route_history.dart
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
class RouteHistory{
|
||||||
|
final String path;
|
||||||
|
final Object? extra;
|
||||||
|
|
||||||
|
RouteHistory(this.path, { this.extra});
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
// Copyright 2021, the Flutter project authors. Please see the AUTHORS file
|
||||||
|
// for details. All rights reserved. Use of this source code is governed by a
|
||||||
|
// BSD-style license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class FadeTransitionPage<T> extends Page<T> {
|
||||||
|
final Widget child;
|
||||||
|
final Duration duration;
|
||||||
|
|
||||||
|
const FadeTransitionPage({
|
||||||
|
super.key,
|
||||||
|
required this.child,
|
||||||
|
this.duration = const Duration(milliseconds: 300),
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Route<T> createRoute(BuildContext context) =>
|
||||||
|
PageBasedFadeTransitionRoute<T>(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
class PageBasedFadeTransitionRoute<T> extends PageRoute<T> {
|
||||||
|
final FadeTransitionPage<T> _page;
|
||||||
|
|
||||||
|
PageBasedFadeTransitionRoute(this._page) : super(settings: _page);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Color? get barrierColor => null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get barrierLabel => null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Duration get transitionDuration => _page.duration;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get maintainState => true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildPage(BuildContext context, Animation<double> animation,
|
||||||
|
Animation<double> secondaryAnimation) {
|
||||||
|
var curveTween = CurveTween(curve: Curves.easeIn);
|
||||||
|
return FadeTransition(
|
||||||
|
opacity: animation.drive(curveTween),
|
||||||
|
child: (settings as FadeTransitionPage).child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildTransitions(BuildContext context, Animation<double> animation,
|
||||||
|
Animation<double> secondaryAnimation, Widget child) =>
|
||||||
|
child;
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
// Copyright 2021, the Flutter project authors. Please see the AUTHORS file
|
||||||
|
// for details. All rights reserved. Use of this source code is governed by a
|
||||||
|
// BSD-style license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class NoTransitionPage<T> extends Page<T> {
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
const NoTransitionPage({
|
||||||
|
super.key,
|
||||||
|
required this.child,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Route<T> createRoute(BuildContext context) => NoTransitionRoute<T>(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
class NoTransitionRoute<T> extends PageRoute<T> {
|
||||||
|
|
||||||
|
final NoTransitionPage<T> _page;
|
||||||
|
|
||||||
|
NoTransitionRoute(this._page) : super(settings: _page);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Color? get barrierColor => null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get barrierLabel => null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Duration get transitionDuration => const Duration(milliseconds: 0);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get maintainState => true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildPage(BuildContext context, Animation<double> animation,
|
||||||
|
Animation<double> secondaryAnimation) {
|
||||||
|
return (settings as NoTransitionPage).child;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildTransitions(BuildContext context, Animation<double> animation,
|
||||||
|
Animation<double> secondaryAnimation, Widget child) =>
|
||||||
|
child;
|
||||||
|
}
|
||||||
34
lib/v6_result_/app/navigation/views/app_navigation.dart
Normal file
34
lib/v6_result_/app/navigation/views/app_navigation.dart
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../router/app_router_delegate.dart';
|
||||||
|
import 'app_navigation_rail.dart';
|
||||||
|
import 'app_top_bar.dart';
|
||||||
|
|
||||||
|
class AppNavigation extends StatelessWidget {
|
||||||
|
const AppNavigation({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
double px1 = 1/View.of(context).devicePixelRatio;
|
||||||
|
return Scaffold(
|
||||||
|
body: Row(
|
||||||
|
children: [
|
||||||
|
const AppNavigationRail(),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
const AppTopBar(),
|
||||||
|
Divider(height: px1,),
|
||||||
|
Expanded(
|
||||||
|
child: Router(
|
||||||
|
routerDelegate: router,
|
||||||
|
backButtonDispatcher: RootBackButtonDispatcher(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
65
lib/v6_result_/app/navigation/views/app_navigation_rail.dart
Normal file
65
lib/v6_result_/app/navigation/views/app_navigation_rail.dart
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:iroute/components/components.dart';
|
||||||
|
import '../router/app_router_delegate.dart';
|
||||||
|
|
||||||
|
class AppNavigationRail extends StatefulWidget {
|
||||||
|
const AppNavigationRail({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AppNavigationRail> createState() => _AppNavigationRailState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AppNavigationRailState extends State<AppNavigationRail> {
|
||||||
|
|
||||||
|
final List<MenuMeta> deskNavBarMenus = const [
|
||||||
|
MenuMeta(label: '颜色板', icon: Icons.color_lens_outlined),
|
||||||
|
MenuMeta(label: '计数器', icon: Icons.add_chart),
|
||||||
|
MenuMeta(label: '我的', icon: Icons.person),
|
||||||
|
MenuMeta(label: '设置', icon: Icons.settings),
|
||||||
|
];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
router.addListener(_onRouterChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
router.removeListener(_onRouterChange);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return DragToMoveWrap(
|
||||||
|
child: TolyNavigationRail(
|
||||||
|
items: deskNavBarMenus,
|
||||||
|
leading: const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 18.0),
|
||||||
|
child: FlutterLogo(),
|
||||||
|
),
|
||||||
|
tail: Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 6.0),
|
||||||
|
child: Text('V0.0.5',style: TextStyle(color: Colors.white,fontSize: 12),),
|
||||||
|
),
|
||||||
|
backgroundColor: const Color(0xff3975c6),
|
||||||
|
onDestinationSelected: _onDestinationSelected,
|
||||||
|
selectedIndex: router.activeIndex,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onDestinationSelected(int index) {
|
||||||
|
if(index==1){
|
||||||
|
router.changePath(kDestinationsPaths[index],keepAlive: true);
|
||||||
|
}else{
|
||||||
|
router.path = kDestinationsPaths[index];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onRouterChange() {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
}
|
||||||
30
lib/v6_result_/app/unit_app.dart
Normal file
30
lib/v6_result_/app/unit_app.dart
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'navigation/router/app_router_delegate.dart';
|
||||||
|
import 'navigation/views/app_navigation.dart';
|
||||||
|
import 'navigation/views/app_navigation_rail.dart';
|
||||||
|
|
||||||
|
class UnitApp extends StatelessWidget {
|
||||||
|
const UnitApp({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
|
||||||
|
return MaterialApp(
|
||||||
|
theme: ThemeData(
|
||||||
|
fontFamily: "宋体",
|
||||||
|
scaffoldBackgroundColor: Colors.white,
|
||||||
|
appBarTheme: const AppBarTheme(
|
||||||
|
elevation: 0,
|
||||||
|
iconTheme: IconThemeData(color: Colors.black),
|
||||||
|
titleTextStyle: TextStyle(
|
||||||
|
color: Colors.black,
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
))),
|
||||||
|
debugShowCheckedModeBanner: false,
|
||||||
|
home: AppNavigation()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
99
lib/v6_result_/pages/color/color_add_page.dart
Normal file
99
lib/v6_result_/pages/color/color_add_page.dart
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
|
||||||
|
|
||||||
|
class ColorAddPage extends StatefulWidget {
|
||||||
|
const ColorAddPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ColorAddPage> createState() => _ColorAddPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ColorAddPageState extends State<ColorAddPage> {
|
||||||
|
late Color _color;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_color = randomColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
String text = '# ${_color.value.toRadixString(16)}';
|
||||||
|
return Scaffold(
|
||||||
|
bottomNavigationBar: Container(
|
||||||
|
margin: EdgeInsets.only(right:20,bottom: 20),
|
||||||
|
// color: Colors.redAccent,
|
||||||
|
child: Row(
|
||||||
|
textDirection:TextDirection.rtl,
|
||||||
|
children: [
|
||||||
|
ElevatedButton(onPressed: (){
|
||||||
|
Navigator.of(context).pop(_color);
|
||||||
|
|
||||||
|
}, child: Text('添加')),
|
||||||
|
SizedBox(width: 12,),
|
||||||
|
OutlinedButton(onPressed: (){
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
}, child: Text('取消')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
// mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16.0,vertical: 16),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(child: Text(text,style: TextStyle(color: _color,fontSize: 24,letterSpacing: 4),)),
|
||||||
|
Container(
|
||||||
|
margin: EdgeInsets.only(left: 10),
|
||||||
|
width: 40,
|
||||||
|
height: 40,
|
||||||
|
child: Icon(
|
||||||
|
Icons.sell_outlined,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _color,
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ColorPicker(
|
||||||
|
colorPickerWidth:200,
|
||||||
|
// enableAlpha: false,
|
||||||
|
displayThumbColor:true,
|
||||||
|
pickerColor: _color,
|
||||||
|
paletteType: PaletteType.hueWheel,
|
||||||
|
onColorChanged: changeColor,
|
||||||
|
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Random _random = Random();
|
||||||
|
|
||||||
|
Color get randomColor {
|
||||||
|
return Color.fromARGB(
|
||||||
|
255,
|
||||||
|
_random.nextInt(256),
|
||||||
|
_random.nextInt(256),
|
||||||
|
_random.nextInt(256),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void changeColor(Color value) {
|
||||||
|
_color = value;
|
||||||
|
setState(() {
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
25
lib/v6_result_/pages/color/color_detail_page.dart
Normal file
25
lib/v6_result_/pages/color/color_detail_page.dart
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
class ColorDetailPage extends StatelessWidget {
|
||||||
|
final Color color;
|
||||||
|
const ColorDetailPage({super.key, required this.color});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
|
||||||
|
const TextStyle style = TextStyle(
|
||||||
|
fontSize: 32,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.white
|
||||||
|
);
|
||||||
|
String text = '# ${color.value.toRadixString(16)}';
|
||||||
|
return Scaffold(
|
||||||
|
body: Container(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
color: color,
|
||||||
|
child: Text(text ,style: style,),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
52
lib/v6_result_/pages/color/color_page.dart
Normal file
52
lib/v6_result_/pages/color/color_page.dart
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:iroute/components/project/colors_panel.dart';
|
||||||
|
import '../../app/navigation/router/app_router_delegate.dart';
|
||||||
|
|
||||||
|
class ColorPage extends StatefulWidget {
|
||||||
|
const ColorPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<ColorPage> createState() => _ColorPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ColorPageState extends State<ColorPage> {
|
||||||
|
final List<Color> _colors = [
|
||||||
|
Colors.red, Colors.black, Colors.blue, Colors.green, Colors.orange,
|
||||||
|
Colors.pink, Colors.purple, Colors.indigo, Colors.amber, Colors.cyan,
|
||||||
|
Colors.redAccent, Colors.grey, Colors.blueAccent, Colors.greenAccent, Colors.orangeAccent,
|
||||||
|
Colors.pinkAccent, Colors.purpleAccent, Colors.indigoAccent, Colors.amberAccent, Colors.cyanAccent,
|
||||||
|
];
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
// appBar: AppBar(title:const Text('颜色主页')),
|
||||||
|
floatingActionButton: FloatingActionButton(
|
||||||
|
onPressed: _toAddPage,
|
||||||
|
child: const Icon(Icons.add),
|
||||||
|
),
|
||||||
|
body: Align(
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
child: ColorsPanel(
|
||||||
|
colors: _colors,
|
||||||
|
onSelect: _selectColor,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _selectColor(Color color){
|
||||||
|
// String value = color.value.toRadixString(16);
|
||||||
|
// router.path = '/color/detail?color=$value';
|
||||||
|
router.changePath('/color/detail',extra: color);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _toAddPage() async {
|
||||||
|
Color? color = await router.changePath('/color/add',forResult: true);
|
||||||
|
if (color != null) {
|
||||||
|
setState(() {
|
||||||
|
_colors.add(color);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
lib/v6_result_/pages/counter/counter_page.dart
Normal file
43
lib/v6_result_/pages/counter/counter_page.dart
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class CounterPage extends StatefulWidget {
|
||||||
|
const CounterPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CounterPage> createState() => _CounterPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CounterPageState extends State<CounterPage> {
|
||||||
|
int _counter = 0;
|
||||||
|
|
||||||
|
void _incrementCounter() {
|
||||||
|
setState(() {
|
||||||
|
_counter++;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
const Text(
|
||||||
|
'You have pushed the button this many times:',
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
'$_counter',
|
||||||
|
style: Theme.of(context).textTheme.headlineMedium,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
floatingActionButton: FloatingActionButton(
|
||||||
|
onPressed: _incrementCounter,
|
||||||
|
tooltip: 'Increment',
|
||||||
|
child: const Icon(Icons.add),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
30
lib/v6_result_/pages/empty/empty_page.dart
Normal file
30
lib/v6_result_/pages/empty/empty_page.dart
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class EmptyPage extends StatelessWidget {
|
||||||
|
const EmptyPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
// appBar: AppBar(
|
||||||
|
// title: Text('界面走丢了'),
|
||||||
|
// ),
|
||||||
|
body: Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: Wrap(
|
||||||
|
spacing: 16,
|
||||||
|
crossAxisAlignment: WrapCrossAlignment.center,
|
||||||
|
direction: Axis.vertical,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.nearby_error,size: 64, color: Colors.grey),
|
||||||
|
Text(
|
||||||
|
'404 界面丢失',
|
||||||
|
style: TextStyle(fontSize: 24, color: Colors.grey),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
11
lib/v6_result_/pages/settings/settings_page.dart
Normal file
11
lib/v6_result_/pages/settings/settings_page.dart
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class SettingPage extends StatelessWidget {
|
||||||
|
const SettingPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
body:Center(child: Text('SettingPage')));
|
||||||
|
}
|
||||||
|
}
|
||||||
859
lib/v6_result_/pages/sort/sort_page.dart
Normal file
859
lib/v6_result_/pages/sort/sort_page.dart
Normal file
File diff suppressed because it is too large
Load Diff
11
lib/v6_result_/pages/user/user_page.dart
Normal file
11
lib/v6_result_/pages/user/user_page.dart
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class UserPage extends StatelessWidget {
|
||||||
|
const UserPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
body:Center(child: Text('UserPage')));
|
||||||
|
}
|
||||||
|
}
|
||||||
1
lib/v7/app.dart
Normal file
1
lib/v7/app.dart
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export 'app/unit_app.dart';
|
||||||
218
lib/v7/app/navigation/router/app_router_delegate.dart
Normal file
218
lib/v7/app/navigation/router/app_router_delegate.dart
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import 'iroute.dart';
|
||||||
|
import 'route_history.dart';
|
||||||
|
|
||||||
|
const List<String> kDestinationsPaths = [
|
||||||
|
'/color',
|
||||||
|
'/counter',
|
||||||
|
'/user',
|
||||||
|
'/settings',
|
||||||
|
];
|
||||||
|
|
||||||
|
AppRouterDelegate router = AppRouterDelegate();
|
||||||
|
|
||||||
|
class AppRouterDelegate extends RouterDelegate<Object> with ChangeNotifier {
|
||||||
|
String _path = '/color';
|
||||||
|
|
||||||
|
String get path => _path;
|
||||||
|
|
||||||
|
AppRouterDelegate() {
|
||||||
|
// keepAlivePath.add('/color');
|
||||||
|
_histories.add(RouteHistory(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<RouteHistory> _histories = [];
|
||||||
|
final List<RouteHistory> _backHistories = [];
|
||||||
|
|
||||||
|
List<RouteHistory> get histories => _histories.reversed.toList();
|
||||||
|
|
||||||
|
bool get hasHistory => _histories.length > 1;
|
||||||
|
|
||||||
|
bool get hasBackHistory => _backHistories.isNotEmpty;
|
||||||
|
|
||||||
|
/// 历史回退操作
|
||||||
|
/// 将当前顶层移除,并加入 _backHistories 撤销列表
|
||||||
|
/// 并转到前一路径
|
||||||
|
void back() {
|
||||||
|
if (!hasHistory) return;
|
||||||
|
RouteHistory top = _histories.removeLast();
|
||||||
|
_backHistories.add(top);
|
||||||
|
if (_histories.isNotEmpty) {
|
||||||
|
_path = _histories.last.path;
|
||||||
|
if (_histories.last.extra != null) {
|
||||||
|
_pathExtraMap[_path] = _histories.last.extra;
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void toHistory(RouteHistory history) {
|
||||||
|
_path = history.path;
|
||||||
|
if (history.extra != null) {
|
||||||
|
_pathExtraMap[_path] = history.extra;
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void closeHistory(int index) {
|
||||||
|
_histories.removeAt(index);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearHistory() {
|
||||||
|
_histories.clear();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 撤销回退操作
|
||||||
|
/// 取出回退列表的最后元素,跳转到该路径
|
||||||
|
void revocation() {
|
||||||
|
RouteHistory target = _backHistories.removeLast();
|
||||||
|
_path = target.path;
|
||||||
|
if (target.extra != null) {
|
||||||
|
_pathExtraMap[_path] = target.extra;
|
||||||
|
}
|
||||||
|
_histories.add(target);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
int? get activeIndex {
|
||||||
|
if (path.startsWith('/color')) return 0;
|
||||||
|
if (path.startsWith('/counter')) return 1;
|
||||||
|
if (path.startsWith('/user')) return 2;
|
||||||
|
if (path.startsWith('/settings')) return 3;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Map<String, Completer<dynamic>> _completerMap = {};
|
||||||
|
|
||||||
|
Completer<dynamic>? completer;
|
||||||
|
|
||||||
|
final Map<String, dynamic> _pathExtraMap = {};
|
||||||
|
|
||||||
|
final List<String> keepAlivePath = [];
|
||||||
|
|
||||||
|
FutureOr<dynamic> changePath(
|
||||||
|
String value, {
|
||||||
|
bool forResult = false,
|
||||||
|
Object? extra,
|
||||||
|
bool keepAlive = false,
|
||||||
|
bool recordHistory = true,
|
||||||
|
}) {
|
||||||
|
if (_path == value) null;
|
||||||
|
if (forResult) {
|
||||||
|
_completerMap[value] = Completer();
|
||||||
|
}
|
||||||
|
if (keepAlive) {
|
||||||
|
if (keepAlivePath.contains(value)) {
|
||||||
|
keepAlivePath.remove(value);
|
||||||
|
}
|
||||||
|
keepAlivePath.add(value);
|
||||||
|
}
|
||||||
|
if (extra != null) {
|
||||||
|
_pathExtraMap[value] = extra;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recordHistory) {
|
||||||
|
_addPathToHistory(value,extra);
|
||||||
|
}
|
||||||
|
|
||||||
|
_path = value;
|
||||||
|
notifyListeners();
|
||||||
|
if (forResult) {
|
||||||
|
return _completerMap[value]!.future;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void _addPathToHistory(String value, Object? extra) {
|
||||||
|
if (_histories.isNotEmpty && value == _histories.last.path) return;
|
||||||
|
_histories.add(RouteHistory(
|
||||||
|
value,
|
||||||
|
extra: _pathExtraMap[path],
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Navigator(
|
||||||
|
onPopPage: _onPopPage,
|
||||||
|
pages: _buildPages(context, path),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Page> _buildPages(BuildContext context, String path) {
|
||||||
|
List<Page> pages = [];
|
||||||
|
List<Page> topPages = _buildPageByPathFromTree(context, path);
|
||||||
|
|
||||||
|
if (keepAlivePath.isNotEmpty) {
|
||||||
|
for (String alivePath in keepAlivePath) {
|
||||||
|
if (alivePath != path) {
|
||||||
|
pages.addAll(_buildPageByPathFromTree(context, alivePath));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 去除和 topPages 中重复的界面
|
||||||
|
pages.removeWhere(
|
||||||
|
(element) => topPages.map((e) => e.key).contains(element.key));
|
||||||
|
}
|
||||||
|
|
||||||
|
pages.addAll(topPages);
|
||||||
|
return pages;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Page> _buildPageByPathFromTree(BuildContext context, String path) {
|
||||||
|
List<Page> result = [];
|
||||||
|
List<IRoute> iRoutes = root.find(path);
|
||||||
|
if (iRoutes.isNotEmpty) {
|
||||||
|
for (int i = 0; i < iRoutes.length; i++) {
|
||||||
|
IRoute iroute = iRoutes[i];
|
||||||
|
String path = iroute.path;
|
||||||
|
Object? extra = _pathExtraMap[path];
|
||||||
|
bool keepAlive = keepAlivePath.contains(path);
|
||||||
|
bool forResult = _completerMap.containsKey(path);
|
||||||
|
Page? page = iroute.builder?.call(
|
||||||
|
context,
|
||||||
|
IRouteData(
|
||||||
|
uri: Uri.parse(path),
|
||||||
|
extra: extra,
|
||||||
|
keepAlive: keepAlive,
|
||||||
|
forResult: forResult,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (page != null) {
|
||||||
|
result.add(page);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> popRoute() async {
|
||||||
|
print('=======popRoute=========');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _onPopPage(Route route, result) {
|
||||||
|
if (_completerMap.containsKey(path)) {
|
||||||
|
_completerMap[path]?.complete(result);
|
||||||
|
_completerMap.remove(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
changePath(backPath(path),recordHistory: false);
|
||||||
|
return route.didPop(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
String backPath(String path) {
|
||||||
|
Uri uri = Uri.parse(path);
|
||||||
|
if (uri.pathSegments.length == 1) return path;
|
||||||
|
List<String> parts = List.of(uri.pathSegments)..removeLast();
|
||||||
|
return '/${parts.join('/')}';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> setNewRoutePath(configuration) async {}
|
||||||
|
}
|
||||||
185
lib/v7/app/navigation/router/iroute.dart
Normal file
185
lib/v7/app/navigation/router/iroute.dart
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../../../pages/color/color_add_page.dart';
|
||||||
|
import '../../../pages/color/color_detail_page.dart';
|
||||||
|
import '../../../pages/color/color_page.dart';
|
||||||
|
import '../../../pages/counter/counter_page.dart';
|
||||||
|
import '../../../pages/user/user_page.dart';
|
||||||
|
import '../../../pages/settings/settings_page.dart';
|
||||||
|
import '../transition/fade_transition_page.dart';
|
||||||
|
|
||||||
|
class IRoute {
|
||||||
|
final String path;
|
||||||
|
final IRoutePageBuilder? builder;
|
||||||
|
final List<IRoute> children;
|
||||||
|
|
||||||
|
const IRoute({
|
||||||
|
required this.path,
|
||||||
|
this.children = const [],
|
||||||
|
this.builder,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'IRoute{path: $path, children: $children}';
|
||||||
|
}
|
||||||
|
|
||||||
|
IRoute? match(String path) {
|
||||||
|
return matchRoute(this, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<IRoute> find(String input){
|
||||||
|
String fixInput = input.substring(1);
|
||||||
|
List<IRoute> nodes = findNodes(this,fixInput,0,'/',[]);
|
||||||
|
if(nodes.isNotEmpty&&nodes.last.path!=input){
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<IRoute> findNodes(IRoute node,String input,int deep,String prefix,List<IRoute> result){
|
||||||
|
String separator = '/';
|
||||||
|
|
||||||
|
List<String> parts = input.split(separator);
|
||||||
|
if(deep>parts.length-1){
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
String target = parts[deep];
|
||||||
|
if(node.children.isNotEmpty){
|
||||||
|
List<IRoute> nodes = node.children.where((e) => e.path==prefix+target).toList();
|
||||||
|
bool match = nodes.isNotEmpty;
|
||||||
|
if(match){
|
||||||
|
IRoute matched = nodes.first;
|
||||||
|
result.add(matched);
|
||||||
|
String nextPrefix = '${matched.path}$separator';
|
||||||
|
findNodes(matched, input, ++deep,nextPrefix,result);
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// List<IRoute> findNodes(IRoute node,String input,int deep,String prefix,List<IRoute> result){
|
||||||
|
// String separator = '/';
|
||||||
|
// List<String> parts = input.split(separator);
|
||||||
|
// if(deep>parts.length-1){
|
||||||
|
// return result;
|
||||||
|
// }
|
||||||
|
// String target = parts[deep];
|
||||||
|
// if(node.children.isNotEmpty){
|
||||||
|
// List<IRoute> nodes = node.children.where((e) => e.path==prefix+target).toList();
|
||||||
|
// bool match = nodes.isNotEmpty;
|
||||||
|
// if(match){
|
||||||
|
// IRoute matched = nodes.first;
|
||||||
|
// result.add(matched);
|
||||||
|
// String nextPrefix = '${matched.path}$separator';
|
||||||
|
// findNodes(matched, input, ++deep,nextPrefix,result);
|
||||||
|
// }
|
||||||
|
// }else{
|
||||||
|
// return result;
|
||||||
|
// }
|
||||||
|
// return result;
|
||||||
|
// }
|
||||||
|
|
||||||
|
IRoute? matchRoute(IRoute route, String path) {
|
||||||
|
if (route.path == path) {
|
||||||
|
return route;
|
||||||
|
} else {
|
||||||
|
if (route.children.isNotEmpty) {
|
||||||
|
for (int i = 0; i < route.children.length; i++) {
|
||||||
|
IRoute current = route.children[i];
|
||||||
|
IRoute? target = matchRoute(current, path);
|
||||||
|
if (target != null) {
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef IRoutePageBuilder = Page? Function(
|
||||||
|
BuildContext context, IRouteData data);
|
||||||
|
|
||||||
|
class IRouteData {
|
||||||
|
final Object? extra;
|
||||||
|
final bool forResult;
|
||||||
|
final Uri uri;
|
||||||
|
final bool keepAlive;
|
||||||
|
|
||||||
|
IRouteData({
|
||||||
|
this.extra,
|
||||||
|
required this.uri,
|
||||||
|
this.forResult = false,
|
||||||
|
this.keepAlive = false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
IRoute root = IRoute(path: 'root', children: kDestinationsIRoutes);
|
||||||
|
|
||||||
|
List<IRoute> kDestinationsIRoutes = [
|
||||||
|
IRoute(
|
||||||
|
path: '/color',
|
||||||
|
builder: (ctx, data) {
|
||||||
|
return const FadeTransitionPage(
|
||||||
|
key: ValueKey('/color'),
|
||||||
|
child: ColorPage(),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
children: [
|
||||||
|
IRoute(
|
||||||
|
path: '/color/detail',
|
||||||
|
builder: (ctx, data) {
|
||||||
|
final Map<String, String> queryParams = data.uri.queryParameters;
|
||||||
|
String? selectedColor = queryParams['color'];
|
||||||
|
Color color = Colors.black;
|
||||||
|
if (selectedColor != null) {
|
||||||
|
color = Color(int.parse(selectedColor, radix: 16));
|
||||||
|
} else if (data.extra is Color) {
|
||||||
|
color = data.extra as Color;
|
||||||
|
}
|
||||||
|
return FadeTransitionPage(
|
||||||
|
key: const ValueKey('/color/detail'),
|
||||||
|
child: ColorDetailPage(color: color),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
IRoute(
|
||||||
|
path: '/color/add',
|
||||||
|
builder: (ctx, data) {
|
||||||
|
return const FadeTransitionPage(
|
||||||
|
key: ValueKey('/color/add'),
|
||||||
|
child: ColorAddPage(),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
IRoute(
|
||||||
|
path: '/counter',
|
||||||
|
builder: (ctx, data) {
|
||||||
|
return const FadeTransitionPage(
|
||||||
|
key: ValueKey('/counter'),
|
||||||
|
child: CounterPage(),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
IRoute(
|
||||||
|
path: '/user',
|
||||||
|
builder: (ctx, data) {
|
||||||
|
return const FadeTransitionPage(
|
||||||
|
key: ValueKey('/user'),
|
||||||
|
child: UserPage(),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
IRoute(
|
||||||
|
path: '/settings',
|
||||||
|
builder: (ctx, data) {
|
||||||
|
return const FadeTransitionPage(
|
||||||
|
key: ValueKey('/settings'),
|
||||||
|
child: SettingPage(),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
];
|
||||||
6
lib/v7/app/navigation/router/route_history.dart
Normal file
6
lib/v7/app/navigation/router/route_history.dart
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
class RouteHistory{
|
||||||
|
final String path;
|
||||||
|
final Object? extra;
|
||||||
|
|
||||||
|
RouteHistory(this.path, { this.extra});
|
||||||
|
}
|
||||||
12
lib/v7/app/navigation/router/route_history_manager.dart
Normal file
12
lib/v7/app/navigation/router/route_history_manager.dart
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
// import 'route_history.dart';
|
||||||
|
//
|
||||||
|
// class RouteHistoryManager{
|
||||||
|
// final List<RouteHistory> _histories = [];
|
||||||
|
// final List<RouteHistory> _backHistories = [];
|
||||||
|
//
|
||||||
|
// List<RouteHistory> get histories => _histories.reversed.toList();
|
||||||
|
//
|
||||||
|
// bool get hasHistory => _histories.length > 1;
|
||||||
|
//
|
||||||
|
// bool get hasBackHistory => _backHistories.isNotEmpty;
|
||||||
|
// }
|
||||||
53
lib/v7/app/navigation/transition/fade_transition_page.dart
Normal file
53
lib/v7/app/navigation/transition/fade_transition_page.dart
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
// Copyright 2021, the Flutter project authors. Please see the AUTHORS file
|
||||||
|
// for details. All rights reserved. Use of this source code is governed by a
|
||||||
|
// BSD-style license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class FadeTransitionPage<T> extends Page<T> {
|
||||||
|
final Widget child;
|
||||||
|
final Duration duration;
|
||||||
|
|
||||||
|
const FadeTransitionPage({
|
||||||
|
super.key,
|
||||||
|
required this.child,
|
||||||
|
this.duration = const Duration(milliseconds: 300),
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Route<T> createRoute(BuildContext context) =>
|
||||||
|
PageBasedFadeTransitionRoute<T>(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
class PageBasedFadeTransitionRoute<T> extends PageRoute<T> {
|
||||||
|
final FadeTransitionPage<T> _page;
|
||||||
|
|
||||||
|
PageBasedFadeTransitionRoute(this._page) : super(settings: _page);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Color? get barrierColor => null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get barrierLabel => null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Duration get transitionDuration => _page.duration;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get maintainState => true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildPage(BuildContext context, Animation<double> animation,
|
||||||
|
Animation<double> secondaryAnimation) {
|
||||||
|
var curveTween = CurveTween(curve: Curves.easeIn);
|
||||||
|
return FadeTransition(
|
||||||
|
opacity: animation.drive(curveTween),
|
||||||
|
child: (settings as FadeTransitionPage).child,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildTransitions(BuildContext context, Animation<double> animation,
|
||||||
|
Animation<double> secondaryAnimation, Widget child) =>
|
||||||
|
child;
|
||||||
|
}
|
||||||
47
lib/v7/app/navigation/transition/no_transition_page.dart
Normal file
47
lib/v7/app/navigation/transition/no_transition_page.dart
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
// Copyright 2021, the Flutter project authors. Please see the AUTHORS file
|
||||||
|
// for details. All rights reserved. Use of this source code is governed by a
|
||||||
|
// BSD-style license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class NoTransitionPage<T> extends Page<T> {
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
const NoTransitionPage({
|
||||||
|
super.key,
|
||||||
|
required this.child,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Route<T> createRoute(BuildContext context) => NoTransitionRoute<T>(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
class NoTransitionRoute<T> extends PageRoute<T> {
|
||||||
|
|
||||||
|
final NoTransitionPage<T> _page;
|
||||||
|
|
||||||
|
NoTransitionRoute(this._page) : super(settings: _page);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Color? get barrierColor => null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get barrierLabel => null;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Duration get transitionDuration => const Duration(milliseconds: 0);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get maintainState => true;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildPage(BuildContext context, Animation<double> animation,
|
||||||
|
Animation<double> secondaryAnimation) {
|
||||||
|
return (settings as NoTransitionPage).child;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget buildTransitions(BuildContext context, Animation<double> animation,
|
||||||
|
Animation<double> secondaryAnimation, Widget child) =>
|
||||||
|
child;
|
||||||
|
}
|
||||||
34
lib/v7/app/navigation/views/app_navigation.dart
Normal file
34
lib/v7/app/navigation/views/app_navigation.dart
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../router/app_router_delegate.dart';
|
||||||
|
import 'app_navigation_rail.dart';
|
||||||
|
import 'app_top_bar/app_top_bar.dart';
|
||||||
|
|
||||||
|
class AppNavigation extends StatelessWidget {
|
||||||
|
const AppNavigation({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
double px1 = 1/View.of(context).devicePixelRatio;
|
||||||
|
return Scaffold(
|
||||||
|
body: Row(
|
||||||
|
children: [
|
||||||
|
const AppNavigationRail(),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
const AppTopBar(),
|
||||||
|
Divider(height: px1,),
|
||||||
|
Expanded(
|
||||||
|
child: Router(
|
||||||
|
routerDelegate: router,
|
||||||
|
backButtonDispatcher: RootBackButtonDispatcher(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
65
lib/v7/app/navigation/views/app_navigation_rail.dart
Normal file
65
lib/v7/app/navigation/views/app_navigation_rail.dart
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:iroute/components/components.dart';
|
||||||
|
import '../router/app_router_delegate.dart';
|
||||||
|
|
||||||
|
class AppNavigationRail extends StatefulWidget {
|
||||||
|
const AppNavigationRail({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AppNavigationRail> createState() => _AppNavigationRailState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AppNavigationRailState extends State<AppNavigationRail> {
|
||||||
|
|
||||||
|
final List<MenuMeta> deskNavBarMenus = const [
|
||||||
|
MenuMeta(label: '颜色板', icon: Icons.color_lens_outlined),
|
||||||
|
MenuMeta(label: '计数器', icon: Icons.add_chart),
|
||||||
|
MenuMeta(label: '我的', icon: Icons.person),
|
||||||
|
MenuMeta(label: '设置', icon: Icons.settings),
|
||||||
|
];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
router.addListener(_onRouterChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
router.removeListener(_onRouterChange);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return DragToMoveWrap(
|
||||||
|
child: TolyNavigationRail(
|
||||||
|
items: deskNavBarMenus,
|
||||||
|
leading: const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 18.0),
|
||||||
|
child: FlutterLogo(),
|
||||||
|
),
|
||||||
|
tail: Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 6.0),
|
||||||
|
child: Text('V0.0.6',style: TextStyle(color: Colors.white,fontSize: 12),),
|
||||||
|
),
|
||||||
|
backgroundColor: const Color(0xff3975c6),
|
||||||
|
onDestinationSelected: _onDestinationSelected,
|
||||||
|
selectedIndex: router.activeIndex,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onDestinationSelected(int index) {
|
||||||
|
if(index==1){
|
||||||
|
router.changePath(kDestinationsPaths[index],keepAlive: true);
|
||||||
|
}else{
|
||||||
|
router.changePath(kDestinationsPaths[index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onRouterChange() {
|
||||||
|
setState(() {});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import 'package:flutter/cupertino.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:iroute/components/toly_ui/button/hover_icon_button.dart';
|
||||||
|
import '../../router/app_router_delegate.dart';
|
||||||
|
|
||||||
|
class AppRouterEditor extends StatefulWidget {
|
||||||
|
final ValueChanged<String>? onSubmit;
|
||||||
|
const AppRouterEditor({super.key, this.onSubmit});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<AppRouterEditor> createState() => _AppRouterEditorState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AppRouterEditorState extends State<AppRouterEditor> {
|
||||||
|
|
||||||
|
final TextEditingController _controller = TextEditingController();
|
||||||
|
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_onRouteChange();
|
||||||
|
router.addListener(_onRouteChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onRouteChange() {
|
||||||
|
_controller.text=router.path;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
router.removeListener(_onRouteChange);
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Stack(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
child: CupertinoTextField(
|
||||||
|
controller: _controller,
|
||||||
|
style: TextStyle(fontSize: 14),
|
||||||
|
padding: EdgeInsets.only(left:12,top: 6,bottom: 6,right: 32),
|
||||||
|
placeholder: '输入路由地址导航',
|
||||||
|
onSubmitted: widget.onSubmit,
|
||||||
|
decoration: BoxDecoration(color: Color(0xffF1F2F3),borderRadius: BorderRadius.circular(6)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 8.0),
|
||||||
|
child: HoverIconButton(
|
||||||
|
icon: Icons.directions_outlined,
|
||||||
|
defaultColor: Color(0xff68696B),
|
||||||
|
onPressed:()=>widget.onSubmit?.call(_controller.text),
|
||||||
|
size: 20
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user