feat: 闲言APP v0.9.1 — 编辑器全面重写 + 迷你编辑器

- 标准编辑器: freezed数据模型 + 5Tab工具栏 + 多图层管理 + 文字增强 + 背景系统 + 导出链路
- 迷你编辑器: 极简6项功能(文字/字号/颜色/背景/预览/导出) + 三种调用方式(全屏/半屏/内嵌)
- 共享组件: GlassSlider/ColorPicker/FontPicker/TipsView
- 服务层: ExportService/ImageImportService/FontService/XycardService
- 设计系统: 统一主题令牌 + Liquid Glass风格
This commit is contained in:
Developer
2026-04-20 07:48:07 +08:00
commit 35202b51e8
461 changed files with 39318 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
/// ============================================================
/// 闲言APP — 上下文扩展
/// 创建时间: 2026-04-20
/// 更新时间: 2026-04-20
/// 作用: 主题扩展快捷访问
/// 上次更新: 初始创建
/// ============================================================
export '../../core/utils/extensions.dart';

View 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),
);
}
}

View 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,
),
),
),
],
],
),
),
);
}
}

View 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,
);
}
}
}

View 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,
),
);
}
}

View 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';