feat: 添加清除结果功能到检查提供者
refactor: 更新URL哈希处理逻辑 feat: 添加聊天消息存储支持 docs: 更新API控制器基类文档 chore: 删除无用脚本文件 fix: 修复分类模型返回类型问题 feat: 添加回执登录功能 build: 更新依赖项配置 style: 统一HTML模板中的哈希ID引用格式 ci: 添加部署和检查脚本
This commit is contained in:
@@ -104,7 +104,7 @@ class SlideActionConfig {
|
||||
}
|
||||
}
|
||||
|
||||
class AppSlidable extends StatelessWidget {
|
||||
class AppSlidable extends StatefulWidget {
|
||||
const AppSlidable({
|
||||
super.key,
|
||||
required this.child,
|
||||
@@ -114,6 +114,8 @@ class AppSlidable extends StatelessWidget {
|
||||
this.groupTag,
|
||||
this.onDismissed,
|
||||
this.borderRadius,
|
||||
this.onOpened,
|
||||
this.onClosed,
|
||||
});
|
||||
|
||||
final Key? slideKey;
|
||||
@@ -123,32 +125,70 @@ class AppSlidable extends StatelessWidget {
|
||||
final String? groupTag;
|
||||
final VoidCallback? onDismissed;
|
||||
final BorderRadius? borderRadius;
|
||||
final VoidCallback? onOpened;
|
||||
final VoidCallback? onClosed;
|
||||
|
||||
@override
|
||||
State<AppSlidable> createState() => _AppSlidableState();
|
||||
}
|
||||
|
||||
class _AppSlidableState extends State<AppSlidable>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late final SlidableController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = SlidableController(this);
|
||||
_controller.actionPaneType.addListener(_onActionPaneTypeChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.actionPaneType.removeListener(_onActionPaneTypeChanged);
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onActionPaneTypeChanged() {
|
||||
final type = _controller.actionPaneType.value;
|
||||
if (type != ActionPaneType.none) {
|
||||
widget.onOpened?.call();
|
||||
} else {
|
||||
widget.onClosed?.call();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ext = AppTheme.ext(context);
|
||||
|
||||
return Slidable(
|
||||
key: slideKey,
|
||||
groupTag: groupTag,
|
||||
endActionPane: rightActions.isEmpty
|
||||
key: widget.slideKey,
|
||||
groupTag: widget.groupTag,
|
||||
controller: _controller,
|
||||
endActionPane: widget.rightActions.isEmpty
|
||||
? null
|
||||
: ActionPane(
|
||||
motion: const BehindMotion(),
|
||||
dismissible: onDismissed != null
|
||||
? DismissiblePane(onDismissed: onDismissed!)
|
||||
dismissible: widget.onDismissed != null
|
||||
? DismissiblePane(onDismissed: widget.onDismissed!)
|
||||
: null,
|
||||
children: rightActions.map((a) => _buildAction(ext, a)).toList(),
|
||||
children: widget.rightActions
|
||||
.map((a) => _buildAction(ext, a))
|
||||
.toList(),
|
||||
),
|
||||
startActionPane: leftActions.isEmpty
|
||||
startActionPane: widget.leftActions.isEmpty
|
||||
? null
|
||||
: ActionPane(
|
||||
motion: const BehindMotion(),
|
||||
children: leftActions.map((a) => _buildAction(ext, a)).toList(),
|
||||
children: widget.leftActions
|
||||
.map((a) => _buildAction(ext, a))
|
||||
.toList(),
|
||||
),
|
||||
child: borderRadius != null
|
||||
? ClipRRect(borderRadius: borderRadius!, child: child)
|
||||
: child,
|
||||
child: widget.borderRadius != null
|
||||
? ClipRRect(borderRadius: widget.borderRadius!, child: widget.child)
|
||||
: widget.child,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
911
lib/shared/widgets/share_sheet.dart
Normal file
911
lib/shared/widgets/share_sheet.dart
Normal file
@@ -0,0 +1,911 @@
|
||||
/// ============================================================
|
||||
/// 闲言APP — 分享Sheet (通用)
|
||||
/// 创建时间: 2026-04-30
|
||||
/// 更新时间: 2026-04-30
|
||||
/// 作用: 通用分享功能 — 二维码/系统分享/卡片预览/笔记/历史/回调
|
||||
/// 上次更新: 扩展ShareData支持富媒体/卡片样式/来源/回调/枚举/笔记
|
||||
/// ============================================================
|
||||
|
||||
import 'dart:io';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
||||
import 'package:xianyan/core/theme/app_theme.dart';
|
||||
import 'package:xianyan/core/theme/app_spacing.dart';
|
||||
import 'package:xianyan/core/theme/app_typography.dart';
|
||||
import 'package:xianyan/core/theme/app_radius.dart';
|
||||
import 'package:xianyan/core/storage/database/app_database.dart';
|
||||
import 'package:drift/drift.dart' show Value;
|
||||
import 'package:xianyan/core/utils/logger.dart';
|
||||
import 'package:xianyan/features/home/providers/daily_card_style_provider.dart';
|
||||
import 'package:xianyan/features/note/providers/note_provider.dart';
|
||||
import 'package:xianyan/features/share/models/share_target.dart';
|
||||
import 'package:stupid_simple_sheet/stupid_simple_sheet.dart';
|
||||
import 'package:xianyan/shared/widgets/bottom_sheet.dart';
|
||||
import 'package:xianyan/shared/widgets/app_toast.dart';
|
||||
|
||||
// ============================================================
|
||||
// 分享场景枚举
|
||||
// ============================================================
|
||||
|
||||
enum ShareScene {
|
||||
sentence('sentence', '📝', '句子'),
|
||||
author('author', '👤', '作者'),
|
||||
collection('collection', '📚', '合集'),
|
||||
topic('topic', '🏷️', '话题'),
|
||||
card('card', '🎴', '卡片'),
|
||||
achievement('achievement', '🏆', '成就'),
|
||||
article('article', '📰', '文章'),
|
||||
note('note', '🗒️', '笔记');
|
||||
|
||||
const ShareScene(this.id, this.emoji, this.label);
|
||||
final String id;
|
||||
final String emoji;
|
||||
final String label;
|
||||
|
||||
static ShareScene fromId(String id) {
|
||||
return ShareScene.values.firstWhere(
|
||||
(s) => s.id == id,
|
||||
orElse: () => ShareScene.sentence,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 分享结果
|
||||
// ============================================================
|
||||
|
||||
enum ShareResultType {
|
||||
copiedText,
|
||||
copiedUrl,
|
||||
systemShare,
|
||||
savedQrCode,
|
||||
savedCardImage,
|
||||
addedNote,
|
||||
}
|
||||
|
||||
class ShareResult {
|
||||
const ShareResult({
|
||||
required this.type,
|
||||
this.data,
|
||||
this.scene = ShareScene.sentence,
|
||||
this.source,
|
||||
});
|
||||
|
||||
final ShareResultType type;
|
||||
final String? data;
|
||||
final ShareScene scene;
|
||||
final String? source;
|
||||
|
||||
String get label => switch (type) {
|
||||
ShareResultType.copiedText => '复制文字',
|
||||
ShareResultType.copiedUrl => '复制链接',
|
||||
ShareResultType.systemShare => '系统分享',
|
||||
ShareResultType.savedQrCode => '保存二维码',
|
||||
ShareResultType.savedCardImage => '保存卡片图片',
|
||||
ShareResultType.addedNote => '添加笔记',
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 分享数据模型
|
||||
// ============================================================
|
||||
|
||||
class ShareData {
|
||||
const ShareData({
|
||||
required this.text,
|
||||
this.author,
|
||||
this.source,
|
||||
this.id,
|
||||
this.title,
|
||||
this.imageUrl,
|
||||
this.imageBytes,
|
||||
this.tags,
|
||||
this.scene = ShareScene.sentence,
|
||||
this.cardStyle,
|
||||
this.shareSource,
|
||||
this.onResult,
|
||||
this.note,
|
||||
});
|
||||
|
||||
final String text;
|
||||
final String? author;
|
||||
final String? source;
|
||||
final String? id;
|
||||
final String? title;
|
||||
|
||||
final String? imageUrl;
|
||||
final Uint8List? imageBytes;
|
||||
final List<String>? tags;
|
||||
final ShareScene scene;
|
||||
final DailyCardStyle? cardStyle;
|
||||
final String? shareSource;
|
||||
final void Function(ShareResult)? onResult;
|
||||
final String? note;
|
||||
|
||||
String get shareText {
|
||||
var result = text;
|
||||
if (author != null && author!.isNotEmpty) {
|
||||
result += '\n—— $author';
|
||||
}
|
||||
if (source != null && source!.isNotEmpty) {
|
||||
result += '\n来源: $source';
|
||||
}
|
||||
if (note != null && note!.isNotEmpty) {
|
||||
result += '\n💬 $note';
|
||||
}
|
||||
if (tags != null && tags!.isNotEmpty) {
|
||||
result += '\n${tags!.map((t) => '#$t').join(' ')}';
|
||||
}
|
||||
result += '\n\n来自「闲言」';
|
||||
return result;
|
||||
}
|
||||
|
||||
String get shareUrl {
|
||||
if (id != null && id!.isNotEmpty) {
|
||||
return 'https://xianyan.app/${scene.id}/$id';
|
||||
}
|
||||
return 'https://xianyan.app';
|
||||
}
|
||||
|
||||
ShareData copyWith({String? note, List<String>? tags}) {
|
||||
return ShareData(
|
||||
text: text,
|
||||
author: author,
|
||||
source: source,
|
||||
id: id,
|
||||
title: title,
|
||||
imageUrl: imageUrl,
|
||||
imageBytes: imageBytes,
|
||||
tags: tags ?? this.tags,
|
||||
scene: scene,
|
||||
cardStyle: cardStyle,
|
||||
shareSource: shareSource,
|
||||
onResult: onResult,
|
||||
note: note ?? this.note,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 分享Sheet入口
|
||||
// ============================================================
|
||||
|
||||
class ShareSheet {
|
||||
static Future<ShareResult?> show({
|
||||
required BuildContext context,
|
||||
required ShareData data,
|
||||
}) {
|
||||
return AppBottomSheet.showCustom<ShareResult>(
|
||||
context: context,
|
||||
builder: (ctx) => _ShareSheetContent(data: data),
|
||||
snappingConfig: const SheetSnappingConfig([
|
||||
0.45,
|
||||
0.7,
|
||||
1.0,
|
||||
], initialSnap: 0.7),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 分享Sheet内容
|
||||
// ============================================================
|
||||
|
||||
class _ShareSheetContent extends StatefulWidget {
|
||||
const _ShareSheetContent({required this.data});
|
||||
|
||||
final ShareData data;
|
||||
|
||||
@override
|
||||
State<_ShareSheetContent> createState() => _ShareSheetContentState();
|
||||
}
|
||||
|
||||
class _ShareSheetContentState extends State<_ShareSheetContent> {
|
||||
final _qrKey = GlobalKey();
|
||||
final _cardKey = GlobalKey();
|
||||
String? _currentNote;
|
||||
|
||||
ShareData get data => widget.data;
|
||||
|
||||
AppThemeExtension get ext => AppTheme.ext(context);
|
||||
|
||||
void _emitResult(ShareResultType type, {String? resultData}) {
|
||||
final result = ShareResult(
|
||||
type: type,
|
||||
data: resultData,
|
||||
scene: data.scene,
|
||||
source: data.shareSource,
|
||||
);
|
||||
data.onResult?.call(result);
|
||||
Log.i(
|
||||
'分享结果: ${result.label} | 场景: ${data.scene.label} | 来源: ${data.shareSource}',
|
||||
);
|
||||
_recordShareHistory(type);
|
||||
}
|
||||
|
||||
Future<void> _recordShareHistory(ShareResultType type) async {
|
||||
try {
|
||||
await AppDatabase.instance.insertShareHistory(
|
||||
ShareHistoriesCompanion(
|
||||
contentId: Value(data.id ?? ''),
|
||||
scene: Value(data.scene.id),
|
||||
shareType: Value(type.name),
|
||||
title: Value(data.title ?? ''),
|
||||
content: Value(data.text),
|
||||
author: Value(data.author ?? ''),
|
||||
source: Value(data.source ?? ''),
|
||||
shareSource: Value(data.shareSource ?? ''),
|
||||
note: Value(_currentNote ?? data.note ?? ''),
|
||||
tags: Value(data.tags?.join(',') ?? ''),
|
||||
imageUrl: Value(data.imageUrl ?? ''),
|
||||
sharedAt: Value(DateTime.now()),
|
||||
),
|
||||
);
|
||||
Log.i('分享历史已记录: ${type.name}');
|
||||
} catch (e) {
|
||||
Log.e('记录分享历史失败', e);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: ext.bgCard,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(36)),
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
AppSpacing.lg,
|
||||
AppSpacing.md,
|
||||
AppSpacing.lg,
|
||||
0,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildDragHandle(),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
_buildTitle(),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
if (data.cardStyle != null) ...[
|
||||
_buildCardPreview(),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
],
|
||||
_buildQrCode(),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
_buildShareActions(),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
if (data.tags != null && data.tags!.isNotEmpty) _buildTagsSection(),
|
||||
_buildUrlSection(),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
_buildNoteSection(),
|
||||
SizedBox(height: MediaQuery.of(context).padding.bottom + 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDragHandle() {
|
||||
return Center(
|
||||
child: Container(
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: ext.textHint.withValues(alpha: 0.3),
|
||||
borderRadius: AppRadius.fullBorder,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTitle() {
|
||||
return Row(
|
||||
children: [
|
||||
Text(data.scene.emoji, style: const TextStyle(fontSize: 20)),
|
||||
const SizedBox(width: AppSpacing.xs),
|
||||
Expanded(
|
||||
child: Text(
|
||||
data.title ?? '分享${data.scene.label}',
|
||||
style: AppTypography.title3.copyWith(
|
||||
color: ext.textPrimary,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (data.shareSource != null)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: ext.accent.withValues(alpha: 0.12),
|
||||
borderRadius: AppRadius.fullBorder,
|
||||
),
|
||||
child: Text(
|
||||
data.shareSource!,
|
||||
style: AppTypography.caption2.copyWith(
|
||||
color: ext.accent,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCardPreview() {
|
||||
final style = data.cardStyle!;
|
||||
return RepaintBoundary(
|
||||
key: _cardKey,
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(style.padding),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [style.colorScheme.c1, style.colorScheme.c2],
|
||||
begin: style.gradientBegin,
|
||||
end: style.gradientEnd,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(style.borderRadius),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: style.colorScheme.c1.withValues(alpha: 0.3),
|
||||
blurRadius: 16,
|
||||
offset: const Offset(0, 6),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
data.text,
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: style.fontSize,
|
||||
height: 1.6,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
maxLines: 4,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (data.author != null && data.author!.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'—— ${data.author}',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.8),
|
||||
fontSize: style.fontSize - 2,
|
||||
),
|
||||
),
|
||||
],
|
||||
if (data.source != null && data.source!.isNotEmpty) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'📖 ${data.source}',
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.6),
|
||||
fontSize: style.fontSize - 4,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildQrCode() {
|
||||
return RepaintBoundary(
|
||||
key: _qrKey,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(AppSpacing.lg),
|
||||
decoration: BoxDecoration(
|
||||
color: CupertinoColors.white,
|
||||
borderRadius: AppRadius.xlBorder,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: ext.isDark
|
||||
? const Color(0xFFFFFFFF).withValues(alpha: 0.05)
|
||||
: const Color(0xFF000000).withValues(alpha: 0.08),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
QrImageView(
|
||||
data: data.shareUrl,
|
||||
version: QrVersions.auto,
|
||||
size: 180,
|
||||
eyeStyle: QrEyeStyle(
|
||||
eyeShape: QrEyeShape.square,
|
||||
color: ext.isDark
|
||||
? const Color(0xFF1C1C1E)
|
||||
: const Color(0xFF000000),
|
||||
),
|
||||
dataModuleStyle: QrDataModuleStyle(
|
||||
dataModuleShape: QrDataModuleShape.square,
|
||||
color: ext.isDark
|
||||
? const Color(0xFF1C1C1E)
|
||||
: const Color(0xFF000000),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
Text(
|
||||
'扫描二维码查看',
|
||||
style: AppTypography.caption1.copyWith(
|
||||
color: ext.isDark
|
||||
? const Color(0xFF8E8E93)
|
||||
: const Color(0xFF636366),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildShareActions() {
|
||||
return FutureBuilder<List<ShareTarget>>(
|
||||
future: ShareTarget.load(),
|
||||
builder: (ctx, snapshot) {
|
||||
final allTargets = snapshot.data ?? ShareTarget.defaults;
|
||||
final enabledTargets = allTargets.where((t) => t.enabled).toList();
|
||||
final visibleTargets = enabledTargets.where((t) {
|
||||
if (t.type == ShareTargetType.saveCardImage &&
|
||||
data.cardStyle == null) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}).toList();
|
||||
|
||||
if (visibleTargets.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: visibleTargets.map((target) {
|
||||
return _ShareActionBtn(
|
||||
icon: target.emoji,
|
||||
label: target.label,
|
||||
ext: ext,
|
||||
onTap: () => _handleShareTarget(target),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _handleShareTarget(ShareTarget target) {
|
||||
switch (target.type) {
|
||||
case ShareTargetType.copyText:
|
||||
_copyText();
|
||||
case ShareTargetType.copyUrl:
|
||||
_copyUrl();
|
||||
case ShareTargetType.systemShare:
|
||||
_systemShare();
|
||||
case ShareTargetType.saveQrCode:
|
||||
_saveQrCode();
|
||||
case ShareTargetType.saveCardImage:
|
||||
_saveCardImage();
|
||||
case ShareTargetType.addNote:
|
||||
_showNoteSheet();
|
||||
case ShareTargetType.wechatFriend:
|
||||
AppToast.showInfo('微信分享需要配置fluwx SDK Key');
|
||||
case ShareTargetType.wechatMoments:
|
||||
AppToast.showInfo('朋友圈分享需要配置fluwx SDK Key');
|
||||
case ShareTargetType.qq:
|
||||
AppToast.showInfo('QQ分享需要配置QQ SDK');
|
||||
case ShareTargetType.weibo:
|
||||
_systemShare();
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildTagsSection() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.sm,
|
||||
vertical: AppSpacing.xs,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: ext.bgSecondary,
|
||||
borderRadius: AppRadius.mdBorder,
|
||||
),
|
||||
child: Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: 4,
|
||||
children: data.tags!.map((tag) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: ext.accent.withValues(alpha: 0.1),
|
||||
borderRadius: AppRadius.fullBorder,
|
||||
),
|
||||
child: Text(
|
||||
'#$tag',
|
||||
style: AppTypography.caption2.copyWith(
|
||||
color: ext.accent,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildUrlSection() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(AppSpacing.sm),
|
||||
decoration: BoxDecoration(
|
||||
color: ext.bgSecondary,
|
||||
borderRadius: AppRadius.mdBorder,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Text('🔗', style: TextStyle(fontSize: 14)),
|
||||
const SizedBox(width: AppSpacing.xs),
|
||||
Expanded(
|
||||
child: Text(
|
||||
data.shareUrl,
|
||||
style: AppTypography.caption1.copyWith(color: ext.textSecondary),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
CupertinoButton(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
minimumSize: Size.zero,
|
||||
onPressed: _copyUrl,
|
||||
child: Text(
|
||||
'复制',
|
||||
style: AppTypography.caption1.copyWith(
|
||||
color: ext.accent,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNoteSection() {
|
||||
final hasNote = _currentNote != null && _currentNote!.isNotEmpty;
|
||||
return GestureDetector(
|
||||
onTap: _showNoteSheet,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(AppSpacing.md),
|
||||
decoration: BoxDecoration(
|
||||
color: ext.bgSecondary,
|
||||
borderRadius: AppRadius.mdBorder,
|
||||
border: hasNote
|
||||
? Border.all(color: ext.accent.withValues(alpha: 0.3))
|
||||
: null,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(hasNote ? '✏️' : '🗒️', style: const TextStyle(fontSize: 16)),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
Expanded(
|
||||
child: Text(
|
||||
hasNote ? _currentNote! : '添加笔记...',
|
||||
style: AppTypography.subhead.copyWith(
|
||||
color: hasNote ? ext.textPrimary : ext.textHint,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Icon(CupertinoIcons.chevron_right, size: 14, color: ext.textHint),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showNoteSheet() {
|
||||
final controller = TextEditingController(text: _currentNote ?? '');
|
||||
AppBottomSheet.showCustom<void>(
|
||||
context: context,
|
||||
builder: (ctx) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: ext.bgCard,
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(36)),
|
||||
),
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
AppSpacing.lg,
|
||||
AppSpacing.md,
|
||||
AppSpacing.lg,
|
||||
0,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Center(
|
||||
child: Container(
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: ext.textHint.withValues(alpha: 0.3),
|
||||
borderRadius: AppRadius.fullBorder,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'🗒️ 添加笔记',
|
||||
style: AppTypography.title3.copyWith(
|
||||
color: ext.textPrimary,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
CupertinoButton(
|
||||
padding: EdgeInsets.zero,
|
||||
minimumSize: Size.zero,
|
||||
onPressed: () {
|
||||
final note = controller.text.trim();
|
||||
if (note.isNotEmpty) {
|
||||
_saveNoteToApp(note);
|
||||
}
|
||||
setState(() => _currentNote = note);
|
||||
Navigator.of(ctx).pop();
|
||||
},
|
||||
child: Text(
|
||||
'完成',
|
||||
style: AppTypography.subhead.copyWith(
|
||||
color: ext.accent,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
CupertinoTextField(
|
||||
controller: controller,
|
||||
placeholder: '写下你的感想...',
|
||||
placeholderStyle: AppTypography.body.copyWith(
|
||||
color: ext.textHint,
|
||||
),
|
||||
style: AppTypography.body.copyWith(color: ext.textPrimary),
|
||||
maxLines: 5,
|
||||
minLines: 3,
|
||||
padding: const EdgeInsets.all(AppSpacing.md),
|
||||
decoration: BoxDecoration(
|
||||
color: ext.bgSecondary,
|
||||
borderRadius: AppRadius.mdBorder,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
Row(
|
||||
children: [
|
||||
CupertinoButton(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
minimumSize: const Size(44, 36),
|
||||
onPressed: () {
|
||||
controller.clear();
|
||||
setState(() => _currentNote = null);
|
||||
Navigator.of(ctx).pop();
|
||||
},
|
||||
child: Text(
|
||||
'清除',
|
||||
style: AppTypography.subhead.copyWith(
|
||||
color: CupertinoColors.systemRed,
|
||||
),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
CupertinoButton(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
minimumSize: const Size(44, 36),
|
||||
onPressed: () => _saveNoteToApp(controller.text.trim()),
|
||||
child: Text(
|
||||
'📝 保存到笔记',
|
||||
style: AppTypography.subhead.copyWith(
|
||||
color: ext.accent,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: MediaQuery.of(context).padding.bottom + 16),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
snappingConfig: const SheetSnappingConfig([
|
||||
0.4,
|
||||
0.6,
|
||||
1.0,
|
||||
], initialSnap: 0.6),
|
||||
);
|
||||
}
|
||||
|
||||
void _saveNoteToApp(String content) {
|
||||
if (content.isEmpty) return;
|
||||
try {
|
||||
final container = ProviderScope.containerOf(context);
|
||||
final notifier = container.read(noteListProvider.notifier);
|
||||
notifier.saveNote(
|
||||
title:
|
||||
'${data.scene.label}笔记 — ${data.text.substring(0, data.text.length > 20 ? 20 : data.text.length)}',
|
||||
content: content,
|
||||
sourceType: data.scene.id,
|
||||
sourceId: int.tryParse(data.id ?? '0') ?? 0,
|
||||
noteType: 'share_note',
|
||||
);
|
||||
_emitResult(ShareResultType.addedNote, resultData: content);
|
||||
AppToast.showSuccess('📝 已保存到笔记');
|
||||
} catch (e) {
|
||||
Log.e('保存笔记失败', e);
|
||||
AppToast.showError('保存笔记失败');
|
||||
}
|
||||
}
|
||||
|
||||
void _copyText() {
|
||||
Clipboard.setData(ClipboardData(text: data.shareText));
|
||||
_emitResult(ShareResultType.copiedText, resultData: data.shareText);
|
||||
AppToast.showSuccess('📋 已复制文字');
|
||||
}
|
||||
|
||||
void _copyUrl() {
|
||||
Clipboard.setData(ClipboardData(text: data.shareUrl));
|
||||
_emitResult(ShareResultType.copiedUrl, resultData: data.shareUrl);
|
||||
AppToast.showSuccess('🔗 已复制链接');
|
||||
}
|
||||
|
||||
Future<void> _systemShare() async {
|
||||
final box = context.findRenderObject() as RenderBox?;
|
||||
await Share.share(
|
||||
data.shareText,
|
||||
subject: data.text,
|
||||
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
|
||||
);
|
||||
_emitResult(ShareResultType.systemShare);
|
||||
}
|
||||
|
||||
Future<void> _saveQrCode() async {
|
||||
try {
|
||||
final boundary =
|
||||
_qrKey.currentContext?.findRenderObject() as RenderRepaintBoundary?;
|
||||
if (boundary == null) {
|
||||
AppToast.showError('保存失败');
|
||||
return;
|
||||
}
|
||||
final image = await boundary.toImage(pixelRatio: 3.0);
|
||||
final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
|
||||
if (byteData == null) {
|
||||
AppToast.showError('保存失败');
|
||||
return;
|
||||
}
|
||||
|
||||
if (Platform.isAndroid || Platform.isIOS) {
|
||||
final file = XFile.fromData(
|
||||
byteData.buffer.asUint8List(),
|
||||
name: 'xianyan_share_qr.png',
|
||||
mimeType: 'image/png',
|
||||
);
|
||||
final box = context.findRenderObject() as RenderBox?;
|
||||
await Share.shareXFiles(
|
||||
[file],
|
||||
text: '来自「闲言」',
|
||||
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
|
||||
);
|
||||
_emitResult(ShareResultType.savedQrCode);
|
||||
AppToast.showSuccess('💾 二维码已分享');
|
||||
} else {
|
||||
AppToast.showSuccess('💾 二维码已生成');
|
||||
}
|
||||
} catch (e) {
|
||||
AppToast.showError('保存失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _saveCardImage() async {
|
||||
try {
|
||||
final boundary =
|
||||
_cardKey.currentContext?.findRenderObject() as RenderRepaintBoundary?;
|
||||
if (boundary == null) {
|
||||
AppToast.showError('保存失败');
|
||||
return;
|
||||
}
|
||||
final image = await boundary.toImage(pixelRatio: 3.0);
|
||||
final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
|
||||
if (byteData == null) {
|
||||
AppToast.showError('保存失败');
|
||||
return;
|
||||
}
|
||||
|
||||
if (Platform.isAndroid || Platform.isIOS) {
|
||||
final file = XFile.fromData(
|
||||
byteData.buffer.asUint8List(),
|
||||
name: 'xianyan_card_${data.id ?? 'share'}.png',
|
||||
mimeType: 'image/png',
|
||||
);
|
||||
final box = context.findRenderObject() as RenderBox?;
|
||||
await Share.shareXFiles(
|
||||
[file],
|
||||
text: data.shareText,
|
||||
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
|
||||
);
|
||||
_emitResult(ShareResultType.savedCardImage);
|
||||
AppToast.showSuccess('🎴 卡片图片已分享');
|
||||
}
|
||||
} catch (e) {
|
||||
AppToast.showError('保存失败: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 分享操作按钮
|
||||
// ============================================================
|
||||
|
||||
class _ShareActionBtn extends StatelessWidget {
|
||||
const _ShareActionBtn({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.ext,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final String icon;
|
||||
final String label;
|
||||
final AppThemeExtension ext;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
width: 72,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: ext.bgSecondary,
|
||||
border: Border.all(
|
||||
color: ext.isDark
|
||||
? const Color(0xFFFFFFFF).withValues(alpha: 0.06)
|
||||
: const Color(0xFF000000).withValues(alpha: 0.04),
|
||||
),
|
||||
borderRadius: AppRadius.mdBorder,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(icon, style: const TextStyle(fontSize: 24)),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
label,
|
||||
style: AppTypography.caption2.copyWith(
|
||||
color: ext.textSecondary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user