接入RPG封面参考图原生导入
RPG作品封面参考图在原生壳内优先走宿主图片导入 封面参考图取消原生选择时不触发浏览器文件输入 同步封面参考图测试、宿主壳协议和共享记忆
This commit is contained in:
@@ -2280,3 +2280,70 @@ test('作品封面取消原生导入时不触发浏览器文件输入', async ()
|
||||
expect(inputClickSpy).not.toHaveBeenCalled();
|
||||
expect(screen.queryByText('裁剪上传封面')).toBeNull();
|
||||
});
|
||||
|
||||
test('作品封面参考图在原生壳内优先走 HostBridge 图片导入', async () => {
|
||||
vi.mocked(hostBridgeServices.canUseNativeHostCapability).mockImplementation(
|
||||
(capability) => capability === 'file.importImage',
|
||||
);
|
||||
vi.mocked(hostBridgeServices.importHostImageFile).mockResolvedValue({
|
||||
action: 'selected',
|
||||
fileName: 'native-cover-reference.png',
|
||||
base64Data:
|
||||
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO7+7aQAAAAASUVORK5CYII=',
|
||||
mimeType: 'image/png',
|
||||
bytes: 68,
|
||||
});
|
||||
|
||||
class MockFileReader {
|
||||
result: string | null = null;
|
||||
error: Error | null = null;
|
||||
onload: null | (() => void) = null;
|
||||
onerror: null | (() => void) = null;
|
||||
|
||||
readAsDataURL() {
|
||||
this.result =
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO7+7aQAAAAASUVORK5CYII=';
|
||||
this.onload?.();
|
||||
}
|
||||
}
|
||||
|
||||
vi.stubGlobal('FileReader', MockFileReader as unknown as typeof FileReader);
|
||||
const inputClickSpy = vi
|
||||
.spyOn(HTMLInputElement.prototype, 'click')
|
||||
.mockImplementation(() => undefined);
|
||||
|
||||
const user = userEvent.setup();
|
||||
render(<CoverEditorFlowHarness />);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'AI 生成' }));
|
||||
await user.click(screen.getByText('上传封面参考图'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(hostBridgeServices.importHostImageFile).toHaveBeenCalledTimes(1);
|
||||
expect(screen.getByText('已载入封面参考图')).toBeTruthy();
|
||||
});
|
||||
expect(inputClickSpy).not.toHaveBeenCalled();
|
||||
expect(screen.getByRole('img', { name: '封面参考图' })).toBeTruthy();
|
||||
});
|
||||
|
||||
test('作品封面参考图取消原生导入时不触发浏览器文件输入', async () => {
|
||||
vi.mocked(hostBridgeServices.canUseNativeHostCapability).mockImplementation(
|
||||
(capability) => capability === 'file.importImage',
|
||||
);
|
||||
vi.mocked(hostBridgeServices.importHostImageFile).mockResolvedValue(false);
|
||||
const inputClickSpy = vi
|
||||
.spyOn(HTMLInputElement.prototype, 'click')
|
||||
.mockImplementation(() => undefined);
|
||||
|
||||
const user = userEvent.setup();
|
||||
render(<CoverEditorFlowHarness />);
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'AI 生成' }));
|
||||
await user.click(screen.getByText('上传封面参考图'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(hostBridgeServices.importHostImageFile).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
expect(inputClickSpy).not.toHaveBeenCalled();
|
||||
expect(screen.queryByText('已载入封面参考图')).toBeNull();
|
||||
});
|
||||
|
||||
@@ -3821,6 +3821,7 @@ function CoverImageGenerationModal({
|
||||
useState<CustomWorldCoverAssetResult | null>(null);
|
||||
const [isExitConfirmOpen, setIsExitConfirmOpen] = useState(false);
|
||||
const referenceImageInputId = useId();
|
||||
const referenceImageInputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
const previewImageSrc =
|
||||
latestResult?.imageSrc || initialPresentation.imageSrc;
|
||||
@@ -3834,11 +3835,7 @@ function CoverImageGenerationModal({
|
||||
.map((role) => role.name)
|
||||
.filter(Boolean);
|
||||
|
||||
const handleReferenceImageChange = async (
|
||||
event: ChangeEvent<HTMLInputElement>,
|
||||
) => {
|
||||
const file = event.target.files?.[0];
|
||||
event.currentTarget.value = '';
|
||||
const setCoverReferenceImageFromFile = async (file: File) => {
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
@@ -3856,6 +3853,39 @@ function CoverImageGenerationModal({
|
||||
}
|
||||
};
|
||||
|
||||
const handleReferenceImageChange = async (
|
||||
event: ChangeEvent<HTMLInputElement>,
|
||||
) => {
|
||||
const file = event.target.files?.[0];
|
||||
event.currentTarget.value = '';
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
await setCoverReferenceImageFromFile(file);
|
||||
};
|
||||
|
||||
const handleReferenceImageUploadClick = () => {
|
||||
if (!canUseNativeHostCapability('file.importImage')) {
|
||||
referenceImageInputRef.current?.click();
|
||||
return;
|
||||
}
|
||||
|
||||
void (async () => {
|
||||
const importedImage = await importHostImageFile();
|
||||
if (!importedImage) {
|
||||
return;
|
||||
}
|
||||
|
||||
await setCoverReferenceImageFromFile(
|
||||
puzzleReferenceImageDataUrlToFile(
|
||||
`data:${importedImage.mimeType};base64,${importedImage.base64Data}`,
|
||||
importedImage.fileName,
|
||||
),
|
||||
);
|
||||
})();
|
||||
};
|
||||
|
||||
const handleRequestClose = () => {
|
||||
if (isGenerating) {
|
||||
return;
|
||||
@@ -3943,8 +3973,15 @@ function CoverImageGenerationModal({
|
||||
size="compact"
|
||||
surface="editorDark"
|
||||
disabled={isGenerating}
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
if (!isGenerating) {
|
||||
handleReferenceImageUploadClick();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
ref={referenceImageInputRef}
|
||||
id={referenceImageInputId}
|
||||
type="file"
|
||||
accept="image/png,image/jpeg,image/webp"
|
||||
|
||||
Reference in New Issue
Block a user