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
.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;

View File

@ -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,

View File

@ -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" />
)}

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 'pictogram':
return undefined;
case 'download':
return `${base}?download`;
default:
return getVideoOrAudioBaseHash(audio, base);
}

View File

@ -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);
}