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 ( +
+
+
+
+ + {fileName} + +

{releaseTime}

+
+
+
+
+
+
+

{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';