mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-20 14:39:25 +08:00
聊天页面支持实况解析;朋友圈页面优化
This commit is contained in:
@@ -397,7 +397,7 @@ function createVideoPlayerWindow(videoPath: string, videoWidth?: number, videoHe
|
||||
/**
|
||||
* 创建独立的图片查看窗口
|
||||
*/
|
||||
function createImageViewerWindow(imagePath: string) {
|
||||
function createImageViewerWindow(imagePath: string, liveVideoPath?: string) {
|
||||
const isDev = !!process.env.VITE_DEV_SERVER_URL
|
||||
const iconPath = isDev
|
||||
? join(__dirname, '../public/icon.ico')
|
||||
@@ -430,7 +430,8 @@ function createImageViewerWindow(imagePath: string) {
|
||||
win.show()
|
||||
})
|
||||
|
||||
const imageParam = `imagePath=${encodeURIComponent(imagePath)}`
|
||||
let imageParam = `imagePath=${encodeURIComponent(imagePath)}`
|
||||
if (liveVideoPath) imageParam += `&liveVideoPath=${encodeURIComponent(liveVideoPath)}`
|
||||
|
||||
if (process.env.VITE_DEV_SERVER_URL) {
|
||||
win.loadURL(`${process.env.VITE_DEV_SERVER_URL}#/image-viewer-window?${imageParam}`)
|
||||
@@ -1119,8 +1120,18 @@ function registerIpcHandlers() {
|
||||
})
|
||||
|
||||
// 打开图片查看窗口
|
||||
ipcMain.handle('window:openImageViewerWindow', (_, imagePath: string) => {
|
||||
createImageViewerWindow(imagePath)
|
||||
ipcMain.handle('window:openImageViewerWindow', async (_, imagePath: string, liveVideoPath?: string) => {
|
||||
// 如果是 dataUrl,写入临时文件
|
||||
if (imagePath.startsWith('data:')) {
|
||||
const commaIdx = imagePath.indexOf(',')
|
||||
const meta = imagePath.slice(5, commaIdx) // e.g. "image/jpeg;base64"
|
||||
const ext = meta.split('/')[1]?.split(';')[0] || 'jpg'
|
||||
const tmpPath = join(app.getPath('temp'), `weflow_preview_${Date.now()}.${ext}`)
|
||||
await writeFile(tmpPath, Buffer.from(imagePath.slice(commaIdx + 1), 'base64'))
|
||||
createImageViewerWindow(`file://${tmpPath.replace(/\\/g, '/')}`, liveVideoPath)
|
||||
} else {
|
||||
createImageViewerWindow(imagePath, liveVideoPath)
|
||||
}
|
||||
})
|
||||
|
||||
// 完成引导,关闭引导窗口并显示主窗口
|
||||
|
||||
@@ -78,8 +78,8 @@ contextBridge.exposeInMainWorld('electronAPI', {
|
||||
ipcRenderer.invoke('window:openVideoPlayerWindow', videoPath, videoWidth, videoHeight),
|
||||
resizeToFitVideo: (videoWidth: number, videoHeight: number) =>
|
||||
ipcRenderer.invoke('window:resizeToFitVideo', videoWidth, videoHeight),
|
||||
openImageViewerWindow: (imagePath: string) =>
|
||||
ipcRenderer.invoke('window:openImageViewerWindow', imagePath),
|
||||
openImageViewerWindow: (imagePath: string, liveVideoPath?: string) =>
|
||||
ipcRenderer.invoke('window:openImageViewerWindow', imagePath, liveVideoPath),
|
||||
openChatHistoryWindow: (sessionId: string, messageId: number) =>
|
||||
ipcRenderer.invoke('window:openChatHistoryWindow', sessionId, messageId)
|
||||
},
|
||||
|
||||
@@ -45,6 +45,7 @@ type DecryptResult = {
|
||||
localPath?: string
|
||||
error?: string
|
||||
isThumb?: boolean // 是否是缩略图(没有高清图时返回缩略图)
|
||||
liveVideoPath?: string // 实况照片的视频路径
|
||||
}
|
||||
|
||||
type HardlinkState = {
|
||||
@@ -61,6 +62,7 @@ export class ImageDecryptService {
|
||||
private cacheIndexed = false
|
||||
private cacheIndexing: Promise<void> | null = null
|
||||
private updateFlags = new Map<string, boolean>()
|
||||
private noLiveSet = new Set<string>() // 已确认无 live 视频的图片路径
|
||||
|
||||
private logInfo(message: string, meta?: Record<string, unknown>): void {
|
||||
if (!this.configService.get('logEnabled')) return
|
||||
@@ -116,8 +118,9 @@ export class ImageDecryptService {
|
||||
} else {
|
||||
this.updateFlags.delete(key)
|
||||
}
|
||||
const liveVideoPath = isThumb ? undefined : this.checkLiveVideoCache(cached)
|
||||
this.emitCacheResolved(payload, key, dataUrl || this.filePathToUrl(cached))
|
||||
return { success: true, localPath: dataUrl || this.filePathToUrl(cached), hasUpdate }
|
||||
return { success: true, localPath: dataUrl || this.filePathToUrl(cached), hasUpdate, liveVideoPath }
|
||||
}
|
||||
if (cached && !this.isImageFile(cached)) {
|
||||
this.resolvedCache.delete(key)
|
||||
@@ -136,8 +139,9 @@ export class ImageDecryptService {
|
||||
} else {
|
||||
this.updateFlags.delete(key)
|
||||
}
|
||||
const liveVideoPath = isThumb ? undefined : this.checkLiveVideoCache(existing)
|
||||
this.emitCacheResolved(payload, key, dataUrl || this.filePathToUrl(existing))
|
||||
return { success: true, localPath: dataUrl || this.filePathToUrl(existing), hasUpdate }
|
||||
return { success: true, localPath: dataUrl || this.filePathToUrl(existing), hasUpdate, liveVideoPath }
|
||||
}
|
||||
}
|
||||
this.logInfo('未找到缓存', { md5: payload.imageMd5, datName: payload.imageDatName })
|
||||
@@ -156,8 +160,9 @@ export class ImageDecryptService {
|
||||
if (cached && existsSync(cached) && this.isImageFile(cached)) {
|
||||
const dataUrl = this.fileToDataUrl(cached)
|
||||
const localPath = dataUrl || this.filePathToUrl(cached)
|
||||
const liveVideoPath = this.isThumbnailPath(cached) ? undefined : this.checkLiveVideoCache(cached)
|
||||
this.emitCacheResolved(payload, cacheKey, localPath)
|
||||
return { success: true, localPath }
|
||||
return { success: true, localPath, liveVideoPath }
|
||||
}
|
||||
if (cached && !this.isImageFile(cached)) {
|
||||
this.resolvedCache.delete(cacheKey)
|
||||
@@ -235,8 +240,9 @@ export class ImageDecryptService {
|
||||
const dataUrl = this.fileToDataUrl(existing)
|
||||
const localPath = dataUrl || this.filePathToUrl(existing)
|
||||
const isThumb = this.isThumbnailPath(existing)
|
||||
const liveVideoPath = isThumb ? undefined : this.checkLiveVideoCache(existing)
|
||||
this.emitCacheResolved(payload, cacheKey, localPath)
|
||||
return { success: true, localPath, isThumb }
|
||||
return { success: true, localPath, isThumb, liveVideoPath }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,7 +302,15 @@ export class ImageDecryptService {
|
||||
const dataUrl = this.bufferToDataUrl(decrypted, finalExt)
|
||||
const localPath = dataUrl || this.filePathToUrl(outputPath)
|
||||
this.emitCacheResolved(payload, cacheKey, localPath)
|
||||
return { success: true, localPath, isThumb }
|
||||
|
||||
// 检测实况照片(Motion Photo)
|
||||
let liveVideoPath: string | undefined
|
||||
if (!isThumb && (finalExt === '.jpg' || finalExt === '.jpeg')) {
|
||||
const videoPath = await this.extractMotionPhotoVideo(outputPath, decrypted)
|
||||
if (videoPath) liveVideoPath = this.filePathToUrl(videoPath)
|
||||
}
|
||||
|
||||
return { success: true, localPath, isThumb, liveVideoPath }
|
||||
} catch (e) {
|
||||
this.logError('解密失败', e, { md5: payload.imageMd5, datName: payload.imageDatName })
|
||||
return { success: false, error: String(e) }
|
||||
@@ -1681,6 +1695,76 @@ export class ImageDecryptService {
|
||||
return mostCommonKey
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查图片对应的 live 视频缓存,返回 file:// URL 或 undefined
|
||||
* 已确认无 live 的路径会被记录,下次直接跳过
|
||||
*/
|
||||
private checkLiveVideoCache(imagePath: string): string | undefined {
|
||||
if (this.noLiveSet.has(imagePath)) return undefined
|
||||
const livePath = imagePath.replace(/\.(jpg|jpeg|png)$/i, '_live.mp4')
|
||||
if (existsSync(livePath)) return this.filePathToUrl(livePath)
|
||||
this.noLiveSet.add(imagePath)
|
||||
return undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测并分离 Motion Photo(实况照片)
|
||||
* Google Motion Photo = JPEG + MP4 拼接在一起
|
||||
* 返回视频文件路径,如果不是实况照片则返回 null
|
||||
*/
|
||||
private async extractMotionPhotoVideo(imagePath: string, imageBuffer: Buffer): Promise<string | null> {
|
||||
// 只处理 JPEG 文件
|
||||
if (imageBuffer.length < 8) return null
|
||||
if (imageBuffer[0] !== 0xff || imageBuffer[1] !== 0xd8) return null
|
||||
|
||||
// 从末尾向前搜索 MP4 ftyp 原子签名
|
||||
// ftyp 原子结构: [4字节大小][ftyp(66 74 79 70)][品牌...]
|
||||
// 实际起始位置在 ftyp 前4字节(大小字段)
|
||||
const ftypSig = [0x66, 0x74, 0x79, 0x70] // 'ftyp'
|
||||
let videoOffset: number | null = null
|
||||
|
||||
const searchEnd = Math.max(0, imageBuffer.length - 8)
|
||||
for (let i = searchEnd; i > 0; i--) {
|
||||
if (imageBuffer[i] === ftypSig[0] &&
|
||||
imageBuffer[i + 1] === ftypSig[1] &&
|
||||
imageBuffer[i + 2] === ftypSig[2] &&
|
||||
imageBuffer[i + 3] === ftypSig[3]) {
|
||||
// ftyp 前4字节是 box size,实际 MP4 从这里开始
|
||||
videoOffset = i - 4
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 备用:从 XMP 元数据中读取偏移量
|
||||
if (videoOffset === null || videoOffset <= 0) {
|
||||
try {
|
||||
const text = imageBuffer.toString('latin1')
|
||||
const match = text.match(/MediaDataOffset="(\d+)"/i) || text.match(/MicroVideoOffset="(\d+)"/i)
|
||||
if (match) {
|
||||
const offset = parseInt(match[1], 10)
|
||||
if (offset > 0 && offset < imageBuffer.length) {
|
||||
videoOffset = imageBuffer.length - offset
|
||||
}
|
||||
}
|
||||
} catch { }
|
||||
}
|
||||
|
||||
if (videoOffset === null || videoOffset <= 100) return null
|
||||
|
||||
// 验证视频部分确实以有效 MP4 数据开头
|
||||
const videoStart = imageBuffer[videoOffset + 4] === 0x66 &&
|
||||
imageBuffer[videoOffset + 5] === 0x74 &&
|
||||
imageBuffer[videoOffset + 6] === 0x79 &&
|
||||
imageBuffer[videoOffset + 7] === 0x70
|
||||
if (!videoStart) return null
|
||||
|
||||
// 写出视频文件
|
||||
const videoPath = imagePath.replace(/\.(jpg|jpeg|png)$/i, '_live.mp4')
|
||||
const videoBuffer = imageBuffer.slice(videoOffset)
|
||||
await writeFile(videoPath, videoBuffer)
|
||||
return videoPath
|
||||
}
|
||||
|
||||
/**
|
||||
* 解包 wxgf 格式
|
||||
* wxgf 是微信的图片格式,内部使用 HEVC 编码
|
||||
|
||||
@@ -1581,6 +1581,23 @@
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
-webkit-app-region: no-drag;
|
||||
|
||||
.media-badge.live {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
left: auto;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
border-radius: 50%;
|
||||
backdrop-filter: blur(4px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.image-update-button {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { useBatchTranscribeStore } from '../stores/batchTranscribeStore'
|
||||
import type { ChatSession, Message } from '../types/models'
|
||||
import { getEmojiPath } from 'wechat-emojis'
|
||||
import { VoiceTranscribeDialog } from '../components/VoiceTranscribeDialog'
|
||||
import { LivePhotoIcon } from '../components/LivePhotoIcon'
|
||||
import { AnimatedStreamingText } from '../components/AnimatedStreamingText'
|
||||
import JumpToDateDialog from '../components/JumpToDateDialog'
|
||||
import * as configService from '../services/config'
|
||||
@@ -2625,6 +2626,7 @@ function MessageBubble({
|
||||
const [imageInView, setImageInView] = useState(false)
|
||||
const imageForceHdAttempted = useRef<string | null>(null)
|
||||
const imageForceHdPending = useRef(false)
|
||||
const [imageLiveVideoPath, setImageLiveVideoPath] = useState<string | undefined>(undefined)
|
||||
const [voiceError, setVoiceError] = useState(false)
|
||||
const [voiceLoading, setVoiceLoading] = useState(false)
|
||||
const [isVoicePlaying, setIsVoicePlaying] = useState(false)
|
||||
@@ -2853,6 +2855,7 @@ function MessageBubble({
|
||||
imageDataUrlCache.set(imageCacheKey, result.localPath)
|
||||
setImageLocalPath(result.localPath)
|
||||
setImageHasUpdate(false)
|
||||
if (result.liveVideoPath) setImageLiveVideoPath(result.liveVideoPath)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -2920,7 +2923,7 @@ function MessageBubble({
|
||||
sessionId: session.username,
|
||||
imageMd5: message.imageMd5 || undefined,
|
||||
imageDatName: message.imageDatName
|
||||
}).then((result: { success: boolean; localPath?: string; hasUpdate?: boolean; error?: string }) => {
|
||||
}).then((result: { success: boolean; localPath?: string; hasUpdate?: boolean; liveVideoPath?: string; error?: string }) => {
|
||||
if (cancelled) return
|
||||
if (result.success && result.localPath) {
|
||||
imageDataUrlCache.set(imageCacheKey, result.localPath)
|
||||
@@ -2928,6 +2931,7 @@ function MessageBubble({
|
||||
setImageLocalPath(result.localPath)
|
||||
setImageError(false)
|
||||
}
|
||||
if (result.liveVideoPath) setImageLiveVideoPath(result.liveVideoPath)
|
||||
setImageHasUpdate(Boolean(result.hasUpdate))
|
||||
}
|
||||
}).catch(() => { })
|
||||
@@ -3423,14 +3427,17 @@ function MessageBubble({
|
||||
alt="图片"
|
||||
className="image-message"
|
||||
onClick={() => {
|
||||
if (imageHasUpdate) {
|
||||
void requestImageDecrypt(true, true)
|
||||
}
|
||||
void window.electronAPI.window.openImageViewerWindow(imageLocalPath)
|
||||
if (imageHasUpdate) void requestImageDecrypt(true, true)
|
||||
void window.electronAPI.window.openImageViewerWindow(imageLocalPath!, imageLiveVideoPath || undefined)
|
||||
}}
|
||||
onLoad={() => setImageError(false)}
|
||||
onError={() => setImageError(true)}
|
||||
/>
|
||||
{imageLiveVideoPath && (
|
||||
<div className="media-badge live">
|
||||
<LivePhotoIcon size={14} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
img {
|
||||
img, video {
|
||||
max-width: none;
|
||||
max-height: none;
|
||||
object-fit: contain;
|
||||
|
||||
@@ -2,15 +2,20 @@
|
||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
import { ZoomIn, ZoomOut, RotateCw, RotateCcw } from 'lucide-react'
|
||||
import { LivePhotoIcon } from '../components/LivePhotoIcon'
|
||||
import './ImageWindow.scss'
|
||||
|
||||
export default function ImageWindow() {
|
||||
const [searchParams] = useSearchParams()
|
||||
const imagePath = searchParams.get('imagePath')
|
||||
const liveVideoPath = searchParams.get('liveVideoPath')
|
||||
const [showLive, setShowLive] = useState(false)
|
||||
const videoRef = useRef<HTMLVideoElement>(null)
|
||||
const [scale, setScale] = useState(1)
|
||||
const [rotation, setRotation] = useState(0)
|
||||
const [position, setPosition] = useState({ x: 0, y: 0 })
|
||||
const [initialScale, setInitialScale] = useState(1)
|
||||
const [imgNatural, setImgNatural] = useState({ w: 0, h: 0 })
|
||||
const viewportRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// 使用 ref 存储拖动状态,避免闭包问题
|
||||
@@ -39,6 +44,7 @@ export default function ImageWindow() {
|
||||
const img = e.currentTarget
|
||||
const naturalWidth = img.naturalWidth
|
||||
const naturalHeight = img.naturalHeight
|
||||
setImgNatural({ w: naturalWidth, h: naturalHeight })
|
||||
|
||||
if (viewportRef.current) {
|
||||
const viewportWidth = viewportRef.current.clientWidth * 0.9
|
||||
@@ -131,6 +137,23 @@ export default function ImageWindow() {
|
||||
<div className="title-bar">
|
||||
<div className="window-drag-area"></div>
|
||||
<div className="title-bar-controls">
|
||||
{liveVideoPath && (
|
||||
<button
|
||||
onClick={() => {
|
||||
const next = !showLive
|
||||
setShowLive(next)
|
||||
if (next && videoRef.current) {
|
||||
videoRef.current.currentTime = 0
|
||||
videoRef.current.play()
|
||||
}
|
||||
}}
|
||||
title={showLive ? '显示照片' : '播放实况'}
|
||||
className={showLive ? 'active' : ''}
|
||||
>
|
||||
<LivePhotoIcon size={16} />
|
||||
<span style={{ fontSize: 13, marginLeft: 4 }}>Live</span>
|
||||
</button>
|
||||
)}
|
||||
<button onClick={handleZoomOut} title="缩小 (-)"><ZoomOut size={16} /></button>
|
||||
<span className="scale-text">{Math.round(displayScale * 100)}%</span>
|
||||
<button onClick={handleZoomIn} title="放大 (+)"><ZoomIn size={16} /></button>
|
||||
@@ -140,18 +163,35 @@ export default function ImageWindow() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="image-viewport"
|
||||
<div
|
||||
className="image-viewport"
|
||||
ref={viewportRef}
|
||||
onWheel={handleWheel}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
onMouseDown={handleMouseDown}
|
||||
>
|
||||
{liveVideoPath && (
|
||||
<video
|
||||
ref={videoRef}
|
||||
src={liveVideoPath}
|
||||
width={imgNatural.w || undefined}
|
||||
height={imgNatural.h || undefined}
|
||||
style={{
|
||||
transform: `translate(${position.x}px, ${position.y}px) scale(${displayScale}) rotate(${rotation}deg)`,
|
||||
position: showLive ? 'relative' : 'absolute',
|
||||
opacity: showLive ? 1 : 0,
|
||||
pointerEvents: showLive ? 'auto' : 'none'
|
||||
}}
|
||||
onEnded={() => setShowLive(false)}
|
||||
/>
|
||||
)}
|
||||
<img
|
||||
src={imagePath}
|
||||
alt="Preview"
|
||||
style={{
|
||||
transform: `translate(${position.x}px, ${position.y}px) scale(${displayScale}) rotate(${rotation}deg)`
|
||||
transform: `translate(${position.x}px, ${position.y}px) scale(${displayScale}) rotate(${rotation}deg)`,
|
||||
opacity: showLive ? 0 : 1,
|
||||
position: showLive ? 'absolute' : 'relative'
|
||||
}}
|
||||
onLoad={handleImageLoad}
|
||||
draggable={false}
|
||||
|
||||
@@ -414,7 +414,8 @@
|
||||
|
||||
&.live {
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
right: 8px;
|
||||
left: auto;
|
||||
transform: none;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useEffect, useLayoutEffect, useState, useRef, useCallback } from 'react'
|
||||
import { RefreshCw, Search, X, Download, FolderOpen, FileJson, FileText, Image, CheckCircle, AlertCircle, Calendar, Users, Info, ChevronLeft, ChevronRight } from 'lucide-react'
|
||||
import { ImagePreview } from '../components/ImagePreview'
|
||||
import JumpToDateDialog from '../components/JumpToDateDialog'
|
||||
import './SnsPage.scss'
|
||||
import { SnsPost } from '../types/sns'
|
||||
@@ -32,7 +31,6 @@ export default function SnsPage() {
|
||||
|
||||
// UI states
|
||||
const [showJumpDialog, setShowJumpDialog] = useState(false)
|
||||
const [previewImage, setPreviewImage] = useState<{ src: string, isVideo?: boolean, liveVideoPath?: string } | null>(null)
|
||||
const [debugPost, setDebugPost] = useState<SnsPost | null>(null)
|
||||
|
||||
// 导出相关状态
|
||||
@@ -332,7 +330,13 @@ export default function SnsPage() {
|
||||
<SnsPostItem
|
||||
key={post.id}
|
||||
post={post}
|
||||
onPreview={(src, isVideo, liveVideoPath) => setPreviewImage({ src, isVideo, liveVideoPath })}
|
||||
onPreview={(src, isVideo, liveVideoPath) => {
|
||||
if (isVideo) {
|
||||
void window.electronAPI.window.openVideoPlayerWindow(src)
|
||||
} else {
|
||||
void window.electronAPI.window.openImageViewerWindow(src, liveVideoPath || undefined)
|
||||
}
|
||||
}}
|
||||
onDebug={(p) => setDebugPost(p)}
|
||||
/>
|
||||
))}
|
||||
@@ -394,15 +398,6 @@ export default function SnsPage() {
|
||||
/>
|
||||
|
||||
{/* Dialogs and Overlays */}
|
||||
{previewImage && (
|
||||
<ImagePreview
|
||||
src={previewImage.src}
|
||||
isVideo={previewImage.isVideo}
|
||||
liveVideoPath={previewImage.liveVideoPath}
|
||||
onClose={() => setPreviewImage(null)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<JumpToDateDialog
|
||||
isOpen={showJumpDialog}
|
||||
onClose={() => setShowJumpDialog(false)}
|
||||
|
||||
4
src/types/electron.d.ts
vendored
4
src/types/electron.d.ts
vendored
@@ -11,7 +11,7 @@ export interface ElectronAPI {
|
||||
setTitleBarOverlay: (options: { symbolColor: string }) => void
|
||||
openVideoPlayerWindow: (videoPath: string, videoWidth?: number, videoHeight?: number) => Promise<void>
|
||||
resizeToFitVideo: (videoWidth: number, videoHeight: number) => Promise<void>
|
||||
openImageViewerWindow: (imagePath: string) => Promise<void>
|
||||
openImageViewerWindow: (imagePath: string, liveVideoPath?: string) => Promise<void>
|
||||
openChatHistoryWindow: (sessionId: string, messageId: number) => Promise<boolean>
|
||||
}
|
||||
config: {
|
||||
@@ -125,7 +125,7 @@ export interface ElectronAPI {
|
||||
|
||||
image: {
|
||||
decrypt: (payload: { sessionId?: string; imageMd5?: string; imageDatName?: string; force?: boolean }) => Promise<{ success: boolean; localPath?: string; error?: string }>
|
||||
resolveCache: (payload: { sessionId?: string; imageMd5?: string; imageDatName?: string }) => Promise<{ success: boolean; localPath?: string; hasUpdate?: boolean; error?: string }>
|
||||
resolveCache: (payload: { sessionId?: string; imageMd5?: string; imageDatName?: string }) => Promise<{ success: boolean; localPath?: string; hasUpdate?: boolean; liveVideoPath?: string; error?: string }>
|
||||
preload: (payloads: Array<{ sessionId?: string; imageMd5?: string; imageDatName?: string }>) => Promise<boolean>
|
||||
onUpdateAvailable: (callback: (payload: { cacheKey: string; imageMd5?: string; imageDatName?: string }) => void) => () => void
|
||||
onCacheResolved: (callback: (payload: { cacheKey: string; imageMd5?: string; imageDatName?: string; localPath: string }) => void) => () => void
|
||||
|
||||
Reference in New Issue
Block a user