feat:分享功能
This commit is contained in:
@@ -28,6 +28,8 @@ PODS:
|
|||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- platform_metadata (0.0.1):
|
- platform_metadata (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- share_plus (0.0.1):
|
||||||
|
- Flutter
|
||||||
- shared_preferences_foundation (0.0.1):
|
- shared_preferences_foundation (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
@@ -54,6 +56,7 @@ DEPENDENCIES:
|
|||||||
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
||||||
- photo_manager (from `.symlinks/plugins/photo_manager/darwin`)
|
- photo_manager (from `.symlinks/plugins/photo_manager/darwin`)
|
||||||
- platform_metadata (from `.symlinks/plugins/platform_metadata/ios`)
|
- platform_metadata (from `.symlinks/plugins/platform_metadata/ios`)
|
||||||
|
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||||
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
|
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
|
||||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||||
@@ -86,6 +89,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/photo_manager/darwin"
|
:path: ".symlinks/plugins/photo_manager/darwin"
|
||||||
platform_metadata:
|
platform_metadata:
|
||||||
:path: ".symlinks/plugins/platform_metadata/ios"
|
:path: ".symlinks/plugins/platform_metadata/ios"
|
||||||
|
share_plus:
|
||||||
|
:path: ".symlinks/plugins/share_plus/ios"
|
||||||
shared_preferences_foundation:
|
shared_preferences_foundation:
|
||||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
||||||
sqflite_darwin:
|
sqflite_darwin:
|
||||||
@@ -109,6 +114,7 @@ SPEC CHECKSUMS:
|
|||||||
permission_handler_apple: 92d754bbaa7361d436db2d6c3c1c2a0fdcec462e
|
permission_handler_apple: 92d754bbaa7361d436db2d6c3c1c2a0fdcec462e
|
||||||
photo_manager: 25fd77df14f4f0ba5ef99e2c61814dde77e2bceb
|
photo_manager: 25fd77df14f4f0ba5ef99e2c61814dde77e2bceb
|
||||||
platform_metadata: a4d1e9569219bb2dcbb54510554611df032ea023
|
platform_metadata: a4d1e9569219bb2dcbb54510554611df032ea023
|
||||||
|
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
||||||
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
||||||
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
||||||
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
|
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
// ignore_for_file: use_build_context_synchronously
|
||||||
|
|
||||||
|
import 'dart:typed_data';
|
||||||
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
|
||||||
|
ui.Rect? shareOriginForContext(BuildContext context) {
|
||||||
|
final renderObject = context.findRenderObject();
|
||||||
|
if (renderObject is! RenderBox || !renderObject.hasSize) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return renderObject.localToGlobal(Offset.zero) & renderObject.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Uint8List> captureWidgetPng(
|
||||||
|
GlobalKey key, {
|
||||||
|
double pixelRatio = 3.0,
|
||||||
|
}) async {
|
||||||
|
await WidgetsBinding.instance.endOfFrame;
|
||||||
|
|
||||||
|
final context = key.currentContext;
|
||||||
|
if (context == null) {
|
||||||
|
throw StateError('share target is not mounted');
|
||||||
|
}
|
||||||
|
|
||||||
|
final renderObject = context.findRenderObject();
|
||||||
|
if (renderObject is! RenderRepaintBoundary) {
|
||||||
|
throw StateError('share target is not a repaint boundary');
|
||||||
|
}
|
||||||
|
|
||||||
|
final image = await renderObject.toImage(pixelRatio: pixelRatio);
|
||||||
|
final byteData = await image.toByteData(format: ui.ImageByteFormat.png);
|
||||||
|
if (byteData == null) {
|
||||||
|
throw StateError('failed to convert share image');
|
||||||
|
}
|
||||||
|
return byteData.buffer.asUint8List();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> sharePngFromBoundary(
|
||||||
|
GlobalKey key, {
|
||||||
|
required String fileName,
|
||||||
|
String? text,
|
||||||
|
String? subject,
|
||||||
|
ui.Rect? sharePositionOrigin,
|
||||||
|
double pixelRatio = 3.0,
|
||||||
|
}) async {
|
||||||
|
final bytes = await captureWidgetPng(key, pixelRatio: pixelRatio);
|
||||||
|
final xFile = XFile.fromData(bytes, mimeType: 'image/png', name: fileName);
|
||||||
|
await SharePlus.instance.share(
|
||||||
|
ShareParams(
|
||||||
|
files: [xFile],
|
||||||
|
text: text,
|
||||||
|
subject: subject,
|
||||||
|
sharePositionOrigin: sharePositionOrigin,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> sharePlainText({
|
||||||
|
required String text,
|
||||||
|
String? subject,
|
||||||
|
ui.Rect? sharePositionOrigin,
|
||||||
|
}) async {
|
||||||
|
await SharePlus.instance.share(
|
||||||
|
ShareParams(
|
||||||
|
text: text,
|
||||||
|
subject: subject,
|
||||||
|
sharePositionOrigin: sharePositionOrigin,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,6 +2,9 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||||
|
|
||||||
|
import '../../../common/config/app_h5_urls.dart';
|
||||||
|
import '../../../common/util/share_util.dart';
|
||||||
|
import '../../../common/widgets/app_toast.dart';
|
||||||
import '../../../common/widgets/prototype_ui.dart';
|
import '../../../common/widgets/prototype_ui.dart';
|
||||||
import '../application/news_controller.dart';
|
import '../application/news_controller.dart';
|
||||||
|
|
||||||
@@ -35,6 +38,33 @@ class NewsDetailPage extends ConsumerWidget {
|
|||||||
item.metal.label,
|
item.metal.label,
|
||||||
style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w600),
|
style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w600),
|
||||||
),
|
),
|
||||||
|
actions: [
|
||||||
|
Builder(
|
||||||
|
builder: (buttonContext) => IconButton(
|
||||||
|
onPressed: () async {
|
||||||
|
final shareText = [
|
||||||
|
item.title,
|
||||||
|
'${item.source} · ${_timeLabel(item.publishedAt)} · ${item.metal.label}',
|
||||||
|
item.summary,
|
||||||
|
'打开金值查看:${AppH5Urls.baseUrl}/news/detail?id=${item.id}',
|
||||||
|
].join('\n');
|
||||||
|
try {
|
||||||
|
await sharePlainText(
|
||||||
|
text: shareText,
|
||||||
|
subject: item.title,
|
||||||
|
sharePositionOrigin: shareOriginForContext(buttonContext),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('share news failed: $e');
|
||||||
|
if (!context.mounted) return;
|
||||||
|
await toastInfo(msg: '分享失败');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.ios_share_outlined, size: 20),
|
||||||
|
color: const Color(0xFF8A8780),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: ListView(
|
child: ListView(
|
||||||
|
|||||||
@@ -4,10 +4,13 @@ import 'package:go_router/go_router.dart';
|
|||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:package_info_plus/package_info_plus.dart';
|
import 'package:package_info_plus/package_info_plus.dart';
|
||||||
import 'package:shadcn_ui/shadcn_ui.dart';
|
import 'package:shadcn_ui/shadcn_ui.dart';
|
||||||
|
import 'dart:ui' as ui;
|
||||||
|
|
||||||
import '../../../common/domain/metal_type.dart';
|
import '../../../common/domain/metal_type.dart';
|
||||||
import '../../../common/domain/money.dart';
|
import '../../../common/domain/money.dart';
|
||||||
|
import '../../../common/widgets/app_toast.dart';
|
||||||
import '../../../common/router/app_router.dart';
|
import '../../../common/router/app_router.dart';
|
||||||
|
import '../../../common/util/share_util.dart';
|
||||||
import '../../../common/widgets/adaptive.dart';
|
import '../../../common/widgets/adaptive.dart';
|
||||||
import '../../../common/widgets/circle_back_button.dart';
|
import '../../../common/widgets/circle_back_button.dart';
|
||||||
import '../../../common/widgets/prototype_ui.dart';
|
import '../../../common/widgets/prototype_ui.dart';
|
||||||
@@ -44,7 +47,7 @@ class InviteRewardPage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 14),
|
const SizedBox(height: 14),
|
||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: () => _toast(context, '演示:拉起微信 / 系统分享面板'),
|
onPressed: () => toastInfo(msg: '演示:拉起微信 / 系统分享面板'),
|
||||||
child: const Text('分享给好友'),
|
child: const Text('分享给好友'),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 14),
|
const SizedBox(height: 14),
|
||||||
@@ -62,11 +65,42 @@ class InviteRewardPage extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ShareCardPage extends ConsumerWidget {
|
class ShareCardPage extends ConsumerStatefulWidget {
|
||||||
const ShareCardPage({super.key});
|
const ShareCardPage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
ConsumerState<ShareCardPage> createState() => _ShareCardPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ShareCardPageState extends ConsumerState<ShareCardPage> {
|
||||||
|
final GlobalKey _previewKey = GlobalKey();
|
||||||
|
bool _sharing = false;
|
||||||
|
|
||||||
|
Future<void> _shareCard(String owner, {ui.Rect? origin}) async {
|
||||||
|
if (_sharing) return;
|
||||||
|
setState(() => _sharing = true);
|
||||||
|
try {
|
||||||
|
await sharePngFromBoundary(
|
||||||
|
_previewKey,
|
||||||
|
fileName: 'jinzhi_share_card.png',
|
||||||
|
text: '$owner 的贵金属估值卡,已通过金值生成。',
|
||||||
|
subject: '金值贵金属估值卡',
|
||||||
|
sharePositionOrigin: origin ?? shareOriginForContext(context),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('share card failed: $e');
|
||||||
|
await Clipboard.setData(ClipboardData(text: '$owner 的贵金属估值卡,已通过金值生成。'));
|
||||||
|
if (!mounted) return;
|
||||||
|
toastInfo(msg: '图片分享失败,已复制分享文案');
|
||||||
|
} finally {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() => _sharing = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
final auth = ref.watch(authProvider);
|
final auth = ref.watch(authProvider);
|
||||||
final currentUser = auth.valueOrNull is LoggedInAuthState
|
final currentUser = auth.valueOrNull is LoggedInAuthState
|
||||||
? (auth.valueOrNull as LoggedInAuthState).user
|
? (auth.valueOrNull as LoggedInAuthState).user
|
||||||
@@ -79,7 +113,10 @@ class ShareCardPage extends ConsumerWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
_SharePreviewCard(owner: owner),
|
RepaintBoundary(
|
||||||
|
key: _previewKey,
|
||||||
|
child: _SharePreviewCard(owner: owner),
|
||||||
|
),
|
||||||
const SizedBox(height: 14),
|
const SizedBox(height: 14),
|
||||||
PrototypeCard(
|
PrototypeCard(
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
@@ -100,13 +137,32 @@ class ShareCardPage extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 14),
|
const SizedBox(height: 14),
|
||||||
FilledButton(
|
Builder(
|
||||||
|
builder: (buttonContext) => FilledButton.icon(
|
||||||
|
onPressed: _sharing
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
final origin = shareOriginForContext(buttonContext);
|
||||||
|
_shareCard(owner, origin: origin);
|
||||||
|
},
|
||||||
|
icon: _sharing
|
||||||
|
? const SizedBox(
|
||||||
|
width: 14,
|
||||||
|
height: 14,
|
||||||
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
|
)
|
||||||
|
: const Icon(Icons.ios_share_outlined),
|
||||||
|
label: Text(_sharing ? '分享中' : '系统分享图片'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
OutlinedButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await Clipboard.setData(
|
await Clipboard.setData(
|
||||||
ClipboardData(text: '$owner 的贵金属估值卡,已通过金值生成。'),
|
ClipboardData(text: '$owner 的贵金属估值卡,已通过金值生成。'),
|
||||||
);
|
);
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
_toast(context, '分享文案已复制');
|
toastInfo(msg: '分享文案已复制');
|
||||||
},
|
},
|
||||||
child: const Text('复制分享文案'),
|
child: const Text('复制分享文案'),
|
||||||
),
|
),
|
||||||
@@ -152,7 +208,7 @@ class LocalBackupPage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 14),
|
const SizedBox(height: 14),
|
||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: () => _toast(context, '已生成本地备份文件'),
|
onPressed: () => toastInfo(msg: '已生成本地备份文件'),
|
||||||
child: const Text('生成备份'),
|
child: const Text('生成备份'),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
@@ -213,7 +269,7 @@ class CloudBackupPage extends ConsumerWidget {
|
|||||||
if (!loggedIn) {
|
if (!loggedIn) {
|
||||||
context.push(AppRoutes.login);
|
context.push(AppRoutes.login);
|
||||||
} else {
|
} else {
|
||||||
_toast(context, '云同步入口已打开');
|
toastInfo(msg: '云同步入口已打开');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Text(loggedIn ? '开启云备份' : '去登录'),
|
child: Text(loggedIn ? '开启云备份' : '去登录'),
|
||||||
@@ -256,7 +312,7 @@ class WidgetSettingsPage extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 14),
|
const SizedBox(height: 14),
|
||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: () => _toast(context, '演示:打开系统小组件设置'),
|
onPressed: () => toastInfo(msg: '演示:打开系统小组件设置'),
|
||||||
child: const Text('添加到桌面'),
|
child: const Text('添加到桌面'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -302,7 +358,7 @@ class PuritySettingsPage extends ConsumerWidget {
|
|||||||
title: purity,
|
title: purity,
|
||||||
value: (settings.purityPresets[purity] ?? 1)
|
value: (settings.purityPresets[purity] ?? 1)
|
||||||
.toStringAsFixed(4),
|
.toStringAsFixed(4),
|
||||||
onTap: () => _toast(context, '演示:自定义「$purity」纯度系数'),
|
onTap: () => toastInfo(msg: '演示:自定义「$purity」纯度系数'),
|
||||||
),
|
),
|
||||||
if (purity != group.$2.last)
|
if (purity != group.$2.last)
|
||||||
const PrototypeDivider(margin: EdgeInsets.zero),
|
const PrototypeDivider(margin: EdgeInsets.zero),
|
||||||
@@ -599,12 +655,12 @@ class AboutPage extends StatelessWidget {
|
|||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_GoldTextAction(
|
_GoldTextAction(
|
||||||
label: '提交意见反馈',
|
label: '提交意见反馈',
|
||||||
onTap: () => _toast(context, '演示:打开反馈表单'),
|
onTap: () => toastInfo(msg: '演示:打开反馈表单'),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
_GoldTextAction(
|
_GoldTextAction(
|
||||||
label: '检查更新',
|
label: '检查更新',
|
||||||
onTap: () => _toast(context, '当前已是最新版本'),
|
onTap: () => toastInfo(msg: '当前已是最新版本'),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -718,7 +774,7 @@ class _InviteCodeCard extends StatelessWidget {
|
|||||||
const ClipboardData(text: 'JINZHI-8K3F'),
|
const ClipboardData(text: 'JINZHI-8K3F'),
|
||||||
);
|
);
|
||||||
if (!context.mounted) return;
|
if (!context.mounted) return;
|
||||||
_toast(context, '已复制邀请码');
|
toastInfo(msg: '已复制邀请码');
|
||||||
},
|
},
|
||||||
child: const Text(
|
child: const Text(
|
||||||
'复制',
|
'复制',
|
||||||
@@ -1419,7 +1475,3 @@ class _GoldDot extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _toast(BuildContext context, String message) {
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(message)));
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1267,6 +1267,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.53.6"
|
version: "0.53.6"
|
||||||
|
share_plus:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: share_plus
|
||||||
|
sha256: "223873d106614442ea6f20db5a038685cc5b32a2fba81cdecaefbbae0523f7fa"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "12.0.2"
|
||||||
|
share_plus_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: share_plus_platform_interface
|
||||||
|
sha256: "88023e53a13429bd65d8e85e11a9b484f49d4c190abbd96c7932b74d6927cc9a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.1.0"
|
||||||
shared_preferences:
|
shared_preferences:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ dependencies:
|
|||||||
url_launcher: ^6.3.1
|
url_launcher: ^6.3.1
|
||||||
gal: ^2.3.2
|
gal: ^2.3.2
|
||||||
image_picker: ^1.1.2
|
image_picker: ^1.1.2
|
||||||
|
share_plus: ^12.0.1
|
||||||
wechat_assets_picker: ^10.1.2
|
wechat_assets_picker: ^10.1.2
|
||||||
flutter_cache_manager: ^3.4.1
|
flutter_cache_manager: ^3.4.1
|
||||||
photo_view: ^0.15.0
|
photo_view: ^0.15.0
|
||||||
|
|||||||
Reference in New Issue
Block a user