diff --git a/custom/homepages/.gitkeep b/custom/homepages/.gitkeep
old mode 100644
new mode 100755
diff --git a/static/app-strings.json b/static/app-strings.json
index 5373770c1..fb8abb180 100644
--- a/static/app-strings.json
+++ b/static/app-strings.json
@@ -2178,6 +2178,16 @@
"Card Last 4": "Card Last 4",
"Search blocked channel name": "Search blocked channel name",
"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%",
"Show more livestreams": "Show more livestreams",
"Creator": "Creator",
diff --git a/static/img/dark_loading.gif b/static/img/dark_loading.gif
new file mode 100644
index 000000000..edb99a6eb
Binary files /dev/null and b/static/img/dark_loading.gif differ
diff --git a/static/img/white_loading.gif b/static/img/white_loading.gif
new file mode 100644
index 000000000..b65b4c25a
Binary files /dev/null and b/static/img/white_loading.gif differ
diff --git a/ui/component/claimPreviewTile/view.jsx b/ui/component/claimPreviewTile/view.jsx
index a5c515ffd..b18d0f343 100644
--- a/ui/component/claimPreviewTile/view.jsx
+++ b/ui/component/claimPreviewTile/view.jsx
@@ -191,9 +191,11 @@ function ClaimPreviewTile(props: Props) {
);
@@ -253,9 +255,11 @@ function ClaimPreviewTile(props: Props) {
-
+
{isChannel ? (
diff --git a/ui/component/dateTime/view.jsx b/ui/component/dateTime/view.jsx
index b8e25210f..3443f066b 100644
--- a/ui/component/dateTime/view.jsx
+++ b/ui/component/dateTime/view.jsx
@@ -104,7 +104,11 @@ class DateTime extends React.Component
{
return null;
}
- return {DateTime.getTimeAgoStr(date)};
+ return (
+
+ {DateTime.getTimeAgoStr(date)}
+
+ );
}
return (
diff --git a/ui/component/downloadProgress/index.js b/ui/component/downloadProgress/index.js
new file mode 100644
index 000000000..d5a4fa943
--- /dev/null
+++ b/ui/component/downloadProgress/index.js
@@ -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);
diff --git a/ui/component/downloadProgress/view.jsx b/ui/component/downloadProgress/view.jsx
new file mode 100644
index 000000000..dd95b5c7c
--- /dev/null
+++ b/ui/component/downloadProgress/view.jsx
@@ -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 (
+ <>
+
+ >
+ );
+ }
+
+ return (
+
+
+
+ {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 (
+
+ {index !== 0 &&
}
+
+
+ );
+ })}
+
+ );
+}
+
+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 (
+
+
+
+ {playing ? (
+ currentTheme === 'light' ? (
+

+ ) : (
+

+ )
+ ) : (
+
{
+ handleCancel(outpoint, true);
+ }}
+ >
+ ×
+
+ )}
+
+
+
+
{progressText}
+ {isCancel && (
+
+
{__('Do you cancel download this file?')}
+
+
+
+
+ )}
+
+ );
+}
+
+export default DownloadProgress;
diff --git a/ui/component/page/view.jsx b/ui/component/page/view.jsx
index 6d288c3bc..a30d56537 100644
--- a/ui/component/page/view.jsx
+++ b/ui/component/page/view.jsx
@@ -7,6 +7,7 @@ import SideNavigation from 'component/sideNavigation';
import SettingsSideNavigation from 'component/settingsSideNavigation';
import Header from 'component/header';
/* @if TARGET='app' */
+import DownloadProgress from '../downloadProgress';
import StatusBar from 'component/common/status-bar';
/* @endif */
import usePersistedState from 'effects/use-persisted-state';
@@ -102,7 +103,7 @@ function Page(props: Props) {
setSidebarOpen(false);
}
// TODO: make sure setState callback for usePersistedState uses useCallback to it doesn't cause effect to re-run
- }, [isOnFilePage, isMediumScreen]);
+ }, [isOnFilePage, isMediumScreen, setSidebarOpen]);
return (
@@ -150,6 +151,9 @@ function Page(props: Props) {
)}
{/* @endif */}
+ {/* @if TARGET='app' */}
+
+ {/* @endif */}
);
}
diff --git a/ui/component/viewers/videoViewer/view.jsx b/ui/component/viewers/videoViewer/view.jsx
index 5619065f9..fc2c9e8e2 100644
--- a/ui/component/viewers/videoViewer/view.jsx
+++ b/ui/component/viewers/videoViewer/view.jsx
@@ -178,7 +178,15 @@ function VideoViewer(props: Props) {
fetch(source, { method: 'HEAD', cache: 'no-store' }).then((response) => {
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(() => {
diff --git a/ui/redux/actions/content.js b/ui/redux/actions/content.js
index 1053b617a..d2625658b 100644
--- a/ui/redux/actions/content.js
+++ b/ui/redux/actions/content.js
@@ -18,27 +18,28 @@ import {
doToast,
makeSelectUrlsForCollectionId,
} from 'lbry-redux';
-import { doPurchaseUri } from 'redux/actions/file';
+import { doPurchaseUri, doDeleteFile } from 'redux/actions/file';
import { makeSelectCostInfoForUri, Lbryio } from 'lbryinc';
import { makeSelectClientSetting, selectosNotificationsEnabled, selectDaemonSettings } from 'redux/selectors/settings';
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
// 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
// @if TARGET='app'
return (dispatch: Dispatch, getState: GetState) => {
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.
// If a file is already deleted, no point to still try update load status
const byOutpoint = selectFileInfosByOutpoint(getState());
if (byOutpoint[outpoint]) {
dispatch(doUpdateLoadStatus(uri, outpoint));
}
- }, DOWNLOAD_POLL_INTERVAL);
+ }, DOWNLOAD_POLL_INTERVAL));
Lbry.file_list({
outpoint,
@@ -53,6 +54,7 @@ export function doUpdateLoadStatus(uri: string, outpoint: string) {
setNextStatusUpdate();
} else if (fileInfo.completed) {
// TODO this isn't going to get called if they reload the client before
+
// the download finished
dispatch({
type: ACTIONS.DOWNLOADING_COMPLETED,
@@ -98,6 +100,25 @@ export function doUpdateLoadStatus(uri: string, outpoint: string) {
// @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) {
return (dispatch: Dispatch) => {
dispatch({
diff --git a/ui/redux/selectors/file_info.js b/ui/redux/selectors/file_info.js
index 26d48204a..cd97aa9c1 100644
--- a/ui/redux/selectors/file_info.js
+++ b/ui/redux/selectors/file_info.js
@@ -1,11 +1,11 @@
import { selectClaimsByUri, selectIsFetchingClaimListMine, selectMyClaims } from 'lbry-redux';
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(
selectIsFetchingFileList,
@@ -13,31 +13,31 @@ export const selectIsFetchingFileListDownloadedOrPublished = createSelector(
(isFetchingFileList, isFetchingClaimListMine) => isFetchingFileList || isFetchingClaimListMine
);
-export const makeSelectFileInfoForUri = uri =>
+export const makeSelectFileInfoForUri = (uri) =>
createSelector(selectClaimsByUri, selectFileInfosByOutpoint, (claims, byOutpoint) => {
const claim = claims[uri];
const outpoint = claim ? `${claim.txid}:${claim.nout}` : 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) => {
if (!fileInfo) return false;
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(
selectFileInfosByOutpoint,
selectMyClaims,
(byOutpoint, myClaims) =>
- Object.values(byOutpoint).filter(fileInfo => {
- const myClaimIds = myClaims.map(claim => claim.claim_id);
+ Object.values(byOutpoint).filter((fileInfo) => {
+ const myClaimIds = myClaims.map((claim) => claim.claim_id);
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 fileInfos = [];
- outpoints.forEach(outpoint => {
+ outpoints.forEach((outpoint) => {
const fileInfo = fileInfosByOutpoint[outpoint];
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 = [];
- fileInfos.forEach(fileInfo => {
+ fileInfos.forEach((fileInfo) => {
progress.push((fileInfo.written_bytes / fileInfo.total_bytes) * 100);
});
@@ -82,4 +82,4 @@ export const selectTotalDownloadProgress = createSelector(selectDownloadingFileI
return -1;
});
-export const selectFileInfoErrors = createSelector(selectState, state => state.errors || {});
+export const selectFileInfoErrors = createSelector(selectState, (state) => state.errors || {});
diff --git a/ui/scss/all.scss b/ui/scss/all.scss
index 7767f9e79..cfdd60fde 100644
--- a/ui/scss/all.scss
+++ b/ui/scss/all.scss
@@ -68,3 +68,4 @@
@import 'component/empty';
@import 'component/stripe-card';
@import 'component/wallet-tip-send';
+@import 'component/download-progress';
diff --git a/ui/scss/component/_claim-list.scss b/ui/scss/component/_claim-list.scss
index 8aed6c484..de482cc48 100644
--- a/ui/scss/component/_claim-list.scss
+++ b/ui/scss/component/_claim-list.scss
@@ -720,6 +720,7 @@
margin: 0 0;
padding: var(--spacing-xxs) var(--spacing-xxs);
height: unset;
+ background-color: var(--color-header-background);
// label (with 'Add' text) hidden by default
.button__label {
diff --git a/ui/scss/component/_download-progress.scss b/ui/scss/component/_download-progress.scss
new file mode 100644
index 000000000..5b0f29346
--- /dev/null
+++ b/ui/scss/component/_download-progress.scss
@@ -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);
+ }
+}
diff --git a/ui/scss/component/menu-button.scss b/ui/scss/component/menu-button.scss
index 593ffa47a..ef1a3bf83 100644
--- a/ui/scss/component/menu-button.scss
+++ b/ui/scss/component/menu-button.scss
@@ -121,6 +121,11 @@
overflow-x: hidden;
}
+.menu__link-disable {
+ @extend .menu__link;
+ color: var(--color-text-subtitle) !important;
+}
+
.menu__link--notification {
width: 100%;
display: flex;
diff --git a/ui/util/array.js b/ui/util/array.js
new file mode 100644
index 000000000..4badb3006
--- /dev/null
+++ b/ui/util/array.js
@@ -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);
+ }
+}
diff --git a/ui/util/detect-user-bandwidth.js b/ui/util/detect-user-bandwidth.js
index fd3f0d187..7e8e8d57b 100644
--- a/ui/util/detect-user-bandwidth.js
+++ b/ui/util/detect-user-bandwidth.js
@@ -3,14 +3,14 @@ const downloadSize = 1093957; // this must match with the image above
let startTime, endTime;
async function measureConnectionSpeed() {
- startTime = (new Date()).getTime();
+ startTime = new Date().getTime();
const cacheBuster = '?nnn=' + startTime;
const download = new Image();
download.src = imageAddr + cacheBuster;
// this returns when the image is finished downloading
await download.decode();
- endTime = (new Date()).getTime();
+ endTime = new Date().getTime();
const duration = (endTime - startTime) / 1000;
const bitsLoaded = downloadSize * 8;
const speedBps = (bitsLoaded / duration).toFixed(2);
diff --git a/web/scss/odysee.scss b/web/scss/odysee.scss
index 1e2cf5ede..b9395f9c1 100644
--- a/web/scss/odysee.scss
+++ b/web/scss/odysee.scss
@@ -69,3 +69,4 @@
@import '../../ui/scss/component/empty';
@import '../../ui/scss/component/stripe-card';
@import '../../ui/scss/component/wallet-tip-send';
+@import '../../ui/scss/component/download-progress';