mirror of
https://github.com/StarCitizenToolBox/app.git
synced 2026-02-12 10:10:23 +00:00
feat: citizen news support
This commit is contained in:
@@ -10,12 +10,13 @@ class FlowNumberText extends HookConsumerWidget {
|
||||
final TextStyle? style;
|
||||
final Curve curve;
|
||||
|
||||
FlowNumberText(
|
||||
{super.key,
|
||||
required this.targetValue,
|
||||
this.duration = const Duration(seconds: 1),
|
||||
this.style,
|
||||
this.curve = Curves.bounceOut});
|
||||
FlowNumberText({
|
||||
super.key,
|
||||
required this.targetValue,
|
||||
this.duration = const Duration(seconds: 1),
|
||||
this.style,
|
||||
this.curve = Curves.bounceOut,
|
||||
});
|
||||
|
||||
final _formatter = NumberFormat.decimalPattern();
|
||||
|
||||
@@ -46,9 +47,6 @@ class FlowNumberText extends HookConsumerWidget {
|
||||
return timer.value?.cancel;
|
||||
}, [targetValue]);
|
||||
|
||||
return Text(
|
||||
_formatter.format(value.value.toInt()),
|
||||
style: style,
|
||||
);
|
||||
return Text(_formatter.format(value.value.toInt()), style: style);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,19 +20,14 @@ class GridItemAnimator extends HookWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// 创建动画控制器
|
||||
final animationController = useAnimationController(
|
||||
duration: duration,
|
||||
);
|
||||
final animationController = useAnimationController(duration: duration);
|
||||
|
||||
// 创建不透明度动画
|
||||
final opacityAnimation = useAnimation(
|
||||
Tween<double>(
|
||||
begin: 0.0, // 开始时完全透明
|
||||
end: 1.0, // 结束时完全不透明
|
||||
).animate(CurvedAnimation(
|
||||
parent: animationController,
|
||||
curve: Curves.easeOut,
|
||||
)),
|
||||
).animate(CurvedAnimation(parent: animationController, curve: Curves.easeOut)),
|
||||
);
|
||||
|
||||
// 创建位移动画
|
||||
@@ -40,23 +35,24 @@ class GridItemAnimator extends HookWidget {
|
||||
Tween<double>(
|
||||
begin: 1.0, // 开始位置
|
||||
end: 0.0, // 结束位置
|
||||
).animate(CurvedAnimation(
|
||||
parent: animationController,
|
||||
curve: Curves.easeOutCubic,
|
||||
)),
|
||||
).animate(CurvedAnimation(parent: animationController, curve: Curves.easeOutCubic)),
|
||||
);
|
||||
|
||||
// 组件挂载后启动动画
|
||||
useEffect(() {
|
||||
// 根据索引计算延迟时间,实现逐个条目入场
|
||||
final delay = delayPerItem * index;
|
||||
bool cancelled = false;
|
||||
|
||||
Future.delayed(delay, () {
|
||||
if (animationController.status != AnimationStatus.completed) {
|
||||
if (!cancelled && animationController.status != AnimationStatus.completed) {
|
||||
animationController.forward();
|
||||
}
|
||||
});
|
||||
return null;
|
||||
|
||||
return () {
|
||||
cancelled = true;
|
||||
};
|
||||
}, const []);
|
||||
|
||||
// 应用动画效果
|
||||
|
||||
115
lib/widgets/src/swiper.dart
Normal file
115
lib/widgets/src/swiper.dart
Normal file
@@ -0,0 +1,115 @@
|
||||
import 'package:card_swiper/card_swiper.dart';
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:flutter_tilt/flutter_tilt.dart';
|
||||
|
||||
class HoverSwiper extends HookWidget {
|
||||
const HoverSwiper({
|
||||
super.key,
|
||||
required this.itemCount,
|
||||
required this.itemBuilder,
|
||||
this.autoplayDelay = 3000,
|
||||
this.paginationActiveSize = 8.0,
|
||||
this.controlSize = 24,
|
||||
this.controlPadding = const EdgeInsets.symmetric(horizontal: 8, vertical: 0),
|
||||
});
|
||||
|
||||
final int itemCount;
|
||||
final IndexedWidgetBuilder itemBuilder;
|
||||
final double paginationActiveSize;
|
||||
final double controlSize;
|
||||
final EdgeInsets controlPadding;
|
||||
final int autoplayDelay;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isHovered = useState(false);
|
||||
final controller = useMemoized(() => SwiperController());
|
||||
|
||||
useEffect(() {
|
||||
return controller.dispose;
|
||||
}, [controller]);
|
||||
|
||||
return MouseRegion(
|
||||
onEnter: (_) {
|
||||
isHovered.value = true;
|
||||
controller.stopAutoplay();
|
||||
},
|
||||
onExit: (_) {
|
||||
isHovered.value = false;
|
||||
controller.startAutoplay();
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
Tilt(
|
||||
shadowConfig: const ShadowConfig(maxIntensity: .3),
|
||||
borderRadius: const BorderRadius.only(topLeft: Radius.circular(12), topRight: Radius.circular(12)),
|
||||
child: Swiper(
|
||||
controller: controller,
|
||||
itemCount: itemCount,
|
||||
itemBuilder: itemBuilder,
|
||||
autoplay: true,
|
||||
autoplayDelay: autoplayDelay,
|
||||
),
|
||||
),
|
||||
// Left control button
|
||||
_buildControlButton(
|
||||
isHovered: isHovered.value,
|
||||
position: 'left',
|
||||
onTap: () => controller.previous(),
|
||||
icon: FluentIcons.chevron_left,
|
||||
),
|
||||
// Right control button
|
||||
_buildControlButton(
|
||||
isHovered: isHovered.value,
|
||||
position: 'right',
|
||||
onTap: () => controller.next(),
|
||||
icon: FluentIcons.chevron_right,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建控制按钮(左/右箭头)
|
||||
Widget _buildControlButton({
|
||||
required bool isHovered,
|
||||
required String position,
|
||||
required VoidCallback onTap,
|
||||
required IconData icon,
|
||||
}) {
|
||||
final isLeft = position == 'left';
|
||||
return Positioned(
|
||||
left: isLeft ? 0 : null,
|
||||
right: isLeft ? null : 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
child: AnimatedOpacity(
|
||||
opacity: isHovered ? 1.0 : 0.0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: IgnorePointer(
|
||||
ignoring: !isHovered,
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: controlPadding,
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(2),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: .3),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Icon(icon, size: controlSize, color: Colors.white.withValues(alpha: .8)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -17,53 +17,42 @@ export 'src/cache_image.dart';
|
||||
export 'src/countdown_time_text.dart';
|
||||
export 'src/cache_svg_image.dart';
|
||||
export 'src/grid_item_animator.dart';
|
||||
export 'src/swiper.dart';
|
||||
|
||||
export '../common/utils/async.dart';
|
||||
export '../common/utils/base_utils.dart';
|
||||
export 'package:starcitizen_doctor/generated/l10n.dart';
|
||||
|
||||
Widget makeLoading(
|
||||
BuildContext context, {
|
||||
double? width,
|
||||
}) {
|
||||
Widget makeLoading(BuildContext context, {double? width}) {
|
||||
width ??= 30;
|
||||
return Center(
|
||||
child: SizedBox(
|
||||
width: width,
|
||||
height: width,
|
||||
child: const ProgressRing(),
|
||||
),
|
||||
child: SizedBox(width: width, height: width, child: const ProgressRing()),
|
||||
);
|
||||
}
|
||||
|
||||
Widget makeDefaultPage(BuildContext context,
|
||||
{Widget? titleRow,
|
||||
List<Widget>? actions,
|
||||
Widget? content,
|
||||
bool automaticallyImplyLeading = true,
|
||||
String title = "",
|
||||
bool useBodyContainer = false}) {
|
||||
Widget makeDefaultPage(
|
||||
BuildContext context, {
|
||||
Widget? titleRow,
|
||||
List<Widget>? actions,
|
||||
Widget? content,
|
||||
bool automaticallyImplyLeading = true,
|
||||
String title = "",
|
||||
bool useBodyContainer = false,
|
||||
}) {
|
||||
return NavigationView(
|
||||
appBar: NavigationAppBar(
|
||||
automaticallyImplyLeading: automaticallyImplyLeading,
|
||||
title: DragToMoveArea(
|
||||
child: titleRow ??
|
||||
Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Text(title),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [...?actions, const WindowButtons()],
|
||||
)),
|
||||
automaticallyImplyLeading: automaticallyImplyLeading,
|
||||
title: DragToMoveArea(
|
||||
child:
|
||||
titleRow ??
|
||||
Column(
|
||||
children: [
|
||||
Expanded(child: Row(children: [Text(title)])),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: Row(mainAxisAlignment: MainAxisAlignment.end, children: [...?actions, const WindowButtons()]),
|
||||
),
|
||||
content: useBodyContainer
|
||||
? Container(
|
||||
decoration: BoxDecoration(
|
||||
@@ -85,55 +74,63 @@ class WindowButtons extends StatelessWidget {
|
||||
return SizedBox(
|
||||
width: 138,
|
||||
height: 50,
|
||||
child: WindowCaption(
|
||||
brightness: theme.brightness,
|
||||
backgroundColor: Colors.transparent,
|
||||
),
|
||||
child: WindowCaption(brightness: theme.brightness, backgroundColor: Colors.transparent),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
List<Widget> makeMarkdownView(String description, {String? attachmentsUrl}) {
|
||||
return MarkdownGenerator().buildWidgets(description,
|
||||
config: MarkdownConfig(configs: [
|
||||
LinkConfig(onTap: (url) {
|
||||
if (url.startsWith("/") && attachmentsUrl != null) {
|
||||
url = "$attachmentsUrl/$url";
|
||||
}
|
||||
launchUrlString(url);
|
||||
}),
|
||||
ImgConfig(builder: (String url, Map<String, String> attributes) {
|
||||
if (url.startsWith("/") && attachmentsUrl != null) {
|
||||
url = "$attachmentsUrl/$url";
|
||||
}
|
||||
return ExtendedImage.network(
|
||||
url,
|
||||
loadStateChanged: (ExtendedImageState state) {
|
||||
switch (state.extendedImageLoadState) {
|
||||
case LoadState.loading:
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
children: [
|
||||
const ProgressRing(),
|
||||
const SizedBox(height: 12),
|
||||
Text(S.current.app_common_loading_images)
|
||||
],
|
||||
return MarkdownGenerator().buildWidgets(
|
||||
description,
|
||||
config: MarkdownConfig(
|
||||
configs: [
|
||||
LinkConfig(
|
||||
onTap: (url) {
|
||||
if (url.startsWith("/") && attachmentsUrl != null) {
|
||||
url = "$attachmentsUrl/$url";
|
||||
}
|
||||
launchUrlString(url);
|
||||
},
|
||||
),
|
||||
ImgConfig(
|
||||
builder: (String url, Map<String, String> attributes) {
|
||||
if (url.startsWith("/") && attachmentsUrl != null) {
|
||||
url = "$attachmentsUrl/$url";
|
||||
}
|
||||
return ExtendedImage.network(
|
||||
url,
|
||||
loadStateChanged: (ExtendedImageState state) {
|
||||
switch (state.extendedImageLoadState) {
|
||||
case LoadState.loading:
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
children: [
|
||||
const ProgressRing(),
|
||||
const SizedBox(height: 12),
|
||||
Text(S.current.app_common_loading_images),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
case LoadState.completed:
|
||||
return ExtendedRawImage(
|
||||
image: state.extendedImageInfo?.image,
|
||||
);
|
||||
case LoadState.failed:
|
||||
return Text("Loading Image error $url");
|
||||
}
|
||||
},
|
||||
);
|
||||
})
|
||||
]));
|
||||
);
|
||||
case LoadState.completed:
|
||||
return ExtendedRawImage(image: state.extendedImageInfo?.image);
|
||||
case LoadState.failed:
|
||||
return Button(
|
||||
onPressed: () {
|
||||
launchUrlString(url);
|
||||
},
|
||||
child: Text("Loading Image error $url"),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
ColorFilter makeSvgColor(Color color) {
|
||||
@@ -142,22 +139,20 @@ ColorFilter makeSvgColor(Color color) {
|
||||
|
||||
CustomTransitionPage<T> myPageBuilder<T>(BuildContext context, GoRouterState state, Widget child) {
|
||||
return CustomTransitionPage(
|
||||
child: child,
|
||||
transitionDuration: const Duration(milliseconds: 150),
|
||||
reverseTransitionDuration: const Duration(milliseconds: 150),
|
||||
transitionsBuilder:
|
||||
(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
|
||||
return SlideTransition(
|
||||
position: Tween<Offset>(
|
||||
begin: const Offset(0.0, 1.0),
|
||||
end: const Offset(0.0, 0.0),
|
||||
).animate(CurvedAnimation(
|
||||
parent: animation,
|
||||
curve: Curves.easeInOut,
|
||||
)),
|
||||
child: child,
|
||||
);
|
||||
});
|
||||
child: child,
|
||||
transitionDuration: const Duration(milliseconds: 150),
|
||||
reverseTransitionDuration: const Duration(milliseconds: 150),
|
||||
transitionsBuilder:
|
||||
(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
|
||||
return SlideTransition(
|
||||
position: Tween<Offset>(
|
||||
begin: const Offset(0.0, 1.0),
|
||||
end: const Offset(0.0, 0.0),
|
||||
).animate(CurvedAnimation(parent: animation, curve: Curves.easeInOut)),
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
class LoadingWidget<T> extends HookConsumerWidget {
|
||||
@@ -192,9 +187,7 @@ class LoadingWidget<T> extends HookConsumerWidget {
|
||||
onPressed: () {
|
||||
_loadData(dataState, errorMsg);
|
||||
},
|
||||
child: Center(
|
||||
child: Text(errorMsg.value),
|
||||
),
|
||||
child: Center(child: Text(errorMsg.value)),
|
||||
);
|
||||
}
|
||||
if (dataState.value == null && data == null) return makeLoading(context);
|
||||
|
||||
Reference in New Issue
Block a user