Add audio/mp4 message preview support
This commit is contained in:
@@ -87,7 +87,7 @@ XML backups from the [SMS Backup & Restore app](https://play.google.com/store/ap
|
|||||||
|
|
||||||
Q: What media and attachments can be previewed?
|
Q: What media and attachments can be previewed?
|
||||||
|
|
||||||
SBV supports most images formats (jpg, png, gif, heic) and video formats (mp4, 3gp). Contact cards (aka vCard or VCF) are supported.
|
SBV supports most images formats (jpg, png, gif, heic), video formats (mp4, 3gp), audio (mp4). Contact cards (aka vCard or VCF) are supported.
|
||||||
|
|
||||||
## Known Issues
|
## Known Issues
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
/* Audio Player Styling - Wide timeline control for easier seeking */
|
||||||
|
|
||||||
|
/* Make audio player fill width and ensure it's displayed properly */
|
||||||
|
.audio-player {
|
||||||
|
width: 100% !important;
|
||||||
|
min-width: 320px;
|
||||||
|
height: 54px;
|
||||||
|
outline: none;
|
||||||
|
display: block;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Chrome/Safari/Edge - Webkit browsers */
|
||||||
|
.audio-player::-webkit-media-controls-panel {
|
||||||
|
width: 100%;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
padding: 8px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure enclosure takes full width */
|
||||||
|
.audio-player::-webkit-media-controls-enclosure {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 100%;
|
||||||
|
border-radius: 0.375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make the timeline container wider */
|
||||||
|
.audio-player::-webkit-media-controls-timeline-container {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make the timeline/seek bar taller and more prominent */
|
||||||
|
.audio-player::-webkit-media-controls-timeline {
|
||||||
|
border-radius: 6px;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
margin: 0 12px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style the timeline track */
|
||||||
|
.audio-player::-webkit-media-controls-timeline::-webkit-slider-runnable-track {
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-player::-webkit-media-controls-timeline::-webkit-slider-thumb {
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make the time displays more readable */
|
||||||
|
.audio-player::-webkit-media-controls-current-time-display,
|
||||||
|
.audio-player::-webkit-media-controls-time-remaining-display {
|
||||||
|
font-size: 14px;
|
||||||
|
min-width: 48px;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style buttons */
|
||||||
|
.audio-player::-webkit-media-controls-play-button,
|
||||||
|
.audio-player::-webkit-media-controls-mute-button {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Firefox - Moz browsers */
|
||||||
|
.audio-player::-moz-range-track {
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: #dee2e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-player::-moz-range-thumb {
|
||||||
|
height: 20px;
|
||||||
|
width: 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #0d6efd;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.audio-player::-moz-range-progress {
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background-color: #0d6efd;
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState, useEffect, useRef } from 'react'
|
import { useState, useEffect, useRef } from 'react'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import VCardPreview from './VCardPreview'
|
import VCardPreview from './VCardPreview'
|
||||||
|
import './LazyMedia.css'
|
||||||
|
|
||||||
const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8081/api'
|
const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8081/api'
|
||||||
|
|
||||||
@@ -128,6 +129,7 @@ function LazyMedia({ messageId, mediaType, className, alt = "MMS attachment" })
|
|||||||
|
|
||||||
const isImage = mediaType.startsWith('image/')
|
const isImage = mediaType.startsWith('image/')
|
||||||
const isVideo = mediaType.startsWith('video/')
|
const isVideo = mediaType.startsWith('video/')
|
||||||
|
const isAudio = mediaType.startsWith('audio/')
|
||||||
const isVCard = mediaType === 'text/x-vcard' ||
|
const isVCard = mediaType === 'text/x-vcard' ||
|
||||||
mediaType === 'text/vcard' ||
|
mediaType === 'text/vcard' ||
|
||||||
mediaType === 'text/directory'
|
mediaType === 'text/directory'
|
||||||
@@ -141,9 +143,9 @@ function LazyMedia({ messageId, mediaType, className, alt = "MMS attachment" })
|
|||||||
className="bg-light rounded d-flex align-items-center justify-content-center position-relative overflow-hidden"
|
className="bg-light rounded d-flex align-items-center justify-content-center position-relative overflow-hidden"
|
||||||
style={{
|
style={{
|
||||||
width: '100%',
|
width: '100%',
|
||||||
aspectRatio: isVideo ? '16/9' : '3/4', // Common phone camera ratio
|
aspectRatio: isVideo ? '16/9' : isAudio ? 'auto' : '3/4', // Common phone camera ratio
|
||||||
minHeight: isVideo ? '200px' : '300px', // Larger to prevent layout shift
|
minHeight: isVideo ? '200px' : isAudio ? '80px' : '300px', // Larger to prevent layout shift
|
||||||
maxHeight: '400px',
|
maxHeight: isAudio ? '80px' : '400px',
|
||||||
backgroundColor: '#f8f9fa',
|
backgroundColor: '#f8f9fa',
|
||||||
backgroundImage: 'linear-gradient(45deg, #e9ecef 25%, transparent 25%, transparent 75%, #e9ecef 75%, #e9ecef), linear-gradient(45deg, #e9ecef 25%, transparent 25%, transparent 75%, #e9ecef 75%, #e9ecef)',
|
backgroundImage: 'linear-gradient(45deg, #e9ecef 25%, transparent 25%, transparent 75%, #e9ecef 75%, #e9ecef), linear-gradient(45deg, #e9ecef 25%, transparent 25%, transparent 75%, #e9ecef 75%, #e9ecef)',
|
||||||
backgroundSize: '20px 20px',
|
backgroundSize: '20px 20px',
|
||||||
@@ -156,7 +158,7 @@ function LazyMedia({ messageId, mediaType, className, alt = "MMS attachment" })
|
|||||||
<div className="spinner-border spinner-border-sm text-secondary mb-2" role="status">
|
<div className="spinner-border spinner-border-sm text-secondary mb-2" role="status">
|
||||||
<span className="visually-hidden">Loading...</span>
|
<span className="visually-hidden">Loading...</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="small text-muted">Loading {isImage ? 'image' : isVideo ? 'video' : isVCard ? 'contact' : 'media'}...</div>
|
<div className="small text-muted">Loading {isImage ? 'image' : isVideo ? 'video' : isAudio ? 'audio' : isVCard ? 'contact' : 'media'}...</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-muted d-flex flex-column align-items-center">
|
<div className="text-muted d-flex flex-column align-items-center">
|
||||||
@@ -170,18 +172,23 @@ function LazyMedia({ messageId, mediaType, className, alt = "MMS attachment" })
|
|||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||||
</svg>
|
</svg>
|
||||||
)}
|
)}
|
||||||
|
{isAudio && (
|
||||||
|
<svg style={{width: '2.5rem', height: '2.5rem'}} className="mb-2 text-secondary opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zM9 10l12-3" />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
{isVCard && (
|
{isVCard && (
|
||||||
<svg style={{width: '2.5rem', height: '2.5rem'}} className="mb-2 text-secondary opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg style={{width: '2.5rem', height: '2.5rem'}} className="mb-2 text-secondary opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||||
</svg>
|
</svg>
|
||||||
)}
|
)}
|
||||||
{!isImage && !isVideo && !isVCard && (
|
{!isImage && !isVideo && !isAudio && !isVCard && (
|
||||||
<svg style={{width: '2.5rem', height: '2.5rem'}} className="mb-2 text-secondary opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg style={{width: '2.5rem', height: '2.5rem'}} className="mb-2 text-secondary opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
||||||
</svg>
|
</svg>
|
||||||
)}
|
)}
|
||||||
<small className="text-muted">
|
<small className="text-muted">
|
||||||
{isImage ? 'Image' : isVideo ? 'Video' : isVCard ? 'Contact' : 'Attachment'}
|
{isImage ? 'Image' : isVideo ? 'Video' : isAudio ? 'Audio' : isVCard ? 'Contact' : 'Attachment'}
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -242,10 +249,19 @@ function LazyMedia({ messageId, mediaType, className, alt = "MMS attachment" })
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{isAudio && src && (
|
||||||
|
<div style={{ width: '100%', animation: 'fadeIn 0.3s ease-in' }}>
|
||||||
|
<audio
|
||||||
|
controls
|
||||||
|
src={src}
|
||||||
|
className="audio-player"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{isVCard && vcfData && (
|
{isVCard && vcfData && (
|
||||||
<VCardPreview vcfText={vcfData} messageId={messageId} />
|
<VCardPreview vcfText={vcfData} messageId={messageId} />
|
||||||
)}
|
)}
|
||||||
{!isImage && !isVideo && !isVCard && (
|
{!isImage && !isVideo && !isAudio && !isVCard && (
|
||||||
<div className="small p-2 rounded bg-light d-flex align-items-center gap-1">
|
<div className="small p-2 rounded bg-light d-flex align-items-center gap-1">
|
||||||
<svg style={{width: '1rem', height: '1rem'}} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg style={{width: '1rem', height: '1rem'}} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.172 7l-6.586 6.586a2 2 0 102.828 2.828l6.414-6.586a4 4 0 00-5.656-5.656l-6.415 6.585a6 6 0 108.486 8.486L20.5 13" />
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ function MessageThread({ conversation, startDate, endDate }) {
|
|||||||
const waitForMediaInElement = (elem) => {
|
const waitForMediaInElement = (elem) => {
|
||||||
const images = Array.from(elem.querySelectorAll('img'))
|
const images = Array.from(elem.querySelectorAll('img'))
|
||||||
const videos = Array.from(elem.querySelectorAll('video'))
|
const videos = Array.from(elem.querySelectorAll('video'))
|
||||||
const media = [...images, ...videos]
|
const audios = Array.from(elem.querySelectorAll('audio'))
|
||||||
|
const media = [...images, ...videos, ...audios]
|
||||||
|
|
||||||
if (media.length === 0) {
|
if (media.length === 0) {
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
@@ -131,7 +132,8 @@ function MessageThread({ conversation, startDate, endDate }) {
|
|||||||
const waitForMediaInElement = (elem) => {
|
const waitForMediaInElement = (elem) => {
|
||||||
const images = Array.from(elem.querySelectorAll('img'))
|
const images = Array.from(elem.querySelectorAll('img'))
|
||||||
const videos = Array.from(elem.querySelectorAll('video'))
|
const videos = Array.from(elem.querySelectorAll('video'))
|
||||||
const media = [...images, ...videos]
|
const audios = Array.from(elem.querySelectorAll('audio'))
|
||||||
|
const media = [...images, ...videos, ...audios]
|
||||||
|
|
||||||
if (media.length === 0) {
|
if (media.length === 0) {
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
|
|||||||
Reference in New Issue
Block a user