mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-25 07:16:51 +00:00
聊天页面支持实况解析;朋友圈页面优化
This commit is contained in:
@@ -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