Compare commits

...

9 commits

Author SHA1 Message Date
bluerabbit001
542253c8a0 Update CSS 2021-10-08 13:27:12 -07:00
bluerabbit001
a6cac05a66 Update Download Progress 2021-10-08 10:21:18 -07:00
bluerabbit001
eecb5bac8b Fix ESLint 2021-10-07 08:03:27 -07:00
bluerabbit001
172d4d35eb Merge branch 'master' of github.com:lbryio/lbry-desktop into issue/7152 2021-10-07 05:29:57 -07:00
bluerabbit001
5dfc247685 Update Dark theme and fix playing issue 2021-10-06 10:32:14 -07:00
bluerabbit001
02e121ea81 ESLint fix 2021-10-05 09:55:14 -07:00
bluerabbit001
c082905fa4 Download Progress 2021-10-05 09:20:34 -07:00
bluerabbit001
3ec129b037 Implement Download Progress 2021-10-01 13:49:19 -07:00
bluerabbit001
ff2c9bf56c Implement Download Progress 2021-10-01 13:00:57 -07:00
19 changed files with 664 additions and 29 deletions

0
custom/homepages/.gitkeep Normal file → Executable file
View file

View file

@ -2178,6 +2178,16 @@
"Card Last 4": "Card Last 4", "Card Last 4": "Card Last 4",
"Search blocked channel name": "Search blocked channel name", "Search blocked channel name": "Search blocked channel name",
"Discuss": "Discuss", "Discuss": "Discuss",
"Play Next (SHIFT+N)": "Play Next (SHIFT+N)",
"Play Previous (SHIFT+P)": "Play Previous (SHIFT+P)",
"Finance": "Finance",
"You followed @veritasium!": "You followed @veritasium!",
"Downloading": "Downloading",
"Do you cancel download this file?": "Do you cancel download this file?",
"%remainingMinutes% minutes %remainSecond% seconds remaining": "%remainingMinutes% minutes %remainSecond% seconds remaining",
"%remainSecond% seconds remaining": "%remainSecond% seconds remaining",
"%written% of %total%": "%written% of %total%",
"(%speed%/sec)": "(%speed%/sec)",
"lbry.tv has been retired. You have been magically transported to Odysee.com. %more%": "lbry.tv has been retired. You have been magically transported to Odysee.com. %more%", "lbry.tv has been retired. You have been magically transported to Odysee.com. %more%": "lbry.tv has been retired. You have been magically transported to Odysee.com. %more%",
"Show more livestreams": "Show more livestreams", "Show more livestreams": "Show more livestreams",
"Creator": "Creator", "Creator": "Creator",

BIN
static/img/dark_loading.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

View file

