Audio: Add download button
This commit is contained in:
parent
e55c264d56
commit
966a049743
@ -13,7 +13,7 @@
|
||||
--color-interactive-buffered: rgba(var(--color-text-green-rgb), 0.4); // Overlays underlying inactive color
|
||||
|
||||
.theme-dark & {
|
||||
--color-text-green-rgb: 255,255,255;
|
||||
--color-text-green-rgb: 255, 255, 255;
|
||||
--color-text-green: var(--color-white);
|
||||
}
|
||||
|
||||
@ -70,6 +70,34 @@
|
||||
}
|
||||
}
|
||||
|
||||
.media-loading {
|
||||
pointer-events: none;
|
||||
|
||||
.interactive {
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.download-button {
|
||||
position: absolute;
|
||||
width: 0.3rem !important;
|
||||
height: 0.3rem !important;
|
||||
left: 1.5rem;
|
||||
top: 1.5rem;
|
||||
border: 2px solid var(--background-color);
|
||||
z-index: 1;
|
||||
|
||||
i {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
&.bigger .download-button {
|
||||
left: 2rem;
|
||||
top: 2rem;
|
||||
border: 2px solid var(--color-background);
|
||||
}
|
||||
|
||||
.content {
|
||||
align-self: center;
|
||||
min-width: 0;
|
||||
|
||||
@ -13,6 +13,7 @@ import { formatMediaDateTime, formatMediaDuration, formatPastTimeShort } from '.
|
||||
import {
|
||||
getMediaDuration,
|
||||
getMediaTransferState,
|
||||
getMessageAudioCaption,
|
||||
getMessageKey,
|
||||
getMessageMediaFormat,
|
||||
getMessageMediaHash,
|
||||
@ -27,6 +28,7 @@ import useMediaWithDownloadProgress from '../../hooks/useMediaWithDownloadProgre
|
||||
import useShowTransition from '../../hooks/useShowTransition';
|
||||
import useBuffering from '../../hooks/useBuffering';
|
||||
import useAudioPlayer from '../../hooks/useAudioPlayer';
|
||||
import useMediaDownload from '../../hooks/useMediaDownload';
|
||||
import useLang, { LangFn } from '../../hooks/useLang';
|
||||
|
||||
import Button from '../ui/Button';
|
||||
@ -123,9 +125,21 @@ const Audio: FC<OwnProps & StateProps> = ({
|
||||
setIsActivated(isPlaying);
|
||||
}, [isPlaying]);
|
||||
|
||||
const {
|
||||
isDownloadStarted,
|
||||
downloadProgress: directDownloadProgress,
|
||||
handleDownloadClick,
|
||||
} = useMediaDownload(getMessageMediaHash(message, 'download'), getMessageAudioCaption(message));
|
||||
|
||||
const isLoadingForPlaying = isActivated && !isBuffered;
|
||||
|
||||
const {
|
||||
isUploading, isTransferring, transferProgress,
|
||||
} = getMediaTransferState(message, uploadProgress || downloadProgress, isActivated && !isBuffered);
|
||||
} = getMediaTransferState(
|
||||
message,
|
||||
isDownloadStarted ? directDownloadProgress : (uploadProgress || downloadProgress),
|
||||
isLoadingForPlaying || isDownloadStarted,
|
||||
);
|
||||
|
||||
const {
|
||||
shouldRender: shouldRenderSpinner,
|
||||
@ -220,7 +234,7 @@ const Audio: FC<OwnProps & StateProps> = ({
|
||||
);
|
||||
|
||||
const buttonClassNames = ['toggle-play'];
|
||||
if (shouldRenderSpinner) {
|
||||
if (isLoadingForPlaying) {
|
||||
buttonClassNames.push('loading');
|
||||
} else if (isPlaying) {
|
||||
buttonClassNames.push('pause');
|
||||
@ -282,15 +296,27 @@ const Audio: FC<OwnProps & StateProps> = ({
|
||||
<i className="icon-pause" />
|
||||
</Button>
|
||||
{shouldRenderSpinner && (
|
||||
<div className={buildClassName('media-loading', spinnerClassNames)}>
|
||||
<div className={buildClassName('media-loading', spinnerClassNames, isLoadingForPlaying && 'interactive')}>
|
||||
<ProgressSpinner
|
||||
progress={transferProgress}
|
||||
transparent
|
||||
size={renderingFor ? 'm' : 's'}
|
||||
onClick={handleButtonClick}
|
||||
onClick={isLoadingForPlaying ? handleButtonClick : undefined}
|
||||
noCross={!isLoadingForPlaying}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{audio && (
|
||||
<Button
|
||||
round
|
||||
size="tiny"
|
||||
className="download-button"
|
||||
ariaLabel={isDownloadStarted ? 'Cancel download' : 'Download'}
|
||||
onClick={handleDownloadClick}
|
||||
>
|
||||
<i className={isDownloadStarted ? 'icon-close' : 'icon-arrow-down'} />
|
||||
</Button>
|
||||
)}
|
||||
{renderingFor === 'searchResult' && renderSearchResult()}
|
||||
{renderingFor !== 'searchResult' && audio && renderAudio(
|
||||
lang, audio, isPlaying, playProgress, bufferedProgress, seekHandlers, date,
|
||||
@ -309,7 +335,7 @@ function renderAudio(
|
||||
bufferedProgress: number,
|
||||
seekHandlers: ISeekMethods,
|
||||
date?: number,
|
||||
handleDateClick?: () => void,
|
||||
handleDateClick?: NoneToVoidFunction,
|
||||
) {
|
||||
const {
|
||||
title, performer, duration, fileName,
|
||||
|
||||
@ -1,13 +1,9 @@
|
||||
import React, {
|
||||
FC, useCallback, useEffect, useMemo, useState,
|
||||
} from '../../lib/teact/teact';
|
||||
import React, { FC, useMemo } from '../../lib/teact/teact';
|
||||
|
||||
import { ApiMessage } from '../../api/types';
|
||||
|
||||
import { IS_MOBILE_SCREEN } from '../../util/environment';
|
||||
import download from '../../util/download';
|
||||
import { getMessageMediaHash } from '../../modules/helpers';
|
||||
import useMediaWithDownloadProgress from '../../hooks/useMediaWithDownloadProgress';
|
||||
import useLang from '../../hooks/useLang';
|
||||
|
||||
import Button from '../ui/Button';
|
||||
@ -16,6 +12,7 @@ import MenuItem from '../ui/MenuItem';
|
||||
import ProgressSpinner from '../ui/ProgressSpinner';
|
||||
|
||||
import './MediaViewerActions.scss';
|
||||
import useMediaDownload from '../../hooks/useMediaDownload';
|
||||
|
||||
type OwnProps = {
|
||||
mediaData?: string;
|
||||
@ -40,29 +37,11 @@ const MediaViewerActions: FC<OwnProps> = ({
|
||||
onForward,
|
||||
onZoomToggle,
|
||||
}) => {
|
||||
const [isVideoDownloadAllowed, setIsVideoDownloadAllowed] = useState(false);
|
||||
const videoMediaHash = isVideo && message ? getMessageMediaHash(message, 'download') : undefined;
|
||||
const {
|
||||
mediaData: videoBlobUrl, downloadProgress,
|
||||
} = useMediaWithDownloadProgress(videoMediaHash, !isVideoDownloadAllowed);
|
||||
|
||||
// Download with browser when fully loaded
|
||||
useEffect(() => {
|
||||
if (isVideoDownloadAllowed && videoBlobUrl) {
|
||||
download(videoBlobUrl, fileName!);
|
||||
setIsVideoDownloadAllowed(false);
|
||||
}
|
||||
}, [fileName, videoBlobUrl, isVideoDownloadAllowed]);
|
||||
|
||||
// Cancel download on slide change
|
||||
useEffect(() => {
|
||||
setIsVideoDownloadAllowed(false);
|
||||
}, [videoMediaHash]);
|
||||
|
||||
const handleVideoDownloadClick = useCallback((e: React.SyntheticEvent<HTMLElement>) => {
|
||||
e.stopPropagation();
|
||||
setIsVideoDownloadAllowed((isAllowed) => !isAllowed);
|
||||
}, []);
|
||||
isDownloadStarted,
|
||||
downloadProgress,
|
||||
handleDownloadClick,
|
||||
} = useMediaDownload(message && isVideo ? getMessageMediaHash(message, 'download') : undefined);
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
@ -98,10 +77,10 @@ const MediaViewerActions: FC<OwnProps> = ({
|
||||
)}
|
||||
{isVideo ? (
|
||||
<MenuItem
|
||||
icon={isVideoDownloadAllowed ? 'close' : 'download'}
|
||||
onClick={handleVideoDownloadClick}
|
||||
icon={isDownloadStarted ? 'close' : 'download'}
|
||||
onClick={handleDownloadClick}
|
||||
>
|
||||
{isVideoDownloadAllowed ? `${Math.round(downloadProgress * 100)}% Downloading...` : 'Download'}
|
||||
{isDownloadStarted ? `${Math.round(downloadProgress * 100)}% Downloading...` : 'Download'}
|
||||
</MenuItem>
|
||||
) : (
|
||||
<MenuItem
|
||||
@ -113,7 +92,7 @@ const MediaViewerActions: FC<OwnProps> = ({
|
||||
</MenuItem>
|
||||
)}
|
||||
</DropdownMenu>
|
||||
{isVideoDownloadAllowed && <ProgressSpinner progress={downloadProgress} size="s" noCross />}
|
||||
{isDownloadStarted && <ProgressSpinner progress={downloadProgress} size="s" noCross />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -139,10 +118,10 @@ const MediaViewerActions: FC<OwnProps> = ({
|
||||
size="smaller"
|
||||
color="translucent-white"
|
||||
ariaLabel={lang('AccActionDownload')}
|
||||
onClick={handleVideoDownloadClick}
|
||||
onClick={handleDownloadClick}
|
||||
>
|
||||
{isVideoDownloadAllowed ? (
|
||||
<ProgressSpinner progress={downloadProgress} size="s" onClick={handleVideoDownloadClick} />
|
||||
{isDownloadStarted ? (
|
||||
<ProgressSpinner progress={downloadProgress} size="s" onClick={handleDownloadClick} />
|
||||
) : (
|
||||
<i className="icon-download" />
|
||||
)}
|
||||
|
||||
37
src/hooks/useMediaDownload.ts
Normal file
37
src/hooks/useMediaDownload.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import React, { useCallback, useEffect, useState } from '../lib/teact/teact';
|
||||
|
||||
import useMediaWithDownloadProgress from './useMediaWithDownloadProgress';
|
||||
import download from '../util/download';
|
||||
|
||||
export default function useMediaDownload(
|
||||
mediaHash?: string,
|
||||
fileName?: string,
|
||||
) {
|
||||
const [isDownloadStarted, setIsDownloadStarted] = useState(false);
|
||||
|
||||
const { mediaData, downloadProgress } = useMediaWithDownloadProgress(mediaHash, !isDownloadStarted);
|
||||
|
||||
// Download with browser when fully loaded
|
||||
useEffect(() => {
|
||||
if (isDownloadStarted && mediaData) {
|
||||
download(mediaData, fileName!);
|
||||
setIsDownloadStarted(false);
|
||||
}
|
||||
}, [fileName, mediaData, isDownloadStarted]);
|
||||
|
||||
// Cancel download on source change
|
||||
useEffect(() => {
|
||||
setIsDownloadStarted(false);
|
||||
}, [mediaHash]);
|
||||
|
||||
const handleDownloadClick = useCallback((e: React.SyntheticEvent<HTMLElement>) => {
|
||||
e.stopPropagation();
|
||||
setIsDownloadStarted((isAllowed) => !isAllowed);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
isDownloadStarted,
|
||||
downloadProgress,
|
||||
handleDownloadClick,
|
||||
};
|
||||
}
|
||||
@ -196,6 +196,8 @@ export function getMessageMediaHash(
|
||||
case 'micro':
|
||||
case 'pictogram':
|
||||
return undefined;
|
||||
case 'download':
|
||||
return `${base}?download`;
|
||||
default:
|
||||
return getVideoOrAudioBaseHash(audio, base);
|
||||
}
|
||||
|
||||
@ -49,8 +49,7 @@ export function getMessageSummaryText(lang: LangFn, message: ApiMessage, noEmoji
|
||||
}
|
||||
|
||||
if (audio) {
|
||||
const caption = [audio.title, audio.performer].filter(Boolean).join(' — ') || (text && text.text);
|
||||
return `${noEmoji ? '' : '🎧 '}${caption || lang('AttachMusic')}`;
|
||||
return `${noEmoji ? '' : '🎧 '}${getMessageAudioCaption(message) || lang('AttachMusic')}`;
|
||||
}
|
||||
|
||||
if (voice) {
|
||||
@ -216,3 +215,9 @@ export function isMessageLocal(message: ApiMessage) {
|
||||
export function isHistoryClearMessage(message: ApiMessage) {
|
||||
return message.content.action && message.content.action.type === 'historyClear';
|
||||
}
|
||||
|
||||
export function getMessageAudioCaption(message: ApiMessage) {
|
||||
const { audio, text } = message.content;
|
||||
|
||||
return (audio && [audio.title, audio.performer].filter(Boolean).join(' — ')) || (text && text.text);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user