feat: 闲言APP v0.9.1 — 编辑器全面重写 + 迷你编辑器
- 标准编辑器: freezed数据模型 + 5Tab工具栏 + 多图层管理 + 文字增强 + 背景系统 + 导出链路 - 迷你编辑器: 极简6项功能(文字/字号/颜色/背景/预览/导出) + 三种调用方式(全屏/半屏/内嵌) - 共享组件: GlassSlider/ColorPicker/FontPicker/TipsView - 服务层: ExportService/ImageImportService/FontService/XycardService - 设计系统: 统一主题令牌 + Liquid Glass风格
This commit is contained in:
9
lib/shared/extensions/context_extensions.dart
Normal file
9
lib/shared/extensions/context_extensions.dart
Normal file
@@ -0,0 +1,9 @@
|
||||
/// ============================================================
|
||||
/// 闲言APP — 上下文扩展
|
||||
/// 创建时间: 2026-04-20
|
||||
/// 更新时间: 2026-04-20
|
||||
/// 作用: 主题扩展快捷访问
|
||||
/// 上次更新: 初始创建
|
||||
/// ============================================================
|
||||
|
||||
export '../../core/utils/extensions.dart';
|
||||
107
lib/shared/widgets/bottom_sheet.dart
Normal file
107
lib/shared/widgets/bottom_sheet.dart
Normal file
@@ -0,0 +1,107 @@
|
||||
/// ============================================================
|
||||
/// 闲言APP — 底部弹窗组件
|
||||
/// 创建时间: 2026-04-20
|
||||
/// 更新时间: 2026-04-20
|
||||
/// 作用: iOS 26 液态玻璃底部面板 (GlassActionSheet + GlassSheet)
|
||||
/// 上次更新: 集成 stupid_simple_sheet + liquid_glass_widgets
|
||||
/// ============================================================
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:liquid_glass_widgets/liquid_glass_widgets.dart';
|
||||
import 'package:stupid_simple_sheet/stupid_simple_sheet.dart';
|
||||
|
||||
import '../../core/utils/page_transitions.dart';
|
||||
|
||||
/// 底部面板选项
|
||||
class BottomSheetOption {
|
||||
const BottomSheetOption({
|
||||
required this.title,
|
||||
this.icon,
|
||||
this.isDestructive = false,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final IconData? icon;
|
||||
final bool isDestructive;
|
||||
final VoidCallback? onTap;
|
||||
}
|
||||
|
||||
/// iOS 26 液态玻璃底部面板
|
||||
///
|
||||
/// 两种模式:
|
||||
/// 1. ActionSheet — 简单选项列表 (使用 GlassActionSheet)
|
||||
/// 2. GlassSheet — 自定义内容面板 (使用 StupidSimpleGlassSheetRoute)
|
||||
class AppBottomSheet {
|
||||
AppBottomSheet._();
|
||||
|
||||
/// 显示 ActionSheet 选项面板
|
||||
static Future<T?> showActions<T>({
|
||||
required BuildContext context,
|
||||
String? title,
|
||||
required List<BottomSheetOption> options,
|
||||
bool showCancel = true,
|
||||
}) {
|
||||
final actions = options.map((opt) {
|
||||
return GlassActionSheetAction(
|
||||
label: opt.title,
|
||||
icon: opt.icon != null ? Icon(opt.icon, size: 20) : null,
|
||||
style: opt.isDestructive
|
||||
? GlassActionSheetStyle.destructive
|
||||
: GlassActionSheetStyle.defaultStyle,
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
opt.onTap?.call();
|
||||
},
|
||||
);
|
||||
}).toList();
|
||||
|
||||
return showGlassActionSheet<T>(
|
||||
context: context,
|
||||
title: title,
|
||||
actions: actions,
|
||||
cancelLabel: '取消',
|
||||
showCancelButton: showCancel,
|
||||
);
|
||||
}
|
||||
|
||||
/// 显示液态玻璃自定义内容面板
|
||||
///
|
||||
/// 使用 StupidSimpleGlassSheetRoute,支持:
|
||||
/// - 弹簧动画 (CupertinoMotion)
|
||||
/// - 拖拽关闭
|
||||
/// - 背景模糊
|
||||
/// - 多级吸附 (SheetSnappingConfig)
|
||||
static Future<T?> showCustom<T>({
|
||||
required BuildContext context,
|
||||
required WidgetBuilder builder,
|
||||
SheetSnappingConfig snappingConfig = SheetSnappingConfig.full,
|
||||
bool blurBehindBarrier = true,
|
||||
}) {
|
||||
return showGlassSheet<T>(
|
||||
context: context,
|
||||
builder: builder,
|
||||
snappingConfig: snappingConfig,
|
||||
blurBehindBarrier: blurBehindBarrier,
|
||||
);
|
||||
}
|
||||
|
||||
/// 显示半屏面板 (detent 风格)
|
||||
///
|
||||
/// iOS 26 风格半屏 Sheet,可拖拽到不同高度。
|
||||
static Future<T?> showHalf<T>({
|
||||
required BuildContext context,
|
||||
required WidgetBuilder builder,
|
||||
}) {
|
||||
return showGlassSheet<T>(
|
||||
context: context,
|
||||
builder: builder,
|
||||
snappingConfig: const SheetSnappingConfig([
|
||||
0.4,
|
||||
0.7,
|
||||
1.0,
|
||||
], initialSnap: 0.7),
|
||||
);
|
||||
}
|
||||
}
|
||||
140
lib/shared/widgets/empty_state.dart
Normal file
140
lib/shared/widgets/empty_state.dart
Normal file
@@ -0,0 +1,140 @@
|
||||
/// ============================================================
|
||||
/// 闲言APP — 空状态组件
|
||||
/// 创建时间: 2026-04-20
|
||||
/// 更新时间: 2026-04-20
|
||||
/// 作用: 统一空数据/错误状态展示,Lottie 动画增强
|
||||
/// 上次更新: 集成 Lottie 动画替代 Emoji
|
||||
/// ============================================================
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:lottie/lottie.dart';
|
||||
|
||||
import '../../core/theme/app_theme.dart';
|
||||
import '../../core/theme/app_spacing.dart';
|
||||
import '../../core/theme/app_typography.dart';
|
||||
|
||||
/// 空状态类型
|
||||
enum EmptyType {
|
||||
noData(
|
||||
'📭',
|
||||
'暂无内容',
|
||||
'快去发现精彩句子吧',
|
||||
'https://lottie.host/4db68bbd-31f6-4cd8-84eb-189de081159a/IGmMCqhzpt.json',
|
||||
),
|
||||
noResult(
|
||||
'🔍',
|
||||
'未找到结果',
|
||||
'换个关键词试试',
|
||||
'https://lottie.host/f0b30f68-c9bd-4b13-9c8e-67b51c6c7e94/7O0CIgEsLQ.json',
|
||||
),
|
||||
networkError(
|
||||
'📡',
|
||||
'网络不给力',
|
||||
'请检查网络连接后重试',
|
||||
'https://lottie.host/a1d0e447-8a7d-4f69-8855-7961b8e6c5d2/dZz4Qq9pYq.json',
|
||||
),
|
||||
noFavorite(
|
||||
'💕',
|
||||
'暂无收藏',
|
||||
'遇到喜欢的句子就收藏吧',
|
||||
'https://lottie.host/4db68bbd-31f6-4cd8-84eb-189de081159a/IGmMCqhzpt.json',
|
||||
),
|
||||
noHistory(
|
||||
'📖',
|
||||
'暂无阅读记录',
|
||||
'开始你的阅读之旅吧',
|
||||
'https://lottie.host/4db68bbd-31f6-4cd8-84eb-189de081159a/IGmMCqhzpt.json',
|
||||
),
|
||||
error(
|
||||
'⚠️',
|
||||
'出了点问题',
|
||||
'请稍后重试',
|
||||
'https://lottie.host/a1d0e447-8a7d-4f69-8855-7961b8e6c5d2/dZz4Qq9pYq.json',
|
||||
);
|
||||
|
||||
const EmptyType(this.emoji, this.title, this.subtitle, this.lottieUrl);
|
||||
|
||||
final String emoji;
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final String lottieUrl;
|
||||
}
|
||||
|
||||
/// 空状态组件
|
||||
///
|
||||
/// 优先展示 Lottie 动画,加载失败时回退到 Emoji。
|
||||
class EmptyState extends StatelessWidget {
|
||||
const EmptyState({
|
||||
super.key,
|
||||
required this.type,
|
||||
this.onRetry,
|
||||
this.actionLabel,
|
||||
this.onAction,
|
||||
});
|
||||
|
||||
final EmptyType type;
|
||||
final VoidCallback? onRetry;
|
||||
final String? actionLabel;
|
||||
final VoidCallback? onAction;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ext = AppTheme.ext(context);
|
||||
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.xl,
|
||||
vertical: AppSpacing.xxl,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 160,
|
||||
height: 160,
|
||||
child: Lottie.network(
|
||||
type.lottieUrl,
|
||||
width: 160,
|
||||
height: 160,
|
||||
fit: BoxFit.contain,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Text(
|
||||
type.emoji,
|
||||
style: const TextStyle(fontSize: 64),
|
||||
);
|
||||
},
|
||||
frameRate: FrameRate(30),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
Text(
|
||||
type.title,
|
||||
style: AppTypography.title3.copyWith(color: ext.textPrimary),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.xs),
|
||||
Text(
|
||||
type.subtitle,
|
||||
style: AppTypography.subhead.copyWith(color: ext.textSecondary),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (onRetry != null || onAction != null) ...[
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
CupertinoButton(
|
||||
onPressed: onAction ?? onRetry,
|
||||
child: Text(
|
||||
actionLabel ?? '重试',
|
||||
style: AppTypography.callout.copyWith(
|
||||
color: ext.isDark
|
||||
? CupertinoColors.activeBlue.darkColor
|
||||
: CupertinoColors.activeBlue.color,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
120
lib/shared/widgets/glass_container.dart
Normal file
120
lib/shared/widgets/glass_container.dart
Normal file
@@ -0,0 +1,120 @@
|
||||
/// ============================================================
|
||||
/// 闲言APP — Liquid Glass 容器组件 (代理层)
|
||||
/// 创建时间: 2026-04-20
|
||||
/// 更新时间: 2026-04-20
|
||||
/// 作用: iOS 26 液态玻璃容器代理,桥接旧 API → liquid_glass_widgets
|
||||
/// 上次更新: 使用 as 前缀避免类名冲突
|
||||
/// ============================================================
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:liquid_glass_widgets/liquid_glass_widgets.dart' as lgw;
|
||||
|
||||
import '../../core/theme/app_theme.dart';
|
||||
import '../../core/theme/app_radius.dart';
|
||||
import '../../core/theme/app_spacing.dart';
|
||||
|
||||
/// Glass 深度层级 (兼容旧 API)
|
||||
enum GlassDepth { base, elevated, prominent, alert }
|
||||
|
||||
/// Liquid Glass 容器
|
||||
///
|
||||
/// 代理层:将旧 GlassContainer API 映射到 liquid_glass_widgets 的
|
||||
/// GlassContainer。支持动态主题,自动跟随 GlassTheme 明暗切换。
|
||||
class GlassContainer extends StatelessWidget {
|
||||
const GlassContainer({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.depth = GlassDepth.base,
|
||||
this.borderRadius,
|
||||
this.padding,
|
||||
this.margin,
|
||||
this.width,
|
||||
this.height,
|
||||
this.borderColor,
|
||||
this.showBorder = true,
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
final GlassDepth depth;
|
||||
final BorderRadius? borderRadius;
|
||||
final EdgeInsetsGeometry? padding;
|
||||
final EdgeInsetsGeometry? margin;
|
||||
final double? width;
|
||||
final double? height;
|
||||
final Color? borderColor;
|
||||
final bool showBorder;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final effectiveRadius = borderRadius?.topLeft.x ?? AppRadius.lg;
|
||||
final effectivePadding = padding ?? const EdgeInsets.all(AppSpacing.md);
|
||||
|
||||
final glassChild = Padding(padding: effectivePadding, child: child);
|
||||
|
||||
final quality = _qualityForDepth(depth);
|
||||
final settings = _settingsForDepth(depth, context);
|
||||
|
||||
return lgw.GlassContainer(
|
||||
width: width,
|
||||
height: height,
|
||||
padding: EdgeInsets.zero,
|
||||
margin: margin,
|
||||
shape: lgw.LiquidRoundedSuperellipse(borderRadius: effectiveRadius),
|
||||
quality: quality,
|
||||
settings: settings,
|
||||
useOwnLayer: depth == GlassDepth.alert,
|
||||
child: glassChild,
|
||||
);
|
||||
}
|
||||
|
||||
lgw.GlassQuality _qualityForDepth(GlassDepth depth) {
|
||||
switch (depth) {
|
||||
case GlassDepth.base:
|
||||
return lgw.GlassQuality.standard;
|
||||
case GlassDepth.elevated:
|
||||
return lgw.GlassQuality.standard;
|
||||
case GlassDepth.prominent:
|
||||
return lgw.GlassQuality.premium;
|
||||
case GlassDepth.alert:
|
||||
return lgw.GlassQuality.premium;
|
||||
}
|
||||
}
|
||||
|
||||
lgw.LiquidGlassSettings? _settingsForDepth(
|
||||
GlassDepth depth,
|
||||
BuildContext context,
|
||||
) {
|
||||
final isDark = AppTheme.ext(context).isDark;
|
||||
|
||||
switch (depth) {
|
||||
case GlassDepth.base:
|
||||
return lgw.LiquidGlassSettings(
|
||||
thickness: isDark ? 25 : 20,
|
||||
blur: isDark ? 4 : 2,
|
||||
lightIntensity: isDark ? 1.0 : 0.8,
|
||||
refractiveIndex: 1.5,
|
||||
);
|
||||
case GlassDepth.elevated:
|
||||
return lgw.LiquidGlassSettings(
|
||||
thickness: isDark ? 30 : 25,
|
||||
blur: isDark ? 5 : 3,
|
||||
lightIntensity: isDark ? 1.2 : 1.0,
|
||||
refractiveIndex: 1.55,
|
||||
);
|
||||
case GlassDepth.prominent:
|
||||
return lgw.LiquidGlassSettings(
|
||||
thickness: isDark ? 40 : 30,
|
||||
blur: isDark ? 6 : 4,
|
||||
lightIntensity: isDark ? 1.5 : 1.2,
|
||||
refractiveIndex: 1.6,
|
||||
);
|
||||
case GlassDepth.alert:
|
||||
return lgw.LiquidGlassSettings(
|
||||
thickness: isDark ? 50 : 35,
|
||||
blur: isDark ? 8 : 5,
|
||||
lightIntensity: isDark ? 1.8 : 1.4,
|
||||
refractiveIndex: 1.65,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
145
lib/shared/widgets/skeleton.dart
Normal file
145
lib/shared/widgets/skeleton.dart
Normal file
@@ -0,0 +1,145 @@
|
||||
/// ============================================================
|
||||
/// 闲言APP — 骨架屏组件
|
||||
/// 创建时间: 2026-04-20
|
||||
/// 更新时间: 2026-04-20
|
||||
/// 作用: Shimmer 骨架屏占位,统一加载态
|
||||
/// 上次更新: 初始创建
|
||||
/// ============================================================
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shimmer/shimmer.dart';
|
||||
|
||||
import '../../core/theme/app_theme.dart';
|
||||
import '../../core/theme/app_radius.dart';
|
||||
import '../../core/theme/app_spacing.dart';
|
||||
|
||||
/// 骨架屏容器
|
||||
///
|
||||
/// 包裹子组件,为其添加 Shimmer 闪烁效果。
|
||||
class SkeletonBox extends StatelessWidget {
|
||||
const SkeletonBox({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.enabled = true,
|
||||
});
|
||||
|
||||
/// 子组件 (通常是灰色占位块)
|
||||
final Widget child;
|
||||
|
||||
/// 是否启用 (false 时直接显示子组件)
|
||||
final bool enabled;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!enabled) return child;
|
||||
|
||||
final ext = AppTheme.ext(context);
|
||||
final baseColor = ext.isDark
|
||||
? const Color(0xFF3A3A5C)
|
||||
: const Color(0xFFE0E0E0);
|
||||
final highlightColor = ext.isDark
|
||||
? const Color(0xFF4A4A6C)
|
||||
: const Color(0xFFF5F5F5);
|
||||
|
||||
return Shimmer.fromColors(
|
||||
baseColor: baseColor,
|
||||
highlightColor: highlightColor,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 句子卡片骨架
|
||||
class SentenceCardSkeleton extends StatelessWidget {
|
||||
const SentenceCardSkeleton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const SkeletonBox(
|
||||
child: _SkeletonCard(
|
||||
lines: 3,
|
||||
showAvatar: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 列表项骨架
|
||||
class ListItemSkeleton extends StatelessWidget {
|
||||
const ListItemSkeleton({super.key, this.lines = 2});
|
||||
|
||||
final int lines;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SkeletonBox(child: _SkeletonCard(lines: lines));
|
||||
}
|
||||
}
|
||||
|
||||
/// 通用骨架卡片
|
||||
class _SkeletonCard extends StatelessWidget {
|
||||
const _SkeletonCard({
|
||||
required this.lines,
|
||||
this.showAvatar = false,
|
||||
});
|
||||
|
||||
final int lines;
|
||||
final bool showAvatar;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ext = AppTheme.ext(context);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(AppSpacing.md),
|
||||
decoration: BoxDecoration(
|
||||
color: ext.bgCard,
|
||||
borderRadius: AppRadius.lgBorder,
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (showAvatar) ...[
|
||||
_buildCircle(40, ext),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
],
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
for (int i = 0; i < lines; i++) ...[
|
||||
_buildLine(i == lines - 1 ? 0.6 : 1.0, ext),
|
||||
if (i < lines - 1) const SizedBox(height: AppSpacing.sm),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLine(double widthFactor, AppThemeExtension ext) {
|
||||
return FractionallySizedBox(
|
||||
widthFactor: widthFactor,
|
||||
child: Container(
|
||||
height: 14,
|
||||
decoration: BoxDecoration(
|
||||
color: ext.textHint.withValues(alpha: 0.3),
|
||||
borderRadius: AppRadius.smBorder,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCircle(double size, AppThemeExtension ext) {
|
||||
return Container(
|
||||
width: size,
|
||||
height: size,
|
||||
decoration: BoxDecoration(
|
||||
color: ext.textHint.withValues(alpha: 0.3),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
12
lib/shared/widgets/widgets.dart
Normal file
12
lib/shared/widgets/widgets.dart
Normal file
@@ -0,0 +1,12 @@
|
||||
/// ============================================================
|
||||
/// 闲言APP — 共享组件导出
|
||||
/// 创建时间: 2026-04-20
|
||||
/// 更新时间: 2026-04-20
|
||||
/// 作用: 统一导出所有共享组件
|
||||
/// 上次更新: 初始创建
|
||||
/// ============================================================
|
||||
|
||||
export 'glass_container.dart';
|
||||
export 'skeleton.dart';
|
||||
export 'empty_state.dart';
|
||||
export 'bottom_sheet.dart';
|
||||
Reference in New Issue
Block a user