fix(share): 图片(type2/3)截图后直接拉系统分享面板(可靠交付),不再走对图片无效的剪贴板/指引/面板
剪贴板PIXELMAP在微信/QQ对话框粘贴不出(§R-19)。图片: 截图(componentSnapshot)/下载→systemImage系统面板→用户选目标App→图片直达+乐观回传; 文本(type1)保留面板+剪贴板+指引不变。删除 writeClipboardImage 及 image/fileIo import。
This commit is contained in:
@@ -1,24 +1,21 @@
|
||||
/**
|
||||
* 分享能力(契约 §8.1/§10.1)。三端统一「剪贴板+指引窗 → 系统分享」,**不依赖任何分享 SDK**。
|
||||
*
|
||||
* H5 调 friendsSharetypeUrlToptitleDescript(data=sharetypeBean JSON):
|
||||
* - sharefriend=="2" → 直接微信指引窗(朋友圈语义,回传 type=2)。
|
||||
* - sharefriend=="1" → 弹自定义面板(微信/QQ/抖音/取消) → 选定渠道。
|
||||
* 选定渠道后按内容类型组装并写剪贴板,再弹 ShareGuideDialog:
|
||||
* - 文本(type1/4/其他):剪贴板 PLAIN_TEXT(title\ndescription,无 url);
|
||||
* - 图片(type2 截图 / type3 图片链接下载,低优先):剪贴板 PIXELMAP(尽力)。
|
||||
* 指引窗动作:打开App(openLink scheme,best-effort) / 用系统分享(systemText|systemImage,可靠) / 取消。
|
||||
* 仅微信乐观回传 sharesuccess:打开/系统分享→{success:2,type};取消→{success:3,type}。QQ/抖音不回传。
|
||||
* H5 调 friendsSharetypeUrlToptitleDescript(data=sharetypeBean JSON),按内容类型分流:
|
||||
* - 图片(type=="2" 截图 / type=="3" 图片链接下载):截图(WebSlot componentSnapshot)/下载落盘后
|
||||
* **直接拉系统分享面板(systemImage)** 把图片交给目标 App;不弹自有面板/指引/不写剪贴板。乐观回传 sharesuccess。
|
||||
* - 文本(其余 type):sharefriend=="1" 弹自定义面板(微信/QQ/抖音)→选定渠道;=="2" 直接微信。
|
||||
* 选定后写剪贴板 PLAIN_TEXT(title\ndescription,无 url) + 弹 ShareGuideDialog:
|
||||
* 打开App(openLink scheme,best-effort) / 用系统分享(systemText) / 取消。仅微信乐观回传 sharesuccess。
|
||||
*
|
||||
* 注:图片"复制剪贴板再去对方App粘贴"能否成功取决于对方是否读 PIXELMAP(鸿蒙不可保证),
|
||||
* 故指引窗"用系统分享"(systemImage) 为可靠兜底;拉起 App 仅打开、不预填(需用户自行粘贴)。
|
||||
* 注:图片"复制剪贴板再去对方App粘贴"鸿蒙下不可靠(对方是否读 PIXELMAP 无保证,§R-19),
|
||||
* 故图片一律走系统分享(对应原工程微信 SDK shareImage 的去SDK等价);拉起 App 仅打开不预填(需自行粘贴)。
|
||||
*/
|
||||
import { systemShare } from '@kit.ShareKit';
|
||||
import { uniformTypeDescriptor as utd } from '@kit.ArkData';
|
||||
import { fileUri, fileIo } from '@kit.CoreFileKit';
|
||||
import { fileUri } from '@kit.CoreFileKit';
|
||||
import { common, bundleManager } from '@kit.AbilityKit';
|
||||
import { BusinessError, pasteboard } from '@kit.BasicServicesKit';
|
||||
import { image } from '@kit.ImageKit';
|
||||
import { util } from '@kit.ArkTS';
|
||||
import { BridgeController } from 'feature_bridge';
|
||||
import { InboundHandlers, OutboundHandlers, SharetypeBean, ShareSuccessResp } from 'contracts';
|
||||
@@ -101,9 +98,14 @@ export class ShareProvider implements CapabilityProvider {
|
||||
this.log.i(`onShare bean sharefriend=${bean.sharefriend} type=${bean.type} `
|
||||
+ `sharetype=${bean.sharetype} url=${bean.webpageUrl} `
|
||||
+ `titleLen=${bean.title.length} descLen=${bean.description.length}`);
|
||||
if (bean.type === '2' || bean.type === '3') {
|
||||
// 图片:截图/下载 → 直接拉系统分享面板(剪贴板对图片不可靠 §R-19),不走面板/指引。
|
||||
this.startImageShare(bean);
|
||||
return;
|
||||
}
|
||||
if (bean.sharefriend === '2') {
|
||||
// 朋友圈语义:不弹面板,直接微信指引窗(契约 §10.1)。
|
||||
this.startShare(bean, 'wechat');
|
||||
// 文本·朋友圈语义:不弹面板,直接微信指引窗(契约 §10.1)。
|
||||
this.startTextShare(bean, 'wechat');
|
||||
return;
|
||||
}
|
||||
this.seq += 1;
|
||||
@@ -116,7 +118,7 @@ export class ShareProvider implements CapabilityProvider {
|
||||
private onPlatformChosen(bean: SharetypeBean, payload?: Object): void {
|
||||
const platform: string = payload !== undefined ? (payload as SharePanelResult).platform : 'cancel';
|
||||
if (platform === 'wechat' || platform === 'qq' || platform === 'douyin') {
|
||||
this.startShare(bean, platform);
|
||||
this.startTextShare(bean, platform);
|
||||
}
|
||||
// cancel:不分享、不回传。
|
||||
}
|
||||
@@ -142,7 +144,7 @@ export class ShareProvider implements CapabilityProvider {
|
||||
this.reportResult(3, up.type === '1' ? 1 : 2);
|
||||
return;
|
||||
}
|
||||
this.onImageReady(bean, 'wechat', filePath);
|
||||
this.deliverImage(ctx.uiAbilityContext, filePath, bean);
|
||||
}
|
||||
|
||||
// —— 组装 + 弹指引窗 ——
|
||||
@@ -152,79 +154,64 @@ export class ShareProvider implements CapabilityProvider {
|
||||
return (v === undefined || v === null) ? '' : `${v}`;
|
||||
}
|
||||
|
||||
private startShare(bean: SharetypeBean, platform: string): void {
|
||||
if (bean.type === '2') {
|
||||
this.log.i(`startShare -> canvas image (platform=${platform})`);
|
||||
this.startCanvasImageShare(bean, platform);
|
||||
} else if (bean.type === '3') {
|
||||
this.log.i(`startShare -> link image (platform=${platform})`);
|
||||
this.startLinkImageShare(bean, platform);
|
||||
} else {
|
||||
this.log.i(`startShare -> text (platform=${platform}, type='${bean.type}')`);
|
||||
this.startTextShare(bean, platform);
|
||||
}
|
||||
}
|
||||
|
||||
private startTextShare(bean: SharetypeBean, platform: string): void {
|
||||
this.writeClipboardText(buildShareText(bean.title, bean.description));
|
||||
this.guide(bean, platform, 'text', '');
|
||||
this.guide(bean, platform);
|
||||
}
|
||||
|
||||
/** type2:截当前 Web canvas → 落盘 → 图片指引窗(捕获失败降级文本指引,非"图片转文本"语义)。 */
|
||||
private startCanvasImageShare(bean: SharetypeBean, platform: string): void {
|
||||
const ctx = this.ctx;
|
||||
if (ctx === undefined) {
|
||||
return;
|
||||
}
|
||||
const cap = ctx.captureCanvas;
|
||||
if (cap === undefined) {
|
||||
this.startTextShare(bean, platform);
|
||||
return;
|
||||
}
|
||||
// sharetype 不是 canvasId(原工程为"闲聊"二级字段),截图始终取默认首个 <canvas>,故传空串。
|
||||
cap('').then((dataUrl: string) => {
|
||||
this.log.i(`canvas captured dataUrlLen=${dataUrl.length}`);
|
||||
const filePath: string = dataUrl !== '' ? this.saveDataUrlToFile(ctx.uiAbilityContext, dataUrl) : '';
|
||||
if (filePath !== '') {
|
||||
this.onImageReady(bean, platform, filePath);
|
||||
} else {
|
||||
this.log.w('canvas capture empty (页面可能无 <canvas>); fallback to text guide');
|
||||
this.startTextShare(bean, platform);
|
||||
}
|
||||
}).catch((e: Error) => {
|
||||
this.log.w(`captureCanvas failed: ${e.message}; fallback to text guide`);
|
||||
this.startTextShare(bean, platform);
|
||||
});
|
||||
}
|
||||
|
||||
/** type3(低优先,需确认 H5 是否使用):从 webpageUrl 下载图片 → 落盘 → 图片指引窗(失败降级文本)。 */
|
||||
private startLinkImageShare(bean: SharetypeBean, platform: string): void {
|
||||
/**
|
||||
* 图片分享(type=2 截图 / type=3 链接下载):落盘后**直接拉系统分享面板**把图片交给目标 App。
|
||||
* 剪贴板粘贴对图片不可靠(§R-19),故图片不走剪贴板/指引/自有面板。乐观回传 sharesuccess。
|
||||
*/
|
||||
private startImageShare(bean: SharetypeBean): void {
|
||||
const ctx = this.ctx;
|
||||
if (ctx === undefined) {
|
||||
return;
|
||||
}
|
||||
const uiCtx: common.UIAbilityContext = ctx.uiAbilityContext;
|
||||
if (bean.type === '3') {
|
||||
const url: string = bean.webpageUrl;
|
||||
if (url === '' || !(url.startsWith('http://') || url.startsWith('https://'))) {
|
||||
this.startTextShare(bean, platform);
|
||||
this.log.w('link image bad url; abort image share');
|
||||
return;
|
||||
}
|
||||
this.seq += 1;
|
||||
const ext: string = url.toLowerCase().indexOf('.png') >= 0 ? '.png' : '.jpg';
|
||||
const savePath: string = `${ctx.uiAbilityContext.filesDir}/share/link_${this.seq}${ext}`;
|
||||
const savePath: string = `${uiCtx.filesDir}/share/link_${this.seq}${ext}`;
|
||||
Downloader.download(url, savePath).then(() => {
|
||||
this.onImageReady(bean, platform, savePath);
|
||||
this.deliverImage(uiCtx, savePath, bean);
|
||||
}).catch((e: Error) => {
|
||||
this.log.w(`link image download failed: ${e.message}; fallback to text guide`);
|
||||
this.startTextShare(bean, platform);
|
||||
this.log.w(`link image download failed: ${e.message}`);
|
||||
});
|
||||
return;
|
||||
}
|
||||
// type=2:截当前激活 Web 的渲染像素(WebSlot 用 componentSnapshot)。
|
||||
const cap = ctx.captureCanvas;
|
||||
if (cap === undefined) {
|
||||
this.log.w('captureCanvas unavailable; abort image share');
|
||||
return;
|
||||
}
|
||||
cap('').then((dataUrl: string) => {
|
||||
this.log.i(`canvas captured dataUrlLen=${dataUrl.length}`);
|
||||
const filePath: string = dataUrl !== '' ? this.saveDataUrlToFile(uiCtx, dataUrl) : '';
|
||||
if (filePath !== '') {
|
||||
this.deliverImage(uiCtx, filePath, bean);
|
||||
} else {
|
||||
this.log.w('canvas capture empty; abort image share');
|
||||
}
|
||||
}).catch((e: Error) => {
|
||||
this.log.w(`captureCanvas failed: ${e.message}`);
|
||||
});
|
||||
}
|
||||
|
||||
private onImageReady(bean: SharetypeBean, platform: string, filePath: string): void {
|
||||
this.writeClipboardImage(filePath);
|
||||
this.guide(bean, platform, 'image', filePath);
|
||||
/** 图片落盘后:拉系统分享面板把图片交给用户选择的 App + 乐观回传 sharesuccess。 */
|
||||
private deliverImage(uiCtx: common.UIAbilityContext, filePath: string, bean: SharetypeBean): void {
|
||||
this.systemImage(uiCtx, filePath, bean);
|
||||
this.reportResult(2, bean.sharefriend === '1' ? 1 : 2);
|
||||
}
|
||||
|
||||
private guide(bean: SharetypeBean, platform: string, kind: string, filePath: string): void {
|
||||
/** 文本分享指引窗(仅文本类用;图片走系统分享,不经此)。 */
|
||||
private guide(bean: SharetypeBean, platform: string): void {
|
||||
// 退订上一个可能悬挂的指引监听(用户上次未操作就再次触发分享)。
|
||||
if (this.guideCancelFn !== undefined) {
|
||||
this.guideCancelFn();
|
||||
@@ -234,13 +221,13 @@ export class ShareProvider implements CapabilityProvider {
|
||||
const resultEvent: string = `share.guide.${this.instanceId}.${this.seq}`;
|
||||
this.guideCancelFn = EventBus.once(resultEvent, (p) => {
|
||||
this.guideCancelFn = undefined;
|
||||
this.onGuideAction(bean, platform, kind, filePath, p);
|
||||
this.onGuideAction(bean, platform, p);
|
||||
});
|
||||
const req: ShareGuideRequest = { platform, contentKind: kind, resultEvent };
|
||||
const req: ShareGuideRequest = { platform, contentKind: 'text', resultEvent };
|
||||
EventBus.emit(ShareEvents.SHOW_GUIDE, req);
|
||||
}
|
||||
|
||||
private onGuideAction(bean: SharetypeBean, platform: string, kind: string, filePath: string, payload?: Object): void {
|
||||
private onGuideAction(bean: SharetypeBean, platform: string, payload?: Object): void {
|
||||
const ctx = this.ctx;
|
||||
if (ctx === undefined) {
|
||||
return;
|
||||
@@ -255,11 +242,7 @@ export class ShareProvider implements CapabilityProvider {
|
||||
this.reportResult(2, reportType);
|
||||
}
|
||||
} else if (action === 'system') {
|
||||
if (kind === 'image' && filePath !== '') {
|
||||
this.systemImage(uiCtx, filePath, bean);
|
||||
} else {
|
||||
this.systemText(uiCtx, bean);
|
||||
}
|
||||
if (platform === 'wechat') {
|
||||
this.reportResult(2, reportType);
|
||||
}
|
||||
@@ -314,38 +297,7 @@ export class ShareProvider implements CapabilityProvider {
|
||||
}
|
||||
}
|
||||
|
||||
/** 图片写剪贴板(尽力;对方能否粘贴 PIXELMAP 不可保证,systemImage 才是可靠交付)。 */
|
||||
private writeClipboardImage(filePath: string): void {
|
||||
let file: fileIo.File;
|
||||
try {
|
||||
file = fileIo.openSync(filePath, fileIo.OpenMode.READ_ONLY);
|
||||
} catch (e) {
|
||||
this.log.w(`clipboard image open failed: ${(e as Error).message}`);
|
||||
return;
|
||||
}
|
||||
const source: image.ImageSource = image.createImageSource(file.fd);
|
||||
source.createPixelMap().then((pm: image.PixelMap) => {
|
||||
let data: pasteboard.PasteData;
|
||||
try {
|
||||
data = pasteboard.createData(pasteboard.MIMETYPE_PIXELMAP, pm);
|
||||
} catch (e) {
|
||||
this.log.w(`clipboard image init failed: ${(e as Error).message}`);
|
||||
pm.release();
|
||||
return;
|
||||
}
|
||||
pasteboard.getSystemPasteboard().setData(data)
|
||||
.then(() => this.log.i('clipboard image set ok'))
|
||||
.catch((e: BusinessError) => this.log.w(`clipboard image failed: ${e.code} ${e.message}`))
|
||||
.finally(() => pm.release());
|
||||
}).catch((e: BusinessError) => {
|
||||
this.log.w(`createPixelMap failed: ${e.code} ${e.message}`);
|
||||
}).finally(() => {
|
||||
source.release();
|
||||
fileIo.closeSync(file);
|
||||
});
|
||||
}
|
||||
|
||||
// —— 系统分享(可靠兜底;文本不下发 url)——
|
||||
// —— 系统分享(图片可靠交付 / 文本兜底;文本不下发 url)——
|
||||
|
||||
private systemText(uiCtx: common.UIAbilityContext, bean: SharetypeBean): void {
|
||||
const text: string = buildShareText(bean.title, bean.description);
|
||||
|
||||
Reference in New Issue
Block a user