feat(app,viewer): WeChat card share + 'Share to...' label + site og tags
- 按钮文字「分享到微信/其他应用」→「分享到...」(用户需求) - 微信卡片分享(fluwx 6):WECHAT_APPID 经 --dart-define 注入;已配置且 装微信 → WXWebpage 卡片(标题=相册名、描述=日期·地点、缩略图=封面压缩 240px PNG ≤32KB、点击进观众链接);未配置/未装 → 回退系统分享面板 - viewer index.html 加站点级 og 标签(微信/QQ 抓取链接预览用;每相册动态 og 需服务端注入,登记后续增强) - 397/397 全绿、analyze 0 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -422,7 +422,7 @@
|
||||
"bannerPickerHint": "Tap to pick a banner image (shown atop the viewer page)",
|
||||
"bannerSelected": "Banner selected — will upload on save",
|
||||
"bannerUploadFailed": "Banner upload failed",
|
||||
"shareCardMode": "Share to WeChat / other apps",
|
||||
"shareCardMode": "Share to...",
|
||||
"sharePosterMode": "Generate QR poster",
|
||||
"posterDownload": "Download poster image",
|
||||
"posterSaved": "Poster saved to gallery",
|
||||
|
||||
@@ -1985,7 +1985,7 @@ abstract class AppLocalizations {
|
||||
/// No description provided for @shareCardMode.
|
||||
///
|
||||
/// In zh, this message translates to:
|
||||
/// **'分享到微信 / 其他应用'**
|
||||
/// **'分享到...'**
|
||||
String get shareCardMode;
|
||||
|
||||
/// No description provided for @sharePosterMode.
|
||||
|
||||
@@ -1015,7 +1015,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get bannerUploadFailed => 'Banner upload failed';
|
||||
|
||||
@override
|
||||
String get shareCardMode => 'Share to WeChat / other apps';
|
||||
String get shareCardMode => 'Share to...';
|
||||
|
||||
@override
|
||||
String get sharePosterMode => 'Generate QR poster';
|
||||
|
||||
@@ -982,7 +982,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get bannerUploadFailed => '宣传图上传失败';
|
||||
|
||||
@override
|
||||
String get shareCardMode => '分享到微信 / 其他应用';
|
||||
String get shareCardMode => '分享到...';
|
||||
|
||||
@override
|
||||
String get sharePosterMode => '生成二维码海报';
|
||||
|
||||
@@ -422,7 +422,7 @@
|
||||
"bannerPickerHint": "点击选择宣传图(展示在观众页顶部)",
|
||||
"bannerSelected": "已选择宣传图——保存时上传",
|
||||
"bannerUploadFailed": "宣传图上传失败",
|
||||
"shareCardMode": "分享到微信 / 其他应用",
|
||||
"shareCardMode": "分享到...",
|
||||
"sharePosterMode": "生成二维码海报",
|
||||
"posterDownload": "下载海报图",
|
||||
"posterSaved": "海报已保存到相册",
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:fluwx/fluwx.dart';
|
||||
|
||||
import 'app/app.dart';
|
||||
import 'app/locale_controller.dart';
|
||||
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
// 微信 SDK(卡片分享):WECHAT_APPID 经 --dart-define 注入(微信开放平台
|
||||
// 注册的应用 ID);未配置(空)时跳过注册——分享回退系统分享面板。
|
||||
// Android 需保证打包签名与微信平台注册签名一致,否则拉不起微信。
|
||||
const wechatAppId = String.fromEnvironment('WECHAT_APPID');
|
||||
if (wechatAppId.isNotEmpty) {
|
||||
await Fluwx().registerApi(appId: wechatAppId, doOnIOS: false);
|
||||
}
|
||||
// 启动先读语言偏好再 runApp:首帧即正确语言(无「system 闪变」,
|
||||
// P3 Task 10)。读取失败由 readPersisted 内部兜底为 system。
|
||||
final localeChoice = await LocaleSettings.readPersisted();
|
||||
|
||||
@@ -10,10 +10,12 @@ library;
|
||||
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:dio/dio.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:fluwx/fluwx.dart';
|
||||
import 'package:gal/gal.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
@@ -47,17 +49,72 @@ final urlLauncherProvider = Provider<UrlLauncher>(
|
||||
(url) => launchUrl(Uri.parse(url), mode: LaunchMode.externalApplication),
|
||||
);
|
||||
|
||||
/// 系统分享注入点(生产=share_plus;测试 override)。
|
||||
typedef ShareLauncher = Future<void> Function(String text);
|
||||
/// 分享请求(卡片字段:微信 SDK 用全集;系统分享回退用 title+url)。
|
||||
class ShareCardRequest {
|
||||
const ShareCardRequest({
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.url,
|
||||
this.thumbnailUrl,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final String description;
|
||||
final String url;
|
||||
|
||||
/// 卡片缩略图(封面 URL;null=无图,微信显示默认图标)。
|
||||
final String? thumbnailUrl;
|
||||
}
|
||||
|
||||
/// 分享注入点(生产:微信已配置且安装→WXWebpage 卡片;否则系统分享面板。
|
||||
/// 测试 override)。
|
||||
typedef ShareLauncher = Future<void> Function(ShareCardRequest card);
|
||||
|
||||
final shareLauncherProvider = Provider<ShareLauncher>(
|
||||
// share_plus 11:SharePlus.instance.share(旧 Share.share 已废弃)。
|
||||
(ref) =>
|
||||
(text) async => SharePlus.instance.share(
|
||||
ShareParams(text: text, subject: text.split('\n').first),
|
||||
),
|
||||
(ref) => (card) async {
|
||||
const wechatAppId = String.fromEnvironment('WECHAT_APPID');
|
||||
if (wechatAppId.isNotEmpty && await Fluwx().isWeChatInstalled) {
|
||||
final thumb = await compressShareThumbnail(card.thumbnailUrl);
|
||||
await Fluwx().share(
|
||||
WeChatShareWebPageModel(
|
||||
card.url,
|
||||
title: card.title,
|
||||
description: card.description,
|
||||
thumbData: thumb,
|
||||
scene: WeChatScene.session,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
// 回退:系统分享面板(文本=标题+链接;接收应用自行渲染链接)。
|
||||
// share_plus 11:SharePlus.instance.share(旧 Share.share 已废弃)。
|
||||
await SharePlus.instance.share(
|
||||
ShareParams(text: '${card.title}\n${card.url}', subject: card.title),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
/// 微信卡片缩略图:下载→缩到 240px→PNG(微信 thumbData 限制 ≤32KB)。
|
||||
Future<Uint8List?> compressShareThumbnail(String? url) async {
|
||||
if (url == null) return null;
|
||||
try {
|
||||
final resp = await Dio().get<List<int>>(
|
||||
url,
|
||||
options: Options(responseType: ResponseType.bytes),
|
||||
);
|
||||
final data = Uint8List.fromList(resp.data!);
|
||||
final codec = await ui.instantiateImageCodec(data, targetWidth: 240);
|
||||
final frame = await codec.getNextFrame();
|
||||
final resized = await frame.image.toByteData(
|
||||
format: ui.ImageByteFormat.png,
|
||||
);
|
||||
return resized?.buffer.asUint8List();
|
||||
} on Object {
|
||||
// 缩略图失败不阻塞分享(无图卡片仍可发)
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 海报保存相册注入点(生产=gal;测试 override)。
|
||||
typedef PosterSaver = Future<void> Function(ui.Image image);
|
||||
|
||||
@@ -89,7 +146,28 @@ class ShareQrPage extends ConsumerWidget {
|
||||
String url,
|
||||
String title,
|
||||
) async {
|
||||
await ref.read(shareLauncherProvider)('$title\n$url');
|
||||
final l10n = AppLocalizations.of(context);
|
||||
final localeName = l10n.localeName;
|
||||
final start = formatAlbumDateTime(
|
||||
album.startAt,
|
||||
album.timezone,
|
||||
localeName,
|
||||
);
|
||||
final end = album.endAt == null
|
||||
? null
|
||||
: formatAlbumDateTime(album.endAt!, album.timezone, localeName);
|
||||
final dateRange = end == null ? start : '$start ~ $end';
|
||||
final venue = albumDisplayVenue(album, localeName);
|
||||
final description = venue == null ? dateRange : '$dateRange · $venue';
|
||||
// 缩略图用封面(列表即有签名 URL,减少一次串行请求)。
|
||||
await ref.read(shareLauncherProvider)(
|
||||
ShareCardRequest(
|
||||
title: title,
|
||||
description: description,
|
||||
url: url,
|
||||
thumbnailUrl: album.cover?.url,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
Reference in New Issue
Block a user