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:
浅念
2026-09-23 16:05:53 +08:00
parent 77b1519209
commit 5a207b395e
20 changed files with 288 additions and 22 deletions
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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 => '生成二维码海报';
+1 -1
View File
@@ -422,7 +422,7 @@
"bannerPickerHint": "点击选择宣传图(展示在观众页顶部)",
"bannerSelected": "已选择宣传图——保存时上传",
"bannerUploadFailed": "宣传图上传失败",
"shareCardMode": "分享到微信 / 其他应用",
"shareCardMode": "分享到...",
"sharePosterMode": "生成二维码海报",
"posterDownload": "下载海报图",
"posterSaved": "海报已保存到相册",
+8
View File
@@ -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();
+86 -8
View File
@@ -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
+8
View File
@@ -418,6 +418,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
fluwx:
dependency: "direct main"
description:
name: fluwx
sha256: "4b526d281be8560a490bd1b945373a23ab2a12088c8fc5997ffb31cc6fb41082"
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.0.2"
fuchsia_remote_debug_protocol:
dependency: transitive
description: flutter
+1
View File
@@ -38,6 +38,7 @@ dependencies:
url_launcher: ^6.3.2
share_plus: ^13.3.0
gal: ^2.3.3
fluwx: ^6.0.2
dev_dependencies:
flutter_test:
@@ -64,7 +64,7 @@ void main() {
reason: '开始时间(相册时区;结束未定不伪造区间)+ 场地(展示回退链)',
);
// UI06 三方式:系统分享(默认)/ 生成二维码海报 / 复制链接。
expect(find.text('Share to WeChat / other apps'), findsOneWidget);
expect(find.text('Share to...'), findsOneWidget);
expect(find.text('Generate QR poster'), findsOneWidget);
expect(find.text('Copy link'), findsOneWidget);
// 可选中链接(二维码与复制共用同一 URL)。