@ -191,9 +191,11 @@ function ClaimPreviewTile(props: Props) {
</div> </div>
<div className="placeholder__wrapper"> <div className="placeholder__wrapper">
<div className="placeholder claim-tile__title" /> <div className="placeholder claim-tile__title" />
<div className={classnames('claim-tile__info placeholder', { <div
'contains_view_count': shouldShowViewCount, className={classnames('claim-tile__info placeholder', {
})} /> contains_view_count: shouldShowViewCount,
})}
/>
</div> </div>
</li> </li>
); );
@ -253,9 +255,11 @@ function ClaimPreviewTile(props: Props) {
<ClaimMenuList uri={uri} collectionId={listId} channelUri={channelUri} /> <ClaimMenuList uri={uri} collectionId={listId} channelUri={channelUri} />
</div> </div>
<div> <div>
<div className={classnames('claim-tile__info', { <div
'contains_view_count': shouldShowViewCount, className={classnames('claim-tile__info', {
})}> contains_view_count: shouldShowViewCount,
})}
>
{isChannel ? ( {isChannel ? (
<div className="claim-tile__about--channel"> <div className="claim-tile__about--channel">
<SubscribeButton uri={repostedChannelUri || uri} /> <SubscribeButton uri={repostedChannelUri || uri} />

View file

@ -104,7 +104,11 @@ class DateTime extends React.Component<Props, State> {
return null; return null;
} }
return <span className="date_time" title={moment(date).format(`MMMM Do, YYYY ${clockFormat}`)}>{DateTime.getTimeAgoStr(date)}</span>; return (
<span className="date_time" title={moment(date).format(`MMMM Do, YYYY ${clockFormat}`)}>
{DateTime.getTimeAgoStr(date)}
</span>
);
} }
return ( return (

View file

@ -0,0 +1,48 @@
import { connect } from 'react-redux';
import DownloadProgress from './view';
import { doSetPlayingUri, doStopDownload, doContinueDownloading, doPurchaseUriWrapper } from 'redux/actions/content';
import { selectFileInfosByOutpoint, SETTINGS } from 'lbry-redux';
import { selectPrimaryUri, selectPlayingUri } from 'redux/selectors/content';
import { makeSelectClientSetting } from 'redux/selectors/settings';
const select = (state) => {
const byOutpoint = selectFileInfosByOutpoint(state);
const runningByOutpoint = [];
const primaryUri = selectPrimaryUri(state);
const playingUri = selectPlayingUri(state);
const uri = playingUri ? playingUri.uri : null;
let primaryOutpoint = null;
let playingOutpoint = null;
for (const key in byOutpoint) {
const item = byOutpoint[key];
if (item && primaryUri && primaryUri.includes(`/${item.claim_name}`)) primaryOutpoint = item.outpoint;
if (item && uri && uri.includes(`/${item.claim_name}`)) playingOutpoint = item.outpoint;
if (item && item.status === 'running') {
runningByOutpoint.push(item);
}
}
return {
byOutpoint: selectFileInfosByOutpoint(state),
primary: {
uri: primaryUri,
outpoint: primaryOutpoint,
},
playing: {
uri,
outpoint: playingOutpoint,
},
currentTheme: makeSelectClientSetting(SETTINGS.THEME)(state),
};
};
const perform = (dispatch) => ({
pause: () => dispatch(doSetPlayingUri({ uri: null })),
doContinueDownloading: (outpoint, force) => dispatch(doContinueDownloading(outpoint, force)),
stopDownload: (outpoint) => dispatch(doStopDownload(outpoint)),
download: (uri) => dispatch(doPurchaseUriWrapper(uri, false, true)),
});
export default connect(select, perform)(DownloadProgress);

View file

@ -0,0 +1,286 @@
// @flow
import React, { useState, useEffect } from 'react';
import { shell } from 'electron';
import Button from 'component/button';
import * as ICONS from 'constants/icons';
import { buildURI } from 'lbry-redux';
import { formatBytes } from 'util/format-bytes';
import { areEqual, removeItem } from 'util/array';
import loadingIcon from '../../../static/img/white_loading.gif';
import darkLoadingIcon from '../../../static/img/dark_loading.gif';
import usePersistedState from 'effects/use-persisted-state';
type Props = {
byOutpoint: any,
primary: any,
playing: any,
currentTheme: string,
stopDownload: (outpoint: string) => void,
doContinueDownloading: (outpoint: string, force: boolean) => void,
download: (uri: string) => void,
};
function DownloadProgress({ byOutpoint, primary, playing, currentTheme, stopDownload, doContinueDownloading }: Props) {
const [isShow, setIsShow] = usePersistedState('download-progress', true);
const [downloading, setDownloading] = usePersistedState('download-progress-downloading', []);
const [cancelHash] = useState({});
const [initDownloadingHash] = useState({});
const [prevPlaying, setPrevPlaying] = useState({});
const [prevPrimary, setPrevPrimary] = useState({});
const handleCancel = (hash, value) => {
cancelHash[hash] = value;
};
const handleStopDownload = (outpoint) => {
const updated = [...downloading];
removeItem(updated, outpoint);
setDownloading(updated);
stopDownload(outpoint);
};
const runningByOutpoint = {};
const currentDownloading = [...downloading];
for (const key in byOutpoint) {
const item = byOutpoint[key];
if (item && item.status === 'running') runningByOutpoint[item.outpoint] = item;
}
Object.keys(runningByOutpoint)
.filter((outpoint) => downloading.indexOf(outpoint) === -1)
.map((outpoint) => {
if (primary.outpoint !== outpoint && playing.outpoint !== outpoint) {
currentDownloading.push(outpoint);
}
});
downloading
.filter((outpoint) => (byOutpoint[outpoint] && byOutpoint[outpoint].status !== 'running') || !byOutpoint[outpoint])
.map((outpoint) => {
removeItem(currentDownloading, outpoint);
});
if (!areEqual(downloading, currentDownloading)) setDownloading(currentDownloading);
if (currentDownloading.length === 0) return null;
if (playing.outpoint !== prevPlaying.outpoint) {
if (downloading.includes(prevPlaying.outpoint)) {
setTimeout(() => {
doContinueDownloading(prevPlaying.outpoint, true);
}, 1000);
}
setPrevPlaying(playing);
}
if (primary.outpoint !== prevPrimary.outpoint) {
if (downloading.includes(prevPrimary.outpoint)) {
setTimeout(() => {
doContinueDownloading(prevPrimary.outpoint, true);
}, 1000);
}
setPrevPrimary(primary);
}
currentDownloading.map((outpoint) => {
if (!initDownloadingHash[outpoint]) {
initDownloadingHash[outpoint] = true;
doContinueDownloading(outpoint, false);
}
});
if (!isShow) {
return (
<>
<Button
iconSize={40}
icon={ICONS.DOWNLOAD}
className="download-progress__toggle-button"
onClick={() => setIsShow(true)}
>
<div className="download-progress__current-downloading">
<span className="notification__bubble">
<span className="notification__count">{currentDownloading.length}</span>
</span>
</div>
</Button>
</>
);
}
return (
<div className="download-progress__header">
<Button className="download-progress__top-close-button" onClick={() => setIsShow(false)}>
<div />
</Button>
{currentDownloading.map((outpoint, index) => {
const item = runningByOutpoint[outpoint];
let releaseTime = '';
let isPlaying = false;
if (item.metadata && item.metadata.release_time) {
releaseTime = new Date(parseInt(item.metadata.release_time) * 1000).toISOString().split('T')[0];
}
if (outpoint === primary.outpoint || outpoint === playing.outpoint) {
isPlaying = true;
}
return (
<div key={item.outpoint}>
{index !== 0 && <hr className="download-progress__divider" />}
<DownloadProgressItem
fileName={item.suggested_file_name}
title={item.metadata.title}
releaseTime={releaseTime}
writtenBytes={item.written_bytes}
totalBytes={item.total_bytes}
addedOn={item.added_on}
directory={item.download_directory}
stopDownload={handleStopDownload}
outpoint={item.outpoint}
isCancel={cancelHash[item.outpoint]}
claimID={item.claim_id}
playing={isPlaying}
claimName={item.claim_name}
handleCancel={handleCancel}
currentTheme={currentTheme}
/>
</div>
);
})}
</div>
);
}
type DownloadProgressItemProps = {
fileName: string,
writtenBytes: number,
totalBytes: number,
addedOn: number,
title: string,
releaseTime: string,
directory: string,
outpoint: string,
isCancel: boolean,
claimID: string,
claimName: string,
playing: boolean,
currentTheme: string,
stopDownload: (outpoint: string) => void,
handleCancel: (hash: string, value: boolean) => void,
};
function DownloadProgressItem({
fileName,
writtenBytes,
totalBytes,
addedOn,
title,
releaseTime,
directory,
outpoint,
isCancel,
claimID,
claimName,
playing,
currentTheme,
stopDownload,
handleCancel,
}: DownloadProgressItemProps) {
const processStopDownload = () => {
handleCancel(outpoint, false);
stopDownload(outpoint);
};
const [percent, setPercent] = useState(0);
const [progressText, setProgressText] = useState('');
useEffect(() => {
const updatePercent = ((writtenBytes / totalBytes) * 100).toFixed(0);
setPercent(updatePercent);
let updateText = '';
const downloadSpeed = Math.ceil(writtenBytes / (Date.now() / 1000 - addedOn));
const remainingSecond = Math.ceil((totalBytes - writtenBytes) / downloadSpeed);
const remainingMinutes = Math.floor(remainingSecond / 60);
if (remainingMinutes > 0) {
updateText += __('%remainingMinutes% minutes %remainSecond% seconds remaining', {
remainingMinutes: remainingMinutes,
remainSecond: remainingSecond - 60 * remainingMinutes,
});
} else {
updateText += __('%remainSecond% seconds remaining', { remainSecond: remainingSecond - 60 * remainingMinutes });
}
updateText += ' -- ';
updateText += __('%written% of %total%', {
written: formatBytes(writtenBytes),
total: formatBytes(totalBytes),
});
updateText += ' ';
updateText += __('(%speed%/sec)', {
speed: formatBytes(downloadSpeed),
});
setProgressText(updateText);
}, [writtenBytes, totalBytes, addedOn]);
const openDownloadFolder = () => {
shell.openPath(directory);
};
return (
<div className="download-progress__state-container">
<div className="download-progress__state-bar">
<Button
label={title}
className="download-progress__state-filename"
navigate={buildURI({ claimName, claimID })}
/>
{playing ? (
currentTheme === 'light' ? (
<img src={loadingIcon} className="download-progress__playing-button" />
) : (
<img src={darkLoadingIcon} className="download-progress__playing-button" />
)
) : (
<div
className="download-progress__close-button"
onClick={() => {
handleCancel(outpoint, true);
}}
>
&times;
</div>
)}
</div>
<div className="download-progress__state-bar">
<a className="download-progress__state-filename-link" onClick={openDownloadFolder}>
{fileName}
</a>
<p className="download-progress__release-time">{releaseTime}</p>
</div>
<div className="download-progress__state-bar">
<div className="download-progress__bar-container">
<div className="download-progress__bar-content" style={{ width: `${percent}%` }} />
</div>
</div>
<p className="download-progress__count-time">{progressText}</p>
{isCancel && (
<div className="download-progress__cancel">
<p>{__('Do you cancel download this file?')}</p>
<div className="download-progress__cancel-confirm">
<Button label={__('Yes')} className="download-progress__cancel-ok" onClick={processStopDownload} />
<Button
label={__('No')}
className="download-progress__cancel-ok"
onClick={() => handleCancel(outpoint, false)}
/>
</div>
</div>
)}
</div>
);
}
export default DownloadProgress;

View file

@ -7,6 +7,7 @@ import SideNavigation from 'component/sideNavigation';
import SettingsSideNavigation from 'component/settingsSideNavigation'; import SettingsSideNavigation from 'component/settingsSideNavigation';
import Header from 'component/header'; import Header from 'component/header';
/* @if TARGET='app' */ /* @if TARGET='app' */
import DownloadProgress from '../downloadProgress';
import StatusBar from 'component/common/status-bar'; import StatusBar from 'component/common/status-bar';
/* @endif */ /* @endif */
import usePersistedState from 'effects/use-persisted-state'; import usePersistedState from 'effects/use-persisted-state';
@ -102,7 +103,7 @@ function Page(props: Props) {
setSidebarOpen(false); setSidebarOpen(false);
} }
// TODO: make sure setState callback for usePersistedState uses useCallback to it doesn't cause effect to re-run // TODO: make sure setState callback for usePersistedState uses useCallback to it doesn't cause effect to re-run
}, [isOnFilePage, isMediumScreen]); }, [isOnFilePage, isMediumScreen, setSidebarOpen]);
return ( return (
<Fragment> <Fragment>
@ -150,6 +151,9 @@ function Page(props: Props) {
</React.Suspense> </React.Suspense>
)} )}
{/* @endif */} {/* @endif */}
{/* @if TARGET='app' */}
<DownloadProgress />
{/* @endif */}
</Fragment> </Fragment>
); );
} }

