Audio: Add download button

This commit is contained in:
Alexander Zinchuk 2021-06-18 00:58:37 +03:00
parent e55c264d56
commit 966a049743
6 changed files with 119 additions and 42 deletions

View File

@ -13,7 +13,7 @@
--color-interactive-buffered: rgba(var(--color-text-green-rgb), 0.4); // Overlays underlying inactive color --color-interactive-buffered: rgba(var(--color-text-green-rgb), 0.4); // Overlays underlying inactive color
.theme-dark & { .theme-dark & {
--color-text-green-rgb: 255,255,255; --color-text-green-rgb: 255, 255, 255;
--color-text-green: var(--color-white); --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 { .content {
align-self: center; align-self: center;
min-width: 0; min-width: 0;

View File

@ -13,6 +13,7 @@ import { formatMediaDateTime, formatMediaDuration, formatPastTimeShort } from '.
import { import {
getMediaDuration, getMediaDuration,
getMediaTransferState, getMediaTransferState,
getMessageAudioCaption,
getMessageKey, getMessageKey,
getMessageMediaFormat, getMessageMediaFormat,
getMessageMediaHash, getMessageMediaHash,
@ -27,6 +28,7 @@ import useMediaWithDownloadProgress from '../../hooks/useMediaWithDownloadProgre
import useShowTransition from '../../hooks/useShowTransition'; import useShowTransition from '../../hooks/useShowTransition';
import useBuffering from '../../hooks/useBuffering'; import useBuffering from '../../hooks/useBuffering';
import useAudioPlayer from '../../hooks/useAudioPlayer'; import useAudioPlayer from '../../hooks/useAudioPlayer';
import useMediaDownload from '../../hooks/useMediaDownload';
import useLang, { LangFn } from '../../hooks/useLang'; import useLang, { LangFn } from '../../hooks/useLang';
import Button from '../ui/Button'; import Button from '../ui/Button';
@ -123,9 +125,21 @@ const Audio: FC<OwnProps & StateProps> = ({
setIsActivated(isPlaying); setIsActivated(isPlaying);
}, [isPlaying]); }, [isPlaying]);
const {
isDownloadStarted,
downloadProgress: directDownloadProgress,
handleDownloadClick,
} = useMediaDownload(getMessageMediaHash(message, 'download'), getMessageAudioCaption(message));
const isLoadingForPlaying = isActivated && !isBuffered;
const { const {
isUploading, isTransferring, transferProgress, isUploading, isTransferring, transferProgress,
} = getMediaTransferState(message, uploadProgress || downloadProgress, isActivated && !isBuffered); } = getMediaTransferState(
message,
isDownloadStarted ? directDownloadProgress : (uploadProgress || downloadProgress),
isLoadingForPlaying || isDownloadStarted,
);
const { const {
shouldRender: shouldRenderSpinner, shouldRender: shouldRenderSpinner,
@ -220,7 +234,7 @@ const Audio: FC<OwnProps & StateProps> = ({
); );
const buttonClassNames = ['toggle-play']; const buttonClassNames = ['toggle-play'];
if (shouldRenderSpinner) { if (isLoadingForPlaying) {
buttonClassNames.push('loading'); buttonClassNames.push('loading');
} else if (isPlaying) { } else if (isPlaying) {
buttonClassNames.push('pause'); buttonClassNames.push('pause');
@ -282,15 +296,27 @@ const Audio: FC<OwnProps & StateProps> = ({
<i className="icon-pause" /> <i className="icon-pause" />
</Button> </Button>
{shouldRenderSpinner && ( {shouldRenderSpinner && (
<div className={buildClassName('media-loading', spinnerClassNames)}> <div className={buildClassName('media-loading', spinnerClassNames, isLoadingForPlaying && 'interactive')}>
<ProgressSpinner <ProgressSpinner
progress={transferProgress} progress={transferProgress}
transparent transparent
size={renderingFor ? 'm' : 's'} size={renderingFor ? 'm' : 's'}
onClick={handleButtonClick} onClick={isLoadingForPlaying ? handleButtonClick : undefined}
noCross={!isLoadingForPlaying}
/> />
</div> </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' && renderSearchResult()}
{renderingFor !== 'searchResult' && audio && renderAudio( {renderingFor !== 'searchResult' && audio && renderAudio(
lang, audio, isPlaying, playProgress, bufferedProgress, seekHandlers, date, lang, audio, isPlaying, playProgress, bufferedProgress, seekHandlers, date,
@ -309,7 +335,7 @@ function renderAudio(
bufferedProgress: number, bufferedProgress: number,
seekHandlers: ISeekMethods, seekHandlers: ISeekMethods,
date?: number, date?: number,
handleDateClick?: () => void, handleDateClick?: NoneToVoidFunction,
) { ) {
const { const {
title, performer, duration, fileName, title, performer, duration, fileName,

View File

@ -1,13 +1,9 @@
import React, { import React, { FC, useMemo } from '../../lib/teact/teact';
FC, useCallback, useEffect, useMemo, useState,
} from '../../lib/teact/teact';
import { ApiMessage } from '../../api/types'; import { ApiMessage } from '../../api/types';
import { IS_MOBILE_SCREEN } from '../../util/environment'; import { IS_MOBILE_SCREEN } from '../../util/environment';
import download from '../../util/download';
import { getMessageMediaHash } from '../../modules/helpers'; import { getMessageMediaHash } from '../../modules/helpers';
import useMediaWithDownloadProgress from '../../hooks/useMediaWithDownloadProgress';
import useLang from '../../hooks/useLang'; import useLang from '../../hooks/useLang';
import Button from '../ui/Button'; import Button from '../ui/Button';
@ -16,6 +12,7 @@ import MenuItem from '../ui/MenuItem';
import ProgressSpinner from '../ui/ProgressSpinner'; import ProgressSpinner from '../ui/ProgressSpinner';
import './MediaViewerActions.scss'; import './MediaViewerActions.scss';
import useMediaDownload from '../../hooks/useMediaDownload';
type OwnProps = { type OwnProps = {
mediaData?: string; mediaData?: string;
@ -40,29 +37,11 @@ const MediaViewerActions: FC<OwnProps> = ({
onForward, onForward,
onZoomToggle, onZoomToggle,
}) => { }) => {
const [isVideoDownloadAllowed, setIsVideoDownloadAllowed] = useState(false);
const videoMediaHash = isVideo && message ? getMessageMediaHash(message, 'download') : undefined;
const { const {
mediaData: videoBlobUrl, downloadProgress, isDownloadStarted,
} = useMediaWithDownloadProgress(videoMediaHash, !isVideoDownloadAllowed); downloadProgress,
handleDownloadClick,
// Download with browser when fully loaded } = useMediaDownload(message && isVideo ? getMessageMediaHash(message, 'download') : undefined);
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);
}, []);
const lang = useLang(); const lang = useLang();
@ -98,10 +77,10 @@ const MediaViewerActions: FC<OwnProps> = ({
)} )}
{isVideo ? ( {isVideo ? (
<MenuItem <MenuItem
icon={isVideoDownloadAllowed ? 'close' : 'download'} icon={isDownloadStarted ? 'close' : 'download'}
onClick={handleVideoDownloadClick} onClick={handleDownloadClick}
> >
{isVideoDownloadAllowed ? `${Math.round(downloadProgress * 100)}% Downloading...` : 'Download'} {isDownloadStarted ? `${Math.round(downloadProgress * 100)}% Downloading...` : 'Download'}
</MenuItem> </MenuItem>
) : ( ) : (
<MenuItem <MenuItem
@ -113,7 +92,7 @@ const MediaViewerActions: FC<OwnProps> = ({
</MenuItem> </MenuItem>
)} )}
</DropdownMenu> </DropdownMenu>
{isVideoDownloadAllowed && <ProgressSpinner progress={downloadProgress} size="s" noCross />} {isDownloadStarted && <ProgressSpinner progress={downloadProgress} size="s" noCross />}
</div> </div>
); );
} }
@ -139,10 +118,10 @@ const MediaViewerActions: FC<OwnProps> = ({
size="smaller" size="smaller"
color="translucent-white" color="translucent-white"
ariaLabel={lang('AccActionDownload')} ariaLabel={lang('AccActionDownload')}
onClick={handleVideoDownloadClick} onClick={handleDownloadClick}
> >
{isVideoDownloadAllowed ? ( {isDownloadStarted ? (
<ProgressSpinner progress={downloadProgress} size="s" onClick={handleVideoDownloadClick} /> <ProgressSpinner progress={downloadProgress} size="s" onClick={handleDownloadClick} />
) : ( ) : (
<i className="icon-download" /> <i className="icon-download" />
)} )}

View 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,
};
}

View File

@ -196,6 +196,8 @@ export function getMessageMediaHash(
case 'micro': case 'micro':
case 'pictogram': case 'pictogram':
return undefined; return undefined;
case 'download':
return `${base}?download`;
default: default:
return getVideoOrAudioBaseHash(audio, base); return getVideoOrAudioBaseHash(audio, base);
} }

View File

@ -49,8 +49,7 @@ export function getMessageSummaryText(lang: LangFn, message: ApiMessage, noEmoji
} }
if (audio) { if (audio) {
const caption = [audio.title, audio.performer].filter(Boolean).join(' — ') || (text && text.text); return `${noEmoji ? '' : '🎧 '}${getMessageAudioCaption(message) || lang('AttachMusic')}`;
return `${noEmoji ? '' : '🎧 '}${caption || lang('AttachMusic')}`;
} }
if (voice) { if (voice) {
@ -216,3 +215,9 @@ export function isMessageLocal(message: ApiMessage) {
export function isHistoryClearMessage(message: ApiMessage) { export function isHistoryClearMessage(message: ApiMessage) {
return message.content.action && message.content.action.type === 'historyClear'; 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);
}