mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-08-17 19:47:08 +08:00
feat: 支持微信视频消息解析与播放
支持视频 XML 解析、本地文件映射与拖动播放。 修复源码乱码注释并统一 UTF-8 编码。
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; media-src 'self' blob: data:;"
|
||||
content="default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https: wxe-media:; media-src 'self' blob: data: wxe-media:;"
|
||||
/>
|
||||
</head>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState } from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import { Sidebar } from './components/Sidebar'
|
||||
import ChatWindow from './components/ChatWindow'
|
||||
import { AppShell } from './components/layout/AppShell'
|
||||
|
||||
@@ -1375,6 +1375,54 @@ body {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.video-message-bubble {
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
background: #101412;
|
||||
border-color: rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.wechat-message-row.mine .video-message-bubble {
|
||||
background: #101412;
|
||||
}
|
||||
|
||||
.video-message {
|
||||
position: relative;
|
||||
width: min(320px, 48vw);
|
||||
min-height: 140px;
|
||||
background: #101412;
|
||||
}
|
||||
|
||||
.video-content {
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-height: 360px;
|
||||
object-fit: contain;
|
||||
background: #101412;
|
||||
}
|
||||
|
||||
.video-duration {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
top: 8px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 8px;
|
||||
color: #fff;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
font-size: 11px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.video-placeholder {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: min(260px, 44vw);
|
||||
min-height: 120px;
|
||||
padding: 16px;
|
||||
color: rgba(255, 255, 255, 0.78);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.image-bubble {
|
||||
position: relative;
|
||||
max-width: min(260px, 46vw);
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
|
||||
interface VideoBubbleProps {
|
||||
md5?: string
|
||||
newMd5?: string
|
||||
rawMd5?: string
|
||||
duration?: number
|
||||
}
|
||||
|
||||
export function VideoBubble({
|
||||
md5,
|
||||
newMd5,
|
||||
rawMd5,
|
||||
duration
|
||||
}: VideoBubbleProps): React.ReactElement {
|
||||
const hashes = useMemo(
|
||||
() => [rawMd5, newMd5, md5].filter((value): value is string => Boolean(value)),
|
||||
[md5, newMd5, rawMd5]
|
||||
)
|
||||
const [media, setMedia] = useState<{ url?: string; poster?: string; error?: string }>({})
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
window.api
|
||||
.getVideo(hashes)
|
||||
.then((result) => {
|
||||
if (!cancelled) setMedia(result.success ? result : { error: result.error })
|
||||
})
|
||||
.catch((error) => {
|
||||
if (!cancelled) setMedia({ error: error instanceof Error ? error.message : String(error) })
|
||||
})
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [hashes])
|
||||
|
||||
if (!media.url) {
|
||||
return <div className="video-placeholder">{media.error || '视频加载中…'}</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="video-message">
|
||||
<video
|
||||
className="video-content"
|
||||
src={media.url}
|
||||
poster={media.poster}
|
||||
controls
|
||||
preload="metadata"
|
||||
/>
|
||||
{duration ? <span className="video-duration">{duration} 秒</span> : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { Contact, Message } from '../../../../shared/types'
|
||||
import { ImageBubble } from '../ImageBubble'
|
||||
import { RichMessageBubble } from '../RichMessageBubble'
|
||||
import { VoicePlayer } from '../VoicePlayer'
|
||||
import { VideoBubble } from '../VideoBubble'
|
||||
import { renderWechatEmojiText } from '../../utils/wechatEmojiText'
|
||||
import { formatMessageTime } from './messageGrouping'
|
||||
|
||||
@@ -27,6 +28,7 @@ export function MessageBubble({
|
||||
}: MessageBubbleProps): React.ReactElement {
|
||||
const isVoice = message.type === '语音'
|
||||
const isImage = message.type === '图片'
|
||||
const isVideo = message.type === '视频'
|
||||
const isRichMedia = RICH_MESSAGE_TYPES.includes(message.type)
|
||||
const hoverTime = formatMessageTime(message)
|
||||
|
||||
@@ -35,7 +37,7 @@ export function MessageBubble({
|
||||
<div
|
||||
className={`message-bubble ${isVoice ? 'voice-bubble' : ''} ${
|
||||
isImage ? 'image-message-bubble' : ''
|
||||
}`}
|
||||
} ${isVideo ? 'video-message-bubble' : ''}`}
|
||||
>
|
||||
{isVoice && message.sessionId ? (
|
||||
<VoicePlayer
|
||||
@@ -50,6 +52,13 @@ export function MessageBubble({
|
||||
sessionId={message.sessionId}
|
||||
onImageClick={onImageClick}
|
||||
/>
|
||||
) : isVideo && message.contentData && message.contentData.type === 'video' ? (
|
||||
<VideoBubble
|
||||
md5={message.contentData.md5}
|
||||
newMd5={message.contentData.newMd5}
|
||||
rawMd5={message.contentData.rawMd5}
|
||||
duration={message.contentData.duration}
|
||||
/>
|
||||
) : isRichMedia && message.contentData ? (
|
||||
<RichMessageBubble contentData={message.contentData} />
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user