返回 AiToEarn
VideoCoverSeting.tsx
1 import {
2 ForwardedRef,
3 forwardRef,
4 memo,
5 useEffect,
6 useRef,
7 useState,
8 } from 'react';
9 import ImgChoose, {
10 formatImg,
11 getImgFile,
12 IImgFile,
13 } from '@/components/Choose/ImgChoose';
14 import styles from './videoCoverSeting.module.scss';
15 import { Alert, Button, message, Modal, Slider, Spin } from 'antd';
16 import { IVideoFile } from '@/components/Choose/VideoChoose';
17 import Cropper from 'cropperjs';
18 import 'cropperjs/dist/cropper.css';
19 import { ipcGetVideoCover } from '@/icp/tools';
20 import { formatSeconds } from '@/utils';
21 import { icpSaveFile } from '@/icp/view';
22 import { useVideoPageStore } from '@/views/publish/children/videoPage/useVideoPageStore';
23 import { useShallow } from 'zustand/react/shallow';
24 import { CloseCircleOutlined } from '@ant-design/icons';
25
26 export interface IVideoCoverSetingRef {}
27
28 export interface IVideoCoverSetingProps {
29 // 封面选择完成
30 onChoosed: (imgFile: IImgFile) => void;
31 // 当前选择的封面
32 value?: IImgFile;
33 // 需要截帧的视频
34 videoFile?: IVideoFile;
35 // 保存图片的唯一值
36 saveImgId: string;
37 // 关闭按钮点击事件,如果有这个事件就会显示关闭按钮
38 onClose?: () => void;
39 }
40
41 // 保存裁剪的图片
42 export const saveCropperImage = (
43 filename: string,
44 file: Blob,
45 ): Promise<string> => {
46 return new Promise((resolve) => {
47 const reader = new FileReader();
48 reader.onload = async function (event) {
49 const arrayBuffer = event.target!.result as ArrayBuffer;
50 const uint8Array = new Uint8Array(arrayBuffer);
51
52 const imgPath = await icpSaveFile({
53 file: uint8Array,
54 saveDir: '/images/cropper',
55 filename: filename,
56 });
57 resolve(imgPath);
58 };
59 reader.readAsArrayBuffer(file);
60 });
61 };
62
63 // 视频封面设置
64 const VideoCoverSeting = memo(
65 forwardRef(
66 (
67 {
68 onChoosed,
69 value,
70 videoFile,
71 saveImgId,
72 onClose,
73 }: IVideoCoverSetingProps,
74 ref: ForwardedRef<IVideoCoverSetingRef>,
75 ) => {
76 const [imgFile, setImgFile] = useState<IImgFile>();
77 const [videoCoverSetingModal, setVideoCoverSetingModal] = useState(false);
78 const cropper = useRef<Cropper>();
79 const cropperImg = useRef<HTMLImageElement>(null);
80 const [videoCoverLoading, setVideoCoverLoading] = useState(false);
81 const [sliderVal, setSliderVal] = useState(0);
82 const { operateId } = useVideoPageStore(
83 useShallow((state) => ({
84 operateId: state.operateId,
85 })),
86 );
87
88 useEffect(() => {
89 if (!videoCoverSetingModal) return;
90 if (value) {
91 setImgFile(value);
92 return;
93 }
94 getVideoCover(0);
95 }, [videoCoverSetingModal]);
96
97 useEffect(() => {
98 if (!imgFile) return;
99 initCropper();
100 }, [imgFile]);
101
102 /* 获取封面 */
103 const getVideoCover = async (n: number) => {
104 if (!videoFile!.videoPath) return;
105 setSliderVal(n);
106 setVideoCoverLoading(true);
107 const pathVideo = await ipcGetVideoCover(
108 videoFile!.videoPath!,
109 formatSeconds(n),
110 );
111 const imgFile = await getImgFile(pathVideo);
112 setVideoCoverLoading(false);
113 setImgFile(imgFile);
114 };
115
116 const close = () => {
117 setVideoCoverSetingModal(false);
118 };
119
120 // 初始化裁剪工具
121 const initCropper = () => {
122 if (!cropperImg.current) return;
123
124 if (cropper.current) {
125 cropper.current.destroy();
126 cropper.current = undefined;
127 }
128
129 cropper.current = new Cropper(cropperImg.current!, {
130 viewMode: 2,
131 zoomable: false,
132 minCropBoxWidth: 100,
133 minCropBoxHeight: 100,
134 ready() {
135 cropper.current!.setCropBoxData({
136 left: 0,
137 top: 0,
138 width: cropperImg.current!.naturalWidth,
139 height: cropperImg.current!.naturalHeight,
140 });
141 },
142 });
143 };
144
145 return (
146 <>
147 <div
148 className={styles.videoCoverSeting}
149 onClick={() => {
150 if (!videoFile) return message.warning('您必须上传一个视频!');
151 setVideoCoverSetingModal(true);
152 }}
153 >
154 <div className="videoCoverSeting-img">
155 {value && (
156 <div className="videoCoverSeting-choosed">
157 <img src={value?.imgUrl} />
158 {onClose && (
159 <CloseCircleOutlined
160 onClick={(e) => {
161 e.stopPropagation();
162 onClose();
163 }}
164 />
165 )}
166 </div>
167 )}
168 </div>
169 <div className="videoCoverSeting-text">上传图片</div>
170 </div>
171
172 <Modal
173 width={600}
174 title="设置封面"
175 maskClosable={false}
176 open={videoCoverSetingModal}
177 onCancel={close}
178 onOk={async () => {
179 const canvas = cropper.current!.getCroppedCanvas();
180 canvas.toBlob(async function (blob) {
181 const imgPath = await saveCropperImage(
182 `${operateId}_${saveImgId}.${imgFile?.file.type.split('/')[1]}`,
183 blob!,
184 );
185 const cover = await formatImg({
186 blob: blob!,
187 path: imgPath,
188 });
189 onChoosed(cover);
190 close();
191 }, 'image/png');
192 }}
193 >
194 <Spin spinning={videoCoverLoading}>
195 <div className={styles.videoCoverSetingModal}>
196 <div className="videoCoverSetingModal-top">
197 <Alert
198 message="支持常用图片格式上传,暂不支持 GIF,上传后图片将按平台要求自动裁剪"
199 type="info"
200 showIcon
201 />
202 <ImgChoose
203 onChoose={(imgFile) => {
204 if (!imgFile) return;
205 setImgFile(imgFile);
206 }}
207 >
208 <Button>本地上传</Button>
209 </ImgChoose>
210 </div>
211
212 <div className="videoCoverSetingModal-cropper">
213 <img
214 style={{ opacity: imgFile?.imgUrl ? '1' : '0' }}
215 ref={cropperImg}
216 src={imgFile?.imgUrl || '/'}
217 />
218 </div>
219
220 <Slider
221 value={sliderVal}
222 style={{ margin: '50px 0' }}
223 step={1}
224 min={0}
225 max={videoFile?.duration}
226 onChange={setSliderVal}
227 onChangeComplete={getVideoCover}
228 />
229 </div>
230 </Spin>
231 </Modal>
232 </>
233 );
234 },
235 ),
236 );
237 VideoCoverSeting.displayName = 'VideoCoverSeting';
238
239 export default VideoCoverSeting;
240
240 lines Plain Text