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
|
--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;
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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" />
|
||||||
)}
|
)}
|
||||||
|
|||||||
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 'micro':
|
||||||
case 'pictogram':
|
case 'pictogram':
|
||||||
return undefined;
|
return undefined;
|
||||||
|
case 'download':
|
||||||
|
return `${base}?download`;
|
||||||
default:
|
default:
|
||||||
return getVideoOrAudioBaseHash(audio, base);
|
return getVideoOrAudioBaseHash(audio, base);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user