View file

@ -178,7 +178,15 @@ function VideoViewer(props: Props) {
fetch(source, { method: 'HEAD', cache: 'no-store' }).then((response) => { fetch(source, { method: 'HEAD', cache: 'no-store' }).then((response) => {
let playerPoweredBy = response.headers.get('x-powered-by') || ''; let playerPoweredBy = response.headers.get('x-powered-by') || '';
analytics.videoStartEvent(claimId, timeToStart, playerPoweredBy, userId, claim.canonical_url, this, bitrateAsBitsPerSecond); analytics.videoStartEvent(
claimId,
timeToStart,
playerPoweredBy,
userId,
claim.canonical_url,
this,
bitrateAsBitsPerSecond
);
}); });
doAnalyticsView(uri, timeToStart).then(() => { doAnalyticsView(uri, timeToStart).then(() => {

View file

@ -18,27 +18,28 @@ import {
doToast, doToast,
makeSelectUrlsForCollectionId, makeSelectUrlsForCollectionId,
} from 'lbry-redux'; } from 'lbry-redux';
import { doPurchaseUri } from 'redux/actions/file'; import { doPurchaseUri, doDeleteFile } from 'redux/actions/file';
import { makeSelectCostInfoForUri, Lbryio } from 'lbryinc'; import { makeSelectCostInfoForUri, Lbryio } from 'lbryinc';
import { makeSelectClientSetting, selectosNotificationsEnabled, selectDaemonSettings } from 'redux/selectors/settings'; import { makeSelectClientSetting, selectosNotificationsEnabled, selectDaemonSettings } from 'redux/selectors/settings';
const DOWNLOAD_POLL_INTERVAL = 1000; const DOWNLOAD_POLL_INTERVAL = 1000;
var timeOutHash = {};
export function doUpdateLoadStatus(uri: string, outpoint: string) { export function doUpdateLoadStatus(uri: any, outpoint: string) {
// Updates the loading status for a uri as it's downloading // Updates the loading status for a uri as it's downloading
// Calls file_list and checks the written_bytes value to see if the number has increased // Calls file_list and checks the written_bytes value to see if the number has increased
// Not needed on web as users aren't actually downloading the file // Not needed on web as users aren't actually downloading the file
// @if TARGET='app' // @if TARGET='app'
return (dispatch: Dispatch, getState: GetState) => { return (dispatch: Dispatch, getState: GetState) => {
const setNextStatusUpdate = () => const setNextStatusUpdate = () =>
setTimeout(() => { (timeOutHash[outpoint] = setTimeout(() => {
// We need to check if outpoint still exists first because user are able to delete file (outpoint) while downloading. // We need to check if outpoint still exists first because user are able to delete file (outpoint) while downloading.
// If a file is already deleted, no point to still try update load status // If a file is already deleted, no point to still try update load status
const byOutpoint = selectFileInfosByOutpoint(getState()); const byOutpoint = selectFileInfosByOutpoint(getState());
if (byOutpoint[outpoint]) { if (byOutpoint[outpoint]) {
dispatch(doUpdateLoadStatus(uri, outpoint)); dispatch(doUpdateLoadStatus(uri, outpoint));
} }
}, DOWNLOAD_POLL_INTERVAL); }, DOWNLOAD_POLL_INTERVAL));
Lbry.file_list({ Lbry.file_list({
outpoint, outpoint,
@ -53,6 +54,7 @@ export function doUpdateLoadStatus(uri: string, outpoint: string) {
setNextStatusUpdate(); setNextStatusUpdate();
} else if (fileInfo.completed) { } else if (fileInfo.completed) {
// TODO this isn't going to get called if they reload the client before // TODO this isn't going to get called if they reload the client before
// the download finished // the download finished
dispatch({ dispatch({
type: ACTIONS.DOWNLOADING_COMPLETED, type: ACTIONS.DOWNLOADING_COMPLETED,
@ -98,6 +100,25 @@ export function doUpdateLoadStatus(uri: string, outpoint: string) {
// @endif // @endif
} }
export function doContinueDownloading(outpoint: string, force: boolean) {
return (dispatch: Dispatch) => {
if (!timeOutHash[outpoint] || force) {
dispatch(doUpdateLoadStatus(null, outpoint));
}
};
}
export function doStopDownload(outpoint: string) {
return (dispatch: Dispatch) => {
if (timeOutHash[outpoint]) {
clearInterval(timeOutHash[outpoint]);
timeOutHash[outpoint] = undefined;
}
dispatch(doDeleteFile(outpoint, false, false, null));
};
}
export function doSetPrimaryUri(uri: ?string) { export function doSetPrimaryUri(uri: ?string) {
return (dispatch: Dispatch) => { return (dispatch: Dispatch) => {
dispatch({ dispatch({

View file

@ -1,11 +1,11 @@
import { selectClaimsByUri, selectIsFetchingClaimListMine, selectMyClaims } from 'lbry-redux'; import { selectClaimsByUri, selectIsFetchingClaimListMine, selectMyClaims } from 'lbry-redux';
import { createSelector } from 'reselect'; import { createSelector } from 'reselect';
export const selectState = state => state.fileInfo || {}; export const selectState = (state) => state.fileInfo || {};
export const selectFileInfosByOutpoint = createSelector(selectState, state => state.byOutpoint || {}); export const selectFileInfosByOutpoint = createSelector(selectState, (state) => state.byOutpoint || {});
export const selectIsFetchingFileList = createSelector(selectState, state => state.isFetchingFileList); export const selectIsFetchingFileList = createSelector(selectState, (state) => state.isFetchingFileList);
export const selectIsFetchingFileListDownloadedOrPublished = createSelector( export const selectIsFetchingFileListDownloadedOrPublished = createSelector(
selectIsFetchingFileList, selectIsFetchingFileList,
@ -13,31 +13,31 @@ export const selectIsFetchingFileListDownloadedOrPublished = createSelector(
(isFetchingFileList, isFetchingClaimListMine) => isFetchingFileList || isFetchingClaimListMine (isFetchingFileList, isFetchingClaimListMine) => isFetchingFileList || isFetchingClaimListMine
); );
export const makeSelectFileInfoForUri = uri => export const makeSelectFileInfoForUri = (uri) =>
createSelector(selectClaimsByUri, selectFileInfosByOutpoint, (claims, byOutpoint) => { createSelector(selectClaimsByUri, selectFileInfosByOutpoint, (claims, byOutpoint) => {
const claim = claims[uri]; const claim = claims[uri];
const outpoint = claim ? `${claim.txid}:${claim.nout}` : undefined; const outpoint = claim ? `${claim.txid}:${claim.nout}` : undefined;
return outpoint ? byOutpoint[outpoint] : undefined; return outpoint ? byOutpoint[outpoint] : undefined;
}); });
export const selectDownloadingByOutpoint = createSelector(selectState, state => state.downloadingByOutpoint || {}); export const selectDownloadingByOutpoint = createSelector(selectState, (state) => state.downloadingByOutpoint || {});
export const makeSelectDownloadingForUri = uri => export const makeSelectDownloadingForUri = (uri) =>
createSelector(selectDownloadingByOutpoint, makeSelectFileInfoForUri(uri), (byOutpoint, fileInfo) => { createSelector(selectDownloadingByOutpoint, makeSelectFileInfoForUri(uri), (byOutpoint, fileInfo) => {
if (!fileInfo) return false; if (!fileInfo) return false;
return byOutpoint[fileInfo.outpoint]; return byOutpoint[fileInfo.outpoint];
}); });
export const selectUrisLoading = createSelector(selectState, state => state.urisLoading || {}); export const selectUrisLoading = createSelector(selectState, (state) => state.urisLoading || {});
export const makeSelectLoadingForUri = uri => createSelector(selectUrisLoading, byUri => byUri && byUri[uri]); export const makeSelectLoadingForUri = (uri) => createSelector(selectUrisLoading, (byUri) => byUri && byUri[uri]);
export const selectFileInfosDownloaded = createSelector( export const selectFileInfosDownloaded = createSelector(
selectFileInfosByOutpoint, selectFileInfosByOutpoint,
selectMyClaims, selectMyClaims,
(byOutpoint, myClaims) => (byOutpoint, myClaims) =>
Object.values(byOutpoint).filter(fileInfo => { Object.values(byOutpoint).filter((fileInfo) => {
const myClaimIds = myClaims.map(claim => claim.claim_id); const myClaimIds = myClaims.map((claim) => claim.claim_id);
return fileInfo && myClaimIds.indexOf(fileInfo.claim_id) === -1 && (fileInfo.completed || fileInfo.written_bytes); return fileInfo && myClaimIds.indexOf(fileInfo.claim_id) === -1 && (fileInfo.completed || fileInfo.written_bytes);
}) })
@ -59,7 +59,7 @@ export const selectDownloadingFileInfos = createSelector(
const outpoints = Object.keys(downloadingByOutpoint); const outpoints = Object.keys(downloadingByOutpoint);
const fileInfos = []; const fileInfos = [];
outpoints.forEach(outpoint => { outpoints.forEach((outpoint) => {
const fileInfo = fileInfosByOutpoint[outpoint]; const fileInfo = fileInfosByOutpoint[outpoint];
if (fileInfo) fileInfos.push(fileInfo); if (fileInfo) fileInfos.push(fileInfo);
@ -69,10 +69,10 @@ export const selectDownloadingFileInfos = createSelector(
} }
); );
export const selectTotalDownloadProgress = createSelector(selectDownloadingFileInfos, fileInfos => { export const selectTotalDownloadProgress = createSelector(selectDownloadingFileInfos, (fileInfos) => {
const progress = []; const progress = [];
fileInfos.forEach(fileInfo => { fileInfos.forEach((fileInfo) => {
progress.push((fileInfo.written_bytes / fileInfo.total_bytes) * 100); progress.push((fileInfo.written_bytes / fileInfo.total_bytes) * 100);
}); });
@ -82,4 +82,4 @@ export const selectTotalDownloadProgress = createSelector(selectDownloadingFileI
return -1; return -1;
}); });
export const selectFileInfoErrors = createSelector(selectState, state => state.errors || {}); export const selectFileInfoErrors = createSelector(selectState, (state) => state.errors || {});

View file

@ -68,3 +68,4 @@
@import 'component/empty'; @import 'component/empty';
@import 'component/stripe-card'; @import 'component/stripe-card';
@import 'component/wallet-tip-send'; @import 'component/wallet-tip-send';
@import 'component/download-progress';

View file

@ -720,6 +720,7 @@
margin: 0 0; margin: 0 0;
padding: var(--spacing-xxs) var(--spacing-xxs); padding: var(--spacing-xxs) var(--spacing-xxs);
height: unset; height: unset;
background-color: var(--color-header-background);
// label (with 'Add' text) hidden by default // label (with 'Add' text) hidden by default
.button__label { .button__label {

View file

@ -0,0 +1,224 @@
.download-progress__header {
padding: 15px;
position: fixed;
bottom: 0;
right: 0;
width: 400px;
display: flex;
flex-direction: column;
background-color: var(--color-header-background); //var(--color-gray-9):dark-mode
border-radius: var(--border-radius);
// border: 1px solid var(--color-gray-3);
z-index: 9999;
}
.download-progress__top-close-button {
position: absolute;
top: 7px;
right: 15px;
font-size: 35px;
background-color: transparent;
width: 15px;
height: 15px;
div {
height: 2px;
width: 13px;
background-color: var(--color-gray-4);
border-radius: var(--border-radius);
}
}
.download-progress__state-container {
margin-top: 10px;
padding-bottom: 10px;
display: flex;
flex-direction: column;
width: 100%;
}
.download-progress__state-filename {
margin: 0;
font-weight: 800;
font-size: 13px;
flex: 1;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
margin-right: 10px;
span.button__label {
color: var(--color-text);
}
}
.download-progress__state-filename-link {
margin: 0;
font-weight: 800;
font-size: 13px;
flex: 1;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
margin-right: 10px;
text-decoration: underline;
cursor: pointer;
}
.download-progress__release-time {
margin: 0;
font-weight: 800;
font-size: 12px;
margin-left: auto;
font-style: italic;
}
.download-progress__state-bar {
display: flex;
flex-direction: row;
width: 100%;
align-items: center;
margin-bottom: 2px;
}
.download-progress__bar-container {
width: 100%;
background-color: var(--color-gray-5);
height: 6px;
border-radius: var(--border-radius);
}
.download-progress__bar-content {
border-radius: var(--border-radius);
height: 100%;
background-color: var(--color-primary);
}
.download-progress__close-button {
flex-shrink: 0;
margin-left: auto;
font-size: 20px;
cursor: pointer;
}
.download-progress__playing-button {
flex-shrink: 0;
margin-left: auto;
width: 29.6px;
height: 29.6px;
}
.download-progress__count-time {
font-size: 11px;
letter-spacing: -0.6px;
}
.download-progress__divider {
border-top: 1px solid var(--color-gray-6);
margin-left: -15px;
width: 110%;
}
.download-progress__cancel {
margin-top: 7px;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.download-progress__cancel p {
margin: 0;
font-size: 12px;
margin: 0;
white-space: nowrap;
width: 285px;
overflow: hidden;
text-overflow: ellipsis;
}
.download-progress__cancel b {
font-size: 13px;
}
.download-progress__cancel-confirm {
width: 90px;
display: flex;
justify-content: space-around;
align-items: center;
}
.download-progress__cancel-ok {
border: none;
background-color: transparent;
font-size: 13px;
color: var(--color-text);
}
.download-progress__cancel-ok:hover {
color: var(--color-primary);
}
.download__container {
position: fixed;
bottom: 10px;
right: 10px;
width: 400px;
height: 300px;
border-radius: var(--border-radius);
box-shadow: 2px 2px 5px var(--color-gray-4);
background-color: var(--color-white);
transition: width 2s;
}
.download-progress__toggle-button {
position: fixed;
bottom: 10px;
right: 10px;
border: none;
background: var(--color-header-background);
color: var(--color-gray-6);
width: 50px;
height: 50px;
border-radius: var(--border-radius);
box-shadow: 0px 5px 4px var(--color-gray-4);
display: flex;
justify-content: center;
align-items: center;
}
.download_close_modal {
float: right;
margin-right: 10px;
font-size: 25px;
}
.download-progress__current-downloading {
position: fixed;
bottom: 25px;
right: 15px;
border: none;
width: 30px;
height: 30px;
display: flex;
justify-content: center;
align-items: center;
margin: 0;
animation-name: downloadcount;
animation-duration: 1.3s;
animation-iteration-count: infinite;
.notification__bubble {
height: 1.5rem;
width: 1.5rem;
border-radius: 50%;
background-color: var(--color-editor-tag);
position: absolute;
top: -0.5rem;
right: -0.5rem;
color: white;
font-size: var(--font-small);
font-weight: bold;
line-height: 1;
display: flex;
align-items: center;
justify-content: center;
}
.notification__bubble--small {
font-size: var(--font-xsmall);
}
.notification__bubble--inline {
@extend .notification__bubble;
top: 0.75rem;
right: 1rem;
}
}
@keyframes downloadcount {
0% {
transform: translateY(-10px);
}
50% {
transform: translateY(-3px);
}
100% {
transform: translateY(-10px);
}
}

View file

@ -121,6 +121,11 @@
overflow-x: hidden; overflow-x: hidden;
} }
.menu__link-disable {
@extend .menu__link;
color: var(--color-text-subtitle) !important;
}
.menu__link--notification { .menu__link--notification {
width: 100%; width: 100%;
display: flex; display: flex;

18
ui/util/array.js Normal file
View file

@ -0,0 +1,18 @@
export function areEqual(first, second) {
if (first.length !== second.length) {
return false;
}
for (let i = 0; i < first.length; i++) {
if (!second.includes(first[i])) {
return false;
}
}
return true;
}
export function removeItem(array, item) {
const index = array.indexOf(item);
if (index > -1) {
array.splice(index, 1);
}
}

View file

@ -3,14 +3,14 @@ const downloadSize = 1093957; // this must match with the image above
let startTime, endTime; let startTime, endTime;
async function measureConnectionSpeed() { async function measureConnectionSpeed() {
startTime = (new Date()).getTime(); startTime = new Date().getTime();
const cacheBuster = '?nnn=' + startTime; const cacheBuster = '?nnn=' + startTime;
const download = new Image(); const download = new Image();
download.src = imageAddr + cacheBuster; download.src = imageAddr + cacheBuster;
// this returns when the image is finished downloading // this returns when the image is finished downloading
await download.decode(); await download.decode();
endTime = (new Date()).getTime(); endTime = new Date().getTime();
const duration = (endTime - startTime) / 1000; const duration = (endTime - startTime) / 1000;
const bitsLoaded = downloadSize * 8; const bitsLoaded = downloadSize * 8;
const speedBps = (bitsLoaded / duration).toFixed(2); const speedBps = (bitsLoaded / duration).toFixed(2);

View file

@ -69,3 +69,4 @@
@import '../../ui/scss/component/empty'; @import '../../ui/scss/component/empty';
@import '../../ui/scss/component/stripe-card'; @import '../../ui/scss/component/stripe-card';
@import '../../ui/scss/component/wallet-tip-send'; @import '../../ui/scss/component/wallet-tip-send';
@import '../../ui/scss/component/download-progress';