v1.0.1
This commit is contained in:
54
lib/widgets/border_text.dart
Normal file
54
lib/widgets/border_text.dart
Normal file
@@ -0,0 +1,54 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class BorderText extends StatelessWidget {
|
||||
final String text;
|
||||
final TextAlign textAlign;
|
||||
final Color color;
|
||||
final double fontSize;
|
||||
final double strokeWidth;
|
||||
const BorderText(
|
||||
this.text, {
|
||||
this.textAlign = TextAlign.left,
|
||||
this.color = Colors.white,
|
||||
this.fontSize = 16,
|
||||
this.strokeWidth = 2.0,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Stack(
|
||||
children: <Widget>[
|
||||
Text(
|
||||
text,
|
||||
softWrap: false,
|
||||
textAlign: textAlign,
|
||||
style: TextStyle(
|
||||
fontSize: fontSize,
|
||||
foreground: Paint()
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = strokeWidth
|
||||
..strokeCap = StrokeCap.round
|
||||
..strokeJoin = StrokeJoin.round
|
||||
..color = getBorderColor(color),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
text,
|
||||
softWrap: false,
|
||||
textAlign: textAlign,
|
||||
style: TextStyle(
|
||||
fontSize: fontSize,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Color getBorderColor(Color color) {
|
||||
var brightness =
|
||||
((color.red * 299) + (color.green * 587) + (color.blue * 114)) / 1000;
|
||||
return brightness > 70 ? Colors.black : Colors.white;
|
||||
}
|
||||
}
|
||||
328
lib/widgets/comment_item_widget.dart
Normal file
328
lib/widgets/comment_item_widget.dart
Normal file
@@ -0,0 +1,328 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_dmzj/app/app_style.dart';
|
||||
import 'package:flutter_dmzj/app/dialog_utils.dart';
|
||||
import 'package:flutter_dmzj/app/utils.dart';
|
||||
import 'package:flutter_dmzj/models/comment/comment_item.dart';
|
||||
import 'package:flutter_dmzj/requests/comment_request.dart';
|
||||
import 'package:flutter_dmzj/routes/app_navigator.dart';
|
||||
import 'package:flutter_dmzj/widgets/net_image.dart';
|
||||
import 'package:flutter_dmzj/widgets/user_photo.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:remixicon/remixicon.dart';
|
||||
|
||||
// ignore: must_be_immutable
|
||||
class CommentItemWidget extends StatelessWidget {
|
||||
final CommentItem item;
|
||||
CommentItemWidget(this.item, {Key? key}) : super(key: key);
|
||||
var expand = false.obs;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
onTap(item);
|
||||
},
|
||||
child: Container(
|
||||
padding: AppStyle.edgeInsetsA12,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
InkWell(
|
||||
onTap: () {
|
||||
AppNavigator.toUserCenter(item.userId);
|
||||
},
|
||||
child: UserPhoto(
|
||||
url: item.photo,
|
||||
),
|
||||
),
|
||||
AppStyle.hGap12,
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Text(
|
||||
item.nickname,
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
// const Text(
|
||||
// "-",
|
||||
// style: TextStyle(color: Colors.grey),
|
||||
// )
|
||||
],
|
||||
),
|
||||
AppStyle.vGap12,
|
||||
item.parents.isNotEmpty
|
||||
? Obx(
|
||||
() => expand.value
|
||||
? createMasterCommentAll(item.parents)
|
||||
: createMasterComment(item),
|
||||
)
|
||||
: Container(),
|
||||
Text(
|
||||
item.content,
|
||||
style: Get.theme.textTheme.bodyMedium,
|
||||
),
|
||||
item.images.isNotEmpty
|
||||
? Padding(
|
||||
padding: AppStyle.edgeInsetsT12,
|
||||
child: Wrap(
|
||||
spacing: 4,
|
||||
runSpacing: 4,
|
||||
children: item.images.map<Widget>((f) {
|
||||
var str = f.split(".").toList();
|
||||
var fileImg = str[0];
|
||||
var fileImgSuffix = str[1];
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
DialogUtils.showImageViewer(0, [
|
||||
"https://images.zaimanhua.com/commentImg/${item.objId % 500}/$f"
|
||||
]);
|
||||
},
|
||||
child: NetImage(
|
||||
"https://images.zaimanhua.com/commentImg/${item.objId % 500}/${fileImg}_small.$fileImgSuffix",
|
||||
width: 100,
|
||||
height: 100,
|
||||
borderRadius: 4,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
)
|
||||
: const SizedBox(),
|
||||
AppStyle.vGap12,
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Text(
|
||||
Utils.formatTimestamp(item.createTime),
|
||||
style:
|
||||
const TextStyle(color: Colors.grey, fontSize: 12),
|
||||
),
|
||||
),
|
||||
Obx(
|
||||
() => GestureDetector(
|
||||
onTap: () {
|
||||
likeComment(item);
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Remix.thumb_up_fill,
|
||||
size: 16,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
Visibility(
|
||||
visible: item.likeAmount.value > 0,
|
||||
child: Padding(
|
||||
padding: AppStyle.edgeInsetsL4,
|
||||
child: Text(
|
||||
item.likeAmount.value.toString(),
|
||||
style: const TextStyle(
|
||||
color: Colors.grey,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
))
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget createMasterComment(CommentItem comment) {
|
||||
var list = comment.parents;
|
||||
if (list.isEmpty) return const SizedBox();
|
||||
List<Widget> items = [];
|
||||
if (list.length > 2) {
|
||||
items.add(createMsterCommentItem(list.first));
|
||||
items.add(InkWell(
|
||||
onTap: () {
|
||||
expand.value = true;
|
||||
},
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(4)),
|
||||
padding: AppStyle.edgeInsetsA8,
|
||||
child: Center(
|
||||
child: Text(
|
||||
"点击展开${list.length - 2}条评论",
|
||||
style: const TextStyle(fontSize: 12, color: Colors.grey),
|
||||
)),
|
||||
),
|
||||
));
|
||||
items.add(AppStyle.vGap8);
|
||||
items.add(createMsterCommentItem(list.last));
|
||||
} else {
|
||||
for (var item in list) {
|
||||
items.add(createMsterCommentItem(item));
|
||||
}
|
||||
}
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: items,
|
||||
);
|
||||
}
|
||||
|
||||
Widget createMasterCommentAll(List<CommentItem> list) {
|
||||
List<Widget> items = list.map<Widget>((item) {
|
||||
return createMsterCommentItem(item);
|
||||
}).toList();
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: items,
|
||||
);
|
||||
}
|
||||
|
||||
Widget createMsterCommentItem(CommentItem item) {
|
||||
return Padding(
|
||||
padding: AppStyle.edgeInsetsB8,
|
||||
child: InkWell(
|
||||
onTap: () {
|
||||
onTap(item);
|
||||
},
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(4)),
|
||||
padding: AppStyle.edgeInsetsA8,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
RichText(
|
||||
text: TextSpan(children: [
|
||||
WidgetSpan(
|
||||
alignment: ui.PlaceholderAlignment.middle,
|
||||
child: InkWell(
|
||||
child: Text(
|
||||
item.nickname,
|
||||
style:
|
||||
TextStyle(color: Get.theme.colorScheme.secondary),
|
||||
),
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: ": ${item.content}",
|
||||
style: Get.theme.textTheme.bodyMedium,
|
||||
)
|
||||
]),
|
||||
),
|
||||
item.images.isNotEmpty
|
||||
? Padding(
|
||||
padding: AppStyle.edgeInsetsT8,
|
||||
child: Wrap(
|
||||
spacing: 4,
|
||||
runSpacing: 4,
|
||||
children: item.images.map<Widget>((f) {
|
||||
var str = f.split(".").toList();
|
||||
var fileImg = str[0];
|
||||
var fileImgSuffix = str[1];
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
DialogUtils.showImageViewer(0, [
|
||||
"https://images.idmzj.com/commentImg/${item.objId % 500}/$f"
|
||||
]);
|
||||
},
|
||||
child: NetImage(
|
||||
"https://images.idmzj.com/commentImg/${item.objId % 500}/${fileImg}_small.$fileImgSuffix",
|
||||
width: 100,
|
||||
height: 100,
|
||||
borderRadius: 4,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
)
|
||||
: Container(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void likeComment(CommentItem item) async {
|
||||
try {
|
||||
await CommentRequest().likeComment(
|
||||
commentId: item.id,
|
||||
objId: item.objId,
|
||||
type: item.type,
|
||||
);
|
||||
item.likeAmount.value += 1;
|
||||
} catch (e) {
|
||||
SmartDialog.showToast(e.toString());
|
||||
}
|
||||
}
|
||||
|
||||
void onTap(CommentItem item) {
|
||||
AppNavigator.showBottomSheet(Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
ListTile(
|
||||
title: Text(item.nickname),
|
||||
leading: UserPhoto(
|
||||
url: item.photo,
|
||||
size: 32,
|
||||
showBoder: true,
|
||||
),
|
||||
onTap: () {
|
||||
AppNavigator.toUserCenter(item.userId);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: const Text("复制内容"),
|
||||
leading: const Icon(Icons.content_copy),
|
||||
onTap: () {
|
||||
Utils.copyText(item.content);
|
||||
|
||||
AppNavigator.closePage();
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: const Text("点赞评论"),
|
||||
leading: const Icon(Icons.thumb_up_outlined),
|
||||
onTap: () {
|
||||
AppNavigator.closePage();
|
||||
likeComment(item);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: const Text("回复评论"),
|
||||
leading: const Icon(Icons.message_outlined),
|
||||
onTap: () {
|
||||
AppNavigator.closePage();
|
||||
AppNavigator.toAddComment(
|
||||
objId: item.objId,
|
||||
type: item.type,
|
||||
replyItem: item,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
));
|
||||
}
|
||||
}
|
||||
439
lib/widgets/custom_header.dart
Normal file
439
lib/widgets/custom_header.dart
Normal file
@@ -0,0 +1,439 @@
|
||||
import 'package:easy_refresh/easy_refresh.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/physics.dart' as physics;
|
||||
|
||||
import 'dart:math' as math;
|
||||
|
||||
/// Material header.
|
||||
class MaterialHeader2 extends Header {
|
||||
final Key? key;
|
||||
|
||||
/// See [ProgressIndicator.backgroundColor].
|
||||
final Color? backgroundColor;
|
||||
|
||||
/// See [ProgressIndicator.color].
|
||||
final Color? color;
|
||||
|
||||
/// See [ProgressIndicator.valueColor].
|
||||
final Animation<Color?>? valueColor;
|
||||
|
||||
/// See [ProgressIndicator.semanticsLabel].
|
||||
final String? semanticsLabel;
|
||||
|
||||
/// See [ProgressIndicator.semanticsLabel].
|
||||
final String? semanticsValue;
|
||||
|
||||
/// Icon when [IndicatorResult.noMore].
|
||||
final Widget? noMoreIcon;
|
||||
|
||||
/// Show bezier background.
|
||||
final bool showBezierBackground;
|
||||
|
||||
/// Bezier background color.
|
||||
/// See [BezierBackground.color].
|
||||
final Color? bezierBackgroundColor;
|
||||
|
||||
/// Bezier background animation.
|
||||
/// See [BezierBackground.useAnimation].
|
||||
final bool bezierBackgroundAnimation;
|
||||
|
||||
/// Bezier background bounce.
|
||||
/// See [BezierBackground.bounce].
|
||||
final bool bezierBackgroundBounce;
|
||||
|
||||
final Widget child;
|
||||
|
||||
const MaterialHeader2({
|
||||
this.key,
|
||||
double triggerOffset = 100,
|
||||
bool clamping = true,
|
||||
IndicatorPosition position = IndicatorPosition.above,
|
||||
Duration processedDuration = const Duration(milliseconds: 200),
|
||||
physics.SpringDescription? spring,
|
||||
bool springRebound = false,
|
||||
SpringBuilder? readySpringBuilder,
|
||||
FrictionFactor? frictionFactor,
|
||||
bool safeArea = true,
|
||||
double? infiniteOffset,
|
||||
bool? hitOver,
|
||||
bool? infiniteHitOver,
|
||||
bool hapticFeedback = false,
|
||||
bool triggerWhenRelease = false,
|
||||
double maxOverOffset = double.infinity,
|
||||
required this.child,
|
||||
this.backgroundColor,
|
||||
this.color,
|
||||
this.valueColor,
|
||||
this.semanticsLabel,
|
||||
this.semanticsValue,
|
||||
this.noMoreIcon,
|
||||
this.showBezierBackground = false,
|
||||
this.bezierBackgroundColor,
|
||||
this.bezierBackgroundAnimation = false,
|
||||
this.bezierBackgroundBounce = false,
|
||||
}) : super(
|
||||
triggerOffset: triggerOffset,
|
||||
clamping: clamping,
|
||||
processedDuration: processedDuration,
|
||||
spring: spring,
|
||||
readySpringBuilder: readySpringBuilder ??
|
||||
(bezierBackgroundAnimation
|
||||
? kBezierSpringBuilder
|
||||
: kMaterialSpringBuilder),
|
||||
springRebound: springRebound,
|
||||
frictionFactor: frictionFactor ??
|
||||
(showBezierBackground
|
||||
? kBezierFrictionFactor
|
||||
: kMaterialFrictionFactor),
|
||||
horizontalFrictionFactor: frictionFactor ??
|
||||
(showBezierBackground
|
||||
? kBezierHorizontalFrictionFactor
|
||||
: kMaterialHorizontalFrictionFactor),
|
||||
safeArea: safeArea,
|
||||
infiniteOffset: infiniteOffset,
|
||||
hitOver: hitOver,
|
||||
infiniteHitOver: infiniteHitOver,
|
||||
position: position,
|
||||
hapticFeedback: hapticFeedback,
|
||||
triggerWhenRelease: triggerWhenRelease,
|
||||
maxOverOffset: maxOverOffset,
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, IndicatorState state) {
|
||||
return _MaterialIndicator(
|
||||
key: key,
|
||||
state: state,
|
||||
disappearDuration: processedDuration,
|
||||
reverse: state.reverse,
|
||||
backgroundColor: backgroundColor,
|
||||
color: color,
|
||||
valueColor: valueColor,
|
||||
semanticsLabel: semanticsLabel,
|
||||
semanticsValue: semanticsValue,
|
||||
noMoreIcon: noMoreIcon,
|
||||
showBezierBackground: showBezierBackground,
|
||||
bezierBackgroundColor: bezierBackgroundColor,
|
||||
bezierBackgroundAnimation: bezierBackgroundAnimation,
|
||||
bezierBackgroundBounce: bezierBackgroundBounce,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MaterialFooter2 extends Footer {
|
||||
final Key? key;
|
||||
|
||||
/// See [ProgressIndicator.backgroundColor].
|
||||
final Color? backgroundColor;
|
||||
|
||||
/// See [ProgressIndicator.color].
|
||||
final Color? color;
|
||||
|
||||
/// See [ProgressIndicator.valueColor].
|
||||
final Animation<Color?>? valueColor;
|
||||
|
||||
/// See [ProgressIndicator.semanticsLabel].
|
||||
final String? semanticsLabel;
|
||||
|
||||
/// See [ProgressIndicator.semanticsLabel].
|
||||
final String? semanticsValue;
|
||||
|
||||
/// Icon when [IndicatorResult.noMore].
|
||||
final Widget? noMoreIcon;
|
||||
|
||||
/// Show bezier background.
|
||||
final bool showBezierBackground;
|
||||
|
||||
/// Bezier background color.
|
||||
/// See [BezierBackground.color].
|
||||
final Color? bezierBackgroundColor;
|
||||
|
||||
/// Bezier background animation.
|
||||
/// See [BezierBackground.useAnimation].
|
||||
final bool bezierBackgroundAnimation;
|
||||
|
||||
/// Bezier background bounce.
|
||||
/// See [BezierBackground.bounce].
|
||||
final bool bezierBackgroundBounce;
|
||||
final Widget child;
|
||||
const MaterialFooter2({
|
||||
this.key,
|
||||
double triggerOffset = 100,
|
||||
bool clamping = true,
|
||||
IndicatorPosition position = IndicatorPosition.above,
|
||||
Duration processedDuration = const Duration(milliseconds: 200),
|
||||
physics.SpringDescription? spring,
|
||||
SpringBuilder? readySpringBuilder,
|
||||
bool springRebound = false,
|
||||
FrictionFactor? frictionFactor,
|
||||
bool safeArea = true,
|
||||
double? infiniteOffset,
|
||||
bool? hitOver,
|
||||
bool? infiniteHitOver,
|
||||
bool hapticFeedback = false,
|
||||
bool triggerWhenRelease = false,
|
||||
double maxOverOffset = double.infinity,
|
||||
required this.child,
|
||||
this.backgroundColor,
|
||||
this.color,
|
||||
this.valueColor,
|
||||
this.semanticsLabel,
|
||||
this.semanticsValue,
|
||||
this.noMoreIcon,
|
||||
this.showBezierBackground = false,
|
||||
this.bezierBackgroundColor,
|
||||
this.bezierBackgroundAnimation = false,
|
||||
this.bezierBackgroundBounce = false,
|
||||
}) : super(
|
||||
triggerOffset: triggerOffset,
|
||||
clamping: clamping,
|
||||
processedDuration: processedDuration,
|
||||
spring: spring,
|
||||
readySpringBuilder: readySpringBuilder ??
|
||||
(bezierBackgroundAnimation
|
||||
? kBezierSpringBuilder
|
||||
: kMaterialSpringBuilder),
|
||||
springRebound: springRebound,
|
||||
frictionFactor: frictionFactor ??
|
||||
(showBezierBackground
|
||||
? kBezierFrictionFactor
|
||||
: kMaterialFrictionFactor),
|
||||
horizontalFrictionFactor: frictionFactor ??
|
||||
(showBezierBackground
|
||||
? kBezierHorizontalFrictionFactor
|
||||
: kMaterialHorizontalFrictionFactor),
|
||||
safeArea: safeArea,
|
||||
infiniteOffset: infiniteOffset,
|
||||
hitOver: hitOver,
|
||||
infiniteHitOver: infiniteHitOver,
|
||||
position: position,
|
||||
hapticFeedback: hapticFeedback,
|
||||
triggerWhenRelease: triggerWhenRelease,
|
||||
maxOverOffset: maxOverOffset,
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, IndicatorState state) {
|
||||
return _MaterialIndicator(
|
||||
key: key,
|
||||
state: state,
|
||||
disappearDuration: processedDuration,
|
||||
reverse: !state.reverse,
|
||||
backgroundColor: backgroundColor,
|
||||
color: color,
|
||||
valueColor: valueColor,
|
||||
semanticsLabel: semanticsLabel,
|
||||
semanticsValue: semanticsValue,
|
||||
noMoreIcon: noMoreIcon,
|
||||
showBezierBackground: showBezierBackground,
|
||||
bezierBackgroundColor: bezierBackgroundColor,
|
||||
bezierBackgroundAnimation: bezierBackgroundAnimation,
|
||||
bezierBackgroundBounce: bezierBackgroundBounce,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Material indicator.
|
||||
/// Base widget for [MaterialHeader] and [MaterialFooter].
|
||||
class _MaterialIndicator extends StatefulWidget {
|
||||
/// Indicator properties and state.
|
||||
final IndicatorState state;
|
||||
|
||||
/// See [ProgressIndicator.backgroundColor].
|
||||
final Color? backgroundColor;
|
||||
|
||||
/// See [ProgressIndicator.color].
|
||||
final Color? color;
|
||||
|
||||
/// See [ProgressIndicator.valueColor].
|
||||
final Animation<Color?>? valueColor;
|
||||
|
||||
/// See [ProgressIndicator.semanticsLabel].
|
||||
final String? semanticsLabel;
|
||||
|
||||
/// See [ProgressIndicator.semanticsLabel].
|
||||
final String? semanticsValue;
|
||||
|
||||
/// Indicator disappears duration.
|
||||
/// When the mode is [IndicatorMode.processed].
|
||||
final Duration disappearDuration;
|
||||
|
||||
/// True for up and left.
|
||||
/// False for down and right.
|
||||
final bool reverse;
|
||||
|
||||
/// Icon when [IndicatorResult.noMore].
|
||||
final Widget? noMoreIcon;
|
||||
|
||||
/// Show bezier background.
|
||||
final bool showBezierBackground;
|
||||
|
||||
/// Bezier background color.
|
||||
/// See [BezierBackground.color].
|
||||
final Color? bezierBackgroundColor;
|
||||
|
||||
/// Bezier background animation.
|
||||
/// See [BezierBackground.useAnimation].
|
||||
final bool bezierBackgroundAnimation;
|
||||
|
||||
/// Bezier background bounce.
|
||||
/// See [BezierBackground.bounce].
|
||||
final bool bezierBackgroundBounce;
|
||||
|
||||
final Widget child;
|
||||
|
||||
const _MaterialIndicator({
|
||||
Key? key,
|
||||
required this.state,
|
||||
required this.disappearDuration,
|
||||
required this.reverse,
|
||||
required this.child,
|
||||
this.backgroundColor,
|
||||
this.color,
|
||||
this.valueColor,
|
||||
this.semanticsLabel,
|
||||
this.semanticsValue,
|
||||
this.noMoreIcon,
|
||||
this.showBezierBackground = false,
|
||||
this.bezierBackgroundColor,
|
||||
this.bezierBackgroundAnimation = false,
|
||||
this.bezierBackgroundBounce = false,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<_MaterialIndicator> createState() => _MaterialIndicatorState();
|
||||
}
|
||||
|
||||
/// See [ProgressIndicator] _kMinCircularProgressIndicatorSize.
|
||||
const double _kCircularProgressIndicatorSize = 48;
|
||||
|
||||
/// Friction factor used by material.
|
||||
double kMaterialFrictionFactor(double overscrollFraction) =>
|
||||
0.875 * math.pow(1 - overscrollFraction, 2);
|
||||
|
||||
/// Friction factor used by material horizontal.
|
||||
double kMaterialHorizontalFrictionFactor(double overscrollFraction) =>
|
||||
1.0 * math.pow(1 - overscrollFraction, 2);
|
||||
|
||||
/// Spring description used by material.
|
||||
physics.SpringDescription kMaterialSpringBuilder({
|
||||
required IndicatorMode mode,
|
||||
required double offset,
|
||||
required double actualTriggerOffset,
|
||||
required double velocity,
|
||||
}) =>
|
||||
physics.SpringDescription.withDampingRatio(
|
||||
mass: 1,
|
||||
stiffness: 500,
|
||||
ratio: 1.1,
|
||||
);
|
||||
|
||||
class _MaterialIndicatorState extends State<_MaterialIndicator> {
|
||||
IndicatorMode get _mode => widget.state.mode;
|
||||
|
||||
IndicatorResult get _result => widget.state.result;
|
||||
|
||||
Axis get _axis => widget.state.axis;
|
||||
|
||||
double get _offset => widget.state.offset;
|
||||
|
||||
double get _actualTriggerOffset => widget.state.actualTriggerOffset;
|
||||
|
||||
/// Build [RefreshProgressIndicator].
|
||||
Widget _buildIndicator() {
|
||||
return Container(
|
||||
alignment: _axis == Axis.vertical
|
||||
? (widget.reverse ? Alignment.topCenter : Alignment.bottomCenter)
|
||||
: (widget.reverse ? Alignment.centerLeft : Alignment.centerRight),
|
||||
height: _axis == Axis.vertical ? _actualTriggerOffset : double.infinity,
|
||||
width: _axis == Axis.horizontal ? _actualTriggerOffset : double.infinity,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
widget.child,
|
||||
if (_mode == IndicatorMode.inactive &&
|
||||
_result == IndicatorResult.noMore)
|
||||
widget.noMoreIcon ?? const Icon(Icons.inbox_outlined),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
double offset = _offset;
|
||||
if (widget.state.indicator.infiniteOffset != null &&
|
||||
widget.state.indicator.position == IndicatorPosition.locator &&
|
||||
(_mode != IndicatorMode.inactive ||
|
||||
_result == IndicatorResult.noMore)) {
|
||||
offset = _actualTriggerOffset;
|
||||
}
|
||||
final padding = math.max(_offset - _kCircularProgressIndicatorSize, 0) / 2;
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: _axis == Axis.vertical ? double.infinity : offset,
|
||||
height: _axis == Axis.horizontal ? double.infinity : offset,
|
||||
),
|
||||
if (widget.showBezierBackground)
|
||||
Positioned(
|
||||
top: _axis == Axis.vertical
|
||||
? widget.reverse
|
||||
? null
|
||||
: 0
|
||||
: 0,
|
||||
left: _axis == Axis.horizontal
|
||||
? widget.reverse
|
||||
? null
|
||||
: 0
|
||||
: 0,
|
||||
right: _axis == Axis.horizontal
|
||||
? widget.reverse
|
||||
? 0
|
||||
: null
|
||||
: 0,
|
||||
bottom: _axis == Axis.vertical
|
||||
? widget.reverse
|
||||
? 0
|
||||
: null
|
||||
: 0,
|
||||
child: BezierBackground(
|
||||
state: widget.state,
|
||||
color: widget.bezierBackgroundColor,
|
||||
useAnimation: widget.bezierBackgroundAnimation,
|
||||
bounce: widget.bezierBackgroundBounce,
|
||||
reverse: widget.reverse,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: _axis == Axis.vertical
|
||||
? widget.reverse
|
||||
? padding
|
||||
: null
|
||||
: 0,
|
||||
bottom: _axis == Axis.vertical
|
||||
? widget.reverse
|
||||
? null
|
||||
: padding
|
||||
: 0,
|
||||
left: _axis == Axis.horizontal
|
||||
? widget.reverse
|
||||
? padding
|
||||
: null
|
||||
: 0,
|
||||
right: _axis == Axis.horizontal
|
||||
? widget.reverse
|
||||
? null
|
||||
: padding
|
||||
: 0,
|
||||
child: Center(
|
||||
child: _buildIndicator(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
22
lib/widgets/keep_alive_wrapper.dart
Normal file
22
lib/widgets/keep_alive_wrapper.dart
Normal file
@@ -0,0 +1,22 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class KeepAliveWrapper extends StatefulWidget {
|
||||
final Widget child;
|
||||
|
||||
const KeepAliveWrapper({Key? key, required this.child}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<KeepAliveWrapper> createState() => _KeepAliveWrapperState();
|
||||
}
|
||||
|
||||
class _KeepAliveWrapperState extends State<KeepAliveWrapper>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
return widget.child;
|
||||
}
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true;
|
||||
}
|
||||
20
lib/widgets/loadding.dart
Normal file
20
lib/widgets/loadding.dart
Normal file
@@ -0,0 +1,20 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_dmzj/app/app_style.dart';
|
||||
|
||||
class LoaddingWidget extends StatelessWidget {
|
||||
const LoaddingWidget({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Container(
|
||||
padding: AppStyle.edgeInsetsA24,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).cardColor,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
69
lib/widgets/local_image.dart
Normal file
69
lib/widgets/local_image.dart
Normal file
@@ -0,0 +1,69 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class LocalImage extends StatelessWidget {
|
||||
final String path;
|
||||
final double? width;
|
||||
final double? height;
|
||||
final BoxFit? fit;
|
||||
final double borderRadius;
|
||||
final bool progress;
|
||||
const LocalImage(this.path,
|
||||
{this.width,
|
||||
this.height,
|
||||
this.fit = BoxFit.cover,
|
||||
this.borderRadius = 0,
|
||||
this.progress = false,
|
||||
Key? key})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (path.isEmpty) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.withOpacity(.1),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.image,
|
||||
color: Colors.grey,
|
||||
size: 24,
|
||||
),
|
||||
);
|
||||
}
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(borderRadius),
|
||||
child: FutureBuilder(
|
||||
future: File(path).readAsBytes(),
|
||||
builder: (_, snap) {
|
||||
if (snap.hasError) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.withOpacity(.1),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.broken_image,
|
||||
color: Colors.grey,
|
||||
size: 24,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (!snap.hasData) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
return Image.memory(
|
||||
snap.data as Uint8List,
|
||||
fit: fit,
|
||||
height: height,
|
||||
width: width,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
137
lib/widgets/net_image.dart
Normal file
137
lib/widgets/net_image.dart
Normal file
@@ -0,0 +1,137 @@
|
||||
import 'package:extended_image/extended_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_dmzj/app/app_style.dart';
|
||||
|
||||
class NetImage extends StatefulWidget {
|
||||
final String picUrl;
|
||||
final double? width;
|
||||
final double? height;
|
||||
final BoxFit? fit;
|
||||
final double borderRadius;
|
||||
final bool progress;
|
||||
const NetImage(this.picUrl,
|
||||
{this.width,
|
||||
this.height,
|
||||
this.fit = BoxFit.cover,
|
||||
this.borderRadius = 0,
|
||||
this.progress = false,
|
||||
Key? key})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<NetImage> createState() => _NetImageState();
|
||||
}
|
||||
|
||||
class _NetImageState extends State<NetImage>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late AnimationController animationController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
animationController = AnimationController(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 400),
|
||||
);
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
var picUrl = widget.picUrl;
|
||||
|
||||
if (picUrl.isEmpty) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.withOpacity(.1),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.image,
|
||||
color: Colors.grey,
|
||||
size: 24,
|
||||
),
|
||||
);
|
||||
}
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(widget.borderRadius),
|
||||
child: ExtendedImage.network(
|
||||
picUrl,
|
||||
fit: widget.fit,
|
||||
height: widget.height,
|
||||
width: widget.width,
|
||||
shape: BoxShape.rectangle,
|
||||
handleLoadingProgress: widget.progress,
|
||||
borderRadius: BorderRadius.circular(widget.borderRadius),
|
||||
headers: const {'Referer': "http://www.zaimanhua.com/"},
|
||||
loadStateChanged: (e) {
|
||||
if (e.extendedImageLoadState == LoadState.loading) {
|
||||
animationController.reset();
|
||||
final double? progress =
|
||||
e.loadingProgress?.expectedTotalBytes != null
|
||||
? e.loadingProgress!.cumulativeBytesLoaded /
|
||||
e.loadingProgress!.expectedTotalBytes!
|
||||
: null;
|
||||
if (widget.progress) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CircularProgressIndicator(
|
||||
value: progress,
|
||||
),
|
||||
AppStyle.vGap4,
|
||||
Text(
|
||||
'${((progress ?? 0.0) * 100).toInt()}%',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.withOpacity(.1),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.image,
|
||||
color: Colors.grey,
|
||||
size: 24,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (e.extendedImageLoadState == LoadState.failed) {
|
||||
animationController.reset();
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.withOpacity(.1),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.broken_image,
|
||||
color: Colors.grey,
|
||||
size: 24,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (e.extendedImageLoadState == LoadState.completed) {
|
||||
if (e.wasSynchronouslyLoaded) {
|
||||
return e.completedWidget;
|
||||
}
|
||||
animationController.forward();
|
||||
|
||||
return FadeTransition(
|
||||
opacity: animationController,
|
||||
child: e.completedWidget,
|
||||
);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
animationController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
83
lib/widgets/page_grid_view.dart
Normal file
83
lib/widgets/page_grid_view.dart
Normal file
@@ -0,0 +1,83 @@
|
||||
import 'package:easy_refresh/easy_refresh.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_dmzj/app/controller/base_controller.dart';
|
||||
import 'package:flutter_dmzj/widgets/status/app_empty_widget.dart';
|
||||
import 'package:flutter_dmzj/widgets/status/app_error_widget.dart';
|
||||
import 'package:flutter_dmzj/widgets/status/app_loadding_widget.dart';
|
||||
|
||||
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class PageGridView extends StatelessWidget {
|
||||
final BasePageController pageController;
|
||||
final IndexedWidgetBuilder itemBuilder;
|
||||
final EdgeInsets? padding;
|
||||
final bool firstRefresh;
|
||||
final Function()? onLoginSuccess;
|
||||
final bool showPageLoadding;
|
||||
final double crossAxisSpacing, mainAxisSpacing;
|
||||
final int crossAxisCount;
|
||||
final bool loadMore;
|
||||
const PageGridView({
|
||||
required this.itemBuilder,
|
||||
required this.pageController,
|
||||
this.padding,
|
||||
this.firstRefresh = false,
|
||||
this.showPageLoadding = false,
|
||||
this.onLoginSuccess,
|
||||
this.crossAxisSpacing = 0.0,
|
||||
this.mainAxisSpacing = 0.0,
|
||||
required this.crossAxisCount,
|
||||
this.loadMore = true,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Obx(
|
||||
() => Stack(
|
||||
children: [
|
||||
EasyRefresh(
|
||||
header: const MaterialHeader(),
|
||||
footer: loadMore
|
||||
? const MaterialFooter(
|
||||
clamping: false, infiniteOffset: 70, triggerOffset: 70)
|
||||
: null,
|
||||
controller: pageController.easyRefreshController,
|
||||
refreshOnStart: firstRefresh,
|
||||
onLoad: loadMore ? pageController.loadData : null,
|
||||
onRefresh: pageController.refreshData,
|
||||
child: MasonryGridView.count(
|
||||
padding: padding ?? EdgeInsets.zero,
|
||||
controller: pageController.scrollController,
|
||||
itemCount: pageController.list.length,
|
||||
itemBuilder: itemBuilder,
|
||||
crossAxisCount: crossAxisCount,
|
||||
crossAxisSpacing: crossAxisSpacing,
|
||||
mainAxisSpacing: mainAxisSpacing,
|
||||
),
|
||||
),
|
||||
Offstage(
|
||||
offstage: !pageController.pageEmpty.value,
|
||||
child: AppEmptyWidget(
|
||||
onRefresh: () => pageController.refreshData(),
|
||||
),
|
||||
),
|
||||
Offstage(
|
||||
offstage: !(showPageLoadding && pageController.pageLoadding.value),
|
||||
child: const AppLoaddingWidget(),
|
||||
),
|
||||
Offstage(
|
||||
offstage: !pageController.pageError.value,
|
||||
child: AppErrorWidget(
|
||||
errorMsg: pageController.errorMsg.value,
|
||||
error: pageController.error,
|
||||
onRefresh: () => pageController.refreshData(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
97
lib/widgets/page_list_view.dart
Normal file
97
lib/widgets/page_list_view.dart
Normal file
@@ -0,0 +1,97 @@
|
||||
import 'package:easy_refresh/easy_refresh.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_dmzj/app/controller/base_controller.dart';
|
||||
import 'package:flutter_dmzj/widgets/status/app_empty_widget.dart';
|
||||
import 'package:flutter_dmzj/widgets/status/app_error_widget.dart';
|
||||
import 'package:flutter_dmzj/widgets/status/app_loadding_widget.dart';
|
||||
|
||||
import 'package:get/get.dart';
|
||||
|
||||
typedef IndexedWidgetBuilder = Widget Function(BuildContext context, int index);
|
||||
|
||||
class PageListView extends StatelessWidget {
|
||||
final BasePageController pageController;
|
||||
final IndexedWidgetBuilder itemBuilder;
|
||||
final IndexedWidgetBuilder? separatorBuilder;
|
||||
final EdgeInsets? padding;
|
||||
final bool firstRefresh;
|
||||
final Function()? onLoginSuccess;
|
||||
final bool showPageLoadding;
|
||||
final bool loadMore;
|
||||
final Widget? header;
|
||||
const PageListView({
|
||||
required this.itemBuilder,
|
||||
required this.pageController,
|
||||
this.padding,
|
||||
this.firstRefresh = false,
|
||||
this.showPageLoadding = false,
|
||||
this.separatorBuilder,
|
||||
this.onLoginSuccess,
|
||||
this.loadMore = true,
|
||||
this.header,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Obx(
|
||||
() => Stack(
|
||||
children: [
|
||||
EasyRefresh(
|
||||
header: const MaterialHeader(),
|
||||
footer: loadMore
|
||||
? const MaterialFooter(
|
||||
clamping: false, infiniteOffset: 70, triggerOffset: 70)
|
||||
: null,
|
||||
controller: pageController.easyRefreshController,
|
||||
refreshOnStart: firstRefresh,
|
||||
onLoad: loadMore ? pageController.loadData : null,
|
||||
onRefresh: pageController.refreshData,
|
||||
child: ListView.separated(
|
||||
padding: padding ?? EdgeInsets.zero,
|
||||
controller: pageController.scrollController,
|
||||
itemCount: header == null
|
||||
? pageController.list.length
|
||||
: pageController.list.length + 1,
|
||||
itemBuilder: header == null
|
||||
? itemBuilder
|
||||
: (context, index) {
|
||||
if (index == 0) {
|
||||
return header;
|
||||
}
|
||||
return itemBuilder.call(context, index - 1);
|
||||
},
|
||||
separatorBuilder: header == null
|
||||
? (separatorBuilder ?? (context, i) => const SizedBox())
|
||||
: (context, index) {
|
||||
if (index == 0) {
|
||||
return const SizedBox();
|
||||
}
|
||||
return separatorBuilder?.call(context, index - 1) ??
|
||||
const SizedBox();
|
||||
},
|
||||
),
|
||||
),
|
||||
Offstage(
|
||||
offstage: !pageController.pageEmpty.value,
|
||||
child: AppEmptyWidget(
|
||||
onRefresh: () => pageController.refreshData(),
|
||||
),
|
||||
),
|
||||
Offstage(
|
||||
offstage: !(showPageLoadding && pageController.pageLoadding.value),
|
||||
child: const AppLoaddingWidget(),
|
||||
),
|
||||
Offstage(
|
||||
offstage: !pageController.pageError.value,
|
||||
child: AppErrorWidget(
|
||||
errorMsg: pageController.errorMsg.value,
|
||||
error: pageController.error,
|
||||
onRefresh: () => pageController.refreshData(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
65
lib/widgets/refresh_until_widget.dart
Normal file
65
lib/widgets/refresh_until_widget.dart
Normal file
@@ -0,0 +1,65 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_dmzj/app/app_style.dart';
|
||||
import 'package:remixicon/remixicon.dart';
|
||||
|
||||
/// 一个加载图标会旋转的加载按钮。加载图标([Remix.refresh_line])在左,文字([text])在
|
||||
/// 右。
|
||||
///
|
||||
/// 在点击widget时会在执行[onRefresh]函数的同时旋转加载图标。加载图标会一直旋转直到该函数
|
||||
/// 返还。
|
||||
///
|
||||
/// 加载图标会旋转不小于1秒的时间,即如果[onRefresh]函数在1秒之内执行完毕,加载图标会继续旋
|
||||
/// 转直到距离onRefresh函数开始执行已经过了1秒。
|
||||
class RefreshUntilWidget extends StatefulWidget {
|
||||
final Future Function() onRefresh;
|
||||
final String text;
|
||||
|
||||
const RefreshUntilWidget({
|
||||
super.key,
|
||||
required this.onRefresh,
|
||||
required this.text,
|
||||
});
|
||||
|
||||
@override
|
||||
State<RefreshUntilWidget> createState() => _RefreshUntilWidgetState();
|
||||
}
|
||||
|
||||
class _RefreshUntilWidgetState extends State<RefreshUntilWidget>
|
||||
with TickerProviderStateMixin {
|
||||
late final AnimationController _controller = AnimationController(
|
||||
duration: const Duration(seconds: 1),
|
||||
vsync: this,
|
||||
);
|
||||
late final Animation<double> _animation = CurvedAnimation(
|
||||
parent: _controller,
|
||||
curve: Curves.linear,
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
_controller.repeat();
|
||||
// 确保在网络很好的情况下,动画不会太快结束(至少1秒)
|
||||
await Future.wait([
|
||||
widget.onRefresh(),
|
||||
Future.delayed(const Duration(seconds: 1)),
|
||||
]);
|
||||
_controller.stop(canceled: false);
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
RotationTransition(
|
||||
turns: _animation,
|
||||
child: const Icon(Remix.refresh_line, size: 18, color: Colors.grey),
|
||||
),
|
||||
AppStyle.hGap4,
|
||||
Text(
|
||||
widget.text,
|
||||
style: const TextStyle(fontSize: 14, color: Colors.grey),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
52
lib/widgets/shadow_card.dart
Normal file
52
lib/widgets/shadow_card.dart
Normal file
@@ -0,0 +1,52 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_dmzj/app/app_style.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class ShadowCard extends StatelessWidget {
|
||||
final Widget child;
|
||||
final double radius;
|
||||
final Function()? onTap;
|
||||
final Function()? onLongPress;
|
||||
const ShadowCard({
|
||||
required this.child,
|
||||
this.radius = 8.0,
|
||||
this.onTap,
|
||||
this.onLongPress,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(radius),
|
||||
boxShadow: Get.isDarkMode
|
||||
? []
|
||||
: [
|
||||
BoxShadow(
|
||||
blurRadius: 4,
|
||||
color: Colors.grey.withOpacity(.2),
|
||||
)
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(radius),
|
||||
child: Material(
|
||||
color: Theme.of(context).cardColor,
|
||||
borderRadius: BorderRadius.circular(radius),
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(radius),
|
||||
onTap: onTap,
|
||||
onLongPress: onLongPress,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: AppStyle.radius8,
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
38
lib/widgets/status/app_empty_widget.dart
Normal file
38
lib/widgets/status/app_empty_widget.dart
Normal file
@@ -0,0 +1,38 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_dmzj/app/app_style.dart';
|
||||
import 'package:lottie/lottie.dart';
|
||||
|
||||
class AppEmptyWidget extends StatelessWidget {
|
||||
final Function()? onRefresh;
|
||||
const AppEmptyWidget({this.onRefresh, Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
onRefresh?.call();
|
||||
},
|
||||
child: Padding(
|
||||
padding: AppStyle.edgeInsetsA12,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
LottieBuilder.asset(
|
||||
'assets/lotties/empty.json',
|
||||
width: 200,
|
||||
height: 200,
|
||||
repeat: false,
|
||||
),
|
||||
const Text(
|
||||
"这里什么都没有",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
63
lib/widgets/status/app_error_widget.dart
Normal file
63
lib/widgets/status/app_error_widget.dart
Normal file
@@ -0,0 +1,63 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_dmzj/app/app_style.dart';
|
||||
import 'package:flutter_dmzj/app/utils.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import 'package:lottie/lottie.dart';
|
||||
|
||||
class AppErrorWidget extends StatelessWidget {
|
||||
final Function()? onRefresh;
|
||||
final String errorMsg;
|
||||
final Error? error;
|
||||
const AppErrorWidget(
|
||||
{this.errorMsg = "", this.onRefresh, this.error, Key? key})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
onRefresh?.call();
|
||||
},
|
||||
child: Padding(
|
||||
padding: AppStyle.edgeInsetsA12,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
LottieBuilder.asset(
|
||||
'assets/lotties/error.json',
|
||||
width: 260,
|
||||
repeat: false,
|
||||
),
|
||||
Text(
|
||||
"$errorMsg\r\n点击刷新",
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(fontSize: 12, color: Colors.grey),
|
||||
),
|
||||
Visibility(
|
||||
visible: error != null,
|
||||
child: Padding(
|
||||
padding: AppStyle.edgeInsetsT12,
|
||||
child: OutlinedButton(
|
||||
style: OutlinedButton.styleFrom(
|
||||
textStyle: Get.textTheme.bodySmall,
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
onPressed: () {
|
||||
Utils.copyText(
|
||||
"$errorMsg\n${error?.stackTrace?.toString()}");
|
||||
SmartDialog.showToast("已复制详细信息");
|
||||
},
|
||||
child: const Text("复制详细信息"),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
20
lib/widgets/status/app_loadding_widget.dart
Normal file
20
lib/widgets/status/app_loadding_widget.dart
Normal file
@@ -0,0 +1,20 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_dmzj/app/app_style.dart';
|
||||
import 'package:lottie/lottie.dart';
|
||||
|
||||
class AppLoaddingWidget extends StatelessWidget {
|
||||
const AppLoaddingWidget({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: AppStyle.edgeInsetsA12,
|
||||
child: LottieBuilder.asset(
|
||||
'assets/lotties/loadding.json',
|
||||
width: 200,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
61
lib/widgets/tab_appbar.dart
Normal file
61
lib/widgets/tab_appbar.dart
Normal file
@@ -0,0 +1,61 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_dmzj/app/app_style.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
class TabAppBar extends StatelessWidget implements PreferredSizeWidget {
|
||||
final List<Tab> tabs;
|
||||
final TabController? controller;
|
||||
final Widget? action;
|
||||
const TabAppBar({required this.tabs, this.controller, this.action, Key? key})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnnotatedRegion<SystemUiOverlayStyle>(
|
||||
value: Get.isDarkMode
|
||||
? SystemUiOverlayStyle.light.copyWith(
|
||||
systemNavigationBarColor: Colors.transparent,
|
||||
)
|
||||
: SystemUiOverlayStyle.dark.copyWith(
|
||||
systemNavigationBarColor: Colors.transparent,
|
||||
),
|
||||
child: Container(
|
||||
padding:
|
||||
EdgeInsets.only(top: MediaQuery.of(context).padding.top, right: 4),
|
||||
height: 56 + MediaQuery.of(context).padding.top,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TabBar(
|
||||
isScrollable: true,
|
||||
controller: controller,
|
||||
labelColor: Theme.of(context).colorScheme.primary,
|
||||
tabAlignment: TabAlignment.start,
|
||||
unselectedLabelColor:
|
||||
Get.isDarkMode ? Colors.white70 : Colors.black87,
|
||||
labelStyle: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
unselectedLabelStyle: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
labelPadding: AppStyle.edgeInsetsH12,
|
||||
indicatorSize: TabBarIndicatorSize.tab,
|
||||
indicatorColor: Colors.transparent,
|
||||
dividerColor: Colors.transparent,
|
||||
tabs: tabs,
|
||||
),
|
||||
),
|
||||
action ?? const SizedBox(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Size get preferredSize => Size.fromHeight(56 + AppStyle.statusBarHeight);
|
||||
}
|
||||
58
lib/widgets/user_photo.dart
Normal file
58
lib/widgets/user_photo.dart
Normal file
@@ -0,0 +1,58 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_dmzj/app/app_style.dart';
|
||||
import 'package:flutter_dmzj/widgets/net_image.dart';
|
||||
import 'package:remixicon/remixicon.dart';
|
||||
|
||||
class UserPhoto extends StatelessWidget {
|
||||
final String? url;
|
||||
final bool showBoder;
|
||||
final double size;
|
||||
const UserPhoto({
|
||||
this.url,
|
||||
this.showBoder = true,
|
||||
this.size = 48,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (url == null || (url?.isEmpty ?? true)) {
|
||||
return Container(
|
||||
width: size,
|
||||
height: size,
|
||||
decoration: BoxDecoration(
|
||||
border: showBoder
|
||||
? Border.all(
|
||||
color: Colors.grey.withOpacity(.2),
|
||||
)
|
||||
: null,
|
||||
color: Colors.grey.withOpacity(.2),
|
||||
borderRadius: AppStyle.radius32,
|
||||
),
|
||||
child: const Icon(
|
||||
Remix.user_fill,
|
||||
color: Colors.white,
|
||||
size: 24,
|
||||
),
|
||||
);
|
||||
}
|
||||
return Container(
|
||||
width: size,
|
||||
height: size,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(56),
|
||||
border: showBoder
|
||||
? Border.all(
|
||||
color: Colors.grey.withOpacity(.2),
|
||||
)
|
||||
: null,
|
||||
),
|
||||
child: NetImage(
|
||||
url!,
|
||||
width: size,
|
||||
height: size,
|
||||
borderRadius: size,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
144
lib/widgets/windows_tab_page.dart
Normal file
144
lib/widgets/windows_tab_page.dart
Normal file
@@ -0,0 +1,144 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart' as fluent;
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Windows平台专用的标签页容器,使用Fluent UI的TabView样式
|
||||
/// 替代移动端的Scaffold + TabAppBar + TabBarView组合
|
||||
class WindowsTabPage extends StatefulWidget {
|
||||
final List<WindowsTabItem> tabs;
|
||||
final int initialIndex;
|
||||
final Widget? headerAction;
|
||||
final ValueChanged<int>? onTabChanged;
|
||||
|
||||
const WindowsTabPage({
|
||||
Key? key,
|
||||
required this.tabs,
|
||||
this.initialIndex = 0,
|
||||
this.headerAction,
|
||||
this.onTabChanged,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<WindowsTabPage> createState() => _WindowsTabPageState();
|
||||
}
|
||||
|
||||
class _WindowsTabPageState extends State<WindowsTabPage> {
|
||||
late int _currentIndex;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_currentIndex = widget.initialIndex;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 使用maybeOf避免没有FluentTheme祖先时抛出异常
|
||||
final fluentTheme =
|
||||
fluent.FluentTheme.maybeOf(context) ?? fluent.FluentThemeData();
|
||||
final materialTheme = Theme.of(context);
|
||||
final isDark = materialTheme.brightness == Brightness.dark;
|
||||
final tabBarBg = isDark ? const Color(0xff202020) : const Color(0xfff0f0f0);
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Fluent 样式标签栏
|
||||
Container(
|
||||
color: tabBarBg,
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: List.generate(widget.tabs.length, (i) {
|
||||
final selected = i == _currentIndex;
|
||||
return _TabButton(
|
||||
label: widget.tabs[i].label,
|
||||
selected: selected,
|
||||
onTap: () {
|
||||
setState(() => _currentIndex = i);
|
||||
widget.onTabChanged?.call(i);
|
||||
},
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.headerAction != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: widget.headerAction!,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
// 分隔线
|
||||
Divider(
|
||||
height: 1,
|
||||
thickness: 1,
|
||||
color: materialTheme.dividerColor,
|
||||
),
|
||||
// 内容区
|
||||
Expanded(
|
||||
child: IndexedStack(
|
||||
index: _currentIndex,
|
||||
children: widget.tabs.map((t) => t.body).toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class WindowsTabItem {
|
||||
final String label;
|
||||
final Widget body;
|
||||
const WindowsTabItem({required this.label, required this.body});
|
||||
}
|
||||
|
||||
/// 单个标签按钮,Fluent Pivot样式
|
||||
class _TabButton extends StatelessWidget {
|
||||
final String label;
|
||||
final bool selected;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _TabButton({
|
||||
required this.label,
|
||||
required this.selected,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final fluentTheme =
|
||||
fluent.FluentTheme.maybeOf(context) ?? fluent.FluentThemeData();
|
||||
final accent = fluentTheme.accentColor;
|
||||
final textColor = selected
|
||||
? accent
|
||||
: fluentTheme.resources.textFillColorSecondary;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(
|
||||
color: selected ? accent : Colors.transparent,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: selected ? 18 : 15,
|
||||
fontWeight: selected ? FontWeight.bold : FontWeight.normal,
|
||||
color: textColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user